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
#=============================
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."

View File

@ -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'],

View File

@ -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();

View File

@ -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);
});
});

View File

@ -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();
}
}
}
}

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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/);
});