Skip to content

Commit 411cdcb

Browse files
committed
feat: add Playwright E2E tests for widget verification pipeline
7 tests covering badge/detail/tooltip modes, error states, event emission, and ARIA accessibility via mocked GitHub API.
1 parent c8950e3 commit 411cdcb

5 files changed

Lines changed: 369 additions & 0 deletions

File tree

e2e/fixture.html

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>E2E Test Fixture</title>
6+
<script type="module" src="/dist/auths-verify.mjs"></script>
7+
</head>
8+
<body>
9+
<!-- Badge mode: repo-based verification (GitHub API will be mocked) -->
10+
<auths-verify
11+
id="badge-repo"
12+
repo="test-org/test-repo"
13+
forge="github"
14+
mode="badge">
15+
</auths-verify>
16+
17+
<!-- Detail mode: repo-based verification -->
18+
<auths-verify
19+
id="detail-repo"
20+
repo="test-org/test-repo"
21+
forge="github"
22+
mode="detail">
23+
</auths-verify>
24+
25+
<!-- Tooltip mode: repo-based verification -->
26+
<auths-verify
27+
id="tooltip-repo"
28+
repo="test-org/test-repo"
29+
forge="github"
30+
mode="tooltip">
31+
</auths-verify>
32+
33+
<!-- Error case: repo with no auths refs -->
34+
<auths-verify
35+
id="badge-empty"
36+
repo="test-org/empty-repo"
37+
forge="github"
38+
mode="badge">
39+
</auths-verify>
40+
</body>
41+
</html>

