Merge pull request #28905 from storybookjs/yann/improve-vitest-viewport

Addon Vitest: Set default viewport if applicable
This commit is contained in:
Yann Braga 2024-08-19 00:31:18 -07:00 committed by GitHub
commit 68afda85b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 213 additions and 8 deletions

View File

@ -0,0 +1,151 @@
/* eslint-disable no-underscore-dangle */
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { page } from '@vitest/browser/context';
import { DEFAULT_VIEWPORT_DIMENSIONS, type ViewportsParam, setViewport } from './viewports';
vi.mock('@vitest/browser/context', () => ({
page: {
viewport: vi.fn(),
},
}));
describe('setViewport', () => {
beforeEach(() => {
vi.clearAllMocks();
globalThis.__vitest_browser__ = true;
});
afterEach(() => {
globalThis.__vitest_browser__ = false;
});
it('should no op outside when not in Vitest browser mode', async () => {
globalThis.__vitest_browser__ = false;
await setViewport();
expect(page.viewport).not.toHaveBeenCalled();
});
it('should fall back to DEFAULT_VIEWPORT_DIMENSIONS if defaultViewport does not exist', async () => {
const viewportsParam: any = {
defaultViewport: 'nonExistentViewport',
};
await setViewport(viewportsParam);
expect(page.viewport).toHaveBeenCalledWith(1200, 900);
});
it('should set the dimensions of viewport from INITIAL_VIEWPORTS', async () => {
const viewportsParam: any = {
// supported by default in addon viewports
defaultViewport: 'ipad',
};
await setViewport(viewportsParam);
expect(page.viewport).toHaveBeenCalledWith(768, 1024);
});
it('should set custom defined viewport dimensions', async () => {
const viewportsParam: ViewportsParam = {
defaultViewport: 'customViewport',
viewports: {
customViewport: {
name: 'Custom Viewport',
type: 'mobile',
styles: {
width: '800px',
height: '600px',
},
},
},
};
await setViewport(viewportsParam);
expect(page.viewport).toHaveBeenCalledWith(800, 600);
});
it('should correctly handle percentage-based dimensions', async () => {
const viewportsParam: ViewportsParam = {
defaultViewport: 'percentageViewport',
viewports: {
percentageViewport: {
name: 'Percentage Viewport',
type: 'desktop',
styles: {
width: '50%',
height: '50%',
},
},
},
};
await setViewport(viewportsParam);
expect(page.viewport).toHaveBeenCalledWith(600, 450); // 50% of 1920 and 1080
});
it('should correctly handle vw and vh based dimensions', async () => {
const viewportsParam: ViewportsParam = {
defaultViewport: 'viewportUnits',
viewports: {
viewportUnits: {
name: 'VW/VH Viewport',
type: 'desktop',
styles: {
width: '50vw',
height: '50vh',
},
},
},
};
await setViewport(viewportsParam);
expect(page.viewport).toHaveBeenCalledWith(600, 450); // 50% of 1920 and 1080
});
it('should correctly handle em based dimensions', async () => {
const viewportsParam: ViewportsParam = {
defaultViewport: 'viewportUnits',
viewports: {
viewportUnits: {
name: 'em/rem Viewport',
type: 'mobile',
styles: {
width: '20em',
height: '40rem',
},
},
},
};
await setViewport(viewportsParam);
expect(page.viewport).toHaveBeenCalledWith(320, 640); // dimensions * 16
});
it('should throw an error for unsupported dimension values', async () => {
const viewportsParam: ViewportsParam = {
defaultViewport: 'invalidViewport',
viewports: {
invalidViewport: {
name: 'Invalid Viewport',
type: 'desktop',
styles: {
width: 'calc(100vw - 20px)',
height: '10pc',
},
},
},
};
await expect(setViewport(viewportsParam)).rejects.toThrowErrorMatchingInlineSnapshot(`
[SB_ADDON_VITEST_0001 (UnsupportedViewportDimensionError): Encountered an unsupported value "calc(100vw - 20px)" when setting the viewport width dimension.
The Storybook plugin only supports values in the following units:
- px, vh, vw, em, rem and %.
You can either change the viewport for this story to use one of the supported units or skip the test by adding '!test' to the story's tags per https://storybook.js.org/docs/writing-stories/tags]
`);
expect(page.viewport).not.toHaveBeenCalled();
});
});

