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
@@ -0,0 +1,52 @@
1
+ # Organize Flows With Page Objects
2
+
3
+ As a suite grows, the same selectors and steps get copy-pasted across tests, and
4
+ one markup change breaks them all. A page object puts the selectors and actions
5
+ for a screen behind named methods, so tests read like intent (`loginAs(...)`) and
6
+ a UI change is a one-line fix in a single class. This recipe wraps the live
7
+ [login example](https://dtopuzov.github.io/craftdriver/examples/login.html) in a
8
+ `LoginPage`.
9
+
10
+ ```ts
11
+ import { Browser } from 'craftdriver';
12
+
13
+ class LoginPage {
14
+ constructor(private readonly browser: Browser) {}
15
+
16
+ async open() {
17
+ await this.browser.navigateTo('https://dtopuzov.github.io/craftdriver/examples/login.html');
18
+ }
19
+
20
+ async loginAs(username: string, password: string) {
21
+ await this.browser.getByLabel('Username').fill(username);
22
+ await this.browser.getByLabel('Password').fill(password);
23
+ await this.browser.getByRole('button', { name: 'Sign in' }).click();
24
+ }
25
+
26
+ async expectWelcome(username: string) {
27
+ await this.browser.expect('#welcome').toContainText(`Welcome back, ${username}!`);
28
+ }
29
+ }
30
+ ```
31
+
32
+ A test then reads as a sequence of intentions, with no selectors in sight:
33
+
34
+ ```ts
35
+ const loginPage = new LoginPage(browser);
36
+
37
+ await loginPage.open();
38
+ await loginPage.loginAs('alice', 'secret');
39
+ await loginPage.expectWelcome('alice');
40
+ ```
41
+
42
+ ## Notes
43
+
44
+ - Expose intent (`loginAs`) rather than mechanics (`fill`, `click`) so tests survive markup changes.
45
+ - Keep assertions the test cares about in the test; put reusable expectations (like `expectWelcome`) on the page object.
46
+ - Pass the `browser` in rather than launching inside the page object, so lifecycle stays in your Vitest hooks.
47
+
48
+ ## Learn More
49
+
50
+ - [Use CraftDriver With Vitest Hooks](./vitest-browser-lifecycle.md)
51
+ - [Selectors](../selectors.md)
52
+ - [Assertions](../assertions.md)
@@ -1,60 +1,47 @@
1
1
  # Capture Failure Evidence With Tracing
2
2
 
3
- Use this pattern when a flaky or complex test needs evidence: actions,
4
- navigation, console output, JavaScript errors, network events, and screenshots.
3
+ When a complex or occasionally-flaky test fails, the message alone rarely tells
4
+ you why. A trace records the actions, navigation, console output, JavaScript
5
+ errors, network events, and screenshots so you can replay what happened. Wrap
6
+ the flow in a helper that starts a trace, keeps it on failure, and always stops
7
+ it. This recipe traces a sign-in against the live
8
+ [login example](https://dtopuzov.github.io/craftdriver/examples/login.html).
5
9
 
6
- ```ts
7
- import { afterAll, beforeAll, describe, it } from 'vitest';
8
- import { Browser } from 'craftdriver';
9
-
10
- describe('checkout', () => {
11
- let browser: Browser;
12
-
13
- beforeAll(async () => {
14
- browser = await Browser.launch({ browserName: 'chrome' });
15
- });
16
-
17
- afterAll(async () => {
18
- await browser.quit();
19
- });
10
+ The `try/catch` here is deliberate — it is what keeps the trace directory when
11
+ the flow throws.
20
12
 
21
- async function withTrace(name: string, run: () => Promise<void>) {
22
- const outDir = `./traces/${name}-${Date.now()}`;
23
- await browser.startTrace({ outDir });
24
-
25
- try {
26
- await run();
27
- } catch (error) {
28
- console.error(`Trace kept at ${outDir}`);
29
- throw error;
30
- } finally {
31
- await browser.stopTrace().catch(() => undefined);
32
- }
13
+ ```ts
14
+ async function withTrace(name: string, run: () => Promise<void>) {
15
+ const outDir = `./traces/${name}-${Date.now()}`;
16
+ await browser.startTrace({ outDir });
17
+
18
+ try {
19
+ await run();
20
+ } catch (error) {
21
+ console.error(`Trace kept at ${outDir}`);
22
+ throw error;
23
+ } finally {
24
+ await browser.stopTrace().catch(() => undefined);
33
25
  }
34
-
35
- it('places an order', async () => {
36
- await withTrace('checkout-order', async () => {
37
- await browser.navigateTo('http://localhost:3000/cart');
38
- await browser.getByRole('button', { name: 'Checkout' }).click();
39
- await browser.getByLabel('Card number').fill('4242424242424242');
40
- await browser.getByRole('button', { name: 'Pay' }).click();
41
- await browser.expect('#order-status').toContainText('Confirmed');
42
- });
43
- });
26
+ }
27
+
28
+ await withTrace('login', async () => {
29
+ await browser.navigateTo('https://dtopuzov.github.io/craftdriver/examples/login.html');
30
+ await browser.getByLabel('Username').fill('alice');
31
+ await browser.getByLabel('Password').fill('secret');
32
+ await browser.getByRole('button', { name: 'Sign in' }).click();
33
+ await browser.expect('#welcome').toContainText('Welcome back, alice!');
44
34
  });
45
35
  ```
46
36
 
47
37
  ## Notes
48
38
 
49
- - The trace file is NDJSON, so it remains useful even if the test throws before cleanup.
39
+ - The trace file is NDJSON, so it stays useful even if the test throws before `stopTrace()`.
50
40
  - Screenshots are stored under the trace directory.
51
- - For high-volume suites, turn screenshots off unless the test is under investigation.
41
+ - For high-volume suites, turn screenshots off unless a test is under investigation:
52
42
 
53
43
  ```ts
54
- await browser.startTrace({
55
- outDir: './traces/smoke',
56
- screenshots: 'off',
57
- });
44
+ await browser.startTrace({ outDir: './traces/smoke', screenshots: 'off' });
58
45
  ```
59
46
 
60
47
  ## Learn More
@@ -1,59 +1,43 @@
1
1
  # Test Time-Sensitive UI With The Virtual Clock
2
2
 
3
- Use this pattern for debounced search, trial banners, idle logout, countdowns,
4
- and other UI that normally makes tests sleep.
3
+ Debounced search, trial banners, idle logout, and countdowns all force tests to
4
+ either sleep (slow, flaky) or never test the timing at all. Install a virtual
5
+ clock and you control time directly: advance it to the exact millisecond a timer
6
+ should fire, or freeze it on a fixed date. This recipe drives the live
7
+ [clock example](https://dtopuzov.github.io/craftdriver/examples/clock.html),
8
+ whose search input debounces for 300 ms.
5
9
 
6
10
  ```ts
7
- import { afterAll, afterEach, beforeAll, beforeEach, describe, it } from 'vitest';
8
- import { Browser } from 'craftdriver';
11
+ await browser.clock.install();
12
+ await browser.navigateTo('https://dtopuzov.github.io/craftdriver/examples/clock.html');
9
13
 
10
- describe('debounced search', () => {
11
- let browser: Browser;
14
+ await browser.fill('#search-input', 'lap');
12
15
 
13
- beforeAll(async () => {
14
- browser = await Browser.launch({ browserName: 'chrome' });
15
- });
16
+ await browser.clock.tick(299); // just before the debounce threshold
17
+ await browser.expect('#search-count').toHaveText('0');
16
18
 
17
- beforeEach(async () => {
18
- await browser.clock.install({ time: '2030-01-01T00:00:00Z' });
19
- await browser.navigateTo('http://localhost:3000/search');
20
- });
21
-
22
- afterEach(async () => {
23
- await browser.clock.uninstall();
24
- });
25
-
26
- afterAll(async () => {
27
- await browser.quit();
28
- });
29
-
30
- it('fires search only after the debounce window', async () => {
31
- await browser.network.mock('**/api/search?q=lap', {
32
- status: 200,
33
- body: { results: ['Laptop stand'] },
34
- });
35
-
36
- await browser.getByLabel('Search').fill('lap');
37
-
38
- await browser.clock.tick(299);
39
- await browser.expect('#results').toHaveText('');
40
-
41
- await browser.clock.tick(1);
42
- await browser.expect('#results').toContainText('Laptop stand');
43
- });
44
- });
19
+ await browser.clock.tick(2); // crosses 300ms — the search fires exactly once
20
+ await browser.expect('#search-count').toHaveText('1');
45
21
  ```
46
22
 
47
23
  ## Fixed Dates
48
24
 
49
- For date-dependent UI, freeze the wall clock before navigation:
25
+ For date-dependent UI, freeze the wall clock before navigation so the page
26
+ renders as if it were that moment:
50
27
 
51
28
  ```ts
52
29
  await browser.clock.setFixedTime('2026-06-15T23:59:00Z');
53
- await browser.navigateTo('http://localhost:3000/billing');
54
- await browser.expect('#trial-banner').toContainText('expires today');
30
+ await browser.navigateTo('https://dtopuzov.github.io/craftdriver/examples/clock.html');
31
+
32
+ await browser.expect('#trial-banner').toContainText('Trial expires today');
55
33
  ```
56
34
 
35
+ ## Notes
36
+
37
+ - `install()` fakes timers so `tick()` advances them deterministically; `uninstall()` restores the real clock.
38
+ - `setFixedTime()` freezes `Date.now()` and `new Date()` without touching timers — ideal for date-keyed UI.
39
+ - Install the clock before navigation so the page picks up the fake timers on load.
40
+
57
41
  ## Learn More
58
42
 
59
43
  - [Virtual Clock](../clock.md)
@@ -1,17 +1,19 @@
1
1
  # Use CraftDriver With Vitest Hooks
2
2
 
3
- Use this pattern when a test file should launch one browser, navigate to a clean
4
- page before each test, and fail if the page reports unexpected JavaScript errors.
3
+ Most test files want the same shape: launch one browser, start each test from a
4
+ known page, and fail loudly if the browser logged an error. This recipe is that
5
+ skeleton — one browser per file, a fresh navigation per test, and an error gate
6
+ in `afterEach`. The rest of the recipes on this site build on it, so they can
7
+ show only the flow they are teaching and assume a launched `browser`.
5
8
 
6
- This keeps tests fast without sharing dirty page state.
9
+ It drives the live [login example](https://dtopuzov.github.io/craftdriver/examples/login.html).
7
10
 
8
11
  ```ts
9
12
  import { afterAll, afterEach, beforeAll, beforeEach, describe, it } from 'vitest';
10
13
  import { Browser } from 'craftdriver';
11
14
 
12
- describe('settings page', () => {
15
+ describe('login page', () => {
13
16
  let browser: Browser;
14
- const baseUrl = 'http://localhost:3000';
15
17
 
16
18
  beforeAll(async () => {
17
19
  browser = await Browser.launch({
@@ -23,7 +25,7 @@ describe('settings page', () => {
23
25
  beforeEach(async () => {
24
26
  browser.logs.clearLogs();
25
27
  await browser.network.removeAllIntercepts();
26
- await browser.navigateTo(`${baseUrl}/settings`);
28
+ await browser.navigateTo('https://dtopuzov.github.io/craftdriver/examples/login.html');
27
29
  });
28
30
 
29
31
  afterEach(() => {
@@ -34,10 +36,11 @@ describe('settings page', () => {
34
36
  await browser.quit();
35
37
  });
36
38
 
37
- it('updates the display name', async () => {
38
- await browser.getByLabel('Display name').fill('Alice');
39
- await browser.getByRole('button', { name: 'Save' }).click();
40
- await browser.expect('#toast').toContainText('Saved');
39
+ it('signs the user in', async () => {
40
+ await browser.getByLabel('Username').fill('alice');
41
+ await browser.getByLabel('Password').fill('secret');
42
+ await browser.getByRole('button', { name: 'Sign in' }).click();
43
+ await browser.expect('#welcome').toContainText('Welcome back, alice!');
41
44
  });
42
45
  });
43
46
  ```
@@ -47,7 +50,7 @@ describe('settings page', () => {
47
50
  - Launch in `beforeAll()` when tests in the file can share one browser process.
48
51
  - Navigate in `beforeEach()` so each test starts from a known URL.
49
52
  - Clear network mocks and logs before each test so one test cannot influence the next.
50
- - Use `afterAll()` for `browser.quit()` so local driver and browser processes are cleaned up.
53
+ - Use `afterAll()` for `browser.quit()` so the local driver and browser processes are cleaned up.
51
54
 
52
55
  ## Learn More
53
56
 
package/docs/recipes.md CHANGED
@@ -1,12 +1,30 @@
1
1
  # Recipes
2
2
 
3
+ _Recipes for brewing great tests._ 🍺
4
+
3
5
  Recipes are short, real-world patterns that combine CraftDriver features into
4
6
  common testing workflows. Use this page as the index; each recipe has its own
5
- page so the list can grow without turning into a wall of code.
7
+ page so the list can grow without turning into a wall of code. New here? Start
8
+ with [Find Elements On The Page](./recipes/find-elements.md) — every other recipe
9
+ builds on knowing how to point at the thing you want.
10
+
11
+ Every snippet is verified in CI against a live example page you can open
12
+ yourself, under
13
+ [dtopuzov.github.io/craftdriver/examples](https://dtopuzov.github.io/craftdriver/examples/login.html).
14
+ To stay readable, a snippet shows only the code it is teaching and assumes a
15
+ launched `browser` — unless it shows a `Browser.launch(...)` call itself. See the
16
+ [Vitest Hooks recipe](./recipes/vitest-browser-lifecycle.md) for the surrounding
17
+ setup.
6
18
 
7
19
  For exact signatures, use the linked feature docs and the
8
20
  [API reference](./api-reference.md).
9
21
 
22
+ ## Start Here
23
+
24
+ | Scenario | Use when | Recipe |
25
+ | ------------------ | ---------------------------------------------------------------- | --------------------------------------------------------- |
26
+ | Find elements | You're new and need to point CraftDriver at the element you want. | [Find Elements On The Page](./recipes/find-elements.md) |
27
+
10
28
  ## Test Structure
11
29
 
12
30
  | Scenario | Use when | Recipe |
@@ -14,6 +32,7 @@ For exact signatures, use the linked feature docs and the
14
32
  | Vitest browser lifecycle | You want one browser per test file and a fresh page per test. | [Use CraftDriver With Vitest Hooks](./recipes/vitest-browser-lifecycle.md) |
15
33
  | Login once, reuse session | Login UI is slow or noisy and most tests start signed in. | [Log In Once And Reuse The Session](./recipes/login-once-reuse-session.md) |
16
34
  | Multi-user flows | You need Alice and Bob signed in at the same time without leaking cookies. | [Test Multi-User Workflows](./recipes/multi-user-contexts.md) |
35
+ | Page objects | Selectors and steps are copy-pasted across tests and break together. | [Organize Flows With Page Objects](./recipes/page-objects.md) |
17
36
 
18
37
  ## App Behavior
19
38