Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ describe('ghostStoriesChannel', () => {
successRate: 1,
successRateWithoutEmptyRender: 1,
categorizedErrors: expect.any(Object),
cssCheck: 'not-run',
uniqueErrorCount: 0,
passedButEmptyRender: 0,
},
Expand Down Expand Up @@ -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,
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ describe('parse-vitest-report', () => {
successRateWithoutEmptyRender: 1.0,
uniqueErrorCount: 0,
categorizedErrors: {},
cssCheck: 'not-run',
});
});

Expand Down Expand Up @@ -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');
});
});
});
62 changes: 62 additions & 0 deletions code/core/src/shared/utils/analyze-test-results.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ describe('analyze-test-results', () => {
successRateWithoutEmptyRender: 1.0,
uniqueErrorCount: 0,
categorizedErrors: {},
cssCheck: 'not-run',
});
});

Expand Down Expand Up @@ -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');
});
});
});
});
21 changes: 21 additions & 0 deletions code/core/src/shared/utils/analyze-test-results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
Expand All @@ -79,5 +99,6 @@ export function analyzeTestResults(results: StoryTestResult[]): TestRunAnalysis
successRateWithoutEmptyRender,
uniqueErrorCount: errorClassification.uniqueErrorCount,
categorizedErrors: errorClassification.categorizedErrors,
cssCheck,
};
}
16 changes: 16 additions & 0 deletions code/core/src/shared/utils/test-result-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,20 @@ export interface TestRunAnalysis {
successRateWithoutEmptyRender: number;
uniqueErrorCount: number;
categorizedErrors: Record<string, CategorizedError>;
/**
* 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';
}
Loading