test(frontend): add legacy visual parity fixtures
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
# Frontend legacy parity
|
||||
|
||||
This document records the reusable browser fixture and the visual contracts that
|
||||
are enforced while the PHP frontend is moved to the Vue applications. The
|
||||
workspace-level `docs/ref-core2026-mapping.md` remains the source of truth for
|
||||
the end-to-end PHP-to-core2026 mapping.
|
||||
|
||||
## Canonical browser fixture
|
||||
|
||||
`tools/frontend-legacy-parity/fixtures/canonical.ts` contains deterministic,
|
||||
non-secret gateway, session, map, and hall-of-fame data. The fixture uses a
|
||||
synthetic `ga_` game access token so the game router guard follows the same
|
||||
authenticated path as a real session without contacting a live database.
|
||||
|
||||
The test suite intercepts only the tRPC operations required by each screen and
|
||||
fails on unknown operations. It also serves the checked-out reference image
|
||||
tree instead of replacing images with layout-neutral placeholders.
|
||||
|
||||
Run the suite from the core2026 repository root:
|
||||
|
||||
```sh
|
||||
pnpm test:e2e:frontend-legacy
|
||||
```
|
||||
|
||||
The suite starts both applications at their public prefixes:
|
||||
|
||||
- gateway: `http://127.0.0.1:15100/gateway/`
|
||||
- game: `http://127.0.0.1:15102/che/`
|
||||
|
||||
Chromium uses the same locale, timezone, device scale factor, and color scheme
|
||||
for desktop and mobile checks. Tests assert geometry, computed typography and
|
||||
texture URLs before checking hover, focus, menu, login mutation, session
|
||||
storage, route guards, and image loading.
|
||||
|
||||
## Enforced contracts
|
||||
|
||||
| Screen | Ref entry point | Current automated contract |
|
||||
| -------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| gateway login/status | `index.php` | 450/700px desktop widths, mobile collapse, Pretendard title, real login mutation/session storage, actual seasonal map asset |
|
||||
| game login hand-off | unauthenticated `hwe/index.php` redirect | `/che/login` delegates to `/gateway/` |
|
||||
| troop | `hwe/v_troop.php` | existing `app/game-frontend/e2e/troop.spec.ts` desktop/mobile geometry and interaction suite |
|
||||
| hall of fame | `hwe/a_hallOfFame.php` | 500/1000px container, 100px ranking cells, 64px natural image, walnut/green textures, Pretendard, close-button focus |
|
||||
|
||||
The global game baseline is black, white, Pretendard 14px. Legacy texture
|
||||
helpers intentionally follow `common.orig.css`: `bg0` is walnut, `bg1` is
|
||||
green, and `bg2` is blue. Shared `PanelCard` uses the same walnut body and green
|
||||
header so screens that still use the common component no longer inherit the
|
||||
discarded Galmuri/parchment visual system.
|
||||
|
||||
## Route coverage rule
|
||||
|
||||
Adding or changing a frontend route requires:
|
||||
|
||||
1. a verified ref entry-point mapping in the workspace mapping document;
|
||||
2. deterministic tRPC fixture data for the visible state;
|
||||
3. desktop and 500px/mobile geometry and computed-style assertions;
|
||||
4. actual Chromium interaction checks for every visible interactive state;
|
||||
5. natural image dimensions and `object-fit` checks where images affect layout.
|
||||
|
||||
Pixel snapshots may be added after these structural assertions pass. Dynamic
|
||||
regions must not be hidden merely to make a pixel threshold pass.
|
||||
+2
-1
@@ -17,7 +17,8 @@
|
||||
"generate:resource-schemas": "pnpm --filter @sammo-ts/tools-scripts generate:resource-schemas",
|
||||
"validate:resources": "pnpm --filter @sammo-ts/tools-scripts validate:resources",
|
||||
"check:legacy:nation": "node tools/compare-command-constraints.mjs --include '^Nation/' --check && node tools/compare-command-logs.mjs --include '^Nation/' --mode action --check",
|
||||
"check:legacy:general": "node tools/compare-command-constraints.mjs --include '^General/' --check && node tools/compare-command-logs.mjs --include '^General/' --mode action --check && node tools/compare-general-turn-contracts.mjs --check"
|
||||
"check:legacy:general": "node tools/compare-command-constraints.mjs --include '^General/' --check && node tools/compare-command-logs.mjs --include '^General/' --mode action --check && node tools/compare-general-turn-contracts.mjs --check",
|
||||
"test:e2e:frontend-legacy": "playwright test --config tools/frontend-legacy-parity/playwright.config.mjs --tsconfig tools/frontend-legacy-parity/tsconfig.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
export const canonicalFrontendFixture = {
|
||||
gateway: {
|
||||
user: {
|
||||
id: 'visual-user-1',
|
||||
username: 'visual-user',
|
||||
displayName: '시각검증',
|
||||
roles: [],
|
||||
},
|
||||
sessionToken: 'session_visual_fixture',
|
||||
profile: {
|
||||
profileName: 'che',
|
||||
profile: 'che',
|
||||
apiPort: 15003,
|
||||
status: 'RUNNING',
|
||||
korName: '체',
|
||||
color: '#00bc8c',
|
||||
scenario: '가상모드22',
|
||||
},
|
||||
},
|
||||
game: {
|
||||
session: {
|
||||
profile: 'che',
|
||||
gameToken: 'ga_frontend_legacy_fixture',
|
||||
general: {
|
||||
id: 1,
|
||||
name: '유비',
|
||||
troopId: 0,
|
||||
},
|
||||
},
|
||||
lobby: {
|
||||
year: 197,
|
||||
month: 7,
|
||||
userCnt: 17,
|
||||
npcCnt: 31,
|
||||
nationCnt: 5,
|
||||
maxUserCnt: 50,
|
||||
turnTerm: 60,
|
||||
fictionMode: '가상',
|
||||
otherTextInfo: '표준',
|
||||
starttime: '0190-01-01T00:00:00.000Z',
|
||||
myGeneral: null,
|
||||
},
|
||||
mapLayout: {
|
||||
mapName: 'che',
|
||||
cityList: [
|
||||
{ id: 1, name: '낙양', level: 7, region: 1, x: 350, y: 245, path: [2] },
|
||||
{ id: 2, name: '장안', level: 7, region: 1, x: 260, y: 240, path: [1] },
|
||||
{ id: 3, name: '업', level: 6, region: 2, x: 390, y: 160, path: [1] },
|
||||
],
|
||||
regionMap: { 1: '사예', 2: '기주' },
|
||||
levelMap: { 6: '주', 7: '수도' },
|
||||
},
|
||||
map: {
|
||||
year: 197,
|
||||
month: 7,
|
||||
startYear: 190,
|
||||
cityList: [
|
||||
[1, 7, 0, 1, 1, 1],
|
||||
[2, 7, 0, 2, 1, 1],
|
||||
[3, 6, 0, 1, 2, 1],
|
||||
],
|
||||
nationList: [
|
||||
[1, '한', '#d32f2f', 1],
|
||||
[2, '진', '#1976d2', 2],
|
||||
],
|
||||
},
|
||||
hallOptions: [
|
||||
{
|
||||
season: 1,
|
||||
scenarios: [{ id: 22, name: '【가상모드22】 찐유저무쌍!', count: 1 }],
|
||||
},
|
||||
],
|
||||
hall: {
|
||||
sections: [
|
||||
{
|
||||
title: '명 성',
|
||||
valueType: 'int',
|
||||
entries: [
|
||||
{
|
||||
generalId: 1,
|
||||
name: '유비',
|
||||
ownerName: '시각검증',
|
||||
nationName: '촉',
|
||||
bgColor: '#006400',
|
||||
fgColor: '#ffffff',
|
||||
picture: 'default.jpg',
|
||||
imageServer: 0,
|
||||
value: 12000,
|
||||
printValue: '12,000',
|
||||
serverName: '체',
|
||||
serverIdx: 1,
|
||||
scenarioName: '가상모드22',
|
||||
startTime: '2026-01-01',
|
||||
unitedTime: '2026-02-01',
|
||||
},
|
||||
{
|
||||
generalId: 2,
|
||||
name: '조조',
|
||||
ownerName: '검증계정',
|
||||
nationName: '위',
|
||||
bgColor: '#8b0000',
|
||||
fgColor: '#ffffff',
|
||||
picture: 'default.jpg',
|
||||
imageServer: 0,
|
||||
value: 11000,
|
||||
printValue: '11,000',
|
||||
serverName: '체',
|
||||
serverIdx: 1,
|
||||
scenarioName: '가상모드22',
|
||||
startTime: '2026-01-01',
|
||||
unitedTime: '2026-02-01',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '계 급',
|
||||
valueType: 'int',
|
||||
entries: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -0,0 +1,48 @@
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
|
||||
export default defineConfig({
|
||||
testDir: '.',
|
||||
testMatch: 'visual-parity.spec.ts',
|
||||
fullyParallel: false,
|
||||
workers: 1,
|
||||
timeout: 30_000,
|
||||
expect: {
|
||||
timeout: 5_000,
|
||||
},
|
||||
reporter: [
|
||||
['list'],
|
||||
['html', { open: 'never', outputFolder: resolve(repositoryRoot, 'playwright-report/frontend-legacy') }],
|
||||
],
|
||||
outputDir: resolve(repositoryRoot, 'test-results/frontend-legacy'),
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
deviceScaleFactor: 1,
|
||||
colorScheme: 'dark',
|
||||
locale: 'ko-KR',
|
||||
timezoneId: 'UTC',
|
||||
trace: 'retain-on-failure',
|
||||
screenshot: 'only-on-failure',
|
||||
},
|
||||
webServer: [
|
||||
{
|
||||
command:
|
||||
'VITE_APP_BASE_PATH=/gateway VITE_GATEWAY_API_URL=/gateway/api/trpc VITE_GAME_API_URL_TEMPLATE=/{profile}/api/trpc VITE_GAME_ASSET_URL=/image pnpm --filter @sammo-ts/gateway-frontend dev --host 127.0.0.1 --port 15100',
|
||||
cwd: repositoryRoot,
|
||||
url: 'http://127.0.0.1:15100/gateway/',
|
||||
reuseExistingServer: false,
|
||||
timeout: 120_000,
|
||||
},
|
||||
{
|
||||
command:
|
||||
'VITE_APP_BASE_PATH=/che VITE_GAME_API_URL=/che/api/trpc VITE_GAME_ASSET_URL=/image VITE_GAME_PROFILE=che VITE_GATEWAY_WEB_URL=/gateway/ pnpm --filter @sammo-ts/game-frontend dev --host 127.0.0.1 --port 15102',
|
||||
cwd: repositoryRoot,
|
||||
url: 'http://127.0.0.1:15102/che/',
|
||||
reuseExistingServer: false,
|
||||
timeout: 120_000,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"target": "ES2022",
|
||||
"types": ["node", "@playwright/test"]
|
||||
},
|
||||
"include": ["./**/*.ts", "./**/*.mjs"]
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
import { expect, test, type Page, type Route } from '@playwright/test';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { dirname, extname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { canonicalFrontendFixture as fixture } from './fixtures/canonical';
|
||||
|
||||
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
const imageRoot = resolve(repositoryRoot, '../../image');
|
||||
|
||||
const response = (data: unknown) => ({ result: { data } });
|
||||
|
||||
const operationNames = (route: Route): string[] => {
|
||||
const pathname = new URL(route.request().url()).pathname;
|
||||
return decodeURIComponent(pathname.slice(pathname.lastIndexOf('/trpc/') + 6)).split(',');
|
||||
};
|
||||
|
||||
const fulfillOperations = async (route: Route, resolveOperation: (operation: string) => unknown): Promise<void> => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(operationNames(route).map((operation) => response(resolveOperation(operation)))),
|
||||
});
|
||||
};
|
||||
|
||||
const installImages = async (page: Page): Promise<void> => {
|
||||
await page.route('**/image/**', async (route) => {
|
||||
const pathname = decodeURIComponent(new URL(route.request().url()).pathname);
|
||||
const relative = pathname.replace(/^\/image\//, '');
|
||||
const candidates = [
|
||||
resolve(imageRoot, relative),
|
||||
resolve(imageRoot, 'game', relative),
|
||||
resolve(imageRoot, 'icons', '22.jpg'),
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
const body = await readFile(candidate);
|
||||
const extension = extname(candidate).toLowerCase();
|
||||
const contentType = extension === '.png' ? 'image/png' : 'image/jpeg';
|
||||
await route.fulfill({ status: 200, contentType, body });
|
||||
return;
|
||||
} catch {
|
||||
// 다음 공개 image root 후보를 확인한다.
|
||||
}
|
||||
}
|
||||
await route.abort('failed');
|
||||
});
|
||||
};
|
||||
|
||||
const installGatewayFixture = async (page: Page): Promise<void> => {
|
||||
let loggedIn = false;
|
||||
await installImages(page);
|
||||
await page.route('**/gateway/api/trpc/**', async (route) => {
|
||||
await fulfillOperations(route, (operation) => {
|
||||
if (operation === 'me') return loggedIn ? fixture.gateway.user : null;
|
||||
if (operation === 'lobby.profiles') return [fixture.gateway.profile];
|
||||
if (operation === 'lobby.notice') return [];
|
||||
if (operation === 'auth.login') {
|
||||
loggedIn = true;
|
||||
return {
|
||||
user: fixture.gateway.user,
|
||||
sessionToken: fixture.gateway.sessionToken,
|
||||
issuedAt: '2026-07-25T00:00:00.000Z',
|
||||
};
|
||||
}
|
||||
if (operation === 'auth.kakaoStart') {
|
||||
return { mode: 'login', state: 'visual-state', authUrl: '/gateway/oauth-started' };
|
||||
}
|
||||
throw new Error(`Unhandled gateway fixture operation: ${operation}`);
|
||||
});
|
||||
});
|
||||
await page.route('**/che/api/trpc/**', async (route) => {
|
||||
await fulfillOperations(route, (operation) => {
|
||||
if (operation === 'lobby.info') return fixture.game.lobby;
|
||||
if (operation === 'public.getMapLayout') return fixture.game.mapLayout;
|
||||
if (operation === 'public.getCachedMap') return fixture.game.map;
|
||||
throw new Error(`Unhandled gateway game fixture operation: ${operation}`);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const installHallFixture = async (page: Page): Promise<void> => {
|
||||
await installImages(page);
|
||||
await page.route('**/che/api/trpc/**', async (route) => {
|
||||
await fulfillOperations(route, (operation) => {
|
||||
if (operation === 'ranking.getHallOfFameOptions') return fixture.game.hallOptions;
|
||||
if (operation === 'ranking.getHallOfFame') return fixture.game.hall;
|
||||
throw new Error(`Unhandled hall fixture operation: ${operation}`);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const installAuthenticatedGameFixture = async (page: Page): Promise<void> => {
|
||||
await installImages(page);
|
||||
await page.addInitScript(
|
||||
({ gameToken, profile }) => {
|
||||
window.localStorage.setItem('sammo-game-token', gameToken);
|
||||
window.localStorage.setItem('sammo-game-profile', profile);
|
||||
},
|
||||
{
|
||||
gameToken: fixture.game.session.gameToken,
|
||||
profile: fixture.game.session.profile,
|
||||
}
|
||||
);
|
||||
await page.route('**/che/api/trpc/**', async (route) => {
|
||||
await fulfillOperations(route, (operation) => {
|
||||
if (operation === 'lobby.info') {
|
||||
return { ...fixture.game.lobby, myGeneral: fixture.game.session.general };
|
||||
}
|
||||
if (operation === 'join.getConfig') return {};
|
||||
if (operation === 'troop.getList') {
|
||||
return {
|
||||
nation: { id: 1, name: '촉' },
|
||||
me: fixture.game.session.general,
|
||||
permission: 0,
|
||||
troops: [],
|
||||
};
|
||||
}
|
||||
throw new Error(`Unhandled authenticated game fixture operation: ${operation}`);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
test.describe('gateway legacy parity', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await installGatewayFixture(page);
|
||||
});
|
||||
|
||||
for (const viewport of [
|
||||
{ name: 'desktop', width: 1365, height: 768 },
|
||||
{ name: 'mobile', width: 390, height: 844 },
|
||||
]) {
|
||||
test(`matches the ref login and status geometry on ${viewport.name}`, async ({ page }) => {
|
||||
await page.setViewportSize(viewport);
|
||||
const mapImage = page.waitForResponse((response) =>
|
||||
response.url().endsWith('/image/game/map/che/bg_fall.jpg')
|
||||
);
|
||||
await page.goto('http://127.0.0.1:15100/gateway/');
|
||||
expect((await mapImage).ok()).toBe(true);
|
||||
await expect(page.locator('#login_card')).toBeVisible();
|
||||
await expect(page.locator('.map-preview-body')).toBeVisible();
|
||||
await expect(page.getByText('지도 이미지 및 현황 데이터 영역')).toHaveCount(0);
|
||||
|
||||
const geometry = await page.evaluate(() => {
|
||||
const rect = (selector: string) => {
|
||||
const box = document.querySelector<HTMLElement>(selector)!.getBoundingClientRect();
|
||||
return { x: box.x, y: box.y, width: box.width, height: box.height };
|
||||
};
|
||||
const titleStyle = getComputedStyle(document.querySelector<HTMLElement>('.gateway-home h2')!);
|
||||
const mapStyle = getComputedStyle(document.querySelector<HTMLElement>('.map-preview-body')!);
|
||||
return {
|
||||
title: rect('.gateway-home h2'),
|
||||
login: rect('#login_card'),
|
||||
status: rect('#map-subframe'),
|
||||
titleStyle: {
|
||||
fontFamily: titleStyle.fontFamily,
|
||||
fontSize: titleStyle.fontSize,
|
||||
fontWeight: titleStyle.fontWeight,
|
||||
},
|
||||
mapBackgroundImage: mapStyle.backgroundImage,
|
||||
};
|
||||
});
|
||||
|
||||
expect(geometry.titleStyle.fontFamily).toContain('Pretendard');
|
||||
expect(geometry.titleStyle.fontSize).toBe('20px');
|
||||
expect(geometry.titleStyle.fontWeight).toBe('400');
|
||||
expect(geometry.login.width).toBeLessThanOrEqual(450);
|
||||
expect(geometry.status.width).toBeLessThanOrEqual(700);
|
||||
expect(geometry.mapBackgroundImage).toContain('bg_fall.jpg');
|
||||
if (viewport.name === 'desktop') {
|
||||
expect(geometry.login.width).toBeCloseTo(450, 0);
|
||||
expect(geometry.status.width).toBeCloseTo(700, 0);
|
||||
} else {
|
||||
expect(geometry.login.width).toBeCloseTo(374, 0);
|
||||
await expect(page.locator('.navbar-toggler')).toBeVisible();
|
||||
await page.locator('.navbar-toggler').click();
|
||||
await expect(page.locator('#gateway-navigation')).toHaveClass(/open/);
|
||||
}
|
||||
|
||||
const loginButton = page.locator('.login-button');
|
||||
const before = await loginButton.evaluate((element) => getComputedStyle(element).backgroundColor);
|
||||
await loginButton.hover();
|
||||
const hover = await loginButton.evaluate((element) => getComputedStyle(element).backgroundColor);
|
||||
await loginButton.focus();
|
||||
await expect(loginButton).toBeFocused();
|
||||
expect(hover).not.toBe(before);
|
||||
});
|
||||
}
|
||||
|
||||
test('submits the real login mutation and stores the session', async ({ page }) => {
|
||||
await page.goto('http://127.0.0.1:15100/gateway/');
|
||||
await page.locator('#username').fill('visual-user');
|
||||
await page.locator('#password').fill('visual-password');
|
||||
await page.locator('.login-button').click();
|
||||
await expect(page).toHaveURL(/\/gateway\/lobby$/);
|
||||
await expect
|
||||
.poll(() => page.evaluate(() => window.localStorage.getItem('sammo-session-token')))
|
||||
.toBe(fixture.gateway.sessionToken);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('hall of fame legacy parity', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await installHallFixture(page);
|
||||
});
|
||||
|
||||
for (const viewport of [
|
||||
{ name: 'desktop', width: 1365, height: 768, expectedWidth: 1000 },
|
||||
{ name: 'mobile', width: 390, height: 844, expectedWidth: 500 },
|
||||
]) {
|
||||
test(`matches the ref fixed grid on ${viewport.name}`, async ({ page }) => {
|
||||
await page.setViewportSize(viewport);
|
||||
await page.goto('http://127.0.0.1:15102/che/hall-of-fame');
|
||||
await expect(page.getByText('유비')).toBeVisible();
|
||||
await expect(page.locator('.rankView')).toHaveCount(2);
|
||||
|
||||
const geometry = await page.evaluate(() => {
|
||||
const container = document.querySelector<HTMLElement>('#container')!;
|
||||
const item = document.querySelector<HTMLElement>('.rankView li')!;
|
||||
const itemStyle = getComputedStyle(item);
|
||||
const titleStyle = getComputedStyle(document.querySelector<HTMLElement>('.rankType')!);
|
||||
const image = document.querySelector<HTMLImageElement>('.generalIcon')!;
|
||||
return {
|
||||
container: container.getBoundingClientRect().width,
|
||||
containerBackgroundImage: getComputedStyle(container).backgroundImage,
|
||||
item: {
|
||||
width: item.getBoundingClientRect().width,
|
||||
minHeight: itemStyle.minHeight,
|
||||
},
|
||||
title: {
|
||||
fontFamily: titleStyle.fontFamily,
|
||||
fontSize: titleStyle.fontSize,
|
||||
backgroundImage: titleStyle.backgroundImage,
|
||||
},
|
||||
image: {
|
||||
width: image.getBoundingClientRect().width,
|
||||
height: image.getBoundingClientRect().height,
|
||||
naturalWidth: image.naturalWidth,
|
||||
naturalHeight: image.naturalHeight,
|
||||
objectFit: getComputedStyle(image).objectFit,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
expect(geometry.container).toBe(viewport.expectedWidth);
|
||||
expect(geometry.containerBackgroundImage).toContain('back_walnut.jpg');
|
||||
expect(geometry.item.width).toBe(100);
|
||||
expect(geometry.title.fontFamily).toContain('Pretendard');
|
||||
expect(geometry.title.backgroundImage).toContain('back_green.jpg');
|
||||
expect(geometry.image).toMatchObject({ width: 64, height: 64, objectFit: 'cover' });
|
||||
expect(geometry.image.naturalWidth).toBeGreaterThan(0);
|
||||
|
||||
const close = page.getByRole('button', { name: '창 닫기' }).first();
|
||||
await close.hover();
|
||||
await close.focus();
|
||||
await expect(close).toBeFocused();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('game login delegates to the gateway like the ref entry point', async ({ page }) => {
|
||||
await page.goto('http://127.0.0.1:15102/che/login');
|
||||
await expect(page).toHaveURL('http://127.0.0.1:15102/gateway/');
|
||||
});
|
||||
|
||||
test('canonical logged-in fixture passes the game route guard', async ({ page }) => {
|
||||
await installAuthenticatedGameFixture(page);
|
||||
await page.goto('http://127.0.0.1:15102/che/troop');
|
||||
await expect(page).toHaveURL(/\/che\/troop$/);
|
||||
await expect(page.getByText('부대 편성')).toBeVisible();
|
||||
});
|
||||
Reference in New Issue
Block a user