@ai-qa/workflow 2.0.10 → 2.0.13
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/agents/playwright-test-generator.agent.md +49 -0
- package/.github/agents/playwright-test-healer.agent.md +32 -3
- package/.github/agents/playwright-test-planner.agent.md +26 -0
- package/.github/copilot-instructions.md +44 -2
- package/.opencode/agents/qa-generator.md +16 -0
- package/.opencode/agents/qa-healer.md +18 -0
- package/.opencode/agents/qa-planner.md +17 -0
- package/.opencode/rules.md +66 -2
- package/.qa-context/auth.json +29 -0
- package/.qa-context/heal-history.json +40 -0
- package/.qa-context/pipeline.json +34 -0
- package/.qa-context/selectors.json +64 -0
- package/.qa-context/traceability.json +30 -0
- package/README.md +399 -196
- package/ai-qa-workflow.js +82 -104
- package/install.js +9 -14
- package/package.json +5 -7
- package/prompting_template.md +283 -0
- package/qa-dashboard/app.js +1 -0
- package/qa-dashboard/routes/review.js +114 -0
- package/qa-dashboard/views/layouts/main.ejs +1 -0
- package/qa-dashboard/views/review.ejs +201 -0
- package/router.md +109 -29
- package/scripts/auth-manager.js +186 -0
- package/scripts/context-manager.js +226 -0
- package/scripts/executor.js +18 -7
- package/scripts/generator.js +18 -124
- package/scripts/healer.js +78 -157
- package/scripts/planner.js +18 -136
- package/scripts/reporter.js +21 -1
- package/scripts/utils.js +2 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
|
|
4
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
5
|
+
const CONTEXT_DIR = path.join(ROOT, '.qa-context');
|
|
6
|
+
|
|
7
|
+
function ensureDir(dir) {
|
|
8
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function readJSON(filePath) {
|
|
12
|
+
if (!fs.existsSync(filePath)) return null;
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
15
|
+
} catch { return null; }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function writeJSON(filePath, data) {
|
|
19
|
+
ensureDir(path.dirname(filePath));
|
|
20
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function pipelinePath() { return path.join(CONTEXT_DIR, 'pipeline.json'); }
|
|
24
|
+
function selectorsPath() { return path.join(CONTEXT_DIR, 'selectors.json'); }
|
|
25
|
+
function healHistoryPath() { return path.join(CONTEXT_DIR, 'heal-history.json'); }
|
|
26
|
+
function traceabilityPath() { return path.join(CONTEXT_DIR, 'traceability.json'); }
|
|
27
|
+
|
|
28
|
+
const context = {
|
|
29
|
+
|
|
30
|
+
// ── Pipeline State ──
|
|
31
|
+
|
|
32
|
+
getPipeline() {
|
|
33
|
+
return readJSON(pipelinePath()) || {
|
|
34
|
+
project_path: '',
|
|
35
|
+
phases: { plan: { completed: [], in_progress: null }, generate: { completed: [], in_progress: null }, execute: { completed: [], in_progress: null }, heal: { completed: [], in_progress: null }, report: { completed: [], in_progress: null } },
|
|
36
|
+
current_story: null,
|
|
37
|
+
last_run_id: null,
|
|
38
|
+
updated_at: '',
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
setPipeline(updates) {
|
|
43
|
+
const data = this.getPipeline();
|
|
44
|
+
Object.assign(data, updates);
|
|
45
|
+
data.updated_at = new Date().toISOString();
|
|
46
|
+
writeJSON(pipelinePath(), data);
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
phaseComplete(phase, item) {
|
|
50
|
+
const data = this.getPipeline();
|
|
51
|
+
if (!data.phases[phase]) data.phases[phase] = { completed: [], in_progress: null };
|
|
52
|
+
if (!data.phases[phase].completed.includes(item)) {
|
|
53
|
+
data.phases[phase].completed.push(item);
|
|
54
|
+
}
|
|
55
|
+
data.phases[phase].in_progress = null;
|
|
56
|
+
data.updated_at = new Date().toISOString();
|
|
57
|
+
writeJSON(pipelinePath(), data);
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
phaseInProgress(phase, item) {
|
|
61
|
+
const data = this.getPipeline();
|
|
62
|
+
if (!data.phases[phase]) data.phases[phase] = { completed: [], in_progress: null };
|
|
63
|
+
data.phases[phase].in_progress = item;
|
|
64
|
+
data.updated_at = new Date().toISOString();
|
|
65
|
+
writeJSON(pipelinePath(), data);
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
setCurrentStory(story) {
|
|
69
|
+
this.setPipeline({ current_story: story });
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
setLastRun(runId) {
|
|
73
|
+
this.setPipeline({ last_run_id: runId });
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
// ── Selectors Memory ──
|
|
77
|
+
|
|
78
|
+
getSelectors() {
|
|
79
|
+
return readJSON(selectorsPath()) || [];
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
addSelector(page, element, selectorValue, options = {}) {
|
|
83
|
+
const selectors = this.getSelectors();
|
|
84
|
+
let entry = selectors.find(s => s.page === page && s.element === element);
|
|
85
|
+
if (!entry) {
|
|
86
|
+
entry = { page, element, selectors: [], recommended: selectorValue };
|
|
87
|
+
selectors.push(entry);
|
|
88
|
+
}
|
|
89
|
+
const existing = entry.selectors.find(s => s.value === selectorValue);
|
|
90
|
+
if (existing) {
|
|
91
|
+
existing.last_used = new Date().toISOString();
|
|
92
|
+
if (options.reliability !== undefined) existing.reliability = options.reliability;
|
|
93
|
+
if (options.healed) existing.healed = true;
|
|
94
|
+
if (options.original) existing.original = options.original;
|
|
95
|
+
} else {
|
|
96
|
+
entry.selectors.push({
|
|
97
|
+
value: selectorValue,
|
|
98
|
+
type: options.type || 'css',
|
|
99
|
+
reliability: options.reliability || 0.5,
|
|
100
|
+
healed: options.healed || false,
|
|
101
|
+
original: options.original || null,
|
|
102
|
+
strategy: options.strategy || null,
|
|
103
|
+
healed_at: options.healed_at || null,
|
|
104
|
+
run_id: options.run_id || null,
|
|
105
|
+
first_seen: new Date().toISOString(),
|
|
106
|
+
last_used: new Date().toISOString(),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
entry.selectors.sort((a, b) => b.reliability - a.reliability);
|
|
110
|
+
entry.recommended = entry.selectors[0].value;
|
|
111
|
+
writeJSON(selectorsPath(), selectors);
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
getRecommendedSelector(page, element) {
|
|
115
|
+
const selectors = this.getSelectors();
|
|
116
|
+
const entry = selectors.find(s => s.page === page && s.element === element);
|
|
117
|
+
return entry ? entry.recommended : null;
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
updateSelectorReliability(page, element, selectorValue, succeeded) {
|
|
121
|
+
const selectors = this.getSelectors();
|
|
122
|
+
const entry = selectors.find(s => s.page === page && s.element === element);
|
|
123
|
+
if (!entry) return;
|
|
124
|
+
const sel = entry.selectors.find(s => s.value === selectorValue);
|
|
125
|
+
if (!sel) return;
|
|
126
|
+
sel.reliability = succeeded
|
|
127
|
+
? Math.min(1, sel.reliability + 0.1)
|
|
128
|
+
: Math.max(0, sel.reliability - 0.2);
|
|
129
|
+
sel.last_used = new Date().toISOString();
|
|
130
|
+
entry.selectors.sort((a, b) => b.reliability - a.reliability);
|
|
131
|
+
entry.recommended = entry.selectors[0].value;
|
|
132
|
+
writeJSON(selectorsPath(), selectors);
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
// ── Healing History ──
|
|
136
|
+
|
|
137
|
+
getHealHistory() {
|
|
138
|
+
return readJSON(healHistoryPath()) || [];
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
addHealAttempt(runId, story, test, element, attempts, success) {
|
|
142
|
+
const history = this.getHealHistory();
|
|
143
|
+
history.push({
|
|
144
|
+
run_id: runId,
|
|
145
|
+
story,
|
|
146
|
+
test,
|
|
147
|
+
element,
|
|
148
|
+
original_selector: attempts[0] && attempts[0].original ? attempts[0].original : null,
|
|
149
|
+
failure_reason: attempts[0] && attempts[0].error || null,
|
|
150
|
+
attempts: attempts.map(a => ({
|
|
151
|
+
selector: a.selector,
|
|
152
|
+
strategy: a.strategy || 'retry',
|
|
153
|
+
success: a.success,
|
|
154
|
+
duration_ms: a.duration_ms || 0,
|
|
155
|
+
})),
|
|
156
|
+
success,
|
|
157
|
+
duration_ms: attempts.reduce((s, a) => s + (a.duration_ms || 0), 0),
|
|
158
|
+
timestamp: new Date().toISOString(),
|
|
159
|
+
});
|
|
160
|
+
writeJSON(healHistoryPath(), history);
|
|
161
|
+
return history;
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
getHealHistoryForStory(story) {
|
|
165
|
+
return this.getHealHistory().filter(h => h.story === story);
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
// ── Traceability ──
|
|
169
|
+
|
|
170
|
+
getTraceability() {
|
|
171
|
+
return readJSON(traceabilityPath()) || { stories: {} };
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
addRunId(story, runId, success, testName) {
|
|
175
|
+
const data = this.getTraceability();
|
|
176
|
+
if (!data.stories[story]) {
|
|
177
|
+
data.stories[story] = { plan: null, spec: null, runs: [], latest_heal: null, status: 'pending' };
|
|
178
|
+
}
|
|
179
|
+
data.stories[story].runs.push({ run_id: runId, success, test: testName || null, timestamp: new Date().toISOString() });
|
|
180
|
+
data.stories[story].status = success ? 'stable' : 'unstable';
|
|
181
|
+
writeJSON(traceabilityPath(), data);
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
addPlan(story, planFile) {
|
|
185
|
+
const data = this.getTraceability();
|
|
186
|
+
if (!data.stories[story]) data.stories[story] = { plan: null, spec: null, runs: [], latest_heal: null, status: 'pending' };
|
|
187
|
+
data.stories[story].plan = planFile;
|
|
188
|
+
writeJSON(traceabilityPath(), data);
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
addSpec(story, specFile) {
|
|
192
|
+
const data = this.getTraceability();
|
|
193
|
+
if (!data.stories[story]) data.stories[story] = { plan: null, spec: null, runs: [], latest_heal: null, status: 'pending' };
|
|
194
|
+
data.stories[story].spec = specFile;
|
|
195
|
+
writeJSON(traceabilityPath(), data);
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
setHealed(story, runId) {
|
|
199
|
+
const data = this.getTraceability();
|
|
200
|
+
if (!data.stories[story]) data.stories[story] = { plan: null, spec: null, runs: [], latest_heal: null, status: 'pending' };
|
|
201
|
+
data.stories[story].latest_heal = runId;
|
|
202
|
+
writeJSON(traceabilityPath(), data);
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
// ── Init ──
|
|
206
|
+
|
|
207
|
+
init() {
|
|
208
|
+
ensureDir(CONTEXT_DIR);
|
|
209
|
+
for (const file of ['pipeline.json', 'selectors.json', 'heal-history.json', 'traceability.json']) {
|
|
210
|
+
const fp = path.join(CONTEXT_DIR, file);
|
|
211
|
+
if (!fs.existsSync(fp)) {
|
|
212
|
+
const defaults = {
|
|
213
|
+
'pipeline.json': { project_path: '', phases: { plan: { completed: [], in_progress: null }, generate: { completed: [], in_progress: null }, execute: { completed: [], in_progress: null }, heal: { completed: [], in_progress: null }, report: { completed: [], in_progress: null } }, current_story: null, last_run_id: null, updated_at: '' },
|
|
214
|
+
'selectors.json': [],
|
|
215
|
+
'heal-history.json': [],
|
|
216
|
+
'traceability.json': { stories: {} },
|
|
217
|
+
};
|
|
218
|
+
writeJSON(fp, defaults[file]);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
contextDir: CONTEXT_DIR,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
module.exports = context;
|
package/scripts/executor.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { DIRS, CONFIG, ensureDir, timestamp, log, writeMarkdown } = require('./utils');
|
|
2
|
+
const context = require('./context-manager');
|
|
2
3
|
const path = require('path');
|
|
3
4
|
const fs = require('fs');
|
|
4
5
|
const { execSync, spawn } = require('child_process');
|
|
@@ -10,15 +11,18 @@ function executeTests(testName, options = {}) {
|
|
|
10
11
|
timeout = testCfg.timeout || 120000,
|
|
11
12
|
retries = testCfg.retries || 0,
|
|
12
13
|
workers = testCfg.workers || 1,
|
|
13
|
-
screenshots = 'off',
|
|
14
|
+
//screenshots = 'off',
|
|
14
15
|
} = options;
|
|
15
16
|
|
|
16
17
|
const runId = `run-${timestamp()}`;
|
|
17
18
|
const runDir = path.join(DIRS.testResults, runId);
|
|
18
19
|
ensureDir(runDir);
|
|
19
20
|
|
|
21
|
+
const storyName = testName ? testName.replace(/\.spec\.ts$/, '').split(/[\/\\]/).pop() : null;
|
|
22
|
+
|
|
20
23
|
log('EXECUTOR', `Run ID: ${runId}`);
|
|
21
24
|
log('EXECUTOR', `Test: ${testName || 'all tests'}`);
|
|
25
|
+
if (storyName) log('EXECUTOR', `Story: ${storyName}`);
|
|
22
26
|
|
|
23
27
|
const args = ['playwright', 'test'];
|
|
24
28
|
|
|
@@ -37,12 +41,7 @@ function executeTests(testName, options = {}) {
|
|
|
37
41
|
if (retries > 0) args.push('--retries', retries.toString());
|
|
38
42
|
if (workers) args.push('--workers', workers.toString());
|
|
39
43
|
if (timeout) args.push('--timeout', timeout.toString());
|
|
40
|
-
|
|
41
|
-
args.push('--screenshot', 'off');
|
|
42
|
-
}
|
|
43
|
-
if (screenshots === 'only-on-failure') {
|
|
44
|
-
args.push('--screenshot', 'only-on-failure');
|
|
45
|
-
}
|
|
44
|
+
// Screenshots configured in playwright.config.ts — not a valid CLI arg
|
|
46
45
|
|
|
47
46
|
const outputPath = path.join(runDir, 'execution-output.json');
|
|
48
47
|
const resultPath = path.join(runDir, 'execution-result.json');
|
|
@@ -64,6 +63,7 @@ function executeTests(testName, options = {}) {
|
|
|
64
63
|
|
|
65
64
|
const result = {
|
|
66
65
|
runId,
|
|
66
|
+
story: storyName,
|
|
67
67
|
test: testName || 'all',
|
|
68
68
|
success: true,
|
|
69
69
|
exitCode: 0,
|
|
@@ -75,6 +75,11 @@ function executeTests(testName, options = {}) {
|
|
|
75
75
|
};
|
|
76
76
|
|
|
77
77
|
writeMarkdown(resultPath, JSON.stringify(result, null, 2));
|
|
78
|
+
context.setLastRun(runId);
|
|
79
|
+
if (storyName) {
|
|
80
|
+
context.addRunId(storyName, runId, true, testName);
|
|
81
|
+
context.phaseComplete('execute', storyName);
|
|
82
|
+
}
|
|
78
83
|
log('EXECUTOR', `Tests passed (${result.duration}ms)`);
|
|
79
84
|
|
|
80
85
|
return result;
|
|
@@ -89,6 +94,7 @@ function executeTests(testName, options = {}) {
|
|
|
89
94
|
|
|
90
95
|
const result = {
|
|
91
96
|
runId,
|
|
97
|
+
story: storyName,
|
|
92
98
|
test: testName || 'all',
|
|
93
99
|
success: false,
|
|
94
100
|
exitCode: err.status || 1,
|
|
@@ -100,6 +106,11 @@ function executeTests(testName, options = {}) {
|
|
|
100
106
|
};
|
|
101
107
|
|
|
102
108
|
writeMarkdown(resultPath, JSON.stringify(result, null, 2));
|
|
109
|
+
context.setLastRun(runId);
|
|
110
|
+
if (storyName) {
|
|
111
|
+
context.addRunId(storyName, runId, false, testName);
|
|
112
|
+
context.phaseComplete('execute', storyName);
|
|
113
|
+
}
|
|
103
114
|
log('EXECUTOR', `Tests failed (${result.duration}ms) - ${failedTests.length} failure(s)`);
|
|
104
115
|
|
|
105
116
|
return result;
|
package/scripts/generator.js
CHANGED
|
@@ -1,130 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const spec = generateSpecContent(content, specName, planName);
|
|
20
|
-
|
|
21
|
-
writeMarkdown(specPath, spec);
|
|
22
|
-
log('GENERATOR', `Test spec saved: tests/${specName}.spec.ts`);
|
|
23
|
-
|
|
24
|
-
return { specName, specPath };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function generateSpecContent(planContent, specName, planFileName) {
|
|
28
|
-
const featureMatch = planContent.match(/\*\*Feature\*\*:\s*(.+)/);
|
|
29
|
-
const feature = featureMatch ? featureMatch[1].trim() : specName;
|
|
30
|
-
|
|
31
|
-
const scenarios = extractScenarios(planContent);
|
|
32
|
-
|
|
33
|
-
const browserType = CONFIG.browser.type || 'chromium';
|
|
34
|
-
const cdpPort = CONFIG.browser.cdpPort || 9222;
|
|
35
|
-
const cdpImport = browserType === 'edge' ? "import { startEdgeCDP } from '../cdpSession';" : `import { chromium } from '@playwright/test';`;
|
|
36
|
-
|
|
37
|
-
let setupBlock;
|
|
38
|
-
if (browserType === 'edge') {
|
|
39
|
-
setupBlock = ` // const { page } = await startEdgeCDP();\n // global.page = page;`;
|
|
40
|
-
} else {
|
|
41
|
-
setupBlock = ` // const browser = await chromium.launch({ headless: !process.env.HEADED });\n // const page = await browser.newPage();\n // global.page = page;`;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
let spec = `import { test, expect } from '@playwright/test';
|
|
45
|
-
${cdpImport}
|
|
46
|
-
|
|
47
|
-
test.describe('${feature.replace(/'/g, "\\'")}', () => {
|
|
48
|
-
|
|
49
|
-
test.beforeEach(async () => {
|
|
50
|
-
${setupBlock}
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test.afterEach(async () => {
|
|
54
|
-
// Cleanup if needed
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
`;
|
|
58
|
-
|
|
59
|
-
scenarios.forEach((scenario, idx) => {
|
|
60
|
-
const safeTitle = scenario.title.replace(/['"]/g, "'");
|
|
61
|
-
spec += ` test('${safeTitle}', async () => {\n`;
|
|
62
|
-
spec += ` // TODO: Implement test steps from plan\n`;
|
|
63
|
-
spec += ` // Reference: specs/${planFileName} (Scenario ${idx + 3}.1)\n`;
|
|
64
|
-
spec += ` // \n`;
|
|
65
|
-
|
|
66
|
-
scenario.steps.forEach((step, si) => {
|
|
67
|
-
const truncated = step.length > 100 ? step.substring(0, 97) + '...' : step;
|
|
68
|
-
spec += ` // Step ${si + 1}: ${truncated}\n`;
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
spec += ` // \n`;
|
|
72
|
-
spec += ` // Expected: ${scenario.expected || 'Workflow completes successfully'}\n`;
|
|
73
|
-
spec += ` \n`;
|
|
74
|
-
spec += ` });\n\n`;
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
spec += `});\n`;
|
|
78
|
-
|
|
79
|
-
return spec;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function extractScenarios(content) {
|
|
83
|
-
const scenarios = [];
|
|
84
|
-
const lines = content.split('\n');
|
|
85
|
-
let current = null;
|
|
86
|
-
|
|
87
|
-
for (let i = 0; i < lines.length; i++) {
|
|
88
|
-
const line = lines[i];
|
|
89
|
-
|
|
90
|
-
const scenarioMatch = line.match(/###\s+\d+\.\d+\s+Scenario:\s*(.+)/i);
|
|
91
|
-
if (scenarioMatch) {
|
|
92
|
-
if (current) scenarios.push(current);
|
|
93
|
-
current = { title: scenarioMatch[1].trim(), steps: [], expected: '' };
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (current) {
|
|
98
|
-
const stepMatch = line.match(/^\d+\.\s+(.+)/);
|
|
99
|
-
if (stepMatch && !line.match(/^\d+\.\s+\*\*/)) {
|
|
100
|
-
current.steps.push(stepMatch[1].trim());
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const expectedMatch = line.match(/Expected Results?[:\]]\s*(.+)/i);
|
|
104
|
-
if (expectedMatch) {
|
|
105
|
-
current.expected = expectedMatch[1].trim();
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (line.match(/---/) && current.steps.length > 0) {
|
|
109
|
-
scenarios.push(current);
|
|
110
|
-
current = null;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (current) scenarios.push(current);
|
|
116
|
-
|
|
117
|
-
return scenarios;
|
|
1
|
+
function generateTestSpec() {
|
|
2
|
+
console.log('');
|
|
3
|
+
console.log('╔══════════════════════════════════════════════════════════╗');
|
|
4
|
+
console.log('║ TEST GENERATION is handled by the AI agent ║');
|
|
5
|
+
console.log('║ ║');
|
|
6
|
+
console.log('║ The AI reads playwright-test-generator.agent.md, ║');
|
|
7
|
+
console.log('║ uses Playwright MCP to capture real selectors, and ║');
|
|
8
|
+
console.log('║ writes complete Playwright tests directly to tests/. ║');
|
|
9
|
+
console.log('║ ║');
|
|
10
|
+
console.log('║ The AI will STOP and ask for your approval before ║');
|
|
11
|
+
console.log('║ proceeding to execution. ║');
|
|
12
|
+
console.log('║ ║');
|
|
13
|
+
console.log('║ Tell your AI agent: ║');
|
|
14
|
+
console.log('║ "Generate tests for the plan in specs/" ║');
|
|
15
|
+
console.log('╚══════════════════════════════════════════════════════════╝');
|
|
16
|
+
console.log('');
|
|
17
|
+
process.exit(0);
|
|
118
18
|
}
|
|
119
19
|
|
|
120
20
|
if (require.main === module) {
|
|
121
|
-
|
|
122
|
-
if (!planName) {
|
|
123
|
-
console.log('Usage: node scripts/generator.js <test-plan-file.md>');
|
|
124
|
-
console.log(`Available: ${require('./utils').listTestPlans().join(', ')}`);
|
|
125
|
-
process.exit(1);
|
|
126
|
-
}
|
|
127
|
-
generateTestSpec(planName);
|
|
21
|
+
generateTestSpec();
|
|
128
22
|
}
|
|
129
23
|
|
|
130
24
|
module.exports = { generateTestSpec };
|