-
Notifications
You must be signed in to change notification settings - Fork 7
test(dashnote): add Playwright e2e suite (smoke, auth, notes, settings) + CI #81
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
9437edf
test(dashnote): add Playwright e2e smoke suite
thephez 0322344
test(dashnote): add Playwright e2e auth coverage
thephez 7e11df9
test(dashnote): add Playwright e2e notes CRUD + concurrency coverage
thephez d30c070
test(dashnote): wait for list to finish loading before seeding search…
thephez 6ff9eed
test(dashnote): add Playwright e2e settings panel coverage
thephez 473b0d3
ci(dashnote): add e2e workflow gated on dashnote path changes
thephez cb134d4
test(dashnote): scope settings auth setup to its own describe block
thephez cb8a152
style(dashnote): reflow seedSearchFixtures spinner-wait assertion
thephez aeed628
ci(dashnote): pin cache/upload-artifact actions to Node 24-compatible…
thephez File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| name: Dashnote E2E | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| paths: | ||
| - 'example-apps/dashnote/**' | ||
| - '.github/workflows/dashnote-e2e.yml' | ||
| pull_request: | ||
| branches: [main] | ||
| paths: | ||
| - 'example-apps/dashnote/**' | ||
| - '.github/workflows/dashnote-e2e.yml' | ||
| workflow_dispatch: | ||
| inputs: | ||
| test_suite: | ||
| description: 'Which specs to run' | ||
| type: choice | ||
| options: | ||
| - read-only | ||
| - read-write | ||
| - all | ||
| default: read-only | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: dashnote-e2e-${{ github.workflow }}-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| e2e-read-only: | ||
| name: e2e (read-only — smoke + settings) | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 20 | ||
| if: > | ||
| github.event_name != 'workflow_dispatch' || | ||
| inputs.test_suite == 'read-only' || | ||
| inputs.test_suite == 'all' | ||
| defaults: | ||
| run: | ||
| working-directory: example-apps/dashnote | ||
| steps: | ||
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 | ||
| with: | ||
| node-version: '20' | ||
|
|
||
| - name: Install dashnote dependencies | ||
| run: npm ci | ||
|
|
||
| - name: Resolve Playwright version | ||
| id: playwright-version | ||
| run: echo "version=$(node -p "require('./package.json').devDependencies['@playwright/test']")" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Cache Playwright browsers | ||
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | ||
| id: playwright-cache | ||
| with: | ||
| path: ~/.cache/ms-playwright | ||
| key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} | ||
|
|
||
| - name: Install Playwright browsers | ||
| if: steps.playwright-cache.outputs.cache-hit != 'true' | ||
| run: npx playwright install --with-deps chromium | ||
|
|
||
| - name: Install browser system deps (cache hit) | ||
| if: steps.playwright-cache.outputs.cache-hit == 'true' | ||
| run: npx playwright install-deps chromium | ||
|
|
||
| - name: Run read-only specs | ||
| env: | ||
| PLATFORM_MNEMONIC: ${{ secrets.PLATFORM_MNEMONIC }} | ||
| run: npx playwright test smoke.spec.ts settings.spec.ts | ||
|
|
||
| - name: Upload Playwright report on failure | ||
| if: failure() | ||
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | ||
| with: | ||
| name: playwright-report-read-only | ||
| path: example-apps/dashnote/playwright-report/ | ||
| retention-days: 14 | ||
|
|
||
| - name: Upload Playwright traces on failure | ||
| if: failure() | ||
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | ||
| with: | ||
| name: playwright-test-results-read-only | ||
| path: example-apps/dashnote/test-results/ | ||
| retention-days: 14 | ||
|
|
||
| e2e-read-write: | ||
| name: e2e (read-write — auth + notes) | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 60 | ||
| if: > | ||
| github.event_name == 'workflow_dispatch' && | ||
| (inputs.test_suite == 'read-write' || inputs.test_suite == 'all') | ||
| concurrency: | ||
| group: dashnote-e2e-read-write | ||
| cancel-in-progress: false | ||
| defaults: | ||
| run: | ||
| working-directory: example-apps/dashnote | ||
| steps: | ||
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 | ||
| with: | ||
| node-version: '20' | ||
|
|
||
| - name: Install dashnote dependencies | ||
| run: npm ci | ||
|
|
||
| - name: Resolve Playwright version | ||
| id: playwright-version | ||
| run: echo "version=$(node -p "require('./package.json').devDependencies['@playwright/test']")" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Cache Playwright browsers | ||
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | ||
| id: playwright-cache | ||
| with: | ||
| path: ~/.cache/ms-playwright | ||
| key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} | ||
|
|
||
| - name: Install Playwright browsers | ||
| if: steps.playwright-cache.outputs.cache-hit != 'true' | ||
| run: npx playwright install --with-deps chromium | ||
|
|
||
| - name: Install browser system deps (cache hit) | ||
| if: steps.playwright-cache.outputs.cache-hit == 'true' | ||
| run: npx playwright install-deps chromium | ||
|
|
||
| - name: Run read-write specs | ||
| env: | ||
| PLATFORM_MNEMONIC: ${{ secrets.PLATFORM_MNEMONIC }} | ||
| run: npx playwright test auth.spec.ts notes.spec.ts | ||
|
|
||
| - name: Upload Playwright report on failure | ||
| if: failure() | ||
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | ||
| with: | ||
| name: playwright-report-read-write | ||
| path: example-apps/dashnote/playwright-report/ | ||
| retention-days: 14 | ||
|
|
||
| - name: Upload Playwright traces on failure | ||
| if: failure() | ||
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | ||
| with: | ||
| name: playwright-test-results-read-write | ||
| path: example-apps/dashnote/test-results/ | ||
| retention-days: 14 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import { defineConfig, devices } from "@playwright/test"; | ||
| import { config as loadEnv } from "dotenv"; | ||
| import { dirname, resolve } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
|
|
||
| const here = dirname(fileURLToPath(import.meta.url)); | ||
|
|
||
| // Load repo-root .env first (where PLATFORM_MNEMONIC lives for the tutorials), | ||
| // then let a local dashnote/.env override it if present. | ||
| loadEnv({ path: resolve(here, "../../.env") }); | ||
| loadEnv({ path: resolve(here, ".env"), override: true }); | ||
|
|
||
| const PORT = 5181; | ||
|
|
||
| export default defineConfig({ | ||
| testDir: "./test/e2e", | ||
| testMatch: "**/*.spec.ts", | ||
| fullyParallel: false, | ||
| forbidOnly: !!process.env.CI, | ||
| retries: 1, | ||
| workers: 1, | ||
| timeout: 30_000, | ||
| expect: { timeout: 7_500 }, | ||
| reporter: process.env.CI ? "list" : [["list"], ["html", { open: "never" }]], | ||
|
|
||
| use: { | ||
| baseURL: `http://localhost:${PORT}`, | ||
| trace: "retain-on-failure", | ||
| permissions: ["clipboard-read", "clipboard-write"], | ||
| }, | ||
|
|
||
| projects: [ | ||
| { | ||
| name: "chromium-desktop", | ||
| use: { ...devices["Desktop Chrome"] }, | ||
| }, | ||
| { | ||
| name: "chromium-mobile", | ||
| use: { ...devices["Pixel 7"] }, | ||
| }, | ||
| ], | ||
|
|
||
| webServer: { | ||
| command: `npx vite --port ${PORT} --strictPort`, | ||
| url: `http://localhost:${PORT}`, | ||
| reuseExistingServer: !process.env.CI, | ||
| timeout: 120_000, | ||
| }, | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| import { | ||
| test, | ||
| expect, | ||
| HAS_MNEMONIC, | ||
| loginViaModal, | ||
| navButton, | ||
| openIdentityMenu, | ||
| openSettingsTab, | ||
| } from "./fixtures"; | ||
|
|
||
| test.skip(!HAS_MNEMONIC, "PLATFORM_MNEMONIC not set — skipping auth specs"); | ||
| test.describe.configure({ mode: "serial" }); | ||
|
|
||
| // Each test starts from a clean session: no remembered identity, no | ||
| // contract override. The base `page` fixture in fixtures.ts already | ||
| // waits for the SDK to connect on `/`, so the IdentityCard renders | ||
| // "Connected" (readonly) by default. | ||
| test.beforeEach(async ({ page }) => { | ||
| await page.evaluate(() => { | ||
| try { | ||
| window.localStorage.removeItem("dashnote.lastIdentity"); | ||
| window.localStorage.removeItem("dashnote.contractId"); | ||
| } catch { | ||
| /* localStorage may be unavailable in some contexts */ | ||
| } | ||
| }); | ||
| await page.reload(); | ||
| await expect(page.locator(".conn-dot.connected").first()).toBeVisible({ | ||
| timeout: 60_000, | ||
| }); | ||
| }); | ||
|
|
||
| test("login with a mnemonic, then logout via the IdentityCard menu", async ({ | ||
| page, | ||
| }) => { | ||
| await loginViaModal(page); | ||
|
|
||
| await openIdentityMenu(page); | ||
| await page.getByRole("menuitem", { name: /^log out$/i }).click(); | ||
|
|
||
| // No remembered identity → session drops back to readonly. The readonly | ||
| // IdentityCard paints "Connected" twice: once as the eyebrow label | ||
| // above the card, and once as the inline status text next to the | ||
| // connection dot — hence count=2, not 1. | ||
| await expect( | ||
| page.locator('aside[aria-label="Main navigation"]').getByText("Connected"), | ||
| ).toHaveCount(2, { timeout: 30_000 }); | ||
| }); | ||
|
|
||
| test("remember-me persists the identity hint across reloads", async ({ | ||
| page, | ||
| }) => { | ||
| await loginViaModal(page, { rememberMe: true }); | ||
|
|
||
| await page.reload(); | ||
| // A fresh load drops the keyManager, so the remembered identity boots | ||
| // into "browsing" (read-only) rather than authenticated. | ||
| await expect( | ||
| page.getByText("Browsing (read-only)", { exact: true }), | ||
| ).toBeVisible({ timeout: 30_000 }); | ||
| }); | ||
|
|
||
| test("forget-this-device via the Settings panel drops back to readonly", async ({ | ||
| page, | ||
| }) => { | ||
| await loginViaModal(page, { rememberMe: true }); | ||
|
|
||
| await openSettingsTab(page); | ||
| // "Danger zone" Section only renders when rememberedIdentityId is set. | ||
| await page.getByRole("button", { name: /forget this device/i }).click(); | ||
|
|
||
| // After forgetting, this session stays authenticated; the localStorage | ||
| // hint is gone, so a reload drops to readonly (not browsing). | ||
| await page.reload(); | ||
| await expect(page.locator(".conn-dot.connected").first()).toBeVisible({ | ||
| timeout: 30_000, | ||
| }); | ||
| await expect( | ||
| page.getByText("Browsing (read-only)", { exact: true }), | ||
| ).toBeHidden(); | ||
| }); | ||
|
|
||
| test("forget-this-device via the LoginModal also clears the remembered identity", async ({ | ||
| page, | ||
| }) => { | ||
| await loginViaModal(page, { rememberMe: true }); | ||
|
|
||
| // Log out so the modal can be opened with the rememberedIdentity panel | ||
| // visible. Logout-without-forget keeps the hint, dropping to browsing. | ||
| await openIdentityMenu(page); | ||
| await page.getByRole("menuitem", { name: /^log out$/i }).click(); | ||
| await expect( | ||
| page.getByText("Browsing (read-only)", { exact: true }), | ||
| ).toBeVisible({ timeout: 30_000 }); | ||
|
|
||
| // Open the login modal — the "Forget this device" button is rendered | ||
| // beside "Use a different identity" when rememberedIdentityId is set. | ||
| // The IdentityCard in browsing state is a button with aria-haspopup="menu" | ||
| // (the "Switch identity" menuitem opens the login modal). | ||
| await openIdentityMenu(page); | ||
| await page.getByRole("menuitem", { name: /switch identity/i }).click(); | ||
|
|
||
| const dialog = page.getByRole("dialog"); | ||
| await expect(dialog).toBeVisible(); | ||
| await dialog.getByRole("button", { name: /forget this device/i }).click(); | ||
|
|
||
| // The form re-renders without the remembered panel. | ||
| await expect( | ||
| dialog.locator('[data-testid="remembered-identity-panel"]'), | ||
| ).toBeHidden(); | ||
|
|
||
| // Close and reload — readonly state confirms the hint is gone. | ||
| await dialog.getByRole("button", { name: /^cancel$/i }).click(); | ||
| await page.reload(); | ||
| await expect( | ||
| page.getByText("Browsing (read-only)", { exact: true }), | ||
| ).toBeHidden(); | ||
| }); | ||
|
|
||
| test("Switch identity from the IdentityCard menu re-opens the login form", async ({ | ||
| page, | ||
| }) => { | ||
| await loginViaModal(page); | ||
|
|
||
| await openIdentityMenu(page); | ||
| await page.getByRole("menuitem", { name: /switch identity/i }).click(); | ||
|
|
||
| // The LoginModal opens with the same form layout regardless of session | ||
| // state. Verify it's open and ready to accept a new secret. | ||
| const dialog = page.getByRole("dialog"); | ||
| await expect(dialog).toBeVisible(); | ||
| await expect(dialog.getByPlaceholder(/mnemonic phrase|wif/i)).toBeVisible(); | ||
| await expect(dialog.getByRole("button", { name: /^Login$/ })).toBeDisabled(); | ||
| }); | ||
|
|
||
| test("Settings tab is reachable from both the IdentityCard menu and the sidebar NavButton", async ({ | ||
| page, | ||
| }) => { | ||
| await loginViaModal(page); | ||
|
|
||
| // Menu path. | ||
| await openSettingsTab(page); | ||
|
|
||
| // Sidebar NavButton path — switch away first, then back. navButton | ||
| // handles the mobile drawer open dance. | ||
| await (await navButton(page, /how it works/i)).click(); | ||
| await expect( | ||
| page.getByRole("heading", { name: /How Dashnote works/i }), | ||
| ).toBeVisible(); | ||
|
|
||
| await (await navButton(page, /settings$/i)).click(); | ||
| await expect( | ||
| page.getByRole("heading", { name: /^Settings$/, level: 1 }), | ||
| ).toBeVisible(); | ||
| }); | ||
|
|
||
| test("Settings panel displays the identity ID once authenticated", async ({ | ||
| page, | ||
| }) => { | ||
| await loginViaModal(page); | ||
| await openSettingsTab(page); | ||
|
|
||
| const idBlock = page.locator('[data-testid="settings-identity-block"]'); | ||
| // Identity IDs are base58 strings ~44 chars long; require something | ||
| // substantial rather than the loading placeholder. | ||
| await expect(idBlock).toContainText(/[0-9A-Za-z]{40,}/); | ||
| }); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.