관리자가 현재 수행할 기능과 무관한 작업 영역 재구성 설명을 제거합니다. 현재 행동 안내만 남기고 실제 Chromium에서 문구 부재와 반응형 카드 위치를 검증합니다.
270 lines
13 KiB
TypeScript
270 lines
13 KiB
TypeScript
import { expect, test, type Page, type Route } from '@playwright/test';
|
|
import { writeFile } from 'node:fs/promises';
|
|
|
|
const response = (data: unknown) => ({ result: { data } });
|
|
|
|
const operationNames = (route: Route): string[] => {
|
|
const url = new URL(route.request().url());
|
|
return decodeURIComponent(url.pathname.slice(url.pathname.lastIndexOf('/trpc/') + 6)).split(',');
|
|
};
|
|
|
|
const installGatewayFixture = async (page: Page, roles: string[]) => {
|
|
const requests: Array<{ method: string; url: string; body: unknown }> = [];
|
|
await page.addInitScript(() => {
|
|
window.localStorage.setItem('sammo-session-token', 'playwright-admin-session');
|
|
});
|
|
await page.route('**/gateway/api/trpc/**', async (route) => {
|
|
requests.push({
|
|
method: route.request().method(),
|
|
url: route.request().url(),
|
|
body: route.request().postDataJSON(),
|
|
});
|
|
const results = operationNames(route).map((operation) => {
|
|
if (operation === 'me') {
|
|
return response({
|
|
id: 'admin-user',
|
|
username: 'admin',
|
|
displayName: '관리자',
|
|
roles,
|
|
createdAt: '2026-07-25T00:00:00.000Z',
|
|
});
|
|
}
|
|
if (operation === 'lobby.notice' || operation === 'admin.system.getNotice') {
|
|
return response(operation === 'lobby.notice' ? '' : { notice: '' });
|
|
}
|
|
if (
|
|
operation === 'lobby.profiles' ||
|
|
operation === 'admin.profiles.listScenarios' ||
|
|
operation === 'admin.operations.list' ||
|
|
operation === 'admin.releases.list'
|
|
) {
|
|
return response([]);
|
|
}
|
|
if (operation === 'admin.profiles.list') {
|
|
return response(
|
|
roles.some((role) => role.includes(':hwe:2'))
|
|
? [
|
|
{
|
|
profileName: 'hwe:2',
|
|
profile: 'hwe',
|
|
instanceKey: '2',
|
|
currentScenario: '1010',
|
|
scenario: '1010',
|
|
status: 'RUNNING',
|
|
buildStatus: 'SUCCEEDED',
|
|
meta: { korName: '환상서버' },
|
|
runtime: {},
|
|
runtimeActions: [],
|
|
},
|
|
]
|
|
: []
|
|
);
|
|
}
|
|
if (operation === 'admin.profiles.listNavigation') {
|
|
return response(
|
|
roles.some((role) => role === 'superuser' || role.includes(':hwe:2'))
|
|
? [
|
|
{
|
|
profileName: 'hwe:2',
|
|
profile: 'hwe',
|
|
instanceKey: '2',
|
|
currentScenario: '1010',
|
|
meta: { korName: '환상서버' },
|
|
},
|
|
]
|
|
: []
|
|
);
|
|
}
|
|
if (operation === 'admin.releases.gatewayState') {
|
|
return response({ id: 'gateway', updatedAt: '2026-08-01T00:00:00.000Z' });
|
|
}
|
|
if (operation === 'admin.users.getLocalAccountStatus') {
|
|
return response({ enabled: true });
|
|
}
|
|
if (operation === 'admin.capabilities.list') {
|
|
return response(
|
|
roles.includes('superuser')
|
|
? [
|
|
{ permission: 'admin.users.manage', scope: 'GLOBAL', scopes: ['*'] },
|
|
{ permission: 'admin.profiles.runtime', scope: 'PROFILE', scopes: ['*'] },
|
|
{ permission: 'admin.profiles.settings', scope: 'PROFILE', scopes: ['*'] },
|
|
{ permission: 'admin.profiles.deploy', scope: 'PROFILE', scopes: ['*'] },
|
|
{ permission: 'admin.scenarios.reset', scope: 'PROFILE', scopes: ['*'] },
|
|
{ permission: 'admin.releases.manage', scope: 'GLOBAL', scopes: ['*'] },
|
|
{ permission: 'admin.notice.manage', scope: 'GLOBAL', scopes: ['*'] },
|
|
{ permission: 'admin.audit.read', scope: 'GLOBAL', scopes: ['*'] },
|
|
]
|
|
: [
|
|
{
|
|
permission: roles[0]?.split(':')[0],
|
|
scope: 'PROFILE',
|
|
scopes: ['hwe:2'],
|
|
},
|
|
]
|
|
);
|
|
}
|
|
throw new Error(`Unhandled tRPC operation: ${operation}`);
|
|
});
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(
|
|
new URL(route.request().url()).searchParams.get('batch') === '1' ? results : results[0]
|
|
),
|
|
});
|
|
});
|
|
return requests;
|
|
};
|
|
|
|
test('bootstrap superuser can navigate the administrator workspace from the lobby', async ({ page }, testInfo) => {
|
|
const requests = await installGatewayFixture(page, ['superuser']);
|
|
|
|
await page.goto('lobby');
|
|
const adminLink = page.getByRole('link', { name: '관리자 페이지' });
|
|
await expect(adminLink).toBeVisible();
|
|
await adminLink.click();
|
|
|
|
await expect(page).toHaveURL(/\/gateway\/admin$/);
|
|
await expect(page.getByRole('heading', { name: '운영 개요' })).toBeVisible();
|
|
await expect(page.getByText('관리 대상을 선택하세요.')).toBeVisible();
|
|
await expect(page.getByRole('region', { name: '관리 원칙' })).toHaveCount(0);
|
|
await expect(page.getByText('작업 영역이 분리되었습니다.')).toHaveCount(0);
|
|
const overviewGrid = page.getByRole('region', { name: '관리 기능' });
|
|
const desktopOverviewGeometry = await overviewGrid.evaluate((element) => {
|
|
const rect = element.getBoundingClientRect();
|
|
return { left: rect.left, right: rect.right, top: rect.top, width: rect.width };
|
|
});
|
|
expect(desktopOverviewGeometry.top).toBeLessThan(300);
|
|
await writeFile(
|
|
testInfo.outputPath('admin-overview-desktop-geometry.json'),
|
|
JSON.stringify(desktopOverviewGeometry)
|
|
);
|
|
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 page.getByRole('button', { name: '관리자 메뉴' }).click();
|
|
await expect(navigation).toBeHidden();
|
|
await page.evaluate(() => window.scrollTo(0, 0));
|
|
await expect.poll(() => page.evaluate(() => window.scrollY)).toBe(0);
|
|
const mobileOverviewGeometry = await overviewGrid.evaluate((element) => {
|
|
const rect = element.getBoundingClientRect();
|
|
return { left: rect.left, right: rect.right, top: rect.top, width: rect.width };
|
|
});
|
|
expect(mobileOverviewGeometry.left).toBeGreaterThanOrEqual(0);
|
|
expect(mobileOverviewGeometry.right).toBeLessThanOrEqual(390);
|
|
expect(mobileOverviewGeometry.top).toBeLessThan(340);
|
|
await writeFile(
|
|
testInfo.outputPath('admin-overview-mobile-content-geometry.json'),
|
|
JSON.stringify(mobileOverviewGeometry)
|
|
);
|
|
await page.screenshot({ path: testInfo.outputPath('admin-overview-mobile-content.png'), fullPage: true });
|
|
|
|
await page.getByRole('button', { name: '관리자 메뉴' }).click();
|
|
await navigation.getByRole('link', { name: 'Gateway 릴리스' }).click();
|
|
await expect(page).toHaveURL(/\/gateway\/admin\/releases$/);
|
|
await expect(page.getByRole('heading', { name: 'Gateway 릴리스', level: 1 })).toBeVisible();
|
|
expect(requests.length).toBeGreaterThan(0);
|
|
expect(requests.every(({ method }) => method === 'POST')).toBe(true);
|
|
expect(requests.every(({ url }) => !new URL(url).searchParams.has('input'))).toBe(true);
|
|
expect(requests.some(({ body }) => JSON.stringify(body).includes('"limit":30'))).toBe(true);
|
|
});
|
|
|
|
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, 75, 76, 140]) {
|
|
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, 76 - scrollY), 0);
|
|
expect(geometry.position).toBe('sticky');
|
|
expect(geometry.backgroundColor).toBe('rgb(17, 17, 19)');
|
|
measurements.push({ scrollY, ...geometry });
|
|
|
|
if (scrollY >= 76) {
|
|
expect(geometry.bottom).toBeCloseTo(geometry.viewportHeight, 0);
|
|
}
|
|
|
|
if (scrollY === 20 || scrollY === 76) {
|
|
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 }) => {
|
|
await installGatewayFixture(page, ['superuser']);
|
|
|
|
await page.goto('admin/server-operations?operationId=legacy-operation');
|
|
await expect(page).toHaveURL(/\/gateway\/admin\/servers\?operationId=legacy-operation$/);
|
|
});
|
|
|
|
test('scoped administrators see the same navigation while ordinary users do not', async ({ browser }) => {
|
|
const scopedContext = await browser.newContext();
|
|
const scopedPage = await scopedContext.newPage();
|
|
await installGatewayFixture(scopedPage, ['admin.profiles.runtime:hwe:2']);
|
|
await scopedPage.goto('lobby');
|
|
await expect(scopedPage.getByRole('link', { name: '관리자 페이지' })).toBeVisible();
|
|
await scopedPage.getByRole('link', { name: '관리자 페이지' }).click();
|
|
const scopedNavigation = scopedPage.getByRole('navigation', { name: '관리자 메뉴' });
|
|
await expect(scopedNavigation.getByRole('link', { name: '환상서버 [2]' })).toBeVisible();
|
|
await expect(scopedNavigation.getByRole('link', { name: 'Gateway 릴리스' })).toHaveCount(0);
|
|
await expect(scopedNavigation.getByRole('link', { name: '사용자 관리' })).toHaveCount(0);
|
|
await scopedContext.close();
|
|
|
|
const userContext = await browser.newContext();
|
|
const userPage = await userContext.newPage();
|
|
await installGatewayFixture(userPage, []);
|
|
await userPage.goto('lobby');
|
|
await expect(userPage.getByRole('link', { name: '관리자 페이지' })).toHaveCount(0);
|
|
await userContext.close();
|
|
});
|