refactor(frontend): every stored key declares its kind, and preferences follow the account
storage.ts's registry gives each key a kind (pref, device, view, cache, public); a key without one does not compile. A contract test refuses a key spelled out elsewhere, an account preference read or written straight from storage instead of through usePref, and an e2e seed of a key nobody reads. The keys the next account's wipe keeps are the public ones. The seven preferences kept only in the browser move onto usePref: start on Home, the onboarding dismissal, the release banner's last version, the channel manager's layout, the Plex playlist layout, the playlist rail's sort and channel discovery's sort. usePref reads an older bare-string copy, and a value only this browser held is saved to the server once, so the account's other devices get it. Performance mode is the device's: it is neither read from the account nor saved to it.
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
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");
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
@@ -5,7 +5,9 @@ import { openApp, gotoPage } from "./helpers";
|
||||
// Appearance change persists on its own via PUT /me/preferences and a transient indicator confirms
|
||||
// it; leaving Settings after a change is no longer guarded.
|
||||
test.describe("settings auto-save", () => {
|
||||
const TOGGLE = "Performance mode"; // an Appearance switch (locale pinned to en)
|
||||
// An Appearance switch saved to the account (locale pinned to en). Not performance mode: that one
|
||||
// is the device's and is never sent.
|
||||
const TOGGLE = "Show hints";
|
||||
|
||||
// The save is debounced (~500ms), so wait for the PUT rather than assuming it's synchronous.
|
||||
const putPrefs = (page: Page) =>
|
||||
|
||||
Reference in New Issue
Block a user