craftdriver 1.1.0 → 1.1.1

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 (55) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +1 -1
  3. package/dist/cli/snapshot.js +31 -4
  4. package/dist/cli/snapshot.js.map +1 -1
  5. package/dist/lib/browser.d.ts +6 -0
  6. package/dist/lib/browser.d.ts.map +1 -1
  7. package/dist/lib/browser.js +6 -0
  8. package/dist/lib/browser.js.map +1 -1
  9. package/dist/lib/by.d.ts +18 -0
  10. package/dist/lib/by.d.ts.map +1 -1
  11. package/dist/lib/by.js +141 -17
  12. package/dist/lib/by.js.map +1 -1
  13. package/dist/lib/locator.d.ts +28 -0
  14. package/dist/lib/locator.d.ts.map +1 -1
  15. package/dist/lib/locator.js +28 -0
  16. package/dist/lib/locator.js.map +1 -1
  17. package/docs/browser-api.md +4 -1
  18. package/docs/public/examples/a11y.html +44 -0
  19. package/docs/public/examples/clock.html +83 -0
  20. package/docs/public/examples/console-errors.html +344 -0
  21. package/docs/public/examples/dialogs.html +42 -0
  22. package/docs/public/examples/download.html +31 -0
  23. package/docs/public/examples/dynamic.html +131 -0
  24. package/docs/public/examples/emulate.html +127 -0
  25. package/docs/public/examples/evaluate.html +28 -0
  26. package/docs/public/examples/hover-select.html +295 -0
  27. package/docs/public/examples/iframe-child.html +51 -0
  28. package/docs/public/examples/iframes.html +35 -0
  29. package/docs/public/examples/keyboard.html +195 -0
  30. package/docs/public/examples/locator.html +131 -0
  31. package/docs/public/examples/login.html +116 -0
  32. package/docs/public/examples/mobile.html +270 -0
  33. package/docs/public/examples/mouse.html +505 -0
  34. package/docs/public/examples/network.html +293 -0
  35. package/docs/public/examples/popup-target.html +26 -0
  36. package/docs/public/examples/popup.html +50 -0
  37. package/docs/public/examples/screenshot.html +99 -0
  38. package/docs/public/examples/selectors.html +150 -0
  39. package/docs/public/examples/session.html +514 -0
  40. package/docs/public/examples/upload.html +36 -0
  41. package/docs/recipes/accessibility-gate.md +24 -40
  42. package/docs/recipes/console-error-gate.md +29 -38
  43. package/docs/recipes/file-upload-download.md +26 -43
  44. package/docs/recipes/find-elements.md +142 -0
  45. package/docs/recipes/login-once-reuse-session.md +24 -44
  46. package/docs/recipes/mobile-flow-with-network-and-logs.md +26 -41
  47. package/docs/recipes/mock-api-and-assert-network.md +22 -35
  48. package/docs/recipes/multi-user-contexts.md +32 -43
  49. package/docs/recipes/page-objects.md +52 -0
  50. package/docs/recipes/trace-failing-test.md +31 -44
  51. package/docs/recipes/virtual-clock-time-sensitive-ui.md +24 -40
  52. package/docs/recipes/vitest-browser-lifecycle.md +14 -11
  53. package/docs/recipes.md +20 -1
  54. package/docs/selectors.md +291 -229
  55. package/package.json +5 -1
@@ -1,56 +1,47 @@
1
1
  # Fail On Console And JavaScript Errors
