메인 연결 페이지의 탭 종료와 복귀 동작 통일
This commit is contained in:
@@ -209,6 +209,22 @@ const installApi = async (page: Page, state: BoardFixture) => {
|
||||
});
|
||||
return response({ id: 22 });
|
||||
}
|
||||
if (operation === 'dashboard.getContextBundleDelta') {
|
||||
const snapshot = (data: unknown, revision: string) => ({
|
||||
kind: 'snapshot',
|
||||
revision,
|
||||
sourceRevision: revision,
|
||||
data,
|
||||
});
|
||||
return response({
|
||||
context: snapshot(generalContext, 'board-context'),
|
||||
commandTable: snapshot({ general: [], nation: [] }, 'board-commands'),
|
||||
boardAccess: snapshot(
|
||||
{ permission: state.permission, canMeeting: state.canMeeting, canSecret: state.canSecret },
|
||||
'board-access'
|
||||
),
|
||||
});
|
||||
}
|
||||
if (operation === 'general.me') return response(generalContext);
|
||||
if (operation === 'world.getMapLayout') {
|
||||
return response({ mapName: 'che', cityList: [], regionMap: {}, levelMap: {} });
|
||||
@@ -498,7 +514,8 @@ test('denies direct secret-room rendering and disables its in-game menu for an o
|
||||
expect(state.requests).not.toContain('board.getArticles');
|
||||
|
||||
state.requests.length = 0;
|
||||
await page.goto('');
|
||||
await page.getByRole('button', { name: '돌아가기', exact: true }).click();
|
||||
await expect(page).toHaveURL(new URL(gamePath('/'), page.url()).href);
|
||||
const nationMenu = page.locator('.main-nation-menu').first();
|
||||
await expect(nationMenu.locator('[data-navigation-id="meeting"]')).toHaveAttribute('href', gamePath('/board'));
|
||||
await expect(nationMenu.locator('[data-navigation-id="secret-board"]')).toHaveAttribute('aria-disabled', 'true');
|
||||
|
||||
@@ -807,7 +807,7 @@ test('nation directory reuses only the public general-directory row on hover and
|
||||
expect(await foreignTrigger.getAttribute('aria-expanded')).toBe('true');
|
||||
expect(await foreignTrigger.evaluate((element) => getComputedStyle(element).outlineStyle)).toBe('dashed');
|
||||
|
||||
await page.getByRole('button', { name: '창 닫기' }).first().focus();
|
||||
await page.getByRole('button', { name: '돌아가기' }).first().focus();
|
||||
await expect(preview).toHaveCount(0);
|
||||
|
||||
if (artifactRoot) {
|
||||
|
||||
@@ -273,6 +273,8 @@ test('왕조 일람과 상세는 현재 profile의 이전 서버 기록만 조
|
||||
expect(state.dynastyRequests.some((request) => request.includes('legacy'))).toBe(true);
|
||||
expect(state.dynastyRequests.every((request) => !request.includes('sourceProfile'))).toBe(true);
|
||||
await page.screenshot({ path: testInfo.outputPath('dynasty-detail-profile-scope-desktop.png'), fullPage: true });
|
||||
await page.getByRole('button', { name: '돌아가기', exact: true }).last().click();
|
||||
await expect(page).toHaveURL(new URL('./', testInfo.project.use.baseURL as string).href);
|
||||
});
|
||||
|
||||
test('연감 국가 라벨은 밝은 배경에 검정, 어두운 배경에 흰 글자를 사용한다', async ({ page }, testInfo) => {
|
||||
|
||||
@@ -6,6 +6,9 @@ import { buildEquipmentTradeItemOptions, loadEquipmentTradeItemOrder } from '../
|
||||
import { loadScenarioDefinitionById } from '@sammo-ts/game-engine/scenario/scenarioLoader.js';
|
||||
import { ITEM_KEYS, loadItemModules } from '@sammo-ts/logic/items/index.js';
|
||||
|
||||
// 브라우저 보조키로 생성한 탭까지 검증하므로 경량 headless shell 대신 전체 Chromium을 사용한다.
|
||||
test.use({ channel: 'chromium' });
|
||||
|
||||
const response = (data: unknown) => ({ result: { data } });
|
||||
const runtimeNavigation = JSON.parse(
|
||||
await readFile(new URL('../../../resources/navigation.json', import.meta.url), 'utf8')
|
||||
@@ -29,6 +32,7 @@ const operationNames = (route: Route) =>
|
||||
decodeURIComponent(new URL(route.request().url()).pathname.split('/trpc/')[1] ?? '').split(',');
|
||||
|
||||
type NavigationFixture = {
|
||||
pageExitAudit?: boolean;
|
||||
decoratedNpcChiefs?: boolean;
|
||||
officerLevel: number;
|
||||
permission: number;
|
||||
@@ -567,6 +571,7 @@ const generalContext = (state: NavigationFixture) => ({
|
||||
const installFixture = async (page: Page | BrowserContext, state: NavigationFixture) => {
|
||||
await page.addInitScript(
|
||||
({ profile }) => {
|
||||
if (!window.location.protocol.startsWith('http')) return;
|
||||
localStorage.setItem('sammo-game-token', 'ga_navigation');
|
||||
localStorage.setItem('sammo-game-profile', profile);
|
||||
},
|
||||
@@ -838,6 +843,12 @@ const installFixture = async (page: Page | BrowserContext, state: NavigationFixt
|
||||
winnerId: state.tournamentWinnerId,
|
||||
});
|
||||
}
|
||||
if (state.pageExitAudit) return errorResponse(operation, 'Navigation audit fixture: data unavailable');
|
||||
if (operation === 'tournament.getRankings') return response([]);
|
||||
if (operation === 'tournament.getSnapshot') return response({ state: null, participants: [], matches: [] });
|
||||
if (operation === 'tournament.getBettingSummary')
|
||||
return response({ totals: {}, myTotals: {}, totalAmount: 0, myAmount: 0 });
|
||||
if (operation === 'tournament.getAdminStatus') return response({ ok: false });
|
||||
return response({ ok: true });
|
||||
});
|
||||
operations.forEach((operation, index) => {
|
||||
@@ -6881,3 +6892,246 @@ test('NPC prefixes stay single in the main nation card', async ({ page }, testIn
|
||||
await card.screenshot({ path: testInfo.outputPath(`npc-main-prefix-${width}.png`) });
|
||||
}
|
||||
});
|
||||
|
||||
test.describe('page exit navigation', () => {
|
||||
for (const width of [1000, 500]) {
|
||||
test(`page exit policy distinguishes ordinary, modifier, copied and same-tab links at ${width}`, async ({
|
||||
page,
|
||||
context,
|
||||
}, testInfo) => {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
await installFixture(context, {
|
||||
officerLevel: 12,
|
||||
permission: 4,
|
||||
nationLevel: 1,
|
||||
stage: 0,
|
||||
npcMode: 1,
|
||||
generalMeCalls: 0,
|
||||
operations: [],
|
||||
});
|
||||
await waitForMain(page);
|
||||
const mainUrl = page.url();
|
||||
const survey = page.locator('[data-menu-position="top"] [data-navigation-id="survey"]');
|
||||
const href = await survey.getAttribute('href');
|
||||
for (const method of ['control', 'middle', 'copy'] as const) {
|
||||
const created = context.waitForEvent('page');
|
||||
if (method === 'copy') {
|
||||
const copied = await context.newPage();
|
||||
await copied.goto(href!);
|
||||
} else {
|
||||
await survey.click(method === 'control' ? { modifiers: ['Control'] } : { button: 'middle' });
|
||||
}
|
||||
const tab = await created;
|
||||
await tab.bringToFront();
|
||||
await tab.waitForLoadState('domcontentloaded');
|
||||
await expect(tab).toHaveURL(`${basePath}/survey`);
|
||||
await expect(tab.locator('.back_bar .back_btn')).toHaveText('돌아가기');
|
||||
await tab.reload();
|
||||
await expect(tab.locator('.bottom_bar .back_btn')).toHaveText('돌아가기');
|
||||
expect(context.pages()).toHaveLength(2);
|
||||
await tab.locator('.bottom_bar .back_btn').click();
|
||||
await expect(tab).toHaveURL(mainUrl);
|
||||
expect(context.pages()).toHaveLength(2);
|
||||
await tab.close();
|
||||
}
|
||||
// The status title intentionally uses the current tab, unlike its menu entry.
|
||||
await page.locator('.tournament-status a').click();
|
||||
await expect(page).toHaveURL(`${basePath}/tournament`);
|
||||
expect(context.pages()).toHaveLength(1);
|
||||
await expect(page.getByRole('button', { name: '돌아가기', exact: true }).first()).toBeVisible();
|
||||
await page.screenshot({ path: testInfo.outputPath(`same-tab-${width}.png`), fullPage: true });
|
||||
await page.getByRole('button', { name: '돌아가기', exact: true }).last().click();
|
||||
await expect(page).toHaveURL(mainUrl);
|
||||
expect(context.pages()).toHaveLength(1);
|
||||
|
||||
// Retain the auxiliary identity through SPA navigation and a reload.
|
||||
const opened = page.waitForEvent('popup');
|
||||
await page.locator('[data-navigation-id="tournament"]').click();
|
||||
const tab = await opened;
|
||||
await expect(tab.getByRole('button', { name: '창 닫기', exact: true }).first()).toBeVisible();
|
||||
await tab.getByRole('tab', { name: '베팅장', exact: true }).click();
|
||||
await expect(tab).toHaveURL(`${basePath}/betting`);
|
||||
await tab.reload();
|
||||
await expect(tab.getByRole('button', { name: '창 닫기', exact: true }).last()).toBeVisible();
|
||||
const closed = tab.waitForEvent('close');
|
||||
await tab
|
||||
.getByRole('button', { name: '창 닫기', exact: true })
|
||||
.last()
|
||||
.click()
|
||||
.catch((error: unknown) => {
|
||||
if (!tab.isClosed()) throw error;
|
||||
});
|
||||
await closed;
|
||||
expect(context.pages()).toHaveLength(1);
|
||||
expect(page.url()).toBe(mainUrl);
|
||||
});
|
||||
}
|
||||
|
||||
const exitAuditEntries = [
|
||||
'nation-betting',
|
||||
'nation-list',
|
||||
'general-list',
|
||||
'best-general',
|
||||
'yearbook',
|
||||
'battle-simulator',
|
||||
'hall-of-fame',
|
||||
'dynasty',
|
||||
'traffic',
|
||||
'npc-list',
|
||||
'survey',
|
||||
'meeting',
|
||||
'secret-board',
|
||||
'troop',
|
||||
'diplomacy',
|
||||
'personnel',
|
||||
'finance',
|
||||
'chief-center',
|
||||
'npc-control',
|
||||
'nation-secret',
|
||||
'tournament',
|
||||
'tournament-menu',
|
||||
'betting',
|
||||
'nation-info',
|
||||
'nation-cities',
|
||||
'nation-generals',
|
||||
'global-info',
|
||||
'current-city',
|
||||
'battle-center',
|
||||
'inherit',
|
||||
'my-page',
|
||||
'auction-resource',
|
||||
'auction-resource-menu',
|
||||
'auction-unique',
|
||||
'my-settings',
|
||||
];
|
||||
for (const width of [1000, 500]) {
|
||||
test(`all main page exits obey tab policy even when page data fails at ${width}`, async ({
|
||||
page,
|
||||
context,
|
||||
}, testInfo) => {
|
||||
test.setTimeout(180_000);
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
await installFixture(context, {
|
||||
pageExitAudit: true,
|
||||
officerLevel: 12,
|
||||
permission: 4,
|
||||
nationLevel: 1,
|
||||
stage: 0,
|
||||
npcMode: 1,
|
||||
generalMeCalls: 0,
|
||||
operations: [],
|
||||
});
|
||||
await waitForMain(page);
|
||||
const mainUrl = page.url();
|
||||
const evidence: unknown[] = [];
|
||||
for (const id of exitAuditEntries) {
|
||||
let link = page.locator(`[data-navigation-id="${id}"]`).first();
|
||||
if (!(await link.isVisible())) {
|
||||
const group = ['tournament-menu', 'betting'].includes(id)
|
||||
? 'tournament-betting'
|
||||
: ['auction-resource-menu', 'auction-unique'].includes(id)
|
||||
? 'auction'
|
||||
: 'game-info';
|
||||
await page.locator(`[data-menu-id="${group}"]`).first().click();
|
||||
}
|
||||
link = page.locator(`[data-navigation-id="${id}"]`).first();
|
||||
const href = await link.getAttribute('href');
|
||||
const newTab = (await link.getAttribute('target')) === '_blank';
|
||||
const opened = newTab ? page.waitForEvent('popup') : null;
|
||||
await link.click();
|
||||
const target = opened ? await opened : page;
|
||||
await target.setViewportSize({ width, height: 900 });
|
||||
await target.waitForURL(new URL(href!, mainUrl).href);
|
||||
const exit = target.getByRole('button', { name: newTab ? '창 닫기' : '돌아가기', exact: true }).first();
|
||||
await expect(exit, id).toBeVisible();
|
||||
expect(context.pages(), id).toHaveLength(newTab ? 2 : 1);
|
||||
await target.evaluate(() => document.fonts.ready);
|
||||
await exit.focus();
|
||||
await exit.hover();
|
||||
evidence.push({
|
||||
id,
|
||||
href,
|
||||
newTab,
|
||||
width,
|
||||
...(await exit.evaluate((el) => ({
|
||||
viewportWidth: window.innerWidth,
|
||||
rect: el.getBoundingClientRect().toJSON(),
|
||||
font: getComputedStyle(el).font,
|
||||
border: getComputedStyle(el).border,
|
||||
text: el.textContent,
|
||||
}))),
|
||||
});
|
||||
await target.screenshot({ path: testInfo.outputPath(`${id}-${width}.png`), fullPage: true });
|
||||
await writeFile(
|
||||
testInfo.outputPath(`${id}-${width}.html`),
|
||||
await target.locator('#app').evaluate((el) => el.outerHTML)
|
||||
);
|
||||
if (newTab) {
|
||||
const closed = target.waitForEvent('close');
|
||||
await exit.click().catch((error: unknown) => {
|
||||
if (!target.isClosed()) throw error;
|
||||
});
|
||||
await closed;
|
||||
// Opening the original href from the browser does not inherit auxiliary identity.
|
||||
const direct = await context.newPage();
|
||||
await direct.setViewportSize({ width, height: 900 });
|
||||
await direct.goto(href!);
|
||||
const back = direct.getByRole('button', { name: '돌아가기', exact: true }).first();
|
||||
await expect(back, id).toBeVisible();
|
||||
await back.click();
|
||||
await expect(direct).toHaveURL(mainUrl);
|
||||
expect(context.pages()).toHaveLength(2);
|
||||
await direct.close();
|
||||
} else {
|
||||
await exit.click();
|
||||
await expect(page).toHaveURL(mainUrl);
|
||||
}
|
||||
expect(context.pages(), id).toHaveLength(1);
|
||||
expect(page.url(), id).toBe(mainUrl);
|
||||
}
|
||||
await writeFile(testInfo.outputPath(`exit-audit-${width}.json`), JSON.stringify(evidence, null, 2));
|
||||
});
|
||||
}
|
||||
|
||||
test('keyboard activation opens a closable tab and blocked popups use the current tab', async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
await installFixture(context, {
|
||||
officerLevel: 0,
|
||||
permission: 0,
|
||||
nationLevel: 0,
|
||||
stage: 0,
|
||||
npcMode: 1,
|
||||
generalMeCalls: 0,
|
||||
operations: [],
|
||||
});
|
||||
await waitForMain(page);
|
||||
const mainUrl = page.url();
|
||||
const link = page.locator('[data-menu-position="top"] [data-navigation-id="survey"]');
|
||||
await link.focus();
|
||||
const opened = page.waitForEvent('popup');
|
||||
await link.press('Enter');
|
||||
const tab = await opened;
|
||||
await expect(tab.getByRole('button', { name: '창 닫기', exact: true }).first()).toBeVisible();
|
||||
expect(await tab.evaluate(() => document.referrer)).toBe('');
|
||||
const closed = tab.waitForEvent('close');
|
||||
await tab
|
||||
.getByRole('button', { name: '창 닫기', exact: true })
|
||||
.first()
|
||||
.click()
|
||||
.catch((error: unknown) => {
|
||||
if (!tab.isClosed()) throw error;
|
||||
});
|
||||
await closed;
|
||||
expect(context.pages()).toHaveLength(1);
|
||||
await page.evaluate(() => {
|
||||
window.open = () => null;
|
||||
});
|
||||
await link.click();
|
||||
await expect(page).toHaveURL(`${basePath}/survey`);
|
||||
await page.getByRole('button', { name: '돌아가기', exact: true }).first().click();
|
||||
await expect(page).toHaveURL(mainUrl);
|
||||
expect(context.pages()).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -536,7 +536,7 @@ test('join refresh shows the assigned preliminary group immediately with accessi
|
||||
|
||||
const refresh = page.getByRole('button', { name: '갱신' });
|
||||
const join = page.getByRole('button', { name: '참가' });
|
||||
const close = page.getByRole('button', { name: '창 닫기' }).first();
|
||||
const close = page.getByRole('button', { name: '돌아가기' }).first();
|
||||
await expect(join).toBeEnabled();
|
||||
await expect(page.getByText('조별 예선 순위')).toBeVisible();
|
||||
await expect(page.getByText('조별 본선 순위')).toHaveCount(0);
|
||||
@@ -797,7 +797,7 @@ test('tournament and betting pages expose same-row navigation tabs beside close'
|
||||
const navigation = page.getByRole('tablist', { name: '토너먼트와 베팅장 이동' });
|
||||
const tournamentTab = navigation.getByRole('tab', { name: '토너먼트' });
|
||||
const bettingTab = navigation.getByRole('tab', { name: '베팅장' });
|
||||
const close = page.getByRole('button', { name: '창 닫기' }).first();
|
||||
const close = page.getByRole('button', { name: '돌아가기' }).first();
|
||||
await expect(tournamentTab).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
const headerCenters = await Promise.all(
|
||||
@@ -945,29 +945,7 @@ test('betting realtime refresh is shared across tabs and preserves local interac
|
||||
await expect(follower.locator('.mobile-bracket').getByLabel('관우 베팅 금액', { exact: true })).toHaveValue('50');
|
||||
});
|
||||
|
||||
test('tournament and betting close only their script-opened popup window', async ({ page }, testInfo) => {
|
||||
const baseURL = testInfo.project.use.baseURL;
|
||||
expect(typeof baseURL).toBe('string');
|
||||
await page.goto('about:blank');
|
||||
|
||||
for (const route of ['tournament', 'betting'] as const) {
|
||||
const popupPromise = page.waitForEvent('popup');
|
||||
await page.evaluate(() => window.open('about:blank', '_blank', 'noopener'));
|
||||
const popup = await popupPromise;
|
||||
await installFixture(popup, { tournamentStage: route === 'betting' ? 6 : 1 });
|
||||
await popup.goto(new URL(route, baseURL as string).href);
|
||||
|
||||
await expect(popup.getByRole('button', { name: '창 닫기' }).first()).toBeVisible();
|
||||
expect(await popup.evaluate(() => window.opener)).toBeNull();
|
||||
|
||||
const closed = popup.waitForEvent('close');
|
||||
await popup.getByRole('button', { name: '창 닫기' }).first().click();
|
||||
await closed;
|
||||
|
||||
expect(page.isClosed()).toBe(false);
|
||||
expect(page.url()).toBe('about:blank');
|
||||
}
|
||||
});
|
||||
// Actual main-link popup closure and tab counts are covered in mainNavigation.spec.ts.
|
||||
|
||||
test('betting bracket shows intelligence for debate tournament candidates', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
|
||||
Reference in New Issue
Block a user