fix(gateway): 모바일 계정 버튼 행을 정리
일반 사용자는 두 버튼을 한 행에 유지하고 관리자 링크는 모바일에서 전체폭 둘째 행으로 분리한다. 500px과 더 좁은 viewport의 Chromium 좌표 및 상호작용 회귀를 추가한다.
This commit is contained in:
@@ -240,6 +240,112 @@ test('desktop administrator sidebar follows the navbar away and then sticks to t
|
|||||||
await writeFile(testInfo.outputPath('admin-sidebar-scroll-geometry.json'), JSON.stringify(measurements, null, 2));
|
await writeFile(testInfo.outputPath('admin-sidebar-scroll-geometry.json'), JSON.stringify(measurements, null, 2));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('keeps mobile account actions on clean rows for users and administrators', async ({ browser }, testInfo) => {
|
||||||
|
const measureAccountActions = async (page: Page) => {
|
||||||
|
const actions = page.locator('.account-actions');
|
||||||
|
await expect(actions).toBeVisible();
|
||||||
|
return actions.evaluate((element) => {
|
||||||
|
const rect = (target: Element) => {
|
||||||
|
const value = target.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
top: value.top,
|
||||||
|
right: value.right,
|
||||||
|
bottom: value.bottom,
|
||||||
|
left: value.left,
|
||||||
|
width: value.width,
|
||||||
|
height: value.height,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const items = Array.from(element.children).map((child) => ({
|
||||||
|
...rect(child),
|
||||||
|
clientWidth: (child as HTMLElement).clientWidth,
|
||||||
|
scrollWidth: (child as HTMLElement).scrollWidth,
|
||||||
|
whiteSpace: getComputedStyle(child).whiteSpace,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
container: rect(element),
|
||||||
|
documentWidth: document.documentElement.scrollWidth,
|
||||||
|
viewportWidth: window.innerWidth,
|
||||||
|
items,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const userContext = await browser.newContext({ viewport: { width: 500, height: 900 } });
|
||||||
|
const userPage = await userContext.newPage();
|
||||||
|
await installGatewayFixture(userPage, []);
|
||||||
|
await userPage.goto('lobby');
|
||||||
|
const userGeometry = await measureAccountActions(userPage);
|
||||||
|
expect(userGeometry.documentWidth).toBe(userGeometry.viewportWidth);
|
||||||
|
expect(userGeometry.items).toHaveLength(2);
|
||||||
|
expect(userGeometry.items[0]?.top).toBeCloseTo(userGeometry.items[1]?.top ?? 0, 0);
|
||||||
|
expect(userGeometry.items[0]?.bottom).toBeCloseTo(userGeometry.items[1]?.bottom ?? 0, 0);
|
||||||
|
expect(userGeometry.items[0]?.width).toBeCloseTo(userGeometry.items[1]?.width ?? 0, 0);
|
||||||
|
expect(userGeometry.items.every(({ scrollWidth, clientWidth }) => scrollWidth <= clientWidth)).toBe(true);
|
||||||
|
await userPage.screenshot({ path: testInfo.outputPath('gateway-account-actions-user-500px.png'), fullPage: true });
|
||||||
|
await userContext.close();
|
||||||
|
|
||||||
|
const adminContext = await browser.newContext({ viewport: { width: 500, height: 900 } });
|
||||||
|
const adminPage = await adminContext.newPage();
|
||||||
|
await installGatewayFixture(adminPage, ['superuser']);
|
||||||
|
await adminPage.goto('lobby');
|
||||||
|
const adminGeometry = await measureAccountActions(adminPage);
|
||||||
|
expect(adminGeometry.documentWidth).toBe(adminGeometry.viewportWidth);
|
||||||
|
expect(adminGeometry.items).toHaveLength(3);
|
||||||
|
const [account, logout, admin] = adminGeometry.items;
|
||||||
|
expect(account?.top).toBeCloseTo(logout?.top ?? 0, 0);
|
||||||
|
expect(account?.bottom).toBeCloseTo(logout?.bottom ?? 0, 0);
|
||||||
|
expect(account?.width).toBeCloseTo(logout?.width ?? 0, 0);
|
||||||
|
expect(admin?.top).toBeCloseTo((logout?.bottom ?? 0) + 16, 0);
|
||||||
|
expect(admin?.left).toBeCloseTo(account?.left ?? 0, 0);
|
||||||
|
expect(admin?.right).toBeCloseTo(logout?.right ?? 0, 0);
|
||||||
|
expect(adminGeometry.items.every(({ scrollWidth, clientWidth }) => scrollWidth <= clientWidth)).toBe(true);
|
||||||
|
expect(account?.whiteSpace).toBe('nowrap');
|
||||||
|
expect(admin?.whiteSpace).toBe('nowrap');
|
||||||
|
|
||||||
|
const adminLink = adminPage.getByRole('link', { name: '관리자 페이지' });
|
||||||
|
const baseBackground = await adminLink.evaluate((element) => getComputedStyle(element).backgroundColor);
|
||||||
|
await adminLink.hover();
|
||||||
|
await expect.poll(() => adminLink.evaluate((element) => getComputedStyle(element).backgroundColor)).not.toBe(
|
||||||
|
baseBackground
|
||||||
|
);
|
||||||
|
await adminLink.focus();
|
||||||
|
await expect(adminLink).toBeFocused();
|
||||||
|
await adminPage.screenshot({ path: testInfo.outputPath('gateway-account-actions-admin-500px.png'), fullPage: true });
|
||||||
|
|
||||||
|
await adminPage.setViewportSize({ width: 390, height: 844 });
|
||||||
|
const adminNarrowGeometry = await measureAccountActions(adminPage);
|
||||||
|
expect(adminNarrowGeometry.documentWidth).toBe(adminNarrowGeometry.viewportWidth);
|
||||||
|
expect(adminNarrowGeometry.items[0]?.top).toBeCloseTo(adminNarrowGeometry.items[1]?.top ?? 0, 0);
|
||||||
|
expect(adminNarrowGeometry.items[2]?.top).toBeCloseTo(
|
||||||
|
(adminNarrowGeometry.items[1]?.bottom ?? 0) + 16,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
expect(adminNarrowGeometry.items.every(({ scrollWidth, clientWidth }) => scrollWidth <= clientWidth)).toBe(true);
|
||||||
|
await adminPage.screenshot({ path: testInfo.outputPath('gateway-account-actions-admin-390px.png'), fullPage: true });
|
||||||
|
|
||||||
|
await adminPage.setViewportSize({ width: 360, height: 800 });
|
||||||
|
const adminSmallGeometry = await measureAccountActions(adminPage);
|
||||||
|
expect(adminSmallGeometry.documentWidth).toBe(adminSmallGeometry.viewportWidth);
|
||||||
|
expect(adminSmallGeometry.items[1]?.top).toBeCloseTo((adminSmallGeometry.items[0]?.bottom ?? 0) + 16, 0);
|
||||||
|
expect(adminSmallGeometry.items[2]?.top).toBeCloseTo((adminSmallGeometry.items[1]?.bottom ?? 0) + 16, 0);
|
||||||
|
expect(adminSmallGeometry.items.every(({ scrollWidth, clientWidth }) => scrollWidth <= clientWidth)).toBe(true);
|
||||||
|
await writeFile(
|
||||||
|
testInfo.outputPath('gateway-account-actions-geometry.json'),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
user: userGeometry,
|
||||||
|
admin: adminGeometry,
|
||||||
|
adminNarrow: adminNarrowGeometry,
|
||||||
|
adminSmall: adminSmallGeometry,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await adminContext.close();
|
||||||
|
});
|
||||||
|
|
||||||
test('legacy server operations URL keeps query parameters and redirects to the server list', async ({ page }) => {
|
test('legacy server operations URL keeps query parameters and redirects to the server list', async ({ page }) => {
|
||||||
await installGatewayFixture(page, ['superuser']);
|
await installGatewayFixture(page, ['superuser']);
|
||||||
|
|
||||||
|
|||||||
@@ -823,10 +823,10 @@ const handleEnter = async (profile: LobbyProfile, targetPath: string) => {
|
|||||||
>
|
>
|
||||||
계 정 관 리
|
계 정 관 리
|
||||||
</div>
|
</div>
|
||||||
<div class="p-6 flex justify-center space-x-4">
|
<div class="account-actions p-6">
|
||||||
<RouterLink
|
<RouterLink
|
||||||
to="/account"
|
to="/account"
|
||||||
class="bg-zinc-800 hover:bg-zinc-700 text-white px-6 py-2 rounded border border-zinc-700 transition-colors"
|
class="account-navigation-button bg-zinc-800 hover:bg-zinc-700 text-white px-6 py-2 rounded border border-zinc-700 transition-colors"
|
||||||
>
|
>
|
||||||
비밀번호 & 전콘 & 탈퇴
|
비밀번호 & 전콘 & 탈퇴
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
@@ -841,7 +841,7 @@ const handleEnter = async (profile: LobbyProfile, targetPath: string) => {
|
|||||||
<RouterLink
|
<RouterLink
|
||||||
v-if="canAccessAdmin"
|
v-if="canAccessAdmin"
|
||||||
to="/admin"
|
to="/admin"
|
||||||
class="bg-zinc-800 hover:bg-zinc-700 text-white px-6 py-2 rounded border border-zinc-700 transition-colors"
|
class="account-navigation-button account-admin-button bg-zinc-800 hover:bg-zinc-700 text-white px-6 py-2 rounded border border-zinc-700 transition-colors"
|
||||||
>
|
>
|
||||||
관리자 페이지
|
관리자 페이지
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
@@ -855,6 +855,20 @@ const handleEnter = async (profile: LobbyProfile, targetPath: string) => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.account-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-navigation-button {
|
||||||
|
display: flex;
|
||||||
|
box-sizing: border-box;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.legacy-logout-button {
|
.legacy-logout-button {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 200px;
|
width: 200px;
|
||||||
@@ -1091,4 +1105,38 @@ const handleEnter = async (profile: LobbyProfile, targetPath: string) => {
|
|||||||
cursor: default;
|
cursor: default;
|
||||||
opacity: 0.65;
|
opacity: 0.65;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.account-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-actions > * {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-navigation-button {
|
||||||
|
height: 48px;
|
||||||
|
padding: 10px 4px;
|
||||||
|
font-size: clamp(12px, 3.2vw, 16px);
|
||||||
|
line-height: 24px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-admin-button {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 360px) {
|
||||||
|
.account-actions {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-admin-button {
|
||||||
|
grid-column: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user