Merge branch 'main' into fix/main-global-menu-npc-reply-20260812
This commit is contained in:
@@ -130,7 +130,12 @@ const emptyMessages = {
|
|||||||
canRespondDiplomacy: false,
|
canRespondDiplomacy: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const install = async (page: Page, mode: 'member' | 'wanderer' | 'admin' = 'member', trade: number | null = 100) => {
|
const install = async (
|
||||||
|
page: Page,
|
||||||
|
mode: 'member' | 'wanderer' | 'admin' = 'member',
|
||||||
|
trade: number | null = 100,
|
||||||
|
globalNationCount = 2
|
||||||
|
) => {
|
||||||
await page.addInitScript((profile) => {
|
await page.addInitScript((profile) => {
|
||||||
localStorage.setItem('sammo-game-token', 'ga_info');
|
localStorage.setItem('sammo-game-token', 'ga_info');
|
||||||
localStorage.setItem('sammo-game-profile', profile);
|
localStorage.setItem('sammo-game-profile', profile);
|
||||||
@@ -250,7 +255,7 @@ const install = async (page: Page, mode: 'member' | 'wanderer' | 'admin' = 'memb
|
|||||||
generalCount: 1,
|
generalCount: 1,
|
||||||
cities: ['허창'],
|
cities: ['허창'],
|
||||||
},
|
},
|
||||||
],
|
].slice(0, globalNationCount),
|
||||||
diplomacy: { 1: { 1: 2, 2: 0 }, 2: { 1: 0, 2: 2 } },
|
diplomacy: { 1: { 1: 2, 2: 0 }, 2: { 1: 0, 2: 2 } },
|
||||||
conflict: [],
|
conflict: [],
|
||||||
map,
|
map,
|
||||||
@@ -375,6 +380,24 @@ test('global-info renders the ref nation summary columns beside the map', async
|
|||||||
await page.setViewportSize({ width: 1200, height: 900 });
|
await page.setViewportSize({ width: 1200, height: 900 });
|
||||||
await go(page, 'global-info');
|
await go(page, 'global-info');
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(async () => {
|
||||||
|
const [matrixWrapBox, matrixBox] = await Promise.all([
|
||||||
|
page.locator('.matrix-wrap').boundingBox(),
|
||||||
|
page.locator('.matrix').boundingBox(),
|
||||||
|
]);
|
||||||
|
if (!matrixWrapBox || !matrixBox) return null;
|
||||||
|
return Math.abs(matrixWrapBox.height - matrixBox.height);
|
||||||
|
})
|
||||||
|
.toBeLessThan(1);
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(() =>
|
||||||
|
page
|
||||||
|
.locator('.map-area .city-icon')
|
||||||
|
.evaluateAll((images: HTMLImageElement[]) => images.every((image) => image.complete && image.naturalWidth > 0))
|
||||||
|
)
|
||||||
|
.toBe(true);
|
||||||
const castleGeometry = await page.locator('.map-area').evaluate((mapArea) => {
|
const castleGeometry = await page.locator('.map-area').evaluate((mapArea) => {
|
||||||
const mapRect = mapArea.getBoundingClientRect();
|
const mapRect = mapArea.getBoundingClientRect();
|
||||||
return Array.from(mapArea.querySelectorAll<HTMLImageElement>('.city-icon')).map((image) => {
|
return Array.from(mapArea.querySelectorAll<HTMLImageElement>('.city-icon')).map((image) => {
|
||||||
@@ -494,6 +517,62 @@ test('global-info renders the ref nation summary columns beside the map', async
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('global-info diplomacy height follows the active nation count', async ({ page }) => {
|
||||||
|
await install(page, 'member', 100, 1);
|
||||||
|
await go(page, 'global-info');
|
||||||
|
|
||||||
|
const matrixWrap = page.locator('.matrix-wrap');
|
||||||
|
const matrix = page.locator('.matrix');
|
||||||
|
const mapSection = page.locator('.map-section');
|
||||||
|
await expect(matrix.locator('tbody tr')).toHaveCount(1);
|
||||||
|
|
||||||
|
for (const viewport of [
|
||||||
|
{ name: 'desktop', width: 1200, height: 900 },
|
||||||
|
{ name: 'mobile', width: 390, height: 844 },
|
||||||
|
]) {
|
||||||
|
await page.setViewportSize(viewport);
|
||||||
|
await expect
|
||||||
|
.poll(async () => {
|
||||||
|
const [wrapBox, matrixBox] = await Promise.all([matrixWrap.boundingBox(), matrix.boundingBox()]);
|
||||||
|
if (!wrapBox || !matrixBox) return null;
|
||||||
|
return Math.abs(wrapBox.height - matrixBox.height);
|
||||||
|
})
|
||||||
|
.toBeLessThan(1);
|
||||||
|
|
||||||
|
const geometry = await page.evaluate(() => {
|
||||||
|
const rect = (selector: string) => document.querySelector(selector)?.getBoundingClientRect();
|
||||||
|
const diplomacy = rect('.section');
|
||||||
|
const matrix = rect('.matrix');
|
||||||
|
const matrixWrap = rect('.matrix-wrap');
|
||||||
|
const mapSection = rect('.map-section');
|
||||||
|
return {
|
||||||
|
diplomacyHeight: diplomacy?.height ?? null,
|
||||||
|
matrixHeight: matrix?.height ?? null,
|
||||||
|
matrixWrapHeight: matrixWrap?.height ?? null,
|
||||||
|
gapToMap: matrixWrap && mapSection ? mapSection.top - matrixWrap.bottom : null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
expect(geometry.matrixHeight).not.toBeNull();
|
||||||
|
expect(geometry.matrixWrapHeight).toBeCloseTo(geometry.matrixHeight!, 0);
|
||||||
|
expect(geometry.diplomacyHeight).toBeLessThan(200);
|
||||||
|
expect(geometry.gapToMap).toBe(21);
|
||||||
|
await expect(mapSection).toBeInViewport();
|
||||||
|
|
||||||
|
if (artifactRoot) {
|
||||||
|
await mkdir(artifactRoot, { recursive: true });
|
||||||
|
await writeFile(
|
||||||
|
resolve(artifactRoot, `core-global-info-one-nation-${viewport.name}.json`),
|
||||||
|
`${JSON.stringify(geometry, null, 2)}\n`,
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
await page.screenshot({
|
||||||
|
path: resolve(artifactRoot, `core-global-info-one-nation-${viewport.name}.png`),
|
||||||
|
fullPage: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('current-city hides values and general rows for a wandering user', async ({ page }) => {
|
test('current-city hides values and general rows for a wandering user', async ({ page }) => {
|
||||||
await install(page, 'wanderer');
|
await install(page, 'wanderer');
|
||||||
await go(page, 'current-city');
|
await go(page, 'current-city');
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import MapViewer from '../components/main/MapViewer.vue';
|
import MapViewer from '../components/main/MapViewer.vue';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
@@ -10,6 +10,8 @@ type Layout = Awaited<ReturnType<typeof trpc.world.getMapLayout.query>>;
|
|||||||
const data = ref<Result | null>(null);
|
const data = ref<Result | null>(null);
|
||||||
const layout = ref<Layout | null>(null);
|
const layout = ref<Layout | null>(null);
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
|
const matrixElement = ref<HTMLTableElement | null>(null);
|
||||||
|
const matrixHeight = ref<number | null>(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const goBack = () => router.push('/');
|
const goBack = () => router.push('/');
|
||||||
const state = (value: number) => ({ 0: '★', 1: '▲', 2: '', 7: '@' })[value] ?? 'ㆍ';
|
const state = (value: number) => ({ 0: '★', 1: '▲', 2: '', 7: '@' })[value] ?? 'ㆍ';
|
||||||
@@ -19,6 +21,23 @@ const nationNameStyle = (color: string) => ({
|
|||||||
backgroundColor: color,
|
backgroundColor: color,
|
||||||
color: legacyNationTextColor(color),
|
color: legacyNationTextColor(color),
|
||||||
});
|
});
|
||||||
|
watch(
|
||||||
|
matrixElement,
|
||||||
|
(element, _previousElement, onCleanup) => {
|
||||||
|
if (!element) {
|
||||||
|
matrixHeight.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const updateHeight = () => {
|
||||||
|
matrixHeight.value = element.getBoundingClientRect().height;
|
||||||
|
};
|
||||||
|
const observer = new ResizeObserver(updateHeight);
|
||||||
|
observer.observe(element);
|
||||||
|
updateHeight();
|
||||||
|
onCleanup(() => observer.disconnect());
|
||||||
|
},
|
||||||
|
{ flush: 'post' }
|
||||||
|
);
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
[data.value, layout.value] = await Promise.all([
|
[data.value, layout.value] = await Promise.all([
|
||||||
@@ -39,8 +58,8 @@ onMounted(async () => {
|
|||||||
<p v-if="error" class="error">{{ error }}</p>
|
<p v-if="error" class="error">{{ error }}</p>
|
||||||
<section v-if="data" class="section">
|
<section v-if="data" class="section">
|
||||||
<h2 class="blue">외교 현황</h2>
|
<h2 class="blue">외교 현황</h2>
|
||||||
<div class="matrix-wrap">
|
<div class="matrix-wrap" :style="{ height: matrixHeight === null ? undefined : `${matrixHeight}px` }">
|
||||||
<table class="matrix">
|
<table ref="matrixElement" class="matrix">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
@@ -194,7 +213,6 @@ onMounted(async () => {
|
|||||||
background: green;
|
background: green;
|
||||||
}
|
}
|
||||||
.matrix-wrap {
|
.matrix-wrap {
|
||||||
height: 1212.5px;
|
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ const history = ref<HistoryData | null>(null);
|
|||||||
const selectedYearMonth = ref<number | null>(null);
|
const selectedYearMonth = ref<number | null>(null);
|
||||||
const settingsOpen = ref(false);
|
const settingsOpen = ref(false);
|
||||||
const rankingBottom = ref(localStorage.getItem('yearbook-ranking-bottom') === 'true');
|
const rankingBottom = ref(localStorage.getItem('yearbook-ranking-bottom') === 'true');
|
||||||
|
let historyRequestId = 0;
|
||||||
const serverID = computed(() => {
|
const serverID = computed(() => {
|
||||||
const value = route.query.serverID;
|
const value = route.query.serverID;
|
||||||
const raw = Array.isArray(value) ? value[0] : value;
|
const raw = Array.isArray(value) ? value[0] : value;
|
||||||
@@ -71,19 +72,27 @@ const loadHistory = async (): Promise<void> => {
|
|||||||
if (selectedYearMonth.value === null) {
|
if (selectedYearMonth.value === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const requestId = ++historyRequestId;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
errorMessage.value = '';
|
errorMessage.value = '';
|
||||||
try {
|
try {
|
||||||
const { year, month } = parseYearMonth(selectedYearMonth.value);
|
const { year, month } = parseYearMonth(selectedYearMonth.value);
|
||||||
const result = await trpc.yearbook.getHistory.query({ year, month, serverID: serverID.value });
|
const result = await trpc.yearbook.getHistory.query({ year, month, serverID: serverID.value });
|
||||||
|
if (requestId !== historyRequestId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if ('data' in result) {
|
if ('data' in result) {
|
||||||
history.value = result.data;
|
history.value = result.data;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
history.value = null;
|
if (requestId !== historyRequestId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
errorMessage.value = error instanceof Error ? error.message : '연감 데이터를 불러오지 못했습니다.';
|
errorMessage.value = error instanceof Error ? error.message : '연감 데이터를 불러오지 못했습니다.';
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
if (requestId === historyRequestId) {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -129,7 +138,13 @@ onMounted(async () => {
|
|||||||
<strong>연 감</strong>
|
<strong>연 감</strong>
|
||||||
<button class="legacy-button close-button" type="button" @click="closePage">창 닫기</button>
|
<button class="legacy-button close-button" type="button" @click="closePage">창 닫기</button>
|
||||||
<span class="settings-menu">
|
<span class="settings-menu">
|
||||||
<button class="legacy-button legacy-button--navigation" type="button" @click="settingsOpen = !settingsOpen">⚙ 설정⌄</button>
|
<button
|
||||||
|
class="legacy-button legacy-button--navigation"
|
||||||
|
type="button"
|
||||||
|
@click="settingsOpen = !settingsOpen"
|
||||||
|
>
|
||||||
|
⚙ 설정⌄
|
||||||
|
</button>
|
||||||
<button v-if="settingsOpen" class="settings-item" type="button" @click="toggleRankingPosition">
|
<button v-if="settingsOpen" class="settings-item" type="button" @click="toggleRankingPosition">
|
||||||
국가 순서 위치 변경(모바일 전용)
|
국가 순서 위치 변경(모바일 전용)
|
||||||
</button>
|
</button>
|
||||||
@@ -164,9 +179,9 @@ onMounted(async () => {
|
|||||||
<div v-if="errorMessage" class="yearbook-message error" role="alert">{{ errorMessage }}</div>
|
<div v-if="errorMessage" class="yearbook-message error" role="alert">{{ errorMessage }}</div>
|
||||||
<div v-else-if="loading && !history" class="yearbook-message">불러오는 중...</div>
|
<div v-else-if="loading && !history" class="yearbook-message">불러오는 중...</div>
|
||||||
|
|
||||||
<section v-if="history" :class="['history-grid', { 'ranking-bottom': rankingBottom }]">
|
<section v-if="history" :class="['history-grid', { 'ranking-bottom': rankingBottom }]" :aria-busy="loading">
|
||||||
<div class="map-position">
|
<div class="map-position">
|
||||||
<MapViewer :map-data="history.map" :map-layout="mapLayout" :loading="loading" />
|
<MapViewer :map-data="history.map" :map-layout="mapLayout" :loading="loading && !history" />
|
||||||
</div>
|
</div>
|
||||||
<aside class="nation-position">
|
<aside class="nation-position">
|
||||||
<table>
|
<table>
|
||||||
|
|||||||
@@ -28,7 +28,16 @@ const fulfillTrpc = async (route: Route, results: unknown[]): Promise<void> => {
|
|||||||
|
|
||||||
type LobbyFixtureOptions = {
|
type LobbyFixtureOptions = {
|
||||||
authenticated?: boolean;
|
authenticated?: boolean;
|
||||||
|
roles?: string[];
|
||||||
|
kakaoVerified?: boolean;
|
||||||
canCreateGeneral?: boolean;
|
canCreateGeneral?: boolean;
|
||||||
|
requiresKakaoVerification?: boolean;
|
||||||
|
specialAccess?: {
|
||||||
|
kind: 'OPERATOR' | 'TESTER' | 'RECOVERY' | 'OTHER';
|
||||||
|
grantId: string | null;
|
||||||
|
expiresAt: string | null;
|
||||||
|
allowsGeneralCreation: boolean;
|
||||||
|
} | null;
|
||||||
myGeneral?: {
|
myGeneral?: {
|
||||||
name: string;
|
name: string;
|
||||||
picture: string;
|
picture: string;
|
||||||
@@ -48,7 +57,11 @@ type LobbyFixtureOptions = {
|
|||||||
const installFixture = async (page: Page, options: LobbyFixtureOptions = {}) => {
|
const installFixture = async (page: Page, options: LobbyFixtureOptions = {}) => {
|
||||||
const {
|
const {
|
||||||
authenticated = true,
|
authenticated = true,
|
||||||
|
roles = ['user'],
|
||||||
|
kakaoVerified = true,
|
||||||
canCreateGeneral = true,
|
canCreateGeneral = true,
|
||||||
|
requiresKakaoVerification = false,
|
||||||
|
specialAccess = null,
|
||||||
myGeneral = {
|
myGeneral = {
|
||||||
name: '선택장수',
|
name: '선택장수',
|
||||||
picture: 'users/core2026/account-hash.png',
|
picture: 'users/core2026/account-hash.png',
|
||||||
@@ -79,8 +92,8 @@ const installFixture = async (page: Page, options: LobbyFixtureOptions = {}) =>
|
|||||||
id: 'lobby-user',
|
id: 'lobby-user',
|
||||||
username: 'lobby-user',
|
username: 'lobby-user',
|
||||||
displayName: '로비사용자',
|
displayName: '로비사용자',
|
||||||
roles: ['user'],
|
roles,
|
||||||
kakaoVerified: true,
|
kakaoVerified,
|
||||||
createdAt: '2026-07-30T00:00:00.000Z',
|
createdAt: '2026-07-30T00:00:00.000Z',
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
@@ -109,8 +122,9 @@ const installFixture = async (page: Page, options: LobbyFixtureOptions = {}) =>
|
|||||||
localAccountPolicy: {
|
localAccountPolicy: {
|
||||||
accessAllowed: true,
|
accessAllowed: true,
|
||||||
canCreateGeneral,
|
canCreateGeneral,
|
||||||
requiresKakaoVerification: false,
|
requiresKakaoVerification,
|
||||||
graceEndsAt: null,
|
graceEndsAt: null,
|
||||||
|
specialAccess,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -216,6 +230,51 @@ test('applies the signed general-acquisition policy to both create and possessio
|
|||||||
await expect(row.getByRole('button', { name: '장수빙의' })).toBeDisabled();
|
await expect(row.getByRole('button', { name: '장수빙의' })).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('shows the Kakao verification banner when a profile still requires verification', async ({ page }) => {
|
||||||
|
await installFixture(page, {
|
||||||
|
kakaoVerified: false,
|
||||||
|
requiresKakaoVerification: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto('lobby');
|
||||||
|
await expect(page.getByText('카카오 인증이 필요합니다.')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hides the Kakao verification banner for operator special access', async ({ page }) => {
|
||||||
|
await installFixture(page, {
|
||||||
|
roles: ['superuser'],
|
||||||
|
kakaoVerified: false,
|
||||||
|
specialAccess: {
|
||||||
|
kind: 'OPERATOR',
|
||||||
|
grantId: null,
|
||||||
|
expiresAt: null,
|
||||||
|
allowsGeneralCreation: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto('lobby');
|
||||||
|
await expect(page.getByText('특수 접근 · OPERATOR')).toBeVisible();
|
||||||
|
await expect(page.getByText('카카오 인증이 필요합니다.')).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hides the Kakao verification banner when a grant removes the remaining verification requirement', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await installFixture(page, {
|
||||||
|
kakaoVerified: false,
|
||||||
|
specialAccess: {
|
||||||
|
kind: 'RECOVERY',
|
||||||
|
grantId: '11111111-1111-4111-8111-111111111111',
|
||||||
|
expiresAt: '2026-08-20T00:00:00.000Z',
|
||||||
|
allowsGeneralCreation: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto('lobby');
|
||||||
|
await expect(page.getByText('특수 접근 · RECOVERY')).toBeVisible();
|
||||||
|
await expect(page.getByText('카카오 인증이 필요합니다.')).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
test('opens the profile root without profile or game token query parameters', async ({ page }) => {
|
test('opens the profile root without profile or game token query parameters', async ({ page }) => {
|
||||||
await installFixture(page);
|
await installFixture(page);
|
||||||
await page.route('**/hwe/', async (route) => {
|
await page.route('**/hwe/', async (route) => {
|
||||||
|
|||||||
@@ -46,7 +46,12 @@ const canAccessAdmin = computed(
|
|||||||
role === 'superuser' || role === 'admin' || role === 'admin.superuser' || role.startsWith('admin.')
|
role === 'superuser' || role === 'admin' || role === 'admin.superuser' || role.startsWith('admin.')
|
||||||
) ?? false
|
) ?? false
|
||||||
);
|
);
|
||||||
const needsKakaoVerification = computed(() => me.value !== null && !me.value.kakaoVerified);
|
const needsKakaoVerification = computed(
|
||||||
|
() =>
|
||||||
|
me.value !== null &&
|
||||||
|
!me.value.kakaoVerified &&
|
||||||
|
profiles.value.some((profile) => profile.localAccountPolicy?.requiresKakaoVerification === true)
|
||||||
|
);
|
||||||
const userIconBaseUrl = configuredUserIconPublicUrl();
|
const userIconBaseUrl = configuredUserIconPublicUrl();
|
||||||
const sharedIconBaseUrl = configuredSharedIconPublicUrl();
|
const sharedIconBaseUrl = configuredSharedIconPublicUrl();
|
||||||
const publicMapProfiles = computed(() =>
|
const publicMapProfiles = computed(() =>
|
||||||
|
|||||||
@@ -232,8 +232,24 @@ export const canonicalFrontendFixture = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
nations: [
|
nations: [
|
||||||
{ id: 1, name: '한', color: '#d32f2f', level: 5, power: 1250, cities: ['낙양', '업'] },
|
{
|
||||||
{ id: 2, name: '진', color: '#1976d2', level: 4, power: 980, cities: ['장안'] },
|
id: 1,
|
||||||
|
name: '한',
|
||||||
|
color: '#d32f2f',
|
||||||
|
level: 5,
|
||||||
|
power: 1250,
|
||||||
|
generalCount: 12,
|
||||||
|
cities: ['낙양', '업'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: '진',
|
||||||
|
color: '#1976d2',
|
||||||
|
level: 4,
|
||||||
|
power: 980,
|
||||||
|
generalCount: 8,
|
||||||
|
cities: ['장안'],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
globalHistory: ['<C>●</> 한이 낙양을 지키고 있습니다.'],
|
globalHistory: ['<C>●</> 한이 낙양을 지키고 있습니다.'],
|
||||||
globalAction: ['<L>●</> 유비가 내정을 수행했습니다.'],
|
globalAction: ['<L>●</> 유비가 내정을 수행했습니다.'],
|
||||||
|
|||||||
@@ -971,6 +971,108 @@ test.describe('yearbook legacy parity', () => {
|
|||||||
await expect(page.getByRole('alert')).toBeVisible();
|
await expect(page.getByRole('alert')).toBeVisible();
|
||||||
await expect(page.getByLabel('연월 선택')).toBeVisible();
|
await expect(page.getByLabel('연월 선택')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('keeps the rendered yearbook in place while moving between months', async ({ page }) => {
|
||||||
|
await page.goto(gameUrl('/yearbook'));
|
||||||
|
await expect(page.getByText('한이 낙양을 지키고 있습니다.')).toBeVisible();
|
||||||
|
|
||||||
|
let releaseHistory: (() => void) | undefined;
|
||||||
|
const historyReleased = new Promise<void>((resolve) => {
|
||||||
|
releaseHistory = resolve;
|
||||||
|
});
|
||||||
|
let markHistoryRequested: (() => void) | undefined;
|
||||||
|
const historyRequested = new Promise<void>((resolve) => {
|
||||||
|
markHistoryRequested = resolve;
|
||||||
|
});
|
||||||
|
await page.route('**/che/api/trpc/**', async (route) => {
|
||||||
|
if (!operationNames(route).includes('yearbook.getHistory')) {
|
||||||
|
await route.fallback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
markHistoryRequested?.();
|
||||||
|
await historyReleased;
|
||||||
|
await fulfillOperations(route, () => ({
|
||||||
|
notModified: false,
|
||||||
|
hash: 'yearbook-previous-month-hash',
|
||||||
|
data: {
|
||||||
|
...fixture.game.yearbook.data,
|
||||||
|
year: 197,
|
||||||
|
month: 6,
|
||||||
|
map: {
|
||||||
|
...fixture.game.yearbook.data.map,
|
||||||
|
year: 197,
|
||||||
|
month: 6,
|
||||||
|
},
|
||||||
|
nations: fixture.game.yearbook.data.nations.map((nation, index) => ({
|
||||||
|
...nation,
|
||||||
|
generalCount: index === 0 ? 12 : 8,
|
||||||
|
})),
|
||||||
|
globalHistory: ['<C>●</> 이전 달의 중원 정세입니다.'],
|
||||||
|
globalAction: ['<L>●</> 이전 달의 장수 동향입니다.'],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const grid = document.querySelector<HTMLElement>('.history-grid')!;
|
||||||
|
const transition = {
|
||||||
|
grid,
|
||||||
|
mapBody: document.querySelector<HTMLElement>('.map-body')!,
|
||||||
|
nationBody: document.querySelector<HTMLElement>('.nation-position tbody')!,
|
||||||
|
removedNodes: 0,
|
||||||
|
};
|
||||||
|
new MutationObserver((records) => {
|
||||||
|
transition.removedNodes += records.reduce((count, record) => count + record.removedNodes.length, 0);
|
||||||
|
}).observe(grid, { childList: true, subtree: true });
|
||||||
|
(window as unknown as { __yearbookTransition: typeof transition }).__yearbookTransition = transition;
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: '◀ 이전달' }).click();
|
||||||
|
await historyRequested;
|
||||||
|
|
||||||
|
await expect(page.locator('.history-grid')).toHaveAttribute('aria-busy', 'true');
|
||||||
|
await expect(page.locator('.map-body')).toHaveCount(1);
|
||||||
|
await expect(page.locator('.map-viewer .skeleton-lines')).toHaveCount(0);
|
||||||
|
await expect(page.getByText('한이 낙양을 지키고 있습니다.')).toBeVisible();
|
||||||
|
expect(
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const transition = (
|
||||||
|
window as unknown as {
|
||||||
|
__yearbookTransition: {
|
||||||
|
grid: Element;
|
||||||
|
mapBody: Element;
|
||||||
|
nationBody: Element;
|
||||||
|
removedNodes: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
).__yearbookTransition;
|
||||||
|
return {
|
||||||
|
gridIsSame: document.querySelector('.history-grid') === transition.grid,
|
||||||
|
mapIsSame: document.querySelector('.map-body') === transition.mapBody,
|
||||||
|
nationIsSame: document.querySelector('.nation-position tbody') === transition.nationBody,
|
||||||
|
removedNodes: transition.removedNodes,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
).toEqual({ gridIsSame: true, mapIsSame: true, nationIsSame: true, removedNodes: 0 });
|
||||||
|
|
||||||
|
releaseHistory?.();
|
||||||
|
await expect(page.getByText('이전 달의 중원 정세입니다.')).toBeVisible();
|
||||||
|
await expect(page.locator('.history-grid')).toHaveAttribute('aria-busy', 'false');
|
||||||
|
expect(
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const transition = (
|
||||||
|
window as unknown as {
|
||||||
|
__yearbookTransition: { grid: Element; mapBody: Element; nationBody: Element };
|
||||||
|
}
|
||||||
|
).__yearbookTransition;
|
||||||
|
return {
|
||||||
|
gridIsSame: document.querySelector('.history-grid') === transition.grid,
|
||||||
|
mapIsSame: document.querySelector('.map-body') === transition.mapBody,
|
||||||
|
nationIsSame: document.querySelector('.nation-position tbody') === transition.nationBody,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
).toEqual({ gridIsSame: true, mapIsSame: true, nationIsSame: true });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('survey legacy parity', () => {
|
test.describe('survey legacy parity', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user