Merge remote-tracking branch 'origin/main' into feature/nation-general-controls-20260813
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
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 { expect, test, type Locator, type Page, type Route } from '@playwright/test';
|
||||
import { gameBasePath, gameProfile, gameTrpcRoute } from './gameTestPaths.js';
|
||||
|
||||
const response = (data: unknown) => ({ result: { data } });
|
||||
@@ -20,6 +20,23 @@ const persistParityArtifact = async (page: Page, name: string, geometry: unknown
|
||||
]);
|
||||
};
|
||||
|
||||
const readGeneralPanelImages = async (panel: Locator) =>
|
||||
panel.evaluate((element) =>
|
||||
[...element.querySelectorAll<HTMLElement>('.general-image')].map((image) => {
|
||||
const rect = image.getBoundingClientRect();
|
||||
const style = getComputedStyle(image);
|
||||
return {
|
||||
label: image.getAttribute('aria-label'),
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
backgroundImage: style.backgroundImage,
|
||||
backgroundSize: style.backgroundSize,
|
||||
pointerEvents: style.pointerEvents,
|
||||
userSelect: style.userSelect,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
type FixtureState = {
|
||||
permission: 'head' | 'member';
|
||||
myset: number;
|
||||
@@ -545,9 +562,15 @@ test('재야 메인은 국가 틀과 성격·특기 표기명을 Chromium에 표
|
||||
|
||||
const generalCard = page.locator('.general-card');
|
||||
await expect(generalCard).toContainText('성격안전');
|
||||
await expect(generalCard).toContainText('전투특기신산');
|
||||
await expect(generalCard).toContainText('내정특기상재');
|
||||
await expect(generalCard).toContainText('특기상재 / 신산');
|
||||
await expect(generalCard).not.toContainText('che_');
|
||||
await expect(generalCard).toHaveAttribute('data-general-basic-card', '');
|
||||
const mainImages = await readGeneralPanelImages(generalCard);
|
||||
expect(mainImages).toHaveLength(2);
|
||||
expect(mainImages[0]).toMatchObject({ width: 64, height: 64, pointerEvents: 'none', userSelect: 'none' });
|
||||
expect(mainImages[0]?.backgroundImage).toContain('/icons/default.jpg');
|
||||
expect(mainImages[1]).toMatchObject({ width: 64, height: 64, pointerEvents: 'none', userSelect: 'none' });
|
||||
expect(mainImages[1]?.backgroundImage).toContain('/game/crewtype1.png');
|
||||
|
||||
const geometry = await nationCard.evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
@@ -587,9 +610,9 @@ test('메인 카드의 국가·수도·관직·계급·병종은 Ref 출력명
|
||||
await expect(nationCard).toContainText('국가 등급주자사');
|
||||
|
||||
const generalCard = page.locator('.general-card');
|
||||
await expect(generalCard.locator('.general-title')).toContainText('검증장수 · 간의대부');
|
||||
await expect(generalCard.locator('.general-title')).toContainText('검증장수 【 간의대부 | 건강 】');
|
||||
await expect(generalCard).toContainText('병종보병');
|
||||
await expect(generalCard).toContainText('계급29품관');
|
||||
await expect(generalCard).toContainText('계급 29품관');
|
||||
|
||||
const cityCard = page.locator('.city-card');
|
||||
await expect(cityCard.locator('.title')).toContainText('【중원 | 특】 업');
|
||||
@@ -646,11 +669,19 @@ test('접속량정보 keeps the legacy public 1016px chart geometry', async ({ p
|
||||
expect(mobileWidth).toBe(1016);
|
||||
});
|
||||
|
||||
test('내 정보&설정 keeps the legacy 1000px/500px geometry and saves in place', async ({ page }) => {
|
||||
test('내 정보&설정 keeps desktop density and becomes a 390px horizontal-identity layout', async ({ page }) => {
|
||||
const state: FixtureState = { permission: 'head', myset: 3, settingMutations: [], accessPages: [] };
|
||||
await install(page, state);
|
||||
await page.setViewportSize({ width: 1000, height: 900 });
|
||||
await page.goto('my-page');
|
||||
await expect(page.locator('.general-table')).toHaveAttribute('data-general-basic-card', '');
|
||||
const myPageImages = await readGeneralPanelImages(page.locator('.general-table'));
|
||||
expect(myPageImages.map(({ width, height }) => ({ width, height }))).toEqual([
|
||||
{ width: 64, height: 64 },
|
||||
{ width: 64, height: 64 },
|
||||
]);
|
||||
expect(myPageImages[0]?.backgroundImage).toContain('/icons/default.jpg');
|
||||
expect(myPageImages[1]?.backgroundImage).toContain('/game/crewtype1.png');
|
||||
await expect(page.locator('.legacy-general-details')).toContainText('계급 29품관');
|
||||
await expect(page.locator('.legacy-general-details')).toContainText('병종 보병');
|
||||
await expect(page.locator('.item-group')).toContainText('명마');
|
||||
@@ -696,7 +727,7 @@ test('내 정보&설정 keeps the legacy 1000px/500px geometry and saves in plac
|
||||
};
|
||||
});
|
||||
expect(desktop.width).toBe(1000);
|
||||
expect(desktop.minWidth).toBe('500px');
|
||||
expect(desktop.minWidth).toBe('0px');
|
||||
expect(desktop.fontSize).toBe('14px');
|
||||
expect(desktop.columns.split(' ')).toHaveLength(2);
|
||||
expect(desktop.titleHeight).toBeCloseTo(54, 0);
|
||||
@@ -766,26 +797,39 @@ test('내 정보&설정 keeps the legacy 1000px/500px geometry and saves in plac
|
||||
expect(state.settingMutations.at(-1)).not.toHaveProperty('generalId');
|
||||
}
|
||||
|
||||
await page.setViewportSize({ width: 500, height: 900 });
|
||||
await page.setViewportSize({ width: 390, height: 900 });
|
||||
await page.reload();
|
||||
const mobile = await page.locator('#container').evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const settings = element.querySelector<HTMLElement>('.settings-column')!.getBoundingClientRect();
|
||||
const icon = element.querySelector<HTMLElement>('[data-general-basic-card] .general-icon')!.getBoundingClientRect();
|
||||
const name = element.querySelector<HTMLElement>('[data-general-basic-card] .general-title')!.getBoundingClientRect();
|
||||
return {
|
||||
width: rect.width,
|
||||
scrollWidth: document.documentElement.scrollWidth,
|
||||
columns: getComputedStyle(element.querySelector('.top-grid')!).gridTemplateColumns,
|
||||
settingsOffset: settings.x - rect.x,
|
||||
settingsWidth: settings.width,
|
||||
identity: {
|
||||
iconRight: icon.right,
|
||||
nameLeft: name.left,
|
||||
iconTop: icon.top,
|
||||
iconBottom: icon.bottom,
|
||||
nameTop: name.top,
|
||||
nameBottom: name.bottom,
|
||||
},
|
||||
};
|
||||
});
|
||||
expect(mobile).toMatchObject({
|
||||
width: 500,
|
||||
scrollWidth: 500,
|
||||
columns: '500px',
|
||||
width: 390,
|
||||
scrollWidth: 390,
|
||||
columns: '390px',
|
||||
settingsOffset: 0,
|
||||
settingsWidth: 500,
|
||||
settingsWidth: 390,
|
||||
});
|
||||
expect(mobile.identity.nameLeft).toBeGreaterThanOrEqual(mobile.identity.iconRight - 1);
|
||||
expect(mobile.identity.nameTop).toBeLessThan(mobile.identity.iconBottom);
|
||||
expect(mobile.identity.nameBottom).toBeGreaterThan(mobile.identity.iconTop);
|
||||
await persistParityArtifact(page, 'core-my-page-mobile', mobile);
|
||||
});
|
||||
|
||||
@@ -1109,10 +1153,15 @@ test('감찰부 keeps the selector interaction and shows the permission error pa
|
||||
await expect(page.locator('.selector-row select').nth(1)).toHaveValue('8');
|
||||
await page.getByRole('button', { name: '다음 ▶' }).click();
|
||||
await expect(page.locator('.selector-row select').nth(1)).toHaveValue('7');
|
||||
await expect(page.locator('.battle-general-name')).toContainText('검증장수 (간의대부)');
|
||||
await expect(page.locator('.battle-general-name')).toContainText('검증장수 【 간의대부 | 건강 】');
|
||||
await expect(page.locator('.battle-general-extra')).toContainText('계급29품관');
|
||||
await expect(page.locator('.battle-general-extra')).toContainText('병종보병');
|
||||
await expect(page.locator('.battle-general-card')).toContainText('병종보병');
|
||||
await expect(page.locator('.battle-general-card')).not.toContainText('che_');
|
||||
await expect(page.locator('.battle-general-card')).toHaveAttribute('data-general-basic-card', '');
|
||||
const battleImages = await readGeneralPanelImages(page.locator('.battle-general-card'));
|
||||
expect(battleImages).toHaveLength(2);
|
||||
expect(battleImages[0]?.backgroundImage).toContain('/icons/default.jpg');
|
||||
expect(battleImages[1]?.backgroundImage).toContain('/game/crewtype1.png');
|
||||
await expect(page.locator('.battle-general-card [role="progressbar"]')).toHaveCount(14);
|
||||
await expect(page.locator('.battle-general-card [aria-label*="1,275,975 (EX+)"]')).toHaveCount(5);
|
||||
expect(
|
||||
|
||||
@@ -514,7 +514,7 @@ const installRealtimeHarness = async (page: Page) => {
|
||||
|
||||
const waitForMain = async (page: Page) => {
|
||||
await page.goto('./');
|
||||
await expect(page.getByRole('heading', { name: '전장 현황' })).toBeVisible();
|
||||
await expect(page.locator('.game-shell__title')).toBeVisible();
|
||||
await expect(page.locator('.main-global-menu').first()).toBeVisible();
|
||||
await expect(page.locator('.main-nation-menu')).toBeVisible();
|
||||
await expect(page.locator('[data-navigation-id="npc-list"]')).toHaveCount(3);
|
||||
@@ -567,8 +567,24 @@ const persistArtifact = async (page: Page, name: string) => {
|
||||
globalPopup: describe('#mobile-global-menu'),
|
||||
nationPopup: describe('#mobile-nation-menu'),
|
||||
quickPopup: describe('#mobile-quick-menu'),
|
||||
commandMenu: describe('.reserved-command-editor details[open] .menu-items'),
|
||||
commandDividers: [...document.querySelectorAll<HTMLElement>('.reserved-command-editor details[open] .menu-divider')].map(
|
||||
(element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const style = getComputedStyle(element);
|
||||
return {
|
||||
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
||||
borderTop: style.borderTop,
|
||||
margin: style.margin,
|
||||
};
|
||||
}
|
||||
),
|
||||
};
|
||||
});
|
||||
const commandMenu = page.locator('.reserved-command-editor details[open] .menu-items').first();
|
||||
if (await commandMenu.isVisible()) {
|
||||
await commandMenu.screenshot({ path: resolve(target, `${name}-menu.png`) });
|
||||
}
|
||||
await Promise.all([
|
||||
page.screenshot({ path: resolve(target, `${name}.png`), fullPage: true }),
|
||||
writeFile(resolve(target, `${name}.json`), `${JSON.stringify(geometry, null, 2)}\n`),
|
||||
@@ -928,6 +944,10 @@ test('main cards and command input stay inside their Ref-sized grid slots', asyn
|
||||
await tenthTurnButton.click();
|
||||
const quickPicker = page.getByTestId('command-picker');
|
||||
await expect(quickPicker).toBeVisible();
|
||||
await tenthTurnButton.click();
|
||||
await expect(quickPicker).toBeHidden();
|
||||
await tenthTurnButton.click();
|
||||
await expect(quickPicker).toBeVisible();
|
||||
const quickPickerAlignment = await quickPicker.evaluate((element) => {
|
||||
const row = element
|
||||
.closest('.reserved-command-editor')
|
||||
@@ -969,6 +989,30 @@ test('main cards and command input stay inside their Ref-sized grid slots', asyn
|
||||
expect(advancedControlGeometry.rangeTop).toBe(advancedControlGeometry.recentTop);
|
||||
expect(advancedControlGeometry.advancedTop).toBeGreaterThan(advancedControlGeometry.rangeTop);
|
||||
expect(advancedControlGeometry.advancedBottom).toBeLessThanOrEqual(advancedControlGeometry.queueTop);
|
||||
const rangeMenu = page.locator('[data-main-target="commands"] .range-menu');
|
||||
await rangeMenu.locator('summary').click();
|
||||
const rangeDividers = rangeMenu.locator('.menu-divider');
|
||||
await expect(rangeDividers).toHaveCount(1);
|
||||
await expect(rangeDividers.first()).toBeVisible();
|
||||
expect(await rangeDividers.first().evaluate((element) => getComputedStyle(element).borderTop)).toBe(
|
||||
'1px solid rgb(68, 68, 68)'
|
||||
);
|
||||
await persistArtifact(page, `${basePath.slice(1)}-command-range-divider-desktop-1200`);
|
||||
await rangeMenu.evaluate((element) => ((element as HTMLDetailsElement).open = false));
|
||||
await expect(rangeMenu).not.toHaveAttribute('open', '');
|
||||
|
||||
const selectedMenu = page.locator('[data-main-target="commands"] .selected-menu');
|
||||
await selectedMenu.locator('summary').click();
|
||||
const selectedMenuDividers = selectedMenu.locator('.menu-divider');
|
||||
await expect(selectedMenuDividers).toHaveCount(3);
|
||||
await expect(selectedMenuDividers.first()).toBeVisible();
|
||||
expect(await selectedMenuDividers.first().evaluate((element) => getComputedStyle(element).borderTop)).toBe(
|
||||
'1px solid rgb(68, 68, 68)'
|
||||
);
|
||||
await persistArtifact(page, `${basePath.slice(1)}-command-selected-dividers-desktop-1200`);
|
||||
await selectedMenu.evaluate((element) => ((element as HTMLDetailsElement).open = false));
|
||||
await expect(selectedMenu).not.toHaveAttribute('open', '');
|
||||
|
||||
await page.locator('[data-main-target="commands"] .select-command').click();
|
||||
const picker = page.getByTestId('command-picker');
|
||||
await expect(picker).toBeVisible();
|
||||
@@ -1157,6 +1201,11 @@ test('main cards and command input stay inside their Ref-sized grid slots', asyn
|
||||
expect(mobileGeometry.controlBoxes).toHaveLength(3);
|
||||
expect(new Set(mobileGeometry.controlBoxes.map(({ y }) => y)).size).toBe(1);
|
||||
await expect(page.locator('[data-main-target="commands"] .edit-column button')).toHaveCount(30);
|
||||
const mobileTurnButton = page.getByRole('button', { name: '10턴 명령 입력' });
|
||||
await mobileTurnButton.click();
|
||||
await expect(page.getByTestId('command-picker')).toBeVisible();
|
||||
await mobileTurnButton.click();
|
||||
await expect(page.getByTestId('command-picker')).toBeHidden();
|
||||
await captureProgress('mobile-500');
|
||||
});
|
||||
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import { expect, test, type Page, type Route } from '@playwright/test';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { mkdir, 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 responsiveArtifactDir = process.env.TOURNAMENT_RESPONSIVE_ARTIFACT_DIR;
|
||||
const imageRoots = [
|
||||
...(process.env.FRONTEND_PARITY_IMAGE_ROOT ? [resolve(process.env.FRONTEND_PARITY_IMAGE_ROOT, 'game')] : []),
|
||||
resolve(repositoryRoot, '../image/game'),
|
||||
resolve(repositoryRoot, '../../image/game'),
|
||||
];
|
||||
const iconRoots = [
|
||||
...(process.env.FRONTEND_PARITY_IMAGE_ROOT ? [resolve(process.env.FRONTEND_PARITY_IMAGE_ROOT, 'icons')] : []),
|
||||
resolve(repositoryRoot, '../image/icons'),
|
||||
resolve(repositoryRoot, '../../image/icons'),
|
||||
resolve(repositoryRoot, '../../sam_rebuild/image/icons'),
|
||||
];
|
||||
const names = [
|
||||
'관우',
|
||||
'장료',
|
||||
@@ -35,6 +42,8 @@ const participants = names.map((name, index) => ({
|
||||
strength: 80,
|
||||
intel: 80,
|
||||
level: 10,
|
||||
picture: 'default.jpg',
|
||||
imageServer: 0,
|
||||
groupId: 10 + (index % 8),
|
||||
groupNo: Math.floor(index / 8),
|
||||
win: 3 - (index % 2),
|
||||
@@ -88,6 +97,26 @@ const readReferenceImage = async (filename: string): Promise<Buffer> => {
|
||||
throw new Error(`Reference image not found: ${filename}`);
|
||||
};
|
||||
|
||||
const readReferenceIcon = async (filename: string): Promise<Buffer> => {
|
||||
for (const iconRoot of iconRoots) {
|
||||
try {
|
||||
return await readFile(resolve(iconRoot, filename));
|
||||
} catch {
|
||||
// Worktrees can be nested at different depths.
|
||||
}
|
||||
}
|
||||
throw new Error(`Reference icon not found: ${filename}`);
|
||||
};
|
||||
|
||||
const persistScreenshot = async (page: Page, name: string, fallbackPath: string) => {
|
||||
if (!responsiveArtifactDir) {
|
||||
await page.screenshot({ path: fallbackPath, fullPage: true });
|
||||
return;
|
||||
}
|
||||
await mkdir(responsiveArtifactDir, { recursive: true });
|
||||
await page.screenshot({ path: resolve(responsiveArtifactDir, `${name}.webp`), fullPage: true });
|
||||
};
|
||||
|
||||
const installFixture = async (page: Page) => {
|
||||
await page.addInitScript((profile) => {
|
||||
window.localStorage.setItem('sammo-game-token', 'ga_tournament_bracket_playwright');
|
||||
@@ -98,6 +127,9 @@ const installFixture = async (page: Page) => {
|
||||
await route.fulfill({ status: 200, contentType: 'image/jpeg', body: await readReferenceImage(filename) });
|
||||
});
|
||||
}
|
||||
await page.route('**/icons/default.jpg', async (route) => {
|
||||
await route.fulfill({ status: 200, contentType: 'image/jpeg', body: await readReferenceIcon('default.jpg') });
|
||||
});
|
||||
await page.route(gameTrpcRoute, async (route) => {
|
||||
const results = operationNames(route).map((operation) => {
|
||||
if (operation === 'auth.status') return response({ ok: true });
|
||||
@@ -125,12 +157,43 @@ const installFixture = async (page: Page) => {
|
||||
}
|
||||
if (operation === 'tournament.getBettingSummary') {
|
||||
return response({
|
||||
totals: Object.fromEntries(participants.map((participant, index) => [participant.id, 100 + index * 10])),
|
||||
totals: Object.fromEntries(
|
||||
participants.map((participant, index) => [participant.id, 100 + index * 10])
|
||||
),
|
||||
myTotals: {},
|
||||
totalAmount: 2800,
|
||||
myAmount: 0,
|
||||
});
|
||||
}
|
||||
if (operation === 'tournament.getRankings') {
|
||||
return response(
|
||||
[
|
||||
['tt', '전 력 전', '종합'],
|
||||
['tl', '통 솔 전', '통솔'],
|
||||
['ts', '일 기 토', '무력'],
|
||||
['ti', '설 전', '지력'],
|
||||
].map(([prefix, title, statLabel]) => ({
|
||||
prefix,
|
||||
title,
|
||||
statLabel,
|
||||
entries: participants.slice(0, 6).map((participant, index) => ({
|
||||
rank: index + 1,
|
||||
generalId: participant.id,
|
||||
name: participant.name,
|
||||
picture: participant.picture,
|
||||
imageServer: participant.imageServer,
|
||||
npcState: 0,
|
||||
stat: 240 - index,
|
||||
games: 10,
|
||||
win: 7,
|
||||
draw: 1,
|
||||
lose: 2,
|
||||
score: 22 - index,
|
||||
prizes: 3,
|
||||
})),
|
||||
}))
|
||||
);
|
||||
}
|
||||
return response(null);
|
||||
});
|
||||
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(results) });
|
||||
@@ -162,16 +225,20 @@ test('desktop bracket connects every real general slot to the next round', async
|
||||
connectorCenter: firstConnector.x + firstConnector.width / 2,
|
||||
championCenter: champion.x + champion.width / 2,
|
||||
finalistCenters: finalists.map((rect) => rect.x + rect.width / 2),
|
||||
connectorQuarters: [firstConnector.x + firstConnector.width / 4, firstConnector.x + (firstConnector.width * 3) / 4],
|
||||
connectorQuarters: [
|
||||
firstConnector.x + firstConnector.width / 4,
|
||||
firstConnector.x + (firstConnector.width * 3) / 4,
|
||||
],
|
||||
};
|
||||
});
|
||||
expect(geometry.canvasWidth).toBe(2000);
|
||||
expect(geometry.canvasWidth).toBeGreaterThanOrEqual(1000);
|
||||
expect(geometry.canvasWidth).toBeLessThanOrEqual(1200);
|
||||
expect(Math.abs(geometry.connectorCenter - geometry.championCenter)).toBeLessThan(1);
|
||||
expect(geometry.finalistCenters).toHaveLength(2);
|
||||
expect(Math.abs(geometry.finalistCenters[0]! - geometry.connectorQuarters[0]!)).toBeLessThan(1);
|
||||
expect(Math.abs(geometry.finalistCenters[1]! - geometry.connectorQuarters[1]!)).toBeLessThan(1);
|
||||
|
||||
await page.screenshot({ path: testInfo.outputPath('tournament-bracket-desktop.webp'), fullPage: true });
|
||||
await persistScreenshot(page, 'tournament-desktop', testInfo.outputPath('tournament-bracket-desktop.webp'));
|
||||
});
|
||||
|
||||
test('mobile bracket shows every round and general within the handheld width', async ({ page }, testInfo) => {
|
||||
@@ -197,5 +264,62 @@ test('mobile bracket shows every round and general within the handheld width', a
|
||||
expect(bounds.width).toBe(390);
|
||||
expect(bounds.minX).toBeGreaterThanOrEqual(0);
|
||||
expect(bounds.maxX).toBeLessThanOrEqual(390);
|
||||
await page.screenshot({ path: testInfo.outputPath('tournament-bracket-mobile.webp'), fullPage: true });
|
||||
const identity = await bracket
|
||||
.locator('.mobile-bracket-name')
|
||||
.first()
|
||||
.evaluate((element) => {
|
||||
const icon = element.querySelector('img')!.getBoundingClientRect();
|
||||
const name = element.querySelector<HTMLElement>('.general-identity-name')!.getBoundingClientRect();
|
||||
return { iconRight: icon.right, nameLeft: name.left, iconY: icon.y, nameY: name.y };
|
||||
});
|
||||
expect(identity.nameLeft).toBeGreaterThanOrEqual(identity.iconRight);
|
||||
expect(Math.abs(identity.iconY - identity.nameY)).toBeLessThan(8);
|
||||
await expect(page.getByRole('tablist', { name: '본선 조 선택' })).toBeVisible();
|
||||
await page.getByRole('tab', { name: '二조' }).first().click();
|
||||
await expect(page.getByRole('tab', { name: '二조' }).first()).toHaveAttribute('aria-selected', 'true');
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390);
|
||||
await persistScreenshot(page, 'tournament-mobile', testInfo.outputPath('tournament-bracket-mobile.webp'));
|
||||
});
|
||||
|
||||
test('mobile betting rankings use tabs and keep dedicated icons beside general names', async ({ page }, testInfo) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await installFixture(page);
|
||||
await page.goto('betting');
|
||||
|
||||
await expect(page.locator('.candidate-card')).toHaveCount(16);
|
||||
await expect(page.getByRole('tablist', { name: '토너먼트 랭킹 종목 선택' })).toBeVisible();
|
||||
await expect(page.locator('.ranking-table:visible')).toHaveCount(1);
|
||||
await page.getByRole('tab', { name: '통솔전' }).click();
|
||||
await expect(page.getByRole('tab', { name: '통솔전' })).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(page.locator('.ranking-table:visible thead')).toContainText('통 솔 전');
|
||||
|
||||
const identity = await page
|
||||
.locator('.ranking-table:visible .general-identity')
|
||||
.first()
|
||||
.evaluate((element) => {
|
||||
const icon = element.querySelector('img')!.getBoundingClientRect();
|
||||
const name = element.querySelector<HTMLElement>('.general-identity-name')!.getBoundingClientRect();
|
||||
return { iconRight: icon.right, nameLeft: name.left, iconY: icon.y, nameY: name.y };
|
||||
});
|
||||
expect(identity.nameLeft).toBeGreaterThanOrEqual(identity.iconRight);
|
||||
expect(Math.abs(identity.iconY - identity.nameY)).toBeLessThan(8);
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390);
|
||||
await persistScreenshot(page, 'tournament-ranking-mobile', testInfo.outputPath('tournament-ranking-mobile.webp'));
|
||||
});
|
||||
|
||||
test('desktop betting presents icon-and-name cards and all four rankings without document overflow', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
await page.setViewportSize({ width: 1365, height: 900 });
|
||||
await installFixture(page);
|
||||
await page.goto('betting');
|
||||
|
||||
await expect(page.locator('.candidate-card')).toHaveCount(16);
|
||||
await expect(page.locator('.ranking-table:visible')).toHaveCount(4);
|
||||
const columns = await page
|
||||
.locator('.candidate-grid')
|
||||
.evaluate((element) => getComputedStyle(element).gridTemplateColumns.split(' ').length);
|
||||
expect(columns).toBe(4);
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(1365);
|
||||
await persistScreenshot(page, 'tournament-ranking-desktop', testInfo.outputPath('tournament-ranking-desktop.webp'));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user