import { test, expect, Page } from '@playwright/test'; import { api, ensureUser, hsReachable, loginUI, PASSWORD } from './localHs'; // Signing out other devices with SSO confirmation. The desktop app opens the SSO // page in the system browser (window.open gives no window back), so the dialog // must still offer "Continue" — before the fix the only way on was starting // over, in a loop (reported by a desktop SSO user). The server is mocked: the // retry succeeds only if it carries the challenge's session. const openDevicesAndSignOutOther = async (page: Page, flows: string[][]) => { const sent: unknown[] = []; await page.route('**/delete_devices', async (route) => { const body = JSON.parse(route.request().postData() ?? '{}'); sent.push(body.auth ?? null); if (body.auth?.session === 'S1') { return route.fulfill({ status: 200, contentType: 'application/json', body: '{}' }); } return route.fulfill({ status: 401, contentType: 'application/json', body: JSON.stringify({ session: 'S1', flows: flows.map((stages) => ({ stages })), params: {}, }), }); }); await page.getByRole('button', { name: 'User Settings' }).click(); const dialog = page.getByRole('dialog').first(); await dialog.getByText('Privacy & security', { exact: true }).first().click(); const card = dialog .getByText('Old laptop', { exact: true }) .locator('xpath=ancestor::*[.//button][1]'); await card.getByRole('button').nth(1).click(); // the delete chip const bar = dialog.getByText(/Logout from selected devices/); await bar .locator('xpath=ancestor::*[.//button[normalize-space()="Logout"]][1]') .getByRole('button', { name: 'Logout', exact: true }) .click(); return { sent, bar }; }; test.describe('SSO confirmation for signing out devices', () => { test.beforeAll(async () => { test.skip(!(await hsReachable()), 'local homeserver not running'); }); test('desktop: no popup window, Continue still completes it', async ({ page }) => { test.setTimeout(120_000); await page.addInitScript(() => { window.open = () => null; // the desktop app hands the page to the system browser }); const user = await ensureUser(`ssod${Date.now().toString(36)}`); await api('POST', '/_matrix/client/v3/login', undefined, { type: 'm.login.password', identifier: { type: 'm.id.user', user: user.localpart }, password: PASSWORD, initial_device_display_name: 'Old laptop', }); await loginUI(page, user); const { sent, bar } = await openDevicesAndSignOutOther(page, [['m.login.sso']]); await page.getByRole('button', { name: 'Continue with SSO' }).click(); await page.getByRole('button', { name: 'Continue', exact: true }).click(); await expect(bar).toHaveCount(0); expect(sent).toEqual([null, { session: 'S1' }]); }); test('password offered first: SSO is still one click away', async ({ page }) => { test.setTimeout(120_000); const user = await ensureUser(`ssop${Date.now().toString(36)}`); await api('POST', '/_matrix/client/v3/login', undefined, { type: 'm.login.password', identifier: { type: 'm.id.user', user: user.localpart }, password: PASSWORD, initial_device_display_name: 'Old laptop', }); await loginUI(page, user); await openDevicesAndSignOutOther(page, [['m.login.password'], ['m.login.sso']]); await expect(page.getByText('Account Password').first()).toBeVisible(); await page.getByRole('button', { name: 'Use SSO instead' }).click(); await expect(page.getByRole('button', { name: 'Continue with SSO' })).toBeVisible(); await expect(page.getByRole('button', { name: 'Use your password instead' })).toBeVisible(); }); });