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
相关产品推荐
相关产品推荐

