diff --git a/.github/workflows/visual-regression.yml b/.github/workflows/visual-regression.yml index 03fef630db..429e8d61bb 100644 --- a/.github/workflows/visual-regression.yml +++ b/.github/workflows/visual-regression.yml @@ -33,7 +33,7 @@ jobs: strategy: fail-fast: false matrix: - shard: [1, 2] + shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] steps: - uses: actions/checkout@v4 @@ -111,7 +111,7 @@ jobs: - name: Run visual regression tests run: | echo "NEW_HOST=${NEW_HOST}" - NODE_OPTIONS=--experimental-vm-modules node_modules/.bin/jest -c jest.visual.config.js --shard="${SHARD}/2" + NODE_OPTIONS=--experimental-vm-modules node_modules/.bin/jest -c jest.visual.config.js --shard="${SHARD}/10" env: TZ: UTC SHARD: ${{ matrix.shard }} diff --git a/test/definitions/index.ts b/test/definitions/index.ts index 299c8e1b1a..ac3bc792ca 100644 --- a/test/definitions/index.ts +++ b/test/definitions/index.ts @@ -28,6 +28,20 @@ import breadcrumbGroupSuite from './visual/breadcrumb-group'; import buttonSuite from './visual/button'; import buttonDropdownSuite from './visual/button-dropdown'; import buttonGroupSuite from './visual/button-group'; +import cardsSuite from './visual/cards'; +import checkboxSuite from './visual/checkbox'; +import codeEditorSuite from './visual/code-editor'; +import collectionPreferencesSuite from './visual/collection-preferences'; +import columnLayoutSuite from './visual/column-layout'; +import containerSuite from './visual/container'; +import containerStickySuite from './visual/container-sticky'; +import contentLayoutSuite from './visual/content-layout'; +import contentLayoutPermutationsSuite from './visual/content-layout-permutations'; +import copyToClipboardSuite from './visual/copy-to-clipboard'; +import dateInputSuite from './visual/date-input'; +import datePickerSuite from './visual/date-picker'; +import dateRangePickerSuite from './visual/date-range-picker'; +import expandableSectionSuite from './visual/expandable-section'; // Per-component exports (grouped by component) export const actionCard: TestSuite[] = [actionCardSuite]; @@ -54,6 +68,18 @@ export const breadcrumbGroup: TestSuite[] = [breadcrumbGroupSuite]; export const button: TestSuite[] = [buttonSuite]; export const buttonDropdown: TestSuite[] = [buttonDropdownSuite]; export const buttonGroup: TestSuite[] = [buttonGroupSuite]; +export const cards: TestSuite[] = [cardsSuite]; +export const checkbox: TestSuite[] = [checkboxSuite]; +export const codeEditor: TestSuite[] = [codeEditorSuite]; +export const collectionPreferences: TestSuite[] = [collectionPreferencesSuite]; +export const columnLayout: TestSuite[] = [columnLayoutSuite]; +export const container: TestSuite[] = [containerSuite, containerStickySuite]; +export const contentLayout: TestSuite[] = [contentLayoutSuite, contentLayoutPermutationsSuite]; +export const copyToClipboard: TestSuite[] = [copyToClipboardSuite]; +export const dateInput: TestSuite[] = [dateInputSuite]; +export const datePicker: TestSuite[] = [datePickerSuite]; +export const dateRangePicker: TestSuite[] = [dateRangePickerSuite]; +export const expandableSection: TestSuite[] = [expandableSectionSuite]; export const allSuites: TestSuite[] = [ ...actionCard, @@ -69,4 +95,16 @@ export const allSuites: TestSuite[] = [ ...button, ...buttonDropdown, ...buttonGroup, + ...cards, + ...checkbox, + ...codeEditor, + ...collectionPreferences, + ...columnLayout, + ...container, + ...contentLayout, + ...copyToClipboard, + ...dateInput, + ...datePicker, + ...dateRangePicker, + ...expandableSection, ]; diff --git a/test/definitions/utils.ts b/test/definitions/utils.ts index a70701ff1e..1e22d52b03 100644 --- a/test/definitions/utils.ts +++ b/test/definitions/utils.ts @@ -60,81 +60,79 @@ async function preparePage( } export function runTestSuites(suites: Array) { - let browser: WebdriverIO.Browser; - - beforeAll(async () => { - const { default: getBrowserCreator } = await import('@cloudscape-design/browser-test-tools/browser'); - const creator = getBrowserCreator('ChromeHeadlessIntegration', 'local', { - seleniumUrl: 'http://localhost:9515', - }); - browser = await creator.getBrowser({ width: defaultWindowSize.width, height: defaultWindowSize.height }); - }); - - afterAll(async () => { - await browser?.deleteSession(); - }); - - registerSuites(suites, () => browser); + registerSuites(suites); } -function registerSuites(suites: Array, getBrowser: () => WebdriverIO.Browser) { +function registerSuites(suites: Array) { for (const item of suites) { if (isTestDefinition(item)) { - registerTest(item, getBrowser); + registerTest(item); } else { describe(item.description, () => { - registerSuites(item.tests, getBrowser); + registerSuites(item.tests); }); } } } -function registerTest(testDef: TestDefinition, getBrowser: () => WebdriverIO.Browser) { +function registerTest(testDef: TestDefinition) { test(testDef.description, async () => { - const browser = getBrowser(); - const page = new VisualTestPageObject(browser); - - const newUrl = buildUrl(newHost, testDef.path, testDef.queryParams); - const oldUrl = buildUrl(oldHost, testDef.path, testDef.queryParams); - - const tolerance = testDef.pixelDiffTolerance ?? 0; + const { default: getBrowserCreator } = await import('@cloudscape-design/browser-test-tools/browser'); + const creator = getBrowserCreator('ChromeHeadlessIntegration', 'local', { + seleniumUrl: 'http://localhost:9515', + }); + const windowSize = { + width: testDef.configuration?.width ?? defaultWindowSize.width, + height: testDef.configuration?.height ?? defaultWindowSize.height, + }; + const browser = await creator.getBrowser(windowSize); + try { + const page = new VisualTestPageObject(browser); + + const newUrl = buildUrl(newHost, testDef.path, testDef.queryParams); + const oldUrl = buildUrl(oldHost, testDef.path, testDef.queryParams); + + const tolerance = testDef.pixelDiffTolerance ?? 0; + + if (testDef.screenshotType === 'permutations') { + await preparePage(browser, page, newUrl, testDef, testDef.configuration); + const newPermutations = await page.capturePermutations(); + + await preparePage(browser, page, oldUrl, testDef, testDef.configuration); + const oldPermutations = await page.capturePermutations(); + + expect(newPermutations.length).toBe(oldPermutations.length); + + // Compare each permutation individually, wrapping each in an Allure step. + let failures = 0; + for (let i = 0; i < newPermutations.length; i++) { + const id = newPermutations[i].id || ''; + const index = `#${(i + 1).toString().padStart(3, '0')}`; + await step(`Permutation ${index}`, async () => { + const permResult = await compareScreenshots(newPermutations[i], oldPermutations[i]); + await attachDiffImages(permResult, index); + if (permResult.diffPixels > tolerance) { + failures++; + throw new Error(`Permutation ${index} differs by ${permResult.diffPixels} pixels\n${id}`); + } + }); + } + expect(failures).toBe(0); + return; + } - if (testDef.screenshotType === 'permutations') { + // Non-permutation: single screenshot comparison await preparePage(browser, page, newUrl, testDef, testDef.configuration); - const newPermutations = await page.capturePermutations(); + const newRaw = await captureSingleScreenshot(page, testDef); await preparePage(browser, page, oldUrl, testDef, testDef.configuration); - const oldPermutations = await page.capturePermutations(); - - expect(newPermutations.length).toBe(oldPermutations.length); - - // Compare each permutation individually, wrapping each in an Allure step. - let failures = 0; - for (let i = 0; i < newPermutations.length; i++) { - const id = newPermutations[i].id || ''; - const index = `#${(i + 1).toString().padStart(3, '0')}`; - await step(`Permutation ${index}`, async () => { - const permResult = await compareScreenshots(newPermutations[i], oldPermutations[i]); - await attachDiffImages(permResult, index); - if (permResult.diffPixels > tolerance) { - failures++; - throw new Error(`Permutation ${index} differs by ${permResult.diffPixels} pixels\n${id}`); - } - }); - } - expect(failures).toBe(0); - return; - } - - // Non-permutation: single screenshot comparison - await preparePage(browser, page, newUrl, testDef, testDef.configuration); - const newRaw = await captureSingleScreenshot(page, testDef); + const oldRaw = await captureSingleScreenshot(page, testDef); - await preparePage(browser, page, oldUrl, testDef, testDef.configuration); - const oldRaw = await captureSingleScreenshot(page, testDef); - - const result = await compareScreenshots(newRaw, oldRaw); - await attachDiffImages(result, testDef.description); - expect(result.diffPixels).toBeLessThanOrEqual(tolerance); + const result = await compareScreenshots(newRaw, oldRaw); + await attachDiffImages(result, testDef.description); + expect(result.diffPixels).toBeLessThanOrEqual(tolerance); + } finally { + await browser.deleteSession(); + } }); } diff --git a/test/definitions/visual/cards.ts b/test/definitions/visual/cards.ts new file mode 100644 index 0000000000..5779061a6c --- /dev/null +++ b/test/definitions/visual/cards.ts @@ -0,0 +1,16 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { TestSuite } from '../types'; + +const suite: TestSuite = { + description: 'Cards', + componentName: 'cards', + tests: [2200, 1920, 1400, 1200, 992, 768].map(width => ({ + description: `permutations at ${width}`, + path: 'cards/permutations', + screenshotType: 'permutations' as const, + configuration: { width }, + })), +}; + +export default suite; diff --git a/test/definitions/visual/checkbox.ts b/test/definitions/visual/checkbox.ts new file mode 100644 index 0000000000..d7c0f05363 --- /dev/null +++ b/test/definitions/visual/checkbox.ts @@ -0,0 +1,36 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { TestSuite } from '../types'; + +const suite: TestSuite = { + description: 'Checkbox', + componentName: 'checkbox', + tests: [ + { + description: 'Permutations', + path: 'checkbox/permutations', + screenshotType: 'permutations', + }, + { + description: 'Checkbox is focused', + path: 'checkbox/focus-test', + screenshotType: 'screenshotArea', + setup: async ({ page }) => { + await page.click('#focus-target'); + await page.focusNextElement(); + }, + }, + { + description: 'Checkbox has label with a correct width', + path: 'checkbox/labels-highlight', + screenshotType: 'screenshotArea', + }, + { + description: 'Style custom page', + path: 'checkbox/style-custom', + screenshotType: 'screenshotArea', + }, + ], +}; + +export default suite; diff --git a/test/definitions/visual/code-editor.ts b/test/definitions/visual/code-editor.ts new file mode 100644 index 0000000000..9f6fb46785 --- /dev/null +++ b/test/definitions/visual/code-editor.ts @@ -0,0 +1,99 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import VisualTestPageObject from '../page-object'; +import { TestSuite } from '../types'; + +async function waitForAceTheme(page: VisualTestPageObject) { + // Each test gets a fresh browser session, so CSP is always correct. + // Ace loads asynchronously — wait for it to initialize and apply the theme. + const browser = (page as any).browser; + try { + await browser.waitUntil( + () => + browser.execute(() => { + const el: HTMLElement | null = document.querySelector( + '.ace_editor.ace-dawn, .ace_editor.ace-tomorrow-night-bright' + ); + return el !== null && el.offsetHeight > 0; + }), + { timeout: 30000, timeoutMsg: 'Ace editor with theme class not found or not visible within 30s' } + ); + } catch (e) { + // Capture a debug screenshot and page state on failure + const screenshot = await browser.takeScreenshot(); + const debugInfo = await browser.execute(() => { + const ace = document.querySelector('.ace_editor'); + const classes = ace ? ace.className : 'NO .ace_editor ELEMENT FOUND'; + const cspMeta = document.querySelector('meta[http-equiv="Content-Security-Policy"]'); + const csp = cspMeta ? cspMeta.getAttribute('content') : 'NO CSP META TAG'; + const url = location.href; + return { classes, csp, url, bodyHTML: document.body.innerHTML.slice(0, 2000) }; + }); + const { attachment } = await import('allure-js-commons'); + await attachment('debug-screenshot-on-failure', Buffer.from(screenshot, 'base64'), 'image/png'); + await attachment('debug-page-state', JSON.stringify(debugInfo, null, 2), 'application/json'); + throw e; + } +} + +const suite: TestSuite = { + description: 'Code editor', + componentName: 'code-editor', + tests: [ + { + description: 'simple', + path: 'code-editor/simple', + screenshotType: 'screenshotArea', + setup: async ({ page }) => { + await waitForAceTheme(page); + }, + }, + { + description: 'error', + path: 'code-editor/error', + screenshotType: 'screenshotArea', + }, + { + description: 'loading', + path: 'code-editor/loading', + screenshotType: 'screenshotArea', + }, + { + description: 'theme resolution', + path: 'code-editor/themes', + screenshotType: 'screenshotArea', + setup: async ({ page }) => { + await waitForAceTheme(page); + }, + }, + { + description: 'permutations', + path: 'code-editor/permutations', + screenshotType: 'permutations', + setup: async ({ page }) => { + await waitForAceTheme(page); + await page.waitForVisible('.ace_gutter-cell.ace_gutter-active-line.ace_error'); + }, + }, + { + description: 'listens to mode change', + path: 'code-editor/simple', + screenshotType: 'screenshotArea', + setup: async ({ page }) => { + await waitForAceTheme(page); + await page.click('#mode-toggle'); + }, + }, + { + description: 'compare simple on small screen', + path: 'code-editor/simple', + screenshotType: 'screenshotArea', + configuration: { width: 360 }, + setup: async ({ page }) => { + await waitForAceTheme(page); + }, + }, + ], +}; + +export default suite; diff --git a/test/definitions/visual/collection-preferences.ts b/test/definitions/visual/collection-preferences.ts new file mode 100644 index 0000000000..4748b24143 --- /dev/null +++ b/test/definitions/visual/collection-preferences.ts @@ -0,0 +1,65 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { TestDefinition, TestSuite } from '../types'; + +const suite: TestSuite = { + description: 'CollectionPreferences', + componentName: 'collection-preferences', + tests: [ + ...( + [ + [600, 1100], + [1280, 700], + ] as [number, number][] + ).flatMap(([width, height]) => [ + { + description: `complete at ${width}x${height}`, + path: 'collection-preferences/simple', + screenshotType: 'viewport' as const, + configuration: { width, height }, + setup: async ({ page }) => { + await page.click('.cp-1 button'); + }, + }, + { + description: `visible content only at ${width}x${height}`, + path: 'collection-preferences/simple', + screenshotType: 'viewport' as const, + setup: async ({ page }) => { + await page.click('.cp-4 button'); + }, + }, + ]), + { + description: 'custom', + path: 'collection-preferences/simple', + screenshotType: 'viewport', + setup: async ({ page }) => { + await page.click('.cp-2 button'); + }, + }, + { + description: 'drag handle focused', + path: 'collection-preferences/reorder-content', + screenshotType: 'viewport', + configuration: { width: 900, height: 650 }, + setup: async ({ page }) => { + await page.click('.cp-1 button'); + await page.keys(Array(5).fill('Tab')); + }, + }, + { + description: 'reordering active', + path: 'collection-preferences/reorder-content', + screenshotType: 'viewport', + configuration: { width: 900, height: 650 }, + setup: async ({ page }) => { + await page.click('.cp-1 button'); + await page.keys(Array(5).fill('Tab')); + await page.keys(['Space']); + }, + }, + ], +}; + +export default suite; diff --git a/test/definitions/visual/column-layout.ts b/test/definitions/visual/column-layout.ts new file mode 100644 index 0000000000..478ee9d0c2 --- /dev/null +++ b/test/definitions/visual/column-layout.ts @@ -0,0 +1,31 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { TestDefinition, TestSuite } from '../types'; + +const TEST_WIDTHS: [string, number][] = [ + ['default', 400], + ['xxs', 500], + ['xs', 800], + ['m', 1200], +]; + +const suite: TestSuite = { + description: 'ColumnLayout', + componentName: 'column-layout', + tests: TEST_WIDTHS.flatMap(([breakpoint, width]) => [ + { + description: `column-layout at "${breakpoint}"`, + path: 'column-layout/simple', + screenshotType: 'screenshotArea' as const, + configuration: { width }, + }, + { + description: `permutations at "${breakpoint}"`, + path: 'column-layout/permutations', + screenshotType: 'permutations' as const, + configuration: { width }, + }, + ]), +}; + +export default suite; diff --git a/test/definitions/visual/container-sticky.ts b/test/definitions/visual/container-sticky.ts new file mode 100644 index 0000000000..d16f05262b --- /dev/null +++ b/test/definitions/visual/container-sticky.ts @@ -0,0 +1,43 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { TestDefinition, TestSuite } from '../types'; + +function stickyTest( + width: number, + description: string, + params?: Record, + withScroll = true +): TestDefinition { + return { + description, + path: 'container/sticky-permutations', + screenshotType: 'viewport', + configuration: { width }, + ...(params && { queryParams: params }), + ...(withScroll && { + setup: async ({ page }) => { + await page.windowScrollTo({ top: 200 }); + }, + }), + }; +} + +const suite: TestSuite = { + description: 'Container sticky permutations', + componentName: 'container', + tests: [1400, 600].flatMap(width => [ + stickyTest(width, `simple - at ${width}px`), + stickyTest(width, `with notifications - at ${width}px`, { hasNotifications: 'true' }), + stickyTest(width, `with breadcrumbs - at ${width}px`, { hasBreadcrumbs: 'true' }), + stickyTest(width, `with an alert - at ${width}px`, { hasNotifications: 'true', hasAlert: 'true' }), + stickyTest( + width, + `with an alert - at ${width}px without scroll`, + { hasNotifications: 'true', hasAlert: 'true' }, + false + ), + stickyTest(width, `with high-contrast header - at ${width}px`, { hasNotifications: 'true', highContrast: 'true' }), + ]), +}; + +export default suite; diff --git a/test/definitions/visual/container.ts b/test/definitions/visual/container.ts new file mode 100644 index 0000000000..5038b51381 --- /dev/null +++ b/test/definitions/visual/container.ts @@ -0,0 +1,58 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { TestDefinition, TestSuite } from '../types'; + +const suite: TestSuite = { + description: 'Container and header', + componentName: 'container', + tests: [ + { + description: 'simple', + path: 'container/simple', + screenshotType: 'screenshotArea', + }, + { + description: 'fit height with footer', + path: 'container/fit-height', + screenshotType: 'screenshotArea', + }, + { + description: 'fit height without footer', + path: 'container/fit-height', + screenshotType: 'screenshotArea', + queryParams: { hideFooters: 'true' }, + }, + { + description: 'correctly displays container with side media', + path: 'container/media', + screenshotType: 'screenshotArea', + queryParams: { position: 'side', width: '33%', content: '16-9' }, + }, + { + description: 'correctly displays container with top media', + path: 'container/media', + screenshotType: 'screenshotArea', + queryParams: { position: 'top', height: '150px', content: '4-3' }, + }, + ...(['side', 'top'] as const).flatMap(position => + [465, 688, 1120].map(width => ({ + description: `media ${position} permutations at ${width}`, + path: `container/media-${position}-permutations`, + screenshotType: 'permutations' as const, + configuration: { width }, + })) + ), + { + description: 'stacked', + path: 'container/stacked-components', + screenshotType: 'screenshotArea', + }, + { + description: 'style-custom', + path: 'container/style-custom', + screenshotType: 'screenshotArea', + }, + ], +}; + +export default suite; diff --git a/test/definitions/visual/content-layout-permutations.ts b/test/definitions/visual/content-layout-permutations.ts new file mode 100644 index 0000000000..c665254d47 --- /dev/null +++ b/test/definitions/visual/content-layout-permutations.ts @@ -0,0 +1,256 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { TestDefinition, TestSuite } from '../types'; + +function contentLayoutPermutation(params: Record, width = 1400): TestDefinition { + return { + description: `${JSON.stringify(params)} at ${width}`, + path: 'content-layout/permutations', + screenshotType: 'screenshotArea' as const, + configuration: { width }, + queryParams: params, + }; +} + +const suite: TestSuite = { + description: 'ContentLayout permutations', + componentName: 'content-layout', + tests: [ + // default and high-contrast headerVariants with all background styles + ...(['default', 'high-contrast'] as const).flatMap(headerVariant => + (['none', 'gradient', 'image'] as const).flatMap(headerBackgroundStyle => + [1400, 600].flatMap(width => [ + contentLayoutPermutation( + { + headerVariant, + headerBackgroundStyle, + hasContainer: 'true', + defaultPadding: 'true', + hasBreadcrumbs: 'false', + hasNotifications: 'false', + disableOverlap: 'false', + hasAppLayout: 'false', + hasAppLayoutWithOpenNavigation: 'false', + hasSecondaryHeader: 'false', + removeHeader: 'false', + }, + width + ), + contentLayoutPermutation( + { + headerVariant, + headerBackgroundStyle, + hasContainer: 'true', + defaultPadding: 'true', + hasBreadcrumbs: 'true', + hasNotifications: 'true', + disableOverlap: 'false', + hasAppLayout: 'false', + hasAppLayoutWithOpenNavigation: 'false', + hasSecondaryHeader: 'false', + removeHeader: 'false', + }, + width + ), + ]) + ) + ), + // maxContentWidth variants + ...(['default', 'high-contrast'] as const).flatMap(headerVariant => + (['none', 'gradient', 'image'] as const).map(headerBackgroundStyle => + contentLayoutPermutation({ + headerVariant, + headerBackgroundStyle, + hasContainer: 'true', + defaultPadding: 'true', + hasBreadcrumbs: 'true', + hasNotifications: 'true', + disableOverlap: 'false', + hasAppLayout: 'false', + hasAppLayoutWithOpenNavigation: 'false', + hasSecondaryHeader: 'false', + removeHeader: 'false', + maxContentWidth: '1000', + }) + ) + ), + // divider headerVariant + ...[1400, 600].flatMap(width => [ + contentLayoutPermutation( + { + headerVariant: 'divider', + defaultPadding: 'true', + hasContainer: 'true', + hasBreadcrumbs: 'false', + hasNotifications: 'false', + disableOverlap: 'false', + hasAppLayout: 'false', + hasAppLayoutWithOpenNavigation: 'false', + hasSecondaryHeader: 'false', + removeHeader: 'false', + }, + width + ), + contentLayoutPermutation( + { + headerVariant: 'divider', + defaultPadding: 'true', + hasContainer: 'true', + hasBreadcrumbs: 'true', + hasNotifications: 'true', + disableOverlap: 'true', + hasAppLayout: 'false', + hasAppLayoutWithOpenNavigation: 'false', + hasSecondaryHeader: 'false', + removeHeader: 'false', + maxContentWidth: '1000', + }, + width + ), + ]), + // with app layout + contentLayoutPermutation({ + headerVariant: 'divider', + defaultPadding: 'true', + hasContainer: 'true', + hasBreadcrumbs: 'false', + hasNotifications: 'false', + disableOverlap: 'true', + hasAppLayout: 'true', + hasAppLayoutWithOpenNavigation: 'false', + hasSecondaryHeader: 'false', + removeHeader: 'false', + }), + contentLayoutPermutation({ + headerVariant: 'high-contrast', + headerBackgroundStyle: 'gradient', + defaultPadding: 'true', + hasContainer: 'true', + hasBreadcrumbs: 'true', + hasNotifications: 'true', + disableOverlap: 'false', + hasAppLayout: 'true', + hasAppLayoutWithOpenNavigation: 'false', + hasSecondaryHeader: 'false', + removeHeader: 'false', + maxContentWidth: '800', + }), + // with open navigation + contentLayoutPermutation({ + headerVariant: 'high-contrast', + defaultPadding: 'true', + hasContainer: 'true', + hasBreadcrumbs: 'false', + hasNotifications: 'false', + disableOverlap: 'false', + hasAppLayout: 'true', + hasAppLayoutWithOpenNavigation: 'true', + hasSecondaryHeader: 'false', + removeHeader: 'false', + }), + contentLayoutPermutation({ + headerVariant: 'divider', + defaultPadding: 'true', + hasContainer: 'true', + hasBreadcrumbs: 'true', + hasNotifications: 'true', + disableOverlap: 'false', + hasAppLayout: 'true', + hasAppLayoutWithOpenNavigation: 'true', + hasSecondaryHeader: 'false', + removeHeader: 'false', + maxContentWidth: '700', + }), + // with secondary header + ...[1400, 600].map(width => + contentLayoutPermutation( + { + headerVariant: 'high-contrast', + defaultPadding: 'true', + hasContainer: 'true', + hasBreadcrumbs: 'false', + hasNotifications: 'false', + disableOverlap: 'false', + hasAppLayout: 'false', + hasAppLayoutWithOpenNavigation: 'false', + hasSecondaryHeader: 'true', + removeHeader: 'false', + }, + width + ) + ), + contentLayoutPermutation({ + headerVariant: 'high-contrast', + defaultPadding: 'true', + hasContainer: 'true', + hasBreadcrumbs: 'false', + hasNotifications: 'false', + disableOverlap: 'false', + hasAppLayout: 'false', + hasAppLayoutWithOpenNavigation: 'false', + hasSecondaryHeader: 'true', + removeHeader: 'false', + maxContentWidth: '900', + }), + // without header + ...[1400, 600].flatMap(width => + [true, false].flatMap(defaultPadding => + (['default', 'high-contrast', 'divider'] as const).map(headerVariant => + contentLayoutPermutation( + { + headerVariant, + defaultPadding: `${defaultPadding}`, + removeHeader: 'true', + hasContainer: `${defaultPadding}`, + hasBreadcrumbs: 'false', + hasNotifications: 'false', + disableOverlap: 'false', + hasAppLayout: 'false', + hasAppLayoutWithOpenNavigation: 'false', + hasSecondaryHeader: 'false', + }, + width + ) + ) + ) + ), + // without default padding + ...[1400, 600].map(width => + contentLayoutPermutation( + { + headerVariant: 'default', + defaultPadding: 'false', + hasContainer: 'false', + hasBreadcrumbs: 'false', + hasNotifications: 'false', + disableOverlap: 'false', + hasAppLayout: 'false', + hasAppLayoutWithOpenNavigation: 'false', + hasSecondaryHeader: 'false', + removeHeader: 'false', + }, + width + ) + ), + // with disabled overlap + ...[1400, 600].map(width => + contentLayoutPermutation( + { + headerVariant: 'high-contrast', + defaultPadding: 'true', + hasContainer: 'true', + hasBreadcrumbs: 'false', + hasNotifications: 'false', + disableOverlap: 'true', + hasAppLayout: 'false', + hasAppLayoutWithOpenNavigation: 'false', + hasSecondaryHeader: 'false', + removeHeader: 'false', + }, + width + ) + ), + ], +}; + +export default suite; diff --git a/test/definitions/visual/content-layout.ts b/test/definitions/visual/content-layout.ts new file mode 100644 index 0000000000..7f3a608d65 --- /dev/null +++ b/test/definitions/visual/content-layout.ts @@ -0,0 +1,59 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { TestDefinition, TestSuite } from '../types'; + +const suite: TestSuite = { + description: 'ContentLayout', + componentName: 'content-layout', + tests: [ + { + description: 'fill content area', + path: 'content-layout/fill-content-area', + screenshotType: 'screenshotArea', + }, + { + description: 'standalone', + path: 'content-layout/standalone', + screenshotType: 'screenshotArea', + }, + { + description: 'with absolute components', + path: 'content-layout/with-absolute-components', + screenshotType: 'screenshotArea', + }, + { + description: 'form with form header', + path: 'content-layout/with-header-toggles', + screenshotType: 'screenshotArea', + }, + { + description: 'form with content layout header', + path: 'content-layout/with-header-toggles', + screenshotType: 'screenshotArea', + setup: async ({ page }) => { + await page.click('[data-testid="toggle-form-header"] input'); + await page.click('[data-testid="toggle-content-layout"] input'); + }, + }, + // without header (from content-layout.test.ts) + ...[1400, 600].flatMap(width => + [false, true].flatMap(hasBreadcrumbs => + [false, true].flatMap(hasNotifications => + [true, false].map(hasOverlap => ({ + description: `without header at ${width} ${hasBreadcrumbs ? 'with' : 'without'} breadcrumbs, ${hasNotifications ? 'with' : 'without'} notifications, ${hasOverlap ? 'with' : 'without'} overlap`, + path: 'content-layout/without-header', + screenshotType: 'screenshotArea' as const, + configuration: { width }, + queryParams: { + hasBreadcrumbs: `${hasBreadcrumbs}`, + hasNotifications: `${hasNotifications}`, + disableOverlap: `${!hasOverlap}`, + }, + })) + ) + ) + ), + ], +}; + +export default suite; diff --git a/test/definitions/visual/copy-to-clipboard.ts b/test/definitions/visual/copy-to-clipboard.ts new file mode 100644 index 0000000000..d8db12599e --- /dev/null +++ b/test/definitions/visual/copy-to-clipboard.ts @@ -0,0 +1,29 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { TestSuite } from '../types'; + +const suite: TestSuite = { + description: 'CopyToClipboard', + componentName: 'copy-to-clipboard', + tests: [ + { + description: 'Variants', + path: 'copy-to-clipboard/simple', + screenshotType: 'screenshotArea', + }, + { + description: 'copy-to-clipboard below bottom split panel is not visible', + path: 'copy-to-clipboard/scenario-split-panel', + screenshotType: 'screenshotArea', + configuration: { width: 1280, height: 900 }, + setup: async ({ page, browser }) => { + await page.click('[aria-label="Copy dummy text"]'); + await browser!.execute((sel: string) => { + document.querySelector(sel)?.scrollIntoView(); + }, '[data-testid="scroll-me"]'); + }, + }, + ], +}; + +export default suite; diff --git a/test/definitions/visual/date-input.ts b/test/definitions/visual/date-input.ts new file mode 100644 index 0000000000..628e120d9f --- /dev/null +++ b/test/definitions/visual/date-input.ts @@ -0,0 +1,22 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { TestSuite } from '../types'; + +const suite: TestSuite = { + description: 'Date input', + componentName: 'date-input', + tests: [ + { + description: 'Permutations: states', + path: 'date-input/permutations-states', + screenshotType: 'permutations', + }, + { + description: 'Permutations: formats', + path: 'date-input/permutations-formats', + screenshotType: 'permutations', + }, + ], +}; + +export default suite; diff --git a/test/definitions/visual/date-picker.ts b/test/definitions/visual/date-picker.ts new file mode 100644 index 0000000000..c2cc20645b --- /dev/null +++ b/test/definitions/visual/date-picker.ts @@ -0,0 +1,54 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { TestSuite } from '../types'; + +const suite: TestSuite = { + description: 'Date picker', + componentName: 'date-picker', + tests: [ + { + description: 'Permutations: states', + path: 'date-picker/permutations', + screenshotType: 'permutations', + setup: async ({ page }) => { + await page.click('[data-testid="date-picker-expanded-example"] button'); + }, + }, + { + description: 'Permutations: formats', + path: 'date-picker/permutations-formats', + screenshotType: 'permutations', + }, + { + description: 'focus ring on selected month', + path: 'date-picker/month-picker', + screenshotType: 'screenshotArea', + setup: async ({ page, wrapper }) => { + await page.click(wrapper.findDatePicker().findOpenCalendarButton().toSelector()); + await page.keys(['Tab', 'Tab', 'Tab']); + }, + }, + { + description: 'focus ring on current month', + path: 'date-picker/month-picker', + screenshotType: 'screenshotArea', + setup: async ({ page, wrapper }) => { + await page.click(wrapper.findDatePicker().findOpenCalendarButton().toSelector()); + await page.keys(['Tab', 'Tab', 'Tab']); + await page.keys(['ArrowRight']); + }, + }, + { + description: 'focus ring on non selected, non current month', + path: 'date-picker/month-picker', + screenshotType: 'screenshotArea', + setup: async ({ page, wrapper }) => { + await page.click(wrapper.findDatePicker().findOpenCalendarButton().toSelector()); + await page.keys(['Tab', 'Tab', 'Tab']); + await page.keys(['ArrowRight', 'ArrowRight']); + }, + }, + ], +}; + +export default suite; diff --git a/test/definitions/visual/date-range-picker.ts b/test/definitions/visual/date-range-picker.ts new file mode 100644 index 0000000000..c18c4a1750 --- /dev/null +++ b/test/definitions/visual/date-range-picker.ts @@ -0,0 +1,107 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { TestDefinition, TestSuite } from '../types'; + +const suite: TestSuite = { + description: 'Date Range Picker', + componentName: 'date-range-picker', + tests: [ + ...[450, 1200].flatMap(width => [ + { + description: `Absolute range at ${width}px`, + path: 'date-range-picker/with-value', + screenshotType: 'screenshotArea' as const, + configuration: { width, height: 950 }, + setup: async ({ page }) => { + await page.click('#focusable-before'); + await page.focusNextElement(); + await page.keys(['Enter']); + await page.click('[data-date="2018-01-09"]'); + if (width >= 1000) { + await page.click('[data-date="2018-02-24"]'); + } else { + await page.click('[data-date="2018-01-27"]'); + } + }, + }, + { + description: `Relative range at ${width}px`, + path: 'date-range-picker/with-value', + screenshotType: 'screenshotArea' as const, + configuration: { width, height: 950 }, + setup: async ({ page }) => { + await page.click('#focusable-before'); + await page.focusNextElement(); + await page.keys(['Enter']); + await page.focusNextElement(); + if (width >= 1000) { + await page.keys(['ArrowLeft']); + await page.keys(['Enter']); + } else { + await page.keys(['Space']); + await page.keys(['ArrowUp']); + await page.keys(['Enter']); + } + await page.focusNextElement(); + await page.keys(['ArrowDown', 'ArrowDown', 'ArrowDown', 'ArrowDown']); + }, + }, + ]), + { + description: 'Absolute range input permutations for day granularity', + path: 'date-range-picker/absolute-format-day-picker.permutations', + screenshotType: 'screenshotArea', + }, + { + description: 'Absolute range input permutations for month granularity', + path: 'date-range-picker/absolute-format-month-picker.permutations', + screenshotType: 'permutations', + }, + { + description: 'Calendar permutations for day granularity', + path: 'date-range-picker/month-calendar-permutations', + screenshotType: 'permutations', + }, + { + description: 'Calendar permutations for month granularity', + path: 'date-range-picker/year-calendar-permutations', + screenshotType: 'permutations', + }, + { + description: 'selects text when double-clicking calendar header', + path: 'date-range-picker/with-value', + screenshotType: 'screenshotArea', + setup: async ({ page, wrapper, browser }) => { + await page.click('#focusable-before'); + await page.focusNextElement(); + await page.keys(['Enter']); + const headerSelector = wrapper.findDateRangePicker().findDropdown().findHeader().find('h2 span').toSelector(); + await browser!.execute((sel: string) => { + const el = document.querySelector(sel); + if (el) { + el.dispatchEvent(new MouseEvent('dblclick', { bubbles: true })); + } + }, headerSelector); + }, + }, + { + description: 'does not select text when double-clicking next button', + path: 'date-range-picker/with-value', + screenshotType: 'screenshotArea', + setup: async ({ page, wrapper, browser }) => { + await page.click('#focusable-before'); + await page.focusNextElement(); + await page.keys(['Enter']); + const nextButtonSelector = wrapper.findDateRangePicker().findDropdown().findNextMonthButton().toSelector(); + await browser!.execute((sel: string) => { + const el = document.querySelector(sel); + if (el) { + el.dispatchEvent(new MouseEvent('dblclick', { bubbles: true })); + } + }, nextButtonSelector); + }, + }, + ], +}; + +export default suite; diff --git a/test/definitions/visual/expandable-section.ts b/test/definitions/visual/expandable-section.ts new file mode 100644 index 0000000000..3d1dcadab2 --- /dev/null +++ b/test/definitions/visual/expandable-section.ts @@ -0,0 +1,59 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { TestDefinition, TestSuite } from '../types'; + +const allVariants = ['container', 'default', 'footer', 'navigation']; +const variantsWithDescription = ['container', 'default', 'footer']; +const variantsWithInteractiveElements = ['container']; + +const headerText = 'Header text'; +const headerDescription = 'Header description'; + +function focusTest(options: Record): TestDefinition { + return { + description: `focus - ${JSON.stringify(options)}`, + path: 'expandable-section/focus', + screenshotType: 'screenshotArea' as const, + queryParams: options, + setup: async ({ page }) => { + await page.click('#focus-target'); + await page.focusNextElement(); + }, + }; +} + +const suite: TestSuite = { + description: 'Expandable section', + componentName: 'expandable-section', + tests: [ + { + description: 'permutations', + path: 'expandable-section/permutations', + screenshotType: 'permutations', + }, + { + description: 'container variant', + path: 'expandable-section/container-variant.permutations', + screenshotType: 'permutations', + }, + // Focus tests - with only heading + ...allVariants.map(variant => focusTest({ headerText, variant })), + // Focus tests - with heading and description + ...variantsWithDescription.map(variant => focusTest({ headerText, headerDescription, variant })), + // Focus tests - with interactive elements + ...variantsWithInteractiveElements.map(variant => + focusTest({ headerText, hasHeaderInfo: 'true', hasHeaderActions: 'true', variant }) + ), + // Focus tests - with interactive elements and description + ...variantsWithInteractiveElements.map(variant => + focusTest({ headerText, headerDescription, hasHeaderInfo: 'true', hasHeaderActions: 'true', variant }) + ), + { + description: 'stacked variant', + path: 'expandable-section/stacked-variant.permutations', + screenshotType: 'permutations', + }, + ], +}; + +export default suite; diff --git a/test/visual/cards.test.ts b/test/visual/cards.test.ts new file mode 100644 index 0000000000..ecb282b266 --- /dev/null +++ b/test/visual/cards.test.ts @@ -0,0 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { runTestSuites } from '../definitions/utils'; +import suite from '../definitions/visual/cards'; + +runTestSuites([suite]); diff --git a/test/visual/checkbox.test.ts b/test/visual/checkbox.test.ts new file mode 100644 index 0000000000..f569e2e7ab --- /dev/null +++ b/test/visual/checkbox.test.ts @@ -0,0 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { runTestSuites } from '../definitions/utils'; +import suite from '../definitions/visual/checkbox'; + +runTestSuites([suite]); diff --git a/test/visual/code-editor.test.ts b/test/visual/code-editor.test.ts new file mode 100644 index 0000000000..735638ea5d --- /dev/null +++ b/test/visual/code-editor.test.ts @@ -0,0 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { runTestSuites } from '../definitions/utils'; +import suite from '../definitions/visual/code-editor'; + +runTestSuites([suite]); diff --git a/test/visual/collection-preferences.test.ts b/test/visual/collection-preferences.test.ts new file mode 100644 index 0000000000..ac54029622 --- /dev/null +++ b/test/visual/collection-preferences.test.ts @@ -0,0 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { runTestSuites } from '../definitions/utils'; +import suite from '../definitions/visual/collection-preferences'; + +runTestSuites([suite]); diff --git a/test/visual/column-layout.test.ts b/test/visual/column-layout.test.ts new file mode 100644 index 0000000000..55749a8e8b --- /dev/null +++ b/test/visual/column-layout.test.ts @@ -0,0 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { runTestSuites } from '../definitions/utils'; +import suite from '../definitions/visual/column-layout'; + +runTestSuites([suite]); diff --git a/test/visual/container-sticky.test.ts b/test/visual/container-sticky.test.ts new file mode 100644 index 0000000000..ce014d9805 --- /dev/null +++ b/test/visual/container-sticky.test.ts @@ -0,0 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { runTestSuites } from '../definitions/utils'; +import suite from '../definitions/visual/container-sticky'; + +runTestSuites([suite]); diff --git a/test/visual/container.test.ts b/test/visual/container.test.ts new file mode 100644 index 0000000000..d738bd8429 --- /dev/null +++ b/test/visual/container.test.ts @@ -0,0 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { runTestSuites } from '../definitions/utils'; +import suite from '../definitions/visual/container'; + +runTestSuites([suite]); diff --git a/test/visual/content-layout-permutations.test.ts b/test/visual/content-layout-permutations.test.ts new file mode 100644 index 0000000000..20de7543dc --- /dev/null +++ b/test/visual/content-layout-permutations.test.ts @@ -0,0 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { runTestSuites } from '../definitions/utils'; +import suite from '../definitions/visual/content-layout-permutations'; + +runTestSuites([suite]); diff --git a/test/visual/content-layout.test.ts b/test/visual/content-layout.test.ts new file mode 100644 index 0000000000..cc62ebf2a4 --- /dev/null +++ b/test/visual/content-layout.test.ts @@ -0,0 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { runTestSuites } from '../definitions/utils'; +import suite from '../definitions/visual/content-layout'; + +runTestSuites([suite]); diff --git a/test/visual/copy-to-clipboard.test.ts b/test/visual/copy-to-clipboard.test.ts new file mode 100644 index 0000000000..8f6f3afa5a --- /dev/null +++ b/test/visual/copy-to-clipboard.test.ts @@ -0,0 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { runTestSuites } from '../definitions/utils'; +import suite from '../definitions/visual/copy-to-clipboard'; + +runTestSuites([suite]); diff --git a/test/visual/date-input.test.ts b/test/visual/date-input.test.ts new file mode 100644 index 0000000000..d711364611 --- /dev/null +++ b/test/visual/date-input.test.ts @@ -0,0 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { runTestSuites } from '../definitions/utils'; +import suite from '../definitions/visual/date-input'; + +runTestSuites([suite]); diff --git a/test/visual/date-picker.test.ts b/test/visual/date-picker.test.ts new file mode 100644 index 0000000000..fd7087585b --- /dev/null +++ b/test/visual/date-picker.test.ts @@ -0,0 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { runTestSuites } from '../definitions/utils'; +import suite from '../definitions/visual/date-picker'; + +runTestSuites([suite]); diff --git a/test/visual/date-range-picker.test.ts b/test/visual/date-range-picker.test.ts new file mode 100644 index 0000000000..df5ed2d355 --- /dev/null +++ b/test/visual/date-range-picker.test.ts @@ -0,0 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { runTestSuites } from '../definitions/utils'; +import suite from '../definitions/visual/date-range-picker'; + +runTestSuites([suite]); diff --git a/test/visual/expandable-section.test.ts b/test/visual/expandable-section.test.ts new file mode 100644 index 0000000000..b052d5cf5f --- /dev/null +++ b/test/visual/expandable-section.test.ts @@ -0,0 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { runTestSuites } from '../definitions/utils'; +import suite from '../definitions/visual/expandable-section'; + +runTestSuites([suite]);