Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,10 @@

# gstack local reports
.gstack/

# Playwright extension QA artifacts
/node_modules/
/playwright-report/
/test-results/
/blob-report/
/.playwright/
15 changes: 15 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ Treat key material with care.
- Test: `cargo test --workspace`.
- Bump the git GPUI stack: `just bump-gpui` (the ONLY way to change those pins).

## Branch hygiene — required before edits
Before changing files, always establish the current branch and its source-of-truth status:

1. Run `git status --short --branch`.
2. If on `main`, run `git fetch origin --prune`, fast-forward from `origin/main`, then create a new
feature branch before editing.
3. If not on `main`, run `git fetch origin --prune` and check whether the branch has already been
merged into current `origin/main`.
- If it has been merged, switch back to `main`, fast-forward from `origin/main`, and create a new
feature branch before editing.
- If it has not been merged, inspect the branch's upstream/ahead/behind state and update local
state before editing. Do not stack unrelated work on a stale or merged branch.
4. If there are uncommitted changes, identify whether they are user changes before switching,
rebasing, stashing, or applying patches.

## Definition of done (all must hold; show command output as evidence)
1. `cargo fmt --all --check` clean
2. `just check` green (both feature configs)
Expand Down
16 changes: 16 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,22 @@ before you're done. (For UI work you must build the app: `just check`.)
4. No new or changed dependencies (`Cargo.toml` / `Cargo.lock`) unless explicitly approved
(the git GPUI stack is bumped only via `just bump-gpui` — never hand-edit those pins)

## Branch hygiene — required before edits

Before changing files, always establish the current branch and its source-of-truth status:

1. Run `git status --short --branch`.
2. If on `main`, run `git fetch origin --prune`, fast-forward from `origin/main`, then create a new
feature branch before editing.
3. If not on `main`, run `git fetch origin --prune` and check whether the branch has already been
merged into current `origin/main`.
- If it has been merged, switch back to `main`, fast-forward from `origin/main`, and create a new
feature branch before editing.
- If it has not been merged, inspect the branch's upstream/ahead/behind state and update local
state before editing. Do not stack unrelated work on a stale or merged branch.
4. If there are uncommitted changes, identify whether they are user changes before switching,
rebasing, stashing, or applying patches.

## Code constraints

**Enforced workspace-wide** by `[workspace.lints]` + `clippy.toml` (CI fails the build):
Expand Down
89 changes: 89 additions & 0 deletions docs/browser-extension-qa.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Browser extension QA

Deckard's browser connector is a local-only, experimental extension. This QA harness is a
small Playwright smoke suite, not a full end-to-end wallet test suite. It checks that the
unpacked extension loads in Chromium, injects the EIP-1193 provider, and can connect the local
test dapp through the Deckard browser bridge.

The suite follows Playwright's Chrome extension guidance: extensions run in Chromium through a
persistent browser context, loaded with `--disable-extensions-except` and `--load-extension`.
Manifest V3 uses a service worker, so the extension id is derived from the service worker URL.

## Install

Install the Node dependencies:

```sh
npm install
```

Install Playwright's bundled Chromium:

```sh
npm run qa:browser:install
```

Use the bundled Playwright Chromium for this harness. Do not switch it to system Chrome or Edge;
those browsers no longer support the extension sideload flags that this local QA path needs.

## Run

Run the extension smoke tests headless:

```sh
npm run qa:extension
```

Run headed while debugging:

```sh
npm run qa:extension:headed
```

The test starts `deckard-browser-bridge` in dev/mock mode on `127.0.0.1:8765`, serves
`examples/browser-bridge-dapp` on `127.0.0.1:8777`, loads `extension/` unpacked, and verifies:

- the Manifest V3 service worker loads
- `window.ethereum` is injected into the local test dapp
- `eth_accounts` returns `[]` before permission
- `eth_requestAccounts` returns the deterministic dev/mock address
- `eth_accounts` returns that address after permission
- `eth_chainId` returns Sepolia (`0xaa36a7`)

If you already have a compatible bridge running on `127.0.0.1:8765`, stop it before running the
suite. To intentionally reuse it instead, set:

```sh
DECKARD_QA_REUSE_BRIDGE=1 npm run qa:extension
```

## Artifacts

Generated artifacts are ignored by git:

- `playwright-report/` — HTML reports
- `test-results/` — screenshots, traces, and videos
- `.playwright/` — persistent Chromium profiles
- `blob-report/` — Playwright blob reports

