@tayo-dev/rtl 1.0.0
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 +250 -0
- package/dist/analyzer/mocks/detector.d.ts +59 -0
- package/dist/analyzer/mocks/detector.d.ts.map +1 -0
- package/dist/analyzer/mocks/detector.js +264 -0
- package/dist/analyzer/mocks/detector.js.map +1 -0
- package/dist/analyzer/mocks/target-analyzer.d.ts +92 -0
- package/dist/analyzer/mocks/target-analyzer.d.ts.map +1 -0
- package/dist/analyzer/mocks/target-analyzer.js +305 -0
- package/dist/analyzer/mocks/target-analyzer.js.map +1 -0
- package/dist/analyzer/visual/element-analyzer.d.ts +44 -0
- package/dist/analyzer/visual/element-analyzer.d.ts.map +1 -0
- package/dist/analyzer/visual/element-analyzer.js +176 -0
- package/dist/analyzer/visual/element-analyzer.js.map +1 -0
- package/dist/analyzer/visual/inspector.d.ts +49 -0
- package/dist/analyzer/visual/inspector.d.ts.map +1 -0
- package/dist/analyzer/visual/inspector.js +109 -0
- package/dist/analyzer/visual/inspector.js.map +1 -0
- package/dist/cli/commands/generate.d.ts +13 -0
- package/dist/cli/commands/generate.d.ts.map +1 -0
- package/dist/cli/commands/generate.js +417 -0
- package/dist/cli/commands/generate.js.map +1 -0
- package/dist/core/generator.d.ts +32 -0
- package/dist/core/generator.d.ts.map +1 -0
- package/dist/core/generator.js +173 -0
- package/dist/core/generator.js.map +1 -0
- package/dist/core/js-parser.d.ts +48 -0
- package/dist/core/js-parser.d.ts.map +1 -0
- package/dist/core/js-parser.js +244 -0
- package/dist/core/js-parser.js.map +1 -0
- package/dist/core/mock-intelligence.d.ts +14 -0
- package/dist/core/mock-intelligence.d.ts.map +1 -0
- package/dist/core/mock-intelligence.js +140 -0
- package/dist/core/mock-intelligence.js.map +1 -0
- package/dist/core/orchestrator.d.ts +49 -0
- package/dist/core/orchestrator.d.ts.map +1 -0
- package/dist/core/orchestrator.js +315 -0
- package/dist/core/orchestrator.js.map +1 -0
- package/dist/core/parser.d.ts +9 -0
- package/dist/core/parser.d.ts.map +1 -0
- package/dist/core/parser.js +120 -0
- package/dist/core/parser.js.map +1 -0
- package/dist/core/recording-intelligence.d.ts +15 -0
- package/dist/core/recording-intelligence.d.ts.map +1 -0
- package/dist/core/recording-intelligence.js +178 -0
- package/dist/core/recording-intelligence.js.map +1 -0
- package/dist/core/resolver.d.ts +58 -0
- package/dist/core/resolver.d.ts.map +1 -0
- package/dist/core/resolver.js +291 -0
- package/dist/core/resolver.js.map +1 -0
- package/dist/core/scanner.d.ts +51 -0
- package/dist/core/scanner.d.ts.map +1 -0
- package/dist/core/scanner.js +310 -0
- package/dist/core/scanner.js.map +1 -0
- package/dist/core/scorer.d.ts +8 -0
- package/dist/core/scorer.d.ts.map +1 -0
- package/dist/core/scorer.js +76 -0
- package/dist/core/scorer.js.map +1 -0
- package/dist/core/validator.d.ts +134 -0
- package/dist/core/validator.d.ts.map +1 -0
- package/dist/core/validator.js +44 -0
- package/dist/core/validator.js.map +1 -0
- package/dist/core/verifier.d.ts +10 -0
- package/dist/core/verifier.d.ts.map +1 -0
- package/dist/core/verifier.js +30 -0
- package/dist/core/verifier.js.map +1 -0
- package/dist/core/writer.d.ts +15 -0
- package/dist/core/writer.d.ts.map +1 -0
- package/dist/core/writer.js +43 -0
- package/dist/core/writer.js.map +1 -0
- package/dist/generator/mocks/builder.d.ts +47 -0
- package/dist/generator/mocks/builder.d.ts.map +1 -0
- package/dist/generator/mocks/builder.js +335 -0
- package/dist/generator/mocks/builder.js.map +1 -0
- package/dist/generator/transforms/dialog-transform.d.ts +35 -0
- package/dist/generator/transforms/dialog-transform.d.ts.map +1 -0
- package/dist/generator/transforms/dialog-transform.js +293 -0
- package/dist/generator/transforms/dialog-transform.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/learner/analyzer.d.ts +13 -0
- package/dist/learner/analyzer.d.ts.map +1 -0
- package/dist/learner/analyzer.js +484 -0
- package/dist/learner/analyzer.js.map +1 -0
- package/dist/learner/index.d.ts +66 -0
- package/dist/learner/index.d.ts.map +1 -0
- package/dist/learner/index.js +247 -0
- package/dist/learner/index.js.map +1 -0
- package/dist/learner/storage.d.ts +68 -0
- package/dist/learner/storage.d.ts.map +1 -0
- package/dist/learner/storage.js +201 -0
- package/dist/learner/storage.js.map +1 -0
- package/dist/learner/types.d.ts +41 -0
- package/dist/learner/types.d.ts.map +1 -0
- package/dist/learner/types.js +31 -0
- package/dist/learner/types.js.map +1 -0
- package/dist/parser/recorder-parser.d.ts +40 -0
- package/dist/parser/recorder-parser.d.ts.map +1 -0
- package/dist/parser/recorder-parser.js +139 -0
- package/dist/parser/recorder-parser.js.map +1 -0
- package/dist/parser/steps/deduplicator.d.ts +19 -0
- package/dist/parser/steps/deduplicator.d.ts.map +1 -0
- package/dist/parser/steps/deduplicator.js +75 -0
- package/dist/parser/steps/deduplicator.js.map +1 -0
- package/dist/parser/steps/dialog-detector.d.ts +38 -0
- package/dist/parser/steps/dialog-detector.d.ts.map +1 -0
- package/dist/parser/steps/dialog-detector.js +290 -0
- package/dist/parser/steps/dialog-detector.js.map +1 -0
- package/dist/parser/steps/noise-filter.d.ts +21 -0
- package/dist/parser/steps/noise-filter.d.ts.map +1 -0
- package/dist/parser/steps/noise-filter.js +138 -0
- package/dist/parser/steps/noise-filter.js.map +1 -0
- package/dist/scorer/index.d.ts +43 -0
- package/dist/scorer/index.d.ts.map +1 -0
- package/dist/scorer/index.js +82 -0
- package/dist/scorer/index.js.map +1 -0
- package/dist/scorer/post-verify.d.ts +17 -0
- package/dist/scorer/post-verify.d.ts.map +1 -0
- package/dist/scorer/post-verify.js +163 -0
- package/dist/scorer/post-verify.js.map +1 -0
- package/dist/scorer/pre-audit.d.ts +32 -0
- package/dist/scorer/pre-audit.d.ts.map +1 -0
- package/dist/scorer/pre-audit.js +99 -0
- package/dist/scorer/pre-audit.js.map +1 -0
- package/dist/scorer/quality-gates.d.ts +17 -0
- package/dist/scorer/quality-gates.d.ts.map +1 -0
- package/dist/scorer/quality-gates.js +304 -0
- package/dist/scorer/quality-gates.js.map +1 -0
- package/dist/scorer/types.d.ts +27 -0
- package/dist/scorer/types.d.ts.map +1 -0
- package/dist/scorer/types.js +5 -0
- package/dist/scorer/types.js.map +1 -0
- package/dist/templates/test-template.d.ts +21 -0
- package/dist/templates/test-template.d.ts.map +1 -0
- package/dist/templates/test-template.js +92 -0
- package/dist/templates/test-template.js.map +1 -0
- package/dist/types/conventions.d.ts +49 -0
- package/dist/types/conventions.d.ts.map +1 -0
- package/dist/types/conventions.js +13 -0
- package/dist/types/conventions.js.map +1 -0
- package/dist/types/recording.d.ts +143 -0
- package/dist/types/recording.d.ts.map +1 -0
- package/dist/types/recording.js +5 -0
- package/dist/types/recording.js.map +1 -0
- package/dist/types/score.d.ts +18 -0
- package/dist/types/score.d.ts.map +1 -0
- package/dist/types/score.js +2 -0
- package/dist/types/score.js.map +1 -0
- package/package.json +51 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RTL test code generation
|
|
3
|
+
* Converts NormalizedRecording into valid React Testing Library test code.
|
|
4
|
+
*
|
|
5
|
+
* Query priority (accessibility-first):
|
|
6
|
+
* getByRole > getByLabelText > getByText > getByPlaceholderText > getByTestId
|
|
7
|
+
*/
|
|
8
|
+
import { importBlock, describeBlock, stepTemplate, describeBlockMultiIt, } from '../templates/test-template.js';
|
|
9
|
+
import pc from 'picocolors';
|
|
10
|
+
/** Convert a CSS selector to an RTL screen query string. */
|
|
11
|
+
function selectorToQuery(selector) {
|
|
12
|
+
if (!selector)
|
|
13
|
+
return 'document.body';
|
|
14
|
+
// data-testid attribute
|
|
15
|
+
const testIdMatch = selector.match(/\[data-testid=['"]?([^'"[\]]+)['"]?\]/);
|
|
16
|
+
if (testIdMatch)
|
|
17
|
+
return `screen.getByTestId('${testIdMatch[1]}')`;
|
|
18
|
+
// aria-label attribute
|
|
19
|
+
const ariaLabelMatch = selector.match(/\[aria-label=['"]?([^'"[\]]+)['"]?\]/);
|
|
20
|
+
if (ariaLabelMatch)
|
|
21
|
+
return `screen.getByLabelText('${ariaLabelMatch[1]}')`;
|
|
22
|
+
// aria-labelledby falls back to getByLabelText with regex
|
|
23
|
+
if (selector.includes('[aria-labelledby')) {
|
|
24
|
+
return `screen.getByLabelText(/* aria-labelledby */ /./)`;
|
|
25
|
+
}
|
|
26
|
+
// Element-level role inference
|
|
27
|
+
if (/(?:^|[\s>])button(?:[^a-z]|$)|\[type=['"]?(?:button|submit)['"]?\]/.test(selector)) {
|
|
28
|
+
return `screen.getByRole('button')`;
|
|
29
|
+
}
|
|
30
|
+
if (/(?:^|[\s>])a(?:[^a-z]|$)/.test(selector)) {
|
|
31
|
+
return `screen.getByRole('link')`;
|
|
32
|
+
}
|
|
33
|
+
if (/\[type=['"]?checkbox['"]?\]/.test(selector)) {
|
|
34
|
+
return `screen.getByRole('checkbox')`;
|
|
35
|
+
}
|
|
36
|
+
if (/\[type=['"]?radio['"]?\]/.test(selector)) {
|
|
37
|
+
return `screen.getByRole('radio')`;
|
|
38
|
+
}
|
|
39
|
+
if (/(?:^|[\s>])select(?:[^a-z]|$)/.test(selector)) {
|
|
40
|
+
return `screen.getByRole('combobox')`;
|
|
41
|
+
}
|
|
42
|
+
if (/(?:^|[\s>])input(?:[^a-z]|$)|\[type=['"]?(?:text|email|password|search|tel|url)['"]?\]/.test(selector)) {
|
|
43
|
+
return `screen.getByRole('textbox')`;
|
|
44
|
+
}
|
|
45
|
+
if (/(?:^|[\s>])textarea(?:[^a-z]|$)/.test(selector)) {
|
|
46
|
+
return `screen.getByRole('textbox')`;
|
|
47
|
+
}
|
|
48
|
+
if (/(?:^|[\s>])h[1-6](?:[^a-z]|$)/.test(selector)) {
|
|
49
|
+
return `screen.getByRole('heading')`;
|
|
50
|
+
}
|
|
51
|
+
if (/(?:^|[\s>])img(?:[^a-z]|$)/.test(selector)) {
|
|
52
|
+
return `screen.getByRole('img')`;
|
|
53
|
+
}
|
|
54
|
+
// Last resort: escape the selector and use as getByTestId placeholder
|
|
55
|
+
const escaped = selector.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
56
|
+
return `screen.getByTestId(/* TODO: replace with RTL query — CSS: '${escaped}' */ '')`;
|
|
57
|
+
}
|
|
58
|
+
function isQueryExpression(target) {
|
|
59
|
+
return /^(screen|document)\./.test(target);
|
|
60
|
+
}
|
|
61
|
+
function looksLikeCssSelector(target) {
|
|
62
|
+
return (/^[#.[]/.test(target) ||
|
|
63
|
+
/^[a-z][a-z0-9-]*(?:[.#[:\s>])/i.test(target) ||
|
|
64
|
+
/^(button|input|select|textarea|a|img|h[1-6])$/i.test(target));
|
|
65
|
+
}
|
|
66
|
+
function reconstructQuery(step) {
|
|
67
|
+
const target = step.target;
|
|
68
|
+
if (!target) {
|
|
69
|
+
return 'document.body';
|
|
70
|
+
}
|
|
71
|
+
if (isQueryExpression(target)) {
|
|
72
|
+
return target;
|
|
73
|
+
}
|
|
74
|
+
if (step.source === 'js' && step.action === 'assert' && step.originalType.startsWith('getBy')) {
|
|
75
|
+
const escapedTarget = target.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
76
|
+
return step.originalType === 'getByRole'
|
|
77
|
+
? `screen.getByRole('${escapedTarget}')`
|
|
78
|
+
: `screen.${step.originalType}('${escapedTarget}')`;
|
|
79
|
+
}
|
|
80
|
+
if (looksLikeCssSelector(target)) {
|
|
81
|
+
return selectorToQuery(target);
|
|
82
|
+
}
|
|
83
|
+
const escapedTarget = target.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
84
|
+
return `screen.getByText('${escapedTarget}')`;
|
|
85
|
+
}
|
|
86
|
+
function generateStepCode(step) {
|
|
87
|
+
// navigate steps use target (the URL), not the CSS-selector path
|
|
88
|
+
if (step.action === 'navigate') {
|
|
89
|
+
return stepTemplate({ action: 'navigate', query: '', value: step.target });
|
|
90
|
+
}
|
|
91
|
+
const query = selectorToQuery(step.target);
|
|
92
|
+
return stepTemplate({ action: step.action, query, value: step.value });
|
|
93
|
+
}
|
|
94
|
+
export function generateTest(recording, options = {}) {
|
|
95
|
+
const testName = recording.title || 'Generated Test';
|
|
96
|
+
const hasUserEvents = recording.steps.some((s) => ['click', 'fill', 'select', 'keyDown'].includes(s.action));
|
|
97
|
+
const stepLines = recording.steps.map((step) => generateStepCode(step));
|
|
98
|
+
const imports = importBlock(hasUserEvents);
|
|
99
|
+
const describe = describeBlock(testName, stepLines, hasUserEvents);
|
|
100
|
+
const code = `${imports}\n\n${describe}\n`;
|
|
101
|
+
return {
|
|
102
|
+
code,
|
|
103
|
+
testName,
|
|
104
|
+
filePath: options.outputPath,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export function emitQuerySummary(queryResults) {
|
|
108
|
+
if (queryResults.length === 0)
|
|
109
|
+
return;
|
|
110
|
+
// Group by method name
|
|
111
|
+
const grouped = new Map();
|
|
112
|
+
for (const r of queryResults) {
|
|
113
|
+
const existing = grouped.get(r.method);
|
|
114
|
+
if (existing) {
|
|
115
|
+
grouped.set(r.method, {
|
|
116
|
+
...existing,
|
|
117
|
+
lines: [...existing.lines, ...(r.line !== undefined ? [r.line] : [])],
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
grouped.set(r.method, {
|
|
122
|
+
quality: r.quality,
|
|
123
|
+
lines: r.line !== undefined ? [r.line] : [],
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Emit one line per unique query method
|
|
128
|
+
for (const [method, { quality, lines }] of grouped) {
|
|
129
|
+
const count = queryResults.filter((r) => r.method === method).length;
|
|
130
|
+
const lineInfo = quality === 'fragile' && lines.length > 0
|
|
131
|
+
? ` — see line${lines.length > 1 ? 's' : ''} ${lines.join(', ')}`
|
|
132
|
+
: '';
|
|
133
|
+
console.log(pc.dim('[taro]') +
|
|
134
|
+
` ${count} ${method} (${quality}${lineInfo})`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export function generateTestFromGroups(title, itGroups, options = {}) {
|
|
138
|
+
const { conventions, queryResults = [], outputPath, dryRun } = options;
|
|
139
|
+
const importStyle = conventions?.importStyle ?? 'esm';
|
|
140
|
+
// Determine if any it block uses user events
|
|
141
|
+
const globalHasUserEvents = itGroups.some((group) => group.steps.some((s) => ['click', 'fill', 'select', 'keyDown'].includes(s.action)));
|
|
142
|
+
// Build query -> matcher map for context-aware assert matchers
|
|
143
|
+
const matcherMap = new Map();
|
|
144
|
+
for (const qr of queryResults) {
|
|
145
|
+
if (qr.matcher) {
|
|
146
|
+
matcherMap.set(qr.query, qr.matcher);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Build ItBlockTemplate[] from ItGroup[]
|
|
150
|
+
const itBlocks = itGroups.map((group) => {
|
|
151
|
+
const hasUserEvents = group.steps.some((s) => ['click', 'fill', 'select', 'keyDown'].includes(s.action));
|
|
152
|
+
const stepLines = group.steps.map((step) => {
|
|
153
|
+
if (step.action === 'navigate') {
|
|
154
|
+
return stepTemplate({ action: 'navigate', query: '', value: step.target });
|
|
155
|
+
}
|
|
156
|
+
const query = reconstructQuery(step);
|
|
157
|
+
const matcher = step.action === 'assert' ? matcherMap.get(query) : undefined;
|
|
158
|
+
return stepTemplate({ action: step.action, query, value: step.value, matcher });
|
|
159
|
+
});
|
|
160
|
+
return { name: group.name, stepLines, hasUserEvents };
|
|
161
|
+
});
|
|
162
|
+
const imports = importBlock(globalHasUserEvents, importStyle);
|
|
163
|
+
const describeCode = describeBlockMultiIt(title, itBlocks);
|
|
164
|
+
const code = `${imports}\n\n${describeCode}\n`;
|
|
165
|
+
return {
|
|
166
|
+
code,
|
|
167
|
+
testName: title,
|
|
168
|
+
filePath: outputPath,
|
|
169
|
+
queryResults,
|
|
170
|
+
itGroupCount: itGroups.length,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=generator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generator.js","sourceRoot":"","sources":["../../src/core/generator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Babel AST-based parser for Testing Library Recorder JS output.
|
|
3
|
+
* Parses the JavaScript recording format and produces structured NormalizedRecording.
|
|
4
|
+
*/
|
|
5
|
+
import type { NormalizedStep, QueryQuality, ItGroup } from '../types/recording.js';
|
|
6
|
+
/**
|
|
7
|
+
* Classifies a Testing Library query method by quality tier.
|
|
8
|
+
* @param method - The RTL query method name (e.g., 'getByRole', 'getByText')
|
|
9
|
+
* @returns QueryQuality tier
|
|
10
|
+
*/
|
|
11
|
+
export declare function classifyQuery(method: string): QueryQuality;
|
|
12
|
+
/**
|
|
13
|
+
* Extracts the environment URL from @jest-environment-options comment.
|
|
14
|
+
* @param code - The JS file content
|
|
15
|
+
* @returns The URL string or undefined if not found
|
|
16
|
+
*/
|
|
17
|
+
export declare function extractEnvironmentUrl(code: string): string | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Segments steps into ItGroup[] based on modal boundary detection.
|
|
20
|
+
* Modal boundary: click + assert with matching target name (within 1-2 steps)
|
|
21
|
+
* @param steps - Normalized steps to segment
|
|
22
|
+
* @returns Array of ItGroup
|
|
23
|
+
*/
|
|
24
|
+
export declare function segmentIntoItGroups(steps: NormalizedStep[]): ItGroup[];
|
|
25
|
+
/**
|
|
26
|
+
* Query selector call extracted from AST
|
|
27
|
+
*/
|
|
28
|
+
export interface QuerySelectorCall {
|
|
29
|
+
selector: string;
|
|
30
|
+
line: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Result of parsing a JS recording
|
|
34
|
+
*/
|
|
35
|
+
export interface JsParseResult {
|
|
36
|
+
title: string;
|
|
37
|
+
environmentUrl: string | undefined;
|
|
38
|
+
steps: NormalizedStep[];
|
|
39
|
+
querySelectorCalls: QuerySelectorCall[];
|
|
40
|
+
itGroups: ItGroup[];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Parses a Testing Library Recorder JS file into structured result.
|
|
44
|
+
* @param code - The JavaScript file content
|
|
45
|
+
* @returns Promise<JsParseResult>
|
|
46
|
+
*/
|
|
47
|
+
export declare function parseJsRecording(code: string): Promise<JsParseResult>;
|
|
48
|
+
//# sourceMappingURL=js-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"js-parser.d.ts","sourceRoot":"","sources":["../../src/core/js-parser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAoB,YAAY,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAA;AAmBpG;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAE1D;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAetE;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,OAAO,EAAE,CAuDtE;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;CACb;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,cAAc,EAAE,MAAM,GAAG,SAAS,CAAA;IAClC,KAAK,EAAE,cAAc,EAAE,CAAA;IACvB,kBAAkB,EAAE,iBAAiB,EAAE,CAAA;IACvC,QAAQ,EAAE,OAAO,EAAE,CAAA;CACpB;AAiCD;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAiI3E"}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Babel AST-based parser for Testing Library Recorder JS output.
|
|
3
|
+
* Parses the JavaScript recording format and produces structured NormalizedRecording.
|
|
4
|
+
*/
|
|
5
|
+
import * as babelParser from '@babel/parser';
|
|
6
|
+
import _traverse from '@babel/traverse';
|
|
7
|
+
// ESM interop for @babel/traverse
|
|
8
|
+
const traverse = _traverse.default ?? _traverse;
|
|
9
|
+
/**
|
|
10
|
+
* Quality classification map for RTL query methods
|
|
11
|
+
*/
|
|
12
|
+
const QUERY_QUALITY_MAP = {
|
|
13
|
+
getByRole: 'excellent',
|
|
14
|
+
getByLabelText: 'excellent',
|
|
15
|
+
getByAltText: 'excellent',
|
|
16
|
+
getByTitle: 'acceptable',
|
|
17
|
+
getByText: 'good',
|
|
18
|
+
getByDisplayValue: 'acceptable',
|
|
19
|
+
getByPlaceholderText: 'acceptable',
|
|
20
|
+
getByTestId: 'fragile',
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Classifies a Testing Library query method by quality tier.
|
|
24
|
+
* @param method - The RTL query method name (e.g., 'getByRole', 'getByText')
|
|
25
|
+
* @returns QueryQuality tier
|
|
26
|
+
*/
|
|
27
|
+
export function classifyQuery(method) {
|
|
28
|
+
return QUERY_QUALITY_MAP[method] ?? 'fragile';
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Extracts the environment URL from @jest-environment-options comment.
|
|
32
|
+
* @param code - The JS file content
|
|
33
|
+
* @returns The URL string or undefined if not found
|
|
34
|
+
*/
|
|
35
|
+
export function extractEnvironmentUrl(code) {
|
|
36
|
+
const match = code.match(/@jest-environment-options\s*(\{[^}]+\})/);
|
|
37
|
+
if (!match) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(match[1]);
|
|
42
|
+
if (typeof parsed.url === 'string') {
|
|
43
|
+
return parsed.url;
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Segments steps into ItGroup[] based on modal boundary detection.
|
|
53
|
+
* Modal boundary: click + assert with matching target name (within 1-2 steps)
|
|
54
|
+
* @param steps - Normalized steps to segment
|
|
55
|
+
* @returns Array of ItGroup
|
|
56
|
+
*/
|
|
57
|
+
export function segmentIntoItGroups(steps) {
|
|
58
|
+
if (steps.length === 0) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
const groups = [];
|
|
62
|
+
let current = [];
|
|
63
|
+
for (let i = 0; i < steps.length; i++) {
|
|
64
|
+
const step = steps[i];
|
|
65
|
+
// Check for modal boundary BEFORE adding current step
|
|
66
|
+
// Boundary: current step is click, next step is assert with matching target
|
|
67
|
+
const next = steps[i + 1];
|
|
68
|
+
const isBoundary = step.action === 'click' &&
|
|
69
|
+
step.target &&
|
|
70
|
+
next &&
|
|
71
|
+
next.action === 'assert' &&
|
|
72
|
+
next.target &&
|
|
73
|
+
next.target.toLowerCase().includes(step.target.toLowerCase());
|
|
74
|
+
if (isBoundary) {
|
|
75
|
+
// Check that there's no navigate between click and assert
|
|
76
|
+
const hasNavigateBetween = false; // immediate next step is assert
|
|
77
|
+
if (!hasNavigateBetween) {
|
|
78
|
+
// Close current group (everything before the boundary click)
|
|
79
|
+
if (current.length > 0) {
|
|
80
|
+
groups.push({ name: current[0]?.target ?? 'flow', steps: current });
|
|
81
|
+
}
|
|
82
|
+
// Start new group with the boundary click
|
|
83
|
+
current = [step];
|
|
84
|
+
// Add the assert too
|
|
85
|
+
current.push(next);
|
|
86
|
+
i++; // Skip the assert since we added it
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
current.push(step);
|
|
91
|
+
}
|
|
92
|
+
// Push remaining steps as final group
|
|
93
|
+
if (current.length > 0) {
|
|
94
|
+
groups.push({ name: current[0]?.target ?? 'recorded flow', steps: current });
|
|
95
|
+
}
|
|
96
|
+
// If no boundaries found, return single group with all steps
|
|
97
|
+
if (groups.length === 0 && steps.length > 0) {
|
|
98
|
+
return [{ name: 'recorded flow', steps }];
|
|
99
|
+
}
|
|
100
|
+
return groups;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Extracts a string argument from a Babel AST node
|
|
104
|
+
*/
|
|
105
|
+
function extractStringArg(node) {
|
|
106
|
+
if (!node)
|
|
107
|
+
return undefined;
|
|
108
|
+
if (node.type === 'StringLiteral') {
|
|
109
|
+
return node.value;
|
|
110
|
+
}
|
|
111
|
+
if (node.type === 'TemplateLiteral' && node.quasis.length > 0) {
|
|
112
|
+
return node.quasis[0].value.cooked;
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Maps userEvent method calls to NormalizedAction
|
|
118
|
+
*/
|
|
119
|
+
function mapUserEventCall(method) {
|
|
120
|
+
const mapping = {
|
|
121
|
+
click: 'click',
|
|
122
|
+
dblClick: 'click',
|
|
123
|
+
tripleClick: 'click',
|
|
124
|
+
type: 'fill',
|
|
125
|
+
keyboard: 'keyDown',
|
|
126
|
+
selectOptions: 'select',
|
|
127
|
+
clear: 'fill',
|
|
128
|
+
};
|
|
129
|
+
return mapping[method] ?? 'unknown';
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Parses a Testing Library Recorder JS file into structured result.
|
|
133
|
+
* @param code - The JavaScript file content
|
|
134
|
+
* @returns Promise<JsParseResult>
|
|
135
|
+
*/
|
|
136
|
+
export async function parseJsRecording(code) {
|
|
137
|
+
// Validate: detect JSON input
|
|
138
|
+
const trimmed = code.trim();
|
|
139
|
+
if (trimmed.startsWith('{')) {
|
|
140
|
+
throw new Error('Expected JS file from Testing Library Recorder extension. Got JSON — use the Chrome Recorder JSON parser instead.');
|
|
141
|
+
}
|
|
142
|
+
// Extract environment URL
|
|
143
|
+
const environmentUrl = extractEnvironmentUrl(code);
|
|
144
|
+
// Extract title from file
|
|
145
|
+
let title = 'Recorded Flow';
|
|
146
|
+
const titleMatch = code.match(/\/\*\*\s*\n\s*\*\s*([^@\*]+)/);
|
|
147
|
+
if (titleMatch) {
|
|
148
|
+
title = titleMatch[1].trim();
|
|
149
|
+
// Sanitize: strip date suffix and replace hyphens
|
|
150
|
+
title = title.replace(/\s+at\s+\d{1,2}:\d{2}:\d{2}/, '').replace(/-/g, ' ');
|
|
151
|
+
}
|
|
152
|
+
// Parse with Babel
|
|
153
|
+
const ast = babelParser.parse(code, {
|
|
154
|
+
sourceType: 'commonjs',
|
|
155
|
+
});
|
|
156
|
+
const steps = [];
|
|
157
|
+
const querySelectorCalls = [];
|
|
158
|
+
// Traverse AST
|
|
159
|
+
traverse(ast, {
|
|
160
|
+
CallExpression(path) {
|
|
161
|
+
const callee = path.node.callee;
|
|
162
|
+
const line = path.node.loc?.start?.line ?? 0;
|
|
163
|
+
// Handle screen.getBy* calls
|
|
164
|
+
if (callee.type === 'MemberExpression' &&
|
|
165
|
+
callee.object.type === 'Identifier' &&
|
|
166
|
+
callee.object.name === 'screen' &&
|
|
167
|
+
callee.property.type === 'Identifier') {
|
|
168
|
+
const methodName = callee.property.name;
|
|
169
|
+
if (methodName && methodName.startsWith('getBy')) {
|
|
170
|
+
const target = extractStringArg(path.node.arguments[0]);
|
|
171
|
+
// All screen queries become asserts in this implementation
|
|
172
|
+
steps.push({
|
|
173
|
+
action: 'assert',
|
|
174
|
+
target: target ?? methodName,
|
|
175
|
+
value: undefined,
|
|
176
|
+
originalType: methodName,
|
|
177
|
+
line,
|
|
178
|
+
source: 'js',
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// Handle document.querySelector calls
|
|
183
|
+
if (callee.type === 'MemberExpression' &&
|
|
184
|
+
callee.object.type === 'Identifier' &&
|
|
185
|
+
callee.object.name === 'document' &&
|
|
186
|
+
callee.property.type === 'Identifier' &&
|
|
187
|
+
callee.property.name === 'querySelector') {
|
|
188
|
+
const selector = extractStringArg(path.node.arguments[0]);
|
|
189
|
+
if (selector) {
|
|
190
|
+
querySelectorCalls.push({ selector, line });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Handle userEvent.* calls
|
|
194
|
+
if (callee.type === 'MemberExpression' &&
|
|
195
|
+
callee.object.type === 'Identifier' &&
|
|
196
|
+
callee.object.name === 'userEvent') {
|
|
197
|
+
const methodName = callee.property.name;
|
|
198
|
+
if (methodName) {
|
|
199
|
+
const action = mapUserEventCall(methodName);
|
|
200
|
+
// Extract target from first argument (element reference)
|
|
201
|
+
const target = extractStringArg(path.node.arguments[0]) ?? methodName;
|
|
202
|
+
// Extract value from second argument for type/keyboard
|
|
203
|
+
const value = extractStringArg(path.node.arguments[1]);
|
|
204
|
+
steps.push({
|
|
205
|
+
action,
|
|
206
|
+
target,
|
|
207
|
+
value,
|
|
208
|
+
originalType: methodName,
|
|
209
|
+
line,
|
|
210
|
+
source: 'js',
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Handle await page.goto(url)
|
|
215
|
+
if (callee.type === 'MemberExpression' &&
|
|
216
|
+
callee.object.type === 'Identifier' &&
|
|
217
|
+
callee.object.name === 'page' &&
|
|
218
|
+
callee.property.type === 'Identifier' &&
|
|
219
|
+
callee.property.name === 'goto') {
|
|
220
|
+
const target = extractStringArg(path.node.arguments[0]);
|
|
221
|
+
if (target) {
|
|
222
|
+
steps.push({
|
|
223
|
+
action: 'navigate',
|
|
224
|
+
target,
|
|
225
|
+
value: undefined,
|
|
226
|
+
originalType: 'goto',
|
|
227
|
+
line,
|
|
228
|
+
source: 'js',
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
// Segment into ItGroups
|
|
235
|
+
const itGroups = segmentIntoItGroups(steps);
|
|
236
|
+
return {
|
|
237
|
+
title,
|
|
238
|
+
environmentUrl,
|
|
239
|
+
steps,
|
|
240
|
+
querySelectorCalls,
|
|
241
|
+
itGroups,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
//# sourceMappingURL=js-parser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"js-parser.js","sourceRoot":"","sources":["../../src/core/js-parser.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ConventionsSchema, MockInstabilityWarning, MockRecommendation, MockTargetUsage, MutationLifecyclePattern } from '../types/conventions.js';
|
|
2
|
+
export interface MockAnalysis {
|
|
3
|
+
conventions: ConventionsSchema | null;
|
|
4
|
+
recommendations: MockRecommendation[];
|
|
5
|
+
repeatedTargets: MockTargetUsage[];
|
|
6
|
+
mutationLifecycles: MutationLifecyclePattern[];
|
|
7
|
+
instabilityWarnings: MockInstabilityWarning[];
|
|
8
|
+
}
|
|
9
|
+
export declare function deriveMockRecommendations(targets: MockTargetUsage[]): MockRecommendation[];
|
|
10
|
+
export declare function scanMockTargets(projectRoot: string): Promise<MockTargetUsage[]>;
|
|
11
|
+
export declare function analyzeMutationLifecycle(projectRoot: string): Promise<MutationLifecyclePattern[]>;
|
|
12
|
+
export declare function detectMockInstability(projectRoot: string): Promise<MockInstabilityWarning[]>;
|
|
13
|
+
export declare function analyzeMocks(projectRoot: string): Promise<MockAnalysis>;
|
|
14
|
+
//# sourceMappingURL=mock-intelligence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock-intelligence.d.ts","sourceRoot":"","sources":["../../src/core/mock-intelligence.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,iBAAiB,EACjB,sBAAsB,EACtB,kBAAkB,EAElB,eAAe,EACf,wBAAwB,EAEzB,MAAM,yBAAyB,CAAA;AAGhC,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAAA;IACrC,eAAe,EAAE,kBAAkB,EAAE,CAAA;IACrC,eAAe,EAAE,eAAe,EAAE,CAAA;IAClC,kBAAkB,EAAE,wBAAwB,EAAE,CAAA;IAC9C,mBAAmB,EAAE,sBAAsB,EAAE,CAAA;CAC9C;AA8HD,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,eAAe,EAAE,GACzB,kBAAkB,EAAE,CActB;AAED,wBAAsB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAGrF;AAED,wBAAsB,wBAAwB,CAC5C,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,wBAAwB,EAAE,CAAC,CAGrC;AAED,wBAAsB,qBAAqB,CACzC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,sBAAsB,EAAE,CAAC,CAGnC;AAED,wBAAsB,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAc7E"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { relative } from 'node:path';
|
|
2
|
+
import { readConventions, readTestFiles } from './scanner.js';
|
|
3
|
+
const MOCK_TARGET_REGEX = /(?:vi|jest)\.mock\(\s*['"`]([^'"`]+)['"`]/g;
|
|
4
|
+
const MUTATION_TRIGGER_REGEX = /\b(mutate|mutation|submit|save|create|update|delete)\b|mock(?:Resolved|Rejected)Value(?:Once)?\(/i;
|
|
5
|
+
const TEST_BLOCK_REGEX = /\b(?:it|test)\s*\(/g;
|
|
6
|
+
const TEST_SCOPED_MOCK_REGEX = /(?:vi|jest)\.mock\(/i;
|
|
7
|
+
const MOCK_RESET_REGEX = /(?:vi|jest)\.(?:clearAllMocks|resetAllMocks|restoreAllMocks)\(/g;
|
|
8
|
+
const MOCK_CONFIGURATION_REGEX = /\.mock(?:ResolvedValue|RejectedValue|Implementation|ReturnValue)(?:Once)?\(/g;
|
|
9
|
+
const STAGE_PATTERNS = {
|
|
10
|
+
loading: [/\bisLoading\b/i, /\bloading\b/i, /\bpending\b/i, /\bsubmitting\b/i, /toBeDisabled\(/],
|
|
11
|
+
success: [
|
|
12
|
+
/mockResolvedValue(?:Once)?\(/,
|
|
13
|
+
/\b(success|saved|created|updated|submitted)\b/i,
|
|
14
|
+
/toHaveBeenCalled(?:Times|With)?\(/,
|
|
15
|
+
],
|
|
16
|
+
error: [
|
|
17
|
+
/mockRejectedValue(?:Once)?\(/,
|
|
18
|
+
/throw new Error\(/,
|
|
19
|
+
/\b(error|failed|failure)\b/i,
|
|
20
|
+
/role:\s*['"`]alert['"`]/,
|
|
21
|
+
],
|
|
22
|
+
};
|
|
23
|
+
function extractMockTargets(content) {
|
|
24
|
+
return [...content.matchAll(MOCK_TARGET_REGEX)].map((match) => match[1]);
|
|
25
|
+
}
|
|
26
|
+
function countMatches(content, pattern) {
|
|
27
|
+
return [...content.matchAll(new RegExp(pattern.source, pattern.flags))].length;
|
|
28
|
+
}
|
|
29
|
+
function findStages(content) {
|
|
30
|
+
return Object.entries(STAGE_PATTERNS)
|
|
31
|
+
.filter(([, patterns]) => patterns.some((pattern) => pattern.test(content)))
|
|
32
|
+
.map(([stage]) => stage);
|
|
33
|
+
}
|
|
34
|
+
function scanMockTargetsInFiles(projectRoot, testFiles) {
|
|
35
|
+
const targets = new Map();
|
|
36
|
+
for (const file of testFiles) {
|
|
37
|
+
for (const target of extractMockTargets(file.content)) {
|
|
38
|
+
const files = targets.get(target) ?? new Set();
|
|
39
|
+
files.add(relative(projectRoot, file.path));
|
|
40
|
+
targets.set(target, files);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return [...targets.entries()]
|
|
44
|
+
.map(([target, files]) => ({
|
|
45
|
+
target,
|
|
46
|
+
files: [...files].sort(),
|
|
47
|
+
count: files.size,
|
|
48
|
+
}))
|
|
49
|
+
.sort((left, right) => right.count - left.count || left.target.localeCompare(right.target));
|
|
50
|
+
}
|
|
51
|
+
function analyzeMutationLifecycleInFiles(projectRoot, testFiles) {
|
|
52
|
+
return testFiles
|
|
53
|
+
.filter((file) => MUTATION_TRIGGER_REGEX.test(file.content))
|
|
54
|
+
.map((file) => {
|
|
55
|
+
const stages = findStages(file.content);
|
|
56
|
+
if (stages.length < 2) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
file: relative(projectRoot, file.path),
|
|
61
|
+
stages,
|
|
62
|
+
evidence: stages.map((stage) => `${stage} cues detected`),
|
|
63
|
+
};
|
|
64
|
+
})
|
|
65
|
+
.filter((entry) => entry !== null)
|
|
66
|
+
.sort((left, right) => left.file.localeCompare(right.file));
|
|
67
|
+
}
|
|
68
|
+
function detectMockInstabilityInFiles(projectRoot, testFiles) {
|
|
69
|
+
const warnings = [];
|
|
70
|
+
for (const file of testFiles) {
|
|
71
|
+
const relativePath = relative(projectRoot, file.path);
|
|
72
|
+
const testBodies = file.content.split(TEST_BLOCK_REGEX).slice(1);
|
|
73
|
+
const scopedMockCount = testBodies.filter((body) => TEST_SCOPED_MOCK_REGEX.test(body)).length;
|
|
74
|
+
if (scopedMockCount > 0) {
|
|
75
|
+
warnings.push({
|
|
76
|
+
file: relativePath,
|
|
77
|
+
kind: 'recreated-factory',
|
|
78
|
+
reason: 'Mocks are declared inside test bodies and may recreate factories per test run',
|
|
79
|
+
evidence: [`${scopedMockCount} test block(s) declare vi.mock/jest.mock`],
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const resetCount = countMatches(file.content, MOCK_RESET_REGEX);
|
|
83
|
+
const configCount = countMatches(file.content, MOCK_CONFIGURATION_REGEX);
|
|
84
|
+
if (resetCount > 0 && configCount >= 2) {
|
|
85
|
+
warnings.push({
|
|
86
|
+
file: relativePath,
|
|
87
|
+
kind: 'per-test-churn',
|
|
88
|
+
reason: 'Mock configuration is reset and redefined repeatedly across tests',
|
|
89
|
+
evidence: [
|
|
90
|
+
`${resetCount} resetAll/clearAll/restoreAll call(s)`,
|
|
91
|
+
`${configCount} mock configuration call(s)`,
|
|
92
|
+
],
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return warnings.sort((left, right) => {
|
|
97
|
+
return left.file.localeCompare(right.file) || left.kind.localeCompare(right.kind);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
export function deriveMockRecommendations(targets) {
|
|
101
|
+
return targets.map((target) => {
|
|
102
|
+
const kind = target.count >= 2 ? 'extract' : 'inline';
|
|
103
|
+
return {
|
|
104
|
+
count: target.count,
|
|
105
|
+
files: target.files,
|
|
106
|
+
kind,
|
|
107
|
+
reason: kind === 'extract'
|
|
108
|
+
? 'Mock target appears in multiple tests and should be shared'
|
|
109
|
+
: 'Mock target appears in one place and can stay local to the test',
|
|
110
|
+
target: target.target,
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
export async function scanMockTargets(projectRoot) {
|
|
115
|
+
const testFiles = await readTestFiles(projectRoot);
|
|
116
|
+
return scanMockTargetsInFiles(projectRoot, testFiles);
|
|
117
|
+
}
|
|
118
|
+
export async function analyzeMutationLifecycle(projectRoot) {
|
|
119
|
+
const testFiles = await readTestFiles(projectRoot);
|
|
120
|
+
return analyzeMutationLifecycleInFiles(projectRoot, testFiles);
|
|
121
|
+
}
|
|
122
|
+
export async function detectMockInstability(projectRoot) {
|
|
123
|
+
const testFiles = await readTestFiles(projectRoot);
|
|
124
|
+
return detectMockInstabilityInFiles(projectRoot, testFiles);
|
|
125
|
+
}
|
|
126
|
+
export async function analyzeMocks(projectRoot) {
|
|
127
|
+
const testFiles = await readTestFiles(projectRoot);
|
|
128
|
+
const [conventions] = await Promise.all([readConventions(projectRoot)]);
|
|
129
|
+
const targets = scanMockTargetsInFiles(projectRoot, testFiles);
|
|
130
|
+
const mutationLifecycles = analyzeMutationLifecycleInFiles(projectRoot, testFiles);
|
|
131
|
+
const instabilityWarnings = detectMockInstabilityInFiles(projectRoot, testFiles);
|
|
132
|
+
return {
|
|
133
|
+
conventions,
|
|
134
|
+
recommendations: deriveMockRecommendations(targets),
|
|
135
|
+
repeatedTargets: targets.filter((target) => target.count > 1),
|
|
136
|
+
mutationLifecycles,
|
|
137
|
+
instabilityWarnings,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=mock-intelligence.js.map
|