diff --git a/.gitignore b/.gitignore index 8f04b375c5..437d05fcd3 100644 --- a/.gitignore +++ b/.gitignore @@ -139,3 +139,5 @@ yarn.lock /playwright-report/ /blob-report/ /playwright/.cache/ + +e2e/screenshots/ diff --git a/config.toml.sample b/config.toml.sample index e8183e0ce5..fb44701a1b 100644 --- a/config.toml.sample +++ b/config.toml.sample @@ -87,3 +87,6 @@ webServerURL = "[Web server website URL. App will use the site instead of local [pipeline] #endpoint = "http://mlops.com:9500" # FastTrack API endpoint. #frontendEndpoint = "http://mlops.com:9500" # FastTrack frontend endpoint. + +[test] +# screenshotPath = "[Absolute path to save screenshots. If blank, it will be saved in the e2e/screenshots directory.]" diff --git a/e2e/screenshot.test.ts b/e2e/screenshot.test.ts new file mode 100644 index 0000000000..7e53152d27 --- /dev/null +++ b/e2e/screenshot.test.ts @@ -0,0 +1,103 @@ +import { loginAsAdmin, getConfigValue, webuiEndpoint } from './test-util'; +import { test } from '@playwright/test'; +import * as path from 'path'; + +const routes = [ + '/summary', + '/session', + '/session/start', + '/serving', + '/service', + '/service/start', + '/import', + '/data', + '/my-environment', + '/agent-summary', + '/statistics', + '/environment', + '/agent', + '/settings', + '/maintenance', + '/information', + '/usersettings', + '/credential', + '/logs', + '/error', + '/unauthorized', +]; + +test.describe('Screenshot all routes', () => { + let screenshotPath: string; + test.beforeEach(async ({ page, request }) => { + await loginAsAdmin(page); + const configValue = await getConfigValue(request, 'test.screenshotPath'); + screenshotPath = configValue + ? configValue + : path.resolve(__dirname + '/screenshots'); + }); + + routes.forEach((route) => { + test(`screenshot ${route}`, async ({ page }) => { + await page.goto(`${webuiEndpoint}${route}`, { + waitUntil: 'networkidle', + }); + await page.screenshot({ + path: path.resolve( + `${screenshotPath}/${route.replace(/\//g, '_')}.png`, + ), + fullPage: true, + }); + }); + test(`screenshot ${route} (dark mode)`, async ({ page }) => { + await page.goto(`${webuiEndpoint}${route}`, { + waitUntil: 'networkidle', + }); + // Wait for the dark mode to be applied + await page.waitForTimeout(500); + await page.getByRole('button', { name: 'moon' }).click(); + await page.screenshot({ + path: path.resolve( + `${screenshotPath}/${route.replace(/\//g, '_')}_dark.png`, + ), + fullPage: true, + }); + }); + test(`screenshot ${route} without sidebar`, async ({ page }) => { + await page.goto(`${webuiEndpoint}${route}`, { + waitUntil: 'networkidle', + }); + await page.screenshot({ + path: path.resolve( + `${screenshotPath}/${route.replace(/\//g, '_')}_no_sidebar.png`, + ), + clip: { + x: 240, + y: 0, + width: (page.viewportSize()?.width || 0) - 240, + height: page.viewportSize()?.height || 0, + }, + }); + }); + test(`screenshot ${route} without sidebar (dark mode)`, async ({ + page, + }) => { + await page.goto(`${webuiEndpoint}${route}`, { + waitUntil: 'networkidle', + }); + await page.getByRole('button', { name: 'moon' }).click(); + // Wait for the dark mode to be applied + await page.waitForTimeout(500); + await page.screenshot({ + path: path.resolve( + `${screenshotPath}/${route.replace(/\//g, '_')}_no_sidebar_dark.png`, + ), + clip: { + x: 240, + y: 0, + width: (page.viewportSize()?.width || 0) - 240, + height: page.viewportSize()?.height || 0, + }, + }); + }); + }); +}); diff --git a/e2e/test-util.ts b/e2e/test-util.ts index 22ce9e5498..58c6775705 100644 --- a/e2e/test-util.ts +++ b/e2e/test-util.ts @@ -290,3 +290,34 @@ export async function modifyConfigToml( }); }); } + +/** + * Get the value of a nested key from an object using dot notation + * + * @param obj + * @param key + * @returns + */ +function getNestedValue(obj: any, key: string) { + const keys = key.split('.'); + let value = obj; + for (const k of keys) { + value = value?.[k]; + } + return value; +} + +/** + * Get the value of the key from the config.toml file + * + * @param request + * @param key + * @returns + */ +export async function getConfigValue(request: APIRequestContext, key: string) { + const configToml = await ( + await request.get(`${webuiEndpoint}/config.toml`) + ).text(); + const config = TOML.parse(configToml); + return getNestedValue(config, key); +} diff --git a/package.json b/package.json index 46ae97c2e4..dadf758293 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "@babel/preset-typescript": "^7.24.7", "@babel/types": "^7.25.2", "@electron/packager": "^18.3.3", - "@playwright/test": "^1.46.1", + "@playwright/test": "^1.49.1", "@rollup/plugin-commonjs": "^25.0.8", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-replace": "^5.0.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ae49ed8efe..0de69265b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -236,8 +236,8 @@ importers: specifier: ^18.3.3 version: 18.3.3 '@playwright/test': - specifier: ^1.46.1 - version: 1.46.1 + specifier: ^1.49.1 + version: 1.49.1 '@rollup/plugin-commonjs': specifier: ^25.0.8 version: 25.0.8(rollup@4.20.0) @@ -3288,6 +3288,11 @@ packages: engines: {node: '>=18'} hasBin: true + '@playwright/test@1.49.1': + resolution: {integrity: sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==} + engines: {node: '>=18'} + hasBin: true + '@pmmmwh/react-refresh-webpack-plugin@0.5.15': resolution: {integrity: sha512-LFWllMA55pzB9D34w/wXUCf8+c+IYKuJDgxiZ3qMhl64KRMBHYM1I3VdGaD2BV5FNPV2/S2596bppxHbv2ZydQ==} engines: {node: '>= 10.13'} @@ -9820,11 +9825,21 @@ packages: engines: {node: '>=18'} hasBin: true + playwright-core@1.49.1: + resolution: {integrity: sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==} + engines: {node: '>=18'} + hasBin: true + playwright@1.46.1: resolution: {integrity: sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng==} engines: {node: '>=18'} hasBin: true + playwright@1.49.1: + resolution: {integrity: sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==} + engines: {node: '>=18'} + hasBin: true + plist@3.1.0: resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} engines: {node: '>=10.4.0'} @@ -16444,6 +16459,10 @@ snapshots: dependencies: playwright: 1.46.1 + '@playwright/test@1.49.1': + dependencies: + playwright: 1.49.1 + '@pmmmwh/react-refresh-webpack-plugin@0.5.15(react-refresh@0.11.0)(type-fest@2.19.0)(webpack-dev-server@4.15.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)(webpack-cli@5.1.4(webpack@5.93.0))(webpack@5.97.1(esbuild@0.19.12)(webpack-cli@5.1.4(webpack@5.93.0))))(webpack-hot-middleware@2.26.1)(webpack@5.97.1(esbuild@0.19.12)(webpack-cli@5.1.4(webpack@5.93.0)))': dependencies: ansi-html: 0.0.9 @@ -18554,17 +18573,17 @@ snapshots: '@webcomponents/webcomponentsjs@2.8.0': {} - '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4(webpack@5.93.0))(webpack@5.93.0(webpack-cli@5.1.4))': + '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4)(webpack@5.93.0)': dependencies: webpack: 5.93.0(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack@5.93.0) - '@webpack-cli/info@2.0.2(webpack-cli@5.1.4(webpack@5.93.0))(webpack@5.93.0(webpack-cli@5.1.4))': + '@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.93.0)': dependencies: webpack: 5.93.0(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack@5.93.0) - '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4(webpack@5.93.0))(webpack@5.93.0(webpack-cli@5.1.4))': + '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack@5.93.0)': dependencies: webpack: 5.93.0(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack@5.93.0) @@ -25096,12 +25115,20 @@ snapshots: playwright-core@1.46.1: {} + playwright-core@1.49.1: {} + playwright@1.46.1: dependencies: playwright-core: 1.46.1 optionalDependencies: fsevents: 2.3.2 + playwright@1.49.1: + dependencies: + playwright-core: 1.49.1 + optionalDependencies: + fsevents: 2.3.2 + plist@3.1.0: dependencies: '@xmldom/xmldom': 0.8.10 @@ -27641,7 +27668,7 @@ snapshots: optionalDependencies: esbuild: 0.19.12 - terser-webpack-plugin@5.3.10(webpack@5.93.0(webpack-cli@5.1.4)): + terser-webpack-plugin@5.3.10(webpack@5.93.0): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 @@ -28412,9 +28439,9 @@ snapshots: webpack-cli@5.1.4(webpack@5.93.0): dependencies: '@discoveryjs/json-ext': 0.5.7 - '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4(webpack@5.93.0))(webpack@5.93.0(webpack-cli@5.1.4)) - '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4(webpack@5.93.0))(webpack@5.93.0(webpack-cli@5.1.4)) - '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4(webpack@5.93.0))(webpack@5.93.0(webpack-cli@5.1.4)) + '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4)(webpack@5.93.0) + '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4)(webpack@5.93.0) + '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4)(webpack@5.93.0) colorette: 2.0.20 commander: 10.0.1 cross-spawn: 7.0.3 @@ -28541,7 +28568,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.10(webpack@5.93.0(webpack-cli@5.1.4)) + terser-webpack-plugin: 5.3.10(webpack@5.93.0) watchpack: 2.4.1 webpack-sources: 3.2.3 optionalDependencies: