fix: 统一报销付款详情布局与票据展示(09-04-reimbursement-state-gap-finance-view)
这个提交包含在:
@@ -87,6 +87,7 @@ export function BusinessDetailActionPanel({
|
||||
primaryAction,
|
||||
secondaryActions,
|
||||
moreActions,
|
||||
inlineActions = false,
|
||||
summary,
|
||||
children,
|
||||
className,
|
||||
@@ -98,6 +99,7 @@ export function BusinessDetailActionPanel({
|
||||
primaryAction?: ReactNode;
|
||||
secondaryActions?: ReactNode;
|
||||
moreActions?: ReactNode;
|
||||
inlineActions?: boolean;
|
||||
summary?: ReactNode;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
@@ -121,9 +123,10 @@ export function BusinessDetailActionPanel({
|
||||
<SectionPanel title={title} actions={status} className={cn("detail-action-panel lg:sticky lg:top-24", className)}>
|
||||
<div className="detail-action-stack" data-testid={testId}>
|
||||
{hasBeforeActions ? <div>{beforeActions}</div> : null}
|
||||
{hasPrimaryAction ? <div className="grid gap-2">{primaryAction}</div> : null}
|
||||
{inlineActions && (hasPrimaryAction || hasMoreActions) ? <div className="flex flex-wrap items-center gap-2" data-testid="detail-inline-actions">{primaryAction}{moreActions}</div> : null}
|
||||
{!inlineActions && hasPrimaryAction ? <div className="grid gap-2">{primaryAction}</div> : null}
|
||||
{hasSecondaryActions ? <div className="workflow-action-bar">{secondaryActions}</div> : null}
|
||||
{hasMoreActions ? <div className="border-t border-zinc-200 pt-3">{moreActions}</div> : null}
|
||||
{!inlineActions && hasMoreActions ? <div className="border-t border-zinc-200 pt-3">{moreActions}</div> : null}
|
||||
{hasChildren ? (
|
||||
<div className={cn(hasActionContent && "border-t border-zinc-200 pt-3")}>{children}</div>
|
||||
) : null}
|
||||
|
||||
@@ -30,7 +30,7 @@ export function ReimbursementDesktopWorkspaceShell({ model, capabilities, slots
|
||||
)}
|
||||
/>
|
||||
{slots.transientFeedback}
|
||||
<BusinessDetailWorkspace className="mx-auto w-full max-w-7xl">
|
||||
<BusinessDetailWorkspace className="detail-workspace-grid--payment mx-auto w-full max-w-7xl">
|
||||
<BusinessDetailMain>
|
||||
{slots.headerDetails}
|
||||
{slots.beforeEvidence}
|
||||
|
||||
@@ -167,6 +167,43 @@ function InvoiceMaterialRow({ invoice, capabilities, linkTargets }: { invoice: R
|
||||
);
|
||||
}
|
||||
|
||||
function InvoiceMaterialsTable({ invoices, capabilities }: { invoices: ReimbursementInvoiceEvidence[]; capabilities: ReimbursementEvidenceCapabilities }) {
|
||||
return (
|
||||
<div className="overflow-hidden rounded-lg border border-zinc-200" data-testid="reimbursement-invoices-table">
|
||||
<table aria-label="报销发票" className="block w-full table-fixed text-left text-sm lg:table">
|
||||
<thead className="hidden bg-zinc-50 text-xs text-zinc-600 lg:table-header-group">
|
||||
<tr>
|
||||
<th className="w-[30%] px-3 py-2.5 font-medium" scope="col">发票号码</th>
|
||||
<th className="px-3 py-2.5 font-medium" scope="col">开票方</th>
|
||||
<th className="w-24 px-3 py-2.5 font-medium" scope="col">日期</th>
|
||||
<th className="w-28 px-3 py-2.5 text-right font-medium" scope="col">金额</th>
|
||||
{capabilities.onUnlinkInvoice ? <th className="w-28 px-3 py-2.5 font-medium" scope="col">操作</th> : null}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="block divide-y divide-zinc-200 bg-white lg:table-row-group">
|
||||
{invoices.map((invoice) => {
|
||||
const title = invoice.invoiceNumber || invoice.originalName || "未命名发票";
|
||||
const statusCopy = evidenceStatusCopy(invoice.amountEvidenceStatus);
|
||||
const hasBlockingIssue = invoice.amountEvidenceStatus === "ocr_failed";
|
||||
return (
|
||||
<tr className="block p-3 lg:table-row lg:p-0" data-testid="approval-invoice-evidence" data-reimbursement-guard-issue={hasBlockingIssue ? "true" : undefined} id={`reimbursement-invoice-${invoice.id}`} key={invoice.id} tabIndex={hasBlockingIssue ? -1 : undefined}>
|
||||
<td className="block break-all align-top lg:table-cell lg:px-3 lg:py-2">
|
||||
{capabilities.onOpenInvoice ? <Button aria-label={`查看发票:${title}`} className="h-auto min-h-11 max-w-full justify-start whitespace-normal break-all px-0 text-left" onClick={() => capabilities.onOpenInvoice?.(invoice)} type="button" variant="link">{title}</Button> : <span className="font-medium text-zinc-900">{title}</span>}
|
||||
{statusCopy ? <p className={hasBlockingIssue ? "text-xs text-red-700" : "text-xs text-zinc-600"}>{statusCopy}</p> : null}
|
||||
</td>
|
||||
<td className="block break-words text-zinc-600 [overflow-wrap:anywhere] lg:table-cell lg:px-3 lg:py-3">{invoice.sellerName || invoice.documentSubtype || invoice.documentCategory || "票据"}</td>
|
||||
<td className="block text-zinc-600 lg:table-cell lg:px-3 lg:py-3">{formatDisplayDate(invoice.invoiceDate)}</td>
|
||||
<td className="block text-right font-medium tabular-nums lg:table-cell lg:px-3 lg:py-3">{formatMoney(invoice.totalAmount)}</td>
|
||||
{capabilities.onUnlinkInvoice ? <td className="block lg:table-cell lg:px-3 lg:py-2"><Button className="min-h-11 whitespace-normal" onClick={() => capabilities.onUnlinkInvoice?.(invoice)} size="sm" type="button" variant="secondary">移除对应关系</Button></td> : null}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AttachmentMaterialRow({ attachment, capabilities }: { attachment: ReimbursementAttachmentEvidence; capabilities: ReimbursementEvidenceCapabilities }) {
|
||||
return <div className="flex flex-wrap items-center justify-between gap-2 rounded-lg border border-zinc-200 px-3 py-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
@@ -197,7 +234,7 @@ export function ReimbursementItemEvidenceList({ amountSummary, evidence, capabil
|
||||
<div className="flex flex-wrap items-center justify-between gap-2"><h3 className="font-medium text-zinc-900">票据与附件</h3>{capabilities.onOpenMaterialBundle ? <Button className="min-h-11" onClick={capabilities.onOpenMaterialBundle} size="sm" type="button" variant="secondary">查看完整材料</Button> : null}</div>
|
||||
<div className="space-y-2" data-testid="approval-invoices">
|
||||
<h4 className="text-sm font-medium text-zinc-800">发票({linkedInvoices.length + evidence.unlinkedInvoices.length})</h4>
|
||||
{linkedInvoices.map((invoice) => <InvoiceMaterialRow capabilities={capabilities} invoice={invoice} key={invoice.id} linkTargets={evidence.items} />)}
|
||||
{linkedInvoices.length ? (mobile ? linkedInvoices.map((invoice) => <InvoiceMaterialRow capabilities={capabilities} invoice={invoice} key={invoice.id} linkTargets={evidence.items} />) : <InvoiceMaterialsTable capabilities={capabilities} invoices={linkedInvoices} />) : null}
|
||||
{evidence.unlinkedInvoices.length ? <div className="space-y-2 rounded-lg border border-red-200 bg-red-50 p-3" data-testid="approval-unlinked-invoices" id="reimbursement-workspace-unlinked"><p className="text-sm font-medium text-red-900">尚未对应明细({evidence.unlinkedInvoices.length})</p><p className="text-sm text-red-800">这些票据当前没有明确费用归属,暂时无法提交;请为每张票据选择对应的报销明细。</p>{evidence.unlinkedInvoices.map((invoice) => <InvoiceMaterialRow capabilities={capabilities} invoice={invoice} key={invoice.id} linkTargets={evidence.items} />)}</div> : null}
|
||||
{!linkedInvoices.length && !evidence.unlinkedInvoices.length ? <p className="text-sm text-zinc-500">暂无发票</p> : null}
|
||||
</div>
|
||||
|
||||
@@ -588,6 +588,10 @@
|
||||
@apply grid gap-5 lg:grid-cols-[minmax(0,1.45fr),minmax(280px,0.72fr)];
|
||||
}
|
||||
|
||||
.detail-workspace-grid--payment {
|
||||
@apply lg:grid-cols-[minmax(0,1fr),320px];
|
||||
}
|
||||
|
||||
.detail-command-panel {
|
||||
border-color: rgba(203, 213, 225, 0.92);
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.9));
|
||||
|
||||
@@ -3,6 +3,7 @@ import { resolveErrorReasonMessage, UNKNOWN_VALIDATION_MESSAGE } from "../api/er
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Link, useSearchParams } from "react-router-dom";
|
||||
import { Eye } from "lucide-react";
|
||||
import {
|
||||
cancelPaymentBatch,
|
||||
createPaymentBatch,
|
||||
@@ -827,7 +828,7 @@ export function PaymentBatchesPage() {
|
||||
<div
|
||||
className={paymentActionModel.primary || paymentActionModel.more.length ? "pb-[calc(var(--mobile-sticky-action-height,78px)+env(safe-area-inset-bottom,0px)+1.25rem)] lg:pb-0" : ""}
|
||||
>
|
||||
<BusinessDetailWorkspace testId="payment-batch-detail-workspace">
|
||||
<BusinessDetailWorkspace className="detail-workspace-grid--payment" testId="payment-batch-detail-workspace">
|
||||
<BusinessDetailMain>
|
||||
<div className="finance-command-panel space-y-3" data-testid="payment-batch-detail-panel">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
@@ -914,7 +915,7 @@ export function PaymentBatchesPage() {
|
||||
<div className="mt-2 detail-fact-grid text-sm md:grid-cols-4">
|
||||
<div className="detail-fact-card"><div className="detail-fact-card__label">收款人</div><div className="detail-fact-card__value">{item.payeeName || "-"}</div></div>
|
||||
<div className="detail-fact-card"><div className="detail-fact-card__label">开户银行</div><div className="detail-fact-card__value">{item.bankName || "-"}</div></div>
|
||||
<div className="detail-fact-card"><div className="detail-fact-card__label">银行账户</div><div className="detail-fact-card__value">{item.bankAccountMasked || "-"}</div>{canRevealPayeeBank && ["reviewed", "failed"].includes(selectedBatch.status) && selectedBatch.paymentAccount ? <Button type="button" size="sm" variant="secondary" className="mt-2" onClick={() => { setActionReason(""); setRevealedPayeeAccount(""); setReasonAction({ kind: "payeeBank", batchId: selectedBatch.id, batchNo: selectedBatch.batchNo, itemId: item.id, entityCode: item.entityCode }); }}>查看完整账号</Button> : null}</div>
|
||||
<div className="detail-fact-card"><div className="detail-fact-card__label">银行账户</div><div className="detail-fact-card__value flex items-center gap-1"><span className="min-w-0 break-all">{item.bankAccountMasked || "-"}</span>{canRevealPayeeBank && ["reviewed", "failed"].includes(selectedBatch.status) && selectedBatch.paymentAccount ? <Button type="button" size="sm" variant="ghost" className="h-11 w-11 shrink-0 p-0" aria-label="查看完整账号" title="查看完整账号" onClick={() => { setActionReason(""); setRevealedPayeeAccount(""); setReasonAction({ kind: "payeeBank", batchId: selectedBatch.id, batchNo: selectedBatch.batchNo, itemId: item.id, entityCode: item.entityCode }); }}><Eye aria-hidden="true" className="size-4" /></Button> : null}</div></div>
|
||||
<div className="detail-fact-card"><div className="detail-fact-card__label">付款渠道</div><div className="detail-fact-card__value">{paymentChannelLabel(item.paymentChannelOverride ?? selectedBatch.defaultPaymentChannel)}</div></div>
|
||||
</div>
|
||||
{["reviewed", "failed"].includes(selectedBatch.status) ? (
|
||||
@@ -1073,18 +1074,21 @@ export function PaymentBatchesPage() {
|
||||
|
||||
<BusinessDetailAside>
|
||||
<BusinessDetailActionPanel
|
||||
inlineActions
|
||||
className="hidden lg:block lg:static"
|
||||
status={<StatusBadge status={selectedBatch.status} label={paymentBatchStatusLabel(selectedBatch.status)} />}
|
||||
primaryAction={paymentActionModel.primary ? renderPaymentActionButton(paymentActionModel.primary) : undefined}
|
||||
moreActions={paymentActionModel.more.length ? (
|
||||
<details className="detail-disclosure">
|
||||
<summary>更多</summary>
|
||||
<div className="mt-3 grid gap-2">
|
||||
{paymentActionModel.more.map((kind) =>
|
||||
renderPaymentActionButton(kind, kind === "cancel" ? "destructive" : "secondary"),
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild><Button type="button" variant="secondary">更多</Button></DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{paymentActionModel.more.map((kind) => (
|
||||
<DropdownMenuItem key={kind} data-testid={`payment-batch-action-${kind}`} disabled={paymentActionDisabled(kind)} className={kind === "cancel" ? "min-h-11 text-red-600 focus:text-red-600" : "min-h-11"} onSelect={() => activatePaymentAction(kind)}>
|
||||
{paymentActionLabel(kind)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : undefined}
|
||||
summary={(
|
||||
<>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Link, useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../components/ui/dropdown-menu";
|
||||
import { fetchExpenseTypes } from "../api/expenseTypes";
|
||||
import type { ExpenseStandardCheck, PrecheckResponse } from "../api/feeControl";
|
||||
import {
|
||||
@@ -114,7 +115,6 @@ export function ReimbursementDetailPage() {
|
||||
const [itemError, setItemError] = useState("");
|
||||
const [confirmDeleteItemId, setConfirmDeleteItemId] = useState("");
|
||||
const [confirmDeleteReimbursement, setConfirmDeleteReimbursement] = useState(false);
|
||||
const [showSecondaryDetail, setShowSecondaryDetail] = useState(false);
|
||||
const [submitConfirmPrecheck, setSubmitConfirmPrecheck] = useState<PrecheckResponse | null>(null);
|
||||
const [isInvoiceDialogOpen, setIsInvoiceDialogOpen] = useState(false);
|
||||
const [invoiceSearchInput, setInvoiceSearchInput] = useState("");
|
||||
@@ -1041,17 +1041,6 @@ export function ReimbursementDetailPage() {
|
||||
<Link to={`/reimbursements/${detail.id}/print`}>打印 / PDF</Link>
|
||||
</Button>
|
||||
) : null,
|
||||
hasMoreActions ? (
|
||||
<Button
|
||||
key="more"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
data-testid="reimbursement-more-toggle"
|
||||
onClick={() => setShowSecondaryDetail((value) => !value)}
|
||||
>
|
||||
{showSecondaryDetail ? "收起更多" : "更多"}
|
||||
</Button>
|
||||
) : null,
|
||||
];
|
||||
|
||||
const workspaceEvidence = buildReimbursementApprovalEvidence(detail, {
|
||||
@@ -1112,8 +1101,10 @@ export function ReimbursementDetailPage() {
|
||||
</>
|
||||
);
|
||||
const workspaceActionPanel = (
|
||||
<BusinessDetailActionPanel className="lg:static" status={<StatusBadge status={detail.deletedAt ? "deleted" : detail.status} />} primaryAction={primaryAction} secondaryActions={secondaryActions} moreActions={showSecondaryDetail ? <div className="grid gap-2" data-testid="reimbursement-secondary-detail">{detail.status !== "paid" ? <Button asChild variant="secondary"><Link to={"/reimbursements/" + detail.id + "/print"}>打印 / PDF</Link></Button> : null}{canDeleteReimbursement ? <Button data-testid="reimbursement-delete-draft" disabled={deleteReimbursementMutation.isPending} onClick={() => setConfirmDeleteReimbursement(true)} type="button" variant="destructive">删除报销单</Button> : null}</div> : null} summary={<><div>下一步:{nextStepText}</div>{workspaceModel.issueAnchors.map((issue) => <a className="block hover:underline" href={"#" + issue.id} key={issue.label}>{issue.label}</a>)}</>} testId="reimbursement-action-panel">
|
||||
<BusinessDetailActionPanel inlineActions className="lg:static" status={<StatusBadge status={detail.deletedAt ? "deleted" : detail.status} />} primaryAction={primaryAction} secondaryActions={secondaryActions} moreActions={hasMoreActions ? <DropdownMenu><DropdownMenuTrigger asChild><Button data-testid="reimbursement-more-toggle" type="button" variant="secondary">更多</Button></DropdownMenuTrigger><DropdownMenuContent align="end" data-testid="reimbursement-secondary-detail">{detail.status !== "paid" ? <DropdownMenuItem asChild className="min-h-11"><Link to={"/reimbursements/" + detail.id + "/print"}>打印 / PDF</Link></DropdownMenuItem> : null}{canDeleteReimbursement ? <DropdownMenuItem className="min-h-11 text-red-600 focus:text-red-600" data-testid="reimbursement-delete-draft" disabled={deleteReimbursementMutation.isPending} onSelect={() => setConfirmDeleteReimbursement(true)}>删除报销单</DropdownMenuItem> : null}</DropdownMenuContent></DropdownMenu> : null} summary={<><div>下一步:{nextStepText}</div>{workspaceModel.issueAnchors.map((issue) => <a className="block hover:underline" href={"#" + issue.id} key={issue.label}>{issue.label}</a>)}</>} testId="reimbursement-action-panel">
|
||||
{(detail.status === "reviewing" && isReimbursementParticipant) || (["reviewing", "approved"].includes(detail.status) && actionError) ? (
|
||||
<ReimbursementActions status={detail.status} showSubmitAction={false} showWithdrawAction={isReimbursementParticipant} actionError={["reviewing", "approved"].includes(detail.status) ? actionError : ""} submitDisabled={blockStandardChecks.length > 0} submitHint="" submitNotice="" isPending={mutation.isPending} onSubmit={() => void handleSubmitAction()} onWithdraw={(comment) => mutation.mutate({ action: "withdraw", comment })} />
|
||||
) : null}
|
||||
</BusinessDetailActionPanel>
|
||||
);
|
||||
|
||||
|
||||
@@ -680,7 +680,7 @@ test.describe("报销付款交接一致性", () => {
|
||||
const link = page.getByRole("link", { name: "查看所在付款批次", exact: true });
|
||||
await expect(link).toHaveCount(1);
|
||||
const linkBounds = await link.boundingBox();
|
||||
expect(linkBounds!.height).toBeGreaterThanOrEqual(44);
|
||||
expect(Math.round(linkBounds!.height * 100) / 100).toBeGreaterThanOrEqual(44);
|
||||
if (!mobile) expect(linkBounds!.width).toBeLessThan(180);
|
||||
await link.click();
|
||||
await expect(page.getByTestId("payment-batch-processing-panel")).toBeVisible();
|
||||
@@ -698,7 +698,99 @@ test.describe("报销付款交接一致性", () => {
|
||||
});
|
||||
}
|
||||
|
||||
test("账号查看图标紧随脱敏值且确认前不请求完整账号", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1440, height: 960 });
|
||||
await mockSystemActionApis(page, {
|
||||
canExportPaymentOrder: true,
|
||||
meOverride: { roleCodes: ["finance"], permissions: [...me.permissions, "employee.bank.read"] },
|
||||
});
|
||||
let reveals = 0;
|
||||
page.on("request", (request) => { if (new URL(request.url()).pathname.endsWith("/payee-bank/reveal")) reveals += 1; });
|
||||
await login(page, user);
|
||||
await page.goto(`/payment-batches?id=${paymentBatchId}`);
|
||||
const reveal = page.getByRole("button", { name: "查看完整账号", exact: true });
|
||||
await expect(reveal).toBeVisible();
|
||||
await expect(reveal).toHaveText("");
|
||||
await expect(reveal).toHaveAttribute("title", "查看完整账号");
|
||||
const icon = await reveal.boundingBox();
|
||||
const account = await reveal.locator("..").locator("span").boundingBox();
|
||||
expect(icon!.width).toBeGreaterThanOrEqual(44);
|
||||
expect(icon!.height).toBeGreaterThanOrEqual(44);
|
||||
expect(icon!.x).toBeGreaterThan(account!.x);
|
||||
await reveal.focus();
|
||||
await page.keyboard.press("Enter");
|
||||
const dialog = page.getByRole("dialog");
|
||||
await expect(dialog).toContainText("本次查看会记录");
|
||||
await expect(dialog.getByRole("textbox", { name: "查看收款账号原因" })).toBeVisible();
|
||||
expect(reveals).toBe(0);
|
||||
await dialog.getByRole("button", { name: "返回", exact: true }).click();
|
||||
expect(reveals).toBe(0);
|
||||
});
|
||||
|
||||
for (const width of [390, 1280, 1440]) {
|
||||
test(`票据紧凑展示${width}多张长名称及预览不丢失`, async ({ page }, testInfo) => {
|
||||
await page.setViewportSize({ width, height: 960 });
|
||||
await mockSystemActionApis(page, { reimbursementStatus: "approved" });
|
||||
const detail = reimbursementDetail("approved");
|
||||
const sellerName = "合成开票方长名称".repeat(8);
|
||||
detail.invoices = Array.from({ length: 5 }, (_, index) => ({
|
||||
...detail.invoices[0],
|
||||
id: `invoice-layout-${index}`,
|
||||
invoiceNumber: `SYNTHETIC-LAYOUT-000000000000000000000${index}`,
|
||||
sellerName,
|
||||
}));
|
||||
detail.attachments = [];
|
||||
await page.route(`**/api/reimbursements/${reimbursementId}`, (route) => fulfillJson(route, detail));
|
||||
let previews = 0;
|
||||
await page.route("**/api/invoice-pool/invoice-layout-0/signed-url", async (route) => {
|
||||
previews += 1;
|
||||
await fulfillJson(route, { signedUrl: "about:blank", originalName: "合成预览.pdf", fileSize: 1024 });
|
||||
});
|
||||
await login(page, user);
|
||||
await page.goto(`/reimbursements/${reimbursementId}`);
|
||||
const table = page.getByTestId("reimbursement-invoices-table");
|
||||
await expect(table).toBeVisible();
|
||||
await expect(table.getByTestId("approval-invoice-evidence")).toHaveCount(5);
|
||||
await expect(table.getByText(sellerName, { exact: true })).toHaveCount(5);
|
||||
if (width >= 1024) await expect(table.getByRole("columnheader", { name: "金额" })).toBeVisible();
|
||||
else await expect(table.locator("thead")).toBeHidden();
|
||||
await expect(page.getByText("未上传补充附件", { exact: true })).toBeVisible();
|
||||
await table.getByRole("button", { name: `查看发票:${detail.invoices[0].invoiceNumber}`, exact: true }).click();
|
||||
await expect.poll(() => previews).toBe(1);
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 2)).toBe(true);
|
||||
await table.screenshot({ path: testInfo.outputPath("invoice-table.png") });
|
||||
});
|
||||
}
|
||||
|
||||
for (const width of [1280, 1440]) {
|
||||
test(`桌面${width}付款与报销侧栏等宽且更多紧随主按钮`, async ({ page }, testInfo) => {
|
||||
await page.setViewportSize({ width, height: 960 });
|
||||
await mockSystemActionApis(page, { reimbursementStatus: "approved", reimbursementProgress: () => progress("reviewed") });
|
||||
await login(page, user);
|
||||
const widths: number[] = [];
|
||||
for (const [url, panelId] of [
|
||||
[`/reimbursements/${reimbursementId}`, "reimbursement-action-panel"],
|
||||
[`/payment-batches?id=${paymentBatchId}`, "payment-batch-action-panel"],
|
||||
]) {
|
||||
await page.goto(url);
|
||||
const panel = page.getByTestId(panelId);
|
||||
await expect(panel).toBeVisible();
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
widths.push((await panel.locator("../..").boundingBox())!.width);
|
||||
const row = panel.getByTestId("detail-inline-actions");
|
||||
const primary = await row.locator("a, button").first().boundingBox();
|
||||
const more = await row.getByRole("button", { name: "更多", exact: true }).boundingBox();
|
||||
expect(Math.abs(primary!.y + primary!.height / 2 - more!.y - more!.height / 2)).toBeLessThan(2);
|
||||
expect(more!.x).toBeGreaterThan(primary!.x + primary!.width);
|
||||
await row.getByRole("button", { name: "更多", exact: true }).click();
|
||||
await expect(page.getByRole("menu")).toBeVisible();
|
||||
await page.keyboard.press("Escape");
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 2)).toBe(true);
|
||||
await page.screenshot({ path: testInfo.outputPath(`${panelId}.png`), fullPage: true });
|
||||
}
|
||||
expect(widths).toEqual([320, 320]);
|
||||
});
|
||||
|
||||
test(`桌面${width}创建批次按钮短文案按内容宽度且只导航不组批`, async ({ page }, testInfo) => {
|
||||
await page.setViewportSize({ width, height: 960 });
|
||||
await mockSystemActionApis(page, { reimbursementStatus: "approved", reimbursementProgress: () => progress("none") });
|
||||
@@ -913,13 +1005,13 @@ test.describe("R7E 全站高风险操作二次确认", () => {
|
||||
const actionPanel = page.getByTestId("payment-batch-action-panel");
|
||||
await expect(actionPanel.getByTestId("payment-batch-action-downloadOrder")).toBeVisible();
|
||||
await actionPanel.getByText("更多", { exact: true }).click();
|
||||
await expect(actionPanel.getByRole("button", { name: "上传银行回单" })).toBeVisible();
|
||||
await expect(page.getByRole("menuitem", { name: "上传银行回单" })).toBeVisible();
|
||||
await expect(page.getByTestId("payment-batch-mobile-actions")).toBeHidden();
|
||||
await expect(page.getByTestId("payment-batch-receipt-panel").getByRole("button", { name: "上传银行回单" })).toHaveCount(0);
|
||||
await expect(page.getByRole("button", { name: "登记付款完成" })).toHaveCount(0);
|
||||
|
||||
const desktopFileChooser = page.waitForEvent("filechooser");
|
||||
await actionPanel.getByRole("button", { name: "上传银行回单" }).click();
|
||||
await page.getByRole("menuitem", { name: "上传银行回单" }).click();
|
||||
await (await desktopFileChooser).setFiles({
|
||||
name: "合成银行回单.pdf",
|
||||
mimeType: "application/pdf",
|
||||
@@ -1008,7 +1100,7 @@ test.describe("R7E 全站高风险操作二次确认", () => {
|
||||
await page.screenshot({ path: testInfo.outputPath("after-payment-order.png"), fullPage: true });
|
||||
await actions.getByText("更多", { exact: true }).click();
|
||||
if (mobile) await page.getByRole("menuitem", { name: "重新下载制单" }).click();
|
||||
else await actions.getByTestId("payment-batch-action-redownloadOrder").click();
|
||||
else await page.getByRole("menuitem", { name: "重新下载制单" }).click();
|
||||
await expect(dialog).toContainText("请勿重复提交");
|
||||
await expect(dialog.getByRole("textbox")).toHaveCount(0);
|
||||
const secondDownload = page.waitForEvent("download");
|
||||
|
||||
在新工单中引用
屏蔽一个用户