-
Notifications
You must be signed in to change notification settings - Fork 71
Add E2E tests for Resources/Namespaces in the dashboard #247
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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' }} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
| 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe we can reuse the debug var or just drop lines |
||
| 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,25 +132,82 @@ jobs: | |
| with: | ||
| # keep in sync with the packageManager version in `ui/package.json` | ||
| version: 9.1.2 | ||
|
|
||
| - name: Install dependencies | ||
| run: | | ||
| echo "Start build" | ||
| pnpm --version | ||
| 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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 }); | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good job~ |
||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 }); | ||
| } | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 }); | ||
| } | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could optimize the debug code or adopt the debug code with debug var