diff --git a/app/gateway-frontend/e2e/lobby-admin-navigation.spec.ts b/app/gateway-frontend/e2e/lobby-admin-navigation.spec.ts index 541f3c27..5ef67985 100644 --- a/app/gateway-frontend/e2e/lobby-admin-navigation.spec.ts +++ b/app/gateway-frontend/e2e/lobby-admin-navigation.spec.ts @@ -139,7 +139,59 @@ test('bootstrap superuser can navigate the administrator workspace from the lobb await navigation.getByRole('link', { name: 'Gateway 릴리스' }).click(); await expect(page).toHaveURL(/\/gateway\/admin\/releases$/); - await expect(page.getByRole('heading', { name: 'Gateway 릴리스' })).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Gateway 릴리스', level: 1 })).toBeVisible(); +}); + +test('desktop administrator sidebar follows the navbar away and then sticks to the viewport top', async ({ + page, +}, testInfo) => { + await page.setViewportSize({ width: 1200, height: 500 }); + await installGatewayFixture(page, ['superuser']); + await page.goto('admin'); + + const sidebar = page.locator('#admin-navigation'); + await expect(sidebar).toBeVisible(); + const measurements: Array<{ + scrollY: number; + top: number; + bottom: number; + height: number; + viewportHeight: number; + position: string; + backgroundColor: string; + }> = []; + + for (const scrollY of [0, 20, 55, 56, 120]) { + await page.evaluate((top) => window.scrollTo(0, top), scrollY); + await expect.poll(() => page.evaluate(() => window.scrollY)).toBe(scrollY); + + const geometry = await sidebar.evaluate((element) => { + const rect = element.getBoundingClientRect(); + const style = getComputedStyle(element); + return { + top: rect.top, + bottom: rect.bottom, + height: rect.height, + viewportHeight: window.innerHeight, + position: style.position, + backgroundColor: style.backgroundColor, + }; + }); + expect(geometry.top).toBeCloseTo(Math.max(0, 56 - scrollY), 0); + expect(geometry.position).toBe('sticky'); + expect(geometry.backgroundColor).toBe('rgb(17, 17, 19)'); + measurements.push({ scrollY, ...geometry }); + + if (scrollY >= 56) { + expect(geometry.bottom).toBeCloseTo(geometry.viewportHeight, 0); + } + + if (scrollY === 20 || scrollY === 56) { + await page.screenshot({ path: testInfo.outputPath(`admin-sidebar-scroll-${scrollY}.png`) }); + } + } + + await writeFile(testInfo.outputPath('admin-sidebar-scroll-geometry.json'), JSON.stringify(measurements, null, 2)); }); test('legacy server operations URL keeps query parameters and redirects to the server list', async ({ page }) => { diff --git a/app/gateway-frontend/src/layouts/AdminConsoleLayout.vue b/app/gateway-frontend/src/layouts/AdminConsoleLayout.vue index 50c4b81f..4cd44cc2 100644 --- a/app/gateway-frontend/src/layouts/AdminConsoleLayout.vue +++ b/app/gateway-frontend/src/layouts/AdminConsoleLayout.vue @@ -201,8 +201,8 @@ onMounted(async () => { .admin-sidebar { position: sticky; - top: 56px; - height: calc(100vh - 56px); + top: 0; + height: 100vh; border-right: 1px solid #27272a; background: #111113; padding: 24px 16px;