The passing dapp-connection test writes `connected-dapp.png` under `test-results/extension/...`.
On failure, Playwright retains traces, screenshots, and videos according to
`playwright.extension.config.ts`.

## Security

- Never use or commit a real seed phrase, private key, production wallet, browser profile, or
mainnet-funded account in this harness.
- The checked-in test uses only a deterministic mock address:
`0xdeC0ded0000000000000000000000000000001193`.
- The extension has no keys and performs no signing.
- Keep transaction and signing flows out of this harness until Deckard has reviewed approval UI
and local-chain-only signing tests.
- Do not log extension storage, browser profile contents, cookies, local storage, seeds, keys, or
wallet state.

## Scope

This suite proves the browser connector slice. It does not replace Rust unit/integration tests,
the real daemon proof for `deckard-signerd`, or a future transaction/signature test suite on a
local chain.
1 change: 1 addition & 0 deletions examples/browser-bridge-dapp/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="data:," />
<title>Deckard EIP-1193 bridge test dapp</title>
<style>
:root { color-scheme: dark; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
Expand Down
76 changes: 76 additions & 0 deletions package-lock.json

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

13 changes: 13 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "deckard",
"private": true,
"type": "module",
"scripts": {
"qa:browser:install": "playwright install chromium",
"qa:extension": "playwright test --config=playwright.extension.config.ts",
"qa:extension:headed": "playwright test --config=playwright.extension.config.ts --headed"
},
"devDependencies": {
"@playwright/test": "^1.61.0"
}
}
28 changes: 28 additions & 0 deletions playwright.extension.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { defineConfig } from '@playwright/test';

export default defineConfig({
testDir: './tests/extension',
fullyParallel: false,
workers: 1,
timeout: 30_000,
reporter: [
['list'],
['html', { outputFolder: 'playwright-report/extension', open: 'never' }],
],
outputDir: 'test-results/extension',
use: {
baseURL: 'http://127.0.0.1:8777',
screenshot: 'only-on-failure',
trace: 'retain-on-failure',
video: 'retain-on-failure',
serviceWorkers: 'allow',
},
webServer: {
command: 'python3 -m http.server 8777 --directory examples/browser-bridge-dapp',
url: 'http://127.0.0.1:8777',
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
stderr: 'pipe',
timeout: 10_000,
},
});
76 changes: 76 additions & 0 deletions tests/extension/browser-bridge-extension.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { test, expect } from './fixtures';

const mockAccount = '0xdeC0ded0000000000000000000000000000001193';

test('extension service worker loads', async ({ context, extensionId }) => {
const worker = context
.serviceWorkers()
.find((serviceWorker) => serviceWorker.url().startsWith(`chrome-extension://${extensionId}/`));

expect(extensionId).toMatch(/^[a-p]{32}$/);
expect(worker?.url()).toBe(`chrome-extension://${extensionId}/background.js`);
});

test('local dapp can connect through the injected provider', async ({ page }, testInfo) => {
const pageErrors: string[] = [];
page.on('pageerror', (error) => pageErrors.push(error.message));
page.on('console', (message) => {
if (message.type() === 'error') {
pageErrors.push(message.text());
}
});

await page.goto('/');
await expect(page.locator('#output')).toContainText('window.ethereum detected');

const providerState = await page.evaluate(async () => {
const provider = window.ethereum;
if (!provider) {
throw new Error('window.ethereum missing');
}
const accountsBefore = await provider.request({ method: 'eth_accounts' });
const requestAccounts = await provider.request({ method: 'eth_requestAccounts' });
const accountsAfter = await provider.request({ method: 'eth_accounts' });
const chainId = await provider.request({ method: 'eth_chainId' });
return {
accountsBefore,
requestAccounts,
accountsAfter,
chainId,
isDeckard: Boolean(provider.isDeckard),
selectedAddress: provider.selectedAddress,
};
});

expect(providerState).toEqual({
accountsBefore: [],
requestAccounts: [mockAccount],
accountsAfter: [mockAccount],
chainId: '0xaa36a7',
isDeckard: true,
selectedAddress: mockAccount,
});
expect(pageErrors).toEqual([]);

await page.evaluate((state) => {
const output = document.querySelector('#output');
if (output) {
output.textContent = JSON.stringify(state, null, 2);
}
}, providerState);

await page.screenshot({
path: testInfo.outputPath('connected-dapp.png'),
fullPage: true,
});
});

declare global {
interface Window {
ethereum?: {
isDeckard?: boolean;
selectedAddress?: string | null;
request(args: { method: string; params?: unknown[] }): Promise<unknown>;
};
}
}
Loading
Loading