testdriverai 7.1.3 → 7.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/test-init.yml +145 -0
- package/agent/lib/commander.js +2 -2
- package/agent/lib/commands.js +3 -3
- package/docs/v7/_drafts/migration.mdx +3 -3
- package/docs/v7/api/act.mdx +1 -1
- package/docs/v7/api/assert.mdx +1 -1
- package/docs/v7/api/assertions.mdx +14 -14
- package/docs/v7/overview/readme.mdx +1 -1
- package/interfaces/cli/commands/init.js +75 -17
- package/manual/test-init-command.js +223 -0
- package/package.json +2 -2
- package/schema.json +5 -5
- package/sdk-log-formatter.js +1 -1
- package/sdk.d.ts +8 -8
- package/sdk.js +5 -5
- package/test/testdriver/hover-text-with-description.test.mjs +0 -3
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
name: Test Init Command
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main, develop ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ main, develop ]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
test-init:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- name: Checkout CLI repository
|
|
16
|
+
uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Setup Node.js
|
|
19
|
+
uses: actions/setup-node@v4
|
|
20
|
+
with:
|
|
21
|
+
node-version: '20'
|
|
22
|
+
cache: 'npm'
|
|
23
|
+
|
|
24
|
+
- name: Install CLI dependencies
|
|
25
|
+
run: npm ci
|
|
26
|
+
|
|
27
|
+
- name: Create test directory
|
|
28
|
+
run: |
|
|
29
|
+
mkdir -p /tmp/test-init-project
|
|
30
|
+
cd /tmp/test-init-project
|
|
31
|
+
|
|
32
|
+
- name: Run init command (skip prompts)
|
|
33
|
+
working-directory: /tmp/test-init-project
|
|
34
|
+
run: |
|
|
35
|
+
# Create .env with API key first to skip the prompt
|
|
36
|
+
echo "TD_API_KEY=${{ secrets.TD_API_KEY }}" > .env
|
|
37
|
+
|
|
38
|
+
# Run init command using the CLI from the repo
|
|
39
|
+
node ${{ github.workspace }}/bin/testdriverai.js init
|
|
40
|
+
env:
|
|
41
|
+
TD_API_KEY: ${{ secrets.TD_API_KEY }}
|
|
42
|
+
|
|
43
|
+
- name: Verify project structure
|
|
44
|
+
working-directory: /tmp/test-init-project
|
|
45
|
+
run: |
|
|
46
|
+
echo "Checking generated files..."
|
|
47
|
+
|
|
48
|
+
# Check for package.json
|
|
49
|
+
if [ ! -f "package.json" ]; then
|
|
50
|
+
echo "❌ package.json not found"
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
53
|
+
echo "✓ package.json exists"
|
|
54
|
+
|
|
55
|
+
# Check for vitest config
|
|
56
|
+
if [ ! -f "vitest.config.js" ]; then
|
|
57
|
+
echo "❌ vitest.config.js not found"
|
|
58
|
+
exit 1
|
|
59
|
+
fi
|
|
60
|
+
echo "✓ vitest.config.js exists"
|
|
61
|
+
|
|
62
|
+
# Check for test file
|
|
63
|
+
if [ ! -f "tests/example.test.js" ]; then
|
|
64
|
+
echo "❌ tests/example.test.js not found"
|
|
65
|
+
exit 1
|
|
66
|
+
fi
|
|
67
|
+
echo "✓ tests/example.test.js exists"
|
|
68
|
+
|
|
69
|
+
# Check for .env file
|
|
70
|
+
if [ ! -f ".env" ]; then
|
|
71
|
+
echo "❌ .env not found"
|
|
72
|
+
exit 1
|
|
73
|
+
fi
|
|
74
|
+
echo "✓ .env exists"
|
|
75
|
+
|
|
76
|
+
# Check for .gitignore
|
|
77
|
+
if [ ! -f ".gitignore" ]; then
|
|
78
|
+
echo "❌ .gitignore not found"
|
|
79
|
+
exit 1
|
|
80
|
+
fi
|
|
81
|
+
echo "✓ .gitignore exists"
|
|
82
|
+
|
|
83
|
+
# Check for GitHub workflow
|
|
84
|
+
if [ ! -f ".github/workflows/testdriver.yml" ]; then
|
|
85
|
+
echo "❌ .github/workflows/testdriver.yml not found"
|
|
86
|
+
exit 1
|
|
87
|
+
fi
|
|
88
|
+
echo "✓ .github/workflows/testdriver.yml exists"
|
|
89
|
+
|
|
90
|
+
- name: Verify vitest config contents
|
|
91
|
+
working-directory: /tmp/test-init-project
|
|
92
|
+
run: |
|
|
93
|
+
echo "Checking vitest.config.js contents..."
|
|
94
|
+
|
|
95
|
+
# Check for TestDriver reporter
|
|
96
|
+
if ! grep -q "TestDriver()" vitest.config.js; then
|
|
97
|
+
echo "❌ TestDriver reporter not found in vitest.config.js"
|
|
98
|
+
cat vitest.config.js
|
|
99
|
+
exit 1
|
|
100
|
+
fi
|
|
101
|
+
echo "✓ TestDriver reporter is configured"
|
|
102
|
+
|
|
103
|
+
# Check for setupFiles
|
|
104
|
+
if ! grep -q "setupFiles.*testdriverai/vitest/setup" vitest.config.js; then
|
|
105
|
+
echo "❌ setupFiles not configured correctly"
|
|
106
|
+
cat vitest.config.js
|
|
107
|
+
exit 1
|
|
108
|
+
fi
|
|
109
|
+
echo "✓ setupFiles is configured"
|
|
110
|
+
|
|
111
|
+
- name: Verify test file contents
|
|
112
|
+
working-directory: /tmp/test-init-project
|
|
113
|
+
run: |
|
|
114
|
+
echo "Checking test file contents..."
|
|
115
|
+
|
|
116
|
+
# Check for .provision usage
|
|
117
|
+
if ! grep -q "\.provision\.chrome" tests/example.test.js; then
|
|
118
|
+
echo "❌ Test does not use .provision.chrome"
|
|
119
|
+
cat tests/example.test.js
|
|
120
|
+
exit 1
|
|
121
|
+
fi
|
|
122
|
+
echo "✓ Test uses .provision.chrome"
|
|
123
|
+
|
|
124
|
+
# Check for TestDriver import
|
|
125
|
+
if ! grep -q "from 'testdriverai/vitest/hooks'" tests/example.test.js; then
|
|
126
|
+
echo "❌ Test does not import from testdriverai/vitest/hooks"
|
|
127
|
+
cat tests/example.test.js
|
|
128
|
+
exit 1
|
|
129
|
+
fi
|
|
130
|
+
echo "✓ Test imports TestDriver from vitest/hooks"
|
|
131
|
+
|
|
132
|
+
- name: Run the generated test
|
|
133
|
+
working-directory: /tmp/test-init-project
|
|
134
|
+
run: npm test
|
|
135
|
+
env:
|
|
136
|
+
TD_API_KEY: ${{ secrets.TD_API_KEY }}
|
|
137
|
+
|
|
138
|
+
- name: Upload test results
|
|
139
|
+
if: always()
|
|
140
|
+
uses: actions/upload-artifact@v4
|
|
141
|
+
with:
|
|
142
|
+
name: test-init-results
|
|
143
|
+
path: /tmp/test-init-project/test-results/
|
|
144
|
+
retention-days: 7
|
|
145
|
+
if-no-files-found: warn
|
package/agent/lib/commander.js
CHANGED
|
@@ -178,9 +178,9 @@ commands:
|
|
|
178
178
|
emitter.emit(events.log.log, generator.jsonToManual(object));
|
|
179
179
|
response = await commands["focus-application"](object.name);
|
|
180
180
|
break;
|
|
181
|
-
case "
|
|
181
|
+
case "extract": {
|
|
182
182
|
emitter.emit(events.log.log, generator.jsonToManual(object));
|
|
183
|
-
let value = await commands["
|
|
183
|
+
let value = await commands["extract"](object.description);
|
|
184
184
|
emitter.emit(events.log.log, value);
|
|
185
185
|
outputsInstance.set(object.output, value);
|
|
186
186
|
break;
|
package/agent/lib/commands.js
CHANGED
|
@@ -1276,11 +1276,11 @@ const createCommands = (
|
|
|
1276
1276
|
return "The application was focused.";
|
|
1277
1277
|
},
|
|
1278
1278
|
/**
|
|
1279
|
-
* Extract
|
|
1279
|
+
* Extract information from the screen using AI
|
|
1280
1280
|
* @param {Object|string} options - Options object or description (for backward compatibility)
|
|
1281
|
-
* @param {string} options.description - What to
|
|
1281
|
+
* @param {string} options.description - What to extract
|
|
1282
1282
|
*/
|
|
1283
|
-
"
|
|
1283
|
+
"extract": async (...args) => {
|
|
1284
1284
|
const rememberStartTime = Date.now();
|
|
1285
1285
|
let description;
|
|
1286
1286
|
|
|
@@ -207,16 +207,16 @@ await testdriver.focusApplication('Google Chrome');
|
|
|
207
207
|
```
|
|
208
208
|
</CodeGroup>
|
|
209
209
|
|
|
210
|
-
###
|
|
210
|
+
### Extract
|
|
211
211
|
|
|
212
212
|
<CodeGroup>
|
|
213
213
|
```yaml YAML
|
|
214
|
-
- command:
|
|
214
|
+
- command: extract
|
|
215
215
|
description: the order number
|
|
216
216
|
```
|
|
217
217
|
|
|
218
218
|
```javascript SDK
|
|
219
|
-
const orderNumber = await testdriver.
|
|
219
|
+
const orderNumber = await testdriver.extract('the order number');
|
|
220
220
|
```
|
|
221
221
|
</CodeGroup>
|
|
222
222
|
|
package/docs/v7/api/act.mdx
CHANGED
|
@@ -202,4 +202,4 @@ describe('E-commerce Flow with AI', () => {
|
|
|
202
202
|
|
|
203
203
|
- [`find()`](/v7/api/find) - Locate specific elements
|
|
204
204
|
- [`assert()`](/v7/api/assert) - Make assertions
|
|
205
|
-
- [`
|
|
205
|
+
- [`extract()`](/v7/api/extract) - Extract information
|
package/docs/v7/api/assert.mdx
CHANGED
|
@@ -280,6 +280,6 @@ describe('Assertions', () => {
|
|
|
280
280
|
|
|
281
281
|
## Related Methods
|
|
282
282
|
|
|
283
|
-
- [`
|
|
283
|
+
- [`extract()`](/v7/api/extract) - Extract information for detailed assertions
|
|
284
284
|
- [`find()`](/v7/api/find) - Locate elements to verify
|
|
285
285
|
- [`ai()`](/v7/api/ai) - Complex AI-driven tasks
|
|
@@ -77,12 +77,12 @@ await waitForAssertion(testdriver, 'results are displayed', 10000);
|
|
|
77
77
|
|
|
78
78
|
## Extracting Information
|
|
79
79
|
|
|
80
|
-
###
|
|
80
|
+
### extract()
|
|
81
81
|
|
|
82
|
-
Extract
|
|
82
|
+
Extract information from the screen using AI.
|
|
83
83
|
|
|
84
84
|
```javascript
|
|
85
|
-
await testdriver.
|
|
85
|
+
await testdriver.extract(description)
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
**Parameters:**
|
|
@@ -94,17 +94,17 @@ await testdriver.remember(description)
|
|
|
94
94
|
|
|
95
95
|
```javascript
|
|
96
96
|
// Extract text from screen
|
|
97
|
-
const password = await testdriver.
|
|
98
|
-
const total = await testdriver.
|
|
99
|
-
const errorMessage = await testdriver.
|
|
97
|
+
const password = await testdriver.extract('the password displayed on screen');
|
|
98
|
+
const total = await testdriver.extract('the order total amount');
|
|
99
|
+
const errorMessage = await testdriver.extract('the error message text');
|
|
100
100
|
|
|
101
101
|
// Extract structured data
|
|
102
|
-
const email = await testdriver.
|
|
103
|
-
const orderId = await testdriver.
|
|
104
|
-
const phoneNumber = await testdriver.
|
|
102
|
+
const email = await testdriver.extract('the email address in the confirmation');
|
|
103
|
+
const orderId = await testdriver.extract('the order ID number');
|
|
104
|
+
const phoneNumber = await testdriver.extract('the phone number');
|
|
105
105
|
|
|
106
106
|
// Use extracted data
|
|
107
|
-
const password = await testdriver.
|
|
107
|
+
const password = await testdriver.extract('the password for standard_user');
|
|
108
108
|
const passwordField = await testdriver.find('password input');
|
|
109
109
|
await passwordField.click();
|
|
110
110
|
await testdriver.type(password);
|
|
@@ -114,8 +114,8 @@ await testdriver.type(password);
|
|
|
114
114
|
|
|
115
115
|
**Dynamic Content:**
|
|
116
116
|
```javascript
|
|
117
|
-
//
|
|
118
|
-
const confirmationCode = await testdriver.
|
|
117
|
+
// Extract generated values
|
|
118
|
+
const confirmationCode = await testdriver.extract('the 6-digit confirmation code');
|
|
119
119
|
console.log('Code:', confirmationCode);
|
|
120
120
|
|
|
121
121
|
// Use it later in the test
|
|
@@ -125,7 +125,7 @@ await testdriver.type(confirmationCode);
|
|
|
125
125
|
**Verification:**
|
|
126
126
|
```javascript
|
|
127
127
|
// Extract and verify
|
|
128
|
-
const displayedTotal = await testdriver.
|
|
128
|
+
const displayedTotal = await testdriver.extract('the cart total');
|
|
129
129
|
const expectedTotal = '$99.99';
|
|
130
130
|
|
|
131
131
|
expect(displayedTotal).toBe(expectedTotal);
|
|
@@ -134,7 +134,7 @@ expect(displayedTotal).toBe(expectedTotal);
|
|
|
134
134
|
**Conditional Logic:**
|
|
135
135
|
```javascript
|
|
136
136
|
// Extract state and make decisions
|
|
137
|
-
const status = await testdriver.
|
|
137
|
+
const status = await testdriver.extract('the order status');
|
|
138
138
|
|
|
139
139
|
if (status.includes('Pending')) {
|
|
140
140
|
// Wait for processing
|
|
@@ -75,7 +75,7 @@ v7/
|
|
|
75
75
|
- Basic assertions
|
|
76
76
|
- Negative assertions (invert parameter)
|
|
77
77
|
- Async assertions
|
|
78
|
-
- `
|
|
78
|
+
- `extract()` - Extract information from screen
|
|
79
79
|
- Testing patterns:
|
|
80
80
|
- Polling assertions
|
|
81
81
|
- Multi-step validation
|
|
@@ -143,7 +143,10 @@ class InitCommand extends BaseCommand {
|
|
|
143
143
|
},
|
|
144
144
|
keywords: ["testdriver", "testing", "e2e"],
|
|
145
145
|
author: "",
|
|
146
|
-
license: "ISC"
|
|
146
|
+
license: "ISC",
|
|
147
|
+
engines: {
|
|
148
|
+
node: ">=20.19.0"
|
|
149
|
+
}
|
|
147
150
|
};
|
|
148
151
|
|
|
149
152
|
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n");
|
|
@@ -159,6 +162,7 @@ class InitCommand extends BaseCommand {
|
|
|
159
162
|
async createVitestExample() {
|
|
160
163
|
const testDir = path.join(process.cwd(), "tests");
|
|
161
164
|
const testFile = path.join(testDir, "example.test.js");
|
|
165
|
+
const loginSnippetFile = path.join(testDir, "login.js");
|
|
162
166
|
const configFile = path.join(process.cwd(), "vitest.config.js");
|
|
163
167
|
|
|
164
168
|
// Create test directory if it doesn't exist
|
|
@@ -167,23 +171,73 @@ class InitCommand extends BaseCommand {
|
|
|
167
171
|
console.log(chalk.gray(` Created directory: ${testDir}`));
|
|
168
172
|
}
|
|
169
173
|
|
|
170
|
-
// Create
|
|
174
|
+
// Create login snippet file
|
|
175
|
+
const loginSnippetContent = `/**
|
|
176
|
+
* Login snippet - reusable login function
|
|
177
|
+
*
|
|
178
|
+
* This demonstrates how to create reusable test snippets that can be
|
|
179
|
+
* imported and used across multiple test files.
|
|
180
|
+
*/
|
|
181
|
+
export async function login(testdriver) {
|
|
182
|
+
|
|
183
|
+
// The password is displayed on screen, have TestDriver extract it
|
|
184
|
+
const password = await testdriver.extract('the password');
|
|
185
|
+
|
|
186
|
+
// Find the username field
|
|
187
|
+
const usernameField = await testdriver.find(
|
|
188
|
+
'Username, label above the username input field on the login form'
|
|
189
|
+
);
|
|
190
|
+
await usernameField.click();
|
|
191
|
+
|
|
192
|
+
// Type username
|
|
193
|
+
await testdriver.type('standard_user');
|
|
194
|
+
|
|
195
|
+
// Enter password form earlier
|
|
196
|
+
// Marked as secret so it's not logged or stored
|
|
197
|
+
await testdriver.pressKeys(['tab']);
|
|
198
|
+
await testdriver.type(password, { secret: true });
|
|
199
|
+
|
|
200
|
+
// Submit the form
|
|
201
|
+
await testdriver.find('submit button on the login form').click();
|
|
202
|
+
}
|
|
203
|
+
`;
|
|
204
|
+
|
|
205
|
+
fs.writeFileSync(loginSnippetFile, loginSnippetContent);
|
|
206
|
+
console.log(chalk.green(` Created login snippet: ${loginSnippetFile}`));
|
|
207
|
+
|
|
208
|
+
// Create example Vitest test that uses the login snippet
|
|
171
209
|
const vitestContent = `import { test, expect } from 'vitest';
|
|
172
|
-
import {
|
|
210
|
+
import { TestDriver } from 'testdriverai/vitest/hooks';
|
|
211
|
+
import { login } from './login.js';
|
|
212
|
+
|
|
213
|
+
test('should login and add item to cart', async (context) => {
|
|
214
|
+
|
|
215
|
+
// Create TestDriver instance - automatically connects to sandbox
|
|
216
|
+
const testdriver = TestDriver(context);
|
|
217
|
+
|
|
218
|
+
// Launch chrome and navigate to demo app
|
|
219
|
+
await testdriver.provision.chrome({ url: 'http://testdriver-sandbox.vercel.app/login' });
|
|
220
|
+
|
|
221
|
+
// Use the login snippet to handle authentication
|
|
222
|
+
// This demonstrates how to reuse test logic across multiple tests
|
|
223
|
+
await login(testdriver);
|
|
173
224
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
});
|
|
225
|
+
// Add item to cart
|
|
226
|
+
const addToCartButton = await testdriver.find(
|
|
227
|
+
'add to cart button under TestDriver Hat'
|
|
228
|
+
);
|
|
229
|
+
await addToCartButton.click();
|
|
180
230
|
|
|
181
|
-
//
|
|
182
|
-
const
|
|
183
|
-
|
|
231
|
+
// Open cart
|
|
232
|
+
const cartButton = await testdriver.find(
|
|
233
|
+
'cart button in the top right corner'
|
|
234
|
+
);
|
|
235
|
+
await cartButton.click();
|
|
184
236
|
|
|
185
|
-
|
|
186
|
-
|
|
237
|
+
// Verify item in cart
|
|
238
|
+
const result = await testdriver.assert('TestDriver Hat is in the cart');
|
|
239
|
+
expect(result).toBeTruthy();
|
|
240
|
+
|
|
187
241
|
});
|
|
188
242
|
`;
|
|
189
243
|
|
|
@@ -194,16 +248,20 @@ test('should navigate to example.com and find elements', async (context) => {
|
|
|
194
248
|
if (!fs.existsSync(configFile)) {
|
|
195
249
|
const configContent = `import { defineConfig } from 'vitest/config';
|
|
196
250
|
import TestDriver from 'testdriverai/vitest';
|
|
197
|
-
import
|
|
251
|
+
import { config } from 'dotenv';
|
|
198
252
|
|
|
199
253
|
// Load environment variables from .env file
|
|
200
|
-
|
|
254
|
+
config();
|
|
201
255
|
|
|
202
256
|
export default defineConfig({
|
|
203
|
-
plugins: [TestDriver()],
|
|
204
257
|
test: {
|
|
205
258
|
testTimeout: 300000,
|
|
206
259
|
hookTimeout: 300000,
|
|
260
|
+
reporters: [
|
|
261
|
+
'default',
|
|
262
|
+
TestDriver(),
|
|
263
|
+
],
|
|
264
|
+
setupFiles: ['testdriverai/vitest/setup'],
|
|
207
265
|
},
|
|
208
266
|
});
|
|
209
267
|
`;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Test Init Command
|
|
5
|
+
*
|
|
6
|
+
* This script tests the `testdriverai init` command by:
|
|
7
|
+
* 1. Creating a temporary test project
|
|
8
|
+
* 2. Running the init command
|
|
9
|
+
* 3. Verifying all files were created correctly
|
|
10
|
+
* 4. Running the generated test
|
|
11
|
+
* 5. Cleaning up
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* node manual/test-init-command.js
|
|
15
|
+
*
|
|
16
|
+
* Requirements:
|
|
17
|
+
* - TD_API_KEY environment variable must be set
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const { execSync } = require('child_process');
|
|
23
|
+
const os = require('os');
|
|
24
|
+
|
|
25
|
+
// Colors for terminal output
|
|
26
|
+
const colors = {
|
|
27
|
+
reset: '\x1b[0m',
|
|
28
|
+
green: '\x1b[32m',
|
|
29
|
+
red: '\x1b[31m',
|
|
30
|
+
cyan: '\x1b[36m',
|
|
31
|
+
yellow: '\x1b[33m',
|
|
32
|
+
gray: '\x1b[90m',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function log(message, color = 'reset') {
|
|
36
|
+
console.log(`${colors[color]}${message}${colors.reset}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function success(message) {
|
|
40
|
+
log(`✓ ${message}`, 'green');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function error(message) {
|
|
44
|
+
log(`✗ ${message}`, 'red');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function info(message) {
|
|
48
|
+
log(message, 'cyan');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function step(message) {
|
|
52
|
+
log(`\n${message}`, 'cyan');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Main test function
|
|
56
|
+
async function testInitCommand() {
|
|
57
|
+
// Check for API key
|
|
58
|
+
if (!process.env.TD_API_KEY) {
|
|
59
|
+
error('TD_API_KEY environment variable is required');
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const testDir = path.join(os.tmpdir(), `test-init-${Date.now()}`);
|
|
64
|
+
const cliPath = path.join(__dirname, '..');
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
step('📦 Creating test directory...');
|
|
68
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
69
|
+
success(`Created: ${testDir}`);
|
|
70
|
+
|
|
71
|
+
step('🔧 Setting up .env file...');
|
|
72
|
+
const envContent = `TD_API_KEY=${process.env.TD_API_KEY}\n`;
|
|
73
|
+
fs.writeFileSync(path.join(testDir, '.env'), envContent);
|
|
74
|
+
success('Created .env with API key');
|
|
75
|
+
|
|
76
|
+
step('🚀 Running init command...');
|
|
77
|
+
try {
|
|
78
|
+
execSync(`node ${path.join(cliPath, 'bin/testdriverai.js')} init`, {
|
|
79
|
+
cwd: testDir,
|
|
80
|
+
stdio: 'inherit',
|
|
81
|
+
env: { ...process.env, TD_API_KEY: process.env.TD_API_KEY }
|
|
82
|
+
});
|
|
83
|
+
success('Init command completed');
|
|
84
|
+
} catch (err) {
|
|
85
|
+
error('Init command failed');
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
step('🔍 Verifying project structure...');
|
|
90
|
+
|
|
91
|
+
const expectedFiles = [
|
|
92
|
+
'package.json',
|
|
93
|
+
'vitest.config.js',
|
|
94
|
+
'tests/example.test.js',
|
|
95
|
+
'tests/login.js',
|
|
96
|
+
'.env',
|
|
97
|
+
'.gitignore',
|
|
98
|
+
'.github/workflows/testdriver.yml'
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
for (const file of expectedFiles) {
|
|
102
|
+
const filePath = path.join(testDir, file);
|
|
103
|
+
if (!fs.existsSync(filePath)) {
|
|
104
|
+
error(`Missing file: ${file}`);
|
|
105
|
+
throw new Error(`Expected file not found: ${file}`);
|
|
106
|
+
}
|
|
107
|
+
success(`Found: ${file}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
step('📝 Verifying vitest.config.js contents...');
|
|
111
|
+
const vitestConfig = fs.readFileSync(path.join(testDir, 'vitest.config.js'), 'utf8');
|
|
112
|
+
|
|
113
|
+
if (!vitestConfig.includes('TestDriver()')) {
|
|
114
|
+
error('TestDriver reporter not found in vitest.config.js');
|
|
115
|
+
console.log(vitestConfig);
|
|
116
|
+
throw new Error('TestDriver reporter not configured');
|
|
117
|
+
}
|
|
118
|
+
success('TestDriver reporter is configured');
|
|
119
|
+
|
|
120
|
+
if (!vitestConfig.includes('setupFiles') || !vitestConfig.includes('testdriverai/vitest/setup')) {
|
|
121
|
+
error('setupFiles not configured correctly');
|
|
122
|
+
console.log(vitestConfig);
|
|
123
|
+
throw new Error('setupFiles not configured');
|
|
124
|
+
}
|
|
125
|
+
success('setupFiles is configured correctly');
|
|
126
|
+
|
|
127
|
+
if (!vitestConfig.includes('reporters')) {
|
|
128
|
+
error('reporters array not found');
|
|
129
|
+
console.log(vitestConfig);
|
|
130
|
+
throw new Error('reporters not configured');
|
|
131
|
+
}
|
|
132
|
+
success('reporters array includes TestDriver');
|
|
133
|
+
|
|
134
|
+
step('📝 Verifying example test contents...');
|
|
135
|
+
const testFile = fs.readFileSync(path.join(testDir, 'tests/example.test.js'), 'utf8');
|
|
136
|
+
|
|
137
|
+
if (!testFile.includes('.provision.chrome')) {
|
|
138
|
+
error('Test does not use .provision.chrome');
|
|
139
|
+
console.log(testFile);
|
|
140
|
+
throw new Error('Test does not use .provision pattern');
|
|
141
|
+
}
|
|
142
|
+
success('Test uses .provision.chrome');
|
|
143
|
+
|
|
144
|
+
if (!testFile.includes("from 'testdriverai/vitest/hooks'")) {
|
|
145
|
+
error('Test does not import from testdriverai/vitest/hooks');
|
|
146
|
+
console.log(testFile);
|
|
147
|
+
throw new Error('Test does not import TestDriver from vitest/hooks');
|
|
148
|
+
}
|
|
149
|
+
success('Test imports TestDriver from vitest/hooks');
|
|
150
|
+
|
|
151
|
+
if (!testFile.includes("from './login.js'")) {
|
|
152
|
+
error('Test does not import login from ./login.js');
|
|
153
|
+
console.log(testFile);
|
|
154
|
+
throw new Error('Test does not import login snippet');
|
|
155
|
+
}
|
|
156
|
+
success('Test imports login snippet');
|
|
157
|
+
|
|
158
|
+
step('📝 Verifying login snippet contents...');
|
|
159
|
+
const loginFile = fs.readFileSync(path.join(testDir, 'tests/login.js'), 'utf8');
|
|
160
|
+
|
|
161
|
+
if (!loginFile.includes('.extract(')) {
|
|
162
|
+
error('Login snippet does not use .extract()');
|
|
163
|
+
console.log(loginFile);
|
|
164
|
+
throw new Error('Login snippet does not demonstrate .extract()');
|
|
165
|
+
}
|
|
166
|
+
success('Login snippet demonstrates .extract()');
|
|
167
|
+
|
|
168
|
+
if (!loginFile.includes('secret: true')) {
|
|
169
|
+
error('Login snippet does not use secret option');
|
|
170
|
+
console.log(loginFile);
|
|
171
|
+
throw new Error('Login snippet does not demonstrate secret typing');
|
|
172
|
+
}
|
|
173
|
+
success('Login snippet demonstrates secret typing');
|
|
174
|
+
|
|
175
|
+
step('🧪 Running generated test...');
|
|
176
|
+
try {
|
|
177
|
+
execSync('npm test', {
|
|
178
|
+
cwd: testDir,
|
|
179
|
+
stdio: 'inherit',
|
|
180
|
+
env: { ...process.env, TD_API_KEY: process.env.TD_API_KEY }
|
|
181
|
+
});
|
|
182
|
+
success('Test execution completed successfully');
|
|
183
|
+
} catch (err) {
|
|
184
|
+
error('Test execution failed');
|
|
185
|
+
throw err;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
step('✅ All checks passed!');
|
|
189
|
+
log('\nTest summary:', 'green');
|
|
190
|
+
log(' • Init command executed successfully', 'green');
|
|
191
|
+
log(' • All expected files created', 'green');
|
|
192
|
+
log(' • Configuration files are correct', 'green');
|
|
193
|
+
log(' • Example test has proper patterns', 'green');
|
|
194
|
+
log(' • Generated test runs successfully', 'green');
|
|
195
|
+
|
|
196
|
+
} catch (err) {
|
|
197
|
+
step('❌ Test failed!');
|
|
198
|
+
error(err.message);
|
|
199
|
+
if (err.stack) {
|
|
200
|
+
log(err.stack, 'gray');
|
|
201
|
+
}
|
|
202
|
+
process.exit(1);
|
|
203
|
+
} finally {
|
|
204
|
+
// Optional: Clean up test directory
|
|
205
|
+
// Commented out so you can inspect the generated files
|
|
206
|
+
// step('🧹 Cleaning up...');
|
|
207
|
+
// if (fs.existsSync(testDir)) {
|
|
208
|
+
// fs.rmSync(testDir, { recursive: true, force: true });
|
|
209
|
+
// success('Cleaned up test directory');
|
|
210
|
+
// }
|
|
211
|
+
|
|
212
|
+
info(`\nTest project preserved at: ${testDir}`);
|
|
213
|
+
info('To clean up manually, run:');
|
|
214
|
+
log(` rm -rf ${testDir}`, 'gray');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Run the test
|
|
219
|
+
testInitCommand().catch(err => {
|
|
220
|
+
error('Unexpected error:');
|
|
221
|
+
console.error(err);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testdriverai",
|
|
3
|
-
"version": "7.1.
|
|
3
|
+
"version": "7.1.4",
|
|
4
4
|
"description": "Next generation autonomous AI agent for end-to-end testing of web & desktop",
|
|
5
5
|
"main": "sdk.js",
|
|
6
6
|
"exports": {
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
},
|
|
86
86
|
"devDependencies": {
|
|
87
87
|
"@eslint/js": "^9.10.0",
|
|
88
|
-
"@vitest/ui": "^4.0.
|
|
88
|
+
"@vitest/ui": "^4.0.15",
|
|
89
89
|
"chai": "^5.1.2",
|
|
90
90
|
"esbuild": "0.20.2",
|
|
91
91
|
"esbuild-plugin-fileloc": "^0.0.6",
|
package/schema.json
CHANGED
|
@@ -618,7 +618,7 @@
|
|
|
618
618
|
}
|
|
619
619
|
}
|
|
620
620
|
},
|
|
621
|
-
"
|
|
621
|
+
"extract": {
|
|
622
622
|
"type": "object",
|
|
623
623
|
"required": [
|
|
624
624
|
"description",
|
|
@@ -628,7 +628,7 @@
|
|
|
628
628
|
"properties": {
|
|
629
629
|
"command": {
|
|
630
630
|
"type": "string",
|
|
631
|
-
"const": "
|
|
631
|
+
"const": "extract"
|
|
632
632
|
},
|
|
633
633
|
"description": {
|
|
634
634
|
"type": "string"
|
|
@@ -783,7 +783,7 @@
|
|
|
783
783
|
"scroll-until-text",
|
|
784
784
|
"scroll-until-image",
|
|
785
785
|
"focus-application",
|
|
786
|
-
"
|
|
786
|
+
"extract",
|
|
787
787
|
"assert",
|
|
788
788
|
"exec",
|
|
789
789
|
"if",
|
|
@@ -971,12 +971,12 @@
|
|
|
971
971
|
"if": {
|
|
972
972
|
"properties": {
|
|
973
973
|
"command": {
|
|
974
|
-
"const": "
|
|
974
|
+
"const": "extract"
|
|
975
975
|
}
|
|
976
976
|
}
|
|
977
977
|
},
|
|
978
978
|
"then": {
|
|
979
|
-
"$ref": "#/$defs/commandsAvailable/
|
|
979
|
+
"$ref": "#/$defs/commandsAvailable/extract"
|
|
980
980
|
}
|
|
981
981
|
},
|
|
982
982
|
{
|
package/sdk-log-formatter.js
CHANGED
package/sdk.d.ts
CHANGED
|
@@ -433,9 +433,9 @@ export interface FocusApplicationOptions {
|
|
|
433
433
|
name: string;
|
|
434
434
|
}
|
|
435
435
|
|
|
436
|
-
/** Options for
|
|
437
|
-
export interface
|
|
438
|
-
/** What to
|
|
436
|
+
/** Options for extract command */
|
|
437
|
+
export interface ExtractOptions {
|
|
438
|
+
/** What to extract */
|
|
439
439
|
description: string;
|
|
440
440
|
}
|
|
441
441
|
|
|
@@ -919,15 +919,15 @@ export default class TestDriverSDK {
|
|
|
919
919
|
assert(assertion: string, options?: object): Promise<boolean>;
|
|
920
920
|
|
|
921
921
|
/**
|
|
922
|
-
* Extract
|
|
922
|
+
* Extract information from the screen using AI
|
|
923
923
|
* @param options - Options object with description
|
|
924
924
|
*/
|
|
925
|
-
|
|
925
|
+
extract(options: { description: string }): Promise<string>;
|
|
926
926
|
/**
|
|
927
|
-
* Extract
|
|
928
|
-
* @param description - What to
|
|
927
|
+
* Extract information from the screen using AI (positional arguments - legacy)
|
|
928
|
+
* @param description - What to extract
|
|
929
929
|
*/
|
|
930
|
-
|
|
930
|
+
extract(description: string): Promise<string>;
|
|
931
931
|
|
|
932
932
|
// Code Execution
|
|
933
933
|
|
package/sdk.js
CHANGED
|
@@ -2101,15 +2101,15 @@ class TestDriverSDK {
|
|
|
2101
2101
|
*/
|
|
2102
2102
|
doc: "Focus an application by name",
|
|
2103
2103
|
},
|
|
2104
|
-
|
|
2105
|
-
name: "
|
|
2104
|
+
extract: {
|
|
2105
|
+
name: "extract",
|
|
2106
2106
|
/**
|
|
2107
|
-
* Extract
|
|
2107
|
+
* Extract information from the screen using AI
|
|
2108
2108
|
* @param {Object|string} options - Options object or description (legacy positional)
|
|
2109
|
-
* @param {string} options.description - What to
|
|
2109
|
+
* @param {string} options.description - What to extract
|
|
2110
2110
|
* @returns {Promise<string>}
|
|
2111
2111
|
*/
|
|
2112
|
-
doc: "Extract
|
|
2112
|
+
doc: "Extract information from the screen",
|
|
2113
2113
|
},
|
|
2114
2114
|
assert: {
|
|
2115
2115
|
name: "assert",
|
|
@@ -17,21 +17,18 @@ describe("Hover Text With Description Test", () => {
|
|
|
17
17
|
await performLogin(testdriver);
|
|
18
18
|
|
|
19
19
|
// Click on "Add to Cart" under TestDriver Hat
|
|
20
|
-
await testdriver.focusApplication("Google Chrome");
|
|
21
20
|
const addToCartButton = await testdriver.find(
|
|
22
21
|
"Add to Cart, add to cart button under TestDriver Hat",
|
|
23
22
|
);
|
|
24
23
|
await addToCartButton.click();
|
|
25
24
|
|
|
26
25
|
// Click on the cart
|
|
27
|
-
await testdriver.focusApplication("Google Chrome");
|
|
28
26
|
const cartButton = await testdriver.find(
|
|
29
27
|
"Cart, cart button in the top right corner",
|
|
30
28
|
);
|
|
31
29
|
await cartButton.click();
|
|
32
30
|
|
|
33
31
|
// Assert the TestDriver Hat is in the cart
|
|
34
|
-
await testdriver.focusApplication("Google Chrome");
|
|
35
32
|
const result = await testdriver.assert("TestDriver Hat is in the cart");
|
|
36
33
|
expect(result).toBeTruthy();
|
|
37
34
|
});
|