From d85ff9971a2f6529bbddf60e2570c4c8eacc5d17 Mon Sep 17 00:00:00 2001 From: HeuJung Date: Sun, 12 Jul 2026 17:09:03 +0900 Subject: [PATCH] =?UTF-8?q?test(engine):=20=EA=B0=90=EC=82=AC=20=EC=A7=80?= =?UTF-8?q?=EC=A0=81=203=EA=B1=B4=20=EB=B0=98=EC=98=81=20=E2=80=94=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=B2=88=EB=93=A4=20E2E?= =?UTF-8?q?=20=EC=BB=A4=EB=B2=84=20+=20=EC=95=BD=ED=95=9C=20=EB=8B=A8?= =?UTF-8?q?=EC=96=B8=20=EC=8B=A4=EC=A7=88=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit D-4 (실질 커버리지 공백): - 코어 번들은 "브라우저가 abort 된 script 를 스스로 재요청한다" 는 실측 근거로 상시-부재 폴백 테스트를 대체본으로 뒀으면서, 형제 경로인 템플릿 컴포넌트 번들에는 그 대체본조차 없었다. 6경로 중 이 1경로만 E2E 무커버였다. 상시-부재 → 폴백 안내 E2E 추가 (수정 전 blade 에서 red 확인). B-9 (제목↔단언 불일치): - 제목은 "렌더는 진행된다" 인데 단언은 executeInitActions 가 reject 하지 않는다는 것뿐이라, 그 뒤 init 체인이 죽어도 통과했다. renderTemplate 직접 단언은 라우터/레이아웃 로드를 거치는 별개 경로라 단위에서는 모킹을 시험하는 꼴이 되므로, 실제 본체인 "초기화가 에러 화면 없이 완주한다" 로 단언을 바꾸고 제목도 그에 맞췄다. B-1 (spy 미사용): - setRoutes spy 를 선언만 하고 쓰지 않아 "에러 화면이 안 떴다" 만 단언했다. 라우트가 비어도 에러 화면은 안 뜬다. 재시도로 받아온 routes 가 실제로 라우터에 반영됐음을 단언한다 (재시도 제거 시 red 확인). --- .../TemplateApp.networkResilience.test.ts | 28 ++++++++++++++- .../specs/smoke/network-resilience.spec.ts | 34 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/resources/js/core/__tests__/TemplateApp.networkResilience.test.ts b/resources/js/core/__tests__/TemplateApp.networkResilience.test.ts index 71bc9dce..e2bb0230 100644 --- a/resources/js/core/__tests__/TemplateApp.networkResilience.test.ts +++ b/resources/js/core/__tests__/TemplateApp.networkResilience.test.ts @@ -156,6 +156,12 @@ describe('TemplateApp 네트워크 복원력 (#463)', () => { String(c[0]).includes('routes.json') ); expect(routesCalls).toHaveLength(2); + + // 재시도로 받아온 routes 가 **실제로 라우터에 반영**돼야 한다. + // "에러 화면이 안 떴다" 만으로는 복구를 증명하지 못한다 — 라우트가 비어도 + // 에러 화면은 안 뜬다. 복구의 본체는 데이터가 흘러 들어갔다는 사실이다. + const routerInstance = (app as any).router; + expect(routerInstance?.setRoutes).toHaveBeenCalled(); }); it('routes.json 이 3회 연속 네트워크 실패하면 에러 화면을 1회 렌더한다', async () => { @@ -313,7 +319,7 @@ describe('TemplateApp 네트워크 복원력 (#463)', () => { * throw 를 삼키므로 미등록 핸들러가 있어도 **렌더는 진행된다**. 이번 수정이 * 그 동작을 깨뜨리지 않았는지 잠근다("ActionError 가 렌더를 차단한다" 는 오해). */ - it('미등록 핸들러가 있어도 init_actions 루프가 삼키고 렌더는 진행된다 (기존 동작 보존)', async () => { + it('미등록 핸들러가 있어도 init_actions 루프가 삼키고 초기화가 에러 화면 없이 완주한다 (기존 동작 보존)', async () => { const { getModuleAssetLoader } = await import('../modules'); const loader = getModuleAssetLoader(); (loader as any).failedJsAssets = new Set(['module']); @@ -338,6 +344,26 @@ describe('TemplateApp 네트워크 복원력 (#463)', () => { ) ).resolves.toBeUndefined(); + // 그리고 **초기화가 에러 화면 없이 완주해야 한다** — 이것이 계획서가 지정한 + // "기존 동작 보존" 의 본체다. "executeInitActions 가 reject 하지 않는다" 만으로는 + // 앱이 살아남았음을 보이지 못한다(그 뒤 init 체인이 죽어도 통과한다). + // 확장 부재는 **조용한 기능 열화**로 끝나야 하고 전면 에러 화면이 되면 안 된다. + // + // (renderTemplate 자체를 단언하지는 않는다 — 그 호출은 라우터 매칭 → 레이아웃 + // 로드를 거치는 별개 경로라, 여기서 모킹으로 재현하면 검증 대상이 아니라 + // 모킹을 시험하는 테스트가 된다. 렌더까지의 실제 도달은 E2E '모듈 번들 상시 + // 부재 → 5초 블로킹 없이 렌더' 가 실브라우저에서 잠근다.) + vi.stubGlobal( + 'fetch', + vi.fn((url: string) => + Promise.resolve(String(url).includes('routes.json') ? routesOk() : configOk()) + ) + ); + + await app.init(); + + expect(errorSpy).not.toHaveBeenCalled(); + (loader as any).failedJsAssets = new Set(); }); }); diff --git a/tests/Playwright/specs/smoke/network-resilience.spec.ts b/tests/Playwright/specs/smoke/network-resilience.spec.ts index b64c68fd..93ea1575 100644 --- a/tests/Playwright/specs/smoke/network-resilience.spec.ts +++ b/tests/Playwright/specs/smoke/network-resilience.spec.ts @@ -171,6 +171,40 @@ test.describe('네트워크 복원력 — 번들이 끝내 부재할 때 (#463)' expect(attempts).toBe(3); }); + /** + * 템플릿 컴포넌트 번들도 코어 번들과 동일한 `