e2e/widget.spec.ts

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
import { test, expect, type Page } from '@playwright/test';
2+
3+
// ---------------------------------------------------------------------------
4+
// Mock data — matches the structure the GitHub adapter expects
5+
// ---------------------------------------------------------------------------
6+
7+
const TEST_DID = 'did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp';
8+
9+
const IDENTITY_JSON = JSON.stringify({ controller_did: TEST_DID });
10+
const ATTESTATION_JSON = JSON.stringify({
11+
version: 1,
12+
rid: 'test-rid',
13+
issuer: TEST_DID,
14+
subject: 'did:key:z6MkDev1Device',
15+
iat: '2025-01-01T00:00:00Z',
16+
signature: 'deadbeef',
17+
});
18+
19+
const IDENTITY_B64 = btoa(IDENTITY_JSON);
20+
const ATTESTATION_B64 = btoa(ATTESTATION_JSON);
21+
22+
// ---------------------------------------------------------------------------
23+
// Route handler: mocks the GitHub REST API for forge adapter
24+
// ---------------------------------------------------------------------------
25+
26+
async function mockGitHubAPI(page: Page) {
27+
// Intercept all requests to api.github.com
28+
await page.route('https://api.github.com/**', async (route) => {
29+
const url = route.request().url();
30+
31+
// 1. List refs — GET /repos/{owner}/{repo}/git/matching-refs/auths/
32+
if (url.includes('test-org/test-repo/git/matching-refs/auths/')) {
33+
return route.fulfill({
34+
status: 200,
35+
contentType: 'application/json',
36+
body: JSON.stringify([
37+
{ ref: 'refs/auths/identity', object: { sha: 'commit-id-1' } },
38+
{ ref: 'refs/auths/keys/dev1/signatures', object: { sha: 'commit-att-1' } },
39+
]),
40+
});
41+
}
42+
43+
// Empty repo — no refs
44+
if (url.includes('test-org/empty-repo/git/matching-refs/auths/')) {
45+
return route.fulfill({
46+
status: 200,
47+
contentType: 'application/json',
48+
body: JSON.stringify([]),
49+
});
50+
}
51+
52+
// 2. Get commit → tree SHA
53+
if (url.includes('git/commits/commit-id-1')) {
54+
return route.fulfill({
55+
status: 200,
56+
contentType: 'application/json',
57+
body: JSON.stringify({ tree: { sha: 'tree-identity' } }),
58+
});
59+
}
60+
61+
if (url.includes('git/commits/commit-att-1')) {
62+
return route.fulfill({
63+
status: 200,
64+
contentType: 'application/json',
65+
body: JSON.stringify({ tree: { sha: 'tree-attestation' } }),
66+
});
67+
}
68+
69+
// 3. Get tree → blob entries
70+
if (url.includes('git/trees/tree-identity')) {
71+
return route.fulfill({
72+
status: 200,
73+
contentType: 'application/json',
74+
body: JSON.stringify({
75+
tree: [{ path: 'identity.json', sha: 'blob-identity' }],
76+
}),
77+
});
78+
}
79+
80+
if (url.includes('git/trees/tree-attestation')) {
81+
return route.fulfill({
82+
status: 200,
83+
contentType: 'application/json',
84+
body: JSON.stringify({
85+
tree: [{ path: 'attestation.json', sha: 'blob-attestation' }],
86+
}),
87+
});
88+
}
89+
90+
// 4. Read blobs
91+
if (url.includes('git/blobs/blob-identity')) {
92+
return route.fulfill({
93+
status: 200,
94+
contentType: 'application/json',
95+
body: JSON.stringify({ content: IDENTITY_B64, encoding: 'base64' }),
96+
});
97+
}
98+
99+
if (url.includes('git/blobs/blob-attestation')) {
100+
return route.fulfill({
101+
status: 200,
102+
contentType: 'application/json',
103+
body: JSON.stringify({ content: ATTESTATION_B64, encoding: 'base64' }),
104+
});
105+
}
106+
107+
// Fallback: 404
108+
return route.fulfill({ status: 404, body: 'Not found' });
109+
});
110+
}
111+
112+
// ---------------------------------------------------------------------------
113+
// Helper: wait for widget to reach a terminal state
114+
// ---------------------------------------------------------------------------
115+
116+
async function waitForState(page: Page, selector: string, timeout = 15_000) {
117+
await page.waitForFunction(
118+
({ sel }) => {
119+
const el = document.querySelector(sel);
120+
if (!el) return false;
121+
const state = el.getAttribute('data-state');
122+
return state && state !== 'idle' && state !== 'loading';
123+
},
124+
{ sel: selector },
125+
{ timeout },
126+
);
127+
}
128+
129+
// ---------------------------------------------------------------------------
130+
// Tests
131+
// ---------------------------------------------------------------------------
132+
133+
test.describe('auths-verify widget E2E', () => {
134+
test.beforeEach(async ({ page }) => {
135+
await mockGitHubAPI(page);
136+
await page.goto('/e2e/fixture.html');
137+
});
138+
139+
test('badge mode: resolves identity from mocked GitHub API and reaches terminal state', async ({ page }) => {
140+
await waitForState(page, '#badge-repo');
141+
142+
const state = await page.getAttribute('#badge-repo', 'data-state');
143+
// The widget fetched refs, read identity.json, attempted WASM verification.
144+
// With fake crypto data the result is either 'verified', 'invalid', or 'error'
145+
// — any of these proves the pipeline ran end-to-end.
146+
expect(['verified', 'invalid', 'error']).toContain(state);
147+
148+
// Verify shadow DOM rendered a label
149+
const label = await page.evaluate(() => {
150+
const el = document.querySelector('#badge-repo');
151+
return el?.shadowRoot?.querySelector('.label')?.textContent;
152+
});
153+
expect(label).toBeTruthy();
154+
expect(label).not.toBe('Not verified'); // moved past idle
155+
expect(label).not.toBe('Verifying...'); // moved past loading
156+
});
157+
158+
test('detail mode: resolves and renders detail panel', async ({ page }) => {
159+
await waitForState(page, '#detail-repo');
160+
161+
const state = await page.getAttribute('#detail-repo', 'data-state');
162+
expect(['verified', 'invalid', 'error']).toContain(state);
163+
164+
// Detail panel should exist in shadow DOM
165+
const hasDetailPanel = await page.evaluate(() => {
166+
const el = document.querySelector('#detail-repo');
167+
return el?.shadowRoot?.querySelector('.detail-panel') !== null;
168+
});
169+
expect(hasDetailPanel).toBe(true);
170+
});
171+
172+
test('tooltip mode: resolves and renders tooltip panel', async ({ page }) => {
173+
await waitForState(page, '#tooltip-repo');
174+
175+
const state = await page.getAttribute('#tooltip-repo', 'data-state');
176+
expect(['verified', 'invalid', 'error']).toContain(state);
177+
178+
// Tooltip wrapper should exist
179+
const hasTooltip = await page.evaluate(() => {
180+
const el = document.querySelector('#tooltip-repo');
181+
return el?.shadowRoot?.querySelector('.tooltip-wrapper') !== null;
182+
});
183+
expect(hasTooltip).toBe(true);
184+
});
185+
186+
test('empty repo: shows error state when no auths refs exist', async ({ page }) => {
187+
await waitForState(page, '#badge-empty');
188+
189+
const state = await page.getAttribute('#badge-empty', 'data-state');
190+
expect(state).toBe('error');
191+
192+
const label = await page.evaluate(() => {
193+
const el = document.querySelector('#badge-empty');
194+
return el?.shadowRoot?.querySelector('.label')?.textContent;
195+
});
196+
expect(label).toBe('Error');
197+
});
198+
199+
test('events: widget emits auths-verified or auths-error', async ({ page }) => {
200+
// Collect events from the badge-repo widget
201+
const events = await page.evaluate(() => {
202+
return new Promise<{ type: string; detail: unknown }[]>((resolve) => {
203+
const collected: { type: string; detail: unknown }[] = [];
204+
const el = document.querySelector('#badge-repo');
205+
if (!el) return resolve([]);
206+
207+
el.addEventListener('auths-verified', (e) => {
208+
collected.push({ type: 'auths-verified', detail: (e as CustomEvent).detail });
209+
});
210+
el.addEventListener('auths-error', (e) => {
211+
collected.push({ type: 'auths-error', detail: (e as CustomEvent).detail });
212+
});
213+
214+
// Wait for event (widget auto-verifies on connect)
215+
setTimeout(() => resolve(collected), 10_000);
216+
});
217+
});
218+
219+
expect(events.length).toBeGreaterThan(0);
220+
expect(['auths-verified', 'auths-error']).toContain(events[0].type);
221+
});
222+
223+
test('accessibility: badge has correct ARIA attributes', async ({ page }) => {
224+
await waitForState(page, '#badge-repo');
225+
226+
const aria = await page.evaluate(() => {
227+
const el = document.querySelector('#badge-repo');
228+
const badge = el?.shadowRoot?.querySelector('.badge');
229+
return {
230+
role: badge?.getAttribute('role'),
231+
ariaLive: badge?.getAttribute('aria-live'),
232+
};
233+
});
234+
expect(aria.role).toBe('status');
235+
expect(aria.ariaLive).toBe('polite');
236+
});
237+
238+
test('accessibility: detail mode has aria-expanded', async ({ page }) => {
239+
await waitForState(page, '#detail-repo');
240+
241+
const expanded = await page.evaluate(() => {
242+
const el = document.querySelector('#detail-repo');
243+
const badge = el?.shadowRoot?.querySelector('.badge');
244+
return badge?.getAttribute('aria-expanded');
245+
});
246+
expect(expanded).toBe('false');
247+
});
248+
249+
});

package-lock.json

Lines changed: 60 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"build:wasm": "cd ../auths/crates/auths-verifier && wasm-pack build --target bundler --no-default-features --features wasm && rm -rf ../../../auths-verify-widget/wasm && mv pkg ../../../auths-verify-widget/wasm",
2727
"typecheck": "tsc --noEmit",
2828
"test": "vitest run",
29+
"test:e2e": "playwright test",
2930
"test:watch": "vitest",
3031
"prepublishOnly": "npm run build:wasm && npm run test && npm run build"
3132
},
@@ -55,6 +56,7 @@
5556
},
5657
"devDependencies": {
5758
"@auths/verifier": "file:../auths/packages/auths-verifier-ts",
59+
"@playwright/test": "^1.58.2",
5860
"happy-dom": "^12.10.3",
5961
"typescript": "^5.3.2",
6062
"vite": "^5.4.0",

0 commit comments

Comments
 (0)