srm-fix-regression.spec.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. import { test, expect } from '../fixtures/auth';
  2. import type { Page, Response } from '@playwright/test';
  3. /**
  4. * SRM schema mismatch 修复后回归:
  5. * Supplier 删除拦截:用 SupplierNumber 真实列查 SRM,应正确拦截
  6. * Material 抽查:原 Routing 引用样本仍应被拦截(不受 SRM 分支移除影响)
  7. */
  8. async function token(page: Page) {
  9. return page.evaluate(() => {
  10. for (let i = 0; i < localStorage.length; i++) {
  11. const k = localStorage.key(i)!;
  12. if (/access-token$/i.test(k) && !/x-access-token$/i.test(k)) {
  13. const raw = localStorage.getItem(k)!;
  14. try { const v = JSON.parse(raw); return typeof v === 'string' ? v : v?.value ?? raw; } catch { return raw; }
  15. }
  16. }
  17. return null;
  18. });
  19. }
  20. async function api<T = any>(page: Page, method: 'GET' | 'DELETE', url: string): Promise<{ status: number; body: T | string | null }> {
  21. const tk = await token(page);
  22. const base = new URL(page.url()).origin;
  23. const r = await page.request.fetch(`${base}${url}`, {
  24. method,
  25. headers: { Authorization: `Bearer ${tk!}` },
  26. });
  27. let body: any = null;
  28. try { body = await r.json(); } catch { body = await r.text().catch(() => null); }
  29. return { status: r.status(), body };
  30. }
  31. test('Supplier 删除被 SRM 引用 → 409 + 红色 toast(修复后)', async ({ authedPage }) => {
  32. // 已知样本:Supp=10001875 在 SRM 有 390 条引用
  33. const SAMPLE_SUPP = '10001875';
  34. await authedPage.goto('/#/dashboard/home', { waitUntil: 'domcontentloaded' });
  35. // 反查 id
  36. const lookup = await api(authedPage, 'GET', `/api/s0/supply/suppliers?Supp=${SAMPLE_SUPP}&page=1&pageSize=5&tenantId=1&factoryId=1`);
  37. const items: any[] = (lookup.body as any)?.list ?? [];
  38. test.skip(items.length === 0, `供应商样本 ${SAMPLE_SUPP} 不存在`);
  39. const sample = { id: Number(items[0].id), supp: items[0].supp };
  40. test.info().annotations.push({ type: 'sample', description: JSON.stringify(sample) });
  41. // 走 UI:Supplier 列表页 → 搜索 → 删除
  42. await authedPage.goto('/#/aidop/s0/supply/supplier', { waitUntil: 'domcontentloaded' });
  43. await authedPage.waitForLoadState('networkidle', { timeout: 10_000 }).catch(() => {});
  44. await expect(authedPage.locator('table tbody tr').first()).toBeVisible({ timeout: 15_000 });
  45. const kw = authedPage.locator('input[placeholder*="编码/简称"]').first();
  46. await expect(kw).toBeVisible({ timeout: 5_000 });
  47. await kw.fill(SAMPLE_SUPP);
  48. const respWait = authedPage.waitForResponse((r) => /\/api\/s0\/supply\/suppliers\?/.test(r.url()) && r.request().method() === 'GET', { timeout: 10_000 });
  49. await authedPage.getByRole('button', { name: /^查询$/ }).first().click();
  50. await respWait;
  51. await authedPage.waitForLoadState('networkidle', { timeout: 5_000 }).catch(() => {});
  52. const row = authedPage.locator('tr', { hasText: SAMPLE_SUPP }).first();
  53. await expect(row).toBeVisible({ timeout: 10_000 });
  54. let delResp: Response | null = null;
  55. authedPage.on('response', (r) => {
  56. if (/\/api\/s0\/supply\/suppliers\/\d+/.test(r.url()) && r.request().method() === 'DELETE') delResp = r;
  57. });
  58. await row.getByRole('button', { name: /删除/ }).click();
  59. const confirm = authedPage.locator('.el-message-box').getByRole('button', { name: /确\s*定/ });
  60. await expect(confirm).toBeVisible({ timeout: 5_000 });
  61. await confirm.click();
  62. await expect.poll(() => delResp?.status() ?? 0, { timeout: 10_000 }).toBe(409);
  63. const body = await delResp!.json().catch(() => null);
  64. test.info().annotations.push({
  65. type: 'api',
  66. description: `DELETE → ${delResp!.status()} body=${JSON.stringify(body).slice(0, 200)}`,
  67. });
  68. expect(body?.code).toBe('S01006');
  69. expect(String(body?.message ?? '')).toMatch(/引用该供应商|SrmPurchase|货源清单/);
  70. const errToast = authedPage.locator('.el-message--error, .el-message.is-error').first();
  71. await expect(errToast).toBeVisible({ timeout: 5_000 });
  72. const toastText = (await errToast.innerText()).replace(/\s+/g, '');
  73. test.info().annotations.push({ type: 'toast', description: toastText });
  74. expect(toastText).toMatch(/引用该供应商|货源清单/);
  75. const okToast = authedPage.locator('.el-message--success').first();
  76. expect(await okToast.isVisible().catch(() => false)).toBeFalsy();
  77. await expect(authedPage.locator('tr', { hasText: SAMPLE_SUPP }).first()).toBeVisible({ timeout: 3_000 });
  78. });
  79. test('Material 连带回归:原 Routing 引用样本仍 409', async ({ authedPage }) => {
  80. // 直接 API 验:DELETE 物料 id 4479 (itemNum 3132C0G6, 9 条 routing 引用)
  81. await authedPage.goto('/#/dashboard/home', { waitUntil: 'domcontentloaded' });
  82. const r = await api(authedPage, 'DELETE', '/api/s0/sales/materials/4479?tenantId=1&factoryId=1');
  83. test.info().annotations.push({
  84. type: 'api',
  85. description: `DELETE /materials/4479 → ${r.status} body=${JSON.stringify(r.body).slice(0, 200)}`,
  86. });
  87. expect(r.status).toBe(409);
  88. expect((r.body as any)?.code).toBe('S01006');
  89. expect(String((r.body as any)?.message ?? '')).toMatch(/工艺路线明细/);
  90. });