Files
buzz/web/tests/e2e/smoke.spec.ts
Tom Brow 4ab4f78608 fix(web): route mobile invite downloads to app stores (#7702)
Route the invite page’s existing download link to the App Store on
iOS/iPadOS and Google Play on Android. Appearance and app-opening
behavior stay unchanged.

The browser test checks platform routing using shared URL constants. Web
build, lint, and all six smoke tests pass. Rebased onto main to pick up
the relay test compilation fix.

Refs #3357. Alternative to #3464 with a smaller implementation and no
duplicated store URLs in tests.

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
2026-09-16 21:48:05 -07:00

378 lines
12 KiB
TypeScript

import { createHash } from "node:crypto";
import { expect, test } from "@playwright/test";
import {
BUZZ_ANDROID_PLAY_STORE_URL,
BUZZ_IOS_APP_STORE_URL,
BUZZ_RELEASES_URL,
} from "../../src/shared/lib/buzz-download";
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("Repositories")).toBeVisible();
});
test("invite requires age and legal consent before opening Buzz", async ({
page,
}) => {
await page.route("**/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: "Download it now" }),
).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("I am 18 years of age or older.");
const agreementConfirmation = page.getByLabel(
"I agree to the Buzz Terms of Service and Privacy Policy.",
);
const acceptInvite = page.getByRole("button", {
name: "Accept invite in Buzz",
});
await expect(ageConfirmation).toBeVisible();
await expect(agreementConfirmation).toBeVisible();
await expect(acceptInvite).toBeDisabled();
const termsLink = page.getByRole("button", { name: "Terms of Service" });
const privacyLink = page.getByRole("button", { name: "Privacy Policy" });
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: "I am 18 years of age or older." })
.click();
await expect(ageConfirmation).toBeChecked();
await expect(acceptInvite).toBeDisabled();
await page
.locator("label")
.filter({
hasText: "I agree to the Buzz Terms of Service and Privacy Policy.",
})
.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<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,
contentType: "application/json",
body: JSON.stringify({ policy: null }),
});
});
let claimObserved = false;
await page.route("**/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: "Join in browser" }).click();
await expect(page).toHaveURL("/");
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("**/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: "Download it now" });
await expect(download).toHaveAttribute("aria-haspopup", "dialog");
await download.click();
const chooser = page.getByRole("dialog", {
name: "Which Mac do you have?",
});
await expect(chooser).toBeVisible();
await expect(chooser.getByRole("link", { name: /Newer Mac/ })).toContainText(
"2021 or later, or a late-2020 Mac with an Apple M1 chip",
);
await expect(chooser.getByRole("link", { name: /Older Mac/ })).toContainText(
"2019 or earlier, or a 2020 Mac with an Intel processor",
);
await expect(chooser.getByText("About This Mac")).toBeVisible();
const openedPagePromise = context.waitForEvent("page");
await chooser.getByRole("link", { name: /Newer Mac/ }).click();
const openedPage = await openedPagePromise;
await expect(chooser).toBeHidden();
await expect(openedPage).toHaveURL("https://github.com/block/buzz/releases");
await expect(page).toHaveURL(/\/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 links to the appropriate platform destination", async ({
browser,
}) => {
const devices = [
{
name: "iPhone Safari",
expectedUrl: BUZZ_IOS_APP_STORE_URL,
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",
expectedUrl: BUZZ_IOS_APP_STORE_URL,
platform: "MacIntel",
userAgent:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15",
maxTouchPoints: 5,
},
{
name: "Android phone",
expectedUrl: BUZZ_ANDROID_PLAY_STORE_URL,
platform: "Linux armv8l",
userAgent:
"Mozilla/5.0 (Linux; Android 15; Pixel 9 Pro) AppleWebKit/537.36 Mobile",
maxTouchPoints: 5,
},
{
name: "Fire tablet",
expectedUrl: BUZZ_RELEASES_URL,
platform: "Linux armv8l",
userAgent:
"Mozilla/5.0 (Linux; Android 9; KFMAWI) AppleWebKit/537.36 Silk/126.0 like Chrome/126.0.0.0 Safari/537.36",
maxTouchPoints: 5,
},
{
name: "ChromeOS",
expectedUrl: BUZZ_RELEASES_URL,
platform: "Linux x86_64",
userAgent: "Mozilla/5.0 (X11; CrOS x86_64 16093.68.0) AppleWebKit/537.36",
maxTouchPoints: 0,
},
];
for (const device of devices) {
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,
// Store routing must not wait for desktop architecture hints.
getHighEntropyValues: () => new Promise(() => {}),
},
},
});
}, device);
const page = await context.newPage();
await page.route("**/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: "Download it now" }),
device.name,
).toHaveAttribute("href", device.expectedUrl);
await context.route(device.expectedUrl, (route) =>
route.fulfill({ contentType: "text/html", body: "Store destination" }),
);
const destination = context.waitForEvent("page");
await page.getByRole("link", { name: "Download it now" }).click();
await expect(await destination).toHaveURL(device.expectedUrl);
await context.close();
}
});