@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.
Files changed (150) hide show
  1. package/README.md +250 -0
  2. package/dist/analyzer/mocks/detector.d.ts +59 -0
  3. package/dist/analyzer/mocks/detector.d.ts.map +1 -0
  4. package/dist/analyzer/mocks/detector.js +264 -0
  5. package/dist/analyzer/mocks/detector.js.map +1 -0
  6. package/dist/analyzer/mocks/target-analyzer.d.ts +92 -0
  7. package/dist/analyzer/mocks/target-analyzer.d.ts.map +1 -0
  8. package/dist/analyzer/mocks/target-analyzer.js +305 -0
  9. package/dist/analyzer/mocks/target-analyzer.js.map +1 -0
  10. package/dist/analyzer/visual/element-analyzer.d.ts +44 -0
  11. package/dist/analyzer/visual/element-analyzer.d.ts.map +1 -0
  12. package/dist/analyzer/visual/element-analyzer.js +176 -0
  13. package/dist/analyzer/visual/element-analyzer.js.map +1 -0
  14. package/dist/analyzer/visual/inspector.d.ts +49 -0
  15. package/dist/analyzer/visual/inspector.d.ts.map +1 -0
  16. package/dist/analyzer/visual/inspector.js +109 -0
  17. package/dist/analyzer/visual/inspector.js.map +1 -0
  18. package/dist/cli/commands/generate.d.ts +13 -0
  19. package/dist/cli/commands/generate.d.ts.map +1 -0
  20. package/dist/cli/commands/generate.js +417 -0
  21. package/dist/cli/commands/generate.js.map +1 -0
  22. package/dist/core/generator.d.ts +32 -0
  23. package/dist/core/generator.d.ts.map +1 -0
  24. package/dist/core/generator.js +173 -0
  25. package/dist/core/generator.js.map +1 -0
  26. package/dist/core/js-parser.d.ts +48 -0
  27. package/dist/core/js-parser.d.ts.map +1 -0
  28. package/dist/core/js-parser.js +244 -0
  29. package/dist/core/js-parser.js.map +1 -0
  30. package/dist/core/mock-intelligence.d.ts +14 -0
  31. package/dist/core/mock-intelligence.d.ts.map +1 -0
  32. package/dist/core/mock-intelligence.js +140 -0
  33. package/dist/core/mock-intelligence.js.map +1 -0
  34. package/dist/core/orchestrator.d.ts +49 -0
  35. package/dist/core/orchestrator.d.ts.map +1 -0
  36. package/dist/core/orchestrator.js +315 -0
  37. package/dist/core/orchestrator.js.map +1 -0
  38. package/dist/core/parser.d.ts +9 -0
  39. package/dist/core/parser.d.ts.map +1 -0
  40. package/dist/core/parser.js +120 -0
  41. package/dist/core/parser.js.map +1 -0
  42. package/dist/core/recording-intelligence.d.ts +15 -0
  43. package/dist/core/recording-intelligence.d.ts.map +1 -0
  44. package/dist/core/recording-intelligence.js +178 -0
  45. package/dist/core/recording-intelligence.js.map +1 -0
  46. package/dist/core/resolver.d.ts +58 -0
  47. package/dist/core/resolver.d.ts.map +1 -0
  48. package/dist/core/resolver.js +291 -0
  49. package/dist/core/resolver.js.map +1 -0
  50. package/dist/core/scanner.d.ts +51 -0
  51. package/dist/core/scanner.d.ts.map +1 -0
  52. package/dist/core/scanner.js +310 -0
  53. package/dist/core/scanner.js.map +1 -0
  54. package/dist/core/scorer.d.ts +8 -0
  55. package/dist/core/scorer.d.ts.map +1 -0
  56. package/dist/core/scorer.js +76 -0
  57. package/dist/core/scorer.js.map +1 -0
  58. package/dist/core/validator.d.ts +134 -0
  59. package/dist/core/validator.d.ts.map +1 -0
  60. package/dist/core/validator.js +44 -0
  61. package/dist/core/validator.js.map +1 -0
  62. package/dist/core/verifier.d.ts +10 -0
  63. package/dist/core/verifier.d.ts.map +1 -0
  64. package/dist/core/verifier.js +30 -0
  65. package/dist/core/verifier.js.map +1 -0
  66. package/dist/core/writer.d.ts +15 -0
  67. package/dist/core/writer.d.ts.map +1 -0
  68. package/dist/core/writer.js +43 -0
  69. package/dist/core/writer.js.map +1 -0
  70. package/dist/generator/mocks/builder.d.ts +47 -0
  71. package/dist/generator/mocks/builder.d.ts.map +1 -0
  72. package/dist/generator/mocks/builder.js +335 -0
  73. package/dist/generator/mocks/builder.js.map +1 -0
  74. package/dist/generator/transforms/dialog-transform.d.ts +35 -0
  75. package/dist/generator/transforms/dialog-transform.d.ts.map +1 -0
  76. package/dist/generator/transforms/dialog-transform.js +293 -0
  77. package/dist/generator/transforms/dialog-transform.js.map +1 -0
  78. package/dist/index.d.ts +7 -0
  79. package/dist/index.d.ts.map +1 -0
  80. package/dist/index.js +18 -0
  81. package/dist/index.js.map +1 -0
  82. package/dist/learner/analyzer.d.ts +13 -0
  83. package/dist/learner/analyzer.d.ts.map +1 -0
  84. package/dist/learner/analyzer.js +484 -0
  85. package/dist/learner/analyzer.js.map +1 -0
  86. package/dist/learner/index.d.ts +66 -0
  87. package/dist/learner/index.d.ts.map +1 -0
  88. package/dist/learner/index.js +247 -0
  89. package/dist/learner/index.js.map +1 -0
  90. package/dist/learner/storage.d.ts +68 -0
  91. package/dist/learner/storage.d.ts.map +1 -0
  92. package/dist/learner/storage.js +201 -0
  93. package/dist/learner/storage.js.map +1 -0
  94. package/dist/learner/types.d.ts +41 -0
  95. package/dist/learner/types.d.ts.map +1 -0
  96. package/dist/learner/types.js +31 -0
  97. package/dist/learner/types.js.map +1 -0
  98. package/dist/parser/recorder-parser.d.ts +40 -0
  99. package/dist/parser/recorder-parser.d.ts.map +1 -0
  100. package/dist/parser/recorder-parser.js +139 -0
  101. package/dist/parser/recorder-parser.js.map +1 -0
  102. package/dist/parser/steps/deduplicator.d.ts +19 -0
  103. package/dist/parser/steps/deduplicator.d.ts.map +1 -0
  104. package/dist/parser/steps/deduplicator.js +75 -0
  105. package/dist/parser/steps/deduplicator.js.map +1 -0
  106. package/dist/parser/steps/dialog-detector.d.ts +38 -0
  107. package/dist/parser/steps/dialog-detector.d.ts.map +1 -0
  108. package/dist/parser/steps/dialog-detector.js +290 -0
  109. package/dist/parser/steps/dialog-detector.js.map +1 -0
  110. package/dist/parser/steps/noise-filter.d.ts +21 -0
  111. package/dist/parser/steps/noise-filter.d.ts.map +1 -0
  112. package/dist/parser/steps/noise-filter.js +138 -0
  113. package/dist/parser/steps/noise-filter.js.map +1 -0
  114. package/dist/scorer/index.d.ts +43 -0
  115. package/dist/scorer/index.d.ts.map +1 -0
  116. package/dist/scorer/index.js +82 -0
  117. package/dist/scorer/index.js.map +1 -0
  118. package/dist/scorer/post-verify.d.ts +17 -0
  119. package/dist/scorer/post-verify.d.ts.map +1 -0
  120. package/dist/scorer/post-verify.js +163 -0
  121. package/dist/scorer/post-verify.js.map +1 -0
  122. package/dist/scorer/pre-audit.d.ts +32 -0
  123. package/dist/scorer/pre-audit.d.ts.map +1 -0
  124. package/dist/scorer/pre-audit.js +99 -0
  125. package/dist/scorer/pre-audit.js.map +1 -0
  126. package/dist/scorer/quality-gates.d.ts +17 -0
  127. package/dist/scorer/quality-gates.d.ts.map +1 -0
  128. package/dist/scorer/quality-gates.js +304 -0
  129. package/dist/scorer/quality-gates.js.map +1 -0
  130. package/dist/scorer/types.d.ts +27 -0
  131. package/dist/scorer/types.d.ts.map +1 -0
  132. package/dist/scorer/types.js +5 -0
  133. package/dist/scorer/types.js.map +1 -0
  134. package/dist/templates/test-template.d.ts +21 -0
  135. package/dist/templates/test-template.d.ts.map +1 -0
  136. package/dist/templates/test-template.js +92 -0
  137. package/dist/templates/test-template.js.map +1 -0
  138. package/dist/types/conventions.d.ts +49 -0
  139. package/dist/types/conventions.d.ts.map +1 -0
  140. package/dist/types/conventions.js +13 -0
  141. package/dist/types/conventions.js.map +1 -0
  142. package/dist/types/recording.d.ts +143 -0
  143. package/dist/types/recording.d.ts.map +1 -0
  144. package/dist/types/recording.js +5 -0
  145. package/dist/types/recording.js.map +1 -0
  146. package/dist/types/score.d.ts +18 -0
  147. package/dist/types/score.d.ts.map +1 -0
  148. package/dist/types/score.js +2 -0
  149. package/dist/types/score.js.map +1 -0
  150. 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