feat(gateway): reorganize admin console navigation

This commit is contained in:
2026-08-08 13:36:37 +00:00
parent d99e18f334
commit 4d46afa6a5
16 changed files with 804 additions and 922 deletions
@@ -134,14 +134,13 @@ const installFixture = async (page: Page) => {
test('operates OAuth grace and scheduled deletion with reasoned audit history', async ({ page }, testInfo) => {
const mutations = await installFixture(page);
page.on('dialog', (dialog) => dialog.accept());
await page.goto('admin');
await page.goto('admin/users');
await page.getByPlaceholder('검색 값 입력').fill('target');
await page.getByRole('button', { name: '조회', exact: true }).click();
await expect(page.getByText('Kakao 인증: 미완료')).toBeVisible();
await expect(page.getByRole('cell', { name: 'che:default' })).toBeVisible();
await expect(page.getByText('SUCCEEDED · admin.users.updateSanctions').first()).toBeVisible();
await expect(page.getByRole('heading', { name: '전체 관리자 감사 원장' })).toBeVisible();
await page.screenshot({ path: testInfo.outputPath('gateway-admin-account-controls-desktop.png'), fullPage: true });
const deletionButton = page.getByRole('button', { name: '보존 기간 후 탈퇴 예약', exact: true });
const baseDeleteColor = await deletionButton.evaluate((button) => getComputedStyle(button).backgroundColor);
@@ -163,6 +162,11 @@ test('operates OAuth grace and scheduled deletion with reasoned audit history',
expect(mutations.some(({ operation }) => operation === 'admin.users.updateKakaoGrace')).toBe(true);
expect(mutations.some(({ operation }) => operation === 'admin.users.scheduleDeletion')).toBe(true);
await page.getByRole('link', { name: '감사 로그' }).click();
await expect(page).toHaveURL(/\/gateway\/admin\/audit$/);
await expect(page.getByRole('heading', { name: '전체 관리자 감사 원장' })).toBeVisible();
await expect(page.getByText('SUCCEEDED · admin.users.updateKakaoGrace').first()).toBeVisible();
await page.setViewportSize({ width: 390, height: 844 });
const geometry = await page
.getByRole('heading', { name: '전체 관리자 감사 원장' })
@@ -173,6 +177,9 @@ test('operates OAuth grace and scheduled deletion with reasoned audit history',
});
expect(geometry.left).toBeGreaterThanOrEqual(0);
expect(geometry.right).toBeLessThanOrEqual(geometry.viewportWidth);
await writeFile(testInfo.outputPath('gateway-admin-account-controls-mobile-geometry.json'), JSON.stringify(geometry));
await writeFile(
testInfo.outputPath('gateway-admin-account-controls-mobile-geometry.json'),
JSON.stringify(geometry)
);
await page.screenshot({ path: testInfo.outputPath('gateway-admin-account-controls-mobile.png'), fullPage: true });
});
@@ -211,8 +211,8 @@ const installFixture = async (
test('reports clock-shift acceptance separately from actual application', async ({ page }) => {
const fixture = await installFixture(page, { deferRequest: true, pendingProfileReads: 1 });
await page.goto('admin');
await expect(page.getByRole('heading', { name: '관리자 콘솔' })).toBeVisible();
await page.goto('admin/servers');
await expect(page.getByRole('heading', { name: '서버 관리' })).toBeVisible();
const duration = page.locator('input[type="number"][min="1"][max="1440"]');
const accelerate = page.getByRole('button', { name: '가속', exact: true });
@@ -249,7 +249,7 @@ test('blocks another clock shift while any recent action is pending', async ({ p
}),
],
});
await page.goto('admin');
await page.goto('admin/servers');
await page.locator('input[type="number"][min="1"][max="1440"]').fill('15');
await expect(page.getByRole('button', { name: '가속', exact: true })).toBeDisabled();
@@ -264,7 +264,7 @@ test('renders a failed terminal outcome without calling it applied', async ({ pa
}),
],
});
await page.goto('admin');
await page.goto('admin/servers');
const failed = page.getByText('FAILED · ACCELERATE 15분');
await expect(failed).toBeVisible();
@@ -282,7 +282,7 @@ test('renders an ignored terminal outcome without calling it applied', async ({
}),
],
});
await page.goto('admin');
await page.goto('admin/servers');
const ignored = page.getByText('IGNORED · DELAY 15분');
await expect(ignored).toBeVisible();
@@ -291,32 +291,20 @@ test('renders an ignored terminal outcome without calling it applied', async ({
await expect(page.getByText(/적용됨|요청 완료/)).toHaveCount(0);
});
test('keeps profile installation disabled while its queued operation is active', async ({ page }, testInfo) => {
const fixture = await installFixture(page, { deferInstall: true });
page.on('dialog', (dialog) => dialog.accept());
await page.goto('admin');
test('directs profile deployment to the centralized version page', async ({ page }) => {
await installFixture(page);
await page.goto('admin/servers');
const installButton = page.getByRole('button', { name: '설치 적용' });
const click = installButton.click();
await expect.poll(() => fixture.requestBodies.length).toBe(1);
await expect(page.getByRole('button', { name: '등록 중…' })).toBeDisabled();
fixture.releaseInstall();
await click;
const operationLink = page.getByRole('link', { name: /77777777-7777-4777-8777-777777777777 상태 보기/ });
await expect(operationLink).toBeVisible();
await expect(page.getByRole('button', { name: '설치 작업 진행 중' })).toBeDisabled();
const releaseLink = page.getByRole('link', { name: '버전 업데이트 열기' });
await expect(releaseLink).toBeVisible();
await expect(releaseLink).toHaveAttribute('href', '/gateway/admin/releases');
await expect(page.getByRole('button', { name: '설치 적용' })).toHaveCount(0);
await page.setViewportSize({ width: 390, height: 844 });
const linkGeometry = await operationLink.evaluate((element) => {
const linkGeometry = await releaseLink.evaluate((element) => {
const rect = element.getBoundingClientRect();
return { left: rect.left, right: rect.right, viewportWidth: window.innerWidth };
});
expect(linkGeometry.left).toBeGreaterThanOrEqual(0);
expect(linkGeometry.right).toBeLessThanOrEqual(linkGeometry.viewportWidth);
await page.screenshot({ path: testInfo.outputPath('admin-install-active-mobile.png'), fullPage: true });
await operationLink.click();
await expect(page).toHaveURL(/\/gateway\/admin\/server-operations\?operationId=77777777/);
await expect(page.getByTestId('operations-table')).toContainText('77777777-7777-4777-8777-777777777777');
});
@@ -27,7 +27,7 @@ const login = async (page: Page, username: string, password: string): Promise<vo
const hweRow = (page: Page) => page.locator('tbody tr').filter({ hasText: /^hwe섭/ });
const resetScenario = async (page: Page, scenarioId: string, sourceCommit: string): Promise<void> => {
await page.goto('/gateway/admin/server-operations');
await page.goto('/gateway/admin/releases');
await page.getByTestId('profile-select').selectOption('hwe:2');
await page.getByTestId('source-commit').check();
await page.getByTestId('source-ref').fill(sourceCommit);
@@ -85,8 +85,8 @@ test('admin resets and opens hwe, then two users create generals and reach main'
await login(page, adminUsername, await readPassword('admin'));
await page.getByRole('link', { name: '관리자 페이지' }).click();
await expect(page).toHaveURL(/\/gateway\/admin$/);
await page.getByRole('link', { name: '서버 배포 · 시나리오 초기화' }).click();
await expect(page).toHaveURL(/\/gateway\/admin\/server-operations$/);
await page.getByRole('link', { name: '버전 업데이트' }).first().click();
await expect(page).toHaveURL(/\/gateway\/admin\/releases$/);
await page.getByTestId('profile-select').selectOption(profileKey);
const profileStatus = page.getByTestId('selected-profile-status');
@@ -1,4 +1,5 @@
import { expect, test, type Page, type Route } from '@playwright/test';
import { writeFile } from 'node:fs/promises';
const response = (data: unknown) => ({ result: { data } });
@@ -53,7 +54,7 @@ const installGatewayFixture = async (page: Page, roles: string[]) => {
});
};
test('bootstrap superuser can navigate from the lobby to server operations', async ({ page }) => {
test('bootstrap superuser can navigate the administrator workspace from the lobby', async ({ page }, testInfo) => {
await installGatewayFixture(page, ['superuser']);
await page.goto('lobby');
@@ -62,9 +63,39 @@ test('bootstrap superuser can navigate from the lobby to server operations', asy
await adminLink.click();
await expect(page).toHaveURL(/\/gateway\/admin$/);
await expect(page.getByRole('heading', { name: '관리자 콘솔' })).toBeVisible();
await page.getByRole('link', { name: '서버 배포 · 시나리오 초기화' }).click();
await expect(page).toHaveURL(/\/gateway\/admin\/server-operations$/);
await expect(page.getByRole('heading', { name: '운영 개요' })).toBeVisible();
const navigation = page.getByRole('navigation', { name: '관리자 메뉴' });
await expect(navigation).toBeVisible();
const userLink = navigation.getByRole('link', { name: '사용자 관리' });
const baseColor = await userLink.evaluate((element) => getComputedStyle(element).color);
await userLink.hover();
await expect.poll(() => userLink.evaluate((element) => getComputedStyle(element).color)).not.toBe(baseColor);
await page.screenshot({ path: testInfo.outputPath('admin-overview-desktop.png'), fullPage: true });
await page.setViewportSize({ width: 390, height: 844 });
await expect(navigation).toBeHidden();
await page.getByRole('button', { name: '관리자 메뉴' }).click();
await expect(navigation).toBeVisible();
const geometry = await navigation.evaluate((element) => {
const rect = element.getBoundingClientRect();
return { left: rect.left, right: rect.right, top: rect.top, viewportWidth: window.innerWidth };
});
expect(geometry.left).toBeGreaterThanOrEqual(0);
expect(geometry.right).toBeLessThanOrEqual(geometry.viewportWidth);
expect(geometry.top).toBeGreaterThanOrEqual(0);
await writeFile(testInfo.outputPath('admin-overview-mobile-geometry.json'), JSON.stringify(geometry));
await page.screenshot({ path: testInfo.outputPath('admin-overview-mobile-menu.png'), fullPage: true });
await navigation.getByRole('link', { name: '버전 업데이트' }).click();
await expect(page).toHaveURL(/\/gateway\/admin\/releases$/);
await expect(page.getByRole('heading', { name: '버전 업데이트' })).toBeVisible();
});
test('legacy server operations URL keeps query parameters and redirects to releases', async ({ page }) => {
await installGatewayFixture(page, ['superuser']);
await page.goto('admin/server-operations?operationId=legacy-operation');
await expect(page).toHaveURL(/\/gateway\/admin\/releases\?operationId=legacy-operation$/);
});
test('scoped administrators see the same navigation while ordinary users do not', async ({ browser }) => {
@@ -218,9 +218,9 @@ test('separates branch and commit semantics and submits a reset from the dedicat
await installFixture(page, state);
page.on('dialog', (dialog) => dialog.accept());
await page.goto('admin/server-operations');
await page.goto('admin/releases');
await expect(page.getByTestId('server-operations-page')).toBeVisible();
await expect(page).toHaveURL(/\/gateway\/admin\/server-operations$/);
await expect(page).toHaveURL(/\/gateway\/admin\/releases$/);
await expect(page.getByTestId('source-help')).toContainText('실제로 시작될 때');
await expect(page.getByTestId('scenario-select')).toHaveValue('2');
@@ -293,7 +293,7 @@ test('starts and stops all runtime roles through the operation controls', async
await installFixture(page, state);
page.on('dialog', (dialog) => dialog.accept());
await page.goto('admin/server-operations');
await page.goto('admin/releases');
await page.getByTestId('start-server').click();
await expect(page.getByText('시작 작업을 요청했습니다.')).toBeVisible();
await expect(page.getByText('RUNNING', { exact: true }).first()).toBeVisible();
@@ -312,7 +312,7 @@ test('separates DB-preserving profile deployment from DB reset', async ({ page }
await installFixture(page, state);
page.on('dialog', (dialog) => dialog.accept());
await page.goto('admin/server-operations');
await page.goto('admin/releases');
await expect(page.getByText('Game frontend')).toBeVisible();
await page.getByTestId('request-deploy').click();
@@ -327,7 +327,7 @@ test('controls gateway deployment and rollback through the external controller q
await installFixture(page, state);
page.on('dialog', (dialog) => dialog.accept());
await page.goto('admin/server-operations');
await page.goto('admin/releases');
const panel = page.getByTestId('gateway-release-panel');
await expect(panel).toBeVisible();
await expect(panel).toContainText('aaaaaaaaaaaa');
@@ -375,7 +375,7 @@ test('renders a failed reset, retries it as a new operation, and reaches success
await installFixture(page, state);
page.on('dialog', (dialog) => dialog.accept());
await page.goto('admin/server-operations');
await page.goto('admin/releases');
await expect(page.getByText('FAILED', { exact: true })).toBeVisible();
await expect(page.getByRole('cell', { name: 'fedcba987654', exact: true })).toBeVisible();
const failure = page.getByText(longError);