test(e2e): honor configured game base paths

This commit is contained in:
2026-07-31 18:08:04 +00:00
parent c3c1ddc0cf
commit 132baf21cc
13 changed files with 88 additions and 72 deletions
+5 -4
View File
@@ -2,6 +2,7 @@ import { expect, test, type Page, type Route } from '@playwright/test';
import { readFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { gameProfile, gameTrpcRoute } from './gameTestPaths.js';
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
const imageRoots = [
@@ -185,10 +186,10 @@ const uniqueDetail = {
};
const installFixture = async (page: Page, state: AuctionFixture) => {
await page.addInitScript(() => {
await page.addInitScript((profile) => {
window.localStorage.setItem('sammo-game-token', 'ga_auction_playwright');
window.localStorage.setItem('sammo-game-profile', 'che:default');
});
window.localStorage.setItem('sammo-game-profile', profile);
}, gameProfile);
for (const filename of ['back_walnut.jpg', 'back_green.jpg', 'back_blue.jpg']) {
await page.route(`**/image/game/${filename}`, async (route) => {
await route.fulfill({
@@ -198,7 +199,7 @@ const installFixture = async (page: Page, state: AuctionFixture) => {
});
});
}
await page.route('**/che/api/trpc/**', async (route) => {
await page.route(gameTrpcRoute, async (route) => {
const results = operationNames(route).map((operation) => {
if (operation === 'auth.status') return response({ ok: true });
if (operation === 'lobby.info') {
+7 -10
View File
@@ -2,6 +2,7 @@ import { expect, test, type Page, type Route } from '@playwright/test';
import { readFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { gameProfile, gameTrpcRoute } from './gameTestPaths.js';
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
const imageRoots = [
@@ -181,17 +182,15 @@ const installImages = async (page: Page) => {
const installApi = async (page: Page, fixture: Fixture) => {
await installImages(page);
await page.addInitScript(() => {
await page.addInitScript((profile) => {
window.localStorage.setItem('sammo-game-token', 'ga_battle_sim_playwright');
window.localStorage.setItem('sammo-game-profile', 'che:default');
});
await page.route('**/che/api/trpc/**', async (route) => {
window.localStorage.setItem('sammo-game-profile', profile);
}, gameProfile);
await page.route(gameTrpcRoute, async (route) => {
const operations = operationNames(route);
const rawRequestBody: unknown = route.request().postData() ? route.request().postDataJSON() : {};
const requestBody =
rawRequestBody && typeof rawRequestBody === 'object'
? (rawRequestBody as Record<string, unknown>)
: {};
rawRequestBody && typeof rawRequestBody === 'object' ? (rawRequestBody as Record<string, unknown>) : {};
const results = operations.map((operation, operationIndex) => {
fixture.requests.push(operation);
if (operation === 'auth.status') return response({ userId: 'battle-sim-user' });
@@ -223,9 +222,7 @@ const installApi = async (page: Page, fixture: Fixture) => {
input?: { json?: unknown };
})
: undefined;
fixture.simulationPayloads.push(
payload?.json ?? payload?.input?.json ?? rawPayload
);
fixture.simulationPayloads.push(payload?.json ?? payload?.input?.json ?? rawPayload);
if (fixture.failNextSimulation) {
fixture.failNextSimulation = false;
return errorResponse(operation, '시뮬레이터 입력 오류');
+8 -7
View File
@@ -2,6 +2,7 @@ import { expect, test, type Page, type Route } from '@playwright/test';
import { readFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { gamePath, gameProfile, gameTrpcRoute } from './gameTestPaths.js';
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
const imageRoots = [
@@ -142,11 +143,11 @@ const emptyMessages = {
const installApi = async (page: Page, state: BoardFixture) => {
await installImages(page);
await page.addInitScript(() => {
await page.addInitScript((profile) => {
window.localStorage.setItem('sammo-game-token', 'ga_board_playwright');
window.localStorage.setItem('sammo-game-profile', 'che:default');
});
await page.route('**/che/api/trpc/**', async (route) => {
window.localStorage.setItem('sammo-game-profile', profile);
}, gameProfile);
await page.route(gameTrpcRoute, async (route) => {
const operations = operationNames(route);
const results = operations.map((operation) => {
state.requests.push(operation);
@@ -444,7 +445,7 @@ test('denies direct secret-room rendering and disables its in-game menu for an o
state.requests.length = 0;
await page.goto('');
const nationMenu = page.locator('.main-nation-menu').first();
await expect(nationMenu.locator('[data-navigation-id="meeting"]')).toHaveAttribute('href', '/che/board');
await expect(nationMenu.locator('[data-navigation-id="meeting"]')).toHaveAttribute('href', gamePath('/board'));
await expect(nationMenu.locator('[data-navigation-id="secret-board"]')).toHaveAttribute('aria-disabled', 'true');
});
@@ -459,9 +460,9 @@ test('enables both in-game menu entries for a chief or delegated secret role', a
await installApi(page, state);
await page.goto('');
const nationMenu = page.locator('.main-nation-menu').first();
await expect(nationMenu.locator('[data-navigation-id="meeting"]')).toHaveAttribute('href', '/che/board');
await expect(nationMenu.locator('[data-navigation-id="meeting"]')).toHaveAttribute('href', gamePath('/board'));
await expect(nationMenu.locator('[data-navigation-id="secret-board"]')).toHaveAttribute(
'href',
'/che/board/secret'
gamePath('/board/secret')
);
});
@@ -1,4 +1,5 @@
import { expect, test, type Page, type Route } from '@playwright/test';
import { gameProfile, gameTrpcRoute } from './gameTestPaths.js';
const response = (data: unknown) => ({ result: { data } });
const errorResponse = (path: string, message: string) => ({
@@ -128,12 +129,12 @@ const chiefCenter = {
const install = async (page: Page, rejectGeneral = false) => {
const requests: unknown[] = [];
await page.addInitScript(() => {
await page.addInitScript((profile) => {
localStorage.setItem('sammo-game-token', 'ga_commands');
localStorage.setItem('sammo-game-profile', 'che:default');
});
localStorage.setItem('sammo-game-profile', profile);
}, gameProfile);
await page.route('**/image/**', (route) => route.fulfill({ status: 404, body: '' }));
await page.route('**/che/api/trpc/**', async (route) => {
await page.route(gameTrpcRoute, async (route) => {
const names = operations(route);
const body = route.request().postDataJSON();
const results = names.map((name) => {
+6
View File
@@ -0,0 +1,6 @@
const normalizedBasePath = (process.env.PLAYWRIGHT_GAME_BASE_PATH ?? 'che').replace(/^\/+|\/+$/g, '');
export const gameBasePath = `/${normalizedBasePath}`;
export const gameProfile = process.env.PLAYWRIGHT_GAME_PROFILE ?? 'che:default';
export const gameTrpcRoute = `**${gameBasePath}/api/trpc/**`;
export const gamePath = (suffix = ''): string => `${gameBasePath}${suffix.startsWith('/') ? suffix : `/${suffix}`}`;
+11 -7
View File
@@ -1,6 +1,7 @@
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { basename, resolve } from 'node:path';
import { expect, test, type Page, type Route } from '@playwright/test';
import { gameBasePath, gameProfile, gameTrpcRoute } from './gameTestPaths.js';
const response = (data: unknown) => ({ result: { data } });
const parityArtifactDir = process.env.MENU_PARITY_ARTIFACT_DIR;
@@ -133,12 +134,15 @@ const battleCenter = (state: FixtureState) => ({
});
const install = async (page: Page, state: FixtureState) => {
await page.addInitScript(() => {
if (location.pathname.startsWith('/che/')) {
localStorage.setItem('sammo-game-token', 'ga_menu-token');
localStorage.setItem('sammo-game-profile', 'che:default');
}
});
await page.addInitScript(
({ basePath, profile }) => {
if (location.pathname.startsWith(`${basePath}/`)) {
localStorage.setItem('sammo-game-token', 'ga_menu-token');
localStorage.setItem('sammo-game-profile', profile);
}
},
{ basePath: gameBasePath, profile: gameProfile }
);
await page.route('**/image/game/**', async (route) => {
const filename = basename(new URL(route.request().url()).pathname);
if (legacyImageRoot && ['back_walnut.jpg', 'back_green.jpg', 'back_blue.jpg'].includes(filename)) {
@@ -151,7 +155,7 @@ const install = async (page: Page, state: FixtureState) => {
}
await route.fulfill({ status: 200, contentType: 'image/jpeg', body: Buffer.from('') });
});
await page.route('**/che/api/trpc/**', async (route) => {
await page.route(gameTrpcRoute, async (route) => {
const operations = operationNames(route);
const rawRequestBody: unknown = route.request().postData() ? route.request().postDataJSON() : {};
const requestBody =
@@ -1,4 +1,5 @@
import { expect, test, type Page, type Route } from '@playwright/test';
import { gameProfile, gameTrpcRoute } from './gameTestPaths.js';
const response = (data: unknown) => ({ result: { data } });
const operations = (route: Route) =>
@@ -28,11 +29,11 @@ const general = {
permission: 'normal',
};
const install = async (page: Page, secretAllowed = true) => {
await page.addInitScript(() => {
await page.addInitScript((profile) => {
localStorage.setItem('sammo-game-token', 'ga_general');
localStorage.setItem('sammo-game-profile', 'che:default');
});
await page.route('**/che/api/trpc/**', async (route) => {
localStorage.setItem('sammo-game-profile', profile);
}, gameProfile);
await page.route(gameTrpcRoute, async (route) => {
const results = operations(route).map((operation) => {
if (operation === 'auth.status') return response({ ok: true });
if (operation === 'lobby.info') return response({ myGeneral: { id: 1, name: '테스트장수' } });
@@ -143,7 +144,7 @@ test('secret office renders summary, turns, and the forbidden error flow', async
await expect(page.locator('#secret-general-list')).toContainText('1 : 징병');
expect(await page.locator('.secret-page').evaluate((el) => el.getBoundingClientRect().width)).toBe(1000);
await page.unroute('**/che/api/trpc/**');
await page.unroute(gameTrpcRoute);
await install(page, false);
await page.goto('nation/secret');
await expect(page.getByRole('alert')).toContainText('권한이 부족합니다.');
+5 -4
View File
@@ -2,6 +2,7 @@ import { expect, test, type Page, type Route } from '@playwright/test';
import { mkdir, readFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { gameProfile, gameTrpcRoute } from './gameTestPaths.js';
type Role = 'leader' | 'head' | 'member';
type FixtureState = {
@@ -186,10 +187,10 @@ const financeFixture = (state: FixtureState) => ({
});
const installFixture = async (page: Page, state: FixtureState) => {
await page.addInitScript(() => {
await page.addInitScript((profile) => {
localStorage.setItem('sammo-game-token', 'ga_office_playwright');
localStorage.setItem('sammo-game-profile', 'che:default');
});
localStorage.setItem('sammo-game-profile', profile);
}, gameProfile);
for (const filename of ['back_walnut.jpg', 'back_green.jpg', 'back_blue.jpg']) {
await page.route(`**/image/game/${filename}`, async (route) =>
route.fulfill({
@@ -206,7 +207,7 @@ const installFixture = async (page: Page, state: FixtureState) => {
body: await referenceAsset('icons/default.jpg'),
})
);
await page.route('**/che/api/trpc/**', async (route) => {
await page.route(gameTrpcRoute, async (route) => {
const operations = operationName(route).split(',');
const rawRequestBody: unknown = route.request().postData() ? route.request().postDataJSON() : {};
const requestBody =
+5 -4
View File
@@ -2,6 +2,7 @@ import { expect, test, type Page, type Route } from '@playwright/test';
import { mkdir, readFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { gameProfile, gameTrpcRoute } from './gameTestPaths.js';
type FixtureState = {
permissionLevel: number;
@@ -138,10 +139,10 @@ const policyFixture = (state: FixtureState) => ({
});
const installFixture = async (page: Page, state: FixtureState) => {
await page.addInitScript(() => {
await page.addInitScript((profile) => {
localStorage.setItem('sammo-game-token', 'ga_npc_policy_playwright');
localStorage.setItem('sammo-game-profile', 'che:default');
});
localStorage.setItem('sammo-game-profile', profile);
}, gameProfile);
for (const filename of ['back_walnut.jpg', 'back_green.jpg', 'back_blue.jpg']) {
await page.route(`**/image/game/${filename}`, async (route) =>
route.fulfill({
@@ -151,7 +152,7 @@ const installFixture = async (page: Page, state: FixtureState) => {
})
);
}
await page.route('**/che/api/trpc/**', async (route) => {
await page.route(gameTrpcRoute, async (route) => {
const operations = operationName(route).split(',');
const results = operations.map((operation) => {
if (operation === 'auth.status') return response({ ok: true });
+5 -4
View File
@@ -1,7 +1,8 @@
import { expect, test, type Page, type Route } from '@playwright/test';
import { gameBasePath, gameProfile } from './gameTestPaths.js';
const response = (data: unknown) => ({ result: { data } });
const basePath = `/${(process.env.PLAYWRIGHT_GAME_BASE_PATH ?? 'che').replace(/^\/+|\/+$/g, '')}`;
const basePath = gameBasePath;
const operationNames = (route: Route) =>
decodeURIComponent(new URL(route.request().url()).pathname.split('/trpc/')[1] ?? '').split(',');
@@ -94,10 +95,10 @@ const findInput = (value: unknown): Record<string, unknown> => {
};
const installFixture = async (page: Page, state: FixtureState): Promise<void> => {
await page.addInitScript(() => {
await page.addInitScript((profile) => {
localStorage.setItem('sammo-game-token', 'ga_npc_possession');
localStorage.setItem('sammo-game-profile', 'che:default');
});
localStorage.setItem('sammo-game-profile', profile);
}, gameProfile);
await page.route('**/image/**', async (route) => {
await route.fulfill({
status: 200,
+6 -5
View File
@@ -2,17 +2,18 @@ import { mkdir, writeFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { expect, test, type Page, type Route } from '@playwright/test';
import { gamePath, gameProfile, gameTrpcRoute } from './gameTestPaths.js';
const response = (data: unknown) => ({ result: { data } });
const operationNames = (route: Route) =>
decodeURIComponent(new URL(route.request().url()).pathname.split('/trpc/')[1] ?? '').split(',');
const installArchive = async (page: Page) => {
await page.addInitScript(() => {
await page.addInitScript((profile) => {
localStorage.setItem('sammo-game-token', 'ga_archive');
localStorage.setItem('sammo-game-profile', 'che:default');
});
await page.route('**/che/api/trpc/**', async (route) => {
localStorage.setItem('sammo-game-profile', profile);
}, gameProfile);
await page.route(gameTrpcRoute, async (route) => {
const results = operationNames(route).map((operation) => {
if (operation === 'auth.status') return response({ ok: true });
if (operation === 'lobby.info') return response({ myGeneral: null });
@@ -77,7 +78,7 @@ test('past plays is available without a current general and preserves desktop in
await expect(page.getByRole('heading', { name: '내 지난 플레이 보기' })).toBeVisible();
await expect(page.getByText('천하쟁패 · 51기')).toBeVisible();
await expect(page.locator('.general-name')).toHaveText('관우');
await expect(page.getByRole('link', { name: '이 기수 국가 정보' })).toHaveAttribute('href', '/che/dynasty/7');
await expect(page.getByRole('link', { name: '이 기수 국가 정보' })).toHaveAttribute('href', gamePath('/dynasty/7'));
const historyToggle = page.locator('.history-toggle');
await expect(historyToggle).toHaveText('보기 (2)');
await historyToggle.click();
+14 -14
View File
@@ -1,4 +1,5 @@
import { expect, test, type Page, type Route } from '@playwright/test';
import { gameBasePath, gameProfile, gameTrpcRoute } from './gameTestPaths.js';
const response = (data: unknown) => ({ result: { data } });
@@ -24,22 +25,23 @@ const publicResponse = (operation: string): unknown => {
};
const seedGameStorage = async (page: Page, gameToken: string): Promise<void> => {
await page.addInitScript((token) => {
window.localStorage.setItem('sammo-game-token', token);
window.localStorage.setItem('sammo-game-profile', 'che:default');
}, gameToken);
await page.addInitScript(
({ token, profile }) => {
window.localStorage.setItem('sammo-game-token', token);
window.localStorage.setItem('sammo-game-profile', profile);
},
{ token: gameToken, profile: gameProfile }
);
};
test('removes an invalid ga_ token and redirects an authenticated route to public', async ({
page,
}) => {
test('removes an invalid ga_ token and redirects an authenticated route to public', async ({ page }) => {
await seedGameStorage(page, 'ga_invalid');
let gatewayRequests = 0;
await page.route('http://127.0.0.1:15120/api/trpc/**', async (route) => {
gatewayRequests += 1;
await route.abort();
});
await page.route('**/che/api/trpc/**', async (route) => {
await page.route(gameTrpcRoute, async (route) => {
const operations = operationNames(route);
if (operations.includes('auth.status')) {
expect(route.request().headers().authorization).toBe('Bearer ga_invalid');
@@ -58,7 +60,7 @@ test('removes an invalid ga_ token and redirects an authenticated route to publi
});
await page.goto('select-general');
await expect(page).toHaveURL(/\/che\/public$/);
await expect(page).toHaveURL(new RegExp(`${gameBasePath}/public$`));
await expect(page.getByRole('heading', { name: '공개 동향' })).toBeVisible();
expect(await page.evaluate(() => window.localStorage.getItem('sammo-game-token'))).toBeNull();
expect(gatewayRequests).toBe(0);
@@ -74,7 +76,7 @@ test('keeps a valid ga_ token when only lobby.info is unavailable', async ({ pag
gatewayRequests += 1;
await route.abort();
});
await page.route('**/che/api/trpc/**', async (route) => {
await page.route(gameTrpcRoute, async (route) => {
const operations = operationNames(route);
if (operations.includes('auth.status')) {
statusRequests += 1;
@@ -103,11 +105,9 @@ test('keeps a valid ga_ token when only lobby.info is unavailable', async ({ pag
});
await page.goto('select-general');
await expect(page).toHaveURL(/\/che\/select-general$/);
await expect(page).toHaveURL(new RegExp(`${gameBasePath}/select-general$`));
await expect(page.locator('.page-title')).toContainText('장 수 선 택');
expect(await page.evaluate(() => window.localStorage.getItem('sammo-game-token'))).toBe(
'ga_valid'
);
expect(await page.evaluate(() => window.localStorage.getItem('sammo-game-token'))).toBe('ga_valid');
expect(statusRequests).toBe(1);
expect(lobbyRequests).toBe(1);
expect(gatewayRequests).toBe(0);
+5 -4
View File
@@ -2,6 +2,7 @@ import { expect, test, type Page, type Route } from '@playwright/test';
import { readFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { gameProfile, gameTrpcRoute } from './gameTestPaths.js';
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
const imageRoots = [
@@ -119,10 +120,10 @@ const gotoTroop = async (page: Page) => {
};
const installApiFixture = async (page: Page, state: FixtureState) => {
await page.addInitScript(() => {
await page.addInitScript((profile) => {
window.localStorage.setItem('sammo-game-token', 'ga_playwright');
window.localStorage.setItem('sammo-game-profile', 'che:default');
});
window.localStorage.setItem('sammo-game-profile', profile);
}, gameProfile);
for (const filename of ['back_walnut.jpg', 'back_green.jpg']) {
await page.route(`**/image/game/${filename}`, async (route) => {
await route.fulfill({
@@ -142,7 +143,7 @@ const installApiFixture = async (page: Page, state: FixtureState) => {
),
});
});
await page.route('**/che/api/trpc/**', async (route) => {
await page.route(gameTrpcRoute, async (route) => {
const operations = operationName(route).split(',');
const results = operations.map((operation) => {
if (operation === 'auth.status') return response({ ok: true });