Skip to content

Commit 307fbec

Browse files
feat: allow custom stories baseline path (#797)
* feat: allow custom stories baseline path fixes #786 * feat: reuse stories baselines folder structure in actual and diff folders --------- Co-authored-by: Wim Selles <[email protected]>
1 parent 782f068 commit 307fbec

8 files changed

Lines changed: 71 additions & 23 deletions

File tree

.changeset/ten-radios-pull.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"webdriver-image-comparison": minor
3+
"@wdio/visual-service": minor
4+
---
5+
6+
Add `getStoriesBaselinePath` to Storybook Runner API, enabling custom file paths (e.g. files with a flat hierarchy in the baselines folder)

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ export const config: WebdriverIO.Config = {
5858
skipStories: ['example-button--secondary', 'example-button--small'],
5959
url: 'https://www.bbc.co.uk/iplayer/storybook/',
6060
version: 6,
61+
// Optional - Allows overriding the baselines path. By default it will group the baselines by category and component (e.g. forms/input/baseline.png)
62+
getStoriesBaselinePath: (category, component) => `path__${category}__${component}`,
6163
},
6264
},
6365
],

packages/visual-service/src/storybook/Types.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { CheckElementMethodOptions, Folders } from 'webdriver-image-comparison'
1+
import type { CheckElementMethodOptions, ClassOptions, Folders } from 'webdriver-image-comparison'
22

33
export interface StorybookData {
44
id: string;
@@ -41,7 +41,7 @@ export type CreateTestFileOptions = {
4141
skipStories: string[] | RegExp,
4242
storiesJson: StorybookData[],
4343
storybookUrl: string;
44-
}
44+
} & Pick<CreateTestContent, 'getStoriesBaselinePath'>
4545

4646
export interface CapabilityMap {
4747
chrome: WebdriverIO.Capabilities;
@@ -60,7 +60,7 @@ export type CreateTestContent = {
6060
skipStories: string[] | RegExp;
6161
stories: StorybookData[];
6262
storybookUrl: string;
63-
}
63+
} & Pick<CreateItContent, 'getStoriesBaselinePath'>
6464

6565
export type CreateItContent = {
6666
additionalSearchParams: URLSearchParams;
@@ -72,7 +72,7 @@ export type CreateItContent = {
7272
skipStories: string[] | RegExp;
7373
storyData: StorybookData;
7474
storybookUrl: string;
75-
}
75+
} & Pick<NonNullable<ClassOptions['storybook']>, 'getStoriesBaselinePath'>
7676

7777
export type CategoryComponent = { category: string, component: string }
7878

