Merge remote-tracking branch 'origin/main' into fix/turn-selector-divider-20260813
This commit is contained in:
@@ -274,6 +274,8 @@ test('matches the ref meeting-room geometry, typography, textures, and controls'
|
||||
'src',
|
||||
'https://sam-image.hided.net/icons/22.jpg'
|
||||
);
|
||||
await expect(page.locator('.article-header .date')).toHaveText('07-26 19:20');
|
||||
await expect(page.locator('.comment-row .date')).toHaveText('07-26 19:25');
|
||||
if (artifactRoot) {
|
||||
await page.screenshot({
|
||||
path: resolve(artifactRoot, 'board-core-desktop.png'),
|
||||
@@ -392,7 +394,9 @@ test('uses the ref 500px responsive form widths', async ({ page }) => {
|
||||
await expect(page.getByRole('heading', { name: '기밀실' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('retains article and comment input after a failed mutation, then reloads after success', async ({ page }, testInfo) => {
|
||||
test('retains article and comment input after a failed mutation, then reloads after success', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
const state: BoardFixture = {
|
||||
permission: 2,
|
||||
canMeeting: true,
|
||||
@@ -408,7 +412,9 @@ test('retains article and comment input after a failed mutation, then reloads af
|
||||
await page.locator('#board-title').fill('새 제목');
|
||||
await page.locator('#board-content').fill('새 내용');
|
||||
await page.locator('#submitArticle').click();
|
||||
const articleToast = page.getByTestId('game-toast').filter({ hasText: '게시물 등록에 실패했습니다: 접속 제한입니다.' });
|
||||
const articleToast = page
|
||||
.getByTestId('game-toast')
|
||||
.filter({ hasText: '게시물 등록에 실패했습니다: 접속 제한입니다.' });
|
||||
await expect(articleToast).toHaveAttribute('data-feedback-kind', 'error');
|
||||
await expect(articleToast).toHaveAttribute('role', 'alert');
|
||||
await expect(page.locator('#board-title')).toHaveValue('새 제목');
|
||||
@@ -416,7 +422,13 @@ test('retains article and comment input after a failed mutation, then reloads af
|
||||
|
||||
const desktopToastGeometry = await articleToast.evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return { left: rect.left, right: rect.right, top: rect.top, width: rect.width, viewportWidth: window.innerWidth };
|
||||
return {
|
||||
left: rect.left,
|
||||
right: rect.right,
|
||||
top: rect.top,
|
||||
width: rect.width,
|
||||
viewportWidth: window.innerWidth,
|
||||
};
|
||||
});
|
||||
expect(desktopToastGeometry.left).toBeGreaterThanOrEqual(0);
|
||||
expect(desktopToastGeometry.right).toBeLessThanOrEqual(desktopToastGeometry.viewportWidth);
|
||||
@@ -435,10 +447,14 @@ test('retains article and comment input after a failed mutation, then reloads af
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
const documentWidthBeforeToast = await page.evaluate(() => document.documentElement.scrollWidth);
|
||||
await commentInput.press('Enter');
|
||||
const commentToast = page.getByTestId('game-toast').filter({ hasText: '댓글 등록에 실패했습니다: 접속 제한입니다.' });
|
||||
const commentToast = page
|
||||
.getByTestId('game-toast')
|
||||
.filter({ hasText: '댓글 등록에 실패했습니다: 접속 제한입니다.' });
|
||||
await expect(commentToast).toBeVisible();
|
||||
await expect
|
||||
.poll(async () => commentToast.evaluate((element) => window.innerHeight - element.getBoundingClientRect().bottom))
|
||||
.poll(async () =>
|
||||
commentToast.evaluate((element) => window.innerHeight - element.getBoundingClientRect().bottom)
|
||||
)
|
||||
.toBeGreaterThanOrEqual(0);
|
||||
const mobileToastGeometry = await commentToast.evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
|
||||
@@ -1224,6 +1224,125 @@ test('the 939/940 boundary switches to the Ref-style 500px single document', asy
|
||||
await persistArtifact(page, `${basePath.slice(1)}-mobile-500`);
|
||||
});
|
||||
|
||||
test('real mobile devices initially fit the complete 500px game canvas', async ({ browser }, testInfo) => {
|
||||
test.setTimeout(60_000);
|
||||
const configuredBaseUrl = testInfo.project.use.baseURL;
|
||||
if (typeof configuredBaseUrl !== 'string') {
|
||||
throw new Error('Playwright baseURL is required for the mobile viewport contract');
|
||||
}
|
||||
|
||||
const deviceWidths = [360, 390, 480];
|
||||
const measurements: Record<string, unknown> = {};
|
||||
|
||||
for (const deviceWidth of deviceWidths) {
|
||||
const context = await browser.newContext({
|
||||
baseURL: configuredBaseUrl,
|
||||
viewport: { width: deviceWidth, height: 844 },
|
||||
screen: { width: deviceWidth, height: 844 },
|
||||
deviceScaleFactor: 1,
|
||||
isMobile: true,
|
||||
hasTouch: true,
|
||||
colorScheme: 'dark',
|
||||
});
|
||||
const mobilePage = await context.newPage();
|
||||
const state: NavigationFixture = {
|
||||
officerLevel: 5,
|
||||
permission: 2,
|
||||
nationLevel: 3,
|
||||
stage: 6,
|
||||
npcMode: 1,
|
||||
generalMeCalls: 0,
|
||||
operations: [],
|
||||
};
|
||||
await installFixture(mobilePage, state);
|
||||
await waitForMain(mobilePage);
|
||||
|
||||
const mainGeometry = await mobilePage.locator('.main-page').evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return {
|
||||
viewportMeta: document.querySelector<HTMLMetaElement>('meta[name="viewport"]')?.content,
|
||||
screenWidth: screen.availWidth,
|
||||
innerWidth: window.innerWidth,
|
||||
layoutViewportWidth: document.documentElement.clientWidth,
|
||||
visualViewportWidth: window.visualViewport?.width ?? null,
|
||||
visualViewportScale: window.visualViewport?.scale ?? null,
|
||||
documentScrollWidth: document.documentElement.scrollWidth,
|
||||
canvas: {
|
||||
left: rect.left,
|
||||
right: rect.right,
|
||||
width: rect.width,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
expect(mainGeometry.viewportMeta).toBe('width=500');
|
||||
expect(mainGeometry.screenWidth).toBe(deviceWidth);
|
||||
expect(mainGeometry.layoutViewportWidth).toBe(500);
|
||||
expect(mainGeometry.visualViewportWidth).toBeCloseTo(500, 2);
|
||||
expect(mainGeometry.visualViewportScale).toBeCloseTo(deviceWidth / 500, 2);
|
||||
expect(mainGeometry.documentScrollWidth).toBeLessThanOrEqual(mainGeometry.innerWidth);
|
||||
expect(mainGeometry.canvas).toEqual({ left: 0, right: 500, width: 500 });
|
||||
expect(mainGeometry.canvas.right).toBeLessThanOrEqual((mainGeometry.visualViewportWidth ?? 0) + 0.01);
|
||||
if (artifactRoot) {
|
||||
await mkdir(artifactRoot, { recursive: true });
|
||||
await mobilePage.screenshot({
|
||||
path: resolve(artifactRoot, `initial-mobile-fit-${deviceWidth}.png`),
|
||||
fullPage: true,
|
||||
});
|
||||
}
|
||||
|
||||
const routeGeometry: Record<string, unknown> = {};
|
||||
if (deviceWidth === 390) {
|
||||
for (const target of [
|
||||
'chief-center',
|
||||
'battle-center',
|
||||
'inherit',
|
||||
'nation-betting',
|
||||
]) {
|
||||
await mobilePage.goto(target);
|
||||
await expect
|
||||
.poll(() => mobilePage.locator('#app').evaluate((element) => getComputedStyle(element).minWidth))
|
||||
.toBe('500px');
|
||||
routeGeometry[target] = await mobilePage.locator('#app').evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return {
|
||||
viewportMeta: document.querySelector<HTMLMetaElement>('meta[name="viewport"]')?.content,
|
||||
layoutViewportWidth: document.documentElement.clientWidth,
|
||||
visualViewportWidth: window.visualViewport?.width ?? null,
|
||||
left: rect.left,
|
||||
right: rect.right,
|
||||
width: rect.width,
|
||||
};
|
||||
});
|
||||
const geometry = routeGeometry[target] as {
|
||||
viewportMeta: string;
|
||||
layoutViewportWidth: number;
|
||||
visualViewportWidth: number;
|
||||
left: number;
|
||||
right: number;
|
||||
width: number;
|
||||
};
|
||||
expect(geometry.viewportMeta).toBe('width=500');
|
||||
expect(geometry.layoutViewportWidth).toBe(500);
|
||||
expect(geometry.visualViewportWidth).toBeCloseTo(500, 2);
|
||||
expect(geometry.left).toBeCloseTo(0, 2);
|
||||
expect(geometry.right).toBeCloseTo(500, 2);
|
||||
expect(geometry.width).toBeCloseTo(500, 2);
|
||||
}
|
||||
}
|
||||
|
||||
measurements[String(deviceWidth)] = { main: mainGeometry, routes: routeGeometry };
|
||||
await context.close();
|
||||
}
|
||||
|
||||
if (artifactRoot) {
|
||||
await writeFile(
|
||||
resolve(artifactRoot, 'initial-mobile-fit-computed-dom.json'),
|
||||
`${JSON.stringify(measurements, null, 2)}\n`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('nation menu presentation follows the server-derived permission matrix', async ({ page }) => {
|
||||
const state: NavigationFixture = {
|
||||
officerLevel: 1,
|
||||
|
||||
Reference in New Issue
Block a user