@testdriverai/agent 7.11.229-canary → 7.11.231-test

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 (100) hide show
  1. package/docs/_data/examples-manifest.json +16 -16
  2. package/docs/_scripts/generate-examples.js +38 -45
  3. package/docs/agent.mdx +174 -93
  4. package/docs/assert.mdx +73 -269
  5. package/docs/aws-setup.mdx +196 -314
  6. package/docs/bring-your-own-machine.mdx +139 -206
  7. package/docs/cache.mdx +144 -151
  8. package/docs/caching.mdx +99 -89
  9. package/docs/captcha.mdx +126 -111
  10. package/docs/ci-cd.mdx +154 -543
  11. package/docs/click.mdx +74 -229
  12. package/docs/client.mdx +177 -231
  13. package/docs/copilot/auto-healing.mdx +86 -370
  14. package/docs/copilot/running-tests.mdx +121 -930
  15. package/docs/customizing-devices.mdx +74 -251
  16. package/docs/dashcam.mdx +72 -405
  17. package/docs/debugging-with-screenshots.mdx +105 -371
  18. package/docs/docs.json +75 -18
  19. package/docs/double-click.mdx +48 -68
  20. package/docs/errors.mdx +102 -176
  21. package/docs/events.mdx +170 -327
  22. package/docs/examples/{hover-text-with-description.mdx → add-to-cart.mdx} +30 -35
  23. package/docs/examples/assert.mdx +28 -41
  24. package/docs/examples/click-button.mdx +71 -0
  25. package/docs/examples/{hover-image.mdx → click-icon.mdx} +32 -28
  26. package/docs/examples/element-not-found.mdx +27 -26
  27. package/docs/examples/findall-coffee-icons.mdx +28 -32
  28. package/docs/examples/installer.mdx +61 -25
  29. package/docs/examples/launch-vscode-linux.mdx +28 -51
  30. package/docs/examples/navigate-and-scroll.mdx +82 -0
  31. package/docs/examples/parse.mdx +24 -28
  32. package/docs/examples/press-keys.mdx +28 -25
  33. package/docs/examples/scroll.mdx +25 -27
  34. package/docs/examples/type.mdx +26 -21
  35. package/docs/exec.mdx +92 -256
  36. package/docs/extract.mdx +69 -170
  37. package/docs/find.mdx +140 -736
  38. package/docs/focus-application.mdx +69 -246
  39. package/docs/generating-tests.mdx +103 -137
  40. package/docs/guide/best-practices-polling.mdx +65 -141
  41. package/docs/hosted.mdx +80 -144
  42. package/docs/hover.mdx +62 -235
  43. package/docs/locating-elements.mdx +74 -369
  44. package/docs/making-assertions.mdx +116 -74
  45. package/docs/mcp-setup.mdx +80 -63
  46. package/docs/mouse-down.mdx +48 -125
  47. package/docs/mouse-up.mdx +47 -129
  48. package/docs/options.mdx +75 -48
  49. package/docs/parse.mdx +95 -184
  50. package/docs/performing-actions.mdx +104 -94
  51. package/docs/press-keys.mdx +59 -299
  52. package/docs/provision.mdx +137 -182
  53. package/docs/quickstart-claude.mdx +85 -0
  54. package/docs/quickstart-github.mdx +62 -32
  55. package/docs/quickstart-manual.mdx +60 -33
  56. package/docs/quickstart-vscode.mdx +97 -0
  57. package/docs/quickstart.mdx +25 -9
  58. package/docs/redraw.mdx +101 -165
  59. package/docs/reusable-code.mdx +97 -200
  60. package/docs/right-click.mdx +47 -89
  61. package/docs/screenshot.mdx +47 -201
  62. package/docs/screenshots.mdx +87 -137
  63. package/docs/scroll.mdx +71 -154
  64. package/docs/secrets.mdx +96 -91
  65. package/docs/self-hosted.mdx +83 -142
  66. package/docs/snippets/enterprise.mdx +15 -0
  67. package/docs/styles.css +68 -0
  68. package/docs/test-results-json.mdx +167 -204
  69. package/docs/type.mdx +56 -304
  70. package/docs/variables.mdx +121 -81
  71. package/docs/wait.mdx +33 -31
  72. package/docs/what-is-testdriver.mdx +80 -32
  73. package/examples/{hover-text-with-description.test.mjs → add-to-cart.test.mjs} +4 -5
  74. package/examples/assert.test.mjs +3 -20
  75. package/examples/{hover-text.test.mjs → click-button.test.mjs} +3 -4
  76. package/examples/{hover-image.test.mjs → click-icon.test.mjs} +5 -6
  77. package/examples/element-not-found.test.mjs +3 -5
  78. package/examples/findall-coffee-icons.test.mjs +3 -3
  79. package/examples/installer.test.mjs +1 -1
  80. package/examples/launch-vscode-linux.test.mjs +6 -33
  81. package/examples/{scroll-keyboard.test.mjs → navigate-and-scroll.test.mjs} +4 -5
  82. package/examples/parse.test.mjs +1 -2
  83. package/examples/press-keys.test.mjs +4 -4
  84. package/examples/scroll.test.mjs +3 -6
  85. package/examples/type.test.mjs +5 -2
  86. package/lib/provision.js +6 -2
  87. package/package.json +1 -1
  88. package/sdk.d.ts +4 -2
  89. package/sdk.js +1 -1
  90. package/docs/claude-mcp-plugin.mdx +0 -244
  91. package/docs/elements.mdx +0 -606
  92. package/docs/examples/ai.mdx +0 -78
  93. package/docs/examples/captcha-api.mdx +0 -92
  94. package/docs/examples/chrome-extension.mdx +0 -156
  95. package/docs/examples/drag-and-drop.mdx +0 -100
  96. package/docs/examples/formatted-logging.mdx +0 -74
  97. package/docs/examples/hover-text.mdx +0 -70
  98. package/docs/examples/match-image.mdx +0 -97
  99. package/docs/examples/scroll-keyboard.mdx +0 -80
  100. package/docs/examples/scroll-until-text.mdx +0 -109
