dev_1.0.0_xuxt_3 完成web和alert的集成测试 #38

Merged
xuxt merged 12 commits from dev_1.0.0_xuxt_3 into dev_1.0.0 2025-10-31 14:18:20 +08:00
8 changed files with 153 additions and 137 deletions
Showing only changes of commit 8a50657b43 - Show all commits

View File

@ -54,7 +54,7 @@ done
#============================= #=============================
# Step 2: Run Playwright tests # 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" cd "$WEB_DIR"
@ -70,9 +70,9 @@ npx playwright install --with-deps > /dev/null
# Clean previous reports # Clean previous reports
rm -rf "$REPORT_DIR" rm -rf "$REPORT_DIR"
# Run Playwright tests with reporters # Run Playwright tests wrapped with xvfb-run to avoid GUI
set +e # temporarily disable exit-on-error to capture test result set +e # temporarily disable exit-on-error
BASE_URL=${FRONTEND_URL} npx playwright test tests/playwright --reporter=list env BASE_URL="$FRONTEND_URL" xvfb-run --auto-servernum npx playwright test tests/playwright --reporter=list
TEST_RESULT=$? TEST_RESULT=$?
set -e # re-enable strict mode set -e # re-enable strict mode
@ -100,4 +100,4 @@ else
log_warn "Report directory not found. Check Playwright execution logs." log_warn "Report directory not found. Check Playwright execution logs."
fi fi
log_success "Web frontend verify success. Playwright automated tests passed." log_success "Web frontend verify finished."

View File

