feat(ci): add e2e visual tests

This commit is contained in:
Lukasz Mierzwa
2026-03-03 16:27:41 +00:00
committed by Łukasz Mierzwa
parent 2ee439a573
commit 17bbd00f70
28 changed files with 810 additions and 0 deletions

View File

@@ -116,6 +116,37 @@ jobs:
directory: ./ui/coverage
token: c77c73a6-81e1-4fa1-8372-9170d7953d41
test-e2e:
name: E2E Tests
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Set up Node JS
uses: actions/setup-node@v6
with:
node-version: 25.7.0
cache: "npm"
cache-dependency-path: "ui/package-lock.json"
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
working-directory: ui
- name: Visual tests
run: make -C ui e2e
env:
NODE_ENV: test
- name: Upload visual diff artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: visual-regression-diffs
path: ui/src/e2e/results/
retention-days: 30
lint-js:
name: Lint JS code
runs-on: ubuntu-latest

1
.gitignore vendored
View File

@@ -5,5 +5,6 @@
/ui/coverage
/ui/node_modules
/ui/.eslintcache
/ui/src/e2e/results
/TODO.md
/.vscode

View File

@@ -18,4 +18,5 @@ ignores:
- fetch-mock
- identity-obj-proxy
- jest
- "@playwright/test"
- terser

View File

@@ -2,6 +2,7 @@ const path = require("path");
const config = {
root: true,
ignorePatterns: ["playwright.config.ts", "src/e2e/"],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 2022,

View File

@@ -34,9 +34,14 @@ build: dist/index.html
test-js: $(NODE_PATH)/vite $(NODE_PATH)/jest
CI=true npm test -- --coverage
.PHONY: e2e
e2e: $(NODE_PATH)/vite $(NODE_PATH)/playwright
npx playwright test
.PHONY: update-snapshots
update-snapshots: $(NODE_PATH)/vite $(NODE_PATH)/jest
CI=true npm test -- -u
npx playwright test --update-snapshots
.PHONY: lint-js
lint-js: $(NODE_PATH)/eslint

64
ui/package-lock.json generated
View File

@@ -53,6 +53,7 @@
},
"devDependencies": {
"@fetch-mock/jest": "0.2.20",
"@playwright/test": "1.58.2",
"@testing-library/jest-dom": "6.9.1",
"@testing-library/react": "16.3.2",
"@testing-library/user-event": "14.6.1",
@@ -3971,6 +3972,22 @@
"url": "https://opencollective.com/pkgr"
}
},
"node_modules/@playwright/test": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.58.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
@@ -10722,6 +10739,53 @@
"node": ">=8"
}
},
"node_modules/playwright": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.58.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",

View File

@@ -49,6 +49,7 @@
},
"devDependencies": {
"@fetch-mock/jest": "0.2.20",
"@playwright/test": "1.58.2",
"@testing-library/jest-dom": "6.9.1",
"@testing-library/react": "16.3.2",
"@testing-library/user-event": "14.6.1",
@@ -98,6 +99,7 @@
"jest": {
"collectCoverageFrom": [
"src/**/*.{js,ts,tsx}",
"!src/e2e/**",
"!src/**/*.stories.{js,ts,tsx}",
"!src/__fixtures__/Stories.{js,ts,tsx}",
"!src/react-app-env.d.ts",
@@ -112,6 +114,9 @@
"moduleNameMapper": {
"\\.(css|less|scss)$": "identity-obj-proxy"
},
"testPathIgnorePatterns": [
"<rootDir>/src/e2e/"
],
"modulePaths": [
"<rootDir>/src"
],

38
ui/playwright.config.ts Normal file
View File

@@ -0,0 +1,38 @@
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./src/e2e",
outputDir: "./src/e2e/results",
snapshotPathTemplate: "{testDir}/snapshots/{arg}{ext}",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: 0,
workers: 1,
reporter: "list",
use: {
baseURL: "http://localhost:3123",
screenshot: "off",
video: "off",
trace: "off",
},
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
viewport: { width: 1280, height: 720 },
},
},
],
webServer: {
command: "npx vite --port 3123 --strictPort",
port: 3123,
reuseExistingServer: !process.env.CI,
},
expect: {
toHaveScreenshot: {
animations: "disabled",
maxDiffPixelRatio: 0,
},
},
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 661 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

15
ui/src/e2e/stories.html Normal file
View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<title>Visual Regression Tests</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/e2e/stories.tsx"></script>
</body>
</html>

