-
-
Notifications
You must be signed in to change notification settings - Fork 9.8k
CSF: Add Storybook test syntax (Storybook v10) #32455
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
81e9c77
1803fb0
78abf69
c2ecdb5
af298b5
5dc7ef7
1086746
f455ffa
5a08df8
d37b759
2390147
ba2ec9c
8fe5aa6
2a43712
a6bb54c
ff40d89
bc01f93
5f9bb80
ff6eb10
462d8ba
e6f92b8
629bdff
e10f871
d73c129
bed61ea
933b626
b53a9e1
9c44db6
bbf1e77
190ceef
9476827
b5bbb76
fb9266c
c07651f
6f86c4a
99c205c
dda2670
3c00cc8
cde0b32
6cc74e2
9d544a6
bef2d9e
87446ca
110822c
b803204
5dce7ad
62cdd05
08b3fb2
40df0de
8acca71
67643a8
d7dc33d
991b3b5
beb32bf
4e90611
9719ea6
82b84e4
f8ff03a
b21eadc
2bae930
200d9bb
3f0e5d7
633478c
0f86613
bbb4ffe
730bbf0
2d729ed
a0151ef
eca1ace
ee34e40
6ab99d4
024cde6
7def7d9
1dba824
2c99471
ce39157
3ab5db3
8e74582
df6dce7
ad68ec3
91f0fc4
864ed55
d14ea0e
32f7796
50fc0b7
512c1e6
0358dcc
620663c
91a0149
68603a9
a444184
c47c4fa
7b564ea
52a3cf3
5c6ef7f
d509fc3
05e57e6
9c78432
700ec5b
cbbd7be
5fe23d4
dfe2b36
5aa436a
6b8ea8f
05a264c
f6c090e
ebe497a
e335dc4
6fa8664
8dee332
d9bf94a
b6c82bc
8e06a46
f70fbda
eefc73e
5640b6e
5344b3f
c166dc4
c0bad22
55ac169
470f1fd
58752c6
6b36e49
035243a
07d688d
37a5853
e1fcd14
c1dd869
2aca744
9f02684
6813ee2
cf297c8
a95c291
b6fbeda
a632036
0b3d490
9f16a26
5705bb5
24253d8
8d18419
15e965e
4831e99
3e8c306
1a4ee93
cd66148
bab3780
f570532
7a03431
07e99ae
b6c6e94
85cf726
639042c
3fa6097
9b7fa17
d61fba2
319ace2
48cb2d9
478e4f1
7aa48c2
f5e829d
0b21aff
f793686
2d64830
86db945
07beaea
c22aa61
f0cfe33
3113762
c2aedef
d98408f
54ecd3a
41c83af
c652477
6941da2
4aaaeba
246e3ef
7a47f95
b52b74a
4c348d3
d28127c
8a65895
7985f43
67dfbfb
00e53ca
8b9cc24
87ddc71
3b12d84
56c04b0
9c1c6d9
a9d51fd
4e32f28
11b3884
c69e759
aac8a9d
ce2cc67
2b1bbd2
304edc3
05a4d33
09c03f9
de396c4
b5d65ed
9f1fa3f
b868eb1
f3a4f4a
ed673f0
25f0f92
9183a9b
7913c9d
ba4758a
0bac997
a163b78
6c7eb65
8df5a47
6aceace
fa3371d
9c8103e
3942b0b
999aa03
9884d89
f31a74c
669680c
2828dec
f0ce3fe
a1da280
0a995fa
fefa45e
8a83592
a25c124
2f19c9d
dea672e
bc54bb1
3f1f0b0
6d28374
2e03469
5e1c061
02995a7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -15,6 +15,7 @@ import path from 'pathe'; | |||||||||||||||||
| import { STATUS_TYPE_ID_A11Y, STATUS_TYPE_ID_COMPONENT_TEST, storeOptions } from '../constants'; | ||||||||||||||||||
| import type { StoreEvent, StoreState } from '../types'; | ||||||||||||||||||
| import { TestManager, type TestManagerOptions } from './test-manager'; | ||||||||||||||||||
| import { DOUBLE_SPACES } from './vitest-manager'; | ||||||||||||||||||
|
|
||||||||||||||||||
| const setTestNamePattern = vi.hoisted(() => vi.fn()); | ||||||||||||||||||
| const vitest = vi.hoisted(() => ({ | ||||||||||||||||||
|
|
@@ -103,6 +104,7 @@ global.fetch = vi.fn().mockResolvedValue({ | |||||||||||||||||
| entries: { | ||||||||||||||||||
| 'story--one': { | ||||||||||||||||||
| type: 'story', | ||||||||||||||||||
| subtype: 'story', | ||||||||||||||||||
| id: 'story--one', | ||||||||||||||||||
| name: 'One', | ||||||||||||||||||
| title: 'story/one', | ||||||||||||||||||
|
|
@@ -111,12 +113,32 @@ global.fetch = vi.fn().mockResolvedValue({ | |||||||||||||||||
| }, | ||||||||||||||||||
| 'another--one': { | ||||||||||||||||||
| type: 'story', | ||||||||||||||||||
| subtype: 'story', | ||||||||||||||||||
| id: 'another--one', | ||||||||||||||||||
| name: 'One', | ||||||||||||||||||
| title: 'another/one', | ||||||||||||||||||
| importPath: 'path/to/another/file', | ||||||||||||||||||
| tags: ['test'], | ||||||||||||||||||
| }, | ||||||||||||||||||
| 'parent--story': { | ||||||||||||||||||
| type: 'story', | ||||||||||||||||||
| subtype: 'story', | ||||||||||||||||||
| id: 'parent--story', | ||||||||||||||||||
| name: 'Parent story', | ||||||||||||||||||
| title: 'parent/story', | ||||||||||||||||||
| importPath: 'path/to/parent/file', | ||||||||||||||||||
| tags: ['test'], | ||||||||||||||||||
| }, | ||||||||||||||||||
| 'parent--story:test': { | ||||||||||||||||||
| type: 'story', | ||||||||||||||||||
| subtype: 'test', | ||||||||||||||||||
| id: 'parent--story:test', | ||||||||||||||||||
| name: 'Test name', | ||||||||||||||||||
| title: 'parent/story', | ||||||||||||||||||
| parent: 'parent--story', | ||||||||||||||||||
| importPath: 'path/to/parent/file', | ||||||||||||||||||
| tags: ['test', 'test-fn'], | ||||||||||||||||||
| }, | ||||||||||||||||||
| }, | ||||||||||||||||||
| } as StoryIndex) | ||||||||||||||||||
| ), | ||||||||||||||||||
|
|
@@ -184,10 +206,56 @@ describe('TestManager', () => { | |||||||||||||||||
| triggeredBy: 'global', | ||||||||||||||||||
| }, | ||||||||||||||||||
| }); | ||||||||||||||||||
| expect(setTestNamePattern).toHaveBeenCalledWith(/^One$/); | ||||||||||||||||||
| expect(setTestNamePattern).toHaveBeenCalledWith(new RegExp(`^One$`)); | ||||||||||||||||||
| expect(vitest.runTestSpecifications).toHaveBeenCalledWith(tests.slice(0, 1), true); | ||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| it('should trigger a single story render test', async () => { | ||||||||||||||||||
| vitest.globTestSpecifications.mockImplementation(() => tests); | ||||||||||||||||||
| const testManager = await TestManager.start(options); | ||||||||||||||||||
|
|
||||||||||||||||||
| await testManager.handleTriggerRunEvent({ | ||||||||||||||||||
| type: 'TRIGGER_RUN', | ||||||||||||||||||
| payload: { | ||||||||||||||||||
| storyIds: ['another--one'], | ||||||||||||||||||
| triggeredBy: 'global', | ||||||||||||||||||
| }, | ||||||||||||||||||
| }); | ||||||||||||||||||
| // regex should be exact match of the story name | ||||||||||||||||||
| expect(setTestNamePattern).toHaveBeenCalledWith(new RegExp(`^One$`)); | ||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| it('should trigger a single story test', async () => { | ||||||||||||||||||
| vitest.globTestSpecifications.mockImplementation(() => tests); | ||||||||||||||||||
| const testManager = await TestManager.start(options); | ||||||||||||||||||
|
|
||||||||||||||||||
| await testManager.handleTriggerRunEvent({ | ||||||||||||||||||
| type: 'TRIGGER_RUN', | ||||||||||||||||||
| payload: { | ||||||||||||||||||
| storyIds: ['parent--story:test'], | ||||||||||||||||||
| triggeredBy: 'global', | ||||||||||||||||||
| }, | ||||||||||||||||||
| }); | ||||||||||||||||||
| // regex should be Parent Story Name + Test Name | ||||||||||||||||||
| expect(setTestNamePattern).toHaveBeenCalledWith( | ||||||||||||||||||
| new RegExp(`^Parent story${DOUBLE_SPACES} Test name$`) | ||||||||||||||||||
| ); | ||||||||||||||||||
|
Comment on lines
+239
to
+242
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Off‑by‑one space in regex (3 spaces instead of 2).
Apply this fix: - new RegExp(`^Parent story${DOUBLE_SPACES} Test name$`)
+ new RegExp(`^Parent story${DOUBLE_SPACES}Test name$`)📝 Committable suggestion
Suggested change
🧰 Tools🪛 ast-grep (0.38.6)[warning] 240-240: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns. (regexp-from-variable) 🤖 Prompt for AI Agents |
||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| it('should trigger all tests of a story', async () => { | ||||||||||||||||||
| vitest.globTestSpecifications.mockImplementation(() => tests); | ||||||||||||||||||
| const testManager = await TestManager.start(options); | ||||||||||||||||||
|
|
||||||||||||||||||
| await testManager.handleTriggerRunEvent({ | ||||||||||||||||||
| type: 'TRIGGER_RUN', | ||||||||||||||||||
| payload: { | ||||||||||||||||||
| storyIds: ['parent--story'], | ||||||||||||||||||
| triggeredBy: 'global', | ||||||||||||||||||
| }, | ||||||||||||||||||
| }); | ||||||||||||||||||
| expect(setTestNamePattern).toHaveBeenCalledWith(new RegExp(`^Parent story${DOUBLE_SPACES}`)); | ||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| it('should restart Vitest before a test run if coverage is enabled', async () => { | ||||||||||||||||||
| const testManager = await TestManager.start(options); | ||||||||||||||||||
| expect(createVitest).toHaveBeenCalledTimes(1); | ||||||||||||||||||
|
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -16,7 +16,6 @@ import * as find from 'empathic/find'; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import path, { dirname, join, normalize } from 'pathe'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import slash from 'slash'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { resolvePackageDir } from '../../../../core/src/shared/utils/module'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { COVERAGE_DIRECTORY } from '../constants'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { log } from '../logger'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { TriggerRunEvent } from '../types'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -30,6 +29,17 @@ const VITEST_WORKSPACE_FILE_EXTENSION = ['ts', 'js', 'json']; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // We have to tell Vitest that it runs as part of Storybook | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.env.VITEST_STORYBOOK = 'true'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * The Storybook vitest plugin adds double space characters so that it's possible to do a regex for | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * all test run use cases. Otherwise, if there were two unrelated stories like "Primary Button" and | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * "Primary Button Mobile", once you run tests for "Primary Button" and its children it would also | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * match "Primary Button Mobile". As it turns out, this limitation is also present in the Vitest | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * VSCode extension and the issue would occur with normal vitest tests as well, but because we use | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * double spaces, we circumvent the issue. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const DOUBLE_SPACES = ' '; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const getTestName = (name: string) => `${name}${DOUBLE_SPACES}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export class VitestManager { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| vitest: Vitest | null = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -168,7 +178,7 @@ export class VitestManager { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private async fetchStories(requestStoryIds?: string[]) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private async fetchStories(requestStoryIds?: string[]): Promise<StoryIndexEntry[]> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const indexUrl = this.testManager.store.getState().indexUrl; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!indexUrl) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -264,18 +274,50 @@ export class VitestManager { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await this.cancelCurrentRun(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const testSpecifications = await this.getStorybookTestSpecifications(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const stories = await this.fetchStories(runPayload?.storyIds); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const allStories = await this.fetchStories(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const filteredStories = runPayload.storyIds | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? allStories.filter((story) => runPayload.storyIds?.includes(story.id)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : allStories; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isSingleStoryRun = runPayload.storyIds?.length === 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isSingleStoryRun) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const storyName = stories[0].name; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const regex = new RegExp(`^${storyName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const selectedStory = filteredStories.find((story) => story.id === runPayload.storyIds?.[0]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!selectedStory) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`Story ${runPayload.storyIds?.[0]} not found`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const storyName = selectedStory.name; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let regex: RegExp; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isParentStory = allStories.some((story) => selectedStory.id === story.parent); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const hasParentStory = allStories.some((story) => selectedStory.parent === story.id); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isParentStory) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Use case 1: "Single" story run on a story with tests | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // -> run all tests of that story, as storyName is a describe block | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const parentName = getTestName(selectedStory.name); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| regex = new RegExp(`^${parentName}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (hasParentStory) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Use case 2: Single story run on a specific story test | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // in this case the regex pattern should be the story parentName + space + story.name | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const parentStory = allStories.find((story) => story.id === selectedStory.parent); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!parentStory) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`Parent story not found for story ${selectedStory.id}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const parentName = getTestName(parentStory.name); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| regex = new RegExp(`^${parentName} ${storyName}$`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Use case 3: Single story run on a story without tests, should be exact match of story name | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| regex = new RegExp(`^${storyName}$`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.vitest!.setGlobalTestNamePattern(regex); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+277
to
315
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Escape user/story names before building RegExp (ReDoS/invalid-pattern hardening) Story names can contain regex meta characters. Escape before interpolation to avoid unexpected matches or runtime errors. - if (isParentStory) {
+ const escapeForRegex = (s: string) => s.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
+ if (isParentStory) {
// Use case 1: "Single" story run on a story with tests
// -> run all tests of that story, as storyName is a describe block
const parentName = getTestName(selectedStory.name);
- regex = new RegExp(`^${parentName}`);
+ regex = new RegExp(`^${escapeForRegex(parentName)}`);
} else if (hasParentStory) {
@@
- const parentName = getTestName(parentStory.name);
- regex = new RegExp(`^${parentName} ${storyName}$`);
+ const parentName = getTestName(parentStory.name);
+ regex = new RegExp(`^${escapeForRegex(parentName)} ${escapeForRegex(storyName)}$`);
} else {
// Use case 3: Single story run on a story without tests, should be exact match of story name
- regex = new RegExp(`^${storyName}$`);
+ regex = new RegExp(`^${escapeForRegex(storyName)}$`);
}📝 Committable suggestion
Suggested change
🧰 Tools🪛 ast-grep (0.38.6)[warning] 300-300: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns. (regexp-from-variable) [warning] 310-310: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns. (regexp-from-variable) [warning] 313-313: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns. (regexp-from-variable) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { filteredTestSpecifications, filteredStoryIds } = this.filterTestSpecifications( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| testSpecifications, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stories | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| filteredStories | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.testManager.store.setState((s) => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import { type RunnerTask, type TaskMeta, type TestContext } from 'vitest'; | ||
|
|
||
| import type { ComponentAnnotations, ComposedStoryFn } from 'storybook/internal/types'; | ||
| import { type Meta, type Story, getStoryChildren, isStory, toTestId } from 'storybook/internal/csf'; | ||
| import type { ComponentAnnotations, ComposedStoryFn, Renderer } from 'storybook/internal/types'; | ||
|
|
||
| import { server } from '@vitest/browser/context'; | ||
| import { type Report, composeStory, getCsfFactoryAnnotations } from 'storybook/preview-api'; | ||
|
|
@@ -32,14 +33,24 @@ export const convertToFilePath = (url: string): string => { | |
|
|
||
| export const testStory = ( | ||
| exportName: string, | ||
| story: ComposedStoryFn, | ||
| meta: ComponentAnnotations, | ||
| skipTags: string[] | ||
| story: ComposedStoryFn | Story<Renderer>, | ||
| meta: ComponentAnnotations | Meta<Renderer>, | ||
| skipTags: string[], | ||
| storyId: string, | ||
| testName?: string | ||
| ) => { | ||
| return async (context: TestContext & { story: ComposedStoryFn }) => { | ||
| const annotations = getCsfFactoryAnnotations(story, meta); | ||
|
|
||
| const test = | ||
| isStory(story) && testName | ||
| ? getStoryChildren(story).find((child) => child.input.name === testName) | ||
| : undefined; | ||
|
|
||
| const storyAnnotations = test ? test.input : annotations.story; | ||
|
|
||
|
Comment on lines
+45
to
+51
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fail fast when a specific testName is not found. Silently running the base story when a test is missing is misleading. - const test =
- isStory(story) && testName
- ? getStoryChildren(story).find((child) => child.input.name === testName)
- : undefined;
+ const test =
+ isStory(story) && testName
+ ? getStoryChildren(story).find((child) => child.input.name === testName)
+ : undefined;
+ if (isStory(story) && testName && !test) {
+ context.skip();
+ return;
+ }
🤖 Prompt for AI Agents |
||
| const composedStory = composeStory( | ||
| annotations.story, | ||
| storyAnnotations, | ||
| annotations.meta!, | ||
| { initialGlobals: (await getInitialGlobals?.()) ?? {} }, | ||
| annotations.preview ?? globalThis.globalProjectAnnotations, | ||
|
|
@@ -55,10 +66,14 @@ export const testStory = ( | |
| const _task = context.task as RunnerTask & { | ||
| meta: TaskMeta & { storyId: string; reports: Report[] }; | ||
| }; | ||
| _task.meta.storyId = composedStory.id; | ||
|
|
||
| // The id will always be present, calculated by CsfFile | ||
| // and is needed so that we can add the test to the story in Storybook's UI for the status | ||
| _task.meta.storyId = storyId; | ||
|
|
||
| await setViewport(composedStory.parameters, composedStory.globals); | ||
| await composedStory.run(); | ||
|
|
||
| await composedStory.run(undefined); | ||
|
|
||
| _task.meta.reports = composedStory.reporting.reports; | ||
| }; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Good addition:
subtype: 'story'aligns with the new discriminant.This matches the PR’s subtype model and will help downstream narrowing. Please also ensure all mocked
entryobjects across stories/fixtures includesubtypefor consistency; addparentNameif the new types expect it.Run to find
entry:objects missingsubtypenear their definitions:Optional: add a test-entry variant to exercise the context menu for tests.
🏁 Script executed:
Length of output: 458
🏁 Script executed:
Length of output: 204
🏁 Script executed:
Length of output: 336
Add missing
subtypefields to mockedentryobjectsScan found these test fixtures lacking
subtype. Add the appropriatesubtypevalues (andparentNamewhere the types require):🤖 Prompt for AI Agents