Skip to content
Closed
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
171 changes: 169 additions & 2 deletions code/lib/cli-storybook/src/ai/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ function getSetupInstructions(projectInfo: ProjectInfo): string {
Your goal is to make Storybook fully functional in this project by analyzing the codebase,
configuring the preview with the right decorators, and writing stories for some components.

The end state should be a Storybook where any component — from a small button to a full page — can be added without story-specific workarounds. All necessary providers, CSS, browser state, and network mocks should live in the shared preview so that just rendering the component in the story is enough.

After each created story, run Vitest to verify it renders.
If the test fails, read the error, fix the issue, and re-run until it passes before moving on.

Expand Down Expand Up @@ -287,6 +289,16 @@ function getSetupInstructions(projectInfo: ProjectInfo): string {

${getNeedsWorkTagExample(projectInfo)}

#### Args vs render

For simple components where props drive the state, prefer \`args\` stories — no \`render\` function needed:

${getArgsStoryExample(projectInfo)}

Use \`render\` when the story needs composition — wrapping the component in layout, combining multiple components, or passing children as JSX:

${getRenderCompositionExample(projectInfo)}

Keep app mocking and runtime setup in preview, not in the stories.
Do not build large story-specific harnesses.
Do not write story files for subcomponents, hooks, contexts, or helpers.
Expand Down Expand Up @@ -366,7 +378,24 @@ function getSetupInstructions(projectInfo: ProjectInfo): string {
- a toast, alert, or badge has the expected accessible text and visual state
- a CSS class or computed style confirms the real state that matters

### Step 7: Cover the patterns you found
### Step 7: Prove CSS is loaded in exactly one story named \`CssCheck\`

In exactly one story, named \`CssCheck\`, assert a component-specific computed style. \`toBeVisible\` passes on an unstyled component; a concrete style value proves the shared preview loaded the app's CSS.

Pick a visually distinctive component, read a styling value from its source, and assert it with \`getComputedStyle\`:

\`\`\`tsx
export const CssCheck: Story = {
args: { children: "Submit" },
play: async ({ canvas }) => {
const button = canvas.getByRole("button", { name: /submit/i });
// PrimaryButton uses bg-blue-600 — fails if Tailwind / global CSS did not load.
await expect(getComputedStyle(button).backgroundColor).toBe("rgb(37, 99, 235)");
},
};
\`\`\`

### Step 8: Cover the patterns you found

Write stories for the real patterns in the codebase, for example:

Expand All @@ -382,7 +411,7 @@ function getSetupInstructions(projectInfo: ProjectInfo): string {

${getPageStoryExample(projectInfo)}

### Step 8: Verify both rendering and types
### Step 9: Verify both rendering and types

As you work, verify the stories with Vitest:

Expand Down Expand Up @@ -651,6 +680,144 @@ function getNeedsWorkTagExample(projectInfo: ProjectInfo): string {
`;
}

function getArgsStoryExample(projectInfo: ProjectInfo): string {
if (projectInfo.hasCsfFactoryPreview) {
return dedent`
\`\`\`tsx
import preview from '#.storybook/preview';
import { expect } from 'storybook/test';
import { Button } from './Button';

const meta = preview.meta({
component: Button,
tags: ['ai-generated'],
});

export const Primary = meta.story({
args: {
variant: 'primary',
children: 'Save',
},
play: async ({ canvas }) => {
await expect(canvas.getByRole('button', { name: /save/i })).toBeVisible();
},
});

export const Disabled = meta.story({
args: {
variant: 'primary',
disabled: true,
children: 'Save',
},
play: async ({ canvas }) => {
await expect(canvas.getByRole('button')).toBeDisabled();
},
});
\`\`\`
`;
}

const typeImport = getTypeImportSource(projectInfo);

return dedent`
\`\`\`tsx
import type { Meta, StoryObj } from '${typeImport}';
import { expect } from 'storybook/test';
import { Button } from './Button';

const meta = {
component: Button,
tags: ['ai-generated'],
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Primary: Story = {
args: {
variant: 'primary',
children: 'Save',
},
play: async ({ canvas }) => {
await expect(canvas.getByRole('button', { name: /save/i })).toBeVisible();
},
};

export const Disabled: Story = {
args: {
variant: 'primary',
disabled: true,
children: 'Save',
},
play: async ({ canvas }) => {
await expect(canvas.getByRole('button')).toBeDisabled();
},
};
\`\`\`
`;
}

function getRenderCompositionExample(projectInfo: ProjectInfo): string {
if (projectInfo.hasCsfFactoryPreview) {
return dedent`
\`\`\`tsx
import preview from '#.storybook/preview';
import { expect } from 'storybook/test';
import { Button } from './Button';
import { Card } from './Card';

const meta = preview.meta({
component: Button,
tags: ['ai-generated'],
});

export const InsideCard = meta.story({
render: () => (
<Card>
<Button disabled={false}>Save</Button>
</Card>
),
play: async ({ canvas, userEvent }) => {
await expect(canvas.getByRole('button', { name: /save/i })).toBeVisible();
await userEvent.click(canvas.getByRole('button', { name: /save/i }));
},
Comment on lines +780 to +783

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid click-only interactions in example play functions.

These clicks have no post-click assertion, which teaches a weaker pattern than the surrounding guidance. Add a post-condition assertion or remove the click.

Based on learnings, "Test real behavior, not just syntax patterns".

Also applies to: 812-815

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/lib/cli-storybook/src/ai/prompt.ts` around lines 780 - 783, The play
function contains a click-only interaction (play async ({ canvas, userEvent })
=> { ... userEvent.click(canvas.getByRole('button', { name: /save/i })) }) with
no post-click assertion; update the play to include a post-condition assertion
after userEvent.click (for example assert that a success message appears, the
save button becomes disabled, a form value changes, or a specific element is
visible/contains expected text) OR remove the click entirely; apply the same
change to the other similar play helper that uses canvas.getByRole('button', {
name: /save/i }) and userEvent.click to ensure each example asserts real
behavior rather than only performing a click.

});
\`\`\`
`;
}

const typeImport = getTypeImportSource(projectInfo);

return dedent`
\`\`\`tsx
import type { Meta, StoryObj } from '${typeImport}';
import { expect } from 'storybook/test';
import { Button } from './Button';
import { Card } from './Card';

const meta = {
component: Button,
tags: ['ai-generated'],
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

export const InsideCard: Story = {
render: () => (
<Card>
<Button disabled={false}>Save</Button>
</Card>
),
play: async ({ canvas, userEvent }) => {
await expect(canvas.getByRole('button', { name: /save/i })).toBeVisible();
await userEvent.click(canvas.getByRole('button', { name: /save/i }));
},
};
\`\`\`
`;
}

function getPageStoryExample(projectInfo: ProjectInfo): string {
if (projectInfo.hasCsfFactoryPreview) {
return dedent`
Expand Down
Loading