craftdriver 1.1.0 → 1.2.0
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 +14 -0
- package/README.md +1 -1
- package/dist/cli/snapshot.js +31 -4
- package/dist/cli/snapshot.js.map +1 -1
- package/dist/lib/bidi/index.d.ts +4 -5
- package/dist/lib/bidi/index.d.ts.map +1 -1
- package/dist/lib/bidi/index.js +18 -8
- package/dist/lib/bidi/index.js.map +1 -1
- package/dist/lib/bidi/logs.d.ts +12 -1
- package/dist/lib/bidi/logs.d.ts.map +1 -1
- package/dist/lib/bidi/logs.js +20 -8
- package/dist/lib/bidi/logs.js.map +1 -1
- package/dist/lib/browser.d.ts +6 -9
- package/dist/lib/browser.d.ts.map +1 -1
- package/dist/lib/browser.js +6 -4
- package/dist/lib/browser.js.map +1 -1
- package/dist/lib/by.d.ts +18 -0
- package/dist/lib/by.d.ts.map +1 -1
- package/dist/lib/by.js +141 -17
- package/dist/lib/by.js.map +1 -1
- package/dist/lib/locator.d.ts +28 -0
- package/dist/lib/locator.d.ts.map +1 -1
- package/dist/lib/locator.js +28 -0
- package/dist/lib/locator.js.map +1 -1
- package/docs/browser-api.md +4 -2
- package/docs/browser-logs.md +8 -23
- package/docs/public/examples/a11y.html +44 -0
- package/docs/public/examples/clock.html +83 -0
- package/docs/public/examples/console-errors.html +344 -0
- package/docs/public/examples/dialogs.html +42 -0
- package/docs/public/examples/download.html +31 -0
- package/docs/public/examples/dynamic.html +131 -0
- package/docs/public/examples/emulate.html +127 -0
- package/docs/public/examples/evaluate.html +28 -0
- package/docs/public/examples/hover-select.html +295 -0
- package/docs/public/examples/iframe-child.html +51 -0
- package/docs/public/examples/iframes.html +35 -0
- package/docs/public/examples/keyboard.html +195 -0
- package/docs/public/examples/locator.html +131 -0
- package/docs/public/examples/login.html +116 -0
- package/docs/public/examples/mobile.html +270 -0
- package/docs/public/examples/mouse.html +505 -0
- package/docs/public/examples/network.html +293 -0
- package/docs/public/examples/popup-target.html +26 -0
- package/docs/public/examples/popup.html +50 -0
- package/docs/public/examples/screenshot.html +99 -0
- package/docs/public/examples/selectors.html +150 -0
- package/docs/public/examples/session.html +514 -0
- package/docs/public/examples/upload.html +36 -0
- package/docs/recipes/accessibility-gate.md +24 -40
- package/docs/recipes/console-error-gate.md +27 -39
- package/docs/recipes/file-upload-download.md +24 -44
- package/docs/recipes/find-elements.md +142 -0
- package/docs/recipes/login-once-reuse-session.md +22 -45
- package/docs/recipes/mobile-flow-with-network-and-logs.md +22 -41
- package/docs/recipes/mock-api-and-assert-network.md +22 -35
- package/docs/recipes/multi-user-contexts.md +32 -43
- package/docs/recipes/page-objects.md +52 -0
- package/docs/recipes/trace-failing-test.md +31 -44
- package/docs/recipes/virtual-clock-time-sensitive-ui.md +24 -40
- package/docs/recipes/vitest-browser-lifecycle.md +15 -15
- package/docs/recipes.md +20 -1
- package/docs/selectors.md +291 -229
- package/docs/tracing.md +2 -2
- package/package.json +5 -1
- package/skills/craftdriver/SKILL.md +3 -2
- package/skills/craftdriver/cheatsheet.md +2 -1
- package/skills/craftdriver/patterns.md +2 -5
|
@@ -1,56 +1,44 @@
|
|
|
1
1
|
# Fail On Console And JavaScript Errors
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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. Logs are captured
|
|
5
|
+
automatically from launch, so you can run the flow and then assert no JavaScript
|
|
6
|
+
errors were reported, breaking the test instead of reaching users. This recipe
|
|
7
|
+
uses the live
|
|
8
|
+
[console errors example](https://dtopuzov.github.io/craftdriver/examples/console-errors.html).
|
|
6
9
|
|
|
7
10
|
```ts
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
});
|
|
11
|
+
const browser = await Browser.launch();
|
|
12
|
+
|
|
13
|
+
await browser.navigateTo('https://dtopuzov.github.io/craftdriver/examples/console-errors.html');
|
|
14
|
+
browser.logs.clearLogs();
|
|
15
|
+
|
|
16
|
+
await browser.click('#btn-console-log'); // your real user flow goes here
|
|
17
|
+
|
|
18
|
+
// Fails the test if the page reported any JavaScript error during the flow.
|
|
19
|
+
browser.logs.assertNoErrors();
|
|
38
20
|
```
|
|
39
21
|
|
|
40
22
|
## Wait For A Known Log
|
|
41
23
|
|
|
42
|
-
When
|
|
24
|
+
When a log line is itself part of the expected behavior, arm the wait before the
|
|
43
25
|
action that emits it:
|
|
44
26
|
|
|
45
27
|
```ts
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
28
|
+
const logged = browser.logs.waitForConsole((message) =>
|
|
29
|
+
message.text.includes('Hello from console.log')
|
|
30
|
+
);
|
|
49
31
|
|
|
50
|
-
await browser.
|
|
51
|
-
await
|
|
32
|
+
await browser.click('#btn-console-log');
|
|
33
|
+
await logged;
|
|
52
34
|
```
|
|
53
35
|
|
|
36
|
+
## Notes
|
|
37
|
+
|
|
38
|
+
- `assertNoErrors()` checks captured JavaScript errors — capture is automatic, nothing to enable.
|
|
39
|
+
- Call `clearLogs()` before the flow so errors from earlier tests don't bleed in.
|
|
40
|
+
- Use `waitForConsole()` / `waitForError()` when a specific message is the thing you're asserting.
|
|
41
|
+
|
|
54
42
|
## Learn More
|
|
55
43
|
|
|
56
44
|
- [Console Logs And JavaScript Errors](../browser-logs.md)
|
|
@@ -1,56 +1,36 @@
|
|
|
1
1
|
# Test File Uploads And Downloads
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
});
|
|
13
|
+
const browser = await Browser.launch({ downloadsDir: '.tmp/downloads' });
|
|
14
|
+
|
|
15
|
+
await browser.navigateTo('https://dtopuzov.github.io/craftdriver/examples/upload.html');
|
|
16
|
+
await browser.find('#file-input').setInputFiles('tests/fixtures/sample.txt');
|
|
17
|
+
await browser.expect('#result').toHaveText('sample.txt');
|
|
18
|
+
|
|
19
|
+
await browser.navigateTo('https://dtopuzov.github.io/craftdriver/examples/download.html');
|
|
20
|
+
const download = await browser.waitForDownload(() => browser.click('#download-btn'));
|
|
21
|
+
|
|
22
|
+
const target = `.tmp/downloads/${download.suggestedFilename}`;
|
|
23
|
+
await download.saveAs(target);
|
|
24
|
+
|
|
25
|
+
expect(existsSync(target)).toBe(true);
|
|
26
|
+
expect(readFileSync(target, 'utf8')).toContain('craftdriver download test');
|
|
47
27
|
```
|
|
48
28
|
|
|
49
29
|
## Notes
|
|
50
30
|
|
|
51
31
|
- Configure `downloadsDir` at launch so files land somewhere predictable.
|
|
52
|
-
- Wrap the click that triggers the download in `waitForDownload()
|
|
53
|
-
-
|
|
32
|
+
- Wrap the click that triggers the download in `waitForDownload()` so the wait is armed first.
|
|
33
|
+
- Call `setInputFiles()` on the actual `<input type="file">` element, not a styled wrapper.
|
|
54
34
|
|
|
55
35
|
## Learn More
|
|
56
36
|
|
|
@@ -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,47 @@
|
|
|
1
1
|
# Log In Once And Reuse The Session
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
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
|
|
18
|
+
const browser = await Browser.launch();
|
|
16
19
|
|
|
17
|
-
await
|
|
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!');
|
|
18
25
|
|
|
19
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
});
|
|
32
|
+
Launch with `storageState` and the browser starts already signed in.
|
|
49
33
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
afterAll(async () => {
|
|
55
|
-
await browser.quit();
|
|
56
|
-
});
|
|
34
|
+
```ts
|
|
35
|
+
const browser = await Browser.launch({ storageState: '.auth/alice.json' });
|
|
57
36
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
});
|
|
61
|
-
});
|
|
37
|
+
await browser.navigateTo('https://dtopuzov.github.io/craftdriver/examples/login.html');
|
|
38
|
+
await browser.expect('#welcome').toContainText('Welcome back, alice!');
|
|
62
39
|
```
|
|
63
40
|
|
|
64
41
|
## Notes
|
|
65
42
|
|
|
66
43
|
- Keep generated auth files out of source control if they contain real secrets.
|
|
67
|
-
- Regenerate auth state when the app changes login
|
|
44
|
+
- Regenerate auth state when the app changes its login or session behavior.
|
|
68
45
|
- Use separate files such as `.auth/admin.json` and `.auth/customer.json` for different roles.
|
|
69
46
|
|
|
70
47
|
## Learn More
|
|
@@ -1,54 +1,35 @@
|
|
|
1
1
|
# Test A Mobile Flow With API Mocks And Logs
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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({ mobileEmulation: 'Pixel 7' });
|
|
13
|
+
|
|
14
|
+
// ?bidi=true makes the demo page issue real requests for interception.
|
|
15
|
+
await browser.navigateTo('https://dtopuzov.github.io/craftdriver/examples/network.html?bidi=true');
|
|
16
|
+
|
|
17
|
+
await browser.network.mock('**/api/users', {
|
|
18
|
+
status: 200,
|
|
19
|
+
body: { users: [{ id: 1, name: 'Alice', plan: 'Pro' }] },
|
|
44
20
|
});
|
|
21
|
+
|
|
22
|
+
await browser.getByRole('button', { name: 'Fetch Users' }).click();
|
|
23
|
+
await browser.expect('#users-result').toContainText('Alice');
|
|
24
|
+
|
|
25
|
+
browser.logs.assertNoErrors();
|
|
45
26
|
```
|
|
46
27
|
|
|
47
28
|
## Notes
|
|
48
29
|
|
|
49
30
|
- Mobile emulation is currently Chrome/Chromium only.
|
|
50
|
-
- Mock before
|
|
51
|
-
-
|
|
31
|
+
- Mock before the action if the page reads data during that step.
|
|
32
|
+
- Console/error capture is automatic, so `assertNoErrors()` always has the full picture.
|
|
52
33
|
|
|
53
34
|
## Learn More
|
|
54
35
|
|
|
@@ -1,46 +1,33 @@
|
|
|
1
1
|
# Mock APIs And Assert Network Traffic
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
-
|
|
53
|
-
- Combine with saved
|
|
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
|
|