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';