merge: 최신 main을 브라우저 전투 시뮬레이터 작업에 통합
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { resolve } from 'node:path';
|
||||
import { expect, test, type Page, type Route } from '@playwright/test';
|
||||
import { expect, test, type Locator, type Page, type Route } from '@playwright/test';
|
||||
|
||||
const response = (data: unknown) => ({ result: { data } });
|
||||
const errorResponse = (path: string, message: string) => ({
|
||||
@@ -97,6 +97,7 @@ const readModelInvalidation = (
|
||||
reservedTurns: boolean;
|
||||
records: boolean;
|
||||
frontStatus: boolean;
|
||||
tournament: boolean;
|
||||
}>
|
||||
) => ({
|
||||
context: false,
|
||||
@@ -108,6 +109,7 @@ const readModelInvalidation = (
|
||||
reservedTurns: false,
|
||||
records: false,
|
||||
frontStatus: false,
|
||||
tournament: false,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
@@ -619,6 +621,97 @@ const gridColumnCount = async (page: Page, selector: string) =>
|
||||
.first()
|
||||
.evaluate((element) => getComputedStyle(element).gridTemplateColumns.split(' ').length);
|
||||
|
||||
const raisedButtonState = async (target: Locator) =>
|
||||
target.evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const style = getComputedStyle(element);
|
||||
return {
|
||||
top: rect.top,
|
||||
bottom: rect.bottom,
|
||||
height: rect.height,
|
||||
backgroundColor: style.backgroundColor,
|
||||
borderTopWidth: style.borderTopWidth,
|
||||
borderLeftWidth: style.borderLeftWidth,
|
||||
borderBottomWidth: style.borderBottomWidth,
|
||||
borderBottomColor: style.borderBottomColor,
|
||||
borderRadius: style.borderRadius,
|
||||
marginTop: style.marginTop,
|
||||
paddingTop: style.paddingTop,
|
||||
paddingBottom: style.paddingBottom,
|
||||
classNames: [...element.classList],
|
||||
};
|
||||
});
|
||||
|
||||
const pointerDownButtonState = async (page: Page, target: Locator) => {
|
||||
const box = await target.boundingBox();
|
||||
if (!box) throw new Error('raised button is not measurable');
|
||||
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||||
await page.mouse.down();
|
||||
const state = await raisedButtonState(target);
|
||||
await page.mouse.move(1, 1);
|
||||
await page.mouse.up();
|
||||
return state;
|
||||
};
|
||||
|
||||
const persistEnlargedRaisedButtonProbe = async (page: Page, source: Locator, name: string) => {
|
||||
if (!artifactRoot) return;
|
||||
const target = resolve(artifactRoot);
|
||||
await mkdir(target, { recursive: true });
|
||||
const probeId = `raised-button-probe-${name}`;
|
||||
const hostId = `${probeId}-host`;
|
||||
await source.evaluate(
|
||||
(element, ids) => {
|
||||
const host = document.createElement('div');
|
||||
host.id = ids.hostId;
|
||||
Object.assign(host.style, {
|
||||
position: 'fixed',
|
||||
inset: '20px auto auto 20px',
|
||||
width: '390px',
|
||||
height: '170px',
|
||||
padding: '10px',
|
||||
background: '#000',
|
||||
zIndex: '2147483647',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
const stage = document.createElement('div');
|
||||
Object.assign(stage.style, {
|
||||
display: 'flow-root',
|
||||
width: '90px',
|
||||
transform: 'scale(4)',
|
||||
transformOrigin: 'top left',
|
||||
});
|
||||
const probe = element.cloneNode(true) as HTMLElement;
|
||||
probe.id = ids.probeId;
|
||||
probe.classList.remove('active');
|
||||
probe.removeAttribute('disabled');
|
||||
probe.style.width = '90px';
|
||||
stage.append(probe);
|
||||
host.append(stage);
|
||||
document.body.append(host);
|
||||
},
|
||||
{ hostId, probeId }
|
||||
);
|
||||
|
||||
const host = page.locator(`#${hostId}`);
|
||||
const probe = page.locator(`#${probeId}`);
|
||||
const states: Record<string, Awaited<ReturnType<typeof raisedButtonState>>> = {};
|
||||
states.default = await raisedButtonState(probe);
|
||||
await host.screenshot({ path: resolve(target, `${name}-large-default.png`) });
|
||||
await probe.hover();
|
||||
states.hover = await raisedButtonState(probe);
|
||||
await host.screenshot({ path: resolve(target, `${name}-large-hover.png`) });
|
||||
const box = await probe.boundingBox();
|
||||
if (!box) throw new Error('enlarged raised button is not measurable');
|
||||
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||||
await page.mouse.down();
|
||||
states.pointerDown = await raisedButtonState(probe);
|
||||
await host.screenshot({ path: resolve(target, `${name}-large-pointer-down.png`) });
|
||||
await page.mouse.move(1, 1);
|
||||
await page.mouse.up();
|
||||
await writeFile(resolve(target, `${name}-large-states.json`), `${JSON.stringify(states, null, 2)}\n`);
|
||||
await host.evaluate((element) => element.remove());
|
||||
};
|
||||
|
||||
const persistArtifact = async (page: Page, name: string) => {
|
||||
if (!artifactRoot) return;
|
||||
const target = resolve(artifactRoot);
|
||||
@@ -1033,7 +1126,7 @@ test('pure NPC message senders are not rendered as reply targets', async ({ page
|
||||
await persistArtifact(page, `${basePath.slice(1)}-npc-reply-targets-desktop-1200`);
|
||||
});
|
||||
|
||||
test('main reserved-turn picker renders the Ref general category order', async ({ page }) => {
|
||||
test('main reserved-turn picker renders the Ref category order and raised button depth', async ({ page }) => {
|
||||
const state: NavigationFixture = {
|
||||
officerLevel: 1,
|
||||
permission: 0,
|
||||
@@ -1081,16 +1174,68 @@ test('main reserved-turn picker renders the Ref general category order', async (
|
||||
expect(desktopGeometry.columns.split(' ')).toHaveLength(3);
|
||||
expect(desktopGeometry.rows).toBe(2);
|
||||
expect(desktopGeometry.horizontalOverflow).toBeLessThanOrEqual(0);
|
||||
expect(desktopGeometry.categoryButton).toEqual({ height: 32, paddingTop: '6px', paddingBottom: '6px' });
|
||||
expect(desktopGeometry.categoryButton).toEqual({ height: 35.5, paddingTop: '5.25px', paddingBottom: '5.25px' });
|
||||
expect(desktopGeometry.commandButton).toEqual(desktopGeometry.categoryButton);
|
||||
|
||||
const strategyCategory = picker.getByRole('button', { name: '계략', exact: true });
|
||||
await page.mouse.move(1, 1);
|
||||
const categoryDefault = await raisedButtonState(strategyCategory);
|
||||
expect(categoryDefault).toMatchObject({
|
||||
height: 35.5,
|
||||
backgroundColor: 'rgb(23, 61, 39)',
|
||||
borderTopWidth: '0px',
|
||||
borderLeftWidth: '1px',
|
||||
borderBottomWidth: '4px',
|
||||
borderBottomColor: 'rgb(21, 55, 35)',
|
||||
borderRadius: '5.25px',
|
||||
marginTop: '0px',
|
||||
classNames: expect.arrayContaining(['legacy-button', 'legacy-button--lumen']),
|
||||
});
|
||||
await strategyCategory.hover();
|
||||
const categoryHover = await raisedButtonState(strategyCategory);
|
||||
expect(categoryHover).toMatchObject({ height: 34.5, borderBottomWidth: '3px', marginTop: '1px' });
|
||||
expect(categoryHover.top).toBe(categoryDefault.top + 1);
|
||||
expect(categoryHover.bottom).toBe(categoryDefault.bottom);
|
||||
const categoryPointerDown = await pointerDownButtonState(page, strategyCategory);
|
||||
expect(categoryPointerDown).toMatchObject({ height: 33.5, borderBottomWidth: '2px', marginTop: '2px' });
|
||||
expect(categoryPointerDown.top).toBe(categoryDefault.top + 2);
|
||||
expect(categoryPointerDown.bottom).toBe(categoryDefault.bottom);
|
||||
await page.keyboard.press('Tab');
|
||||
await strategyCategory.focus();
|
||||
await expect(strategyCategory).toBeFocused();
|
||||
await expect.poll(() => strategyCategory.evaluate((element) => element.matches(':focus-visible'))).toBe(true);
|
||||
await strategyCategory.click();
|
||||
await expect(strategyCategory).toHaveClass(/active/);
|
||||
await expect(picker.locator('.command-item')).toHaveText(['화계']);
|
||||
await page.mouse.move(1, 1);
|
||||
const commandButton = picker.locator('.command-item').first();
|
||||
const commandDefault = await raisedButtonState(commandButton);
|
||||
expect(commandDefault).toMatchObject({
|
||||
height: 35.5,
|
||||
backgroundColor: 'rgb(48, 32, 22)',
|
||||
borderTopWidth: '0px',
|
||||
borderLeftWidth: '1px',
|
||||
borderBottomWidth: '4px',
|
||||
borderBottomColor: 'rgb(43, 29, 20)',
|
||||
borderRadius: '5.25px',
|
||||
marginTop: '0px',
|
||||
classNames: expect.arrayContaining(['legacy-button', 'legacy-button--lumen']),
|
||||
});
|
||||
await commandButton.hover();
|
||||
const commandHover = await raisedButtonState(commandButton);
|
||||
expect(commandHover).toMatchObject({ height: 34.5, borderBottomWidth: '3px', marginTop: '1px' });
|
||||
expect(commandHover.top).toBe(commandDefault.top + 1);
|
||||
expect(commandHover.bottom).toBe(commandDefault.bottom);
|
||||
const commandPointerDown = await pointerDownButtonState(page, commandButton);
|
||||
expect(commandPointerDown).toMatchObject({ height: 33.5, borderBottomWidth: '2px', marginTop: '2px' });
|
||||
expect(commandPointerDown.top).toBe(commandDefault.top + 2);
|
||||
expect(commandPointerDown.bottom).toBe(commandDefault.bottom);
|
||||
await page.keyboard.press('Tab');
|
||||
await commandButton.focus();
|
||||
await expect(commandButton).toBeFocused();
|
||||
await expect.poll(() => commandButton.evaluate((element) => element.matches(':focus-visible'))).toBe(true);
|
||||
await persistEnlargedRaisedButtonProbe(page, strategyCategory, 'turn-selector-category');
|
||||
await persistEnlargedRaisedButtonProbe(page, commandButton, 'turn-selector-command');
|
||||
await persistArtifact(page, `${basePath.slice(1)}-main-reserved-ref-categories-desktop-1200`);
|
||||
|
||||
await page.setViewportSize({ width: 500, height: 900 });
|
||||
@@ -1118,7 +1263,11 @@ test('main reserved-turn picker renders the Ref general category order', async (
|
||||
};
|
||||
});
|
||||
expect(mobileGeometry.horizontalOverflow).toBeLessThanOrEqual(0);
|
||||
expect(mobileGeometry.categoryButton).toEqual({ height: 32, paddingTop: '6px', paddingBottom: '6px' });
|
||||
expect(mobileGeometry.categoryButton).toEqual({
|
||||
height: 35.5,
|
||||
paddingTop: '5.25px',
|
||||
paddingBottom: '5.25px',
|
||||
});
|
||||
expect(mobileGeometry.commandButton).toEqual(mobileGeometry.categoryButton);
|
||||
await persistArtifact(page, `${basePath.slice(1)}-main-reserved-ref-categories-mobile-500`);
|
||||
});
|
||||
@@ -1327,6 +1476,9 @@ test('main cards and command input stay inside their Ref-sized grid slots', asyn
|
||||
await page.locator('[data-main-target="commands"] .select-command').click();
|
||||
const picker = page.getByTestId('command-picker');
|
||||
await expect(picker).toBeVisible();
|
||||
// The trigger can end up directly above a newly opened category button.
|
||||
// Measure the default grid after leaving the intentional Lumen hover state.
|
||||
await page.mouse.move(1, 1);
|
||||
const pickerGeometry = await picker.evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const editor = element.closest('.reserved-command-editor')?.getBoundingClientRect();
|
||||
@@ -2150,6 +2302,22 @@ test('realtime read-model events skip clock-only work, merge bursts, patch in pl
|
||||
await page.setViewportSize({ width: 1200, height: 900 });
|
||||
await waitForMain(page);
|
||||
await expect(page.locator('.general-title')).toContainText('메뉴검증장수');
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(() => (window as unknown as { __hasMainRealtime: () => boolean }).__hasMainRealtime())
|
||||
)
|
||||
.toBe(true);
|
||||
await expect(page.locator('.tournament-status')).toHaveText('토너먼트: 경기 없음');
|
||||
await expect(page.locator('[data-navigation-id="tournament"]')).not.toHaveClass(/highlight/u);
|
||||
|
||||
const operationsBeforeTournament = state.operations.length;
|
||||
state.stage = 1;
|
||||
await emitReadModelInvalidation(page, readModelInvalidation({ tournament: true }));
|
||||
await expect
|
||||
.poll(() => state.operations.slice(operationsBeforeTournament), { timeout: 3_000 })
|
||||
.toEqual(['dashboard.getContextBundleDelta', 'tournament.getState']);
|
||||
await expect(page.locator('.tournament-status')).toHaveText('토너먼트: 참가 모집중');
|
||||
await expect(page.locator('[data-navigation-id="tournament"]')).toHaveClass(/highlight/u);
|
||||
|
||||
await page.evaluate(() => {
|
||||
const general = document.querySelector('[data-main-target="general"]');
|
||||
@@ -2202,6 +2370,7 @@ test('realtime read-model events skip clock-only work, merge bursts, patch in pl
|
||||
reservedTurns: false,
|
||||
records: false,
|
||||
frontStatus: false,
|
||||
tournament: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -2270,6 +2439,7 @@ test('realtime read-model events skip clock-only work, merge bursts, patch in pl
|
||||
reservedTurns: false,
|
||||
records: false,
|
||||
frontStatus: true,
|
||||
tournament: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -2427,6 +2597,7 @@ test('realtime read-model events skip clock-only work, merge bursts, patch in pl
|
||||
reservedTurns: false,
|
||||
records: false,
|
||||
frontStatus: false,
|
||||
tournament: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -2601,6 +2772,23 @@ test('same-account main tabs share one realtime diff and exclude a tab while syn
|
||||
const followerPage = pages[followerIndex];
|
||||
if (!leaderPage || !followerPage) throw new Error('realtime leader election failed');
|
||||
|
||||
const operationsBeforeTournament = state.operations.length;
|
||||
state.stage = 1;
|
||||
await emitReadModelInvalidation(leaderPage, readModelInvalidation({ tournament: true }));
|
||||
await expect
|
||||
.poll(() => state.operations.slice(operationsBeforeTournament), { timeout: 3_000 })
|
||||
.toEqual(['dashboard.getContextBundleDelta', 'tournament.getState']);
|
||||
await Promise.all(
|
||||
pages.map((currentPage) =>
|
||||
expect(currentPage.locator('.tournament-status')).toHaveText('토너먼트: 참가 모집중')
|
||||
)
|
||||
);
|
||||
await Promise.all(
|
||||
pages.map((currentPage) =>
|
||||
expect(currentPage.locator('[data-navigation-id="tournament"]')).toHaveClass(/highlight/u)
|
||||
)
|
||||
);
|
||||
|
||||
const callsBeforeSharedRefresh = state.generalMeCalls;
|
||||
state.generalName = '탭공유갱신장수';
|
||||
await leaderPage.evaluate(() => {
|
||||
@@ -2617,6 +2805,7 @@ test('same-account main tabs share one realtime diff and exclude a tab while syn
|
||||
reservedTurns: false,
|
||||
records: false,
|
||||
frontStatus: false,
|
||||
tournament: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -2644,6 +2833,7 @@ test('same-account main tabs share one realtime diff and exclude a tab while syn
|
||||
reservedTurns: false,
|
||||
records: false,
|
||||
frontStatus: false,
|
||||
tournament: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -130,7 +130,12 @@ const commandTitle = (command: CommandAvailability) =>
|
||||
<button
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
:class="['category-btn', { active: selectedCategory === category.id }]"
|
||||
:class="[
|
||||
'category-btn',
|
||||
'legacy-button',
|
||||
'legacy-button--lumen',
|
||||
{ active: selectedCategory === category.id },
|
||||
]"
|
||||
@click="selectedCategory = category.id"
|
||||
>
|
||||
{{ category.label }}
|
||||
@@ -143,6 +148,8 @@ const commandTitle = (command: CommandAvailability) =>
|
||||
:key="command.key"
|
||||
:class="[
|
||||
'command-item',
|
||||
'legacy-button',
|
||||
'legacy-button--lumen',
|
||||
command.status === 'available' ? 'ok' : '',
|
||||
command.status === 'blocked' ? 'blocked' : '',
|
||||
command.status === 'blocked' && props.allowBlocked ? 'reservable' : '',
|
||||
@@ -173,26 +180,19 @@ const commandTitle = (command: CommandAvailability) =>
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.category-btn,
|
||||
.command-item {
|
||||
min-height: 32px;
|
||||
padding-block: 6px;
|
||||
}
|
||||
|
||||
.category-btn {
|
||||
border: 0;
|
||||
border-right: 1px solid #666;
|
||||
border-bottom: 1px solid #666;
|
||||
.category-btn.legacy-button--lumen {
|
||||
--legacy-button-bg: #173d27;
|
||||
--legacy-button-border: #153723;
|
||||
--legacy-button-color: #fff;
|
||||
min-height: 0;
|
||||
padding-inline: 4px;
|
||||
background: #173d27;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.category-btn.active {
|
||||
background: #28633f;
|
||||
color: #ffe38a;
|
||||
.category-btn.legacy-button--lumen.active {
|
||||
--legacy-button-bg: #28633f;
|
||||
--legacy-button-border: #245939;
|
||||
--legacy-button-color: #ffe38a;
|
||||
}
|
||||
|
||||
.command-grid {
|
||||
@@ -201,19 +201,17 @@ const commandTitle = (command: CommandAvailability) =>
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.command-item {
|
||||
border: 0;
|
||||
border-right: 1px solid #666;
|
||||
border-bottom: 1px solid #666;
|
||||
.command-item.legacy-button--lumen {
|
||||
--legacy-button-bg: #302016 var(--sammo-texture-walnut);
|
||||
--legacy-button-border: #2b1d14;
|
||||
--legacy-button-color: #fff;
|
||||
min-height: 0;
|
||||
padding-inline: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #302016 var(--sammo-texture-walnut);
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.command-item.ok {
|
||||
|
||||
@@ -46,6 +46,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
type ReservedTurnView = Awaited<ReturnType<typeof trpc.turns.reserved.getGeneral.query>>['turns'][number];
|
||||
type RecentRecord = Awaited<ReturnType<typeof trpc.general.getRecentRecords.query>>['global'][number];
|
||||
type FrontStatus = Awaited<ReturnType<typeof trpc.general.getFrontStatus.query>>;
|
||||
type TournamentState = Awaited<ReturnType<typeof trpc.tournament.getState.query>>;
|
||||
type ContextBundleDelta = Awaited<ReturnType<typeof trpc.dashboard.getContextBundleDelta.query>>;
|
||||
type DashboardReadModelPatch = {
|
||||
contextSnapshot?: GeneralContext;
|
||||
@@ -72,6 +73,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
generalRecords?: RecentRecord[];
|
||||
worldHistory?: RecentRecord[];
|
||||
frontStatus?: FrontStatus | null;
|
||||
tournamentStage?: number;
|
||||
};
|
||||
type DashboardTabMessage =
|
||||
{ kind: 'patch'; patch: DashboardReadModelPatch } | { kind: 'status'; status: 'idle' | 'connected' };
|
||||
@@ -112,6 +114,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
const generalRecords = ref<RecentRecord[]>([]);
|
||||
const worldHistory = ref<RecentRecord[]>([]);
|
||||
const frontStatus = ref<FrontStatus | null>(null);
|
||||
const tournamentStage = ref(0);
|
||||
const surveyNotice = ref<NonNullable<FrontStatus['latestVote']> | null>(null);
|
||||
let lastGeneralRecordId = 0;
|
||||
let lastWorldHistoryId = 0;
|
||||
@@ -430,6 +433,9 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
} else if (patch.frontStatus !== undefined) {
|
||||
updateFrontStatus(patch.frontStatus);
|
||||
}
|
||||
if (patch.tournamentStage !== undefined) {
|
||||
tournamentStage.value = patch.tournamentStage;
|
||||
}
|
||||
if (patch.contextRevision !== undefined) {
|
||||
contextRevision = patch.contextRevision;
|
||||
contextSourceRevision = patch.contextSourceRevision ?? null;
|
||||
@@ -476,6 +482,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
patch.generalRecords = toRaw(generalRecords.value);
|
||||
patch.worldHistory = toRaw(worldHistory.value);
|
||||
patch.frontStatus = toRaw(frontStatus.value);
|
||||
patch.tournamentStage = tournamentStage.value;
|
||||
return patch;
|
||||
};
|
||||
|
||||
@@ -585,7 +592,8 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
frontStatusError.value = resolveErrorMessage(err);
|
||||
return null;
|
||||
});
|
||||
const [layout, lobby, map, messageData, contacts, generalTurns, records, nextFrontStatus] =
|
||||
const tournamentPromise = trpc.tournament.getState.query().catch(() => undefined);
|
||||
const [layout, lobby, map, messageData, contacts, generalTurns, records, nextFrontStatus, tournamentState] =
|
||||
await Promise.all([
|
||||
layoutPromise,
|
||||
trpc.lobby.info.query(),
|
||||
@@ -595,6 +603,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
generalTurnsPromise,
|
||||
recordsPromise,
|
||||
frontStatusPromise,
|
||||
tournamentPromise,
|
||||
]);
|
||||
|
||||
general.value = structurallyShare(general.value, context.general);
|
||||
@@ -617,6 +626,9 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
if (nextFrontStatus) {
|
||||
updateFrontStatus(nextFrontStatus);
|
||||
}
|
||||
if (tournamentState !== undefined) {
|
||||
tournamentStage.value = tournamentState?.stage ?? 0;
|
||||
}
|
||||
if (initializedMailboxGeneralId !== id) {
|
||||
targetMailbox.value = MESSAGE_MAILBOX_NATIONAL_BASE + context.general.nationId;
|
||||
initializedMailboxGeneralId = id;
|
||||
@@ -705,14 +717,18 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
return null;
|
||||
})
|
||||
: Promise.resolve(undefined);
|
||||
const tournamentPromise: Promise<TournamentState | undefined> = plan.tournament
|
||||
? trpc.tournament.getState.query().catch(() => undefined)
|
||||
: Promise.resolve(undefined);
|
||||
|
||||
const [lobby, map, contacts, generalTurns, records, nextFrontStatus] = await Promise.all([
|
||||
const [lobby, map, contacts, generalTurns, records, nextFrontStatus, tournamentState] = await Promise.all([
|
||||
lobbyPromise,
|
||||
mapPromise,
|
||||
contactsPromise,
|
||||
reservedPromise,
|
||||
recordsPromise,
|
||||
frontPromise,
|
||||
tournamentPromise,
|
||||
]);
|
||||
|
||||
const patch: DashboardReadModelPatch = { ...contextPatch };
|
||||
@@ -733,6 +749,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
patch.worldHistory = nextWorldHistory;
|
||||
}
|
||||
if (nextFrontStatus) patch.frontStatus = nextFrontStatus;
|
||||
if (tournamentState !== undefined) patch.tournamentStage = tournamentState?.stage ?? 0;
|
||||
applyDashboardPatch(patch);
|
||||
publishDashboardPatch(patch);
|
||||
} catch (err) {
|
||||
@@ -1241,6 +1258,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
generalRecords,
|
||||
worldHistory,
|
||||
frontStatus,
|
||||
tournamentStage,
|
||||
surveyNotice,
|
||||
messageDraftText,
|
||||
targetMailbox,
|
||||
|
||||
@@ -27,7 +27,6 @@ const session = useSessionStore();
|
||||
const dashboard = useMainDashboardStore();
|
||||
const isMobile = useMediaQuery('(max-width: 939.98px)');
|
||||
|
||||
const tournamentStage = ref(0);
|
||||
const npcMode = ref(0);
|
||||
|
||||
const {
|
||||
@@ -52,6 +51,7 @@ const {
|
||||
generalRecords,
|
||||
worldHistory,
|
||||
frontStatus,
|
||||
tournamentStage,
|
||||
surveyNotice,
|
||||
messageDraftText,
|
||||
targetMailbox,
|
||||
@@ -107,12 +107,10 @@ const repeatGeneralTurns = (amount: number) => {
|
||||
};
|
||||
|
||||
const loadMainData = async () => {
|
||||
const [, state, worldState] = await Promise.all([
|
||||
const [, worldState] = await Promise.all([
|
||||
dashboard.loadMainData(),
|
||||
trpc.tournament.getState.query().catch(() => null),
|
||||
trpc.world.getState.query().catch(() => null),
|
||||
]);
|
||||
tournamentStage.value = state?.stage ?? 0;
|
||||
npcMode.value = worldState?.config.npcMode ?? 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ void test('last-turn-time-only events do not schedule any dashboard query', () =
|
||||
reservedTurns: false,
|
||||
records: false,
|
||||
frontStatus: false,
|
||||
tournament: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,6 +47,7 @@ void test('selects only the read models affected by the current identity', () =>
|
||||
reservedTurns: true,
|
||||
records: true,
|
||||
frontStatus: false,
|
||||
tournament: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -79,6 +81,7 @@ void test('routes defence, tax-rate, and current-city-state events to their exac
|
||||
reservedTurns: false,
|
||||
records: false,
|
||||
frontStatus: false,
|
||||
tournament: false,
|
||||
});
|
||||
|
||||
const taxRate = resolveDashboardRefreshPlan(
|
||||
@@ -100,6 +103,7 @@ void test('routes defence, tax-rate, and current-city-state events to their exac
|
||||
reservedTurns: false,
|
||||
records: false,
|
||||
frontStatus: false,
|
||||
tournament: false,
|
||||
});
|
||||
|
||||
const cityState = resolveDashboardRefreshPlan(
|
||||
@@ -161,6 +165,7 @@ void test('refreshes only front status for a global survey projection change', (
|
||||
reservedTurns: false,
|
||||
records: false,
|
||||
frontStatus: true,
|
||||
tournament: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -174,8 +179,8 @@ void test('targets a submitted survey projection to its own general', () => {
|
||||
assert.equal(resolveDashboardRefreshPlan(changes, { generalId: 8, cityId: 3, nationId: 2 }).frontStatus, false);
|
||||
});
|
||||
|
||||
void test('keeps the access bundle projection-free for map, records, and front-status-only plans', () => {
|
||||
for (const slice of ['map', 'records', 'frontStatus'] as const) {
|
||||
void test('keeps the access bundle projection-free for independent read-model plans', () => {
|
||||
for (const slice of ['map', 'records', 'frontStatus', 'tournament'] as const) {
|
||||
const plan = { ...createEmptyRealtimeReadModelInvalidation(), [slice]: true };
|
||||
assert.deepEqual(resolveDashboardContextBundleInclude(plan), {
|
||||
context: false,
|
||||
|
||||
Reference in New Issue
Block a user