설문 제목과 알림을 닫을 수 있는 새 창으로 열기

This commit is contained in:
2026-09-07 12:42:27 +00:00
parent bdb4e22028
commit d57fd3e553
3 changed files with 89 additions and 57 deletions
+86 -54
View File
@@ -1,6 +1,6 @@
import { mkdir, readFile, writeFile } from 'node:fs/promises'; import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { resolve } from 'node:path'; import { resolve } from 'node:path';
import { expect, test, type Locator, type Page, type Route } from '@playwright/test'; import { expect, test, type BrowserContext, type Locator, type Page, type Route } from '@playwright/test';
import { buildEquipmentTradeItemOptions, loadEquipmentTradeItemOrder } from '../../game-api/src/turns/commandInput.js'; import { buildEquipmentTradeItemOptions, loadEquipmentTradeItemOrder } from '../../game-api/src/turns/commandInput.js';
import { loadScenarioDefinitionById } from '@sammo-ts/game-engine/scenario/scenarioLoader.js'; import { loadScenarioDefinitionById } from '@sammo-ts/game-engine/scenario/scenarioLoader.js';
@@ -554,7 +554,7 @@ const generalContext = (state: NavigationFixture) => ({
penalties: {}, penalties: {},
}); });
const installFixture = async (page: Page, state: NavigationFixture) => { const installFixture = async (page: Page | BrowserContext, state: NavigationFixture) => {
await page.addInitScript( await page.addInitScript(
({ profile }) => { ({ profile }) => {
localStorage.setItem('sammo-game-token', 'ga_navigation'); localStorage.setItem('sammo-game-token', 'ga_navigation');
@@ -625,6 +625,8 @@ const installFixture = async (page: Page, state: NavigationFixture) => {
} }
const results = operations.map((operation, index) => { const results = operations.map((operation, index) => {
if (operation === 'auth.status') return response({ ok: true }); if (operation === 'auth.status') return response({ ok: true });
if (operation === 'vote.getVoteList') return response({ polls: [], voteReward: 150 });
if (operation === 'vote.getAdminStatus') return response({ ok: false });
if (operation === 'lobby.info') { if (operation === 'lobby.info') {
return response({ return response({
myGeneral: { id: 7, name: '메뉴검증장수' }, myGeneral: { id: 7, name: '메뉴검증장수' },
@@ -1280,59 +1282,89 @@ test('keeps the survey footer close button the same size as the top close button
} }
}); });
test('closes the survey popup from both close buttons without navigating the opener', async ({ page }, testInfo) => { for (const entry of ['menu', 'status', 'notice'] as const) {
const state: NavigationFixture = { test(`closes the survey popup opened from ${entry} without navigating the opener`, async ({
officerLevel: 0, page,
permission: 0, context,
nationLevel: 0, }, testInfo) => {
stage: 0, const state: NavigationFixture = {
npcMode: 1, officerLevel: 0,
generalMeCalls: 0, permission: 0,
operations: [], nationLevel: 0,
}; stage: 0,
await installFixture(page, state); npcMode: 1,
await waitForMain(page); generalMeCalls: 0,
const openerUrl = page.url(); operations: [],
for (const width of [1000, 500]) { };
for (const bar of ['back_bar', 'bottom_bar']) { await installFixture(context, state);
const popupPromise = page.waitForEvent('popup'); await waitForMain(page);
await page.evaluate(() => window.open('about:blank', '_blank')); const openerUrl = page.url();
const popup = await popupPromise; for (const width of [1000, 500]) {
await installFixture(popup, state); await page.setViewportSize({ width, height: 900 });
await popup.setViewportSize({ width, height: 900 }); for (const bar of ['back_bar', 'bottom_bar']) {
await popup.goto(`${basePath}/survey`); const link =
const button = popup.locator(`.${bar} .back_btn`); entry === 'menu'
await expect(button).toHaveText('창 닫기'); ? page.locator('[data-menu-position="top"] [data-navigation-id="survey"]')
await popup.evaluate(() => document.fonts.ready); : entry === 'status'
await button.focus(); ? page.locator('.vote-status a')
await expect(button).toBeFocused(); : page.locator('.survey-notice a');
await button.hover(); if (entry === 'notice' && width === 500) {
const artifactName = `survey-close-${width}-${bar}`; const noticeRect = await page.locator('.survey-notice').boundingBox();
await popup.screenshot({ path: testInfo.outputPath(`${artifactName}.png`), fullPage: true }); const menuRect = await page.locator('.main-mobile-bottom').boundingBox();
await writeFile( expect(noticeRect!.y + noticeRect!.height).toBeLessThanOrEqual(menuRect!.y - 16);
testInfo.outputPath(`${artifactName}.html`), await page.screenshot({ path: testInfo.outputPath(`survey-notice-${width}-${bar}.png`) });
await popup.locator('.pageVote').evaluate((el) => el.outerHTML) await writeFile(
); testInfo.outputPath(`survey-notice-${width}-${bar}.json`),
await writeFile( JSON.stringify({ noticeRect, menuRect }, null, 2)
testInfo.outputPath(`${artifactName}.json`), );
JSON.stringify( }
await button.evaluate((el) => ({ const popupPromise = page.waitForEvent('popup', { timeout: 5_000 });
rect: el.getBoundingClientRect().toJSON(), await link.click();
style: { height: getComputedStyle(el).height, font: getComputedStyle(el).font }, const popup = await popupPromise;
})), await popup.waitForLoadState('domcontentloaded');
null, await popup.setViewportSize({ width, height: 900 });
2 await expect(popup).toHaveURL(`${basePath}/survey`);
) expect(await popup.evaluate(() => window.opener === null)).toBe(true);
); if (bar === 'bottom_bar') await popup.reload();
const closed = popup.waitForEvent('close'); const button = popup.locator(`.${bar} .back_btn`);
await button.click(); await expect(button).toHaveText('창 닫기');
await closed; await popup.evaluate(() => document.fonts.ready);
expect(popup.isClosed()).toBe(true); await button.focus();
expect(page.isClosed()).toBe(false); await expect(button).toBeFocused();
expect(page.url()).toBe(openerUrl); await button.hover();
const artifactName = `survey-close-${width}-${bar}`;
await popup.screenshot({ path: testInfo.outputPath(`${artifactName}.png`), fullPage: true });
await writeFile(
testInfo.outputPath(`${artifactName}.html`),
await popup.locator('.pageVote').evaluate((el) => el.outerHTML)
);
await writeFile(
testInfo.outputPath(`${artifactName}.json`),
JSON.stringify(
await button.evaluate((el) => ({
rect: el.getBoundingClientRect().toJSON(),
openerIsNull: window.opener === null,
historyLength: window.history.length,
documentWidth: document.documentElement.scrollWidth,
style: { height: getComputedStyle(el).height, font: getComputedStyle(el).font },
})),
null,
2
)
);
const closed = popup.waitForEvent('close');
// 실제 창이 닫히면 click 응답보다 CDP target 종료가 먼저 도착할 수 있다.
await button.click().catch((error: unknown) => {
if (!popup.isClosed()) throw error;
});
await closed;
expect(popup.isClosed()).toBe(true);
expect(page.isClosed()).toBe(false);
expect(page.url()).toBe(openerUrl);
}
} }
} });
}); }
test('notifies only for a new incoming private message and marks it read from the notice', async ({ test('notifies only for a new incoming private message and marks it read from the notice', async ({
page, page,
@@ -132,7 +132,7 @@ onUnmounted(() => {
</RouterLink> </RouterLink>
</div> </div>
<div class="status-row vote-status"> <div class="status-row vote-status">
<RouterLink v-if="status?.latestVote" to="/survey"> <RouterLink v-if="status?.latestVote" to="/survey" target="_blank" rel="noopener noreferrer">
<span class="vote-label">설문: </span>{{ status.latestVote.title }} <span class="vote-label">설문: </span>{{ status.latestVote.title }}
</RouterLink> </RouterLink>
<span v-else class="vote-empty">설문: 진행 중인 설문 없음</span> <span v-else class="vote-empty">설문: 진행 중인 설문 없음</span>
+2 -2
View File
@@ -321,7 +321,7 @@ watch(
<strong>설문조사 안내</strong> <strong>설문조사 안내</strong>
<button type="button" aria-label="설문조사 알림 닫기" @click="dashboard.dismissSurveyNotice">×</button> <button type="button" aria-label="설문조사 알림 닫기" @click="dashboard.dismissSurveyNotice">×</button>
</div> </div>
<RouterLink to="/survey">새로운 설문조사가 있습니다.</RouterLink> <RouterLink to="/survey" target="_blank" rel="noopener noreferrer">새로운 설문조사가 있습니다.</RouterLink>
</aside> </aside>
<aside <aside
@@ -1133,7 +1133,7 @@ button {
.survey-notice { .survey-notice {
z-index: 90; z-index: 90;
bottom: 16px; bottom: 61px;
} }
.private-message-notice { .private-message-notice {