@auto-engineer/component-implementor-react 1.101.0 → 1.103.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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +5 -5
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +74 -0
- package/dist/src/commands/implement-component.d.ts.map +1 -1
- package/dist/src/commands/implement-component.js +4 -0
- package/dist/src/commands/implement-component.js.map +1 -1
- package/dist/src/commands/implement-component.test.js +52 -0
- package/dist/src/commands/implement-component.test.js.map +1 -1
- package/dist/src/pipeline/run-pipeline.d.ts +8 -0
- package/dist/src/pipeline/run-pipeline.d.ts.map +1 -1
- package/dist/src/pipeline/run-pipeline.js +8 -0
- package/dist/src/pipeline/run-pipeline.js.map +1 -1
- package/dist/src/pipeline/run-pipeline.test.js +39 -3
- package/dist/src/pipeline/run-pipeline.test.js.map +1 -1
- package/dist/src/pipeline/steps/generate-component.test.js +4 -0
- package/dist/src/pipeline/steps/generate-component.test.js.map +1 -1
- package/dist/src/pipeline/steps/generate-story.test.js +4 -0
- package/dist/src/pipeline/steps/generate-story.test.js.map +1 -1
- package/dist/src/pipeline/steps/generate-test.test.js +4 -0
- package/dist/src/pipeline/steps/generate-test.test.js.map +1 -1
- package/dist/src/pipeline/steps/lint-fix-loop.d.ts.map +1 -1
- package/dist/src/pipeline/steps/lint-fix-loop.js +4 -3
- package/dist/src/pipeline/steps/lint-fix-loop.js.map +1 -1
- package/dist/src/pipeline/steps/lint-fix-loop.test.js +17 -0
- package/dist/src/pipeline/steps/lint-fix-loop.test.js.map +1 -1
- package/dist/src/pipeline/steps/story-fix-loop.d.ts.map +1 -1
- package/dist/src/pipeline/steps/story-fix-loop.js +1 -0
- package/dist/src/pipeline/steps/story-fix-loop.js.map +1 -1
- package/dist/src/pipeline/steps/story-fix-loop.test.js +4 -0
- package/dist/src/pipeline/steps/story-fix-loop.test.js.map +1 -1
- package/dist/src/pipeline/steps/storybook-test.test.js +4 -0
- package/dist/src/pipeline/steps/storybook-test.test.js.map +1 -1
- package/dist/src/pipeline/steps/test-fix-loop.d.ts.map +1 -1
- package/dist/src/pipeline/steps/test-fix-loop.js +1 -0
- package/dist/src/pipeline/steps/test-fix-loop.js.map +1 -1
- package/dist/src/pipeline/steps/test-fix-loop.test.js +4 -0
- package/dist/src/pipeline/steps/test-fix-loop.test.js.map +1 -1
- package/dist/src/pipeline/steps/type-fix-loop.d.ts.map +1 -1
- package/dist/src/pipeline/steps/type-fix-loop.js +1 -0
- package/dist/src/pipeline/steps/type-fix-loop.js.map +1 -1
- package/dist/src/pipeline/steps/type-fix-loop.test.js +4 -0
- package/dist/src/pipeline/steps/type-fix-loop.test.js.map +1 -1
- package/dist/src/prompt.d.ts +3 -3
- package/dist/src/prompt.d.ts.map +1 -1
- package/dist/src/prompt.js +29 -9
- package/dist/src/prompt.js.map +1 -1
- package/dist/src/prompt.test.js +143 -0
- package/dist/src/prompt.test.js.map +1 -1
- package/dist/src/tools/lint-runner.d.ts.map +1 -1
- package/dist/src/tools/lint-runner.js +9 -4
- package/dist/src/tools/lint-runner.js.map +1 -1
- package/dist/src/tools/lint-runner.test.js +3 -7
- package/dist/src/tools/lint-runner.test.js.map +1 -1
- package/dist/src/tools/test-runner.js +8 -4
- package/dist/src/tools/test-runner.js.map +1 -1
- package/dist/src/tools/test-runner.test.js +49 -3
- package/dist/src/tools/test-runner.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/scripts/generate-all.ts +673 -0
- package/src/commands/implement-component.test.ts +52 -0
- package/src/commands/implement-component.ts +4 -0
- package/src/pipeline/run-pipeline.test.ts +39 -3
- package/src/pipeline/run-pipeline.ts +16 -0
- package/src/pipeline/steps/generate-component.test.ts +4 -0
- package/src/pipeline/steps/generate-story.test.ts +4 -0
- package/src/pipeline/steps/generate-test.test.ts +4 -0
- package/src/pipeline/steps/lint-fix-loop.test.ts +21 -0
- package/src/pipeline/steps/lint-fix-loop.ts +5 -3
- package/src/pipeline/steps/story-fix-loop.test.ts +4 -0
- package/src/pipeline/steps/story-fix-loop.ts +1 -0
- package/src/pipeline/steps/storybook-test.test.ts +4 -0
- package/src/pipeline/steps/test-fix-loop.test.ts +4 -0
- package/src/pipeline/steps/test-fix-loop.ts +1 -0
- package/src/pipeline/steps/type-fix-loop.test.ts +4 -0
- package/src/pipeline/steps/type-fix-loop.ts +1 -0
- package/src/prompt.test.ts +176 -0
- package/src/prompt.ts +29 -9
- package/src/tools/lint-runner.test.ts +6 -7
- package/src/tools/lint-runner.ts +10 -4
- package/src/tools/test-runner.test.ts +55 -3
- package/src/tools/test-runner.ts +8 -4
package/src/prompt.ts
CHANGED
|
@@ -90,7 +90,12 @@ RULES
|
|
|
90
90
|
10. **Import paths.** Import composed UI components using the exact paths shown in the **Composed Components** section of the project context. Import utility functions from \`@/lib/utils\` (e.g. \`import { cn } from '@/lib/utils'\`). NEVER use made-up package names like \`'your-design-system'\`, \`'@company/ui'\`, or \`'@acme/components'\`. NEVER guess import paths — only use paths explicitly provided in the prompt.
|
|
91
91
|
11. **Code only.** Return ONLY the component file code. No markdown fences, no commentary, no file names.
|
|
92
92
|
12. **Numeric formatting.** Use the \`tabular-nums\` Tailwind class on all numeric displays (prices, counts, statistics). Use \`Intl.NumberFormat\` for currency formatting rather than string template literals with \`$\`.
|
|
93
|
-
13. **Type imports for composed component props.** When you need to reference a composed component's prop type (e.g. to type an array of items), use \`import type { XProps } from '...'\` with the exact import path from the Composed Components section. Never reference a type without importing it
|
|
93
|
+
13. **Type imports for composed component props.** When you need to reference a composed component's prop type (e.g. to type an array of items), use \`import type { XProps } from '...'\` with the exact import path from the Composed Components section. Never reference a type without importing it.
|
|
94
|
+
14. **Strict package boundary.** You may ONLY import from packages listed in the project's \`package.json\` dependencies/devDependencies and from local \`@/\` paths shown in the project context. Do NOT import from any other npm package — even well-known ones. If the spec implies behavior that would normally require an external package (routing, data fetching, animations, icons), implement it with plain React, callback props, and CSS/Tailwind instead.
|
|
95
|
+
15. **UI components from barrel only.** Import UI components ONLY from \`@/components/ui\` using the exact export names shown in the barrel file (\`src/components/ui/index.ts\`). Do NOT import UI components that aren't listed in the barrel. Do NOT guess component names — if a UI component isn't in the barrel, build the equivalent with plain HTML + Tailwind.
|
|
96
|
+
16. **Page components use callback props for navigation.** Never import a router library. Implement navigation via callback props (e.g. \`onNavigate?: (path: string) => void\`) and render \`<a>\` tags or \`<button>\` elements that call the callback. The project does not include a router.
|
|
97
|
+
17. **Controlled input initialization.** When using \`useState\` for form input values, always initialize text/number inputs with an empty string (\`useState('')\`), never \`undefined\` or \`null\`. Inputs with \`value={undefined}\` become uncontrolled and return \`null\` from \`.value\`, which breaks \`toHaveValue('')\` assertions.
|
|
98
|
+
18. **Array keys without indices.** Never use the \`.map()\` callback index parameter as a React key — not directly (\`key={i}\`) nor inside a template literal (\`key={\\\`item-\\\${i}\\\`}\`). Biome's \`noArrayIndexKey\` rule flags both forms. For data items, use a unique property (\`key={item.id}\`). For generated placeholders (skeletons, spacers), pre-compute an ID array outside the JSX: \`const ids = Array.from({ length: count }, (_, i) => \\\`placeholder-\\\${i}\\\`)\`, then \`ids.map(id => <div key={id}>…</div>)\`.`;
|
|
94
99
|
|
|
95
100
|
const COMPONENT_CHECKLIST = `─────────────────────────────────────────────
|
|
96
101
|
QUALITY CHECKLIST
|
|
@@ -108,7 +113,9 @@ Before returning, verify:
|
|
|
108
113
|
- [ ] Existing component behavior is preserved when modifying
|
|
109
114
|
- [ ] Numeric values use tabular-nums and Intl.NumberFormat for currency
|
|
110
115
|
- [ ] forwardRef uses \`export const X = React.forwardRef(...)\` syntax, never \`export function X = forwardRef(...)\`
|
|
111
|
-
- [ ] All referenced types from composed components are imported with \`import type { ... }
|
|
116
|
+
- [ ] All referenced types from composed components are imported with \`import type { ... }\`
|
|
117
|
+
- [ ] Form input \`useState\` calls are initialized with \`''\`, not \`undefined\` or \`null\`
|
|
118
|
+
- [ ] No \`.map()\` callback index parameter used as a React key (directly or in template literals)`;
|
|
112
119
|
|
|
113
120
|
export const componentPromptSections = {
|
|
114
121
|
PREAMBLE: COMPONENT_PREAMBLE,
|
|
@@ -205,7 +212,13 @@ RULES
|
|
|
205
212
|
9. **Test descriptions mirror specs.** Use spec language directly in test descriptions so traceability is obvious.
|
|
206
213
|
10. **Code only.** Return ONLY the test file code. No markdown fences, no commentary, no file names.
|
|
207
214
|
11. **Tailwind classes, not inline styles.** The project uses Tailwind CSS. In JSDOM, Tailwind utility classes are just class names — they do NOT produce computed inline styles. Use \`toHaveClass('class-name')\` for class-based assertions. NEVER use \`toHaveStyle()\` for Tailwind utilities like \`tabular-nums\`, \`touch-manipulation\`, \`min-w-[44px]\`, etc. For hover states, transforms, transitions, and pseudo-elements — JSDOM cannot compute these. Skip those assertions entirely.
|
|
208
|
-
12. **Verify semantic elements from component source.** Some UI library components use unexpected elements (e.g., CardTitle renders a \`<div>\`, not a heading). If composed component source code is provided, check the actual rendered element before choosing your query strategy. Prefer \`getByText\` when the semantic element is uncertain
|
|
215
|
+
12. **Verify semantic elements from component source.** Some UI library components use unexpected elements (e.g., CardTitle renders a \`<div>\`, not a heading). If composed component source code is provided, check the actual rendered element before choosing your query strategy. Prefer \`getByText\` when the semantic element is uncertain.
|
|
216
|
+
13. **Strict package boundary.** Only import from \`vitest\`, \`@testing-library/react\`, \`@testing-library/user-event\`, and the component under test (via \`./ComponentName\`). Never import \`@testing-library/dom\` or any other \`@testing-library/*\` sub-package. Do NOT import any other npm package. Do NOT import other components (siblings, composed components, UI components) — only import the one component you are testing.
|
|
217
|
+
14. **jsdom limitations.** Tests run in jsdom, which does NOT support: \`DataTransfer\`, \`ClipboardEvent\`, \`IntersectionObserver\`, \`ResizeObserver\`, \`window.matchMedia\`, \`getBoundingClientRect\` returning real dimensions, CSS computed styles, \`scrollIntoView\`, or HTML5 drag-and-drop events (\`dragstart\`, \`drop\`). Do NOT write tests that depend on these APIs. For drag-and-drop specs, test the callbacks directly (e.g. call \`onDrop\` with mock data) instead of simulating drag events. For scroll-to-bottom specs, test that the scroll container ref exists rather than asserting \`scrollIntoView\` was called. For specs requiring unavailable APIs, test the closest observable behavior that jsdom supports.
|
|
218
|
+
15. **Test Tailwind classes individually.** When asserting CSS classes, use separate \`toHaveClass\` calls for each important class: \`expect(el).toHaveClass('rounded-md')\`, \`expect(el).toHaveClass('shadow-sm')\`. Do NOT assert the entire class string as one value — the component may include additional utility classes. Focus on the classes that the spec explicitly mentions.
|
|
219
|
+
16. **Mock composed components.** If the component under test composes other project components (not built-in HTML elements), mock them with \`vi.mock\` so tests are isolated. Mock pattern: \`vi.mock('./ChildComponent', () => ({ ChildComponent: ({ children, ...rest }: Record<string, unknown>) => <div data-testid="child-component">{children as React.ReactNode}</div> }))\`. Do NOT spread props (\`{...props}\`) onto the mock div — callback functions are not valid DOM attributes. When testing a page or container that mocks its children, only test the page's own rendering (loading text, error messages, layout containers). Do NOT query for interactive elements (buttons, inputs) that only exist inside the real (unmocked) children. When asserting props passed to a mocked component, NEVER use \`toHaveBeenCalledWith\` — React passes \`(props, ref)\` where \`ref\` is \`undefined\` for non-forwardRef components, and \`expect.anything()\` does NOT match \`undefined\`. Instead, access mock calls directly: \`const [props] = MockChild.mock.calls[0]; expect(props).toMatchObject({...});\`. This is mandatory for ALL mock component prop assertions.
|
|
220
|
+
17. **vi.mock factory hoisting.** \`vi.mock\` calls are hoisted to the top of the file by vitest. This means the factory function cannot reference any variables declared in the test file scope (imports, constants, etc.). Keep mock factories self-contained — define all needed values inline within the factory. If you need to spy on the mock, import it after the \`vi.mock\` call.
|
|
221
|
+
18. **Semantic HTML role mapping.** When querying elements with \`getByRole\`, use the correct ARIA role, not the HTML tag name. Common mappings: \`<section>\` has role="region" (only when it has an accessible name via \`aria-label\` or \`aria-labelledby\` — otherwise it has NO implicit role and you must use a different query like \`getByTestId\`). \`<article>\` → \`"article"\`, \`<nav>\` → \`"navigation"\`, \`<aside>\` → \`"complementary"\`, \`<header>\` → \`"banner"\`, \`<footer>\` → \`"contentinfo"\`, \`<main>\` → \`"main"\`. There is NO role called \`"section"\` — never use \`getByRole('section')\`.`;
|
|
209
222
|
|
|
210
223
|
const TEST_CHECKLIST = `─────────────────────────────────────────────
|
|
211
224
|
QUALITY CHECKLIST
|
|
@@ -558,7 +571,7 @@ export function buildComponentPrompt(input: ComponentPromptInput): PromptResult
|
|
|
558
571
|
}
|
|
559
572
|
|
|
560
573
|
if (input.projectSection) {
|
|
561
|
-
parts.push(
|
|
574
|
+
parts.push(`\n\n${input.projectSection}`);
|
|
562
575
|
}
|
|
563
576
|
|
|
564
577
|
return {
|
|
@@ -583,7 +596,7 @@ export function buildTestPrompt(input: TestPromptInput): PromptResult {
|
|
|
583
596
|
}
|
|
584
597
|
|
|
585
598
|
if (input.projectSection) {
|
|
586
|
-
parts.push(
|
|
599
|
+
parts.push(`\n\n${input.projectSection}`);
|
|
587
600
|
}
|
|
588
601
|
|
|
589
602
|
return {
|
|
@@ -613,7 +626,7 @@ export function buildStoryPrompt(input: StoryPromptInput): PromptResult {
|
|
|
613
626
|
}
|
|
614
627
|
|
|
615
628
|
if (input.projectSection) {
|
|
616
|
-
parts.push(
|
|
629
|
+
parts.push(`\n\n${input.projectSection}`);
|
|
617
630
|
}
|
|
618
631
|
|
|
619
632
|
return {
|
|
@@ -653,7 +666,7 @@ export function buildReconcilerPrompt(input: ReconcilerPromptInput): PromptResul
|
|
|
653
666
|
parts.push('```');
|
|
654
667
|
|
|
655
668
|
if (input.projectSection) {
|
|
656
|
-
parts.push(
|
|
669
|
+
parts.push(`\n\n${input.projectSection}`);
|
|
657
670
|
}
|
|
658
671
|
|
|
659
672
|
return {
|
|
@@ -725,7 +738,13 @@ Rules:
|
|
|
725
738
|
- Only modify the test if it has a genuine bug (e.g. wrong import path, typo in query). Never weaken assertions.
|
|
726
739
|
- Make minimal changes to pass the failing tests without breaking passing ones.
|
|
727
740
|
- Return complete files, not diffs.
|
|
728
|
-
- No commentary, no explanations. Just the two code blocks
|
|
741
|
+
- No commentary, no explanations. Just the two code blocks.
|
|
742
|
+
- **Unresolvable imports.** If the error is "Failed to resolve import" for a package (e.g. react-router-dom, lucide-react, @tanstack/react-query), that package is NOT installed. Fix both the component and test by removing the unavailable import entirely and rewriting the code to work without it. Replace router hooks with callback props, replace icon components with inline \`<span>\` or \`<svg>\` elements, replace data-fetching hooks with props.
|
|
743
|
+
- **Missing UI component imports.** If the error is "Failed to resolve import" for a \`@/components/ui/*\` path, that UI component does not exist in the project. Remove the import and replace usage with plain HTML elements + Tailwind classes that provide equivalent functionality.
|
|
744
|
+
- **vi.mock hoisting errors.** If the error mentions "vi.mock" and "hoisted to top of the file", the mock factory references variables from the file scope. Fix by inlining all values within the factory function — do not reference imports, constants, or variables declared outside the factory.
|
|
745
|
+
- **Mock spy argument count mismatch.** If a \`toHaveBeenCalledWith\` assertion on a mocked component fails because of an extra \`undefined\` argument, React is passing \`(props, ref)\` where \`ref\` is \`undefined\`. \`expect.anything()\` does NOT match \`undefined\`, so do NOT use it. Fix the TEST: replace \`toHaveBeenCalledWith\` with direct mock call access: \`const [props] = MockedComponent.mock.calls[0]; expect(props).toMatchObject({...});\`.
|
|
746
|
+
- **toHaveStyle() failures.** jsdom does NOT compute CSS from Tailwind classes. If a \`toHaveStyle()\` assertion fails because the received style is empty or missing, replace \`expect(el).toHaveStyle({prop: value})\` with \`expect(el).toHaveClass('tailwind-class')\` using the corresponding Tailwind utility class. Never use \`toHaveStyle()\` in jsdom tests.
|
|
747
|
+
- **ReferenceError on mocked component.** If the error is \`ReferenceError: X is not defined\` and X is a component that was mocked with \`vi.mock\`, the test is accessing \`X.mock.calls\` without importing X. Add the import after the \`vi.mock\` call: \`import { X } from './X';\` — vitest resolves this to the mock.`;
|
|
729
748
|
|
|
730
749
|
export interface TestFixInput {
|
|
731
750
|
componentCode: string;
|
|
@@ -779,7 +798,8 @@ Rules:
|
|
|
779
798
|
- Fix ONLY the lint errors listed. Do not refactor unrelated code.
|
|
780
799
|
- Preserve all behavior and functionality.
|
|
781
800
|
- Return complete files, not diffs.
|
|
782
|
-
- No commentary, no explanations. Just the two code blocks
|
|
801
|
+
- No commentary, no explanations. Just the two code blocks.
|
|
802
|
+
- **noArrayIndexKey.** If the error is \`lint/suspicious/noArrayIndexKey\`, the \`.map()\` callback index is used as a React key — either directly (\`key={i}\`) or inside a template literal (\`key={\\\`prefix-\\\${i}\\\`}\`). Both are flagged by Biome. Fix by using a unique property from the item data (\`key={item.id}\`). If no \`id\` field exists, use another unique item property (\`key={item.title}\`). For generated arrays (skeletons, placeholders) where items have no unique property, pre-compute IDs outside JSX: \`const ids = Array.from({ length: n }, (_, i) => \\\`skeleton-\\\${i}\\\`)\`, then \`ids.map(id => <div key={id}>…</div>)\` — this moves the index out of the \`.map()\` callback.`;
|
|
783
803
|
|
|
784
804
|
export interface LintFixInput {
|
|
785
805
|
componentCode: string;
|
|
@@ -14,7 +14,7 @@ describe('runLint', () => {
|
|
|
14
14
|
const result = runLint(['src/Button.tsx'], '/project');
|
|
15
15
|
|
|
16
16
|
expect(result).toEqual({ passed: true, errors: [] });
|
|
17
|
-
expect(execSync).toHaveBeenCalledWith('npx biome check src/Button.tsx', {
|
|
17
|
+
expect(execSync).toHaveBeenCalledWith('npx biome check --config-path=/project/biome.json src/Button.tsx', {
|
|
18
18
|
cwd: '/project',
|
|
19
19
|
stdio: 'pipe',
|
|
20
20
|
encoding: 'utf-8',
|
|
@@ -43,11 +43,10 @@ describe('runLint', () => {
|
|
|
43
43
|
|
|
44
44
|
runLint(['src/Button.tsx', 'src/Button.test.tsx'], '/project');
|
|
45
45
|
|
|
46
|
-
expect(execSync).toHaveBeenCalledWith(
|
|
47
|
-
|
|
48
|
-
stdio: 'pipe',
|
|
49
|
-
|
|
50
|
-
});
|
|
46
|
+
expect(execSync).toHaveBeenCalledWith(
|
|
47
|
+
'npx biome check --config-path=/project/biome.json src/Button.tsx src/Button.test.tsx',
|
|
48
|
+
{ cwd: '/project', stdio: 'pipe', encoding: 'utf-8' },
|
|
49
|
+
);
|
|
51
50
|
});
|
|
52
51
|
});
|
|
53
52
|
|
|
@@ -58,7 +57,7 @@ describe('runLintFix', () => {
|
|
|
58
57
|
const result = runLintFix(['src/Button.tsx'], '/project');
|
|
59
58
|
|
|
60
59
|
expect(result).toEqual({ passed: true, errors: [] });
|
|
61
|
-
expect(execSync).toHaveBeenCalledWith('npx biome check --write src/Button.tsx', {
|
|
60
|
+
expect(execSync).toHaveBeenCalledWith('npx biome check --write --config-path=/project/biome.json src/Button.tsx', {
|
|
62
61
|
cwd: '/project',
|
|
63
62
|
stdio: 'pipe',
|
|
64
63
|
encoding: 'utf-8',
|
package/src/tools/lint-runner.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
|
|
3
4
|
export type LintResult = {
|
|
4
5
|
passed: boolean;
|
|
5
6
|
errors: string[];
|
|
6
7
|
};
|
|
7
8
|
|
|
9
|
+
function biomeCmd(action: string, filePaths: string[], targetDir: string): string {
|
|
10
|
+
const configPath = path.resolve(targetDir, 'biome.json');
|
|
11
|
+
return `npx biome ${action} --config-path=${configPath} ${filePaths.join(' ')}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
8
14
|
export function runLint(filePaths: string[], targetDir: string): LintResult {
|
|
9
15
|
try {
|
|
10
|
-
execSync(
|
|
16
|
+
execSync(biomeCmd('check', filePaths, targetDir), {
|
|
11
17
|
cwd: targetDir,
|
|
12
18
|
stdio: 'pipe',
|
|
13
19
|
encoding: 'utf-8',
|
|
@@ -20,7 +26,7 @@ export function runLint(filePaths: string[], targetDir: string): LintResult {
|
|
|
20
26
|
if ('stdout' in err && typeof err.stdout === 'string') stdout = err.stdout;
|
|
21
27
|
if ('stderr' in err && typeof err.stderr === 'string') stderr = err.stderr;
|
|
22
28
|
}
|
|
23
|
-
const combined = stdout
|
|
29
|
+
const combined = `${stdout}\n${stderr}`;
|
|
24
30
|
|
|
25
31
|
const errors = combined.split('\n').filter((line) => line.trim().length > 0);
|
|
26
32
|
|
|
@@ -30,7 +36,7 @@ export function runLint(filePaths: string[], targetDir: string): LintResult {
|
|
|
30
36
|
|
|
31
37
|
export function runLintFix(filePaths: string[], targetDir: string): LintResult {
|
|
32
38
|
try {
|
|
33
|
-
execSync(
|
|
39
|
+
execSync(biomeCmd('check --write', filePaths, targetDir), {
|
|
34
40
|
cwd: targetDir,
|
|
35
41
|
stdio: 'pipe',
|
|
36
42
|
encoding: 'utf-8',
|
|
@@ -43,7 +49,7 @@ export function runLintFix(filePaths: string[], targetDir: string): LintResult {
|
|
|
43
49
|
if ('stdout' in err && typeof err.stdout === 'string') stdout = err.stdout;
|
|
44
50
|
if ('stderr' in err && typeof err.stderr === 'string') stderr = err.stderr;
|
|
45
51
|
}
|
|
46
|
-
const combined = stdout
|
|
52
|
+
const combined = `${stdout}\n${stderr}`;
|
|
47
53
|
|
|
48
54
|
const errors = combined.split('\n').filter((line) => line.trim().length > 0);
|
|
49
55
|
|
|
@@ -102,13 +102,13 @@ describe('runTests', () => {
|
|
|
102
102
|
});
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
-
it('truncates output to
|
|
106
|
-
const longOutput = JSON.stringify({ testResults: [] }) + 'x'.repeat(
|
|
105
|
+
it('truncates output to 4000 chars', () => {
|
|
106
|
+
const longOutput = JSON.stringify({ testResults: [] }) + 'x'.repeat(5000);
|
|
107
107
|
vi.mocked(execSync).mockReturnValue(longOutput);
|
|
108
108
|
|
|
109
109
|
const result = runTests('src/Button.test.tsx', '/project');
|
|
110
110
|
|
|
111
|
-
expect(result.output.length).toBeLessThanOrEqual(
|
|
111
|
+
expect(result.output.length).toBeLessThanOrEqual(4000);
|
|
112
112
|
});
|
|
113
113
|
|
|
114
114
|
it('handles error object without stdout or stderr properties', () => {
|
|
@@ -197,6 +197,58 @@ describe('runTests', () => {
|
|
|
197
197
|
});
|
|
198
198
|
});
|
|
199
199
|
|
|
200
|
+
it('captures suite-level error when assertionResults is empty but suite failed', () => {
|
|
201
|
+
const json = JSON.stringify({
|
|
202
|
+
testResults: [
|
|
203
|
+
{
|
|
204
|
+
status: 'failed',
|
|
205
|
+
message: 'Failed to collect tests: Cannot find module @testing-library/jest-dom/vitest',
|
|
206
|
+
assertionResults: [],
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
});
|
|
210
|
+
vi.mocked(execSync).mockReturnValue(json);
|
|
211
|
+
|
|
212
|
+
const result = runTests('src/Button.test.tsx', '/project');
|
|
213
|
+
|
|
214
|
+
expect(result).toEqual({
|
|
215
|
+
passed: false,
|
|
216
|
+
numPassed: 0,
|
|
217
|
+
numFailed: 1,
|
|
218
|
+
failures: ['Suite error: Failed to collect tests: Cannot find module @testing-library/jest-dom/vitest'],
|
|
219
|
+
output: expect.any(String),
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('does not capture suite-level error when assertionResults already has failures', () => {
|
|
224
|
+
const json = JSON.stringify({
|
|
225
|
+
testResults: [
|
|
226
|
+
{
|
|
227
|
+
status: 'failed',
|
|
228
|
+
message: 'Some suite message',
|
|
229
|
+
assertionResults: [
|
|
230
|
+
{
|
|
231
|
+
status: 'failed',
|
|
232
|
+
fullName: 'test one',
|
|
233
|
+
failureMessages: ['assertion failed'],
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
});
|
|
239
|
+
vi.mocked(execSync).mockReturnValue(json);
|
|
240
|
+
|
|
241
|
+
const result = runTests('src/Button.test.tsx', '/project');
|
|
242
|
+
|
|
243
|
+
expect(result).toEqual({
|
|
244
|
+
passed: false,
|
|
245
|
+
numPassed: 0,
|
|
246
|
+
numFailed: 1,
|
|
247
|
+
failures: ['test one: assertion failed'],
|
|
248
|
+
output: expect.any(String),
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
200
252
|
it('handles malformed JSON after opening brace', () => {
|
|
201
253
|
vi.mocked(execSync).mockReturnValue('prefix {invalid json');
|
|
202
254
|
|
package/src/tools/test-runner.ts
CHANGED
|
@@ -25,7 +25,7 @@ export function runTests(testFilePath: string, targetDir: string): TestRunResult
|
|
|
25
25
|
if ('stdout' in err && typeof err.stdout === 'string') stdout = err.stdout;
|
|
26
26
|
if ('stderr' in err && typeof err.stderr === 'string') stderr = err.stderr;
|
|
27
27
|
}
|
|
28
|
-
const combined = stdout
|
|
28
|
+
const combined = `${stdout}\n${stderr}`;
|
|
29
29
|
|
|
30
30
|
const parsed = parseVitestJson(combined);
|
|
31
31
|
return parsed;
|
|
@@ -40,7 +40,7 @@ function parseVitestJson(output: string): TestRunResult {
|
|
|
40
40
|
numPassed: 0,
|
|
41
41
|
numFailed: 0,
|
|
42
42
|
failures: [],
|
|
43
|
-
output: output.slice(0,
|
|
43
|
+
output: output.slice(0, 4000),
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -63,6 +63,10 @@ function parseVitestJson(output: string): TestRunResult {
|
|
|
63
63
|
failures.push(`${name}: ${message}`);
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
+
if (suite.status === 'failed' && suite.message && failures.length === 0) {
|
|
67
|
+
failures.push(`Suite error: ${suite.message}`);
|
|
68
|
+
numFailed++;
|
|
69
|
+
}
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
return {
|
|
@@ -70,7 +74,7 @@ function parseVitestJson(output: string): TestRunResult {
|
|
|
70
74
|
numPassed,
|
|
71
75
|
numFailed,
|
|
72
76
|
failures,
|
|
73
|
-
output: output.slice(0,
|
|
77
|
+
output: output.slice(0, 4000),
|
|
74
78
|
};
|
|
75
79
|
} catch {
|
|
76
80
|
return {
|
|
@@ -78,7 +82,7 @@ function parseVitestJson(output: string): TestRunResult {
|
|
|
78
82
|
numPassed: 0,
|
|
79
83
|
numFailed: 0,
|
|
80
84
|
failures: [],
|
|
81
|
-
output: output.slice(0,
|
|
85
|
+
output: output.slice(0, 4000),
|
|
82
86
|
};
|
|
83
87
|
}
|
|
84
88
|
}
|