@tayo-dev/rtl 1.3.1 → 1.4.1
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/README.md +38 -97
- package/assets/claude/commands/@tayo-dev/rtl/generate.md +43 -6
- package/assets/claude/commands/@tayo-dev/rtl/help.md +2 -2
- package/assets/codex/@tayo-dev/rtl-conventions/SKILL.md +38 -6
- package/assets/codex/@tayo-dev/rtl-generate/SKILL.md +125 -13
- package/assets/codex/@tayo-dev/rtl-generate/references/assertion-markers.md +62 -0
- package/assets/codex/@tayo-dev/rtl-generate/references/auth.md +92 -0
- package/assets/codex/@tayo-dev/rtl-generate/references/conventions-schema.md +184 -0
- package/assets/codex/@tayo-dev/rtl-generate/references/entry-path-fidelity.md +68 -0
- package/assets/codex/@tayo-dev/rtl-generate/references/intent-model.md +232 -0
- package/assets/codex/@tayo-dev/rtl-generate/references/mock-store.md +18 -0
- package/assets/codex/@tayo-dev/rtl-generate/references/quality-scoring.md +189 -0
- package/assets/codex/@tayo-dev/rtl-generate/references/state-schema.md +119 -0
- package/assets/codex/@tayo-dev/rtl-generate/references/test-index.md +12 -0
- package/assets/codex/@tayo-dev/rtl-generate/references/verification-gate.md +93 -0
- package/assets/codex/@tayo-dev/rtl-help/SKILL.md +21 -7
- package/assets/codex/@tayo-dev/rtl-mocks/SKILL.md +55 -9
- package/assets/gemini/commands/@tayo-dev/rtl/generate.toml +28 -6
- package/assets/gemini/commands/@tayo-dev/rtl/help.toml +2 -2
- package/assets/opencode/commands/@tayo-dev/rtl-generate.md +32 -6
- package/assets/opencode/commands/@tayo-dev/rtl-help.md +2 -2
- package/dist/cli/commands/generate.d.ts +1 -7
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +264 -101
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/install.js +6 -6
- package/dist/core/baseline-normalizer.d.ts.map +1 -1
- package/dist/core/baseline-normalizer.js +42 -0
- package/dist/core/baseline-normalizer.js.map +1 -1
- package/dist/core/generator.d.ts +0 -2
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +81 -8
- package/dist/core/generator.js.map +1 -1
- package/dist/core/input-loader.d.ts +2 -2
- package/dist/core/input-loader.d.ts.map +1 -1
- package/dist/core/input-loader.js +7 -16
- package/dist/core/input-loader.js.map +1 -1
- package/dist/core/js-parser.d.ts +2 -1
- package/dist/core/js-parser.d.ts.map +1 -1
- package/dist/core/js-parser.js +70 -1
- package/dist/core/js-parser.js.map +1 -1
- package/dist/core/orchestrator.d.ts +1 -1
- package/dist/core/orchestrator.js +4 -4
- package/dist/core/parser.js +2 -2
- package/dist/core/recording-intelligence.d.ts +1 -1
- package/dist/core/recording-intelligence.d.ts.map +1 -1
- package/dist/core/recording-intelligence.js +298 -4
- package/dist/core/recording-intelligence.js.map +1 -1
- package/dist/core/resolver.d.ts +2 -1
- package/dist/core/resolver.d.ts.map +1 -1
- package/dist/core/resolver.js +334 -4
- package/dist/core/resolver.js.map +1 -1
- package/dist/core/scanner.d.ts +3 -3
- package/dist/core/scanner.js +9 -9
- package/dist/core/scorer.d.ts +6 -2
- package/dist/core/scorer.d.ts.map +1 -1
- package/dist/core/scorer.js +75 -7
- package/dist/core/scorer.js.map +1 -1
- package/dist/core/suite-planner.d.ts.map +1 -1
- package/dist/core/suite-planner.js +186 -17
- package/dist/core/suite-planner.js.map +1 -1
- package/dist/core/writer.d.ts +0 -1
- package/dist/core/writer.d.ts.map +1 -1
- package/dist/core/writer.js +3 -3
- package/dist/core/writer.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +19 -15
- package/dist/index.js.map +1 -1
- package/dist/install/planner.js +1 -1
- package/dist/install/runtimes/codex.d.ts.map +1 -1
- package/dist/install/runtimes/codex.js +18 -0
- package/dist/install/runtimes/codex.js.map +1 -1
- package/dist/install/types.d.ts +1 -1
- package/dist/learner/index.d.ts +2 -2
- package/dist/learner/index.js +3 -3
- package/dist/learner/storage.d.ts +1 -1
- package/dist/learner/storage.js +2 -2
- package/dist/templates/test-template.d.ts +4 -0
- package/dist/templates/test-template.d.ts.map +1 -1
- package/dist/templates/test-template.js +10 -1
- package/dist/templates/test-template.js.map +1 -1
- package/dist/types/recording.d.ts +118 -0
- package/dist/types/recording.d.ts.map +1 -1
- package/dist/types/recording.js.map +1 -1
- package/dist/types/score.d.ts +15 -0
- package/dist/types/score.d.ts.map +1 -1
- package/package.json +5 -5
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Generate command
|
|
3
|
-
*
|
|
4
|
-
* Converts Recorder exports into React Testing Library test files.
|
|
3
|
+
* Internal runtime-only generation pipeline for Testing Library Recorder JS exports.
|
|
5
4
|
*/
|
|
6
5
|
import { Command } from 'commander';
|
|
7
6
|
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
8
7
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
9
8
|
import { cwd } from 'node:process';
|
|
10
9
|
import pc from 'picocolors';
|
|
11
|
-
import { generateTest } from '../../core/generator.js';
|
|
12
10
|
import { writeTestFile } from '../../core/writer.js';
|
|
13
11
|
import { captureVisualState, resolveSelector, } from '../../core/resolver.js';
|
|
14
12
|
import { scoreGeneratedTest } from '../../core/scorer.js';
|
|
@@ -21,26 +19,106 @@ import { generateTestFromGroups, emitQuerySummary } from '../../core/generator.j
|
|
|
21
19
|
import { loadInput } from '../../core/input-loader.js';
|
|
22
20
|
import { normalizeJsBaseline } from '../../core/baseline-normalizer.js';
|
|
23
21
|
import { planJsSuite } from '../../core/suite-planner.js';
|
|
22
|
+
const EMPTY_MARKER_COVERAGE = {
|
|
23
|
+
detected: 0,
|
|
24
|
+
emitted: 0,
|
|
25
|
+
unresolved: 0,
|
|
26
|
+
};
|
|
27
|
+
const UNRESOLVED_MARKER_REASON_GUIDANCE = {
|
|
28
|
+
'missing-marker-candidate': 'Semantic marker candidate metadata is missing. Re-record or keep marker metadata intact.',
|
|
29
|
+
'missing-anchor': 'Marker has no reliable anchor step. Re-record with marker near the intended assertion moment.',
|
|
30
|
+
'missing-query': 'Recorder evidence is missing an accessible query. Capture a clearer role/name or visible text.',
|
|
31
|
+
'unsupported-proof-subject': 'Marker proof subject is unsupported for safe RTL conversion. Use role/name or visible text proof.',
|
|
32
|
+
'ambiguous-field-context': 'Field context is ambiguous. Capture a single, specific field label or value target.',
|
|
33
|
+
'unsupported-field-context': 'Field context could not map to a trusted RTL field query. Record a clearer label/placeholder.',
|
|
34
|
+
'generic-container': 'Marker points to a generic container. Capture the concrete user-facing element instead.',
|
|
35
|
+
'css-only-evidence': 'Marker is backed only by CSS-like evidence. Capture semantic role/name or visible text evidence.',
|
|
36
|
+
'icon-only-target': 'Marker target is icon-only and ambiguous. Capture surrounding accessible text context.',
|
|
37
|
+
'hidden-evidence': 'Marker evidence depends on hidden/implementation selectors. Capture user-visible evidence instead.',
|
|
38
|
+
};
|
|
24
39
|
function deriveOutputPath(inputPath) {
|
|
25
40
|
const dir = dirname(inputPath);
|
|
26
|
-
const name = basename(inputPath).replace(/\.
|
|
41
|
+
const name = basename(inputPath).replace(/\.[cm]?[jt]sx?$/, '');
|
|
27
42
|
return join(dir, `${name}.test.tsx`);
|
|
28
43
|
}
|
|
29
44
|
function logScore(scoreResult) {
|
|
30
|
-
|
|
45
|
+
const markerCoverageSummary = `markers: detected=${scoreResult.markerCoverage.detected}, ` +
|
|
46
|
+
`emitted=${scoreResult.markerCoverage.emitted}, ` +
|
|
47
|
+
`unresolved=${scoreResult.markerCoverage.unresolved}`;
|
|
48
|
+
console.log(pc.dim('[tayo]') +
|
|
31
49
|
` Score: ${scoreResult.total}/100 (${scoreResult.grade}) — ` +
|
|
32
50
|
`query: ${scoreResult.dimensions.queryQuality}, ` +
|
|
33
51
|
`assertions: ${scoreResult.dimensions.assertionSpecificity}, ` +
|
|
34
52
|
`structure: ${scoreResult.dimensions.testStructure}, ` +
|
|
35
|
-
`boundary: ${scoreResult.dimensions.boundaryIsolation}`
|
|
53
|
+
`boundary: ${scoreResult.dimensions.boundaryIsolation}, ` +
|
|
54
|
+
markerCoverageSummary);
|
|
55
|
+
}
|
|
56
|
+
function emitMarkerCoverageSection(scoreResult) {
|
|
57
|
+
const gateStatus = scoreResult.markerQualityGate.failing ? pc.red('FAIL') : pc.green('PASS');
|
|
58
|
+
console.log(pc.dim('[tayo]') + ' Marker coverage:');
|
|
59
|
+
console.log(pc.dim('[tayo]') + ` detected: ${scoreResult.markerCoverage.detected}`);
|
|
60
|
+
console.log(pc.dim('[tayo]') + ` emitted: ${scoreResult.markerCoverage.emitted}`);
|
|
61
|
+
console.log(pc.dim('[tayo]') + ` unresolved: ${scoreResult.markerCoverage.unresolved}`);
|
|
62
|
+
console.log(pc.dim('[tayo]') +
|
|
63
|
+
` QUAL-02 gate: ${gateStatus} (${scoreResult.markerQualityGate.reason})`);
|
|
64
|
+
if (scoreResult.markerQualityGate.failing) {
|
|
65
|
+
console.error(pc.red(`[tayo] QUAL-02 FAIL: ${scoreResult.markerQualityGate.message}`));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function normalizeUnresolvedMarkerHint(marker) {
|
|
69
|
+
const hint = marker.proofText ?? marker.target ?? marker.query?.raw ?? marker.selector?.selector;
|
|
70
|
+
const normalized = hint?.replace(/\s+/g, ' ').trim();
|
|
71
|
+
return normalized && normalized.length > 0 ? normalized : 'none';
|
|
72
|
+
}
|
|
73
|
+
function formatUnresolvedMarkerLine(marker) {
|
|
74
|
+
const line = marker.line ?? marker.sourceContext.line;
|
|
75
|
+
return Number.isFinite(line) ? String(line) : 'unknown';
|
|
76
|
+
}
|
|
77
|
+
function formatUnresolvedMarkerWarning(marker) {
|
|
78
|
+
const line = formatUnresolvedMarkerLine(marker);
|
|
79
|
+
const hint = normalizeUnresolvedMarkerHint(marker);
|
|
80
|
+
const guidance = UNRESOLVED_MARKER_REASON_GUIDANCE[marker.reason];
|
|
81
|
+
return (`MKR-03 unresolved-marker marker=${marker.markerStepId} ` +
|
|
82
|
+
`line: ${line} reason=${marker.reason} ` +
|
|
83
|
+
`detail="${guidance}" hint="${hint}"`);
|
|
84
|
+
}
|
|
85
|
+
function collectUnresolvedMarkerAssertions(suitePlan) {
|
|
86
|
+
const seenMarkerStepIds = new Set();
|
|
87
|
+
const unresolvedMarkers = [];
|
|
88
|
+
for (const scenario of suitePlan.scenarios) {
|
|
89
|
+
for (const unresolvedMarker of scenario.unresolvedMarkerAssertions ?? []) {
|
|
90
|
+
if (seenMarkerStepIds.has(unresolvedMarker.markerStepId)) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
seenMarkerStepIds.add(unresolvedMarker.markerStepId);
|
|
94
|
+
unresolvedMarkers.push(unresolvedMarker);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return unresolvedMarkers;
|
|
98
|
+
}
|
|
99
|
+
function emitUnresolvedMarkerWarnings(suitePlan) {
|
|
100
|
+
if (!suitePlan) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const unresolvedMarkers = collectUnresolvedMarkerAssertions(suitePlan);
|
|
104
|
+
for (const unresolvedMarker of unresolvedMarkers) {
|
|
105
|
+
console.warn(pc.yellow(`[tayo] ${formatUnresolvedMarkerWarning(unresolvedMarker)}`));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function enforceMarkerGateExit(scoreResult) {
|
|
109
|
+
if (!scoreResult.markerQualityGate.failing) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
process.exitCode = 1;
|
|
113
|
+
console.error(pc.red('[tayo] Exiting with code 1: QUAL-02 gate failed after generation.'));
|
|
36
114
|
}
|
|
37
115
|
function emitLowConfidenceBanner(scoreResult) {
|
|
38
116
|
if (!scoreResult.requiresReview) {
|
|
39
117
|
return;
|
|
40
118
|
}
|
|
41
|
-
console.warn(pc.yellow(`[
|
|
119
|
+
console.warn(pc.yellow(`[tayo] Manual review required — this generated test is still a draft (${scoreResult.total}/100, ${scoreResult.grade}).`));
|
|
42
120
|
if (scoreResult.blockers.length > 0) {
|
|
43
|
-
console.warn(pc.yellow(`[
|
|
121
|
+
console.warn(pc.yellow(`[tayo] Top blockers: ${scoreResult.blockers.join(' | ')}`));
|
|
44
122
|
}
|
|
45
123
|
}
|
|
46
124
|
function emitScoreHints(scoreResult, queryResults = [], boundaryIssues = analyzeBoundaryIsolation('')) {
|
|
@@ -48,18 +126,18 @@ function emitScoreHints(scoreResult, queryResults = [], boundaryIssues = analyze
|
|
|
48
126
|
const testIdCount = queryResults.filter((queryResult) => {
|
|
49
127
|
return queryResult.method === 'getByTestId';
|
|
50
128
|
}).length;
|
|
51
|
-
console.log(pc.yellow(`[
|
|
129
|
+
console.log(pc.yellow(`[tayo] Tip: ${testIdCount} getByTestId queries — consider adding aria-label`));
|
|
52
130
|
}
|
|
53
131
|
if (scoreResult.dimensions.assertionSpecificity < 60) {
|
|
54
|
-
console.log(pc.yellow('[
|
|
132
|
+
console.log(pc.yellow('[tayo] Tip: Add specific matchers like toHaveValue() for better assertions'));
|
|
55
133
|
}
|
|
56
134
|
if (scoreResult.dimensions.testStructure < 60) {
|
|
57
|
-
console.log(pc.yellow('[
|
|
135
|
+
console.log(pc.yellow('[tayo] Tip: Split into multiple it() blocks for better test organization'));
|
|
58
136
|
}
|
|
59
137
|
if (scoreResult.dimensions.boundaryIsolation < 60) {
|
|
60
138
|
for (const issue of boundaryIssues) {
|
|
61
|
-
console.warn(pc.yellow(`[
|
|
62
|
-
console.warn(pc.yellow(`[
|
|
139
|
+
console.warn(pc.yellow(`[tayo] Boundary: ${issue.message}`));
|
|
140
|
+
console.warn(pc.yellow(`[tayo] Tip: ${issue.suggestion}`));
|
|
63
141
|
}
|
|
64
142
|
}
|
|
65
143
|
}
|
|
@@ -69,6 +147,12 @@ function summarizeCleanup(analyzedRecording) {
|
|
|
69
147
|
if (diagnostics.removedRedundantClicks > 0) {
|
|
70
148
|
parts.push(`${diagnostics.removedRedundantClicks} redundant click(s)`);
|
|
71
149
|
}
|
|
150
|
+
if ((diagnostics.preservedSemanticMarkers ?? 0) > 0) {
|
|
151
|
+
parts.push(`${diagnostics.preservedSemanticMarkers} preserved semantic marker(s)`);
|
|
152
|
+
}
|
|
153
|
+
if ((diagnostics.unresolvedSemanticMarkers ?? 0) > 0) {
|
|
154
|
+
parts.push(`${diagnostics.unresolvedSemanticMarkers} unresolved semantic marker(s)`);
|
|
155
|
+
}
|
|
72
156
|
if (diagnostics.removedDoubleClickNoise > 0) {
|
|
73
157
|
parts.push(`${diagnostics.removedDoubleClickNoise} dblClick noise event(s)`);
|
|
74
158
|
}
|
|
@@ -81,7 +165,69 @@ function summarizeCleanup(analyzedRecording) {
|
|
|
81
165
|
if (parts.length === 0) {
|
|
82
166
|
return;
|
|
83
167
|
}
|
|
84
|
-
console.log(pc.dim('[
|
|
168
|
+
console.log(pc.dim('[tayo]') + ` Recording cleanup: ${parts.join(', ')}`);
|
|
169
|
+
}
|
|
170
|
+
function countPlannedScenarioMarkers(scenarios) {
|
|
171
|
+
return scenarios.reduce((totals, scenario) => ({
|
|
172
|
+
emitted: totals.emitted + (scenario.markerAssertions?.length ?? 0),
|
|
173
|
+
unresolved: totals.unresolved + (scenario.unresolvedMarkerAssertions?.length ?? 0),
|
|
174
|
+
}), {
|
|
175
|
+
emitted: 0,
|
|
176
|
+
unresolved: 0,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
function buildMarkerCoverageSummary(params) {
|
|
180
|
+
const { analyzedRecording, suitePlan } = params;
|
|
181
|
+
const preservedMarkers = analyzedRecording.diagnostics.preservedSemanticMarkers ?? 0;
|
|
182
|
+
const diagnosticUnresolvedMarkers = analyzedRecording.diagnostics.unresolvedSemanticMarkers ?? 0;
|
|
183
|
+
if (!suitePlan) {
|
|
184
|
+
return {
|
|
185
|
+
detected: preservedMarkers + diagnosticUnresolvedMarkers,
|
|
186
|
+
emitted: 0,
|
|
187
|
+
unresolved: diagnosticUnresolvedMarkers,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const plannedMarkerTotals = countPlannedScenarioMarkers(suitePlan.scenarios);
|
|
191
|
+
const unresolved = plannedMarkerTotals.unresolved;
|
|
192
|
+
const detected = Math.max(preservedMarkers + unresolved, plannedMarkerTotals.emitted + unresolved);
|
|
193
|
+
return {
|
|
194
|
+
detected,
|
|
195
|
+
emitted: plannedMarkerTotals.emitted,
|
|
196
|
+
unresolved,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function mergeAnalyzedStepState(recording, analyzedRecording) {
|
|
200
|
+
const analyzedStepsById = new Map(analyzedRecording.steps
|
|
201
|
+
.filter((step) => Boolean(step.id))
|
|
202
|
+
.map((step) => [step.id, step]));
|
|
203
|
+
return {
|
|
204
|
+
...recording,
|
|
205
|
+
steps: recording.steps.map((step) => {
|
|
206
|
+
if (!step.id) {
|
|
207
|
+
return step;
|
|
208
|
+
}
|
|
209
|
+
const analyzedStep = analyzedStepsById.get(step.id);
|
|
210
|
+
if (!analyzedStep) {
|
|
211
|
+
return step;
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
...step,
|
|
215
|
+
...(analyzedStep.semanticMarkerCandidate
|
|
216
|
+
? { semanticMarkerCandidate: analyzedStep.semanticMarkerCandidate }
|
|
217
|
+
: {}),
|
|
218
|
+
...(analyzedStep.semanticMarkerLink
|
|
219
|
+
? { semanticMarkerLink: analyzedStep.semanticMarkerLink }
|
|
220
|
+
: {}),
|
|
221
|
+
...(analyzedStep.unresolvedSemanticMarker
|
|
222
|
+
? { unresolvedSemanticMarker: analyzedStep.unresolvedSemanticMarker }
|
|
223
|
+
: {}),
|
|
224
|
+
metadata: {
|
|
225
|
+
...step.metadata,
|
|
226
|
+
...analyzedStep.metadata,
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}),
|
|
230
|
+
};
|
|
85
231
|
}
|
|
86
232
|
function toItGroups(analyzedRecording, fallbackTitle) {
|
|
87
233
|
if (analyzedRecording.intentGroups.length > 0) {
|
|
@@ -164,6 +310,37 @@ function rehydrateSuitePlan(plan, steps) {
|
|
|
164
310
|
})),
|
|
165
311
|
};
|
|
166
312
|
}
|
|
313
|
+
function isSemanticMarkerStep(step) {
|
|
314
|
+
return Boolean(step.semanticMarkerLink || step.unresolvedSemanticMarker);
|
|
315
|
+
}
|
|
316
|
+
function stripSemanticMarkerStepsFromItGroups(itGroups) {
|
|
317
|
+
return itGroups
|
|
318
|
+
.map((group) => ({
|
|
319
|
+
...group,
|
|
320
|
+
steps: group.steps.filter((step) => !isSemanticMarkerStep(step)),
|
|
321
|
+
}))
|
|
322
|
+
.filter((group) => group.steps.length > 0);
|
|
323
|
+
}
|
|
324
|
+
function stripSemanticMarkerStepsFromHelpers(helpers) {
|
|
325
|
+
return helpers
|
|
326
|
+
.map((helper) => ({
|
|
327
|
+
...helper,
|
|
328
|
+
steps: helper.steps.filter((step) => !isSemanticMarkerStep(step)),
|
|
329
|
+
}))
|
|
330
|
+
.filter((helper) => helper.steps.length > 0);
|
|
331
|
+
}
|
|
332
|
+
function stripSemanticMarkerStepsFromScenarios(scenarios, helpers) {
|
|
333
|
+
const helperNames = new Set(helpers.map((helper) => helper.name));
|
|
334
|
+
return scenarios
|
|
335
|
+
.map((scenario) => ({
|
|
336
|
+
...scenario,
|
|
337
|
+
steps: scenario.steps.filter((step) => !isSemanticMarkerStep(step)),
|
|
338
|
+
helperRefs: scenario.helperRefs.filter((helperRef) => helperNames.has(helperRef)),
|
|
339
|
+
}))
|
|
340
|
+
.filter((scenario) => scenario.steps.length > 0 ||
|
|
341
|
+
scenario.helperRefs.length > 0 ||
|
|
342
|
+
(scenario.markerAssertions?.length ?? 0) > 0);
|
|
343
|
+
}
|
|
167
344
|
function dedupeQueryResults(queryResults) {
|
|
168
345
|
const seen = new Set();
|
|
169
346
|
return queryResults.filter((queryResult) => {
|
|
@@ -189,7 +366,7 @@ function summarizeVisualState(visualState) {
|
|
|
189
366
|
if (visualState.screenshotPath) {
|
|
190
367
|
parts.push(`screenshot=${visualState.screenshotPath}`);
|
|
191
368
|
}
|
|
192
|
-
console.log(pc.dim('[
|
|
369
|
+
console.log(pc.dim('[tayo]') + ` Visual state: ${parts.join(', ')}`);
|
|
193
370
|
}
|
|
194
371
|
function summarizeMockAnalysis(mockAnalysis) {
|
|
195
372
|
if (!mockAnalysis) {
|
|
@@ -208,25 +385,25 @@ function summarizeMockAnalysis(mockAnalysis) {
|
|
|
208
385
|
if (parts.length === 0) {
|
|
209
386
|
return;
|
|
210
387
|
}
|
|
211
|
-
console.log(pc.dim('[
|
|
388
|
+
console.log(pc.dim('[tayo]') + ` Mock analysis: ${parts.join(', ')}`);
|
|
212
389
|
const topRecommendation = mockAnalysis.recommendations[0];
|
|
213
390
|
if (topRecommendation) {
|
|
214
|
-
console.log(pc.dim('[
|
|
391
|
+
console.log(pc.dim('[tayo]') +
|
|
215
392
|
` Mock hint: ${topRecommendation.kind} ${topRecommendation.target} (${topRecommendation.count} file(s))`);
|
|
216
393
|
}
|
|
217
394
|
const topLifecycle = mockAnalysis.mutationLifecycles[0];
|
|
218
395
|
if (topLifecycle) {
|
|
219
|
-
console.log(pc.dim('[
|
|
396
|
+
console.log(pc.dim('[tayo]') +
|
|
220
397
|
` Mutation lifecycle: ${topLifecycle.stages.join(' -> ')} in ${topLifecycle.file}`);
|
|
221
398
|
}
|
|
222
399
|
const topWarning = mockAnalysis.instabilityWarnings[0];
|
|
223
400
|
if (topWarning) {
|
|
224
|
-
console.warn(pc.yellow(`[
|
|
401
|
+
console.warn(pc.yellow(`[tayo] Mock stability: ${topWarning.reason} (${topWarning.file})`));
|
|
225
402
|
}
|
|
226
403
|
}
|
|
227
404
|
function summarizeBoundaryWarnings(warnings) {
|
|
228
405
|
for (const warning of warnings) {
|
|
229
|
-
console.warn(pc.yellow(`[
|
|
406
|
+
console.warn(pc.yellow(`[tayo] Boundary: ${warning}`));
|
|
230
407
|
}
|
|
231
408
|
}
|
|
232
409
|
function tokenizeSuiteHint(value) {
|
|
@@ -288,7 +465,7 @@ function applyRepoRenderTarget(suitePlan, renderTarget) {
|
|
|
288
465
|
resolvedTarget: renderTarget.symbol,
|
|
289
466
|
confidence: suitePlan.renderBoundary.confidence === 'low' ? 'medium' : suitePlan.renderBoundary.confidence,
|
|
290
467
|
},
|
|
291
|
-
warnings: suitePlan.warnings.filter((warning) => !warning.includes('
|
|
468
|
+
warnings: suitePlan.warnings.filter((warning) => !warning.includes('Tayo could not resolve the exact render target from repo context') &&
|
|
292
469
|
!warning.includes('Prefer a repo-local module/container render boundary')),
|
|
293
470
|
};
|
|
294
471
|
}
|
|
@@ -313,7 +490,7 @@ async function resolveJsGeneration(recording, itGroups) {
|
|
|
313
490
|
.map((step) => [step.id, step]));
|
|
314
491
|
const updatedSteps = new Map();
|
|
315
492
|
if (selectorGroups.size > 0 && recording.url) {
|
|
316
|
-
console.log(pc.dim('[
|
|
493
|
+
console.log(pc.dim('[tayo]') +
|
|
317
494
|
` Resolving ${baseline.selectors.length} selector(s) via Playwright...`);
|
|
318
495
|
}
|
|
319
496
|
for (const [stepId, selectors] of selectorGroups) {
|
|
@@ -373,7 +550,7 @@ async function resolveJsGeneration(recording, itGroups) {
|
|
|
373
550
|
}
|
|
374
551
|
function summarizeSelectorWarnings(warnings) {
|
|
375
552
|
for (const warning of warnings) {
|
|
376
|
-
console.warn(pc.yellow(`[
|
|
553
|
+
console.warn(pc.yellow(`[tayo] ${warning}`));
|
|
377
554
|
}
|
|
378
555
|
}
|
|
379
556
|
async function maybeCaptureVisualState(params) {
|
|
@@ -382,7 +559,7 @@ async function maybeCaptureVisualState(params) {
|
|
|
382
559
|
return null;
|
|
383
560
|
}
|
|
384
561
|
const candidates = findVisualCaptureCandidates(analyzedRecording);
|
|
385
|
-
const visualDir = join(projectRoot, '.
|
|
562
|
+
const visualDir = join(projectRoot, '.tayo', 'visual');
|
|
386
563
|
if (candidates.length > 0) {
|
|
387
564
|
await mkdir(visualDir, { recursive: true });
|
|
388
565
|
return captureVisualState(url, {
|
|
@@ -410,7 +587,7 @@ async function maybeAnalyzeMocks(projectRoot) {
|
|
|
410
587
|
}
|
|
411
588
|
}
|
|
412
589
|
async function appendHistoryEntry(projectRoot, historyEntry) {
|
|
413
|
-
const taroDir = join(projectRoot, '.
|
|
590
|
+
const taroDir = join(projectRoot, '.tayo');
|
|
414
591
|
await mkdir(taroDir, { recursive: true });
|
|
415
592
|
const historyPath = join(taroDir, 'history.json');
|
|
416
593
|
let history = [];
|
|
@@ -429,12 +606,12 @@ async function finalizeGeneratedOutput(params) {
|
|
|
429
606
|
const { code, outputPath, projectRoot, recordingFile, scoreResult } = params;
|
|
430
607
|
const verification = verifySyntax(code, outputPath);
|
|
431
608
|
if (!verification.valid) {
|
|
432
|
-
console.error(pc.red('[
|
|
609
|
+
console.error(pc.red('[tayo] Error: Post-write verification failed'));
|
|
433
610
|
console.error(pc.red(` ${verification.error}`));
|
|
434
|
-
console.error(pc.red(' This is a
|
|
611
|
+
console.error(pc.red(' This is a Tayo bug. Please report it.'));
|
|
435
612
|
process.exit(1);
|
|
436
613
|
}
|
|
437
|
-
console.log(pc.green('[
|
|
614
|
+
console.log(pc.green('[tayo] ✓ post-write verified'));
|
|
438
615
|
await appendHistoryEntry(projectRoot, {
|
|
439
616
|
timestamp: new Date().toISOString(),
|
|
440
617
|
recordingFile,
|
|
@@ -451,14 +628,11 @@ async function finalizeGeneratedOutput(params) {
|
|
|
451
628
|
}
|
|
452
629
|
}
|
|
453
630
|
export function createGenerateCommand() {
|
|
454
|
-
const generate = new Command('
|
|
631
|
+
const generate = new Command('__generate');
|
|
455
632
|
generate
|
|
456
|
-
.description('
|
|
457
|
-
.argument('<file>', 'Path to the recorder export file (.js
|
|
458
|
-
.
|
|
459
|
-
.option('-d, --dry-run', 'Preview the generated test without writing to disk', false)
|
|
460
|
-
.option('-f, --force', 'Overwrite existing test file', false)
|
|
461
|
-
.action(async (file, options) => {
|
|
633
|
+
.description('Internal runtime-only generator for Testing Library Recorder JS exports')
|
|
634
|
+
.argument('<file>', 'Path to the recorder export file (.js)')
|
|
635
|
+
.action(async (file) => {
|
|
462
636
|
const filePath = resolve(file);
|
|
463
637
|
const projectRoot = cwd();
|
|
464
638
|
// 1. Verify file is accessible
|
|
@@ -477,23 +651,18 @@ export function createGenerateCommand() {
|
|
|
477
651
|
console.error(pc.red('Error:') + ` Failed to parse recording: ${pc.bold(filePath)}\n${String(err)}`);
|
|
478
652
|
process.exit(1);
|
|
479
653
|
}
|
|
480
|
-
const normalizedRecording =
|
|
481
|
-
let conventions =
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
conventions = await
|
|
485
|
-
if (!conventions) {
|
|
486
|
-
console.log(pc.dim('[taro]') + ' Scanning project conventions...');
|
|
487
|
-
conventions = await scanConventions(projectRoot);
|
|
488
|
-
}
|
|
489
|
-
repoRenderTargets = await discoverRepoRenderTargets(projectRoot);
|
|
654
|
+
const normalizedRecording = normalizeJsBaseline(parsedInput);
|
|
655
|
+
let conventions = await readConventions(projectRoot);
|
|
656
|
+
if (!conventions) {
|
|
657
|
+
console.log(pc.dim('[tayo]') + ' Scanning project conventions...');
|
|
658
|
+
conventions = await scanConventions(projectRoot);
|
|
490
659
|
}
|
|
660
|
+
const repoRenderTargets = await discoverRepoRenderTargets(projectRoot);
|
|
491
661
|
console.log(pc.green('Parsed:') +
|
|
492
662
|
` ${pc.bold(normalizedRecording.title)} — ${normalizedRecording.steps.length} steps` +
|
|
493
|
-
|
|
494
|
-
? `, ${normalizedRecording.baseline?.itGroups.length ?? 0} test group(s)`
|
|
495
|
-
: ''));
|
|
663
|
+
`, ${normalizedRecording.baseline?.itGroups.length ?? 0} test group(s)`);
|
|
496
664
|
const analyzedRecording = analyzeRecording(normalizedRecording);
|
|
665
|
+
const markerAwareRecording = mergeAnalyzedStepState(normalizedRecording, analyzedRecording);
|
|
497
666
|
summarizeCleanup(analyzedRecording);
|
|
498
667
|
const visualState = await maybeCaptureVisualState({
|
|
499
668
|
analyzedRecording,
|
|
@@ -504,79 +673,72 @@ export function createGenerateCommand() {
|
|
|
504
673
|
summarizeVisualState(visualState);
|
|
505
674
|
const mockAnalysis = await maybeAnalyzeMocks(projectRoot);
|
|
506
675
|
summarizeMockAnalysis(mockAnalysis);
|
|
507
|
-
const outputPath =
|
|
508
|
-
const rawJsSuitePlan =
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
:
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
mockAnalysis,
|
|
521
|
-
suitePlan: rawJsSuitePlan,
|
|
522
|
-
})
|
|
523
|
-
: null;
|
|
676
|
+
const outputPath = deriveOutputPath(filePath);
|
|
677
|
+
const rawJsSuitePlan = planJsSuite({
|
|
678
|
+
recording: markerAwareRecording,
|
|
679
|
+
analyzedRecording,
|
|
680
|
+
mockAnalysis,
|
|
681
|
+
fallbackTitle: normalizedRecording.title,
|
|
682
|
+
});
|
|
683
|
+
const repoRenderTarget = resolveRepoRenderTarget({
|
|
684
|
+
candidates: repoRenderTargets,
|
|
685
|
+
recording: normalizedRecording,
|
|
686
|
+
mockAnalysis,
|
|
687
|
+
suitePlan: rawJsSuitePlan,
|
|
688
|
+
});
|
|
524
689
|
const jsSuitePlan = rawJsSuitePlan
|
|
525
690
|
? applyRepoRenderTarget(rawJsSuitePlan, repoRenderTarget)
|
|
526
691
|
: null;
|
|
527
692
|
if (jsSuitePlan) {
|
|
528
693
|
summarizeBoundaryWarnings(jsSuitePlan.warnings);
|
|
529
694
|
}
|
|
530
|
-
const resolvedJsGeneration =
|
|
531
|
-
? await resolveJsGeneration(normalizedRecording, jsSuitePlan?.itGroups ?? toItGroups(analyzedRecording, normalizedRecording.title))
|
|
532
|
-
: null;
|
|
695
|
+
const resolvedJsGeneration = await resolveJsGeneration(markerAwareRecording, jsSuitePlan?.itGroups ?? toItGroups(analyzedRecording, normalizedRecording.title));
|
|
533
696
|
if (resolvedJsGeneration) {
|
|
534
697
|
summarizeSelectorWarnings(resolvedJsGeneration.warnings);
|
|
535
698
|
}
|
|
536
|
-
const hydratedSuitePlan =
|
|
537
|
-
? rehydrateSuitePlan(jsSuitePlan, resolvedJsGeneration?.recording.steps ??
|
|
699
|
+
const hydratedSuitePlan = jsSuitePlan
|
|
700
|
+
? rehydrateSuitePlan(jsSuitePlan, resolvedJsGeneration?.recording.steps ?? markerAwareRecording.steps)
|
|
538
701
|
: jsSuitePlan;
|
|
539
|
-
const
|
|
540
|
-
?
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
702
|
+
const generationHelpers = hydratedSuitePlan
|
|
703
|
+
? stripSemanticMarkerStepsFromHelpers(hydratedSuitePlan.helpers)
|
|
704
|
+
: undefined;
|
|
705
|
+
const generationScenarios = hydratedSuitePlan && generationHelpers
|
|
706
|
+
? stripSemanticMarkerStepsFromScenarios(hydratedSuitePlan.scenarios, generationHelpers)
|
|
707
|
+
: undefined;
|
|
708
|
+
const generationItGroups = stripSemanticMarkerStepsFromItGroups(resolvedJsGeneration?.itGroups ??
|
|
709
|
+
hydratedSuitePlan?.itGroups ??
|
|
710
|
+
toItGroups(analyzedRecording, normalizedRecording.title));
|
|
711
|
+
const generated = generateTestFromGroups(normalizedRecording.title, generationItGroups, {
|
|
712
|
+
outputPath,
|
|
713
|
+
conventions,
|
|
714
|
+
queryResults: resolvedJsGeneration?.queryResults ?? [],
|
|
715
|
+
helpers: generationHelpers,
|
|
716
|
+
scenarios: generationScenarios,
|
|
717
|
+
renderTarget: repoRenderTarget,
|
|
718
|
+
});
|
|
719
|
+
const markerCoverage = buildMarkerCoverageSummary({
|
|
720
|
+
analyzedRecording,
|
|
721
|
+
suitePlan: hydratedSuitePlan,
|
|
722
|
+
});
|
|
550
723
|
if (hydratedSuitePlan?.warnings.length) {
|
|
551
724
|
generated.code = [
|
|
552
|
-
...hydratedSuitePlan.warnings.map((warning) => `//
|
|
725
|
+
...hydratedSuitePlan.warnings.map((warning) => `// tayo-boundary-warning: ${warning}`),
|
|
553
726
|
generated.code,
|
|
554
727
|
].join('\n');
|
|
555
728
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
: scoreGeneratedTest(generated.code);
|
|
729
|
+
emitQuerySummary(resolvedJsGeneration?.queryResults ?? []);
|
|
730
|
+
const scoreResult = scoreGeneratedTest(generated.code, {
|
|
731
|
+
queryResults: resolvedJsGeneration?.queryResults ?? [],
|
|
732
|
+
markerCoverage,
|
|
733
|
+
});
|
|
562
734
|
const boundaryIssues = analyzeBoundaryIsolation(generated.code);
|
|
563
735
|
logScore(scoreResult);
|
|
736
|
+
emitMarkerCoverageSection(scoreResult);
|
|
737
|
+
emitUnresolvedMarkerWarnings(hydratedSuitePlan);
|
|
564
738
|
emitLowConfidenceBanner(scoreResult);
|
|
565
739
|
emitScoreHints(scoreResult, resolvedJsGeneration?.queryResults ?? [], boundaryIssues);
|
|
566
|
-
if (options.dryRun) {
|
|
567
|
-
console.log(pc.yellow('\nDry run — test preview:\n'));
|
|
568
|
-
console.log(pc.dim('─'.repeat(60)));
|
|
569
|
-
console.log(generated.code);
|
|
570
|
-
console.log(pc.dim('─'.repeat(60)));
|
|
571
|
-
console.log(pc.dim(`\n[taro] Score: ${scoreResult.total}/100 (${scoreResult.grade})`));
|
|
572
|
-
console.log(pc.yellow(`\nWould write to: ${pc.bold(outputPath)}`));
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
740
|
try {
|
|
576
|
-
const result = await writeTestFile(generated.code, outputPath, {
|
|
577
|
-
force: options.force,
|
|
578
|
-
createDir: true,
|
|
579
|
-
});
|
|
741
|
+
const result = await writeTestFile(generated.code, outputPath, { createDir: true });
|
|
580
742
|
await finalizeGeneratedOutput({
|
|
581
743
|
code: generated.code,
|
|
582
744
|
outputPath: result.filePath,
|
|
@@ -586,6 +748,7 @@ export function createGenerateCommand() {
|
|
|
586
748
|
});
|
|
587
749
|
const action = result.overwritten ? pc.yellow('Updated') : pc.green('Created');
|
|
588
750
|
console.log(`${action}: ${pc.bold(result.filePath)}`);
|
|
751
|
+
enforceMarkerGateExit(scoreResult);
|
|
589
752
|
}
|
|
590
753
|
catch (err) {
|
|
591
754
|
console.error(pc.red('Error:') + ` ${String(err)}`);
|