242 lines
10 KiB
JavaScript
242 lines
10 KiB
JavaScript
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);
|
|
});
|