generated from storybookjs/addon-kit
-
Notifications
You must be signed in to change notification settings - Fork 72
/
transformPlaywrightJson.ts
200 lines (179 loc) Β· 6.2 KB
/
transformPlaywrightJson.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
import * as t from '@babel/types';
import generate from '@babel/generator';
import { ComponentTitle, StoryId, StoryName, toId } from '@storybook/csf';
import { testPrefixer } from './transformPlaywright';
import { getTagOptions } from '../util/getTagOptions';
const makeTest = ({
entry,
shouldSkip,
metaOrStoryPlay,
}: {
entry: V4Entry;
shouldSkip: boolean;
metaOrStoryPlay: boolean;
}): t.Statement => {
const result = testPrefixer({
name: t.stringLiteral(entry.name),
title: t.stringLiteral(entry.title),
id: t.stringLiteral(entry.id),
// FIXME
storyExport: t.identifier(entry.id),
});
const stmt = (result as Array<t.ExpressionStatement>)[1];
return t.expressionStatement(
t.callExpression(shouldSkip ? t.identifier('it.skip') : t.identifier('it'), [
t.stringLiteral(metaOrStoryPlay ? 'play-test' : 'smoke-test'),
stmt.expression,
])
);
};
export const makeDescribe = (title: string, stmts: t.Statement[]) => {
// When there are no tests at all, we skip. The reason is that the file already went through Jest's transformation,
// so we have to skip the describe to achieve a "excluded test" experience.
// The code below recreates the following source:
// describe.skip(`${title}`, () => { it('no-op', () => {}) });
if (stmts.length === 0) {
const noOpIt = t.expressionStatement(
t.callExpression(t.identifier('it'), [
t.stringLiteral('no-op'),
t.arrowFunctionExpression([], t.blockStatement([])),
])
);
return t.expressionStatement(
t.callExpression(t.memberExpression(t.identifier('describe'), t.identifier('skip')), [
t.stringLiteral(title),
t.arrowFunctionExpression([], t.blockStatement([noOpIt])),
])
);
}
return t.expressionStatement(
t.callExpression(t.identifier('describe'), [
t.stringLiteral(title),
t.arrowFunctionExpression([], t.blockStatement(stmts)),
])
);
};
type V3Story = Omit<V4Entry, 'type'> & { parameters?: StoryParameters };
export type V3StoriesIndex = {
v: 3;
stories: Record<StoryId, V3Story>;
};
type V4Entry = {
type?: 'story' | 'docs';
id: StoryId;
name: StoryName;
title: ComponentTitle;
tags?: string[];
};
export type V4Index = {
v: 4;
entries: Record<StoryId, V4Entry>;
};
type V5Entry = V4Entry & { tags: string[] };
export type V5Index = {
v: 5;
entries: Record<StoryId, V5Entry>;
};
type StoryParameters = {
__id: StoryId;
docsOnly?: boolean;
fileName?: string;
};
export type UnsupportedVersion = { v: number };
const isV3DocsOnly = (stories: V3Story[]) => stories.length === 1 && stories[0].name === 'Page';
function v3TitleMapToV4TitleMap(titleIdToStories: Record<string, V3Story[]>) {
return Object.fromEntries(
Object.entries(titleIdToStories).map(([id, stories]) => [
id,
stories.map(
({ parameters, ...story }) =>
({
type: isV3DocsOnly(stories) ? 'docs' : 'story',
tags: isV3DocsOnly(stories) ? [] : ['test', 'dev'],
...story,
}) satisfies V4Entry
),
])
);
}
/**
* Storybook 8.0 and below did not automatically tag stories with 'dev'.
* Therefore Storybook 8.1 and above would not show composed 8.0 stories by default.
* This function adds the 'dev' tag to all stories in the index to workaround this issue.
*/
function v4TitleMapToV5TitleMap(titleIdToStories: Record<string, V4Entry[]>) {
return Object.fromEntries(
Object.entries(titleIdToStories).map(([id, stories]) => [
id,
stories.map(
(story) =>
({
...story,
tags: story.tags ? ['test', 'dev', ...story.tags] : ['test', 'dev'],
}) satisfies V4Entry
),
])
);
}
function groupByTitleId<T extends { title: ComponentTitle }>(entries: T[]) {
return entries.reduce<Record<string, T[]>>((acc, entry) => {
const titleId = toId(entry.title);
acc[titleId] = acc[titleId] || [];
acc[titleId].push(entry);
return acc;
}, {});
}
/**
* Generate one test file per component so that Jest can
* run them in parallel.
*/
export const transformPlaywrightJson = (index: V3StoriesIndex | V4Index | UnsupportedVersion) => {
let titleIdToEntries: Record<string, V4Entry[]>;
if (index.v === 3) {
const titleIdToStories = groupByTitleId<V3Story>(
Object.values((index as V3StoriesIndex).stories)
);
titleIdToEntries = v3TitleMapToV4TitleMap(titleIdToStories);
} else if (index.v === 4) {
// TODO: Once Storybook 8.0 is released, we should only support v4 and higher
titleIdToEntries = groupByTitleId<V4Entry>(Object.values((index as V4Index).entries));
titleIdToEntries = v4TitleMapToV5TitleMap(titleIdToEntries);
} else if (index.v === 5) {
titleIdToEntries = groupByTitleId<V4Entry>(Object.values((index as V4Index).entries));
} else {
throw new Error(`Unsupported version ${index.v}`);
}
const { includeTags, excludeTags, skipTags } = getTagOptions();
const titleIdToTest = Object.entries(titleIdToEntries).reduce<Record<string, string>>(
(acc, [titleId, entries]) => {
const stories = entries.filter((s) => s.type !== 'docs');
if (stories.length) {
const storyTests = stories
.filter((story) => {
// If includeTags is passed, check if the story has any of them - else include by default
const isIncluded =
includeTags.length === 0 || includeTags.some((tag) => story.tags?.includes(tag));
// If excludeTags is passed, check if the story does not have any of them
const isNotExcluded = excludeTags.every((tag) => !story.tags?.includes(tag));
return isIncluded && isNotExcluded;
})
.map((story) => {
const shouldSkip = skipTags.some((tag) => story.tags?.includes(tag));
return makeDescribe(story.name, [
makeTest({
entry: story,
shouldSkip,
metaOrStoryPlay: story.tags?.includes('play-fn') ?? false,
}),
]);
});
const program = t.program([makeDescribe(stories[0].title, storyTests)]);
const { code } = generate(program, {});
acc[titleId] = code;
}
return acc;
},
{}
);
return titleIdToTest;
};