feat(game-ui): add action feedback layer
This commit is contained in:
@@ -392,7 +392,7 @@ 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 }) => {
|
||||
test('retains article and comment input after a failed mutation, then reloads after success', async ({ page }, testInfo) => {
|
||||
const state: BoardFixture = {
|
||||
permission: 2,
|
||||
canMeeting: true,
|
||||
@@ -407,25 +407,54 @@ 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('새 내용');
|
||||
page.once('dialog', async (dialog) => {
|
||||
expect(dialog.message()).toBe('실패했습니다. :접속 제한입니다.');
|
||||
await dialog.accept();
|
||||
});
|
||||
await page.locator('#submitArticle').click();
|
||||
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('새 제목');
|
||||
await expect(page.locator('#board-content')).toHaveValue('새 내용');
|
||||
|
||||
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 };
|
||||
});
|
||||
expect(desktopToastGeometry.left).toBeGreaterThanOrEqual(0);
|
||||
expect(desktopToastGeometry.right).toBeLessThanOrEqual(desktopToastGeometry.viewportWidth);
|
||||
expect(desktopToastGeometry.top).toBeGreaterThanOrEqual(0);
|
||||
expect(desktopToastGeometry.width).toBeGreaterThan(250);
|
||||
await page.screenshot({ path: testInfo.outputPath('game-toast-desktop.png'), fullPage: true });
|
||||
await articleToast.getByRole('button', { name: '알림 닫기' }).click();
|
||||
await expect(articleToast).toHaveCount(0);
|
||||
|
||||
await page.locator('#submitArticle').click();
|
||||
await expect(page.getByText('새 제목', { exact: true })).toBeVisible();
|
||||
await expect(page.locator('#board-title')).toHaveValue('');
|
||||
|
||||
const commentInput = page.locator('.comment-input').first();
|
||||
await commentInput.fill('새 댓글');
|
||||
page.once('dialog', async (dialog) => {
|
||||
expect(dialog.message()).toBe('실패했습니다: 접속 제한입니다.');
|
||||
await dialog.accept();
|
||||
});
|
||||
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: '댓글 등록에 실패했습니다: 접속 제한입니다.' });
|
||||
await expect(commentToast).toBeVisible();
|
||||
await expect
|
||||
.poll(async () => commentToast.evaluate((element) => window.innerHeight - element.getBoundingClientRect().bottom))
|
||||
.toBeGreaterThanOrEqual(0);
|
||||
const mobileToastGeometry = await commentToast.evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return {
|
||||
left: rect.left,
|
||||
right: window.innerWidth - rect.right,
|
||||
bottom: window.innerHeight - rect.bottom,
|
||||
viewportWidth: window.innerWidth,
|
||||
documentWidth: document.documentElement.scrollWidth,
|
||||
};
|
||||
});
|
||||
expect(mobileToastGeometry.left).toBeGreaterThanOrEqual(0);
|
||||
expect(mobileToastGeometry.right).toBeGreaterThanOrEqual(0);
|
||||
expect(mobileToastGeometry.bottom).toBeGreaterThanOrEqual(0);
|
||||
expect(mobileToastGeometry.documentWidth).toBe(documentWidthBeforeToast);
|
||||
await page.screenshot({ path: testInfo.outputPath('game-toast-mobile.png') });
|
||||
await expect(commentInput).toHaveValue('새 댓글');
|
||||
|
||||
await commentInput.press('Enter');
|
||||
|
||||
@@ -838,9 +838,7 @@ test('내 정보 즉시행동은 timeout 재시도 ID를 유지하고 성공 후
|
||||
|
||||
await instantRetreatButton.click();
|
||||
await expect.poll(() => state.instantRetreatInputs?.length).toBe(1);
|
||||
await expect
|
||||
.poll(() => dialogs.some((message) => message.includes('요청 처리 결과를 확인하지 못했습니다.')))
|
||||
.toBe(true);
|
||||
await expect(page.getByTestId('game-toast')).toContainText('요청 처리 결과를 확인하지 못했습니다.');
|
||||
await expect.poll(() => state.generalMeQueries).toBe(2);
|
||||
await expect.poll(() => state.ensurePrestartQueries).toBe(2);
|
||||
|
||||
@@ -948,13 +946,9 @@ test('가오픈 장수 삭제는 레거시 표시와 확인을 보존하고 time
|
||||
const timeoutReload = page.waitForEvent('framenavigated', (frame) => frame === page.mainFrame());
|
||||
await deleteButton.click();
|
||||
await expect.poll(() => state.dieOnPrestartInputs?.length).toBe(1);
|
||||
await expect
|
||||
.poll(() =>
|
||||
dialogs.some((message) =>
|
||||
message.includes('alert:실패했습니다: 요청은 접수됐지만 처리 결과를 아직 확인하지 못했습니다.')
|
||||
)
|
||||
)
|
||||
.toBe(true);
|
||||
const failureDialog = page.getByRole('alertdialog', { name: '장수 삭제 실패' });
|
||||
await expect(failureDialog).toContainText('요청은 접수됐지만 처리 결과를 아직 확인하지 못했습니다.');
|
||||
await failureDialog.getByRole('button', { name: '확인' }).click();
|
||||
await timeoutReload;
|
||||
await page.waitForLoadState('networkidle');
|
||||
await expect(deleteButton).toBeVisible();
|
||||
|
||||
@@ -106,6 +106,13 @@ const installFixture = async (page: Page, state: FixtureState): Promise<void> =>
|
||||
body: '<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><rect width="64" height="64" fill="#777"/></svg>',
|
||||
});
|
||||
});
|
||||
await page.route('https://sam-image.hided.net/icons/**', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'image/svg+xml',
|
||||
body: '<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><rect width="64" height="64" fill="#777"/></svg>',
|
||||
});
|
||||
});
|
||||
await page.route('**/gateway/api/user-icons/default.jpg', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
@@ -265,7 +272,10 @@ test('renders Ref-shaped token cards, preserves keep cooldown and retries posses
|
||||
await tooltip.focus();
|
||||
await expect(tooltip).toBeFocused();
|
||||
await expect(tooltipPopup).toHaveText('안전을 중시합니다.');
|
||||
await expect(page.locator('.npc-card-image').first()).toHaveAttribute('src', '/gateway/api/user-icons/default.jpg');
|
||||
await expect(page.locator('.npc-card-image').first()).toHaveAttribute(
|
||||
'src',
|
||||
'https://sam-image.hided.net/icons/default.jpg'
|
||||
);
|
||||
|
||||
await page.locator('#btn-load-general-list').click();
|
||||
await expect(page.locator('#tb-general-list')).toBeVisible();
|
||||
@@ -347,11 +357,31 @@ test('renders Ref-shaped token cards, preserves keep cooldown and retries posses
|
||||
await expect(page.locator('.npc-token-expired')).toBeVisible();
|
||||
await expect(possessButton).toBeEnabled();
|
||||
await expect(refreshButton).toBeDisabled();
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
const documentWidthBeforeDialog = await page.evaluate(() => document.documentElement.scrollWidth);
|
||||
await page.locator('#btn-retry-possession').click();
|
||||
const successDialog = page.getByRole('alertdialog', { name: '완료' });
|
||||
await expect(successDialog).toContainText('빙의에 성공했습니다.');
|
||||
await expect(successDialog.getByRole('button', { name: '확인' })).toBeFocused();
|
||||
const dialogGeometry = await successDialog.evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return {
|
||||
left: rect.left,
|
||||
right: window.innerWidth - rect.right,
|
||||
bottom: window.innerHeight - rect.bottom,
|
||||
width: rect.width,
|
||||
documentWidth: document.documentElement.scrollWidth,
|
||||
};
|
||||
});
|
||||
expect(dialogGeometry.left).toBeGreaterThanOrEqual(0);
|
||||
expect(dialogGeometry.right).toBeGreaterThanOrEqual(0);
|
||||
expect(dialogGeometry.bottom).toBeGreaterThanOrEqual(0);
|
||||
expect(dialogGeometry.documentWidth).toBe(documentWidthBeforeDialog);
|
||||
await page.screenshot({ path: testInfo.outputPath('game-notice-dialog.png') });
|
||||
await successDialog.getByRole('button', { name: '확인' }).click();
|
||||
await expect(page).toHaveURL(new RegExp(`${basePath}/$`));
|
||||
expect(state.possessInputs).toHaveLength(2);
|
||||
expect(state.possessInputs[1]?.clientRequestId).toBe(firstRequestId);
|
||||
expect(dialogs).toContain('빙의에 성공했습니다.');
|
||||
expect(await page.evaluate(() => window.sessionStorage.getItem('sammo-npc-possess-pending-action'))).toBeNull();
|
||||
|
||||
await page.screenshot({
|
||||
|
||||
@@ -216,13 +216,15 @@ test.describe('NPC possession through live PostgreSQL, Redis, API, daemon, and C
|
||||
|
||||
await startDaemon();
|
||||
await possessButton.click();
|
||||
const successDialog = page.getByRole('alertdialog', { name: '완료' });
|
||||
await expect(successDialog).toContainText('빙의에 성공했습니다.');
|
||||
await successDialog.getByRole('button', { name: '확인' }).click();
|
||||
await expect(page).toHaveURL(/\/hwe\/$/);
|
||||
expect(requestIds).toHaveLength(2);
|
||||
expect(requestIds[1]).toBe(requestIds[0]);
|
||||
expect(await db.general.count({ where: { userId } })).toBe(1);
|
||||
await expect(db.npcSelectionToken.findUnique({ where: { ownerUserId: userId } })).resolves.toBeNull();
|
||||
expect(await page.evaluate(() => window.sessionStorage.getItem('sammo-npc-possess-pending-action'))).toBeNull();
|
||||
expect(dialogs).toContain('빙의에 성공했습니다.');
|
||||
const event = await db.inputEvent.findUniqueOrThrow({
|
||||
where: { requestId: eventRequestId },
|
||||
});
|
||||
|
||||
@@ -406,8 +406,8 @@ test.describe('scenario 903 live selection pool', () => {
|
||||
await expect(page.locator('.selected-card')).toHaveCount(1);
|
||||
await page.locator('.custom-form select').selectOption('che_안전');
|
||||
await page.locator('#build-general').click();
|
||||
await expect.poll(() => dialogs).toContain(
|
||||
'실패했습니다: 장수 선택 요청은 접수됐지만 처리 결과를 아직 확인하지 못했습니다. 같은 요청으로 다시 시도해 주세요.'
|
||||
await expect(page.getByTestId('game-toast')).toContainText(
|
||||
'장수 생성에 실패했습니다: 장수 선택 요청은 접수됐지만 처리 결과를 아직 확인하지 못했습니다. 같은 요청으로 다시 시도해 주세요.'
|
||||
);
|
||||
await waitForPool(page);
|
||||
const retryCard = page
|
||||
@@ -417,9 +417,11 @@ test.describe('scenario 903 live selection pool', () => {
|
||||
await retryCard.locator('.select-button').click();
|
||||
await page.locator('.custom-form select').selectOption('che_안전');
|
||||
await page.locator('#build-general').click();
|
||||
const createDialog = page.getByRole('alertdialog', { name: '완료' });
|
||||
await expect(createDialog).toContainText('선택한 장수로 생성했습니다.');
|
||||
await createDialog.getByRole('button', { name: '확인' }).click();
|
||||
await expect(page).toHaveURL(/\/hwe\/$/);
|
||||
expect(dialogs.filter((message) => message === '이 장수로 생성할까요?')).toHaveLength(2);
|
||||
await expect.poll(() => dialogs).toContain('선택한 장수로 생성했습니다.');
|
||||
expect(createClientRequestIds).toHaveLength(2);
|
||||
expect(createClientRequestIds[1]).toBe(createClientRequestIds[0]);
|
||||
|
||||
@@ -452,7 +454,9 @@ test.describe('scenario 903 live selection pool', () => {
|
||||
|
||||
dialogs.length = 0;
|
||||
await page.goto('select-general');
|
||||
await expect.poll(() => dialogs).toContain('실패했습니다: 아직 다시 고를 수 없습니다');
|
||||
await expect(page.getByTestId('game-toast')).toContainText(
|
||||
'장수 선택 정보를 불러오지 못했습니다: 아직 다시 고를 수 없습니다'
|
||||
);
|
||||
await expect(page.locator('.error-text')).toHaveText('아직 다시 고를 수 없습니다');
|
||||
|
||||
const availableAt = '2026-07-29T00:00:00.000Z';
|
||||
@@ -495,9 +499,11 @@ test.describe('scenario 903 live selection pool', () => {
|
||||
expect(targetIndex).toBeGreaterThanOrEqual(0);
|
||||
const targetName = names[targetIndex]!.trim();
|
||||
await cards.nth(targetIndex).locator('.select-button').click();
|
||||
const reselectDialog = page.getByRole('alertdialog', { name: '완료' });
|
||||
await expect(reselectDialog).toContainText('선택한 장수로 변경했습니다.');
|
||||
await reselectDialog.getByRole('button', { name: '확인' }).click();
|
||||
await expect(page).toHaveURL(/\/hwe\/$/);
|
||||
await expect.poll(() => dialogs).toContain(`이 장수를 선택할까요? : ${targetName}`);
|
||||
await expect.poll(() => dialogs).toContain('선택한 장수로 변경했습니다.');
|
||||
|
||||
await expect
|
||||
.poll(async () => (await db.general.findUniqueOrThrow({ where: { id: created.id } })).name)
|
||||
|
||||
Reference in New Issue
Block a user