package/docs/scroll.mdx CHANGED
@@ -1,206 +1,123 @@
1
1
  ---
2
2
  title: "scroll()"
3
3
  sidebarTitle: "scroll"
4
- description: "Scroll pages and elements"
4
+ description: "Scroll the page up or down with the mouse wheel."
5
5
  icon: "arrows-up-down"
6
6
  ---
7
7
 
8
- ## Overview
8
+ Scroll the page up or down with the mouse wheel:
9
9
 
10
- Scroll the page or the active element in any direction with the mouse wheel or the keyboard.
10
+ ```javascript
11
+ await testdriver.scroll('down', { amount: 1000 });
12
+ ```
11
13
 
12
- <Warning>
13
- **Focus Requirements**
14
-
15
- A scroll needs focus on the page or on a frame. If an input field or another interactive element has focus, scroll commands can fail. Before you scroll, put the focus on the page:
16
- - Click a non-interactive area (for example, the page background)
17
- - Push the Escape key to remove the focus from interactive elements
18
- - Click outside of input fields or text areas
19
-
20
- **If the scroll still does not work**, use the Page Down or Page Up keys directly:
21
- ```javascript
22
- await testdriver.pressKeys(['pagedown']); // Scroll down
23
- await testdriver.pressKeys(['pageup']); // Scroll up
24
- ```
25
- </Warning>
14
+ If the page doesn't move, click somewhere on it first to give it focus.
26
15
 
27
- ## Syntax
16
+ ## Signature
28
17
 
29
- ```javascript
30
- await testdriver.scroll(direction, options)
18
+ ```typescript
19
+ testdriver.scroll(direction?: 'up' | 'down', options?: { amount?: number }): Promise<void>
31
20
  ```
32
21
 
33
22
  ## Parameters
34
23
 
35
- <ParamField path="direction" type="string" default="down">
36
- Direction to scroll: `'up'`, `'down'`
24
+ <ParamField path="direction" type="'up' | 'down'" default="down">
25
+ Which way to scroll. `'left'` and `'right'` aren't supported: TestDriver logs an error and doesn't scroll.
37
26
  </ParamField>
38
27
 
39
28
  <ParamField path="options" type="object">
40
29
  <Expandable title="properties">
41
30
  <ParamField path="amount" type="number" default="300">
42
- Amount to scroll in pixels
31
+ How far to scroll. Larger values scroll further.
43
32
  </ParamField>
44
33
  </Expandable>
45
34
  </ParamField>
46
35
 
47
- ## Returns
36
+ ## Errors
48
37
 
49
- `Promise<void>`
38
+ Throws an error if `direction` isn't `'up'`, `'down'`, `'left'` or `'right'`.
50
39
 
51
- ## Examples
40
+ If the screen doesn't change after the scroll, TestDriver logs a warning instead of throwing. This usually means the page didn't have focus.
41
+
42
+ ## Scroll until text appears
52
43
 
53
- ### Basic Scrolling
44
+ `testdriver.scrollUntilText()` scrolls in steps until it finds some text on screen:
54
45
 
55
46
  ```javascript
