craftdriver 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +12 -3
  2. package/README.md +80 -160
  3. package/dist/lib/driver.d.ts.map +1 -1
  4. package/dist/lib/driver.js +2 -0
  5. package/dist/lib/driver.js.map +1 -1
  6. package/dist/lib/driverManager.d.ts.map +1 -1
  7. package/dist/lib/driverManager.js +67 -24
  8. package/dist/lib/driverManager.js.map +1 -1
  9. package/dist/lib/http.d.ts +1 -1
  10. package/dist/lib/http.d.ts.map +1 -1
  11. package/dist/lib/http.js +35 -7
  12. package/dist/lib/http.js.map +1 -1
  13. package/dist/lib/timing.d.ts +13 -0
  14. package/dist/lib/timing.d.ts.map +1 -1
  15. package/dist/lib/timing.js +13 -0
  16. package/dist/lib/timing.js.map +1 -1
  17. package/dist/lib/types.d.ts +2 -0
  18. package/dist/lib/types.d.ts.map +1 -1
  19. package/docs/agents.md +92 -0
  20. package/docs/api-reference.md +108 -46
  21. package/docs/browser-api.md +1 -1
  22. package/docs/browser-context.md +4 -7
  23. package/docs/browser-logs.md +158 -0
  24. package/docs/cli.md +26 -26
  25. package/docs/element-api.md +37 -6
  26. package/docs/emulation.md +6 -2
  27. package/docs/index.md +64 -0
  28. package/docs/keyboard-mouse.md +28 -10
  29. package/docs/mobile-emulation.md +5 -2
  30. package/docs/network.md +203 -0
  31. package/docs/public/craftdriver-mark.svg +15 -0
  32. package/docs/public/social-card.svg +27 -0
  33. package/docs/recipes/accessibility-gate.md +53 -0
  34. package/docs/recipes/console-error-gate.md +57 -0
  35. package/docs/recipes/file-upload-download.md +58 -0
  36. package/docs/recipes/login-once-reuse-session.md +73 -0
  37. package/docs/recipes/mobile-flow-with-network-and-logs.md +57 -0
  38. package/docs/recipes/mock-api-and-assert-network.md +60 -0
  39. package/docs/recipes/multi-user-contexts.md +58 -0
  40. package/docs/recipes/trace-failing-test.md +64 -0
  41. package/docs/recipes/virtual-clock-time-sensitive-ui.md +60 -0
  42. package/docs/recipes/vitest-browser-lifecycle.md +56 -0
  43. package/docs/recipes.md +33 -0
  44. package/docs/selectors.md +22 -1
  45. package/docs/session-management.md +18 -1
  46. package/docs/standards.md +40 -0
  47. package/docs/why-craftdriver.md +28 -0
  48. package/docs/zero-config-drivers.md +52 -0
  49. package/package.json +19 -5
  50. package/docs/bidi-features.md +0 -470
