179 lines
7.4 KiB
JavaScript
179 lines
7.4 KiB
JavaScript
import { createHash } from 'node:crypto';
|
|
import { mkdir, readFile } from 'node:fs/promises';
|
|
import { dirname, resolve } from 'node:path';
|
|
|
|
import { chromium } from '@playwright/test';
|
|
|
|
const baseUrl = process.env.REF_MESSAGE_URL ?? 'http://127.0.0.1:3400/sam/';
|
|
const username = process.env.REF_MESSAGE_USER ?? 'refuser1';
|
|
const passwordFile = process.env.REF_MESSAGE_PASSWORD_FILE;
|
|
const artifactRoot = process.env.REF_MESSAGE_ARTIFACT_DIR;
|
|
|
|
if (!passwordFile) {
|
|
throw new Error('REF_MESSAGE_PASSWORD_FILE is required.');
|
|
}
|
|
|
|
const password = (await readFile(passwordFile, 'utf8')).trim();
|
|
|
|
const login = async (context, page) => {
|
|
await page.goto(baseUrl, { waitUntil: 'networkidle', timeout: 60_000 });
|
|
const globalSalt = await page.locator('#global_salt').inputValue();
|
|
const passwordHash = createHash('sha512')
|
|
.update(globalSalt + password + globalSalt)
|
|
.digest('hex');
|
|
const response = await context.request.post(new URL('api.php?path=Login/LoginByID', baseUrl).toString(), {
|
|
data: { username, password: passwordHash },
|
|
});
|
|
const result = await response.json();
|
|
if (!response.ok() || result.result !== true) {
|
|
throw new Error('Reference login failed.');
|
|
}
|
|
};
|
|
|
|
const ensureGeneral = async (page) => {
|
|
await page.goto(new URL('hwe/index.php', baseUrl).toString(), {
|
|
waitUntil: 'networkidle',
|
|
timeout: 60_000,
|
|
});
|
|
if (await page.locator('.MessagePanel').isVisible()) {
|
|
return;
|
|
}
|
|
|
|
await page.goto(new URL('hwe/v_join.php', baseUrl).toString(), {
|
|
waitUntil: 'networkidle',
|
|
timeout: 60_000,
|
|
});
|
|
const create = page.getByRole('button', { name: '장수 생성', exact: true });
|
|
await create.waitFor({ state: 'visible', timeout: 30_000 });
|
|
page.once('dialog', (dialog) => dialog.accept());
|
|
await create.click();
|
|
await page.locator('.MessagePanel').waitFor({ state: 'visible', timeout: 60_000 });
|
|
};
|
|
|
|
const measure = async (browser, name, viewport) => {
|
|
const context = await browser.newContext({
|
|
viewport,
|
|
deviceScaleFactor: 1,
|
|
colorScheme: 'dark',
|
|
locale: 'ko-KR',
|
|
timezoneId: 'UTC',
|
|
ignoreHTTPSErrors: true,
|
|
});
|
|
try {
|
|
const page = await context.newPage();
|
|
await login(context, page);
|
|
await ensureGeneral(page);
|
|
await page.locator('.MessagePanel').waitFor({ state: 'visible', timeout: 30_000 });
|
|
await page.locator('.BoardHeader').first().waitFor({ state: 'visible' });
|
|
const marker = `computed-dom-${name}-${Date.now()}`;
|
|
await page.locator('.MessageInputForm select').selectOption('9999');
|
|
await page.locator('.MessageInputForm input').fill(marker);
|
|
await page.getByRole('button', { name: '서신전달&갱신' }).click();
|
|
await page.getByText(marker, { exact: true }).waitFor({ state: 'visible', timeout: 30_000 });
|
|
|
|
if (artifactRoot) {
|
|
const path = resolve(artifactRoot, `message-ref-${name}.png`);
|
|
await mkdir(dirname(path), { recursive: true });
|
|
await page.locator('.MessagePanel').screenshot({
|
|
path,
|
|
animations: 'disabled',
|
|
});
|
|
}
|
|
|
|
const result = await page.evaluate(() => {
|
|
const rect = (element) => {
|
|
const box = element.getBoundingClientRect();
|
|
return {
|
|
x: box.x,
|
|
y: box.y,
|
|
width: box.width,
|
|
height: box.height,
|
|
};
|
|
};
|
|
const required = (selector) => {
|
|
const element = document.querySelector(selector);
|
|
if (!element) throw new Error(`Missing reference selector: ${selector}`);
|
|
return element;
|
|
};
|
|
const optionalRect = (selector) => {
|
|
const element = document.querySelector(selector);
|
|
return element ? rect(element) : null;
|
|
};
|
|
const style = (selector) => getComputedStyle(required(selector));
|
|
const input = required('.MessageInputForm input');
|
|
const select = required('.MessageInputForm select');
|
|
const submit = required('#msg_submit-col button');
|
|
const firstPlate = document.querySelector('.msg_plate');
|
|
const firstIcon = document.querySelector('.msg_plate .generalIcon');
|
|
const panelStyle = style('.MessagePanel');
|
|
const headerStyle = style('.BoardHeader');
|
|
const plateStyle = firstPlate ? getComputedStyle(firstPlate) : null;
|
|
const iconStyle = firstIcon ? getComputedStyle(firstIcon) : null;
|
|
return {
|
|
panel: rect(required('.MessagePanel')),
|
|
inputForm: rect(required('.MessageInputForm')),
|
|
select: rect(select),
|
|
input: rect(input),
|
|
submit: rect(submit),
|
|
publicSection: rect(required('.PublicTalk')),
|
|
nationalSection: rect(required('.NationalTalk')),
|
|
privateSection: rect(required('.PrivateTalk')),
|
|
diplomacySection: rect(required('.DiplomacyTalk')),
|
|
firstHeader: rect(required('.BoardHeader')),
|
|
firstPlate: optionalRect('.msg_plate'),
|
|
firstIcon: optionalRect('.msg_plate .generalIcon'),
|
|
computed: {
|
|
panelDisplay: panelStyle.display,
|
|
panelColumns: panelStyle.gridTemplateColumns,
|
|
panelFontSize: panelStyle.fontSize,
|
|
headerColor: headerStyle.color,
|
|
headerOutlineWidth: headerStyle.outlineWidth,
|
|
headerBackgroundImage: headerStyle.backgroundImage,
|
|
plateBackgroundColor: plateStyle?.backgroundColor ?? null,
|
|
plateFontSize: plateStyle?.fontSize ?? null,
|
|
plateMinHeight: plateStyle?.minHeight ?? null,
|
|
iconObjectFit: iconStyle?.objectFit ?? null,
|
|
},
|
|
};
|
|
});
|
|
|
|
const submit = page.locator('#msg_submit-col button');
|
|
await submit.hover();
|
|
const hover = await submit.evaluate((element) => {
|
|
const style = getComputedStyle(element);
|
|
return {
|
|
cursor: style.cursor,
|
|
backgroundColor: style.backgroundColor,
|
|
};
|
|
});
|
|
await submit.focus();
|
|
const focus = await submit.evaluate((element) => {
|
|
const style = getComputedStyle(element);
|
|
return {
|
|
outline: style.outline,
|
|
boxShadow: style.boxShadow,
|
|
};
|
|
});
|
|
const markerPlate = page.locator('.msg_plate').filter({ hasText: marker });
|
|
const deleteButton = markerPlate.locator('.btn-delete-msg');
|
|
if (await deleteButton.isVisible()) {
|
|
page.once('dialog', (dialog) => dialog.accept());
|
|
await deleteButton.click();
|
|
}
|
|
return { ...result, interaction: { hover, focus } };
|
|
} finally {
|
|
await context.close();
|
|
}
|
|
};
|
|
|
|
const browser = await chromium.launch({ headless: true });
|
|
try {
|
|
const measurements = {
|
|
desktop: await measure(browser, 'desktop', { width: 1000, height: 900 }),
|
|
mobile: await measure(browser, 'mobile', { width: 500, height: 900 }),
|
|
};
|
|
process.stdout.write(`${JSON.stringify(measurements, null, 2)}\n`);
|
|
} finally {
|
|
await browser.close();
|
|
}
|