Files
peter 94418a087e fix(auth): a tab acts only as the account it was built for, and the review's other findings
A tab names the account it was claimed for on every request, beacon and upload
(X-Siftlode-Expect-Account); current_user and /auth/logout refuse a mismatch with
401 account_changed and keep the session, so a tab left showing the account
signed in before reloads instead of writing to the new one.

/auth/login clears a connect marker left by an abandoned connect, and a link
completes only on a session that still holds. The prefs merge reads the row
under a lock. A tab's sessionStorage records its own owner. A page the account
loses is given up, address bar and remembered page included. The panel layouts
are preferences; the storage contract also refuses a key handed to the Storage
API or to usePref outside its kind. A refused sign-in as someone else and a
failed sign-out say so. The chromium-only e2e specs are in the gate register,
the member walk waits for its requests, and devices.spec checks the save itself.
2026-10-21 01:05:12 +02:00

62 lines
2.8 KiB
TypeScript

import { test, expect, devices, type Browser, type Page } from "@playwright/test";
import { MEMBER_STORAGE, gotoPage, openApp } from "./helpers";
// One member on two devices, a desktop and an iPad, each with its own browser storage. A preference is
// a choice about the account, so a change on one device shows on the other; a device setting (here
// performance mode, for a weak GPU) stays on the device it was set on. Kept in browser storage only, a
// preference used to stay on the one device it was changed on.
async function device(browser: Browser, descriptor: (typeof devices)[string]): Promise<Page> {
const context = await browser.newContext({
...descriptor,
baseURL: test.info().project.use.baseURL,
locale: "en-US",
storageState: MEMBER_STORAGE,
});
return context.newPage();
}
const savedPrefs = (page: Page) =>
page.waitForResponse(
(r) => r.url().includes("/me/preferences") && r.request().method() === "PUT",
);
async function pickLayout(page: Page, from: string, to: string): Promise<void> {
await page.getByRole("button", { name: `Layout: ${from}` }).click();
await Promise.all([savedPrefs(page), page.getByRole("menuitemradio", { name: to }).click()]);
await expect(page.getByRole("button", { name: `Layout: ${to}` })).toBeVisible();
}
test("a preference follows the member to another device, a device setting stays", async ({
browser,
}) => {
const desktop = await device(browser, devices["Desktop Chrome"]);
const ipad = await device(browser, devices["iPad (gen 7) landscape"]);
try {
await openApp(desktop);
await gotoPage(desktop, "channels");
await pickLayout(desktop, "Table", "Cards");
await gotoPage(desktop, "settings");
await desktop.getByRole("switch", { name: "Performance mode" }).click();
await expect(desktop.locator("html")).toHaveAttribute("data-perf", "1");
// The Settings prefs save as one payload: the next one (a hints change) would carry performance
// mode if it went to the account. Waited for, so the iPad below cannot simply load first.
const hints = desktop.getByRole("switch", { name: "Show hints" });
const [save] = await Promise.all([savedPrefs(desktop), hints.click()]);
expect(Object.keys(save.request().postDataJSON() as object)).not.toContain("performanceMode");
await Promise.all([savedPrefs(desktop), hints.click()]);
await openApp(ipad);
await expect(ipad.locator("html")).not.toHaveAttribute("data-perf", "1");
await gotoPage(ipad, "channels");
await expect(ipad.getByRole("button", { name: "Layout: Cards" })).toBeVisible();
} finally {
// The member is shared by the suite: put the layout back (performance mode went with the
// desktop's storage).
await gotoPage(desktop, "channels");
await pickLayout(desktop, "Cards", "Table");
await desktop.context().close();
await ipad.context().close();
}
});