-
Notifications
You must be signed in to change notification settings - Fork 2
test: add Playwright E2E test suite with mock mode and CI #16
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
base: main
Are you sure you want to change the base?
Changes from all commits
be62323
f8a07db
6a2405c
0f97e3f
6576a88
1885776
c724030
069e697
a9b343d
250efc8
60a746d
5a851e9
1d112c1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,3 +25,26 @@ jobs: | |
| run: npm run build | ||
| - name: Test | ||
| run: npm test -- --run --passWithNoTests | ||
|
|
||
| e2e: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 22 | ||
| cache: npm | ||
| - run: npm ci | ||
| - name: Install Playwright browser | ||
| run: npm run test:e2e:install | ||
| - name: Run deterministic Playwright suite | ||
| run: npm run test:e2e | ||
| - name: Upload Playwright artifacts | ||
| if: ${{ !cancelled() }} | ||
| uses: actions/upload-artifact@v4 | ||
|
Comment on lines
+32
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "1) Find mutable action refs in workflow:"
rg -n 'uses:\s*actions\/[^@]+@v[0-9]+' .github/workflows/ci.yml || true
echo
echo "2) Show checkout step context to verify persist-credentials hardening:"
rg -n -A6 -B2 'uses:\s*actions/checkout@' .github/workflows/ci.yml || true
echo
echo "Expected:"
echo "- No actions/* refs pinned only to `@v`* tags (use full commit SHAs)."
echo "- Every checkout step includes: with: persist-credentials: false"Repository: dashpay/dash-bridge Length of output: 981 🏁 Script executed: #!/bin/bash
set -euo pipefail
WF=".github/workflows/ci.yml"
echo "=== File excerpt around reported lines (24-60) ==="
nl -ba "$WF" | sed -n '24,60p'
echo
echo "=== All action uses in this workflow ==="
rg -n '^\s*-\s*uses:\s*actions\/' "$WF" || true
echo
echo "=== Any persist-credentials setting ==="
rg -n 'persist-credentials' "$WF" || true
echo
echo "=== Checkout steps with their immediate context ==="
rg -n -A8 -B1 'uses:\s*actions/checkout@' "$WF" || true
echo
echo "=== Attempt to detect unpinned actions/*`@v`* refs (fixed regex) ==="
rg -n '^\s*-\s*uses:\s*actions\/[^@]+@v[0-9]+' "$WF" || trueRepository: dashpay/dash-bridge Length of output: 175 🏁 Script executed: #!/bin/bash
set -euo pipefail
WF=".github/workflows/ci.yml"
echo "=== File excerpt around reported lines (24-60) ==="
awk 'NR>=24 && NR<=60 {printf "%6d: %s\n", NR, $0}' "$WF"
echo
echo "=== All action uses in this workflow (actions/* only) ==="
rg -n '^\s*-\s*uses:\s*actions\/' "$WF" || true
echo
echo "=== Any persist-credentials setting ==="
rg -n 'persist-credentials' "$WF" || true
echo
echo "=== Checkout steps with immediate context ==="
rg -n -A8 -B1 'uses:\s*actions/checkout@' "$WF" || true
echo
echo "=== Attempt to detect unpinned actions/*`@v`* refs (fixed regex) ==="
# match: uses: actions/<name>`@v`<number> (e.g., `@v4`, `@v3`)
rg -n '^\s*-\s*uses:\s*actions\/[^@]+@v[0-9]+' "$WF" || trueRepository: dashpay/dash-bridge Length of output: 2168 🏁 Script executed: #!/bin/bash
set -euo pipefail
WF=".github/workflows/ci.yml"
echo "=== Search: uses actions/*`@v`<number> (unpinned) ==="
rg -n 'uses:\s*actions/.*`@v`[0-9]+' "$WF" || true
echo
echo "=== Search: persist-credentials ==="
rg -n 'persist-credentials' "$WF" || true
echo
echo "=== Search: checkout step with surrounding lines ==="
rg -n -A8 -B1 'uses:\s*actions/checkout@' "$WF" || trueRepository: dashpay/dash-bridge Length of output: 1018 🌐 Web query:
💡 Result: The default value of actions/checkout input persist-credentials is true (i.e., credentials are persisted unless you set persist-credentials: false). [1][2] Evidence: the actions/checkout README and inputs documentation describe persist-credentials as “Default: true,” and indicate you can opt out by setting persist-credentials: false. [2][3] For security context, GitHub notes that the auth token is persisted locally to enable authenticated git commands and is removed during post-job cleanup; setting persist-credentials: false opts out. [2][3] Citations:
Pin GitHub Actions to commit SHAs and harden
🧰 Tools🪛 zizmor (1.25.2)[warning] 32-32: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false (artipacked) [error] 32-32: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy) (unpinned-uses) [error] 33-33: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy) (unpinned-uses) [error] 44-44: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy) (unpinned-uses) 🤖 Prompt for AI Agents |
||
| with: | ||
| name: playwright-artifacts | ||
| path: | | ||
| playwright-report/ | ||
| test-results/ | ||
| if-no-files-found: warn | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,3 +3,6 @@ dist/ | |
| .claude/ | ||
| *.log | ||
| .DS_Store | ||
| playwright-report/ | ||
| test-results/ | ||
| .env.playwright-live | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| import { expect, test } from '@playwright/test'; | ||
| import { | ||
| E2E_MOCK_DPNS_WIF, | ||
| E2E_MOCK_IDENTITY_ID, | ||
| E2E_MOCK_MANAGE_WIF, | ||
| } from '../src/e2e-mock-constants'; | ||
|
|
||
| const MOCK_QUERY = '/?network=testnet&e2e=mock'; | ||
|
|
||
| test.describe('Deterministic UI E2E (mock mode)', () => { | ||
| test('create identity flow transitions to completion and DPNS registration', async ({ page }) => { | ||
| await page.goto(MOCK_QUERY); | ||
|
|
||
| await page.click('#mode-create-btn'); | ||
| await expect(page.getByRole('button', { name: 'Continue' })).toBeVisible(); | ||
|
|
||
| await page.click('#continue-btn'); | ||
|
|
||
| await expect(page.locator('.deposit-headline')).toBeVisible(); | ||
| await expect.poll(async () => { | ||
| return page.evaluate(() => typeof (window as { __e2eMockAdvance?: () => void }).__e2eMockAdvance); | ||
| }).toBe('function'); | ||
| await page.evaluate(() => (window as { __e2eMockAdvance?: () => void }).__e2eMockAdvance?.()); | ||
|
|
||
| await expect(page.getByText('Save your keys')).toBeVisible(); | ||
| await expect(page.locator('.contract-id-section', { hasText: 'Your Identity ID' }).locator('.identity-id')).toHaveText(E2E_MOCK_IDENTITY_ID); | ||
|
|
||
| await page.click('#dpns-from-identity-btn'); | ||
| await expect(page.getByText('Choose Your Usernames')).toBeVisible(); | ||
|
|
||
| const usernameInput = page.locator('.dpns-username-input').first(); | ||
| await usernameInput.fill('alpha'); | ||
| await page.click('#check-availability-btn'); | ||
|
|
||
| await expect(page.getByText('Review Usernames')).toBeVisible(); | ||
| await expect(page.getByText('Available (Contested)')).toBeVisible(); | ||
| await expect(page.locator('#dpns-contested-checkbox')).toBeVisible(); | ||
| await expect(page.locator('#register-dpns-btn')).toBeDisabled(); | ||
|
|
||
| await page.locator('#dpns-contested-checkbox').click({ force: true }); | ||
| await expect(page.locator('#register-dpns-btn')).toBeEnabled(); | ||
|
|
||
| await page.click('#register-dpns-btn'); | ||
| await expect(page.getByText('Registration Complete!')).toBeVisible(); | ||
| }); | ||
|
|
||
| test('top up flow validates identity input and reaches completion', async ({ page }) => { | ||
| await page.goto(MOCK_QUERY); | ||
|
|
||
| await page.click('#mode-topup-btn'); | ||
| await expect(page.locator('#identity-id-input')).toBeVisible(); | ||
|
|
||
| await page.click('#continue-topup-btn'); | ||
| await expect(page.locator('#validation-msg')).toContainText('Please enter a valid identity ID'); | ||
|
|
||
| await page.fill('#identity-id-input', E2E_MOCK_IDENTITY_ID); | ||
| await page.click('#continue-topup-btn'); | ||
|
|
||
| await expect(page.locator('.deposit-headline')).toBeVisible(); | ||
| await expect.poll(async () => { | ||
| return page.evaluate(() => typeof (window as { __e2eMockAdvance?: () => void }).__e2eMockAdvance); | ||
| }).toBe('function'); | ||
| await page.evaluate(() => (window as { __e2eMockAdvance?: () => void }).__e2eMockAdvance?.()); | ||
|
|
||
| await expect(page.getByText('Top-up complete!')).toBeVisible(); | ||
| await expect(page.locator('.contract-id-section', { hasText: 'Identity ID' }).locator('.identity-id')).toHaveText(E2E_MOCK_IDENTITY_ID); | ||
| }); | ||
|
|
||
| test('manage identity flow validates key and applies changes', async ({ page }) => { | ||
| await page.goto(MOCK_QUERY); | ||
|
|
||
| await page.click('#mode-manage-btn'); | ||
| await expect(page.locator('#manage-identity-id-input')).toBeVisible(); | ||
|
|
||
| await page.fill('#manage-identity-id-input', E2E_MOCK_IDENTITY_ID); | ||
| await page.locator('#manage-identity-id-input').press('Tab'); | ||
| await expect(page.getByText('Identity found with 2 keys')).toBeVisible(); | ||
|
|
||
| await page.fill('#manage-private-key-input', 'bad-key'); | ||
| await page.locator('#manage-private-key-input').press('Tab'); | ||
| await expect(page.getByText('Mock mode: use the configured test private key')).toBeVisible(); | ||
|
|
||
| await page.fill('#manage-private-key-input', E2E_MOCK_MANAGE_WIF); | ||
| await page.locator('#manage-private-key-input').blur(); | ||
| await expect(page.getByText('Manage Keys')).toBeVisible(); | ||
|
|
||
| await page.click('#add-manage-key-btn'); | ||
| await page.locator('.manage-disable-key-checkbox').first().click({ force: true }); | ||
| await expect(page.getByText('Will add 1 key, disable 1 key')).toBeVisible(); | ||
|
|
||
| await page.click('#apply-manage-btn'); | ||
| await expect(page.getByText('Update Complete!')).toBeVisible(); | ||
| }); | ||
|
|
||
| test('standalone DPNS flow validates identity + key and completes registration', async ({ page }) => { | ||
| await page.goto(MOCK_QUERY); | ||
|
|
||
| await page.click('#mode-dpns-btn'); | ||
| await expect(page.getByText('Register a Username')).toBeVisible(); | ||
|
|
||
| await page.click('#dpns-choose-existing-btn'); | ||
| await expect(page.locator('#dpns-identity-id-input')).toBeVisible(); | ||
|
|
||
| await page.fill('#dpns-identity-id-input', E2E_MOCK_IDENTITY_ID); | ||
| await page.locator('#dpns-identity-id-input').press('Tab'); | ||
| await expect(page.getByText('Identity found with 2 keys')).toBeVisible(); | ||
|
|
||
| await page.fill('#dpns-private-key-input', E2E_MOCK_DPNS_WIF); | ||
| await page.locator('#dpns-private-key-input').press('Tab'); | ||
| await expect(page.getByText('Key matches key #1 (CRITICAL level)')).toBeVisible(); | ||
|
|
||
| await page.click('#dpns-identity-continue-btn'); | ||
| await expect(page.getByText('Choose Your Usernames')).toBeVisible(); | ||
|
|
||
| await page.locator('.dpns-username-input').first().fill('noncontested123456789012345'); | ||
| await page.click('#check-availability-btn'); | ||
|
|
||
| await expect(page.getByText('Review Usernames')).toBeVisible(); | ||
| await expect(page.locator('#register-dpns-btn')).toBeEnabled(); | ||
|
|
||
| await page.click('#register-dpns-btn'); | ||
| await expect(page.getByText('Registration Complete!')).toBeVisible(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| import { expect, test } from '@playwright/test'; | ||
|
|
||
| const LIVE_ENABLED = process.env.PW_E2E_LIVE === '1'; | ||
| const LIVE_TOPUP_IDENTITY_ID = process.env.PW_LIVE_TOPUP_IDENTITY_ID; | ||
| const LIVE_DPNS_IDENTITY_ID = process.env.PW_LIVE_DPNS_IDENTITY_ID; | ||
| const LIVE_DPNS_PRIVATE_KEY_WIF = process.env.PW_LIVE_DPNS_PRIVATE_KEY_WIF; | ||
| const LIVE_MANAGE_IDENTITY_ID = process.env.PW_LIVE_MANAGE_IDENTITY_ID; | ||
| const LIVE_MANAGE_PRIVATE_KEY_WIF = process.env.PW_LIVE_MANAGE_PRIVATE_KEY_WIF; | ||
|
|
||
| test.describe('Live testnet E2E (optional)', () => { | ||
| test.skip(!LIVE_ENABLED, 'Set PW_E2E_LIVE=1 to enable live testnet checks'); | ||
|
|
||
| test('create mode renders real deposit details on testnet', async ({ page }) => { | ||
| await page.goto('/?network=testnet&e2e=live'); | ||
|
|
||
| await page.click('#mode-create-btn'); | ||
| await page.click('#continue-btn'); | ||
|
|
||
| await expect(page.locator('.deposit-headline')).toBeVisible({ timeout: 15000 }); | ||
| await expect(page.getByRole('button', { name: 'Request Testnet Funds' })).toBeVisible({ timeout: 15000 }); | ||
| await expect(page.locator('.address')).toBeVisible({ timeout: 15000 }); | ||
| }); | ||
|
|
||
| test('top up mode validates and renders testnet deposit step', async ({ page }) => { | ||
| test.skip(!LIVE_TOPUP_IDENTITY_ID, 'Set PW_LIVE_TOPUP_IDENTITY_ID to run top-up live check'); | ||
|
|
||
| await page.goto('/?network=testnet&e2e=live'); | ||
|
|
||
| await page.click('#mode-topup-btn'); | ||
| await page.fill('#identity-id-input', LIVE_TOPUP_IDENTITY_ID!); | ||
| await page.click('#continue-topup-btn'); | ||
|
|
||
| await expect(page.locator('.deposit-headline')).toContainText('Top up', { timeout: 15000 }); | ||
| await expect(page.locator('.address')).toBeVisible({ timeout: 15000 }); | ||
| }); | ||
|
|
||
| test('dpns existing identity key validation works against testnet', async ({ page }) => { | ||
| test.skip( | ||
| !LIVE_DPNS_IDENTITY_ID || !LIVE_DPNS_PRIVATE_KEY_WIF, | ||
| 'Set PW_LIVE_DPNS_IDENTITY_ID and PW_LIVE_DPNS_PRIVATE_KEY_WIF to run DPNS live check' | ||
| ); | ||
|
|
||
| await page.goto('/?network=testnet&e2e=live'); | ||
|
|
||
| await page.click('#mode-dpns-btn'); | ||
| await page.click('#dpns-choose-existing-btn'); | ||
|
|
||
| await page.fill('#dpns-identity-id-input', LIVE_DPNS_IDENTITY_ID!); | ||
| await page.locator('#dpns-identity-id-input').press('Tab'); | ||
|
|
||
| await expect(page.locator('.identity-status.success')).toContainText('Identity found with', { timeout: 90000 }); | ||
|
|
||
| await page.fill('#dpns-private-key-input', LIVE_DPNS_PRIVATE_KEY_WIF!); | ||
| await page.locator('#dpns-private-key-input').press('Tab'); | ||
|
|
||
| await expect(page.locator('.key-status.success')).toContainText('Key matches key', { timeout: 30000 }); | ||
| await expect(page.locator('#dpns-identity-continue-btn')).toBeEnabled(); | ||
| }); | ||
|
|
||
| test('manage identity key validation works against testnet', async ({ page }) => { | ||
| test.skip( | ||
| !LIVE_MANAGE_IDENTITY_ID || !LIVE_MANAGE_PRIVATE_KEY_WIF, | ||
| 'Set PW_LIVE_MANAGE_IDENTITY_ID and PW_LIVE_MANAGE_PRIVATE_KEY_WIF to run manage live check' | ||
| ); | ||
|
|
||
| await page.goto('/?network=testnet&e2e=live'); | ||
|
|
||
| await page.click('#mode-manage-btn'); | ||
| await page.fill('#manage-identity-id-input', LIVE_MANAGE_IDENTITY_ID!); | ||
| await page.locator('#manage-identity-id-input').press('Tab'); | ||
|
|
||
| await expect(page.locator('.identity-status.success')).toContainText('Identity found with', { timeout: 90000 }); | ||
|
|
||
| await page.fill('#manage-private-key-input', LIVE_MANAGE_PRIVATE_KEY_WIF!); | ||
| await page.locator('#manage-private-key-input').press('Tab'); | ||
|
|
||
| await expect(page.locator('.key-status.success')).toContainText('MASTER level', { timeout: 30000 }); | ||
| await expect(page.locator('#manage-identity-continue-btn')).toBeEnabled(); | ||
| }); | ||
| }); |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.