diff --git a/code/core/src/core-server/server-channel/ghost-stories-channel.test.ts b/code/core/src/core-server/server-channel/ghost-stories-channel.test.ts index 0d401aec4194..f02bf9ac06a7 100644 --- a/code/core/src/core-server/server-channel/ghost-stories-channel.test.ts +++ b/code/core/src/core-server/server-channel/ghost-stories-channel.test.ts @@ -218,6 +218,7 @@ describe('ghostStoriesChannel', () => { successRate: 1, successRateWithoutEmptyRender: 1, categorizedErrors: expect.any(Object), + cssCheck: 'not-run', uniqueErrorCount: 0, passedButEmptyRender: 0, }, @@ -316,6 +317,7 @@ describe('ghostStoriesChannel', () => { successRate: 0, // categorizedErrors is now an object with categories as keys categorizedErrors: expect.any(Object), + cssCheck: 'not-run', uniqueErrorCount: expect.any(Number), passedButEmptyRender: 0, }), diff --git a/code/core/src/core-server/utils/ghost-stories/parse-vitest-report.test.ts b/code/core/src/core-server/utils/ghost-stories/parse-vitest-report.test.ts index 227b38417720..e86024aed86d 100644 --- a/code/core/src/core-server/utils/ghost-stories/parse-vitest-report.test.ts +++ b/code/core/src/core-server/utils/ghost-stories/parse-vitest-report.test.ts @@ -49,6 +49,7 @@ describe('parse-vitest-report', () => { successRateWithoutEmptyRender: 1.0, uniqueErrorCount: 0, categorizedErrors: {}, + cssCheck: 'not-run', }); }); @@ -261,5 +262,36 @@ describe('parse-vitest-report', () => { expect(result.summary?.total).toBe(0); expect(result.summary?.successRate).toBe(0); }); + + it('surfaces the CssCheck story outcome via summary.cssCheck', () => { + const mockVitestResults = { + success: false, + numTotalTests: 2, + numPassedTests: 1, + numFailedTests: 1, + testResults: [ + { + assertionResults: [ + { + fullName: 'components-button--primary', + status: 'passed', + meta: { storyId: 'components-button--primary' }, + failureMessages: [], + }, + { + fullName: 'components-button--css-check', + status: 'failed', + meta: { storyId: 'components-button--css-check' }, + failureMessages: ['Error: expected rgb(37, 99, 235) but got rgba(0, 0, 0, 0)'], + }, + ], + }, + ], + }; + + const result = parseVitestResults(mockVitestResults); + + expect(result.summary?.cssCheck).toBe('fail'); + }); }); }); diff --git a/code/core/src/shared/utils/analyze-test-results.test.ts b/code/core/src/shared/utils/analyze-test-results.test.ts index 8a8feeee2746..99313e8bf76b 100644 --- a/code/core/src/shared/utils/analyze-test-results.test.ts +++ b/code/core/src/shared/utils/analyze-test-results.test.ts @@ -77,6 +77,7 @@ describe('analyze-test-results', () => { successRateWithoutEmptyRender: 1.0, uniqueErrorCount: 0, categorizedErrors: {}, + cssCheck: 'not-run', }); }); @@ -122,5 +123,66 @@ describe('analyze-test-results', () => { expect(analysis.passed).toBe(1); expect(analysis.successRate).toBe(0.5); }); + + describe('cssCheck', () => { + it("is 'pass' when a --css-check story passed", () => { + const results: StoryTestResult[] = [ + { storyId: 'components-button--primary', status: 'PASS' }, + { storyId: 'components-button--css-check', status: 'PASS' }, + ]; + expect(analyzeTestResults(results).cssCheck).toBe('pass'); + }); + + it("is 'fail' when a --css-check story failed", () => { + const results: StoryTestResult[] = [ + { + storyId: 'components-button--css-check', + status: 'FAIL', + error: 'expected rgb(37, 99, 235) but got rgba(0, 0, 0, 0)', + }, + ]; + expect(analyzeTestResults(results).cssCheck).toBe('fail'); + }); + + it("is 'not-run' when no --css-check story is present", () => { + const results: StoryTestResult[] = [ + { storyId: 'components-button--primary', status: 'PASS' }, + ]; + expect(analyzeTestResults(results).cssCheck).toBe('not-run'); + }); + + it("is 'not-run' when the --css-check story was skipped / pending / todo", () => { + // PENDING covers any non-pass / non-fail Vitest status (skipped, + // pending, todo, filtered out). No pass/fail signal available → + // 'not-run', same bucket as "story wasn't authored at all". + const results: StoryTestResult[] = [ + { storyId: 'components-button--css-check', status: 'PENDING' }, + ]; + expect(analyzeTestResults(results).cssCheck).toBe('not-run'); + }); + + it("is 'not-run' for an empty result list", () => { + expect(analyzeTestResults([]).cssCheck).toBe('not-run'); + }); + + it('uses the first match when multiple --css-check stories exist', () => { + // Prompt violation: the AI setup prompt asks for exactly one. + // First match wins; downstream aggregates still reflect all of them. + const results: StoryTestResult[] = [ + { storyId: 'components-button--css-check', status: 'PASS' }, + { storyId: 'components-card--css-check', status: 'FAIL', error: 'style mismatch' }, + ]; + expect(analyzeTestResults(results).cssCheck).toBe('pass'); + }); + + it('is case-insensitive on the suffix (defensive)', () => { + // CSF already lowercases storyIds. This keeps the check resilient + // to a future upstream change in sanitization. + const results: StoryTestResult[] = [ + { storyId: 'components-button--CSS-CHECK', status: 'PASS' }, + ]; + expect(analyzeTestResults(results).cssCheck).toBe('pass'); + }); + }); }); }); diff --git a/code/core/src/shared/utils/analyze-test-results.ts b/code/core/src/shared/utils/analyze-test-results.ts index 4a15c144fdc8..1be681c39af3 100644 --- a/code/core/src/shared/utils/analyze-test-results.ts +++ b/code/core/src/shared/utils/analyze-test-results.ts @@ -56,6 +56,12 @@ export function extractCategorizedErrors( }; } +/** + * StoryId suffix for a story named `CssCheck` (after Storybook's CSF + * `toStartCaseStr` + `sanitize`: `CssCheck` → `Css Check` → `css-check`). + */ +const CSS_CHECK_STORY_ID_SUFFIX = '--css-check'; + /** * Analyze a list of story test results and produce a TestRunAnalysis with pass/fail counts, success * rates, empty render detection, and categorized errors. @@ -71,6 +77,20 @@ export function analyzeTestResults(results: StoryTestResult[]): TestRunAnalysis const errorClassification = extractCategorizedErrors(results); + // `'not-run'` covers both "no CssCheck story in the suite" and "story + // existed but wasn't executed" — they're the same signal for consumers + // (no pass/fail outcome available). Collapsing them avoids a fourth + // state and keeps dashboards from interpreting an absent field. + const cssCheckMatch = results.find((r) => + r.storyId.toLowerCase().endsWith(CSS_CHECK_STORY_ID_SUFFIX) + ); + const cssCheck: TestRunAnalysis['cssCheck'] = + cssCheckMatch?.status === 'PASS' + ? 'pass' + : cssCheckMatch?.status === 'FAIL' + ? 'fail' + : 'not-run'; + return { total, passed, @@ -79,5 +99,6 @@ export function analyzeTestResults(results: StoryTestResult[]): TestRunAnalysis successRateWithoutEmptyRender, uniqueErrorCount: errorClassification.uniqueErrorCount, categorizedErrors: errorClassification.categorizedErrors, + cssCheck, }; } diff --git a/code/core/src/shared/utils/test-result-types.ts b/code/core/src/shared/utils/test-result-types.ts index c99727ab3586..097279ac3493 100644 --- a/code/core/src/shared/utils/test-result-types.ts +++ b/code/core/src/shared/utils/test-result-types.ts @@ -28,4 +28,20 @@ export interface TestRunAnalysis { successRateWithoutEmptyRender: number; uniqueErrorCount: number; categorizedErrors: Record; + /** + * Outcome of the `CssCheck` story — a story (id suffix `--css-check`) + * whose `play` asserts a component-specific computed style via + * `getComputedStyle`. Distinguishes "component mounted" from "the + * user's CSS actually loaded". + * + * - `'pass'` — a `CssCheck` story ran and passed. + * - `'fail'` — a `CssCheck` story ran and failed. + * - `'not-run'` — no pass/fail signal available: either no `CssCheck` + * story is in the suite, or the story existed but was + * not executed (skipped, pending, todo, filtered out). + * + * Only the three-valued enum is emitted — no storyId or component + * name — so no user-authored data enters telemetry. + */ + cssCheck: 'pass' | 'fail' | 'not-run'; }