云
5 小时以前 724a3d9d2cdb001bd9fae4eaea0ffabfa5ffaf6d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
const { chromium } = require('playwright');
const fs = require('fs'), path = require('path'), assert = require('assert/strict');
const base = process.env.MOBILE_TEST_URL || 'http://127.0.0.1:5174';
const out = path.resolve('.export_tmp/mobile-quality-sales'); fs.mkdirSync(out, { recursive: true });
(async () => {
  const browser = await chromium.launch({ headless: true, channel: 'msedge' });
  const context = await browser.newContext({ viewport: { width: 480, height: 800 } });
  const page = await context.newPage(), errors = [], writes = [], picked = [];
  let quality = null, salesStatus = 2, firstScan = true, readOnly = false;
  const permissions = ['mes:qc-seed-quality:submit', 'mes:qc-seed-quality:query', 'mes:wm-product-sales:query', 'mes:wm-product-sales:stock', 'mes:wm-product-sales:shipping', 'mes:wm-product-sales:finish'];
  page.on('pageerror', e => errors.push(e.message));
  page.on('dialog', d => d.accept());
  await context.route('**/*', async route => {
    const req = route.request(), u = new URL(req.url()), p = u.pathname;
    if (u.origin !== base) return route.abort();
    if (!p.startsWith('/admin-api/')) return route.continue();
    let data = null;
    if (p.endsWith('/auth/login')) data = { accessToken: 'test-token', refreshToken: 'test-refresh', userId: 7, expiresTime: Date.now() + 3600000 };
    else if (p.endsWith('/get-permission-info')) data = { user: { id: 7, nickname: '质检与仓库测试员', username: 'fixture' }, roles: ['worker'], permissions: readOnly ? ['mes:wm-product-sales:query'] : permissions, menus: ['quality', 'quality-records', 'sales'].map((key, i) => ({ id: i + 1, path: '/mobile/' + key, name: key, visible: true, component: 'mobile/' + key })) };
    else if (p.endsWith('/warehouse/simple-list')) data = [{ id: 8, name: '种子合格库', type: 20 }, { id: 9, name: '冻结合格库', type: 20, frozen: true }, { id: 10, name: '暂存库', type: 10 }];
    else if (p.endsWith('/seed-quality/resolve')) {
      const type = u.searchParams.get('scanType') || 'BAG';
      data = { code: type === 'BAG' ? 'TRACE-1' : type + '-TEST', businessCode: '8888888888888888', batchCode: 'BATCH-1', scanType: type, inbound: true, canceled: false, qualityId: quality?.id, result: quality?.result, totalCount: type === 'BAG' ? 1 : 3, pendingBagIds: quality ? [] : type === 'BAG' ? [1] : [2,3], completedBagIds: quality ? type === 'BAG' ? [1] : [1,2,3] : type === 'BAG' ? [] : [1] };
    }
    else if (p.endsWith('/seed-quality/submit-group')) {
      const body = req.postDataJSON(); writes.push({ quality: body });
      quality = { id: 101, code: body.code, batchCode: 'BATCH-1', result: 1, inspectedAt: '2026-10-10 12:00:00' };
      return route.abort('timedout');
    } else if (p.endsWith('/seed-quality/page')) data = { list: quality ? [quality] : [], total: quality ? 1 : 0 };
    else if (p.endsWith('/product-sales/page')) data = { list: [{ id: 11, code: 'SALE-1', clientName: '测试经销商', status: salesStatus }], total: 1 };
    else if (p.endsWith('/product-sales/get')) data = { id: 11, code: 'SALE-1', clientName: '测试经销商', contactName: '现场收货员', contactAddress: '南通市测试仓库', status: salesStatus, carrier: salesStatus === 3 || salesStatus === 4 ? '顺丰' : '', shippingNumber: salesStatus === 3 || salesStatus === 4 ? 'SF12345' : '' };
    else if (p.endsWith('/product-sales-line/page')) data = { list: [{ id: 21, itemId: 1, itemName: '塔河4号', quantity: 2, unitMeasureName: '袋' }], total: 1 };
    else if (p.endsWith('/product-sales-detail/list-by-line')) data = picked;
    else if (p.endsWith('/product-sales/scan-preview')) {
      const type = u.searchParams.get('scanType') || 'BAG', bagId = u.searchParams.get('code') === 'BAG-2' ? 32 : 31;
      const options = (type === 'BAG' ? [bagId] : [31,32,33]).map(id => ({ id, code: 'TRACE-' + id, businessCode: 'BUS-' + id, selectable: id !== 33 && !picked.some(d => d.bagCodeId === id), unavailableReason: id === 33 ? '尚未质检合格' : picked.some(d => d.bagCodeId === id) ? '已加入出库单拣货' : undefined }));
      data = { scanType: type, codeId: type === 'BAG' ? bagId : type === 'PALLET' ? 200 : 100, scanContent: u.searchParams.get('code'), itemId: 1, batchCode: 'BATCH-1', scanQuantity: options.filter(b => b.selectable).length, bags: options };
    }
    else if (p.endsWith('/product-sales/scan')) {
      const body = req.postDataJSON(); writes.push({ scan: body });
      assert.equal(body.quantity, body.bagIds.length);
      for (const bagId of body.bagIds) picked.push({ id: 50 + picked.length, lineId: 21, bagCodeId: bagId, quantity: 1, batchCode: 'BATCH-1', warehouseName: '种子合格库', locationName: '一区', areaName: 'A01', remark: '袋码 TRACE-' + bagId });
      if (firstScan) { firstScan = false; return route.abort('timedout'); }
      data = { outboundQuantity: 1, alreadyScanned: false };
    } else if (p.endsWith('/product-sales/check-quantity')) data = picked.length === 2;
    else if (p.endsWith('/product-sales/stock')) { writes.push({ stock: true }); salesStatus = 10; data = true; }
    else if (p.endsWith('/product-sales/shipping')) { writes.push({ shipping: req.postDataJSON() }); salesStatus = 3; return route.abort('timedout'); }
    else if (p.endsWith('/product-sales/finish')) { writes.push({ finish: true }); salesStatus = 4; data = true; }
    else if (p.endsWith('/simple-list') || p.endsWith('/get-unread-count')) data = [];
    return route.fulfill({ json: { code: 0, data, msg: '' } });
  });
  async function scan(value) { await page.getByRole('textbox', { name: '码值 / 溯源链接' }).fill(value); await page.getByRole('button', { name: '识别码值', exact: true }).click(); }
  async function noOverflow() { assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), 'Page must fit mobile width'); }
  async function capture(name) { await page.locator('#__app-loading__').waitFor({ state: 'detached' }); await page.evaluate(() => document.activeElement?.blur()); await page.waitForFunction(() => { window.scrollTo({ top: 0, behavior: 'instant' }); return window.scrollY <= 1; }); await page.screenshot({ path: path.join(out, name), fullPage: true }); }
  try {
    await page.goto(base + '/auth/login');
    await page.getByRole('radio', { name: '移动端', exact: true }).check();
    await page.locator('input[autocomplete=username]').fill('fixture'); await page.locator('input[autocomplete=current-password]').fill('fixture');
    await page.getByRole('button', { name: '登录移动端', exact: true }).click(); await page.waitForURL('**/mobile/home');
    await page.locator('#__app-loading__').waitFor({ state: 'detached' });
    await page.getByRole('link', { name: '种子质检 袋码、托盘与批次' }).waitFor();
    await capture('home-480.png');
    await page.goto(base + '/mobile/quality'); await scan('8888888888888888');
    await page.getByRole('heading', { name: '本次检验对象' }).waitFor(); assert.equal(writes.length, 0);
    for (const field of await page.getByRole('spinbutton', { name: '实测值', exact: true }).all()) await field.fill('90');
    await page.getByRole('spinbutton', { name: '合格下限', exact: true }).first().fill('95');
    assert(await page.getByRole('textbox', { name: '不合格原因(必填)' }).evaluate(el => el.required));
    await page.getByRole('spinbutton', { name: '合格下限', exact: true }).first().fill('0');
    await page.getByRole('button', { name: '目标合格库 请选择', exact: true }).click();
    assert.equal(await page.getByRole('button', { name: '冻结合格库', exact: true }).count(), 0);
    await page.getByRole('button', { name: '种子合格库', exact: true }).click();
    await noOverflow(); await capture('quality-480.png');
    await page.getByRole('button', { name: '确认提交质检', exact: true }).click();
    await page.getByRole('heading', { name: '质检结果待确认' }).waitFor(); assert.equal(writes.length, 1); assert.equal(writes[0].quality.code, 'TRACE-1');
    await page.reload(); await page.getByRole('button', { name: '核实质检结果' }).click();
    await page.getByText(/已核实:本次袋码均已有质检记录/).waitFor(); assert.equal(writes.length, 1, 'Verification must not resubmit quality');
    await page.goto(base + '/mobile/quality-records');
    await page.getByRole('heading', { name: '批量检验与审核', exact: true }).waitFor();
    await page.getByText('合格', { exact: true }).last().waitFor();
    await capture('quality-records-480.png');
    for (const type of ['PALLET', 'BATCH']) {
      quality = null;
      await page.goto(base + '/mobile/quality');
      await page.getByRole('combobox', { name: '质检扫码类型' }).selectOption(type);
      await scan(type + '-TEST'); await page.getByRole('heading', { name: '本次检验对象' }).waitFor();
      await page.getByText('2 袋', { exact: true }).waitFor();
      const previousWrites = writes.length;
      for (const field of await page.getByRole('spinbutton', { name: '实测值', exact: true }).all()) await field.fill('90');
      await page.getByRole('button', { name: '目标合格库 请选择', exact: true }).click();
      await page.getByRole('button', { name: '种子合格库', exact: true }).click();
      await noOverflow(); await capture('quality-' + type.toLowerCase() + '-480.png');
      assert.equal(writes.length, previousWrites, 'Group recognition must not write inventory');
      await page.getByRole('button', { name: '确认提交质检' }).click();
      await page.getByRole('heading', { name: '质检结果待确认' }).waitFor();
      assert.equal(writes.at(-1).quality.scanType, type); assert.deepEqual(writes.at(-1).quality.bagIds, [2,3]);
      await page.reload(); await page.getByRole('button', { name: '核实质检结果' }).click();
      await page.getByText(/已核实:本次袋码均已有质检记录/).waitFor();
      assert.equal(writes.length, previousWrites + 1, 'Group recovery must not resubmit');
    }
    await page.goto(base + '/mobile/sales'); await page.getByRole('button', { name: 'SALE-1 待拣货 测试经销商' }).click();
    await scan('BAG-1'); await page.getByRole('heading', { name: '核对拣货对象' }).waitFor(); assert.equal(writes.length, 3);
    await page.getByRole('button', { name: '取消本次扫码' }).click();
    await page.getByRole('combobox', { name: '拣货扫码类型' }).selectOption('PALLET');await scan('PALLET-200');
    assert(await page.getByRole('button', { name: '确认加入拣货' }).isDisabled());
    assert(await page.getByRole('checkbox', { name: '选择袋码 TRACE-33', exact: true }).isDisabled());
    await page.getByRole('spinbutton', { name: '本次拣货数量' }).fill('1');await page.getByRole('button', { name: '按数量选择袋码' }).click();
    await page.getByRole('checkbox', { name: '选择袋码 TRACE-31', exact: true }).uncheck();await page.getByRole('checkbox', { name: '选择袋码 TRACE-32', exact: true }).check();
    await noOverflow(); await capture('sales-picking-480.png');
    await page.getByRole('button', { name: '确认加入拣货' }).click(); await page.getByRole('heading', { name: '出库操作结果待确认' }).waitFor();
    await page.reload(); await page.getByRole('button', { name: '核实操作结果' }).click(); await page.getByText('已核实:该码已在拣货清单中。').waitFor();
    assert.equal(writes.length, 4);assert.deepEqual(writes.at(-1).scan.bagIds, [32]);assert.equal(writes.at(-1).scan.scanType, 'PALLET');
    await page.getByRole('combobox', { name: '拣货扫码类型' }).selectOption('BATCH');await scan('BATCH-100');
    assert(await page.getByRole('checkbox', { name: '选择袋码 TRACE-32', exact: true }).isDisabled());
    await page.getByRole('spinbutton', { name: '本次拣货数量' }).fill('1');await page.getByRole('button', { name: '按数量选择袋码' }).click();
    await capture('sales-batch-selection-480.png');await page.setViewportSize({width:320,height:640});await noOverflow();await capture('sales-batch-selection-320.png');await page.setViewportSize({width:480,height:800});
    await page.getByRole('button', { name: '确认加入拣货' }).click();
    await page.getByText('已加入拣货:1 袋。').waitFor();assert.deepEqual(writes.at(-1).scan.bagIds,[31]);assert.equal(writes.at(-1).scan.scanType,'BATCH'); await page.getByRole('button', { name: '完成拣货并核对数量' }).click();
    await page.getByRole('heading', { name: '填写运单', exact: true }).waitFor();
    await page.getByRole('textbox', { name: '承运商', exact: true }).fill('顺丰'); await page.getByRole('textbox', { name: '运输单号', exact: true }).fill('SF12345');
    await capture('sales-shipping-480.png');
    await page.getByRole('button', { name: '保存运单' }).click(); await page.getByRole('heading', { name: '出库操作结果待确认' }).waitFor();
    await page.getByRole('button', { name: '核实操作结果' }).click(); await page.getByRole('button', { name: '确认执行出库' }).click();
    await page.getByText('该单已完成出库。').waitFor(); assert.equal(writes.length, 8); await noOverflow();
    await capture('sales-finished-480.png');
    await page.setViewportSize({ width: 320, height: 640 }); await noOverflow();
    await capture('sales-finished-320.png');
    readOnly = true; await page.reload(); await page.goto(base + '/mobile/quality'); await page.waitForURL('**/mobile/home**');
    assert.equal(await page.getByRole('link', { name: /种子质检/ }).count(), 0);
    assert.deepEqual(errors, []); console.log('PASS: bag/pallet/batch quality submit/timeout recovery, records, sales preview/picking/shipping/outbound, 480/320 widths and permissions');
  } finally { await browser.close(); }
})().catch(e => { console.error(e); process.exitCode = 1; });