fix: 복구 2배속 베팅 마감을 게임 시각으로 판정

This commit is contained in:
2026-09-16 01:05:40 +00:00
parent 47ee6491da
commit a51574d20c
3 changed files with 70 additions and 3 deletions
@@ -192,6 +192,14 @@ const persistScreenshot = async (page: Page, name: string, fallbackPath: string)
const installFixture = async (
page: Page,
options: {
clock?: {
serverTime: string;
serverWallTime: string;
clockMode: 'realtime' | 'manual';
clockRunning: boolean;
clockRecovery?: { startsAt: string; endsAt: string };
};
bettingCloseAt?: string;
applicationOpen?: boolean;
tournamentType?: number;
tournamentStage?: number;
@@ -224,7 +232,7 @@ const installFixture = async (
const results = operationNames(route).map((operation) => {
options.onOperation?.(operation, route.request().headers());
if (operation === 'auth.status') return response({ ok: true });
if (operation === 'lobby.info') return response({ myGeneral: { id: 1, name: names[0] } });
if (operation === 'lobby.info') return response({ myGeneral: { id: 1, name: names[0] }, ...options.clock });
if (operation === 'join.getConfig') return response({});
if (operation === 'general.me') return response({ general: { id: 1, name: names[0] } });
if (operation === 'tournament.getAdminStatus') return response({ ok: false });
@@ -244,6 +252,7 @@ const installFixture = async (
openMonth: 1,
termSeconds: 60,
nextAt: '2026-08-02T00:00:00.000Z',
bettingCloseAt: options.bettingCloseAt,
winnerId: tournamentStage === 0 ? 1 : undefined,
},
participants:
@@ -1213,3 +1222,58 @@ for (const width of [1365, 801, 390, 320]) {
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width);
});
}
for (const width of [1365, 390]) {
for (const displayMode of ['game', 'real']) {
test(`recovery betting uses game deadline at ${width}px in ${displayMode} display`, async ({
page,
}, testInfo) => {
await page.setViewportSize({ width, height: 900 });
const wallNow = new Date('2026-09-16T12:00:00Z');
await page.clock.install({ time: wallNow });
await page.clock.setFixedTime(wallNow);
await page.addInitScript(
({ profile, displayMode }) => {
localStorage.setItem(`sammo-clock-display:${profile}:/${profile.split(':')[0]}/`, displayMode);
},
{ profile: gameProfile, displayMode }
);
const { placedBets } = await installFixture(page, {
tournamentStage: 6,
bettingCloseAt: '2026-09-16T11:50:20Z',
clock: {
serverTime: '2026-09-16T11:50:00Z',
serverWallTime: wallNow.toISOString(),
clockMode: 'realtime',
clockRunning: true,
clockRecovery: { startsAt: wallNow.toISOString(), endsAt: '2026-09-16T12:10:00Z' },
},
});
await page.goto('betting');
const button = page.getByRole('button', { name: '관우에게 베팅하기', exact: true });
await expect(button).toBeVisible();
await button.click();
await expect(page.getByRole('status').filter({ hasText: '10금 베팅 완료' })).toBeVisible();
expect(placedBets).toEqual([{ targetId: 1, amount: 10 }]);
await page.clock.pauseAt(wallNow);
await page.clock.setSystemTime(wallNow);
await page.clock.runFor(9_750);
await expect(button).toBeVisible();
await page.evaluate(() => document.fonts.ready);
const geometry = await button.evaluate((el) => ({
rect: el.getBoundingClientRect().toJSON(),
fontSize: getComputedStyle(el).fontSize,
disabled: (el as HTMLButtonElement).disabled,
html: document.querySelector('#tournament-betting-container')?.outerHTML,
}));
expect(geometry.disabled).toBe(false);
expect(geometry.rect.width).toBeGreaterThan(0);
await writeFile(testInfo.outputPath('recovery-betting-geometry.json'), JSON.stringify(geometry));
await page.screenshot({ path: testInfo.outputPath('recovery-betting-open.png'), fullPage: true });
// 250ms 표시 갱신 주기의 위상과 무관하게 마감 직후를 확인한다.
await page.clock.runFor(500);
await expect(button).toHaveCount(0);
await page.screenshot({ path: testInfo.outputPath('recovery-betting-closed.png'), fullPage: true });
});
}
}
@@ -63,6 +63,8 @@ export const clockSampleIsStale = (): boolean =>
const projection = computed(() =>
sample.value ? projectServerClock(sample.value, haltedAt.value ?? now.value) : null
);
// 마감 판정은 표시 모드와 무관하게 서버가 투영한 GAME 시각을 사용한다.
const gameTime = computed(() => projection.value?.time ?? null);
const accelerated = computed(() => projection.value?.rate === 2 && engineRunning.value !== false);
const label = computed(() => (mode.value === 'real' ? '실제 시간 기준' : '게임 시간 기준'));
const time = computed(() => {
@@ -90,6 +92,7 @@ export const useClockDisplay = () => ({
mode,
label,
time,
gameTime,
accelerated,
toggle,
projectTime,
+2 -2
View File
@@ -2,7 +2,7 @@
import { usePageExit } from '../composables/usePageExit';
import { useClockDisplay } from '../composables/useClockDisplay';
const { formatTime: formatGameTime } = useClockDisplay();
const { formatTime: formatGameTime, gameTime } = useClockDisplay();
import { storeToRefs } from 'pinia';
import { computed, onMounted, onUnmounted, ref } from 'vue';
import TournamentBracket from '../components/tournament/TournamentBracket.vue';
@@ -62,7 +62,7 @@ const bettingOpen = computed(() => {
const state = snapshot.value?.state;
if (!state || state.stage !== 6) return false;
if (!state.bettingCloseAt) return true;
return new Date(state.bettingCloseAt).getTime() > Date.now();
return gameTime.value !== null && new Date(state.bettingCloseAt).getTime() > gameTime.value.getTime();
});
const placeBet = async (target: TournamentBracketSlot) => {