Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- name: Upload Playwright artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-artifacts
path: |
playwright-report/
test-results/
if-no-files-found: warn
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ dist/
.claude/
*.log
.DS_Store
playwright-report/
test-results/
.env.playwright-live
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,63 @@ npm run build
npm run preview
```

## Playwright E2E

This repository includes two E2E modes:

- Deterministic UI mode (CI-safe): runs against `?e2e=mock` with no live funds/keys required.
- Optional live testnet mode: runs against testnet endpoints with real identity/key validation and realistic click-through.

### Install browser for E2E

```bash
npm run test:e2e:install
```

### Deterministic E2E (used in CI)

```bash
# Headless
npm run test:e2e

# Headed (local debugging)
npm run test:e2e:headed
```

### Optional live testnet E2E

Live suite is opt-in and skips when required variables are missing.
Never paste real private keys directly inline in shell commands. Inline secrets are saved in shell history.

```bash
# Safer approach: store sensitive values in a protected env file and source it
cat > .env.playwright-live <<'EOF'
PW_E2E_LIVE=1
PW_LIVE_TOPUP_IDENTITY_ID=<44-char-base58-id>
PW_LIVE_DPNS_IDENTITY_ID=<44-char-base58-id>
PW_LIVE_DPNS_PRIVATE_KEY_WIF=<wif-auth-key>
PW_LIVE_MANAGE_IDENTITY_ID=<44-char-base58-id>
PW_LIVE_MANAGE_PRIVATE_KEY_WIF=<wif-master-key>
EOF

chmod 600 .env.playwright-live
set -a
source .env.playwright-live
set +a
npm run test:e2e:live
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Environment variables used by the live suite:

- `PW_E2E_LIVE`: set to `1` to enable live tests.
- `PW_LIVE_TOPUP_IDENTITY_ID`: identity ID used for top-up navigation check.
- `PW_LIVE_DPNS_IDENTITY_ID`: identity ID used for DPNS key validation.
- `PW_LIVE_DPNS_PRIVATE_KEY_WIF`: DPNS AUTHENTICATION key for that identity. Prefer CRITICAL; HIGH is also accepted.
- `PW_LIVE_MANAGE_IDENTITY_ID`: identity ID used for key-management validation.
- `PW_LIVE_MANAGE_PRIVATE_KEY_WIF`: MASTER key for identity-management validation.
- `PLAYWRIGHT_BASE_URL` (optional): override app URL if running against a pre-started server.
- `PLAYWRIGHT_HOST` / `PLAYWRIGHT_PORT` (optional): host/port used by Playwright-managed dev server.

## Deployment

This project automatically deploys to GitHub Pages on push to `main` via GitHub Actions.
Expand Down
124 changes: 124 additions & 0 deletions e2e/deterministic.spec.ts
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();
});
});
80 changes: 80 additions & 0 deletions e2e/live.testnet.spec.ts
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();
});
});
64 changes: 64 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading