mirror of
https://github.com/dotnetcore/AgileConfig.git
synced 2026-10-16 20:21:29 +08:00
test: add end-to-end automation coverage
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
name: Docker E2E
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 18 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
cluster:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/AgileConfig.Server.UI/react-ui-antd
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: npm
|
||||
cache-dependency-path: src/AgileConfig.Server.UI/react-ui-antd/package-lock.json
|
||||
- run: npm ci
|
||||
- run: npm run test:e2e:docker
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
working-directory: src/AgileConfig.Server.UI/react-ui-antd
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x]
|
||||
node-version: [20.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -21,9 +21,15 @@ jobs:
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- run: npx playwright install --with-deps chromium
|
||||
- run: npm run test:e2e
|
||||
build-dotnet:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
working-directory: src/AgileConfig.Server.UI/react-ui-antd
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x]
|
||||
node-version: [20.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -21,9 +21,15 @@ jobs:
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- run: npx playwright install --with-deps chromium
|
||||
- run: npm run test:e2e
|
||||
build-dotnet:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
@@ -273,4 +273,6 @@ __pycache__/
|
||||
/src/AgileConfig.Server.Apisite/wwwroot/ui/
|
||||
/src/AgileConfig.Server.Apisite/agile_config.db
|
||||
/coverage
|
||||
/src/AgileConfig.Server.UI/react-ui-antd/playwright-report/
|
||||
/src/AgileConfig.Server.UI/react-ui-antd/test-results/
|
||||
/AGENTS.md
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ WORKDIR /app
|
||||
EXPOSE 5000
|
||||
|
||||
# 前端与架构无关,固定跑在构建机原生平台上,避免 QEMU 模拟拖慢 npm
|
||||
FROM --platform=$BUILDPLATFORM node:16 AS ui-build
|
||||
FROM --platform=$BUILDPLATFORM node:20 AS ui-build
|
||||
WORKDIR /src/AgileConfig.Server.UI/react-ui-antd
|
||||
COPY ["src/AgileConfig.Server.UI/react-ui-antd/package.json", "src/AgileConfig.Server.UI/react-ui-antd/package-lock.json", "./"]
|
||||
RUN npm ci
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# End-to-end tests
|
||||
|
||||
The Playwright suite starts a production-mode Apisite on `127.0.0.1:5187` with a temporary SQLite database. It builds the React UI when `dist` is absent, copies that build into the API release output, then visits the UI through `/ui`.
|
||||
|
||||
Run the complete suite from `src/AgileConfig.Server.UI/react-ui-antd`:
|
||||
|
||||
```powershell
|
||||
npx playwright install chromium
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
Run an individual layer with `npm run test:e2e:ui` or `npm run test:e2e:protocol`. Failed runs retain traces, screenshots, and video in `test-results`.
|
||||
|
||||
The Docker cluster test is intentionally separate from the browser suite. It builds the image, starts MySQL plus three AgileConfig nodes, then publishes through the admin node and verifies a WebSocket reload and configuration retrieval from a second node:
|
||||
|
||||
```powershell
|
||||
npm run test:e2e:docker
|
||||
```
|
||||
|
||||
It requires a running Docker daemon. GitHub Actions runs it nightly at 02:00 China Standard Time and on manual dispatch.
|
||||
@@ -0,0 +1,56 @@
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.4
|
||||
environment:
|
||||
MYSQL_DATABASE: agileconfig
|
||||
MYSQL_ROOT_PASSWORD: e2e-mysql-password
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mysqladmin ping -h localhost -uroot -pe2e-mysql-password"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
|
||||
admin:
|
||||
build:
|
||||
context: ../../../../..
|
||||
image: agileconfig-e2e:local
|
||||
ports:
|
||||
- "15100:5000"
|
||||
environment: &server-environment
|
||||
adminConsole: "true"
|
||||
saPassword: e2e-admin-password
|
||||
cluster: "true"
|
||||
db__provider: mysql
|
||||
db__conn: "database=agileconfig;data source=mysql;User Id=root;password=e2e-mysql-password;port=3306"
|
||||
otlp__logs__endpoint: ""
|
||||
otlp__traces__endpoint: ""
|
||||
otlp__metrics__endpoint: ""
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
|
||||
node1:
|
||||
image: agileconfig-e2e:local
|
||||
ports:
|
||||
- "15101:5000"
|
||||
environment:
|
||||
<<: *server-environment
|
||||
adminConsole: "false"
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
admin:
|
||||
condition: service_started
|
||||
|
||||
node2:
|
||||
image: agileconfig-e2e:local
|
||||
ports:
|
||||
- "15102:5000"
|
||||
environment:
|
||||
<<: *server-environment
|
||||
adminConsole: "false"
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
admin:
|
||||
condition: service_started
|
||||
@@ -0,0 +1,99 @@
|
||||
const { execFileSync } = require('child_process');
|
||||
const { setTimeout: delay } = require('timers/promises');
|
||||
const path = require('path');
|
||||
const WebSocket = require('ws');
|
||||
|
||||
const composeFile = path.join(__dirname, 'docker-compose.yml');
|
||||
const compose = (args) => execFileSync('docker', ['compose', '-f', composeFile, ...args], { stdio: 'inherit' });
|
||||
const adminUrl = 'http://127.0.0.1:15100';
|
||||
const nodeUrl = 'http://127.0.0.1:15102';
|
||||
|
||||
async function waitFor(url, timeoutMs = 120_000) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (response.ok) return;
|
||||
} catch {
|
||||
// The container has not completed startup yet.
|
||||
}
|
||||
await delay(1_000);
|
||||
}
|
||||
throw new Error(`Timed out waiting for ${url}.`);
|
||||
}
|
||||
|
||||
async function json(url, options) {
|
||||
const response = await fetch(url, options);
|
||||
if (!response.ok) throw new Error(`${options?.method ?? 'GET'} ${url} failed with ${response.status}.`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function main() {
|
||||
compose(['up', '--build', '--detach']);
|
||||
try {
|
||||
await waitFor(`${adminUrl}/home/echo`);
|
||||
await waitFor(`${nodeUrl}/home/echo`);
|
||||
|
||||
const login = await json(`${adminUrl}/admin/jwt/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ userName: 'admin', password: 'e2e-admin-password' }),
|
||||
});
|
||||
if (login.status !== 'ok') throw new Error('Admin login failed.');
|
||||
const adminHeaders = { Authorization: `Bearer ${login.token}`, 'Content-Type': 'application/json' };
|
||||
const suffix = Date.now().toString();
|
||||
const appId = `e2e-cluster-${suffix}`;
|
||||
const secret = 'e2e-cluster-secret';
|
||||
|
||||
const app = await json(`${adminUrl}/app/add`, {
|
||||
method: 'POST', headers: adminHeaders,
|
||||
body: JSON.stringify({ id: appId, name: appId, secret, enabled: true, inheritanced: true }),
|
||||
});
|
||||
if (!app.success) throw new Error(`Application creation failed: ${app.message ?? 'unknown error'}.`);
|
||||
|
||||
const socket = new WebSocket(`${nodeUrl.replace('http', 'ws')}/ws?client_name=e2e-cluster`, {
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from(`${appId}:${secret}`).toString('base64')}`,
|
||||
appid: appId,
|
||||
env: 'DEV',
|
||||
'client-v': '1.8.0',
|
||||
},
|
||||
});
|
||||
await new Promise((resolve, reject) => { socket.once('open', resolve); socket.once('error', reject); });
|
||||
|
||||
try {
|
||||
const config = await json(`${adminUrl}/config/add?env=DEV`, {
|
||||
method: 'POST', headers: adminHeaders,
|
||||
body: JSON.stringify({ appId, key: 'cluster.reload', value: 'ok', group: '', description: 'E2E cluster' }),
|
||||
});
|
||||
if (!config.success) throw new Error(`Configuration creation failed: ${config.message ?? 'unknown error'}.`);
|
||||
|
||||
const reload = new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error('Timed out waiting for cross-node reload.')), 30_000);
|
||||
socket.once('message', (data) => { clearTimeout(timeout); resolve(JSON.parse(data.toString())); });
|
||||
});
|
||||
const publish = await json(`${adminUrl}/config/publish?env=DEV`, {
|
||||
method: 'POST', headers: adminHeaders,
|
||||
body: JSON.stringify({ appId, ids: [], log: 'E2E cluster publish' }),
|
||||
});
|
||||
if (!publish.success) throw new Error(`Configuration publish failed: ${publish.message ?? 'unknown error'}.`);
|
||||
const notification = await reload;
|
||||
if (notification.Module !== 'c' || notification.Action !== 'reload') {
|
||||
throw new Error(`Unexpected WebSocket notification: ${JSON.stringify(notification)}.`);
|
||||
}
|
||||
|
||||
const published = await json(`${nodeUrl}/api/v2/applications/${appId}/environments/DEV/published-configurations`, {
|
||||
headers: { Authorization: `Basic ${Buffer.from(`${appId}:${secret}`).toString('base64')}` },
|
||||
});
|
||||
if (!published.some((item) => item.key === 'cluster.reload' && item.value === 'ok')) {
|
||||
throw new Error('Published configuration was not available from node2.');
|
||||
}
|
||||
} finally {
|
||||
socket.close();
|
||||
}
|
||||
} finally {
|
||||
compose(['down', '--volumes', '--remove-orphans']);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => { console.error(error); process.exitCode = 1; });
|
||||
@@ -0,0 +1,64 @@
|
||||
const { execFileSync, spawn } = require('child_process');
|
||||
const { cpSync, existsSync, mkdtempSync, mkdirSync } = require('fs');
|
||||
const { tmpdir } = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const frontendRoot = path.resolve(__dirname, '..', '..');
|
||||
const repositoryRoot = path.resolve(frontendRoot, '..', '..', '..');
|
||||
const apiProject = path.join(repositoryRoot, 'src', 'AgileConfig.Server.Apisite');
|
||||
const apiOutput = path.join(apiProject, 'bin', 'Release', 'net10.0');
|
||||
const uiSource = path.join(frontendRoot, 'dist');
|
||||
const uiDestination = path.join(apiOutput, 'wwwroot', 'ui');
|
||||
const runtimeDirectory = mkdtempSync(path.join(tmpdir(), 'agileconfig-e2e-'));
|
||||
const databasePath = path.join(runtimeDirectory, 'agileconfig.db');
|
||||
|
||||
function run(command, args, cwd) {
|
||||
execFileSync(command, args, { cwd, stdio: 'inherit', shell: process.platform === 'win32' });
|
||||
}
|
||||
|
||||
if (!existsSync(uiSource)) {
|
||||
run('npm', ['run', 'build'], frontendRoot);
|
||||
}
|
||||
run('dotnet', ['build', apiProject, '-c', 'Release', '--nologo'], repositoryRoot);
|
||||
|
||||
if (!existsSync(uiSource)) {
|
||||
throw new Error(`React build output was not found at ${uiSource}.`);
|
||||
}
|
||||
|
||||
mkdirSync(path.dirname(uiDestination), { recursive: true });
|
||||
cpSync(uiSource, uiDestination, { recursive: true, force: true });
|
||||
|
||||
const environment = {
|
||||
...process.env,
|
||||
ASPNETCORE_ENVIRONMENT: 'Production',
|
||||
urls: 'http://127.0.0.1:5187',
|
||||
adminConsole: 'true',
|
||||
saPassword: 'e2e-admin-password',
|
||||
defaultApp: '',
|
||||
cluster: 'false',
|
||||
preview_mode: 'false',
|
||||
alwaysTrustSsl: 'false',
|
||||
serviceHealthCheckInterval: '60',
|
||||
serviceUnhealthInterval: '60',
|
||||
removeServiceInterval: '0',
|
||||
db__provider: 'sqlite',
|
||||
db__conn: `Data Source=${databasePath}`,
|
||||
otlp__logs__endpoint: '',
|
||||
otlp__traces__endpoint: '',
|
||||
otlp__metrics__endpoint: '',
|
||||
SSO__enabled: 'false',
|
||||
};
|
||||
|
||||
const server = spawn('dotnet', ['run', '--no-build', '-c', 'Release', '--project', apiProject], {
|
||||
cwd: repositoryRoot,
|
||||
env: environment,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
function stopServer() {
|
||||
if (!server.killed) server.kill();
|
||||
}
|
||||
|
||||
process.once('SIGINT', stopServer);
|
||||
process.once('SIGTERM', stopServer);
|
||||
server.once('exit', (code) => process.exit(code ?? 0));
|
||||
@@ -0,0 +1,51 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test('an administrator can publish a configuration from the hosted management UI', async ({ page, request }) => {
|
||||
const suffix = `${Date.now()}-${test.info().parallelIndex}`;
|
||||
const appId = `e2e-ui-${suffix}`;
|
||||
const appName = `E2E UI ${suffix}`;
|
||||
const key = `e2e.key.${suffix}`;
|
||||
const value = `value-${suffix}`;
|
||||
|
||||
await page.goto('/ui#/user/login');
|
||||
await page.getByRole('textbox', { name: 'Username', exact: true }).fill('admin');
|
||||
await page.getByRole('textbox', { name: 'Password', exact: true }).fill('e2e-admin-password');
|
||||
await page.getByRole('button', { name: 'Login', exact: true }).click();
|
||||
await expect(page).toHaveURL(/#\/home$/);
|
||||
await page.getByRole('button', { name: 'Add Now', exact: true }).click();
|
||||
|
||||
await page.goto('/ui#/app');
|
||||
await page.getByRole('button', { name: /Create$/ }).click();
|
||||
const applicationDialog = page.getByRole('dialog');
|
||||
await applicationDialog.getByLabel('Name').fill(appName);
|
||||
await applicationDialog.getByLabel('AppID').fill(appId);
|
||||
await applicationDialog.getByLabel('Secret').fill('e2e-app-secret');
|
||||
await applicationDialog.getByRole('button', { name: 'Submit', exact: true }).click();
|
||||
await expect(page.getByText(appId, { exact: true })).toBeVisible();
|
||||
|
||||
await page.goto(`/ui#/app/config/${appId}/${encodeURIComponent(appName)}`);
|
||||
await page.getByRole('button', { name: /Add$/ }).click();
|
||||
const configurationDialog = page.getByRole('dialog');
|
||||
await configurationDialog.getByLabel('Key').fill(key);
|
||||
await configurationDialog.getByLabel('Value').fill(value);
|
||||
await configurationDialog.getByRole('button', { name: 'Submit', exact: true }).click();
|
||||
await expect(page.getByText(key, { exact: true })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: /Publish All$/ }).click();
|
||||
const publishDialog = page.getByRole('dialog');
|
||||
await publishDialog.getByRole('button', { name: 'OK', exact: true }).click();
|
||||
await expect(page.getByText('Publish Success!', { exact: true })).toBeVisible();
|
||||
|
||||
const published = await request.get(
|
||||
`/api/v2/applications/${appId}/environments/DEV/published-configurations`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from(`${appId}:e2e-app-secret`).toString('base64')}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
await expect(published).toBeOK();
|
||||
await expect(published.json()).resolves.toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({ key, value })]),
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import { expect, test, type APIRequestContext } from '@playwright/test';
|
||||
|
||||
const baseUrl = 'http://127.0.0.1:5187';
|
||||
const adminCredentials = { userName: 'admin', password: 'e2e-admin-password' };
|
||||
|
||||
async function login(request: APIRequestContext, credentials: { userName: string; password: string }) {
|
||||
const response = await request.post(`${baseUrl}/admin/jwt/login`, { data: credentials });
|
||||
await expect(response).toBeOK();
|
||||
const body = await response.json();
|
||||
expect(body.status).toBe('ok');
|
||||
return body.token as string;
|
||||
}
|
||||
|
||||
test('a read-only user cannot create applications through the UI or API', async ({ page, request }) => {
|
||||
const suffix = `${Date.now()}-${test.info().parallelIndex}`;
|
||||
const roleId = `e2e-read-role-${suffix}`;
|
||||
const userName = `e2e-read-user-${suffix}`;
|
||||
const password = 'e2e-read-password';
|
||||
const adminToken = await login(request, adminCredentials);
|
||||
const adminAuthorization = { Authorization: `Bearer ${adminToken}` };
|
||||
|
||||
const createRole = await request.post(`${baseUrl}/role/add`, {
|
||||
headers: adminAuthorization,
|
||||
data: { id: roleId, name: `E2E Read Role ${suffix}`, description: 'Read-only E2E role', functions: ['APP_READ'] },
|
||||
});
|
||||
await expect(createRole).toBeOK();
|
||||
expect((await createRole.json()).success).toBe(true);
|
||||
|
||||
const createUser = await request.post(`${baseUrl}/user/add`, {
|
||||
headers: adminAuthorization,
|
||||
data: { userName, password, team: 'E2E', userRoleIds: [roleId] },
|
||||
});
|
||||
await expect(createUser).toBeOK();
|
||||
expect((await createUser.json()).success).toBe(true);
|
||||
|
||||
await page.goto('/ui#/user/login');
|
||||
await page.getByRole('textbox', { name: 'Username', exact: true }).fill(userName);
|
||||
await page.getByRole('textbox', { name: 'Password', exact: true }).fill(password);
|
||||
await page.getByRole('button', { name: 'Login', exact: true }).click();
|
||||
await expect(page).toHaveURL(/#\/home$/);
|
||||
|
||||
await page.goto('/ui#/app');
|
||||
await expect(page.getByRole('button', { name: /Create$/ })).toHaveCount(0);
|
||||
|
||||
const userToken = await page.evaluate(() => localStorage.getItem('token'));
|
||||
const rejectedCreate = await request.post(`${baseUrl}/app/add`, {
|
||||
headers: { Authorization: `Bearer ${userToken}` },
|
||||
data: { id: `forbidden-${suffix}`, name: 'Forbidden', secret: 'secret', enabled: true },
|
||||
});
|
||||
expect(rejectedCreate.status()).toBe(403);
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import { expect, test, type APIRequestContext } from '@playwright/test';
|
||||
import WebSocket from 'ws';
|
||||
|
||||
const baseUrl = 'http://127.0.0.1:5187';
|
||||
|
||||
async function login(request: APIRequestContext) {
|
||||
const response = await request.post(`${baseUrl}/admin/jwt/login`, {
|
||||
data: { userName: 'admin', password: 'e2e-admin-password' },
|
||||
});
|
||||
await expect(response).toBeOK();
|
||||
return (await response.json()).token as string;
|
||||
}
|
||||
|
||||
test('publishing a configuration notifies its connected WebSocket client', async ({ request }) => {
|
||||
const suffix = `${Date.now()}-${test.info().parallelIndex}`;
|
||||
const appId = `e2e-ws-${suffix}`;
|
||||
const secret = 'e2e-ws-secret';
|
||||
const token = await login(request);
|
||||
const authorization = { Authorization: `Bearer ${token}` };
|
||||
|
||||
const addApp = await request.post(`${baseUrl}/app/add`, {
|
||||
headers: authorization,
|
||||
data: { id: appId, name: appId, secret, enabled: true, inheritanced: true },
|
||||
});
|
||||
await expect(addApp).toBeOK();
|
||||
|
||||
// Publish events are forwarded through registered nodes, including this local test node.
|
||||
const addLocalNode = await request.post(`${baseUrl}/ServerNode/Add`, {
|
||||
headers: authorization,
|
||||
data: { address: baseUrl, remark: 'E2E local node' },
|
||||
});
|
||||
await expect(addLocalNode).toBeOK();
|
||||
|
||||
const socket = new WebSocket(baseUrl.replace('http', 'ws') + '/ws?client_name=e2e', {
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from(`${appId}:${secret}`).toString('base64')}`,
|
||||
appid: appId,
|
||||
env: 'DEV',
|
||||
'client-v': '1.8.0',
|
||||
},
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
socket.once('open', resolve);
|
||||
socket.once('error', reject);
|
||||
});
|
||||
|
||||
try {
|
||||
const addConfig = await request.post(`${baseUrl}/config/add?env=DEV`, {
|
||||
headers: authorization,
|
||||
data: { appId, key: 'reload-key', value: 'reload-value', group: '', description: 'E2E' },
|
||||
});
|
||||
await expect(addConfig).toBeOK();
|
||||
|
||||
const reload = new Promise<string>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error('Timed out waiting for reload notification.')), 15_000);
|
||||
socket.once('message', (data) => {
|
||||
clearTimeout(timeout);
|
||||
resolve(data.toString());
|
||||
});
|
||||
});
|
||||
|
||||
const publish = await request.post(`${baseUrl}/config/publish?env=DEV`, {
|
||||
headers: authorization,
|
||||
data: { appId, ids: [], log: 'E2E publish' },
|
||||
});
|
||||
await expect(publish).toBeOK();
|
||||
expect(JSON.parse(await reload)).toEqual(expect.objectContaining({ Module: 'c', Action: 'reload' }));
|
||||
} finally {
|
||||
socket.close();
|
||||
}
|
||||
});
|
||||
+5070
-1268
File diff suppressed because it is too large
Load Diff
@@ -32,6 +32,11 @@
|
||||
"test": "max test",
|
||||
"test:all": "node ./tests/run-tests.js",
|
||||
"test:component": "max test ./src/components",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test e2e/specs/configuration-lifecycle.spec.ts",
|
||||
"test:e2e:protocol": "playwright test e2e/specs/websocket-publish.spec.ts",
|
||||
"test:e2e:permissions": "playwright test e2e/specs/permissions.spec.ts",
|
||||
"test:e2e:docker": "node e2e/docker/run-cluster.js",
|
||||
"tsc": "tsc --noEmit"
|
||||
},
|
||||
"lint-staged": {
|
||||
@@ -66,6 +71,7 @@
|
||||
"umi-request": "^1.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.62.1",
|
||||
"@types/classnames": "^2.2.7",
|
||||
"@types/express": "^4.17.0",
|
||||
"@types/history": "^4.7.2",
|
||||
@@ -75,6 +81,7 @@
|
||||
"@types/react": "^18.3.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-helmet": "^6.1.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@umijs/fabric": "^2.5.1",
|
||||
"@umijs/utils": "4.7.5",
|
||||
"carlo": "^0.9.46",
|
||||
@@ -92,7 +99,8 @@
|
||||
"prettier": "^2.0.1",
|
||||
"puppeteer-core": "^5.0.0",
|
||||
"stylelint": "^13.0.0",
|
||||
"typescript": "^5.9.3"
|
||||
"typescript": "^5.9.3",
|
||||
"ws": "^8.21.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e/specs',
|
||||
fullyParallel: false,
|
||||
workers: 1,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
reporter: process.env.CI ? [['github'], ['html', { open: 'never' }]] : 'list',
|
||||
use: {
|
||||
baseURL: 'http://127.0.0.1:5187',
|
||||
locale: 'en-US',
|
||||
trace: 'retain-on-failure',
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
webServer: {
|
||||
command: 'node e2e/scripts/start-server.js',
|
||||
url: 'http://127.0.0.1:5187/ui',
|
||||
timeout: 240_000,
|
||||
reuseExistingServer: false,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user