diff --git a/ui/apps/dashboard/e2e/policy/propagationpolicy/cluster/propagationpolicy-cluster-delete.spec.ts b/ui/apps/dashboard/e2e/policy/propagationpolicy/cluster/propagationpolicy-cluster-delete.spec.ts new file mode 100644 index 00000000..46d85a64 --- /dev/null +++ b/ui/apps/dashboard/e2e/policy/propagationpolicy/cluster/propagationpolicy-cluster-delete.spec.ts @@ -0,0 +1,96 @@ +/* +Copyright 2025 The Karmada Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { test, expect } from '@playwright/test'; +import { setupDashboardAuthentication, generateTestClusterPropagationPolicyYaml, createK8sClusterPropagationPolicy, getClusterPropagationPolicyNameFromYaml} from './test-utils'; + +test.beforeEach(async ({ page }) => { + await setupDashboardAuthentication(page); +}); + +test('should delete cluster propagationpolicy successfully', async ({ page }) => { + // Create a test cluster propagationpolicy directly via API to set up test data + const testClusterPropagationPolicyYaml = generateTestClusterPropagationPolicyYaml(); + const clusterPropagationPolicyName = getClusterPropagationPolicyNameFromYaml(testClusterPropagationPolicyYaml); + + // Setup: Create cluster propagationpolicy using API + await createK8sClusterPropagationPolicy(testClusterPropagationPolicyYaml); + + // Open Policies menu + await page.click('text=Policies'); + + // Click Propagation Policy menu item + const propagationPolicyMenuItem = page.locator('text=Propagation Policy'); + await propagationPolicyMenuItem.waitFor({ state: 'visible', timeout: 30000 }); + await propagationPolicyMenuItem.click(); + + // Click Cluster level tab + const clusterLevelTab = page.locator('role=option[name="Cluster level"]'); + await clusterLevelTab.waitFor({ state: 'visible', timeout: 30000 }); + await clusterLevelTab.click(); + + // Verify selected state + await expect(clusterLevelTab).toHaveAttribute('aria-selected', 'true'); + await expect(page.locator('table')).toBeVisible({ timeout: 30000 }); + + // Wait for cluster propagationpolicy to appear in list + const table = page.locator('table'); + await expect(table.locator(`text=${clusterPropagationPolicyName}`)).toBeVisible({ timeout: 30000 }); + + // Find row containing test cluster propagationpolicy name + const targetRow = page.locator(`table tbody tr:has-text("${clusterPropagationPolicyName}")`); + await expect(targetRow).toBeVisible({ timeout: 15000 }); + + // Find Delete button in that row and click + const deleteButton = targetRow.locator('button[type="button"]').filter({ hasText: /^(Delete)$/ }); + await expect(deleteButton).toBeVisible({ timeout: 10000 }); + + // Listen for delete API call + const deleteApiPromise = page.waitForResponse(response => { + return response.url().includes('/propagationpolicy') && + response.request().method() === 'DELETE' && + response.status() === 200; + }, { timeout: 15000 }); + + await deleteButton.click(); + + // Wait for delete confirmation tooltip to appear + await page.waitForSelector('[role="tooltip"]', { timeout: 10000 }); + + // Click Confirm button to confirm deletion + const confirmButton = page.locator('[role="tooltip"] button').filter({ hasText: /^(Confirm)$/ }); + await expect(confirmButton).toBeVisible({ timeout: 5000 }); + await confirmButton.click(); + + // Wait for delete API call to succeed + await deleteApiPromise; + + // Wait for tooltip to close + await page.waitForSelector('[role="tooltip"]', { state: 'detached', timeout: 10000 }).catch(() => {}); + + // Refresh page to ensure UI is updated after deletion + await page.reload(); + await page.click('text=Policies'); + await expect(table).toBeVisible({ timeout: 30000 }); + + // Verify cluster propagationpolicy no longer exists in table + await expect(table.locator(`text=${clusterPropagationPolicyName}`)).toHaveCount(0, { timeout: 30000 }); + + // Debug + if(process.env.DEBUG === 'true'){ + await page.screenshot({ path: 'debug-cluster-propagationpolicy-delete.png', fullPage: true }); + } +}); diff --git a/ui/apps/dashboard/e2e/policy/propagationpolicy/cluster/propagationpolicy-cluster-list.spec.ts b/ui/apps/dashboard/e2e/policy/propagationpolicy/cluster/propagationpolicy-cluster-list.spec.ts new file mode 100644 index 00000000..fe1bb8c6 --- /dev/null +++ b/ui/apps/dashboard/e2e/policy/propagationpolicy/cluster/propagationpolicy-cluster-list.spec.ts @@ -0,0 +1,50 @@ +/* +Copyright 2025 The Karmada Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// apps/dashboard/e2e/propagationpolicy-cluster-list.spec.ts +import { test, expect } from '@playwright/test'; +import { setupDashboardAuthentication } from './test-utils'; + +test.beforeEach(async ({ page }) => { + await setupDashboardAuthentication(page); +}); + +test('should display propagationpolicy cluster list', async ({ page }) => { + // Open Policies menu + await page.click('text=Policies'); + + // Click Propagation Policy menu item + const propagationPolicyMenuItem = page.locator('text=Propagation Policy'); + await propagationPolicyMenuItem.waitFor({ state: 'visible', timeout: 30000 }); + await propagationPolicyMenuItem.click(); + + // Click Cluster level tab + const clusterLevelTab = page.locator('role=option[name="Cluster level"]'); + await clusterLevelTab.waitFor({ state: 'visible', timeout: 30000 }); + await clusterLevelTab.click(); + + // Verify selected state + await expect(clusterLevelTab).toHaveAttribute('aria-selected', 'true'); + + // Verify PropagationPolicy list table is visible + const table = page.locator('table'); + await expect(table).toBeVisible({ timeout: 30000 }); + + // Debug + if (process.env.DEBUG === 'true') { + await page.screenshot({ path: 'debug-propagationpolicy-cluster-list.png', fullPage: true }); + } +}); diff --git a/ui/apps/dashboard/e2e/policy/propagationpolicy/cluster/propagationpolicy-cluster-view.spec.ts b/ui/apps/dashboard/e2e/policy/propagationpolicy/cluster/propagationpolicy-cluster-view.spec.ts new file mode 100644 index 00000000..f2c62013 --- /dev/null +++ b/ui/apps/dashboard/e2e/policy/propagationpolicy/cluster/propagationpolicy-cluster-view.spec.ts @@ -0,0 +1,76 @@ +/* +Copyright 2025 The Karmada Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { test, expect } from '@playwright/test'; +import { setupDashboardAuthentication, generateTestClusterPropagationPolicyYaml, createK8sClusterPropagationPolicy, getClusterPropagationPolicyNameFromYaml, deleteK8sClusterPropagationPolicy } from './test-utils'; + +test.beforeEach(async ({ page }) => { + await setupDashboardAuthentication(page); +}); + +test('should view cluster propagationpolicy details', async ({ page }) => { + // Create a test cluster propagationpolicy directly via API to set up test data + const testClusterPropagationPolicyYaml = generateTestClusterPropagationPolicyYaml(); + const clusterPropagationPolicyName = getClusterPropagationPolicyNameFromYaml(testClusterPropagationPolicyYaml); + + // Setup: Create cluster propagationpolicy using API + await createK8sClusterPropagationPolicy(testClusterPropagationPolicyYaml); + + // Open Policies menu + await page.click('text=Policies'); + + // Click Propagation Policy menu item + const propagationPolicyMenuItem = page.locator('text=Propagation Policy'); + await propagationPolicyMenuItem.waitFor({ state: 'visible', timeout: 30000 }); + await propagationPolicyMenuItem.click(); + + // Click Cluster level tab + const clusterLevelTab = page.locator('role=option[name="Cluster level"]'); + await clusterLevelTab.waitFor({ state: 'visible', timeout: 30000 }); + await clusterLevelTab.click(); + + // Verify selected state + await expect(clusterLevelTab).toHaveAttribute('aria-selected', 'true'); + await expect(page.locator('table')).toBeVisible({ timeout: 30000 }); + + // Wait for cluster propagationpolicy to appear in list + const table = page.locator('table'); + await expect(table.locator(`text=${clusterPropagationPolicyName}`)).toBeVisible({ timeout: 30000 }); + + // Find row containing test cluster propagationpolicy name + const targetRow = page.locator(`table tbody tr:has-text("${clusterPropagationPolicyName}")`); + await expect(targetRow).toBeVisible({ timeout: 15000 }); + + // Find View button in that row and click + const viewButton = targetRow.getByText('View'); + await expect(viewButton).toBeVisible({ timeout: 15000 }); + await viewButton.click(); + + // Verify details page is displayed + await page.waitForLoadState('networkidle'); + + // Cleanup: Delete the created cluster propagationpolicy + try { + await deleteK8sClusterPropagationPolicy(clusterPropagationPolicyName); + } catch (error) { + console.warn(`Failed to cleanup cluster propagationpolicy ${clusterPropagationPolicyName}:`, error); + } + + // Debug + if(process.env.DEBUG === 'true'){ + await page.screenshot({ path: 'debug-cluster-propagationpolicy-view.png', fullPage: true }); + } +}); diff --git a/ui/apps/dashboard/e2e/policy/propagationpolicy/cluster/test-utils.ts b/ui/apps/dashboard/e2e/policy/propagationpolicy/cluster/test-utils.ts new file mode 100644 index 00000000..7ec97b62 --- /dev/null +++ b/ui/apps/dashboard/e2e/policy/propagationpolicy/cluster/test-utils.ts @@ -0,0 +1,180 @@ +/*Copyright 2024 The Karmada Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { Page, expect } from '@playwright/test'; +import * as k8s from '@kubernetes/client-node'; +import { parse } from 'yaml'; +import _ from 'lodash'; + +// Set webServer.url and use.baseURL with the location of the WebServer +const HOST = process.env.HOST || 'localhost'; +const PORT = process.env.PORT || 5173; +const baseURL = `http://${HOST}:${PORT}`; +const basePath = '/multicloud-resource-manage'; +const token = process.env.KARMADA_TOKEN || ''; + +// Karmada API server configuration - can be overridden by environment variables +const KARMADA_HOST = process.env.KARMADA_HOST || 'localhost'; +const KARMADA_PORT = process.env.KARMADA_PORT || '5443'; // Standard Karmada API server port +const KARMADA_API_SERVER = `https://${KARMADA_HOST}:${KARMADA_PORT}`; + +/** + * Creates a configured Kubernetes API client for Karmada + * @returns Kubernetes CustomObjectsApi client + */ +function createKarmadaApiClient(): k8s.CustomObjectsApi { + const kc = new k8s.KubeConfig(); + + // Try to use existing kubeconfig first (for CI) + if (process.env.KUBECONFIG) { + try { + kc.loadFromFile(process.env.KUBECONFIG); + // Set context to karmada-apiserver if available + const contexts = kc.getContexts(); + const karmadaContext = contexts.find(c => c.name === 'karmada-apiserver'); + if (karmadaContext) { + kc.setCurrentContext('karmada-apiserver'); + } + return kc.makeApiClient(k8s.CustomObjectsApi); + } catch (error) { + console.error('Failed to load kubeconfig:', error); + } + } + + // Fallback to custom config for local development + const kubeConfigYaml = ` +apiVersion: v1 +kind: Config +clusters: +- cluster: + insecure-skip-tls-verify: true + server: ${KARMADA_API_SERVER} + name: karmada-apiserver +contexts: +- context: + cluster: karmada-apiserver + user: karmada-dashboard + name: default +current-context: default +users: +- name: karmada-dashboard + user: + token: ${token} +`; + + kc.loadFromString(kubeConfigYaml); + return kc.makeApiClient(k8s.CustomObjectsApi); +} + +/** + * Setup dashboard authentication and navigate to configmap page + */ +export async function setupDashboardAuthentication(page: Page) { + await page.goto(`${baseURL}/login`, { waitUntil: 'networkidle' }); + await page.evaluate((t) => localStorage.setItem('token', t), token); + await page.goto(`${baseURL}${basePath}`, { waitUntil: 'networkidle' }); + await page.evaluate((t) => localStorage.setItem('token', t), token); + await page.reload({ waitUntil: 'networkidle' }); + await page.waitForSelector('text=Overview', { timeout: 30000 }); +} + +/** + * Generate test ClusterPropagationPolicy YAML with timestamp + */ +export function generateTestClusterPropagationPolicyYaml() { + const timestamp = Date.now(); + return `apiVersion: policy.karmada.io/v1alpha1 +kind: ClusterPropagationPolicy +metadata: + name: test-clusterpropagationpolicy-${timestamp} + labels: + app: test-app +spec: + resourceSelectors: + - apiVersion: apps/v1 + kind: Deployment + name: nginx-deployment + placement: + clusterAffinity: + clusterNames: + - member1 + - member2`; +} + +/** + * Creates a Kubernetes ClusterPropagationPolicy using the Kubernetes JavaScript client. + * This is a more robust way to set up test data than UI interaction. + * @param yamlContent The YAML content of the clusterpropagationpolicy. + * @returns A Promise that resolves when the clusterpropagationpolicy is created. + */ +export async function createK8sClusterPropagationPolicy(yamlContent: string): Promise { + try { + const k8sApi = createKarmadaApiClient(); + const yamlObject = parse(yamlContent) as any; + + // ClusterPropagationPolicy is cluster-scoped, so no namespace needed + await k8sApi.createClusterCustomObject({ + group: 'policy.karmada.io', + version: 'v1alpha1', + plural: 'clusterpropagationpolicies', + body: yamlObject + }); + + } catch (error: any) { + throw new Error(`Failed to create clusterpropagationpolicy: ${error.message}`); + } +} + +/** + * Deletes a Kubernetes ClusterPropagationPolicy using the Kubernetes JavaScript client. + * @param clusterPropagationPolicyName The name of the clusterpropagationpolicy to delete. + * @returns A Promise that resolves when the clusterpropagationpolicy is deleted. + */ +export async function deleteK8sClusterPropagationPolicy(clusterPropagationPolicyName: string): Promise { + try { + const k8sApi = createKarmadaApiClient(); + + // Assert parameters are valid for test clusterpropagationpolicy + expect(clusterPropagationPolicyName).toBeTruthy(); + expect(clusterPropagationPolicyName).not.toBe(''); + + await k8sApi.deleteClusterCustomObject({ + group: 'policy.karmada.io', + version: 'v1alpha1', + plural: 'clusterpropagationpolicies', + name: clusterPropagationPolicyName + }); + + } catch (error: any) { + if (error.response?.status === 404 || error.statusCode === 404) { + // ClusterPropagationPolicy not found - already deleted, this is fine + return; + } + throw new Error(`Failed to delete clusterpropagationpolicy: ${error.message}`); + } +} + +/** + * Gets clusterpropagationpolicy name from YAML content using proper YAML parsing. + * @param yamlContent The YAML content. + * @returns The clusterpropagationpolicy name. + */ +export function getClusterPropagationPolicyNameFromYaml(yamlContent: string): string { + const yamlObject = parse(yamlContent); + const clusterPropagationPolicyName = _.get(yamlObject, 'metadata.name'); + + if (!clusterPropagationPolicyName) { + throw new Error('Could not extract clusterpropagationpolicy name from YAML'); + } + + return clusterPropagationPolicyName; +}