packages/visual-service/src/storybook/launcher.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export default class VisualLauncher extends BaseClass {
9898
const additionalSearchParamsOption = this.#options?.storybook?.additionalSearchParams
9999
const additionalSearchParamsArgv = getArgvValue('--additionalSearchParams', value => new URLSearchParams(value))
100100
const additionalSearchParams = additionalSearchParamsOption ?? additionalSearchParamsArgv ?? new URLSearchParams()
101+
const getStoriesBaselinePath = this.#options?.storybook?.getStoriesBaselinePath
101102

102103
// Create the test files
103104
createTestFiles({
@@ -108,6 +109,7 @@ export default class VisualLauncher extends BaseClass {
108109
directoryPath: tempDir,
109110
folders: this.folders,
110111
framework,
112+
getStoriesBaselinePath,
111113
numShards,
112114
skipStories: parsedSkipStories,
113115
storiesJson,

packages/visual-service/src/storybook/utils.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -149,11 +149,19 @@ export function getArgvValue<ParseFuncReturnType>(
149149
return undefined
150150
}
151151

152+
/**
153+
* Get the story baseline path for the given category and component
154+
*/
155+
const getStoriesBaselinePathFn = ((
156+
category: CategoryComponent['category'],
157+
component: CategoryComponent['component']
158+
) => `./${category}/${component}/`)
159+
152160
/**
153161
* Creates a it function for the test file
154162
* @TODO: improve this
155163
*/
156-
export function itFunction({ additionalSearchParams, clip, clipSelector, compareOptions, folders: { baselineFolder }, framework, skipStories, storyData, storybookUrl }: CreateItContent) {
164+
export function itFunction({ additionalSearchParams, clip, clipSelector, compareOptions, folders, framework, skipStories, storyData, storybookUrl, getStoriesBaselinePath = getStoriesBaselinePathFn }: CreateItContent) {
157165
const { id } = storyData
158166
const screenshotType = clip ? 'n element' : ' viewport'
159167
const DEFAULT_IT_TEXT = 'it'
@@ -173,9 +181,12 @@ export function itFunction({ additionalSearchParams, clip, clipSelector, compare
173181

174182
// Setup the folder structure
175183
const { category, component } = extractCategoryAndComponent(id)
184+
const storiesBaselinePath = getStoriesBaselinePath(category, component)
176185
const checkMethodOptions = {
177186
...compareOptions,
178-
baselineFolder: join(baselineFolder, `./${category}/${component}/`),
187+
actualFolder: join(folders.actualFolder, storiesBaselinePath),
188+
baselineFolder: join(folders.baselineFolder, storiesBaselinePath),
189+
diffFolder: join(folders.diffFolder, storiesBaselinePath),
179190
}
180191

181192
const it = `
@@ -212,11 +223,11 @@ export function writeTestFile(directoryPath: string, fileID: string, testContent
212223
* Create the test content
213224
*/
214225
export function createTestContent(
215-
{ additionalSearchParams, clip, clipSelector, compareOptions, folders, framework, skipStories, stories, storybookUrl }: CreateTestContent,
226+
{ additionalSearchParams, clip, clipSelector, compareOptions, folders, framework, getStoriesBaselinePath, skipStories, stories, storybookUrl }: CreateTestContent,
216227
// For testing purposes only
217228
itFunc = itFunction
218229
): string {
219-
const itFunctionOptions = { additionalSearchParams, clip, clipSelector, compareOptions, folders, framework, skipStories, storybookUrl }
230+
const itFunctionOptions = { additionalSearchParams, clip, clipSelector, compareOptions, folders, framework, getStoriesBaselinePath, skipStories, storybookUrl }
220231

221232
return stories.reduce((acc, storyData) => acc + itFunc({ ...itFunctionOptions, storyData }), '')
222233
}
@@ -319,14 +330,14 @@ function filterStories(storiesJson: Stories): StorybookData[] {
319330
* Create the test files
320331
*/
321332
export function createTestFiles(
322-
{ additionalSearchParams, clip, clipSelector, compareOptions, directoryPath, folders, framework, numShards, skipStories, storiesJson, storybookUrl }: CreateTestFileOptions,
333+
{ additionalSearchParams, clip, clipSelector, compareOptions, directoryPath, folders, framework, getStoriesBaselinePath, numShards, skipStories, storiesJson, storybookUrl }: CreateTestFileOptions,
323334
// For testing purposes only
324335
createTestCont = createTestContent,
325336
createFileD = createFileData,
326337
writeTestF = writeTestFile
327338
) {
328339
const fileNamePrefix = 'visual-storybook'
329-
const createTestContentData = { additionalSearchParams, clip, clipSelector, compareOptions, folders, framework, skipStories, stories: storiesJson, storybookUrl }
340+
const createTestContentData = { additionalSearchParams, clip, clipSelector, compareOptions, folders, framework, getStoriesBaselinePath, skipStories, stories: storiesJson, storybookUrl }
330341

331342
if (numShards === 1) {
332343
const testContent = createTestCont(createTestContentData)

packages/visual-service/tests/storybook/__snapshots__/utils.test.ts.snap

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ exports[`Storybook utils > itFunction > generates correct mocha test code with n
125125
storybookUrl: 'http://storybook.com/',
126126
additionalSearchParams: new URLSearchParams('foo=bar'),
127127
});
128-
await expect(browser).toMatchScreenSnapshot('category-component--story1', {"ignoreLess":true,"baselineFolder":"baseline/category/component/"})
128+
await expect(browser).toMatchScreenSnapshot('category-component--story1', {"ignoreLess":true,"actualFolder":"actual/category/component/","baselineFolder":"baseline/category/component/","diffFolder":"diff/category/component/"})
129129
});
130130
"
131131
`;
@@ -139,7 +139,7 @@ exports[`Storybook utils > itFunction > generates correct mocha test code with s
139139
storybookUrl: 'http://storybook.com/',
140140
additionalSearchParams: new URLSearchParams('foo=bar'),
141141
});
142-
await expect(browser).toMatchScreenSnapshot('category-component--story1', {"ignoreLess":true,"baselineFolder":"baseline/category/component/"})
142+
await expect(browser).toMatchScreenSnapshot('category-component--story1', {"ignoreLess":true,"actualFolder":"actual/category/component/","baselineFolder":"baseline/category/component/","diffFolder":"diff/category/component/"})
143143
});
144144
"
145145
`;
@@ -153,7 +153,7 @@ exports[`Storybook utils > itFunction > generates correct test code with Jasmine
153153
storybookUrl: 'http://storybook.com/',
154154
additionalSearchParams: new URLSearchParams('foo=bar'),
155155
});
156-
await expect(browser).toMatchScreenSnapshot('category-component--story1', {"ignoreLess":true,"baselineFolder":"baseline/category/component/"})
156+
await expect(browser).toMatchScreenSnapshot('category-component--story1', {"ignoreLess":true,"actualFolder":"actual/category/component/","baselineFolder":"baseline/category/component/","diffFolder":"diff/category/component/"})
157157
});
158158
"
159159
`;
@@ -167,26 +167,26 @@ exports[`Storybook utils > itFunction > generates correct test code with Jasmine
167167
storybookUrl: 'http://storybook.com/',
168168
additionalSearchParams: new URLSearchParams('foo=bar'),
169169
});
170-
await expect(browser).toMatchScreenSnapshot('category-component--story1', {"ignoreLess":true,"baselineFolder":"baseline/category/component/"})
170+
await expect(browser).toMatchScreenSnapshot('category-component--story1', {"ignoreLess":true,"actualFolder":"actual/category/component/","baselineFolder":"baseline/category/component/","diffFolder":"diff/category/component/"})
171171
});
172172
"
173173
`;
174174

175-
exports[`Storybook utils > itFunction > generates correct test code with for a clipped test 1`] = `
175+
exports[`Storybook utils > itFunction > generates correct test code with a custom stories baseline folder 1`] = `
176176
"
177-
it(\`should take an element screenshot of category-component--story1\`, async () => {
177+
it(\`should take a viewport screenshot of category-component--story1\`, async () => {
178178
await browser.waitForStorybookComponentToBeLoaded({
179179
clipSelector: '#id',
180180
id: 'category-component--story1',
181181
storybookUrl: 'http://storybook.com/',
182182
additionalSearchParams: new URLSearchParams('foo=bar'),
183183
});
184-
await expect($('#id')).toMatchElementSnapshot('category-component--story1-element', {"ignoreLess":true,"baselineFolder":"baseline/category/component/"})
184+
await expect(browser).toMatchScreenSnapshot('category-component--story1', {"ignoreLess":true,"actualFolder":"actual/category__component","baselineFolder":"baseline/category__component","diffFolder":"diff/category__component"})
185185
});
186186
"
187187
`;
188188