56
- // Scroll down (default)
57
- await testdriver.scroll();
47
+ await testdriver.scrollUntilText({ text: 'Privacy policy' });
48
+ await testdriver.find('Privacy policy link in the footer').click();
49
+ ```
58
50
 
59
- // Scroll down 5 clicks
60
- await testdriver.scroll('down', { amount: 5 });
51
+ <ParamField path="options" type="object" required>
52
+ <Expandable title="properties">
53
+ <ParamField path="text" type="string" required>
54
+ The text to look for.
55
+ </ParamField>
56
+ <ParamField path="direction" type="'up' | 'down'" default="down">
57
+ Which way to scroll.
58
+ </ParamField>
59
+ <ParamField path="maxDistance" type="number" default="10000">
60
+ How far to scroll in total before giving up. Each step scrolls 500.
61
+ </ParamField>
62
+ <ParamField path="invert" type="boolean" default="false">
63
+ Scroll until the text is no longer on screen.
64
+ </ParamField>
65
+ </Expandable>
66
+ </ParamField>
61
67
 
62
- // Scroll up
63
- await testdriver.scroll('up');
68
+ It throws an error if the text doesn't appear within `maxDistance`. `testdriver.scrollUntilImage({ description })` works the same way for an image you describe.
64
69
 
65
- // Scroll up 2 clicks
66
- await testdriver.scroll('up', { amount: 2 });
67
- ```
70
+ ## Examples
68
71
 
69
- ### Horizontal Scrolling
72
+ ### Focus the page, then scroll
70
73
 
71
74
  ```javascript
72
- // Scroll right
73
- await testdriver.scroll('right', { amount: 3 });
74
-
75
- // Scroll left
76
- await testdriver.scroll('left', { amount: 3 });
75
+ await testdriver.find('the page heading').click();
76
+ await testdriver.scroll('down', { amount: 1000 });
77
77
  ```
78
78
 
79
- ### Scroll Methods
79
+ ### Load more results in an infinite list
80
80
 
81
81
  ```javascript
82
- // Mouse wheel scroll (default)
83
- await testdriver.scroll('down', { amount: 3 });
84
-
85
- // For keyboard-based scrolling, use pressKeys instead
86
- await testdriver.pressKeys(['pagedown']);
82
+ for (let i = 0; i < 3; i++) {
83
+ await testdriver.scroll('down', { amount: 1000 });
84
+ }
85
+ await testdriver.assert('more than 20 results are listed');
87
86
  ```
88
87
 
89
- ## Best Practices
90
-
91
- <Check>
92
- **Make sure that the page has focus before you scroll**
93
-
94
- ```javascript
95
- // After typing in an input, unfocus it first
96
- await testdriver.find('email input').click();
97
- await testdriver.type('user@example.com');
98
-
99
- // Click elsewhere or press Escape before scrolling
100
- await testdriver.pressKeys(['escape']);
101
- // Or click a non-interactive area
102
- // await testdriver.find('page background').click();
103
-
104
- // Now scroll will work properly
105
- await testdriver.scroll('down');
106
-
107
- // If scroll still doesn't work, use Page Down directly
108
- // await testdriver.pressKeys(['pagedown']);
109
- ```
110
- </Check>
111
-
112
- <Check>
113
- **Control the scroll distance with the options object**
114
-
115
- ```javascript
116
- // For web pages, mouse scroll works well
117
- await testdriver.scroll('down', { amount: 3 });
118
-
119
- // For desktop apps or when mouse doesn't work, use keyboard
120
- await testdriver.pressKeys(['pagedown']);
121
- ```
122
- </Check>
123
-
124
- <Warning>
125
- **The keyboard scroll uses Page Down and Page Up**
126
-
127
- A keyboard scroll usually moves one "page" each time. This can be more than the click amount that you set. It is more compatible than a mouse scroll, but it is less precise.
128
- </Warning>
129
-
130
- ## Use Cases
131
-
132
- <AccordionGroup>
133
- <Accordion title="Infinite Scroll">
134
- ```javascript
135
- // Scroll multiple times for infinite scroll
136
- for (let i = 0; i < 5; i++) {
137
- await testdriver.scroll('down', { amount: 5 });
138
- await new Promise(r => setTimeout(r, 1000)); // Wait for load
139
- }
140
- ```
141
- </Accordion>
142
-
143
- <Accordion title="Horizontal Gallery">
144
- ```javascript
145
- // Navigate horizontal carousel
146
- await testdriver.scroll('right', { amount: 3 });
147
- await new Promise(r => setTimeout(r, 500));
148
-
149
- const nextImage = await testdriver.find('next image in carousel');
150
- await nextImage.click();
151
- ```
152
- </Accordion>
153
- </AccordionGroup>
154
-
155
- ## Complete Example
88
+ ### Use the keyboard instead
89
+
90
+ Some apps don't respond to the mouse wheel. Press Page Down with [`pressKeys()`](/press-keys):
156
91
 