2
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.
3
+ A flow can look correct in the DOM while quietly throwing in the console — a
4
+ failed fetch, an undefined access, a rejected promise. Capture logs at launch,
5
+ run the flow, then assert no JavaScript errors were reported so those failures
6
+ break the test instead of reaching users. This recipe uses the live
7
+ [console errors example](https://dtopuzov.github.io/craftdriver/examples/console-errors.html).
8
+ It enables `captureLogs` at launch, so it shows the `launch` call.
6
9
 
7
10
  ```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
- });
11
+ const browser = await Browser.launch({
12
+ browserName: 'chrome',
13
+ captureLogs: true,
37
14
  });
15
+
16
+ await browser.navigateTo('https://dtopuzov.github.io/craftdriver/examples/console-errors.html');
17
+ browser.logs.clearLogs();
18
+
19
+ await browser.click('#btn-console-log'); // your real user flow goes here
20
+
21
+ // Fails the test if the page reported any JavaScript error during the flow.
22
+ browser.logs.assertNoErrors();
38
23
  ```
39
24
 
40
25
  ## Wait For A Known Log
41
26
 
42
- When the log itself is part of the expected behavior, arm the wait before the
27
+ When a log line is itself part of the expected behavior, arm the wait before the
43
28
  action that emits it:
44
29
 
45
30
  ```ts
46
- const saved = browser.logs.waitForConsole((message) => {
47
- return message.level === 'info' && message.text.includes('settings:saved');
48
- });
31
+ const logged = browser.logs.waitForConsole((message) =>
32
+ message.text.includes('Hello from console.log')
33
+ );
49
34
 
50
- await browser.getByRole('button', { name: 'Save' }).click();
51
- await saved;
35
+ await browser.click('#btn-console-log');
36
+ await logged;
52
37
  ```
53
38
 
39
+ ## Notes
40
+
41
+ - `assertNoErrors()` checks captured JavaScript errors, so keep `captureLogs: true`.
42
+ - Call `clearLogs()` before the flow so errors from earlier tests don't bleed in.
43
+ - Use `waitForConsole()` / `waitForError()` when a specific message is the thing you're asserting.
44
+
54
45
  ## Learn More
55
46
 
56
47
  - [Console Logs And JavaScript Errors](../browser-logs.md)
@@ -1,56 +1,39 @@
1
1
  # Test File Uploads And Downloads
2
2
 
3
- Use this pattern when a workflow imports data, uploads an attachment, exports a
4
- report, or verifies a generated file.
3
+ Import, attachment, and export flows are easy to break and awkward to test by
4
+ hand. CraftDriver sets files on a real `<input type="file">` and captures
5
+ downloads to a directory you choose, so you can assert on the bytes that land on
6
+ disk. This recipe uploads a fixture to the live
7
+ [upload example](https://dtopuzov.github.io/craftdriver/examples/upload.html),
8
+ then downloads a report from the
9
+ [download example](https://dtopuzov.github.io/craftdriver/examples/download.html).
10
+ It sets `downloadsDir` at launch, so it shows the `launch` call.
5
11
 
6
12
  ```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
- });
13
+ const browser = await Browser.launch({
14
+ browserName: 'chrome',
15
+ downloadsDir: '.tmp/downloads',
46
16
  });
17
+
18
+ await browser.navigateTo('https://dtopuzov.github.io/craftdriver/examples/upload.html');
19
+ await browser.find('#file-input').setInputFiles('tests/fixtures/sample.txt');
20
+ await browser.expect('#result').toHaveText('sample.txt');
21
+
22
+ await browser.navigateTo('https://dtopuzov.github.io/craftdriver/examples/download.html');
23
+ const download = await browser.waitForDownload(() => browser.click('#download-btn'));
24
+
25
+ const target = `.tmp/downloads/${download.suggestedFilename}`;
26
+ await download.saveAs(target);
27
+
28
+ expect(existsSync(target)).toBe(true);
29
+ expect(readFileSync(target, 'utf8')).toContain('craftdriver download test');
47
30
  ```
48
31
 
49
32
  ## Notes
50
33
 
51
34
  - 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.
35
+ - Wrap the click that triggers the download in `waitForDownload()` so the wait is armed first.
36
+ - Call `setInputFiles()` on the actual `<input type="file">` element, not a styled wrapper.
54
37
 
55
38
  ## Learn More
56
39
 
@@ -0,0 +1,142 @@
1
+ # Find Elements On The Page
2
+
3
+ Everything you do with CraftDriver — click, fill, assert — starts by pointing at
4
+ an element. Before you can automate anything, you have to name the thing you
5
+ want. It's the first skill to learn, and it pays to learn it in the right order:
6
+ some ways of naming an element survive a redesign or a translation, and some
7
+ don't. This recipe starts with the anchors that hold up almost everywhere, then
8
+ the more expressive ones and where they earn their place.
9
+
10
+ It drives the live
11
+ [login example](https://dtopuzov.github.io/craftdriver/examples/login.html).
12
+
13
+ ## Inspect the page first
14
+
15
+ Open the page in a real browser, right-click the element you care about, and
16
+ choose **Inspect**. Then look for a handle, roughly in this order:
17
+
18
+ 1. **A stable attribute you can anchor to** — a form field's `name`, an input
19
+ `type`, a hand-written `id`, or a `data-testid`. None of these change when the
20
+ wording or the layout does.
21
+ 2. **What the element is to a user** — its *role* (button, link, heading,
22
+ checkbox) and its *accessible name* (its visible words, or an input's
23
+ `<label>`).
24
+ 3. **Its visible text**, when that's the thing that identifies it.
25
+
26
+ The login form, inspected, has ids and `name`s on the fields and a typed submit
27
+ button:
28
+
29
+ ```html
30
+ <h1>Login</h1>
31
+ <label for="username">Username</label>
32
+ <input id="username" name="username" />
33
+ <label for="password">Password</label>
34
+ <input id="password" name="password" />
35
+ <button id="submit" type="submit">Sign in</button>
36
+ ```
37
+
38
+ ## Start with anchors that don't move
39
+
40
+ Some anchors don't change when the copy changes, because they aren't copy — a
41
+ form field's `name` is a backend contract, an input `type` is semantic, a
42
+ hand-written `id` is put there on purpose. Reach for these first: they read
43
+ clearly, they survive redesigns, and they resolve the same whether the page ships
44
+ in English or German.
45
+
46
+ ```ts
47
+ // Fields by their `name` — a form contract, not UI text
48
+ await browser.find(By.name('username')).fill('alice');
49
+ await browser.find(By.name('password')).fill('secret');
50
+
51
+ // The submit button by its semantic type
52
+ await browser.find('button[type="submit"]').click();
53
+
54
+ // The result by its stable id
55
+ await browser.locator('#welcome').expect().toBeVisible();
56
+ ```
57
+
58
+ Each has a boundary worth knowing: `name` and `type` live mostly on form
59
+ controls, so they won't help you point at a heading or a menu item; and an `id`
60
+ is only trustworthy when it's authored for meaning — framework-generated ids like
61
+ `#mui-4213` churn between builds, so never anchor to those.
62
+
63
+ A word on `data-testid`, since it comes up constantly. It's the most robust
64
+ anchor there is *when it's in the page you test* — invisible to users, untouched
65
+ by redesign or translation. But test ids aren't a browser or "dev mode" feature;
66
+ they're attributes a team chooses to add, and some builds strip them to trim the
67
+ payload. So don't assume they exist: open the real production DOM and look. If
68
+ you own the app, add them and make sure the production build keeps them; if you
69
+ don't, lean on `name`, `type`, and honest semantic HTML — those are always
70
+ shipped.
71
+
72
+ ## Match the way a user sees the page
73
+
74
+ The anchors above are *plumbing* — attributes wired into the markup. Sometimes
75
+ you want the opposite: to find an element the way a person, or a screen reader,
76
+ actually perceives it. That's what `getByRole`, `getByLabel`, and `getByText`
77
+ are for, and they read almost like a spoken description:
78
+
79
+ ```ts
80
+ // "the Login heading"
81
+ await browser.getByRole('heading', { name: 'Login' }).expect().toBeVisible();
82
+
83
+ // "the Username / Password fields", found through their <label>
84
+ await browser.getByLabel('Username').fill('alice');
85
+ await browser.getByLabel('Password').fill('secret');
86
+
87
+ // "the Sign in button", by role + accessible name
88
+ await browser.getByRole('button', { name: 'Sign in' }).click();
89
+
90
+ // "the welcome message", by its visible text
91
+ await browser.getByText('Welcome back, alice!').expect().toBeVisible();
92
+ ```
93
+
94
+ The match is meaningful, not just readable: if your test can find the button by
95
+ its role and name, the page is exposing the semantics users depend on. These
96
+ locators also shrug off markup refactors, since they don't care how many `<div>`s
97
+ wrap the element.
98
+
99
+ Pitfalls worth knowing:
100
+
101
+ - **They rely on correct semantic HTML.** `getByRole('button')` finds a real
102
+ `<button>`, not a `<div onclick>`. That's often a bug worth surfacing — but it
103
+ means these locators expose how honest your markup is.
104
+ - **The name is usually user-facing text**, so it's tied to the current language
105
+ — see the next section.
106
+ - **Name matching is an approximation.** CraftDriver handles the common cases
107
+ (`aria-label`, simple `aria-labelledby`, visible text, form labels, `alt`,
108
+ `title`), but if a role query is fussy, use `getByLabel`, `getByTestId`, or a
109
+ stable attribute instead of guessing.
110
+
111
+ ## Working across languages
112
+
113
+ Here's the catch these accessibility locators carry: the *name* you match on is
114
+ translated text. `getByLabel('Username')` finds nothing on a page that renders
115
+ `Benutzername`. Two honest ways to handle it:
116
+
117
+ - **Anchor on the language-neutral attributes from the top of this page** —
118
+ `name`, `type`, id, test id. The same test then runs in every locale unchanged.
119
+ It's the simplest thing that works, and where most localised suites end up.
120
+ - **Feed the locator from the same translations the app uses** —
121
+ `getByRole('button', { name: t('auth.signIn') })`. You keep the accessibility
122
+ benefits *and* run per-locale. The cost is discipline: the test has to read the
123
+ exact key the UI renders, and you run it once per language — otherwise a
124
+ translation change quietly becomes a false failure.
125
+
126
+ A rule that applies either way: **a bare string is always a CSS selector**. To
127
+ match by text or role you must use a `getBy*` method or a `By.*` locator; there's
128
+ no `text=` string shorthand.
129
+
130
+ ## Notes
131
+
132
+ - Names match the **whole** text by default. Pass `{ exact: false }` to match a
133
+ substring, e.g. `getByText('Welcome back', { exact: false })`.
134
+ - To search *inside* one card or row instead of the whole page, chain from a
135
+ [locator](../selectors.md#locators-lazy--chainable):
136
+ `card.getByRole('button', { name: 'Buy' })`.
137
+
138
+ ## Learn More
139
+
140
+ - [Selectors & Locators](../selectors.md) — the full reference for every strategy
141
+ - [Organize Flows With Page Objects](./page-objects.md)
142
+ - [Assertions](../assertions.md)
@@ -1,70 +1,50 @@
1
1
  # Log In Once And Reuse The Session
2
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.
3
+ Signing in through the UI in every test is slow, and for most tests logging in
4
+ is not the thing being proven. Do it once, save the resulting cookies and
5
+ localStorage, then launch later tests already authenticated with `storageState`.
6
+ Both halves below run against the live
7
+ [login example](https://dtopuzov.github.io/craftdriver/examples/login.html),
8
+ which persists its session in a cookie.
6
9
 
7
10
  ## Generate Auth State
8
11
 
9
- Run this as a setup step before the tests that need an authenticated user.
12
+ Run this once as a setup step. It is one of the few recipes that shows
13
+ `launch`/`quit`, because saving state is a self-contained script, not a test.
10
14
 
11
15
  ```ts
12
- import { mkdir } from 'node:fs/promises';
13
16
  import { Browser } from 'craftdriver';
14
17
 
15
- const authState = '.auth/alice.json';
16
-
17
- await mkdir('.auth', { recursive: true });
18
-
19
18
  const browser = await Browser.launch({ browserName: 'chrome' });
20
19
 
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');
20
+ await browser.navigateTo('https://dtopuzov.github.io/craftdriver/examples/login.html');
21
+ await browser.getByLabel('Username').fill('alice');
22
+ await browser.getByLabel('Password').fill('secret');
23
+ await browser.getByRole('button', { name: 'Sign in' }).click();
24
+ await browser.expect('#welcome').toContainText('Welcome back, alice!');
27
25
 
28
- await browser.saveState(authState);
29
- } finally {
30
- await browser.quit();
31
- }
26
+ await browser.saveState('.auth/alice.json');
27
+ await browser.quit();
32
28
  ```
33
29
 
34
30
  ## Use Auth State In Tests
35
31
 
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;
32
+ Launch with `storageState` and the browser starts already signed in.
42
33
 
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
- });
34
+ ```ts
35
+ const browser = await Browser.launch({
36
+ browserName: 'chrome',
37
+ storageState: '.auth/alice.json',
61
38
  });
39
+
40
+ await browser.navigateTo('https://dtopuzov.github.io/craftdriver/examples/login.html');
41
+ await browser.expect('#welcome').toContainText('Welcome back, alice!');
62
42
  ```
63
43
 
64
44
  ## Notes
65
45
 
66
46
  - Keep generated auth files out of source control if they contain real secrets.
67
- - Regenerate auth state when the app changes login/session behavior.
47
+ - Regenerate auth state when the app changes its login or session behavior.
68
48
  - Use separate files such as `.auth/admin.json` and `.auth/customer.json` for different roles.
69
49
 
70
50
  ## Learn More
@@ -1,54 +1,39 @@
1
1
  # Test A Mobile Flow With API Mocks And Logs
2
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.
3
+ Mobile bugs often need three things set up at once: a real device viewport, a
4
+ predictable backend response, and a check that nothing errored in the console on
5
+ the smaller layout. This recipe combines all three — it launches with a Pixel 7
6
+ preset, mocks the API, and gates on browser errors — against the live
7
+ [network example](https://dtopuzov.github.io/craftdriver/examples/network.html).
8
+ Because mobile emulation is a launch-time capability, this recipe shows the
9
+ `launch` call.
6
10
 
7
11
  ```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
- });
12
+ const browser = await Browser.launch({
13
+ browserName: 'chrome', // mobile emulation is Chrome/Chromium only
14
+ mobileEmulation: 'Pixel 7',
15
+ captureLogs: true,
44
16
  });
17
+
18
+ // ?bidi=true makes the demo page issue real requests for interception.
19
+ await browser.navigateTo('https://dtopuzov.github.io/craftdriver/examples/network.html?bidi=true');
20
+
21
+ await browser.network.mock('**/api/users', {
22
+ status: 200,
23
+ body: { users: [{ id: 1, name: 'Alice', plan: 'Pro' }] },
24
+ });
25
+
26
+ await browser.getByRole('button', { name: 'Fetch Users' }).click();
27
+ await browser.expect('#users-result').toContainText('Alice');
28
+
29
+ browser.logs.assertNoErrors();
45
30
  ```
46
31
 
47
32
  ## Notes
48
33
 
49
34
  - 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.
35
+ - Mock before the action if the page reads data during that step.
36
+ - Keep `captureLogs: true` so `assertNoErrors()` has something to check.
52
37
 
53
38
  ## Learn More
54
39
 
@@ -1,46 +1,33 @@
1
1
  # Mock APIs And Assert Network Traffic
2
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.
3
+ Testing the UI against a real backend makes tests slow and flaky and couples them
4
+ to data you do not control. Mock the response the page needs, then wait for the
5
+ request or response the user action triggers so you can assert both the UI and
6
+ the traffic. This recipe drives the live
7
+ [network example](https://dtopuzov.github.io/craftdriver/examples/network.html).
6
8
 
7
9
  ```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
- }
10
+ // The ?bidi=true query just tells this demo page to make real network
11
+ // requests instead of its built-in stub, so the mock below is what answers.
12
+ await browser.navigateTo('https://dtopuzov.github.io/craftdriver/examples/network.html?bidi=true');
13
+
14
+ await browser.network.mock('**/api/users', {
15
+ status: 200,
16
+ body: { users: [{ id: 1, name: 'Alice', plan: 'Pro' }] },
38
17
  });
18
+
19
+ const [response] = await Promise.all([
20
+ browser.waitForResponse('**/api/users'),
21
+ browser.getByRole('button', { name: 'Fetch Users' }).click(),
22
+ ]);
23
+
24
+ expect(response.status).toBe(200);
25
+ await browser.expect('#users-result').toContainText('Alice');
39
26
  ```
40
27
 
41
28
  ## Keep Mocks Isolated
42
29
 
43
- If you share a browser across tests, clear intercepts in `afterEach()`:
30
+ When a browser is shared across tests, clear intercepts between them:
44
31
 
45
32
  ```ts
46
33
  afterEach(async () => {
@@ -50,7 +37,7 @@ afterEach(async () => {
50
37
 
51
38
  ## Notes
52
39
 
53
- - Register waits before the click that triggers the request.
40
+ - Register the wait before the click that triggers the request (the `Promise.all` above).
54
41
  - Use a glob for simple URL matching and a predicate when method, status, or headers matter.
55
42
  - Mock before navigation if the page fetches data during initial load.
56
43
 
@@ -1,56 +1,45 @@
1
1
  # Test Multi-User Workflows
2
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.
3
+ Chat, approvals, permissions, and admin/customer flows need two signed-in users
4
+ at the same time but sharing one browser leaks cookies between them. A browser
5
+ context is an isolated cookie and storage jar inside one launched browser, so two
6
+ users can act independently without a second browser process. This recipe signs
7
+ two users into the live
8
+ [login example](https://dtopuzov.github.io/craftdriver/examples/login.html) and
9
+ shows they stay isolated.
7
10
 
8
11
  ```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
- });
12
+ const alice = await browser.newContext();
13
+ const bob = await browser.newContext();
14
+
15
+ const alicePage = await alice.newPage({
16
+ url: 'https://dtopuzov.github.io/craftdriver/examples/login.html',
17
+ });
18
+ await alicePage.find('#username').fill('alice');
19
+ await alicePage.find('#password').fill('secret');
20
+ await alicePage.find('#submit').click();
21
+ await alicePage.expect('#welcome').toContainText('Welcome back, alice!');
22
+
23
+ const bobPage = await bob.newPage({
24
+ url: 'https://dtopuzov.github.io/craftdriver/examples/login.html',
46
25
  });
26
+ await bobPage.find('#username').fill('bob');
27
+ await bobPage.find('#password').fill('secret');
28
+ await bobPage.find('#submit').click();
29
+ await bobPage.expect('#welcome').toContainText('Welcome back, bob!');
30
+
31
+ // Alice is unaffected by Bob signing in — cookies are per-context.
32
+ await alicePage.expect('#welcome').toContainText('Welcome back, alice!');
33
+
34
+ await alice.close();
35
+ await bob.close();
47
36
  ```
48
37
 
49
38
  ## Notes
50
39
 
51
40
  - 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.
41
+ - In a real suite, close contexts in `afterEach()` so a failed assertion cannot leak sessions.
42
+ - Combine with saved `storageState` when users should start already signed in.
54
43
 
55
44
  ## Learn More
56
45