You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Playwright+XState模型测试:单测试中无法切换用户登录

XState + Playwright 多用户登录会话清除问题解决

我采用基于模型的测试方案,将XState与Playwright集成,测试需要两名用户登录交互推进的系统工作流。

当前遇到的问题:在同一浏览器中登出后重新登录,浏览器仍保留第一个用户的登录状态,登出操作未彻底清除认证会话,即便输入新用户信息也无效。

临时解决办法:在测试间添加afterEach钩子彻底关闭浏览器,可清除所有Cookie,第二个测试能正常用第二个用户登录,但单个测试步骤内切换登录时依然失效——因为步骤间未关闭浏览器。

核心困境:无法在测试模型中控制/引用浏览器实例,也无法打开多个浏览器,因为浏览器实例是在测试钩子后期创建的。希望能在步骤间关闭并重新打开浏览器窗口,最好集成到登录/登出函数中,或找到登出时正确清除会话的方法,实现新用户重新登录。


现有代码

钩子及XState测试代码

test.beforeEach(async ({ browser }) => {
  page = await browser.newPage();
  await page.goto(URL);
  testNum++
});

test.afterEach(async ({browser}) => {
  for(let context of browser.contexts())
    await context.close()
});

test.describe('CE workflows', () => {
  const testPlans = testModel.getSimplePathPlans();

  testPlans.forEach((plan, i) => {
    plan.paths.forEach((path, i) => {
      test(path.description, async ({}) => {
        await path.test(page);
      });
    });
  });
})

登录/登出封装函数

async function login(email) {
  await page.goto(URL);
  await page.getByTestId('loginEmail').fill(email);
  await page.getByTestId('loginPassword').fill('password');
  await page.getByTestId('btnLogin').click();

  if (await page.getByRole('button', { name: 'Allow cookies' }).isVisible())
  await page.getByRole('button', { name: 'No thanks' }).click();
}

async function logout() {
  await page.getByTestId('btnOptions').click();
  await page.getByRole('button', { name: 'Logout' }).click();
}

XState模型事件示例

每个步骤逻辑为「登录→执行操作→登出」,交给下一个用户:

const testModel = createModel(CEWorkflowMachine, {
  events: {
    CREATE_CE_CLIENT: async () => {
      await login()

      await page.goto(URL);
      await page.getByRole('link', { name: 'Create compensation event' }).click();

      await page.getByLabel('Title').fill(`Test for model based CE ${testNum}`);
      await page.getByLabel('Description').fill('Test Description');
      await page.getByLabel('Project manager\'s assumptions').fill('Test Assumption');
      await page.getByText('The Project Manager gives an').click();

      await page.getByText('Notify').click();

      await logout()
    },

解决方案

方案1:登出后强制清除会话数据

如果系统自带的登出逻辑未彻底清除Cookie或本地存储,可在logout函数末尾手动补充清除操作:

async function logout() {
  await page.getByTestId('btnOptions').click();
  await page.getByRole('button', { name: 'Logout' }).click();
  // 强制清除当前页面的所有Cookie、localStorage和sessionStorage
  await page.context().clearCookies();
  await page.evaluate(() => {
    localStorage.clear();
    sessionStorage.clear();
  });
}

无需关闭页面即可彻底清除用户会话残留,确保下次登录新用户时无干扰。

方案2:步骤间切换浏览器上下文(推荐)

Playwright的BrowserContext是隔离的会话环境,每个上下文相当于独立的轻量浏览器实例,比关闭整个浏览器更高效。修改登录/登出函数,每次登录前创建新上下文,登出后销毁:

// 全局变量存储当前上下文和页面
let currentContext;
let currentPage;

async function login(email) {
  // 销毁之前的上下文(如果存在)
  if (currentContext) {
    await currentContext.close();
  }
  // 创建新的隔离上下文
  currentContext = await browser.newContext();
  currentPage = await currentContext.newPage();
  await currentPage.goto(URL);
  
  await currentPage.getByTestId('loginEmail').fill(email);
  await currentPage.getByTestId('loginPassword').fill('password');
  await currentPage.getByTestId('btnLogin').click();

  if (await currentPage.getByRole('button', { name: 'Allow cookies' }).isVisible())
    await currentPage.getByRole('button', { name: 'No thanks' }).click();
  
  return currentPage;
}

async function logout() {
  await currentPage.getByTestId('btnOptions').click();
  await currentPage.getByRole('button', { name: 'Logout' }).click();
  // 销毁上下文彻底清除会话
  await currentContext.close();
  currentContext = null;
  currentPage = null;
}

同时修改测试钩子和XState事件代码:

// 修改测试钩子,把browser实例存到全局
test.beforeEach(async ({ browser }) => {
  global.browser = browser;
  testNum++
});

test.afterEach(async () => {
  // 清理残留的上下文
  if (currentContext) {
    await currentContext.close();
  }
});

// 修改XState事件,使用返回的页面
CREATE_CE_CLIENT: async () => {
  const page = await login('user1@example.com'); // 传入具体用户邮箱

  await page.goto(URL);
  await page.getByRole('link', { name: 'Create compensation event' }).click();

  // ... 后续操作代码不变

  await logout()
},

方案3:向XState模型注入浏览器实例

如果需要更灵活的控制,可将浏览器实例作为参数传入XState的事件处理函数:

// 修改测试代码,传入browser实例
test(path.description, async ({ browser }) => {
  await path.test(page, browser);
});

// 修改XState模型事件,接收并使用browser实例
const testModel = createModel(CEWorkflowMachine, {
  events: {
    CREATE_CE_CLIENT: async (_, { browser }) => {
      // 创建新上下文和页面
      const context = await browser.newContext();
      const page = await context.newPage();
      
      await page.goto(URL);
      await page.getByTestId('loginEmail').fill('user1@example.com');
      // ... 登录和操作代码
      
      await context.close();
    },

内容的提问来源于stack exchange,提问作者sam evans

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 13:39:54