@ -10,7 +10,16 @@ export default defineConfig({
viewport: { width: 1280, height: 720 }, viewport: { width: 1280, height: 720 },
ignoreHTTPSErrors: true, ignoreHTTPSErrors: true,
screenshot: 'only-on-failure', 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: [ reporter: [
['list'], ['list'],

View File

@ -7,17 +7,17 @@ test.describe("Alerts 页面功能测试", () => {
}); });
test("页面加载并显示告警统计", async ({page}) => { test("页面加载并显示告警统计", async ({page}) => {
await expect(page.locator("text=告警详情")).toBeVisible(); await expect(page.locator("text=告警详情").first()).toBeVisible();
await expect(page.locator("text=总数")).toBeVisible(); await expect(page.locator("text=总数").first()).toBeVisible();
await expect(page.locator("text=严重")).toBeVisible(); await expect(page.locator("text=严重").first()).toBeVisible();
await expect(page.locator("text=警告")).toBeVisible(); await expect(page.locator("text=警告").first()).toBeVisible();
await expect(page.locator("text=信息")).toBeVisible(); await expect(page.locator("text=信息").first()).toBeVisible();
}); });
test("筛选功能验证", async ({page}) => { test("筛选功能验证", async ({page}) => {
const severitySelect = page.getByLabel("严重性"); const severitySelect = page.getByRole('combobox', {name: '严重性'});
const stateSelect = page.getByLabel("状态"); const stateSelect = page.getByRole('combobox', {name: '状态'});
const nodeSelect = page.getByLabel("节点"); const nodeSelect = page.getByRole('combobox', {name: '节点'});
await severitySelect.selectOption("critical"); await severitySelect.selectOption("critical");
await expect(severitySelect).toHaveValue("critical"); await expect(severitySelect).toHaveValue("critical");
@ -30,18 +30,18 @@ test.describe("Alerts 页面功能测试", () => {
}); });
test("排序功能", async ({page}) => { 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(); // 切换升序
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();
await instanceHeader.click(); await instanceHeader.click();
}); });
test("分页功能", async ({page}) => { test("分页功能", async ({page}) => {
const nextButton = page.locator("button:has-text('下一页')"); const nextButton = page.locator("button:has-text('下一页')").first();
const prevButton = page.locator("button:has-text('上一页')"); const prevButton = page.locator("button:has-text('上一页')").first();
if (await nextButton.isEnabled()) { if (await nextButton.isEnabled()) {
await nextButton.click(); await nextButton.click();
@ -61,8 +61,8 @@ test.describe("Alerts 页面功能测试", () => {
}); });
test("自动刷新开关与刷新按钮", async ({page}) => { test("自动刷新开关与刷新按钮", async ({page}) => {
const switchBtn = page.getByRole("switch", {name: "自动刷新"}); const switchBtn = page.locator("div[role='switch']").first();
const refreshBtn = page.getByTitle("刷新"); const refreshBtn = page.getByTitle("刷新").first();
await expect(switchBtn).toBeVisible(); await expect(switchBtn).toBeVisible();
await expect(refreshBtn).toBeVisible(); await expect(refreshBtn).toBeVisible();

View File

@ -9,7 +9,7 @@ test.describe('Dashboard 页面测试', () => {
}); });
test('应能成功加载页面并显示标题', async ({page}) => { test('应能成功加载页面并显示标题', async ({page}) => {
await expect(page.locator('text=仪表盘')).toBeVisible(); await expect(page.locator('text=仪表盘').first()).toBeVisible();
}); });
test('应显示节点健康状态卡片', async ({page}) => { test('应显示节点健康状态卡片', async ({page}) => {
@ -29,14 +29,14 @@ test.describe('Dashboard 页面测试', () => {
// 检查告警类别 // 检查告警类别
const labels = ['总数', '严重', '警告', '信息']; const labels = ['总数', '严重', '警告', '信息'];
for (const label of labels) { for (const label of labels) {
await expect(page.locator(`text=${label}`)).toBeVisible(); await expect(page.locator(`text=${label}`).first()).toBeVisible();
} }
}); });
test('应正确渲染集群节点表格', async ({page}) => { test('应正确渲染集群节点表格', async ({page}) => {
const tableHeaders = ['ID', '名称', '状态', '类型', '版本']; const tableHeaders = ['ID', '名称', '状态', '类型', '版本'];
for (const header of tableHeaders) { for (const header of tableHeaders) {
await expect(page.locator(`th:has-text("${header}")`)).toBeVisible(); await expect(page.locator(`th:has-text("${header}")`).first()).toBeVisible();
} }
// 至少有一行节点数据 // 至少有一行节点数据
@ -44,13 +44,6 @@ test.describe('Dashboard 页面测试', () => {
expect(rows).toBeGreaterThan(0); 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}) => { test('页面应无加载错误提示', async ({page}) => {
await expect(page.locator('text=加载中...')).toHaveCount(0); await expect(page.locator('text=加载中...')).toHaveCount(0);
await expect(page.locator('text=数据加载失败')).toHaveCount(0); await expect(page.locator('text=数据加载失败')).toHaveCount(0);

View File

@ -1,21 +1,25 @@
import { Page, expect } from '@playwright/test'; import { Page, expect } from '@playwright/test';
import type { metricsEntries } from '../../../src/config/entries'; import type { metricsEntries } from '../../../src/config/entries';
export async function testEntryCards(page: Page, entries: typeof metricsEntries, checkLinkNavigation = false) { export async function testEntryCards(
page: Page,
entries: typeof metricsEntries,
checkLinkNavigation = false
) {
for (const entry of entries) { for (const entry of entries) {
// 卡片文本可见 // 更具体选择器,直接定位 a 标签包含文本
const card = page.locator(`text=${entry.label}`); const link = page.locator(`a:has-text("${entry.label}")`);
await expect(card).toBeVisible(); await expect(link).toBeVisible({ timeout: 10000 }); // 等待元素可见
// 卡片链接正确 // href 属性检查
const link = card.locator('..').locator('a');
await expect(link).toHaveAttribute('href', entry.href); await expect(link).toHaveAttribute('href', entry.href);
// 图标存在 // 图标存在:寻找 a 下的 img
const img = card.locator('..').locator('img'); const img = link.locator('img');
await expect(img).toBeVisible(); await expect(img).toBeVisible();
await expect(img).toHaveAttribute('src', /\/assets\/.+/); await expect(img).toHaveAttribute('src', /\/assets\/.+/);
// 可选:点击链接检查导航
if (checkLinkNavigation) { if (checkLinkNavigation) {
const [newPage] = await Promise.all([ const [newPage] = await Promise.all([
page.context().waitForEvent('page'), page.context().waitForEvent('page'),

View File

@ -1,12 +1,17 @@
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
import { logsEntries } from './test-entries'; import { logsEntries } from './test-entries';
import { testEntryCards } from './helpers/entrycards-helpers'; import { testEntryCards } from './helpers/entrycards-helpers';
import { BASE_URL } from './helpers/utils' import { BASE_URL } from './helpers/utils';
test.describe('Logs Page', () => { test.describe('Logs Page', () => {
test('should render all log cards', async ({ page }) => { test('should render all log cards', async ({ page }) => {
await page.goto(`${BASE_URL}/logs`); await page.goto(`${BASE_URL}/logs`);
await expect(page.locator('h2', { hasText: '日志详情' })).toBeVisible();
// 等待标题可见
const title = page.locator('h2', { hasText: '日志详情' });
await expect(title).toBeVisible({ timeout: 10000 });
// 测试所有 log card
await testEntryCards(page, logsEntries); await testEntryCards(page, logsEntries);
}); });
}); });

View File

@ -1,13 +1,15 @@
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
import { metricsEntries } from './test-entries'; import { metricsEntries } from './test-entries';
import { testEntryCards } from './helpers/entrycards-helpers'; import { testEntryCards } from './helpers/entrycards-helpers';
import { BASE_URL } from './helpers/utils' import { BASE_URL } from './helpers/utils';
test.describe('Metrics Page', () => { test.describe('Metrics Page', () => {
test('should render all metric cards', async ({ page }) => { test('should render all metric cards', async ({ page }) => {
await page.goto(`${BASE_URL}/metrics`); await page.goto(`${BASE_URL}/metrics`);
await expect(page.locator('h2', { hasText: '指标详情' })).toBeVisible();
const title = page.locator('h2', { hasText: '指标详情' });
await expect(title).toBeVisible({ timeout: 10000 });
await testEntryCards(page, metricsEntries); await testEntryCards(page, metricsEntries);
}); });
}); });

View File

@ -2,72 +2,74 @@ import { test, expect } from "@playwright/test";
import {BASE_URL} from './helpers/utils' import {BASE_URL} from './helpers/utils'
test.describe("节点信息页面 NodeInfo", () => { test.describe("节点信息页面 NodeInfo", () => {
// 每次测试前打开目标页面
test.beforeEach(async ({page}) => { test.beforeEach(async ({page}) => {
await page.goto(`${BASE_URL}/node`); await page.goto(`${BASE_URL}/node`);
}); });
test("页面标题应该正确显示", async ({page}) => { test("页面标题应该正确显示", async ({page}) => {
const title = page.getByRole("heading", { name: "节点信息" }); const title = page.locator('h1,h2,h3:has-text("节点信息")').first();
await title.waitFor({timeout: 10000});
await expect(title).toBeVisible(); await expect(title).toBeVisible();
}); });
test("节点表格应该加载数据", async ({page}) => { test("节点表格应该加载数据", async ({page}) => {
const rows = page.locator("table tbody tr"); const rows = page.locator("table tbody tr");
await rows.first().waitFor({timeout: 10000});
const count = await rows.count(); const count = await rows.count();
expect(count).toBeGreaterThan(0); expect(count).toBeGreaterThan(0);
}); });
test('节点详情测试', async ({page}) => { test('节点详情测试', async ({page}) => {
// 点击第一个节点的“查看详情”
const firstDetailBtn = page.locator('text=查看详情').first(); 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="节点详情"]'); const drawer = page.locator('role=dialog[name="节点详情"]');
await drawer.waitFor({timeout: 10000});
await expect(drawer).toBeVisible(); await expect(drawer).toBeVisible();
// ======================== // ========================
// 1⃣ 验证基础信息 // 1⃣ 验证基础信息
// ======================== // ========================
await expect(drawer.locator('text=注册时间')).toBeVisible(); for (const label of ['注册时间', '最近上报时间', '最后更新时间']) {
await expect(drawer.locator('text=最近上报时间')).toBeVisible(); const el = drawer.locator(`text=${label}`).first();
await expect(drawer.locator('text=最后更新时间')).toBeVisible(); await el.waitFor({timeout: 5000});
await expect(el).toBeVisible();
}
// ======================== // ========================
// 2⃣ NodeConfigCard 编辑/保存 // 2⃣ NodeConfigCard 编辑/保存
// ======================== // ========================
const configEditBtn = drawer.locator('div:has-text("配置信息") >> role=button', { hasText: '' }); const configEditBtn = drawer.locator('div:has-text("配置信息") >> role=button').first();
await configEditBtn.click(); // 开启编辑 await configEditBtn.scrollIntoViewIfNeeded();
await configEditBtn.click({force: true});
const keyInput = drawer.locator('input[placeholder="Key"]').first(); const keyInput = drawer.locator('input[placeholder="Key"]').first();
const valueInput = drawer.locator('input[placeholder="Value"]').first(); const valueInput = drawer.locator('input[placeholder="Value"]').first();
await keyInput.fill('testKey'); await keyInput.fill('testKey');
await valueInput.fill('testValue'); 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 saveBtn.click();
await expect(drawer.locator('text=testKey').first()).toBeVisible();
// 保存后,新配置应该显示在列表中 await expect(drawer.locator('text=testValue').first()).toBeVisible();
await expect(drawer.locator('text=testKey')).toBeVisible();
await expect(drawer.locator('text=testValue')).toBeVisible();
// ======================== // ========================
// 3⃣ NodeLabelCard 标签管理 // 3⃣ NodeLabelCard 标签管理
// ======================== // ========================
const labelEditBtn = drawer.locator('div:has-text("标签信息") >> role=button', { hasText: '' }); const labelEditBtn = drawer.locator('div:has-text("标签信息") >> role=button').first();
await labelEditBtn.click(); // 开启编辑 await labelEditBtn.click({force: true});
const newTagInput = drawer.locator('input[placeholder="新增标签"]'); const newTagInput = drawer.locator('input[placeholder="新增标签"]');
await newTagInput.fill('newTag1'); await newTagInput.fill('newTag1');
const addTagBtn = drawer.locator('div:has-text("标签信息") >> role=button', { hasText: '' }).nth(1); const addTagBtn = drawer.locator('div:has-text("标签信息") >> role=button').nth(1);
await addTagBtn.click(); await addTagBtn.click({force: true});
// 保存后标签应该显示 const saveLabelBtn = drawer.locator('div:has-text("标签信息") >> role=button').first();
const saveLabelBtn = drawer.locator('div:has-text("标签信息") >> role=button', { hasText: '' }).first(); await saveLabelBtn.click({force: true});
await saveLabelBtn.click(); await expect(drawer.locator('text=newTag1').first()).toBeVisible();
await expect(drawer.locator('text=newTag1')).toBeVisible();
// ======================== // ========================
// 4⃣ NodeHealthCard 健康信息展示 // 4⃣ NodeHealthCard 健康信息展示
@ -75,23 +77,24 @@ test.describe("节点信息页面 NodeInfo", () => {
const healthModule = drawer.locator('div:has-text("健康信息") >> text=healthy').first(); const healthModule = drawer.locator('div:has-text("健康信息") >> text=healthy').first();
await expect(healthModule).toBeVisible(); await expect(healthModule).toBeVisible();
// 可选择打开 Info Popover
const infoBtn = drawer.locator('div:has-text("健康信息") >> role=button').first(); 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(); const errorCount = await drawer.locator('text=Error').count();
expect(errorCount).toBeGreaterThan(0); expect(errorCount).toBeGreaterThan(0);
// ======================== // ========================
// 5⃣ Drawer 关闭 // 5⃣ Drawer 关闭
// ======================== // ========================
const closeBtn = drawer.locator('button[aria-label="Close"]'); const closeBtn = drawer.locator('button[aria-label="Close"]').first();
await closeBtn.click(); await closeBtn.scrollIntoViewIfNeeded();
await expect(drawer).toHaveCount(0); await closeBtn.click({force: true});
await expect(drawer).toBeHidden();
}); });
}); });
test("Grafana按钮链接应正确", async ({page}) => { test("Grafana按钮链接应正确", async ({page}) => {
await page.goto(`${BASE_URL}/node`);
const grafanaLink = page.getByRole("link", {name: "Grafana"}).first(); const grafanaLink = page.getByRole("link", {name: "Grafana"}).first();
await grafanaLink.waitFor({timeout: 10000});
await expect(grafanaLink).toHaveAttribute("href", /\/d\/node_gpu_metrics/); await expect(grafanaLink).toHaveAttribute("href", /\/d\/node_gpu_metrics/);
}); });