Merge remote-tracking branch 'origin/main' into feat/ref-nation-card-20260813

This commit is contained in:
2026-08-13 17:58:01 +00:00
3 changed files with 139 additions and 9 deletions
+60 -1
View File
@@ -590,6 +590,8 @@ const persistArtifact = async (page: Page, name: string) => {
executionStatus: describe('.execution-status'), executionStatus: describe('.execution-status'),
tournamentStatus: describe('.tournament-status'), tournamentStatus: describe('.tournament-status'),
voteStatus: describe('.vote-status'), voteStatus: describe('.vote-status'),
autoRefresh: describe('[data-bottom-menu="auto-refresh"]'),
manualRefresh: describe('[data-bottom-menu="manual-refresh"]'),
commandMenu: describe('.reserved-command-editor details[open] .menu-items'), commandMenu: describe('.reserved-command-editor details[open] .menu-items'),
commandDividers: [ commandDividers: [
...document.querySelectorAll<HTMLElement>('.reserved-command-editor details[open] .menu-divider'), ...document.querySelectorAll<HTMLElement>('.reserved-command-editor details[open] .menu-divider'),
@@ -1515,6 +1517,7 @@ test('mobile single document refreshes once and preserves tokens on lobby return
generalMeCalls: 0, generalMeCalls: 0,
operations: [], operations: [],
}; };
await installRealtimeHarness(page);
await installFixture(page, state); await installFixture(page, state);
await page.setViewportSize({ width: 500, height: 900 }); await page.setViewportSize({ width: 500, height: 900 });
await waitForMain(page); await waitForMain(page);
@@ -1537,9 +1540,65 @@ test('mobile single document refreshes once and preserves tokens on lobby return
await expect(page.locator(selector)).toBeVisible(); await expect(page.locator(selector)).toBeVisible();
} }
const autoRefresh = page.getByRole('button', { name: '자동 갱신 ON' });
const manualRefresh = page.getByRole('button', { name: '직접 갱신' });
await expect(autoRefresh).toHaveAttribute('aria-pressed', 'true');
await expect(autoRefresh.locator('strong')).toHaveCSS('color', 'rgb(158, 240, 184)');
await expect(manualRefresh).toHaveAttribute('aria-busy', 'false');
await expect
.poll(() => page.evaluate(() => (window as unknown as { __hasMainRealtime: () => boolean }).__hasMainRealtime()))
.toBe(true);
const refreshGeometry = await page.locator('.bottom-refresh-controls').evaluate((controls) => {
const auto = controls.querySelector<HTMLElement>('[data-bottom-menu="auto-refresh"]');
const manual = controls.querySelector<HTMLElement>('[data-bottom-menu="manual-refresh"]');
if (!auto || !manual) throw new Error('mobile refresh controls are incomplete');
const controlsRect = controls.getBoundingClientRect();
const autoRect = auto.getBoundingClientRect();
const manualRect = manual.getBoundingClientRect();
return {
controls: { left: controlsRect.left, right: controlsRect.right, width: controlsRect.width },
auto: { left: autoRect.left, right: autoRect.right, width: autoRect.width },
manual: { left: manualRect.left, right: manualRect.right, width: manualRect.width },
overflow: controls.scrollWidth - controls.clientWidth,
};
});
expect(refreshGeometry.controls.width).toBe(125);
expect(refreshGeometry.auto.width).toBe(85);
expect(refreshGeometry.manual.width).toBe(40);
expect(refreshGeometry.auto.left).toBe(refreshGeometry.controls.left);
expect(refreshGeometry.auto.right).toBe(refreshGeometry.manual.left);
expect(refreshGeometry.manual.right).toBe(refreshGeometry.controls.right);
expect(refreshGeometry.overflow).toBeLessThanOrEqual(0);
await autoRefresh.focus();
await expect(autoRefresh).toBeFocused();
await autoRefresh.hover();
await expect(autoRefresh).toHaveCSS('filter', 'brightness(1.14)');
await autoRefresh.click();
const disabledAutoRefresh = page.getByRole('button', { name: '자동 갱신 OFF' });
await expect(disabledAutoRefresh).toHaveAttribute('aria-pressed', 'false');
await expect(disabledAutoRefresh.locator('strong')).toHaveCSS('color', 'rgb(187, 187, 187)');
await expect
.poll(() => page.evaluate(() => (window as unknown as { __hasMainRealtime: () => boolean }).__hasMainRealtime()))
.toBe(false);
await persistArtifact(page, `${basePath.slice(1)}-mobile-auto-refresh-controls-off`);
state.generalName = '직접갱신된장수';
const callsBeforeRefresh = state.generalMeCalls; const callsBeforeRefresh = state.generalMeCalls;
await page.getByRole('button', { name: '갱 신' }).click(); await manualRefresh.click();
await expect.poll(() => state.generalMeCalls).toBeGreaterThan(callsBeforeRefresh); await expect.poll(() => state.generalMeCalls).toBeGreaterThan(callsBeforeRefresh);
await expect(page.locator('.general-title')).toContainText('직접갱신된장수');
const callsBeforeEnable = state.generalMeCalls;
await page.getByRole('button', { name: '자동 갱신 OFF' }).click();
await expect(page.getByRole('button', { name: '자동 갱신 ON' })).toHaveAttribute('aria-pressed', 'true');
await expect.poll(() => state.generalMeCalls).toBeGreaterThan(callsBeforeEnable);
await expect
.poll(() => page.evaluate(() => (window as unknown as { __hasMainRealtime: () => boolean }).__hasMainRealtime()))
.toBe(true);
await persistArtifact(page, `${basePath.slice(1)}-mobile-auto-refresh-controls`);
await page.evaluate(() => { await page.evaluate(() => {
localStorage.setItem('sammo-session-token', 'session_navigation'); localStorage.setItem('sammo-session-token', 'session_navigation');
@@ -18,10 +18,13 @@ const props = defineProps<{
tournamentStage: number; tournamentStage: number;
nationColor: string; nationColor: string;
npcMode: number; npcMode: number;
realtimeEnabled: boolean;
refreshing: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
refresh: []; refresh: [];
toggleRealtime: [];
lobby: []; lobby: [];
quick: [item: QuickNavigationItem]; quick: [item: QuickNavigationItem];
}>(); }>();
@@ -189,14 +192,31 @@ const onQuick = (item: QuickNavigationItem) => {
</ul> </ul>
</div> </div>
<button <div class="bottom-refresh-controls">
class="bottom-trigger refresh-trigger" <button
type="button" class="bottom-trigger auto-refresh-trigger"
data-bottom-menu="refresh" :class="{ active: realtimeEnabled }"
@click="emit('refresh')" type="button"
> data-bottom-menu="auto-refresh"
갱신 :aria-pressed="realtimeEnabled"
</button> @click="emit('toggleRealtime')"
>
<span>자동 갱신</span>
<strong>{{ realtimeEnabled ? 'ON' : 'OFF' }}</strong>
</button>
<button
class="bottom-trigger manual-refresh-trigger"
type="button"
data-bottom-menu="manual-refresh"
aria-label="직접 갱신"
title="직접 갱신"
:disabled="refreshing"
:aria-busy="refreshing"
@click="emit('refresh')"
>
<span aria-hidden="true"></span>
</button>
</div>
</nav> </nav>
</template> </template>
@@ -221,6 +241,17 @@ const onQuick = (item: QuickNavigationItem) => {
position: relative; position: relative;
} }
.bottom-refresh-controls {
display: grid;
width: 125px;
height: 45px;
grid-template-columns: minmax(0, 1fr) 40px;
}
.bottom-refresh-controls > .bottom-trigger {
width: auto;
}
.bottom-trigger { .bottom-trigger {
box-sizing: border-box; box-sizing: border-box;
width: 125px; width: 125px;
@@ -245,6 +276,43 @@ const onQuick = (item: QuickNavigationItem) => {
background: #212529; background: #212529;
} }
.auto-refresh-trigger {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2px 1px;
font-size: 12px;
line-height: 1.15;
}
.auto-refresh-trigger strong {
color: #bbb;
font-size: 11px;
line-height: 1;
}
.auto-refresh-trigger.active {
background-color: #164f2c;
}
.auto-refresh-trigger.active strong {
color: #9ef0b8;
}
.manual-refresh-trigger {
padding: 0;
background: #212529;
font-size: 22px;
line-height: 1;
}
.manual-refresh-trigger:disabled {
cursor: wait;
filter: grayscale(0.6);
opacity: 0.55;
}
.bottom-trigger:hover, .bottom-trigger:hover,
.bottom-trigger:focus-visible, .bottom-trigger:focus-visible,
.bottom-trigger[aria-expanded='true'] { .bottom-trigger[aria-expanded='true'] {
+3
View File
@@ -455,7 +455,10 @@ watch(
:tournament-stage="tournamentStage" :tournament-stage="tournamentStage"
:nation-color="nationColor" :nation-color="nationColor"
:npc-mode="npcMode" :npc-mode="npcMode"
:realtime-enabled="realtimeEnabled"
:refreshing="refreshing"
@refresh="loadMainData" @refresh="loadMainData"
@toggle-realtime="dashboard.setRealtimeEnabled(!realtimeEnabled)"
@lobby="moveLobby" @lobby="moveLobby"
@quick="moveQuick" @quick="moveQuick"
/> />