@@ -0,0 +1,203 @@
1
+ # Network Mocking And Request Waiting
2
+
3
+ Use CraftDriver network tools when a test needs to control or observe HTTP traffic:
4
+
5
+ - mock an API response without changing the backend
6
+ - block analytics, ads, or flaky third-party services
7
+ - add headers such as auth tokens or test markers
8
+ - wait for the request or response triggered by a click
9
+ - assert that the app sent the right method, URL, or headers
10
+
11
+ > **Requires the default browser event connection.** Network interception and
12
+ > request/response waiting require `enableBiDi: true` (the default) and a
13
+ > browser that supports WebDriver BiDi. If you launch with
14
+ > `enableBiDi: false`, these methods throw a clear `requires BiDi` error.
15
+
16
+ ## Browser Support
17
+
18
+ CraftDriver supports these network APIs on Chrome, Chromium, and Firefox.
19
+
20
+ ## Quick Mock
21
+
22
+ ```typescript
23
+ await browser.network.mock('**/api/users', {
24
+ status: 200,
25
+ body: {
26
+ users: [{ id: 1, name: 'Test User' }],
27
+ },
28
+ });
29
+
30
+ await browser.navigateTo('https://example.com/dashboard');
31
+ await browser.expect('#users').toContainText('Test User');
32
+ ```
33
+
34
+ The pattern uses URL globs. `**/api/users` matches that path on any host.
35
+
36
+ ## Common Tasks
37
+
38
+ | Goal | API |
39
+ | ---- | --- |
40
+ | Return a fake response | `browser.network.mock(pattern, response)` |
41
+ | Block matching requests | `browser.network.block(pattern)` |
42
+ | Dynamically inspect and respond | `browser.network.intercept(pattern, handler)` |
43
+ | Remove one route | `browser.network.removeIntercept(id)` |
44
+ | Remove every route | `browser.network.removeAllIntercepts()` |
45
+ | Add headers to outgoing requests | `browser.network.setExtraHeaders(headers)` |
46
+ | Bypass or restore browser cache | `browser.network.setCacheBehavior(behavior)` |
47
+ | Wait until traffic settles | `browser.network.waitForNetworkIdle(opts?)` |
48
+ | Wait for one request | `browser.waitForRequest(patternOrPredicate, opts?)` |
49
+ | Wait for one response | `browser.waitForResponse(patternOrPredicate, opts?)` |
50
+
51
+ ## Mock Responses
52
+
53
+ ### Static response
54
+
55
+ ```typescript
56
+ await browser.network.mock('**/api/login', {
57
+ status: 401,
58
+ body: { error: 'Invalid credentials' },
59
+ });
60
+
61
+ await browser.getByLabel('Username').fill('baduser');
62
+ await browser.getByLabel('Password').fill('wrongpass');
63
+ await browser.getByRole('button', { name: 'Sign in' }).click();
64
+
65
+ await browser.expect('#error').toHaveText('Invalid credentials');
66
+ ```
67
+
68
+ ### Dynamic response
69
+
70
+ ```typescript
71
+ await browser.network.mock('**/api/items/*', (request) => {
72
+ const id = request.url.split('/').pop();
73
+
74
+ return {
75
+ status: 200,
76
+ body: { id, name: `Item ${id}`, price: 9.99 },
77
+ };
78
+ });
79
+ ```
80
+
81
+ ## Block Requests
82
+
83
+ ```typescript
84
+ await browser.network.block('**/analytics/**');
85
+ await browser.network.block('**/tracking/**');
86
+ ```
87
+
88
+ This is useful when third-party services make tests slower or less predictable.
89
+
90
+ ## Add Headers
91
+
92
+ ```typescript
93
+ await browser.network.setExtraHeaders({
94
+ 'X-Test-Mode': 'true',
95
+ Authorization: 'Bearer test-token',
96
+ });
97
+ ```
98
+
99
+ Extra headers are applied to matching browser requests after they are configured.
100
+
101
+ ## Control Cache
102
+
103
+ ```typescript
104
+ await browser.network.setCacheBehavior('bypass');
105
+
106
+ // Later, restore normal browser caching.
107
+ await browser.network.setCacheBehavior('default');
108
+ ```
109
+
110
+ ## Intercept Requests
111
+
112
+ Use `intercept()` when the response depends on request details or asynchronous
113
+ test setup.
114
+
115
+ ```typescript
116
+ const seen: string[] = [];
117
+
118
+ const interceptId = await browser.network.intercept('**/api/**', async (request) => {
119
+ seen.push(`${request.method} ${request.url}`);
120
+
121
+ return {
122
+ status: 200,
123
+ body: { intercepted: true, originalUrl: request.url },
124
+ };
125
+ });
126
+
127
+ // Later, remove only this intercept.
128
+ await browser.network.removeIntercept(interceptId);
129
+ ```
130
+
131
+ You can also delay a response to test loading UI:
132
+
133
+ ```typescript
134
+ await browser.network.intercept('**/api/**', async () => {
135
+ await new Promise((resolve) => setTimeout(resolve, 3000));
136
+ return { status: 200, body: { data: 'delayed response' } };
137
+ });
138
+
139
+ await browser.getByRole('button', { name: 'Load data' }).click();
140
+ await browser.expect('#loading-spinner').toBeVisible();
141
+ ```
142
+
143
+ ## Wait For Real Traffic
144
+
145
+ `browser.waitForRequest()` and `browser.waitForResponse()` observe real network
146
+ traffic without mocking it. Register the wait before the action that triggers the
147
+ request. `Promise.all` is the safest pattern:
148
+
149
+ ```typescript
150
+ const [response] = await Promise.all([
151
+ browser.waitForResponse('**/api/users'),
152
+ browser.getByRole('button', { name: 'Load users' }).click(),
153
+ ]);
154
+
155
+ expect(response.status).toBe(200);
156
+ ```
157
+
158
+ Both methods accept a URL glob or a predicate:
159
+
160
+ ```typescript
161
+ const [response] = await Promise.all([
162
+ browser.waitForResponse((res) => {
163
+ return res.url.includes('/api/users') && res.status === 200;
164
+ }),
165
+ browser.getByRole('button', { name: 'Load users' }).click(),
166
+ ]);
167
+ ```
168
+
169
+ ### Response shape
170
+
171
+ | Property | Type | Description |
172
+ | -------- | ---- | ----------- |
173
+ | `url` | `string` | Full request URL |
174
+ | `status` | `number` | HTTP status code |
175
+ | `statusText` | `string` | Status text such as `"OK"` |
176
+ | `headers` | `Record<string, string>` | Response headers |
177
+ | `mimeType` | `string` | Response MIME type |
178
+ | `fromCache` | `boolean` | Whether the browser served it from cache |
179
+ | `request` | `{ id, url, method, headers }` | Matching request info |
180
+
181
+ ### Request shape
182
+
183
+ | Property | Type | Description |
184
+ | -------- | ---- | ----------- |
185
+ | `id` | `string` | Browser request id |
186
+ | `url` | `string` | Full URL |
187
+ | `method` | `string` | Request method, such as `"GET"` or `"POST"` |
188
+ | `headers` | `Record<string, string>` | Request headers |
189
+
190
+ ## Timeouts
191
+
192
+ Both wait methods accept `{ timeout?: number }`. The default is the browser
193
+ navigation timeout, 30 seconds.
194
+
195
+ ```typescript
196
+ await browser.waitForResponse('**/api/users', { timeout: 10_000 });
197
+ ```
198
+
199
+ On timeout, CraftDriver throws a clear error:
200
+
201
+ ```text
202
+ waitForResponse("**/api/users") timed out after 30000ms
203
+ ```
@@ -0,0 +1,15 @@
1
+ <svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="title desc">
2
+ <title id="title">CraftDriver mark</title>
3
+ <desc id="desc">A browser window with a command prompt and connection nodes.</desc>
4
+ <rect x="12" y="24" width="136" height="112" rx="18" fill="#111827"/>
5
+ <rect x="24" y="42" width="112" height="78" rx="10" fill="#f8fafc"/>
6
+ <circle cx="34" cy="34" r="5" fill="#f97316"/>
7
+ <circle cx="50" cy="34" r="5" fill="#f59e0b"/>
8
+ <circle cx="66" cy="34" r="5" fill="#22c55e"/>
9
+ <path d="M44 78L60 64L44 50" stroke="#b45309" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
10
+ <path d="M70 82H104" stroke="#2563eb" stroke-width="8" stroke-linecap="round"/>
11
+ <path d="M91 60H112C121.941 60 130 68.0589 130 78V78C130 87.9411 121.941 96 112 96H91" stroke="#94a3b8" stroke-width="6" stroke-linecap="round"/>
12
+ <circle cx="88" cy="60" r="8" fill="#2563eb"/>
13
+ <circle cx="88" cy="96" r="8" fill="#f59e0b"/>
14
+ <circle cx="118" cy="78" r="8" fill="#22c55e"/>
15
+ </svg>
@@ -0,0 +1,27 @@
1
+ <svg width="1200" height="630" viewBox="0 0 1200 630" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="title desc">
2
+ <title id="title">CraftDriver social card</title>
3
+ <desc id="desc">Standards-based browser automation for Node.js, tests, scripts, and AI agents.</desc>
4
+ <rect width="1200" height="630" fill="#0f172a"/>
5
+ <rect x="72" y="72" width="1056" height="486" rx="32" fill="#111827" stroke="#334155" stroke-width="2"/>
6
+ <rect x="116" y="122" width="396" height="276" rx="24" fill="#f8fafc"/>
7
+ <rect x="116" y="122" width="396" height="54" rx="24" fill="#1f2937"/>
8
+ <circle cx="154" cy="149" r="9" fill="#f97316"/>
9
+ <circle cx="184" cy="149" r="9" fill="#f59e0b"/>
10
+ <circle cx="214" cy="149" r="9" fill="#22c55e"/>
11
+ <path d="M184 274L230 232L184 190" stroke="#b45309" stroke-width="18" stroke-linecap="round" stroke-linejoin="round"/>
12
+ <path d="M264 286H382" stroke="#2563eb" stroke-width="18" stroke-linecap="round"/>
13
+ <path d="M349 210H414C446.585 210 473 236.415 473 269V269C473 301.585 446.585 328 414 328H349" stroke="#94a3b8" stroke-width="12" stroke-linecap="round"/>
14
+ <circle cx="346" cy="210" r="17" fill="#2563eb"/>
15
+ <circle cx="346" cy="328" r="17" fill="#f59e0b"/>
16
+ <circle cx="432" cy="269" r="17" fill="#22c55e"/>
17
+ <text x="574" y="212" fill="#f8fafc" font-family="Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="82" font-weight="800">CraftDriver</text>
18
+ <text x="578" y="281" fill="#f59e0b" font-family="Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="34" font-weight="700">WebDriver automation for humans and AI agents</text>
19
+ <text x="578" y="356" fill="#cbd5e1" font-family="Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="30">Playwright-style ergonomics. WebDriver standards. AI friendly.</text>
20
+ <text x="578" y="408" fill="#cbd5e1" font-family="Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="30">Library API, CLI, MCP server, and packaged agent skills.</text>
21
+ <rect x="578" y="462" width="156" height="48" rx="24" fill="#b45309"/>
22
+ <text x="614" y="495" fill="#fff7ed" font-family="Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="22" font-weight="700">Node.js</text>
23
+ <rect x="754" y="462" width="178" height="48" rx="24" fill="#1d4ed8"/>
24
+ <text x="790" y="495" fill="#eff6ff" font-family="Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="22" font-weight="700">WebDriver</text>
25
+ <rect x="952" y="462" width="102" height="48" rx="24" fill="#15803d"/>
26
+ <text x="984" y="495" fill="#f0fdf4" font-family="Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="22" font-weight="700">MCP</text>
27
+ </svg>
@@ -0,0 +1,53 @@
1
+ # Run Accessibility Gates
2
+
3
+ Use this pattern when CI should reject serious accessibility regressions, while
4
+ still allowing deliberate rule exceptions in one place.
5
+
6
+ ```ts
7
+ import { afterAll, beforeAll, beforeEach, describe, it } from 'vitest';
8
+ import { Browser } from 'craftdriver';
9
+
10
+ const A11Y_OPTIONS = {
11
+ minImpact: 'serious' as const,
12
+ disableRules: [
13
+ // Example: keep temporary exceptions explicit and reviewed.
14
+ 'color-contrast',
15
+ ],
16
+ };
17
+
18
+ describe('accessibility gate', () => {
19
+ let browser: Browser;
20
+
21
+ beforeAll(async () => {
22
+ browser = await Browser.launch({ browserName: 'chrome' });
23
+ });
24
+
25
+ beforeEach(async () => {
26
+ await browser.navigateTo('http://localhost:3000/settings');
27
+ });
28
+
29
+ afterAll(async () => {
30
+ await browser.quit();
31
+ });
32
+
33
+ it('passes page-level accessibility checks', async () => {
34
+ await browser.a11y.check(A11Y_OPTIONS);
35
+ });
36
+
37
+ it('passes modal accessibility checks', async () => {
38
+ await browser.getByRole('button', { name: 'Edit profile' }).click();
39
+ await browser.locator('#profile-modal').a11y.check(A11Y_OPTIONS);
40
+ });
41
+ });
42
+ ```
43
+
44
+ ## Notes
45
+
46
+ - Use `check()` when violations should fail the test.
47
+ - Use `audit()` when you want to write a report or inspect violations manually.
48
+ - Prefer scoped checks for dynamic UI such as dialogs, menus, and checkout panels.
49
+
50
+ ## Learn More
51
+
52
+ - [Accessibility](../accessibility.md)
53
+ - [Selectors](../selectors.md)
@@ -0,0 +1,57 @@
1
+ # Fail On Console And JavaScript Errors
2
+
3
+ Use this pattern when a flow can look correct in the DOM while still logging
4
+ client-side errors. Start log capture early, run the flow, then fail on
5
+ unexpected JavaScript errors.
6
+
7
+ ```ts
8
+ import { afterAll, beforeAll, beforeEach, describe, it } from 'vitest';
9
+ import { Browser } from 'craftdriver';
10
+
11
+ describe('critical browser flows', () => {
12
+ let browser: Browser;
13
+
14
+ beforeAll(async () => {
15
+ browser = await Browser.launch({
16
+ browserName: 'chrome',
17
+ captureLogs: true,
18
+ });
19
+ });
20
+
21
+ beforeEach(async () => {
22
+ browser.logs.clearLogs();
23
+ });
24
+
25
+ afterAll(async () => {
26
+ await browser.quit();
27
+ });
28
+
29
+ it('saves settings without browser errors', async () => {
30
+ await browser.navigateTo('http://localhost:3000/settings');
31
+ await browser.getByLabel('Display name').fill('Alice');
32
+ await browser.getByRole('button', { name: 'Save' }).click();
33
+
34
+ await browser.expect('#toast').toContainText('Saved');
35
+ browser.logs.assertNoErrors();
36
+ });
37
+ });
38
+ ```
39
+
40
+ ## Wait For A Known Log
41
+
42
+ When the log itself is part of the expected behavior, arm the wait before the
43
+ action that emits it:
44
+
45
+ ```ts
46
+ const saved = browser.logs.waitForConsole((message) => {
47
+ return message.level === 'info' && message.text.includes('settings:saved');
48
+ });
49
+
50
+ await browser.getByRole('button', { name: 'Save' }).click();
51
+ await saved;
52
+ ```
53
+
54
+ ## Learn More
55
+
56
+ - [Console Logs And JavaScript Errors](../browser-logs.md)
57
+ - [Tracing](../tracing.md)
@@ -0,0 +1,58 @@
1
+ # Test File Uploads And Downloads
2
+
3
+ Use this pattern when a workflow imports data, uploads an attachment, exports a
4
+ report, or verifies a generated file.
5
+
6
+ ```ts
7
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
8
+ import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
9
+ import { resolve, join } from 'node:path';
10
+ import { Browser } from 'craftdriver';
11
+
12
+ describe('reports', () => {
13
+ let browser: Browser;
14
+ const downloadsDir = resolve('.tmp/downloads');
15
+ const fixture = resolve('tests/fixtures/sample.txt');
16
+
17
+ beforeAll(async () => {
18
+ mkdirSync(downloadsDir, { recursive: true });
19
+ browser = await Browser.launch({
20
+ browserName: 'chrome',
21
+ downloadsDir,
22
+ });
23
+ });
24
+
25
+ afterAll(async () => {
26
+ await browser.quit();
27
+ rmSync(downloadsDir, { recursive: true, force: true });
28
+ });
29
+
30
+ it('uploads a source file and downloads a report', async () => {
31
+ await browser.navigateTo('http://localhost:3000/reports');
32
+
33
+ await browser.find('#source-file').setInputFiles(fixture);
34
+ await browser.expect('#upload-status').toContainText('sample.txt');
35
+
36
+ const download = await browser.waitForDownload(() => {
37
+ return browser.getByRole('button', { name: 'Export report' }).click();
38
+ });
39
+
40
+ const target = join(downloadsDir, download.suggestedFilename);
41
+ await download.saveAs(target);
42
+
43
+ expect(existsSync(target)).toBe(true);
44
+ expect(readFileSync(target, 'utf8')).toContain('Report');
45
+ });
46
+ });
47
+ ```
48
+
49
+ ## Notes
50
+
51
+ - Configure `downloadsDir` at launch so files land somewhere predictable.
52
+ - Wrap the click that triggers the download in `waitForDownload()`.
53
+ - Use `setInputFiles()` on the actual `<input type="file">` element.
54
+
55
+ ## Learn More
56
+
57
+ - [Element API](../element-api.md)
58
+ - [Browser API](../browser-api.md)
@@ -0,0 +1,73 @@
1
+ # Log In Once And Reuse The Session
2
+
3
+ Use this pattern when login is slow, rate-limited, or not the thing most tests
4
+ are trying to prove. Log in once, save cookies and localStorage, then launch
5
+ future tests already signed in.
6
+
7
+ ## Generate Auth State
8
+
9
+ Run this as a setup step before the tests that need an authenticated user.
10
+
11
+ ```ts
12
+ import { mkdir } from 'node:fs/promises';
13
+ import { Browser } from 'craftdriver';
14
+
15
+ const authState = '.auth/alice.json';
16
+
17
+ await mkdir('.auth', { recursive: true });
18
+
19
+ const browser = await Browser.launch({ browserName: 'chrome' });
20
+
21
+ try {
22
+ await browser.navigateTo('http://localhost:3000/login');
23
+ await browser.getByLabel('Email').fill('alice@example.com');
24
+ await browser.getByLabel('Password').fill(process.env.TEST_PASSWORD!);
25
+ await browser.getByRole('button', { name: 'Sign in' }).click();
26
+ await browser.expect('#account').toContainText('Alice');
27
+
28
+ await browser.saveState(authState);
29
+ } finally {
30
+ await browser.quit();
31
+ }
32
+ ```
33
+
34
+ ## Use Auth State In Tests
35
+
36
+ ```ts
37
+ import { afterAll, beforeAll, beforeEach, describe, it } from 'vitest';
38
+ import { Browser } from 'craftdriver';
39
+
40
+ describe('authenticated dashboard', () => {
41
+ let browser: Browser;
42
+
43
+ beforeAll(async () => {
44
+ browser = await Browser.launch({
45
+ browserName: 'chrome',
46
+ storageState: '.auth/alice.json',
47
+ });
48
+ });
49
+
50
+ beforeEach(async () => {
51
+ await browser.navigateTo('http://localhost:3000/dashboard');
52
+ });
53
+
54
+ afterAll(async () => {
55
+ await browser.quit();
56
+ });
57
+
58
+ it('shows account data', async () => {
59
+ await browser.expect('#account').toContainText('Alice');
60
+ });
61
+ });
62
+ ```
63
+
64
+ ## Notes
65
+
66
+ - Keep generated auth files out of source control if they contain real secrets.
67
+ - Regenerate auth state when the app changes login/session behavior.
68
+ - Use separate files such as `.auth/admin.json` and `.auth/customer.json` for different roles.
69
+
70
+ ## Learn More
71
+
72
+ - [Session Management](../session-management.md)
73
+ - [Browser Contexts](../browser-context.md)
@@ -0,0 +1,57 @@
1
+ # Test A Mobile Flow With API Mocks And Logs
2
+
3
+ Use this pattern when mobile layout depends on a device preset and a backend
4
+ configuration response. This combines mobile emulation, network mocking, and log
5
+ capture in one test.
6
+
7
+ ```ts
8
+ import { afterAll, beforeAll, describe, it } from 'vitest';
9
+ import { Browser } from 'craftdriver';
10
+
11
+ describe('mobile navigation', () => {
12
+ let browser: Browser;
13
+
14
+ beforeAll(async () => {
15
+ browser = await Browser.launch({
16
+ browserName: 'chrome',
17
+ mobileEmulation: 'Pixel 7',
18
+ captureLogs: true,
19
+ });
20
+ });
21
+
22
+ afterAll(async () => {
23
+ await browser.quit();
24
+ });
25
+
26
+ it('shows the mobile menu from mocked config', async () => {
27
+ browser.logs.clearLogs();
28
+
29
+ await browser.network.mock('**/api/mobile-config', {
30
+ status: 200,
31
+ body: {
32
+ navigation: 'bottom-tabs',
33
+ showInstallPrompt: false,
34
+ },
35
+ });
36
+
37
+ await browser.navigateTo('http://localhost:3000');
38
+ await browser.getByRole('button', { name: 'Menu' }).click();
39
+
40
+ await browser.expect('#mobile-menu').toBeVisible();
41
+ await browser.expect('#desktop-nav').not.toBeVisible();
42
+ browser.logs.assertNoErrors();
43
+ });
44
+ });
45
+ ```
46
+
47
+ ## Notes
48
+
49
+ - Mobile emulation is currently Chrome/Chromium only.
50
+ - Mock before navigation when the page reads mobile config during startup.
51
+ - Keep `captureLogs: true` if startup logs matter.
52
+
53
+ ## Learn More
54
+
55
+ - [Mobile Emulation](../mobile-emulation.md)
56
+ - [Network Mocking And Request Waiting](../network.md)
57
+ - [Console Logs And JavaScript Errors](../browser-logs.md)
@@ -0,0 +1,60 @@
1
+ # Mock APIs And Assert Network Traffic
2
+
3
+ Use this pattern when the UI should be tested independently from unstable,
4
+ slow, or expensive backend services. Mock the response the app needs, then wait
5
+ for the request or response caused by the user action.
6
+
7
+ ```ts
8
+ import { expect, it } from 'vitest';
9
+ import { Browser } from 'craftdriver';
10
+
11
+ it('loads mocked users and verifies the request', async () => {
12
+ const browser = await Browser.launch({ browserName: 'chrome' });
13
+
14
+ try {
15
+ await browser.navigateTo('http://localhost:3000/users');
16
+
17
+ await browser.network.mock('**/api/users', {
18
+ status: 200,
19
+ body: {
20
+ users: [{ id: 1, name: 'Alice', plan: 'Pro' }],
21
+ },
22
+ });
23
+
24
+ const [request, response] = await Promise.all([
25
+ browser.waitForRequest((req) => {
26
+ return req.url.includes('/api/users') && req.method === 'GET';
27
+ }),
28
+ browser.waitForResponse('**/api/users'),
29
+ browser.getByRole('button', { name: 'Load users' }).click(),
30
+ ]);
31
+
32
+ expect(request.method).toBe('GET');
33
+ expect(response.status).toBe(200);
34
+ await browser.expect('#user-list').toContainText('Alice');
35
+ } finally {
36
+ await browser.quit();
37
+ }
38
+ });
39
+ ```
40
+
41
+ ## Keep Mocks Isolated
42
+
43
+ If you share a browser across tests, clear intercepts in `afterEach()`:
44
+
45
+ ```ts
46
+ afterEach(async () => {
47
+ await browser.network.removeAllIntercepts();
48
+ });
49
+ ```
50
+
51
+ ## Notes
52
+
53
+ - Register waits before the click that triggers the request.
54
+ - Use a glob for simple URL matching and a predicate when method, status, or headers matter.
55
+ - Mock before navigation if the page fetches data during initial load.
56
+
57
+ ## Learn More
58
+
59
+ - [Network Mocking And Request Waiting](../network.md)
60
+ - [Console Logs And JavaScript Errors](../browser-logs.md)
@@ -0,0 +1,58 @@
1
+ # Test Multi-User Workflows
2
+
3
+ Use this pattern for chat, permissions, approvals, collaboration, admin/customer
4
+ flows, or any test where two signed-in users must exist at the same time.
5
+
6
+ Browser contexts isolate cookies and storage while sharing one launched browser.
7
+
8
+ ```ts
9
+ import { afterAll, beforeAll, describe, it } from 'vitest';
10
+ import { Browser } from 'craftdriver';
11
+
12
+ describe('team invitation', () => {
13
+ let browser: Browser;
14
+ const baseUrl = 'http://localhost:3000';
15
+
16
+ beforeAll(async () => {
17
+ browser = await Browser.launch({ browserName: 'chrome' });
18
+ });
19
+
20
+ afterAll(async () => {
21
+ await browser.quit();
22
+ });
23
+
24
+ it('admin invites a teammate who accepts in another context', async () => {
25
+ const admin = await browser.newContext({ storageState: '.auth/admin.json' });
26
+ const teammate = await browser.newContext({ storageState: '.auth/alice.json' });
27
+
28
+ try {
29
+ const adminPage = await admin.newPage({ url: `${baseUrl}/team` });
30
+ await adminPage.find('#invite-member').click();
31
+ await adminPage.find('#invite-email').fill('alice@example.com');
32
+ await adminPage.find('#send-invite').click();
33
+ await adminPage.expect('#invite-status').toContainText('Sent');
34
+
35
+ const alicePage = await teammate.newPage({ url: `${baseUrl}/invites` });
36
+ await alicePage.find('#accept-invite').click();
37
+ await alicePage.expect('#membership').toContainText('Member');
38
+
39
+ await adminPage.find('#refresh-members').click();
40
+ await adminPage.expect('#member-list').toContainText('alice@example.com');
41
+ } finally {
42
+ await admin.close();
43
+ await teammate.close();
44
+ }
45
+ });
46
+ });
47
+ ```
48
+
49
+ ## Notes
50
+
51
+ - Use one context per user or role.
52
+ - Put context cleanup in `finally` so failed assertions do not leak sessions.
53
+ - Combine with saved storage state when users should start already signed in.
54
+
55
+ ## Learn More
56
+
57
+ - [Browser Contexts](../browser-context.md)
58
+ - [Session Management](../session-management.md)