merge: 최신 main을 가오픈 서버 시계 수정에 통합한다

This commit is contained in:
2026-08-21 16:57:52 +00:00
43 changed files with 1643 additions and 481 deletions
+48 -4
View File
@@ -210,7 +210,44 @@ const install = async (
}
if (operation === 'general.me') return response(generalContext);
if (operation === 'world.getMap') return response(mapFixture);
if (operation === 'turns.getCommandTable') return response({ general: [], nation: [] });
if (operation === 'turns.getCommandTable')
return response({
general: [
{
category: '군사',
values: [
{
key: 'che_징병',
name: '징병',
reqArg: true,
status: 'needsInput',
possible: true,
inputFields: [],
},
{
key: 'che_화계',
name: '화계',
reqArg: true,
status: 'needsInput',
possible: true,
inputFields: [],
},
],
},
],
nation: [],
inputOptions: {
cities: [{ value: 1, label: '업 (아국)' }],
nations: [],
generals: [],
crewTypes: [{ value: 1, label: '보병' }],
armTypes: [],
nationTypes: [],
colors: [],
items: {},
recruitment: null,
},
});
if (operation === 'turns.reserved.getGeneral' || operation === 'turns.reserved.getNation') {
return response({ turns: [], revision: 0 });
}
@@ -373,7 +410,12 @@ const install = async (
crew: 500,
train: 90,
atmos: 90,
turns: denseCurrentCity ? ['징병', '훈련'] : ['징병'],
turns: denseCurrentCity
? [
{ action: 'che_징병', args: { crewType: 1, amount: 300 } },
{ action: 'che_화계', args: { destCityId: 1 } },
]
: [{ action: 'che_징병', args: { crewType: 1, amount: 300 } }],
},
...(denseCurrentCity
? Array.from({ length: 12 }, (_, index) => ({
@@ -1082,8 +1124,10 @@ test('current-city wraps dense general names and only shrinks reserved turns', a
const rows = page.locator('.generals tbody tr');
const reservedTurns = rows.nth(0).locator('.turns');
const npcTurns = rows.nth(1).locator('.turns');
await expect(reservedTurns).toContainText('1 : 징병');
await expect(reservedTurns).toContainText('2 : 훈련');
await expect(reservedTurns).toContainText('1 : 【보병】 300명 징병');
await expect(reservedTurns).toContainText('2 : 【업】에 화계실행');
await expect(reservedTurns.locator('.turn-line').nth(0)).toHaveAttribute('title', '【보병】 300명 징병');
await expect(reservedTurns.locator('.turn-line').nth(1)).toHaveAttribute('title', '【업】에 화계실행');
await expect(reservedTurns).toHaveClass(/turns--reserved/);
await expect(npcTurns).toHaveText('NPC 장수');
await expect(npcTurns).not.toHaveClass(/turns--reserved/);
+24 -26
View File
@@ -38,6 +38,18 @@ const readGeneralPanelImages = async (panel: Locator) =>
})
);
const readGeneralSummaryRows = async (summary: Locator) =>
summary.evaluate((element) => {
const rows = new Map<number, string[]>();
for (const label of element.querySelectorAll<HTMLElement>(':scope > span')) {
const top = Math.round(label.getBoundingClientRect().top * 100) / 100;
const row = rows.get(top) ?? [];
row.push(label.textContent?.trim() ?? '');
rows.set(top, row);
}
return [...rows.values()];
});
type FixtureState = {
permission: 'head' | 'member';
myset: number;
@@ -1242,19 +1254,12 @@ test('내 정보&설정 keeps desktop density and becomes a 390px horizontal-ide
await expect(page.locator('.battle-general-extra')).toContainText('피살6,789');
await expect(page.locator('.battle-general-extra__recent-value')).toHaveText('01-01 00:00');
await expect(page.locator('.legacy-general-details')).toHaveCount(0);
await expect(page.locator('.battle-general-extra > span')).toHaveText([
'명성',
'계',
'전투',
'승리',
'패배',
'계략',
'사관',
'사살',
'피살',
'승률',
'살상률',
'최근 전투',
expect(await readGeneralSummaryRows(page.locator('.battle-general-extra'))).toEqual([
['명성', '계급', ''],
['전투', '계', '사관'],
['승률', '승리', '패배'],
['살상률', '사살', '피살'],
['최근 전투'],
]);
await expect(page.locator('.item-group')).toContainText('명마');
await expect(page.locator('#container')).not.toContainText('che_');
@@ -2043,19 +2048,12 @@ test('감찰부 keeps the selector interaction and shows the permission error pa
await expect(page.locator('.battle-general-extra')).toContainText('사관4년');
await expect(page.locator('.battle-general-extra')).toContainText('승률62.50%');
await expect(page.locator('.battle-general-extra')).toContainText('살상률181.84%');
await expect(page.locator('.battle-general-extra > span')).toHaveText([
'명성',
'계',
'전투',
'승리',
'패배',
'계략',
'사관',
'사살',
'피살',
'승률',
'살상률',
'최근 전투',
expect(await readGeneralSummaryRows(page.locator('.battle-general-extra'))).toEqual([
['명성', '계급', ''],
['전투', '계', '사관'],
['승률', '승리', '패배'],
['살상률', '사살', '피살'],
['최근 전투'],
]);
const battleImages = await readGeneralPanelImages(page.locator('.battle-general-card'));
expect(battleImages).toHaveLength(2);
+351 -4
View File
@@ -15,6 +15,7 @@ const errorResponse = (path: string, message: string) => ({
});
const artifactRoot = process.env.MAIN_NAVIGATION_ARTIFACT_DIR;
const autoRefreshArtifactRoot = process.env.AUTO_REFRESH_ARTIFACT_DIR;
const mapSeasonArtifactRoot = process.env.MAP_SEASON_ARTIFACT_DIR;
const productionBundle = process.env.PLAYWRIGHT_FRONTEND_MODE === 'production';
const basePath = `/${(process.env.PLAYWRIGHT_GAME_BASE_PATH ?? 'che').replace(/^\/+|\/+$/g, '')}`;
const gameProfile = process.env.PLAYWRIGHT_GAME_PROFILE ?? 'che:default';
@@ -55,6 +56,10 @@ type NavigationFixture = {
refCommandCategories?: boolean;
currentYear?: number;
currentMonth?: number;
mapName?: string;
validMapImages?: boolean;
mapImageGate?: Promise<void>;
imageRequests?: string[];
serverId?: string;
profile?: string;
gameIdx?: number;
@@ -146,6 +151,13 @@ const emitReadModelInvalidation = (page: Page, invalidation: ReturnType<typeof r
);
}, invalidation);
const waitForMainRealtime = (page: Page) =>
expect
.poll(() =>
page.evaluate(() => (window as unknown as { __hasMainRealtime: () => boolean }).__hasMainRealtime())
)
.toBe(true);
const refCommandCategoryFixture = ['개인', '내정', '군사', '인사', '계략', '국가'].map((category, index) => ({
category,
values: [
@@ -506,9 +518,29 @@ const installFixture = async (page: Page, state: NavigationFixture) => {
{ profile: gameProfile }
);
await page.route('**/image/**', async (route) => {
await route.fulfill({ status: 200, contentType: 'image/jpeg', body: Buffer.from('') });
page.on('request', (request) => {
const url = new URL(request.url());
if (url.pathname.includes('/image/') || url.hostname === 'sam-image.hided.net') {
(state.imageRequests ??= []).push(url.pathname);
}
});
const handleImageRoute = async (route: Route) => {
await state.mapImageGate;
if (state.validMapImages) {
const transparentPixel = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M/wHwAEAQH/69aQ6wAAAABJRU5ErkJggg==',
'base64'
);
await route.fulfill({ status: 200, contentType: 'image/png', body: transparentPixel });
return;
}
await route.fulfill({ status: 200, contentType: 'image/jpeg', body: Buffer.from('') });
};
if (process.env.SAMMO_E2E_REAL_MAP_ASSETS !== '1') {
await page.route('**/image/**', handleImageRoute);
await page.route('https://sam-image.hided.net/game/**', handleImageRoute);
}
await page.route('**/events**', async (route) => {
await route.abort();
});
@@ -663,7 +695,7 @@ const installFixture = async (page: Page, state: NavigationFixture) => {
}
if (operation === 'world.getMapLayout') {
return response({
mapName: 'che',
mapName: state.mapName ?? 'che',
cityList: [{ id: 1, name: '업', level: 8, region: 1, x: 200, y: 120, path: [] }],
regionMap: { 1: '하북' },
levelMap: { 8: '특' },
@@ -2228,7 +2260,43 @@ test('main cards and command input stay inside their Ref-sized grid slots', asyn
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 selectCommand = page.locator('[data-main-target="commands"] .select-command');
await expect(selectCommand).toHaveClass(/legacy-button--info/u);
await page.mouse.move(1, 1);
const measureSelectCommand = () =>
selectCommand.evaluate((element) => {
const rect = element.getBoundingClientRect();
const style = getComputedStyle(element);
return {
top: rect.top,
bottom: rect.bottom,
height: rect.height,
marginTop: style.marginTop,
borderBottomWidth: style.borderBottomWidth,
borderRadius: style.borderRadius,
backgroundColor: style.backgroundColor,
};
});
const selectDefault = await measureSelectCommand();
expect(selectDefault).toMatchObject({
height: 34,
marginTop: '0px',
borderBottomWidth: '4px',
borderRadius: '5.25px',
backgroundColor: 'rgb(52, 152, 219)',
});
await selectCommand.hover();
const selectHover = await measureSelectCommand();
expect(selectHover).toMatchObject({ height: 33, marginTop: '1px', borderBottomWidth: '3px' });
expect(selectHover.bottom).toBeCloseTo(selectDefault.bottom, 2);
const selectBox = await selectCommand.boundingBox();
if (!selectBox) throw new Error('select command control is not measurable');
await page.mouse.move(selectBox.x + selectBox.width / 2, selectBox.y + selectBox.height / 2);
await page.mouse.down();
const selectActive = await measureSelectCommand();
expect(selectActive).toMatchObject({ height: 32, marginTop: '2px', borderBottomWidth: '2px' });
expect(selectActive.bottom).toBeCloseTo(selectDefault.bottom, 2);
await page.mouse.up();
const picker = page.getByTestId('command-picker');
await expect(picker).toBeVisible();
// The trigger can end up directly above a newly opened category button.
@@ -4224,6 +4292,285 @@ test('global activity, world history, and a month boundary refresh their visible
);
});
test('seasonal map decodes the next background and crossfades it without remounting map content', async ({ page }) => {
const useRealAssets = process.env.SAMMO_E2E_REAL_MAP_ASSETS === '1';
let releaseInitialImages: () => void = () => undefined;
const initialImageGate = new Promise<void>((resolveGate) => {
releaseInitialImages = resolveGate;
});
const state: NavigationFixture = {
officerLevel: 5,
permission: 2,
nationLevel: 3,
stage: 0,
npcMode: 1,
generalMeCalls: 0,
operations: [],
currentMonth: 3,
validMapImages: true,
mapImageGate: useRealAssets ? undefined : initialImageGate,
};
await installRealtimeHarness(page);
await installFixture(page, state);
await page.setViewportSize({ width: 1200, height: 900 });
await waitForMain(page);
await waitForMainRealtime(page);
const map = page.locator('[data-main-target="map"] .map-viewer').first();
if (!useRealAssets) {
await expect(map.locator('.skeleton-line')).toHaveCount(4);
await expect(map.locator('.map-area')).toHaveCount(0);
releaseInitialImages();
state.mapImageGate = undefined;
}
const currentLayer = map.locator('[data-map-background-layer="current"]');
const outgoingLayer = map.locator('[data-map-background-layer="outgoing"]');
await expect(currentLayer.locator('img')).toHaveAttribute('src', /bg_spring\.jpg/u);
await expect(map.locator('.map-area')).toBeVisible();
await expect
.poll(() => state.imageRequests?.some((url) => url.endsWith('/game/map/che/bg_summer.jpg')) ?? false)
.toBe(true);
const initialGeometry = await map.locator('.map-area').evaluate((area) => {
const rect = area.getBoundingClientRect();
const road = area.querySelector('.map-bgroad');
const city = area.querySelector('.city-base');
Object.defineProperty(window, '__mapSeasonTransitionProbe', {
configurable: true,
value: { area, road, city },
});
return { width: rect.width, height: rect.height };
});
expect(initialGeometry).toEqual({ width: 700, height: 500 });
if (mapSeasonArtifactRoot) {
await mkdir(mapSeasonArtifactRoot, { recursive: true });
await map.screenshot({ path: resolve(mapSeasonArtifactRoot, 'season-spring-initial.png') });
}
state.currentMonth = 4;
await emitReadModelInvalidation(page, readModelInvalidation({ lobby: true, map: true }));
await expect(map).toContainText('185年 4月');
await expect(outgoingLayer).toHaveClass(/is-transitioning/u);
await expect(outgoingLayer).toHaveCSS('transition-duration', '0.48s');
let midpointOpacity = 0;
await expect
.poll(
async () => {
midpointOpacity = Number.parseFloat(
await outgoingLayer.evaluate((element) => getComputedStyle(element).opacity)
);
return midpointOpacity > 0 && midpointOpacity < 1;
},
{ intervals: [16, 16, 16, 16, 16, 16], timeout: 350 }
)
.toBe(true);
await expect(currentLayer.locator('img')).toHaveAttribute('src', /bg_summer\.jpg/u);
await expect(outgoingLayer.locator('img')).toHaveAttribute('src', /bg_spring\.jpg/u);
if (mapSeasonArtifactRoot) {
await map.screenshot({ path: resolve(mapSeasonArtifactRoot, 'season-spring-to-summer-midpoint.png') });
}
await expect(outgoingLayer).not.toHaveClass(/is-transitioning/u);
await expect(outgoingLayer.locator('img')).toHaveCount(0);
const finalState = await map.locator('.map-area').evaluate((area) => {
const probe = (
window as unknown as {
__mapSeasonTransitionProbe: { area: Element; road: Element | null; city: Element | null };
}
).__mapSeasonTransitionProbe;
const rect = area.getBoundingClientRect();
return {
areaMounted: probe.area === area,
roadMounted: probe.road === area.querySelector('.map-bgroad'),
cityMounted: probe.city === area.querySelector('.city-base'),
width: rect.width,
height: rect.height,
};
});
expect(finalState).toEqual({
areaMounted: true,
roadMounted: true,
cityMounted: true,
width: 700,
height: 500,
});
if (mapSeasonArtifactRoot) {
await map.screenshot({ path: resolve(mapSeasonArtifactRoot, 'season-summer-complete.png') });
}
await page.setViewportSize({ width: 500, height: 900 });
await expect
.poll(() =>
map.locator('.map-area').evaluate((area) => {
const rect = area.getBoundingClientRect();
return { width: rect.width, height: rect.height };
})
)
.toEqual({ width: 500, height: 357.140625 });
await map.locator('.map-area').evaluate((area) => {
Object.defineProperty(window, '__mobileMapSeasonTransitionProbe', {
configurable: true,
value: area,
});
});
state.currentMonth = 7;
await emitReadModelInvalidation(page, readModelInvalidation({ lobby: true, map: true }));
await expect(map).toContainText('185年 7月');
await expect(currentLayer.locator('img')).toHaveAttribute('src', /bg_fall\.jpg/u);
await expect(outgoingLayer).not.toHaveClass(/is-transitioning/u);
await expect(outgoingLayer.locator('img')).toHaveCount(0);
expect(
await map.locator('.map-area').evaluate((area) => {
const probe = (window as unknown as { __mobileMapSeasonTransitionProbe: Element })
.__mobileMapSeasonTransitionProbe;
const rect = area.getBoundingClientRect();
return {
areaMounted: probe === area,
width: rect.width,
height: rect.height,
};
})
).toEqual({ areaMounted: true, width: 500, height: 357.140625 });
if (mapSeasonArtifactRoot) {
await map.screenshot({ path: resolve(mapSeasonArtifactRoot, 'season-fall-mobile-complete.png') });
}
});
test('reduced-motion map swaps the decoded seasonal background without a fade', async ({ page }) => {
const state: NavigationFixture = {
officerLevel: 5,
permission: 2,
nationLevel: 3,
stage: 0,
npcMode: 1,
generalMeCalls: 0,
operations: [],
currentMonth: 3,
validMapImages: true,
};
await page.emulateMedia({ reducedMotion: 'reduce' });
await installRealtimeHarness(page);
await installFixture(page, state);
await page.setViewportSize({ width: 1200, height: 900 });
await waitForMain(page);
await waitForMainRealtime(page);
const map = page.locator('[data-main-target="map"] .map-viewer').first();
const currentLayer = map.locator('[data-map-background-layer="current"]');
const outgoingLayer = map.locator('[data-map-background-layer="outgoing"]');
await expect(currentLayer.locator('img')).toHaveAttribute('src', /bg_spring\.jpg/u);
await outgoingLayer.evaluate((outgoing) => {
let transitionCount = 0;
const observer = new MutationObserver(() => {
if (outgoing.classList.contains('is-transitioning')) transitionCount += 1;
});
observer.observe(outgoing, { attributes: true, attributeFilter: ['class'] });
Object.defineProperty(window, '__reducedMotionMapProbe', {
configurable: true,
value: {
observer,
get transitionCount() {
return transitionCount;
},
},
});
});
state.currentMonth = 4;
await emitReadModelInvalidation(page, readModelInvalidation({ lobby: true, map: true }));
await expect(map).toContainText('185年 4月');
await expect(currentLayer.locator('img')).toHaveAttribute('src', /bg_summer\.jpg/u);
await page.waitForTimeout(100);
await expect(outgoingLayer).not.toHaveClass(/is-transitioning/u);
await expect(outgoingLayer.locator('img')).toHaveCount(0);
expect(
await outgoingLayer.evaluate(() => {
const probe = (
window as unknown as {
__reducedMotionMapProbe: { observer: MutationObserver; transitionCount: number };
}
).__reducedMotionMapProbe;
probe.observer.disconnect();
return probe.transitionCount;
})
).toBe(0);
});
test('seasonless map keeps its fixed background and does not start a month-boundary crossfade', async ({ page }) => {
const state: NavigationFixture = {
officerLevel: 5,
permission: 2,
nationLevel: 3,
stage: 0,
npcMode: 1,
generalMeCalls: 0,
operations: [],
currentMonth: 3,
mapName: 'ludo_rathowm',
validMapImages: true,
};
await installRealtimeHarness(page);
await installFixture(page, state);
await page.setViewportSize({ width: 1200, height: 900 });
await waitForMain(page);
await waitForMainRealtime(page);
const map = page.locator('[data-main-target="map"] .map-viewer').first();
const currentLayer = map.locator('[data-map-background-layer="current"]');
const outgoingLayer = map.locator('[data-map-background-layer="outgoing"]');
await expect(currentLayer.locator('img')).toHaveAttribute('src', /map\/ludo_rathowm\/back\.jpg/u);
await map.locator('.map-area').evaluate((area) => {
const outgoing = area.querySelector('[data-map-background-layer="outgoing"]');
let transitionCount = 0;
const observer = new MutationObserver(() => {
if (outgoing?.classList.contains('is-transitioning')) transitionCount += 1;
});
if (outgoing) observer.observe(outgoing, { attributes: true, attributeFilter: ['class'] });
Object.defineProperty(window, '__seasonlessMapProbe', {
configurable: true,
value: {
area,
outgoing,
observer,
get transitionCount() {
return transitionCount;
},
},
});
});
state.currentMonth = 4;
await emitReadModelInvalidation(page, readModelInvalidation({ lobby: true, map: true }));
await expect(map).toContainText('185年 4月');
await page.waitForTimeout(650);
await expect(currentLayer.locator('img')).toHaveAttribute('src', /map\/ludo_rathowm\/back\.jpg/u);
await expect(outgoingLayer).not.toHaveClass(/is-transitioning/u);
await expect(outgoingLayer.locator('img')).toHaveCount(0);
expect(state.imageRequests?.some((url) => url.includes('/game/map/che/bg_summer.jpg')) ?? false).toBe(false);
expect(
await map.locator('.map-area').evaluate((area) => {
const probe = (
window as unknown as {
__seasonlessMapProbe: {
area: Element;
observer: MutationObserver;
transitionCount: number;
};
}
).__seasonlessMapProbe;
probe.observer.disconnect();
return { areaMounted: probe.area === area, transitionCount: probe.transitionCount };
})
).toEqual({ areaMounted: true, transitionCount: 0 });
if (mapSeasonArtifactRoot) {
await map.screenshot({ path: resolve(mapSeasonArtifactRoot, 'seasonless-ludo-complete.png') });
}
});
test('same-account main tabs share one realtime diff and exclude a tab while sync is off', async ({
context,
page,
@@ -205,6 +205,86 @@ test('nation generals keeps the 1000px legacy grid and redacted member columns',
await expect(page.locator('#nation-general-list')).toContainText('?');
});
test('nation generals top controls share fixed Lumen state geometry on desktop and mobile', async ({
page,
}, testInfo) => {
await install(page);
const evidence: Record<string, unknown> = {};
for (const viewport of [
{ width: 1200, height: 900 },
{ width: 500, height: 900 },
]) {
await page.setViewportSize(viewport);
await page.goto('nation/generals');
await expect(page.locator('#nation-general-list')).toBeVisible();
const controls = [
page.getByRole('button', { name: '돌아가기' }),
page.getByRole('button', { name: '갱신' }),
page.getByRole('button', { name: '보기 모드⌄' }),
page.getByRole('button', { name: '열 선택⌄' }),
];
const viewportEvidence: Record<string, unknown> = {};
for (const control of controls) {
const label = (await control.textContent())?.trim() ?? 'unknown';
await expect(control).toHaveClass(/legacy-button--fixed-height/u);
const measure = () =>
control.evaluate((element) => {
const rect = element.getBoundingClientRect();
const style = getComputedStyle(element);
return {
top: rect.top,
bottom: rect.bottom,
height: rect.height,
marginTop: style.marginTop,
borderBottomWidth: style.borderBottomWidth,
borderRadius: style.borderRadius,
backgroundColor: style.backgroundColor,
fontFamily: style.fontFamily,
fontSize: style.fontSize,
};
});
await page.mouse.move(viewport.width - 1, viewport.height - 1);
const base = await measure();
expect(base).toMatchObject({
height: 32,
marginTop: '0px',
borderBottomWidth: '4px',
borderRadius: '5.25px',
fontSize: '14px',
});
expect(base.fontFamily).toContain('Pretendard');
await control.hover();
const hover = await measure();
expect(hover).toMatchObject({ height: 31, marginTop: '1px', borderBottomWidth: '3px' });
expect(hover.bottom).toBeCloseTo(base.bottom, 2);
const box = await control.boundingBox();
if (!box) throw new Error(`${label} control is not measurable`);
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
const active = await measure();
expect(active).toMatchObject({ height: 30, marginTop: '2px', borderBottomWidth: '2px' });
expect(active.bottom).toBeCloseTo(base.bottom, 2);
await page.mouse.move(viewport.width - 1, viewport.height - 1);
await page.mouse.up();
viewportEvidence[label] = { default: base, hover, active };
}
evidence[`${viewport.width}x${viewport.height}`] = viewportEvidence;
await page.screenshot({
path: testInfo.outputPath(`nation-general-buttons-${viewport.width}.png`),
fullPage: true,
});
}
await testInfo.attach('nation-general-button-geometry', {
body: JSON.stringify(evidence, null, 2),
contentType: 'application/json',
});
});
test('nation generals restores Ref group, saved view, sort, and Korean search behavior', async ({ page }, testInfo) => {
await install(page);
await page.setViewportSize({ width: 1200, height: 900 });
@@ -405,17 +485,20 @@ test('secret office renders five Ref-style command briefs and the forbidden erro
'5 : 휴식',
]);
await expect(commandRows.nth(2)).toHaveAttribute('title', '【다른장수】에게 쌀 200을 증여');
const geometry = await page.locator('#secret-general-list .turns').first().evaluate((element) => {
const rect = element.getBoundingClientRect();
const style = getComputedStyle(element);
return {
width: rect.width,
height: rect.height,
fontSize: style.fontSize,
textAlign: style.textAlign,
horizontalOverflow: element.scrollWidth - element.clientWidth,
};
});
const geometry = await page
.locator('#secret-general-list .turns')
.first()
.evaluate((element) => {
const rect = element.getBoundingClientRect();
const style = getComputedStyle(element);
return {
width: rect.width,
height: rect.height,
fontSize: style.fontSize,
textAlign: style.textAlign,
horizontalOverflow: element.scrollWidth - element.clientWidth,
};
});
expect(geometry.width).toBeGreaterThanOrEqual(190);
expect(geometry.width).toBeLessThanOrEqual(230);
expect(geometry.height).toBeGreaterThanOrEqual(60);
+2 -2
View File
@@ -278,8 +278,8 @@ test('past plays is available without a current general and preserves desktop in
await expect(detailToggle).toHaveAttribute('aria-expanded', 'true');
await expect(page.locator('.archive-general-card')).toHaveAttribute('data-general-basic-card', '');
await expect(page.locator('.archive-general-card [role="progressbar"]')).toHaveCount(14);
await expect(page.locator('[data-general-battle-summary]')).toContainText('승률62.5%');
await expect(page.locator('[data-general-battle-summary]')).toContainText('살상률75.0%');
await expect(page.locator('[data-general-battle-summary]')).toContainText('승률62.50%');
await expect(page.locator('[data-general-battle-summary]')).toContainText('살상률75.00%');
const hallBattle = page.locator('[data-hall-battle-record]');
await expect(hallBattle).toContainText('명예의 전당 보존 기록');
await expect(hallBattle).toContainText('항목별 기록 시점이 서로 다를 수 있습니다.');
@@ -395,7 +395,10 @@ test('join refresh shows the assigned preliminary group immediately with accessi
await refresh.focus();
await expect(refresh).toBeFocused();
await refresh.hover();
await expect(refresh).toHaveCSS('filter', 'brightness(1.25)');
await expect(refresh).toHaveCSS('filter', 'none');
await expect(refresh).toHaveCSS('height', '43px');
await expect(refresh).toHaveCSS('margin-top', '1px');
await expect(refresh).toHaveCSS('border-bottom-width', '3px');
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390);
});
@@ -602,7 +605,11 @@ test('mobile betting rankings use tabs and keep dedicated icons beside general n
await expect(dialog.getByText('예상 환수금 280')).toBeVisible();
await dialog.getByLabel('베팅 금액').selectOption('50');
await expect(dialog.getByText('예상 환수금 1,400')).toBeVisible();
await persistScreenshot(page, 'tournament-betting-dialog-mobile', testInfo.outputPath('betting-dialog-mobile.webp'));
await persistScreenshot(
page,
'tournament-betting-dialog-mobile',
testInfo.outputPath('betting-dialog-mobile.webp')
);
await dialog.getByRole('button', { name: '베팅 등록' }).click();
await expect(dialog).not.toBeVisible();
await expect(page.getByRole('status')).toHaveText('베팅이 등록되었습니다.');