merge: 최신 main을 개인 기록 시각 수정에 재반영

This commit is contained in:
2026-08-20 16:11:54 +00:00
3 changed files with 164 additions and 46 deletions
+2 -2
View File
@@ -1271,7 +1271,7 @@ test('내 정보&설정의 지난 플레이는 기본 탐색과 분리되어 오
} }
}); });
test('내 정보&설정에서 모바일 메인 패널을 드래그하거나 버튼으로 재정렬하고 Ref 순서로 복원한다', async ({ page }) => { test('내 정보&설정에서 모바일 메인 패널을 드래그하거나 버튼으로 재정렬하고 기본 순서로 복원한다', async ({ page }) => {
const state: FixtureState = { permission: 'head', myset: 3, settingMutations: [], accessPages: [] }; const state: FixtureState = { permission: 'head', myset: 3, settingMutations: [], accessPages: [] };
await install(page, state); await install(page, state);
await page.setViewportSize({ width: 390, height: 844 }); await page.setViewportSize({ width: 390, height: 844 });
@@ -1334,7 +1334,7 @@ test('내 정보&설정에서 모바일 메인 패널을 드래그하거나 버
.toEqual(['messages', 'commands', 'nation-menu', 'nation', 'general', 'map', 'city', 'records', 'global-menu']); .toEqual(['messages', 'commands', 'nation-menu', 'nation', 'general', 'map', 'city', 'records', 'global-menu']);
await page.getByRole('button', { name: '순서 바꾸기', exact: true }).click(); await page.getByRole('button', { name: '순서 바꾸기', exact: true }).click();
await dialog.getByRole('button', { name: 'Ref 초깃값' }).click(); await dialog.getByRole('button', { name: '기본값', exact: true }).click();
await expect.poll(readOrder).toEqual(defaultOrder); await expect.poll(readOrder).toEqual(defaultOrder);
await dialog.getByRole('button', { name: '적용', exact: true }).click(); await dialog.getByRole('button', { name: '적용', exact: true }).click();
await expect await expect
+161 -43
View File
@@ -792,6 +792,103 @@ const gridColumnCount = async (page: Page, selector: string) =>
.first() .first()
.evaluate((element) => getComputedStyle(element).gridTemplateColumns.split(' ').length); .evaluate((element) => getComputedStyle(element).gridTemplateColumns.split(' ').length);
const setMobilePanelOrder = async (page: Page, order: readonly string[]) => {
await page.evaluate((nextOrder) => {
localStorage.setItem('sam.mobileMainPanelOrder.v1', JSON.stringify(nextOrder));
document.dispatchEvent(new CustomEvent('sam-mobile-main-panel-order-changed'));
}, order);
};
const inspectMobilePanelLayout = async (page: Page) =>
page.locator('.layout-mobile').evaluate((container) => {
const containerStyle = getComputedStyle(container);
const panels = [...container.querySelectorAll<HTMLElement>(':scope > [data-mobile-panel-id]')].map(
(element, domIndex) => {
const rect = element.getBoundingClientRect();
const style = getComputedStyle(element);
const content = element.firstElementChild as HTMLElement | null;
const contentStyle = content ? getComputedStyle(content) : null;
return {
id: element.dataset.mobilePanelId ?? '',
domIndex,
top: rect.top,
bottom: rect.bottom,
left: rect.left,
right: rect.right,
width: rect.width,
height: rect.height,
display: style.display,
position: style.position,
inset: [style.top, style.right, style.bottom, style.left],
order: style.order,
transform: style.transform,
float: style.cssFloat,
gridRow: `${style.gridRowStart} / ${style.gridRowEnd}`,
gridColumn: `${style.gridColumnStart} / ${style.gridColumnEnd}`,
marginTop: style.marginTop,
marginBottom: style.marginBottom,
content: contentStyle
? {
position: contentStyle.position,
order: contentStyle.order,
transform: contentStyle.transform,
marginTop: contentStyle.marginTop,
marginBottom: contentStyle.marginBottom,
height: contentStyle.height,
}
: null,
};
}
);
return {
container: {
display: containerStyle.display,
flexDirection: containerStyle.flexDirection,
position: containerStyle.position,
transform: containerStyle.transform,
},
panels,
visualOrder: [...panels]
.sort((left, right) => left.top - right.top || left.left - right.left)
.map(({ id }) => id),
};
});
const expectMobilePanelVisualOrder = async (page: Page, expectedOrder: readonly string[]) => {
await expect
.poll(() =>
page
.locator('.layout-mobile > [data-mobile-panel-id]')
.evaluateAll((elements) => elements.map((element) => element.getAttribute('data-mobile-panel-id')))
)
.toEqual(expectedOrder);
const audit = await inspectMobilePanelLayout(page);
expect(audit.container).toEqual({
display: 'flex',
flexDirection: 'column',
position: 'static',
transform: 'none',
});
expect(audit.panels.map(({ id }) => id)).toEqual(expectedOrder);
expect(audit.visualOrder).toEqual(expectedOrder);
expect(audit.panels.every(({ left, right, width }) => left >= 0 && right <= 500 && width === 500)).toBe(true);
expect(
audit.panels.every((panel, index) => index === 0 || panel.top >= audit.panels[index - 1]!.bottom)
).toBe(true);
for (const panel of audit.panels) {
expect(panel.display, `${panel.id}: display`).not.toBe('none');
expect(['static', 'relative'], `${panel.id}: position`).toContain(panel.position);
expect(
panel.inset.every((value) => value === 'auto' || value === '0px'),
`${panel.id}: inset ${panel.inset.join(' ')}`
).toBe(true);
expect(panel.order, `${panel.id}: order`).toBe('0');
expect(panel.transform, `${panel.id}: transform`).toBe('none');
expect(panel.float, `${panel.id}: float`).toBe('none');
}
return audit;
};
const raisedButtonState = async (target: Locator) => const raisedButtonState = async (target: Locator) =>
target.evaluate((element) => { target.evaluate((element) => {
const rect = element.getBoundingClientRect(); const rect = element.getBoundingClientRect();
@@ -2213,49 +2310,45 @@ test('the 939/940 boundary switches to the Ref-style 500px single document', asy
]) { ]) {
await expect(page.locator(selector)).toBeVisible(); await expect(page.locator(selector)).toBeVisible();
} }
const readMobilePanelOrder = () => const defaultOrder = [
page 'commands',
.locator('.layout-mobile > [data-mobile-panel-id]') 'nation-menu',
.evaluateAll((elements) => elements.map((element) => element.getAttribute('data-mobile-panel-id'))); 'nation',
await expect 'general',
.poll(readMobilePanelOrder) 'city',
.toEqual(['commands', 'nation-menu', 'nation', 'general', 'city', 'map', 'records', 'global-menu', 'messages']); 'map',
await page.evaluate(() => { 'records',
localStorage.setItem( 'global-menu',
'sam.mobileMainPanelOrder.v1', 'messages',
JSON.stringify([ ];
'messages', const customOrder = [
'map', 'messages',
'commands', 'map',
'nation-menu', 'commands',
'nation', 'nation-menu',
'general', 'nation',
'city', 'general',
'records', 'city',
'global-menu', 'records',
]) 'global-menu',
];
const reverseOrder = [...defaultOrder].reverse();
const mobilePanelAudits = {
default: await expectMobilePanelVisualOrder(page, defaultOrder),
custom: null as Awaited<ReturnType<typeof inspectMobilePanelLayout>> | null,
reverse: null as Awaited<ReturnType<typeof inspectMobilePanelLayout>> | null,
};
await setMobilePanelOrder(page, customOrder);
mobilePanelAudits.custom = await expectMobilePanelVisualOrder(page, customOrder);
await setMobilePanelOrder(page, reverseOrder);
mobilePanelAudits.reverse = await expectMobilePanelVisualOrder(page, reverseOrder);
if (artifactRoot) {
await mkdir(artifactRoot, { recursive: true });
await writeFile(
resolve(artifactRoot, `${basePath.slice(1)}-mobile-panel-css-order-audit.json`),
`${JSON.stringify(mobilePanelAudits, null, 2)}\n`
); );
document.dispatchEvent(new CustomEvent('sam-mobile-main-panel-order-changed')); }
});
await expect
.poll(readMobilePanelOrder)
.toEqual(['messages', 'map', 'commands', 'nation-menu', 'nation', 'general', 'city', 'records', 'global-menu']);
const customPanelGeometry = await page.locator('.layout-mobile > [data-mobile-panel-id]').evaluateAll((elements) =>
elements.map((element) => {
const rect = element.getBoundingClientRect();
return {
id: element.getAttribute('data-mobile-panel-id'),
top: rect.top,
bottom: rect.bottom,
left: rect.left,
right: rect.right,
};
})
);
expect(customPanelGeometry.every(({ left, right }) => left >= 0 && right <= 500)).toBe(true);
expect(
customPanelGeometry.every((panel, index) => index === 0 || panel.top >= customPanelGeometry[index - 1]!.bottom)
).toBe(true);
await persistArtifact(page, `${basePath.slice(1)}-mobile-500`); await persistArtifact(page, `${basePath.slice(1)}-mobile-500`);
}); });
@@ -2556,6 +2649,27 @@ test('real mobile devices initially fit the complete 500px game canvas', async (
expect(mainGeometry.documentScrollWidth).toBeLessThanOrEqual(mainGeometry.innerWidth); expect(mainGeometry.documentScrollWidth).toBeLessThanOrEqual(mainGeometry.innerWidth);
expect(mainGeometry.canvas).toEqual({ left: 0, right: 500, width: 500 }); expect(mainGeometry.canvas).toEqual({ left: 0, right: 500, width: 500 });
expect(mainGeometry.canvas.right).toBeLessThanOrEqual((mainGeometry.visualViewportWidth ?? 0) + 0.01); expect(mainGeometry.canvas.right).toBeLessThanOrEqual((mainGeometry.visualViewportWidth ?? 0) + 0.01);
let physicalPanelOrderAudit: unknown = null;
if (deviceWidth === 390) {
const defaultOrder = [
'commands',
'nation-menu',
'nation',
'general',
'city',
'map',
'records',
'global-menu',
'messages',
];
const reverseOrder = [...defaultOrder].reverse();
const defaultAudit = await expectMobilePanelVisualOrder(mobilePage, defaultOrder);
await setMobilePanelOrder(mobilePage, reverseOrder);
const reverseAudit = await expectMobilePanelVisualOrder(mobilePage, reverseOrder);
physicalPanelOrderAudit = { default: defaultAudit, reverse: reverseAudit };
await setMobilePanelOrder(mobilePage, defaultOrder);
await expectMobilePanelVisualOrder(mobilePage, defaultOrder);
}
if (artifactRoot) { if (artifactRoot) {
await mkdir(artifactRoot, { recursive: true }); await mkdir(artifactRoot, { recursive: true });
await mobilePage.screenshot({ await mobilePage.screenshot({
@@ -2599,7 +2713,11 @@ test('real mobile devices initially fit the complete 500px game canvas', async (
} }
} }
measurements[String(deviceWidth)] = { main: mainGeometry, routes: routeGeometry }; measurements[String(deviceWidth)] = {
main: mainGeometry,
mobilePanelOrder: physicalPanelOrderAudit,
routes: routeGeometry,
};
await context.close(); await context.close();
} }
+1 -1
View File
@@ -743,7 +743,7 @@ onMounted(() => {
</li> </li>
</ol> </ol>
<div class="mobile-layout-dialog__actions"> <div class="mobile-layout-dialog__actions">
<button type="button" @click="resetMobileLayoutOrder">Ref 초깃</button> <button type="button" @click="resetMobileLayoutOrder">기본</button>
<form method="dialog"><button type="submit">취소</button></form> <form method="dialog"><button type="submit">취소</button></form>
<button class="mobile-layout-apply" type="button" @click="applyMobileLayoutOrder">적용</button> <button class="mobile-layout-apply" type="button" @click="applyMobileLayoutOrder">적용</button>
</div> </div>