| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147 |
- /**
- * S0 组织 ID 类型一致性|运行时取样(简化版)
- *
- * 目标:不做 Vue 内部反射,只靠「UI 驱动 + 网络请求体 + 可见错误文案」留证。
- * 每个样本页面分别记录:
- * - 打开新增弹窗后拦截到的 GET /api/sysOrg/list 响应体中 id 的 JSON 原文类型
- * - 选完公司/工厂后,点保存捕获的 POST/PUT 请求体里 companyRefId / factoryRefId 的 JSON 原文类型
- * - 可见错误提示文案(form-item error 或 el-message)
- *
- * 本轮只收集证据;结论写入 artifacts/S0-org-id-runtime-sampling-report.md。
- */
- import { test, expect } from './fixtures/auth';
- import type { Page, Request, Response } from '@playwright/test';
- interface Capture {
- orgListIdSample: string | null; // raw JSON substring, e.g. `"id":"1300000000001"` vs `"id":1300000000001`
- saveRequestBody: string | null; // raw JSON body
- saveStatus: number | null;
- saveResponseBody: string | null;
- visibleErrors: string[];
- consoleErrors: string[];
- }
- function attachCaptures(page: Page, cap: Capture): void {
- page.on('response', async (resp: Response) => {
- try {
- const url = resp.url();
- if (/\/api\/sysOrg\/list/.test(url) && cap.orgListIdSample === null) {
- const txt = await resp.text();
- const m = txt.match(/"id":\s*("[^"]+"|\d+)/);
- if (m) cap.orgListIdSample = m[0];
- }
- } catch {}
- });
- page.on('request', async (req: Request) => {
- const url = req.url();
- const method = req.method();
- if (!/\/api\/s0\//.test(url)) return;
- if (!['POST', 'PUT'].includes(method)) return;
- if (cap.saveRequestBody === null) cap.saveRequestBody = req.postData() ?? '';
- });
- page.on('console', (m) => {
- if (m.type() === 'error') cap.consoleErrors.push(m.text());
- });
- page.on('pageerror', (e) => cap.consoleErrors.push(`pageerror: ${e.message}`));
- }
- async function clickAddButtonInContent(page: Page, name: string): Promise<boolean> {
- // Scope to the active tab content, not global — avoids picking home-page buttons.
- const scope = page.locator('.el-main, .layout-view-bg-white, .app-main, main').first();
- const scoped = scope.isVisible().catch(() => false);
- const search = (await scoped) ? scope : page;
- const btn = search.getByRole('button', { name }).first();
- if (!(await btn.isVisible({ timeout: 10_000 }).catch(() => false))) return false;
- await btn.click();
- await page.waitForTimeout(700);
- return true;
- }
- async function pickFirstInSelect(page: Page, selectLocator: string, nth: number): Promise<string | null> {
- const select = page.locator(selectLocator).nth(nth);
- if (!(await select.isVisible().catch(() => false))) return null;
- const wrapper = select.locator('.el-select__wrapper, .select-trigger').first();
- await wrapper.click({ force: true });
- await page.waitForTimeout(300);
- const opt = page
- .locator('.el-select-dropdown:visible .el-select-dropdown__item:not(.is-disabled)')
- .first();
- if (!(await opt.isVisible({ timeout: 5000 }).catch(() => false))) return null;
- const text = (await opt.innerText()).trim();
- await opt.click({ force: true }).catch(() => {});
- await page.waitForTimeout(400);
- return text;
- }
- const SAMPLES: Array<{ name: string; hash: string; addBtnName: RegExp | string }> = [
- { name: 'sales/CustomerList (Pattern A, baseline)', hash: '#/aidop/s0/sales/customer', addBtnName: /^新增客户$/ },
- { name: 'sales/MaterialList (Pattern B+C, form default=0)', hash: '#/aidop/s0/sales/material', addBtnName: /新 ?增/ },
- { name: 'sales/OrderReviewCycleList (Pattern B+C, form default=0)', hash: '#/aidop/s0/sales/order-review-cycle', addBtnName: /^新 ?增$/ },
- { name: 'manufacturing/ProductionLineList (cascade)', hash: '#/aidop/s0/manufacturing/production-line', addBtnName: /^新 ?增$/ },
- { name: 'supply/SupplierList (cross-domain)', hash: '#/aidop/s0/supply/supplier', addBtnName: /新 ?增/ },
- ];
- for (const s of SAMPLES) {
- test(`runtime sample: ${s.name}`, async ({ authedPage }) => {
- const page = authedPage;
- const cap: Capture = {
- orgListIdSample: null,
- saveRequestBody: null,
- saveStatus: null,
- saveResponseBody: null,
- visibleErrors: [],
- consoleErrors: [],
- };
- attachCaptures(page, cap);
- await page.goto(`/${s.hash}`, { waitUntil: 'domcontentloaded' });
- await page.waitForLoadState('networkidle', { timeout: 20_000 }).catch(() => {});
- await page.waitForTimeout(1000);
- const opened = await clickAddButtonInContent(
- page,
- typeof s.addBtnName === 'string' ? s.addBtnName : (s.addBtnName as any),
- );
- console.log(`[${s.name}] dialog opened: ${opened}`);
- await page.waitForTimeout(800);
- if (opened) {
- // Pick first and second el-select inside the visible dialog.
- const selInDialog = '.el-dialog:visible .el-select, .el-drawer:visible .el-select';
- const company = await pickFirstInSelect(page, selInDialog, 0);
- const factory = await pickFirstInSelect(page, selInDialog, 1);
- console.log(`[${s.name}] picked company=${company} factory=${factory}`);
- // Attempt to click Save. Response listener is already attached via attachCaptures.
- const saveBtn = page
- .locator('.el-dialog:visible, .el-drawer:visible')
- .getByRole('button', { name: /^(确 ?定|保 ?存|提 ?交)$/ })
- .first();
- if (await saveBtn.isVisible().catch(() => false)) {
- const respPromise = page
- .waitForResponse(
- (r) => /\/api\/s0\//.test(r.url()) && ['POST', 'PUT'].includes(r.request().method()),
- { timeout: 6000 },
- )
- .catch(() => null);
- await saveBtn.click();
- const resp = await respPromise;
- if (resp) {
- cap.saveStatus = resp.status();
- cap.saveResponseBody = (await resp.text()).slice(0, 400);
- }
- await page.waitForTimeout(600);
- const errs = await page
- .locator('.el-form-item__error, .el-message--error, .el-message__content')
- .allInnerTexts()
- .catch(() => []);
- cap.visibleErrors = errs.map((x) => x.trim()).filter(Boolean);
- } else {
- console.log(`[${s.name}] save button not visible`);
- }
- }
- console.log(`\n=== EVIDENCE: ${s.name} ===\n${JSON.stringify(cap, null, 2)}\n`);
- expect(true).toBe(true);
- });
- }
|