From 8a50657b43b9cf27afd6ae0d4e57d835717c084c Mon Sep 17 00:00:00 2001 From: "xiuting.xu" Date: Thu, 30 Oct 2025 14:57:10 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9playwright=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/sys/tests/scripts/16_web_verify.sh | 10 +-- src/web/playwright.config.ts | 11 ++- src/web/tests/playwright/alerts.spec.ts | 30 +++---- src/web/tests/playwright/dashboard.spec.ts | 85 +++++++++---------- .../playwright/helpers/entrycards-helpers.ts | 44 +++++----- src/web/tests/playwright/logs.spec.ts | 17 ++-- src/web/tests/playwright/metric.spec.ts | 16 ++-- src/web/tests/playwright/node-info.spec.ts | 77 +++++++++-------- 8 files changed, 153 insertions(+), 137 deletions(-) diff --git a/src/sys/tests/scripts/16_web_verify.sh b/src/sys/tests/scripts/16_web_verify.sh index 08d2f0c..b340c1c 100644 --- a/src/sys/tests/scripts/16_web_verify.sh +++ b/src/sys/tests/scripts/16_web_verify.sh @@ -54,7 +54,7 @@ done #============================= # Step 2: Run Playwright tests #============================= -log_info "[2/4] Running Playwright automated tests..." +log_info "[2/4] Running Playwright automated tests in headless mode..." cd "$WEB_DIR" @@ -70,9 +70,9 @@ npx playwright install --with-deps > /dev/null # Clean previous reports rm -rf "$REPORT_DIR" -# Run Playwright tests with reporters -set +e # temporarily disable exit-on-error to capture test result -BASE_URL=${FRONTEND_URL} npx playwright test tests/playwright --reporter=list +# Run Playwright tests wrapped with xvfb-run to avoid GUI +set +e # temporarily disable exit-on-error +env BASE_URL="$FRONTEND_URL" xvfb-run --auto-servernum npx playwright test tests/playwright --reporter=list TEST_RESULT=$? set -e # re-enable strict mode @@ -100,4 +100,4 @@ else log_warn "Report directory not found. Check Playwright execution logs." fi -log_success "Web frontend verify success. Playwright automated tests passed." +log_success "Web frontend verify finished." diff --git a/src/web/playwright.config.ts b/src/web/playwright.config.ts index d6592de..a764205 100644 --- a/src/web/playwright.config.ts +++ b/src/web/playwright.config.ts @@ -10,7 +10,16 @@ export default defineConfig({ viewport: { width: 1280, height: 720 }, ignoreHTTPSErrors: true, screenshot: 'only-on-failure', - video: 'retain-on-failure' + video: 'retain-on-failure', + launchOptions: { + args: [ + '--no-sandbox', + '--disable-gpu', + '--disable-dev-shm-usage', + '--disable-software-rasterizer', + '--headless=new' + ], + }, }, reporter: [ ['list'], diff --git a/src/web/tests/playwright/alerts.spec.ts b/src/web/tests/playwright/alerts.spec.ts index cfb5681..ee3d9ae 100644 --- a/src/web/tests/playwright/alerts.spec.ts +++ b/src/web/tests/playwright/alerts.spec.ts @@ -1,5 +1,5 @@ import {test, expect} from "@playwright/test"; -import { BASE_URL } from './helpers/utils' +import {BASE_URL} from './helpers/utils' test.describe("Alerts 页面功能测试", () => { test.beforeEach(async ({page}) => { @@ -7,17 +7,17 @@ test.describe("Alerts 页面功能测试", () => { }); test("页面加载并显示告警统计", async ({page}) => { - await expect(page.locator("text=告警详情")).toBeVisible(); - await expect(page.locator("text=总数")).toBeVisible(); - await expect(page.locator("text=严重")).toBeVisible(); - await expect(page.locator("text=警告")).toBeVisible(); - await expect(page.locator("text=信息")).toBeVisible(); + await expect(page.locator("text=告警详情").first()).toBeVisible(); + await expect(page.locator("text=总数").first()).toBeVisible(); + await expect(page.locator("text=严重").first()).toBeVisible(); + await expect(page.locator("text=警告").first()).toBeVisible(); + await expect(page.locator("text=信息").first()).toBeVisible(); }); test("筛选功能验证", async ({page}) => { - const severitySelect = page.getByLabel("严重性"); - const stateSelect = page.getByLabel("状态"); - const nodeSelect = page.getByLabel("节点"); + const severitySelect = page.getByRole('combobox', {name: '严重性'}); + const stateSelect = page.getByRole('combobox', {name: '状态'}); + const nodeSelect = page.getByRole('combobox', {name: '节点'}); await severitySelect.selectOption("critical"); await expect(severitySelect).toHaveValue("critical"); @@ -30,18 +30,18 @@ test.describe("Alerts 页面功能测试", () => { }); test("排序功能", async ({page}) => { - const severityHeader = page.locator("th:has-text('严重性') button"); + const severityHeader = page.locator("th:has-text('严重性') button").first(); await severityHeader.click(); // 切换升序 await severityHeader.click(); // 切换降序 - const instanceHeader = page.locator("th:has-text('节点') button"); + const instanceHeader = page.locator("th:has-text('节点') button").first(); await instanceHeader.click(); await instanceHeader.click(); }); test("分页功能", async ({page}) => { - const nextButton = page.locator("button:has-text('下一页')"); - const prevButton = page.locator("button:has-text('上一页')"); + const nextButton = page.locator("button:has-text('下一页')").first(); + const prevButton = page.locator("button:has-text('上一页')").first(); if (await nextButton.isEnabled()) { await nextButton.click(); @@ -61,8 +61,8 @@ test.describe("Alerts 页面功能测试", () => { }); test("自动刷新开关与刷新按钮", async ({page}) => { - const switchBtn = page.getByRole("switch", {name: "自动刷新"}); - const refreshBtn = page.getByTitle("刷新"); + const switchBtn = page.locator("div[role='switch']").first(); + const refreshBtn = page.getByTitle("刷新").first(); await expect(switchBtn).toBeVisible(); await expect(refreshBtn).toBeVisible(); diff --git a/src/web/tests/playwright/dashboard.spec.ts b/src/web/tests/playwright/dashboard.spec.ts index b01036f..72f6ae6 100644 --- a/src/web/tests/playwright/dashboard.spec.ts +++ b/src/web/tests/playwright/dashboard.spec.ts @@ -1,59 +1,52 @@ -import { test, expect } from '@playwright/test'; -import { BASE_URL } from './helpers/utils' +import {test, expect} from '@playwright/test'; +import {BASE_URL} from './helpers/utils' test.describe('Dashboard 页面测试', () => { - test.beforeEach(async ({ page }) => { - // 打开仪表盘页面 - await page.goto(`${BASE_URL}/dashboard`, { waitUntil: 'networkidle' }); - }); + test.beforeEach(async ({page}) => { + // 打开仪表盘页面 + await page.goto(`${BASE_URL}/dashboard`, {waitUntil: 'networkidle'}); + }); - test('应能成功加载页面并显示标题', async ({ page }) => { - await expect(page.locator('text=仪表盘')).toBeVisible(); - }); + test('应能成功加载页面并显示标题', async ({page}) => { + await expect(page.locator('text=仪表盘').first()).toBeVisible(); + }); - test('应显示节点健康状态卡片', async ({ page }) => { - const healthCard = page.locator('text=节点健康状态'); - await expect(healthCard).toBeVisible(); + test('应显示节点健康状态卡片', async ({page}) => { + const healthCard = page.locator('text=节点健康状态'); + await expect(healthCard).toBeVisible(); - // 检查环形图是否渲染 - const ring = page.locator('svg'); // RingProgress 是 SVG 渲染的 - const ringCount = await ring.count(); - expect(ringCount).toBeGreaterThan(0); - }); + // 检查环形图是否渲染 + const ring = page.locator('svg'); // RingProgress 是 SVG 渲染的 + const ringCount = await ring.count(); + expect(ringCount).toBeGreaterThan(0); + }); - test('应显示告警统计信息', async ({ page }) => { - const alertCard = page.locator('text=告警统计'); - await expect(alertCard).toBeVisible(); + test('应显示告警统计信息', async ({page}) => { + const alertCard = page.locator('text=告警统计'); + await expect(alertCard).toBeVisible(); - // 检查告警类别 - const labels = ['总数', '严重', '警告', '信息']; - for (const label of labels) { - await expect(page.locator(`text=${label}`)).toBeVisible(); - } - }); + // 检查告警类别 + const labels = ['总数', '严重', '警告', '信息']; + for (const label of labels) { + await expect(page.locator(`text=${label}`).first()).toBeVisible(); + } + }); - test('应正确渲染集群节点表格', async ({ page }) => { - const tableHeaders = ['ID', '名称', '状态', '类型', '版本']; - for (const header of tableHeaders) { - await expect(page.locator(`th:has-text("${header}")`)).toBeVisible(); - } + test('应正确渲染集群节点表格', async ({page}) => { + const tableHeaders = ['ID', '名称', '状态', '类型', '版本']; + for (const header of tableHeaders) { + await expect(page.locator(`th:has-text("${header}")`).first()).toBeVisible(); + } - // 至少有一行节点数据 - const rows = await page.locator('tbody tr').count(); - expect(rows).toBeGreaterThan(0); - }); + // 至少有一行节点数据 + const rows = await page.locator('tbody tr').count(); + expect(rows).toBeGreaterThan(0); + }); - test('“查看更多”链接应存在并指向 /nodeInfo', async ({ page }) => { - const link = page.locator('a:has-text("查看更多")'); - await expect(link).toBeVisible(); - const href = await link.getAttribute('href'); - expect(href).toContain('/nodeInfo'); - }); - - test('页面应无加载错误提示', async ({ page }) => { - await expect(page.locator('text=加载中...')).toHaveCount(0); - await expect(page.locator('text=数据加载失败')).toHaveCount(0); - }); + test('页面应无加载错误提示', async ({page}) => { + await expect(page.locator('text=加载中...')).toHaveCount(0); + await expect(page.locator('text=数据加载失败')).toHaveCount(0); + }); }); diff --git a/src/web/tests/playwright/helpers/entrycards-helpers.ts b/src/web/tests/playwright/helpers/entrycards-helpers.ts index 91ee503..6c6c380 100644 --- a/src/web/tests/playwright/helpers/entrycards-helpers.ts +++ b/src/web/tests/playwright/helpers/entrycards-helpers.ts @@ -1,28 +1,32 @@ import { Page, expect } from '@playwright/test'; import type { metricsEntries } from '../../../src/config/entries'; -export async function testEntryCards(page: Page, entries: typeof metricsEntries, checkLinkNavigation = false) { - for (const entry of entries) { - // 卡片文本可见 - const card = page.locator(`text=${entry.label}`); - await expect(card).toBeVisible(); +export async function testEntryCards( + page: Page, + entries: typeof metricsEntries, + checkLinkNavigation = false +) { + for (const entry of entries) { + // 更具体选择器,直接定位 a 标签包含文本 + const link = page.locator(`a:has-text("${entry.label}")`); + await expect(link).toBeVisible({ timeout: 10000 }); // 等待元素可见 - // 卡片链接正确 - const link = card.locator('..').locator('a'); - await expect(link).toHaveAttribute('href', entry.href); + // href 属性检查 + await expect(link).toHaveAttribute('href', entry.href); - // 图标存在 - const img = card.locator('..').locator('img'); - await expect(img).toBeVisible(); - await expect(img).toHaveAttribute('src', /\/assets\/.+/); + // 图标存在:寻找 a 下的 img + const img = link.locator('img'); + await expect(img).toBeVisible(); + await expect(img).toHaveAttribute('src', /\/assets\/.+/); - if (checkLinkNavigation) { - const [newPage] = await Promise.all([ - page.context().waitForEvent('page'), - link.click(), - ]); - await expect(newPage).toHaveURL(entry.href); - await newPage.close(); + // 可选:点击链接检查导航 + if (checkLinkNavigation) { + const [newPage] = await Promise.all([ + page.context().waitForEvent('page'), + link.click(), + ]); + await expect(newPage).toHaveURL(entry.href); + await newPage.close(); + } } - } } diff --git a/src/web/tests/playwright/logs.spec.ts b/src/web/tests/playwright/logs.spec.ts index 4b72456..35f0f00 100644 --- a/src/web/tests/playwright/logs.spec.ts +++ b/src/web/tests/playwright/logs.spec.ts @@ -1,12 +1,17 @@ import { test, expect } from '@playwright/test'; import { logsEntries } from './test-entries'; import { testEntryCards } from './helpers/entrycards-helpers'; -import { BASE_URL } from './helpers/utils' +import { BASE_URL } from './helpers/utils'; test.describe('Logs Page', () => { - test('should render all log cards', async ({ page }) => { - await page.goto(`${BASE_URL}/logs`); - await expect(page.locator('h2', { hasText: '日志详情' })).toBeVisible(); - await testEntryCards(page, logsEntries); - }); + test('should render all log cards', async ({ page }) => { + await page.goto(`${BASE_URL}/logs`); + + // 等待标题可见 + const title = page.locator('h2', { hasText: '日志详情' }); + await expect(title).toBeVisible({ timeout: 10000 }); + + // 测试所有 log card + await testEntryCards(page, logsEntries); + }); }); diff --git a/src/web/tests/playwright/metric.spec.ts b/src/web/tests/playwright/metric.spec.ts index b9089ec..41bf955 100644 --- a/src/web/tests/playwright/metric.spec.ts +++ b/src/web/tests/playwright/metric.spec.ts @@ -1,13 +1,15 @@ import { test, expect } from '@playwright/test'; import { metricsEntries } from './test-entries'; import { testEntryCards } from './helpers/entrycards-helpers'; -import { BASE_URL } from './helpers/utils' - +import { BASE_URL } from './helpers/utils'; test.describe('Metrics Page', () => { - test('should render all metric cards', async ({ page }) => { - await page.goto(`${BASE_URL}/metrics`); - await expect(page.locator('h2', { hasText: '指标详情' })).toBeVisible(); - await testEntryCards(page, metricsEntries); - }); + test('should render all metric cards', async ({ page }) => { + await page.goto(`${BASE_URL}/metrics`); + + const title = page.locator('h2', { hasText: '指标详情' }); + await expect(title).toBeVisible({ timeout: 10000 }); + + await testEntryCards(page, metricsEntries); + }); }); diff --git a/src/web/tests/playwright/node-info.spec.ts b/src/web/tests/playwright/node-info.spec.ts index 59db616..72874bd 100644 --- a/src/web/tests/playwright/node-info.spec.ts +++ b/src/web/tests/playwright/node-info.spec.ts @@ -1,73 +1,75 @@ -import { test, expect } from "@playwright/test"; -import { BASE_URL } from './helpers/utils' +import {test, expect} from "@playwright/test"; +import {BASE_URL} from './helpers/utils' test.describe("节点信息页面 NodeInfo", () => { - // 每次测试前打开目标页面 - test.beforeEach(async ({ page }) => { + test.beforeEach(async ({page}) => { await page.goto(`${BASE_URL}/node`); }); - test("页面标题应该正确显示", async ({ page }) => { - const title = page.getByRole("heading", { name: "节点信息" }); + test("页面标题应该正确显示", async ({page}) => { + const title = page.locator('h1,h2,h3:has-text("节点信息")').first(); + await title.waitFor({timeout: 10000}); await expect(title).toBeVisible(); }); - test("节点表格应该加载数据", async ({ page }) => { + test("节点表格应该加载数据", async ({page}) => { const rows = page.locator("table tbody tr"); + await rows.first().waitFor({timeout: 10000}); const count = await rows.count(); expect(count).toBeGreaterThan(0); }); - test('节点详情测试', async ({ page }) => { - // 点击第一个节点的“查看详情” + test('节点详情测试', async ({page}) => { const firstDetailBtn = page.locator('text=查看详情').first(); - await firstDetailBtn.click(); + await firstDetailBtn.waitFor({timeout: 10000}); + await firstDetailBtn.scrollIntoViewIfNeeded(); + await firstDetailBtn.click({force: true}); const drawer = page.locator('role=dialog[name="节点详情"]'); + await drawer.waitFor({timeout: 10000}); await expect(drawer).toBeVisible(); // ======================== // 1️⃣ 验证基础信息 // ======================== - await expect(drawer.locator('text=注册时间')).toBeVisible(); - await expect(drawer.locator('text=最近上报时间')).toBeVisible(); - await expect(drawer.locator('text=最后更新时间')).toBeVisible(); + for (const label of ['注册时间', '最近上报时间', '最后更新时间']) { + const el = drawer.locator(`text=${label}`).first(); + await el.waitFor({timeout: 5000}); + await expect(el).toBeVisible(); + } // ======================== // 2️⃣ NodeConfigCard 编辑/保存 // ======================== - const configEditBtn = drawer.locator('div:has-text("配置信息") >> role=button', { hasText: '' }); - await configEditBtn.click(); // 开启编辑 + const configEditBtn = drawer.locator('div:has-text("配置信息") >> role=button').first(); + await configEditBtn.scrollIntoViewIfNeeded(); + await configEditBtn.click({force: true}); const keyInput = drawer.locator('input[placeholder="Key"]').first(); const valueInput = drawer.locator('input[placeholder="Value"]').first(); - await keyInput.fill('testKey'); await valueInput.fill('testValue'); - const saveBtn = drawer.locator('div:has-text("配置信息") >> role=button', { hasText: '' }).filter({ hasText: '' }).first(); + const saveBtn = drawer.locator('div:has-text("配置信息") >> role=button').first(); await saveBtn.click(); - - // 保存后,新配置应该显示在列表中 - await expect(drawer.locator('text=testKey')).toBeVisible(); - await expect(drawer.locator('text=testValue')).toBeVisible(); + await expect(drawer.locator('text=testKey').first()).toBeVisible(); + await expect(drawer.locator('text=testValue').first()).toBeVisible(); // ======================== // 3️⃣ NodeLabelCard 标签管理 // ======================== - const labelEditBtn = drawer.locator('div:has-text("标签信息") >> role=button', { hasText: '' }); - await labelEditBtn.click(); // 开启编辑 + const labelEditBtn = drawer.locator('div:has-text("标签信息") >> role=button').first(); + await labelEditBtn.click({force: true}); const newTagInput = drawer.locator('input[placeholder="新增标签"]'); await newTagInput.fill('newTag1'); - const addTagBtn = drawer.locator('div:has-text("标签信息") >> role=button', { hasText: '' }).nth(1); - await addTagBtn.click(); + const addTagBtn = drawer.locator('div:has-text("标签信息") >> role=button').nth(1); + await addTagBtn.click({force: true}); - // 保存后标签应该显示 - const saveLabelBtn = drawer.locator('div:has-text("标签信息") >> role=button', { hasText: '' }).first(); - await saveLabelBtn.click(); - await expect(drawer.locator('text=newTag1')).toBeVisible(); + const saveLabelBtn = drawer.locator('div:has-text("标签信息") >> role=button').first(); + await saveLabelBtn.click({force: true}); + await expect(drawer.locator('text=newTag1').first()).toBeVisible(); // ======================== // 4️⃣ NodeHealthCard 健康信息展示 @@ -75,23 +77,24 @@ test.describe("节点信息页面 NodeInfo", () => { const healthModule = drawer.locator('div:has-text("健康信息") >> text=healthy').first(); await expect(healthModule).toBeVisible(); - // 可选择打开 Info Popover const infoBtn = drawer.locator('div:has-text("健康信息") >> role=button').first(); - await infoBtn.click(); + await infoBtn.click({force: true}); const errorCount = await drawer.locator('text=Error').count(); expect(errorCount).toBeGreaterThan(0); // ======================== // 5️⃣ Drawer 关闭 // ======================== - const closeBtn = drawer.locator('button[aria-label="Close"]'); - await closeBtn.click(); - await expect(drawer).toHaveCount(0); + const closeBtn = drawer.locator('button[aria-label="Close"]').first(); + await closeBtn.scrollIntoViewIfNeeded(); + await closeBtn.click({force: true}); + await expect(drawer).toBeHidden(); }); }); - -test("Grafana按钮链接应正确", async ({ page }) => { - const grafanaLink = page.getByRole("link", { name: "Grafana" }).first(); +test("Grafana按钮链接应正确", async ({page}) => { + await page.goto(`${BASE_URL}/node`); + const grafanaLink = page.getByRole("link", {name: "Grafana"}).first(); + await grafanaLink.waitFor({timeout: 10000}); await expect(grafanaLink).toHaveAttribute("href", /\/d\/node_gpu_metrics/); });