604
ui/src/e2e/stories.tsx Normal file
View File

@@ -0,0 +1,604 @@
import React from "react";
import { createRoot } from "react-dom/client";
import { addHours } from "date-fns/addHours";
import { addDays } from "date-fns/addDays";
import { faArrowUp } from "@fortawesome/free-solid-svg-icons/faArrowUp";
import { faExclamation } from "@fortawesome/free-solid-svg-icons/faExclamation";
import { ThemeContext } from "Components/Theme";
import type { ThemeCtx } from "Components/Theme";
import {
ReactSelectColors,
ReactSelectStyles,
} from "Components/Theme/ReactSelect";
import { AlertStore } from "Stores/AlertStore";
import { Settings } from "Stores/Settings";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { MockGrid } from "__fixtures__/Stories";
import { MockSilence } from "__fixtures__/Alerts";
import { FatalError } from "Components/Grid/FatalError";
import { UpgradeNeeded } from "Components/Grid/UpgradeNeeded";
import { ReloadNeeded } from "Components/Grid/ReloadNeeded";
import { EmptyGrid } from "Components/Grid/EmptyGrid";
import { NoUpstream } from "Components/Grid/NoUpstream";
import Grid from "Components/Grid";
import NavBar from "Components/NavBar";
import { InternalError } from "ErrorBoundary";
import { MainModalContent } from "Components/MainModal/MainModalContent";
import { OverviewModalContent } from "Components/OverviewModal/OverviewModalContent";
import { ManagedSilence } from "Components/ManagedSilence";
import { SilenceModalContent } from "Components/SilenceModal/SilenceModalContent";
import { Toast } from "Components/Toast";
import {
ToastMessage,
UpgradeToastMessage,
} from "Components/Toast/ToastMessages";
import "Styles/Percy.scss";
const jsonResponse = (data: unknown): Response =>
new Response(JSON.stringify(data), {
status: 200,
headers: { "Content-Type": "application/json" },
});
const mockSilenceEntry = (index: number) => {
const s = MockSilence();
s.startsAt = "2018-08-14T16:00:00Z";
s.endsAt = `2018-08-14T18:${index < 10 ? "0" + index : index}:00Z`;
s.matchers.push({
name: "thisIsAveryLongNameToTestMatcherWrapping",
value: "valueIsAlsoAbitLong",
isRegex: false,
isEqual: true,
});
s.matchers.push({
name: "alertname",
value: "(foo1|foo2|foo3|foo4)",
isRegex: true,
isEqual: true,
});
s.id = `silence${index}`;
return { cluster: "am", alertCount: (index - 1) * 9, silence: s };
};
const countersData = {
total: 90,
counters: [
{
name: "@receiver",
hits: 2,
values: [
{
value: "by-cluster-service",
raw: "@receiver=by-cluster-service",
hits: 2,
percent: 100,
offset: 0,
},
],
},
{
name: "alertname",
hits: 90,
values: [
{
value: "Fake Alert",
raw: "alertname=Fake Alert",
hits: 45,
percent: 50,
offset: 0,
},
{
value: "Second Fake Alert",
raw: "alertname=Second Fake Alert",
hits: 45,
percent: 50,
offset: 50,
},
],
},
{
name: "group",
hits: 100,
values: [
{
value: "group1",
raw: "group=group1",
hits: 25,
percent: 25,
offset: 0,
},
{
value: "group2",
raw: "group=group2",
hits: 70,
percent: 70,
offset: 25,
},
{
value: "group3",
raw: "group=group3",
hits: 4,
percent: 4,
offset: 95,
},
{
value: "group4",
raw: "group=group4",
hits: 1,
percent: 1,
offset: 99,
},
],
},
],
};
const originalFetch = window.fetch;
window.fetch = async (
input: RequestInfo | URL,
init?: RequestInit,
): Promise<Response> => {
const url =
typeof input === "string"
? input
: input instanceof URL
? input.toString()
: input.url;
const story = window.location.hash.replace("#", "").split("?")[0];
if (url.includes("labelNames.json"))
return jsonResponse(["cluster", "job", "instance"]);
if (url.includes("labelValues.json"))
return jsonResponse(["dev", "staging", "prod"]);
if (url.includes("alertList.json")) return jsonResponse({ alerts: [] });
if (url.includes("autocomplete.json")) return jsonResponse([]);
if (url.includes("alerts.json"))
return jsonResponse({ alerts: [], totalAlerts: 0 });
if (url.includes("counters.json")) {
if (story === "OverviewModal") return jsonResponse(countersData);
return jsonResponse({ total: 0, counters: [] });
}
if (url.includes("silences.json")) {
if (story === "SilenceModalBrowser") {
const silences = Array.from({ length: 18 }, (_, i) =>
mockSilenceEntry(i + 1),
);
return jsonResponse(silences);
}
return jsonResponse([]);
}
return originalFetch(input, init);
};
type StoryMap = Record<string, () => React.ReactNode>;
const Modal: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<div className="modal d-block" style={{ position: "relative" }}>
<div className="modal-dialog modal-lg" role="document">
<div className="modal-content">{children}</div>
</div>
</div>
);
const makeAlertStore = (): AlertStore => {
const alertStore = new AlertStore([]);
alertStore.data.setUpstreams({
counters: { total: 1, healthy: 1, failed: 0 },
instances: [
{
name: "am1",
cluster: "am",
clusterMembers: ["am1"],
uri: "http://localhost:9093",
publicURI: "http://example.com",
readonly: false,
error: "",
version: "0.24.0",
headers: {},
corsCredentials: "include",
},
],
clusters: { am: ["am1"] },
});
return alertStore;
};
const makeReadOnlyAlertStore = (): AlertStore => {
const alertStore = new AlertStore([]);
alertStore.data.setUpstreams({
counters: { healthy: 1, failed: 0, total: 1 },
clusters: { ro: ["readonly"] },
instances: [
{
name: "readonly",
uri: "http://localhost:8080",
publicURI: "http://example.com",
readonly: true,
headers: {},
corsCredentials: "include",
error: "",
version: "0.24.0",
cluster: "ro",
clusterMembers: ["readonly"],
},
],
});
return alertStore;
};
const gridStory = (): React.ReactNode => {
const alertStore = new AlertStore([]);
const settingsStore = new Settings(null);
const silenceFormStore = new SilenceFormStore();
MockGrid(alertStore);
return (
<Grid
alertStore={alertStore}
settingsStore={settingsStore}
silenceFormStore={silenceFormStore}
/>
);
};
const navBarStory = (): React.ReactNode => {
const alertStore = new AlertStore([]);
const settingsStore = new Settings(null);
const silenceFormStore = new SilenceFormStore();
alertStore.data.setUpstreams({
counters: { total: 1, healthy: 1, failed: 0 },
instances: [
{
name: "dev",
cluster: "dev",
clusterMembers: ["dev"],
uri: "https://am.example.com",
publicURI: "https://am.example.com",
error: "",
readonly: false,
headers: {},
corsCredentials: "include",
version: "",
},
],
clusters: { dev: ["dev"] },
});
alertStore.info.setTotalAlerts(197);
settingsStore.filterBarConfig.setAutohide(false);
return (
<NavBar
alertStore={alertStore}
settingsStore={settingsStore}
silenceFormStore={silenceFormStore}
fixedTop={false}
/>
);
};
const fatalErrorStory = (): React.ReactNode => (
<FatalError message="Something failed with a veryLongStringToTestTextWrappingveryLongStringToTestTextWrappingveryLongStringToTestTextWrapping" />
);
const upgradeNeededStory = (): React.ReactNode => (
<UpgradeNeeded newVersion="1.2.3" reloadAfter={100000000} />
);
const reloadNeededStory = (): React.ReactNode => (
<ReloadNeeded reloadAfter={100000000} />
);
const emptyGridStory = (): React.ReactNode => (
<div className="text-center">
<EmptyGrid />
</div>
);
const noUpstreamStory = (): React.ReactNode => <NoUpstream />;
const internalErrorStory = (): React.ReactNode => (
<InternalError
message="React error boundary message with a veryLongStringToTestTextWrappingveryLongStringToTestTextWrappingveryLongStringToTestTextWrapping"
secondsLeft={45}
progressLeft={66}
/>
);
const mainModalStory = (): React.ReactNode => {
const alertStore = makeAlertStore();
const settingsStore = new Settings(null);
alertStore.info.setVersion("1.2.3");
return (
<Modal>
<MainModalContent
alertStore={alertStore}
settingsStore={settingsStore}
onHide={() => {}}
expandAllOptions={true}
/>
</Modal>
);
};
const overviewModalStory = (): React.ReactNode => {
const alertStore = new AlertStore(["full"]);
MockGrid(alertStore);
return (
<Modal>
<OverviewModalContent alertStore={alertStore} onHide={() => {}} />
</Modal>
);
};
const managedSilenceStory = (): React.ReactNode => {
const alertStore = makeAlertStore();
const alertStoreReadOnly = makeReadOnlyAlertStore();
const silenceFormStore = new SilenceFormStore();
const silence = MockSilence();
silence.startsAt = "2018-08-14T16:00:00Z";
silence.endsAt = "2018-08-14T18:00:00Z";
const expiredSilence = MockSilence();
expiredSilence.startsAt = "2018-08-14T10:00:00Z";
expiredSilence.endsAt = "2018-08-14T11:00:00Z";
return (
<div className="modal-dialog modal-lg" role="document">
<div className="modal-content p-2">
<ManagedSilence
cluster="am"
alertCount={123}
alertCountAlwaysVisible={true}
silence={silence}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
onDidUpdate={() => {}}
/>
<ManagedSilence
cluster="am"
alertCount={123}
alertCountAlwaysVisible={true}
silence={silence}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
onDidUpdate={() => {}}
isOpen={true}
/>
<ManagedSilence
cluster="ro"
alertCount={123}
alertCountAlwaysVisible={true}
silence={silence}
alertStore={alertStoreReadOnly}
silenceFormStore={silenceFormStore}
onDidUpdate={() => {}}
isOpen={true}
/>
<ManagedSilence
cluster="am"
alertCount={123}
alertCountAlwaysVisible={true}
silence={expiredSilence}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
onDidUpdate={() => {}}
/>
<ManagedSilence
cluster="am"
alertCount={123}
alertCountAlwaysVisible={true}
silence={expiredSilence}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
onDidUpdate={() => {}}
isOpen={true}
/>
<ManagedSilence
cluster="ro"
alertCount={123}
alertCountAlwaysVisible={true}
silence={expiredSilence}
alertStore={alertStoreReadOnly}
silenceFormStore={silenceFormStore}
onDidUpdate={() => {}}
isOpen={true}
/>
</div>
</div>
);
};
const silenceModalEditorReadOnlyStory = (): React.ReactNode => {
const alertStore = makeReadOnlyAlertStore();
const settingsStore = new Settings(null);
const silenceFormStore = new SilenceFormStore();
silenceFormStore.tab.setTab("editor");
return (
<Modal>
<SilenceModalContent
alertStore={alertStore}
silenceFormStore={silenceFormStore}
settingsStore={settingsStore}
onHide={() => {}}
/>
</Modal>
);
};
const silenceModalEditorStory = (): React.ReactNode => {
const alertStore = makeAlertStore();
const settingsStore = new Settings(null);
const silenceFormStore = new SilenceFormStore();
silenceFormStore.data.setAuthor("me@example.com");
silenceFormStore.data.setComment("This is a test silence");
silenceFormStore.data.setStart(new Date("2018-08-14T17:36:40"));
silenceFormStore.data.setEnd(
addDays(addHours(new Date("2018-08-14T17:36:40"), 2), 10),
);
silenceFormStore.tab.setTab("editor");
return (
<Modal>
<SilenceModalContent
alertStore={alertStore}
silenceFormStore={silenceFormStore}
settingsStore={settingsStore}
onHide={() => {}}
previewOpen={true}
/>
</Modal>
);
};
const silenceModalBrowserStory = (): React.ReactNode => {
const alertStore = makeAlertStore();
const settingsStore = new Settings(null);
const silenceFormStore = new SilenceFormStore();
silenceFormStore.tab.setTab("browser");
return (
<Modal>
<SilenceModalContent
alertStore={alertStore}
silenceFormStore={silenceFormStore}
settingsStore={settingsStore}
onHide={() => {}}
/>
</Modal>
);
};
const silenceModalEmptyBrowserStory = (): React.ReactNode => {
const alertStore = makeAlertStore();
const settingsStore = new Settings(null);
const silenceFormStore = new SilenceFormStore();
silenceFormStore.tab.setTab("browser");
return (
<Modal>
<SilenceModalContent
alertStore={alertStore}
silenceFormStore={silenceFormStore}
settingsStore={settingsStore}
onHide={() => {}}
/>
</Modal>
);
};
const toastStory = (): React.ReactNode => {
const alertStore = new AlertStore([]);
alertStore.info.setVersion("999.99.0");
return (
<div className="d-flex flex-column">
<Toast
icon={faExclamation}
iconClass="text-danger"
message={
<ToastMessage
title="Alertmanager am1 raised an error"
message="connection refused"
/>
}
hasClose={true}
/>
<Toast
icon={faArrowUp}
iconClass="text-success"
message={<UpgradeToastMessage alertStore={alertStore} />}
hasClose={false}
/>
</div>
);
};
const stories: StoryMap = {
Grid: gridStory,
NavBar: navBarStory,
FatalError: fatalErrorStory,
UpgradeNeeded: upgradeNeededStory,
ReloadNeeded: reloadNeededStory,
EmptyGrid: emptyGridStory,
NoUpstream: noUpstreamStory,
InternalError: internalErrorStory,
MainModal: mainModalStory,
OverviewModal: overviewModalStory,
ManagedSilence: managedSilenceStory,
SilenceModalEditorReadOnly: silenceModalEditorReadOnlyStory,
SilenceModalEditor: silenceModalEditorStory,
SilenceModalBrowser: silenceModalBrowserStory,
SilenceModalEmptyBrowser: silenceModalEmptyBrowserStory,
Toast: toastStory,
};
const lightTheme: ThemeCtx = {
isDark: false,
reactSelectStyles: ReactSelectStyles(ReactSelectColors.Light),
animations: { duration: 0 },
};
const darkTheme: ThemeCtx = {
isDark: true,
reactSelectStyles: ReactSelectStyles(ReactSelectColors.Dark),
animations: { duration: 0 },
};
const StoryRenderer = ({ storyFn }: { storyFn: () => React.ReactNode }) => (
<div>
<div className="theme-light">
<ThemeContext.Provider value={lightTheme}>
{storyFn()}
</ThemeContext.Provider>
</div>
<div
style={{
height: "16px",
width: "100%",
backgroundColor: "#eee",
marginTop: "4px",
marginBottom: "4px",
}}
/>
<div className="theme-dark">
<ThemeContext.Provider value={darkTheme}>
{storyFn()}
</ThemeContext.Provider>
</div>
</div>
);
const VisualTestApp = () => {
const storyName = window.location.hash.replace("#", "") || "";
const storyFn = stories[storyName];
if (!storyFn) {
return (
<div style={{ padding: "2rem" }}>
<h1>Visual Regression Test Stories</h1>
<ul>
{Object.keys(stories).map((name) => (
<li key={name}>
<a href={`#${name}`}>{name}</a>
</li>
))}
</ul>
</div>
);
}
return <StoryRenderer storyFn={storyFn} />;
};
const rootEl = document.getElementById("root");
if (rootEl) {
const root = createRoot(rootEl);
root.render(<VisualTestApp />);
}

