|
|
@@ -0,0 +1,147 @@
|
|
|
+/**
|
|
|
+ * 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);
|
|
|
+ });
|
|
|
+}
|