merge: 최신 main을 빈 특기 획득 나이 표시에 통합한다

This commit is contained in:
2026-08-22 05:05:53 +00:00
26 changed files with 1169 additions and 821 deletions
+74 -10
View File
@@ -22,6 +22,13 @@ const persistParityArtifact = async (page: Page, name: string, geometry: unknown
]);
};
const waitForVisualAssets = async (page: Page): Promise<void> => {
await page.waitForLoadState('networkidle');
await page.evaluate(async () => {
await document.fonts.ready;
});
};
const readGeneralPanelImages = async (panel: Locator) =>
panel.evaluate((element) =>
[...element.querySelectorAll<HTMLElement>('.general-image')].map((image) => {
@@ -1313,6 +1320,9 @@ test('내 정보&설정 keeps desktop density and becomes a 390px horizontal-ide
await expect(page.locator('#container')).not.toContainText('che_');
await expect(page.locator('.title-row')).toContainText('내 정 보');
await expect(page.locator('#set_my_setting')).toBeVisible();
await expect(page.getByRole('radiogroup', { name: '화면 폭 모드' })).toHaveCount(0);
await expect(page.getByRole('button', { name: '순서 바꾸기', exact: true })).toHaveCount(0);
await expect(page.locator('#custom_css')).toHaveCount(0);
await expect(page.locator('.general-column [role="progressbar"]')).toHaveCount(14);
await expect(page.locator('.general-column [aria-label*="1,275,975 (EX+)"]')).toHaveCount(5);
await expect.poll(() => state.generalMeQueries).toBeGreaterThan(0);
@@ -1333,7 +1343,6 @@ test('내 정보&설정 keeps desktop density and becomes a 390px horizontal-ide
const settings = element.querySelector<HTMLElement>('.settings-column')!.getBoundingClientRect();
const saveButton = element.querySelector<HTMLElement>('#set_my_setting')!;
const save = saveButton.getBoundingClientRect();
const customCss = element.querySelector<HTMLElement>('#custom_css')!.getBoundingClientRect();
const columns = getComputedStyle(element.querySelector('.top-grid')!).gridTemplateColumns;
return {
width: rect.width,
@@ -1345,8 +1354,6 @@ test('내 정보&설정 keeps desktop density and becomes a 390px horizontal-ide
saveWidth: save.width,
saveHeight: save.height,
saveBackground: getComputedStyle(saveButton).backgroundColor,
customCssWidth: customCss.width,
customCssHeight: customCss.height,
backgroundImage: getComputedStyle(element).backgroundImage,
sectionBackgroundImage: getComputedStyle(element.querySelector('.section-title')!).backgroundImage,
};
@@ -1360,8 +1367,6 @@ test('내 정보&설정 keeps desktop density and becomes a 390px horizontal-ide
expect(desktop.saveWidth).toBe(160);
expect(desktop.saveHeight).toBe(30);
expect(desktop.saveBackground).toBe('rgb(34, 85, 0)');
expect(desktop.customCssWidth).toBe(420);
expect(desktop.customCssHeight).toBe(150);
expect(desktop.backgroundImage).toContain('back_walnut.jpg');
expect(desktop.sectionBackgroundImage).toContain('back_green.jpg');
await expectLumenButtonStates(page, page.locator('#set_my_setting'), 'rgb(34, 85, 0)');
@@ -1626,11 +1631,68 @@ test('내 정보&설정의 지난 플레이는 기본 탐색과 분리되어 오
}
});
test('내 정보&설정에서 모바일 메인 패널을 드래그하거나 버튼으로 재정렬하고 기본 순서로 복원한다', async ({ page }) => {
test('화면 설정에서 화면 폭과 개인 CSS를 저장하고 게임 설정 API는 호출하지 않는다', async ({ page }, testInfo) => {
const state: FixtureState = { permission: 'head', myset: 3, settingMutations: [], accessPages: [] };
await install(page, state);
await page.setViewportSize({ width: 1000, height: 900 });
await page.goto('my-settings');
await waitForVisualAssets(page);
await expect(page.locator('.title-row')).toContainText('화 면 설 정');
await expect(
page.getByText('이 설정은 이 기기의 화면 표시만 바꾸며 게임 상태에는 영향을 주지 않습니다.')
).toHaveCount(0);
await expect(page.locator('#set_my_setting')).toHaveCount(0);
await expect(page.locator('#custom_css')).toBeVisible();
expect(state.settingMutations).toHaveLength(0);
const desktop = await page.locator('#interface-settings').evaluate((element) => {
const rect = element.getBoundingClientRect();
const css = element.querySelector<HTMLTextAreaElement>('#custom_css')!.getBoundingClientRect();
return {
width: rect.width,
columns: getComputedStyle(element.querySelector('.settings-grid')!).gridTemplateColumns,
cssWidth: css.width,
cssHeight: css.height,
backgroundImage: getComputedStyle(element).backgroundImage,
sectionBackgroundImage: getComputedStyle(element.querySelector('.section-title')!).backgroundImage,
};
});
expect(desktop.width).toBe(1000);
expect(desktop.columns.split(' ')).toHaveLength(2);
expect(desktop.cssWidth).toBe(420);
expect(desktop.cssHeight).toBe(150);
expect(desktop.backgroundImage).toContain('back_walnut.jpg');
expect(desktop.sectionBackgroundImage).toContain('back_green.jpg');
await page.screenshot({ path: testInfo.outputPath('interface-settings-desktop.png'), fullPage: true });
await page.getByRole('radio', { name: '500px' }).check();
await expect.poll(() => page.evaluate(() => localStorage.getItem('sam.screenMode'))).toBe('500px');
await expect(page.locator('meta[name="viewport"]')).toHaveAttribute('content', 'width=500');
const cssText = '#interface-settings { --ui-settings-e2e: 23px; }';
await page.getByLabel('개인용 CSS').fill(cssText);
await expect(page.locator('.custom-css span')).toHaveText('(저장 중)');
await expect.poll(() => page.evaluate(() => localStorage.getItem('sam_customCSS'))).toBe(cssText);
await expect.poll(() => page.locator('#sammo-custom-css').textContent()).toBe(cssText);
await page.reload();
await expect.poll(() => page.locator('#sammo-custom-css').textContent()).toBe(cssText);
expect(state.settingMutations).toHaveLength(0);
await page.getByLabel('개인용 CSS').fill('');
await expect.poll(() => page.evaluate(() => localStorage.getItem('sam_customCSS'))).toBe('');
await page.getByRole('radio', { name: '자동' }).check();
await persistParityArtifact(page, 'core-interface-settings-desktop', desktop);
});
test('화면 설정에서 모바일 메인 패널을 드래그하거나 버튼으로 재정렬하고 기본 순서로 복원한다', async ({
page,
}, testInfo) => {
const state: FixtureState = { permission: 'head', myset: 3, settingMutations: [], accessPages: [] };
await install(page, state);
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('my-page');
await page.goto('my-settings');
await waitForVisualAssets(page);
await page.getByRole('button', { name: '순서 바꾸기', exact: true }).click();
const dialog = page.getByRole('dialog', { name: '모바일 레이아웃 순서 바꾸기' });
@@ -1680,7 +1742,8 @@ test('내 정보&설정에서 모바일 메인 패널을 드래그하거나 버
expect(dialogGeometry.firstItem?.height).toBeGreaterThanOrEqual(44);
expect(dialogGeometry.moveButton?.width).toBeGreaterThanOrEqual(36);
expect(dialogGeometry.documentWidth).toBe(390);
await persistParityArtifact(page, 'core-my-page-mobile-layout-order-dialog', dialogGeometry);
await dialog.screenshot({ path: testInfo.outputPath('interface-settings-mobile-layout-dialog.png') });
await persistParityArtifact(page, 'core-interface-settings-mobile-layout-order-dialog', dialogGeometry);
await dialog.getByRole('button', { name: '적용', exact: true }).click();
await expect(dialog).toBeHidden();
@@ -1697,7 +1760,7 @@ test('내 정보&설정에서 모바일 메인 패널을 드래그하거나 버
.toEqual(defaultOrder);
});
test('실제 모바일 터치로 메인 패널 순서를 재정렬한다', async ({ browser }, testInfo) => {
test('화면 설정에서 실제 모바일 터치로 메인 패널 순서를 재정렬한다', async ({ browser }, testInfo) => {
const configuredBaseUrl = testInfo.project.use.baseURL;
if (typeof configuredBaseUrl !== 'string') {
throw new Error('Playwright baseURL is required for the mobile touch contract');
@@ -1715,7 +1778,8 @@ test('실제 모바일 터치로 메인 패널 순서를 재정렬한다', async
try {
const state: FixtureState = { permission: 'head', myset: 3, settingMutations: [], accessPages: [] };
await install(mobilePage, state);
await mobilePage.goto('my-page');
await mobilePage.goto('my-settings');
await waitForVisualAssets(mobilePage);
await mobilePage.getByRole('button', { name: '순서 바꾸기', exact: true }).click();
const dialog = mobilePage.getByRole('dialog', { name: '모바일 레이아웃 순서 바꾸기' });
+137 -12
View File
@@ -29,6 +29,8 @@ type NavigationFixture = {
permission: number;
nationLevel: number;
stage: number;
tournamentType?: 0 | 1 | 2 | 3;
tournamentWinnerId?: number;
npcMode: number;
generalMeCalls: number;
operations: string[];
@@ -758,7 +760,16 @@ const installFixture = async (page: Page, state: NavigationFixture) => {
canSecret: state.permission >= 2,
});
}
if (operation === 'tournament.getState') return response({ stage: state.stage });
if (operation === 'tournament.getState') {
if (state.stage === 0 && state.tournamentType === undefined && state.tournamentWinnerId === undefined) {
return response(null);
}
return response({
stage: state.stage,
type: state.tournamentType ?? 0,
winnerId: state.tournamentWinnerId,
});
}
return response({ ok: true });
});
operations.forEach((operation, index) => {
@@ -1260,7 +1271,27 @@ test('desktop menus preserve ref columns, prefix-safe routes, and controlled dro
await expect(global.locator('[data-navigation-id="board-community"]')).toHaveAttribute('href', '/xe/community');
await expect(global.locator('[data-navigation-id="official-chat"]')).toHaveAttribute('target', '_blank');
await expect(global.locator('[data-navigation-id="survey"]')).toHaveClass(/highlight/);
await expect(page.locator('.main-nation-menu [data-navigation-id="tournament"]')).toHaveClass(/highlight/);
const nationMenu = page.locator('.main-nation-menu:visible');
await expect(nationMenu.locator(':scope > *')).toHaveCount(20);
const tournamentMain = nationMenu.locator('[data-navigation-id="tournament"]');
const tournamentToggle = nationMenu.locator('[data-menu-id="tournament-betting"]');
await expect(tournamentMain).toHaveClass(/highlight/);
await expect(tournamentMain).toHaveAttribute('href', `${basePath}/tournament`);
await expect(tournamentToggle).toHaveAttribute('aria-expanded', 'false');
await expect(nationMenu.locator('[data-navigation-id="my-settings"]')).toHaveAttribute(
'href',
`${basePath}/my-settings`
);
await tournamentToggle.click();
await expect(tournamentToggle).toHaveAttribute('aria-expanded', 'true');
await expect(
nationMenu.locator('#nation-menu-tournament-betting [data-navigation-id="tournament-menu"]')
).toHaveText('토너먼트');
await expect(nationMenu.locator('#nation-menu-tournament-betting [data-navigation-id="betting"]')).toHaveAttribute(
'href',
`${basePath}/betting`
);
await page.keyboard.press('Escape');
const gameInfoButton = global.locator('[data-menu-id="game-info"]');
const bettingButton = global.locator('[data-navigation-id="nation-betting"]');
@@ -1417,6 +1448,95 @@ test('shows the persisted official game index beside the scenario title without
}
});
test('tournament split main action follows recruitment, betting, finals, and tournament type on desktop and mobile', async ({
page,
}, testInfo) => {
const state: NavigationFixture = {
officerLevel: 5,
permission: 2,
nationLevel: 3,
stage: 1,
tournamentType: 3,
npcMode: 1,
scenarioTitle: '토너먼트 동적 메뉴 검증 시나리오',
generalMeCalls: 0,
operations: [],
};
await installFixture(page, state);
const cases = [
{ stage: 1, type: 3 as const, label: '설 전', route: '/tournament' },
{ stage: 5, type: 2 as const, label: '일 기 토', route: '/tournament' },
{ stage: 6, type: 2 as const, label: '베 팅 장', route: '/betting' },
{ stage: 7, type: 2 as const, label: '일 기 토', route: '/tournament' },
{ stage: 10, type: 3 as const, label: '설 전', route: '/tournament' },
{ stage: 0, type: 3 as const, label: '설 전', route: '/tournament', winnerId: 17 },
];
for (const viewport of [
{ width: 1200, height: 900 },
{ width: 500, height: 900 },
]) {
await page.setViewportSize(viewport);
for (const lifecycle of cases) {
state.stage = lifecycle.stage;
state.tournamentType = lifecycle.type;
state.tournamentWinnerId = lifecycle.winnerId;
if (page.url() === 'about:blank') {
await waitForMain(page);
} else {
await page.reload();
await waitForMain(page);
}
const nationMenu = page.locator('.main-nation-menu:visible');
const main = nationMenu.locator('[data-navigation-id="tournament"]');
await expect(main).toHaveText(lifecycle.label);
await expect(main).toHaveAttribute('href', `${basePath}${lifecycle.route}`);
await expect(main).toHaveClass(/highlight/);
const toggle = nationMenu.locator('[data-menu-id="tournament-betting"]');
await toggle.click();
const tournamentItem = nationMenu.locator(
'#nation-menu-tournament-betting [data-navigation-id="tournament-menu"]'
);
const bettingItem = nationMenu.locator('#nation-menu-tournament-betting [data-navigation-id="betting"]');
if (lifecycle.stage === 6) {
await expect(tournamentItem).not.toHaveClass(/highlight/);
await expect(bettingItem).toHaveClass(/highlight/);
} else {
await expect(tournamentItem).toHaveClass(/highlight/);
await expect(bettingItem).not.toHaveClass(/highlight/);
}
await page.keyboard.press('Escape');
if (viewport.width === 500) {
const nationTrigger = page.locator('[data-bottom-menu="nation"]');
await nationTrigger.click();
const mobileTournament = page.locator('#mobile-nation-menu [data-navigation-id="tournament-menu"]');
const mobileBetting = page.locator('#mobile-nation-menu [data-navigation-id="betting"]');
if (lifecycle.stage === 6) {
await expect(mobileTournament).not.toHaveClass(/highlight/);
await expect(mobileBetting).toHaveClass(/highlight/);
} else {
await expect(mobileTournament).toHaveClass(/highlight/);
await expect(mobileBetting).not.toHaveClass(/highlight/);
}
await page.keyboard.press('Escape');
}
}
await page
.locator('.main-nation-menu:visible [data-menu-id="tournament-betting"]')
.locator('..')
.screenshot({
path: artifactRoot
? resolve(artifactRoot, `tournament-dynamic-main-${viewport.width}.png`)
: testInfo.outputPath(`tournament-dynamic-main-${viewport.width}.png`),
});
}
});
test('shows game index zero for a profile whose first game starts at zero', async ({ page }) => {
const state: NavigationFixture = {
officerLevel: 5,
@@ -1455,7 +1575,7 @@ test('nation split buttons keep square inner corners and a single divider in eve
officerLevel: 5,
permission: 2,
nationLevel: 3,
stage: 1,
stage: 6,
npcMode: 1,
scenarioTitle: '분할 버튼 이음새 검증 시나리오',
generalMeCalls: 0,
@@ -1515,14 +1635,21 @@ test('nation split buttons keep square inner corners and a single divider in eve
for (const width of [1200, 500]) {
await page.setViewportSize({ width, height: 900 });
await waitForMain(page);
const nationSplit = page.locator('.main-nation-menu:visible .nation-menu-split').first();
const pairs: Array<[string, Locator, Locator]> = [
[
'nation',
nationSplit.locator('[data-navigation-id="auction-resource"]'),
nationSplit.locator('[data-menu-id="auction"]'),
'tournament',
page.locator('.main-nation-menu:visible [data-navigation-id="tournament"]'),
page.locator('.main-nation-menu:visible [data-menu-id="tournament-betting"]'),
],
[
'auction',
page.locator('.main-nation-menu:visible [data-navigation-id="auction-resource"]'),
page.locator('.main-nation-menu:visible [data-menu-id="auction"]'),
],
];
await expect(page.locator('.main-nation-menu:visible [data-navigation-id="tournament"]')).toHaveClass(
/highlight/
);
for (const [label, main, toggle] of pairs) {
await expect(main).toBeVisible();
@@ -3959,9 +4086,7 @@ for (const viewport of [
await picker.getByLabel('장비 종류', { exact: true }).selectOption('weapon');
const equipment = picker.getByLabel('장비', { exact: true });
await equipment.selectOption('청룡언월도');
const optionLabelBeforeRefresh = await equipment
.locator('option[value="청룡언월도"]')
.textContent();
const optionLabelBeforeRefresh = await equipment.locator('option[value="청룡언월도"]').textContent();
await equipment.evaluate((element) => {
const select = element as HTMLSelectElement;
const valueDescriptor = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'value');
@@ -4091,8 +4216,8 @@ test('keeps an Android Chromium native command select untouched while a turn sig
await waitForMain(mobilePage);
await expect
.poll(() =>
mobilePage.evaluate(
() => (window as unknown as { __hasMainRealtime: () => boolean }).__hasMainRealtime()
mobilePage.evaluate(() =>
(window as unknown as { __hasMainRealtime: () => boolean }).__hasMainRealtime()
)
)
.toBe(true);
+149 -2
View File
@@ -86,8 +86,55 @@ const matches = [
defenderId: index * 8 + 5,
winnerId: index * 8 + 1,
})),
{ id: 15, stage: 10, roundIndex: 0, attackerId: 1, defenderId: 9, winnerId: 1 },
{
id: 15,
stage: 10,
roundIndex: 0,
attackerId: 1,
defenderId: 9,
winnerId: 1,
log: ['<S>●</> <Y>관우</> <C>(800)</> vs <C>(790)</> <Y>여포</>', '<S>●</> <Y>관우</> <S>우승</>!'],
},
];
const buildGroupFightMatches = (stage: 2 | 4) => {
const groupStart = stage === 2 ? 0 : 10;
return Array.from({ length: 8 }, (_, index) => ({
id: stage * 100 + groupStart + index + 1,
stage,
roundIndex: groupStart + index,
groupId: groupStart + index,
attackerId: index * 2 + 1,
defenderId: index * 2 + 2,
winnerId: index * 2 + 1,
log: [
`<S>●</> <Y>${names[index * 2]}</> <C>(800)</> vs <C>(790)</> <Y>${names[index * 2 + 1]}</>`,
'<S>●</> 01合 : <C>720</><span class="ev_highlight">(-080)</span> vs <span class="ev_highlight">(-090)</span><C>700</>',
`<S>●</> <Y>${names[index * 2]}</> <S>승리</>!`,
],
}));
};
const matchesForStage = (stage: number) => {
if (stage === 2 || stage === 3) {
return [...buildGroupFightMatches(2), ...matches];
}
if (stage === 4 || stage === 5) {
return [...buildGroupFightMatches(2), ...buildGroupFightMatches(4), ...matches];
}
if (stage === 7) {
return matches.map((match, index) =>
match.stage === 7 && index === 0
? {
...match,
log: [
'<S>●</> <Y>관우</> <C>(800)</> vs <C>(790)</> <Y>장료</>',
'<S>●</> <Y>관우</> <S>승리</>!',
],
}
: match
);
}
return matches;
};
const response = (data: unknown) => ({ result: { data } });
const asRecord = (value: unknown): Record<string, unknown> | null =>
@@ -205,7 +252,7 @@ const installFixture = async (
},
]
: participants,
matches,
matches: matchesForStage(tournamentStage),
betCount: 16,
});
}
@@ -441,6 +488,82 @@ test('final group section appears before the later knockout section', async ({ p
await persistScreenshot(page, 'tournament-final-stage-mobile', testInfo.outputPath('tournament-final-stage.webp'));
});
test('preliminary stage renders the latest fight log for all eight groups', async ({ page }, testInfo) => {
await page.setViewportSize({ width: 1365, height: 900 });
await installFixture(page, { tournamentStage: 2 });
await page.goto('tournament');
const region = page.getByRole('region', { name: '예선 조별 전투 로그' });
const logs = region.locator('.fight-log');
await expect(logs).toHaveCount(8);
await expect(logs).toHaveText([
/ .*.*.*/s,
/ .*.*.*/s,
/ /s,
/ /s,
/ /s,
/ /s,
/ /s,
/ /s,
]);
const geometry = await logs.evaluateAll((elements) =>
elements.map((element) => {
const bounds = element.getBoundingClientRect();
return { top: bounds.top, left: bounds.left, right: bounds.right, width: bounds.width };
})
);
expect(new Set(geometry.slice(0, 4).map((item) => item.top)).size).toBe(1);
expect(geometry[4]!.top).toBeGreaterThan(geometry[0]!.top);
expect(geometry.every((item) => item.left >= 0 && item.right <= 1365 && item.width > 0)).toBe(true);
await expect(logs.first().locator('p').first().locator('span').first()).toHaveCSS('color', 'rgb(135, 206, 235)');
await persistScreenshot(page, 'tournament-preliminary-fight-logs', testInfo.outputPath('preliminary-logs.webp'));
});
test('final group stage keeps all eight fight logs visible on mobile without overflow', async ({ page }, testInfo) => {
await page.setViewportSize({ width: 390, height: 844 });
await installFixture(page, { tournamentStage: 4 });
await page.goto('tournament');
const region = page.getByRole('region', { name: '본선 조별 전투 로그' });
const logs = region.locator('.fight-log');
await expect(logs).toHaveCount(8);
for (let index = 0; index < 8; index += 1) {
await expect(logs.nth(index)).toBeVisible();
}
const geometry = await logs.evaluateAll((elements) =>
elements.map((element) => {
const bounds = element.getBoundingClientRect();
return { top: bounds.top, left: bounds.left, right: bounds.right };
})
);
expect(geometry.every((item, index) => index === 0 || item.top > geometry[index - 1]!.top)).toBe(true);
expect(geometry.every((item) => item.left >= 0 && item.right <= 390)).toBe(true);
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390);
await persistScreenshot(page, 'tournament-final-fight-logs-mobile', testInfo.outputPath('final-logs-mobile.webp'));
});
test('knockout stage shows the latest completed match instead of the next empty match', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await installFixture(page, { tournamentStage: 7 });
await page.goto('tournament');
const region = page.getByRole('region', { name: '현재 토너먼트 전투 로그' });
await expect(region).toContainText('관우 vs 장료');
await expect(region).toContainText('관우 승리!');
await expect(region).not.toContainText('<S>');
await expect(region.locator('p').first().locator('span').first()).toHaveCSS('color', 'rgb(135, 206, 235)');
});
test('completed tournament retains the final fight log like Ref', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await installFixture(page);
await page.goto('tournament');
const region = page.getByRole('region', { name: '현재 토너먼트 전투 로그' });
await expect(region).toContainText('관우 vs 여포');
await expect(region).toContainText('관우 우승!');
});
test('mobile bracket exposes every round through tabs with standard horizontal identities', async ({
page,
}, testInfo) => {
@@ -570,6 +693,30 @@ test('tournament and betting pages expose same-row navigation tabs beside close'
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390);
});
test('tournament and betting close only their script-opened popup window', async ({ page }, testInfo) => {
const baseURL = testInfo.project.use.baseURL;
expect(typeof baseURL).toBe('string');
await page.goto('about:blank');
for (const route of ['tournament', 'betting'] as const) {
const popupPromise = page.waitForEvent('popup');
await page.evaluate(() => window.open('about:blank', '_blank', 'noopener'));
const popup = await popupPromise;
await installFixture(popup, { tournamentStage: route === 'betting' ? 6 : 1 });
await popup.goto(new URL(route, baseURL as string).href);
await expect(popup.getByRole('button', { name: '창 닫기' }).first()).toBeVisible();
expect(await popup.evaluate(() => window.opener)).toBeNull();
const closed = popup.waitForEvent('close');
await popup.getByRole('button', { name: '창 닫기' }).first().click();
await closed;
expect(page.isClosed()).toBe(false);
expect(page.url()).toBe('about:blank');
}
});
test('mobile betting rankings use tabs and keep dedicated icons beside general names', async ({ page }, testInfo) => {
await page.setViewportSize({ width: 390, height: 844 });
const { placedBets } = await installFixture(page, { tournamentStage: 6 });