From cda168cf6028ab0018fc60501289ab64dca67ac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E7=BF=94?= Date: Thu, 9 Apr 2026 13:17:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9B=E5=BB=BAPage=20Object=20Model?= =?UTF-8?q?=E5=9F=BA=E7=A1=80=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增文件: - e2e/pages/AdminLoginPage.ts - 管理员登录页面对象 - e2e/pages/AdminContentPage.ts - 内容管理页面对象 - e2e/pages/AdminUserPage.ts - 用户管理页面对象 - e2e/pages/FrontendNewsPage.ts - 前端新闻页面对象 - e2e/pages/FrontendProductPage.ts - 前端产品页面对象 - e2e/pages/index.ts - 导出索引文件 功能特性: - 封装页面交互逻辑,减少测试代码重复 - 提供清晰的API接口,提升测试可读性 - 支持内容创建、删除、验证等核心操作 - 统一等待策略,提升测试稳定性 --- e2e/pages/AdminContentPage.ts | 85 ++++++++++++++++++++++++++++++++ e2e/pages/AdminLoginPage.ts | 25 ++++++++++ e2e/pages/AdminUserPage.ts | 39 +++++++++++++++ e2e/pages/FrontendNewsPage.ts | 29 +++++++++++ e2e/pages/FrontendProductPage.ts | 20 ++++++++ e2e/pages/index.ts | 5 ++ 6 files changed, 203 insertions(+) create mode 100644 e2e/pages/AdminContentPage.ts create mode 100644 e2e/pages/AdminLoginPage.ts create mode 100644 e2e/pages/AdminUserPage.ts create mode 100644 e2e/pages/FrontendNewsPage.ts create mode 100644 e2e/pages/FrontendProductPage.ts create mode 100644 e2e/pages/index.ts diff --git a/e2e/pages/AdminContentPage.ts b/e2e/pages/AdminContentPage.ts new file mode 100644 index 0000000..dabb64f --- /dev/null +++ b/e2e/pages/AdminContentPage.ts @@ -0,0 +1,85 @@ +import { Page, expect } from '@playwright/test'; + +export interface ContentData { + type: 'news' | 'product' | 'service' | 'case'; + title: string; + slug: string; + excerpt?: string; + content?: string; + category?: string; + tags?: string[]; + status?: 'draft' | 'published' | 'archived'; +} + +export class AdminContentPage { + constructor(private page: Page) {} + + async goto() { + await this.page.goto('/admin/content'); + await this.page.waitForLoadState('networkidle'); + } + + async gotoCreate() { + await this.page.goto('/admin/content/new'); + await this.page.waitForLoadState('domcontentloaded'); + await this.page.waitForSelector('input[placeholder="请输入标题"]', { timeout: 60000 }); + } + + async createContent(data: ContentData): Promise { + await this.gotoCreate(); + + await this.page.fill('input[placeholder="请输入标题"]', data.title); + await this.page.fill('input[placeholder="url-slug"]', data.slug); + + if (data.excerpt) { + await this.page.fill('textarea', data.excerpt); + } + + if (data.type) { + await this.page.locator('select').first().selectOption(data.type); + } + + if (data.status) { + await this.page.locator('select').nth(1).selectOption(data.status); + } + + if (data.category) { + await this.page.fill('input[placeholder="分类名称"]', data.category); + } + + await this.page.click('button:has-text("发布")'); + + await this.page.waitForURL(/\/admin\/content\/[a-zA-Z0-9]+/, { timeout: 15000 }); + + const url = this.page.url(); + const match = url.match(/\/admin\/content\/([a-zA-Z0-9]+)/); + return match ? match[1] : null; + } + + async deleteContent(contentId: string) { + await this.goto(); + const row = this.page.locator(`tr:has-text("${contentId}")`); + + if (await row.count() > 0) { + await row.locator('button:has-text("删除")').click(); + await this.page.locator('button:has-text("确认"), button:has-text("确定")').click(); + await this.page.waitForResponse(resp => + resp.url().includes('/api/admin/content') && + resp.request().method() === 'DELETE', + { timeout: 10000 } + ); + } + } + + async expectContentInList(title: string) { + await this.goto(); + const row = this.page.locator(`tr:has-text("${title}")`); + await expect(row).toBeVisible(); + } + + async expectContentNotInList(title: string) { + await this.goto(); + const row = this.page.locator(`tr:has-text("${title}")`); + await expect(row).not.toBeVisible(); + } +} diff --git a/e2e/pages/AdminLoginPage.ts b/e2e/pages/AdminLoginPage.ts new file mode 100644 index 0000000..73519c6 --- /dev/null +++ b/e2e/pages/AdminLoginPage.ts @@ -0,0 +1,25 @@ +import { Page, expect } from '@playwright/test'; + +export class AdminLoginPage { + constructor(private page: Page) {} + + async goto() { + await this.page.goto('/admin/login'); + await this.page.waitForLoadState('networkidle'); + } + + async login(email: string, password: string) { + await this.page.fill('#email', email); + await this.page.fill('#password', password); + await this.page.click('button[type="submit"]'); + await this.page.waitForURL(/\/admin(?!\/login)/); + } + + async expectLoginSuccess() { + await expect(this.page).toHaveURL(/\/admin(?!\/login)/); + } + + async expectLoginError() { + await expect(this.page.locator('[role="alert"]')).toBeVisible(); + } +} diff --git a/e2e/pages/AdminUserPage.ts b/e2e/pages/AdminUserPage.ts new file mode 100644 index 0000000..e41727d --- /dev/null +++ b/e2e/pages/AdminUserPage.ts @@ -0,0 +1,39 @@ +import { Page, expect } from '@playwright/test'; + +export interface UserData { + email: string; + password: string; + name?: string; + role?: 'admin' | 'editor' | 'viewer'; +} + +export class AdminUserPage { + constructor(private page: Page) {} + + async goto() { + await this.page.goto('/admin/users'); + await this.page.waitForLoadState('networkidle'); + } + + async createUser(data: UserData) { + await this.page.click('button:has-text("新建用户")'); + await this.page.fill('input[name="email"]', data.email); + await this.page.fill('input[name="password"]', data.password); + + if (data.name) { + await this.page.fill('input[name="name"]', data.name); + } + + if (data.role) { + await this.page.selectOption('select[name="role"]', data.role); + } + + await this.page.click('button[type="submit"]'); + } + + async expectUserInList(email: string) { + await this.goto(); + const row = this.page.locator(`tr:has-text("${email}")`); + await expect(row).toBeVisible(); + } +} diff --git a/e2e/pages/FrontendNewsPage.ts b/e2e/pages/FrontendNewsPage.ts new file mode 100644 index 0000000..c55c4fc --- /dev/null +++ b/e2e/pages/FrontendNewsPage.ts @@ -0,0 +1,29 @@ +import { Page, expect } from '@playwright/test'; + +export class FrontendNewsPage { + constructor(private page: Page) {} + + async goto() { + await this.page.goto('/news'); + await this.page.waitForLoadState('networkidle'); + } + + async expectNewsVisible(title: string) { + const newsCard = this.page.locator(`text="${title}"`); + await expect(newsCard).toBeVisible(); + } + + async expectNewsNotVisible(title: string) { + const newsCard = this.page.locator(`text="${title}"`); + await expect(newsCard).not.toBeVisible(); + } + + async clickNews(title: string) { + await this.page.locator(`text="${title}"`).click(); + await this.page.waitForLoadState('networkidle'); + } + + async expectNewsDetailVisible(content: string) { + await expect(this.page.locator(`text=${content}`)).toBeVisible(); + } +} diff --git a/e2e/pages/FrontendProductPage.ts b/e2e/pages/FrontendProductPage.ts new file mode 100644 index 0000000..eebf66c --- /dev/null +++ b/e2e/pages/FrontendProductPage.ts @@ -0,0 +1,20 @@ +import { Page, expect } from '@playwright/test'; + +export class FrontendProductPage { + constructor(private page: Page) {} + + async goto() { + await this.page.goto('/products'); + await this.page.waitForLoadState('networkidle'); + } + + async expectProductVisible(title: string) { + const productCard = this.page.locator(`text="${title}"`); + await expect(productCard).toBeVisible(); + } + + async clickProduct(title: string) { + await this.page.locator(`text="${title}"`).click(); + await this.page.waitForLoadState('networkidle'); + } +} diff --git a/e2e/pages/index.ts b/e2e/pages/index.ts new file mode 100644 index 0000000..668403c --- /dev/null +++ b/e2e/pages/index.ts @@ -0,0 +1,5 @@ +export { AdminLoginPage } from './AdminLoginPage'; +export { AdminContentPage, type ContentData } from './AdminContentPage'; +export { AdminUserPage, type UserData } from './AdminUserPage'; +export { FrontendNewsPage } from './FrontendNewsPage'; +export { FrontendProductPage } from './FrontendProductPage';