const fs = require('fs'); const path = require('path'); const { chromium } = require('playwright'); const ROOT = path.resolve(__dirname, '..'); const PROFILE = path.join(ROOT, 'diagnostics', 'erp-playwright-profile'); const RUN_ID = new Date().toISOString().replace(/[-:T.Z]/g, '').slice(0, 14); const RUN_DIR = path.join(ROOT, 'diagnostics', 'split-child-execute', `run-${RUN_ID}`); const ORDER = { parentGroup: 'LW-260704A-A', date: '2026-07-04', customerName: 'LW云南七彩金桥国旅(昆明景腾)', customerId: '778', pax: { adult: 5, childBed: 0, childNoBed: 0, infant: 0, leader: 0 }, prices: { adult: 500, childBed: 0, childNoBed: 0, infant: 0, leader: 0 }, op: '测试', sales: '琳琳', origin: '云南', note: '测试散拼子单,自动化测试后可删除。', }; fs.mkdirSync(RUN_DIR, { recursive: true }); function log(message, data) { console.log(`[${new Date().toISOString()}] ${message}${data === undefined ? '' : ` ${JSON.stringify(data)}`}`); } async function setField(scope, selector, value) { const locator = scope.locator(selector).first(); await locator.waitFor({ state: 'attached', timeout: 15000 }); await locator.fill(String(value)); await locator.evaluate((el) => { el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); el.dispatchEvent(new Event('blur', { bubbles: true })); }); } async function setDomValue(frame, id, value) { await frame.evaluate(([fieldId, fieldValue]) => { const el = document.getElementById(fieldId) || document.querySelector(`[name="${fieldId}"]`); if (!el) throw new Error(`field not found: ${fieldId}`); el.value = String(fieldValue); el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); el.dispatchEvent(new Event('blur', { bubbles: true })); }, [id, value]); } async function queryParent(page) { await page.goto('https://ltjt.yunzhi.run/System/Business/plan.asp', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(900); await setField(page, '#S_chufariqi', ORDER.date); await setField(page, '#S_chufarizhi', ORDER.date); await setField(page, '#S_tuanxuhao', ORDER.parentGroup); await page.locator('#SearchButton').click(); await page.waitForTimeout(2500); } async function getChildFrame(page) { for (let i = 0; i < 60; i += 1) { const frame = page.frames().find((f) => f.url().includes('/Business/plan_order.asp')); if (frame) return frame; await page.waitForTimeout(500); } throw new Error('plan_order frame not found'); } async function readAllText(page) { const texts = []; for (const frame of page.frames()) { const text = await frame.evaluate(() => document.body.innerText.replace(/\s+/g, ' ').trim()).catch(() => ''); if (text) texts.push(text); } return texts.join('\n'); } function extractChildOrderNumber(text) { const matches = text.match(/D\d{4,}/g) || []; return matches.length ? matches[matches.length - 1] : null; } async function clickDialogOk(page) { const selectors = ['button:has-text("确定")', 'input[value="确定"]', 'a:has-text("确定")', '.aui_state_highlight']; for (const selector of selectors) { const locator = page.locator(selector).first(); if (await locator.count().catch(() => 0)) { await locator.click().catch(() => {}); await page.waitForTimeout(500); return true; } } return false; } async function main() { log('run-start', { RUN_DIR, ORDER }); const context = await chromium.launchPersistentContext(PROFILE, { headless: false, viewport: { width: 1920, height: 1000 }, acceptDownloads: true, }); const page = context.pages()[0] || await context.newPage(); page.setDefaultTimeout(15000); try { await queryParent(page); await page.screenshot({ path: path.join(RUN_DIR, '01-parent-before.png'), fullPage: false }); const beforeListText = await readAllText(page); fs.writeFileSync(path.join(RUN_DIR, '01-parent-before.txt'), beforeListText, 'utf8'); await page.locator('a:has-text("拼单")').first().click(); const frame = await getChildFrame(page); await frame.waitForSelector('#zutuanshe', { timeout: 30000 }); await setDomValue(frame, 'zutuanshe', ORDER.customerName); await setDomValue(frame, 'zutuansheid', ORDER.customerId); await setDomValue(frame, 'gendanren', ORDER.op); await setDomValue(frame, 'xiaoshouren', ORDER.sales); await setDomValue(frame, 'keyuandi', ORDER.origin); await setDomValue(frame, 'xiadanbeizhu', ORDER.note); await setDomValue(frame, 'darenshu', ORDER.pax.adult); await setDomValue(frame, 'xiaorenshu', ORDER.pax.childBed); await setDomValue(frame, 'ertrenshu', ORDER.pax.childNoBed); await setDomValue(frame, 'yingrenshu', ORDER.pax.infant); await setDomValue(frame, 'quanrenshu', ORDER.pax.leader); const rows = [ { row: 0, qty: ORDER.pax.adult, price: ORDER.prices.adult }, { row: 1, qty: ORDER.pax.childBed, price: ORDER.prices.childBed }, { row: 2, qty: ORDER.pax.childNoBed, price: ORDER.prices.childNoBed }, { row: 3, qty: ORDER.pax.infant, price: ORDER.prices.infant }, ]; for (const item of rows) { await setDomValue(frame, `ys_danwei${item.row}`, ORDER.customerName).catch(() => {}); await setDomValue(frame, `ys_danweiid${item.row}`, ORDER.customerId).catch(() => {}); await setDomValue(frame, `ys_shuliang${item.row}`, item.qty).catch(() => {}); await setDomValue(frame, `ys_danjia${item.row}`, item.price).catch(() => {}); await setDomValue(frame, `ys_jine${item.row}`, item.qty * item.price).catch(() => {}); } await page.waitForTimeout(1000); await page.screenshot({ path: path.join(RUN_DIR, '02-before-save.png'), fullPage: false }); const beforeSave = await readAllText(page); fs.writeFileSync(path.join(RUN_DIR, '02-before-save.txt'), beforeSave, 'utf8'); await frame.locator('#SubmitButton, [name="SubmitButton"]').first().click(); await page.waitForTimeout(4000); await page.screenshot({ path: path.join(RUN_DIR, '03-after-save.png'), fullPage: false }); const afterSave = await readAllText(page); fs.writeFileSync(path.join(RUN_DIR, '03-after-save.txt'), afterSave, 'utf8'); const childOrder = extractChildOrderNumber(afterSave); if (!childOrder) throw new Error(`could not capture child order number: ${afterSave.slice(0, 800)}`); await clickDialogOk(page); await queryParent(page); await page.screenshot({ path: path.join(RUN_DIR, '04-parent-after.png'), fullPage: false }); const afterListText = await readAllText(page); fs.writeFileSync(path.join(RUN_DIR, '04-parent-after.txt'), afterListText, 'utf8'); const receivedOk = afterListText.includes(ORDER.parentGroup) && afterListText.includes('成人5'); const result = { runDir: RUN_DIR, parentGroup: ORDER.parentGroup, date: ORDER.date, childOrder, receivedOk }; fs.writeFileSync(path.join(RUN_DIR, 'result.json'), JSON.stringify(result, null, 2), 'utf8'); log('run-complete', result); } catch (error) { log('error', { message: error.message, stack: error.stack }); await page.screenshot({ path: path.join(RUN_DIR, 'error.png'), fullPage: false }).catch(() => {}); throw error; } finally { await context.close(); } } main().catch((error) => { console.error(error); process.exit(1); });