testdriverai 7.3.4 → 7.3.6

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 (60) hide show
  1. package/.github/workflows/acceptance-linux-scheduled.yaml +1 -1
  2. package/.github/workflows/acceptance.yaml +38 -1
  3. package/.github/workflows/windows-self-hosted.yaml +9 -1
  4. package/CHANGELOG.md +8 -0
  5. package/docs/_data/examples-manifest.json +105 -0
  6. package/docs/_data/examples-manifest.schema.json +41 -0
  7. package/docs/_scripts/extract-example-urls.js +165 -0
  8. package/docs/_scripts/generate-examples.js +534 -0
  9. package/docs/docs.json +242 -212
  10. package/docs/v7/aws-setup.mdx +1 -1
  11. package/docs/v7/examples/ai.mdx +72 -0
  12. package/docs/v7/examples/assert.mdx +72 -0
  13. package/docs/v7/examples/captcha-api.mdx +92 -0
  14. package/docs/v7/examples/chrome-extension.mdx +132 -0
  15. package/docs/v7/examples/drag-and-drop.mdx +100 -0
  16. package/docs/v7/examples/element-not-found.mdx +67 -0
  17. package/docs/v7/examples/hover-image.mdx +94 -0
  18. package/docs/v7/examples/hover-text.mdx +69 -0
  19. package/docs/v7/examples/installer.mdx +91 -0
  20. package/docs/v7/examples/launch-vscode-linux.mdx +101 -0
  21. package/docs/v7/examples/match-image.mdx +96 -0
  22. package/docs/v7/examples/press-keys.mdx +92 -0
  23. package/docs/v7/examples/scroll-keyboard.mdx +79 -0
  24. package/docs/v7/examples/scroll-until-image.mdx +81 -0
  25. package/docs/v7/examples/scroll-until-text.mdx +109 -0
  26. package/docs/v7/examples/scroll.mdx +81 -0
  27. package/docs/v7/examples/type.mdx +92 -0
  28. package/docs/v7/examples/windows-installer.mdx +89 -0
  29. package/examples/ai.test.mjs +2 -1
  30. package/examples/assert.test.mjs +2 -2
  31. package/examples/captcha-api.test.mjs +3 -2
  32. package/examples/chrome-extension.test.mjs +3 -2
  33. package/examples/config.mjs +5 -0
  34. package/examples/drag-and-drop.test.mjs +2 -1
  35. package/examples/element-not-found.test.mjs +2 -1
  36. package/examples/exec-output.test.mjs +2 -1
  37. package/examples/exec-pwsh.test.mjs +2 -1
  38. package/examples/focus-window.test.mjs +2 -1
  39. package/examples/formatted-logging.test.mjs +2 -1
  40. package/examples/hover-image.test.mjs +2 -1
  41. package/examples/hover-text-with-description.test.mjs +2 -1
  42. package/examples/hover-text.test.mjs +2 -1
  43. package/examples/installer.test.mjs +3 -2
  44. package/examples/launch-vscode-linux.test.mjs +3 -2
  45. package/examples/match-image.test.mjs +2 -1
  46. package/examples/no-provision.test.mjs +2 -3
  47. package/examples/press-keys.test.mjs +7 -13
  48. package/examples/prompt.test.mjs +2 -1
  49. package/examples/scroll-keyboard.test.mjs +2 -1
  50. package/examples/scroll-until-image.test.mjs +2 -1
  51. package/examples/scroll-until-text.test.mjs +2 -1
  52. package/examples/scroll.test.mjs +2 -1
  53. package/examples/type.test.mjs +3 -2
  54. package/examples/windows-installer.test.mjs +2 -1
  55. package/interfaces/vitest-plugin.mjs +50 -18
  56. package/package.json +3 -1
  57. package/sdk.js +49 -38
  58. package/vitest.config.mjs +1 -1
  59. package/docs/v7/examples.mdx +0 -5
  60. package/jsconfig.json +0 -26
