From 1734a70ebbb23a9f91d7fa3e722ab8970249570c Mon Sep 17 00:00:00 2001 From: SunsetB612 <10235101575@stu.ecnu.edu.cn> Date: Sat, 16 Aug 2025 00:21:32 +0800 Subject: [PATCH] Add E2E tests for Resources/Namespaces in the dashboard Signed-off-by: SunsetB612 <10235101575@stu.ecnu.edu.cn> --- .github/workflows/ci.yml | 82 +++++++++++++++- .../dashboard/e2e/{ => login}/login.spec.ts | 0 .../e2e/namespace/namespace-create.spec.ts | 64 +++++++++++++ .../e2e/namespace/namespace-delete.spec.ts | 96 +++++++++++++++++++ .../e2e/namespace/namespace-list.spec.ts | 52 ++++++++++ .../namespace/namespace-network-error.spec.ts | 53 ++++++++++ ui/apps/dashboard/playwright.config.ts | 2 +- 7 files changed, 346 insertions(+), 3 deletions(-) rename ui/apps/dashboard/e2e/{ => login}/login.spec.ts (100%) create mode 100644 ui/apps/dashboard/e2e/namespace/namespace-create.spec.ts create mode 100644 ui/apps/dashboard/e2e/namespace/namespace-delete.spec.ts create mode 100644 ui/apps/dashboard/e2e/namespace/namespace-list.spec.ts create mode 100644 ui/apps/dashboard/e2e/namespace/namespace-network-error.spec.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d6f005e9..b2952404 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,10 @@ concurrency: cancel-in-progress: true permissions: contents: read # for actions/checkout to fetch code + +env: + DEBUG: false + jobs: golangci: name: lint @@ -89,20 +93,37 @@ jobs: large-packages: false docker-images: false swap-storage: false + - name: Checkout karmada repo uses: actions/checkout@v4 with: repository: karmada-io/karmada path: karmada + - name: setup e2e test environment run: | cd karmada export CLUSTER_VERSION=kindest/node:${{ matrix.k8s }} hack/local-up-karmada.sh + echo "KUBECONFIG=/home/runner/.kube/karmada.config" >> $GITHUB_ENV + - name: Debug cluster status + if: ${{ env.DEBUG == 'true' }} + run: | + kubectl cluster-info || echo "Cluster info failed" + kubectl get nodes -o wide || echo "Get nodes failed" + kubectl get pods -A || echo "Get pods failed" + kind get clusters || echo "Kind get clusters failed" - name: Checkout dashboard repo uses: actions/checkout@v4 with: path: karmada-dashboard + + - name: Generate KARMADA_TOKEN for CI + run: | + kubectl --context=karmada-host apply -k karmada-dashboard/artifacts/overlays/nodeport-mode + kubectl --context=karmada-apiserver apply -f karmada-dashboard/artifacts/dashboard/karmada-dashboard-sa.yaml + TOKEN=$(kubectl --context=karmada-apiserver -n karmada-system get secret/karmada-dashboard-secret -o go-template="{{.data.token | base64decode}}") + echo "KARMADA_TOKEN=$TOKEN" >> $GITHUB_ENV - name: Use Node.js 20 uses: actions/setup-node@v4 with: @@ -111,6 +132,7 @@ jobs: with: # keep in sync with the packageManager version in `ui/package.json` version: 9.1.2 + - name: Install dependencies run: | echo "Start build" @@ -118,18 +140,74 @@ jobs: cd karmada-dashboard/ui pnpm install pnpm turbo build + - name: Build API binary + run: | + cd karmada-dashboard + make build + - name: Start API server + run: | + cd karmada-dashboard + make run-api & + echo $! > .api.pid + sleep 5 + - name: Wait for API server to be ready + run: | + echo "Waiting for API server to be ready..." + timeout 60s bash -c 'while ! nc -z localhost 8000; do echo "Waiting for API server port..."; sleep 5; done' || { + echo "API server failed to start within 60 seconds" + echo "=== API Server Debug ===" + if [ -f karmada-dashboard/.api.pid ]; then + API_PID=$(cat karmada-dashboard/.api.pid) + ps aux | grep $API_PID || echo "API process not found" + fi + ss -tuln | grep 8000 || echo "Port 8000 not listening" + exit 1 + } + echo "API server is ready!" - name: Install Playwright Browsers run: | cd karmada-dashboard/ui/apps/dashboard pnpm exec playwright install --with-deps + - name: Debug environment before starting dashboard + if: ${{ env.DEBUG == 'true' }} + run: | + echo "KARMADA_TOKEN is set: ${{ env.KARMADA_TOKEN != '' }}" + echo "KUBECONFIG: $KUBECONFIG" + echo "Current directory: $(pwd)" + echo "Dashboard directory contents:" + ls -la karmada-dashboard/ || echo "No dashboard directory" + echo "Check if Makefile exists:" + ls -la karmada-dashboard/Makefile || echo "No Makefile found" + - name: Build frontend (for e2e) + working-directory: karmada-dashboard/ui/apps/dashboard + run: | + pnpm build + - name: Test API connectivity before running tests + run: | + echo "Testing API connectivity..." + curl -f http://localhost:8000/api/v1/cluster && echo "✓ Cluster API works" || echo "✗ Cluster API failed" + curl -f http://localhost:8000/api/v1/namespace && echo "✓ Namespace API works" || echo "✗ Namespace API failed" + echo "API tests completed" + - name: Debug working dir + if: ${{ env.DEBUG == 'true' }} + run: | + pwd + ls -al - name: Run Playwright tests + working-directory: karmada-dashboard/ui/apps/dashboard run: | - cd karmada-dashboard/ui/apps/dashboard pnpm exec playwright test + - name: Cleanup processes + if: always() + run: | + echo "Cleaning up processes..." + if [ -f karmada-dashboard/.api.pid ]; then + API_PID=$(cat karmada-dashboard/.api.pid) + kill $API_PID 2>/dev/null || echo "API process already stopped" + fi - uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: name: playwright-report-${{ matrix.k8s }} path: karmada-dashboard/ui/apps/dashboard/playwright-report/ retention-days: 30 - diff --git a/ui/apps/dashboard/e2e/login.spec.ts b/ui/apps/dashboard/e2e/login/login.spec.ts similarity index 100% rename from ui/apps/dashboard/e2e/login.spec.ts rename to ui/apps/dashboard/e2e/login/login.spec.ts diff --git a/ui/apps/dashboard/e2e/namespace/namespace-create.spec.ts b/ui/apps/dashboard/e2e/namespace/namespace-create.spec.ts new file mode 100644 index 00000000..a1f45ed6 --- /dev/null +++ b/ui/apps/dashboard/e2e/namespace/namespace-create.spec.ts @@ -0,0 +1,64 @@ +/* +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. +*/ + +// apps/dashboard/e2e/namespace-create.spec.ts +import { test, expect } from '@playwright/test'; + +// 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 || ''; + +test.beforeEach(async ({ 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=Dashboard', { timeout: 30000 }); +}); + +test('should create a new namespace', async ({ page }) => { + // 打开 Namespaces 页面 + await page.waitForSelector('text=Namespaces', { timeout: 60000 }); + await page.click('text=Namespaces'); + + // 点击 "Add" 创建新 namespace + await page.waitForSelector('button:has-text("Add")', { timeout: 30000 }); + await page.click('button:has-text("Add")'); + + // 填写唯一 namespace 名称 + const namespaceName = `test-${Date.now()}`; + await page.waitForSelector('#name', { timeout: 30000 }); + await page.fill('#name', namespaceName); + + // 提交创建 + await page.click('label:has-text("No")'); + await page.click('button:has-text("Submit")'); + + // 搜索并验证 namespace 已创建 + const searchBox = page.getByPlaceholder('Search by Name'); + await searchBox.fill(namespaceName); + await searchBox.press('Enter'); + await expect(page.locator('table')).toContainText(namespaceName); + + // Debug + if(process.env.DEBUG === 'true'){ + await page.screenshot({ path: 'debug-namespace-create.png', fullPage: true }); + } +}); diff --git a/ui/apps/dashboard/e2e/namespace/namespace-delete.spec.ts b/ui/apps/dashboard/e2e/namespace/namespace-delete.spec.ts new file mode 100644 index 00000000..8b24abde --- /dev/null +++ b/ui/apps/dashboard/e2e/namespace/namespace-delete.spec.ts @@ -0,0 +1,96 @@ +/* +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. +*/ + +// apps/dashboard/e2e/namespace-delete.spec.ts +import { test, expect } from '@playwright/test'; + +// 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 || ''; + +test.beforeEach(async ({ 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=Dashboard', { timeout: 30000 }); +}); + +test('should delete a namespace', async ({ page }) => { + await page.waitForSelector('text=Namespaces', { timeout: 60000 }); + await page.click('text=Namespaces'); + + // 创建临时 namespace + const namespaceName = `test-to-delete-${Date.now()}`; + await page.click('button:has-text("Add")'); + await page.fill('#name', namespaceName); + await page.click('label:has-text("No")'); + await page.click('button:has-text("Submit")'); + + // 使用搜索框确认创建成功 + const searchBox = page.getByPlaceholder('Search by Name'); + await searchBox.fill(namespaceName); + await searchBox.press('Enter'); + await page.waitForSelector(`tr:has-text("${namespaceName}")`, { timeout: 30000 }); + + // 删除 namespace + await page.click(`tr:has-text("${namespaceName}") button:has-text("Delete")`); + await page.click('button:has-text("Confirm")'); + + // 刷新页面,确保表格拉取最新数据 + await page.reload({ waitUntil: 'networkidle' }); + + // 使用搜索框确认删除 + await searchBox.fill(namespaceName); + await searchBox.press('Enter'); + + //确认 namespace 已删除 + const table = page.locator('table'); + const start = Date.now(); + let gone = false; + + while (Date.now() - start < 120000) { // 最多等 120 秒 + const content = await table.innerText(); + if (!content.includes(namespaceName)) { + console.log(`Namespace ${namespaceName} 已彻底删除`); + gone = true; + break; + } else if (content.includes('Terminating')) { + console.log(`Namespace ${namespaceName} Terminating`); + } else { + console.log(`Namespace ${namespaceName} 仍然存在`); + } + await page.waitForTimeout(5000); // 每 5 秒检查一次 + await page.reload({ waitUntil: 'networkidle' }); // 强制刷新,拿最新数据 + await searchBox.fill(namespaceName); + await searchBox.press('Enter'); + } + + // 确认最终被删除(如果超时则失败) + expect(gone).toBeTruthy(); + + // 清空搜索框 + await searchBox.clear(); + + // Debug + if(process.env.DEBUG === 'true'){ + await page.screenshot({ path: 'debug-namespace-delete.png', fullPage: true }); + } +}); diff --git a/ui/apps/dashboard/e2e/namespace/namespace-list.spec.ts b/ui/apps/dashboard/e2e/namespace/namespace-list.spec.ts new file mode 100644 index 00000000..dabd859c --- /dev/null +++ b/ui/apps/dashboard/e2e/namespace/namespace-list.spec.ts @@ -0,0 +1,52 @@ +/* +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. +*/ + +// apps/dashboard/e2e/namespace-list.spec.ts +import { test, expect } from '@playwright/test'; + +// 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 || ''; + +test.beforeEach(async ({ 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=Dashboard', { timeout: 30000 }); +}); + +test('should display namespace list', async ({ page }) => { + // 打开 Namespaces 页面 + await page.waitForSelector('text=Namespaces', { timeout: 60000 }); + await page.click('text=Namespaces'); + + // 获取表格元素并验证可见 + const table = page.locator('table'); + await expect(table).toBeVisible({ timeout: 30000 }); + + // 验证表格中包含默认 namespace + await expect(table).toContainText('default'); + + // Debug + if(process.env.DEBUG === 'true'){ + await page.screenshot({ path: 'debug-namespace-list.png', fullPage: true }); + } +}); diff --git a/ui/apps/dashboard/e2e/namespace/namespace-network-error.spec.ts b/ui/apps/dashboard/e2e/namespace/namespace-network-error.spec.ts new file mode 100644 index 00000000..42205a55 --- /dev/null +++ b/ui/apps/dashboard/e2e/namespace/namespace-network-error.spec.ts @@ -0,0 +1,53 @@ +/* +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. +*/ + +// apps/dashboard/e2e/namespace/namespace-network-error.spec.ts +import { test, expect } from '@playwright/test'; + +// 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 || ''; + +test('Namespace network failure with refresh', async ({ page }) => { + // 阻塞 Namespace API 请求 + await page.route('**/api/v1/namespaces', route => route.abort()); + + await page.goto(`${baseURL}/login`, { waitUntil: 'networkidle' }); + await page.evaluate((t) => localStorage.setItem('token', t), token); + + // 导航到页面 + await page.goto(`${baseURL}${basePath}`, { waitUntil: 'networkidle' }); + + // 设置 token 并刷新,保证登录状态 + await page.evaluate((t) => localStorage.setItem('token', t), token); + await page.reload({ waitUntil: 'networkidle' }); + + // 等待关键元素加载完成,宽松等待 Namespaces 文字 + await page.waitForSelector('text=Namespaces', { timeout: 15000 }); + + // 验证表格 + const tableRows = page.locator('table tbody tr'); + // 最终断言:网络错误时表格应该为空 + await expect(tableRows).toHaveCount(0); + + // Debug + if(process.env.DEBUG === 'true'){ + await page.screenshot({ path: 'debug-namespace-network-failure.png', fullPage: true }); + } +}); diff --git a/ui/apps/dashboard/playwright.config.ts b/ui/apps/dashboard/playwright.config.ts index 583aa583..6c2e2738 100644 --- a/ui/apps/dashboard/playwright.config.ts +++ b/ui/apps/dashboard/playwright.config.ts @@ -74,4 +74,4 @@ export default defineConfig({ timeout: 120 * 1000, reuseExistingServer: !process.env.CI, }, -}); +}); \ No newline at end of file