test(engine): 감사 지적 3건 반영 — 컴포넌트 번들 E2E 커버 + 약한 단언 실질화

D-4 (실질 커버리지 공백):
- 코어 번들은 "브라우저가 abort 된 script 를 스스로 재요청한다" 는 실측 근거로
 상시-부재 폴백 테스트를 대체본으로 뒀으면서, 형제 경로인 템플릿 컴포넌트
 번들에는 그 대체본조차 없었다. 6경로 중 이 1경로만 E2E 무커버였다.
 상시-부재 → 폴백 안내 E2E 추가 (수정 전 blade 에서 red 확인).

B-9 (제목↔단언 불일치):
- 제목은 "렌더는 진행된다" 인데 단언은 executeInitActions 가 reject 하지
 않는다는 것뿐이라, 그 뒤 init 체인이 죽어도 통과했다. renderTemplate 직접
 단언은 라우터/레이아웃 로드를 거치는 별개 경로라 단위에서는 모킹을 시험하는
 꼴이 되므로, 실제 본체인 "초기화가 에러 화면 없이 완주한다" 로 단언을 바꾸고
 제목도 그에 맞췄다.

B-1 (spy 미사용):
- setRoutes spy 를 선언만 하고 쓰지 않아 "에러 화면이 안 떴다" 만 단언했다.
 라우트가 비어도 에러 화면은 안 뜬다. 재시도로 받아온 routes 가 실제로
 라우터에 반영됐음을 단언한다 (재시도 제거 시 red 확인).
This commit is contained in:
HeuJung
2026-07-12 17:09:03 +09:00
parent 793afb95dc
commit d85ff9971a
2 changed files with 61 additions and 1 deletions
@@ -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();
});
});
@@ -171,6 +171,40 @@ test.describe('네트워크 복원력 — 번들이 끝내 부재할 때 (#463)'
expect(attempts).toBe(3);
});
/**
* 템플릿 컴포넌트 번들도 코어 번들과 동일한 `<script src>` 사망 경로다.
* 이쪽이 끝내 부재하면 코어 엔진(`G7Core`)은 살아 있지만 렌더할 컴포넌트가 없어
* 화면을 만들 수 없다 — 백지로 끝나면 안 되고 폴백 안내로 끝나야 한다.
*
* (1건 취소 → 복구 는 검증하지 않는다. 위 'script 번들 경로' 주석대로 브라우저가
* abort 된 script 를 스스로 재요청해 수정 전에도 통과하기 때문. 재시도가 실제로
* 값을 갖는 지점은 브라우저가 포기한 뒤인 여기다.)
*/
test('@smoke 템플릿 컴포넌트 번들 상시 부재 → 백지가 아니라 폴백 안내 + 새로고침 버튼', async ({ page }) => {
let attempts = 0;
await page.route('**/api/templates/assets/**/components.iife.js*', (route) => {
attempts += 1;
return route.abort('failed');
});
await page.goto('/');
await page.waitForLoadState('domcontentloaded', { timeout: 30_000 });
await page.waitForFunction(
() => (document.querySelector('#app')?.childElementCount ?? 0) > 0,
{ timeout: 20_000 }
);
const text = await bodyText(page);
// 백지가 아니어야 한다
expect(text.length).toBeGreaterThan(0);
expect(text).toMatch(FALLBACK_UI);
await expect(page.locator('#app button')).toBeVisible();
// 재시도 3시도 (초기 1 + 재시도 2)
expect(attempts).toBe(3);
});
/**
* 재시도를 소진한 **진짜 실패**는 조용히 삼키면 안 된다. 사용자가 상황을 알고
* 재시도(새로고침)할 수 있어야 한다 — 복원력이 은폐가 되어서는 안 된다.