feat: 모바일 메인 패널 순서 설정 추가

This commit is contained in:
2026-08-20 15:46:32 +00:00
parent 1431995a41
commit 331851c481
6 changed files with 701 additions and 168 deletions
+74 -1
View File
@@ -1231,7 +1231,9 @@ test('내 정보&설정의 지난 플레이는 기본 탐색과 분리되어 오
const actions = element.querySelector<HTMLElement>('.title-actions')!.getBoundingClientRect();
const navigation = element.querySelector<HTMLElement>('.navigation-actions')!.getBoundingClientRect();
const back = element.querySelector<HTMLAnchorElement>('.navigation-actions a')!.getBoundingClientRect();
const refresh = element.querySelector<HTMLButtonElement>('.navigation-actions button')!.getBoundingClientRect();
const refresh = element
.querySelector<HTMLButtonElement>('.navigation-actions button')!
.getBoundingClientRect();
const past = element.querySelector<HTMLAnchorElement>('.past-plays-link')!.getBoundingClientRect();
const pastStyle = getComputedStyle(element.querySelector<HTMLAnchorElement>('.past-plays-link')!);
return {
@@ -1269,6 +1271,77 @@ test('내 정보&설정의 지난 플레이는 기본 탐색과 분리되어 오
}
});
test('내 정보&설정에서 모바일 메인 패널을 드래그하거나 버튼으로 재정렬하고 Ref 순서로 복원한다', async ({ page }) => {
const state: FixtureState = { permission: 'head', myset: 3, settingMutations: [], accessPages: [] };
await install(page, state);
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('my-page');
await page.getByRole('button', { name: '순서 바꾸기', exact: true }).click();
const dialog = page.getByRole('dialog', { name: '모바일 레이아웃 순서 바꾸기' });
await expect(dialog).toBeVisible();
const readOrder = () =>
dialog
.locator('[data-mobile-layout-id]')
.evaluateAll((elements) => elements.map((element) => element.getAttribute('data-mobile-layout-id')));
const defaultOrder = [
'commands',
'nation-menu',
'nation',
'general',
'city',
'map',
'records',
'global-menu',
'messages',
];
await expect.poll(readOrder).toEqual(defaultOrder);
await dialog
.locator('[data-mobile-layout-id="messages"]')
.dragTo(dialog.locator('[data-mobile-layout-id="commands"]'));
await expect
.poll(readOrder)
.toEqual(['messages', 'commands', 'nation-menu', 'nation', 'general', 'city', 'map', 'records', 'global-menu']);
await dialog.getByRole('button', { name: '지도 위로' }).click();
await expect
.poll(readOrder)
.toEqual(['messages', 'commands', 'nation-menu', 'nation', 'general', 'map', 'city', 'records', 'global-menu']);
const dialogGeometry = await dialog.evaluate((element) => {
const rect = element.getBoundingClientRect();
const firstItem = element.querySelector<HTMLElement>('[data-mobile-layout-id]')?.getBoundingClientRect();
const moveButton = element.querySelector<HTMLButtonElement>('[aria-label$="아래로"]')?.getBoundingClientRect();
return {
rect: rect.toJSON(),
firstItem: firstItem?.toJSON() ?? null,
moveButton: moveButton?.toJSON() ?? null,
overflowX: getComputedStyle(element).overflowX,
documentWidth: document.documentElement.scrollWidth,
};
});
expect(dialogGeometry.rect.left).toBeGreaterThanOrEqual(0);
expect(dialogGeometry.rect.right).toBeLessThanOrEqual(390);
expect(dialogGeometry.firstItem?.height).toBeGreaterThanOrEqual(44);
expect(dialogGeometry.moveButton?.width).toBeGreaterThanOrEqual(36);
expect(dialogGeometry.documentWidth).toBe(390);
await persistParityArtifact(page, 'core-my-page-mobile-layout-order-dialog', dialogGeometry);
await dialog.getByRole('button', { name: '적용', exact: true }).click();
await expect(dialog).toBeHidden();
await expect
.poll(() => page.evaluate(() => JSON.parse(localStorage.getItem('sam.mobileMainPanelOrder.v1') ?? '[]')))
.toEqual(['messages', 'commands', 'nation-menu', 'nation', 'general', 'map', 'city', 'records', 'global-menu']);
await page.getByRole('button', { name: '순서 바꾸기', exact: true }).click();
await dialog.getByRole('button', { name: 'Ref 초깃값' }).click();
await expect.poll(readOrder).toEqual(defaultOrder);
await dialog.getByRole('button', { name: '적용', exact: true }).click();
await expect
.poll(() => page.evaluate(() => JSON.parse(localStorage.getItem('sam.mobileMainPanelOrder.v1') ?? '[]')))
.toEqual(defaultOrder);
});
for (const [label, failure] of [
['daemon timeout', 'TIMEOUT'],
['engine transaction 오류', 'INTERNAL_SERVER_ERROR'],
+78 -39
View File
@@ -95,11 +95,10 @@ type DashboardBundleInput = {
const operationInput = (route: Route, index: number): DashboardBundleInput => {
const request = route.request();
const queryInput = new URL(request.url()).searchParams.get('input');
const parsed = (request.postData()
? request.postDataJSON()
: queryInput
? JSON.parse(queryInput)
: {}) as Record<string, unknown>;
const parsed = (request.postData() ? request.postDataJSON() : queryInput ? JSON.parse(queryInput) : {}) as Record<
string,
unknown
>;
const entry = (parsed[String(index)] ?? parsed) as { json?: DashboardBundleInput };
return entry.json ?? (entry as DashboardBundleInput);
};
@@ -253,32 +252,32 @@ const commandTableFixture = (large: boolean, blockedCount = 0, refCategories = f
general: draftCommands
? draftCommandGroups
: refCategories
? refCommandCategoryFixture
: large
? ['내정', '군사', '계략'].map((category, categoryIndex) => ({
category,
values: Array.from({ length: 16 }, (_, localIndex) => {
const index = categoryIndex * 16 + localIndex;
return {
key: `command-${index}`,
name: index === 0 ? '주민 선정과 장기 도시 개발' : `명령 ${index}`,
reqArg: index % 2 === 0,
possible: index >= blockedCount,
status: index >= blockedCount ? 'available' : 'blocked',
inputFields: [
{
key: 'amount',
label: '수량',
kind: 'number',
required: true,
min: 1,
max: 10_000,
},
],
};
}),
}))
: [],
? refCommandCategoryFixture
: large
? ['내정', '군사', '계략'].map((category, categoryIndex) => ({
category,
values: Array.from({ length: 16 }, (_, localIndex) => {
const index = categoryIndex * 16 + localIndex;
return {
key: `command-${index}`,
name: index === 0 ? '주민 선정과 장기 도시 개발' : `명령 ${index}`,
reqArg: index % 2 === 0,
possible: index >= blockedCount,
status: index >= blockedCount ? 'available' : 'blocked',
inputFields: [
{
key: 'amount',
label: '수량',
kind: 'number',
required: true,
min: 1,
max: 10_000,
},
],
};
}),
}))
: [],
nation: [],
inputOptions: {
cities: Array.from({ length: 20 }, (_, index) => ({ value: index + 1, label: `도시 ${index + 1}` })),
@@ -500,7 +499,7 @@ const installFixture = async (page: Page, state: NavigationFixture) => {
? response({ id: 'user-7', username: 'menu-user', displayName: '메뉴 사용자' })
: operation === 'navigation.get'
? response(runtimeNavigation)
: response({ ok: true })
: response({ ok: true })
);
await route.fulfill({
status: 200,
@@ -1558,10 +1557,7 @@ test('message targets keep reply behavior and use nation-color contrast in label
await page.setViewportSize({ width: 500, height: 900 });
const mobilePanel = page.locator('.mobile-message-panel');
await expect(mobilePanel.locator('.msg-plate[data-id="101"] .msg-target')).toHaveCSS(
'color',
'rgb(255, 255, 255)'
);
await expect(mobilePanel.locator('.msg-plate[data-id="101"] .msg-target')).toHaveCSS('color', 'rgb(255, 255, 255)');
await expect(mobilePanel.locator('.msg-plate[data-id="103"] .msg-target')).toHaveCSS('color', 'rgb(0, 0, 0)');
await expect(mobilePanel.locator('#mailbox_list optgroup[label="밝은국"]')).toHaveCSS('color', 'rgb(0, 0, 0)');
await persistArtifact(page, `${basePath.slice(1)}-message-nation-contrast-mobile-500`);
@@ -2217,6 +2213,49 @@ test('the 939/940 boundary switches to the Ref-style 500px single document', asy
]) {
await expect(page.locator(selector)).toBeVisible();
}
const readMobilePanelOrder = () =>
page
.locator('.layout-mobile > [data-mobile-panel-id]')
.evaluateAll((elements) => elements.map((element) => element.getAttribute('data-mobile-panel-id')));
await expect
.poll(readMobilePanelOrder)
.toEqual(['commands', 'nation-menu', 'nation', 'general', 'city', 'map', 'records', 'global-menu', 'messages']);
await page.evaluate(() => {
localStorage.setItem(
'sam.mobileMainPanelOrder.v1',
JSON.stringify([
'messages',
'map',
'commands',
'nation-menu',
'nation',
'general',
'city',
'records',
'global-menu',
])
);
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`);
});
@@ -3040,9 +3079,9 @@ for (const viewport of [
await refreshActivityAndCommands();
await expect(picker.getByLabel('장비 종류', { exact: true })).toHaveValue('weapon');
await expect(picker.getByLabel('장비', { exact: true })).toHaveValue('청룡언월도');
await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(
viewport.width
);
await expect
.poll(() => page.evaluate(() => document.documentElement.scrollWidth))
.toBeLessThanOrEqual(viewport.width);
});
}