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.
- package/CHANGELOG.md +12 -3
- package/README.md +80 -160
- package/dist/lib/driver.d.ts.map +1 -1
- package/dist/lib/driver.js +2 -0
- package/dist/lib/driver.js.map +1 -1
- package/dist/lib/driverManager.d.ts.map +1 -1
- package/dist/lib/driverManager.js +67 -24
- package/dist/lib/driverManager.js.map +1 -1
- package/dist/lib/http.d.ts +1 -1
- package/dist/lib/http.d.ts.map +1 -1
- package/dist/lib/http.js +35 -7
- package/dist/lib/http.js.map +1 -1
- package/dist/lib/timing.d.ts +13 -0
- package/dist/lib/timing.d.ts.map +1 -1
- package/dist/lib/timing.js +13 -0
- package/dist/lib/timing.js.map +1 -1
- package/dist/lib/types.d.ts +2 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/docs/agents.md +92 -0
- package/docs/api-reference.md +108 -46
- package/docs/browser-api.md +1 -1
- package/docs/browser-context.md +4 -7
- package/docs/browser-logs.md +158 -0
- package/docs/cli.md +26 -26
- package/docs/element-api.md +37 -6
- package/docs/emulation.md +6 -2
- package/docs/index.md +64 -0
- package/docs/keyboard-mouse.md +28 -10
- package/docs/mobile-emulation.md +5 -2
- package/docs/network.md +203 -0
- package/docs/public/craftdriver-mark.svg +15 -0
- package/docs/public/social-card.svg +27 -0
- package/docs/recipes/accessibility-gate.md +53 -0
- package/docs/recipes/console-error-gate.md +57 -0
- package/docs/recipes/file-upload-download.md +58 -0
- package/docs/recipes/login-once-reuse-session.md +73 -0
- package/docs/recipes/mobile-flow-with-network-and-logs.md +57 -0
- package/docs/recipes/mock-api-and-assert-network.md +60 -0
- package/docs/recipes/multi-user-contexts.md +58 -0
- package/docs/recipes/trace-failing-test.md +64 -0
- package/docs/recipes/virtual-clock-time-sensitive-ui.md +60 -0
- package/docs/recipes/vitest-browser-lifecycle.md +56 -0
- package/docs/recipes.md +33 -0
- package/docs/selectors.md +22 -1
- package/docs/session-management.md +18 -1
- package/docs/standards.md +40 -0
- package/docs/why-craftdriver.md +28 -0
- package/docs/zero-config-drivers.md +52 -0
- package/package.json +19 -5
- package/docs/bidi-features.md +0 -470
package/docs/network.md
ADDED
|
@@ -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)
|