mirror of
https://github.com/block/buzz.git
synced 2026-10-30 22:11:36 +00:00
test(e2e): verify app origin before storage setup
Signed-off-by: Brother Darryl <146fb160a3266e6165bfa385f6048c975eda9e21cf65da097a0b5ea7952532a5@buzz.block.builderlab.xyz>
This commit is contained in:
10
AGENTS.md
10
AGENTS.md
@@ -384,9 +384,13 @@ you; prefer them over a manual build plus `playwright test`.
|
||||
serves old code. Kill port 4173 and re-run `pnpm build:e2e` before re-running
|
||||
tests after code changes.
|
||||
|
||||
**`addInitScript` before bridge:** `page.addInitScript` (localStorage seeding)
|
||||
must run BEFORE `installMockBridge(page)` — React reads state on mount, the
|
||||
bridge triggers mount.
|
||||
**Bootstrap before app interaction:** New Playwright specs must use
|
||||
`bootstrapE2ePage(page, { beforeNavigate })` from `tests/helpers/bootstrap`
|
||||
for their first app navigation. Register `addInitScript` seeding in
|
||||
`beforeNavigate` (and before `installMockBridge(page)`) so React reads it at
|
||||
mount. The helper requires a successful navigation to the configured HTTP
|
||||
origin and reports a missing build or preview server before a later
|
||||
`localStorage` access can misreport it as a storage failure.
|
||||
|
||||
**Live messages:** Call `waitForMockLiveSubscription(page, channelName)` before
|
||||
`__BUZZ_E2E_EMIT_MOCK_MESSAGE__` — messages are silently dropped without a
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
|
||||
import { E2E_APP_ORIGIN, bootstrapE2ePage } from "../helpers/bootstrap";
|
||||
import { installMockBridge, openChannelBrowser } from "../helpers/bridge";
|
||||
|
||||
const MOCK_PUBKEY = "deadbeef".repeat(8);
|
||||
@@ -7,13 +8,18 @@ const CUSTOM_SECTION = { id: "sec-projects", name: "Projects", order: 0 };
|
||||
|
||||
async function seedCustomSection(page: Page) {
|
||||
await page.addInitScript(
|
||||
({ pubkey, section }) => {
|
||||
({ expectedOrigin, pubkey, section }) => {
|
||||
if (location.origin !== expectedOrigin) return;
|
||||
window.localStorage.setItem(
|
||||
`buzz-channel-sections.v1:${pubkey}`,
|
||||
JSON.stringify({ version: 1, sections: [section], assignments: {} }),
|
||||
);
|
||||
},
|
||||
{ pubkey: MOCK_PUBKEY, section: CUSTOM_SECTION },
|
||||
{
|
||||
expectedOrigin: E2E_APP_ORIGIN,
|
||||
pubkey: MOCK_PUBKEY,
|
||||
section: CUSTOM_SECTION,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,7 +33,7 @@ test.beforeEach(async ({ page }, testInfo) => {
|
||||
});
|
||||
|
||||
test("keyboard shortcut opens the channel browser dialog", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
await expect(page.getByTestId("app-sidebar")).toBeVisible();
|
||||
|
||||
const isMacBrowser = await page.evaluate(() =>
|
||||
@@ -53,7 +59,7 @@ test("keyboard shortcut opens the channel browser dialog", async ({ page }) => {
|
||||
});
|
||||
|
||||
test("channel browser shows channels not yet joined", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
|
||||
await openChannelBrowser(page);
|
||||
await expect(page.getByTestId("channel-browser-dialog")).toBeVisible();
|
||||
@@ -72,7 +78,7 @@ test("channel browser shows channels not yet joined", async ({ page }) => {
|
||||
test("channel browser sorts alphabetically or by member count", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
|
||||
await openChannelBrowser(page);
|
||||
const rows = page.locator('[data-testid^="browse-channel-"]');
|
||||
@@ -112,7 +118,7 @@ test("channel browser sorts alphabetically or by member count", async ({
|
||||
});
|
||||
|
||||
test("channel browser sorts by recent activity", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
|
||||
await openChannelBrowser(page);
|
||||
const rows = page.locator('[data-testid^="browse-channel-"]');
|
||||
@@ -139,7 +145,7 @@ test("channel browser sorts by recent activity", async ({ page }) => {
|
||||
});
|
||||
|
||||
test("channel browser search filters by name", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
|
||||
await openChannelBrowser(page);
|
||||
await expect(page.getByTestId("channel-browser-dialog")).toBeVisible();
|
||||
@@ -152,7 +158,7 @@ test("channel browser search filters by name", async ({ page }) => {
|
||||
});
|
||||
|
||||
test("channel browser search filters by description", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
|
||||
await openChannelBrowser(page);
|
||||
await page.getByTestId("channel-browser-search").fill("pipeline");
|
||||
@@ -165,7 +171,7 @@ test("channel browser search filters by description", async ({ page }) => {
|
||||
test("channel browser shows no results for unmatched search", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
|
||||
await openChannelBrowser(page);
|
||||
await page.getByTestId("channel-browser-search").fill("zzz-nonexistent");
|
||||
@@ -174,7 +180,7 @@ test("channel browser shows no results for unmatched search", async ({
|
||||
});
|
||||
|
||||
test("channel browser fuzzy-matches a subsequence", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
|
||||
await openChannelBrowser(page);
|
||||
// "engr" is not a substring of "engineering", but it is an in-order
|
||||
@@ -186,7 +192,7 @@ test("channel browser fuzzy-matches a subsequence", async ({ page }) => {
|
||||
});
|
||||
|
||||
test("channel browser matches a scattered subsequence", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
|
||||
await openChannelBrowser(page);
|
||||
// "sls" is neither a substring nor a prefix of "sales" — it only matches as
|
||||
@@ -198,7 +204,7 @@ test("channel browser matches a scattered subsequence", async ({ page }) => {
|
||||
});
|
||||
|
||||
test("channel browser ranks the best match first", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
|
||||
await openChannelBrowser(page);
|
||||
// "gen" is a prefix of "general" (strong match) but only a substring of
|
||||
@@ -216,7 +222,7 @@ test("channel browser ranks the best match first", async ({ page }) => {
|
||||
test("sidebar add-channel button creates without treating the click as a callback", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
await expect(page.getByTestId("app-sidebar")).toBeVisible();
|
||||
|
||||
await page.getByTestId("section-actions-channels-quick-create").click();
|
||||
@@ -235,7 +241,7 @@ test("custom section add button creates directly into that section", async ({
|
||||
page,
|
||||
}) => {
|
||||
await seedCustomSection(page);
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
|
||||
const addButton = page.getByTestId(
|
||||
`section-actions-${CUSTOM_SECTION.id}-quick-create`,
|
||||
@@ -261,7 +267,7 @@ test("canceling section create does not affect the next global create", async ({
|
||||
page,
|
||||
}) => {
|
||||
await seedCustomSection(page);
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
|
||||
await page
|
||||
.getByTestId(`section-actions-${CUSTOM_SECTION.id}-quick-create`)
|
||||
@@ -290,7 +296,7 @@ test("failed section create retry still assigns to the section", async ({
|
||||
page,
|
||||
}) => {
|
||||
await seedCustomSection(page);
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
|
||||
await page
|
||||
.getByTestId(`section-actions-${CUSTOM_SECTION.id}-quick-create`)
|
||||
@@ -309,7 +315,7 @@ test("failed section create retry still assigns to the section", async ({
|
||||
});
|
||||
|
||||
test("create affordance is visible on open before typing", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
|
||||
await openChannelBrowser(page);
|
||||
|
||||
@@ -323,7 +329,7 @@ test("create affordance is visible on open before typing", async ({ page }) => {
|
||||
test("typing a partial match surfaces a persistent create row", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
|
||||
await openChannelBrowser(page);
|
||||
// "desig" matches "design" by substring but is not an exact channel name,
|
||||
@@ -337,7 +343,7 @@ test("typing a partial match surfaces a persistent create row", async ({
|
||||
});
|
||||
|
||||
test("exact name match hides the create row", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
|
||||
await openChannelBrowser(page);
|
||||
await page.getByTestId("channel-browser-search").fill("general");
|
||||
@@ -349,7 +355,7 @@ test("exact name match hides the create row", async ({ page }) => {
|
||||
test("no-match search pins a create row above the empty state", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
|
||||
await openChannelBrowser(page);
|
||||
await page.getByTestId("channel-browser-search").fill("zzz-nonexistent");
|
||||
@@ -361,7 +367,7 @@ test("no-match search pins a create row above the empty state", async ({
|
||||
});
|
||||
|
||||
test("create row leads to the prefilled create form", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
|
||||
await openChannelBrowser(page);
|
||||
await page.getByTestId("channel-browser-search").fill("desig");
|
||||
@@ -380,7 +386,7 @@ test("creating from the browser adds the channel to the sidebar", async ({
|
||||
}) => {
|
||||
const channelName = `browse-created-${Date.now()}`;
|
||||
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
|
||||
await openChannelBrowser(page);
|
||||
await page.getByTestId("channel-browser-search").fill(channelName);
|
||||
@@ -399,7 +405,7 @@ test("creating from the browser adds the channel to the sidebar", async ({
|
||||
test("Enter with no matches jumps to create", async ({ page }) => {
|
||||
const channelName = `enter-created-${Date.now()}`;
|
||||
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
|
||||
await openChannelBrowser(page);
|
||||
await page.getByTestId("channel-browser-search").fill(channelName);
|
||||
@@ -413,7 +419,7 @@ test("Enter with no matches jumps to create", async ({ page }) => {
|
||||
test("arrow keys reach the pinned create row and Enter activates it", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
|
||||
await openChannelBrowser(page);
|
||||
// "desig" keeps a channel match (#design) AND the create row visible, so the
|
||||
@@ -436,7 +442,7 @@ test("arrow keys reach the pinned create row and Enter activates it", async ({
|
||||
test("Enter selects a channel when create row is not highlighted", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
|
||||
await openChannelBrowser(page);
|
||||
// With the create row present but NOT highlighted, Enter should still select
|
||||
@@ -453,7 +459,7 @@ test("Enter selects a channel when create row is not highlighted", async ({
|
||||
test("joining a channel from browser adds it to the sidebar", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
|
||||
// Verify "design" is not in the sidebar
|
||||
const streamList = page.getByTestId("stream-list");
|
||||
@@ -479,7 +485,7 @@ test("joining a channel from browser adds it to the sidebar", async ({
|
||||
test("clicking a joined channel in browser navigates to it", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
|
||||
await openChannelBrowser(page);
|
||||
await expect(page.getByTestId("channel-browser-dialog")).toBeVisible();
|
||||
@@ -495,7 +501,7 @@ test("clicking a joined channel in browser navigates to it", async ({
|
||||
test("channel browser does not show DM or private channels", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
|
||||
await openChannelBrowser(page);
|
||||
await expect(page.getByTestId("channel-browser-dialog")).toBeVisible();
|
||||
@@ -509,7 +515,7 @@ test("channel browser does not show DM or private channels", async ({
|
||||
});
|
||||
|
||||
test("channel browser closes on escape", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
|
||||
await openChannelBrowser(page);
|
||||
await expect(page.getByTestId("channel-browser-dialog")).toBeVisible();
|
||||
@@ -519,7 +525,7 @@ test("channel browser closes on escape", async ({ page }) => {
|
||||
});
|
||||
|
||||
test("keyboard navigation works in channel browser", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
|
||||
await openChannelBrowser(page);
|
||||
await expect(page.getByTestId("channel-browser-dialog")).toBeVisible();
|
||||
@@ -536,7 +542,7 @@ test("keyboard navigation works in channel browser", async ({ page }) => {
|
||||
});
|
||||
|
||||
test("sidebar only shows channels the user has joined", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
|
||||
const streamList = page.getByTestId("stream-list");
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { waitForAnimations } from "../helpers/animations";
|
||||
import { E2E_APP_ORIGIN, bootstrapE2ePage } from "../helpers/bootstrap";
|
||||
import { TEST_IDENTITIES, installMockBridge } from "../helpers/bridge";
|
||||
|
||||
const SHOTS = "test-results/drafts-all-fix";
|
||||
@@ -18,34 +19,39 @@ test("Inbox All hides drafts while the Drafts filter keeps them", async ({
|
||||
page,
|
||||
}) => {
|
||||
const draftKey = `channel:${GENERAL_CHANNEL_ID}`;
|
||||
await page.addInitScript(
|
||||
({ draftStoreKey, draftStorageKey, channelId }) => {
|
||||
const timestamp = new Date().toISOString();
|
||||
window.localStorage.setItem(
|
||||
draftStoreKey,
|
||||
JSON.stringify({
|
||||
[draftStorageKey]: {
|
||||
channelId,
|
||||
content: "Draft that must stay out of the All view",
|
||||
createdAt: timestamp,
|
||||
pendingImeta: [],
|
||||
selectionEnd: 41,
|
||||
selectionStart: 41,
|
||||
spoileredAttachmentUrls: [],
|
||||
status: "active",
|
||||
updatedAt: timestamp,
|
||||
},
|
||||
}),
|
||||
await bootstrapE2ePage(page, {
|
||||
beforeNavigate: async () => {
|
||||
await page.addInitScript(
|
||||
({ expectedOrigin, draftStoreKey, draftStorageKey, channelId }) => {
|
||||
if (location.origin !== expectedOrigin) return;
|
||||
const timestamp = new Date().toISOString();
|
||||
window.localStorage.setItem(
|
||||
draftStoreKey,
|
||||
JSON.stringify({
|
||||
[draftStorageKey]: {
|
||||
channelId,
|
||||
content: "Draft that must stay out of the All view",
|
||||
createdAt: timestamp,
|
||||
pendingImeta: [],
|
||||
selectionEnd: 41,
|
||||
selectionStart: 41,
|
||||
spoileredAttachmentUrls: [],
|
||||
status: "active",
|
||||
updatedAt: timestamp,
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
{
|
||||
channelId: GENERAL_CHANNEL_ID,
|
||||
expectedOrigin: E2E_APP_ORIGIN,
|
||||
draftStorageKey: draftKey,
|
||||
draftStoreKey: `buzz-drafts.v2:ws://localhost:3000:${MOCK_IDENTITY_PUBKEY}`,
|
||||
},
|
||||
);
|
||||
await installMockBridge(page);
|
||||
},
|
||||
{
|
||||
channelId: GENERAL_CHANNEL_ID,
|
||||
draftStorageKey: draftKey,
|
||||
draftStoreKey: `buzz-drafts.v2:ws://localhost:3000:${MOCK_IDENTITY_PUBKEY}`,
|
||||
},
|
||||
);
|
||||
await installMockBridge(page);
|
||||
await page.goto("/");
|
||||
});
|
||||
await page.waitForFunction(() => {
|
||||
const win = window as MockFeedWindow;
|
||||
return typeof win.__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__ === "function";
|
||||
|
||||
62
desktop/tests/helpers/bootstrap.ts
Normal file
62
desktop/tests/helpers/bootstrap.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
|
||||
export const E2E_APP_ORIGIN = "http://127.0.0.1:4173";
|
||||
|
||||
type BootstrapE2ePageOptions = {
|
||||
/** Relative route to load after all test setup has been registered. */
|
||||
path?: string;
|
||||
/** Test setup such as bridge or storage init-script registration. */
|
||||
beforeNavigate?: () => Promise<void>;
|
||||
expectedOrigin?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers page setup and commits the first app navigation before a spec uses
|
||||
* browser storage or page evaluation. This makes an unavailable preview server
|
||||
* fail at its source instead of later as an opaque-origin storage error.
|
||||
*/
|
||||
export async function bootstrapE2ePage(
|
||||
page: Page,
|
||||
{
|
||||
path = "/",
|
||||
beforeNavigate,
|
||||
expectedOrigin = E2E_APP_ORIGIN,
|
||||
}: BootstrapE2ePageOptions = {},
|
||||
): Promise<void> {
|
||||
await beforeNavigate?.();
|
||||
|
||||
const target = new URL(path, expectedOrigin).toString();
|
||||
let response: Awaited<ReturnType<Page["goto"]>>;
|
||||
try {
|
||||
response = await page.goto(target);
|
||||
} catch (error) {
|
||||
throw bootstrapFailure(expectedOrigin, page.url(), error);
|
||||
}
|
||||
|
||||
if (!response?.ok()) {
|
||||
throw bootstrapFailure(
|
||||
expectedOrigin,
|
||||
page.url(),
|
||||
`navigation returned HTTP ${response?.status() ?? "no response"}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (new URL(page.url()).origin !== expectedOrigin) {
|
||||
throw bootstrapFailure(
|
||||
expectedOrigin,
|
||||
page.url(),
|
||||
"navigation changed origin",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function bootstrapFailure(
|
||||
expectedOrigin: string,
|
||||
currentUrl: string,
|
||||
cause: unknown,
|
||||
): Error {
|
||||
return new Error(
|
||||
`E2E app navigation did not commit to ${expectedOrigin} (current URL: ${currentUrl}). ` +
|
||||
`Run pnpm test:e2e:smoke or run pnpm build:e2e and check the preview server. Cause: ${String(cause)}`,
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type { Page } from "@playwright/test";
|
||||
import type { ChannelTemplate, RelayEvent } from "../../src/shared/api/types";
|
||||
import type { MockManagedAgentSeed } from "../../src/testing/e2eBridge";
|
||||
import { FEATURE_OVERRIDES_STORAGE_KEY, PREVIEW_FEATURE_IDS } from "./features";
|
||||
import { E2E_APP_ORIGIN } from "./bootstrap";
|
||||
|
||||
export const TEST_IDENTITIES = {
|
||||
tyler: {
|
||||
@@ -741,7 +742,14 @@ async function seedOnboardingCompletionForKnownIdentities(
|
||||
...Object.values(TEST_IDENTITIES).map(({ pubkey }) => pubkey),
|
||||
];
|
||||
await page.addInitScript(
|
||||
({ onboardingPrefix, pubkeys: pubkeysToSeed, relayUrl, welcomePrefix }) => {
|
||||
({
|
||||
expectedOrigin,
|
||||
onboardingPrefix,
|
||||
pubkeys: pubkeysToSeed,
|
||||
relayUrl,
|
||||
welcomePrefix,
|
||||
}) => {
|
||||
if (location.origin !== expectedOrigin) return;
|
||||
const welcomeScope = encodeURIComponent(relayUrl);
|
||||
for (const pubkey of pubkeysToSeed) {
|
||||
window.localStorage.setItem(`${onboardingPrefix}${pubkey}`, "true");
|
||||
@@ -752,6 +760,7 @@ async function seedOnboardingCompletionForKnownIdentities(
|
||||
}
|
||||
},
|
||||
{
|
||||
expectedOrigin: E2E_APP_ORIGIN,
|
||||
onboardingPrefix: ONBOARDING_COMPLETION_STORAGE_KEY_PREFIX,
|
||||
pubkeys,
|
||||
relayUrl: relayWsUrl ?? DEFAULT_RELAY_WS_URL,
|
||||
@@ -766,7 +775,8 @@ async function seedDefaultCommunity(
|
||||
relayWsUrl?: string,
|
||||
) {
|
||||
await page.addInitScript(
|
||||
({ fallback, identityOverrideKey, relayUrl }) => {
|
||||
({ expectedOrigin, fallback, identityOverrideKey, relayUrl }) => {
|
||||
if (location.origin !== expectedOrigin) return;
|
||||
// If seedActiveIdentity() ran before this script (the normal ordering),
|
||||
// use its pubkey so the community matches the active identity and
|
||||
// migrateMachineOnboardingCompletion's strict voucher accepts it.
|
||||
@@ -807,6 +817,7 @@ async function seedDefaultCommunity(
|
||||
window.localStorage.setItem("buzz-active-community-id", communityId);
|
||||
},
|
||||
{
|
||||
expectedOrigin: E2E_APP_ORIGIN,
|
||||
fallback: fallbackPubkey,
|
||||
identityOverrideKey: "buzz:e2e-identity-override.v1",
|
||||
relayUrl: relayWsUrl ?? DEFAULT_RELAY_WS_URL,
|
||||
@@ -816,12 +827,17 @@ async function seedDefaultCommunity(
|
||||
|
||||
async function seedPreviewFeaturesEnabled(page: Page) {
|
||||
await page.addInitScript(
|
||||
({ key, ids }) => {
|
||||
({ expectedOrigin, key, ids }) => {
|
||||
if (location.origin !== expectedOrigin) return;
|
||||
const overrides: Record<string, boolean> = {};
|
||||
for (const id of ids) overrides[id] = true;
|
||||
window.localStorage.setItem(key, JSON.stringify(overrides));
|
||||
},
|
||||
{ key: FEATURE_OVERRIDES_STORAGE_KEY, ids: PREVIEW_FEATURE_IDS },
|
||||
{
|
||||
expectedOrigin: E2E_APP_ORIGIN,
|
||||
key: FEATURE_OVERRIDES_STORAGE_KEY,
|
||||
ids: PREVIEW_FEATURE_IDS,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"private": true,
|
||||
"packageManager": "pnpm@11.4.0",
|
||||
"scripts": {
|
||||
"check": "pnpm -r check"
|
||||
"check": "pnpm -r check",
|
||||
"test:e2e:smoke": "pnpm --dir desktop test:e2e:smoke"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.4.6"
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { bootstrapE2ePage } from "../helpers/bootstrap";
|
||||
|
||||
test("home page loads with Buzz branding", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
await expect(
|
||||
page.getByRole("main").getByRole("img", { name: "Buzz" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("home page shows repositories section", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await bootstrapE2ePage(page);
|
||||
await expect(page.getByText("Repositories")).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -66,7 +68,7 @@ test("invite requires age and legal consent before opening Buzz", async ({
|
||||
]),
|
||||
});
|
||||
});
|
||||
await page.goto("/invite/demo-code");
|
||||
await bootstrapE2ePage(page, { path: "/invite/demo-code" });
|
||||
|
||||
await expect(
|
||||
page.getByRole("link", { name: "Download it now" }),
|
||||
@@ -124,30 +126,32 @@ test("invite can enroll a NIP-07 identity for browser access", async ({
|
||||
page,
|
||||
}) => {
|
||||
const pubkey = "ab".repeat(32);
|
||||
await page.addInitScript((extensionPubkey) => {
|
||||
(
|
||||
window as Window & {
|
||||
nostr?: {
|
||||
getPublicKey(): Promise<string>;
|
||||
signEvent(
|
||||
event: Record<string, unknown>,
|
||||
): Promise<Record<string, unknown>>;
|
||||
};
|
||||
}
|
||||
).nostr = {
|
||||
async getPublicKey() {
|
||||
return extensionPubkey;
|
||||
},
|
||||
async signEvent(event) {
|
||||
return {
|
||||
...event,
|
||||
id: "cd".repeat(32),
|
||||
pubkey: extensionPubkey,
|
||||
sig: "ef".repeat(64),
|
||||
};
|
||||
},
|
||||
};
|
||||
}, pubkey);
|
||||
const installNip07Extension = async () => {
|
||||
await page.addInitScript((extensionPubkey) => {
|
||||
(
|
||||
window as Window & {
|
||||
nostr?: {
|
||||
getPublicKey(): Promise<string>;
|
||||
signEvent(
|
||||
event: Record<string, unknown>,
|
||||
): Promise<Record<string, unknown>>;
|
||||
};
|
||||
}
|
||||
).nostr = {
|
||||
async getPublicKey() {
|
||||
return extensionPubkey;
|
||||
},
|
||||
async signEvent(event) {
|
||||
return {
|
||||
...event,
|
||||
id: "cd".repeat(32),
|
||||
pubkey: extensionPubkey,
|
||||
sig: "ef".repeat(64),
|
||||
};
|
||||
},
|
||||
};
|
||||
}, pubkey);
|
||||
};
|
||||
await page.route("**/api/join-policy", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
@@ -195,7 +199,10 @@ test("invite can enroll a NIP-07 identity for browser access", async ({
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/invite/browser-code");
|
||||
await bootstrapE2ePage(page, {
|
||||
beforeNavigate: installNip07Extension,
|
||||
path: "/invite/browser-code",
|
||||
});
|
||||
await page.getByRole("button", { name: "Join in browser" }).click();
|
||||
await expect(page).toHaveURL("/");
|
||||
expect(claimObserved).toBe(true);
|
||||
@@ -227,7 +234,7 @@ test("invite asks Safari users to choose their Mac download", async ({
|
||||
await route.fulfill({ status: 500 });
|
||||
});
|
||||
|
||||
await page.goto("/invite/demo-code");
|
||||
await bootstrapE2ePage(page, { path: "/invite/demo-code" });
|
||||
const download = page.getByRole("link", { name: "Download it now" });
|
||||
await expect(download).toHaveAttribute("aria-haspopup", "dialog");
|
||||
await download.click();
|
||||
@@ -339,7 +346,7 @@ test("invite download falls back for mobile and non-desktop devices", async ({
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/invite/demo-code");
|
||||
await bootstrapE2ePage(page, { path: "/invite/demo-code" });
|
||||
await expect(
|
||||
page.getByRole("link", { name: "Download it now" }),
|
||||
device.name,
|
||||
|
||||
62
web/tests/helpers/bootstrap.ts
Normal file
62
web/tests/helpers/bootstrap.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
|
||||
export const E2E_APP_ORIGIN = "http://127.0.0.1:4173";
|
||||
|
||||
type BootstrapE2ePageOptions = {
|
||||
/** Relative route to load after all test setup has been registered. */
|
||||
path?: string;
|
||||
/** Test setup such as extension or storage init-script registration. */
|
||||
beforeNavigate?: () => Promise<void>;
|
||||
expectedOrigin?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers page setup and commits the first app navigation before a spec uses
|
||||
* browser storage or page evaluation. This makes an unavailable preview server
|
||||
* fail at its source instead of later as an opaque-origin storage error.
|
||||
*/
|
||||
export async function bootstrapE2ePage(
|
||||
page: Page,
|
||||
{
|
||||
path = "/",
|
||||
beforeNavigate,
|
||||
expectedOrigin = E2E_APP_ORIGIN,
|
||||
}: BootstrapE2ePageOptions = {},
|
||||
): Promise<void> {
|
||||
await beforeNavigate?.();
|
||||
|
||||
const target = new URL(path, expectedOrigin).toString();
|
||||
let response: Awaited<ReturnType<Page["goto"]>>;
|
||||
try {
|
||||
response = await page.goto(target);
|
||||
} catch (error) {
|
||||
throw bootstrapFailure(expectedOrigin, page.url(), error);
|
||||
}
|
||||
|
||||
if (!response?.ok()) {
|
||||
throw bootstrapFailure(
|
||||
expectedOrigin,
|
||||
page.url(),
|
||||
`navigation returned HTTP ${response?.status() ?? "no response"}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (new URL(page.url()).origin !== expectedOrigin) {
|
||||
throw bootstrapFailure(
|
||||
expectedOrigin,
|
||||
page.url(),
|
||||
"navigation changed origin",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function bootstrapFailure(
|
||||
expectedOrigin: string,
|
||||
currentUrl: string,
|
||||
cause: unknown,
|
||||
): Error {
|
||||
return new Error(
|
||||
`E2E app navigation did not commit to ${expectedOrigin} (current URL: ${currentUrl}). ` +
|
||||
`Run pnpm test:e2e:smoke or run pnpm build and check the preview server. Cause: ${String(cause)}`,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user