31
ui/src/e2e/visual.spec.ts Normal file
View File

@@ -0,0 +1,31 @@
import { test, expect } from "@playwright/test";
const stories: { name: string; waitFor: string }[] = [
{ name: "Grid", waitFor: "#root > div" },
{ name: "NavBar", waitFor: "#root > div" },
{ name: "FatalError", waitFor: "#root > div" },
{ name: "UpgradeNeeded", waitFor: "#root > div" },
{ name: "ReloadNeeded", waitFor: "#root > div" },
{ name: "EmptyGrid", waitFor: "#root > div" },
{ name: "NoUpstream", waitFor: "#root > div" },
{ name: "InternalError", waitFor: "#root > div" },
{ name: "MainModal", waitFor: ".modal-content" },
{ name: "OverviewModal", waitFor: ".modal-content" },
{ name: "ManagedSilence", waitFor: ".components-managed-silence" },
{ name: "SilenceModalEditorReadOnly", waitFor: ".modal-content" },
{ name: "SilenceModalEditor", waitFor: ".modal-content" },
{ name: "SilenceModalBrowser", waitFor: ".components-managed-silence" },
{ name: "SilenceModalEmptyBrowser", waitFor: ".modal-content" },
{ name: "Toast", waitFor: "#root > div" },
];
for (const story of stories) {
test(story.name, async ({ page }) => {
await page.clock.install({ time: new Date("2018-08-14T17:00:00Z") });
await page.goto(`/src/e2e/stories.html#${story.name}`);
await page.locator(story.waitFor).first().waitFor({ timeout: 10000 });
await expect(page).toHaveScreenshot(`${story.name}.png`, {
fullPage: true,
});
});
}

View File

@@ -10,6 +10,20 @@ export default defineConfig({
open: true,
port: 3000,
},
css: {
preprocessorOptions: {
scss: {
silenceDeprecations: [
"abs-percent",
"import",
"if-function",
"duplicate-var-flags",
"global-builtin",
"color-functions",
],
},
},
},
build: {
sourcemap: true,
},