@@ -0,0 +1,534 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Generate Example Docs from Test Files
5
+ *
6
+ * Reads example test files and the examples manifest to generate
7
+ * MDX documentation pages with embedded test run iframes.
8
+ *
9
+ * Usage:
10
+ * node generate-examples.js
11
+ * node generate-examples.js --dry-run
12
+ * node generate-examples.js --skip-ai
13
+ */
14
+
15
+ const fs = require("fs");
16
+ const path = require("path");
17
+
18
+ // Paths
19
+ const EXAMPLES_DIR = path.join(__dirname, "../../examples");
20
+ const MANIFEST_PATH = path.join(__dirname, "../_data/examples-manifest.json");
21
+ const OUTPUT_DIR = path.join(__dirname, "../v7/examples");
22
+ const DOCS_JSON_PATH = path.join(__dirname, "../docs.json");
23
+
24
+ // Icon mapping based on test type/content
25
+ const ICON_MAP = {
26
+ ai: "wand-magic-sparkles",
27
+ assert: "check-circle",
28
+ captcha: "shield-check",
29
+ chrome: "chrome",
30
+ drag: "arrows-up-down-left-right",
31
+ exec: "terminal",
32
+ focus: "window-maximize",
33
+ hover: "hand-pointer",
34
+ installer: "download",
35
+ match: "image",
36
+ press: "keyboard",
37
+ scroll: "scroll",
38
+ type: "keyboard",
39
+ prompt: "message",
40
+ element: "crosshairs",
41
+ window: "window-maximize",
42
+ default: "play",
43
+ };
44
+
45
+ // Parse command line arguments
46
+ function parseArgs() {
47
+ const args = process.argv.slice(2);
48
+ return {
49
+ dryRun: args.includes("--dry-run"),
50
+ help: args.includes("--help") || args.includes("-h"),
51
+ verbose: args.includes("--verbose") || args.includes("-v"),
52
+ };
53
+ }
54
+
55
+ // Load manifest
56
+ function loadManifest() {
57
+ try {
58
+ const content = fs.readFileSync(MANIFEST_PATH, "utf-8");
59
+ return JSON.parse(content);
60
+ } catch (error) {
61
+ console.warn("āš ļø No manifest found, generating docs without URLs");
62
+ return { examples: {} };
63
+ }
64
+ }
65
+
66
+ // Load docs.json
67
+ function loadDocsJson() {
68
+ const content = fs.readFileSync(DOCS_JSON_PATH, "utf-8");
69
+ return JSON.parse(content);
70
+ }
71
+
72
+ // Save docs.json
73
+ function saveDocsJson(docsJson) {
74
+ fs.writeFileSync(DOCS_JSON_PATH, JSON.stringify(docsJson, null, 2) + "\n", "utf-8");
75
+ }
76
+
77
+ // Get all example files
78
+ function getExampleFiles() {
79
+ return fs
80
+ .readdirSync(EXAMPLES_DIR)
81
+ .filter((f) => f.endsWith(".test.mjs"))
82
+ .sort();
83
+ }
84
+
85
+ // Parse test file to extract metadata
86
+ function parseTestFile(filePath) {
87
+ const content = fs.readFileSync(filePath, "utf-8");
88
+ const filename = path.basename(filePath);
89
+
90
+ // Extract JSDoc comment at top of file
91
+ const jsdocMatch = content.match(/^\/\*\*\n([\s\S]*?)\n\s*\*\//);
92
+ const jsdoc = jsdocMatch
93
+ ? jsdocMatch[1]
94
+ .split("\n")
95
+ .map((line) => line.replace(/^\s*\*\s?/, "").trim())
96
+ .filter((line) => line && !line.startsWith("@"))
97
+ .join(" ")
98
+ : null;
99
+
100
+ // Extract describe() name
101
+ const describeMatch = content.match(/describe\s*\(\s*["'`]([^"'`]+)["'`]/);
102
+ const describeName = describeMatch ? describeMatch[1] : null;
103
+
104
+ // Extract it() name
105
+ const itMatch = content.match(/it\s*\(\s*["'`]([^"'`]+)["'`]/);
106
+ const itName = itMatch ? itMatch[1] : null;
107
+
108
+ // Get icon based on filename
109
+ const icon = Object.entries(ICON_MAP).find(([key]) =>
110
+ filename.toLowerCase().includes(key)
111
+ )?.[1] || ICON_MAP.default;
112
+
113
+ return {
114
+ filename,
115
+ content,
116
+ jsdoc,
117
+ describeName,
118
+ itName,
119
+ icon,
120
+ };
121
+ }
122
+
123
+ // Generate slug from filename
124
+ function generateSlug(filename) {
125
+ return filename
126
+ .replace(".test.mjs", "")
127
+ .replace(/[^a-z0-9]+/gi, "-")
128
+ .toLowerCase();
129
+ }
130
+
131
+ // Generate human-readable title from filename
132
+ function generateTitle(filename, describeName) {
133
+ if (describeName) {
134
+ return describeName;
135
+ }
136
+ return filename
137
+ .replace(".test.mjs", "")
138
+ .split(/[-_]/)
139
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
140
+ .join(" ");
141
+ }
142
+
143
+ // Generate short sidebar title
144
+ function generateSidebarTitle(filename) {
145
+ return filename
146
+ .replace(".test.mjs", "")
147
+ .split(/[-_]/)
148
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
149
+ .join(" ");
150
+ }
151
+
152
+ // Call OpenAI API to generate description
153
+ async function generateAIDescription(testMeta, options) {
154
+ if (options.skipAi) {
155
+ return generateFallbackDescription(testMeta);
156
+ }
157
+
158
+ const apiKey = process.env.OPENAI_API_KEY;
159
+ if (!apiKey) {
160
+ console.warn("āš ļø OPENAI_API_KEY not set, using fallback descriptions");
161
+ return generateFallbackDescription(testMeta);
162
+ }
163
+
164
+ const prompt = `You are a technical writer for TestDriver.ai documentation. Generate a concise 2-3 paragraph description for this test example.
165
+
166
+ Test file: ${testMeta.filename}
167
+ Test suite name: ${testMeta.describeName || "N/A"}
168
+ Test name: ${testMeta.itName || "N/A"}
169
+ JSDoc: ${testMeta.jsdoc || "N/A"}
170
+
171
+ Test code:
172
+ \`\`\`javascript
173
+ ${testMeta.content}
174
+ \`\`\`
175
+
176
+ Write a description that:
177
+ 1. Explains what this test demonstrates (first paragraph)
178
+ 2. Describes the key TestDriver SDK methods used (second paragraph)
179
+ 3. Mentions any important patterns or best practices shown (optional third paragraph)
180
+
181
+ Keep it professional and focused on helping developers understand the example. Do not include code blocks in your response.`;
182
+
183
+ try {
184
+ const response = await fetch("https://api.openai.com/v1/chat/completions", {
185
+ method: "POST",
186
+ headers: {
187
+ "Content-Type": "application/json",
188
+ Authorization: `Bearer ${apiKey}`,
189
+ },
190
+ body: JSON.stringify({
191
+ model: "gpt-4o-mini",
192
+ messages: [{ role: "user", content: prompt }],
193
+ max_tokens: 500,
194
+ temperature: 0.7,
195
+ }),
196
+ });
197
+
198
+ if (!response.ok) {
199
+ throw new Error(`OpenAI API error: ${response.status}`);
200
+ }
201
+
202
+ const data = await response.json();
203
+ return data.choices[0].message.content.trim();
204
+ } catch (error) {
205
+ console.warn(`āš ļø AI generation failed for ${testMeta.filename}: ${error.message}`);
206
+ return generateFallbackDescription(testMeta);
207
+ }
208
+ }
209
+
210
+ // Generate fallback description when AI is not available
211
+ function generateFallbackDescription(testMeta) {
212
+ const parts = [];
213
+
214
+ if (testMeta.jsdoc) {
215
+ parts.push(testMeta.jsdoc);
216
+ } else if (testMeta.describeName && testMeta.itName) {
217
+ parts.push(
218
+ `This example demonstrates the "${testMeta.describeName}" test suite. Specifically, it shows how to ${testMeta.itName.toLowerCase()}.`
219
+ );
220
+ } else {
221
+ const title = generateTitle(testMeta.filename, testMeta.describeName);
222
+ parts.push(
223
+ `This example demonstrates ${title.toLowerCase()} functionality using TestDriver.ai.`
224
+ );
225
+ }
226
+
227
+ parts.push(
228
+ "\nReview the source code below to understand the implementation details and patterns used."
229
+ );
230
+
231
+ return parts.join("\n");
232
+ }
233
+
234
+ // Generate short description for frontmatter
235
+ function generateShortDescription(testMeta) {
236
+ if (testMeta.jsdoc) {
237
+ // Take first sentence
238
+ const firstSentence = testMeta.jsdoc.split(/\.\s/)[0];
239
+ return firstSentence.endsWith(".") ? firstSentence : firstSentence + ".";
240
+ }
241
+ if (testMeta.itName) {
242
+ return `Example: ${testMeta.itName}`;
243
+ }
244
+ return `TestDriver example for ${generateTitle(testMeta.filename, testMeta.describeName).toLowerCase()}`;
245
+ }
246
+
247
+ // Extract testcase ID from manifest URL
248
+ // URL format: http://localhost:3001/runs/{runId}/{testcaseId}
249
+ function extractTestcaseId(url) {
250
+ if (!url) return null;
251
+ const pathParts = new URL(url).pathname.split('/').filter(Boolean);
252
+ // The testcase ID is the last segment (e.g., /runs/runId/testcaseId)
253
+ return pathParts.length >= 2 ? pathParts[pathParts.length - 1] : null;
254
+ }
255
+
256
+ // Generate replay URL from testcase ID
257
+ function generateReplayUrl(testcaseId) {
258
+ // Use the API replay endpoint which handles the redirect with embed=true
259
+ const apiRoot = process.env.TD_API_ROOT || 'https://testdriver-api.onrender.com';
260
+ return `${apiRoot}/api/v1/testdriver/testcase/${testcaseId}/replay`;
261
+ }
262
+
263
+ // Update existing MDX file by finding the marker comment and replacing the iframe
264
+ function updateExistingMDX(existingContent, filename, testcaseId) {
265
+ const marker = `{/* ${filename} output */}`;
266
+
267
+ if (!existingContent.includes(marker)) {
268
+ return null; // Marker not found, can't update
269
+ }
270
+
271
+ const replayUrl = generateReplayUrl(testcaseId);
272
+
273
+ // Pattern to match the marker followed by the iframe tag
274
+ const pattern = new RegExp(
275
+ `(\\{/\\* ${filename.replace('.', '\\.')} output \\*/\\}\\s*)<iframe[^>]*src="[^"]*"([^]*)/>`,
276
+ 's'
277
+ );
278
+
279
+ const replacement = `$1<iframe \n src="${replayUrl}"$2/>`;
280
+ const updated = existingContent.replace(pattern, replacement);
281
+
282
+ if (updated === existingContent) {
283
+ return null; // No change made
284
+ }
285
+
286
+ return updated;
287
+ }
288
+
289
+ // Generate MDX content
290
+ function generateMDX(testMeta, manifest, description) {
291
+ const slug = generateSlug(testMeta.filename);
292
+ const title = generateTitle(testMeta.filename, testMeta.describeName);
293
+ const sidebarTitle = generateSidebarTitle(testMeta.filename);
294
+ const shortDescription = generateShortDescription(testMeta);
295
+ const manifestEntry = manifest.examples[testMeta.filename];
296
+ const testcaseId = manifestEntry?.url ? extractTestcaseId(manifestEntry.url) : null;
297
+
298
+ let mdx = `---
299
+ title: "${title}"
300
+ sidebarTitle: "${sidebarTitle}"
301
+ description: "${shortDescription.replace(/"/g, '\\"')}"
302
+ icon: "${testMeta.icon}"
303
+ ---
304
+
305
+ ## Overview
306
+
307
+ ${description}
308
+
309
+ `;
310
+
311
+ // Add Live Test Run section if URL exists
312
+ if (testcaseId) {
313
+ const replayUrl = generateReplayUrl(testcaseId);
314
+ mdx += `## Live Test Run
315
+
316
+ Watch this test execute in a real sandbox environment:
317
+
318
+ {/* ${testMeta.filename} output */}
319
+ <iframe
320
+ src="${replayUrl}"
321
+ width="100%"
322
+ height="600"
323
+ style={{ border: "1px solid #333", borderRadius: "8px" }}
324
+ allow="fullscreen"
325
+ />
326
+
327
+ `;
328
+ } else {
329
+ mdx += `## Live Test Run
330
+
331
+ <Note>
332
+ A live test recording will be available after the next CI run.
333
+ </Note>
334
+
335
+ `;
336
+ }
337
+
338
+ // Add Source Code section
339
+ mdx += `## Source Code
340
+
341
+ \`\`\`javascript title="${testMeta.filename}"
342
+ ${testMeta.content.trim()}
343
+ \`\`\`
344
+
345
+ ## Running This Example
346
+
347
+ \`\`\`bash
348
+ # Clone the TestDriver repository
349
+ git clone https://github.com/testdriverai/testdriverai
350
+
351
+ # Install dependencies
352
+ cd testdriverai
353
+ npm install
354
+
355
+ # Run this specific example
356
+ npx vitest run examples/${testMeta.filename}
357
+ \`\`\`
358
+
359
+ <Note>
360
+ Make sure you have \`TD_API_KEY\` set in your environment. Get one at [testdriver.ai](https://testdriver.ai).
361
+ </Note>
362
+ `;
363
+
364
+ return mdx;
365
+ }
366
+
367
+ // Update docs.json navigation
368
+ function updateDocsNavigation(docsJson, examplePages, options) {
369
+ // Find v7 version in navigation
370
+ const v7Version = docsJson.navigation.versions.find((v) => v.version === "v7");
371
+ if (!v7Version) {
372
+ console.error("āŒ Could not find v7 version in docs.json");
373
+ return false;
374
+ }
375
+
376
+ // Find or create Examples group
377
+ let examplesGroup = v7Version.groups.find((g) =>
378
+ g.group === "Examples" ||
379
+ (typeof g === "object" && g.group === "Examples")
380
+ );
381
+
382
+ const examplesPages = examplePages.map((slug) => `/v7/examples/${slug}`);
383
+
384
+ if (examplesGroup) {
385
+ // Update existing group
386
+ examplesGroup.pages = examplesPages;
387
+ if (options.verbose) {
388
+ console.log("šŸ”„ Updated existing Examples group in navigation");
389
+ }
390
+ } else {
391
+ // Create new group after Overview
392
+ const overviewIndex = v7Version.groups.findIndex((g) => g.group === "Overview");
393
+ const newGroup = {
394
+ group: "Examples",
395
+ icon: "code",
396
+ pages: examplesPages,
397
+ };
398
+
399
+ if (overviewIndex !== -1) {
400
+ v7Version.groups.splice(overviewIndex + 1, 0, newGroup);
401
+ } else {
402
+ v7Version.groups.push(newGroup);
403
+ }
404
+
405
+ if (options.verbose) {
406
+ console.log("āž• Added new Examples group to navigation");
407
+ }
408
+ }
409
+
410
+ return true;
411
+ }
412
+
413
+ // Show help
414
+ function showHelp() {
415
+ console.log(`
416
+ Update Example Docs Iframe URLs
417
+
418
+ Usage:
419
+ node generate-examples.js [options]
420
+
421
+ Options:
422
+ --dry-run Preview changes without writing files
423
+ --verbose Show detailed output
424
+ --help, -h Show this help message
425
+
426
+ Environment Variables:
427
+ TD_API_ROOT API root URL (default: https://api.testdriver.ai)
428
+
429
+ Description:
430
+ Reads existing MDX files in docs/v7/examples/ and updates the iframe
431
+ src URLs based on the examples-manifest.json.
432
+
433
+ Files must contain a marker comment like: {/* filename.test.mjs output */}
434
+ The iframe following the marker will have its src updated to the
435
+ API replay endpoint.
436
+ `);
437
+ }
438
+
439
+ // Main function
440
+ async function main() {
441
+ const options = parseArgs();
442
+
443
+ if (options.help) {
444
+ showHelp();
445
+ process.exit(0);
446
+ }
447
+
448
+ console.log("šŸš€ Updating example documentation iframes...\n");
449
+
450
+ if (options.dryRun) {
451
+ console.log("šŸ“‹ DRY RUN - no files will be written\n");
452
+ }
453
+
454
+ // Load manifest
455
+ const manifest = loadManifest();
456
+
457
+ // Get existing MDX files in output directory
458
+ const existingFiles = fs.existsSync(OUTPUT_DIR)
459
+ ? fs.readdirSync(OUTPUT_DIR).filter((f) => f.endsWith(".mdx"))
460
+ : [];
461
+
462
+ console.log(`šŸ“‚ Found ${existingFiles.length} existing MDX files\n`);
463
+
464
+ let updated = 0;
465
+ let skipped = 0;
466
+ let errors = 0;
467
+
468
+ for (const mdxFile of existingFiles) {
469
+ const outputPath = path.join(OUTPUT_DIR, mdxFile);
470
+
471
+ try {
472
+ const existingContent = fs.readFileSync(outputPath, 'utf-8');
473
+
474
+ // Find the marker in the file to get the test filename
475
+ const markerMatch = existingContent.match(/\{\/\* ([^*]+\.test\.mjs) output \*\/\}/);
476
+
477
+ if (!markerMatch) {
478
+ skipped++;
479
+ if (options.verbose) {
480
+ console.log(`ā­ļø ${mdxFile} (no marker)`);
481
+ }
482
+ continue;
483
+ }
484
+
485
+ const testFilename = markerMatch[1];
486
+ const manifestEntry = manifest.examples[testFilename];
487
+ const testcaseId = manifestEntry?.url ? extractTestcaseId(manifestEntry.url) : null;
488
+
489
+ if (!testcaseId) {
490
+ skipped++;
491
+ if (options.verbose) {
492
+ console.log(`ā­ļø ${mdxFile} (no URL in manifest for ${testFilename})`);
493
+ }
494
+ continue;
495
+ }
496
+
497
+ const updatedContent = updateExistingMDX(existingContent, testFilename, testcaseId);
498
+
499
+ if (updatedContent) {
500
+ if (!options.dryRun) {
501
+ fs.writeFileSync(outputPath, updatedContent, 'utf-8');
502
+ }
503
+ updated++;
504
+ console.log(`šŸ”„ ${mdxFile} (updated iframe)`);
505
+ } else {
506
+ skipped++;
507
+ if (options.verbose) {
508
+ console.log(`ā­ļø ${mdxFile} (unchanged)`);
509
+ }
510
+ }
511
+ } catch (error) {
512
+ console.error(`āŒ ${mdxFile}: ${error.message}`);
513
+ errors++;
514
+ }
515
+ }
516
+
517
+ console.log(`\n✨ Complete!`);
518
+ console.log(` Updated: ${updated} docs`);
519
+ console.log(` Skipped: ${skipped} unchanged`);
520
+ if (errors > 0) {
521
+ console.log(` Errors: ${errors}`);
522
+ }
523
+ console.log(`\nšŸ“‚ Output: ${OUTPUT_DIR}`);
524
+
525
+ if (options.dryRun) {
526
+ console.log("\nāš ļø This was a dry run - no files were written");
527
+ }
528
+ }
529
+
530
+ // Run
531
+ main().catch((error) => {
532
+ console.error("āŒ Fatal error:", error.message);
533
+ process.exit(1);
534
+ });