From 6c4a442b5c1484e50412934bcf4e52c34e5125a6 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Fri, 16 Aug 2024 10:50:54 +0200 Subject: [PATCH 1/7] set default viewport if applicable --- .../vitest/src/plugin/viewports.test.ts | 111 ++++++++++++++++++ code/addons/vitest/src/plugin/viewports.ts | 29 +++-- 2 files changed, 129 insertions(+), 11 deletions(-) create mode 100644 code/addons/vitest/src/plugin/viewports.test.ts diff --git a/code/addons/vitest/src/plugin/viewports.test.ts b/code/addons/vitest/src/plugin/viewports.test.ts new file mode 100644 index 000000000000..114de8cee33b --- /dev/null +++ b/code/addons/vitest/src/plugin/viewports.test.ts @@ -0,0 +1,111 @@ +/* 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'; +import { INITIAL_VIEWPORTS } from '../../../viewport/src/defaults'; + +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 do nothing if __vitest_browser__ is false', async () => { + globalThis.__vitest_browser__ = false; + + const result = await setViewport(); + expect(result).toBeNull(); + expect(page.viewport).not.toHaveBeenCalled(); + }); + + it('should set the viewport to the specified dimensions 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 the viewport to the specified dimensions if defaultViewport is valid', async () => { + const viewportsParam: ViewportsParam = { + defaultViewport: 'small', + viewports: { + small: { + name: 'Small screen', + type: 'mobile', + styles: { + width: '375px', + height: '667px', + }, + }, + }, + }; + + await setViewport(viewportsParam); + expect(page.viewport).toHaveBeenCalledWith(375, 667); + }); + + it('should set the viewport to DEFAULT_VIEWPORT_DIMENSIONS if defaultViewport has unparseable styles', async () => { + const viewportsParam: ViewportsParam = { + defaultViewport: 'oddSizes', + viewports: { + oddSizes: { + name: 'foo', + type: 'other', + styles: { + width: 'calc(100vw - 20px)', + height: '100%', + }, + }, + }, + }; + + await setViewport(viewportsParam); + expect(page.viewport).toHaveBeenCalledWith( + DEFAULT_VIEWPORT_DIMENSIONS.width, + DEFAULT_VIEWPORT_DIMENSIONS.height + ); + }); + + it('should merge provided viewports with initial viewports', 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 fallback to DEFAULT_VIEWPORT_DIMENSIONS if defaultViewport does not exist', async () => { + const viewportsParam: any = { + defaultViewport: 'nonExistentViewport', + }; + + await setViewport(viewportsParam); + expect(page.viewport).toHaveBeenCalledWith(1200, 900); + }); +}); diff --git a/code/addons/vitest/src/plugin/viewports.ts b/code/addons/vitest/src/plugin/viewports.ts index ec9fa8706f5f..5dad238a9bb6 100644 --- a/code/addons/vitest/src/plugin/viewports.ts +++ b/code/addons/vitest/src/plugin/viewports.ts @@ -9,33 +9,40 @@ declare global { var __vitest_browser__: boolean; } -interface ViewportsParam { +export interface ViewportsParam { defaultViewport: string; viewports: ViewportMap; } +export const DEFAULT_VIEWPORT_DIMENSIONS = { + width: 1200, + height: 900, +}; + export const setViewport = async (viewportsParam: ViewportsParam = {} as ViewportsParam) => { const defaultViewport = viewportsParam.defaultViewport; - - if (!page || !globalThis.__vitest_browser__ || !defaultViewport) { - return null; - } + if (!page || !globalThis.__vitest_browser__ || !defaultViewport) return null; const viewports = { ...INITIAL_VIEWPORTS, ...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 validPixelOrNumber = /^\d+(px)?$/; + + // if both dimensions are not valid numbers e.g. 'calc(100vh - 10px)' or '100%', use the default dimensions instead + if (validPixelOrNumber.test(styles.width) && validPixelOrNumber.test(styles.height)) { + viewportWidth = Number.parseInt(styles.width, 10); + viewportHeight = Number.parseInt(styles.height, 10); + } } } - return null; + return page.viewport(viewportWidth, viewportHeight); }; From b4fc809a39f72f76a98f6a45824a9f0e72352cac Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Fri, 16 Aug 2024 11:48:06 +0200 Subject: [PATCH 2/7] fix lint issues --- code/addons/vitest/src/plugin/viewports.test.ts | 1 - code/addons/vitest/src/plugin/viewports.ts | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/code/addons/vitest/src/plugin/viewports.test.ts b/code/addons/vitest/src/plugin/viewports.test.ts index 114de8cee33b..ef65a287fe42 100644 --- a/code/addons/vitest/src/plugin/viewports.test.ts +++ b/code/addons/vitest/src/plugin/viewports.test.ts @@ -4,7 +4,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { page } from '@vitest/browser/context'; import { DEFAULT_VIEWPORT_DIMENSIONS, type ViewportsParam, setViewport } from './viewports'; -import { INITIAL_VIEWPORTS } from '../../../viewport/src/defaults'; vi.mock('@vitest/browser/context', () => ({ page: { diff --git a/code/addons/vitest/src/plugin/viewports.ts b/code/addons/vitest/src/plugin/viewports.ts index 5dad238a9bb6..ced337bb23eb 100644 --- a/code/addons/vitest/src/plugin/viewports.ts +++ b/code/addons/vitest/src/plugin/viewports.ts @@ -21,7 +21,10 @@ export const DEFAULT_VIEWPORT_DIMENSIONS = { export const setViewport = async (viewportsParam: ViewportsParam = {} as ViewportsParam) => { const defaultViewport = viewportsParam.defaultViewport; - if (!page || !globalThis.__vitest_browser__ || !defaultViewport) return null; + + if (!page || !globalThis.__vitest_browser__ || !defaultViewport) { + return null; + } const viewports = { ...INITIAL_VIEWPORTS, From 25d2a8e1802677c2d3b09d511d815ce3b1e5beec Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Fri, 16 Aug 2024 12:17:14 +0200 Subject: [PATCH 3/7] refactor return type --- code/addons/vitest/src/plugin/viewports.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/addons/vitest/src/plugin/viewports.ts b/code/addons/vitest/src/plugin/viewports.ts index ced337bb23eb..747a90b57bc8 100644 --- a/code/addons/vitest/src/plugin/viewports.ts +++ b/code/addons/vitest/src/plugin/viewports.ts @@ -23,7 +23,7 @@ export const setViewport = async (viewportsParam: ViewportsParam = {} as Viewpor const defaultViewport = viewportsParam.defaultViewport; if (!page || !globalThis.__vitest_browser__ || !defaultViewport) { - return null; + return; } const viewports = { @@ -47,5 +47,5 @@ export const setViewport = async (viewportsParam: ViewportsParam = {} as Viewpor } } - return page.viewport(viewportWidth, viewportHeight); + await page.viewport(viewportWidth, viewportHeight); }; From 52903fad217707af3afe960378b5affd78ab55e3 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Fri, 16 Aug 2024 13:04:34 +0200 Subject: [PATCH 4/7] fix tests --- code/addons/vitest/src/plugin/viewports.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/code/addons/vitest/src/plugin/viewports.test.ts b/code/addons/vitest/src/plugin/viewports.test.ts index ef65a287fe42..811c813c3bff 100644 --- a/code/addons/vitest/src/plugin/viewports.test.ts +++ b/code/addons/vitest/src/plugin/viewports.test.ts @@ -24,8 +24,7 @@ describe('setViewport', () => { it('should do nothing if __vitest_browser__ is false', async () => { globalThis.__vitest_browser__ = false; - const result = await setViewport(); - expect(result).toBeNull(); + await setViewport(); expect(page.viewport).not.toHaveBeenCalled(); }); From a853126d1de6255088b30d86ad4451d7c39fccf7 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Fri, 16 Aug 2024 17:59:12 +0200 Subject: [PATCH 5/7] refactor --- .../vitest/src/plugin/viewports.test.ts | 108 ++++++++++++------ code/addons/vitest/src/plugin/viewports.ts | 37 ++++-- code/core/src/preview-errors.ts | 20 ++++ 3 files changed, 125 insertions(+), 40 deletions(-) diff --git a/code/addons/vitest/src/plugin/viewports.test.ts b/code/addons/vitest/src/plugin/viewports.test.ts index 811c813c3bff..e78182964966 100644 --- a/code/addons/vitest/src/plugin/viewports.test.ts +++ b/code/addons/vitest/src/plugin/viewports.test.ts @@ -21,14 +21,23 @@ describe('setViewport', () => { globalThis.__vitest_browser__ = false; }); - it('should do nothing if __vitest_browser__ is false', async () => { + 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 set the viewport to the specified dimensions from INITIAL_VIEWPORTS', async () => { + 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', @@ -38,72 +47,105 @@ describe('setViewport', () => { expect(page.viewport).toHaveBeenCalledWith(768, 1024); }); - it('should set the viewport to the specified dimensions if defaultViewport is valid', async () => { - const viewportsParam: ViewportsParam = { - defaultViewport: 'small', + it('should set custom defined viewport dimensions', async () => { + const viewportsParam: ViewportsParam = { + defaultViewport: 'customViewport', viewports: { - small: { - name: 'Small screen', + customViewport: { + name: 'Custom Viewport', type: 'mobile', styles: { - width: '375px', - height: '667px', + width: '800px', + height: '600px', }, }, }, }; await setViewport(viewportsParam); - expect(page.viewport).toHaveBeenCalledWith(375, 667); + expect(page.viewport).toHaveBeenCalledWith(800, 600); }); - it('should set the viewport to DEFAULT_VIEWPORT_DIMENSIONS if defaultViewport has unparseable styles', async () => { + it('should correctly handle percentage-based dimensions', async () => { const viewportsParam: ViewportsParam = { - defaultViewport: 'oddSizes', + defaultViewport: 'percentageViewport', viewports: { - oddSizes: { - name: 'foo', - type: 'other', + percentageViewport: { + name: 'Percentage Viewport', + type: 'desktop', styles: { - width: 'calc(100vw - 20px)', - height: '100%', + width: '50%', + height: '50%', }, }, }, }; await setViewport(viewportsParam); - expect(page.viewport).toHaveBeenCalledWith( - DEFAULT_VIEWPORT_DIMENSIONS.width, - DEFAULT_VIEWPORT_DIMENSIONS.height - ); + expect(page.viewport).toHaveBeenCalledWith(600, 450); // 50% of 1920 and 1080 }); - it('should merge provided viewports with initial viewports', async () => { + it('should correctly handle vw and vh based dimensions', async () => { const viewportsParam: ViewportsParam = { - defaultViewport: 'customViewport', + defaultViewport: 'viewportUnits', viewports: { - customViewport: { - name: 'Custom Viewport', - type: 'mobile', + viewportUnits: { + name: 'VW/VH Viewport', + type: 'desktop', styles: { - width: '800px', - height: '600px', + width: '50vw', + height: '50vh', }, }, }, }; await setViewport(viewportsParam); - expect(page.viewport).toHaveBeenCalledWith(800, 600); + expect(page.viewport).toHaveBeenCalledWith(600, 450); // 50% of 1920 and 1080 }); - it('should fallback to DEFAULT_VIEWPORT_DIMENSIONS if defaultViewport does not exist', async () => { - const viewportsParam: any = { - defaultViewport: 'nonExistentViewport', + 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(1200, 900); + 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 or use one of the supported units.] + `); + expect(page.viewport).not.toHaveBeenCalled(); }); }); diff --git a/code/addons/vitest/src/plugin/viewports.ts b/code/addons/vitest/src/plugin/viewports.ts index 747a90b57bc8..084129bd8903 100644 --- a/code/addons/vitest/src/plugin/viewports.ts +++ b/code/addons/vitest/src/plugin/viewports.ts @@ -3,6 +3,7 @@ import { page } from '@vitest/browser/context'; import { INITIAL_VIEWPORTS } from '../../../viewport/src/defaults'; import type { ViewportMap, ViewportStyles } from '../../../viewport/src/types'; +import { UnsupportedViewportDimensionError } from 'storybook/internal/preview-errors'; declare global { // eslint-disable-next-line no-var, @typescript-eslint/naming-convention @@ -19,6 +20,32 @@ export const DEFAULT_VIEWPORT_DIMENSIONS = { 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; @@ -37,13 +64,9 @@ export const setViewport = async (viewportsParam: ViewportsParam = {} as Viewpor if (defaultViewport in viewports) { const styles = viewports[defaultViewport].styles as ViewportStyles; if (styles?.width && styles?.height) { - const validPixelOrNumber = /^\d+(px)?$/; - - // if both dimensions are not valid numbers e.g. 'calc(100vh - 10px)' or '100%', use the default dimensions instead - if (validPixelOrNumber.test(styles.width) && validPixelOrNumber.test(styles.height)) { - viewportWidth = Number.parseInt(styles.width, 10); - viewportHeight = Number.parseInt(styles.height, 10); - } + const { width, height } = styles; + viewportWidth = parseDimension(width, 'width'); + viewportHeight = parseDimension(height, 'height'); } } diff --git a/code/core/src/preview-errors.ts b/code/core/src/preview-errors.ts index 79df5e45d8dd..a97271116967 100644 --- a/code/core/src/preview-errors.ts +++ b/code/core/src/preview-errors.ts @@ -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 or use one of the supported units. + `, + }); + } +} From 223e2f94f83238b8680b416203b4968ba1b34b6f Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Fri, 16 Aug 2024 18:25:52 +0200 Subject: [PATCH 6/7] update error message --- code/addons/vitest/src/plugin/viewports.test.ts | 2 +- code/core/src/preview-errors.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/code/addons/vitest/src/plugin/viewports.test.ts b/code/addons/vitest/src/plugin/viewports.test.ts index e78182964966..aa15fee3320e 100644 --- a/code/addons/vitest/src/plugin/viewports.test.ts +++ b/code/addons/vitest/src/plugin/viewports.test.ts @@ -144,7 +144,7 @@ describe('setViewport', () => { 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 or use one of the supported units.] + 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(); }); diff --git a/code/core/src/preview-errors.ts b/code/core/src/preview-errors.ts index a97271116967..817645d6134b 100644 --- a/code/core/src/preview-errors.ts +++ b/code/core/src/preview-errors.ts @@ -331,8 +331,8 @@ export class UnsupportedViewportDimensionError extends StorybookError { 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 or use one of the supported units. + + 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 `, }); } From 0969121205522c29c5ffaf086353931aacce996f Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Sat, 17 Aug 2024 09:28:08 +0200 Subject: [PATCH 7/7] fix lint errors --- code/addons/vitest/src/plugin/viewports.test.ts | 2 +- code/addons/vitest/src/plugin/viewports.ts | 3 ++- code/core/src/preview-errors.ts | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/code/addons/vitest/src/plugin/viewports.test.ts b/code/addons/vitest/src/plugin/viewports.test.ts index aa15fee3320e..7b99e252ffe9 100644 --- a/code/addons/vitest/src/plugin/viewports.test.ts +++ b/code/addons/vitest/src/plugin/viewports.test.ts @@ -48,7 +48,7 @@ describe('setViewport', () => { }); it('should set custom defined viewport dimensions', async () => { - const viewportsParam: ViewportsParam = { + const viewportsParam: ViewportsParam = { defaultViewport: 'customViewport', viewports: { customViewport: { diff --git a/code/addons/vitest/src/plugin/viewports.ts b/code/addons/vitest/src/plugin/viewports.ts index 084129bd8903..c68047877006 100644 --- a/code/addons/vitest/src/plugin/viewports.ts +++ b/code/addons/vitest/src/plugin/viewports.ts @@ -1,9 +1,10 @@ /* 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'; import type { ViewportMap, ViewportStyles } from '../../../viewport/src/types'; -import { UnsupportedViewportDimensionError } from 'storybook/internal/preview-errors'; declare global { // eslint-disable-next-line no-var, @typescript-eslint/naming-convention diff --git a/code/core/src/preview-errors.ts b/code/core/src/preview-errors.ts index 817645d6134b..4bdf0fb3aefb 100644 --- a/code/core/src/preview-errors.ts +++ b/code/core/src/preview-errors.ts @@ -320,7 +320,7 @@ export class UnknownArgTypesError extends StorybookError { } export class UnsupportedViewportDimensionError extends StorybookError { - constructor(public data: { dimension: string; value: string; }) { + constructor(public data: { dimension: string; value: string }) { super({ category: Category.ADDON_VITEST, code: 1,