import { createHash } from "node:crypto"; import { expect, test } from "@playwright/test"; test("home page loads with Buzz branding", async ({ page }) => { await page.goto("./"); await expect( page.getByRole("main").getByRole("img", { name: "Buzz" }), ).toBeVisible(); }); test("home page shows repositories section", async ({ page }) => { await page.goto("./"); await expect(page.getByText("代码仓库")).toBeVisible(); }); test("defaults to Simplified Chinese and persists the selected locale", async ({ page, }) => { await page.goto("./"); await expect(page.locator("html")).toHaveAttribute("lang", "zh-Hans"); await expect(page.getByText("代码仓库")).toBeVisible(); await page.getByRole("button", { name: "切换为 English" }).click(); await expect(page.locator("html")).toHaveAttribute("lang", "en"); await expect(page.getByText("Repositories")).toBeVisible(); await page.reload(); await expect(page.locator("html")).toHaveAttribute("lang", "en"); await expect(page.getByText("Repositories")).toBeVisible(); }); test("uses the /buzz base path for relay URLs and invite APIs", async ({ page, }) => { let policyUrl = ""; await page.route("**/buzz/api/join-policy", async (route) => { policyUrl = route.request().url(); await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ policy: null }), }); }); await page.goto("./invite/base-path-code"); await expect( page.getByRole("link", { name: "在 Buzz 中接受邀请" }), ).toHaveAttribute("href", /relay=ws%3A%2F%2F127\.0\.0\.1%3A4173%2Fbuzz/); expect(policyUrl).toContain("/buzz/api/join-policy"); }); test("invite requires age and legal consent before opening Buzz", async ({ page, }) => { await page.route("**/buzz/api/join-policy", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ policy: { terms_markdown: "# Terms", privacy_markdown: "# Privacy", age_attestation_required: true, version: "policy-v1", }, }), }); }); await page.route("https://api.github.com/**", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", headers: { "Access-Control-Allow-Origin": "*" }, body: JSON.stringify([ { draft: false, prerelease: false, assets: [] }, { draft: false, prerelease: false, assets: [ { name: "Buzz_0.4.9_aarch64.dmg", browser_download_url: "https://github.com/block/buzz/releases/download/v0.4.9/Buzz_0.4.9_aarch64.dmg", }, { name: "Buzz_0.4.9_x64.dmg", browser_download_url: "https://github.com/block/buzz/releases/download/v0.4.9/Buzz_0.4.9_x64.dmg", }, { name: "Buzz_0.4.9_amd64.AppImage", browser_download_url: "https://github.com/block/buzz/releases/download/v0.4.9/Buzz_0.4.9_amd64.AppImage", }, { name: "Buzz_0.4.9_x64-setup_alpha-unsigned.exe", browser_download_url: "https://github.com/block/buzz/releases/download/v0.4.9/Buzz_0.4.9_x64-setup_alpha-unsigned.exe", }, ], }, ]), }); }); await page.goto("./invite/demo-code"); await expect(page.getByRole("link", { name: "立即下载" })).toHaveAttribute( "href", "https://github.com/block/buzz/releases/download/v0.4.9/Buzz_0.4.9_x64-setup_alpha-unsigned.exe", ); const ageConfirmation = page.getByLabel("我已年满 18 周岁。"); const agreementConfirmation = page.getByLabel( "我同意 Buzz 的服务条款和隐私政策。", ); const acceptInvite = page.getByRole("button", { name: "在 Buzz 中接受邀请", }); await expect(ageConfirmation).toBeVisible(); await expect(agreementConfirmation).toBeVisible(); await expect(acceptInvite).toBeDisabled(); const termsLink = page.getByRole("button", { name: "服务条款" }); const privacyLink = page.getByRole("button", { name: "隐私政策" }); await expect(termsLink).toHaveCSS("text-decoration-line", "none"); await expect(privacyLink).toHaveCSS("text-decoration-line", "none"); await termsLink.hover(); await expect(termsLink).toHaveCSS("text-decoration-line", "underline"); await page.mouse.move(0, 0); await privacyLink.hover(); await expect(privacyLink).toHaveCSS("text-decoration-line", "underline"); await page.locator("label").filter({ hasText: "我已年满 18 周岁。" }).click(); await expect(ageConfirmation).toBeChecked(); await expect(acceptInvite).toBeDisabled(); await page .locator("label") .filter({ hasText: "我同意 Buzz 的服务条款和隐私政策。", }) .click({ position: { x: 8, y: 8 } }); await expect(agreementConfirmation).toBeChecked(); await expect(acceptInvite).toBeEnabled(); const consentBox = await page .getByTestId("invite-join-policy-notice") .boundingBox(); const acceptButtonBox = await acceptInvite.boundingBox(); expect(consentBox?.y).toBeLessThan(acceptButtonBox?.y ?? 0); expect(consentBox?.width).toBe(acceptButtonBox?.width); }); 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; signEvent( event: Record, ): Promise>; }; } ).nostr = { async getPublicKey() { return extensionPubkey; }, async signEvent(event) { return { ...event, id: "cd".repeat(32), pubkey: extensionPubkey, sig: "ef".repeat(64), }; }, }; }, pubkey); await page.route("**/buzz/api/join-policy", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ policy: null }), }); }); let claimObserved = false; await page.route("**/buzz/api/invites/claim", async (route) => { claimObserved = true; const request = route.request(); const body = request.postData() ?? ""; expect(JSON.parse(body)).toEqual({ code: "browser-code", }); const authorization = request.headers().authorization; expect(authorization).toMatch(/^Nostr /); const event = JSON.parse( Buffer.from(authorization.slice("Nostr ".length), "base64").toString( "utf8", ), ) as { pubkey: string; tags: string[][]; }; expect(event.pubkey).toBe(pubkey); expect(event.tags).toContainEqual(["u", request.url()]); expect(event.tags).toContainEqual(["method", "POST"]); expect(event.tags).toContainEqual([ "payload", createHash("sha256").update(body).digest("hex"), ]); await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ status: "joined", community_id: "community-id", host: "127.0.0.1", role: "member", }), }); }); await page.goto("./invite/browser-code"); await page.getByRole("button", { name: "在浏览器中加入" }).click(); await expect(page).toHaveURL(/\/buzz\/$/); expect(claimObserved).toBe(true); }); test("invite asks Safari users to choose their Mac download", async ({ browser, }) => { const context = await browser.newContext({ userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Version/26.5 Safari/605.1.15", }); await context.addInitScript(() => { Object.defineProperties(navigator, { platform: { configurable: true, value: "MacIntel" }, maxTouchPoints: { configurable: true, value: 0 }, userAgentData: { configurable: true, value: undefined }, }); }); const page = await context.newPage(); await page.route("**/buzz/api/join-policy", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ policy: null }), }); }); await page.route("https://api.github.com/**", async (route) => { await route.fulfill({ status: 500 }); }); await page.goto("./invite/demo-code"); const download = page.getByRole("link", { name: "立即下载" }); await expect(download).toHaveAttribute("aria-haspopup", "dialog"); await download.click(); const chooser = page.getByRole("dialog", { name: "你的 Mac 是哪一款?", }); await expect(chooser).toBeVisible(); await expect(chooser.getByRole("link", { name: /较新的 Mac/ })).toContainText( "2021 年或之后发布", ); await expect(chooser.getByRole("link", { name: /较早的 Mac/ })).toContainText( "2019 年或之前发布", ); await expect(chooser.getByText("关于本机")).toBeVisible(); const openedPagePromise = context.waitForEvent("page"); await chooser.getByRole("link", { name: /较新的 Mac/ }).click(); const openedPage = await openedPagePromise; await expect(chooser).toBeHidden(); await expect(openedPage).toHaveURL("https://github.com/block/buzz/releases"); await expect(page).toHaveURL(/\/buzz\/invite\/demo-code$/); await openedPage.close(); await download.click(); await expect(chooser).toBeVisible(); await page.keyboard.press("Escape"); await expect(chooser).toBeHidden(); await expect(download).toBeFocused(); await context.close(); }); test("invite download falls back for mobile and non-desktop devices", async ({ browser, }) => { const unsupportedDevices = [ { name: "iPhone Safari", platform: "iPhone", userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15", maxTouchPoints: 5, }, { name: "iPadOS desktop mode", platform: "MacIntel", userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15", maxTouchPoints: 5, }, { name: "Android phone", platform: "Linux armv8l", userAgent: "Mozilla/5.0 (Linux; Android 15; Pixel 9 Pro) AppleWebKit/537.36 Mobile", maxTouchPoints: 5, }, { name: "ChromeOS", platform: "Linux x86_64", userAgent: "Mozilla/5.0 (X11; CrOS x86_64 16093.68.0) AppleWebKit/537.36", maxTouchPoints: 0, }, ]; for (const device of unsupportedDevices) { const context = await browser.newContext({ userAgent: device.userAgent }); await context.addInitScript(({ platform, maxTouchPoints }) => { Object.defineProperties(navigator, { platform: { configurable: true, value: platform }, maxTouchPoints: { configurable: true, value: maxTouchPoints }, userAgentData: { configurable: true, value: { platform, mobile: maxTouchPoints > 0 }, }, }); }, device); const page = await context.newPage(); await page.route("**/buzz/api/join-policy", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ policy: null }), }); }); await page.route("https://api.github.com/**", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", headers: { "Access-Control-Allow-Origin": "*" }, body: JSON.stringify([ { draft: false, prerelease: false, assets: [ { name: "Buzz_0.4.9_x64.dmg", browser_download_url: "https://github.com/block/buzz/releases/download/v0.4.9/Buzz_0.4.9_x64.dmg", }, { name: "Buzz_0.4.9_amd64.AppImage", browser_download_url: "https://github.com/block/buzz/releases/download/v0.4.9/Buzz_0.4.9_amd64.AppImage", }, ], }, ]), }); }); await page.goto("./invite/demo-code"); await expect( page.getByRole("link", { name: "立即下载" }), device.name, ).toHaveAttribute("href", "https://github.com/block/buzz/releases"); await context.close(); } });