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; });
|