merge: 최신 main을 Gateway 초기화 수명주기 브랜치에 재통합

This commit is contained in:
2026-08-21 01:19:45 +00:00
11 changed files with 458 additions and 16 deletions
+110
View File
@@ -260,6 +260,116 @@ describe('messages router missing-flow compatibility', () => {
expect(queryRaw.mock.calls[0]?.slice(1)).toEqual(expect.arrayContaining([9002, 'diplomacy']));
});
it('allows a ruler to send a recruitment advertisement to the wanderer mailbox', async () => {
const ruler = { ...general, officerLevel: 12 } as GeneralRow;
const queryRaw = vi.fn(async () => [{ id: 53 }]);
const changeJournal = new ChangeJournal();
const { caller } = buildContext(
{
$queryRaw: queryRaw,
general: {
findUnique: vi.fn(async () => ruler),
findMany: vi.fn(async () => []),
},
nation: {
findMany: vi.fn(async () => []),
findUnique: vi.fn(async () => ({ id: 1, name: '위', color: '#112233', meta: {} })),
},
},
{ changeJournal }
);
const result = await caller.messages.send({
generalId: ruler.id,
mailbox: 9000,
text: '우리 나라로 와주세요',
});
expect(result.msgType).toBe('diplomacy');
expect(queryRaw.mock.calls[0]?.slice(1)).toEqual(expect.arrayContaining([9000, 'diplomacy']));
expect(queryRaw).toHaveBeenCalledTimes(2);
expect(changeJournal.snapshot()).toEqual([
{ domain: 'messages.mailbox', entityId: 9000 },
{ domain: 'messages.mailbox', entityId: 9001 },
]);
});
it('keeps the wanderer mailbox unavailable to a non-diplomat on the server', async () => {
const queryRaw = vi.fn(async () => [{ id: 54 }]);
const { caller } = buildContext({
$queryRaw: queryRaw,
nation: {
findMany: vi.fn(async () => []),
findUnique: vi.fn(async () => ({ id: 1, name: '위', color: '#112233', meta: {} })),
},
});
const result = await caller.messages.send({
generalId: general.id,
mailbox: 9000,
text: '권한 없는 재야 광고',
});
expect(result.msgType).toBe('national');
expect(queryRaw).toHaveBeenCalledTimes(1);
expect(queryRaw.mock.calls[0]?.slice(1)).toEqual(expect.arrayContaining([9001, 'national']));
});
it('shows a wanderer the advertisement stored in mailbox 9000 without diplomacy redaction', async () => {
const wanderer = { ...general, nationId: 0, officerLevel: 0 } as GeneralRow;
const advertisementRow = {
id: 55,
mailbox: 9000,
type: 'diplomacy',
src: 9001,
dest: 9000,
time: new Date(),
valid_until: new Date('9999-12-31T00:00:00Z'),
message: {
src: {
generalId: 1,
generalName: '위왕',
nationId: 1,
nationName: '위',
color: '#112233',
icon: '',
},
dest: {
generalId: 0,
generalName: '',
nationId: 0,
nationName: '재야',
color: '#000000',
icon: '',
},
text: '우리 나라로 와주세요',
option: {},
},
};
const queryRaw = vi.fn(async (...args: unknown[]) => {
const values = args.slice(1);
return values.includes(9000) && values.includes('diplomacy') ? [advertisementRow] : [];
});
const { caller } = buildContext({
$queryRaw: queryRaw,
general: {
findUnique: vi.fn(async () => wanderer),
findMany: vi.fn(async () => []),
},
});
const result = await caller.messages.getRecent({ generalId: wanderer.id });
expect(result.permission).toBe(-1);
expect(result.diplomacy).toEqual([
expect.objectContaining({
text: '우리 나라로 와주세요',
dest: expect.objectContaining({ nationId: 0, nationName: '재야' }),
option: {},
}),
]);
});
it('blocks private messages between foreign ambassadors', async () => {
const ambassador = {
...general,
+140 -7
View File
@@ -885,9 +885,9 @@ const expectMobilePanelVisualOrder = async (page: Page, expectedOrder: readonly
expect(audit.panels.map(({ id }) => id)).toEqual(expectedOrder);
expect(audit.visualOrder).toEqual(expectedOrder);
expect(audit.panels.every(({ left, right, width }) => left >= 0 && right <= 500 && width === 500)).toBe(true);
expect(
audit.panels.every((panel, index) => index === 0 || panel.top >= audit.panels[index - 1]!.bottom)
).toBe(true);
expect(audit.panels.every((panel, index) => index === 0 || panel.top >= audit.panels[index - 1]!.bottom)).toBe(
true
);
for (const panel of audit.panels) {
expect(panel.display, `${panel.id}: display`).not.toBe('none');
expect(['static', 'relative'], `${panel.id}: position`).toContain(panel.position);
@@ -1125,9 +1125,7 @@ test('desktop menus preserve ref columns, prefix-safe routes, and controlled dro
await expect(page.locator('.main-mobile-bottom')).toBeHidden();
await expect(page.locator('.layout-desktop')).toBeVisible();
await expect(page.locator('.layout-mobile')).toHaveCount(0);
await expect(page.getByRole('heading', { name: '메인 화면 검증 시나리오 체섭 101기', exact: true })).toHaveCount(
1
);
await expect(page.getByRole('heading', { name: '메인 화면 검증 시나리오 체섭 101기', exact: true })).toHaveCount(1);
await expect(page.locator('.game-shell__subtitle')).toHaveCount(0);
await expect(page.locator('.legacy-game-info')).toContainText('현재: 185년 1월');
await expect(page.locator('.legacy-game-info')).toContainText('턴: 10분');
@@ -1279,7 +1277,9 @@ test('desktop menus preserve ref columns, prefix-safe routes, and controlled dro
await persistArtifact(page, `${basePath.slice(1)}-desktop-1200`);
});
test('shows the persisted official game index beside the scenario title without viewport overflow', async ({ page }) => {
test('shows the persisted official game index beside the scenario title without viewport overflow', async ({
page,
}) => {
const state: NavigationFixture = {
officerLevel: 5,
permission: 2,
@@ -2794,6 +2794,139 @@ test('real mobile devices initially fit the complete 500px game canvas', async (
}
});
test('automatic screen mode switches wide mobile screens to the 1000px layout at the Ref boundary', 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 automatic screen-mode contract');
}
const measurements: Record<string, unknown> = {};
for (const deviceWidth of [699, 700, 820]) {
const context = await browser.newContext({
baseURL: configuredBaseUrl,
viewport: { width: deviceWidth, height: 1180 },
screen: { width: deviceWidth, height: 1180 },
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 expectedWideLayout = deviceWidth >= 700;
await expect(mobilePage.locator(expectedWideLayout ? '.layout-desktop' : '.layout-mobile')).toBeVisible();
expect(
await mobilePage.evaluate(() => document.querySelector<HTMLMetaElement>('meta[name="viewport"]')?.content)
).toBe(expectedWideLayout ? 'width=1000' : 'width=device-width, initial-scale=1');
const modeMeasurements: Record<string, unknown> = {
auto: await mobilePage.locator('.main-page').evaluate((element) => {
const rect = element.getBoundingClientRect();
const mobileLayout = document.querySelector<HTMLElement>('.layout-mobile');
const desktopLayout = document.querySelector<HTMLElement>('.layout-desktop');
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,
mobileDisplay: mobileLayout ? getComputedStyle(mobileLayout).display : null,
desktopDisplay: desktopLayout ? getComputedStyle(desktopLayout).display : null,
canvas: { left: rect.left, right: rect.right, width: rect.width },
};
}),
};
if (deviceWidth === 820) {
await mobilePage.evaluate(() => {
localStorage.setItem('sam.screenMode', '500px');
document.dispatchEvent(new CustomEvent('tryChangeScreenMode'));
});
await expect(mobilePage.locator('.layout-mobile')).toBeVisible();
expect(
await mobilePage.evaluate(
() => document.querySelector<HTMLMetaElement>('meta[name="viewport"]')?.content
)
).toBe('width=500');
modeMeasurements.forced500 = await mobilePage.locator('.main-page').evaluate(() => {
const mobileLayout = document.querySelector<HTMLElement>('.layout-mobile');
const desktopLayout = document.querySelector<HTMLElement>('.layout-desktop');
return {
viewportMeta: document.querySelector<HTMLMetaElement>('meta[name="viewport"]')?.content,
layoutViewportWidth: document.documentElement.clientWidth,
visualViewportWidth: window.visualViewport?.width ?? null,
mobileDisplay: mobileLayout ? getComputedStyle(mobileLayout).display : null,
desktopDisplay: desktopLayout ? getComputedStyle(desktopLayout).display : null,
};
});
await mobilePage.evaluate(() => {
localStorage.setItem('sam.screenMode', '1000px');
document.dispatchEvent(new CustomEvent('tryChangeScreenMode'));
});
await expect(mobilePage.locator('.layout-desktop')).toBeVisible();
expect(
await mobilePage.evaluate(
() => document.querySelector<HTMLMetaElement>('meta[name="viewport"]')?.content
)
).toBe('width=1000');
modeMeasurements.forced1000 = await mobilePage.locator('.main-page').evaluate(() => {
const mobileLayout = document.querySelector<HTMLElement>('.layout-mobile');
const desktopLayout = document.querySelector<HTMLElement>('.layout-desktop');
return {
viewportMeta: document.querySelector<HTMLMetaElement>('meta[name="viewport"]')?.content,
layoutViewportWidth: document.documentElement.clientWidth,
visualViewportWidth: window.visualViewport?.width ?? null,
mobileDisplay: mobileLayout ? getComputedStyle(mobileLayout).display : null,
desktopDisplay: desktopLayout ? getComputedStyle(desktopLayout).display : null,
};
});
await mobilePage.evaluate(() => {
localStorage.setItem('sam.screenMode', 'auto');
document.dispatchEvent(new CustomEvent('tryChangeScreenMode'));
});
await expect(mobilePage.locator('.layout-desktop')).toBeVisible();
expect(
await mobilePage.evaluate(
() => document.querySelector<HTMLMetaElement>('meta[name="viewport"]')?.content
)
).toBe('width=1000');
}
measurements[String(deviceWidth)] = modeMeasurements;
if (artifactRoot) {
await mkdir(artifactRoot, { recursive: true });
await mobilePage.screenshot({
path: resolve(artifactRoot, `auto-screen-mode-${deviceWidth}.png`),
fullPage: true,
});
}
await context.close();
}
if (artifactRoot) {
await writeFile(
resolve(artifactRoot, 'auto-screen-mode-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,
@@ -294,8 +294,19 @@ test('nation generals filter buttons open Ref operator menus and apply compound
await page.setViewportSize({ width: 500, height: 900 });
expect(await page.locator('.general-page').evaluate((element) => element.getBoundingClientRect().width)).toBe(1000);
const generalSearch = page.getByLabel('장수명 필터');
await expect(generalSearch).toHaveCSS('touch-action', 'manipulation');
const viewportContract = await page.evaluate(() => ({
content: document.querySelector<HTMLMetaElement>('meta[name="viewport"]')?.content ?? '',
scale: window.visualViewport?.scale ?? 1,
}));
expect(viewportContract.content).not.toMatch(/(?:user-scalable|minimum-scale|maximum-scale)/u);
await generalSearch.focus();
await expect(generalSearch).toBeFocused();
expect(await page.evaluate(() => window.visualViewport?.scale ?? 1)).toBe(viewportContract.scale);
await nameMenuButton.click();
await expect(namePopup).toBeVisible();
await expect(page.getByLabel('장수명 첫 번째 필터 값')).toHaveCSS('touch-action', 'manipulation');
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeGreaterThanOrEqual(1000);
await page.screenshot({ path: testInfo.outputPath('core-mobile-filter-menu.png'), fullPage: true });
});
+9
View File
@@ -63,6 +63,15 @@ textarea {
font: inherit;
}
/*
* Firefox for Android may zoom to a focused search field. `manipulation`
* suppresses that focus-only zoom while retaining ordinary pan and pinch zoom.
*/
input[type='search'],
input[inputmode='search'] {
touch-action: manipulation;
}
/*
* Ref's `.bg0/.bg1/.bg2` set a background image and nothing else, so the
* element stays transparent where the texture does not cover it. Adding a
+2
View File
@@ -4,8 +4,10 @@ import App from './App.vue';
import router from './router';
import './assets/main.css';
import { installImageAssetCssVariables } from './utils/imageAssets';
import { installScreenModeViewport } from './utils/screenModeViewport';
installImageAssetCssVariables();
installScreenModeViewport();
const app = createApp(App);
@@ -220,7 +220,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
label: '외교메시지',
color: '#000000',
options: contacts
.filter((nation) => nation.mailbox !== ownMailbox && nation.nationId > 0)
.filter((nation) => nation.mailbox !== ownMailbox)
.map((nation) => ({
label: nation.name,
value: nation.mailbox,
@@ -0,0 +1,78 @@
export const SCREEN_MODE_KEY = 'sam.screenMode';
export const SCREEN_MODE_CHANGE_EVENT = 'tryChangeScreenMode';
export type ScreenMode = 'auto' | '500px' | '1000px';
export type AutoViewportMeasurements = {
deviceWidth: number;
viewportHeight: number;
targetHeight?: number;
};
export const normalizeScreenMode = (value: string | null): ScreenMode =>
value === '500px' || value === '1000px' ? value : 'auto';
export const resolveAutoViewportContent = ({
deviceWidth,
viewportHeight,
targetHeight = 700,
}: AutoViewportMeasurements): string => {
if (deviceWidth < 500) {
return 'width=500';
}
if (viewportHeight < targetHeight) {
const widthAtTargetHeight = (deviceWidth / viewportHeight) * targetHeight;
return widthAtTargetHeight >= 700 ? 'width=1000' : `height=${Math.ceil(targetHeight)}`;
}
return deviceWidth >= 700 ? 'width=1000' : 'width=device-width, initial-scale=1';
};
export const resolveViewportContent = (mode: ScreenMode, measurements: AutoViewportMeasurements): string => {
if (mode === '500px') return 'width=500';
if (mode === '1000px') return 'width=1000';
return resolveAutoViewportContent(measurements);
};
const findOrCreateViewportMeta = (): HTMLMetaElement => {
const existing = document.querySelector<HTMLMetaElement>('meta[name="viewport"]');
if (existing) return existing;
const viewportMeta = document.createElement('meta');
viewportMeta.name = 'viewport';
document.head.appendChild(viewportMeta);
return viewportMeta;
};
export const installScreenModeViewport = (targetHeight = 700): void => {
if (typeof window === 'undefined' || typeof document === 'undefined') return;
const viewportMeta = findOrCreateViewportMeta();
let previousMode: ScreenMode | null = null;
let previousDeviceWidth: number | null = null;
const adjustViewport = () => {
const mode = normalizeScreenMode(window.localStorage.getItem(SCREEN_MODE_KEY));
const deviceWidth = window.screen.availWidth;
if (mode === previousMode && mode === 'auto' && deviceWidth === previousDeviceWidth) return;
if (mode === previousMode && mode !== 'auto') return;
previousMode = mode;
previousDeviceWidth = deviceWidth;
viewportMeta.content = resolveViewportContent(mode, {
deviceWidth,
viewportHeight: window.innerHeight,
targetHeight,
});
};
adjustViewport();
window.addEventListener('resize', adjustViewport);
window.addEventListener('orientationchange', adjustViewport);
window.addEventListener('storage', (event) => {
if (event.key === SCREEN_MODE_KEY) adjustViewport();
});
document.addEventListener(SCREEN_MODE_CHANGE_EVENT, adjustViewport);
};
+2 -3
View File
@@ -10,6 +10,7 @@ import { resolveGeneralIconUrl, useDefaultGeneralIcon } from '../utils/generalIc
import LegacyGeneralProgress from '../components/ui/LegacyGeneralProgress.vue';
import GeneralBasicCard from '../components/main/GeneralBasicCard.vue';
import { useGameFeedback } from '../composables/useGameFeedback';
import { SCREEN_MODE_CHANGE_EVENT, SCREEN_MODE_KEY, type ScreenMode } from '../utils/screenModeViewport';
import {
DEFAULT_MOBILE_MAIN_PANEL_ORDER,
loadMobileMainPanelOrder,
@@ -19,11 +20,9 @@ import {
type MobileMainPanelId,
} from '../utils/mobileMainPanelOrder';
const SCREEN_MODE_KEY = 'sam.screenMode';
const CUSTOM_CSS_KEY = 'sam_customCSS';
const PENDING_DIE_ON_PRESTART_KEY = 'sam.pending.dieOnPrestart';
const { success: showSuccessToast, error: showErrorToast, showDialog } = useGameFeedback();
type ScreenMode = 'auto' | '500px' | '1000px';
type LogType = 'generalHistory' | 'battleDetail' | 'battleResult' | 'generalAction';
type ItemSlotKey = 'horse' | 'weapon' | 'book' | 'item';
type MyGeneralResponse = Awaited<ReturnType<typeof trpc.general.me.query>>;
@@ -373,7 +372,7 @@ const dropItem = (item: { key: ItemSlotKey; slotName: string; displayName: strin
watch(screenMode, (mode) => {
localStorage.setItem(SCREEN_MODE_KEY, mode);
document.dispatchEvent(new CustomEvent('tryChangeScreenMode'));
document.dispatchEvent(new CustomEvent(SCREEN_MODE_CHANGE_EVENT));
});
watch(customCss, (text) => {
@@ -0,0 +1,35 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
normalizeScreenMode,
resolveAutoViewportContent,
resolveViewportContent,
} from '../src/utils/screenModeViewport.ts';
void test('automatic mode follows the Ref physical-screen thresholds', () => {
assert.equal(resolveAutoViewportContent({ deviceWidth: 390, viewportHeight: 844 }), 'width=500');
assert.equal(
resolveAutoViewportContent({ deviceWidth: 699, viewportHeight: 900 }),
'width=device-width, initial-scale=1'
);
assert.equal(resolveAutoViewportContent({ deviceWidth: 700, viewportHeight: 900 }), 'width=1000');
assert.equal(resolveAutoViewportContent({ deviceWidth: 820, viewportHeight: 1180 }), 'width=1000');
});
void test('automatic mode preserves the Ref short-viewport aspect-ratio branch', () => {
assert.equal(resolveAutoViewportContent({ deviceWidth: 600, viewportHeight: 650 }), 'height=700');
assert.equal(resolveAutoViewportContent({ deviceWidth: 650, viewportHeight: 600 }), 'width=1000');
});
void test('explicit modes override automatic measurements and invalid storage falls back to auto', () => {
const phone = { deviceWidth: 390, viewportHeight: 844 };
const tablet = { deviceWidth: 820, viewportHeight: 1180 };
assert.equal(resolveViewportContent('1000px', phone), 'width=1000');
assert.equal(resolveViewportContent('500px', tablet), 'width=500');
assert.equal(normalizeScreenMode('1000px'), '1000px');
assert.equal(normalizeScreenMode('500px'), '500px');
assert.equal(normalizeScreenMode('unexpected'), 'auto');
assert.equal(normalizeScreenMode(null), 'auto');
});
@@ -1,8 +1,11 @@
import { expect, test, type Page, type Route } from '@playwright/test';
import { mkdir } from 'node:fs/promises';
import { resolve } from 'node:path';
import { canonicalFrontendFixture as fixture } from './fixtures/canonical.js';
const gamePort = process.env.FRONTEND_PARITY_GAME_PORT ?? '15102';
const artifactRoot = process.env.FRONTEND_PARITY_ARTIFACT_DIR;
const response = (data: unknown) => ({ result: { data } });
const errorResponse = (path: string, message: string) => ({
error: {
@@ -42,7 +45,35 @@ const general = {
const generalContext = {
general,
city: null,
nation: null,
nation: {
id: 1,
name: '테스트국',
color: '#d32f2f',
level: 1,
levelName: '군벌',
gold: 1000,
rice: 1000,
tech: 1000,
typeCode: 'test',
typeName: '테스트',
typePros: '-',
typeCons: '-',
capitalCityId: 1,
capitalCityName: '낙양',
population: { cityCount: 1, current: 10000, max: 20000 },
crew: { generalCount: 2, current: 1000, max: 16000 },
power: 100,
bill: 10,
taxRate: 10,
strategicCommandLimit: 0,
diplomaticLimit: 0,
prohibitScout: false,
prohibitWar: false,
techLevel: 1,
techLimited: false,
topChiefs: { 12: null, 11: null },
impossibleStrategicCommands: [],
},
settings: {},
penalties: {},
};
@@ -373,14 +404,25 @@ for (const viewport of [
boxShadow: getComputedStyle(element).boxShadow,
}))
).toEqual({ outlineWidth: '0px', boxShadow: 'none' });
if (artifactRoot) {
await mkdir(artifactRoot, { recursive: true });
await page.locator('.MessagePanel').screenshot({
path: resolve(artifactRoot, `message-panel-${viewport.width}.png`),
animations: 'disabled',
});
}
});
}
test('exposes ambassador targets, reply, read, delete, and successful send interactions', async ({ page }) => {
test('exposes nation targets including wanderers, reply, read, delete, and successful send interactions', async ({
page,
}) => {
const mutations = await installFixture(page, { permission: 4 });
await openMessages(page, { width: 500, height: 900 });
const select = page.getByLabel('메시지 수신 대상');
await expect(select.locator('optgroup[label="외교메시지"] option[value="9000"]')).toHaveText('재야');
await expect(select.locator('option[value="9002"]')).toHaveCount(1);
await expect(select.locator('option[value="8"]')).toBeDisabled();
await expect(select.locator('option[value="9"]')).toBeEnabled();
@@ -396,11 +438,21 @@ test('exposes ambassador targets, reply, read, delete, and successful send inter
await deleteButton.click();
await expect.poll(() => mutations.filter((entry) => entry.operation === 'messages.delete').length).toBe(1);
await select.selectOption('9999');
await page.getByLabel('메시지 입력').fill('전송 성공');
await select.selectOption('9000');
await page.getByLabel('메시지 입력').fill('우리 나라로 와주세요');
if (artifactRoot) {
await mkdir(artifactRoot, { recursive: true });
await page.locator('.MessageInputForm').screenshot({
path: resolve(artifactRoot, 'wanderer-recruitment-target-500.png'),
animations: 'disabled',
});
}
await page.getByRole('button', { name: '서신전달&갱신' }).click();
await expect(page.getByLabel('메시지 입력')).toHaveValue('');
await expect.poll(() => mutations.filter((entry) => entry.operation === 'messages.send').length).toBe(1);
expect(JSON.stringify(mutations.find((entry) => entry.operation === 'messages.send')?.body)).toContain(
'"mailbox":9000'
);
});
test('accepts recruitment and declines invader prompts through private-message controls', async ({ page }) => {
@@ -442,6 +494,7 @@ test('redacts diplomacy for a low-permission general and preserves the failed-se
await openMessages(page, { width: 500, height: 900 });
const select = page.getByLabel('메시지 수신 대상');
await expect(select.locator('option[value="9000"]')).toHaveCount(0);
await expect(select.locator('option[value="9002"]')).toHaveCount(0);
await expect(page.locator('.DiplomacyTalk')).toContainText('삭제된 메시지입니다');
await expect(page.locator('.DiplomacyTalk')).not.toContainText('외교 메시지 본문');
@@ -65,6 +65,14 @@ const measure = async (browser, name, viewport) => {
await ensureGeneral(page);
await page.locator('.MessagePanel').waitFor({ state: 'visible', timeout: 30_000 });
await page.locator('.BoardHeader').first().waitFor({ state: 'visible' });
const mailboxOptions = await page.locator('.MessageInputForm select option').evaluateAll((options) =>
options.map((option) => ({
value: option.value,
label: option.textContent?.trim() ?? '',
group: option.parentElement?.tagName === 'OPTGROUP' ? option.parentElement.label : '',
disabled: option.disabled,
}))
);
const marker = `computed-dom-${name}-${Date.now()}`;
await page.locator('.MessageInputForm select').selectOption('9999');
await page.locator('.MessageInputForm input').fill(marker);
@@ -160,7 +168,11 @@ const measure = async (browser, name, viewport) => {
page.once('dialog', (dialog) => dialog.accept());
await deleteButton.click();
}
return { ...result, interaction: { hover, focus } };
return {
...result,
mailboxOptions,
interaction: { hover, focus },
};
} finally {
await context.close();
}