This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
import type { Page, Route } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
const projectId = '01M00000000000000000000101';
|
||||
const receivableId = '01M00000000000000000000102';
|
||||
const invoiceId = '01M00000000000000000000103';
|
||||
const sourceDocumentId = '01M00000000000000000000108';
|
||||
|
||||
function envelope(data: unknown, totalElements?: number) {
|
||||
return {
|
||||
data,
|
||||
...(typeof totalElements === 'number'
|
||||
? { meta: { page: 1, size: 20, totalElements, totalPages: totalElements ? 1 : 0 } }
|
||||
: {}),
|
||||
requestId: '01M00000000000000000000199',
|
||||
};
|
||||
}
|
||||
|
||||
function session() {
|
||||
return {
|
||||
authenticated: true,
|
||||
user: {
|
||||
publicId: '01M00000000000000000000104',
|
||||
username: 'finance-e2e',
|
||||
displayName: '财务验收用户',
|
||||
departmentName: '财务部',
|
||||
mustChangePassword: false,
|
||||
},
|
||||
roles: [{ code: 'FINANCE_MANAGER', name: '财务管理人员', workbenchRoute: '/workbench/finance' }],
|
||||
activeRole: 'FINANCE_MANAGER',
|
||||
permissions: [
|
||||
'receivable:receipt:view',
|
||||
'receivable:receipt:create',
|
||||
'receivable:receipt:export',
|
||||
'receivable:invoice:view',
|
||||
'receivable:invoice:create',
|
||||
'receivable:invoice:export',
|
||||
'receivable:invoice:review',
|
||||
'source:form:view',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function receivable() {
|
||||
return {
|
||||
publicId: receivableId,
|
||||
businessNo: 'REC-E2E-001',
|
||||
company: { publicId: '01M00000000000000000000105', code: 'C-E2E', name: '凯迪测试公司' },
|
||||
project: { publicId: projectId, code: 'P-E2E', name: '应收穿透项目' },
|
||||
contract: { publicId: '01M00000000000000000000106', code: 'HT-E2E', name: '测试合同' },
|
||||
counterparty: { publicId: '01M00000000000000000000107', code: 'KH-E2E', name: '测试客户' },
|
||||
businessDate: '2026-08-15',
|
||||
amount: '125000.00',
|
||||
settledAmount: '25000.00',
|
||||
outstandingAmount: '100000.00',
|
||||
currency: 'CNY',
|
||||
status: 'PARTIALLY_SETTLED',
|
||||
sourceInvoiceId: invoiceId,
|
||||
version: 1,
|
||||
createdAt: '2026-08-15T08:00:00Z',
|
||||
updatedAt: '2026-08-16T08:00:00Z',
|
||||
};
|
||||
}
|
||||
|
||||
function invoice(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
publicId: invoiceId,
|
||||
businessNo: 'INV-E2E-001',
|
||||
company: receivable().company,
|
||||
project: receivable().project,
|
||||
contract: receivable().contract,
|
||||
counterparty: receivable().counterparty,
|
||||
invoiceType: 'SPECIAL',
|
||||
amount: '125000.00',
|
||||
currency: 'CNY',
|
||||
taxRate: '0.13',
|
||||
requestedDate: '2026-08-15',
|
||||
invoiceNo: 'FP-E2E-001',
|
||||
issuedDate: '2026-08-15',
|
||||
status: 'ISSUED',
|
||||
contractLimit: '500000.00',
|
||||
contractCommittedAmount: '125000.00',
|
||||
contractAvailableAmount: '375000.00',
|
||||
version: 2,
|
||||
createdAt: '2026-08-15T08:00:00Z',
|
||||
updatedAt: '2026-08-16T08:00:00Z',
|
||||
allowedActions: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
async function installFixture(page: Page, receivableQueries: URLSearchParams[]) {
|
||||
const userSession = session();
|
||||
await page.route('**/api/v1/**', async (route: Route) => {
|
||||
const request = route.request();
|
||||
const url = new URL(request.url());
|
||||
const pathname = url.pathname;
|
||||
if (pathname === '/api/v1/auth/session') return route.fulfill({ json: envelope(userSession) });
|
||||
if (pathname === '/api/v1/auth/profile') return route.fulfill({ json: envelope(userSession.user) });
|
||||
if (pathname === '/api/v1/auth/roles') return route.fulfill({ json: envelope(userSession.roles) });
|
||||
if (pathname === '/api/v1/receivables/references') {
|
||||
return route.fulfill({
|
||||
json: envelope({
|
||||
companies: [receivable().company],
|
||||
projects: [receivable().project],
|
||||
counterparties: [receivable().counterparty],
|
||||
contracts: [receivable().contract],
|
||||
receivables: [],
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (pathname === '/api/v1/receivables') {
|
||||
receivableQueries.push(url.searchParams);
|
||||
return route.fulfill({ json: envelope([receivable()], 1) });
|
||||
}
|
||||
if (pathname === `/api/v1/invoices/${invoiceId}`) return route.fulfill({ json: envelope(invoice()) });
|
||||
if (request.method() === 'GET') return route.fulfill({ json: envelope([], 0) });
|
||||
return route.fulfill({ status: 204 });
|
||||
});
|
||||
}
|
||||
|
||||
async function installInvoiceReviewFixture(
|
||||
page: Page,
|
||||
invoiceQueries: URLSearchParams[],
|
||||
reviewRequests: Array<Record<string, unknown>>,
|
||||
reviewHeaders: Array<Record<string, string>>,
|
||||
) {
|
||||
const userSession = session();
|
||||
let currentInvoice = invoice({
|
||||
businessNo: 'INV-E2E-RETURN-001',
|
||||
sourceDocumentId,
|
||||
sourceVersionId: '01M00000000000000000000109',
|
||||
invoiceNo: null,
|
||||
issuedDate: null,
|
||||
status: 'REVIEWING',
|
||||
version: 7,
|
||||
allowedActions: ['REVIEW'],
|
||||
});
|
||||
|
||||
await page.route('**/api/v1/**', async (route: Route) => {
|
||||
const request = route.request();
|
||||
const url = new URL(request.url());
|
||||
const pathname = url.pathname;
|
||||
if (pathname === '/api/v1/auth/session') return route.fulfill({ json: envelope(userSession) });
|
||||
if (pathname === '/api/v1/auth/profile') return route.fulfill({ json: envelope(userSession.user) });
|
||||
if (pathname === '/api/v1/auth/roles') return route.fulfill({ json: envelope(userSession.roles) });
|
||||
if (pathname === '/api/v1/auth/csrf') {
|
||||
return route.fulfill({
|
||||
headers: { 'set-cookie': 'XSRF-TOKEN=page14-e2e; Path=/' },
|
||||
json: envelope({ token: 'page14-e2e' }),
|
||||
});
|
||||
}
|
||||
if (request.method() === 'GET' && pathname === '/api/v1/receivables/references') {
|
||||
return route.fulfill({
|
||||
json: envelope({
|
||||
companies: [receivable().company],
|
||||
projects: [receivable().project],
|
||||
counterparties: [receivable().counterparty],
|
||||
contracts: [receivable().contract],
|
||||
receivables: [],
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (request.method() === 'GET' && pathname === '/api/v1/invoices') {
|
||||
invoiceQueries.push(new URLSearchParams(url.searchParams));
|
||||
return route.fulfill({ json: envelope([currentInvoice], 1) });
|
||||
}
|
||||
if (request.method() === 'GET' && pathname === `/api/v1/invoices/${invoiceId}`) {
|
||||
return route.fulfill({ json: envelope(currentInvoice) });
|
||||
}
|
||||
if (request.method() === 'POST' && pathname === `/api/v1/invoices/${invoiceId}/review`) {
|
||||
const payload = request.postDataJSON() as Record<string, unknown>;
|
||||
reviewRequests.push(payload);
|
||||
reviewHeaders.push(request.headers());
|
||||
currentInvoice = invoice({
|
||||
...currentInvoice,
|
||||
status: 'RETURNED',
|
||||
version: 8,
|
||||
reviewOpinion: payload.opinion,
|
||||
allowedActions: [],
|
||||
});
|
||||
return route.fulfill({ json: envelope(currentInvoice) });
|
||||
}
|
||||
if (request.method() === 'GET') return route.fulfill({ json: envelope([], 0) });
|
||||
return route.fulfill({ status: 204 });
|
||||
});
|
||||
}
|
||||
|
||||
test('PAGE-14 shows the project receivable and drills into its exact source invoice', async ({ page }) => {
|
||||
const receivableQueries: URLSearchParams[] = [];
|
||||
await installFixture(page, receivableQueries);
|
||||
|
||||
await page.goto(`/finance/receipts-invoices?tab=receivables&projectId=${projectId}&keyword=REC-E2E-001`);
|
||||
await expect(page.getByRole('heading', { name: '收款与发票', exact: true })).toBeVisible();
|
||||
await expect.poll(() => receivableQueries.length).toBeGreaterThan(0);
|
||||
|
||||
const query = receivableQueries.at(-1)!;
|
||||
expect(query.get('projectId')).toBe(projectId);
|
||||
expect(query.get('keyword')).toBe('REC-E2E-001');
|
||||
expect(query.get('sort')).toBe('updatedAt,desc');
|
||||
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.getByText('100,000.00', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('部分核销', { exact: true })).toBeVisible();
|
||||
await page.getByRole('button', { name: '查看', exact: true }).click();
|
||||
const drawer = page.locator('.t-drawer');
|
||||
await expect(drawer.getByText('应收金额', { exact: true })).toBeVisible();
|
||||
await expect(drawer.getByText('未收金额', { exact: true })).toBeVisible();
|
||||
|
||||
await drawer.getByRole('button', { name: '查看来源发票', exact: true }).click();
|
||||
await expect(drawer.locator('.t-drawer__header')).toHaveText('INV-E2E-001');
|
||||
await expect(drawer.getByText('FP-E2E-001', { exact: true })).toBeVisible();
|
||||
await expect(drawer.getByText(/13%/)).toBeVisible();
|
||||
});
|
||||
|
||||
test('PAGE-14 returns an OA-08 invoice with exact correction fields', async ({ page }) => {
|
||||
const invoiceQueries: URLSearchParams[] = [];
|
||||
const reviewRequests: Array<Record<string, unknown>> = [];
|
||||
const reviewHeaders: Array<Record<string, string>> = [];
|
||||
await installInvoiceReviewFixture(page, invoiceQueries, reviewRequests, reviewHeaders);
|
||||
|
||||
await page.goto('/finance/receipts-invoices?tab=invoices');
|
||||
await expect(page.getByText('INV-E2E-RETURN-001', { exact: true }).first()).toBeVisible();
|
||||
await expect.poll(() => invoiceQueries.length).toBeGreaterThan(0);
|
||||
const initialListRequests = invoiceQueries.length;
|
||||
|
||||
await page.getByRole('button', { name: '更多操作', exact: true }).click();
|
||||
await page.getByText('审核申请', { exact: true }).click();
|
||||
const dialog = page.locator('.t-dialog').filter({ hasText: '审核申请' });
|
||||
await dialog.getByText('退回补件', { exact: true }).click();
|
||||
await expect(dialog.getByText('需要更正的来源字段', { exact: true })).toBeVisible();
|
||||
await expect(dialog.getByText('驳回', { exact: true })).toHaveCount(0);
|
||||
|
||||
await dialog.getByRole('button', { name: '确认', exact: true }).click();
|
||||
await expect(page.locator('.t-message').filter({ hasText: '请填写审核意见' })).toBeVisible();
|
||||
await dialog.locator('textarea').fill('请更正本次开票金额和开票要求');
|
||||
await dialog.getByRole('button', { name: '确认', exact: true }).click();
|
||||
await expect(page.locator('.t-message').filter({ hasText: '请选择至少一个需要更正的来源字段' })).toBeVisible();
|
||||
|
||||
await dialog.locator('label.t-checkbox').filter({ hasText: '本次开票金额' }).click();
|
||||
await dialog.locator('label.t-checkbox').filter({ hasText: '开票要求' }).click();
|
||||
await dialog.getByRole('button', { name: '确认', exact: true }).click();
|
||||
|
||||
await expect.poll(() => reviewRequests.length).toBe(1);
|
||||
expect(reviewRequests[0]).toEqual({
|
||||
version: 7,
|
||||
decision: 'RETURN',
|
||||
opinion: '请更正本次开票金额和开票要求',
|
||||
returnPoints: ['invoiceAmount', 'invoiceRequirements'],
|
||||
});
|
||||
expect(reviewHeaders[0]['x-xsrf-token']).toBe('page14-e2e');
|
||||
expect(reviewHeaders[0]['idempotency-key']).toBeTruthy();
|
||||
await expect(dialog).toBeHidden();
|
||||
await expect.poll(() => invoiceQueries.length).toBeGreaterThan(initialListRequests);
|
||||
await expect(page.getByText('已退回', { exact: true })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: '查看', exact: true }).click();
|
||||
const drawer = page.locator('.t-drawer');
|
||||
await expect(drawer.getByText('该候选已退回来源表单,需由原申请人更正并重新完成审批。')).toBeVisible();
|
||||
await drawer.getByRole('button', { name: '查看来源表单', exact: true }).click();
|
||||
await expect(page).toHaveURL(`/forms/OA-08/${sourceDocumentId}`);
|
||||
});
|
||||
Reference in New Issue
Block a user