156 lines
6.4 KiB
JavaScript
156 lines
6.4 KiB
JavaScript
import { chromium } from '@playwright/test';
|
|
import { createHash } from 'node:crypto';
|
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
import { resolve } from 'node:path';
|
|
|
|
const baseUrl = process.env.REF_RANKING_URL ?? 'https://dev-sam-ref.hided.net/sam/';
|
|
const username = process.env.REF_RANKING_USER ?? 'refuser1';
|
|
const passwordFile = process.env.REF_RANKING_PASSWORD_FILE;
|
|
const artifactRoot = resolve(process.env.REF_RANKING_ARTIFACT_DIR ?? 'test-results/reference-rankings');
|
|
|
|
if (!passwordFile) {
|
|
throw new Error('REF_RANKING_PASSWORD_FILE is required.');
|
|
}
|
|
|
|
const password = (await readFile(passwordFile, 'utf8')).trim();
|
|
await mkdir(artifactRoot, { recursive: true });
|
|
|
|
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 measureRanking = async (page) =>
|
|
page.evaluate(() => {
|
|
const pick = (selector) => {
|
|
const element = document.querySelector(selector);
|
|
if (!element) {
|
|
return null;
|
|
}
|
|
const rect = element.getBoundingClientRect();
|
|
const style = getComputedStyle(element);
|
|
return {
|
|
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
style: {
|
|
fontFamily: style.fontFamily,
|
|
fontSize: style.fontSize,
|
|
lineHeight: style.lineHeight,
|
|
backgroundImage: style.backgroundImage,
|
|
backgroundColor: style.backgroundColor,
|
|
color: style.color,
|
|
borderTopColor: style.borderTopColor,
|
|
borderTopWidth: style.borderTopWidth,
|
|
borderRadius: style.borderRadius,
|
|
padding: style.padding,
|
|
fontWeight: style.fontWeight,
|
|
cursor: style.cursor,
|
|
minHeight: style.minHeight,
|
|
objectFit: style.objectFit,
|
|
},
|
|
};
|
|
};
|
|
const image = document.querySelector('.generalIcon');
|
|
return {
|
|
title: document.title,
|
|
container: pick('#container'),
|
|
rankType: pick('.rankType'),
|
|
rankCell: pick('.rankView li'),
|
|
uniqueCell: pick('.rankView li.no_value'),
|
|
image: image
|
|
? {
|
|
...pick('.generalIcon'),
|
|
naturalWidth: image.naturalWidth,
|
|
naturalHeight: image.naturalHeight,
|
|
}
|
|
: null,
|
|
firstButton: pick('button, input[type="submit"], input[type="button"]'),
|
|
rankSectionCount: document.querySelectorAll('.rankView').length,
|
|
document: {
|
|
width: document.documentElement.scrollWidth,
|
|
height: document.documentElement.scrollHeight,
|
|
},
|
|
};
|
|
});
|
|
|
|
const browser = await chromium.launch({ headless: true });
|
|
try {
|
|
const result = {};
|
|
for (const viewport of [
|
|
{ name: 'desktop', width: 1365, height: 768 },
|
|
{ name: 'mobile', width: 390, height: 844 },
|
|
]) {
|
|
const context = await browser.newContext({
|
|
viewport: { width: viewport.width, height: viewport.height },
|
|
deviceScaleFactor: 1,
|
|
locale: 'ko-KR',
|
|
timezoneId: 'UTC',
|
|
colorScheme: 'dark',
|
|
ignoreHTTPSErrors: true,
|
|
});
|
|
const page = await context.newPage();
|
|
await login(context, page);
|
|
await page.goto(new URL('hwe/', baseUrl).toString(), { waitUntil: 'networkidle', timeout: 60_000 });
|
|
|
|
await page.goto(new URL('hwe/a_bestGeneral.php', baseUrl).toString(), {
|
|
waitUntil: 'networkidle',
|
|
timeout: 60_000,
|
|
});
|
|
await page.locator('#container').waitFor();
|
|
const bestGeneral = await measureRanking(page);
|
|
const userButton = page.getByRole('button', { name: '유저 보기' });
|
|
await userButton.hover();
|
|
bestGeneral.userButtonHover = await userButton.evaluate((element) => {
|
|
const style = getComputedStyle(element);
|
|
return { backgroundColor: style.backgroundColor, cursor: style.cursor };
|
|
});
|
|
await userButton.focus();
|
|
bestGeneral.userButtonFocus = await userButton.evaluate((element) => getComputedStyle(element).outline);
|
|
await page.screenshot({
|
|
path: resolve(artifactRoot, `ref-best-general-${viewport.name}.png`),
|
|
fullPage: true,
|
|
animations: 'disabled',
|
|
});
|
|
|
|
await page.goto(new URL('hwe/a_hallOfFame.php', baseUrl).toString(), {
|
|
waitUntil: 'networkidle',
|
|
timeout: 60_000,
|
|
});
|
|
await page.locator('#container').waitFor();
|
|
const hallOfFame = await measureRanking(page);
|
|
const scenario = page.locator('#by_scenario');
|
|
hallOfFame.scenario = await scenario.evaluate((element) => {
|
|
const rect = element.getBoundingClientRect();
|
|
const style = getComputedStyle(element);
|
|
return {
|
|
rect: { width: rect.width, height: rect.height },
|
|
fontFamily: style.fontFamily,
|
|
fontSize: style.fontSize,
|
|
};
|
|
});
|
|
await scenario.focus();
|
|
hallOfFame.scenarioFocus = await scenario.evaluate((element) => getComputedStyle(element).outline);
|
|
await page.screenshot({
|
|
path: resolve(artifactRoot, `ref-hall-of-fame-${viewport.name}.png`),
|
|
fullPage: true,
|
|
animations: 'disabled',
|
|
});
|
|
|
|
result[viewport.name] = { bestGeneral, hallOfFame };
|
|
await context.close();
|
|
}
|
|
await writeFile(resolve(artifactRoot, 'computed-dom.json'), `${JSON.stringify(result, null, 2)}\n`);
|
|
process.stdout.write(`${JSON.stringify({ ok: true, artifactRoot, viewports: Object.keys(result) })}\n`);
|
|
} finally {
|
|
await browser.close();
|
|
}
|