View File

@ -1,4 +1,6 @@
/* eslint-disable no-underscore-dangle */
import { UnsupportedViewportDimensionError } from 'storybook/internal/preview-errors';
import { page } from '@vitest/browser/context';
import { INITIAL_VIEWPORTS } from '../../../viewport/src/defaults';
@ -9,16 +11,47 @@ declare global {
var __vitest_browser__: boolean;
}
interface ViewportsParam {
export interface ViewportsParam {
defaultViewport: string;
viewports: ViewportMap;
}
export const DEFAULT_VIEWPORT_DIMENSIONS = {
width: 1200,
height: 900,
};
const validPixelOrNumber = /^\d+(px)?$/;
const percentagePattern = /^(\d+(\.\d+)?%)$/;
const vwPattern = /^(\d+(\.\d+)?vw)$/;
const vhPattern = /^(\d+(\.\d+)?vh)$/;
const emRemPattern = /^(\d+)(em|rem)$/;
const parseDimension = (value: string, dimension: 'width' | 'height') => {
if (validPixelOrNumber.test(value)) {
return Number.parseInt(value, 10);
} else if (percentagePattern.test(value)) {
const percentageValue = parseFloat(value) / 100;
return Math.round(DEFAULT_VIEWPORT_DIMENSIONS[dimension] * percentageValue);
} else if (vwPattern.test(value)) {
const vwValue = parseFloat(value) / 100;
return Math.round(DEFAULT_VIEWPORT_DIMENSIONS.width * vwValue);
} else if (vhPattern.test(value)) {
const vhValue = parseFloat(value) / 100;
return Math.round(DEFAULT_VIEWPORT_DIMENSIONS.height * vhValue);
} else if (emRemPattern.test(value)) {
const emRemValue = Number.parseInt(value, 10);
return emRemValue * 16;
} else {
throw new UnsupportedViewportDimensionError({ dimension, value });
}
};
export const setViewport = async (viewportsParam: ViewportsParam = {} as ViewportsParam) => {
const defaultViewport = viewportsParam.defaultViewport;
if (!page || !globalThis.__vitest_browser__ || !defaultViewport) {
return null;
return;
}
const viewports = {
@ -26,16 +59,17 @@ export const setViewport = async (viewportsParam: ViewportsParam = {} as Viewpor
...viewportsParam.viewports,
};
let viewportWidth = DEFAULT_VIEWPORT_DIMENSIONS.width;
let viewportHeight = DEFAULT_VIEWPORT_DIMENSIONS.height;
if (defaultViewport in viewports) {
const styles = viewports[defaultViewport].styles as ViewportStyles;
if (styles?.width && styles?.height) {
const { width, height } = {
width: Number.parseInt(styles.width, 10),
height: Number.parseInt(styles.height, 10),
};
await page.viewport(width, height);
const { width, height } = styles;
viewportWidth = parseDimension(width, 'width');
viewportHeight = parseDimension(height, 'height');
}
}
return null;
await page.viewport(viewportWidth, viewportHeight);
};

View File

@ -30,6 +30,7 @@ export enum Category {
RENDERER_VUE3 = 'RENDERER_VUE3',
RENDERER_WEB_COMPONENTS = 'RENDERER_WEB-COMPONENTS',
FRAMEWORK_NEXTJS = 'FRAMEWORK_NEXTJS',
ADDON_VITEST = 'ADDON_VITEST',
}
export class MissingStoryAfterHmrError extends StorybookError {
@ -317,3 +318,22 @@ export class UnknownArgTypesError extends StorybookError {
});
}
}
export class UnsupportedViewportDimensionError extends StorybookError {
constructor(public data: { dimension: string; value: string }) {
super({
category: Category.ADDON_VITEST,
code: 1,
// TODO: Add documentation about viewports support
// documentation: '',
message: dedent`
Encountered an unsupported value "${data.value}" when setting the viewport ${data.dimension} dimension.
The Storybook plugin only supports values in the following units:
- px, vh, vw, em, rem and %.
You can either change the viewport for this story to use one of the supported units or skip the test by adding '!test' to the story's tags per https://storybook.js.org/docs/writing-stories/tags
`,
});
}
}