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.
- package/.github/workflows/acceptance-linux-scheduled.yaml +1 -1
- package/.github/workflows/acceptance.yaml +38 -1
- package/.github/workflows/windows-self-hosted.yaml +9 -1
- package/CHANGELOG.md +8 -0
- package/docs/_data/examples-manifest.json +105 -0
- package/docs/_data/examples-manifest.schema.json +41 -0
- package/docs/_scripts/extract-example-urls.js +165 -0
- package/docs/_scripts/generate-examples.js +534 -0
- package/docs/docs.json +242 -212
- package/docs/v7/aws-setup.mdx +1 -1
- package/docs/v7/examples/ai.mdx +72 -0
- package/docs/v7/examples/assert.mdx +72 -0
- package/docs/v7/examples/captcha-api.mdx +92 -0
- package/docs/v7/examples/chrome-extension.mdx +132 -0
- package/docs/v7/examples/drag-and-drop.mdx +100 -0
- package/docs/v7/examples/element-not-found.mdx +67 -0
- package/docs/v7/examples/hover-image.mdx +94 -0
- package/docs/v7/examples/hover-text.mdx +69 -0
- package/docs/v7/examples/installer.mdx +91 -0
- package/docs/v7/examples/launch-vscode-linux.mdx +101 -0
- package/docs/v7/examples/match-image.mdx +96 -0
- package/docs/v7/examples/press-keys.mdx +92 -0
- package/docs/v7/examples/scroll-keyboard.mdx +79 -0
- package/docs/v7/examples/scroll-until-image.mdx +81 -0
- package/docs/v7/examples/scroll-until-text.mdx +109 -0
- package/docs/v7/examples/scroll.mdx +81 -0
- package/docs/v7/examples/type.mdx +92 -0
- package/docs/v7/examples/windows-installer.mdx +89 -0
- package/examples/ai.test.mjs +2 -1
- package/examples/assert.test.mjs +2 -2
- package/examples/captcha-api.test.mjs +3 -2
- package/examples/chrome-extension.test.mjs +3 -2
- package/examples/config.mjs +5 -0
- package/examples/drag-and-drop.test.mjs +2 -1
- package/examples/element-not-found.test.mjs +2 -1
- package/examples/exec-output.test.mjs +2 -1
- package/examples/exec-pwsh.test.mjs +2 -1
- package/examples/focus-window.test.mjs +2 -1
- package/examples/formatted-logging.test.mjs +2 -1
- package/examples/hover-image.test.mjs +2 -1
- package/examples/hover-text-with-description.test.mjs +2 -1
- package/examples/hover-text.test.mjs +2 -1
- package/examples/installer.test.mjs +3 -2
- package/examples/launch-vscode-linux.test.mjs +3 -2
- package/examples/match-image.test.mjs +2 -1
- package/examples/no-provision.test.mjs +2 -3
- package/examples/press-keys.test.mjs +7 -13
- package/examples/prompt.test.mjs +2 -1
- package/examples/scroll-keyboard.test.mjs +2 -1
- package/examples/scroll-until-image.test.mjs +2 -1
- package/examples/scroll-until-text.test.mjs +2 -1
- package/examples/scroll.test.mjs +2 -1
- package/examples/type.test.mjs +3 -2
- package/examples/windows-installer.test.mjs +2 -1
- package/interfaces/vitest-plugin.mjs +50 -18
- package/package.json +3 -1
- package/sdk.js +49 -38
- package/vitest.config.mjs +1 -1
- package/docs/v7/examples.mdx +0 -5
- 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
|
+
});
|