-
-
Notifications
You must be signed in to change notification settings - Fork 31
refactor(browser): centralize browser config validation in @rstest/browser #951
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import { defineConfig } from '@rstest/core'; | ||
|
|
||
| export default defineConfig({ | ||
| browser: { | ||
| enabled: true, | ||
| provider: 'invalid' as unknown as 'playwright', | ||
| headless: true, | ||
| }, | ||
| include: ['tests/**/*.test.ts'], | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import { describe, expect, it } from '@rstest/core'; | ||
|
|
||
| describe('smoke', () => { | ||
| it('should not run with invalid provider', () => { | ||
| expect(true).toBe(true); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import type { Rstest } from '@rstest/core/browser'; | ||
| import { resolveBrowserViewportPreset } from './viewportPresets'; | ||
|
|
||
| const SUPPORTED_PROVIDERS = ['playwright'] as const; | ||
|
|
||
| const isPlainObject = (value: unknown): value is Record<string, unknown> => { | ||
| return Object.prototype.toString.call(value) === '[object Object]'; | ||
| }; | ||
|
|
||
| const validateViewport = (viewport: unknown): void => { | ||
| if (viewport == null) { | ||
| return; | ||
| } | ||
|
|
||
| if (typeof viewport === 'string') { | ||
| const presetId = viewport.trim(); | ||
| if (!presetId) { | ||
| throw new Error('browser.viewport must be a non-empty preset id.'); | ||
| } | ||
| if (!resolveBrowserViewportPreset(presetId)) { | ||
| throw new Error( | ||
| `browser.viewport must be a valid preset id. Received: ${viewport}`, | ||
| ); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| if (isPlainObject(viewport)) { | ||
| const width = (viewport as any).width; | ||
| const height = (viewport as any).height; | ||
| if (!Number.isFinite(width) || width <= 0) { | ||
| throw new Error('browser.viewport.width must be a positive number.'); | ||
| } | ||
| if (!Number.isFinite(height) || height <= 0) { | ||
| throw new Error('browser.viewport.height must be a positive number.'); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| throw new Error( | ||
| 'browser.viewport must be either a preset id or { width, height }.', | ||
| ); | ||
| }; | ||
|
|
||
| export const validateBrowserConfig = (context: Rstest): void => { | ||
| for (const project of context.projects) { | ||
| const browser = project.normalizedConfig.browser; | ||
| if (!browser.enabled) { | ||
| continue; | ||
| } | ||
|
|
||
| if (!browser.provider) { | ||
| throw new Error( | ||
| 'browser.provider is required when browser.enabled is true.', | ||
| ); | ||
| } | ||
|
|
||
| if (!SUPPORTED_PROVIDERS.includes(browser.provider)) { | ||
| throw new Error( | ||
| `browser.provider must be one of: ${SUPPORTED_PROVIDERS.join(', ')}.`, | ||
| ); | ||
| } | ||
|
|
||
| validateViewport(browser.viewport); | ||
| } | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| /** | ||
| * Runtime source of truth for browser viewport presets. | ||
| * | ||
| * IMPORTANT: Keep this list/map in sync with `DevicePreset` typing in | ||
| * `@rstest/core` (`packages/core/src/types/config.ts`) so `defineConfig` | ||
| * autocomplete and runtime validation stay consistent. | ||
| */ | ||
| export const BROWSER_VIEWPORT_PRESET_IDS = [ | ||
| 'iPhoneSE', | ||
| 'iPhoneXR', | ||
| 'iPhone12Pro', | ||
| 'iPhone14ProMax', | ||
| 'Pixel7', | ||
| 'SamsungGalaxyS8Plus', | ||
| 'SamsungGalaxyS20Ultra', | ||
| 'iPadMini', | ||
| 'iPadAir', | ||
| 'iPadPro', | ||
| 'SurfacePro7', | ||
| 'SurfaceDuo', | ||
| 'GalaxyZFold5', | ||
| 'AsusZenbookFold', | ||
| 'SamsungGalaxyA51A71', | ||
| 'NestHub', | ||
| 'NestHubMax', | ||
| ] as const; | ||
|
|
||
| type BrowserViewportPresetId = (typeof BROWSER_VIEWPORT_PRESET_IDS)[number]; | ||
|
|
||
| type BrowserViewportSize = { | ||
| width: number; | ||
| height: number; | ||
| }; | ||
|
|
||
| export const BROWSER_VIEWPORT_PRESET_DIMENSIONS: Record< | ||
| BrowserViewportPresetId, | ||
| BrowserViewportSize | ||
| > = { | ||
| iPhoneSE: { width: 375, height: 667 }, | ||
| iPhoneXR: { width: 414, height: 896 }, | ||
| iPhone12Pro: { width: 390, height: 844 }, | ||
| iPhone14ProMax: { width: 430, height: 932 }, | ||
| Pixel7: { width: 412, height: 915 }, | ||
| SamsungGalaxyS8Plus: { width: 360, height: 740 }, | ||
| SamsungGalaxyS20Ultra: { width: 412, height: 915 }, | ||
| iPadMini: { width: 768, height: 1024 }, | ||
| iPadAir: { width: 820, height: 1180 }, | ||
| iPadPro: { width: 1024, height: 1366 }, | ||
| SurfacePro7: { width: 912, height: 1368 }, | ||
| SurfaceDuo: { width: 540, height: 720 }, | ||
| GalaxyZFold5: { width: 344, height: 882 }, | ||
| AsusZenbookFold: { width: 853, height: 1280 }, | ||
| SamsungGalaxyA51A71: { width: 412, height: 914 }, | ||
| NestHub: { width: 1024, height: 600 }, | ||
| NestHubMax: { width: 1280, height: 800 }, | ||
| }; | ||
|
|
||
| export const resolveBrowserViewportPreset = ( | ||
| presetId: string, | ||
| ): BrowserViewportSize | null => { | ||
| const size = | ||
| BROWSER_VIEWPORT_PRESET_DIMENSIONS[presetId as BrowserViewportPresetId]; | ||
| return size ?? null; | ||
|
fi3ework marked this conversation as resolved.
|
||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,7 +28,10 @@ async function runBrowserModeTests( | |
| options: BrowserTestRunOptions, | ||
| ): Promise<BrowserTestRunResult | void> { | ||
| const projectRoots = browserProjects.map((p) => p.rootPath); | ||
| const { runBrowserTests } = await loadBrowserModule({ projectRoots }); | ||
| const { validateBrowserConfig, runBrowserTests } = await loadBrowserModule({ | ||
| projectRoots, | ||
| }); | ||
| validateBrowserConfig(context); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| return runBrowserTests(context, options); | ||
| } | ||
|
|
||
|
|
@@ -166,6 +169,10 @@ export async function runTests(context: Rstest): Promise<void> { | |
| skipOnTestRunEnd: shouldUnifyReporter, | ||
| shardedEntries: shard ? browserEntries : undefined, | ||
| }); | ||
|
|
||
| // Prevent an unhandled rejection window in mixed node+browser runs. | ||
| // We still await the original promise later to surface the error. | ||
| browserResultPromise.catch(() => {}); | ||
| } | ||
|
|
||
| // If there are no node tests to run, we can potentially exit early. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.