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-parent-execute', `run-${RUN_ID}`); const ORDER = { productId: '412', productName: '老挝行程--广东 8D7N', dateFrom: '2026-07-01', dateTo: '2026-07-31', explicitDates: ['2026-7-4', '2026-7-11', '2026-7-18', '2026-7-25'], plannedGuests: 10, op: '测试', }; fs.mkdirSync(RUN_DIR, { recursive: true }); function log(message, data) { console.log(`[${new Date().toISOString()}] ${message}${data === undefined ? '' : ` ${JSON.stringify(data)}`}`); } async function setField(frame, selector, value) { const locator = frame.locator(selector).first(); 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 getPlanFrame(page) { for (let i = 0; i < 60; i += 1) { const frame = page.frames().find((f) => f.url().includes('/Business/plan_add.asp')); if (frame) return frame; await page.waitForTimeout(500); } throw new Error('plan_add 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 extractGroupNumbers(text) { return Array.from(new Set(text.match(/[A-Z]{1,4}-\d{6,8}[A-Z]?(?:-[A-Z])?/g) || [])); } async function selectExplicitDates(frame, dates) { await frame.evaluate((targetDates) => { const seen = new Set(); for (const el of Array.from(document.querySelectorAll('input[name="zhidingzhouqi"]'))) { el.checked = false; if (targetDates.includes(el.value) && !seen.has(el.value)) { el.checked = true; seen.add(el.value); } el.dispatchEvent(new Event('change', { bubbles: true })); } }, dates); const selected = await frame.evaluate(() => Array.from(document.querySelectorAll('input[name="zhidingzhouqi"]')) .filter((el) => el.checked) .map((el) => el.value) ); const missing = dates.filter((d) => !selected.includes(d)); if (missing.length) throw new Error(`explicit date checkboxes missing after selection: ${missing.join(', ')}`); if (selected.length !== dates.length) throw new Error(`selected date count mismatch: ${selected.join(', ')}`); log('selected-explicit-dates', selected); } 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 page.goto('https://ltjt.yunzhi.run/System/Business/plan.asp', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(1200); await page.locator('input[value="新增计划"], a:has-text("新增计划"), button:has-text("新增计划")').first().click(); const frame = await getPlanFrame(page); await frame.waitForSelector('#Riqi1', { timeout: 30000 }); await setField(frame, '#Riqi1', ORDER.dateFrom); await setField(frame, '#Riqi2', ORDER.dateTo); await setField(frame, '#jihuashu', ORDER.plannedGuests); await setField(frame, '#jiedairen', ORDER.op); await page.waitForTimeout(1200); await selectExplicitDates(frame, ORDER.explicitDates); await frame.locator(`input[name="cpid"][value="${ORDER.productId}"]`).check(); await page.waitForTimeout(1200); await page.screenshot({ path: path.join(RUN_DIR, '01-before-save.png'), fullPage: false }); const before = await readAllText(page); fs.writeFileSync(path.join(RUN_DIR, 'before-save.txt'), before, 'utf8'); if (!before.includes(ORDER.productName.replace(' 8D7N', ''))) { throw new Error(`target product not visible before save: ${ORDER.productName}`); } await frame.locator('#SubmitButton, [name="SubmitButton"]').first().click(); await page.waitForTimeout(4000); await page.screenshot({ path: path.join(RUN_DIR, '02-after-save.png'), fullPage: false }); const after = await readAllText(page); fs.writeFileSync(path.join(RUN_DIR, 'after-save.txt'), after, 'utf8'); const groupNumbers = extractGroupNumbers(after).filter((n) => n.startsWith('LW-2607')); if (groupNumbers.length < ORDER.explicitDates.length) { throw new Error(`could not capture enough parent group numbers: ${groupNumbers.join(', ')}`); } await clickDialogOk(page); const result = { runDir: RUN_DIR, dates: ORDER.explicitDates, parentGroupNumbers: groupNumbers, text: after.slice(0, 1200) }; 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); });