Skip to content

Commit 63f2ec5

Browse files
committed
Add E2E tests for Workloads/Cronjob in the dashboard
Signed-off-by: SunsetB612 <[email protected]>
1 parent aa6df58 commit 63f2ec5

File tree

6 files changed

+778
-0
lines changed

6 files changed

+778
-0
lines changed
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/*
2+
Copyright 2025 The Karmada Authors.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
import { test, expect } from '@playwright/test';
15+
import { setupDashboardAuthentication, generateTestCronJobYaml, deleteK8sCronJob } from './test-utils';
16+
import { parse } from 'yaml';
17+
import _ from 'lodash';
18+
19+
test.beforeEach(async ({ page }) => {
20+
await setupDashboardAuthentication(page);
21+
});
22+
23+
test('should create a new cronjob', async ({ page }) => {
24+
// Navigate to workload menu
25+
await page.click('text=Workloads');
26+
27+
// Click visible Cronjob tab
28+
const cronjobTab = page.locator('role=option[name="Cronjob"]');
29+
await cronjobTab.waitFor({ state: 'visible', timeout: 30000 });
30+
await cronjobTab.click();
31+
32+
// Verify selected state
33+
await expect(cronjobTab).toHaveAttribute('aria-selected', 'true');
34+
await expect(page.locator('table')).toBeVisible({ timeout: 30000 });
35+
await page.click('button:has-text("Add")');
36+
await page.waitForSelector('[role="dialog"]', { timeout: 10000 });
37+
38+
// Listen for API calls
39+
const apiRequestPromise = page.waitForResponse(response => {
40+
return response.url().includes('/api/v1/_raw/CronJob') && response.status() === 200;
41+
}, { timeout: 15000 });
42+
43+
const testCronJobYaml = generateTestCronJobYaml();
44+
45+
// Set Monaco editor DOM content
46+
await page.evaluate((yaml) => {
47+
const textarea = document.querySelector('.monaco-editor textarea') as HTMLTextAreaElement;
48+
if (textarea) {
49+
textarea.value = yaml;
50+
textarea.focus();
51+
}
52+
}, testCronJobYaml);
53+
54+
// Call React onChange callback to update component state
55+
await page.evaluate((yaml) => {
56+
57+
const findReactFiber = (element: any) => {
58+
const keys = Object.keys(element);
59+
return keys.find(key => key.startsWith('__reactFiber') || key.startsWith('__reactInternalInstance'));
60+
};
61+
62+
const monacoContainer = document.querySelector('.monaco-editor');
63+
if (monacoContainer) {
64+
const fiberKey = findReactFiber(monacoContainer);
65+
if (fiberKey) {
66+
let fiber = (monacoContainer as any)[fiberKey];
67+
68+
while (fiber) {
69+
if (fiber.memoizedProps && fiber.memoizedProps.onChange) {
70+
fiber.memoizedProps.onChange(yaml);
71+
return;
72+
}
73+
fiber = fiber.return;
74+
}
75+
}
76+
}
77+
78+
const dialog = document.querySelector('[role="dialog"]');
79+
if (dialog) {
80+
const fiberKey = findReactFiber(dialog);
81+
if (fiberKey) {
82+
let fiber = (dialog as any)[fiberKey];
83+
84+
const traverse = (node: any, depth = 0) => {
85+
if (!node || depth > 20) return false;
86+
87+
if (node.memoizedProps && node.memoizedProps.onChange) {
88+
node.memoizedProps.onChange(yaml);
89+
return true;
90+
}
91+
92+
if (node.child && traverse(node.child, depth + 1)) return true;
93+
if (node.sibling && traverse(node.sibling, depth + 1)) return true;
94+
95+
return false;
96+
};
97+
98+
traverse(fiber);
99+
}
100+
}
101+
}, testCronJobYaml);
102+
103+
// Wait for submit button to become enabled
104+
await expect(page.locator('[role="dialog"] button:has-text("Submit")')).toBeEnabled();
105+
await page.click('[role="dialog"] button:has-text("Submit")');
106+
107+
// Wait for API call to succeed
108+
await apiRequestPromise;
109+
110+
// Wait for dialog to close
111+
await page.waitForSelector('[role="dialog"]', { state: 'detached', timeout: 5000 }).catch(() => {
112+
// Dialog may already be closed
113+
});
114+
115+
// Verify new cronjob appears in list
116+
const yamlObject = parse(testCronJobYaml);
117+
const cronJobName = _.get(yamlObject,'metadata.name');
118+
119+
// Assert cronjob name exists
120+
expect(cronJobName).toBeTruthy();
121+
expect(cronJobName).toBeDefined();
122+
123+
try {
124+
await expect(page.locator('table').locator(`text=${cronJobName}`)).toBeVisible({
125+
timeout: 15000
126+
});
127+
} catch {
128+
// If not shown immediately in list, may be due to cache or refresh delay
129+
// But API success indicates cronjob was created
130+
}
131+
132+
// Cleanup: Delete the created cronjob
133+
try {
134+
await deleteK8sCronJob(cronJobName, 'default');
135+
} catch (error) {
136+
console.warn(`Failed to cleanup cronjob ${cronJobName}:`, error);
137+
}
138+
139+
// Debug
140+
if(process.env.DEBUG === 'true'){
141+
await page.screenshot({ path: 'debug-cronjob-create.png', fullPage: true });
142+
}
143+
144+
});
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
Copyright 2025 The Karmada Authors.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
import { test, expect } from '@playwright/test';
15+
import { setupDashboardAuthentication, generateTestCronJobYaml, createK8sCronJob, getCronJobNameFromYaml} from './test-utils';
16+
17+
test.beforeEach(async ({ page }) => {
18+
await setupDashboardAuthentication(page);
19+
});
20+
21+
test('should delete cronjob successfully', async ({ page }) => {
22+
// Create a test cronjob directly via kubectl to set up test data
23+
const testCronJobYaml = generateTestCronJobYaml();
24+
const cronJobName = getCronJobNameFromYaml(testCronJobYaml);
25+
26+
// Setup: Create cronjob using kubectl
27+
await createK8sCronJob(testCronJobYaml);
28+
29+
// Navigate to workload page
30+
await page.click('text=Workloads');
31+
32+
// Click visible Cronjob tab
33+
const cronjobTab = page.locator('role=option[name="Cronjob"]');
34+
await cronjobTab.waitFor({ state: 'visible', timeout: 30000 });
35+
await cronjobTab.click();
36+
37+
// Verify selected state
38+
await expect(cronjobTab).toHaveAttribute('aria-selected', 'true');
39+
await expect(page.locator('table')).toBeVisible({ timeout: 30000 });
40+
41+
// Wait for cronjob to appear in list
42+
const table = page.locator('table');
43+
await expect(table.locator(`text=${cronJobName}`)).toBeVisible({ timeout: 30000 });
44+
45+
// Find row containing test cronjob name
46+
const targetRow = page.locator(`table tbody tr:has-text("${cronJobName}")`);
47+
await expect(targetRow).toBeVisible({ timeout: 15000 });
48+
49+
// Find Delete button in that row and click
50+
const deleteButton = targetRow.locator('button[type="button"]').filter({ hasText: /^(Delete)$/ });
51+
await expect(deleteButton).toBeVisible({ timeout: 10000 });
52+
53+
// Listen for delete API call
54+
const deleteApiPromise = page.waitForResponse(response => {
55+
return response.url().includes('/_raw/cronjob') &&
56+
response.url().includes(`name/${cronJobName}`) &&
57+
response.request().method() === 'DELETE' &&
58+
response.status() === 200;
59+
}, { timeout: 15000 });
60+
61+
await deleteButton.click();
62+
63+
// Wait for delete confirmation tooltip to appear
64+
await page.waitForSelector('[role="tooltip"]', { timeout: 10000 });
65+
66+
// Click Confirm button to confirm deletion
67+
const confirmButton = page.locator('[role="tooltip"] button').filter({ hasText: /^(Confirm)$/ });
68+
await expect(confirmButton).toBeVisible({ timeout: 5000 });
69+
await confirmButton.click();
70+
71+
// Wait for delete API call to succeed
72+
await deleteApiPromise;
73+
74+
// Wait for tooltip to close
75+
await page.waitForSelector('[role="tooltip"]', { state: 'detached', timeout: 10000 }).catch(() => {});
76+
77+
// Refresh page to ensure UI is updated after deletion
78+
await page.reload();
79+
await page.click('text=Workloads');
80+
await expect(table).toBeVisible({ timeout: 30000 });
81+
82+
// Verify cronjob no longer exists in table
83+
await expect(table.locator(`text=${cronJobName}`)).toHaveCount(0, { timeout: 30000 });
84+
85+
// Debug
86+
if(process.env.DEBUG === 'true'){
87+
await page.screenshot({ path: 'debug-cronjob-delete-kubectl.png', fullPage: true });
88+
}
89+
});

0 commit comments

Comments
 (0)