157
92
  ```javascript
158
- import { beforeAll, afterAll, describe, it } from 'vitest';
159
- import TestDriver from 'testdriverai';
93
+ await testdriver.pressKeys(['pagedown']);
94
+ ```
160
95
 
161
- describe('Scrolling', () => {
162
- let testdriver;
96
+ ## Complete example
163
97
 
164
- beforeAll(async () => {
165
- client = new TestDriver(process.env.TD_API_KEY);
166
- await testdriver.auth();
167
- await testdriver.connect();
168
- });
98
+ ```javascript tests/scroll.test.mjs
99
+ import { describe, expect, it } from 'vitest';
100
+ import { TestDriver } from 'testdriverai/vitest/hooks';
169
101
 
170
- afterAll(async () => {
171
- await testdriver.disconnect();
172
- });
102
+ describe('Scroll', () => {
103
+ it('scrolls the heading out of view', async (context) => {
104
+ const testdriver = TestDriver(context);
105
+ await testdriver.provision.chrome({ url: 'https://www.webhamster.com/' });
173
106
 
174
- it('should scroll to find elements', async () => {
175
- await testdriver.focusApplication('Google Chrome');
176
-
177
- // Scroll down the page
178
- await testdriver.scroll('down', { amount: 5 });
179
-
180
- // Click footer link
181
- const privacyLink = await testdriver.find('Privacy Policy link');
182
- await privacyLink.click();
183
-
184
- await testdriver.assert('privacy policy page is displayed');
185
- });
107
+ // Click the page so it has focus before scrolling
108
+ await testdriver.find('The Hamster Dance, large heading at the top of the page').click();
109
+ await testdriver.scroll('down', { amount: 1000 });
186
110
 
187
- it('should handle infinite scroll', async () => {
188
- await testdriver.focusApplication('Google Chrome');
189
-
190
- // Scroll multiple times to load content
191
- for (let i = 0; i < 3; i++) {
192
- await testdriver.scroll('down', { amount: 5 });
193
- await new Promise(r => setTimeout(r, 1500)); // Wait for load
194
- }
195
-
196
- // Verify content loaded
197
- await testdriver.assert('more than 10 items are visible');
111
+ const result = await testdriver.assert(
112
+ "the text 'The Hamster Dance' isn't visible in the page content",
113
+ );
114
+ expect(result).toBe(true);
198
115
  });
199
116
  });
200
117
  ```
201
118
 
202
- ## Related Methods
119
+ ## Related
203
120
 
204
- - [`find()`](/find) - Find elements after a scroll
205
- - [`pressKeys()`](/press-keys) - Use the Page Down and Page Up keys
206
- - [`wait()`](/wait) - Wait after a scroll
121
+ - [pressKeys()](/press-keys): scroll with Page Down, Home and End
122
+ - [find()](/find): locate an element after scrolling to it
123
+ - [Perform actions](/performing-actions): every kind of action in one guide
package/docs/secrets.mdx CHANGED
@@ -1,115 +1,120 @@
1
1
  ---
2
- title: "Using Secrets"
3
- description: "Securely manage passwords and sensitive data in your tests"
2
+ title: "Use secrets"
3
+ sidebarTitle: "Using secrets"
4
+ description: "Keep passwords and tokens out of your test code and TestDriver's logs."
4
5
  icon: "key"
5
6
  ---
6
7
 
7
- Protect sensitive information such as passwords, API keys, and tokens in your TestDriver tests.
8
+ Store credentials outside your code, and mark them as secret when you type them.
8
9
 
9
- ## Typing Secrets Securely
10
+ - Read passwords and tokens from environment variables, never from the test file.
11
+ - Type them with `secret: true` so TestDriver doesn't log or store them.
10
12
 
11
- When you type sensitive information such as passwords, use the `secret: true` option. This stops TestDriver from a log or a store of the value:
13
+ ## Type a secret
12
14
 
13
- ```javascript
14
- import { test } from 'vitest';
15
- import { chrome } from 'testdriverai/presets';
15
+ Pass `{ secret: true }` to [`type()`](/type) for any sensitive value. Click the field first, then type:
16
16
 
17
- test('login with secure password', async (context) => {
18
- const { testdriver } = await chrome(context, {
19
- url: 'https://myapp.com/login'
20
- });
17
+ ```javascript tests/login.test.mjs
18
+ import { expect, it } from 'vitest';
19
+ import { TestDriver } from 'testdriverai/vitest/hooks';
21
20
 
22
- await testdriver.find('email input').click();
23
- await testdriver.type(process.env.TD_USERNAME);
24
-
25
- await testdriver.find('password input').click();
26
- // Password is masked in logs and recordings
27
- await testdriver.type(process.env.TD_PASSWORD, { secret: true });
28
-
29
- await testdriver.find('login button').click();
30
- await testdriver.assert('dashboard is visible');
21
+ it('signs in', async (context) => {
22
+ const testdriver = TestDriver(context);
23
+ await testdriver.provision.chrome({ url: `${process.env.APP_URL}/login` });
24
+
25
+ const usernameField = await testdriver.find('Username input field');
26
+ await usernameField.click();
27
+ await testdriver.type(process.env.TEST_USERNAME);
28
+
29
+ const passwordField = await testdriver.find('Password input field');
30
+ await passwordField.click();
31
+ await testdriver.type(process.env.TEST_PASSWORD, { secret: true });
32
+
33
+ const signInButton = await testdriver.find('Sign in button below the password field');
34
+ await signInButton.click();
35
+
36
+ const result = await testdriver.assert('the dashboard is visible');
37
+ expect(result).toBe(true);
31
38
  });
32
39
  ```
33
40
 
34
- <Note>
35
- When you set `secret: true`, the typed text shows as `****` in all logs, recordings, and dashcam output.
36
- </Note>
41
+ ## What `secret: true` hides
37
42
 
38
- ## Storing Secrets in GitHub
43
+ With `secret: true`, TestDriver replaces the text with `****`:
39
44
 
40
- Store sensitive credentials as GitHub repository secrets. Then they are never in your code:
45
+ - In the SDK's console output.
46
+ - In the record of the action that TestDriver stores for the test run.
41
47
 
42
- <Steps>
43
- <Step title="Navigate to Repository Settings">
44
- Go to your GitHub repository → **Settings** → **Secrets and variables** → **Actions**
45
- </Step>
46
- <Step title="Add Repository Secrets">
47
- Click **New repository secret** and add your secrets:
48
- - `TD_API_KEY` - Your TestDriver API key
49
- - `TD_USERNAME` - Test account username
50
- - `TD_PASSWORD` - Test account password
51
- </Step>
52
- <Step title="Use in GitHub Actions">
53
- Reference secrets in your workflow file:
54
- ```yaml .github/workflows/test.yml
55
- - name: Run TestDriver tests
56
- env:
57
- TD_API_KEY: ${{ secrets.TD_API_KEY }}
58
- TD_USERNAME: ${{ secrets.TD_USERNAME }}
59
- TD_PASSWORD: ${{ secrets.TD_PASSWORD }}
60
- run: vitest run
61
- ```
62
- </Step>
63
- </Steps>
48
+ The text is still typed into the sandbox, so `secret: true` can't hide what the screen shows. If the field displays its contents, the value appears in the [Dashcam](/dashcam) recording and in screenshots. Password fields usually mask what you type, but plain text fields don't.
49
+
50
+ <Warning>
51
+ With [`autoScreenshots`](/options#param-auto-screenshots) on, screenshot file names include the first characters of the typed text, even when `secret` is `true`. Leave `autoScreenshots` off in tests that type secrets.
52
+ </Warning>
64
53
 
65
- ## Local Development
54
+ ## Store secrets locally
66
55
 
67
- For local development, store secrets in a `.env` file:
56
+ TestDriver loads a `.env` file in your project root automatically. Put your local credentials there:
68
57
 
69
58
  ```bash .env
70
- TD_API_KEY=your_api_key_here
71
- TD_USERNAME=testuser@example.com
72
- TD_PASSWORD=your_secure_password
59
+ TD_API_KEY=your_api_key
60
+ APP_URL=https://staging.example.com
61
+ TEST_USERNAME=qa@example.com
62
+ TEST_PASSWORD=your_test_password
73
63
  ```
74
64
 
75
- <Warning>
76
- Never commit `.env` files to version control. Add `.env` to your `.gitignore` file.
77
- </Warning>
65
+ Add `.env` to `.gitignore` so it's never committed:
78
66
 
79
- ## Complete Example
80
-
81
- Here's a full login test with proper secrets handling:
82
-
83
- ```javascript tests/login.test.js
84
- import { test, expect } from 'vitest';
85
- import { chrome } from 'testdriverai/presets';
86
-
87
- test('secure login flow', async (context) => {
88
- const { testdriver } = await chrome(context, {
89
- url: process.env.TD_WEBSITE || 'https://staging.myapp.com'
90
- });
91
-
92
- // Enter username (not sensitive)
93
- await testdriver.find('email input').click();
94
- await testdriver.type(process.env.TD_USERNAME);
95
-
96
- // Enter password securely
97
- await testdriver.find('password input').click();
98
- await testdriver.type(process.env.TD_PASSWORD, { secret: true });
99
-
100
- // Submit login
101
- await testdriver.find('login button').click();
102
-
103
- // Verify successful login
104
- const loggedIn = await testdriver.assert('user is logged in');
105
- expect(loggedIn).toBeTruthy();
106
- });
67
+ ```bash .gitignore
68
+ .env
107
69
  ```
108
70
 
109
- <Card title="Secrets Best Practices" icon="shield-check">
110
- - **Always use `secret: true`** when typing passwords, tokens, or sensitive data
111
- - **Use environment variables** to keep secrets out of code
112
- - **Store secrets in your CI provider** (GitHub Actions, GitLab CI, etc.)
113
- - **Never commit secrets** to version control
114
- - **Rotate secrets regularly** to maintain security
115
- </Card>
71
+ ## Store secrets in CI
72
+
73
+ In GitHub Actions, store each credential as a repository secret:
74
+
75
+ <Steps>
76
+ <Step title="Open your repository's secrets">
77
+ In GitHub, go to **Settings** > **Secrets and variables** > **Actions**.
78
+ </Step>
79
+ <Step title="Add each secret">
80
+ Click **New repository secret** and add `TEST_USERNAME` and `TEST_PASSWORD`.
81
+ </Step>
82
+ <Step title="Pass them to the test step">
83
+ Map the secrets to environment variables in your workflow:
84
+
85
+ ```yaml .github/workflows/testdriver.yml
86
+ - name: Run TestDriver tests
87
+ run: npx vitest run
88
+ env:
89
+ APP_URL: https://staging.example.com
90
+ TEST_USERNAME: ${{ secrets.TEST_USERNAME }}
91
+ TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }}
92
+ ```
93
+ </Step>
94
+ </Steps>
95
+
96
+ You don't need to store your TestDriver API key this way. The `testdriverai/action` step authenticates with GitHub OIDC. See [Run in CI/CD](/ci-cd) for the full workflow.
97
+
98
+ ## Checklist
99
+
100
+ - Type passwords, tokens, and other sensitive values with `secret: true`.
101
+ - Read credentials from environment variables, not from the test file.
102
+ - Keep `.env` out of version control.
103
+ - Use test accounts with only the access your tests need.
104
+
105
+ ## Next steps
106
+
107
+ <CardGroup cols={2}>
108
+ <Card title="Use variables and test data" icon="square-root-variable" href="/variables">
109
+ Drive tests with environment variables and fixtures.
110
+ </Card>
111
+ <Card title="type()" icon="keyboard" href="/type">
112
+ Every option for typing text.
113
+ </Card>
114
+ <Card title="Run in CI/CD" icon="code-branch" href="/ci-cd">
115
+ Authenticate and run tests in your pipeline.
116
+ </Card>
117
+ <Card title="Reuse code" icon="recycle" href="/reusable-code">
118
+ Put your login steps in a shared helper.
119
+ </Card>
120
+ </CardGroup>