From 42cf6c201fb226557a6e8884a4faad2ccc9dc694 Mon Sep 17 00:00:00 2001 From: Lukas Spirig Date: Tue, 14 May 2024 10:13:03 +0200 Subject: [PATCH] refactor: reduce tests --- .../button/button/button.snapshot.spec.ts | 17 ++--- .../core/testing/private/describe-each.ts | 20 +++--- .../testing/private/describe-viewports.ts | 30 +++++++-- .../private/visual-regression-snapshot.ts | 64 +++++++++++++------ src/components/core/testing/test-setup.ts | 19 ++++-- 5 files changed, 106 insertions(+), 44 deletions(-) diff --git a/src/components/button/button/button.snapshot.spec.ts b/src/components/button/button/button.snapshot.spec.ts index abe7497126..e632e4a594 100644 --- a/src/components/button/button/button.snapshot.spec.ts +++ b/src/components/button/button/button.snapshot.spec.ts @@ -7,7 +7,6 @@ import { isVisualRegressionRun, visualRegressionSnapshot, } from '../../core/testing/private.js'; -import type { SbbButtonSize } from '../common.js'; import './button.js'; @@ -15,14 +14,17 @@ describe(`sbb-button`, () => { if (isVisualRegressionRun()) { describe('visual-regression', () => { const cases = { - size: ['s', 'm', 'l'] as SbbButtonSize[], disabled: [false, true], negative: [false, true], - iconName: [undefined, 'arrow-right-small'], + state: [ + { icon: undefined, text: 'Button' }, + { icon: 'arrow-right-small', text: 'Button' }, + { icon: 'arrow-right-small', text: '' }, + ], }; - describeViewports(() => { - describeEach(cases, ({ size, disabled, negative, iconName }) => { + describeViewports({ viewports: ['zero', 'medium'] }, () => { + describeEach(cases, ({ disabled, negative, state }) => { let root: HTMLElement; beforeEach(async () => { root = await fixture(html` @@ -33,11 +35,10 @@ describe(`sbb-button`, () => { > Button${state.text} `); diff --git a/src/components/core/testing/private/describe-each.ts b/src/components/core/testing/private/describe-each.ts index bfa5d2c1ef..c9889bc6dd 100644 --- a/src/components/core/testing/private/describe-each.ts +++ b/src/components/core/testing/private/describe-each.ts @@ -1,3 +1,12 @@ +function generateDescribeName(payload: Record): string { + return Object.entries(payload) + .map( + ([key, value]) => + `${key}=${typeof value === 'object' && value ? `(${generateDescribeName(value as Record)})` : value}`, + ) + .join(', '); +} + function partialDescribeEach>( cases: T, payload: Record, @@ -16,14 +25,9 @@ function partialDescribeEach>( } else { for (const value of values) { const finalPayload = { ...payload, [key]: value }; - describe( - Object.entries(finalPayload) - .map(([key, value]) => `${key}=${value}`) - .join(', '), - function () { - suiteRun.call(this, finalPayload); - }, - ); + describe(generateDescribeName(finalPayload), function () { + suiteRun.call(this, finalPayload); + }); } } } diff --git a/src/components/core/testing/private/describe-viewports.ts b/src/components/core/testing/private/describe-viewports.ts index 09364e05dd..4e9d33e868 100644 --- a/src/components/core/testing/private/describe-viewports.ts +++ b/src/components/core/testing/private/describe-viewports.ts @@ -16,13 +16,35 @@ const viewportSizes = { large: SbbBreakpointLargeMin, wide: SbbBreakpointWideMin, ultra: SbbBreakpointUltraMin, -}; +} as const; -export function describeViewports(fn: (this: Mocha.Suite) => void, viewportHeight = 400): void { - for (const [size, value] of Object.entries(viewportSizes)) { +export interface DescribeViewportOptions { + viewports?: (keyof typeof viewportSizes)[]; + viewportHeight?: number; +} + +export function describeViewports( + options: DescribeViewportOptions, + fn: (this: Mocha.Suite) => void, +): void; +export function describeViewports(fn: (this: Mocha.Suite) => void): void; +export function describeViewports( + optionsOrFn: DescribeViewportOptions | ((this: Mocha.Suite) => void), + fn?: (this: Mocha.Suite) => void, +): void { + const options = typeof optionsOrFn === 'object' ? optionsOrFn : {}; + fn ??= optionsOrFn as (this: Mocha.Suite) => void; + let viewportSizeTests = Object.entries(viewportSizes); + if (options.viewports?.length) { + viewportSizeTests = viewportSizeTests.filter(([key, _value]) => + options.viewports?.includes(key as keyof typeof viewportSizes), + ); + } + + for (const [size, value] of viewportSizeTests) { describe(`viewport=${size}`, function () { before(async () => { - await setViewport({ width: value, height: viewportHeight }); + await setViewport({ width: value, height: options.viewportHeight ?? 400 }); }); fn.call(this); diff --git a/src/components/core/testing/private/visual-regression-snapshot.ts b/src/components/core/testing/private/visual-regression-snapshot.ts index 8c63501b1e..db75c0dfc1 100644 --- a/src/components/core/testing/private/visual-regression-snapshot.ts +++ b/src/components/core/testing/private/visual-regression-snapshot.ts @@ -1,41 +1,67 @@ -import { sendKeys, sendMouse } from '@web/test-runner-commands'; +import { resetMouse, sendKeys, sendMouse } from '@web/test-runner-commands'; import { visualDiff } from '@web/test-runner-visual-regression'; export function imageName(test: Mocha.Runnable): string { return test!.fullTitle().replaceAll(', ', '-').replaceAll(' ', '_'); } -export function visualRegressionSnapshot(snapshotElement: () => HTMLElement): void { +function findElementCenter(snapshotElement: () => HTMLElement): [number, number] { + const element = snapshotElement(); + // Look for the first sbb-* element and get center of the element to + // move the mouse cursor over it. + const positionElement = element.localName.startsWith('sbb-') + ? element + : element.firstElementChild!; + const position = positionElement.getBoundingClientRect(); + return [ + Math.round(position.x + position.width / 2), + Math.round(position.y + position.height / 2), + ]; +} + +export function testVisualDiff(snapshotElement: () => HTMLElement): void { it('default', async function () { await visualDiff(snapshotElement(), imageName(this.test!)); }); +} +export function testVisualDiffFocus(snapshotElement: () => HTMLElement): void { it('focus', async function () { await sendKeys({ press: 'Tab' }); await visualDiff(snapshotElement(), imageName(this.test!)); }); +} +export function testVisualDiffHover(snapshotElement: () => HTMLElement): void { it('hover', async function () { - const element = snapshotElement(); - const positionElement = element.localName.startsWith('sbb-') - ? element - : element.firstElementChild!; - const position = positionElement.getBoundingClientRect(); - await sendMouse({ - type: 'move', - position: [ - Math.round(position.x + position.width / 2), - Math.round(position.y + position.height / 2), - ], - }); + const position = findElementCenter(snapshotElement); try { - await visualDiff(element, imageName(this.test!)); + await sendMouse({ type: 'move', position }); + await visualDiff(snapshotElement(), imageName(this.test!)); } finally { - await sendMouse({ - type: 'move', - position: [0, 0], - }); + await resetMouse(); } }); } + +export function testVisualDiffActive(snapshotElement: () => HTMLElement): void { + it('active', async function () { + const position = findElementCenter(snapshotElement); + + try { + await sendMouse({ type: 'move', position }); + await sendMouse({ type: 'down' }); + await visualDiff(snapshotElement(), imageName(this.test!)); + } finally { + await resetMouse(); + } + }); +} + +export function visualRegressionSnapshot(snapshotElement: () => HTMLElement): void { + testVisualDiff(snapshotElement); + testVisualDiffFocus(snapshotElement); + testVisualDiffHover(snapshotElement); + testVisualDiffActive(snapshotElement); +} diff --git a/src/components/core/testing/test-setup.ts b/src/components/core/testing/test-setup.ts index 8489618e69..bcccbc8319 100644 --- a/src/components/core/testing/test-setup.ts +++ b/src/components/core/testing/test-setup.ts @@ -1,10 +1,23 @@ +import { getSvgContent } from '../../icon.js'; import { sbbInputModalityDetector } from '../a11y.js'; import type { SbbIconConfig } from '../config.js'; import { mergeConfig } from '../config.js'; import { isHydratedSsr, isVisualRegressionRun } from './private.js'; -function setupIconConfig(): void { +if (isVisualRegressionRun()) { + const preloadedIcons = ['arrow-right-small']; + await Promise.all(preloadedIcons.map((icon) => getSvgContent('default', icon, true))); + + mergeConfig({ + icon: { + interceptor({ namespace, name }) { + throw new Error(`Icon ${namespace}:${name} must be preloaded in test-setup.ts!`); + }, + }, + }); +} else { + // Setup mock configuration for icons const testNamespaces = ['default', 'picto']; const icon: SbbIconConfig = { interceptor: ({ namespace, name, request }) => { @@ -27,10 +40,6 @@ function setupIconConfig(): void { mergeConfig({ icon }); } -if (!isVisualRegressionRun()) { - setupIconConfig(); -} - if (isHydratedSsr()) { await import('@lit-labs/ssr-client/lit-element-hydrate-support.js'); }