Skip to content

Commit db90716

Browse files
committed
Add E2E tests for Propagation Policy/Namespace level in the dashboard
Signed-off-by: SunsetB612 <[email protected]>
1 parent 75a7cca commit db90716

File tree

9 files changed

+1014
-0
lines changed

9 files changed

+1014
-0
lines changed

.github/workflows/ci.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,17 @@ jobs:
187187
echo "Testing API connectivity..."
188188
curl -f http://localhost:8000/api/v1/cluster && echo "✓ Cluster API works" || echo "✗ Cluster API failed"
189189
curl -f http://localhost:8000/api/v1/namespace && echo "✓ Namespace API works" || echo "✗ Namespace API failed"
190+
echo ""
191+
echo "========== DEBUG: Testing PropagationPolicy API =========="
192+
echo "Testing GET /api/v1/propagationpolicy..."
193+
echo "Response:"
194+
curl -v http://localhost:8000/api/v1/propagationpolicy 2>&1 | head -50
195+
echo ""
196+
echo "Testing GET /api/v1/propagationpolicy/default (with namespace)..."
197+
echo "Response:"
198+
curl -v http://localhost:8000/api/v1/propagationpolicy/default 2>&1 | head -50
199+
echo "========== DEBUG: END =========="
200+
echo ""
190201
echo "API tests completed"
191202
- name: Debug working dir
192203
if: ${{ env.DEBUG == 'true' }}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
Copyright 2025 The Karmada Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { test } from '@playwright/test';
18+
import {
19+
setupDashboardAuthentication,
20+
generateTestPropagationPolicyYaml,
21+
deleteK8sPropagationPolicy,
22+
getPropagationPolicyNameFromYaml,
23+
createPropagationPolicyResourceTest
24+
} from './test-utils';
25+
26+
test.beforeEach(async ({ page }) => {
27+
await setupDashboardAuthentication(page);
28+
});
29+
30+
test('should create a new propagationpolicy', async ({ page }) => {
31+
const testPropagationPolicyYaml = generateTestPropagationPolicyYaml();
32+
33+
await createPropagationPolicyResourceTest(page, {
34+
resourceType: 'propagationpolicy',
35+
tabName: 'Namespace level',
36+
apiEndpoint: '/propagationpolicy',
37+
yamlContent: testPropagationPolicyYaml,
38+
getResourceName: getPropagationPolicyNameFromYaml,
39+
deleteResource: deleteK8sPropagationPolicy,
40+
screenshotName: 'debug-propagationpolicy-create.png'
41+
});
42+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
Copyright 2025 The Karmada Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { test } from '@playwright/test';
18+
import {
19+
setupDashboardAuthentication,
20+
generateTestPropagationPolicyYaml,
21+
createK8sPropagationPolicy,
22+
getPropagationPolicyNameFromYaml,
23+
deletePropagationPolicyResourceTest
24+
} from './test-utils';
25+
26+
test.beforeEach(async ({ page }) => {
27+
await setupDashboardAuthentication(page);
28+
});
29+
30+
test('should delete propagationpolicy successfully', async ({ page }) => {
31+
const testPropagationPolicyYaml = generateTestPropagationPolicyYaml();
32+
33+
await deletePropagationPolicyResourceTest(page, {
34+
resourceType: 'propagationpolicy',
35+
tabName: 'Namespace level',
36+
apiEndpointPattern: '/propagationpolicy',
37+
yamlContent: testPropagationPolicyYaml,
38+
getResourceName: getPropagationPolicyNameFromYaml,
39+
createResource: createK8sPropagationPolicy,
40+
screenshotName: 'debug-propagationpolicy-delete-kubectl.png'
41+
});
42+
});
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
/*
2+
Copyright 2025 The Karmada Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { test, expect } from '@playwright/test';
18+
import {
19+
setupDashboardAuthentication,
20+
generateTestPropagationPolicyYaml,
21+
createK8sPropagationPolicy,
22+
getPropagationPolicyNameFromYaml,
23+
deleteK8sPropagationPolicy,
24+
setMonacoEditorContent,
25+
waitForResourceInList,
26+
debugScreenshot,
27+
DeepRequired
28+
} from './test-utils';
29+
import { IResponse } from '@/services/base.ts';
30+
31+
// PropagationPolicy type for K8s API response
32+
interface PropagationPolicy {
33+
apiVersion?: string;
34+
kind?: string;
35+
metadata?: {
36+
name?: string;
37+
namespace?: string;
38+
[key: string]: any;
39+
};
40+
spec?: {
41+
resourceSelectors?: Array<{
42+
apiVersion?: string;
43+
kind?: string;
44+
name?: string;
45+
[key: string]: any;
46+
}>;
47+
placement?: {
48+
clusterAffinity?: {
49+
clusterNames?: string[];
50+
[key: string]: any;
51+
};
52+
[key: string]: any;
53+
};
54+
[key: string]: any;
55+
};
56+
[key: string]: any;
57+
}
58+
59+
test.beforeEach(async ({ page }) => {
60+
await setupDashboardAuthentication(page);
61+
});
62+
63+
test('should edit propagationpolicy successfully', async ({ page }) => {
64+
// Create a test propagationpolicy directly via API to set up test data
65+
const testPropagationPolicyYaml = generateTestPropagationPolicyYaml();
66+
const propagationPolicyName = getPropagationPolicyNameFromYaml(testPropagationPolicyYaml);
67+
68+
// ========== DEBUG: START ==========
69+
console.log(`[DEBUG] [EDIT TEST] Attempting to create resource: ${propagationPolicyName}`);
70+
// ========== DEBUG: END ==========
71+
72+
// Setup: Create propagationpolicy using kubectl
73+
try {
74+
await createK8sPropagationPolicy(testPropagationPolicyYaml);
75+
// ========== DEBUG: START ==========
76+
console.log(`[DEBUG] [EDIT TEST] Resource created successfully: ${propagationPolicyName}`);
77+
// ========== DEBUG: END ==========
78+
} catch (error) {
79+
// ========== DEBUG: START ==========
80+
console.error(`[DEBUG] [EDIT TEST] Failed to create resource: ${propagationPolicyName}`, error);
81+
throw error;
82+
// ========== DEBUG: END ==========
83+
}
84+
85+
// ========== DEBUG: START ==========
86+
// Verify the resource was created by querying K8s API directly
87+
const k8s = await import('@kubernetes/client-node');
88+
const { createKarmadaApiClient } = await import('../test-utils');
89+
const k8sApi = createKarmadaApiClient(k8s.CustomObjectsApi);
90+
try {
91+
await k8sApi.getNamespacedCustomObject({
92+
group: 'policy.karmada.io',
93+
version: 'v1alpha1',
94+
namespace: 'default',
95+
plural: 'propagationpolicies',
96+
name: propagationPolicyName
97+
});
98+
console.log(`[DEBUG] [EDIT TEST] Resource verified in K8s API: ${propagationPolicyName}`);
99+
} catch (verifyError) {
100+
console.error(`[DEBUG] [EDIT TEST] CRITICAL: Resource NOT found in K8s API: ${propagationPolicyName}`, verifyError);
101+
}
102+
// ========== DEBUG: END ==========
103+
104+
// Open Policies menu
105+
await page.click('text=Policies');
106+
107+
// Click Propagation Policy menu item
108+
const propagationPolicyMenuItem = page.locator('text=Propagation Policy');
109+
await propagationPolicyMenuItem.waitFor({ state: 'visible', timeout: 30000 });
110+
await propagationPolicyMenuItem.click();
111+
112+
// Click Namespace level tab
113+
const namespaceLevelTab = page.locator('role=option[name="Namespace level"]');
114+
await namespaceLevelTab.waitFor({ state: 'visible', timeout: 30000 });
115+
await namespaceLevelTab.click();
116+
117+
// Verify selected state
118+
await expect(namespaceLevelTab).toHaveAttribute('aria-selected', 'true');
119+
120+
await expect(page.locator('table')).toBeVisible({ timeout: 30000 });
121+
122+
// Wait for propagationpolicy to appear in list and get target row
123+
const targetRow = await waitForResourceInList(page, propagationPolicyName);
124+
125+
// Find Edit button in that row and click
126+
const editButton = targetRow.getByText('Edit');
127+
await expect(editButton).toBeVisible({ timeout: 15000 });
128+
129+
// Listen for edit API call
130+
const apiRequestPromise = page.waitForResponse(response => {
131+
return response.url().includes('_raw/propagationpolicy') && response.status() === 200;
132+
}, { timeout: 15000 });
133+
134+
await editButton.click();
135+
136+
// Wait for edit dialog to appear
137+
await page.waitForSelector('[role="dialog"]', { timeout: 10000 });
138+
139+
// Wait for network request to complete and get response data
140+
const apiResponse = await apiRequestPromise;
141+
const responseData = (await apiResponse.json()) as IResponse<DeepRequired<PropagationPolicy>>;
142+
143+
// Verify Monaco editor is loaded
144+
await expect(page.locator('.monaco-editor')).toBeVisible({ timeout: 10000 });
145+
146+
// Wait for editor content to load
147+
let yamlContent = '';
148+
let attempts = 0;
149+
const maxAttempts = 30;
150+
151+
const expectedName = responseData?.data?.metadata?.name || '';
152+
const expectedKind = responseData?.data?.kind || '';
153+
154+
while (attempts < maxAttempts) {
155+
yamlContent = await page.evaluate(() => {
156+
const textarea = document.querySelector('.monaco-editor textarea') as HTMLTextAreaElement;
157+
return textarea ? textarea.value : '';
158+
});
159+
160+
if (yamlContent && yamlContent.length > 0) {
161+
const containsExpectedName = !expectedName || yamlContent.includes(expectedName);
162+
const containsExpectedKind = !expectedKind || yamlContent.includes(expectedKind);
163+
164+
if (containsExpectedName && containsExpectedKind) {
165+
break;
166+
}
167+
}
168+
169+
await page.waitForSelector('.monaco-editor textarea[value*="apiVersion"]', { timeout: 500 }).catch(() => {});
170+
attempts++;
171+
}
172+
173+
// If content is still empty, manually set content from API response
174+
if (!yamlContent || yamlContent.length === 0) {
175+
yamlContent = await page.evaluate((apiData) => {
176+
const data = apiData.data;
177+
const yaml = `apiVersion: ${data.apiVersion}
178+
kind: ${data.kind}
179+
metadata:
180+
name: ${data.metadata?.name || 'test-propagationpolicy'}
181+
namespace: ${data.metadata?.namespace || 'default'}
182+
spec:
183+
resourceSelectors:
184+
- apiVersion: ${data.spec?.resourceSelectors?.[0]?.apiVersion || 'apps/v1'}
185+
kind: ${data.spec?.resourceSelectors?.[0]?.kind || 'Deployment'}
186+
name: ${data.spec?.resourceSelectors?.[0]?.name || 'nginx-deployment'}
187+
placement:
188+
clusterAffinity:
189+
clusterNames:
190+
- ${data.spec?.placement?.clusterAffinity?.clusterNames?.[0] || 'member1'}
191+
- ${data.spec?.placement?.clusterAffinity?.clusterNames?.[1] || 'member2'}`;
192+
193+
const textarea = document.querySelector('.monaco-editor textarea') as HTMLTextAreaElement;
194+
if (textarea) {
195+
textarea.value = yaml;
196+
textarea.focus();
197+
textarea.dispatchEvent(new Event('input', { bubbles: true }));
198+
return yaml;
199+
}
200+
return '';
201+
}, responseData);
202+
}
203+
204+
// If still unable to get content, report error
205+
if (!yamlContent || yamlContent.length === 0) {
206+
throw new Error(`Edit feature error: Monaco editor does not load propagationpolicy YAML content. Expected name: "${expectedName}", kind: "${expectedKind}"`);
207+
}
208+
209+
// Modify YAML content (change cluster name)
210+
let modifiedYaml = yamlContent.replace(/- member1/, '- member3');
211+
212+
// Verify modification took effect
213+
if (modifiedYaml === yamlContent) {
214+
// Try alternative modification - change deployment name
215+
const alternativeModified = yamlContent.replace(/nginx-deployment/, 'httpd-deployment');
216+
if (alternativeModified !== yamlContent) {
217+
modifiedYaml = alternativeModified;
218+
} else {
219+
// If still can't modify, try changing resource selector kind
220+
const kindModified = yamlContent.replace(/kind: Deployment/, 'kind: StatefulSet');
221+
if (kindModified !== yamlContent) {
222+
modifiedYaml = kindModified;
223+
}
224+
}
225+
}
226+
227+
// Set modified YAML content and trigger React onChange callback
228+
await setMonacoEditorContent(page, modifiedYaml);
229+
230+
// Wait for submit button to become enabled and click
231+
await expect(page.locator('[role="dialog"] button:has-text("Submit")')).toBeEnabled();
232+
await page.click('[role="dialog"] button:has-text("Submit")');
233+
234+
// Wait for edit success message or dialog to close
235+
try {
236+
// Try waiting for success message
237+
await expect(page.locator('text=Updated')).toBeVisible({ timeout: 3000 });
238+
} catch (e) {
239+
try {
240+
// If no success message, wait for dialog to close
241+
await page.waitForSelector('[role="dialog"]', { state: 'detached', timeout: 3000 });
242+
} catch (e2) {
243+
// If dialog close also failed, check if page still exists
244+
try {
245+
const isPageActive = await page.evaluate(() => document.readyState);
246+
247+
if (isPageActive === 'complete') {
248+
// Edit operation may have succeeded
249+
}
250+
} catch (e3) {
251+
// Page appears to be closed or crashed
252+
}
253+
}
254+
}
255+
256+
// Cleanup: Delete the created propagationpolicy
257+
try {
258+
await deleteK8sPropagationPolicy(propagationPolicyName, 'default');
259+
} catch (error) {
260+
console.warn(`Failed to cleanup propagationpolicy ${propagationPolicyName}:`, error);
261+
}
262+
263+
// Debug
264+
await debugScreenshot(page, 'debug-propagationpolicy-edit.png');
265+
});

0 commit comments

Comments
 (0)