fix(game-ui): iPhone Safari 확인 조작을 안정화
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
import { expect, type Page } from '@playwright/test';
|
||||
|
||||
export const acceptAppConfirmation = async (page: Page, message: string): Promise<void> => {
|
||||
const dialog = page.getByTestId('game-notice-dialog');
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect(dialog).toContainText(message);
|
||||
await dialog.getByRole('button', { name: '확인', exact: true }).click();
|
||||
await expect(dialog).toBeHidden();
|
||||
};
|
||||
@@ -1,9 +1,10 @@
|
||||
import { expect, test, type Page, type Route } from '@playwright/test';
|
||||
import { devices, expect, test, type Page, type Route } from '@playwright/test';
|
||||
import { mkdir, readFile } from 'node:fs/promises';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { gameProfile, gameTrpcRoute } from './gameTestPaths.js';
|
||||
import { expectLumenButtonStates } from './lumenButton.js';
|
||||
import { acceptAppConfirmation } from './appConfirmation.js';
|
||||
|
||||
type Role = 'leader' | 'head' | 'member';
|
||||
type FixtureState = {
|
||||
@@ -394,11 +395,8 @@ test('leader can grant two ambassador and auditor permissions by click or touch
|
||||
expect(ambassadorGeometry.width).toBeGreaterThanOrEqual(100);
|
||||
await screenshot(page, 'core-personnel-mobile-permission-picker-open.png');
|
||||
|
||||
page.once('dialog', async (dialog) => {
|
||||
expect(dialog.message()).toBe('외교권자를 변경할까요?');
|
||||
await dialog.accept();
|
||||
});
|
||||
await page.getByRole('button', { name: '외교권자 임명 반영' }).click();
|
||||
await acceptAppConfirmation(page, '외교권자를 변경할까요?');
|
||||
await expect(page.getByTestId('game-toast').filter({ hasText: '권한을 변경했습니다.' })).toBeVisible();
|
||||
await expect.poll(() => state.permissionMutationInput).toEqual({ isAmbassador: true, targetGeneralIds: [7, 6] });
|
||||
|
||||
@@ -409,11 +407,8 @@ test('leader can grant two ambassador and auditor permissions by click or touch
|
||||
await expect(auditorOptions.getByRole('option', { name: '허저' })).toHaveCount(0);
|
||||
await auditorOptions.getByRole('option', { name: '장료' }).click();
|
||||
await expect(auditorTrigger).toHaveAccessibleName('조언자 선택, 현재 2명');
|
||||
page.once('dialog', async (dialog) => {
|
||||
expect(dialog.message()).toBe('조언자를 변경할까요?');
|
||||
await dialog.accept();
|
||||
});
|
||||
await page.getByRole('button', { name: '조언자 임명 반영' }).click();
|
||||
await acceptAppConfirmation(page, '조언자를 변경할까요?');
|
||||
await expect(page.getByTestId('game-toast').filter({ hasText: '권한을 변경했습니다.' })).toBeVisible();
|
||||
await expect.poll(() => state.permissionMutationInput).toEqual({ isAmbassador: false, targetGeneralIds: [8, 6] });
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(390);
|
||||
@@ -441,11 +436,8 @@ test('personnel selects an informed general and reports the JosaUtil-composed re
|
||||
await candidate.focus();
|
||||
expect(await candidate.evaluate((button) => getComputedStyle(button).outlineStyle)).not.toBe('none');
|
||||
await screenshot(page, 'core-personnel-desktop-general-picker.png');
|
||||
page.once('dialog', async (dialog) => {
|
||||
expect(dialog.message()).toBe('장료를 주부직에 임명하시겠습니까?');
|
||||
await dialog.accept();
|
||||
});
|
||||
await candidate.click();
|
||||
await acceptAppConfirmation(page, '장료를 주부직에 임명하시겠습니까?');
|
||||
|
||||
await expect(page.getByTestId('game-toast')).toContainText('장료를 임명했습니다.');
|
||||
expect(state.appointedGeneralId).toBe(6);
|
||||
@@ -531,11 +523,8 @@ test('personnel reflows row-level appointments at 500px and 390px without gradie
|
||||
await picker.getByRole('button', { name: /장료/ }).evaluate((button) => getComputedStyle(button).outlineStyle)
|
||||
).not.toBe('none');
|
||||
await screenshot(page, 'core-personnel-mobile-city-picker.png');
|
||||
page.once('dialog', async (dialog) => {
|
||||
expect(dialog.message()).toBe('장료를 허창 태수직에 임명하시겠습니까?');
|
||||
await dialog.accept();
|
||||
});
|
||||
await picker.getByRole('button', { name: /장료/ }).click();
|
||||
await acceptAppConfirmation(page, '장료를 허창 태수직에 임명하시겠습니까?');
|
||||
await expect(page.getByTestId('game-toast')).toContainText('장료를 임명했습니다.');
|
||||
expect(state.appointedGeneralId).toBe(6);
|
||||
expect(state.appointedCityId).toBe(1);
|
||||
@@ -572,6 +561,29 @@ test('personnel reflows row-level appointments at 500px and 390px without gradie
|
||||
await screenshot(page, 'core-personnel-mobile-rows.png');
|
||||
});
|
||||
|
||||
test('@ios-webkit iPhone touch appoints a city officer after the picker closes', async ({ browser }, testInfo) => {
|
||||
const configuredBaseUrl = testInfo.project.use.baseURL;
|
||||
if (typeof configuredBaseUrl !== 'string') throw new Error('Playwright baseURL is required');
|
||||
const context = await browser.newContext({ ...devices['iPhone 15'], baseURL: configuredBaseUrl });
|
||||
const page = await context.newPage();
|
||||
const state: FixtureState = { role: 'head', rate: 20 };
|
||||
try {
|
||||
await installFixture(page, state);
|
||||
await gotoOffice(page, 'nation/personnel');
|
||||
await page.getByRole('button', { name: '허창 태수 변경하기', exact: true }).click();
|
||||
const picker = page.getByTestId('personnel-selection-dialog');
|
||||
await picker.getByRole('button', { name: /장료/ }).click();
|
||||
await expect(picker).toBeHidden();
|
||||
await acceptAppConfirmation(page, '장료를 허창 태수직에 임명하시겠습니까?');
|
||||
await expect(page.getByTestId('game-toast')).toContainText('장료를 임명했습니다.');
|
||||
expect(state.appointedGeneralId).toBe(6);
|
||||
expect(state.appointedCityId).toBe(1);
|
||||
expect(state.appointedOfficerLevel).toBe(4);
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('personnel hides every mutation control for an ordinary member and exposes load errors', async ({ page }) => {
|
||||
await installFixture(page, { role: 'member', rate: 20 });
|
||||
await gotoOffice(page, 'nation/personnel');
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { expect, test, type Page, type Route } from '@playwright/test';
|
||||
import { devices, expect, test, type Page, type Route } from '@playwright/test';
|
||||
import { mkdir, readFile } from 'node:fs/promises';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { gameProfile, gameTrpcRoute } from './gameTestPaths.js';
|
||||
import { touchDrag } from './touchDrag.js';
|
||||
import { acceptAppConfirmation } from './appConfirmation.js';
|
||||
|
||||
type FixtureState = {
|
||||
permissionLevel: number;
|
||||
failNextMutation?: boolean;
|
||||
failLoad?: boolean;
|
||||
mutations: string[];
|
||||
nationPriorityInput?: string[];
|
||||
currentNationPriority?: string[];
|
||||
};
|
||||
|
||||
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
|
||||
@@ -124,7 +127,7 @@ const policyFixture = (state: FixtureState) => ({
|
||||
reqNPCDevelGold: 540,
|
||||
},
|
||||
defaultNationPriority: nationPriority,
|
||||
currentNationPriority: nationPriority,
|
||||
currentNationPriority: state.currentNationPriority ?? nationPriority,
|
||||
availableNationPriorityItems: nationPriority,
|
||||
defaultGeneralActionPriority: generalPriority,
|
||||
currentGeneralActionPriority: generalPriority,
|
||||
@@ -155,7 +158,14 @@ const installFixture = async (page: Page, state: FixtureState) => {
|
||||
}
|
||||
await page.route(gameTrpcRoute, async (route) => {
|
||||
const operations = operationName(route).split(',');
|
||||
const results = operations.map((operation) => {
|
||||
const rawBody: unknown = route.request().postData() ? route.request().postDataJSON() : {};
|
||||
const requestBody = rawBody && typeof rawBody === 'object' ? (rawBody as Record<string, unknown>) : {};
|
||||
const results = operations.map((operation, operationIndex) => {
|
||||
const rawPayload = requestBody[String(operationIndex)] ?? (operations.length === 1 ? requestBody : {});
|
||||
const payload = rawPayload && typeof rawPayload === 'object' ? (rawPayload as Record<string, unknown>) : {};
|
||||
const jsonInput = Array.isArray(rawPayload)
|
||||
? rawPayload
|
||||
: (payload.json ?? (payload.input as { json?: unknown } | undefined)?.json);
|
||||
if (operation === 'auth.status') return response({ ok: true });
|
||||
if (operation === 'lobby.info') return response({ myGeneral: { id: 22, name: '정책담당' } });
|
||||
if (operation === 'join.getConfig') return response({});
|
||||
@@ -170,6 +180,9 @@ const installFixture = async (page: Page, state: FixtureState) => {
|
||||
operation === 'npc.setGeneralPriority'
|
||||
) {
|
||||
state.mutations.push(operation);
|
||||
if (operation === 'npc.setNationPriority' && Array.isArray(jsonInput)) {
|
||||
state.nationPriorityInput = jsonInput.map(String);
|
||||
}
|
||||
if (state.failNextMutation) {
|
||||
state.failNextMutation = false;
|
||||
return errorResponse(operation, '권한이 부족합니다.', 'FORBIDDEN');
|
||||
@@ -280,8 +293,8 @@ test('desktop geometry, typography, textures, drag, focus, tooltip, and successf
|
||||
).toBeVisible();
|
||||
|
||||
await goldInput.fill('12345');
|
||||
page.once('dialog', (dialog) => dialog.accept());
|
||||
await page.locator('#container > .control_bar').getByRole('button', { name: '설정' }).click();
|
||||
await acceptAppConfirmation(page, '저장할까요?');
|
||||
await expect(page.locator('[data-testid="game-toast"][data-feedback-kind="success"]')).toContainText(
|
||||
'NPC 정책이 반영되었습니다.'
|
||||
);
|
||||
@@ -289,6 +302,29 @@ test('desktop geometry, typography, textures, drag, focus, tooltip, and successf
|
||||
await screenshot(page, 'core-npc-policy-desktop.png');
|
||||
});
|
||||
|
||||
test('@ios-webkit iPhone touch saves the reordered NPC priority', async ({ browser }, testInfo) => {
|
||||
const configuredBaseUrl = testInfo.project.use.baseURL;
|
||||
if (typeof configuredBaseUrl !== 'string') throw new Error('Playwright baseURL is required');
|
||||
const context = await browser.newContext({ ...devices['iPhone 15'], baseURL: configuredBaseUrl });
|
||||
const page = await context.newPage();
|
||||
const reorderedPriority = [nationPriority[1]!, nationPriority[0]!, ...nationPriority.slice(2)];
|
||||
const state: FixtureState = { permissionLevel: 4, mutations: [], currentNationPriority: reorderedPriority };
|
||||
try {
|
||||
await installFixture(page, state);
|
||||
await gotoPolicy(page);
|
||||
const panel = page.locator('.priority-panel').first();
|
||||
const activeList = panel.locator('.priority-column').nth(1).locator('.priority-list');
|
||||
await expect(activeList.locator('.priority_info > span:nth-child(2)').first()).toHaveText('선전포고');
|
||||
await panel.getByRole('button', { name: '설정' }).click();
|
||||
await acceptAppConfirmation(page, '저장할까요?');
|
||||
await expect(page.getByTestId('game-toast')).toContainText('NPC 정책이 반영되었습니다.');
|
||||
expect(state.nationPriorityInput?.[0]).toBe('선전포고');
|
||||
expect(state.nationPriorityInput).toContain('불가침제의');
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('500px layout stacks policy fields and priority panels like the reference', async ({ page }) => {
|
||||
await installFixture(page, { permissionLevel: 4, mutations: [] });
|
||||
await page.setViewportSize({ width: 500, height: 900 });
|
||||
@@ -346,19 +382,9 @@ test('physical mobile touch reorders NPC priority across active and inactive lis
|
||||
const nationPanel = mobilePage.locator('.priority-panel').first();
|
||||
const activeList = nationPanel.locator('.priority-column').nth(1).locator('.priority-list');
|
||||
const activeRows = activeList.locator('.priority-item');
|
||||
await touchDrag(
|
||||
mobilePage,
|
||||
activeRows.nth(0),
|
||||
activeRows.nth(3),
|
||||
{ targetYRatio: 0.9 }
|
||||
);
|
||||
await touchDrag(mobilePage, activeRows.nth(0), activeRows.nth(3), { targetYRatio: 0.9 });
|
||||
await expect
|
||||
.poll(() =>
|
||||
activeList
|
||||
.locator('.priority-item .priority_info > span:nth-child(2)')
|
||||
.first()
|
||||
.textContent()
|
||||
)
|
||||
.poll(() => activeList.locator('.priority-item .priority_info > span:nth-child(2)').first().textContent())
|
||||
.toBe('선전포고');
|
||||
|
||||
const activeItem = activeList.getByText('불가침제의', { exact: true });
|
||||
@@ -366,9 +392,7 @@ test('physical mobile touch reorders NPC priority across active and inactive lis
|
||||
await touchDrag(mobilePage, activeItem, inactiveList.locator('.inactive-header'));
|
||||
|
||||
await expect(inactiveList.getByText('불가침제의', { exact: true })).toBeVisible();
|
||||
await expect(
|
||||
activeList.getByText('불가침제의', { exact: true })
|
||||
).toHaveCount(0);
|
||||
await expect(activeList.getByText('불가침제의', { exact: true })).toHaveCount(0);
|
||||
await mobilePage.screenshot({ path: testInfo.outputPath('npc-priority-mobile-touch.png'), fullPage: true });
|
||||
} finally {
|
||||
await context.close();
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { expect, test, type Page, type Route } from '@playwright/test';
|
||||
import { devices, expect, test, type Page, type Route } from '@playwright/test';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { gameProfile, gameTrpcRoute } from './gameTestPaths.js';
|
||||
import { acceptAppConfirmation } from './appConfirmation.js';
|
||||
|
||||
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
|
||||
const imageRoots = [
|
||||
@@ -348,6 +349,11 @@ const installApiFixture = async (page: Page, state: FixtureState) => {
|
||||
state.troops[0]!.members = state.troops[0]!.members.filter((member) => member.id !== 3);
|
||||
return response({ ok: true });
|
||||
}
|
||||
if (operation === 'troop.exit') {
|
||||
state.me.troopId = 0;
|
||||
state.troops = state.troops.filter((troop) => troop.id !== state.me.id);
|
||||
return response({ ok: true });
|
||||
}
|
||||
return errorResponse(operation, `Unhandled fixture operation: ${operation}`);
|
||||
});
|
||||
await fulfillJson(route, results);
|
||||
@@ -555,19 +561,19 @@ test('shows API failure then creates a troop successfully', async ({ page }) =>
|
||||
await expect(input).toBeHidden();
|
||||
});
|
||||
|
||||
test('renames and kicks through confirm dialogs, then refreshes state', async ({ page }) => {
|
||||
test('renames and kicks through app confirmation dialogs, then refreshes state', async ({ page }) => {
|
||||
const state: FixtureState = {
|
||||
me: { id: 1, troopId: 1 },
|
||||
permission: 4,
|
||||
troops: baseTroops(),
|
||||
};
|
||||
await installApiFixture(page, state);
|
||||
page.on('dialog', (dialog) => dialog.accept());
|
||||
await gotoTroop(page);
|
||||
|
||||
await page.getByRole('button', { name: '부대명 변경...' }).first().click();
|
||||
await page.getByRole('textbox', { name: '새 부대명' }).fill('백마의종');
|
||||
await page.getByRole('button', { name: '변경', exact: true }).click();
|
||||
await acceptAppConfirmation(page, '백마대 부대의 이름을 백마의종으로 바꾸시겠습니까?');
|
||||
await expect(page.locator('[data-testid="game-toast"][data-feedback-kind="success"]')).toContainText(
|
||||
'부대명을 변경했습니다.'
|
||||
);
|
||||
@@ -576,12 +582,44 @@ test('renames and kicks through confirm dialogs, then refreshes state', async ({
|
||||
await page.getByRole('button', { name: '부대원 추방...' }).click();
|
||||
await page.getByRole('combobox', { name: '추방할 부대원' }).selectOption('3');
|
||||
await page.getByRole('button', { name: '추방', exact: true }).click();
|
||||
await acceptAppConfirmation(page, '백마의종 부대에서 조운을 추방하시겠습니까?');
|
||||
await expect(
|
||||
page.locator('[data-testid="game-toast"][data-feedback-kind="success"]').filter({ hasText: '조운' })
|
||||
).toContainText('조운을 추방했습니다.');
|
||||
await expect(page.locator('.troopMembers').first()).not.toContainText('조운');
|
||||
});
|
||||
|
||||
test('@ios-webkit iPhone touch renames and disbands a troop through app confirmations', async ({
|
||||
browser,
|
||||
}, testInfo) => {
|
||||
const configuredBaseUrl = testInfo.project.use.baseURL;
|
||||
if (typeof configuredBaseUrl !== 'string') throw new Error('Playwright baseURL is required');
|
||||
const context = await browser.newContext({ ...devices['iPhone 15'], baseURL: configuredBaseUrl });
|
||||
const page = await context.newPage();
|
||||
const state: FixtureState = {
|
||||
me: { id: 1, troopId: 1 },
|
||||
permission: 4,
|
||||
troops: baseTroops(),
|
||||
};
|
||||
try {
|
||||
await installApiFixture(page, state);
|
||||
await gotoTroop(page);
|
||||
await page.getByRole('button', { name: '부대명 변경...' }).first().click();
|
||||
await page.getByRole('textbox', { name: '새 부대명' }).fill('백마의종');
|
||||
await page.getByRole('button', { name: '변경', exact: true }).click();
|
||||
await acceptAppConfirmation(page, '백마대 부대의 이름을 백마의종으로 바꾸시겠습니까?');
|
||||
await expect(page.locator('.troopInfo').filter({ hasText: '백마의종' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: '부대 해산' }).click();
|
||||
await acceptAppConfirmation(page, '백마의종 부대를 해산하겠습니까?');
|
||||
await expect(page.getByTestId('game-toast').filter({ hasText: '부대를 해산했습니다.' })).toBeVisible();
|
||||
expect(state.me.troopId).toBe(0);
|
||||
await expect(page.locator('[data-troop-id="1"]')).toHaveCount(0);
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('does not render management controls for an unauthorized member', async ({ page }) => {
|
||||
await installApiFixture(page, {
|
||||
me: { id: 3, troopId: 1 },
|
||||
|
||||
Reference in New Issue
Block a user