Files

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();
}