R28-1 R28-4: 收口网页门禁与公网 smoke
这个提交包含在:
+27
-61
@@ -2,6 +2,9 @@ name: ci-e2e
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "10 19 * * *"
|
||||
@@ -9,7 +12,7 @@ on:
|
||||
jobs:
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
timeout-minutes: 60
|
||||
|
||||
services:
|
||||
postgres:
|
||||
@@ -27,24 +30,26 @@ jobs:
|
||||
--health-retries=10
|
||||
|
||||
env:
|
||||
NODE_ENV: test
|
||||
PORT: 3000
|
||||
HOST: 127.0.0.1
|
||||
APP_ORIGIN: http://127.0.0.1:3000
|
||||
DATABASE_URL: postgresql://hyboa_user:[email protected]:5432/hyb_oa?schema=public
|
||||
JWT_SECRET: ci_e2e_secret_key
|
||||
OCR_PROVIDER: mock
|
||||
OCR_MAX_ATTEMPTS: 3
|
||||
OCR_POLL_CRON: "*/5 * * * * *"
|
||||
DELEGATION_REVOKE_CRON: "0 * * * *"
|
||||
SERVE_CLIENT_DIST: "true"
|
||||
CLIENT_DIST_DIR: "../client/dist"
|
||||
E2E_BASE_URL: http://127.0.0.1:3000
|
||||
QA_DB_HOST: 127.0.0.1
|
||||
QA_DB_PORT: 5432
|
||||
QA_DB_USER: hyboa_user
|
||||
QA_DB_PASSWORD: hyboa_password
|
||||
QA_DB_ADMIN_DB: postgres
|
||||
QA_PORT: 3000
|
||||
QA_HOST: 127.0.0.1
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Select gate mode
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
echo "WEB_GATE_MODE=smoke-local" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "WEB_GATE_MODE=full-local" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -62,61 +67,22 @@ jobs:
|
||||
working-directory: client
|
||||
run: npm ci
|
||||
|
||||
- name: Migrate database
|
||||
working-directory: server
|
||||
run: npm run prisma:migrate:deploy
|
||||
|
||||
- name: Seed database
|
||||
working-directory: server
|
||||
run: npm run seed:dev
|
||||
|
||||
- name: Build client
|
||||
working-directory: client
|
||||
run: npm run build
|
||||
- name: Install PostgreSQL client
|
||||
run: sudo apt-get update && sudo apt-get install -y postgresql-client
|
||||
|
||||
- name: Install Playwright browsers
|
||||
working-directory: client
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
- name: Start server
|
||||
working-directory: server
|
||||
run: |
|
||||
nohup npm run dev > /tmp/hyb-oa-server.log 2>&1 &
|
||||
echo $! > /tmp/hyb-oa-server.pid
|
||||
|
||||
- name: Wait for ready
|
||||
run: |
|
||||
for i in {1..45}; do
|
||||
if curl -fsS "http://127.0.0.1:3000/ready" >/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "Server readiness check timed out" >&2
|
||||
exit 1
|
||||
|
||||
- name: Run Playwright E2E
|
||||
working-directory: client
|
||||
run: npm run test:e2e
|
||||
|
||||
- name: Stop server
|
||||
if: always()
|
||||
run: |
|
||||
if [[ -f /tmp/hyb-oa-server.pid ]]; then
|
||||
kill "$(cat /tmp/hyb-oa-server.pid)" || true
|
||||
fi
|
||||
|
||||
- name: Dump server log on failure
|
||||
if: failure()
|
||||
run: |
|
||||
echo "==== /tmp/hyb-oa-server.log ===="
|
||||
cat /tmp/hyb-oa-server.log || true
|
||||
- name: Run web gate
|
||||
run: bash scripts/qa/run-web-gates.sh "$WEB_GATE_MODE"
|
||||
|
||||
- name: Upload Playwright report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-report
|
||||
path: client/playwright-report
|
||||
name: playwright-report-${{ env.WEB_GATE_MODE }}-${{ github.run_id }}
|
||||
path: |
|
||||
client/playwright-report
|
||||
client/test-results
|
||||
if-no-files-found: ignore
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
name: ci-live-smoke
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "20 20 * * *"
|
||||
|
||||
jobs:
|
||||
live-smoke:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
LIVE_SMOKE_BASE_URL: ${{ secrets.LIVE_SMOKE_BASE_URL }}
|
||||
LIVE_SMOKE_ADMIN_IDENTIFIER: ${{ secrets.LIVE_SMOKE_ADMIN_IDENTIFIER }}
|
||||
LIVE_SMOKE_ADMIN_PASSWORD: ${{ secrets.LIVE_SMOKE_ADMIN_PASSWORD }}
|
||||
LIVE_SMOKE_EMPLOYEE_IDENTIFIER: ${{ secrets.LIVE_SMOKE_EMPLOYEE_IDENTIFIER }}
|
||||
LIVE_SMOKE_EMPLOYEE_PASSWORD: ${{ secrets.LIVE_SMOKE_EMPLOYEE_PASSWORD }}
|
||||
LIVE_EXPECT_WECOM_STATUS: ${{ secrets.LIVE_EXPECT_WECOM_STATUS }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: npm
|
||||
cache-dependency-path: client/package-lock.json
|
||||
|
||||
- name: Install client dependencies
|
||||
working-directory: client
|
||||
run: npm ci
|
||||
|
||||
- name: Install Playwright browsers
|
||||
working-directory: client
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
- name: Run live smoke
|
||||
run: bash scripts/qa/run-web-gates.sh smoke-live
|
||||
|
||||
- name: Upload Playwright report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: live-smoke-report-${{ github.run_id }}
|
||||
path: |
|
||||
client/playwright-report
|
||||
client/test-results
|
||||
if-no-files-found: ignore
|
||||
@@ -8,6 +8,9 @@
|
||||
"build": "npm run typecheck && vite build",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:smoke": "playwright test tests/e2e/login-portal-ux.spec.ts tests/e2e/wecom-entry.spec.ts",
|
||||
"test:e2e:full": "playwright test",
|
||||
"test:e2e:live": "playwright test tests/e2e/live-smoke.spec.ts",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { defineConfig } from "@playwright/test";
|
||||
|
||||
const baseURL = process.env.E2E_BASE_URL ?? "http://127.0.0.1:3000";
|
||||
const e2eMode = process.env.E2E_MODE === "live" ? "live" : "local";
|
||||
const baseURL = e2eMode === "live"
|
||||
? process.env.LIVE_SMOKE_BASE_URL?.trim() || "https://oa.hyinda.com"
|
||||
: process.env.E2E_BASE_URL?.trim() || "http://127.0.0.1:3000";
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests/e2e",
|
||||
@@ -18,4 +21,3 @@ export default defineConfig({
|
||||
},
|
||||
reporter: process.env.CI ? [["github"], ["html", { open: "never" }]] : [["list"], ["html"]],
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
|
||||
import { logout } from "./auth";
|
||||
|
||||
export type LiveSmokeRole = "admin" | "employee";
|
||||
|
||||
export type LiveSmokeUser = {
|
||||
role: LiveSmokeRole;
|
||||
identifier: string;
|
||||
password: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
function requireEnv(key: string) {
|
||||
const value = process.env[key]?.trim();
|
||||
if (!value) {
|
||||
throw new Error(`缺少 live smoke 环境变量:${key}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function getExpectedWecomStatus() {
|
||||
return process.env.LIVE_EXPECT_WECOM_STATUS?.trim() || "disabled";
|
||||
}
|
||||
|
||||
export function getLiveSmokeUser(role: LiveSmokeRole) {
|
||||
if (role === "admin") {
|
||||
return {
|
||||
role,
|
||||
identifier: requireEnv("LIVE_SMOKE_ADMIN_IDENTIFIER"),
|
||||
password: requireEnv("LIVE_SMOKE_ADMIN_PASSWORD"),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
role,
|
||||
identifier: requireEnv("LIVE_SMOKE_EMPLOYEE_IDENTIFIER"),
|
||||
password: requireEnv("LIVE_SMOKE_EMPLOYEE_PASSWORD"),
|
||||
};
|
||||
}
|
||||
|
||||
async function readCurrentUserName(page: Page): Promise<string> {
|
||||
const profileTrigger = page.getByTestId("topbar-profile-trigger").first();
|
||||
await expect(profileTrigger).toBeVisible();
|
||||
const name = (await profileTrigger.textContent())?.trim();
|
||||
if (!name) {
|
||||
throw new Error("无法从顶栏读取当前登录用户名称");
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
export async function loginLiveUser(page: Page, role: LiveSmokeRole): Promise<LiveSmokeUser> {
|
||||
const credentials = getLiveSmokeUser(role);
|
||||
|
||||
await page.goto("/login");
|
||||
await expect(page.getByTestId("login-identifier")).toBeVisible();
|
||||
await page.getByTestId("login-identifier").fill(credentials.identifier);
|
||||
await page.getByTestId("login-password").fill(credentials.password);
|
||||
await page.getByTestId("login-submit").click();
|
||||
await expect(page).toHaveURL(/\/portal/);
|
||||
|
||||
const name = await readCurrentUserName(page);
|
||||
|
||||
return {
|
||||
...credentials,
|
||||
name,
|
||||
};
|
||||
}
|
||||
|
||||
export async function logoutLiveUser(page: Page, user?: Pick<LiveSmokeUser, "name">) {
|
||||
await logout(page, user?.name);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
|
||||
export type WecomEntryConfig = {
|
||||
enabled: boolean;
|
||||
entryEnabled: boolean;
|
||||
oauthEnabled: boolean;
|
||||
corpId: string;
|
||||
agentId: string;
|
||||
loginUrl: string | null;
|
||||
bindEnabled: boolean;
|
||||
defaultReturnTo: string;
|
||||
status: string;
|
||||
statusReasons: string[];
|
||||
};
|
||||
|
||||
type Envelope<T> = {
|
||||
data: T;
|
||||
error?: {
|
||||
code?: string;
|
||||
message?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export async function assertPortalLoaded(page: Page) {
|
||||
await expect(page).toHaveURL(/\/portal/);
|
||||
await expect(page.getByRole("heading", { name: "Portal" })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Portal" }).first()).toBeVisible();
|
||||
}
|
||||
|
||||
export async function assertMainNavVisible(page: Page) {
|
||||
await expect(page.getByRole("link", { name: "Portal" }).first()).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "团队" }).first()).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "项目" }).first()).toBeVisible();
|
||||
}
|
||||
|
||||
export async function assertBackupModulesCollapsed(page: Page) {
|
||||
const summary = page.locator("summary").filter({ hasText: "备用模块" }).first();
|
||||
await expect(summary).toBeVisible();
|
||||
const details = summary.locator("xpath=ancestor::details[1]");
|
||||
const isOpen = await details.evaluate((node) => (node as HTMLDetailsElement).open);
|
||||
expect(isOpen).toBe(false);
|
||||
await expect(page.getByRole("link", { name: "采购申请" })).not.toBeVisible();
|
||||
}
|
||||
|
||||
export async function assertWecomBindingCardVisible(page: Page) {
|
||||
await expect(page.getByRole("heading", { name: "企业微信绑定" })).toBeVisible();
|
||||
await expect(page.getByText("接入状态", { exact: true })).toBeVisible();
|
||||
}
|
||||
|
||||
export async function fetchWecomConfig(page: Page) {
|
||||
const response = await page.request.get("/api/auth/wecom/config");
|
||||
const text = await response.text();
|
||||
let parsed: Envelope<WecomEntryConfig> | null = null;
|
||||
|
||||
try {
|
||||
parsed = text ? JSON.parse(text) as Envelope<WecomEntryConfig> : null;
|
||||
} catch {
|
||||
parsed = null;
|
||||
}
|
||||
|
||||
return {
|
||||
status: response.status(),
|
||||
ok: response.ok(),
|
||||
body: text,
|
||||
data: parsed?.data ?? null,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import { expect, test, type Page, type TestInfo } from "@playwright/test";
|
||||
|
||||
import { getExpectedWecomStatus, loginLiveUser, logoutLiveUser } from "./helpers/liveAuth";
|
||||
import { openProfileSettings } from "./helpers/navigation";
|
||||
import {
|
||||
assertBackupModulesCollapsed,
|
||||
assertMainNavVisible,
|
||||
assertPortalLoaded,
|
||||
assertWecomBindingCardVisible,
|
||||
fetchWecomConfig,
|
||||
} from "./helpers/smokeAssertions";
|
||||
|
||||
async function attachDebugArtifacts(page: Page, testInfo: TestInfo, consoleMessages: string[]) {
|
||||
await testInfo.attach("current-url.txt", {
|
||||
body: Buffer.from(page.url() || "about:blank"),
|
||||
contentType: "text/plain",
|
||||
});
|
||||
|
||||
await testInfo.attach("console.log", {
|
||||
body: Buffer.from(consoleMessages.join("\n") || "(no console output)"),
|
||||
contentType: "text/plain",
|
||||
});
|
||||
|
||||
try {
|
||||
const wecomConfig = await fetchWecomConfig(page);
|
||||
await testInfo.attach("wecom-config.json", {
|
||||
body: Buffer.from(JSON.stringify(wecomConfig, null, 2)),
|
||||
contentType: "application/json",
|
||||
});
|
||||
} catch (error) {
|
||||
await testInfo.attach("wecom-config-error.txt", {
|
||||
body: Buffer.from(String(error)),
|
||||
contentType: "text/plain",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
test.describe("live smoke", () => {
|
||||
test.skip(process.env.E2E_MODE !== "live", "仅在 live smoke 模式下执行");
|
||||
test.describe.configure({ mode: "serial" });
|
||||
|
||||
test("公网基础探活与企微配置状态", async ({ request, page }, testInfo) => {
|
||||
const consoleMessages: string[] = [];
|
||||
page.on("console", (message) => {
|
||||
consoleMessages.push(`[${message.type()}] ${message.text()}`);
|
||||
});
|
||||
|
||||
try {
|
||||
const readyResponse = await request.get("/ready");
|
||||
expect(readyResponse.ok()).toBeTruthy();
|
||||
|
||||
await page.goto("/login");
|
||||
await expect(page.getByTestId("login-identifier")).toBeVisible();
|
||||
|
||||
const wecomConfig = await fetchWecomConfig(page);
|
||||
expect(wecomConfig.status).toBe(200);
|
||||
expect(wecomConfig.data?.status).toBe(getExpectedWecomStatus());
|
||||
} finally {
|
||||
await attachDebugArtifacts(page, testInfo, consoleMessages);
|
||||
}
|
||||
});
|
||||
|
||||
test("管理员只读网页 smoke", async ({ page }, testInfo) => {
|
||||
const consoleMessages: string[] = [];
|
||||
page.on("console", (message) => {
|
||||
consoleMessages.push(`[${message.type()}] ${message.text()}`);
|
||||
});
|
||||
|
||||
let userName = "管理员";
|
||||
try {
|
||||
const admin = await loginLiveUser(page, "admin");
|
||||
userName = admin.name;
|
||||
|
||||
await assertPortalLoaded(page);
|
||||
await assertMainNavVisible(page);
|
||||
await assertBackupModulesCollapsed(page);
|
||||
|
||||
await page.goto("/team");
|
||||
await expect(page.getByRole("heading", { name: "团队状态", exact: true })).toBeVisible();
|
||||
|
||||
await page.goto("/projects");
|
||||
await expect(page.getByRole("heading", { name: "项目", exact: true })).toBeVisible();
|
||||
|
||||
await page.goto("/announcements");
|
||||
await expect(page.getByRole("heading", { name: "公告", exact: true })).toBeVisible();
|
||||
|
||||
await page.goto("/system/workspace");
|
||||
await expect(page.getByRole("heading", { name: "工作区设置", exact: true })).toBeVisible();
|
||||
|
||||
await page.goto("/system/feature-flags");
|
||||
await expect(page.getByRole("heading", { name: "功能开关", exact: true })).toBeVisible();
|
||||
|
||||
await page.goto("/audit-logs");
|
||||
await expect(page.getByRole("heading", { name: "审计工作台", exact: true })).toBeVisible();
|
||||
|
||||
await openProfileSettings(page, userName);
|
||||
await assertWecomBindingCardVisible(page);
|
||||
|
||||
await logoutLiveUser(page, { name: userName });
|
||||
} finally {
|
||||
await attachDebugArtifacts(page, testInfo, consoleMessages);
|
||||
}
|
||||
});
|
||||
|
||||
test("员工只读网页 smoke", async ({ page }, testInfo) => {
|
||||
const consoleMessages: string[] = [];
|
||||
page.on("console", (message) => {
|
||||
consoleMessages.push(`[${message.type()}] ${message.text()}`);
|
||||
});
|
||||
|
||||
let userName = "员工";
|
||||
try {
|
||||
const employee = await loginLiveUser(page, "employee");
|
||||
userName = employee.name;
|
||||
|
||||
await assertPortalLoaded(page);
|
||||
await assertMainNavVisible(page);
|
||||
|
||||
await page.goto("/team");
|
||||
await expect(page.getByRole("heading", { name: "团队状态", exact: true })).toBeVisible();
|
||||
|
||||
await page.goto("/projects");
|
||||
await expect(page.getByRole("heading", { name: "项目", exact: true })).toBeVisible();
|
||||
|
||||
await page.goto("/announcements");
|
||||
await expect(page.getByRole("heading", { name: "公告", exact: true })).toBeVisible();
|
||||
|
||||
await page.goto("/system/workspace");
|
||||
await expect(page.getByRole("heading", { name: "工作区设置", exact: true })).toBeVisible();
|
||||
|
||||
await page.goto("/system/feature-flags");
|
||||
await expect(page.getByRole("heading", { name: "功能开关", exact: true })).toBeVisible();
|
||||
|
||||
await page.goto("/audit-logs");
|
||||
await expect(page.getByRole("heading", { name: "审计工作台", exact: true })).toBeVisible();
|
||||
|
||||
await openProfileSettings(page, userName);
|
||||
await assertWecomBindingCardVisible(page);
|
||||
|
||||
await logoutLiveUser(page, { name: userName });
|
||||
} finally {
|
||||
await attachDebugArtifacts(page, testInfo, consoleMessages);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { expect, test, type Page, type TestInfo } from "@playwright/test";
|
||||
|
||||
import { EMPLOYEE, login, logout, type E2EUser } from "./helpers/auth";
|
||||
|
||||
const ADMIN: E2EUser = {
|
||||
identifier: "[email protected]",
|
||||
password: "AdminPassword1234",
|
||||
name: "Admin",
|
||||
};
|
||||
|
||||
async function attachScreenshot(page: Page, testInfo: TestInfo, name: string) {
|
||||
await testInfo.attach(name, {
|
||||
body: await page.screenshot({ fullPage: true }),
|
||||
contentType: "image/png",
|
||||
});
|
||||
}
|
||||
|
||||
test.describe("visual portal", () => {
|
||||
test("员工 Portal 视觉基线", async ({ page }, testInfo) => {
|
||||
await page.setViewportSize({ width: 1440, height: 1200 });
|
||||
await login(page, EMPLOYEE);
|
||||
|
||||
await expect(page.locator('[aria-label="portal-card-my_todos"]')).toBeVisible();
|
||||
await expect(page.locator('[aria-label="portal-card-today_presence"]')).toBeVisible();
|
||||
await expect(page.locator('[aria-label="portal-card-my_weekly_report"]')).toBeVisible();
|
||||
await expect(page.locator('[aria-label="portal-card-my_projects"]')).toBeVisible();
|
||||
await expect(page.getByText("我的申请与资产")).toBeVisible();
|
||||
|
||||
await attachScreenshot(page, testInfo, "portal-employee.png");
|
||||
});
|
||||
|
||||
test("管理员 Portal 视觉基线", async ({ page }, testInfo) => {
|
||||
await page.setViewportSize({ width: 1440, height: 1200 });
|
||||
await login(page, ADMIN);
|
||||
|
||||
await expect(page.locator('[aria-label="portal-card-pending_approvals"]')).toBeVisible();
|
||||
await expect(page.locator('[aria-label="portal-card-team_presence"]')).toBeVisible();
|
||||
await expect(page.locator('[aria-label="portal-card-weekly_missing"]')).toBeVisible();
|
||||
await expect(page.locator('[aria-label="portal-card-weekly_help"]')).toBeVisible();
|
||||
await expect(page.getByText("今日管理重点")).toBeVisible();
|
||||
await expect(page.getByText("试点观测摘要")).toBeVisible();
|
||||
|
||||
await attachScreenshot(page, testInfo, "portal-admin.png");
|
||||
await logout(page, ADMIN.name);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { expect, test, type Page, type TestInfo } from "@playwright/test";
|
||||
|
||||
import { EMPLOYEE, login } from "./helpers/auth";
|
||||
|
||||
async function attachScreenshot(page: Page, testInfo: TestInfo, name: string) {
|
||||
await testInfo.attach(name, {
|
||||
body: await page.screenshot({ fullPage: true }),
|
||||
contentType: "image/png",
|
||||
});
|
||||
}
|
||||
|
||||
test.describe("visual shell", () => {
|
||||
test("登录页入口布局基线", async ({ page }, testInfo) => {
|
||||
await page.setViewportSize({ width: 1440, height: 1024 });
|
||||
await page.goto("/login");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "登录海音达工作台" })).toBeVisible();
|
||||
await expect(page.getByText("统一入口").first()).toBeVisible();
|
||||
await expect(page.getByTestId("login-password-primary-panel")).toBeVisible();
|
||||
|
||||
await attachScreenshot(page, testInfo, "login-shell.png");
|
||||
});
|
||||
|
||||
test("登录后壳层布局基线", async ({ page }, testInfo) => {
|
||||
await page.setViewportSize({ width: 1440, height: 1024 });
|
||||
await login(page, EMPLOYEE);
|
||||
|
||||
await expect(page.locator("aside")).toBeVisible();
|
||||
await expect(page.getByTestId("topbar-profile-trigger")).toBeVisible();
|
||||
await expect(page.locator("aside").getByRole("link", { name: "Portal", exact: true })).toBeVisible();
|
||||
await expect(page.locator("summary").filter({ hasText: "备用模块" })).toBeVisible();
|
||||
|
||||
await attachScreenshot(page, testInfo, "app-shell.png");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
ID,Title,Description,Acceptance,Test_Method,Tools,Dev_Status,Review1_Status,Regression_Status,Files,Dependencies,Notes
|
||||
R28-0,执行合同建档与范围冻结,为网页自动化测试分层与公网 smoke 建立执行合同并冻结本轮范围,Issue CSV 校验通过 且范围锁定为本地 smoke 全量 E2E 公网 smoke CI 与文档,python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-03-22_21-28-38-r28-web-automation-gates.csv,manual,DONE,DONE,DONE,issues/2026-03-22_21-28-38-r28-web-automation-gates.csv,[],none
|
||||
R28-1,本地 smoke 与 full 测试入口,新增本地 smoke full 两层 Playwright 运行模式与脚本入口,client 支持 test:e2e:smoke test:e2e:full 且 scripts/qa/run-web-gates.sh 可稳定执行 smoke-local full-local,cd client && npm run typecheck && bash scripts/qa/run-web-gates.sh smoke-local,manual,DONE,DONE,DONE,client/package.json|client/playwright.config.ts|scripts/qa/run-web-gates.sh,"[""R28-0""]",固定使用本地临时库与显式 env 启动 server
|
||||
R28-2,公网 live smoke 用例与 helper,新增只读公网 smoke spec 与 live 登录 helper/断言 helper,client 支持 test:e2e:live 且 live-smoke 仅执行只读检查并校验 wecom config 状态,cd client && E2E_MODE=live npm run test:e2e:live,manual,DONE,DONE,DONE,client/tests/e2e/live-smoke.spec.ts|client/tests/e2e/helpers/liveAuth.ts|client/tests/e2e/helpers/smokeAssertions.ts,"[""R28-1""]",默认依赖 LIVE_SMOKE_* 环境变量和 smoke 账号
|
||||
R28-3,CI 分层与定时巡检接入,调整现有 ci-e2e 并新增 ci-live-smoke 让 PR 夜间与公网 smoke 分层运行,PR 跑 smoke 主分支和夜间跑 full 且新增 ci-live-smoke 支持 workflow_dispatch 与 schedule,.github/workflows/ci-e2e.yml + .github/workflows/ci-live-smoke.yml 静态审阅 + git diff --check,manual,DONE,DONE,DONE,.github/workflows/ci-e2e.yml|.github/workflows/ci-live-smoke.yml,"[""R28-1"",""R28-2""]",沿用现有 ci-regression 不推翻结构
|
||||
R28-4,部署与试点文档补强,把网页自动化门禁和 live smoke 步骤写入 trialrun 手册与 checklist,发布前后和试点检查单中可按文档执行 smoke-local full-local smoke-live,git diff --check,manual,DONE,DONE,DONE,deploy/trialrun/checklist.md|deploy/trialrun/runbook.md|docs/admin-handbook.md,"[""R28-1"",""R28-2"",""R28-3""]",文档需明确公网 smoke 禁止写操作
|
||||
R28-5,回归验证与收口,执行本地 smoke 本地 full 与必要静态检查并完成 git 提交,本地 smoke full typecheck build 通过 且 issue 状态同步完成,bash scripts/qa/run-web-gates.sh smoke-local && bash scripts/qa/run-web-gates.sh full-local && cd client && npm run build,manual,DONE,DONE,DONE,issues/2026-03-22_21-28-38-r28-web-automation-gates.csv|scripts/qa/run-web-gates.sh,"[""R28-3"",""R28-4""]",live smoke 因依赖真实 smoke 账号 默认仅提供可执行入口与 workflow
|
||||
|
在新工单中引用
屏蔽一个用户