diff --git a/README.md b/README.md index 43b5ad73..db4c9008 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,8 @@ Node.js는 `.nvmrc`에서 24.x로 고정합니다. 의존성 설치와 검증에 `package.json`과 `pnpm-lock.yaml`을 함께 사용해 주세요. 모든 workspace package는 내부 전용(`private`)이므로 manifest의 `0.0.0`은 배포 버전이 아닙니다. 배포 source는 full Git commit으로 고정하고, 실험 릴리스 같은 milestone은 annotated Git tag로 -식별합니다. +식별합니다. Profile frontend는 같은 commit의 정적 버전 문서를 주기적으로 확인하고, +현재 열린 bundle과 달라졌을 때만 강제 reload 없이 공용 toast로 한 번 안내합니다. ## 개발 환경 diff --git a/app/game-frontend/e2e/deploymentVersionNotice.spec.ts b/app/game-frontend/e2e/deploymentVersionNotice.spec.ts new file mode 100644 index 00000000..f8a40e43 --- /dev/null +++ b/app/game-frontend/e2e/deploymentVersionNotice.spec.ts @@ -0,0 +1,64 @@ +import { expect, test, type Page } from '@playwright/test'; +import { gamePath } from './gameTestPaths.js'; + +const currentCommitSha = process.env.PLAYWRIGHT_BUILD_COMMIT_SHA ?? '0123456789abcdef0123456789abcdef01234567'; +const nextCommitSha = '89abcdef0123456789abcdef0123456789abcdef'; +const noticeMessage = '새 버전이 준비되었습니다. 새로고침하면 변경사항이 반영됩니다.'; + +const installVersionFixture = async (page: Page) => { + let availableCommitSha = currentCommitSha; + let requests = 0; + await page.route('**/deployment-version.json*', async (route) => { + requests += 1; + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ commitSha: availableCommitSha }), + }); + }); + return { + deployNextVersion: () => { + availableCommitSha = nextCommitSha; + }, + requestCount: () => requests, + }; +}; + +for (const viewport of [ + { name: 'desktop', width: 1280, height: 800 }, + { name: 'mobile', width: 390, height: 844 }, +]) { + test(`shows one quiet update toast without forcing reload on ${viewport.name}`, async ({ page }) => { + await page.setViewportSize({ width: viewport.width, height: viewport.height }); + const fixture = await installVersionFixture(page); + await page.goto(gamePath('/version-notice-fixture')); + await expect(page.getByRole('heading', { name: 'Not Found' })).toBeVisible(); + await expect.poll(fixture.requestCount).toBeGreaterThan(0); + await page.evaluate(() => { + Object.assign(window, { __versionNoticePageMarker: 'kept' }); + }); + + fixture.deployNextVersion(); + await expect + .poll(async () => { + await page.evaluate(() => window.dispatchEvent(new Event('online'))); + return fixture.requestCount(); + }) + .toBeGreaterThan(1); + const toast = page.getByTestId('game-toast').filter({ hasText: noticeMessage }); + await expect(toast).toBeVisible(); + await expect(toast).toHaveAttribute('data-feedback-kind', 'info'); + await expect(toast).toHaveCSS('transform', 'none'); + const box = await toast.boundingBox(); + expect(box).not.toBeNull(); + expect(box!.x).toBeGreaterThanOrEqual(0); + expect(box!.y).toBeGreaterThanOrEqual(0); + expect(box!.x + box!.width).toBeLessThanOrEqual(viewport.width); + expect(box!.y + box!.height).toBeLessThanOrEqual(viewport.height); + expect(await page.evaluate(() => Reflect.get(window, '__versionNoticePageMarker'))).toBe('kept'); + + await page.getByRole('button', { name: '알림 닫기' }).click(); + await page.evaluate(() => window.dispatchEvent(new Event('online'))); + await expect(toast).toHaveCount(0); + }); +} diff --git a/app/game-frontend/e2e/playwright.config.mjs b/app/game-frontend/e2e/playwright.config.mjs index 55766660..5f422500 100644 --- a/app/game-frontend/e2e/playwright.config.mjs +++ b/app/game-frontend/e2e/playwright.config.mjs @@ -42,6 +42,7 @@ export default defineConfig({ 'session-auth.spec.ts', 'npcPossession.spec.ts', 'joinLayout.spec.ts', + 'deploymentVersionNotice.spec.ts', ], fullyParallel: false, workers: 1, diff --git a/app/game-frontend/src/App.vue b/app/game-frontend/src/App.vue index 6baf484f..eae06717 100644 --- a/app/game-frontend/src/App.vue +++ b/app/game-frontend/src/App.vue @@ -1,6 +1,9 @@