this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Re-delete the restored web e2e suite

Restored in a61a611 from git history but never rewired against the
post-SDK architecture. Two tests already had stale TS errors against
HEAD (service-worker.test.ts: missing ServiceWorker types; sharing
.test.ts: broken Page import), the rest carried pre-SDK assumptions
that would need re-authoring anyway. No CI runs this suite, so the
rot was silent.

Drops tests/tests/web/ and its snapshots, the web-specific helpers
(web-fixture, seed-phrase), Playwright config, and the global-setup.
Removes @playwright/test from tests/, the test:web* scripts, the
tests/web/** vitest exclude (redundant now), and the e2e-web justfile
target. CLI e2e suite is untouched.

+3 -2290
+1 -5
justfile
··· 123 123 e2e-cli: 124 124 cd tests && bun test tests/cli/ 125 125 126 - # Run web e2e tests (requires running web + indexer) 127 - e2e-web: 128 - cd tests && bun test tests/web/ 129 - 130 126 # Run all e2e tests 131 - e2e: e2e-cli e2e-web 127 + e2e: e2e-cli 132 128 133 129 # --------------------------------------------------------------------------- 134 130 # CI / validation
-95
tests/global-setup.ts
··· 1 - // Global setup for Playwright web e2e tests. 2 - // 3 - // Starts a fake-pds instance (OAuth mode) and a Vite dev server pointing at it. 4 - // Registers a pool of test accounts for parallel isolation. 5 - 6 - import { createFakePds, type FakePds } from "fake-pds"; 7 - import { spawn, type ChildProcess } from "node:child_process"; 8 - import { writeFileSync, unlinkSync, mkdirSync } from "node:fs"; 9 - import path from "node:path"; 10 - import { generateAccounts, AccountPool } from "fake-pds"; 11 - 12 - const STATE_FILE = path.join(import.meta.dirname, ".e2e-state.json"); 13 - 14 - 15 - function waitForViteReady(proc: ChildProcess, timeoutMs = 30_000): Promise<string> { 16 - return new Promise((resolve, reject) => { 17 - const timeout = setTimeout(() => { 18 - reject(new Error(`Vite dev server did not start within ${timeoutMs}ms`)); 19 - }, timeoutMs); 20 - 21 - const onData = (chunk: Buffer) => { 22 - const text = chunk.toString(); 23 - const match = /https?:\/\/localhost:\d+/.exec(text); 24 - if (match) { 25 - clearTimeout(timeout); 26 - proc.stdout?.off("data", onData); 27 - resolve(match[0]); 28 - } 29 - }; 30 - 31 - proc.stdout?.on("data", onData); 32 - proc.stderr?.on("data", (chunk: Buffer) => { 33 - process.stderr.write(chunk); 34 - }); 35 - 36 - proc.on("error", (err) => { 37 - clearTimeout(timeout); 38 - reject(err); 39 - }); 40 - 41 - proc.on("exit", (code) => { 42 - clearTimeout(timeout); 43 - if (code !== null && code !== 0) { 44 - reject(new Error(`Vite dev server exited with code ${code}`)); 45 - } 46 - }); 47 - }); 48 - } 49 - 50 - export default async function globalSetup(): Promise<() => Promise<void>> { 51 - // 1. Generate account pool and start fake-pds 52 - const pool = generateAccounts(); 53 - const pds: FakePds = await createFakePds({ 54 - accounts: [...pool], 55 - auth: "oauth", 56 - }); 57 - 58 - // 2. Start Vite dev server with env vars pointing at fake-pds 59 - const webRoot = path.resolve(import.meta.dirname, "../web"); 60 - const vite: ChildProcess = spawn("bun", ["run", "dev"], { 61 - cwd: webRoot, 62 - env: { 63 - ...process.env, 64 - VITE_RESOLVE_API: pds.url, 65 - VITE_PLC_DIRECTORY_URL: pds.url, 66 - VITE_INDEXER_URL: "", 67 - }, 68 - stdio: ["pipe", "pipe", "pipe"], 69 - }); 70 - 71 - const webUrl = await waitForViteReady(vite); 72 - 73 - // 3. Write state + pool for test fixtures 74 - mkdirSync(path.dirname(STATE_FILE), { recursive: true }); 75 - writeFileSync( 76 - STATE_FILE, 77 - JSON.stringify({ pdsUrl: pds.url, webUrl, pool }), 78 - ); 79 - 80 - // 4. Clean stale lock files from prior runs 81 - const accountPool = new AccountPool(pool); 82 - accountPool.clearLocks(); 83 - 84 - // 5. Teardown 85 - return async () => { 86 - vite.kill("SIGTERM"); 87 - await pds.close(); 88 - accountPool.clearLocks(); 89 - try { 90 - unlinkSync(STATE_FILE); 91 - } catch { 92 - // already cleaned up 93 - } 94 - }; 95 - }
-92
tests/helpers/seed-phrase.ts
··· 1 - // Shared helper: complete the seed phrase setup flow in the browser. 2 - // 3 - // Used by any test that needs encryption (file browser, sharing). 4 - // Handles stale PDS state from prior tests by deleting the publicKey 5 - // and reloading to force fresh identity state. 6 - 7 - import { expect, type Page } from "@playwright/test"; 8 - 9 - /** 10 - * Ensure the page is showing fresh identity state ("Create my key"), 11 - * then complete the full seed phrase generation + confirmation flow. 12 - * 13 - * If a prior test left a publicKey on the PDS (causing "remote_only" 14 - * state), this is handled by the caller passing `ensureFresh` context. 15 - */ 16 - export async function completeSeedPhraseSetup( 17 - page: Page, 18 - ensureFresh?: { pdsUrl: string; handle: string; did: string }, 19 - ): Promise<void> { 20 - // If the page isn't showing fresh state, clean up and reload 21 - const createKey = page.getByText(/Create my key/); 22 - const isAlreadyFresh = await createKey.isVisible().catch(() => false); 23 - 24 - if (!isAlreadyFresh && ensureFresh) { 25 - // Delete existing publicKey via authenticated XRPC (per-account, no global reset) 26 - await deletePublicKeyViaXrpc(ensureFresh.pdsUrl, ensureFresh.handle, ensureFresh.did); 27 - await page.reload(); 28 - await expect(createKey).toBeVisible({ timeout: 10_000 }); 29 - } 30 - 31 - await createKey.click(); 32 - await expect(page.getByRole("list")).toBeVisible({ timeout: 10_000 }); 33 - 34 - // Read the 24 words (keyed by displayed number, not DOM order) 35 - const items = page.getByRole("listitem"); 36 - const words: string[] = new Array(24).fill(""); 37 - const count = await items.count(); 38 - for (let i = 0; i < count; i++) { 39 - const text = await items.nth(i).textContent(); 40 - const match = text?.match(/^(\d+)\.\s*(.+)$/); 41 - if (match) { 42 - words[parseInt(match[1]!, 10) - 1] = match[2]!.trim(); 43 - } 44 - } 45 - 46 - await page.getByLabel(/I have written down/).check(); 47 - await page.getByRole("button", { name: /Continue/ }).click(); 48 - 49 - // Fill the 3 random confirmation words 50 - await expect(page.getByText(/Confirm your seed phrase/)).toBeVisible(); 51 - const confirmLabels = page.locator("label").filter({ hasText: /^Word #/ }); 52 - const labelCount = await confirmLabels.count(); 53 - for (let i = 0; i < labelCount; i++) { 54 - const labelText = await confirmLabels.nth(i).textContent(); 55 - const wordNum = parseInt(labelText?.match(/Word #(\d+)/)?.[1] ?? "0", 10); 56 - const word = words[wordNum - 1]; 57 - if (word) { 58 - await confirmLabels.nth(i).locator("input").fill(word); 59 - } 60 - } 61 - await page.getByRole("button", { name: /Confirm/ }).click(); 62 - 63 - await expect(page.getByText(/You're all set/)).toBeVisible({ timeout: 15_000 }); 64 - } 65 - 66 - /** Delete a publicKey record via PAR → token → deleteRecord. */ 67 - export async function deletePublicKeyViaXrpc(pdsUrl: string, handle: string, did: string): Promise<void> { 68 - const parRes = await fetch(`${pdsUrl}/oauth/par`, { 69 - method: "POST", 70 - headers: { "Content-Type": "application/x-www-form-urlencoded" }, 71 - body: new URLSearchParams({ client_id: "test", login_hint: handle }).toString(), 72 - }); 73 - if (!parRes.ok) return; 74 - const { code } = (await parRes.json()) as { code: string }; 75 - 76 - const tokenRes = await fetch(`${pdsUrl}/oauth/token`, { 77 - method: "POST", 78 - headers: { "Content-Type": "application/x-www-form-urlencoded" }, 79 - body: new URLSearchParams({ grant_type: "authorization_code", code }).toString(), 80 - }); 81 - if (!tokenRes.ok) return; 82 - const { access_token } = (await tokenRes.json()) as { access_token: string }; 83 - 84 - await fetch(`${pdsUrl}/xrpc/com.atproto.repo.deleteRecord`, { 85 - method: "POST", 86 - headers: { 87 - "Content-Type": "application/json", 88 - Authorization: `DPoP ${access_token}`, 89 - }, 90 - body: JSON.stringify({ repo: did, collection: "app.opake.publicKey", rkey: "self" }), 91 - }); 92 - }
-110
tests/helpers/web-fixture.ts
··· 1 - // Playwright test fixtures for web e2e tests. 2 - // 3 - // Core fixtures: pdsUrl, webUrl, account (auto-acquired from pool), 4 - // browserLogin, and resetPds. See global-setup.ts for server lifecycle. 5 - 6 - import { test as base, expect, type Page } from "@playwright/test"; 7 - import { readFileSync } from "node:fs"; 8 - import path from "node:path"; 9 - import { AccountPool, type Account } from "fake-pds"; 10 - import { deletePublicKeyViaXrpc } from "./seed-phrase.js"; 11 - 12 - interface E2eState { 13 - readonly pdsUrl: string; 14 - readonly webUrl: string; 15 - readonly pool: readonly Account[]; 16 - } 17 - 18 - let cachedState: E2eState | null = null; 19 - 20 - function loadState(): E2eState { 21 - if (cachedState) return cachedState; 22 - const statePath = path.join(import.meta.dirname, "../.e2e-state.json"); 23 - cachedState = JSON.parse(readFileSync(statePath, "utf-8")) as E2eState; 24 - return cachedState; 25 - } 26 - 27 - /** 28 - * Perform a full browser-based OAuth login against fake-pds. 29 - * Waits for the redirect chain to complete and the devices page to render. 30 - */ 31 - async function doLogin( 32 - page: Page, 33 - webUrl: string, 34 - handle: string, 35 - ): Promise<void> { 36 - await page.goto(`${webUrl}/devices/login`); 37 - await page.getByLabel("AT Protocol handle").fill(handle); 38 - await page.getByRole("button", { name: /Sign in/ }).click(); 39 - 40 - // Wait for OAuth redirect chain: login → PAR → authorize → callback → /devices 41 - await expect( 42 - page.getByText( 43 - /Setting things up|Welcome to Opake|You're all set|Welcome back/, 44 - ), 45 - ).toBeVisible({ timeout: 15_000 }); 46 - } 47 - 48 - interface WebFixtures { 49 - pdsUrl: string; 50 - webUrl: string; 51 - /** Auto-acquired unique account from the pool — cleaned up after test. */ 52 - account: Account; 53 - resetPds: () => Promise<void>; 54 - browserLogin: (handle?: string) => Promise<void>; 55 - } 56 - 57 - export const test = base.extend<WebFixtures>({ 58 - pdsUrl: async ({}, use) => { 59 - const { pdsUrl } = loadState(); 60 - await use(pdsUrl); 61 - }, 62 - 63 - webUrl: async ({}, use) => { 64 - const { webUrl } = loadState(); 65 - await use(webUrl); 66 - }, 67 - 68 - account: async ({ pdsUrl }, use) => { 69 - const { pool: accounts } = loadState(); 70 - const accountPool = new AccountPool(accounts); 71 - const { account, release } = accountPool.acquire(); 72 - // Clean up state from any prior test run (per-DID, not global reset) 73 - await fetch( 74 - `${pdsUrl}/_test/cleanup?did=${encodeURIComponent(account.did)}`, 75 - { 76 - method: "POST", 77 - }, 78 - ); 79 - await use(account); 80 - release(); 81 - }, 82 - 83 - resetPds: async ({ pdsUrl }, use) => { 84 - await use(async () => { 85 - await fetch(`${pdsUrl}/_test/reset`, { method: "POST" }); 86 - }); 87 - }, 88 - 89 - browserLogin: async ({ page, webUrl, account }, use) => { 90 - // Surface browser errors (not warnings or debug noise) for test diagnostics. 91 - page.on("console", (msg) => { 92 - if (msg.type() === "error") { 93 - const text = msg.text(); 94 - // Suppress known non-errors 95 - if (text.startsWith("Failed to load resource")) return; 96 - if (text.includes("no identity for")) return; 97 - console.log(`[browser:error] ${text}`); 98 - } 99 - }); 100 - page.on("pageerror", (err) => { 101 - console.log(`[browser:pageerror] ${err.message}`); 102 - }); 103 - 104 - await use(async (handle?: string) => { 105 - await doLogin(page, webUrl, handle ?? account.handle); 106 - }); 107 - }, 108 - }); 109 - 110 - export { expect };
+1 -5
tests/package.json
··· 4 4 "type": "module", 5 5 "scripts": { 6 6 "test": "vitest run", 7 - "test:watch": "vitest", 8 - "test:web": "playwright test", 9 - "test:web:headed": "playwright test --headed", 10 - "test:web:ui": "playwright test --ui" 7 + "test:watch": "vitest" 11 8 }, 12 9 "devDependencies": { 13 - "@playwright/test": "^1.58.2", 14 10 "@types/node": "^22", 15 11 "fake-pds": "^0.7.0", 16 12 "typescript": "^5.7",
-22
tests/playwright.config.ts
··· 1 - import { defineConfig } from "@playwright/test"; 2 - 3 - export default defineConfig({ 4 - testDir: "tests/web", 5 - timeout: 60_000, 6 - retries: 2, 7 - globalSetup: "./global-setup.ts", 8 - // Tests share one fake-pds — use different accounts per file to avoid 9 - // state conflicts. Login tests use resetPds and run serially. 10 - // 4 workers balances parallelism with Vite dev server capacity. 11 - // Higher values cause timeout flakes under concurrent WASM + OAuth load. 12 - workers: 4, 13 - use: { 14 - headless: true, 15 - screenshot: "only-on-failure", 16 - trace: "retain-on-failure", 17 - }, 18 - expect: { 19 - toHaveScreenshot: { maxDiffPixelRatio: 0.02 }, 20 - }, 21 - projects: [{ name: "chromium", use: { browserName: "chromium" } }], 22 - });
-82
tests/tests/web/download.test.ts
··· 1 - // File download: intercept browser download event, verify filename and content. 2 - 3 - import { test, expect } from "../../helpers/web-fixture.js"; 4 - import { completeSeedPhraseSetup } from "../../helpers/seed-phrase.js"; 5 - import { readFile } from "node:fs/promises"; 6 - 7 - test.describe("file download", () => { 8 - test("download file via action menu", async ({ 9 - page, 10 - webUrl, 11 - pdsUrl, 12 - account, 13 - browserLogin, 14 - }) => { 15 - await browserLogin(); 16 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 17 - 18 - await page.goto(`${webUrl}/cabinet/files`); 19 - await page.waitForTimeout(2_000); 20 - 21 - // Upload via filechooser 22 - const [fileChooser] = await Promise.all([ 23 - page.waitForEvent("filechooser"), 24 - page.getByTestId("file-upload").click({ force: true }), 25 - ]); 26 - await fileChooser.setFiles({ 27 - name: "download-test.txt", 28 - mimeType: "text/plain", 29 - buffer: Buffer.from("download me"), 30 - }); 31 - 32 - // Wait for uploaded file to appear with decrypted name 33 - const fileRow = page.locator('[aria-label*="download-test"]').first(); 34 - await expect(fileRow).toBeVisible({ timeout: 30_000 }); 35 - 36 - // Open action menu and download 37 - await fileRow.locator("button[aria-haspopup]").click(); 38 - 39 - const downloadPromise = page.waitForEvent("download"); 40 - await page.locator("ul.menu").getByRole("button", { name: "Download" }).click(); 41 - const download = await downloadPromise; 42 - 43 - expect(download.suggestedFilename()).toBe("download-test.txt"); 44 - 45 - const downloadPath = await download.path(); 46 - expect(downloadPath).toBeTruthy(); 47 - const content = await readFile(downloadPath!, "utf-8"); 48 - expect(content).toBe("download me"); 49 - }); 50 - 51 - test("download button exists for uploaded files", async ({ 52 - page, 53 - webUrl, 54 - pdsUrl, 55 - account, 56 - browserLogin, 57 - }) => { 58 - await browserLogin(); 59 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 60 - 61 - await page.goto(`${webUrl}/cabinet/files`); 62 - await page.waitForTimeout(2_000); 63 - 64 - const [fileChooser] = await Promise.all([ 65 - page.waitForEvent("filechooser"), 66 - page.getByTestId("file-upload").click({ force: true }), 67 - ]); 68 - await fileChooser.setFiles({ 69 - name: "menu-check.txt", 70 - mimeType: "text/plain", 71 - buffer: Buffer.from("check action menu"), 72 - }); 73 - 74 - const fileRow = page.locator('[aria-label*="menu-check"]').first(); 75 - await expect(fileRow).toBeVisible({ timeout: 30_000 }); 76 - 77 - await fileRow.locator("button[aria-haspopup]").click(); 78 - await expect( 79 - page.locator("ul.menu").getByRole("button", { name: "Download" }), 80 - ).toBeVisible(); 81 - }); 82 - });
-128
tests/tests/web/file-browser.test.ts
··· 1 - // File browser: upload, folder creation, delete, and navigation. 2 - 3 - import { test, expect } from "../../helpers/web-fixture.js"; 4 - import { completeSeedPhraseSetup } from "../../helpers/seed-phrase.js"; 5 - 6 - test.describe("file lifecycle", () => { 7 - test("upload, verify, and delete a file", async ({ 8 - page, 9 - webUrl, 10 - pdsUrl, 11 - account, 12 - browserLogin, 13 - }) => { 14 - await browserLogin(); 15 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 16 - 17 - await page.goto(`${webUrl}/cabinet/files`); 18 - 19 - await expect(page.getByText("Nothing here yet")).toBeVisible({ 20 - timeout: 10_000, 21 - }); 22 - await expect(page).toHaveScreenshot("file-browser-empty.png"); 23 - 24 - // Upload via filechooser 25 - const [fileChooser] = await Promise.all([ 26 - page.waitForEvent("filechooser"), 27 - page.getByTestId("file-upload").click({ force: true }), 28 - ]); 29 - await fileChooser.setFiles({ 30 - name: "test-file.txt", 31 - mimeType: "text/plain", 32 - buffer: Buffer.from("hello from e2e test"), 33 - }); 34 - 35 - // Wait for uploaded file to appear with decrypted name 36 - const fileRow = page.locator('[aria-label*="test-file"]').first(); 37 - await expect(fileRow).toBeVisible({ timeout: 30_000 }); 38 - 39 - // Open action menu and delete 40 - await fileRow.locator("button[aria-haspopup]").click(); 41 - await page.getByRole("button", { name: "Delete" }).click(); 42 - 43 - // Confirm deletion 44 - const deleteDialog = page.locator('dialog[aria-label="Delete file?"]'); 45 - await expect(deleteDialog).toBeVisible(); 46 - await deleteDialog.getByRole("button", { name: "Delete" }).click(); 47 - 48 - // File gone, empty state returns 49 - await expect(fileRow).not.toBeVisible({ timeout: 10_000 }); 50 - await expect(page.getByText("Nothing here yet")).toBeVisible(); 51 - }); 52 - }); 53 - 54 - test.describe("folders", () => { 55 - test("create folder and navigate into it", async ({ 56 - page, 57 - webUrl, 58 - pdsUrl, 59 - account, 60 - browserLogin, 61 - }) => { 62 - await browserLogin(); 63 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 64 - 65 - await page.goto(`${webUrl}/cabinet/files`); 66 - await page.waitForLoadState("networkidle"); 67 - 68 - await page.getByRole("button", { name: "New" }).click(); 69 - await page.getByRole("button", { name: "New folder" }).click(); 70 - 71 - const folderDialog = page.locator('dialog[aria-label="New folder"]'); 72 - await expect(folderDialog).toBeVisible(); 73 - await folderDialog.getByLabel("Folder name").fill("Test Folder"); 74 - await folderDialog.getByRole("button", { name: "Create" }).click(); 75 - 76 - const folderRow = page.locator('[aria-label="Test Folder, folder"]'); 77 - await expect(folderRow).toBeVisible({ timeout: 10_000 }); 78 - 79 - // Navigate into folder 80 - await folderRow.click(); 81 - 82 - await expect( 83 - page.locator(".breadcrumbs").getByText("Test Folder").first(), 84 - ).toBeVisible(); 85 - await expect( 86 - page.getByRole("link", { name: "Your Cabinet" }).first(), 87 - ).toBeVisible(); 88 - 89 - // Inside is empty 90 - await expect(page.getByText("Nothing here yet")).toBeVisible(); 91 - }); 92 - }); 93 - 94 - test.describe("new folder dialog validation", () => { 95 - test("create button disabled when name is empty", async ({ 96 - page, 97 - webUrl, 98 - pdsUrl, 99 - account, 100 - browserLogin, 101 - }) => { 102 - await browserLogin(); 103 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 104 - 105 - await page.goto(`${webUrl}/cabinet/files`); 106 - await page.waitForLoadState("networkidle"); 107 - 108 - await page.getByRole("button", { name: "New" }).click(); 109 - await page.getByRole("button", { name: "New folder" }).click(); 110 - 111 - const folderDialog = page.locator('dialog[aria-label="New folder"]'); 112 - await expect(folderDialog).toBeVisible(); 113 - 114 - const createButton = folderDialog.getByRole("button", { name: "Create" }); 115 - const nameInput = folderDialog.getByLabel("Folder name"); 116 - 117 - await expect(createButton).toBeDisabled(); 118 - 119 - await nameInput.fill("Some Folder"); 120 - await expect(createButton).toBeEnabled(); 121 - 122 - await nameInput.clear(); 123 - await expect(createButton).toBeDisabled(); 124 - 125 - await folderDialog.getByRole("button", { name: "Cancel" }).click(); 126 - await expect(folderDialog).not.toBeVisible(); 127 - }); 128 - });
tests/tests/web/file-browser.test.ts-snapshots/file-browser-empty-chromium-darwin.png

This is a binary file and will not be displayed.

-128
tests/tests/web/identity-setup.test.ts
··· 1 - // Identity setup: seed phrase generation, display, and confirmation. 2 - 3 - import { test, expect } from "../../helpers/web-fixture.js"; 4 - 5 - test.describe("fresh account identity setup", () => { 6 - test.beforeEach(async ({ browserLogin }) => { 7 - await browserLogin(); 8 - }); 9 - 10 - test("shows welcome screen for fresh account", async ({ page }) => { 11 - await expect(page.getByText(/Welcome to Opake/)).toBeVisible(); 12 - await expect(page.getByText(/Create my key/)).toBeVisible(); 13 - await expect(page).toHaveScreenshot("identity-fresh-welcome.png"); 14 - }); 15 - 16 - test("create-key button starts seed phrase generation", async ({ page }) => { 17 - await page.getByText(/Create my key/).click(); 18 - 19 - await expect(page.getByRole("list")).toBeVisible({ timeout: 10_000 }); 20 - }); 21 - 22 - test("seed phrase shows 24 words", async ({ page }) => { 23 - await page.getByText(/Create my key/).click(); 24 - 25 - await expect(page.getByRole("list")).toBeVisible({ timeout: 10_000 }); 26 - 27 - const items = page.getByRole("listitem"); 28 - await expect(items).toHaveCount(24); 29 - }); 30 - 31 - test("continue button disabled until checkbox checked", async ({ page }) => { 32 - await page.getByText(/Create my key/).click(); 33 - await expect(page.getByRole("list")).toBeVisible({ timeout: 10_000 }); 34 - 35 - const continueButton = page.getByRole("button", { name: /Continue/ }); 36 - await expect(continueButton).toBeDisabled(); 37 - 38 - await page.getByLabel(/I have written down/).check(); 39 - await expect(continueButton).toBeEnabled(); 40 - }); 41 - 42 - test("full seed phrase confirmation flow reaches ready state", async ({ 43 - page, 44 - }) => { 45 - await page.getByText(/Create my key/).click(); 46 - await expect(page.getByRole("list")).toBeVisible({ timeout: 10_000 }); 47 - 48 - // Read the 24 words (keyed by displayed number, not DOM order) 49 - const items = page.getByRole("listitem"); 50 - const words: string[] = new Array(24).fill(""); 51 - const count = await items.count(); 52 - for (let i = 0; i < count; i++) { 53 - const text = await items.nth(i).textContent(); 54 - const match = text?.match(/^(\d+)\.\s*(.+)$/); 55 - if (match) { 56 - words[parseInt(match[1]!, 10) - 1] = match[2]!.trim(); 57 - } 58 - } 59 - expect(words.filter(Boolean)).toHaveLength(24); 60 - 61 - await page.getByLabel(/I have written down/).check(); 62 - await page.getByRole("button", { name: /Continue/ }).click(); 63 - 64 - // Fill confirmation words 65 - await expect(page.getByText(/Confirm your seed phrase/)).toBeVisible(); 66 - 67 - const confirmLabels = page.locator("label").filter({ hasText: /^Word #/ }); 68 - const labelCount = await confirmLabels.count(); 69 - expect(labelCount).toBe(3); 70 - 71 - for (let i = 0; i < labelCount; i++) { 72 - const labelText = await confirmLabels.nth(i).textContent(); 73 - const wordNum = parseInt(labelText?.match(/Word #(\d+)/)?.[1] ?? "0", 10); 74 - const word = words[wordNum - 1]; 75 - if (word) { 76 - await confirmLabels.nth(i).locator("input").fill(word); 77 - } 78 - } 79 - 80 - await page.getByRole("button", { name: /Confirm/ }).click(); 81 - 82 - await expect(page.getByText(/You're all set/)).toBeVisible({ 83 - timeout: 15_000, 84 - }); 85 - await expect(page).toHaveScreenshot("identity-ready.png"); 86 - }); 87 - }); 88 - 89 - test.describe("seed phrase confirmation failures", () => { 90 - test.beforeEach(async ({ browserLogin }) => { 91 - await browserLogin(); 92 - }); 93 - 94 - test("wrong confirmation words show error", async ({ page }) => { 95 - await page.getByText(/Create my key/).click(); 96 - await expect(page.getByRole("list")).toBeVisible({ timeout: 10_000 }); 97 - 98 - await page.getByLabel(/I have written down/).check(); 99 - await page.getByRole("button", { name: /Continue/ }).click(); 100 - await expect(page.getByText(/Confirm your seed phrase/)).toBeVisible(); 101 - 102 - const confirmInputs = page 103 - .locator("label") 104 - .filter({ hasText: /^Word #/ }) 105 - .locator("input"); 106 - const inputCount = await confirmInputs.count(); 107 - for (let i = 0; i < inputCount; i++) { 108 - await confirmInputs.nth(i).fill("wrongword"); 109 - } 110 - 111 - await page.getByRole("button", { name: /Confirm/ }).click(); 112 - await expect(page.locator("[role='alert']")).toBeVisible(); 113 - }); 114 - 115 - test("back button returns to seed phrase display", async ({ page }) => { 116 - await page.getByText(/Create my key/).click(); 117 - await expect(page.getByRole("list")).toBeVisible({ timeout: 10_000 }); 118 - 119 - await page.getByLabel(/I have written down/).check(); 120 - await page.getByRole("button", { name: /Continue/ }).click(); 121 - await expect(page.getByText(/Confirm your seed phrase/)).toBeVisible(); 122 - 123 - await page.getByRole("button", { name: /Back/ }).click(); 124 - 125 - await expect(page.getByRole("list")).toBeVisible(); 126 - await expect(page.getByRole("listitem")).toHaveCount(24); 127 - }); 128 - });
tests/tests/web/identity-setup.test.ts-snapshots/identity-fresh-welcome-chromium-darwin.png

This is a binary file and will not be displayed.

tests/tests/web/identity-setup.test.ts-snapshots/identity-ready-chromium-darwin.png

This is a binary file and will not be displayed.

-96
tests/tests/web/login.test.ts
··· 1 - // Login flow: full OAuth browser flow against fake-pds. 2 - 3 - import { test, expect } from "../../helpers/web-fixture.js"; 4 - 5 - test.describe("login page", () => { 6 - test("renders login form", async ({ page, webUrl }) => { 7 - await page.goto(`${webUrl}/devices/login`); 8 - 9 - await expect(page.getByLabel("AT Protocol handle")).toBeVisible(); 10 - await expect(page.getByRole("button", { name: /Sign in/ })).toBeVisible(); 11 - await expect(page).toHaveScreenshot("login-form.png"); 12 - }); 13 - 14 - test("sign-in button is disabled while empty", async ({ page, webUrl }) => { 15 - await page.goto(`${webUrl}/devices/login`); 16 - 17 - const input = page.getByLabel("AT Protocol handle"); 18 - await expect(input).toHaveValue(""); 19 - 20 - const button = page.getByRole("button", { name: /Sign in/ }); 21 - await button.click(); 22 - 23 - // Still on login page (form validation prevented submit) 24 - await expect(input).toBeVisible(); 25 - }); 26 - }); 27 - 28 - test.describe("OAuth login flow", () => { 29 - test("successful login reaches devices page", async ({ 30 - page, 31 - webUrl, 32 - account, 33 - browserLogin, 34 - }) => { 35 - await browserLogin(); 36 - 37 - // Should be on /devices (not /devices/login) 38 - expect(page.url()).toContain("/devices"); 39 - expect(page.url()).not.toContain("/login"); 40 - 41 - await expect(page).toHaveScreenshot("post-login-devices.png"); 42 - }); 43 - 44 - test("invalid handle shows error", async ({ page, webUrl }) => { 45 - await page.goto(`${webUrl}/devices/login`); 46 - 47 - await page.getByLabel("AT Protocol handle").fill("nobody.test"); 48 - await page.getByRole("button", { name: /Sign in/ }).click(); 49 - 50 - await expect(page.locator("[role='alert']")).toBeVisible({ timeout: 10_000 }); 51 - await expect(page).toHaveScreenshot("login-error-invalid-handle.png"); 52 - }); 53 - }); 54 - 55 - test.describe("OAuth callback", () => { 56 - test("direct callback access without params shows error", async ({ 57 - page, 58 - webUrl, 59 - }) => { 60 - await page.goto(`${webUrl}/devices/oauth-callback`); 61 - 62 - await expect( 63 - page.getByText(/Login failed|Sign in/), 64 - ).toBeVisible({ timeout: 10_000 }); 65 - 66 - await expect(page).toHaveScreenshot("callback-no-params.png"); 67 - }); 68 - }); 69 - 70 - test.describe("auth guards", () => { 71 - test("unauthenticated access to cabinet redirects to login", async ({ 72 - page, 73 - webUrl, 74 - }) => { 75 - await page.goto(`${webUrl}/cabinet/files`); 76 - 77 - await expect(page.getByLabel("AT Protocol handle")).toBeVisible({ 78 - timeout: 10_000, 79 - }); 80 - expect(page.url()).toContain("/login"); 81 - }); 82 - 83 - test("logged-in user on login page redirects to devices", async ({ 84 - page, 85 - webUrl, 86 - browserLogin, 87 - }) => { 88 - await browserLogin(); 89 - 90 - await page.goto(`${webUrl}/devices/login`); 91 - 92 - await expect( 93 - page.getByText(/Welcome to Opake|You're all set|Setting things up/), 94 - ).toBeVisible({ timeout: 10_000 }); 95 - }); 96 - });
tests/tests/web/login.test.ts-snapshots/callback-no-params-chromium-darwin.png

This is a binary file and will not be displayed.

tests/tests/web/login.test.ts-snapshots/login-error-invalid-handle-chromium-darwin.png

This is a binary file and will not be displayed.

tests/tests/web/login.test.ts-snapshots/login-form-chromium-darwin.png

This is a binary file and will not be displayed.

tests/tests/web/login.test.ts-snapshots/post-login-devices-chromium-darwin.png

This is a binary file and will not be displayed.

-68
tests/tests/web/logout.test.ts
··· 1 - // Logout: sign out from devices page and from cabinet user menu. 2 - 3 - import { test, expect } from "../../helpers/web-fixture.js"; 4 - import { completeSeedPhraseSetup } from "../../helpers/seed-phrase.js"; 5 - 6 - test.describe("logout", () => { 7 - test("log out from devices page clears session", async ({ 8 - page, 9 - pdsUrl, 10 - account, 11 - browserLogin, 12 - }) => { 13 - await browserLogin(); 14 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 15 - 16 - await page.getByRole("button", { name: "Log out" }).click(); 17 - 18 - await expect(page.getByLabel("AT Protocol handle")).toBeVisible({ 19 - timeout: 10_000, 20 - }); 21 - }); 22 - 23 - test("sign out from cabinet user menu clears session", async ({ 24 - page, 25 - webUrl, 26 - pdsUrl, 27 - account, 28 - browserLogin, 29 - }) => { 30 - await browserLogin(); 31 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 32 - 33 - await page.goto(`${webUrl}/cabinet/settings`); 34 - 35 - await expect(page.getByRole("definition").first()).toBeVisible({ 36 - timeout: 10_000, 37 - }); 38 - 39 - await page.locator("button[aria-haspopup='true']").last().click(); 40 - await page.getByRole("button", { name: "Sign out" }).click(); 41 - 42 - await expect(page.getByLabel("AT Protocol handle")).toBeVisible({ 43 - timeout: 10_000, 44 - }); 45 - }); 46 - 47 - test("after logout, cabinet access redirects to login", async ({ 48 - page, 49 - webUrl, 50 - pdsUrl, 51 - account, 52 - browserLogin, 53 - }) => { 54 - await browserLogin(); 55 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 56 - 57 - await page.getByRole("button", { name: "Log out" }).click(); 58 - await expect(page.getByLabel("AT Protocol handle")).toBeVisible({ 59 - timeout: 10_000, 60 - }); 61 - 62 - await page.goto(`${webUrl}/cabinet/files`); 63 - await expect(page.getByLabel("AT Protocol handle")).toBeVisible({ 64 - timeout: 10_000, 65 - }); 66 - expect(page.url()).toContain("/login"); 67 - }); 68 - });
-211
tests/tests/web/markdown-editor.test.ts
··· 1 - // Markdown editor: create, edit, save, preview toggle, toolbar actions. 2 - // 3 - // Tests are grouped to minimize login + seed phrase setup overhead. 4 - // Each describe block does one setup and covers multiple assertions. 5 - 6 - import { test, expect } from "../../helpers/web-fixture.js"; 7 - import { completeSeedPhraseSetup } from "../../helpers/seed-phrase.js"; 8 - 9 - /** Log in, set up identity, navigate to cabinet, wait for empty state. */ 10 - async function setupCabinet( 11 - page: import("@playwright/test").Page, 12 - opts: { webUrl: string; pdsUrl: string; handle: string; did: string }, 13 - browserLogin: () => Promise<void>, 14 - ) { 15 - await browserLogin(); 16 - await completeSeedPhraseSetup(page, { 17 - pdsUrl: opts.pdsUrl, 18 - handle: opts.handle, 19 - did: opts.did, 20 - }); 21 - await page.goto(`${opts.webUrl}/cabinet/files`); 22 - await expect(page.getByText("Nothing here yet")).toBeVisible({ timeout: 15_000 }); 23 - } 24 - 25 - /** Navigate to the "New note" editor from the cabinet file list. */ 26 - async function openNewDocumentEditor(page: import("@playwright/test").Page) { 27 - await page.getByRole("button", { name: "New" }).click(); 28 - await page.getByRole("button", { name: "New note" }).click(); 29 - await expect(page).toHaveURL(/\/cabinet\/editor\/new/); 30 - const editor = page.locator('[aria-label="Document editor"]'); 31 - await expect(editor).toBeVisible({ timeout: 10_000 }); 32 - return editor; 33 - } 34 - 35 - /** Upload a markdown file and wait for it to appear in the file list. */ 36 - async function uploadMarkdownFile( 37 - page: import("@playwright/test").Page, 38 - name: string, 39 - content: string, 40 - ) { 41 - const [fileChooser] = await Promise.all([ 42 - page.waitForEvent("filechooser"), 43 - page.getByTestId("file-upload").click({ force: true }), 44 - ]); 45 - await fileChooser.setFiles({ 46 - name, 47 - mimeType: "text/markdown", 48 - buffer: Buffer.from(content), 49 - }); 50 - 51 - await expect(page.getByText("File uploaded").first()).toBeVisible({ timeout: 30_000 }); 52 - const fileRow = page.locator(`[aria-label*="${name.replace(".md", "")}"]`).first(); 53 - await expect(fileRow).toBeVisible({ timeout: 30_000 }); 54 - return fileRow; 55 - } 56 - 57 - test.describe("new document flow", () => { 58 - test("create, save, verify promotion, save button state, and close", async ({ 59 - page, 60 - webUrl, 61 - pdsUrl, 62 - account, 63 - browserLogin, 64 - }, testInfo) => { 65 - testInfo.setTimeout(90_000); 66 - await setupCabinet(page, { webUrl, pdsUrl, ...account }, browserLogin); 67 - const editor = await openNewDocumentEditor(page); 68 - 69 - // Name input defaults to "Untitled.md" 70 - const nameInput = page.getByPlaceholder("document-name.md").first(); 71 - await expect(nameInput).toHaveValue("Untitled.md"); 72 - await nameInput.clear(); 73 - await nameInput.fill("test-note.md"); 74 - 75 - // Save button disabled when empty 76 - const saveButton = page.getByRole("button", { name: "Save" }); 77 - await expect(saveButton).toBeDisabled(); 78 - 79 - // Type content — save enables 80 - await editor.click(); 81 - await page.keyboard.type("# Hello World\n\nThis is a test document."); 82 - await expect(saveButton).toBeEnabled(); 83 - 84 - // Save with Cmd+S 85 - await page.keyboard.press("Meta+s"); 86 - await expect(page.getByText("Document created").first()).toBeVisible({ timeout: 15_000 }); 87 - 88 - // URL promoted silently from /new to /$rkey 89 - await expect(page).toHaveURL(/\/cabinet\/editor\/[a-z0-9]+/); 90 - await expect(page).not.toHaveURL(/\/new/); 91 - 92 - // Close returns to file browser 93 - await page.getByRole("button", { name: "Close editor" }).click(); 94 - await expect(page).toHaveURL(/\/cabinet\/files/, { timeout: 10_000 }); 95 - }); 96 - }); 97 - 98 - test.describe("edit existing document", () => { 99 - test("double-click to open, edit, save, reopen to verify persistence", async ({ 100 - page, 101 - webUrl, 102 - pdsUrl, 103 - account, 104 - browserLogin, 105 - }, testInfo) => { 106 - testInfo.setTimeout(90_000); 107 - await setupCabinet(page, { webUrl, pdsUrl, ...account }, browserLogin); 108 - 109 - // Upload a markdown file 110 - const fileRow = await uploadMarkdownFile(page, "edit-test.md", "# Before Edit"); 111 - 112 - // Double-click to open in editor 113 - await fileRow.dblclick(); 114 - await expect(page).toHaveURL(/\/cabinet\/editor\/[a-z0-9]+/, { timeout: 10_000 }); 115 - 116 - const editor = page.locator('[aria-label="Document editor"]'); 117 - await expect(editor).toBeVisible({ timeout: 15_000 }); 118 - await expect(editor).toContainText("Before Edit"); 119 - 120 - // Append text and save 121 - await editor.click(); 122 - await page.keyboard.press("End"); 123 - await page.keyboard.type("\n\nAfter Edit"); 124 - await page.keyboard.press("Meta+s"); 125 - await expect(page.getByText("Document saved").first()).toBeVisible({ timeout: 15_000 }); 126 - 127 - // Navigate back 128 - await page.getByRole("button", { name: "Close editor" }).click(); 129 - await expect(page).toHaveURL(/\/cabinet\/files/, { timeout: 10_000 }); 130 - 131 - // Reopen and verify the edit persisted 132 - const fileRowAgain = page.locator('[aria-label*="edit-test"]').first(); 133 - await expect(fileRowAgain).toBeVisible({ timeout: 10_000 }); 134 - await fileRowAgain.dblclick(); 135 - 136 - const editorAgain = page.locator('[aria-label="Document editor"]'); 137 - await expect(editorAgain).toBeVisible({ timeout: 15_000 }); 138 - await expect(editorAgain).toContainText("After Edit"); 139 - }); 140 - }); 141 - 142 - test.describe("preview toggle and toolbar", () => { 143 - test("escape to preview, click to edit, eye toggle, toolbar visibility", async ({ 144 - page, 145 - webUrl, 146 - pdsUrl, 147 - account, 148 - browserLogin, 149 - }, testInfo) => { 150 - testInfo.setTimeout(90_000); 151 - await setupCabinet(page, { webUrl, pdsUrl, ...account }, browserLogin); 152 - const editor = await openNewDocumentEditor(page); 153 - 154 - // Type content with formatting 155 - await editor.click(); 156 - await page.keyboard.type("# Preview Test\n\nSome **bold** text."); 157 - 158 - // Toolbar should be visible in edit mode 159 - const toolbar = page.locator('[role="toolbar"][aria-label="Formatting"]'); 160 - await expect(toolbar).toBeVisible(); 161 - 162 - // Escape switches to preview 163 - await page.keyboard.press("Escape"); 164 - const previewArea = page.locator('[aria-label="Click to edit"]'); 165 - await expect(previewArea).toBeVisible({ timeout: 5_000 }); 166 - await expect(previewArea.locator("strong")).toContainText("bold"); 167 - 168 - // Toolbar hidden in preview mode 169 - await expect(toolbar).not.toBeVisible(); 170 - 171 - // Click preview to return to editor 172 - await previewArea.click(); 173 - await expect(editor).toBeVisible({ timeout: 5_000 }); 174 - await expect(toolbar).toBeVisible(); 175 - 176 - // Eye button toggles to preview 177 - await page.getByRole("button", { name: "Switch to preview" }).click(); 178 - await expect(previewArea).toBeVisible(); 179 - 180 - // Pencil button toggles back 181 - await page.getByRole("button", { name: "Switch to editor" }).click(); 182 - await expect(editor).toBeVisible({ timeout: 5_000 }); 183 - }); 184 - }); 185 - 186 - test.describe("edit button in preview pane", () => { 187 - test("pencil icon appears for markdown files and navigates to editor", async ({ 188 - page, 189 - webUrl, 190 - pdsUrl, 191 - account, 192 - browserLogin, 193 - }, testInfo) => { 194 - testInfo.setTimeout(90_000); 195 - await setupCabinet(page, { webUrl, pdsUrl, ...account }, browserLogin); 196 - 197 - await uploadMarkdownFile(page, "preview-edit.md", "# Click Edit"); 198 - 199 - // Single-click to open preview pane 200 - const fileRow = page.locator('[aria-label*="preview-edit"]').first(); 201 - await fileRow.click(); 202 - 203 - // Edit button should be visible in the preview header 204 - const editButton = page.getByRole("button", { name: "Edit document" }); 205 - await expect(editButton).toBeVisible({ timeout: 10_000 }); 206 - 207 - // Click it — should navigate to editor 208 - await editButton.click(); 209 - await expect(page).toHaveURL(/\/cabinet\/editor\/[a-z0-9]+/, { timeout: 10_000 }); 210 - }); 211 - });
-149
tests/tests/web/metadata.test.ts
··· 1 - // Edit Metadata dialog: rename, tags, description, validation, cancel. 2 - 3 - import { test, expect } from "../../helpers/web-fixture.js"; 4 - import { completeSeedPhraseSetup } from "../../helpers/seed-phrase.js"; 5 - 6 - /** Upload a test file and wait for its row to appear. */ 7 - async function setupFileForMetadata( 8 - page: import("@playwright/test").Page, 9 - opts: { webUrl: string; pdsUrl: string; handle: string; did: string }, 10 - browserLogin: () => Promise<void>, 11 - ) { 12 - await browserLogin(); 13 - await completeSeedPhraseSetup(page, { pdsUrl: opts.pdsUrl, handle: opts.handle, did: opts.did }); 14 - 15 - await page.goto(`${opts.webUrl}/cabinet/files`); 16 - await page.waitForTimeout(2_000); 17 - 18 - const [fileChooser] = await Promise.all([ 19 - page.waitForEvent("filechooser"), 20 - page.getByTestId("file-upload").click({ force: true }), 21 - ]); 22 - await fileChooser.setFiles({ 23 - name: "meta-test.txt", 24 - mimeType: "text/plain", 25 - buffer: Buffer.from("metadata test"), 26 - }); 27 - 28 - const fileRow = page.locator('[aria-label*="meta-test"]').first(); 29 - await expect(fileRow).toBeVisible({ timeout: 30_000 }); 30 - return fileRow; 31 - } 32 - 33 - /** Open the edit details dialog for a file row. */ 34 - async function openEditDialog(page: import("@playwright/test").Page, fileRow: import("@playwright/test").Locator) { 35 - await fileRow.locator("button[aria-haspopup]").click(); 36 - await page.getByRole("button", { name: "Edit details" }).click(); 37 - const dialog = page.locator('dialog[aria-label="Edit file metadata"]'); 38 - await expect(dialog).toBeVisible(); 39 - return dialog; 40 - } 41 - 42 - test.describe("edit metadata dialog", () => { 43 - test("opens with current name pre-filled", async ({ 44 - page, 45 - webUrl, 46 - pdsUrl, 47 - account, 48 - browserLogin, 49 - }) => { 50 - const fileRow = await setupFileForMetadata(page, { webUrl, pdsUrl, ...account }, browserLogin); 51 - const dialog = await openEditDialog(page, fileRow); 52 - 53 - const nameInput = dialog.locator('input[placeholder="File name"]'); 54 - await expect(nameInput).toHaveValue("meta-test.txt"); 55 - }); 56 - 57 - // FIXME: Save completes but the file list doesn't update — the metadata 58 - // re-encryption round-trip may be failing silently. Needs investigation. 59 - test.fixme("rename file via dialog", async ({ 60 - page, 61 - webUrl, 62 - pdsUrl, 63 - account, 64 - browserLogin, 65 - }, testInfo) => { 66 - testInfo.setTimeout(90_000); 67 - const fileRow = await setupFileForMetadata(page, { webUrl, pdsUrl, ...account }, browserLogin); 68 - const dialog = await openEditDialog(page, fileRow); 69 - 70 - const nameInput = dialog.locator('input[placeholder="File name"]'); 71 - await nameInput.clear(); 72 - await nameInput.fill("renamed.txt"); 73 - await dialog.getByRole("button", { name: "Save" }).click(); 74 - 75 - // Re-encryption + PDS round-trip can be slow under parallel load 76 - const renamedRow = page.locator('[aria-label*="renamed"]').first(); 77 - await expect(renamedRow).toBeVisible({ timeout: 30_000 }); 78 - }); 79 - 80 - test("add and remove tags", async ({ 81 - page, 82 - webUrl, 83 - pdsUrl, 84 - account, 85 - browserLogin, 86 - }, testInfo) => { 87 - testInfo.setTimeout(90_000); 88 - const fileRow = await setupFileForMetadata(page, { webUrl, pdsUrl, ...account }, browserLogin); 89 - const dialog = await openEditDialog(page, fileRow); 90 - 91 - const tagInput = dialog.locator('input[placeholder="Add tag…"]'); 92 - 93 - await tagInput.fill("finance"); 94 - await tagInput.press("Enter"); 95 - await expect(dialog.getByText("finance")).toBeVisible(); 96 - 97 - await tagInput.fill("2026"); 98 - await tagInput.press("Enter"); 99 - await expect(dialog.getByText("2026")).toBeVisible(); 100 - 101 - // Remove the first tag 102 - await dialog.getByLabel("Remove tag finance").click(); 103 - await expect(dialog.getByText("finance")).not.toBeVisible(); 104 - await expect(dialog.getByText("2026")).toBeVisible(); 105 - 106 - await dialog.getByRole("button", { name: "Save" }).click(); 107 - }); 108 - 109 - test("save disabled when name is empty", async ({ 110 - page, 111 - webUrl, 112 - pdsUrl, 113 - account, 114 - browserLogin, 115 - }) => { 116 - const fileRow = await setupFileForMetadata(page, { webUrl, pdsUrl, ...account }, browserLogin); 117 - const dialog = await openEditDialog(page, fileRow); 118 - 119 - const nameInput = dialog.locator('input[placeholder="File name"]'); 120 - const saveButton = dialog.getByRole("button", { name: "Save" }); 121 - 122 - await nameInput.clear(); 123 - await expect(saveButton).toBeDisabled(); 124 - 125 - await nameInput.fill("something.txt"); 126 - await expect(saveButton).toBeEnabled(); 127 - }); 128 - 129 - test("cancel closes without saving", async ({ 130 - page, 131 - webUrl, 132 - pdsUrl, 133 - account, 134 - browserLogin, 135 - }) => { 136 - const fileRow = await setupFileForMetadata(page, { webUrl, pdsUrl, ...account }, browserLogin); 137 - const dialog = await openEditDialog(page, fileRow); 138 - 139 - const nameInput = dialog.locator('input[placeholder="File name"]'); 140 - await nameInput.clear(); 141 - await nameInput.fill("should-not-save.txt"); 142 - 143 - await dialog.getByRole("button", { name: "Cancel" }).click(); 144 - await expect(dialog).not.toBeVisible(); 145 - 146 - // Original name still in the list 147 - await expect(page.locator('[aria-label*="meta-test"]').first()).toBeVisible(); 148 - }); 149 - });
-59
tests/tests/web/mobile.test.ts
··· 1 - // Mobile viewport: layout and navigation on small screens. 2 - 3 - import { test, expect } from "../../helpers/web-fixture.js"; 4 - 5 - const MOBILE_VIEWPORT = { width: 375, height: 812 }; 6 - 7 - test("login page renders on mobile", async ({ page, webUrl }) => { 8 - await page.setViewportSize(MOBILE_VIEWPORT); 9 - await page.goto(`${webUrl}/devices/login`); 10 - 11 - await expect(page.getByLabel("AT Protocol handle")).toBeVisible(); 12 - await expect(page.getByRole("button", { name: /Sign in/ })).toBeVisible(); 13 - await expect(page).toHaveScreenshot("mobile-login.png"); 14 - }); 15 - 16 - test("cabinet shows hamburger menu on mobile", async ({ 17 - page, 18 - webUrl, 19 - browserLogin, 20 - }) => { 21 - await browserLogin(); 22 - await page.setViewportSize(MOBILE_VIEWPORT); 23 - await page.goto(`${webUrl}/cabinet/files`); 24 - 25 - // Hamburger button should exist on mobile 26 - const menuButton = page.getByLabel(/Open menu|Close menu/i); 27 - await expect(menuButton).toBeVisible({ timeout: 10_000 }); 28 - 29 - // Click hamburger — sidebar links should become visible 30 - await menuButton.click(); 31 - await expect(page.getByRole("link", { name: "Settings" }).first()).toBeVisible(); 32 - await expect(page).toHaveScreenshot("mobile-menu-open.png"); 33 - }); 34 - 35 - test("settings page renders on mobile", async ({ 36 - page, 37 - webUrl, 38 - browserLogin, 39 - }) => { 40 - await browserLogin(); 41 - await page.setViewportSize(MOBILE_VIEWPORT); 42 - await page.goto(`${webUrl}/cabinet/settings`); 43 - 44 - const definitions = page.getByRole("definition"); 45 - await expect(definitions.first()).toBeVisible({ timeout: 10_000 }); 46 - }); 47 - 48 - test("devices page renders on mobile", async ({ 49 - page, 50 - browserLogin, 51 - }) => { 52 - await browserLogin(); 53 - await page.setViewportSize(MOBILE_VIEWPORT); 54 - 55 - await expect( 56 - page.getByText(/Welcome to Opake|You're all set|Setting things up/), 57 - ).toBeVisible({ timeout: 10_000 }); 58 - await expect(page).toHaveScreenshot("mobile-devices.png"); 59 - });
tests/tests/web/mobile.test.ts-snapshots/mobile-devices-chromium-darwin.png

This is a binary file and will not be displayed.

tests/tests/web/mobile.test.ts-snapshots/mobile-login-chromium-darwin.png

This is a binary file and will not be displayed.

tests/tests/web/mobile.test.ts-snapshots/mobile-menu-open-chromium-darwin.png

This is a binary file and will not be displayed.

-94
tests/tests/web/navigation.test.ts
··· 1 - // Sidebar navigation: link routing, breadcrumbs, and active state. 2 - 3 - import { test, expect } from "../../helpers/web-fixture.js"; 4 - import { completeSeedPhraseSetup } from "../../helpers/seed-phrase.js"; 5 - 6 - test.describe("sidebar links navigate correctly", () => { 7 - test("sidebar links route to correct pages", async ({ 8 - page, 9 - webUrl, 10 - pdsUrl, 11 - account, 12 - browserLogin, 13 - }) => { 14 - await browserLogin(); 15 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 16 - await page.goto(`${webUrl}/cabinet/files`); 17 - await page.waitForLoadState("networkidle"); 18 - 19 - await page.getByRole("link", { name: "Sharing" }).click(); 20 - await expect(page).toHaveURL(/\/cabinet\/shared/); 21 - 22 - await page.getByRole("link", { name: "Settings" }).click(); 23 - await expect(page).toHaveURL(/\/cabinet\/settings/); 24 - 25 - await page.getByRole("link", { name: "Your Cabinet" }).click(); 26 - await expect(page).toHaveURL(/\/cabinet\/files/); 27 - }); 28 - }); 29 - 30 - test.describe("breadcrumb navigation", () => { 31 - test("breadcrumb navigates back to root from subfolder", async ({ 32 - page, 33 - webUrl, 34 - pdsUrl, 35 - account, 36 - browserLogin, 37 - }) => { 38 - await browserLogin(); 39 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 40 - 41 - await page.goto(`${webUrl}/cabinet/files`); 42 - await page.waitForLoadState("networkidle"); 43 - 44 - // Create folder 45 - await page.getByRole("button", { name: "New" }).click(); 46 - await page.getByRole("button", { name: "New folder" }).click(); 47 - 48 - const folderDialog = page.locator('dialog[aria-label="New folder"]'); 49 - await expect(folderDialog).toBeVisible(); 50 - await folderDialog.getByLabel("Folder name").fill("Nav Test"); 51 - await folderDialog.getByRole("button", { name: "Create" }).click(); 52 - 53 - // Navigate into folder 54 - const folderRow = page.locator('[aria-label="Nav Test, folder"]'); 55 - await expect(folderRow).toBeVisible({ timeout: 10_000 }); 56 - await folderRow.click(); 57 - 58 - await expect( 59 - page.locator(".breadcrumbs").getByText("Nav Test").first(), 60 - ).toBeVisible(); 61 - 62 - // Click "Your Cabinet" breadcrumb to go back to root 63 - await page.locator(".breadcrumbs").getByRole("link", { name: "Your Cabinet" }).click(); 64 - await expect(page).toHaveURL(/\/cabinet\/files$/); 65 - 66 - // Folder should be visible at root level again 67 - await expect(folderRow).toBeVisible({ timeout: 10_000 }); 68 - }); 69 - }); 70 - 71 - test.describe("sidebar highlights active route", () => { 72 - test("active link has accent styling", async ({ 73 - page, 74 - webUrl, 75 - pdsUrl, 76 - account, 77 - browserLogin, 78 - }) => { 79 - await browserLogin(); 80 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 81 - await page.goto(`${webUrl}/cabinet/settings`); 82 - 83 - const settingsLink = page.getByRole("link", { name: "Settings" }); 84 - await expect(settingsLink).toBeVisible({ timeout: 10_000 }); 85 - await expect(settingsLink).toHaveClass(/bg-accent/); 86 - 87 - // Other links should not have active styling 88 - const cabinetLink = page.getByRole("link", { name: "Your Cabinet" }); 89 - await expect(cabinetLink).not.toHaveClass(/bg-accent/); 90 - 91 - const sharingLink = page.getByRole("link", { name: "Sharing" }); 92 - await expect(sharingLink).not.toHaveClass(/bg-accent/); 93 - }); 94 - });
-98
tests/tests/web/pairing.test.ts
··· 1 - // Device pairing: UI states only (actual pairing requires two browser contexts 2 - // coordinating via PDS relay, which is out of scope for these tests). 3 - 4 - import { test, expect } from "../../helpers/web-fixture.js"; 5 - import { completeSeedPhraseSetup } from "../../helpers/seed-phrase.js"; 6 - 7 - test.describe("pair request page", () => { 8 - test("shows fingerprint and waiting state", async ({ 9 - page, 10 - webUrl, 11 - pdsUrl, 12 - account, 13 - browserLogin, 14 - }) => { 15 - await browserLogin(); 16 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 17 - 18 - await page.goto(`${webUrl}/devices/pair/request`); 19 - 20 - await expect(page.getByText("Pair this device")).toBeVisible({ 21 - timeout: 10_000, 22 - }); 23 - 24 - const fingerprint = page.locator(".font-mono.text-primary"); 25 - await expect(fingerprint).toBeVisible(); 26 - 27 - await expect(page.getByText("Waiting for approval")).toBeVisible(); 28 - }); 29 - }); 30 - 31 - test.describe("pair accept page", () => { 32 - test("loads and shows heading", async ({ 33 - page, 34 - webUrl, 35 - pdsUrl, 36 - account, 37 - browserLogin, 38 - }) => { 39 - await browserLogin(); 40 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 41 - 42 - await page.goto(`${webUrl}/devices/pair/accept`); 43 - 44 - // Page shows either "No pending requests" or "Approve a device" 45 - // depending on whether other tests created pair requests 46 - await expect( 47 - page.getByText("No pending requests").or( 48 - page.getByText("Approve a device"), 49 - ), 50 - ).toBeVisible({ timeout: 10_000 }); 51 - 52 - await expect(page).toHaveScreenshot("pairing-accept.png"); 53 - }); 54 - }); 55 - 56 - test.describe("pairing navigation", () => { 57 - test("pair accept accessible from ready view", async ({ 58 - page, 59 - pdsUrl, 60 - account, 61 - browserLogin, 62 - }) => { 63 - await browserLogin(); 64 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 65 - 66 - await expect(page.getByText("You're all set")).toBeVisible(); 67 - 68 - await page.getByText("Set up another device").click(); 69 - await expect(page).toHaveURL(/\/devices\/pair\/accept/); 70 - }); 71 - 72 - test("pair request accessible from recovery view", async ({ 73 - page, 74 - webUrl, 75 - pdsUrl, 76 - account, 77 - browserLogin, 78 - }) => { 79 - // First login + seed phrase (publishes publicKey to PDS) 80 - await browserLogin(); 81 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 82 - 83 - // Logout 84 - await page.getByRole("button", { name: "Log out" }).click(); 85 - await expect(page.getByLabel("AT Protocol handle")).toBeVisible({ 86 - timeout: 10_000, 87 - }); 88 - 89 - // Login again — PDS has publicKey but browser has no local keys → remote_only 90 - await browserLogin(); 91 - await expect(page.getByText("Welcome back")).toBeVisible({ 92 - timeout: 15_000, 93 - }); 94 - 95 - await page.getByText("Copy from another device").click(); 96 - await expect(page).toHaveURL(/\/devices\/pair\/request/); 97 - }); 98 - });
tests/tests/web/pairing.test.ts-snapshots/pairing-accept-chromium-darwin.png

This is a binary file and will not be displayed.

-73
tests/tests/web/pending-share.test.ts
··· 1 - // Pending share: web share dialog queues when recipient hasn't set up Opake. 2 - 3 - import { test, expect, type Page } from "../../helpers/web-fixture.js"; 4 - import { completeSeedPhraseSetup } from "../../helpers/seed-phrase.js"; 5 - 6 - /** Upload a file via the hidden file input + filechooser. */ 7 - async function uploadTestFile( 8 - page: Page, 9 - name: string, 10 - content: string, 11 - ): Promise<void> { 12 - const [fileChooser] = await Promise.all([ 13 - page.waitForEvent("filechooser"), 14 - page.getByTestId("file-upload").click({ force: true }), 15 - ]); 16 - await fileChooser.setFiles({ 17 - name, 18 - mimeType: "text/plain", 19 - buffer: Buffer.from(content), 20 - }); 21 - } 22 - 23 - /** Open the share dialog for a file row. */ 24 - async function openShareDialog(page: Page, fileNamePattern: string) { 25 - const fileRow = page.locator(`[aria-label*="${fileNamePattern}"]`); 26 - await expect(fileRow).toBeVisible({ timeout: 15_000 }); 27 - await fileRow.locator("button[aria-haspopup]").click(); 28 - await page.getByRole("button", { name: /Share\u2026/ }).click(); 29 - const dialog = page.locator('dialog[aria-label="Share file"]'); 30 - await expect(dialog).toBeVisible(); 31 - return dialog; 32 - } 33 - 34 - test.describe("pending share", () => { 35 - test("shows queued message when recipient has no public key", async ({ 36 - page, 37 - webUrl, 38 - pdsUrl, 39 - account, 40 - browserLogin, 41 - }) => { 42 - await browserLogin(); 43 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 44 - 45 - // Create a fresh account on the fake-pds with no public key record 46 - const createRes = await fetch(`${pdsUrl}/_test/create-account`, { 47 - method: "POST", 48 - headers: { "Content-Type": "application/json" }, 49 - body: JSON.stringify({ handle: "pending-target.test" }), 50 - }); 51 - expect(createRes.ok).toBe(true); 52 - 53 - await page.goto(`${webUrl}/cabinet/files`); 54 - await page.waitForTimeout(2_000); 55 - 56 - await uploadTestFile(page, "pending-web.txt", "pending share content"); 57 - const shareDialog = await openShareDialog(page, "pending-web"); 58 - 59 - await shareDialog 60 - .locator("#share-recipient") 61 - .fill("pending-target.test"); 62 - await shareDialog.getByRole("button", { name: "Share" }).click(); 63 - 64 - // Should show a success toast with "queued" message, not an error 65 - // Use first() to handle strict mode if the toast text appears in multiple elements 66 - await expect( 67 - page.getByText(/queued/).first(), 68 - ).toBeVisible({ timeout: 15_000 }); 69 - 70 - // Dialog should close (success path, not error) 71 - await expect(shareDialog).not.toBeVisible({ timeout: 5_000 }); 72 - }); 73 - });
-117
tests/tests/web/recovery.test.ts
··· 1 - // Recovery: seed phrase recovery when identity is remote_only. 2 - 3 - import { test, expect } from "../../helpers/web-fixture.js"; 4 - import { completeSeedPhraseSetup } from "../../helpers/seed-phrase.js"; 5 - 6 - /** Login → seed phrase → logout → login again (triggers remote_only state). */ 7 - async function setupRemoteOnly( 8 - page: import("@playwright/test").Page, 9 - webUrl: string, 10 - pdsUrl: string, 11 - account: { handle: string; did: string }, 12 - ): Promise<void> { 13 - // First login + complete seed phrase (publishes publicKey to PDS) 14 - await page.goto(`${webUrl}/devices/login`); 15 - await page.getByLabel("AT Protocol handle").fill(account.handle); 16 - await page.getByRole("button", { name: /Sign in/ }).click(); 17 - await expect( 18 - page.getByText(/Setting things up|Welcome to Opake|You're all set/), 19 - ).toBeVisible({ timeout: 15_000 }); 20 - 21 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 22 - 23 - // Logout 24 - await page.getByRole("button", { name: "Log out" }).click(); 25 - await expect(page.getByLabel("AT Protocol handle")).toBeVisible({ 26 - timeout: 10_000, 27 - }); 28 - 29 - // Login again — PDS has publicKey but browser has no local keys → remote_only 30 - await page.getByLabel("AT Protocol handle").fill(account.handle); 31 - await page.getByRole("button", { name: /Sign in/ }).click(); 32 - await expect(page.getByText("Welcome back")).toBeVisible({ 33 - timeout: 15_000, 34 - }); 35 - } 36 - 37 - test.describe("seed phrase recovery", () => { 38 - test("remote-only account shows recovery view with two options", async ({ 39 - page, 40 - webUrl, 41 - pdsUrl, 42 - account, 43 - }) => { 44 - await setupRemoteOnly(page, webUrl, pdsUrl, account); 45 - 46 - await expect(page.getByText("Copy from another device")).toBeVisible(); 47 - await expect(page.getByText("Use your recovery phrase")).toBeVisible(); 48 - 49 - await expect(page).toHaveScreenshot("recovery-welcome-back.png"); 50 - }); 51 - 52 - test("recovery phrase input shows 24 word fields", async ({ 53 - page, 54 - webUrl, 55 - pdsUrl, 56 - account, 57 - }) => { 58 - await setupRemoteOnly(page, webUrl, pdsUrl, account); 59 - 60 - await page.getByText("Use your recovery phrase").click(); 61 - 62 - await expect(page.getByText("Enter your seed phrase")).toBeVisible(); 63 - await expect( 64 - page.locator('[role="group"][aria-label="Seed phrase input"]'), 65 - ).toBeVisible(); 66 - 67 - await expect(page.getByLabel("Word 1", { exact: true })).toBeVisible(); 68 - await expect(page.getByLabel("Word 24", { exact: true })).toBeVisible(); 69 - 70 - await expect( 71 - page.getByRole("button", { name: "Recover" }), 72 - ).toBeDisabled(); 73 - await expect(page.getByText("0 of 24 words entered")).toBeVisible(); 74 - }); 75 - 76 - test("cancel returns to recovery choice view", async ({ 77 - page, 78 - webUrl, 79 - pdsUrl, 80 - account, 81 - }) => { 82 - await setupRemoteOnly(page, webUrl, pdsUrl, account); 83 - 84 - await page.getByText("Use your recovery phrase").click(); 85 - await expect(page.getByText("Enter your seed phrase")).toBeVisible(); 86 - 87 - await page.getByRole("button", { name: "Cancel" }).click(); 88 - 89 - await expect(page.getByText("Welcome back")).toBeVisible(); 90 - }); 91 - 92 - test("wrong seed phrase shows mismatch warning", async ({ 93 - page, 94 - webUrl, 95 - pdsUrl, 96 - account, 97 - }) => { 98 - await setupRemoteOnly(page, webUrl, pdsUrl, account); 99 - 100 - await page.getByText("Use your recovery phrase").click(); 101 - await expect(page.getByText("Enter your seed phrase")).toBeVisible(); 102 - 103 - // Fill all 24 fields with a valid but WRONG phrase (BIP-39 test vector) 104 - const wrongPhrase = [...Array(23).fill("abandon"), "art"] as readonly string[]; 105 - for (let i = 0; i < 24; i++) { 106 - await page.getByLabel(`Word ${i + 1}`, { exact: true }).fill(wrongPhrase[i]!); 107 - } 108 - 109 - await expect(page.getByRole("button", { name: "Recover" })).toBeEnabled(); 110 - await page.getByRole("button", { name: "Recover" }).click(); 111 - 112 - // Should show mismatch warning or error 113 - await expect( 114 - page.getByText("Key mismatch").or(page.locator("[role='alert']")), 115 - ).toBeVisible({ timeout: 10_000 }); 116 - }); 117 - });
tests/tests/web/recovery.test.ts-snapshots/recovery-welcome-back-chromium-darwin.png

This is a binary file and will not be displayed.

-241
tests/tests/web/search.test.ts
··· 1 - // Search: typing navigates to search page, results match, clicking navigates to file. 2 - // 3 - // Skipped: the search input was hidden in commit 814f540 because the current 4 - // filter only covers the loaded directory, not the full cabinet or inbox. 5 - // Un-skip once SDK-backed full-tree/inbox search lands — no assertions need 6 - // to change, only the describe modifiers. 7 - 8 - import { test, expect } from "../../helpers/web-fixture.js"; 9 - import { completeSeedPhraseSetup } from "../../helpers/seed-phrase.js"; 10 - 11 - test.describe.skip("search navigation", () => { 12 - test("typing in search bar navigates to /cabinet/search", async ({ 13 - page, 14 - webUrl, 15 - pdsUrl, 16 - account, 17 - browserLogin, 18 - }) => { 19 - await browserLogin(); 20 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 21 - 22 - await page.goto(`${webUrl}/cabinet/files`); 23 - await page.waitForLoadState("networkidle"); 24 - 25 - const searchInput = page.getByPlaceholder("Search your cabinet…"); 26 - await searchInput.fill("test"); 27 - 28 - await expect(page).toHaveURL(/\/cabinet\/search\?q=test/); 29 - await expect(page.locator(".breadcrumbs").getByText(/Search:/).first()).toBeVisible(); 30 - }); 31 - 32 - test("clearing search navigates back to files", async ({ 33 - page, 34 - webUrl, 35 - pdsUrl, 36 - account, 37 - browserLogin, 38 - }) => { 39 - await browserLogin(); 40 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 41 - 42 - await page.goto(`${webUrl}/cabinet/files`); 43 - await page.waitForLoadState("networkidle"); 44 - 45 - const searchInput = page.getByPlaceholder("Search your cabinet…"); 46 - await searchInput.fill("test"); 47 - await expect(page).toHaveURL(/\/cabinet\/search/); 48 - 49 - // Clear via the X button 50 - await page.locator("header").getByRole("button").filter({ has: page.locator("svg") }).first().click(); 51 - 52 - await expect(page).toHaveURL(/\/cabinet\/files/); 53 - }); 54 - 55 - test("direct navigation to /cabinet/search?q=foo populates input", async ({ 56 - page, 57 - webUrl, 58 - pdsUrl, 59 - account, 60 - browserLogin, 61 - }) => { 62 - await browserLogin(); 63 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 64 - 65 - await page.goto(`${webUrl}/cabinet/search?q=hello`); 66 - 67 - const searchInput = page.getByPlaceholder("Search your cabinet…"); 68 - await expect(searchInput).toHaveValue("hello", { timeout: 5_000 }); 69 - }); 70 - }); 71 - 72 - test.describe.skip("search results", () => { 73 - test("uploaded file appears in search results", async ({ 74 - page, 75 - webUrl, 76 - pdsUrl, 77 - account, 78 - browserLogin, 79 - }) => { 80 - await browserLogin(); 81 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 82 - 83 - await page.goto(`${webUrl}/cabinet/files`); 84 - await expect(page.getByText("Nothing here yet")).toBeVisible({ 85 - timeout: 10_000, 86 - }); 87 - 88 - // Upload a file 89 - const [fileChooser] = await Promise.all([ 90 - page.waitForEvent("filechooser"), 91 - page.getByTestId("file-upload").click({ force: true }), 92 - ]); 93 - await fileChooser.setFiles({ 94 - name: "searchable-doc.txt", 95 - mimeType: "text/plain", 96 - buffer: Buffer.from("content for search test"), 97 - }); 98 - 99 - // Wait for file to appear 100 - await expect( 101 - page.locator('[aria-label*="searchable-doc"]').first(), 102 - ).toBeVisible({ timeout: 30_000 }); 103 - 104 - // Search for it 105 - const searchInput = page.getByPlaceholder("Search your cabinet…"); 106 - await searchInput.fill("searchable"); 107 - 108 - await expect(page).toHaveURL(/\/cabinet\/search/); 109 - 110 - // Result should appear under "Your Cabinet" section 111 - await expect(page.getByText("Your Cabinet").first()).toBeVisible({ 112 - timeout: 10_000, 113 - }); 114 - await expect( 115 - page.locator('[aria-label*="searchable-doc"]').first(), 116 - ).toBeVisible(); 117 - }); 118 - 119 - test("no results shows empty state", async ({ 120 - page, 121 - webUrl, 122 - pdsUrl, 123 - account, 124 - browserLogin, 125 - }) => { 126 - await browserLogin(); 127 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 128 - 129 - await page.goto(`${webUrl}/cabinet/files`); 130 - await page.waitForLoadState("networkidle"); 131 - 132 - const searchInput = page.getByPlaceholder("Search your cabinet…"); 133 - await searchInput.fill("xyznonexistent"); 134 - 135 - await expect(page).toHaveURL(/\/cabinet\/search/); 136 - await expect(page.getByText(/No results for/)).toBeVisible({ 137 - timeout: 10_000, 138 - }); 139 - }); 140 - 141 - test("clicking a search result navigates to the file", async ({ 142 - page, 143 - webUrl, 144 - pdsUrl, 145 - account, 146 - browserLogin, 147 - }) => { 148 - await browserLogin(); 149 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 150 - 151 - await page.goto(`${webUrl}/cabinet/files`); 152 - await expect(page.getByText("Nothing here yet")).toBeVisible({ 153 - timeout: 10_000, 154 - }); 155 - 156 - // Upload a file 157 - const [fileChooser] = await Promise.all([ 158 - page.waitForEvent("filechooser"), 159 - page.getByTestId("file-upload").click({ force: true }), 160 - ]); 161 - await fileChooser.setFiles({ 162 - name: "clickme.txt", 163 - mimeType: "text/plain", 164 - buffer: Buffer.from("click test"), 165 - }); 166 - 167 - await expect( 168 - page.locator('[aria-label*="clickme"]').first(), 169 - ).toBeVisible({ timeout: 30_000 }); 170 - 171 - // Search and click the result 172 - const searchInput = page.getByPlaceholder("Search your cabinet…"); 173 - await searchInput.fill("clickme"); 174 - 175 - await expect(page).toHaveURL(/\/cabinet\/search/); 176 - 177 - const result = page.locator('[aria-label*="clickme"]').first(); 178 - await expect(result).toBeVisible({ timeout: 10_000 }); 179 - await result.click(); 180 - 181 - // Should navigate away from search to the file browser 182 - await expect(page).toHaveURL(/\/cabinet\/files/); 183 - }); 184 - 185 - test("folder appears in search results", async ({ 186 - page, 187 - webUrl, 188 - pdsUrl, 189 - account, 190 - browserLogin, 191 - }) => { 192 - await browserLogin(); 193 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 194 - 195 - await page.goto(`${webUrl}/cabinet/files`); 196 - await page.waitForLoadState("networkidle"); 197 - 198 - // Create a folder 199 - await page.getByRole("button", { name: "New" }).click(); 200 - await page.getByRole("button", { name: "New folder" }).click(); 201 - 202 - const folderDialog = page.locator('dialog[aria-label="New folder"]'); 203 - await expect(folderDialog).toBeVisible(); 204 - await folderDialog.getByLabel("Folder name").fill("Search Folder"); 205 - await folderDialog.getByRole("button", { name: "Create" }).click(); 206 - 207 - await expect( 208 - page.locator('[aria-label="Search Folder, folder"]'), 209 - ).toBeVisible({ timeout: 10_000 }); 210 - 211 - // Search for the folder 212 - const searchInput = page.getByPlaceholder("Search your cabinet…"); 213 - await searchInput.fill("Search Folder"); 214 - 215 - await expect(page).toHaveURL(/\/cabinet\/search/); 216 - await expect( 217 - page.locator('[aria-label*="Search Folder"]').first(), 218 - ).toBeVisible({ timeout: 10_000 }); 219 - }); 220 - }); 221 - 222 - test.describe.skip("search with panel shell", () => { 223 - test("search page shows breadcrumbs and footer", async ({ 224 - page, 225 - webUrl, 226 - pdsUrl, 227 - account, 228 - browserLogin, 229 - }) => { 230 - await browserLogin(); 231 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 232 - 233 - await page.goto(`${webUrl}/cabinet/search?q=test`); 234 - 235 - // Breadcrumb should show search context 236 - await expect(page.locator(".breadcrumbs").getByText(/Search/).first()).toBeVisible({ timeout: 5_000 }); 237 - 238 - // Footer should show result count 239 - await expect(page.getByText(/\d+ results? · Encrypted/)).toBeVisible(); 240 - }); 241 - });
-146
tests/tests/web/service-worker.test.ts
··· 1 - // Service Worker: registration, task scheduling, and message handling. 2 - 3 - import { test, expect } from "../../helpers/web-fixture.js"; 4 - 5 - test.describe("service worker", () => { 6 - test("registers after login", async ({ page, webUrl, browserLogin }) => { 7 - await browserLogin(); 8 - 9 - const swRegistered = await page.evaluate(async () => { 10 - if (!("serviceWorker" in navigator)) return false; 11 - const registration = await navigator.serviceWorker.ready; 12 - return registration.active !== null; 13 - }); 14 - 15 - expect(swRegistered).toBe(true); 16 - }); 17 - 18 - test("handles session-refresh message", async ({ page, browserLogin }) => { 19 - await browserLogin(); 20 - 21 - // Wait for SW to be active and controllable 22 - await page.evaluate(async () => { 23 - if ("serviceWorker" in navigator) { 24 - await navigator.serviceWorker.ready; 25 - } 26 - }); 27 - 28 - // The SW may not control this page yet on first load — reload to ensure 29 - await page.reload(); 30 - await page.waitForTimeout(1_000); 31 - 32 - const hasController = await page.evaluate(() => { 33 - return navigator.serviceWorker.controller !== null; 34 - }); 35 - 36 - // If controller is available, trigger a refresh and verify no error 37 - if (hasController) { 38 - const swErrors: string[] = []; 39 - page.on("console", (msg) => { 40 - const text = msg.text(); 41 - if (text.includes("session refresh failed")) { 42 - swErrors.push(text); 43 - } 44 - }); 45 - 46 - await page.evaluate(() => { 47 - navigator.serviceWorker.controller?.postMessage({ 48 - type: "session-refresh", 49 - }); 50 - }); 51 - await page.waitForTimeout(3_000); 52 - 53 - expect(swErrors.length).toBe(0); 54 - } 55 - 56 - expect(hasController).toBe(true); 57 - }); 58 - 59 - test("handles pair-cleanup message without error", async ({ 60 - page, 61 - browserLogin, 62 - }) => { 63 - const swErrors: string[] = []; 64 - page.on("console", (msg) => { 65 - const text = msg.text(); 66 - if (text.includes("pair cleanup failed")) { 67 - swErrors.push(text); 68 - } 69 - }); 70 - 71 - await browserLogin(); 72 - await page.evaluate(async () => { 73 - if ("serviceWorker" in navigator) { 74 - await navigator.serviceWorker.ready; 75 - } 76 - }); 77 - 78 - await page.evaluate(() => { 79 - navigator.serviceWorker.controller?.postMessage({ 80 - type: "pair-cleanup", 81 - }); 82 - }); 83 - await page.waitForTimeout(3_000); 84 - 85 - expect(swErrors.length).toBe(0); 86 - }); 87 - 88 - test("handles grant-healing message without error", async ({ 89 - page, 90 - browserLogin, 91 - }) => { 92 - const swErrors: string[] = []; 93 - page.on("console", (msg) => { 94 - const text = msg.text(); 95 - if (text.includes("grant healing failed")) { 96 - swErrors.push(text); 97 - } 98 - }); 99 - 100 - await browserLogin(); 101 - await page.evaluate(async () => { 102 - if ("serviceWorker" in navigator) { 103 - await navigator.serviceWorker.ready; 104 - } 105 - }); 106 - 107 - await page.evaluate(() => { 108 - navigator.serviceWorker.controller?.postMessage({ 109 - type: "grant-healing", 110 - }); 111 - }); 112 - await page.waitForTimeout(3_000); 113 - 114 - expect(swErrors.length).toBe(0); 115 - }); 116 - 117 - test("refreshed session is usable after SW cycle", async ({ 118 - page, 119 - webUrl, 120 - pdsUrl, 121 - account, 122 - browserLogin, 123 - }) => { 124 - const { completeSeedPhraseSetup } = await import( 125 - "../../helpers/seed-phrase.js" 126 - ); 127 - await browserLogin(); 128 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 129 - 130 - await page.evaluate(async () => { 131 - if ("serviceWorker" in navigator) { 132 - await navigator.serviceWorker.ready; 133 - navigator.serviceWorker.controller?.postMessage({ 134 - type: "session-refresh", 135 - }); 136 - } 137 - }); 138 - await page.waitForTimeout(3_000); 139 - 140 - // Navigate to file browser — if the SW broke the session, this fails 141 - await page.goto(`${webUrl}/cabinet/files`); 142 - await expect( 143 - page.getByText("Nothing here yet").or(page.getByTestId("file-list")), 144 - ).toBeVisible({ timeout: 15_000 }); 145 - }); 146 - });
-87
tests/tests/web/settings.test.ts
··· 1 - // Settings page: account info display and preferences. 2 - 3 - import { test, expect } from "../../helpers/web-fixture.js"; 4 - 5 - test.describe("settings page", () => { 6 - test.beforeEach(async ({ browserLogin }) => { 7 - await browserLogin(); 8 - }); 9 - 10 - test("displays account info", async ({ page, webUrl, pdsUrl, account }) => { 11 - await page.goto(`${webUrl}/cabinet/settings`); 12 - 13 - const definitions = page.getByRole("definition"); 14 - await expect(definitions.filter({ hasText: account.handle })).toBeVisible({ 15 - timeout: 10_000, 16 - }); 17 - await expect(definitions.filter({ hasText: new RegExp(account.did) })).toBeVisible(); 18 - await expect(definitions.filter({ hasText: pdsUrl })).toBeVisible(); 19 - 20 - await expect(page).toHaveScreenshot("settings-account-info.png"); 21 - }); 22 - 23 - test("displays preferences section", async ({ page, webUrl }) => { 24 - await page.goto(`${webUrl}/cabinet/settings`); 25 - 26 - await expect(page.getByText("Preferences", { exact: true })).toBeVisible({ 27 - timeout: 10_000, 28 - }); 29 - await expect(page.getByLabel("Enable telemetry")).toBeVisible(); 30 - await expect(page.getByLabel("Indexer URL")).toBeVisible(); 31 - }); 32 - 33 - test("telemetry toggle changes state", async ({ page, webUrl }) => { 34 - await page.goto(`${webUrl}/cabinet/settings`); 35 - 36 - const toggle = page.getByLabel("Enable telemetry"); 37 - await expect(toggle).toBeVisible({ timeout: 10_000 }); 38 - 39 - const initialState = await toggle.isChecked(); 40 - await toggle.click(); 41 - 42 - await expect(toggle).toBeChecked({ checked: !initialState }); 43 - }); 44 - 45 - test("save button disabled when indexer URL unchanged", async ({ 46 - page, 47 - webUrl, 48 - }) => { 49 - await page.goto(`${webUrl}/cabinet/settings`); 50 - 51 - await expect(page.getByLabel("Indexer URL")).toBeVisible({ 52 - timeout: 10_000, 53 - }); 54 - 55 - const saveButton = page.getByRole("button", { name: /Save/ }); 56 - await expect(saveButton).toBeDisabled(); 57 - }); 58 - 59 - test("save button enabled after changing indexer URL", async ({ 60 - page, 61 - webUrl, 62 - }) => { 63 - await page.goto(`${webUrl}/cabinet/settings`); 64 - 65 - const input = page.getByLabel("Indexer URL"); 66 - await expect(input).toBeVisible({ timeout: 10_000 }); 67 - 68 - await input.fill("http://localhost:9999"); 69 - 70 - const saveButton = page.getByRole("button", { name: /Save/ }); 71 - await expect(saveButton).toBeEnabled(); 72 - }); 73 - }); 74 - 75 - test.describe("settings access control", () => { 76 - test("unauthenticated access redirects to login", async ({ 77 - page, 78 - webUrl, 79 - }) => { 80 - await page.goto(`${webUrl}/cabinet/settings`); 81 - 82 - await expect(page.getByLabel("AT Protocol handle")).toBeVisible({ 83 - timeout: 10_000, 84 - }); 85 - expect(page.url()).toContain("/login"); 86 - }); 87 - });
tests/tests/web/settings.test.ts-snapshots/settings-account-info-chromium-darwin.png

This is a binary file and will not be displayed.

-168
tests/tests/web/sharing.test.ts
··· 1 - // Sharing page: empty state, share dialog interactions, error handling. 2 - 3 - import { test, expect, type Page } from "../../helpers/web-fixture.js"; 4 - import { completeSeedPhraseSetup } from "../../helpers/seed-phrase.js"; 5 - 6 - /** Upload a file via the opacity:0 file input + filechooser. */ 7 - async function uploadTestFile(page: Page, name: string, content: string): Promise<void> { 8 - const [fileChooser] = await Promise.all([ 9 - page.waitForEvent("filechooser"), 10 - page.getByTestId("file-upload").click({ force: true }), 11 - ]); 12 - await fileChooser.setFiles({ 13 - name, 14 - mimeType: "text/plain", 15 - buffer: Buffer.from(content), 16 - }); 17 - } 18 - 19 - /** Open the share dialog for a file row (action menu → Share…). */ 20 - async function openShareDialog(page: Page, fileNamePattern: string) { 21 - const fileRow = page.locator(`[aria-label*="${fileNamePattern}"]`); 22 - await expect(fileRow).toBeVisible({ timeout: 15_000 }); 23 - await fileRow.locator("button[aria-haspopup]").click(); 24 - await page.getByRole("button", { name: /Share\u2026/ }).click(); 25 - const dialog = page.locator('dialog[aria-label="Share file"]'); 26 - await expect(dialog).toBeVisible(); 27 - return dialog; 28 - } 29 - 30 - test.describe("shared page empty state", () => { 31 - test("shows empty state and info banner", async ({ 32 - page, 33 - webUrl, 34 - pdsUrl, 35 - account, 36 - browserLogin, 37 - }) => { 38 - await browserLogin(); 39 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 40 - 41 - await page.goto(`${webUrl}/cabinet/shared`); 42 - 43 - await expect(page.getByText("Nothing shared with you yet")).toBeVisible({ 44 - timeout: 10_000, 45 - }); 46 - await expect( 47 - page.getByText("Files others share with your handle show up here."), 48 - ).toBeVisible(); 49 - 50 - const banner = page.getByRole("alert"); 51 - await expect(banner).toBeVisible(); 52 - await expect( 53 - banner.getByText("End-to-end encrypted sharing"), 54 - ).toBeVisible(); 55 - 56 - await expect(page).toHaveScreenshot("sharing-empty-state.png"); 57 - }); 58 - }); 59 - 60 - test.describe("share dialog", () => { 61 - test("opens from file action menu with correct state", async ({ 62 - page, 63 - webUrl, 64 - pdsUrl, 65 - account, 66 - browserLogin, 67 - }) => { 68 - await browserLogin(); 69 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 70 - 71 - await page.goto(`${webUrl}/cabinet/files`); 72 - await page.waitForTimeout(2_000); 73 - 74 - await uploadTestFile(page, "share-test.txt", "file to share"); 75 - const shareDialog = await openShareDialog(page, "share-test"); 76 - 77 - await expect(shareDialog.getByText("share-test")).toBeVisible(); 78 - const recipientInput = shareDialog.locator("#share-recipient"); 79 - await expect(recipientInput).toHaveValue(""); 80 - await expect( 81 - shareDialog.getByRole("button", { name: "Share" }), 82 - ).toBeDisabled(); 83 - }); 84 - 85 - test("share button enables when handle is entered", async ({ 86 - page, 87 - webUrl, 88 - pdsUrl, 89 - account, 90 - browserLogin, 91 - }) => { 92 - await browserLogin(); 93 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 94 - 95 - await page.goto(`${webUrl}/cabinet/files`); 96 - await page.waitForTimeout(2_000); 97 - 98 - await uploadTestFile(page, "enable-test.txt", "testing share button"); 99 - const shareDialog = await openShareDialog(page, "enable-test"); 100 - 101 - const shareButton = shareDialog.getByRole("button", { name: "Share" }); 102 - const recipientInput = shareDialog.locator("#share-recipient"); 103 - 104 - await expect(shareButton).toBeDisabled(); 105 - await recipientInput.fill("someone.test"); 106 - await expect(shareButton).toBeEnabled(); 107 - await recipientInput.clear(); 108 - await expect(shareButton).toBeDisabled(); 109 - }); 110 - 111 - test("shows error for unresolvable recipient", async ({ 112 - page, 113 - webUrl, 114 - pdsUrl, 115 - account, 116 - browserLogin, 117 - }) => { 118 - await browserLogin(); 119 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 120 - 121 - await page.goto(`${webUrl}/cabinet/files`); 122 - await page.waitForTimeout(2_000); 123 - 124 - await uploadTestFile(page, "error-test.txt", "testing error path"); 125 - const shareDialog = await openShareDialog(page, "error-test"); 126 - 127 - await shareDialog.locator("#share-recipient").fill("nonexistent.handle"); 128 - await shareDialog.getByRole("button", { name: "Share" }).click(); 129 - 130 - const errorAlert = shareDialog.locator("#share-error"); 131 - await expect(errorAlert).toBeVisible({ timeout: 10_000 }); 132 - await expect(errorAlert).toHaveAttribute("role", "alert"); 133 - }); 134 - 135 - test("cancel closes the dialog", async ({ 136 - page, 137 - webUrl, 138 - pdsUrl, 139 - account, 140 - browserLogin, 141 - }) => { 142 - await browserLogin(); 143 - await completeSeedPhraseSetup(page, { pdsUrl, ...account }); 144 - 145 - await page.goto(`${webUrl}/cabinet/files`); 146 - await page.waitForTimeout(2_000); 147 - 148 - await uploadTestFile(page, "cancel-test.txt", "testing cancel"); 149 - const shareDialog = await openShareDialog(page, "cancel-test"); 150 - 151 - await shareDialog.getByRole("button", { name: "Cancel" }).click(); 152 - await expect(shareDialog).not.toBeVisible(); 153 - }); 154 - }); 155 - 156 - test.describe("shared page access control", () => { 157 - test("unauthenticated access redirects to login", async ({ 158 - page, 159 - webUrl, 160 - }) => { 161 - await page.goto(`${webUrl}/cabinet/shared`); 162 - 163 - await expect(page.getByLabel("AT Protocol handle")).toBeVisible({ 164 - timeout: 10_000, 165 - }); 166 - expect(page.url()).toContain("/login"); 167 - }); 168 - });
-15
tests/tests/web/smoke.test.ts
··· 1 - // Smoke test: verify infrastructure is working. 2 - 3 - import { test, expect } from "../../helpers/web-fixture.js"; 4 - 5 - test("fake-pds is reachable", async ({ pdsUrl }) => { 6 - const res = await fetch(`${pdsUrl}/.well-known/oauth-authorization-server`); 7 - expect(res.status).toBe(200); 8 - const body = (await res.json()) as { issuer: string }; 9 - expect(body.issuer).toBe(pdsUrl); 10 - }); 11 - 12 - test("web app loads", async ({ page, webUrl }) => { 13 - await page.goto(webUrl); 14 - await expect(page.locator("body")).toBeVisible(); 15 - });
+1 -1
tests/vitest.config.ts
··· 4 4 test: { 5 5 testTimeout: 30_000, 6 6 hookTimeout: 30_000, 7 - exclude: ["tests/web/**", "node_modules/**"], 7 + exclude: ["node_modules/**"], 8 8 }, 9 9 });