9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_宁波本机 <908705107@qq.com>
393 lines
16 KiB
TypeScript
393 lines
16 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import { installMockBridge } from "../helpers/bridge";
|
|
import { waitForAnimations } from "../helpers/animations";
|
|
import {
|
|
dropFileOnTestId,
|
|
endWindowFileDrag,
|
|
startWindowFileDrag,
|
|
} from "../helpers/fileDrag";
|
|
|
|
async function enterMachineBackup(page: import("@playwright/test").Page) {
|
|
await installMockBridge(page, undefined, {
|
|
skipCommunitySeed: true,
|
|
skipOnboardingSeed: true,
|
|
});
|
|
await page.goto("/");
|
|
await page.getByRole("button", { name: "Create a new identity key" }).click();
|
|
}
|
|
|
|
async function openBackupOptions(page: import("@playwright/test").Page) {
|
|
await expect(page.getByTestId("backup-intro-logo")).toHaveCount(0);
|
|
await page.getByTestId("backup-options-link").click();
|
|
await expect(
|
|
page.getByTestId("onboarding-page-backup-options"),
|
|
).toBeVisible();
|
|
}
|
|
|
|
async function openPasswordBackup(page: import("@playwright/test").Page) {
|
|
await openBackupOptions(page);
|
|
await page.getByTestId("backup-option-password").click();
|
|
await expect(page.getByTestId("onboarding-page-download")).toBeVisible();
|
|
}
|
|
|
|
async function invokedCommands(page: import("@playwright/test").Page) {
|
|
return page.evaluate(
|
|
() =>
|
|
(window as Window & { __BUZZ_E2E_COMMANDS__?: string[] })
|
|
.__BUZZ_E2E_COMMANDS__ ?? [],
|
|
);
|
|
}
|
|
|
|
const SHOTS = "test-results/screenshots-onboarding";
|
|
|
|
// Mirrors the mock bridge's MOCK_NCRYPTSEC (e2eBridge.ts): the blob the
|
|
// mocked `create_ncryptsec_backup` returns, i.e. the "downloaded file"
|
|
// contents the test-your-backup dropzone expects.
|
|
const MOCK_NCRYPTSEC =
|
|
"ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p";
|
|
|
|
test("backup step appears on fresh-key path after profile submit", async ({
|
|
page,
|
|
}) => {
|
|
await enterMachineBackup(page);
|
|
|
|
await expect(page.getByTestId("onboarding-page-backup")).toBeVisible();
|
|
|
|
// Perceived-loading intro: the animated logo and "Creating" title show
|
|
// first, then the finished state replaces them after the hold.
|
|
await expect(
|
|
page.getByRole("heading", { name: "Creating your identity key" }),
|
|
).toBeVisible();
|
|
await expect(page.getByTestId("backup-intro-logo")).toBeVisible();
|
|
await expect(page.getByTestId("onboarding-next")).toBeVisible();
|
|
await expect(page.getByTestId("onboarding-next")).toBeDisabled();
|
|
await expect(page.getByTestId("onboarding-back")).toBeEnabled();
|
|
|
|
await expect(
|
|
page.getByRole("heading", {
|
|
name: "Your unique identity key has been created",
|
|
}),
|
|
).toBeVisible();
|
|
await expect(page.getByTestId("backup-intro-logo")).toHaveCount(0);
|
|
await expect(page.getByTestId("onboarding-next")).toBeEnabled();
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Key-created view: masked key with reveal toggle. Backup options open the
|
|
// dark security view; the raw key is fetched only on explicit reveal/copy.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("key view reveals explicitly; options copy explicitly", async ({
|
|
page,
|
|
}) => {
|
|
await page.context().grantPermissions(["clipboard-read", "clipboard-write"]);
|
|
await enterMachineBackup(page);
|
|
|
|
await expect(page.getByTestId("backup-intro-logo")).toHaveCount(0);
|
|
|
|
// Masked by default: decorative mask only, no key material in the DOM.
|
|
const key = page.getByTestId("backup-key-value");
|
|
await expect(key).toBeVisible();
|
|
await expect(key).toHaveClass(/blur/);
|
|
await expect(key).not.toContainText("nsec1");
|
|
expect(await invokedCommands(page)).not.toContain("get_nsec");
|
|
|
|
// Reveal fetches the key; box must not reflow (same-length monospace mask).
|
|
await page.getByTestId("backup-key-reveal-toggle").click();
|
|
await expect(key).toContainText("nsec1mock");
|
|
await expect(key).toHaveClass(/select-text/);
|
|
|
|
await waitForAnimations(page);
|
|
await page.screenshot({ path: `${SHOTS}/02-backup-chooser-revealed.png` });
|
|
|
|
// Hide again.
|
|
await page.getByTestId("backup-key-reveal-toggle").click();
|
|
await expect(key).not.toContainText("nsec1");
|
|
|
|
// Copy is available only after opening the dark backup-options view.
|
|
await page.getByTestId("backup-options-link").click();
|
|
await expect(
|
|
page.getByTestId("onboarding-page-backup-options"),
|
|
).toBeVisible();
|
|
await expect(page.getByTestId("onboarding-next")).toHaveCount(0);
|
|
await page.getByTestId("backup-copy-key").click();
|
|
await expect
|
|
.poll(async () => invokedCommands(page))
|
|
.toContain("copy_text_to_clipboard");
|
|
expect(await invokedCommands(page)).toContain("get_nsec");
|
|
|
|
// Return restores the yellow key-created view; its Next skips backup and
|
|
// continues directly to setup.
|
|
await page.getByTestId("backup-return-to-onboarding").click();
|
|
await expect(page.getByTestId("onboarding-page-backup")).toBeVisible();
|
|
await expect(page.getByTestId("onboarding-next")).toBeEnabled();
|
|
await page.getByTestId("onboarding-next").click();
|
|
await expect(page.getByTestId("onboarding-page-2")).toBeVisible();
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Encrypted download path ("Backup your key" step): password → encrypt
|
|
// locally → native save → saved confirmation. The raw key must never be
|
|
// fetched on this path.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("download happy path: generated password, encrypt, native save, Next", async ({
|
|
page,
|
|
}) => {
|
|
await enterMachineBackup(page);
|
|
|
|
// Password backup opens from the dark options state without adding an
|
|
// onboarding progress step.
|
|
await openPasswordBackup(page);
|
|
|
|
// The password field starts empty; the create button sits in the footer's
|
|
// primary slot and stays disabled until a valid password exists.
|
|
const input = page.getByTestId("backup-passphrase-input");
|
|
await expect(input).toHaveValue("");
|
|
await expect(page.getByTestId("encrypted-backup-create")).toBeDisabled();
|
|
await expect(page.getByTestId("onboarding-next")).toHaveCount(0);
|
|
await expect(page.getByTestId("backup-return-to-onboarding")).toBeVisible();
|
|
const passwordPanel = page.getByTestId("backup-password-panel");
|
|
await expect(passwordPanel).toBeVisible();
|
|
await expect(passwordPanel).not.toHaveClass(/buzz-card-textured/);
|
|
await expect(passwordPanel).toHaveCSS("padding-left", "24px");
|
|
|
|
// The inset refresh icon opens the generator popover and immediately
|
|
// fills the field (mock default: 3 words, spaces).
|
|
await page.getByTestId("backup-passphrase-generate").click();
|
|
await expect(input).toHaveValue("mock horse battery");
|
|
const generatorPopover = page.getByRole("dialog");
|
|
await expect(generatorPopover).toBeVisible();
|
|
await expect(generatorPopover).not.toHaveClass(/buzz-card-textured/);
|
|
|
|
// Popover controls regenerate in place: word count (slider) and separator.
|
|
await page.getByTestId("backup-passphrase-words").focus();
|
|
await page.keyboard.press("ArrowRight");
|
|
await expect(input).toHaveValue("mock horse battery staple");
|
|
await page
|
|
.getByTestId("backup-passphrase-separator")
|
|
.selectOption({ label: "Hyphens" });
|
|
await expect(input).toHaveValue("mock-horse-battery-staple");
|
|
|
|
// Clicking the inset icon again re-rolls without closing the popover.
|
|
await page.getByTestId("backup-passphrase-generate").click();
|
|
await expect(page.getByTestId("backup-passphrase-separator")).toBeVisible();
|
|
|
|
await waitForAnimations(page);
|
|
await page.screenshot({ path: `${SHOTS}/03-backup-download-passphrase.png` });
|
|
|
|
// Encryption may still be running when the user commits the download. The
|
|
// explicit click queues the native save without exposing the password or
|
|
// fetching the raw key.
|
|
await page.keyboard.press("Escape");
|
|
await expect(page.getByTestId("backup-passphrase-separator")).toHaveCount(0);
|
|
|
|
// Saving commits the encrypted payload only after this explicit action.
|
|
await page.getByTestId("encrypted-backup-create").click();
|
|
|
|
// Only a successful save (the mock "picks" a path) advances to the
|
|
// "Optionally, test your backup" flow: a select-file button for the saved file
|
|
// (a composer-style drop overlay takes over the card while a file drag is
|
|
// over the window), then the password to unlock it.
|
|
await expect(
|
|
page.getByRole("heading", { name: "Optionally, test your backup" }),
|
|
).toBeVisible();
|
|
const dropzone = page.getByTestId("backup-test-dropzone");
|
|
await expect(dropzone).toBeVisible();
|
|
await expect(
|
|
page.getByRole("button", { name: "Re-download backup" }),
|
|
).toBeVisible();
|
|
|
|
// The optional security subview has no onboarding Next action. Returning to
|
|
// the yellow key view is the single exit throughout the ceremony.
|
|
await expect(page.getByTestId("onboarding-next")).toHaveCount(0);
|
|
await expect(page.getByTestId("backup-return-to-onboarding")).toBeVisible();
|
|
|
|
await waitForAnimations(page);
|
|
await page.screenshot({ path: `${SHOTS}/04-backup-test-dropzone.png` });
|
|
|
|
// A wrong file is rejected with an inline error; the dropzone stays.
|
|
await page.getByTestId("backup-test-file-input").setInputFiles({
|
|
name: "notes.txt",
|
|
mimeType: "text/plain",
|
|
buffer: Buffer.from("not a key backup"),
|
|
});
|
|
await expect(page.getByTestId("backup-test-error")).toBeVisible();
|
|
|
|
// A file drag over the window swaps in the drop overlay; leaving without
|
|
// dropping restores the select button.
|
|
const dropOverlay = page.getByTestId("backup-test-drop-overlay");
|
|
await startWindowFileDrag(page);
|
|
await expect(dropOverlay).toBeVisible();
|
|
await waitForAnimations(page);
|
|
await page.screenshot({ path: `${SHOTS}/04b-backup-test-drop-overlay.png` });
|
|
await endWindowFileDrag(page);
|
|
await expect(dropOverlay).toHaveCount(0);
|
|
|
|
// Dropping the freshly downloaded file on the overlay advances to the
|
|
// password check.
|
|
await startWindowFileDrag(page);
|
|
await expect(dropOverlay).toBeVisible();
|
|
await dropFileOnTestId(page, "backup-test-drop-overlay", MOCK_NCRYPTSEC);
|
|
const password = page.getByTestId("backup-test-password");
|
|
await expect(password).toBeVisible();
|
|
await expect(dropOverlay).toHaveCount(0);
|
|
|
|
await waitForAnimations(page);
|
|
await page.screenshot({ path: `${SHOTS}/05-backup-test-password.png` });
|
|
|
|
// Verification is explicit and clears every submitted attempt.
|
|
await password.fill("mock-horse-battery-staplX");
|
|
await page.getByTestId("backup-test-verify").click();
|
|
await expect(page.getByTestId("backup-test-error")).toBeVisible();
|
|
await expect(password).toHaveValue("");
|
|
|
|
await password.fill("mock horse battery staple lake orbit");
|
|
await page.getByTestId("backup-test-verify").click();
|
|
await expect(page.getByTestId("backup-test-success")).toBeVisible();
|
|
|
|
// The celebration is driven by motion's rAF loop, which
|
|
// `waitForAnimations` (WAAPI-only) cannot observe — hold until the badge
|
|
// and copy have faded in before capturing.
|
|
await page.waitForTimeout(1200);
|
|
await waitForAnimations(page);
|
|
await page.screenshot({ path: `${SHOTS}/06-backup-test-success.png` });
|
|
|
|
// The download path must never have fetched the raw key.
|
|
const commands = await invokedCommands(page);
|
|
expect(commands).not.toContain("get_nsec");
|
|
expect(commands).toContain("create_ncryptsec_backup");
|
|
|
|
// Completion remains inside the optional security subview. Return to the
|
|
// yellow key view, whose standard Next action continues onboarding.
|
|
await expect(page.getByTestId("onboarding-next")).toHaveCount(0);
|
|
await page.getByTestId("backup-return-to-onboarding").click();
|
|
await expect(page.getByTestId("onboarding-page-backup")).toBeVisible();
|
|
await expect(page.getByTestId("onboarding-next")).toBeEnabled();
|
|
await page.getByTestId("onboarding-next").click();
|
|
await expect(page.getByTestId("onboarding-page-2")).toBeVisible();
|
|
});
|
|
|
|
test("security view returns to the yellow onboarding view", async ({
|
|
page,
|
|
}) => {
|
|
await enterMachineBackup(page);
|
|
await openPasswordBackup(page);
|
|
|
|
await expect(page.getByTestId("backup-passphrase-input")).toBeVisible();
|
|
await expect(page.getByTestId("onboarding-step-dots")).toHaveCount(0);
|
|
await expect(page.getByTestId("onboarding-next")).toHaveCount(0);
|
|
|
|
await page.getByTestId("backup-return-to-onboarding").click();
|
|
await expect(page.getByTestId("onboarding-page-backup")).toBeVisible();
|
|
await expect(page.getByTestId("backup-key-value")).toBeVisible();
|
|
await expect(page.getByTestId("onboarding-next")).toBeVisible();
|
|
});
|
|
|
|
test("returning to onboarding resets password-backup progress", async ({
|
|
page,
|
|
}) => {
|
|
await enterMachineBackup(page);
|
|
await openPasswordBackup(page);
|
|
|
|
const input = page.getByTestId("backup-passphrase-input");
|
|
await input.fill("mock-horse-battery-staple");
|
|
await page.getByTestId("encrypted-backup-create").click();
|
|
await expect(
|
|
page.getByRole("heading", { name: "Optionally, test your backup" }),
|
|
).toBeVisible();
|
|
|
|
await page.getByTestId("backup-return-to-onboarding").click();
|
|
await expect(page.getByTestId("onboarding-page-backup")).toBeVisible();
|
|
|
|
// Re-entering the security flow intentionally starts a fresh optional
|
|
// backup session so no password or completed state leaks across navigation.
|
|
await openPasswordBackup(page);
|
|
await expect(
|
|
page.getByRole("heading", { name: "Backup your key with a password" }),
|
|
).toBeVisible();
|
|
await expect(page.getByTestId("backup-passphrase-input")).toHaveValue("");
|
|
await expect(page.getByTestId("encrypted-backup-create")).toBeDisabled();
|
|
});
|
|
|
|
test("typed password requires 12 characters", async ({ page }) => {
|
|
await enterMachineBackup(page);
|
|
await openPasswordBackup(page);
|
|
|
|
const create = page.getByTestId("encrypted-backup-create");
|
|
await expect(create).toBeDisabled(); // empty field
|
|
|
|
await page.getByTestId("backup-passphrase-input").fill("short");
|
|
await expect(page.getByTestId("backup-passphrase-issue")).toBeVisible();
|
|
await expect(create).toBeDisabled();
|
|
|
|
await page
|
|
.getByTestId("backup-passphrase-input")
|
|
.fill("a much longer passphrase");
|
|
await expect(page.getByTestId("backup-passphrase-issue")).toHaveCount(0);
|
|
await expect(create).toBeEnabled();
|
|
});
|
|
|
|
test("backup step back button returns to machine identity choice", async ({
|
|
page,
|
|
}) => {
|
|
await enterMachineBackup(page);
|
|
|
|
await expect(page.getByTestId("onboarding-page-backup")).toBeVisible();
|
|
await page.getByTestId("onboarding-back").click();
|
|
|
|
// Backing out preserves the loaded key — primary CTA continues setup rather
|
|
// than minting another identity (#2318).
|
|
await expect(
|
|
page.getByRole("button", { name: "Continue setup" }),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByRole("button", { name: "Use a different key instead" }),
|
|
).toBeVisible();
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// B4: Error path coverage (reveal/copy)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("reveal shows inline error when get_nsec fails and Next still advances", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(
|
|
page,
|
|
{ nsecError: "Keychain locked" },
|
|
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
await page.getByRole("button", { name: "Create a new identity key" }).click();
|
|
|
|
await expect(page.getByTestId("onboarding-page-backup")).toBeVisible();
|
|
await page.getByTestId("backup-key-reveal-toggle").click();
|
|
|
|
await expect(page.getByTestId("backup-copy-error")).toBeVisible();
|
|
// Keychain failure does not trap the user: Next still skips backup and
|
|
// advances directly to setup.
|
|
await expect(page.getByTestId("onboarding-next")).toBeEnabled();
|
|
await page.getByTestId("onboarding-next").click();
|
|
await expect(page.getByTestId("onboarding-page-2")).toBeVisible();
|
|
});
|
|
|
|
test("reveal retry succeeds after initial failure", async ({ page }) => {
|
|
// First call fails, second succeeds (sequenced via nsecErrors).
|
|
await installMockBridge(
|
|
page,
|
|
{ nsecErrors: ["Keychain locked", null] },
|
|
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
await page.getByRole("button", { name: "Create a new identity key" }).click();
|
|
|
|
await page.getByTestId("backup-key-reveal-toggle").click();
|
|
await expect(page.getByTestId("backup-copy-error")).toBeVisible();
|
|
|
|
// Retry — second call succeeds and clears the error.
|
|
await page.getByTestId("backup-key-reveal-toggle").click();
|
|
await expect(page.getByTestId("backup-key-value")).toContainText("nsec1mock");
|
|
await expect(page.getByTestId("backup-copy-error")).not.toBeVisible();
|
|
});
|