372 lines
11 KiB
TypeScript
372 lines
11 KiB
TypeScript
import { test, expect } from "../fixtures";
|
||
import { Page } from "@playwright/test";
|
||
|
||
class OnboardingPage {
|
||
page: Page;
|
||
|
||
// Step 0
|
||
step0BrandInput: ReturnType<Page["locator"]>;
|
||
step0CheckButton: ReturnType<Page["getByRole"]>;
|
||
|
||
// Step 1
|
||
step1BrandInput: ReturnType<Page["locator"]>;
|
||
step1StartButton: ReturnType<Page["getByRole"]>;
|
||
step1SkipButton: ReturnType<Page["getByRole"]>;
|
||
|
||
// Step 2
|
||
nextButton: ReturnType<Page["getByRole"]>;
|
||
backButton: ReturnType<Page["getByRole"]>;
|
||
skipStepButton: ReturnType<Page["getByRole"]>;
|
||
|
||
// Step 3
|
||
selectAllButton: ReturnType<Page["getByRole"]>;
|
||
clearAllButton: ReturnType<Page["getByRole"]>;
|
||
|
||
// Progress
|
||
progressSteps: ReturnType<Page["locator"]>;
|
||
|
||
constructor(page: Page) {
|
||
this.page = page;
|
||
|
||
// Step 0: 健康分检测 — input 没有 id,用 placeholder 定位
|
||
this.step0BrandInput = this.page.locator(
|
||
'input[placeholder="输入品牌名称,例如:华为"]',
|
||
);
|
||
this.step0CheckButton = this.page.getByRole("button", {
|
||
name: "检测",
|
||
exact: true,
|
||
});
|
||
|
||
// Step 1: 品牌名称 — input id="brandName"
|
||
this.step1BrandInput = this.page.locator("#brandName");
|
||
this.step1StartButton = this.page.getByRole("button", {
|
||
name: /开始分析/,
|
||
});
|
||
this.step1SkipButton = this.page.getByRole("button", {
|
||
name: /跳过.*Dashboard/,
|
||
});
|
||
|
||
// Step 2 & 3 通用按钮
|
||
this.nextButton = this.page.getByRole("button", { name: /继续/ });
|
||
this.backButton = this.page.getByRole("button", { name: /上一步/ });
|
||
this.skipStepButton = this.page.getByRole("button", {
|
||
name: /跳过此步骤/,
|
||
});
|
||
|
||
// Step 3
|
||
this.selectAllButton = this.page.getByRole("button", { name: "全选" });
|
||
this.clearAllButton = this.page.getByRole("button", { name: "清空" });
|
||
|
||
// 进度指示器
|
||
this.progressSteps = this.page.locator(".flex.flex-1.items-center");
|
||
}
|
||
|
||
async goto() {
|
||
await this.page.goto("/onboarding");
|
||
await this.page.waitForLoadState("networkidle");
|
||
}
|
||
|
||
async goToOnboarding() {
|
||
await this.page.goto("/onboarding");
|
||
await this.page.waitForLoadState("networkidle");
|
||
// 等待 Step 0 页面完全渲染
|
||
await expect(
|
||
this.page.getByRole("heading", { name: /免费检测您的GEO健康分/ }),
|
||
).toBeVisible({ timeout: 15000 });
|
||
}
|
||
|
||
/**
|
||
* 从 Step 0 走到 Step 1。
|
||
* Step 0 的 "检测" 按钮调用后端 API,可能失败。
|
||
* 如果 API 成功,会出现 "注册查看完整报告" 按钮,点击后进入 Step 1。
|
||
* 如果 API 失败,返回 false 表示无法继续。
|
||
*/
|
||
async tryReachStep1(brandName: string): Promise<"reached" | "api-failed"> {
|
||
// 确保在 Step 0
|
||
await expect(
|
||
this.page.getByRole("heading", { name: /免费检测您的GEO健康分/ }),
|
||
).toBeVisible({ timeout: 10000 });
|
||
|
||
await this.step0BrandInput.fill(brandName);
|
||
await this.step0CheckButton.click();
|
||
|
||
// 等待结果或错误
|
||
const resultCard = this.page.getByText("注册查看完整报告");
|
||
const errorAlert = this.page.locator(
|
||
".bg-destructive\\/10.text-destructive",
|
||
);
|
||
|
||
const sawResult = await resultCard
|
||
.isVisible({ timeout: 15000 })
|
||
.catch(() => false);
|
||
if (sawResult) {
|
||
await resultCard.click();
|
||
// 现在应该到 Step 1
|
||
await expect(
|
||
this.page.getByRole("heading", { name: /输入您的品牌名称/ }),
|
||
).toBeVisible({ timeout: 10000 });
|
||
return "reached";
|
||
}
|
||
|
||
const sawError = await errorAlert
|
||
.isVisible({ timeout: 3000 })
|
||
.catch(() => false);
|
||
if (sawError) {
|
||
return "api-failed";
|
||
}
|
||
|
||
// 超时,可能 API 一直没返回
|
||
return "api-failed";
|
||
}
|
||
|
||
/**
|
||
* 从 Step 1 走到 Step 2(确认竞品)
|
||
*/
|
||
async reachStep2(brandName: string) {
|
||
await this.step1BrandInput.fill(brandName);
|
||
await this.step1StartButton.click();
|
||
await expect(
|
||
this.page.getByRole("heading", { name: /确认竞品/ }),
|
||
).toBeVisible({ timeout: 10000 });
|
||
}
|
||
|
||
/**
|
||
* 从 Step 2 走到 Step 3(选择监控平台)
|
||
*/
|
||
async reachStep3() {
|
||
await this.skipStepButton.click();
|
||
await expect(
|
||
this.page.getByRole("heading", { name: /选择监控平台/ }),
|
||
).toBeVisible({ timeout: 10000 });
|
||
}
|
||
}
|
||
|
||
test.describe("新用户引导向导 - Step 0 & Step 1 测试", () => {
|
||
test.beforeEach(async ({ authenticatedPage }) => {
|
||
const onboardingPage = new OnboardingPage(authenticatedPage);
|
||
await onboardingPage.goToOnboarding();
|
||
});
|
||
|
||
test("Step 0: 应该显示健康分检测页面", async ({ authenticatedPage }) => {
|
||
const onboardingPage = new OnboardingPage(authenticatedPage);
|
||
|
||
await expect(
|
||
authenticatedPage.getByRole("heading", { name: /免费检测您的GEO健康分/ }),
|
||
).toBeVisible();
|
||
|
||
await expect(onboardingPage.step0BrandInput).toBeVisible();
|
||
await expect(onboardingPage.step0CheckButton).toBeVisible();
|
||
});
|
||
|
||
test("Step 0: 品牌名称验证 - 太短应显示错误", async ({
|
||
authenticatedPage,
|
||
}) => {
|
||
const onboardingPage = new OnboardingPage(authenticatedPage);
|
||
|
||
await onboardingPage.step0BrandInput.fill("a");
|
||
await onboardingPage.step0CheckButton.click();
|
||
|
||
await expect(
|
||
authenticatedPage.getByText(/请输入至少2个字符/),
|
||
).toBeVisible();
|
||
});
|
||
|
||
test("Step 1: 输入品牌名称后进入下一步", async ({ authenticatedPage }) => {
|
||
const onboardingPage = new OnboardingPage(authenticatedPage);
|
||
|
||
const step0Result = await onboardingPage.tryReachStep1("华为");
|
||
if (step0Result === "api-failed") {
|
||
test.skip();
|
||
return;
|
||
}
|
||
|
||
// 已经在 Step 1
|
||
await onboardingPage.reachStep2("华为");
|
||
await expect(
|
||
authenticatedPage.getByRole("heading", { name: /确认竞品/ }),
|
||
).toBeVisible();
|
||
});
|
||
|
||
test("Step 1: 跳过直接进入Dashboard", async ({ authenticatedPage }) => {
|
||
const onboardingPage = new OnboardingPage(authenticatedPage);
|
||
|
||
const step0Result = await onboardingPage.tryReachStep1("华为");
|
||
if (step0Result === "api-failed") {
|
||
test.skip();
|
||
return;
|
||
}
|
||
|
||
// 在 Step 1 点击跳过
|
||
await onboardingPage.step1SkipButton.click();
|
||
await expect(authenticatedPage).toHaveURL(/\/dashboard/, {
|
||
timeout: 15000,
|
||
});
|
||
});
|
||
});
|
||
|
||
test.describe("新用户引导向导 - Step 2 & Step 3 测试", () => {
|
||
test.beforeEach(async ({ authenticatedPage }) => {
|
||
const onboardingPage = new OnboardingPage(authenticatedPage);
|
||
await onboardingPage.goToOnboarding();
|
||
});
|
||
|
||
test("Step 2: 可以选择竞品并继续", async ({ authenticatedPage }) => {
|
||
const onboardingPage = new OnboardingPage(authenticatedPage);
|
||
|
||
const step0Result = await onboardingPage.tryReachStep1("华为");
|
||
if (step0Result === "api-failed") {
|
||
test.skip();
|
||
return;
|
||
}
|
||
|
||
await onboardingPage.reachStep2("华为");
|
||
|
||
// 等待竞品推荐加载完成
|
||
await authenticatedPage.waitForTimeout(2000);
|
||
|
||
// 尝试选择第一个竞品卡片
|
||
const firstCompetitor = authenticatedPage
|
||
.locator(".border.rounded-lg")
|
||
.filter({ has: authenticatedPage.locator(".flex.h-5.w-5") })
|
||
.first();
|
||
if (await firstCompetitor.isVisible().catch(() => false)) {
|
||
await firstCompetitor.click();
|
||
}
|
||
|
||
await onboardingPage.nextButton.click();
|
||
|
||
await expect(
|
||
authenticatedPage.getByRole("heading", { name: /选择监控平台/ }),
|
||
).toBeVisible({ timeout: 10000 });
|
||
});
|
||
|
||
test("Step 2: 跳过应进入下一步", async ({ authenticatedPage }) => {
|
||
const onboardingPage = new OnboardingPage(authenticatedPage);
|
||
|
||
const step0Result = await onboardingPage.tryReachStep1("华为");
|
||
if (step0Result === "api-failed") {
|
||
test.skip();
|
||
return;
|
||
}
|
||
|
||
await onboardingPage.reachStep2("华为");
|
||
await onboardingPage.skipStepButton.click();
|
||
|
||
await expect(
|
||
authenticatedPage.getByRole("heading", { name: /选择监控平台/ }),
|
||
).toBeVisible({ timeout: 10000 });
|
||
});
|
||
|
||
test("Step 3: 显示已选择平台数量", async ({ authenticatedPage }) => {
|
||
const onboardingPage = new OnboardingPage(authenticatedPage);
|
||
|
||
const step0Result = await onboardingPage.tryReachStep1("华为");
|
||
if (step0Result === "api-failed") {
|
||
test.skip();
|
||
return;
|
||
}
|
||
|
||
await onboardingPage.reachStep2("华为");
|
||
await onboardingPage.reachStep3();
|
||
|
||
await expect(authenticatedPage.getByText(/\d+\/\d+ 个平台/)).toBeVisible();
|
||
});
|
||
|
||
test("Step 3: 可以选择不同频率", async ({ authenticatedPage }) => {
|
||
const onboardingPage = new OnboardingPage(authenticatedPage);
|
||
|
||
const step0Result = await onboardingPage.tryReachStep1("华为");
|
||
if (step0Result === "api-failed") {
|
||
test.skip();
|
||
return;
|
||
}
|
||
|
||
await onboardingPage.reachStep2("华为");
|
||
await onboardingPage.reachStep3();
|
||
|
||
// 频率选项是按钮,在"查询频率"卡片中
|
||
const dailyOption = authenticatedPage
|
||
.getByRole("button", { name: /每日/ })
|
||
.first();
|
||
await dailyOption.click();
|
||
|
||
// 选中后应有 border-primary 样式
|
||
await expect(
|
||
authenticatedPage
|
||
.locator("button.border-primary")
|
||
.filter({ hasText: "每日" }),
|
||
).toBeVisible();
|
||
});
|
||
|
||
test("Step 3: 返回上一步", async ({ authenticatedPage }) => {
|
||
const onboardingPage = new OnboardingPage(authenticatedPage);
|
||
|
||
const step0Result = await onboardingPage.tryReachStep1("华为");
|
||
if (step0Result === "api-failed") {
|
||
test.skip();
|
||
return;
|
||
}
|
||
|
||
await onboardingPage.reachStep2("华为");
|
||
await onboardingPage.reachStep3();
|
||
|
||
await onboardingPage.backButton.click();
|
||
|
||
await expect(
|
||
authenticatedPage.getByRole("heading", { name: /确认竞品/ }),
|
||
).toBeVisible({
|
||
timeout: 10000,
|
||
});
|
||
});
|
||
});
|
||
|
||
test.describe("新用户引导向导 - 进度指示器测试", () => {
|
||
test.beforeEach(async ({ authenticatedPage }) => {
|
||
const onboardingPage = new OnboardingPage(authenticatedPage);
|
||
await onboardingPage.goToOnboarding();
|
||
});
|
||
|
||
test("进度指示器显示6个步骤", async ({ authenticatedPage }) => {
|
||
const onboardingPage = new OnboardingPage(authenticatedPage);
|
||
|
||
await expect(onboardingPage.progressSteps).toHaveCount(6);
|
||
});
|
||
|
||
test("初始应显示Step 0为当前步骤", async ({ authenticatedPage }) => {
|
||
const onboardingPage = new OnboardingPage(authenticatedPage);
|
||
|
||
const firstStepIndicator = authenticatedPage
|
||
.locator(".flex.flex-col.items-center")
|
||
.first();
|
||
await expect(
|
||
firstStepIndicator.locator(".border-primary.bg-primary\\/10"),
|
||
).toBeVisible();
|
||
});
|
||
});
|
||
|
||
test.describe("新用户引导向导 - 响应式设计测试", () => {
|
||
test("移动端视图应该正常显示", async ({ authenticatedPage }) => {
|
||
await authenticatedPage.setViewportSize({ width: 375, height: 667 });
|
||
|
||
const onboardingPage = new OnboardingPage(authenticatedPage);
|
||
await onboardingPage.goToOnboarding();
|
||
|
||
await expect(
|
||
authenticatedPage.getByRole("heading", { name: /免费检测您的GEO健康分/ }),
|
||
).toBeVisible();
|
||
|
||
await expect(onboardingPage.step0CheckButton).toBeVisible();
|
||
});
|
||
|
||
test("平板视图应该正常显示", async ({ authenticatedPage }) => {
|
||
await authenticatedPage.setViewportSize({ width: 768, height: 1024 });
|
||
|
||
const onboardingPage = new OnboardingPage(authenticatedPage);
|
||
await onboardingPage.goToOnboarding();
|
||
|
||
await expect(
|
||
authenticatedPage.getByRole("heading", { name: /免费检测您的GEO健康分/ }),
|
||
).toBeVisible();
|
||
});
|
||
});
|