Skip to content

Commit ae3d467

Browse files
authored
fix: add Playwright UI smoke and worker-node deploy/repair regression tests (#86)
1 parent 969de87 commit ae3d467

9 files changed

Lines changed: 731 additions & 30 deletions

File tree

.github/workflows/build.yml

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ jobs:
5050
permissions:
5151
contents: read
5252
actions: write
53-
timeout-minutes: 20
53+
timeout-minutes: 30
5454
services:
5555
mysql:
5656
image: mysql:8
@@ -74,6 +74,8 @@ jobs:
7474
E2E_TEST_TOKEN: casos-e2e-${{ github.run_id }}-${{ github.run_attempt }}
7575
steps:
7676
- uses: actions/checkout@v4
77+
with:
78+
fetch-depth: 0
7779
- uses: actions/setup-go@v4
7880
with:
7981
go-version: ${{ env.GO_VERSION }}
@@ -85,14 +87,57 @@ jobs:
8587
cache-dependency-path: ./web/yarn.lock
8688
- run: yarn install --frozen-lockfile
8789
working-directory: ./web
90+
- run: yarn run ui:test:selector:check
91+
working-directory: ./web
8892
- run: yarn playwright install --with-deps chromium
8993
working-directory: ./web
90-
- name: Run UI tests
94+
- name: Run UI smoke tests
95+
working-directory: ./web
96+
shell: bash
97+
run: |
98+
set -o pipefail
99+
yarn run ui:test:smoke 2>&1 | tee ui-smoke-test.log
100+
- name: Select UI regression tests
101+
id: ui-regression-tests
102+
shell: bash
103+
env:
104+
BASE_REF: ${{ github.base_ref }}
105+
EVENT_NAME: ${{ github.event_name }}
106+
run: |
107+
set -euo pipefail
108+
if [[ "$EVENT_NAME" == "pull_request" ]]; then
109+
git fetch --no-tags --prune origin "+refs/heads/$BASE_REF:refs/remotes/origin/$BASE_REF"
110+
git diff --name-only "origin/$BASE_REF"...HEAD > web/ui-changed-files.txt
111+
elif git rev-parse HEAD^ >/dev/null 2>&1; then
112+
git diff --name-only HEAD^ HEAD > web/ui-changed-files.txt
113+
else
114+
git diff-tree --no-commit-id --name-only -r HEAD > web/ui-changed-files.txt
115+
fi
116+
node web/scripts/select-ui-tests.js web/ui-changed-files.txt > web/ui-regression-tests.txt
117+
echo "Changed files considered for UI regression:"
118+
cat web/ui-changed-files.txt
119+
echo "Selected UI regression tests:"
120+
if [[ -s web/ui-regression-tests.txt ]]; then
121+
cat web/ui-regression-tests.txt
122+
echo "has_tests=true" >> "$GITHUB_OUTPUT"
123+
else
124+
echo "(none; fixed smoke already ran)"
125+
echo "has_tests=false" >> "$GITHUB_OUTPUT"
126+
fi
127+
- name: Run UI regression tests
128+
if: steps.ui-regression-tests.outputs.has_tests == 'true'
91129
working-directory: ./web
92130
shell: bash
93131
run: |
94132
set -o pipefail
95-
yarn run ui:test 2>&1 | tee ui-test.log
133+
mapfile -t ui_test_files < ui-regression-tests.txt
134+
yarn run ui:test:regression -- "${ui_test_files[@]}" 2>&1 | tee ui-test.log
135+
- name: Skip UI regression tests
136+
if: steps.ui-regression-tests.outputs.has_tests != 'true'
137+
working-directory: ./web
138+
shell: bash
139+
run: |
140+
echo "No changed-path UI regression tests selected; fixed smoke already ran." | tee ui-test.log
96141
- name: Upload Playwright artifacts
97142
if: always()
98143
uses: actions/upload-artifact@v4
@@ -101,6 +146,9 @@ jobs:
101146
path: |
102147
./web/playwright-report
103148
./web/test-results
149+
./web/ui-changed-files.txt
150+
./web/ui-regression-tests.txt
151+
./web/ui-smoke-test.log
104152
./web/ui-test.log
105153
if-no-files-found: ignore
106154

web/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
"start": "cross-env PORT=8001 craco start",
2626
"build": "craco build",
2727
"ui:test": "playwright test",
28+
"ui:test:selector:check": "node scripts/select-ui-tests-check.js",
29+
"ui:test:smoke": "playwright test --grep @smoke",
30+
"ui:test:regression": "playwright test --grep-invert @smoke",
2831
"ui:test:headed": "playwright test --headed",
2932
"fix": "eslint --fix src/ --ext .js",
3033
"lint:js": "eslint --fix src/ --ext .js",

web/playwright.config.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ const backendURL = `http://127.0.0.1:${backendPort}`;
1010
const e2eToken = process.env.E2E_TEST_TOKEN || randomUUID();
1111
const e2eDataDir = process.env.E2E_DATA_DIR || path.join(os.tmpdir(), `casos-e2e-${process.pid}`);
1212
const backendDir = path.resolve(__dirname, "..");
13+
const browserChannel = process.env.E2E_BROWSER_CHANNEL;
14+
const videoMode = process.env.E2E_DISABLE_VIDEO ? "off" : "retain-on-failure";
1315

1416
process.env.E2E_TEST_TOKEN = e2eToken;
1517

@@ -27,12 +29,15 @@ module.exports = defineConfig({
2729
baseURL,
2830
screenshot: "only-on-failure",
2931
trace: "retain-on-failure",
30-
video: "retain-on-failure",
32+
video: videoMode,
3133
},
3234
projects: [
3335
{
3436
name: "chromium",
35-
use: {...devices["Desktop Chrome"]},
37+
use: {
38+
...devices["Desktop Chrome"],
39+
...(browserChannel ? {channel: browserChannel} : {}),
40+
},
3641
},
3742
],
3843
webServer: [
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
const assert = require("assert");
2+
const {execFileSync} = require("child_process");
3+
const fs = require("fs");
4+
const path = require("path");
5+
const {ALL_REGRESSION_TESTS, selectRegressionTests} = require("./select-ui-tests");
6+
7+
function expectSelection(name, changedFiles, expectedTests) {
8+
assert.deepStrictEqual(selectRegressionTests(changedFiles), expectedTests, name);
9+
}
10+
11+
expectSelection(
12+
"worker node UI changes select worker node regression",
13+
["web/src/MachineNodeDeployPanel.js"],
14+
["tests/ui/worker-node.spec.js"]
15+
);
16+
17+
expectSelection(
18+
"worker node backend changes select worker node regression once",
19+
["controllers/machine.go", "object/machine_node_deploy.go", "web/src/MachineListPage.js"],
20+
["tests/ui/worker-node.spec.js"]
21+
);
22+
23+
expectSelection(
24+
"site changes rely on fixed smoke coverage",
25+
["web/src/SiteEditPage.js"],
26+
[]
27+
);
28+
29+
expectSelection(
30+
"site list and backend changes select site regression",
31+
["web/src/SiteListPage.js", "web/src/backend/SiteBackend.js", "object/site.go"],
32+
["tests/ui/site-e2e.spec.js"]
33+
);
34+
35+
expectSelection(
36+
"docs-only changes do not request extra regression tests",
37+
["README.md", "docs/ci.md"],
38+
[]
39+
);
40+
41+
expectSelection(
42+
"UI test infrastructure changes run all regression tests",
43+
["web/tests/ui/e2e-helpers.js"],
44+
ALL_REGRESSION_TESTS
45+
);
46+
47+
expectSelection(
48+
"unknown frontend code changes run all regression tests",
49+
["web/src/DeploymentListPage.js"],
50+
ALL_REGRESSION_TESTS
51+
);
52+
53+
expectSelection(
54+
"UI selector script changes run all regression tests",
55+
["web/scripts/select-ui-tests.js"],
56+
ALL_REGRESSION_TESTS
57+
);
58+
59+
expectSelection(
60+
"non-array inputs fall back to all regression tests",
61+
null,
62+
ALL_REGRESSION_TESTS
63+
);
64+
65+
const cliInputPath = path.join(__dirname, `.select-ui-tests-${process.pid}.txt`);
66+
fs.writeFileSync(cliInputPath, "web/src/MachineNodeDeployPanel.js\n", "utf8");
67+
try {
68+
const output = execFileSync(process.execPath, [path.join(__dirname, "select-ui-tests.js"), cliInputPath], {
69+
encoding: "utf8",
70+
});
71+
assert.strictEqual(output, "tests/ui/worker-node.spec.js\n", "CLI prints selected regression tests");
72+
} finally {
73+
fs.rmSync(cliInputPath, {force: true});
74+
}

web/scripts/select-ui-tests.js

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
const fs = require("fs");
2+
const path = require("path");
3+
4+
// Registry of non-smoke regression specs selectable from changed paths.
5+
const ALL_REGRESSION_TESTS = [
6+
"tests/ui/site-e2e.spec.js",
7+
"tests/ui/worker-node.spec.js",
8+
];
9+
10+
const WORKER_NODE_PATTERNS = [
11+
/^controllers\/machine(_node_deploy)?\.go$/,
12+
/^object\/machine(_node_deploy)?\.go$/,
13+
/^web\/src\/Machine(ListPage|EditPage|NodeDeployPanel)\.js$/,
14+
/^web\/src\/backend\/Machine(NodeDeploy)?Backend\.js$/,
15+
/^web\/tests\/ui\/worker-node\.spec\.js$/,
16+
];
17+
18+
const SMOKE_COVERED_PATTERNS = [
19+
/^web\/src\/SiteEditPage\.js$/,
20+
];
21+
22+
const SITE_PATTERNS = [
23+
/^controllers\/site\.go$/,
24+
/^object\/site\.go$/,
25+
/^web\/src\/SiteListPage\.js$/,
26+
/^web\/src\/backend\/SiteBackend\.js$/,
27+
/^web\/tests\/ui\/site-e2e\.spec\.js$/,
28+
];
29+
30+
const FULL_REGRESSION_PATTERNS = [
31+
/^\.github\/workflows\//,
32+
/^conf\/app\.conf$/,
33+
/^routers\/router\.go$/,
34+
/^web\/package\.json$/,
35+
/^web\/playwright\.config\.js$/,
36+
/^web\/src\/(Conf|Setting)\.js$/,
37+
/^web\/src\/locales\//,
38+
/^web\/tests\/ui\/e2e-helpers\.js$/,
39+
/^web\/yarn\.lock$/,
40+
];
41+
42+
const DOCS_ONLY_PATTERNS = [
43+
/(^|\/)(README|CHANGELOG|LICENSE)(\.[^/]*)?$/i,
44+
/\.md$/i,
45+
/^docs\//,
46+
];
47+
48+
const CODE_ROOT_PATTERNS = [
49+
/^conf\//,
50+
/^controllers\//,
51+
/^main\.go$/,
52+
/^object\//,
53+
/^proxy\//,
54+
/^routers\//,
55+
/^web\/scripts\//,
56+
/^web\/src\//,
57+
];
58+
59+
function normalizeChangedPath(filePath) {
60+
return String(filePath || "")
61+
.trim()
62+
.replace(/\\/g, "/")
63+
.replace(/^\.\//, "");
64+
}
65+
66+
function matchesAny(filePath, patterns) {
67+
return patterns.some(pattern => pattern.test(filePath));
68+
}
69+
70+
function isCodePath(filePath) {
71+
return matchesAny(filePath, CODE_ROOT_PATTERNS);
72+
}
73+
74+
function normalizeChangedFiles(changedFiles) {
75+
if (!Array.isArray(changedFiles)) {
76+
return [];
77+
}
78+
return Array.from(new Set(
79+
changedFiles.map(normalizeChangedPath).filter(Boolean)
80+
));
81+
}
82+
83+
function selectRegressionTestsFromNormalized(normalizedFiles) {
84+
if (normalizedFiles.length === 0) {
85+
return [...ALL_REGRESSION_TESTS];
86+
}
87+
88+
const selectedTests = new Set();
89+
let runAllRegression = false;
90+
91+
// Ordering matters: skip docs, honor all-regression triggers, then apply targeted and smoke-covered matches.
92+
for (const filePath of normalizedFiles) {
93+
if (matchesAny(filePath, DOCS_ONLY_PATTERNS)) {
94+
continue;
95+
}
96+
if (matchesAny(filePath, FULL_REGRESSION_PATTERNS)) {
97+
runAllRegression = true;
98+
continue;
99+
}
100+
if (matchesAny(filePath, WORKER_NODE_PATTERNS)) {
101+
selectedTests.add("tests/ui/worker-node.spec.js");
102+
continue;
103+
}
104+
if (matchesAny(filePath, SITE_PATTERNS)) {
105+
selectedTests.add("tests/ui/site-e2e.spec.js");
106+
continue;
107+
}
108+
if (matchesAny(filePath, SMOKE_COVERED_PATTERNS)) {
109+
continue;
110+
}
111+
if (isCodePath(filePath)) {
112+
runAllRegression = true;
113+
}
114+
}
115+
116+
if (runAllRegression) {
117+
return [...ALL_REGRESSION_TESTS];
118+
}
119+
120+
return ALL_REGRESSION_TESTS.filter(testFile => selectedTests.has(testFile));
121+
}
122+
123+
// Selects non-smoke UI regression specs for repository-relative changed paths.
124+
function selectRegressionTests(changedFiles) {
125+
return selectRegressionTestsFromNormalized(normalizeChangedFiles(changedFiles));
126+
}
127+
128+
function main(argv) {
129+
const changedFilesPath = argv[2];
130+
if (!changedFilesPath) {
131+
process.stderr.write("Usage: node scripts/select-ui-tests.js <changed-files.txt>\n");
132+
process.exitCode = 1;
133+
return;
134+
}
135+
136+
const repoRoot = path.resolve(__dirname, "..", "..");
137+
let resolvedChangedFilesPath;
138+
try {
139+
resolvedChangedFilesPath = fs.realpathSync(path.resolve(changedFilesPath));
140+
} catch (error) {
141+
process.stderr.write(`Error reading changed files list: ${error.message}\n`);
142+
process.exitCode = 1;
143+
return;
144+
}
145+
146+
const changedFilesPathRelative = path.relative(repoRoot, resolvedChangedFilesPath);
147+
if (changedFilesPathRelative.startsWith("..")) {
148+
process.stderr.write(`Error: changed files path is outside the repository: ${changedFilesPath}\n`);
149+
process.exitCode = 1;
150+
return;
151+
}
152+
153+
let rawChangedFiles;
154+
try {
155+
rawChangedFiles = fs.readFileSync(resolvedChangedFilesPath, "utf8");
156+
} catch (error) {
157+
process.stderr.write(`Error reading changed files list: ${error.message}\n`);
158+
process.exitCode = 1;
159+
return;
160+
}
161+
162+
const changedFiles = rawChangedFiles.split(/\r?\n/);
163+
const normalizedFiles = normalizeChangedFiles(changedFiles);
164+
if (normalizedFiles.length === 0) {
165+
process.stderr.write("Warning: no changed files detected; falling back to all regression tests.\n");
166+
}
167+
const tests = selectRegressionTestsFromNormalized(normalizedFiles);
168+
process.stdout.write(tests.length > 0 ? `${tests.join("\n")}\n` : "");
169+
}
170+
171+
if (require.main === module) {
172+
main(process.argv);
173+
}
174+
175+
module.exports = {
176+
ALL_REGRESSION_TESTS,
177+
selectRegressionTests,
178+
};

web/src/MachineListPage.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ class MachineListPage extends React.Component {
119119
}
120120

121121
render() {
122+
const deployWorkerNodeLabel = i18next.t("machine:Deploy worker node", {defaultValue: "Deploy worker node"});
122123
const columns = [
123124
{
124125
title: i18next.t("general:Name"),
@@ -172,8 +173,8 @@ class MachineListPage extends React.Component {
172173
fixed: "right",
173174
render: (text, record) => (
174175
<div style={{display: "flex", alignItems: "center", gap: "2px"}}>
175-
<Tooltip title={i18next.t("machine:Deploy worker node", "Deploy worker node")}>
176-
<Button type="text" size="small" icon={<CloudSyncOutlined />} style={{width: "28px", height: "28px", padding: 0, borderRadius: "6px"}} onClick={() => this.openDeployPanel(record)} />
176+
<Tooltip title={deployWorkerNodeLabel}>
177+
<Button aria-label={deployWorkerNodeLabel} type="text" size="small" icon={<CloudSyncOutlined />} style={{width: "28px", height: "28px", padding: 0, borderRadius: "6px"}} onClick={() => this.openDeployPanel(record)} />
177178
</Tooltip>
178179
<Tooltip title={i18next.t("general:Edit")}>
179180
<Button type="text" size="small" icon={<EditOutlined />} style={{width: "28px", height: "28px", padding: 0, borderRadius: "6px"}} onClick={() => this.props.history.push(`/machines/${record.name}`)} />

0 commit comments

Comments
 (0)