@fragments-sdk/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +84 -0
- package/dist/index.d.ts +2873 -0
- package/dist/index.js +1431 -0
- package/dist/index.js.map +1 -0
- package/package.json +75 -0
- package/src/__tests__/preview-runtime.test.tsx +111 -0
- package/src/composition.test.ts +262 -0
- package/src/composition.ts +318 -0
- package/src/constants.ts +114 -0
- package/src/context.ts +2 -0
- package/src/defineFragment.ts +141 -0
- package/src/figma.ts +263 -0
- package/src/fragment-types.ts +214 -0
- package/src/index.ts +207 -0
- package/src/performance-presets.ts +142 -0
- package/src/preview-runtime.tsx +144 -0
- package/src/schema.ts +229 -0
- package/src/storyAdapter.test.ts +571 -0
- package/src/storyAdapter.ts +761 -0
- package/src/storyFilters.test.ts +350 -0
- package/src/storyFilters.ts +253 -0
- package/src/storybook-csf.ts +11 -0
- package/src/token-parser.ts +321 -0
- package/src/token-types.ts +287 -0
- package/src/types.ts +784 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for storyModuleToFragment and related utilities.
|
|
3
|
+
* Tests both CSF2 (Template.bind) and CSF3 (object stories) patterns.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
7
|
+
import { createElement, type ComponentType } from "react";
|
|
8
|
+
import {
|
|
9
|
+
storyModuleToFragment,
|
|
10
|
+
setPreviewConfig,
|
|
11
|
+
toId,
|
|
12
|
+
storyNameFromExport,
|
|
13
|
+
isExportStory,
|
|
14
|
+
type StoryModule,
|
|
15
|
+
type StoryMeta,
|
|
16
|
+
type Story,
|
|
17
|
+
type CSF2Story,
|
|
18
|
+
type PreviewConfig,
|
|
19
|
+
} from "./storyAdapter.js";
|
|
20
|
+
|
|
21
|
+
// Mock component for testing
|
|
22
|
+
const MockComponent = (({ label = "Test" }: { label?: string }) =>
|
|
23
|
+
createElement("button", null, label)) as ComponentType<unknown>;
|
|
24
|
+
(MockComponent as unknown as { displayName: string }).displayName = "MockComponent";
|
|
25
|
+
|
|
26
|
+
describe("storyModuleToFragment", () => {
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
// Reset preview config before each test
|
|
29
|
+
setPreviewConfig({});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("basic conversion", () => {
|
|
33
|
+
it("should convert a basic story module to a fragment definition", () => {
|
|
34
|
+
const storyModule: StoryModule = {
|
|
35
|
+
default: {
|
|
36
|
+
title: "Components/Button",
|
|
37
|
+
component: MockComponent,
|
|
38
|
+
},
|
|
39
|
+
Primary: {
|
|
40
|
+
args: { label: "Primary" },
|
|
41
|
+
} as Story,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const fragment = storyModuleToFragment(storyModule, "Button.stories.tsx");
|
|
45
|
+
|
|
46
|
+
expect(fragment).not.toBeNull();
|
|
47
|
+
expect(fragment!.meta.name).toBe("Button");
|
|
48
|
+
expect(fragment!.meta.category).toBe("general");
|
|
49
|
+
expect(fragment!.component).toBe(MockComponent);
|
|
50
|
+
expect(fragment!.variants).toHaveLength(1);
|
|
51
|
+
expect(fragment!.variants[0].name).toBe("Primary");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should return null for stories without a component", () => {
|
|
55
|
+
const storyModule: StoryModule = {
|
|
56
|
+
default: {
|
|
57
|
+
title: "Docs/Introduction",
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const fragment = storyModuleToFragment(storyModule, "Intro.stories.tsx");
|
|
62
|
+
expect(fragment).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should extract component name from title path", () => {
|
|
66
|
+
const storyModule: StoryModule = {
|
|
67
|
+
default: {
|
|
68
|
+
title: "Design System/Forms/TextField",
|
|
69
|
+
component: MockComponent,
|
|
70
|
+
},
|
|
71
|
+
Default: { args: {} } as Story,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const fragment = storyModuleToFragment(storyModule, "TextField.stories.tsx");
|
|
75
|
+
|
|
76
|
+
expect(fragment!.meta.name).toBe("TextField");
|
|
77
|
+
expect(fragment!.meta.category).toBe("forms");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should use component displayName when title is not provided", () => {
|
|
81
|
+
const storyModule: StoryModule = {
|
|
82
|
+
default: {
|
|
83
|
+
component: MockComponent,
|
|
84
|
+
},
|
|
85
|
+
Default: { args: {} } as Story,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const fragment = storyModuleToFragment(storyModule, "path/to/Component.stories.tsx");
|
|
89
|
+
|
|
90
|
+
expect(fragment!.meta.name).toBe("MockComponent");
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("CSF2 Template.bind() pattern", () => {
|
|
95
|
+
it("should handle CSF2 story functions", () => {
|
|
96
|
+
// Simulate Template.bind({}) pattern
|
|
97
|
+
const Template: CSF2Story = (args) =>
|
|
98
|
+
createElement(MockComponent, args);
|
|
99
|
+
|
|
100
|
+
const Primary: CSF2Story = Template.bind({});
|
|
101
|
+
Primary.args = { label: "Primary Button" };
|
|
102
|
+
|
|
103
|
+
const Secondary: CSF2Story = Template.bind({});
|
|
104
|
+
Secondary.args = { label: "Secondary Button" };
|
|
105
|
+
|
|
106
|
+
const storyModule: StoryModule = {
|
|
107
|
+
default: {
|
|
108
|
+
title: "Components/Button",
|
|
109
|
+
component: MockComponent,
|
|
110
|
+
},
|
|
111
|
+
Primary,
|
|
112
|
+
Secondary,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const fragment = storyModuleToFragment(storyModule, "Button.stories.tsx");
|
|
116
|
+
|
|
117
|
+
expect(fragment!.variants).toHaveLength(2);
|
|
118
|
+
expect(fragment!.variants[0].name).toBe("Primary");
|
|
119
|
+
expect(fragment!.variants[1].name).toBe("Secondary");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should use storyName when provided on CSF2 story", () => {
|
|
123
|
+
const Template: CSF2Story = (args) =>
|
|
124
|
+
createElement(MockComponent, args);
|
|
125
|
+
|
|
126
|
+
const MyStory: CSF2Story = Template.bind({});
|
|
127
|
+
MyStory.args = { label: "Test" };
|
|
128
|
+
MyStory.storyName = "Custom Story Name";
|
|
129
|
+
|
|
130
|
+
const storyModule: StoryModule = {
|
|
131
|
+
default: {
|
|
132
|
+
title: "Components/Button",
|
|
133
|
+
component: MockComponent,
|
|
134
|
+
},
|
|
135
|
+
MyStory,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const fragment = storyModuleToFragment(storyModule, "Button.stories.tsx");
|
|
139
|
+
|
|
140
|
+
expect(fragment!.variants[0].name).toBe("Custom Story Name");
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("CSF3 object stories", () => {
|
|
145
|
+
it("should handle CSF3 story objects with args", () => {
|
|
146
|
+
const storyModule: StoryModule = {
|
|
147
|
+
default: {
|
|
148
|
+
title: "Components/Button",
|
|
149
|
+
component: MockComponent,
|
|
150
|
+
},
|
|
151
|
+
Primary: {
|
|
152
|
+
args: { label: "Primary" },
|
|
153
|
+
} as Story,
|
|
154
|
+
WithCustomRender: {
|
|
155
|
+
args: { label: "Custom" },
|
|
156
|
+
render: (args) => createElement("div", null, createElement(MockComponent, args)),
|
|
157
|
+
} as Story,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const fragment = storyModuleToFragment(storyModule, "Button.stories.tsx");
|
|
161
|
+
|
|
162
|
+
expect(fragment!.variants).toHaveLength(2);
|
|
163
|
+
expect(fragment!.variants[0].name).toBe("Primary");
|
|
164
|
+
expect(fragment!.variants[1].name).toBe("With Custom Render");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("should use story name property when provided", () => {
|
|
168
|
+
const storyModule: StoryModule = {
|
|
169
|
+
default: {
|
|
170
|
+
title: "Components/Button",
|
|
171
|
+
component: MockComponent,
|
|
172
|
+
},
|
|
173
|
+
MyStory: {
|
|
174
|
+
name: "Explicit Name",
|
|
175
|
+
args: { label: "Test" },
|
|
176
|
+
} as Story,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const fragment = storyModuleToFragment(storyModule, "Button.stories.tsx");
|
|
180
|
+
|
|
181
|
+
expect(fragment!.variants[0].name).toBe("Explicit Name");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("should detect play functions", () => {
|
|
185
|
+
const storyModule: StoryModule = {
|
|
186
|
+
default: {
|
|
187
|
+
title: "Components/Button",
|
|
188
|
+
component: MockComponent,
|
|
189
|
+
},
|
|
190
|
+
WithInteraction: {
|
|
191
|
+
args: { label: "Click me" },
|
|
192
|
+
play: async () => {
|
|
193
|
+
// Interaction test
|
|
194
|
+
},
|
|
195
|
+
} as Story,
|
|
196
|
+
NoInteraction: {
|
|
197
|
+
args: { label: "No interaction" },
|
|
198
|
+
} as Story,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const fragment = storyModuleToFragment(storyModule, "Button.stories.tsx");
|
|
202
|
+
|
|
203
|
+
expect(fragment!.variants[0].hasPlayFunction).toBe(true);
|
|
204
|
+
expect(fragment!.variants[1].hasPlayFunction).toBeUndefined();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe("argTypes mapping", () => {
|
|
209
|
+
it("should convert basic argTypes to prop definitions", () => {
|
|
210
|
+
const storyModule: StoryModule = {
|
|
211
|
+
default: {
|
|
212
|
+
title: "Components/Button",
|
|
213
|
+
component: MockComponent,
|
|
214
|
+
argTypes: {
|
|
215
|
+
label: {
|
|
216
|
+
control: "text",
|
|
217
|
+
description: "Button label text",
|
|
218
|
+
},
|
|
219
|
+
disabled: {
|
|
220
|
+
control: "boolean",
|
|
221
|
+
description: "Disable the button",
|
|
222
|
+
},
|
|
223
|
+
size: {
|
|
224
|
+
control: "select",
|
|
225
|
+
options: ["small", "medium", "large"],
|
|
226
|
+
description: "Button size",
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
Default: { args: {} } as Story,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const fragment = storyModuleToFragment(storyModule, "Button.stories.tsx");
|
|
234
|
+
|
|
235
|
+
expect(fragment!.props.label.type).toBe("string");
|
|
236
|
+
expect(fragment!.props.label.description).toBe("Button label text");
|
|
237
|
+
expect(fragment!.props.disabled.type).toBe("boolean");
|
|
238
|
+
expect(fragment!.props.size.type).toBe("enum");
|
|
239
|
+
expect(fragment!.props.size.values).toEqual(["small", "medium", "large"]);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("should handle all control types", () => {
|
|
243
|
+
const storyModule: StoryModule = {
|
|
244
|
+
default: {
|
|
245
|
+
title: "Components/Test",
|
|
246
|
+
component: MockComponent,
|
|
247
|
+
argTypes: {
|
|
248
|
+
text: { control: "text" },
|
|
249
|
+
number: { control: "number" },
|
|
250
|
+
range: { control: { type: "range", min: 0, max: 100 } },
|
|
251
|
+
boolean: { control: "boolean" },
|
|
252
|
+
color: { control: "color" },
|
|
253
|
+
date: { control: "date" },
|
|
254
|
+
object: { control: "object" },
|
|
255
|
+
radio: { control: "radio", options: ["a", "b"] },
|
|
256
|
+
inlineRadio: { control: "inline-radio", options: ["x", "y"] },
|
|
257
|
+
select: { control: "select", options: ["1", "2"] },
|
|
258
|
+
multiSelect: { control: "multi-select", options: ["m", "n"] },
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
Default: { args: {} } as Story,
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const fragment = storyModuleToFragment(storyModule, "Test.stories.tsx");
|
|
265
|
+
|
|
266
|
+
expect(fragment!.props.text.type).toBe("string");
|
|
267
|
+
expect(fragment!.props.number.type).toBe("number");
|
|
268
|
+
expect(fragment!.props.range.type).toBe("number");
|
|
269
|
+
expect(fragment!.props.range.controlType).toBe("range");
|
|
270
|
+
expect(fragment!.props.range.controlOptions).toEqual({ min: 0, max: 100 });
|
|
271
|
+
expect(fragment!.props.boolean.type).toBe("boolean");
|
|
272
|
+
expect(fragment!.props.color.type).toBe("string");
|
|
273
|
+
expect(fragment!.props.color.controlType).toBe("color");
|
|
274
|
+
expect(fragment!.props.date.type).toBe("string");
|
|
275
|
+
expect(fragment!.props.date.controlType).toBe("date");
|
|
276
|
+
expect(fragment!.props.object.type).toBe("object");
|
|
277
|
+
expect(fragment!.props.radio.type).toBe("enum");
|
|
278
|
+
expect(fragment!.props.inlineRadio.type).toBe("enum");
|
|
279
|
+
expect(fragment!.props.select.type).toBe("enum");
|
|
280
|
+
expect(fragment!.props.multiSelect.type).toBe("enum");
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("should handle action argTypes as functions", () => {
|
|
284
|
+
const storyModule: StoryModule = {
|
|
285
|
+
default: {
|
|
286
|
+
title: "Components/Button",
|
|
287
|
+
component: MockComponent,
|
|
288
|
+
argTypes: {
|
|
289
|
+
onClick: {
|
|
290
|
+
action: "clicked",
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
Default: { args: {} } as Story,
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const fragment = storyModuleToFragment(storyModule, "Button.stories.tsx");
|
|
298
|
+
|
|
299
|
+
expect(fragment!.props.onClick.type).toBe("function");
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("should skip disabled argTypes", () => {
|
|
303
|
+
const storyModule: StoryModule = {
|
|
304
|
+
default: {
|
|
305
|
+
title: "Components/Button",
|
|
306
|
+
component: MockComponent,
|
|
307
|
+
argTypes: {
|
|
308
|
+
visible: { control: "boolean" },
|
|
309
|
+
hidden: { control: "boolean", table: { disable: true } },
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
Default: { args: {} } as Story,
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const fragment = storyModuleToFragment(storyModule, "Button.stories.tsx");
|
|
316
|
+
|
|
317
|
+
expect(fragment!.props.visible).toBeDefined();
|
|
318
|
+
expect(fragment!.props.hidden).toBeUndefined();
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe("decorators", () => {
|
|
323
|
+
it("should apply story decorators", () => {
|
|
324
|
+
let decoratorCalled = false;
|
|
325
|
+
|
|
326
|
+
const storyModule: StoryModule = {
|
|
327
|
+
default: {
|
|
328
|
+
title: "Components/Button",
|
|
329
|
+
component: MockComponent,
|
|
330
|
+
},
|
|
331
|
+
WithDecorator: {
|
|
332
|
+
args: { label: "Test" },
|
|
333
|
+
decorators: [
|
|
334
|
+
(Story) => {
|
|
335
|
+
decoratorCalled = true;
|
|
336
|
+
return createElement("div", { className: "wrapper" }, Story());
|
|
337
|
+
},
|
|
338
|
+
],
|
|
339
|
+
} as Story,
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const fragment = storyModuleToFragment(storyModule, "Button.stories.tsx");
|
|
343
|
+
fragment!.variants[0].render();
|
|
344
|
+
|
|
345
|
+
expect(decoratorCalled).toBe(true);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("should apply meta decorators to all stories", () => {
|
|
349
|
+
let metaDecoratorCount = 0;
|
|
350
|
+
|
|
351
|
+
const storyModule: StoryModule = {
|
|
352
|
+
default: {
|
|
353
|
+
title: "Components/Button",
|
|
354
|
+
component: MockComponent,
|
|
355
|
+
decorators: [
|
|
356
|
+
(Story) => {
|
|
357
|
+
metaDecoratorCount++;
|
|
358
|
+
return Story();
|
|
359
|
+
},
|
|
360
|
+
],
|
|
361
|
+
},
|
|
362
|
+
Story1: { args: { label: "1" } } as Story,
|
|
363
|
+
Story2: { args: { label: "2" } } as Story,
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const fragment = storyModuleToFragment(storyModule, "Button.stories.tsx");
|
|
367
|
+
fragment!.variants[0].render();
|
|
368
|
+
fragment!.variants[1].render();
|
|
369
|
+
|
|
370
|
+
expect(metaDecoratorCount).toBe(2);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("should apply global decorators from preview config", () => {
|
|
374
|
+
let globalDecoratorCalled = false;
|
|
375
|
+
|
|
376
|
+
setPreviewConfig({
|
|
377
|
+
decorators: [
|
|
378
|
+
(Story) => {
|
|
379
|
+
globalDecoratorCalled = true;
|
|
380
|
+
return Story();
|
|
381
|
+
},
|
|
382
|
+
],
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
const storyModule: StoryModule = {
|
|
386
|
+
default: {
|
|
387
|
+
title: "Components/Button",
|
|
388
|
+
component: MockComponent,
|
|
389
|
+
},
|
|
390
|
+
Default: { args: { label: "Test" } } as Story,
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const fragment = storyModuleToFragment(storyModule, "Button.stories.tsx");
|
|
394
|
+
fragment!.variants[0].render();
|
|
395
|
+
|
|
396
|
+
expect(globalDecoratorCalled).toBe(true);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
describe("includeStories/excludeStories filtering", () => {
|
|
401
|
+
it("should filter stories using includeStories array", () => {
|
|
402
|
+
const storyModule: StoryModule = {
|
|
403
|
+
default: {
|
|
404
|
+
title: "Components/Button",
|
|
405
|
+
component: MockComponent,
|
|
406
|
+
includeStories: ["Primary", "Secondary"],
|
|
407
|
+
},
|
|
408
|
+
Primary: { args: { label: "Primary" } } as Story,
|
|
409
|
+
Secondary: { args: { label: "Secondary" } } as Story,
|
|
410
|
+
Excluded: { args: { label: "Excluded" } } as Story,
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const fragment = storyModuleToFragment(storyModule, "Button.stories.tsx");
|
|
414
|
+
|
|
415
|
+
expect(fragment!.variants).toHaveLength(2);
|
|
416
|
+
expect(fragment!.variants.map((v) => v.name)).toContain("Primary");
|
|
417
|
+
expect(fragment!.variants.map((v) => v.name)).toContain("Secondary");
|
|
418
|
+
expect(fragment!.variants.map((v) => v.name)).not.toContain("Excluded");
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it("should filter stories using excludeStories array", () => {
|
|
422
|
+
const storyModule: StoryModule = {
|
|
423
|
+
default: {
|
|
424
|
+
title: "Components/Button",
|
|
425
|
+
component: MockComponent,
|
|
426
|
+
excludeStories: ["Internal"],
|
|
427
|
+
},
|
|
428
|
+
Primary: { args: { label: "Primary" } } as Story,
|
|
429
|
+
Internal: { args: { label: "Internal" } } as Story,
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const fragment = storyModuleToFragment(storyModule, "Button.stories.tsx");
|
|
433
|
+
|
|
434
|
+
expect(fragment!.variants).toHaveLength(1);
|
|
435
|
+
expect(fragment!.variants[0].name).toBe("Primary");
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it("should filter stories using excludeStories regex", () => {
|
|
439
|
+
const storyModule: StoryModule = {
|
|
440
|
+
default: {
|
|
441
|
+
title: "Components/Button",
|
|
442
|
+
component: MockComponent,
|
|
443
|
+
excludeStories: /^_/,
|
|
444
|
+
},
|
|
445
|
+
Primary: { args: { label: "Primary" } } as Story,
|
|
446
|
+
_Private: { args: { label: "Private" } } as Story,
|
|
447
|
+
_Internal: { args: { label: "Internal" } } as Story,
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const fragment = storyModuleToFragment(storyModule, "Button.stories.tsx");
|
|
451
|
+
|
|
452
|
+
expect(fragment!.variants).toHaveLength(1);
|
|
453
|
+
expect(fragment!.variants[0].name).toBe("Primary");
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
describe("loaders", () => {
|
|
458
|
+
it("should collect loaders from story", () => {
|
|
459
|
+
const storyModule: StoryModule = {
|
|
460
|
+
default: {
|
|
461
|
+
title: "Components/Button",
|
|
462
|
+
component: MockComponent,
|
|
463
|
+
},
|
|
464
|
+
WithLoader: {
|
|
465
|
+
args: { label: "Test" },
|
|
466
|
+
loaders: [async () => ({ data: "loaded" })],
|
|
467
|
+
} as Story,
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const fragment = storyModuleToFragment(storyModule, "Button.stories.tsx");
|
|
471
|
+
|
|
472
|
+
expect(fragment!.variants[0].loaders).toBeDefined();
|
|
473
|
+
expect(fragment!.variants[0].loaders).toHaveLength(1);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it("should collect loaders from meta and story", () => {
|
|
477
|
+
const storyModule: StoryModule = {
|
|
478
|
+
default: {
|
|
479
|
+
title: "Components/Button",
|
|
480
|
+
component: MockComponent,
|
|
481
|
+
loaders: [async () => ({ metaData: "meta" })],
|
|
482
|
+
},
|
|
483
|
+
WithLoader: {
|
|
484
|
+
args: { label: "Test" },
|
|
485
|
+
loaders: [async () => ({ storyData: "story" })],
|
|
486
|
+
} as Story,
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const fragment = storyModuleToFragment(storyModule, "Button.stories.tsx");
|
|
490
|
+
|
|
491
|
+
// Should have both meta and story loaders
|
|
492
|
+
expect(fragment!.variants[0].loaders).toHaveLength(2);
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
describe("story ID generation", () => {
|
|
497
|
+
it("should generate correct story IDs using toId", () => {
|
|
498
|
+
const storyModule: StoryModule = {
|
|
499
|
+
default: {
|
|
500
|
+
title: "Components/Forms/Button",
|
|
501
|
+
component: MockComponent,
|
|
502
|
+
},
|
|
503
|
+
Primary: { args: { label: "Primary" } } as Story,
|
|
504
|
+
WithIcon: { args: { label: "With Icon" } } as Story,
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
const fragment = storyModuleToFragment(storyModule, "Button.stories.tsx");
|
|
508
|
+
|
|
509
|
+
// toId lowercases the export name without adding hyphens
|
|
510
|
+
expect(fragment!.variants[0].storyId).toBe("components-forms-button--primary");
|
|
511
|
+
expect(fragment!.variants[1].storyId).toBe("components-forms-button--withicon");
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
describe("@storybook/csf utilities", () => {
|
|
517
|
+
describe("toId", () => {
|
|
518
|
+
it("should generate kebab-case story IDs", () => {
|
|
519
|
+
// toId converts title to kebab-case and lowercases export name
|
|
520
|
+
expect(toId("Components/Button", "Primary")).toBe("components-button--primary");
|
|
521
|
+
expect(toId("Design System/Forms/Input", "WithLabel")).toBe(
|
|
522
|
+
"design-system-forms-input--withlabel"
|
|
523
|
+
);
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
describe("storyNameFromExport", () => {
|
|
528
|
+
it("should convert export names to display names", () => {
|
|
529
|
+
expect(storyNameFromExport("Primary")).toBe("Primary");
|
|
530
|
+
expect(storyNameFromExport("WithIcon")).toBe("With Icon");
|
|
531
|
+
expect(storyNameFromExport("PrimaryButton")).toBe("Primary Button");
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
describe("isExportStory", () => {
|
|
536
|
+
it("should identify valid story exports", () => {
|
|
537
|
+
const meta: StoryMeta = {
|
|
538
|
+
title: "Test",
|
|
539
|
+
component: MockComponent,
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
// All named exports are considered potential stories by isExportStory
|
|
543
|
+
// The actual filtering of non-story exports (like "default") happens
|
|
544
|
+
// in our extractVariants function where we check for story-like objects
|
|
545
|
+
expect(isExportStory("Primary", meta)).toBe(true);
|
|
546
|
+
expect(isExportStory("Secondary", meta)).toBe(true);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it("should respect includeStories", () => {
|
|
550
|
+
const meta: StoryMeta = {
|
|
551
|
+
title: "Test",
|
|
552
|
+
component: MockComponent,
|
|
553
|
+
includeStories: ["Primary"],
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
expect(isExportStory("Primary", meta)).toBe(true);
|
|
557
|
+
expect(isExportStory("Secondary", meta)).toBe(false);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it("should respect excludeStories", () => {
|
|
561
|
+
const meta: StoryMeta = {
|
|
562
|
+
title: "Test",
|
|
563
|
+
component: MockComponent,
|
|
564
|
+
excludeStories: ["Internal"],
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
expect(isExportStory("Primary", meta)).toBe(true);
|
|
568
|
+
expect(isExportStory("Internal", meta)).toBe(false);
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
});
|