fix: align legacy general access call boundaries

This commit is contained in:
2026-07-31 07:33:26 +00:00
parent 821da19731
commit 98a3cf514f
34 changed files with 909 additions and 177 deletions
@@ -66,13 +66,35 @@ const bettingSummary = {
myAmount: 50,
};
type FixtureState = {
betCalls: number;
joinCalls: number;
snapshotQueries: number;
summaryQueries: number;
rankingQueries: number;
accessCalls: number;
stage: number;
myGeneralId: number;
};
const fixtureByPage = new WeakMap<Page, FixtureState>();
const installFixture = async (page: Page) => {
const state: FixtureState = {
betCalls: 0,
joinCalls: 0,
snapshotQueries: 0,
summaryQueries: 0,
rankingQueries: 0,
accessCalls: 0,
stage: snapshot.state.stage,
myGeneralId: 64,
};
fixtureByPage.set(page, state);
await page.addInitScript(() => {
window.localStorage.setItem('sammo-game-token', 'ga_tournament_visual');
window.localStorage.setItem('sammo-game-profile', 'che:default');
});
let betCalls = 0;
let joinCalls = 0;
await page.route('**/image/game/**', (route) =>
route.fulfill({
status: 200,
@@ -82,16 +104,32 @@ const installFixture = async (page: Page) => {
);
await page.route('**/che/api/trpc/**', async (route) => {
const results = operationNames(route).map((operation) => {
if (operation === 'auth.status') {
return response({ userId: 'tournament-user' });
}
if (operation === 'lobby.info') {
return response({ myGeneral: { id: 64, name: '내장수' } });
return response({ myGeneral: { id: state.myGeneralId, name: '내장수' } });
}
if (operation === 'join.getConfig') return response({});
if (operation === 'general.me') {
return response({ general: { id: 64, name: '내장수' }, nation: null, city: null });
return response({ general: { id: state.myGeneralId, name: '내장수' }, nation: null, city: null });
}
if (operation === 'tournament.getSnapshot') {
state.snapshotQueries += 1;
return response({
...snapshot,
state: {
...snapshot.state,
stage: state.stage,
},
});
}
if (operation === 'tournament.getBettingSummary') {
state.summaryQueries += 1;
return response(bettingSummary);
}
if (operation === 'tournament.getSnapshot') return response(snapshot);
if (operation === 'tournament.getBettingSummary') return response(bettingSummary);
if (operation === 'tournament.getRankings') {
state.rankingQueries += 1;
return response(
[
['tt', '전 력 전', '종합'],
@@ -123,17 +161,21 @@ const installFixture = async (page: Page) => {
if (operation === 'tournament.getAdminStatus')
return errorResponse(operation, 'Admin permission is required.');
if (operation === 'tournament.join') {
joinCalls += 1;
return joinCalls === 1
state.joinCalls += 1;
return state.joinCalls === 1
? errorResponse(operation, '금이 부족합니다.')
: response({ ok: true, count: 64 });
}
if (operation === 'tournament.placeBet') {
betCalls += 1;
return betCalls === 1
state.betCalls += 1;
return state.betCalls === 1
? errorResponse(operation, '500금까지만 베팅 가능합니다.')
: response({ ok: true });
}
if (operation === 'public.recordAccess') {
state.accessCalls += 1;
return response({ recorded: true });
}
return errorResponse(operation, `Unhandled fixture operation: ${operation}`);
});
await route.fulfill({
@@ -204,11 +246,33 @@ test('tournament keeps the fixed legacy canvas at a 1024px viewport', async ({ p
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeGreaterThanOrEqual(2000);
});
test('tournament reloads its snapshot after failed and successful joins without route access', async ({ page }) => {
const state = fixtureByPage.get(page)!;
state.stage = 1;
state.myGeneralId = 999;
await page.goto(`${gameUrl}/che/tournament`);
const join = page.getByRole('button', { name: '참가', exact: true });
await expect(join).toBeVisible();
await expect.poll(() => state.snapshotQueries).toBe(1);
await join.click();
await expect(page.getByRole('status')).toHaveText('금이 부족합니다.');
await expect.poll(() => state.snapshotQueries).toBe(2);
await join.click();
await expect(page.getByRole('status')).toHaveText('참가 신청이 반영되었습니다.');
await expect.poll(() => state.snapshotQueries).toBe(3);
expect(state.summaryQueries).toBe(3);
expect(state.accessCalls).toBe(0);
});
test('betting keeps the 1120px and 16 by 70px layout and retains a failed selection', async ({ page }) => {
const state = fixtureByPage.get(page)!;
await page.setViewportSize({ width: 1280, height: 900 });
await page.goto(`${gameUrl}/che/betting`);
await expect(page.getByText('베 팅 장')).toBeVisible();
await expect(page.locator('.names span')).toHaveCount(16);
await expect.poll(() => state.snapshotQueries).toBe(1);
const geometry = await page.locator('#tournament-betting-container').evaluate((container) => {
const rect = container.getBoundingClientRect();
@@ -251,7 +315,12 @@ test('betting keeps the 1120px and 16 by 70px layout and retains a failed select
await bet.click();
await expect(page.getByRole('status')).toHaveText('500금까지만 베팅 가능합니다.');
await expect(select).toHaveValue('500');
await expect.poll(() => state.snapshotQueries).toBe(2);
await bet.click();
await expect(page.getByRole('status')).toHaveText('베팅이 등록되었습니다.');
await expect.poll(() => state.snapshotQueries).toBe(3);
expect(state.summaryQueries).toBe(3);
expect(state.rankingQueries).toBe(3);
expect(state.accessCalls).toBe(0);
});
@@ -16,7 +16,9 @@ const artifactRoot = process.env.FRONTEND_PARITY_ARTIFACT_DIR;
const passwordPublicKey = generateKeyPairSync('rsa', {
modulusLength: 2048,
publicExponent: 0x10001,
}).publicKey.export({ format: 'pem', type: 'spki' }).toString();
})
.publicKey.export({ format: 'pem', type: 'spki' })
.toString();
const response = (data: unknown) => ({ result: { data } });
@@ -154,9 +156,7 @@ const installGatewayFixture = async (page: Page): Promise<void> => {
return {
available: true,
normalizedValue:
fieldInput?.field === 'username'
? fieldInput.value?.toLowerCase()
: fieldInput?.value,
fieldInput?.field === 'username' ? fieldInput.value?.toLowerCase() : fieldInput?.value,
message: '사용할 수 있습니다.',
};
}
@@ -216,15 +216,31 @@ const installGatewayFixture = async (page: Page): Promise<void> => {
});
};
const installHallFixture = async (page: Page): Promise<void> => {
type HallFixtureCalls = {
options: number;
hallInputs: unknown[];
};
const installHallFixture = async (page: Page): Promise<HallFixtureCalls> => {
const calls: HallFixtureCalls = {
options: 0,
hallInputs: [],
};
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;
await fulfillOperations(route, (operation, input) => {
if (operation === 'ranking.getHallOfFameOptions') {
calls.options += 1;
return fixture.game.hallOptions;
}
if (operation === 'ranking.getHallOfFame') {
calls.hallInputs.push(input);
return fixture.game.hall;
}
throw new Error(`Unhandled hall fixture operation: ${operation}`);
});
});
return calls;
};
const installAuthenticatedGameFixture = async (page: Page): Promise<void> => {
@@ -730,8 +746,10 @@ test.describe('best general legacy parity', () => {
});
test.describe('hall of fame legacy parity', () => {
let hallCalls: HallFixtureCalls;
test.beforeEach(async ({ page }) => {
await installHallFixture(page);
hallCalls = await installHallFixture(page);
});
for (const viewport of [
@@ -742,6 +760,9 @@ test.describe('hall of fame legacy parity', () => {
await page.setViewportSize(viewport);
await page.goto('http://127.0.0.1:15102/che/hall-of-fame');
await expect(page.getByText('유비')).toBeVisible();
await expect.poll(() => hallCalls.options).toBe(1);
await expect.poll(() => hallCalls.hallInputs.length).toBe(1);
expect(hallCalls.hallInputs[0]).toEqual({ season: 1 });
await expect(page.locator('.rankView')).toHaveCount(2);
if (artifactRoot) {
await page.screenshot({
@@ -807,14 +828,19 @@ test.describe('hall of fame legacy parity', () => {
await expect(scenario).toBeFocused();
await scenario.selectOption('scenario:1:22');
await expect(scenario).toHaveValue('scenario:1:22');
await expect.poll(() => hallCalls.hallInputs.length).toBe(2);
expect(hallCalls.hallInputs[1]).toEqual({ season: 1, scenario: 22 });
expect(hallCalls.options).toBe(1);
});
}
test('keeps the selected scenario after a hall API error', async ({ page }) => {
await page.goto('http://127.0.0.1:15102/che/hall-of-fame');
await expect(page.getByText('유비')).toBeVisible();
let failedHallRequests = 0;
await page.route('**/che/api/trpc/**', async (route) => {
if (operationNames(route).includes('ranking.getHallOfFame')) {
failedHallRequests += 1;
await route.fulfill({
status: 500,
contentType: 'application/json',
@@ -829,6 +855,9 @@ test.describe('hall of fame legacy parity', () => {
await expect(page.getByRole('alert')).toBeVisible();
await expect(scenario).toHaveValue('scenario:1:22');
await expect(page.getByText('유비')).toBeVisible();
await expect.poll(() => failedHallRequests).toBe(1);
await page.waitForTimeout(250);
expect(failedHallRequests).toBe(1);
});
});