feat(08-05-business-detail-routing): 完成采购与资产独立路由
这个提交包含在:
@@ -15,21 +15,12 @@ async function fulfillJson(route: Route, data: unknown, status = 200) {
|
||||
}
|
||||
|
||||
const screenshotDir = path.resolve(process.cwd(), "../output/playwright/r103");
|
||||
const taskScreenshotDir = path.resolve(
|
||||
process.cwd(),
|
||||
"../output/playwright/07-18-small-team-internal-readiness",
|
||||
);
|
||||
|
||||
function screenshotPath(fileName: string) {
|
||||
mkdirSync(screenshotDir, { recursive: true });
|
||||
return path.join(screenshotDir, fileName);
|
||||
}
|
||||
|
||||
function taskScreenshotPath(fileName: string) {
|
||||
mkdirSync(taskScreenshotDir, { recursive: true });
|
||||
return path.join(taskScreenshotDir, fileName);
|
||||
}
|
||||
|
||||
const me = {
|
||||
id: "employee-r103",
|
||||
employeeNo: "HYD000103",
|
||||
@@ -501,6 +492,11 @@ async function mockApis(page: Page, options: { roleCodes?: string[] } = {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === `/api/assets/${assetId}` && request.method() === "GET") {
|
||||
await fulfillJson(route, { asset: assetListItem() });
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === `/api/purchase-requests/${purchaseId}` && request.method() === "PATCH") {
|
||||
await fulfillJson(route, { ...purchaseDetail(), ...request.postDataJSON() });
|
||||
return;
|
||||
@@ -622,7 +618,7 @@ async function expectResponsiveBusinessTable(
|
||||
const operation = table.getByRole("link").or(table.getByRole("button")).last();
|
||||
await expect(operation).toBeVisible();
|
||||
await operation.click({ trial: true });
|
||||
await page.screenshot({ path: taskScreenshotPath(target.screenshotName), fullPage: true });
|
||||
await page.screenshot({ path: screenshotPath(target.screenshotName), fullPage: true });
|
||||
}
|
||||
|
||||
async function expectLedgerHeaderActions(
|
||||
@@ -710,6 +706,7 @@ test.describe("R103 cross page UI consistency", () => {
|
||||
await expect(page.getByText("/purchase-requests")).toHaveCount(0);
|
||||
|
||||
await page.goto(`/purchase-requests?id=${purchaseId}`);
|
||||
await expect(page).toHaveURL(new RegExp(`/purchase-requests/${purchaseId}$`));
|
||||
await expect(page.getByTestId("purchase-requests-table")).toHaveCount(0);
|
||||
await expect(page.getByTestId("purchase-advanced-filters")).toHaveCount(0);
|
||||
await expect(page.getByTestId("purchase-attachment-upload")).toHaveText("上传附件");
|
||||
@@ -754,6 +751,72 @@ test.describe("R103 cross page UI consistency", () => {
|
||||
await page.screenshot({ path: screenshotPath("purchase-desktop.png"), fullPage: true });
|
||||
});
|
||||
|
||||
test("采购与资产列表、详情各自只请求职责内数据并恢复 URL 筛选", async ({ page }) => {
|
||||
const requests: Array<{ method: string; pathname: string; search: string }> = [];
|
||||
page.on("request", (request) => {
|
||||
const url = new URL(request.url());
|
||||
if (url.pathname.startsWith("/api/purchase-requests") || url.pathname.startsWith("/api/assets")) {
|
||||
requests.push({ method: request.method(), pathname: url.pathname, search: url.search });
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto("/purchase-requests?status=draft&scope=own&supplierId=supplier-r103");
|
||||
await expect(page.getByTestId("purchase-requests-table")).toBeVisible();
|
||||
expect(requests.some((item) => item.method === "GET" && item.pathname === `/api/purchase-requests/${purchaseId}`)).toBe(false);
|
||||
requests.length = 0;
|
||||
|
||||
await page.getByTestId("purchase-row-open").click();
|
||||
await expect(page).toHaveURL(new RegExp(`/purchase-requests/${purchaseId}$`));
|
||||
await expect(page.getByTestId("purchase-title")).toBeVisible();
|
||||
expect(requests.some((item) => item.method === "GET" && item.pathname === "/api/purchase-requests")).toBe(false);
|
||||
await page.getByRole("button", { name: "返回采购列表", exact: true }).click();
|
||||
await expect(page).toHaveURL(/\/purchase-requests\?status=draft&scope=own&supplierId=supplier-r103$/);
|
||||
|
||||
requests.length = 0;
|
||||
await page.goto("/assets?q=研发&status=in_use&scope=own");
|
||||
await expect(page.getByTestId("assets-table")).toBeVisible();
|
||||
expect(requests.some((item) => item.method === "GET" && item.pathname === `/api/assets/${assetId}`)).toBe(false);
|
||||
requests.length = 0;
|
||||
|
||||
await page.getByRole("button", { name: "查看详情", exact: true }).first().click();
|
||||
await expect(page).toHaveURL(new RegExp(`/assets/${assetId}$`));
|
||||
await expect(page.getByTestId("asset-name")).toHaveValue("研发测试笔记本电脑");
|
||||
expect(requests.some((item) => item.method === "GET" && item.pathname === "/api/assets")).toBe(false);
|
||||
await page.getByRole("button", { name: "返回资产列表", exact: true }).click();
|
||||
await expect(page).toHaveURL(/\/assets\?/);
|
||||
const restoredAssetUrl = new URL(page.url());
|
||||
expect(restoredAssetUrl.pathname).toBe("/assets");
|
||||
expect(Object.fromEntries(restoredAssetUrl.searchParams)).toEqual({ q: "研发", status: "in_use", scope: "own" });
|
||||
});
|
||||
|
||||
test("采购与资产长表单详情对 403/404 使用同一安全错误态", async ({ page }) => {
|
||||
const safeError = (message: string) => JSON.stringify({
|
||||
requestId: "mock-business-detail-denied",
|
||||
error: { code: "NOT_FOUND", message },
|
||||
});
|
||||
await page.route("**/api/purchase-requests/missing-purchase", async (route) => {
|
||||
await route.fulfill({ status: 404, contentType: "application/json", body: safeError("内部采购对象不存在") });
|
||||
});
|
||||
await page.route("**/api/assets/missing-asset", async (route) => {
|
||||
await route.fulfill({ status: 403, contentType: "application/json", body: safeError("内部资产对象无权访问") });
|
||||
});
|
||||
|
||||
await page.goto("/purchase-requests/missing-purchase");
|
||||
await expect(page.getByText("无法查看该记录,记录可能不存在或你无权访问。", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("内部采购对象不存在")).toHaveCount(0);
|
||||
|
||||
await page.goto("/assets/missing-asset");
|
||||
await expect(page.getByText("无法查看该记录,记录可能不存在或你无权访问。", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("内部资产对象无权访问")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("资产创建路由不会把列表查看权限当成 asset.manage", async ({ page }) => {
|
||||
await page.goto("/assets");
|
||||
await expect(page).not.toHaveURL(/\/403$/);
|
||||
await page.goto("/assets/new");
|
||||
await expect(page).toHaveURL(/\/403$/);
|
||||
});
|
||||
|
||||
test("票据绑定报销单不展示 UUID 兜底", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1440, height: 960 });
|
||||
await page.goto("/invoice-pool");
|
||||
@@ -782,16 +845,16 @@ test.describe("R103 cross page UI consistency", () => {
|
||||
|
||||
test("采购长表单切回记录列表时支持取消保持与确认放弃", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1280, height: 900 });
|
||||
await page.goto(`/purchase-requests?id=${purchaseId}`);
|
||||
await page.goto(`/purchase-requests/${purchaseId}`);
|
||||
const title = page.getByTestId("purchase-title");
|
||||
await expect(title).toBeVisible();
|
||||
await title.fill("办公用品采购(未保存)");
|
||||
|
||||
const returnToList = page.getByRole("button", { name: "返回列表", exact: true });
|
||||
const returnToList = page.getByRole("button", { name: "返回采购列表", exact: true });
|
||||
await returnToList.click();
|
||||
await expect(page.getByTestId("unsaved-changes-dialog")).toContainText("采购表单尚未保存");
|
||||
await page.getByTestId("unsaved-changes-continue").click();
|
||||
await expect(page).toHaveURL(new RegExp(`/purchase-requests\\?id=${purchaseId}$`));
|
||||
await expect(page).toHaveURL(new RegExp(`/purchase-requests/${purchaseId}$`));
|
||||
await expect(title).toHaveValue("办公用品采购(未保存)");
|
||||
|
||||
await returnToList.click();
|
||||
@@ -799,7 +862,7 @@ test.describe("R103 cross page UI consistency", () => {
|
||||
await expect(page).toHaveURL(/\/purchase-requests$/);
|
||||
await expect(page.getByTestId("purchase-row-open")).toBeVisible();
|
||||
|
||||
await page.goto(`/purchase-requests?id=${purchaseId}`);
|
||||
await page.goto(`/purchase-requests/${purchaseId}`);
|
||||
await page.getByTestId("purchase-title").fill("办公用品采购(已保存)");
|
||||
await page.getByTestId("purchase-save").click();
|
||||
await expect(page.getByText("采购草稿已保存")).toBeVisible();
|
||||
@@ -810,17 +873,21 @@ test.describe("R103 cross page UI consistency", () => {
|
||||
|
||||
test("资产长表单切回记录列表时支持取消保持与确认放弃", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1280, height: 900 });
|
||||
const requestedPaths: string[] = [];
|
||||
page.on("request", (request) => requestedPaths.push(new URL(request.url()).pathname));
|
||||
await page.goto("/assets");
|
||||
await page.getByTestId("asset-create").click();
|
||||
await expect(page).toHaveURL(/\/assets\/new$/);
|
||||
expect(requestedPaths).not.toContain("/api/assets/new");
|
||||
const name = page.getByTestId("asset-name");
|
||||
await expect(name).toBeVisible();
|
||||
await name.fill("合成未保存资产");
|
||||
|
||||
const returnToList = page.getByRole("button", { name: "返回列表", exact: true });
|
||||
const returnToList = page.getByRole("button", { name: "返回资产列表", exact: true });
|
||||
await returnToList.click();
|
||||
await expect(page.getByTestId("unsaved-changes-dialog")).toContainText("资产表单尚未保存");
|
||||
await page.getByTestId("unsaved-changes-continue").click();
|
||||
await expect(page).toHaveURL(/\/assets$/);
|
||||
await expect(page).toHaveURL(/\/assets\/new$/);
|
||||
await expect(name).toHaveValue("合成未保存资产");
|
||||
|
||||
await returnToList.click();
|
||||
@@ -834,7 +901,7 @@ test.describe("R103 cross page UI consistency", () => {
|
||||
await page.getByRole("link", { name: "采购", exact: true }).first().click();
|
||||
await expect(page).toHaveURL(/\/purchase-requests$/);
|
||||
await page.getByTestId("purchase-row-open").click();
|
||||
await expect(page).toHaveURL(new RegExp(`/purchase-requests\\?id=${purchaseId}$`));
|
||||
await expect(page).toHaveURL(new RegExp(`/purchase-requests/${purchaseId}$`));
|
||||
const title = page.getByTestId("purchase-title");
|
||||
await title.fill("浏览器历史未保存验证");
|
||||
await expect(page.locator("html")).toHaveAttribute("data-unsaved-changes", "true");
|
||||
@@ -842,7 +909,7 @@ test.describe("R103 cross page UI consistency", () => {
|
||||
await page.goBack();
|
||||
await expect(page.getByTestId("unsaved-changes-dialog")).toContainText("采购表单尚未保存");
|
||||
await page.getByTestId("unsaved-changes-continue").click();
|
||||
await expect(page).toHaveURL(new RegExp(`/purchase-requests\\?id=${purchaseId}$`));
|
||||
await expect(page).toHaveURL(new RegExp(`/purchase-requests/${purchaseId}$`));
|
||||
await expect(title).toHaveValue("浏览器历史未保存验证");
|
||||
|
||||
const beforeUnloadPromise = page.waitForEvent("dialog");
|
||||
@@ -851,7 +918,7 @@ test.describe("R103 cross page UI consistency", () => {
|
||||
expect(beforeUnload.type()).toBe("beforeunload");
|
||||
await beforeUnload.dismiss();
|
||||
await reloadPromise;
|
||||
await expect(page).toHaveURL(new RegExp(`/purchase-requests\\?id=${purchaseId}$`));
|
||||
await expect(page).toHaveURL(new RegExp(`/purchase-requests/${purchaseId}$`));
|
||||
await expect(title).toHaveValue("浏览器历史未保存验证");
|
||||
|
||||
const acceptedBeforeUnloadPromise = page.waitForEvent("dialog");
|
||||
@@ -860,15 +927,14 @@ test.describe("R103 cross page UI consistency", () => {
|
||||
expect(acceptedBeforeUnload.type()).toBe("beforeunload");
|
||||
await acceptedBeforeUnload.accept();
|
||||
await acceptedReloadPromise;
|
||||
await expect(page).toHaveURL(new RegExp(`/purchase-requests\\?id=${purchaseId}$`));
|
||||
await expect(page).toHaveURL(new RegExp(`/purchase-requests/${purchaseId}$`));
|
||||
await expect(title).toHaveValue("办公用品采购");
|
||||
await title.fill("浏览器历史再次未保存验证");
|
||||
|
||||
await page.goBack();
|
||||
await expect(page.getByTestId("unsaved-changes-dialog")).toBeVisible();
|
||||
await page.getByTestId("unsaved-changes-discard").click();
|
||||
// 详情查询参数由列表页使用 replace 写入,因此它的真实上一项是进入采购页前的工作台。
|
||||
await expect(page).toHaveURL(/\/portal$/);
|
||||
await expect(page).toHaveURL(/\/purchase-requests$/);
|
||||
});
|
||||
|
||||
test("/projects /assets /reimbursements 在中等宽度下操作列可达且页面不横溢", async ({ page }) => {
|
||||
|
||||
在新工单中引用
屏蔽一个用户