feat(08-05-contract-detail-workspace): 完成合同详情与终止状态治理
这个提交包含在:
@@ -0,0 +1,334 @@
|
||||
import { mkdirSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { expect, test, type Page, type Route } from "@playwright/test";
|
||||
|
||||
test.use({ serviceWorkers: "block" });
|
||||
|
||||
const screenshotDir = path.resolve(process.cwd(), "../output/playwright/08-05-contract-detail-workspace");
|
||||
mkdirSync(screenshotDir, { recursive: true });
|
||||
|
||||
function screenshotPath(fileName: string) {
|
||||
return path.join(screenshotDir, fileName);
|
||||
}
|
||||
|
||||
function envelope(data: unknown) {
|
||||
return { requestId: "mock-contract-detail-workspace", data };
|
||||
}
|
||||
|
||||
async function fulfillJson(route: Route, data: unknown, status = 200) {
|
||||
await route.fulfill({ status, contentType: "application/json", body: JSON.stringify(envelope(data)) });
|
||||
}
|
||||
|
||||
const employee = {
|
||||
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
employeeNo: "HYD000001",
|
||||
name: "合同管理员",
|
||||
phone: "13800000000",
|
||||
email: "[email protected]",
|
||||
departmentId: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
|
||||
directManagerId: null,
|
||||
status: "active",
|
||||
notificationPushEnabled: true,
|
||||
roleCodes: ["manager"],
|
||||
bankAccount: { bankName: null, bankInstitutionCode: null, accountNumberMasked: null },
|
||||
profileCompleteness: { hasEmergencyContact: true, hasBankAccount: false, hasAvatar: false, hasIdNumber: false, score: 25 },
|
||||
wecomBinding: null,
|
||||
};
|
||||
|
||||
const contractId = "55555555-5555-4555-8555-555555555551";
|
||||
const projectId = "11111111-1111-4111-8111-111111111111";
|
||||
|
||||
function buildContract(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: contractId,
|
||||
contractNo: "HT-S-2026-0001",
|
||||
projectId,
|
||||
contractType: "service",
|
||||
counterpartyName: "合成客户有限公司",
|
||||
summary: "年度技术服务合同摘要",
|
||||
amount: "120000.00",
|
||||
currency: "CNY",
|
||||
signedDate: "2026-08-01",
|
||||
effectiveDate: "2026-08-01",
|
||||
expiryDate: "2027-07-31",
|
||||
confidentialityLevel: "restricted",
|
||||
archiveLocation: "线下档案柜 A-01",
|
||||
offlineArchiveNo: "OFF-CONTRACT-001",
|
||||
ownerEmployeeId: employee.id,
|
||||
status: "active",
|
||||
persistedStatus: "active",
|
||||
terminatedAt: null,
|
||||
terminatedReason: null,
|
||||
terminatedBy: null,
|
||||
createdAt: "2026-08-01T08:00:00.000Z",
|
||||
updatedAt: "2026-08-07T08:00:00.000Z",
|
||||
owner: { id: employee.id, employeeNo: employee.employeeNo, name: employee.name },
|
||||
terminator: null,
|
||||
project: { id: projectId, projectNo: "PRJ-2026-001", name: "合成项目" },
|
||||
documentRefs: [
|
||||
{
|
||||
id: "77777777-7777-4777-8777-777777777777",
|
||||
title: "涉密资料线下索引",
|
||||
documentNo: "DOC-2026-001",
|
||||
confidentialityLevel: "restricted",
|
||||
storageType: "offline_archive",
|
||||
offlineArchiveNo: "OFF-DOC-001",
|
||||
createdAt: "2026-08-01T08:00:00.000Z",
|
||||
updatedAt: "2026-08-01T08:00:00.000Z",
|
||||
owner: { id: employee.id, employeeNo: employee.employeeNo, name: employee.name },
|
||||
},
|
||||
],
|
||||
history: [
|
||||
{
|
||||
id: "audit-created",
|
||||
action: "CONTRACT_REGISTRY_CREATED",
|
||||
summary: "创建合同台账",
|
||||
createdAt: "2026-08-01T08:00:00.000Z",
|
||||
actor: { id: employee.id, employeeNo: employee.employeeNo, name: employee.name },
|
||||
},
|
||||
],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function navigationItem(key: string, href: string, label: string, icon: string) {
|
||||
return { key, href, label, icon, placement: "main", permissionCodes: [], featureFlagKey: null };
|
||||
}
|
||||
|
||||
async function installContractMocks(page: Page, initialStatus: "active" | "archived" | "terminated" = "active") {
|
||||
let contract = buildContract(
|
||||
initialStatus === "archived"
|
||||
? { status: "archived", persistedStatus: "archived" }
|
||||
: initialStatus === "terminated"
|
||||
? {
|
||||
status: "terminated",
|
||||
persistedStatus: "terminated",
|
||||
terminatedAt: "2026-08-07T09:00:00.000Z",
|
||||
terminatedReason: "双方确认提前终止合同并保留原编号",
|
||||
terminatedBy: employee.id,
|
||||
terminator: { id: employee.id, employeeNo: employee.employeeNo, name: employee.name },
|
||||
}
|
||||
: {},
|
||||
);
|
||||
|
||||
await page.route("**/api/**", async (route) => {
|
||||
const request = route.request();
|
||||
const url = new URL(request.url());
|
||||
const pathName = url.pathname;
|
||||
|
||||
if (pathName === "/api/auth/wecom/config") {
|
||||
await fulfillJson(route, { enabled: false, entryEnabled: false, oauthEnabled: false, corpId: "", agentId: "", loginUrl: null, bindEnabled: false, defaultReturnTo: "/portal", status: "disabled", statusReasons: [] });
|
||||
return;
|
||||
}
|
||||
if (pathName === "/api/auth/login" || pathName === "/api/auth/refresh") {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
headers: { "set-cookie": "has_session=1; Path=/; SameSite=Lax" },
|
||||
body: JSON.stringify(envelope({ accessToken: "mock-contract-token", expiresIn: 900, mustChangePassword: false })),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (pathName === "/api/me") {
|
||||
await fulfillJson(route, employee);
|
||||
return;
|
||||
}
|
||||
if (pathName === "/api/system/navigation") {
|
||||
await fulfillJson(route, {
|
||||
workspace: { id: "workspace-contract", persisted: true, profile: "small_team_internal", orgMode: "single_company", primaryEntryMode: "password", notificationStrategy: "in_app", devicePriority: "desktop", moduleVisibility: {}, createdAt: null, updatedAt: null, updatedBy: null },
|
||||
sections: { main: [navigationItem("portal", "/portal", "工作台", "home_app_logo"), navigationItem("contracts", "/contracts-registry", "合同", "description"), navigationItem("projects", "/projects", "项目", "topic")], admin: [], backup: [] },
|
||||
recommendedRolePresets: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (pathName === "/api/notifications") {
|
||||
await fulfillJson(route, { page: 1, pageSize: 1, total: 0, unreadOnly: true, unreadCount: 0, items: [] });
|
||||
return;
|
||||
}
|
||||
if (pathName === "/api/portal/home") {
|
||||
await fulfillJson(route, { workspace: { id: "workspace-contract", persisted: true, profile: "small_team_internal" }, greeting: { name: employee.name }, cards: [], pendingTasks: { count: 0, href: "/workflow/tasks" }, myApplications: [], riskSummary: null, announcements: [], quickLinks: [], recentActivities: [] });
|
||||
return;
|
||||
}
|
||||
if (pathName === "/api/projects") {
|
||||
await fulfillJson(route, { items: [{ id: projectId, projectNo: "PRJ-2026-001", name: "合成项目", clientName: "合成客户", clientAlias: null, status: "active", startsAt: "2026-08-01", targetEndAt: null, actualEndAt: null, summary: null, isConfidential: true, owner: null, manager: null, counts: { members: 1, milestones: 0, tasks: 0, risks: 0, overdueMilestones: 0, highPriorityTasks: 0, openRisks: 0 }, updatedAt: null }] });
|
||||
return;
|
||||
}
|
||||
if (pathName === "/api/employees") {
|
||||
await fulfillJson(route, { page: 1, pageSize: 100, total: 1, items: [{ ...employee, department: null, title: "项目经理", employmentStatus: "active", hireDate: null, avatarUrl: null, createdAt: null, updatedAt: null }] });
|
||||
return;
|
||||
}
|
||||
if (pathName === "/api/contracts-registry/expiring") {
|
||||
await fulfillJson(route, { days: 30, total: 0, items: [] });
|
||||
return;
|
||||
}
|
||||
if (pathName === "/api/contracts-registry" && request.method() === "GET") {
|
||||
const {
|
||||
documentRefs: _documentRefs,
|
||||
history: _history,
|
||||
terminatedAt: _terminatedAt,
|
||||
terminatedReason: _terminatedReason,
|
||||
terminatedBy: _terminatedBy,
|
||||
terminator: _terminator,
|
||||
...item
|
||||
} = contract;
|
||||
await fulfillJson(route, { items: [item] });
|
||||
return;
|
||||
}
|
||||
if (pathName === `/api/contracts-registry/${contractId}` && request.method() === "GET") {
|
||||
await fulfillJson(route, { item: contract });
|
||||
return;
|
||||
}
|
||||
if (pathName === `/api/contracts-registry/${contractId}` && request.method() === "PATCH") {
|
||||
const body = request.postDataJSON() as Record<string, unknown>;
|
||||
contract = {
|
||||
...contract,
|
||||
...body,
|
||||
status: body.status === "archived" ? "archived" : body.status === "active" ? "active" : contract.status,
|
||||
persistedStatus: body.status === "archived" ? "archived" : body.status === "active" ? "active" : contract.persistedStatus,
|
||||
history: [{ id: `audit-${Date.now()}`, action: "CONTRACT_REGISTRY_STATUS_CHANGED", summary: body.status === "archived" ? "状态由“生效中”变为“已归档”" : "状态由“已归档”变为“生效中”", createdAt: "2026-08-07T10:00:00.000Z", actor: { id: employee.id, employeeNo: employee.employeeNo, name: employee.name } }, ...contract.history],
|
||||
};
|
||||
await fulfillJson(route, { item: contract });
|
||||
return;
|
||||
}
|
||||
if (pathName === `/api/contracts-registry/${contractId}/terminate` && request.method() === "POST") {
|
||||
const body = request.postDataJSON() as { reason: string };
|
||||
contract = {
|
||||
...contract,
|
||||
status: "terminated",
|
||||
persistedStatus: "terminated",
|
||||
terminatedAt: "2026-08-07T10:30:00.000Z",
|
||||
terminatedReason: body.reason,
|
||||
terminatedBy: employee.id,
|
||||
terminator: { id: employee.id, employeeNo: employee.employeeNo, name: employee.name },
|
||||
history: [{ id: "audit-terminated", action: "CONTRACT_REGISTRY_TERMINATED", summary: `终止原因:${body.reason}`, createdAt: "2026-08-07T10:30:00.000Z", actor: { id: employee.id, employeeNo: employee.employeeNo, name: employee.name } }, ...contract.history],
|
||||
};
|
||||
await fulfillJson(route, { item: contract });
|
||||
return;
|
||||
}
|
||||
if (pathName === `/api/contracts-registry/${contractId}/corrections` && request.method() === "POST") {
|
||||
const body = request.postDataJSON() as { summary: string };
|
||||
contract = {
|
||||
...contract,
|
||||
history: [{ id: "audit-correction", action: "CONTRACT_REGISTRY_CORRECTION_APPENDED", summary: `追加修正:${body.summary}`, createdAt: "2026-08-07T11:00:00.000Z", actor: { id: employee.id, employeeNo: employee.employeeNo, name: employee.name } }, ...contract.history],
|
||||
};
|
||||
await fulfillJson(route, { accepted: true });
|
||||
return;
|
||||
}
|
||||
if (pathName === "/api/contracts-registry" && request.method() === "POST") {
|
||||
const body = request.postDataJSON() as Record<string, unknown>;
|
||||
contract = { ...contract, ...body, id: contractId, status: body.status ?? "active", persistedStatus: body.status ?? "active" };
|
||||
await fulfillJson(route, { item: contract }, 201);
|
||||
return;
|
||||
}
|
||||
if (pathName === `/api/contracts-registry/${contractId}/renew`) {
|
||||
await fulfillJson(route, { item: { ...contract, id: "55555555-5555-4555-8555-555555555552", contractNo: "HT-S-2026-0001-R1", status: "draft", persistedStatus: "draft" }, sourceItem: { ...contract, status: "archived", persistedStatus: "archived" } });
|
||||
return;
|
||||
}
|
||||
|
||||
await fulfillJson(route, { message: `unhandled mock api: ${pathName}` }, 404);
|
||||
});
|
||||
}
|
||||
|
||||
async function login(page: Page) {
|
||||
await page.goto("/login");
|
||||
await page.getByTestId("login-identifier").fill(employee.email);
|
||||
await page.getByTestId("login-password").fill("ContractPassword1234");
|
||||
await page.getByTestId("login-submit").click();
|
||||
await expect(page).toHaveURL(/\/portal$/);
|
||||
await page.evaluate(() => {
|
||||
document.cookie = "has_session=1; path=/; SameSite=Lax";
|
||||
});
|
||||
}
|
||||
|
||||
test.describe("合同详情工作区", () => {
|
||||
test("列表只负责查找,新增与详情使用稳定路由和证据结构", async ({ page }) => {
|
||||
await installContractMocks(page);
|
||||
await login(page);
|
||||
await page.goto("/contracts-registry");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "合同", exact: true })).toBeVisible();
|
||||
await expect(page.getByTestId("contract-editor")).toHaveCount(0);
|
||||
await expect(page.getByTestId("contracts-registry-table").getByRole("link", { name: "查看详情" })).toBeVisible();
|
||||
|
||||
await page.getByTestId("contracts-registry-table").getByRole("link", { name: "查看详情" }).click();
|
||||
await expect(page).toHaveURL(new RegExp(`/contracts-registry/${contractId}$`));
|
||||
await expect(page.locator(".section-panel__title").first()).toBeVisible();
|
||||
const sectionTitles = await page.locator(".section-panel__title").allTextContents();
|
||||
expect(sectionTitles.slice(0, 4)).toEqual(["状态与下一步", "业务摘要", "证据区", "变更与操作历史"]);
|
||||
await expect(page.getByText("涉密资料请登记线下归档位置", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("涉密资料线下索引", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("附件完整性", { exact: true })).toHaveCount(0);
|
||||
await expect(page.getByText("上传合同", { exact: false })).toHaveCount(0);
|
||||
await page.screenshot({ path: screenshotPath("contract-detail-desktop.png"), fullPage: true });
|
||||
|
||||
await page.setViewportSize({ width: 375, height: 812 });
|
||||
await expect(page.getByTestId("contract-mobile-sticky-actions")).toBeVisible();
|
||||
await expect(page.getByTestId("contract-detail-action-panel")).toBeHidden();
|
||||
const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
||||
expect(overflow).toBeLessThanOrEqual(2);
|
||||
await page.screenshot({ path: screenshotPath("contract-detail-mobile-375x812.png"), fullPage: true });
|
||||
|
||||
await page.goto("/contracts-registry");
|
||||
await page.getByRole("link", { name: "新增合同" }).click();
|
||||
await expect(page).toHaveURL(/\/contracts-registry\/new$/);
|
||||
await expect(page.getByText("涉密资料请登记线下归档位置", { exact: true })).toBeVisible();
|
||||
await page.getByTestId("contract-counterparty").fill("未保存的相对方");
|
||||
await page.getByRole("link", { name: "取消" }).click();
|
||||
await expect(page.getByTestId("unsaved-changes-dialog")).toContainText("合同表单尚未保存");
|
||||
await page.getByTestId("unsaved-changes-continue").click();
|
||||
await expect(page).toHaveURL(/\/contracts-registry\/new$/);
|
||||
await page.getByRole("link", { name: "取消" }).click();
|
||||
await page.getByTestId("unsaved-changes-discard").click();
|
||||
await expect(page).toHaveURL(/\/contracts-registry$/);
|
||||
});
|
||||
|
||||
test("终止原因禁用说明、对象摘要确认和不可逆终态完整可见", async ({ page }) => {
|
||||
await installContractMocks(page);
|
||||
await login(page);
|
||||
await page.goto(`/contracts-registry/${contractId}`);
|
||||
|
||||
const terminateButton = page.getByRole("button", { name: "终止合同", exact: true });
|
||||
await expect(terminateButton).toBeDisabled();
|
||||
await expect(page.getByRole("alert")).toContainText("请填写终止原因,至少 10 个字");
|
||||
await page.getByLabel("终止原因").fill("原因太短");
|
||||
await expect(terminateButton).toBeDisabled();
|
||||
await page.getByLabel("终止原因").fill("客户项目已取消,双方确认不再继续履行");
|
||||
await expect(terminateButton).toBeEnabled();
|
||||
await terminateButton.click();
|
||||
|
||||
const confirm = page.getByTestId("action-confirm-dialog");
|
||||
await expect(confirm).toContainText("HT-S-2026-0001");
|
||||
await expect(confirm).toContainText("合成客户有限公司");
|
||||
await expect(confirm).toContainText("120,000.00");
|
||||
await expect(confirm).toContainText("不可逆");
|
||||
await page.getByTestId("action-confirm-submit").click();
|
||||
|
||||
await expect(page.getByText("已终止", { exact: true }).first()).toBeVisible();
|
||||
const statusSection = page.locator(".section-panel").filter({
|
||||
has: page.getByRole("heading", { name: "状态与下一步", exact: true }),
|
||||
});
|
||||
await expect(statusSection).toContainText("客户项目已取消,双方确认不再继续履行");
|
||||
await expect(page.getByRole("button", { name: "编辑", exact: true })).toHaveCount(0);
|
||||
await expect(page.getByRole("button", { name: "续约", exact: true })).toHaveCount(0);
|
||||
await expect(page.getByRole("button", { name: "归档", exact: true })).toHaveCount(0);
|
||||
await expect(page.getByRole("button", { name: "恢复", exact: true })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("归档合同锁定编辑并通过追加记录修正,恢复需二次确认", async ({ page }) => {
|
||||
await installContractMocks(page, "archived");
|
||||
await login(page);
|
||||
await page.goto(`/contracts-registry/${contractId}`);
|
||||
|
||||
await expect(page.getByRole("button", { name: "编辑", exact: true })).toHaveCount(0);
|
||||
await expect(page.getByText("普通字段已锁定;如需纠错,请追加修正记录。", { exact: true })).toBeVisible();
|
||||
await page.getByLabel("修正说明").fill("原归档位置登记有误,实际以离线档案号为准");
|
||||
await page.getByRole("button", { name: "追加记录" }).click();
|
||||
await expect(page.getByTestId("contract-history")).toContainText("追加修正:原归档位置登记有误,实际以离线档案号为准");
|
||||
|
||||
await page.getByRole("button", { name: "恢复", exact: true }).click();
|
||||
await expect(page.getByTestId("action-confirm-dialog")).toContainText("恢复后合同重新进入生效台账和到期提醒");
|
||||
await page.getByTestId("action-confirm-submit").click();
|
||||
await expect(page.getByText("生效中", { exact: true }).first()).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "编辑", exact: true }).first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -497,6 +497,30 @@ const secondContractRegistryItem = {
|
||||
offlineArchiveNo: "OFF-2026-W3-02",
|
||||
};
|
||||
|
||||
const contractRegistryDetail = {
|
||||
...contractRegistryItem,
|
||||
terminatedAt: null,
|
||||
terminatedReason: null,
|
||||
terminatedBy: null,
|
||||
terminator: null,
|
||||
documentRefs: [],
|
||||
history: [
|
||||
{
|
||||
id: "audit-contract-wave3",
|
||||
action: "CONTRACT_REGISTRY_CREATED",
|
||||
summary: "创建合同台账",
|
||||
createdAt: "2026-05-20T09:00:00.000Z",
|
||||
actor: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const secondContractRegistryDetail = {
|
||||
...contractRegistryDetail,
|
||||
...secondContractRegistryItem,
|
||||
history: contractRegistryDetail.history,
|
||||
};
|
||||
|
||||
const purchaseDetail = {
|
||||
id: "purchase-wave3",
|
||||
requestNo: "CG20260527001",
|
||||
@@ -997,6 +1021,16 @@ async function mockWave3Apis(page: Page) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.method() === "GET" && ["/api/contracts-registry/contract-wave3", "/api/contracts-registry/contract-wave3-second"].includes(pathName)) {
|
||||
await fulfillJson(route, { item: pathName.endsWith("-second") ? secondContractRegistryDetail : contractRegistryDetail });
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === "/api/contracts-registry/contract-wave3" && request.method() === "PATCH") {
|
||||
await fulfillJson(route, { item: { ...contractRegistryDetail, status: "archived", persistedStatus: "archived" } });
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === "/api/contracts-registry") {
|
||||
await fulfillJson(route, { items: [contractRegistryItem, secondContractRegistryItem] });
|
||||
return;
|
||||
@@ -1409,19 +1443,18 @@ test("Wave3 PC 详情和通知中心视觉截图", async ({ page }) => {
|
||||
const contractRows = page.getByTestId("contracts-registry-table").getByTestId("contracts-registry-row");
|
||||
await expect(contractRows).toHaveCount(2);
|
||||
await expect(contractRows.first()).toBeVisible();
|
||||
await page.getByRole("button", { name: "筛选", exact: true }).click();
|
||||
await expect(page.getByTestId("contracts-advanced-filters")).toBeVisible();
|
||||
await page.getByRole("button", { name: "新增合同" }).click();
|
||||
await expect(page.getByTestId("contracts-editor-panel")).toBeVisible();
|
||||
await page.getByRole("button", { name: "关闭" }).click();
|
||||
await expect(contractRows).toHaveCount(2);
|
||||
await page.getByTestId("contracts-registry-table").getByRole("button", { name: "更多" }).first().click();
|
||||
await page.getByRole("menuitem", { name: "归档" }).click();
|
||||
await page.getByRole("button", { name: "更多筛选", exact: true }).click();
|
||||
await expect(page.getByLabel("到期开始日期")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "编辑", exact: true })).toHaveCount(0);
|
||||
await expect(page.getByRole("button", { name: "更多", exact: true })).toHaveCount(0);
|
||||
await page.getByTestId("contracts-registry-table").getByRole("link", { name: "查看详情" }).first().click();
|
||||
await expect(page).toHaveURL(/\/contracts-registry\/contract-wave3$/);
|
||||
await expect(page.getByText("状态与下一步", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("涉密资料请登记线下归档位置", { exact: true })).toBeVisible();
|
||||
await page.getByRole("button", { name: "归档", exact: true }).click();
|
||||
await expect(page.getByTestId("action-confirm-dialog")).toContainText("确认归档");
|
||||
await page.getByTestId("action-confirm-cancel").click();
|
||||
await expect(page.getByTestId("action-confirm-dialog")).toHaveCount(0);
|
||||
await page.locator(".ops-command-bar__actions").getByRole("button", { name: "更多" }).click();
|
||||
await expect(page.getByRole("menuitem", { name: "导出 CSV" })).toBeVisible();
|
||||
await page.screenshot({ path: screenshotPath("04-pc-contracts-registry.png"), fullPage: true });
|
||||
});
|
||||
|
||||
@@ -1464,7 +1497,7 @@ test("/contracts-registry 在 768x1024 与 1024x768 下操作列可达", async (
|
||||
expect(["auto", "scroll"]).toContain(metrics.overflowX);
|
||||
expect(metrics.scrollWidth).toBeGreaterThanOrEqual(metrics.clientWidth);
|
||||
expect(metrics.documentOverflow).toBeLessThanOrEqual(2);
|
||||
const operation = table.getByRole("button", { name: "更多" }).first();
|
||||
const operation = table.getByRole("link", { name: "查看详情" }).first();
|
||||
await expect(operation).toBeVisible();
|
||||
await operation.click({ trial: true });
|
||||
await page.screenshot({
|
||||
@@ -1474,24 +1507,27 @@ test("/contracts-registry 在 768x1024 与 1024x768 下操作列可达", async (
|
||||
}
|
||||
});
|
||||
|
||||
test("合同长表单从记录 A 切换到记录 B 时支持取消保持与确认放弃", async ({ page }) => {
|
||||
test("合同详情编辑离开路由时支持取消保持与确认放弃", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1280, height: 900 });
|
||||
await page.goto("/contracts-registry");
|
||||
const table = page.getByTestId("contracts-registry-table");
|
||||
const editButtons = table.getByRole("button", { name: "编辑", exact: true });
|
||||
await editButtons.first().click();
|
||||
const counterparty = page.getByPlaceholder("合同对方名称");
|
||||
await table.getByRole("link", { name: "查看详情" }).first().click();
|
||||
await expect(page).toHaveURL(/\/contracts-registry\/contract-wave3$/);
|
||||
await page.getByRole("button", { name: "编辑", exact: true }).first().click();
|
||||
const counterparty = page.getByTestId("contract-counterparty");
|
||||
await counterparty.fill("合成未保存合同相对方");
|
||||
|
||||
await editButtons.nth(1).click();
|
||||
await page.getByRole("link", { name: "返回合同列表" }).click();
|
||||
await expect(page.getByTestId("unsaved-changes-dialog")).toContainText("合同表单尚未保存");
|
||||
await page.getByTestId("unsaved-changes-continue").click();
|
||||
await expect(page).toHaveURL(/\/contracts-registry$/);
|
||||
await expect(page).toHaveURL(/\/contracts-registry\/contract-wave3$/);
|
||||
await expect(counterparty).toHaveValue("合成未保存合同相对方");
|
||||
|
||||
await editButtons.nth(1).click();
|
||||
await page.getByRole("link", { name: "返回合同列表" }).click();
|
||||
await page.getByTestId("unsaved-changes-discard").click();
|
||||
await expect(counterparty).toHaveValue(secondContractRegistryItem.counterpartyName);
|
||||
await expect(page).toHaveURL(/\/contracts-registry$/);
|
||||
await page.getByTestId("contracts-registry-table").getByRole("link", { name: "查看详情" }).nth(1).click();
|
||||
await expect(page.getByText(secondContractRegistryItem.counterpartyName, { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test("Wave3 移动关键页视觉截图", async ({ page }) => {
|
||||
|
||||
在新工单中引用
屏蔽一个用户