fix: 복구 2배속 베팅 마감을 게임 시각으로 판정
This commit is contained in:
@@ -192,6 +192,14 @@ const persistScreenshot = async (page: Page, name: string, fallbackPath: string)
|
|||||||
const installFixture = async (
|
const installFixture = async (
|
||||||
page: Page,
|
page: Page,
|
||||||
options: {
|
options: {
|
||||||
|
clock?: {
|
||||||
|
serverTime: string;
|
||||||
|
serverWallTime: string;
|
||||||
|
clockMode: 'realtime' | 'manual';
|
||||||
|
clockRunning: boolean;
|
||||||
|
clockRecovery?: { startsAt: string; endsAt: string };
|
||||||
|
};
|
||||||
|
bettingCloseAt?: string;
|
||||||
applicationOpen?: boolean;
|
applicationOpen?: boolean;
|
||||||
tournamentType?: number;
|
tournamentType?: number;
|
||||||
tournamentStage?: number;
|
tournamentStage?: number;
|
||||||
@@ -224,7 +232,7 @@ const installFixture = async (
|
|||||||
const results = operationNames(route).map((operation) => {
|
const results = operationNames(route).map((operation) => {
|
||||||
options.onOperation?.(operation, route.request().headers());
|
options.onOperation?.(operation, route.request().headers());
|
||||||
if (operation === 'auth.status') return response({ ok: true });
|
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 === 'join.getConfig') return response({});
|
||||||
if (operation === 'general.me') return response({ general: { id: 1, name: names[0] } });
|
if (operation === 'general.me') return response({ general: { id: 1, name: names[0] } });
|
||||||
if (operation === 'tournament.getAdminStatus') return response({ ok: false });
|
if (operation === 'tournament.getAdminStatus') return response({ ok: false });
|
||||||
@@ -244,6 +252,7 @@ const installFixture = async (
|
|||||||
openMonth: 1,
|
openMonth: 1,
|
||||||
termSeconds: 60,
|
termSeconds: 60,
|
||||||
nextAt: '2026-08-02T00:00:00.000Z',
|
nextAt: '2026-08-02T00:00:00.000Z',
|
||||||
|
bettingCloseAt: options.bettingCloseAt,
|
||||||
winnerId: tournamentStage === 0 ? 1 : undefined,
|
winnerId: tournamentStage === 0 ? 1 : undefined,
|
||||||
},
|
},
|
||||||
participants:
|
participants:
|
||||||
@@ -1213,3 +1222,58 @@ for (const width of [1365, 801, 390, 320]) {
|
|||||||
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width);
|
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(() =>
|
const projection = computed(() =>
|
||||||
sample.value ? projectServerClock(sample.value, haltedAt.value ?? now.value) : null
|
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 accelerated = computed(() => projection.value?.rate === 2 && engineRunning.value !== false);
|
||||||
const label = computed(() => (mode.value === 'real' ? '실제 시간 기준' : '게임 시간 기준'));
|
const label = computed(() => (mode.value === 'real' ? '실제 시간 기준' : '게임 시간 기준'));
|
||||||
const time = computed(() => {
|
const time = computed(() => {
|
||||||
@@ -90,6 +92,7 @@ export const useClockDisplay = () => ({
|
|||||||
mode,
|
mode,
|
||||||
label,
|
label,
|
||||||
time,
|
time,
|
||||||
|
gameTime,
|
||||||
accelerated,
|
accelerated,
|
||||||
toggle,
|
toggle,
|
||||||
projectTime,
|
projectTime,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { usePageExit } from '../composables/usePageExit';
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { useClockDisplay } from '../composables/useClockDisplay';
|
import { useClockDisplay } from '../composables/useClockDisplay';
|
||||||
const { formatTime: formatGameTime } = useClockDisplay();
|
const { formatTime: formatGameTime, gameTime } = useClockDisplay();
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||||
import TournamentBracket from '../components/tournament/TournamentBracket.vue';
|
import TournamentBracket from '../components/tournament/TournamentBracket.vue';
|
||||||
@@ -62,7 +62,7 @@ const bettingOpen = computed(() => {
|
|||||||
const state = snapshot.value?.state;
|
const state = snapshot.value?.state;
|
||||||
if (!state || state.stage !== 6) return false;
|
if (!state || state.stage !== 6) return false;
|
||||||
if (!state.bettingCloseAt) return true;
|
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) => {
|
const placeBet = async (target: TournamentBracketSlot) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user