189-
exports[`Storybook utils > itFunction > generates correct test code with for a clipped test 2`] = `
189+
exports[`Storybook utils > itFunction > generates correct test code with for a clipped test 1`] = `
190190
"
191191
it(\`should take an element screenshot of category-component--story1\`, async () => {
192192
await browser.waitForStorybookComponentToBeLoaded({
@@ -195,7 +195,7 @@ exports[`Storybook utils > itFunction > generates correct test code with for a c
195195
storybookUrl: 'http://storybook.com/',
196196
additionalSearchParams: new URLSearchParams('foo=bar'),
197197
});
198-
await expect($('#id')).toMatchElementSnapshot('category-component--story1-element', {"ignoreLess":true,"baselineFolder":"baseline/category/component/"})
198+
await expect($('#id')).toMatchElementSnapshot('category-component--story1-element', {"ignoreLess":true,"actualFolder":"actual/category/component/","baselineFolder":"baseline/category/component/","diffFolder":"diff/category/component/"})
199199
});
200200
"
201201
`;
@@ -209,7 +209,7 @@ exports[`Storybook utils > itFunction > generates correct test code with mocha f
209209
storybookUrl: 'http://storybook.com/',
210210
additionalSearchParams: new URLSearchParams('foo=bar'),
211211
});
212-
await expect(browser).toMatchScreenSnapshot('category-component--story1', {"ignoreLess":true,"baselineFolder":"baseline/category/component/"})
212+
await expect(browser).toMatchScreenSnapshot('category-component--story1', {"ignoreLess":true,"actualFolder":"actual/category/component/","baselineFolder":"baseline/category/component/","diffFolder":"diff/category/component/"})
213213
});
214214
"
215215
`;
@@ -223,7 +223,7 @@ exports[`Storybook utils > itFunction > generates correct test code with mocha f
223223
storybookUrl: 'http://storybook.com/',
224224
additionalSearchParams: new URLSearchParams('foo=bar'),
225225
});
226-
await expect(browser).toMatchScreenSnapshot('category-component--story1', {"ignoreLess":true,"baselineFolder":"baseline/category/component/"})
226+
await expect(browser).toMatchScreenSnapshot('category-component--story1', {"ignoreLess":true,"actualFolder":"actual/category/component/","baselineFolder":"baseline/category/component/","diffFolder":"diff/category/component/"})
227227
});
228228
"
229229
`;

packages/visual-service/tests/storybook/utils.test.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,10 +278,20 @@ describe('Storybook utils', () => {
278278
clip,
279279
clipSelector: '#id',
280280
compareOptions: { ignoreLess: true },
281-
folders: { baselineFolder: 'baseline' },
281+
folders: {
282+
actualFolder: 'actual',
283+
baselineFolder: 'baseline',
284+
diffFolder: 'diff',
285+
},
282286
framework,
283287
skipStories,
284-
storyData: { id: 'category-component--story1' },
288+
storyData: {
289+
id: 'category-component--story1',
290+
title: 'title',
291+
name: 'name',
292+
importPath: 'import/path',
293+
tags: ['tag1', 'tag2'],
294+
},
285295
storybookUrl: 'http://storybook.com/',
286296
additionalSearchParams: new URLSearchParams({ foo: 'bar' })
287297
})
@@ -340,6 +350,12 @@ describe('Storybook utils', () => {
340350
const result = itFunction(testArgs)
341351

342352
expect(result).toMatchSnapshot()
353+
})
354+
355+
it('generates correct test code with a custom stories baseline folder', () => {
356+
const testArgs = commonSetup('mocha', [])
357+
const getStoriesBaselinePath = (category: string, component: string) => `${category}__${component}`
358+
const result = itFunction({ ...testArgs, getStoriesBaselinePath })
343359

344360
expect(result).toMatchSnapshot()
345361
})

packages/webdriver-image-comparison/src/helpers/options.interfaces.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,17 @@ export interface ClassOptions {
111111
*
112112
*/
113113
additionalSearchParams?: URLSearchParams;
114+
/**
115+
* Path builder for the story baseline
116+
*
117+
* @param category The component category (e.g. "forms" when the story is "Forms/Input")
118+
* @param component The component name (e.g. "input" when the story is "Forms/Input")
119+
* @returns The path where the baseline will be saved, under the `baselineFolder` folder.
120+
*
121+
* @example (category, component) => `${category}__${component}`
122+
* @default (category, component) => `./${category}/${component}/`
123+
*/
124+
getStoriesBaselinePath?: (category: string, component: string) => string;
114125
}
115126
}
116127

0 commit comments

Comments
 (0)