/** * 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 { // 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 { 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); }); }