import assert from 'node:assert/strict' import { chromium } from 'playwright' const browser = await chromium.launch({ headless: true, channel: 'chrome' }) try { for (const { mobile, kind, title } of [ { mobile: false, kind: 'acid', title: '酸派车日历' }, { mobile: false, kind: 'alkali', title: '碱派车日历' }, { mobile: false, kind: 'other', title: '其他日历' }, { mobile: false, kind: 'site', title: '现场日历' }, { mobile: true, kind: 'acid', title: '酸派车日历' } ]) { const context = await browser.newContext({ permissions: ['clipboard-read', 'clipboard-write'], viewport: mobile ? { width: 375, height: 812 } : { width: 1440, height: 1000 } }) const page = await context.newPage() await page.clock.setFixedTime(new Date('2026-10-26T04:00:00Z')) page.setDefaultTimeout(20000) page.on('console', (msg) => { if (msg.type() === 'error') console.log('CONSOLE', msg.text()) }) page.on('requestfailed', (r) => console.log('FAILED', r.url(), r.failure())) const errors = [] page.on('pageerror', (e) => errors.push(e.message)) let rows = [] let writes = 0 let failSave = false let allowManage = true let failLoad = false await context.addInitScript(() => { localStorage.setItem( 'sys-v3.0.2-user', JSON.stringify({ isLogin: true, accessToken: 'fixture', info: { id: 'fixture', userId: 'fixture', userName: '测试', role: 'admin', roles: ['admin'] } }) ) localStorage.setItem('sys-version', '3.0.2') }) await context.route('**/api/**', async (route) => { const req = route.request(), url = new URL(req.url()), path = url.pathname.slice(4) if (!url.pathname.startsWith('/api/')) return route.continue() const user = { id: 'fixture', userId: 'fixture', userName: '测试', role: 'admin', roles: ['admin'] } if (path === '/web/auth/mp-webview-exchange') return route.fulfill({ json: { code: 200, data: { token: 'fixture', refreshToken: 'refresh', expiresIn: 3600, userInfo: user } } }) if (path === '/web/auth/current-user') return route.fulfill({ json: { code: 200, data: user } }) if (path === '/web/auth/menu-codes') return route.fulfill({ json: { code: 200, data: { all: false, menuCodes: [ 'pl:dispatch-calendar:list', 'mini:pl:dispatch-calendar:list', ...(allowManage ? ['pl:dispatch-calendar:manage'] : []) ] } } }) if (path === '/transport/dispatch-calendar/vehicles') return route.fulfill({ json: { list: [ { id: 'v1', licensePlate: '皖H12345' }, { id: 'v2', licensePlate: '皖H54321' } ], total: 2 } }) if (path === '/transport/dispatch-calendar/products') return route.fulfill({ json: { list: [{ id: 'p1', productName: '液碱' }], total: 1 } }) if (path.startsWith('/transport/dispatch-calendar')) { if (req.method() === 'POST' || req.method() === 'PUT') { writes++ if (failSave) { failSave = false return route.fulfill({ status: 500, json: { detail: 'fixture' } }) } const body = req.postDataJSON(), id = req.method() === 'PUT' ? path.split('/').pop() : String(writes) const row = { ...body, id, licensePlate: '皖H12345', productName: '液碱' } rows = rows.filter((r) => r.id !== id).concat(row) return route.fulfill({ json: row }) } if (req.method() === 'DELETE') { rows = rows.filter((r) => r.id !== path.split('/').pop()) return route.fulfill({ json: { deleted: 1 } }) } if (failLoad) { failLoad = false return route.fulfill({ status: 500, json: { detail: 'expected load failure' } }) } const kind = url.searchParams.get('calendarKind') || 'acid' const list = rows.filter( (r) => r.dispatchDate.startsWith(url.searchParams.get('month')) && (r.calendarKind || 'acid') === kind ) return route.fulfill({ json: { list, total: list.length } }) } if (path === '/transport/plan/') return route.fulfill({ json: { list: [], total: 0 } }) return route.fulfill({ json: { code: 200, data: path.includes('favorites') || path.includes('upgrade-logs') ? [] : {} } }) }) await page.goto( `http://localhost:3006/#/${mobile ? 'm/pl/dispatch-calendar?mpTicket=manual-test' : 'pl/calendar-module/dispatch-calendar'}` ) await page .getByRole('tab', { name: '酸派车日历', exact: true }) .waitFor() .catch(async (e) => { console.log('PAGE', await page.locator('body').innerText(), errors) throw e }) if (!mobile) { assert.deepEqual( await page.locator('.dispatch-calendar-tabs [role="tab"]').allTextContents(), ['酸派车日历', '碱派车日历', '其他日历', '现场日历', '实际派车信息'] ) } await page.getByRole('tab', { name: title, exact: true }).click() if (mobile) { await page.getByLabel('查看月份', { exact: true }).click() const popup = page.locator('.mp-calendar-date-picker:visible') await popup.locator('.van-picker-column').nth(1).getByText('09', { exact: true }).click() await popup.getByRole('button', { name: '确认', exact: true }).click() } else { await page.getByRole('button', { name: '上月', exact: true }).click() } await page.getByRole('button', { name: '今天', exact: true }).click() await page.locator('[data-date="2026-10-26"].selected').waitFor() if (mobile) { const size = await page.getByRole('button', { name: '今天', exact: true }).boundingBox() assert.ok(size.height >= 44) assert.equal( await page.locator('.mp-calendar-month-row').evaluate((el) => getComputedStyle(el).display), 'grid' ) } await page .getByRole('button', { name: '新增2026-10-26派车计划', exact: true }) .click({ timeout: 10000 }) const editor = page.locator('[data-testid="manual-editor"]:visible') if (mobile) { await editor.getByPlaceholder('选择车辆').click() await page .getByRole('button', { name: '皖H12345', exact: true }) .click({ timeout: 8000 }) .catch(async (e) => { console.log('PICKER', await page.locator('.mp-manual-sheet:visible').allTextContents()) await page.screenshot({ path: '/tmp/manual-calendar-picker-debug.png' }) throw e }) await editor.getByPlaceholder('选择产品').click() await page.getByRole('button', { name: '液碱', exact: true }).click() } else { await editor.getByRole('combobox', { name: '车牌号', exact: true }).click() await page.getByRole('option', { name: '皖H12345', exact: true }).click() await editor.getByRole('combobox', { name: '产品名称', exact: true }).click() await page.getByRole('option', { name: '液碱', exact: true }).click() } // 起止地留空也应保存成功,编辑后仍可补填。 await editor.getByRole('button', { name: '保存', exact: true }).click() await editor.waitFor({ state: 'hidden' }) assert.equal(rows.length, 1) assert.equal(rows[0].origin, '') assert.equal(rows[0].destination, '') assert.equal(rows[0].calendarKind, kind) await page.locator('[data-testid="manual-record"]').first().click() await page.getByRole('button', { name: '编辑', exact: true }).click() await editor.getByPlaceholder('请输入始发地').fill('山峡') await editor.getByPlaceholder('请输入目的地').fill('云栋') failSave = true await editor.getByRole('button', { name: '保存', exact: true }).click() await editor.getByText('保存失败,请检查填写内容或权限后重试').waitFor() assert.equal(await editor.getByPlaceholder('请输入始发地').inputValue(), '山峡') await editor.getByRole('button', { name: '保存', exact: true }).click() await editor.waitFor({ state: 'hidden' }) assert.equal(rows.length, 1) assert.equal(rows[0].calendarKind, kind) const card = page.locator('[data-testid="manual-record"]').first() await card.waitFor() await page.screenshot({ path: `/tmp/manual-calendar-${mobile ? 'mobile' : 'web'}-${kind}.png`, fullPage: false }) assert.match(await card.innerText(), /山峡 → 云栋/) await page .locator('[data-date="2026-10-26"]') .getByRole('button', { name: mobile ? '复制' : '复制2026-10-26派车计划', exact: true }) .click() assert.match( await page.evaluate(() => navigator.clipboard.readText()), /皖H12345.*液碱.*山峡 → 云栋/s ) await card.click() await page.getByRole('button', { name: '编辑', exact: true }).click() await editor.getByPlaceholder('请输入目的地').fill('安庆') await editor.getByRole('button', { name: '保存', exact: true }).click() await editor.waitFor({ state: 'hidden' }) await page.getByText('山峡 → 安庆', { exact: true }).first().waitFor() await page.locator('[data-testid="manual-record"]').first().click() await page.getByRole('button', { name: '删除', exact: true }).click() await page.getByRole('button', { name: mobile ? '确认删除' : '确定', exact: true }).click() await page.locator('[data-testid="manual-record"]').waitFor({ state: 'hidden' }) assert.equal(rows.length, 0) failLoad = true await page.getByRole('tab', { name: '实际派车信息', exact: true }).click() await page.getByRole('tab', { name: title, exact: true }).click() await page.getByText('派车日历加载失败,请重试', { exact: false }).first().waitFor() await page.getByRole('button', { name: '重试', exact: true }).click() await page .getByText('派车日历加载失败,请重试', { exact: false }) .first() .waitFor({ state: 'hidden' }) if (mobile) for (const width of [320, 375, 414]) { await page.setViewportSize({ width, height: 812 }) assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1)) } await page.getByRole('tab', { name: '实际派车信息', exact: true }).click() await page.getByText('本月暂无派车计划', { exact: true }).first().waitFor() allowManage = false rows = [ { id: 'readonly', dispatchDate: '2026-10-26', vehicleId: 'v1', licensePlate: '皖H12345', productId: 'p1', productName: '液碱', origin: '山峡', destination: '云栋', remark: '', calendarKind: kind } ] await page.getByRole('tab', { name: title, exact: true }).click() await page.locator('[data-testid="manual-record"]').first().waitFor() assert.equal(await page.getByRole('button', { name: '新增派车计划', exact: true }).count(), 0) await page.locator('[data-testid="manual-record"]').first().click() assert.equal(await page.getByRole('button', { name: '编辑', exact: true }).count(), 0) assert.equal(await page.getByRole('button', { name: '删除', exact: true }).count(), 0) assert.deepEqual(errors, []) await context.close() console.log(`${mobile ? 'mobile' : 'web'} ${kind} CRUD passed`) } } finally { await browser.close() }