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 { mkdir, readFile } from 'node:fs/promises';
|
||||||
import { dirname, resolve } from 'node:path';
|
import { dirname, resolve } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { gameProfile, gameTrpcRoute } from './gameTestPaths.js';
|
import { gameProfile, gameTrpcRoute } from './gameTestPaths.js';
|
||||||
import { expectLumenButtonStates } from './lumenButton.js';
|
import { expectLumenButtonStates } from './lumenButton.js';
|
||||||
|
import { acceptAppConfirmation } from './appConfirmation.js';
|
||||||
|
|
||||||
type Role = 'leader' | 'head' | 'member';
|
type Role = 'leader' | 'head' | 'member';
|
||||||
type FixtureState = {
|
type FixtureState = {
|
||||||
@@ -394,11 +395,8 @@ test('leader can grant two ambassador and auditor permissions by click or touch
|
|||||||
expect(ambassadorGeometry.width).toBeGreaterThanOrEqual(100);
|
expect(ambassadorGeometry.width).toBeGreaterThanOrEqual(100);
|
||||||
await screenshot(page, 'core-personnel-mobile-permission-picker-open.png');
|
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 page.getByRole('button', { name: '외교권자 임명 반영' }).click();
|
||||||
|
await acceptAppConfirmation(page, '외교권자를 변경할까요?');
|
||||||
await expect(page.getByTestId('game-toast').filter({ hasText: '권한을 변경했습니다.' })).toBeVisible();
|
await expect(page.getByTestId('game-toast').filter({ hasText: '권한을 변경했습니다.' })).toBeVisible();
|
||||||
await expect.poll(() => state.permissionMutationInput).toEqual({ isAmbassador: true, targetGeneralIds: [7, 6] });
|
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 expect(auditorOptions.getByRole('option', { name: '허저' })).toHaveCount(0);
|
||||||
await auditorOptions.getByRole('option', { name: '장료' }).click();
|
await auditorOptions.getByRole('option', { name: '장료' }).click();
|
||||||
await expect(auditorTrigger).toHaveAccessibleName('조언자 선택, 현재 2명');
|
await expect(auditorTrigger).toHaveAccessibleName('조언자 선택, 현재 2명');
|
||||||
page.once('dialog', async (dialog) => {
|
|
||||||
expect(dialog.message()).toBe('조언자를 변경할까요?');
|
|
||||||
await dialog.accept();
|
|
||||||
});
|
|
||||||
await page.getByRole('button', { name: '조언자 임명 반영' }).click();
|
await page.getByRole('button', { name: '조언자 임명 반영' }).click();
|
||||||
|
await acceptAppConfirmation(page, '조언자를 변경할까요?');
|
||||||
await expect(page.getByTestId('game-toast').filter({ hasText: '권한을 변경했습니다.' })).toBeVisible();
|
await expect(page.getByTestId('game-toast').filter({ hasText: '권한을 변경했습니다.' })).toBeVisible();
|
||||||
await expect.poll(() => state.permissionMutationInput).toEqual({ isAmbassador: false, targetGeneralIds: [8, 6] });
|
await expect.poll(() => state.permissionMutationInput).toEqual({ isAmbassador: false, targetGeneralIds: [8, 6] });
|
||||||
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(390);
|
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();
|
await candidate.focus();
|
||||||
expect(await candidate.evaluate((button) => getComputedStyle(button).outlineStyle)).not.toBe('none');
|
expect(await candidate.evaluate((button) => getComputedStyle(button).outlineStyle)).not.toBe('none');
|
||||||
await screenshot(page, 'core-personnel-desktop-general-picker.png');
|
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 candidate.click();
|
||||||
|
await acceptAppConfirmation(page, '장료를 주부직에 임명하시겠습니까?');
|
||||||
|
|
||||||
await expect(page.getByTestId('game-toast')).toContainText('장료를 임명했습니다.');
|
await expect(page.getByTestId('game-toast')).toContainText('장료를 임명했습니다.');
|
||||||
expect(state.appointedGeneralId).toBe(6);
|
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)
|
await picker.getByRole('button', { name: /장료/ }).evaluate((button) => getComputedStyle(button).outlineStyle)
|
||||||
).not.toBe('none');
|
).not.toBe('none');
|
||||||
await screenshot(page, 'core-personnel-mobile-city-picker.png');
|
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 picker.getByRole('button', { name: /장료/ }).click();
|
||||||
|
await acceptAppConfirmation(page, '장료를 허창 태수직에 임명하시겠습니까?');
|
||||||
await expect(page.getByTestId('game-toast')).toContainText('장료를 임명했습니다.');
|
await expect(page.getByTestId('game-toast')).toContainText('장료를 임명했습니다.');
|
||||||
expect(state.appointedGeneralId).toBe(6);
|
expect(state.appointedGeneralId).toBe(6);
|
||||||
expect(state.appointedCityId).toBe(1);
|
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');
|
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 }) => {
|
test('personnel hides every mutation control for an ordinary member and exposes load errors', async ({ page }) => {
|
||||||
await installFixture(page, { role: 'member', rate: 20 });
|
await installFixture(page, { role: 'member', rate: 20 });
|
||||||
await gotoOffice(page, 'nation/personnel');
|
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 { mkdir, readFile } from 'node:fs/promises';
|
||||||
import { dirname, resolve } from 'node:path';
|
import { dirname, resolve } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { gameProfile, gameTrpcRoute } from './gameTestPaths.js';
|
import { gameProfile, gameTrpcRoute } from './gameTestPaths.js';
|
||||||
import { touchDrag } from './touchDrag.js';
|
import { touchDrag } from './touchDrag.js';
|
||||||
|
import { acceptAppConfirmation } from './appConfirmation.js';
|
||||||
|
|
||||||
type FixtureState = {
|
type FixtureState = {
|
||||||
permissionLevel: number;
|
permissionLevel: number;
|
||||||
failNextMutation?: boolean;
|
failNextMutation?: boolean;
|
||||||
failLoad?: boolean;
|
failLoad?: boolean;
|
||||||
mutations: string[];
|
mutations: string[];
|
||||||
|
nationPriorityInput?: string[];
|
||||||
|
currentNationPriority?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
|
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
|
||||||
@@ -124,7 +127,7 @@ const policyFixture = (state: FixtureState) => ({
|
|||||||
reqNPCDevelGold: 540,
|
reqNPCDevelGold: 540,
|
||||||
},
|
},
|
||||||
defaultNationPriority: nationPriority,
|
defaultNationPriority: nationPriority,
|
||||||
currentNationPriority: nationPriority,
|
currentNationPriority: state.currentNationPriority ?? nationPriority,
|
||||||
availableNationPriorityItems: nationPriority,
|
availableNationPriorityItems: nationPriority,
|
||||||
defaultGeneralActionPriority: generalPriority,
|
defaultGeneralActionPriority: generalPriority,
|
||||||
currentGeneralActionPriority: generalPriority,
|
currentGeneralActionPriority: generalPriority,
|
||||||
@@ -155,7 +158,14 @@ const installFixture = async (page: Page, state: FixtureState) => {
|
|||||||
}
|
}
|
||||||
await page.route(gameTrpcRoute, async (route) => {
|
await page.route(gameTrpcRoute, async (route) => {
|
||||||
const operations = operationName(route).split(',');
|
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 === 'auth.status') return response({ ok: true });
|
||||||
if (operation === 'lobby.info') return response({ myGeneral: { id: 22, name: '정책담당' } });
|
if (operation === 'lobby.info') return response({ myGeneral: { id: 22, name: '정책담당' } });
|
||||||
if (operation === 'join.getConfig') return response({});
|
if (operation === 'join.getConfig') return response({});
|
||||||
@@ -170,6 +180,9 @@ const installFixture = async (page: Page, state: FixtureState) => {
|
|||||||
operation === 'npc.setGeneralPriority'
|
operation === 'npc.setGeneralPriority'
|
||||||
) {
|
) {
|
||||||
state.mutations.push(operation);
|
state.mutations.push(operation);
|
||||||
|
if (operation === 'npc.setNationPriority' && Array.isArray(jsonInput)) {
|
||||||
|
state.nationPriorityInput = jsonInput.map(String);
|
||||||
|
}
|
||||||
if (state.failNextMutation) {
|
if (state.failNextMutation) {
|
||||||
state.failNextMutation = false;
|
state.failNextMutation = false;
|
||||||
return errorResponse(operation, '권한이 부족합니다.', 'FORBIDDEN');
|
return errorResponse(operation, '권한이 부족합니다.', 'FORBIDDEN');
|
||||||
@@ -280,8 +293,8 @@ test('desktop geometry, typography, textures, drag, focus, tooltip, and successf
|
|||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await goldInput.fill('12345');
|
await goldInput.fill('12345');
|
||||||
page.once('dialog', (dialog) => dialog.accept());
|
|
||||||
await page.locator('#container > .control_bar').getByRole('button', { name: '설정' }).click();
|
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(
|
await expect(page.locator('[data-testid="game-toast"][data-feedback-kind="success"]')).toContainText(
|
||||||
'NPC 정책이 반영되었습니다.'
|
'NPC 정책이 반영되었습니다.'
|
||||||
);
|
);
|
||||||
@@ -289,6 +302,29 @@ test('desktop geometry, typography, textures, drag, focus, tooltip, and successf
|
|||||||
await screenshot(page, 'core-npc-policy-desktop.png');
|
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 }) => {
|
test('500px layout stacks policy fields and priority panels like the reference', async ({ page }) => {
|
||||||
await installFixture(page, { permissionLevel: 4, mutations: [] });
|
await installFixture(page, { permissionLevel: 4, mutations: [] });
|
||||||
await page.setViewportSize({ width: 500, height: 900 });
|
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 nationPanel = mobilePage.locator('.priority-panel').first();
|
||||||
const activeList = nationPanel.locator('.priority-column').nth(1).locator('.priority-list');
|
const activeList = nationPanel.locator('.priority-column').nth(1).locator('.priority-list');
|
||||||
const activeRows = activeList.locator('.priority-item');
|
const activeRows = activeList.locator('.priority-item');
|
||||||
await touchDrag(
|
await touchDrag(mobilePage, activeRows.nth(0), activeRows.nth(3), { targetYRatio: 0.9 });
|
||||||
mobilePage,
|
|
||||||
activeRows.nth(0),
|
|
||||||
activeRows.nth(3),
|
|
||||||
{ targetYRatio: 0.9 }
|
|
||||||
);
|
|
||||||
await expect
|
await expect
|
||||||
.poll(() =>
|
.poll(() => activeList.locator('.priority-item .priority_info > span:nth-child(2)').first().textContent())
|
||||||
activeList
|
|
||||||
.locator('.priority-item .priority_info > span:nth-child(2)')
|
|
||||||
.first()
|
|
||||||
.textContent()
|
|
||||||
)
|
|
||||||
.toBe('선전포고');
|
.toBe('선전포고');
|
||||||
|
|
||||||
const activeItem = activeList.getByText('불가침제의', { exact: true });
|
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 touchDrag(mobilePage, activeItem, inactiveList.locator('.inactive-header'));
|
||||||
|
|
||||||
await expect(inactiveList.getByText('불가침제의', { exact: true })).toBeVisible();
|
await expect(inactiveList.getByText('불가침제의', { exact: true })).toBeVisible();
|
||||||
await expect(
|
await expect(activeList.getByText('불가침제의', { exact: true })).toHaveCount(0);
|
||||||
activeList.getByText('불가침제의', { exact: true })
|
|
||||||
).toHaveCount(0);
|
|
||||||
await mobilePage.screenshot({ path: testInfo.outputPath('npc-priority-mobile-touch.png'), fullPage: true });
|
await mobilePage.screenshot({ path: testInfo.outputPath('npc-priority-mobile-touch.png'), fullPage: true });
|
||||||
} finally {
|
} finally {
|
||||||
await context.close();
|
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 { readFile } from 'node:fs/promises';
|
||||||
import { dirname, resolve } from 'node:path';
|
import { dirname, resolve } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { gameProfile, gameTrpcRoute } from './gameTestPaths.js';
|
import { gameProfile, gameTrpcRoute } from './gameTestPaths.js';
|
||||||
|
import { acceptAppConfirmation } from './appConfirmation.js';
|
||||||
|
|
||||||
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
|
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
|
||||||
const imageRoots = [
|
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);
|
state.troops[0]!.members = state.troops[0]!.members.filter((member) => member.id !== 3);
|
||||||
return response({ ok: true });
|
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}`);
|
return errorResponse(operation, `Unhandled fixture operation: ${operation}`);
|
||||||
});
|
});
|
||||||
await fulfillJson(route, results);
|
await fulfillJson(route, results);
|
||||||
@@ -555,19 +561,19 @@ test('shows API failure then creates a troop successfully', async ({ page }) =>
|
|||||||
await expect(input).toBeHidden();
|
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 = {
|
const state: FixtureState = {
|
||||||
me: { id: 1, troopId: 1 },
|
me: { id: 1, troopId: 1 },
|
||||||
permission: 4,
|
permission: 4,
|
||||||
troops: baseTroops(),
|
troops: baseTroops(),
|
||||||
};
|
};
|
||||||
await installApiFixture(page, state);
|
await installApiFixture(page, state);
|
||||||
page.on('dialog', (dialog) => dialog.accept());
|
|
||||||
await gotoTroop(page);
|
await gotoTroop(page);
|
||||||
|
|
||||||
await page.getByRole('button', { name: '부대명 변경...' }).first().click();
|
await page.getByRole('button', { name: '부대명 변경...' }).first().click();
|
||||||
await page.getByRole('textbox', { name: '새 부대명' }).fill('백마의종');
|
await page.getByRole('textbox', { name: '새 부대명' }).fill('백마의종');
|
||||||
await page.getByRole('button', { name: '변경', exact: true }).click();
|
await page.getByRole('button', { name: '변경', exact: true }).click();
|
||||||
|
await acceptAppConfirmation(page, '백마대 부대의 이름을 백마의종으로 바꾸시겠습니까?');
|
||||||
await expect(page.locator('[data-testid="game-toast"][data-feedback-kind="success"]')).toContainText(
|
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('button', { name: '부대원 추방...' }).click();
|
||||||
await page.getByRole('combobox', { name: '추방할 부대원' }).selectOption('3');
|
await page.getByRole('combobox', { name: '추방할 부대원' }).selectOption('3');
|
||||||
await page.getByRole('button', { name: '추방', exact: true }).click();
|
await page.getByRole('button', { name: '추방', exact: true }).click();
|
||||||
|
await acceptAppConfirmation(page, '백마의종 부대에서 조운을 추방하시겠습니까?');
|
||||||
await expect(
|
await expect(
|
||||||
page.locator('[data-testid="game-toast"][data-feedback-kind="success"]').filter({ hasText: '조운' })
|
page.locator('[data-testid="game-toast"][data-feedback-kind="success"]').filter({ hasText: '조운' })
|
||||||
).toContainText('조운을 추방했습니다.');
|
).toContainText('조운을 추방했습니다.');
|
||||||
await expect(page.locator('.troopMembers').first()).not.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 }) => {
|
test('does not render management controls for an unauthorized member', async ({ page }) => {
|
||||||
await installApiFixture(page, {
|
await installApiFixture(page, {
|
||||||
me: { id: 3, troopId: 1 },
|
me: { id: 3, troopId: 1 },
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"test:e2e:troop": "playwright test troop.spec.ts --config e2e/playwright.config.mjs",
|
"test:e2e:troop": "playwright test troop.spec.ts --config e2e/playwright.config.mjs",
|
||||||
"test:e2e:nation-offices": "playwright test nationOffices.spec.ts --config e2e/playwright.config.mjs",
|
"test:e2e:nation-offices": "playwright test nationOffices.spec.ts --config e2e/playwright.config.mjs",
|
||||||
"test:e2e:npc-policy": "playwright test npcPolicy.spec.ts --config e2e/playwright.config.mjs",
|
"test:e2e:npc-policy": "playwright test npcPolicy.spec.ts --config e2e/playwright.config.mjs",
|
||||||
|
"test:e2e:ios-webkit": "playwright test nationOffices.spec.ts troop.spec.ts npcPolicy.spec.ts --config e2e/playwright.config.mjs --browser=webkit --grep @ios-webkit",
|
||||||
"test:e2e:board": "playwright test board.spec.ts --config e2e/playwright.config.mjs",
|
"test:e2e:board": "playwright test board.spec.ts --config e2e/playwright.config.mjs",
|
||||||
"test:e2e:directories": "playwright test directoryLists.spec.ts --config e2e/playwright.config.mjs",
|
"test:e2e:directories": "playwright test directoryLists.spec.ts --config e2e/playwright.config.mjs",
|
||||||
"test:e2e:auction": "playwright test auction.spec.ts --config e2e/playwright.config.mjs",
|
"test:e2e:auction": "playwright test auction.spec.ts --config e2e/playwright.config.mjs",
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
import { nextTick, onBeforeUnmount, ref, watch } from 'vue';
|
import { nextTick, onBeforeUnmount, ref, watch } from 'vue';
|
||||||
import { useGameFeedback, type GameFeedbackKind } from '../../composables/useGameFeedback';
|
import { useGameFeedback, type GameFeedbackKind } from '../../composables/useGameFeedback';
|
||||||
|
|
||||||
const { toasts, dialog, dismissToast, acknowledgeDialog } = useGameFeedback();
|
const { toasts, dialog, dismissToast, acknowledgeDialog, cancelDialog } = useGameFeedback();
|
||||||
const dialogPanel = ref<HTMLElement | null>(null);
|
const dialogPanel = ref<HTMLElement | null>(null);
|
||||||
const acknowledgeButton = ref<HTMLButtonElement | null>(null);
|
const acknowledgeButton = ref<HTMLButtonElement | null>(null);
|
||||||
|
const cancelButton = ref<HTMLButtonElement | null>(null);
|
||||||
let returnFocus: HTMLElement | null = null;
|
let returnFocus: HTMLElement | null = null;
|
||||||
let previousBodyOverflow = '';
|
let previousBodyOverflow = '';
|
||||||
|
|
||||||
@@ -37,7 +38,7 @@ watch(
|
|||||||
}
|
}
|
||||||
if (next) {
|
if (next) {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
acknowledgeButton.value?.focus();
|
(next.cancelLabel ? cancelButton.value : acknowledgeButton.value)?.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (previous) restorePage();
|
if (previous) restorePage();
|
||||||
@@ -48,7 +49,8 @@ watch(
|
|||||||
const handleDialogKeydown = (event: KeyboardEvent): void => {
|
const handleDialogKeydown = (event: KeyboardEvent): void => {
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
acknowledgeDialog();
|
if (dialog.value?.cancelLabel) cancelDialog();
|
||||||
|
else acknowledgeDialog();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.key !== 'Tab' || !dialogPanel.value) return;
|
if (event.key !== 'Tab' || !dialogPanel.value) return;
|
||||||
@@ -119,6 +121,15 @@ onBeforeUnmount(() => {
|
|||||||
</header>
|
</header>
|
||||||
<p id="game-dialog-message">{{ dialog.message }}</p>
|
<p id="game-dialog-message">{{ dialog.message }}</p>
|
||||||
<footer>
|
<footer>
|
||||||
|
<button
|
||||||
|
v-if="dialog.cancelLabel"
|
||||||
|
ref="cancelButton"
|
||||||
|
type="button"
|
||||||
|
class="game-dialog-cancel"
|
||||||
|
@click="cancelDialog"
|
||||||
|
>
|
||||||
|
{{ dialog.cancelLabel }}
|
||||||
|
</button>
|
||||||
<button ref="acknowledgeButton" type="button" @click="acknowledgeDialog">
|
<button ref="acknowledgeButton" type="button" @click="acknowledgeDialog">
|
||||||
{{ dialog.acknowledgeLabel }}
|
{{ dialog.acknowledgeLabel }}
|
||||||
</button>
|
</button>
|
||||||
@@ -277,6 +288,7 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
.game-dialog-panel footer {
|
.game-dialog-panel footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,6 +308,12 @@ onBeforeUnmount(() => {
|
|||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.game-dialog-panel footer .game-dialog-cancel {
|
||||||
|
color: #ddd;
|
||||||
|
background: #292929;
|
||||||
|
border-color: #626262;
|
||||||
|
}
|
||||||
|
|
||||||
.game-toast-enter-active,
|
.game-toast-enter-active,
|
||||||
.game-toast-leave-active,
|
.game-toast-leave-active,
|
||||||
.game-toast-move,
|
.game-toast-move,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export type GameNoticeDialog = {
|
|||||||
title: string;
|
title: string;
|
||||||
message: string;
|
message: string;
|
||||||
acknowledgeLabel: string;
|
acknowledgeLabel: string;
|
||||||
|
cancelLabel: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GameNoticeDialogOptions = {
|
export type GameNoticeDialogOptions = {
|
||||||
@@ -23,9 +24,13 @@ export type GameNoticeDialogOptions = {
|
|||||||
acknowledgeLabel?: string;
|
acknowledgeLabel?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GameConfirmDialogOptions = GameNoticeDialogOptions & {
|
||||||
|
cancelLabel?: string;
|
||||||
|
};
|
||||||
|
|
||||||
type QueuedDialog = {
|
type QueuedDialog = {
|
||||||
dialog: GameNoticeDialog;
|
dialog: GameNoticeDialog;
|
||||||
resolve: () => void;
|
resolve: (confirmed: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const titleFor = (kind: GameFeedbackKind): string => {
|
const titleFor = (kind: GameFeedbackKind): string => {
|
||||||
@@ -39,7 +44,7 @@ export const createGameFeedbackStore = () => {
|
|||||||
const activeDialog = ref<GameNoticeDialog | null>(null);
|
const activeDialog = ref<GameNoticeDialog | null>(null);
|
||||||
const dismissTimers = new Map<number, ReturnType<typeof setTimeout>>();
|
const dismissTimers = new Map<number, ReturnType<typeof setTimeout>>();
|
||||||
const dialogQueue: QueuedDialog[] = [];
|
const dialogQueue: QueuedDialog[] = [];
|
||||||
let activeDialogResolve: (() => void) | null = null;
|
let activeDialogResolve: ((confirmed: boolean) => void) | null = null;
|
||||||
let nextId = 1;
|
let nextId = 1;
|
||||||
|
|
||||||
const dismissToast = (id: number): void => {
|
const dismissToast = (id: number): void => {
|
||||||
@@ -61,7 +66,10 @@ export const createGameFeedbackStore = () => {
|
|||||||
const id = nextId++;
|
const id = nextId++;
|
||||||
visibleToasts.value = [...visibleToasts.value.slice(-3), { id, kind, message: normalizedMessage }];
|
visibleToasts.value = [...visibleToasts.value.slice(-3), { id, kind, message: normalizedMessage }];
|
||||||
if (durationMs > 0) {
|
if (durationMs > 0) {
|
||||||
dismissTimers.set(id, setTimeout(() => dismissToast(id), durationMs));
|
dismissTimers.set(
|
||||||
|
id,
|
||||||
|
setTimeout(() => dismissToast(id), durationMs)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return id;
|
return id;
|
||||||
};
|
};
|
||||||
@@ -89,6 +97,28 @@ export const createGameFeedbackStore = () => {
|
|||||||
title: options.title?.trim() || titleFor(kind),
|
title: options.title?.trim() || titleFor(kind),
|
||||||
message,
|
message,
|
||||||
acknowledgeLabel: options.acknowledgeLabel?.trim() || '확인',
|
acknowledgeLabel: options.acknowledgeLabel?.trim() || '확인',
|
||||||
|
cancelLabel: null,
|
||||||
|
},
|
||||||
|
resolve: () => resolve(),
|
||||||
|
});
|
||||||
|
if (!activeDialog.value) activateNextDialog();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirm = (options: GameConfirmDialogOptions | string): Promise<boolean> => {
|
||||||
|
const normalizedOptions = typeof options === 'string' ? { message: options } : options;
|
||||||
|
const message = normalizedOptions.message.trim();
|
||||||
|
if (!message) return Promise.resolve(false);
|
||||||
|
const kind = normalizedOptions.kind ?? 'info';
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
dialogQueue.push({
|
||||||
|
dialog: {
|
||||||
|
id: nextId++,
|
||||||
|
kind,
|
||||||
|
title: normalizedOptions.title?.trim() || '확인',
|
||||||
|
message,
|
||||||
|
acknowledgeLabel: normalizedOptions.acknowledgeLabel?.trim() || '확인',
|
||||||
|
cancelLabel: normalizedOptions.cancelLabel?.trim() || '취소',
|
||||||
},
|
},
|
||||||
resolve,
|
resolve,
|
||||||
});
|
});
|
||||||
@@ -96,14 +126,17 @@ export const createGameFeedbackStore = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const acknowledgeDialog = (): void => {
|
const resolveDialog = (confirmed: boolean): void => {
|
||||||
const resolve = activeDialogResolve;
|
const resolve = activeDialogResolve;
|
||||||
activeDialog.value = null;
|
activeDialog.value = null;
|
||||||
activeDialogResolve = null;
|
activeDialogResolve = null;
|
||||||
resolve?.();
|
resolve?.(confirmed);
|
||||||
activateNextDialog();
|
activateNextDialog();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const acknowledgeDialog = (): void => resolveDialog(true);
|
||||||
|
const cancelDialog = (): void => resolveDialog(false);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
toasts: readonly(visibleToasts),
|
toasts: readonly(visibleToasts),
|
||||||
dialog: readonly(activeDialog),
|
dialog: readonly(activeDialog),
|
||||||
@@ -112,7 +145,9 @@ export const createGameFeedbackStore = () => {
|
|||||||
error: (message: string, durationMs?: number) => showToast(message, 'error', durationMs),
|
error: (message: string, durationMs?: number) => showToast(message, 'error', durationMs),
|
||||||
info: (message: string, durationMs?: number) => showToast(message, 'info', durationMs),
|
info: (message: string, durationMs?: number) => showToast(message, 'info', durationMs),
|
||||||
showDialog,
|
showDialog,
|
||||||
|
confirm,
|
||||||
acknowledgeDialog,
|
acknowledgeDialog,
|
||||||
|
cancelDialog,
|
||||||
dismissToast,
|
dismissToast,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { JosaUtil } from '@sammo-ts/common/util/JosaUtil';
|
import { JosaUtil } from '@sammo-ts/common/util/JosaUtil';
|
||||||
@@ -39,7 +39,7 @@ const kickTargetId = ref(0);
|
|||||||
const ambassadorSelection = ref<number[]>([]);
|
const ambassadorSelection = ref<number[]>([]);
|
||||||
const auditorSelection = ref<number[]>([]);
|
const auditorSelection = ref<number[]>([]);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { success: showSuccessToast, error: showErrorToast } = useGameFeedback();
|
const { success: showSuccessToast, error: showErrorToast, confirm: showConfirm } = useGameFeedback();
|
||||||
|
|
||||||
const resolveErrorMessage = (value: unknown): string =>
|
const resolveErrorMessage = (value: unknown): string =>
|
||||||
value instanceof Error ? value.message : typeof value === 'string' ? value : 'unknown_error';
|
value instanceof Error ? value.message : typeof value === 'string' ? value : 'unknown_error';
|
||||||
@@ -135,7 +135,7 @@ const appointChief = async (level: number, targetId: number) => {
|
|||||||
const prompt = target
|
const prompt = target
|
||||||
? `${JosaUtil.put(target.name, '을')} ${office}직에 임명하시겠습니까?`
|
? `${JosaUtil.put(target.name, '을')} ${office}직에 임명하시겠습니까?`
|
||||||
: `${office}직을 비우시겠습니까?`;
|
: `${office}직을 비우시겠습니까?`;
|
||||||
if (!window.confirm(prompt)) return;
|
if (!(await showConfirm(prompt))) return;
|
||||||
await runMutation(
|
await runMutation(
|
||||||
() => trpc.nation.appoint.mutate({ destGeneralId: targetId, destCityId: 0, officerLevel: level }),
|
() => trpc.nation.appoint.mutate({ destGeneralId: targetId, destCityId: 0, officerLevel: level }),
|
||||||
target ? `${JosaUtil.put(target.name, '을')} 임명했습니다.` : '관직을 비웠습니다.'
|
target ? `${JosaUtil.put(target.name, '을')} 임명했습니다.` : '관직을 비웠습니다.'
|
||||||
@@ -148,7 +148,7 @@ const appointCityOfficer = async (level: OfficerLevel, cityId: number, targetId:
|
|||||||
const prompt = target
|
const prompt = target
|
||||||
? `${JosaUtil.put(target.name, '을')} ${city?.name ?? ''} ${officerLabels[level]}직에 임명하시겠습니까?`
|
? `${JosaUtil.put(target.name, '을')} ${city?.name ?? ''} ${officerLabels[level]}직에 임명하시겠습니까?`
|
||||||
: `${city?.name ?? ''} ${officerLabels[level]}직을 비우시겠습니까?`;
|
: `${city?.name ?? ''} ${officerLabels[level]}직을 비우시겠습니까?`;
|
||||||
if (!window.confirm(prompt)) return;
|
if (!(await showConfirm(prompt))) return;
|
||||||
await runMutation(
|
await runMutation(
|
||||||
() =>
|
() =>
|
||||||
trpc.nation.appoint.mutate({
|
trpc.nation.appoint.mutate({
|
||||||
@@ -248,6 +248,7 @@ const applySelection = async (id: number): Promise<void> => {
|
|||||||
const context = selectionContext.value;
|
const context = selectionContext.value;
|
||||||
if (!context) return;
|
if (!context) return;
|
||||||
selectionContext.value = null;
|
selectionContext.value = null;
|
||||||
|
await nextTick();
|
||||||
if (context.kind === 'chief-general') await appointChief(context.level, id);
|
if (context.kind === 'chief-general') await appointChief(context.level, id);
|
||||||
else await appointCityOfficer(context.level, context.cityId, id);
|
else await appointCityOfficer(context.level, context.cityId, id);
|
||||||
};
|
};
|
||||||
@@ -258,7 +259,7 @@ const reportPermissionLimit = () => {
|
|||||||
|
|
||||||
const changePermissions = async (isAmbassador: boolean) => {
|
const changePermissions = async (isAmbassador: boolean) => {
|
||||||
const selection = isAmbassador ? ambassadorSelection.value : auditorSelection.value;
|
const selection = isAmbassador ? ambassadorSelection.value : auditorSelection.value;
|
||||||
if (!window.confirm(`${isAmbassador ? '외교권자' : '조언자'}를 변경할까요?`)) return;
|
if (!(await showConfirm(`${isAmbassador ? '외교권자' : '조언자'}를 변경할까요?`))) return;
|
||||||
await runMutation(
|
await runMutation(
|
||||||
() => trpc.nation.changePermission.mutate({ isAmbassador, targetGeneralIds: selection }),
|
() => trpc.nation.changePermission.mutate({ isAmbassador, targetGeneralIds: selection }),
|
||||||
'권한을 변경했습니다.'
|
'권한을 변경했습니다.'
|
||||||
@@ -267,7 +268,7 @@ const changePermissions = async (isAmbassador: boolean) => {
|
|||||||
|
|
||||||
const kickGeneral = async () => {
|
const kickGeneral = async () => {
|
||||||
const target = generalMap.value.get(kickTargetId.value);
|
const target = generalMap.value.get(kickTargetId.value);
|
||||||
if (!target || !window.confirm(`${JosaUtil.put(target.name, '을')} 추방하시겠습니까?`)) return;
|
if (!target || !(await showConfirm(`${JosaUtil.put(target.name, '을')} 추방하시겠습니까?`))) return;
|
||||||
await runMutation(
|
await runMutation(
|
||||||
() => trpc.nation.kick.mutate({ destGeneralId: target.id }),
|
() => trpc.nation.kick.mutate({ destGeneralId: target.id }),
|
||||||
`${JosaUtil.put(target.name, '을')} 추방했습니다.`
|
`${JosaUtil.put(target.name, '을')} 추방했습니다.`
|
||||||
|
|||||||
@@ -45,7 +45,12 @@ const nationPriority = ref<PriorityListState | null>(null);
|
|||||||
const generalPriority = ref<PriorityListState | null>(null);
|
const generalPriority = ref<PriorityListState | null>(null);
|
||||||
const lastSavedNationPriority = ref<string[]>([]);
|
const lastSavedNationPriority = ref<string[]>([]);
|
||||||
const lastSavedGeneralPriority = ref<string[]>([]);
|
const lastSavedGeneralPriority = ref<string[]>([]);
|
||||||
const { success: showSuccessToast, error: showErrorToast, info: showInfoToast } = useGameFeedback();
|
const {
|
||||||
|
success: showSuccessToast,
|
||||||
|
error: showErrorToast,
|
||||||
|
info: showInfoToast,
|
||||||
|
confirm: showConfirm,
|
||||||
|
} = useGameFeedback();
|
||||||
const canManagePolicy = computed(() => (data.value?.permissionLevel ?? -1) >= 3);
|
const canManagePolicy = computed(() => (data.value?.permissionLevel ?? -1) >= 3);
|
||||||
|
|
||||||
const resolveErrorMessage = (value: unknown): string => {
|
const resolveErrorMessage = (value: unknown): string => {
|
||||||
@@ -282,20 +287,20 @@ const priorityPanels = computed<PriorityPanel[]>(() => {
|
|||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
const resetPolicy = () => {
|
const resetPolicy = async () => {
|
||||||
if (!canManagePolicy.value || !data.value || !window.confirm('초기 설정으로 되돌릴까요?')) return;
|
if (!canManagePolicy.value || !data.value || !(await showConfirm('초기 설정으로 되돌릴까요?'))) return;
|
||||||
policyDraft.value = clonePolicy(data.value.defaultNationPolicy);
|
policyDraft.value = clonePolicy(data.value.defaultNationPolicy);
|
||||||
showInfoToast('서버 초깃값을 적용했습니다. 설정 버튼을 누르면 반영됩니다.');
|
showInfoToast('서버 초깃값을 적용했습니다. 설정 버튼을 누르면 반영됩니다.');
|
||||||
};
|
};
|
||||||
|
|
||||||
const rollbackPolicy = () => {
|
const rollbackPolicy = async () => {
|
||||||
if (!canManagePolicy.value || !lastSavedPolicy.value || !window.confirm('이전 설정으로 되돌릴까요?')) return;
|
if (!canManagePolicy.value || !lastSavedPolicy.value || !(await showConfirm('이전 설정으로 되돌릴까요?'))) return;
|
||||||
policyDraft.value = clonePolicy(lastSavedPolicy.value);
|
policyDraft.value = clonePolicy(lastSavedPolicy.value);
|
||||||
showInfoToast('이전 설정으로 되돌렸습니다.');
|
showInfoToast('이전 설정으로 되돌렸습니다.');
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitPolicy = async () => {
|
const submitPolicy = async () => {
|
||||||
if (!canManagePolicy.value || !policyDraft.value || !window.confirm('저장할까요?')) return;
|
if (!canManagePolicy.value || !policyDraft.value || !(await showConfirm('저장할까요?'))) return;
|
||||||
try {
|
try {
|
||||||
await trpc.npc.setNationPolicy.mutate(policyDraft.value);
|
await trpc.npc.setNationPolicy.mutate(policyDraft.value);
|
||||||
lastSavedPolicy.value = clonePolicy(policyDraft.value);
|
lastSavedPolicy.value = clonePolicy(policyDraft.value);
|
||||||
@@ -305,8 +310,8 @@ const submitPolicy = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetPriority = (section: PrioritySectionKey) => {
|
const resetPriority = async (section: PrioritySectionKey) => {
|
||||||
if (!canManagePolicy.value || !data.value || !window.confirm('초기 설정으로 되돌릴까요?')) return;
|
if (!canManagePolicy.value || !data.value || !(await showConfirm('초기 설정으로 되돌릴까요?'))) return;
|
||||||
if (section === 'nation') {
|
if (section === 'nation') {
|
||||||
nationPriority.value = assignPriorityState(
|
nationPriority.value = assignPriorityState(
|
||||||
data.value.defaultNationPriority,
|
data.value.defaultNationPriority,
|
||||||
@@ -321,8 +326,8 @@ const resetPriority = (section: PrioritySectionKey) => {
|
|||||||
showInfoToast('서버 초깃값을 적용했습니다. 설정 버튼을 누르면 반영됩니다.');
|
showInfoToast('서버 초깃값을 적용했습니다. 설정 버튼을 누르면 반영됩니다.');
|
||||||
};
|
};
|
||||||
|
|
||||||
const rollbackPriority = (section: PrioritySectionKey) => {
|
const rollbackPriority = async (section: PrioritySectionKey) => {
|
||||||
if (!canManagePolicy.value || !data.value || !window.confirm('이전 설정으로 되돌릴까요?')) return;
|
if (!canManagePolicy.value || !data.value || !(await showConfirm('이전 설정으로 되돌릴까요?'))) return;
|
||||||
if (section === 'nation') {
|
if (section === 'nation') {
|
||||||
nationPriority.value = assignPriorityState(
|
nationPriority.value = assignPriorityState(
|
||||||
lastSavedNationPriority.value,
|
lastSavedNationPriority.value,
|
||||||
@@ -339,7 +344,7 @@ const rollbackPriority = (section: PrioritySectionKey) => {
|
|||||||
|
|
||||||
const submitPriority = async (section: PrioritySectionKey) => {
|
const submitPriority = async (section: PrioritySectionKey) => {
|
||||||
const state = section === 'nation' ? nationPriority.value : generalPriority.value;
|
const state = section === 'nation' ? nationPriority.value : generalPriority.value;
|
||||||
if (!canManagePolicy.value || !state || !window.confirm('저장할까요?')) return;
|
if (!canManagePolicy.value || !state || !(await showConfirm('저장할까요?'))) return;
|
||||||
try {
|
try {
|
||||||
if (section === 'nation') {
|
if (section === 'nation') {
|
||||||
await trpc.npc.setNationPriority.mutate(state.active);
|
await trpc.npc.setNationPriority.mutate(state.active);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const dialogTroopId = ref(0);
|
|||||||
const popupMember = ref<Member | null>(null);
|
const popupMember = ref<Member | null>(null);
|
||||||
const popupTop = ref(0);
|
const popupTop = ref(0);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { success: showSuccessToast, error: showErrorToast } = useGameFeedback();
|
const { success: showSuccessToast, error: showErrorToast, confirm: showConfirm } = useGameFeedback();
|
||||||
|
|
||||||
const me = computed(() => data.value?.me ?? null);
|
const me = computed(() => data.value?.me ?? null);
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ const joinTroop = async (troop: Troop) => {
|
|||||||
const exitTroop = async (troop: Troop) => {
|
const exitTroop = async (troop: Troop) => {
|
||||||
const isLeader = me.value?.id === troop.id;
|
const isLeader = me.value?.id === troop.id;
|
||||||
const prompt = isLeader ? `${troop.name} 부대를 해산하겠습니까?` : `${troop.name} 부대에서 탈퇴하겠습니까?`;
|
const prompt = isLeader ? `${troop.name} 부대를 해산하겠습니까?` : `${troop.name} 부대에서 탈퇴하겠습니까?`;
|
||||||
if (!window.confirm(prompt)) {
|
if (!(await showConfirm(prompt))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await runAction(async () => {
|
await runAction(async () => {
|
||||||
@@ -130,7 +130,7 @@ const hasFinalConsonant = (value: string): boolean => {
|
|||||||
const renameTroop = async (troop: Troop) => {
|
const renameTroop = async (troop: Troop) => {
|
||||||
const troopName = editName.value;
|
const troopName = editName.value;
|
||||||
const particle = hasFinalConsonant(troopName) ? '으로' : '로';
|
const particle = hasFinalConsonant(troopName) ? '으로' : '로';
|
||||||
if (!window.confirm(`${troop.name} 부대의 이름을 ${troopName}${particle} 바꾸시겠습니까?`)) {
|
if (!(await showConfirm(`${troop.name} 부대의 이름을 ${troopName}${particle} 바꾸시겠습니까?`))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await runAction(async () => {
|
await runAction(async () => {
|
||||||
@@ -147,7 +147,7 @@ const kickMember = async (troop: Troop) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const particle = hasFinalConsonant(member.name) ? '을' : '를';
|
const particle = hasFinalConsonant(member.name) ? '을' : '를';
|
||||||
if (!window.confirm(`${troop.name} 부대에서 ${member.name}${particle} 추방하시겠습니까?`)) {
|
if (!(await showConfirm(`${troop.name} 부대에서 ${member.name}${particle} 추방하시겠습니까?`))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await runAction(async () => {
|
await runAction(async () => {
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ void test('queues acknowledgement dialogs and resolves each request in order', a
|
|||||||
title: '완료',
|
title: '완료',
|
||||||
message: '다음 확인',
|
message: '다음 확인',
|
||||||
acknowledgeLabel: '계속',
|
acknowledgeLabel: '계속',
|
||||||
|
cancelLabel: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
feedback.acknowledgeDialog();
|
feedback.acknowledgeDialog();
|
||||||
@@ -54,3 +55,24 @@ void test('queues acknowledgement dialogs and resolves each request in order', a
|
|||||||
assert.equal(secondResolved, true);
|
assert.equal(secondResolved, true);
|
||||||
assert.equal(feedback.dialog.value, null);
|
assert.equal(feedback.dialog.value, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
void test('confirmation dialogs resolve accept, cancel, and escape-safe cancellation distinctly', async () => {
|
||||||
|
const feedback = createGameFeedbackStore();
|
||||||
|
|
||||||
|
const accepted = feedback.confirm({ message: '저장할까요?', acknowledgeLabel: '저장' });
|
||||||
|
assert.deepEqual(feedback.dialog.value, {
|
||||||
|
id: 1,
|
||||||
|
kind: 'info',
|
||||||
|
title: '확인',
|
||||||
|
message: '저장할까요?',
|
||||||
|
acknowledgeLabel: '저장',
|
||||||
|
cancelLabel: '취소',
|
||||||
|
});
|
||||||
|
feedback.acknowledgeDialog();
|
||||||
|
assert.equal(await accepted, true);
|
||||||
|
|
||||||
|
const cancelled = feedback.confirm('해산할까요?');
|
||||||
|
feedback.cancelDialog();
|
||||||
|
assert.equal(await cancelled, false);
|
||||||
|
assert.equal(feedback.dialog.value, null);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user