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', 'single-team-execute', `run-${RUN_ID}`); const LOG_FILE = path.join(RUN_DIR, 'run.log'); const ORDER = { product: '遇见老挝', departureDate: '2026-07-12', pax: { adult: 2, childBed: 1, childNoBed: 1, infant: 0, leader: 0 }, rooms: { SGL: 0, TWN: 0, TRP: 0, DBL: 2, HNM: 0, TL: 0 }, prices: { adult: 500, childBed: 200, childNoBed: 100, infant: 0, leader: 0 }, op: '测试', sales: '琳琳', note: '测试团队单个下单,自动化测试后可删除。', }; fs.mkdirSync(RUN_DIR, { recursive: true }); function log(message, data) { const line = `[${new Date().toISOString()}] ${message}${data === undefined ? '' : ` ${JSON.stringify(data)}`}`; fs.appendFileSync(LOG_FILE, `${line}\n`, 'utf8'); console.log(line); } async function shot(page, name) { const file = path.join(RUN_DIR, `${name}.png`); await page.screenshot({ path: file, fullPage: false }); log('screenshot', { file }); } async function getOrderFrame(page) { for (let i = 0; i < 60; i += 1) { const frame = page.frames().find((f) => f.url().includes('/Business/orders_add.asp')); if (frame) return frame; await page.waitForTimeout(500); } throw new Error('orders_add.asp frame not found'); } async function dumpFrame(frame, name) { const data = await frame.evaluate(() => { const valueOf = (id) => { const el = document.getElementById(id) || document.querySelector(`[name="${id}"]`); return el ? el.value : null; }; const values = {}; for (const id of [ 'chufari', 'zutuanshe', 'zutuansheid', 'zhuanxianming', 'chanpinming', 'TianShu', 'tuanxuhao1', 'tuanxuhao2', 'gendanren', 'xiaoshouren', 'darenshu', 'xiaorenshu', 'ertrenshu', 'yingrenshu', 'quanrenshu', 'frenshu0', 'frenshu1', 'frenshu2', 'frenshu3', 'frenshu4', 'frenshu5', 'ys_danwei0', 'ys_xiangmu0', 'ys_bizhong0', 'ys_shuliang0', 'ys_danjia0', 'ys_jine0', 'ys_danwei1', 'ys_xiangmu1', 'ys_bizhong1', 'ys_shuliang1', 'ys_danjia1', 'ys_jine1', 'ys_danwei2', 'ys_xiangmu2', 'ys_bizhong2', 'ys_shuliang2', 'ys_danjia2', 'ys_jine2', 'ys_danwei3', 'ys_xiangmu3', 'ys_bizhong3', 'ys_shuliang3', 'ys_danjia3', 'ys_jine3', ]) values[id] = valueOf(id); return { url: location.href, title: document.title, values, bodyText: document.body.innerText.replace(/\s+/g, ' ').slice(0, 2500), }; }); const file = path.join(RUN_DIR, `${name}.json`); fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf8'); log('dump', { file, values: data.values, body: data.bodyText.slice(0, 600) }); return data; } async function setField(frame, selector, value) { const locator = frame.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 clickAndWait(page, locator) { await locator.click(); await page.waitForTimeout(1000); } function extractGroupNumber(text) { const match = text.match(/[A-Z]{1,4}-\d{6,8}[A-Z]?(?:-[A-Z])?/); return match ? match[0] : null; } async function readSuccessDialog(page) { for (let i = 0; i < 60; i += 1) { const result = await page.evaluate(() => { const text = document.body.innerText.replace(/\s+/g, ' ').trim(); const okButtons = Array.from(document.querySelectorAll('button, input, a')).map((el) => ({ text: (el.innerText || el.value || '').replace(/\s+/g, ' ').trim(), id: el.id || '', cls: el.className || '', })); return { text, okButtons }; }).catch(() => ({ text: '', okButtons: [] })); if (result.text.includes('下单成功') || result.text.includes('操作成功')) return result.text; await page.waitForTimeout(500); } return ''; } async function clickDialogOk(page) { const candidates = [ 'button:has-text("确定")', 'input[value="确定"]', 'a:has-text("确定")', '.aui_state_highlight', ]; for (const selector of candidates) { 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, downloadsPath: path.join(RUN_DIR, 'downloads'), }); const page = context.pages()[0] || await context.newPage(); page.setDefaultTimeout(15000); const browserDialogs = []; page.on('dialog', async (dialog) => { const item = { type: dialog.type(), message: dialog.message() }; browserDialogs.push(item); log('browser-dialog', item); await dialog.accept().catch(() => {}); }); try { await page.goto('https://ltjt.yunzhi.run/System/Business/orders.asp', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(1000); await shot(page, '01-orders'); await page.locator('input[onclick="OPEN_update(0,0,0)"]').click(); const frame = await getOrderFrame(page); await frame.waitForSelector('#chanpinming, [name="chanpinming"]', { timeout: 30000 }); await page.waitForTimeout(1000); await setField(frame, '#chanpinming, [name="chanpinming"]', ORDER.product); await clickAndWait(page, frame.locator('#gengxinxingcheng, a[onclick*="Find_product"]').first()); await page.waitForTimeout(2200); await shot(page, '02-after-product-update'); const afterProduct = await dumpFrame(frame, '02-after-product-update'); const requiredAfterProduct = { zutuanshe: afterProduct.values.zutuanshe, zhuanxianming: afterProduct.values.zhuanxianming, chanpinming: afterProduct.values.chanpinming, TianShu: afterProduct.values.TianShu, tuanxuhao1: afterProduct.values.tuanxuhao1, tuanxuhao2: afterProduct.values.tuanxuhao2, gendanren: afterProduct.values.gendanren, xiaoshouren: afterProduct.values.xiaoshouren, }; const missing = Object.entries(requiredAfterProduct).filter(([, value]) => !String(value || '').trim()).map(([key]) => key); if (missing.length) { throw new Error(`required fields still empty after product update: ${missing.join(', ')}`); } await setField(frame, '#chufari, [name="chufari"]', ORDER.departureDate); await setField(frame, '#gendanren, [name="gendanren"]', ORDER.op); await setField(frame, '#xiaoshouren, [name="xiaoshouren"]', ORDER.sales); await setField(frame, '#darenshu, [name="darenshu"]', ORDER.pax.adult); await setField(frame, '#xiaorenshu, [name="xiaorenshu"]', ORDER.pax.childBed); await setField(frame, '#ertrenshu, [name="ertrenshu"]', ORDER.pax.childNoBed); await setField(frame, '#yingrenshu, [name="yingrenshu"]', ORDER.pax.infant); await setField(frame, '#quanrenshu, [name="quanrenshu"]', ORDER.pax.leader); await setField(frame, '#frenshu0, [name="frenshu0"]', ORDER.rooms.SGL); await setField(frame, '#frenshu1, [name="frenshu1"]', ORDER.rooms.TWN); await setField(frame, '#frenshu2, [name="frenshu2"]', ORDER.rooms.TRP); await setField(frame, '#frenshu3, [name="frenshu3"]', ORDER.rooms.DBL); await setField(frame, '#frenshu4, [name="frenshu4"]', ORDER.rooms.HNM); await setField(frame, '#frenshu5, [name="frenshu5"]', ORDER.rooms.TL); await setField(frame, '#xiadanbeizhu, [name="xiadanbeizhu"]', ORDER.note).catch(() => {}); const receivables = [ { 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 receivables) { await setField(frame, `#ys_shuliang${item.row}, [name="ys_shuliang${item.row}"]`, item.qty).catch(() => {}); await setField(frame, `#ys_danjia${item.row}, [name="ys_danjia${item.row}"]`, item.price).catch(() => {}); await setField(frame, `#ys_jine${item.row}, [name="ys_jine${item.row}"]`, item.qty * item.price).catch(() => {}); } await page.waitForTimeout(1000); await shot(page, '03-before-save'); const beforeSave = await dumpFrame(frame, '03-before-save'); const requiredBeforeSave = ['chufari', 'zutuanshe', 'zhuanxianming', 'chanpinming', 'TianShu', 'tuanxuhao1', 'tuanxuhao2', 'gendanren', 'xiaoshouren']; const stillMissing = requiredBeforeSave.filter((key) => !String(beforeSave.values[key] || '').trim()); if (stillMissing.length) throw new Error(`required fields missing before save: ${stillMissing.join(', ')}`); log('click-save'); await frame.locator('#SubmitButton, [name="SubmitButton"]').first().click(); await page.waitForTimeout(1500); const successText = await readSuccessDialog(page); await shot(page, '04-after-save'); log('success-dialog-text', successText); const groupNumber = extractGroupNumber(successText); if (!groupNumber) { throw new Error(`could not extract group number from save dialog: ${successText.slice(0, 500)}`); } await clickDialogOk(page); const result = { groupNumber, successText, browserDialogs, runDir: RUN_DIR }; 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 shot(page, 'error').catch(() => {}); throw error; } finally { await context.close(); } } main().catch((error) => { console.error(error); process.exit(1); });