Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/visual-regression.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 }}
Expand Down
38 changes: 38 additions & 0 deletions test/definitions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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,
Expand All @@ -69,4 +95,16 @@ export const allSuites: TestSuite[] = [
...button,
...buttonDropdown,
...buttonGroup,
...cards,
...checkbox,
...codeEditor,
...collectionPreferences,
...columnLayout,
...container,
...contentLayout,
...copyToClipboard,
...dateInput,
...datePicker,
...dateRangePicker,
...expandableSection,
];
116 changes: 57 additions & 59 deletions test/definitions/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,81 +60,79 @@
}

export function runTestSuites(suites: Array<TestDefinition | TestSuite>) {
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<TestDefinition | TestSuite>, getBrowser: () => WebdriverIO.Browser) {
function registerSuites(suites: Array<TestDefinition | TestSuite>) {
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);

Check warning on line 133 in test/definitions/utils.ts

View workflow job for this annotation

GitHub Actions / Visual regression / Visual regression (shard 5)

RETRY 1: Container and header › correctly displays container with top media

expect(received).toBeLessThanOrEqual(expected) Expected: <= 0 Received: 322487 at Object.<anonymous> (test/definitions/utils.ts:133:33)

Check warning on line 133 in test/definitions/utils.ts

View workflow job for this annotation

GitHub Actions / Visual regression / Visual regression (shard 3)

RETRY 1: ContentLayout permutations › {"headerVariant":"default"

expect(received).toBeLessThanOrEqual(expected) Expected: <= 0 Received: 62547 at Object.<anonymous> (test/definitions/utils.ts:133:33)
} finally {
await browser.deleteSession();
}
});
}
16 changes: 16 additions & 0 deletions test/definitions/visual/cards.ts
Original file line number Diff line number Diff line change
@@ -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;
36 changes: 36 additions & 0 deletions test/definitions/visual/checkbox.ts
Original file line number Diff line number Diff line change
@@ -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;
99 changes: 99 additions & 0 deletions test/definitions/visual/code-editor.ts
Original file line number Diff line number Diff line change
@@ -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(

Check failure on line 11 in test/definitions/visual/code-editor.ts

View workflow job for this annotation

GitHub Actions / Visual regression / Visual regression (shard 7)

Code editor › compare simple on small screen

Ace editor with theme class not found or not visible within 30s at node_modules/webdriverio/build/node.js:6083:15 at Browser.wrapCommandFn (node_modules/@wdio/utils/build/index.js:982:23) at waitForAceTheme (test/definitions/visual/code-editor.ts:11:5) at Object.setup (test/definitions/visual/code-editor.ts:93:9) at preparePage (test/definitions/utils.ts:58:5) at Object.<anonymous> (test/definitions/utils.ts:128:7)

Check warning on line 11 in test/definitions/visual/code-editor.ts

View workflow job for this annotation

GitHub Actions / Visual regression / Visual regression (shard 7)

RETRY 2: Code editor › compare simple on small screen

Ace editor with theme class not found or not visible within 30s at node_modules/webdriverio/build/node.js:6083:15 at Browser.wrapCommandFn (node_modules/@wdio/utils/build/index.js:982:23) at waitForAceTheme (test/definitions/visual/code-editor.ts:11:5) at Object.setup (test/definitions/visual/code-editor.ts:93:9) at preparePage (test/definitions/utils.ts:58:5) at Object.<anonymous> (test/definitions/utils.ts:128:7)

Check warning on line 11 in test/definitions/visual/code-editor.ts

View workflow job for this annotation

GitHub Actions / Visual regression / Visual regression (shard 7)

RETRY 1: Code editor › compare simple on small screen

Ace editor with theme class not found or not visible within 30s at node_modules/webdriverio/build/node.js:6083:15 at Browser.wrapCommandFn (node_modules/@wdio/utils/build/index.js:982:23) at waitForAceTheme (test/definitions/visual/code-editor.ts:11:5) at Object.setup (test/definitions/visual/code-editor.ts:93:9) at preparePage (test/definitions/utils.ts:58:5) at Object.<anonymous> (test/definitions/utils.ts:128:7)

Check failure on line 11 in test/definitions/visual/code-editor.ts

View workflow job for this annotation

GitHub Actions / Visual regression / Visual regression (shard 7)

Code editor › listens to mode change

Ace editor with theme class not found or not visible within 30s at node_modules/webdriverio/build/node.js:6083:15 at Browser.wrapCommandFn (node_modules/@wdio/utils/build/index.js:982:23) at waitForAceTheme (test/definitions/visual/code-editor.ts:11:5) at Object.setup (test/definitions/visual/code-editor.ts:83:9) at preparePage (test/definitions/utils.ts:58:5) at Object.<anonymous> (test/definitions/utils.ts:128:7)

Check warning on line 11 in test/definitions/visual/code-editor.ts

View workflow job for this annotation

GitHub Actions / Visual regression / Visual regression (shard 7)

RETRY 2: Code editor › listens to mode change

Ace editor with theme class not found or not visible within 30s at node_modules/webdriverio/build/node.js:6083:15 at Browser.wrapCommandFn (node_modules/@wdio/utils/build/index.js:982:23) at waitForAceTheme (test/definitions/visual/code-editor.ts:11:5) at Object.setup (test/definitions/visual/code-editor.ts:83:9) at preparePage (test/definitions/utils.ts:58:5) at Object.<anonymous> (test/definitions/utils.ts:128:7)

Check warning on line 11 in test/definitions/visual/code-editor.ts

View workflow job for this annotation

GitHub Actions / Visual regression / Visual regression (shard 7)

RETRY 1: Code editor › listens to mode change

Ace editor with theme class not found or not visible within 30s at node_modules/webdriverio/build/node.js:6083:15 at Browser.wrapCommandFn (node_modules/@wdio/utils/build/index.js:982:23) at waitForAceTheme (test/definitions/visual/code-editor.ts:11:5) at Object.setup (test/definitions/visual/code-editor.ts:83:9) at preparePage (test/definitions/utils.ts:58:5) at Object.<anonymous> (test/definitions/utils.ts:128:7)

Check failure on line 11 in test/definitions/visual/code-editor.ts

View workflow job for this annotation

GitHub Actions / Visual regression / Visual regression (shard 7)

Code editor › permutations

Ace editor with theme class not found or not visible within 30s at node_modules/webdriverio/build/node.js:6083:15 at Browser.wrapCommandFn (node_modules/@wdio/utils/build/index.js:982:23) at waitForAceTheme (test/definitions/visual/code-editor.ts:11:5) at Object.setup (test/definitions/visual/code-editor.ts:74:9) at preparePage (test/definitions/utils.ts:58:5) at Object.<anonymous> (test/definitions/utils.ts:101:9)

Check warning on line 11 in test/definitions/visual/code-editor.ts

View workflow job for this annotation

GitHub Actions / Visual regression / Visual regression (shard 7)

RETRY 2: Code editor › permutations

Ace editor with theme class not found or not visible within 30s at node_modules/webdriverio/build/node.js:6083:15 at Browser.wrapCommandFn (node_modules/@wdio/utils/build/index.js:982:23) at waitForAceTheme (test/definitions/visual/code-editor.ts:11:5) at Object.setup (test/definitions/visual/code-editor.ts:74:9) at preparePage (test/definitions/utils.ts:58:5) at Object.<anonymous> (test/definitions/utils.ts:101:9)

Check warning on line 11 in test/definitions/visual/code-editor.ts

View workflow job for this annotation

GitHub Actions / Visual regression / Visual regression (shard 7)

RETRY 1: Code editor › permutations

Ace editor with theme class not found or not visible within 30s at node_modules/webdriverio/build/node.js:6083:15 at Browser.wrapCommandFn (node_modules/@wdio/utils/build/index.js:982:23) at waitForAceTheme (test/definitions/visual/code-editor.ts:11:5) at Object.setup (test/definitions/visual/code-editor.ts:74:9) at preparePage (test/definitions/utils.ts:58:5) at Object.<anonymous> (test/definitions/utils.ts:101:9)

Check failure on line 11 in test/definitions/visual/code-editor.ts

View workflow job for this annotation

GitHub Actions / Visual regression / Visual regression (shard 7)

Code editor › theme resolution

Ace editor with theme class not found or not visible within 30s at node_modules/webdriverio/build/node.js:6083:15 at Browser.wrapCommandFn (node_modules/@wdio/utils/build/index.js:982:23) at waitForAceTheme (test/definitions/visual/code-editor.ts:11:5) at Object.setup (test/definitions/visual/code-editor.ts:66:9) at preparePage (test/definitions/utils.ts:58:5) at Object.<anonymous> (test/definitions/utils.ts:128:7)

Check warning on line 11 in test/definitions/visual/code-editor.ts

View workflow job for this annotation

GitHub Actions / Visual regression / Visual regression (shard 7)

RETRY 2: Code editor › theme resolution

Ace editor with theme class not found or not visible within 30s at node_modules/webdriverio/build/node.js:6083:15 at Browser.wrapCommandFn (node_modules/@wdio/utils/build/index.js:982:23) at waitForAceTheme (test/definitions/visual/code-editor.ts:11:5) at Object.setup (test/definitions/visual/code-editor.ts:66:9) at preparePage (test/definitions/utils.ts:58:5) at Object.<anonymous> (test/definitions/utils.ts:128:7)

Check warning on line 11 in test/definitions/visual/code-editor.ts

View workflow job for this annotation

GitHub Actions / Visual regression / Visual regression (shard 7)

RETRY 1: Code editor › theme resolution

Ace editor with theme class not found or not visible within 30s at node_modules/webdriverio/build/node.js:6083:15 at Browser.wrapCommandFn (node_modules/@wdio/utils/build/index.js:982:23) at waitForAceTheme (test/definitions/visual/code-editor.ts:11:5) at Object.setup (test/definitions/visual/code-editor.ts:66:9) at preparePage (test/definitions/utils.ts:58:5) at Object.<anonymous> (test/definitions/utils.ts:128:7)

Check failure on line 11 in test/definitions/visual/code-editor.ts

View workflow job for this annotation

GitHub Actions / Visual regression / Visual regression (shard 7)

Code editor › simple

Ace editor with theme class not found or not visible within 30s at node_modules/webdriverio/build/node.js:6083:15 at Browser.wrapCommandFn (node_modules/@wdio/utils/build/index.js:982:23) at waitForAceTheme (test/definitions/visual/code-editor.ts:11:5) at Object.setup (test/definitions/visual/code-editor.ts:48:9) at preparePage (test/definitions/utils.ts:58:5) at Object.<anonymous> (test/definitions/utils.ts:128:7)

Check warning on line 11 in test/definitions/visual/code-editor.ts

View workflow job for this annotation

GitHub Actions / Visual regression / Visual regression (shard 7)

RETRY 2: Code editor › simple

Ace editor with theme class not found or not visible within 30s at node_modules/webdriverio/build/node.js:6083:15 at Browser.wrapCommandFn (node_modules/@wdio/utils/build/index.js:982:23) at waitForAceTheme (test/definitions/visual/code-editor.ts:11:5) at Object.setup (test/definitions/visual/code-editor.ts:48:9) at preparePage (test/definitions/utils.ts:58:5) at Object.<anonymous> (test/definitions/utils.ts:128:7)

Check warning on line 11 in test/definitions/visual/code-editor.ts

View workflow job for this annotation

GitHub Actions / Visual regression / Visual regression (shard 7)

RETRY 1: Code editor › simple

Ace editor with theme class not found or not visible within 30s at node_modules/webdriverio/build/node.js:6083:15 at Browser.wrapCommandFn (node_modules/@wdio/utils/build/index.js:982:23) at waitForAceTheme (test/definitions/visual/code-editor.ts:11:5) at Object.setup (test/definitions/visual/code-editor.ts:48:9) at preparePage (test/definitions/utils.ts:58:5) at Object.<anonymous> (test/definitions/utils.ts:128:7)
() =>
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;
Loading
Loading