commit 23c9b511ec05e46a574dd42034556d4a85abdd0b
parent 6f3249a2ad39b13b02074c317ccd35b91aec2142
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Wed, 30 Sep 2026 00:49:21 -0400
editor(e2e): site-scope — a site picked in another tab, and no ?site= once a page settles (review M1, L2)
- New case: tab A stores alpha and sits on /settings (passive refresh off, so
only the broadcast can move it); tab B picks beta; tab A's picker becomes
beta without a reload, and its next Channels page is beta's; a client-side
visit to /sites/alpha from tab A records alpha, and tab B follows. With the
broadcast's refresh removed, tab A stays on alpha.
- `expectNoSiteParam()`: after the page hydrates and settles, a no-retry check
that the URL has no `site` param, at the three inverted URL assertions and
after each settle in the first-paint case. `toHaveURL` alone passes on its
first poll, before a replace from an effect could land.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
1 file changed, 57 insertions(+), 1 deletion(-)
diff --git a/editor/e2e/site-scope.spec.ts b/editor/e2e/site-scope.spec.ts
@@ -1,7 +1,7 @@
import { test, expect, type Page } from "@playwright/test";
import { activeSiteCookieName } from "../app/lib/activeSite";
import { baseUrl } from "./baseUrl";
-import { readJson, resetData, writeSite } from "./helpers";
+import { readJson, resetData, writeSettings, writeSite } from "./helpers";
type SiteFile = { channels: { slug: string }[] };
@@ -38,12 +38,14 @@ test("selecting a site scopes the channels list and persists", async ({
await expect(page).toHaveURL(/\/channels$/);
await expect(page.getByRole("link", { name: "slow-a" })).toBeVisible();
await expect(page.getByRole("link", { name: "slow-b" })).toHaveCount(0);
+ await expectNoSiteParam(page);
// The cookie persists the choice: revisiting with no param re-applies it.
await page.goto("/channels");
await expect(page.getByLabel("Active site")).toHaveValue("alpha");
await expect(page).toHaveURL(/\/channels$/);
await expect(page.getByRole("link", { name: "slow-b" })).toHaveCount(0);
+ await expectNoSiteParam(page);
// Switching to beta flips the scope.
await page.getByLabel("Active site").selectOption("beta");
@@ -79,6 +81,7 @@ test("charts is a site's tab, and the picker follows the path", async ({
await page.goto("/channels");
await expect(page.getByLabel("Active site")).toHaveValue("beta");
await expect(page).toHaveURL(/\/channels$/);
+ await expectNoSiteParam(page);
// "All sites" is the family page.
await page.goto("/sites/beta/charts");
await page.getByLabel("Active site").selectOption("__all__");
@@ -212,6 +215,15 @@ function watchHydration(page: Page): string[] {
return seen;
}
+// No `?site=` on the URL once the page has hydrated and settled, read with no
+// retry. `toHaveURL` alone would pass on its first poll, before a `replace` from
+// an effect (how the old picker seeded the param) could land.
+async function expectNoSiteParam(page: Page) {
+ await hydrated(page);
+ await page.waitForTimeout(SETTLE_MS);
+ expect(new URL(page.url()).searchParams.has("site"), page.url()).toBe(false);
+}
+
async function storedCookie(page: Page): Promise<string | undefined> {
const cookies = await page.context().cookies();
return cookies.find((c) => c.name === COOKIE)?.value;
@@ -246,6 +258,7 @@ test("a stored site is the picker's first paint on every page, never All sites",
await hydrated(page);
await page.waitForTimeout(SETTLE_MS);
expect(await pickerValues(page), `${path}: every value`).toEqual(["alpha"]);
+ expect(new URL(page.url()).searchParams.has("site"), page.url()).toBe(false);
}
// Client-side navigations: the layout persists, the picker re-renders.
@@ -265,6 +278,7 @@ test("a stored site is the picker's first paint on every page, never All sites",
expect(await pickerValues(page), "every value across the navigations").toEqual([
"alpha",
]);
+ expect(new URL(page.url()).searchParams.has("site"), page.url()).toBe(false);
// Visiting another site's page records it; Dashboard then paints it at once.
expect(await firstPaint(page, "/sites/beta"), "/sites/beta: first paint").toBe(
@@ -356,3 +370,45 @@ test("Back to a site's page shows that site, not the choice made there", async (
await expect(page).toHaveURL(/\/sites\/alpha\/charts$/);
await expect(picker).toHaveValue("alpha");
});
+
+// A site picked in one tab reaches the others. The cookie is shared, but a
+// tab's picker reads its root layout, which a client-side navigation does not
+// re-render; without the broadcast, tab A kept showing alpha over beta's pages
+// and skipped recording a visit to alpha's page.
+test("a site picked in another tab reaches this one", async ({ context }) => {
+ await twoSites();
+ // Passive refresh off: a tree refresh whenever the pulse moves would also
+ // re-read the layout, and only the broadcast may move tab A here.
+ await writeSettings({ autoRefreshIntervalSeconds: 0 });
+ const a = await context.newPage();
+ const pickerA = a.getByLabel("Active site");
+ await a.goto("/channels");
+ await pickerA.selectOption("alpha");
+ await expect(a.getByRole("link", { name: "slow-b" })).toHaveCount(0);
+ await a.goto("/settings");
+ await hydrated(a);
+
+ const b = await context.newPage();
+ await b.goto("/channels");
+ await b.getByLabel("Active site").selectOption("beta");
+ await expect(b.getByRole("link", { name: "slow-a" })).toHaveCount(0);
+ expect(await storedCookie(b)).toBe("beta");
+
+ // Tab A follows without a reload, and its next page agrees with its picker.
+ await expect(pickerA).toHaveValue("beta");
+ await a.locator("aside nav").getByRole("link", { name: "Channels", exact: true }).click();
+ await expect(a).toHaveURL(/\/channels$/);
+ await expect(pickerA).toHaveValue("beta");
+ await expect(a.getByRole("link", { name: "slow-b" })).toBeVisible();
+ await expect(a.getByRole("link", { name: "slow-a" })).toHaveCount(0);
+
+ // And a client-side visit to alpha's page is recorded again: tab A no longer
+ // believes alpha is stored.
+ await a.locator("aside nav").getByRole("link", { name: "Sites", exact: true }).click();
+ await expect(a).toHaveURL(/\/sites$/);
+ await a.locator('main a[href="/sites/alpha"]').click();
+ await expect(a).toHaveURL(/\/sites\/alpha$/);
+ await expect.poll(() => storedCookie(a), { timeout: 15_000 }).toBe("alpha");
+ // Tab B hears of that write too.
+ await expect(b.getByLabel("Active site")).toHaveValue("alpha");
+});