Files
Gnuboard7/tests/Playwright/specs/admin/settings-info-opcache.spec.ts
HeuJung 0f0ef8fa8d test(e2e): 편집기 저장 spec 전용 시드 화면 분리 + 실패 10건 수정
레이아웃 편집기 spec 중 저장(PUT)까지 수행하는 것들이 제품 화면에 편집 결과를
영속시켜, 실행할 때마다 개발 사이트에 노드가 누적됐다(home 에 빈 표 7개,
관리자 대시보드에 빈 차트 5개). spec 안에 "추가한 노드 삭제 후 재저장" 원복을
넣어도 소용이 없었다 — 원복 후에도 레이아웃이 오염 시점과 정확히 같은 크기로
되돌아왔다. 원복에 기대는 대신 저장 대상 자체를 E2E 전용 시드 화면으로 분리한다.

시드 화면은 활성 템플릿 디렉토리에만 설치되므로 배포 원본과 릴리스 산출물은
변하지 않는다. globalSetup 이 매 실행 fixture 원본으로 덮어써 회차 간 누적이
성립하지 않으므로 저장 spec 에 원복 절차가 필요 없다. 설치는 레이아웃 파일과
라우트, DB 행 세 가지를 함께 다루되 시드 행만 건드린다 — 전체 재동기화를 쓰면
편집기로 저장한(파일에 없는) 변경이 실행할 때마다 사라지기 때문이다.

전수 실행에서 드러난 기존 실패도 함께 처리했다. 35건 중 27건은 절전·부하
아티팩트였고 나머지는 원인을 확정해 고쳤다. 템플릿 식별자 자리에 모듈 id 를
넣어 라우트 트리가 비어 있었고, "캔버스 텍스트가 길어진다"는 대리 지표가 렌더
여부와 무관했으며(정상 렌더에도 짧아진다), 팔레트 삽입 위치가 잠금 서브트리
안이면 선택이 조상으로 밀려 노드를 지목할 수 없었고, 상태 개수 단언이 상태
추가로 낡았고, if 게이트상 그 탭에 등록되지 않는 소스를 refetch 하고 있었고,
무통신 구간이 오지 않는 관리자 화면에 networkidle 을 기다렸고, 테스트 종료 후
도착한 route 콜백이 워커를 중단시켰고, 같은 설정 화면에 저장하는 테스트들이
병렬로 충돌했다.

측정 방법에서 반복해 틀린 지점들은 E2E 가이드에 표로 남겼다 — 가림은 가시성
판정으로 잡히지 않고, 길이 증감은 렌더 신호가 아니며, 총 개수 단언은 스펙이
늘면 깨지고, networkidle 은 실시간 연결이 있는 화면에서 오지 않는다.
2026-07-31 00:43:58 +09:00

152 lines
6.5 KiB
TypeScript

/**
* E2E: 환경설정 > 정보 — OPcache 상태 행 및 성능 저하 경고
*
* @scenario admin_settings_info_opcache
* @effects opcache_row_mounted, opcache_warning_conditional
*
* 배경: OPcache 는 코어 어디에서도 상태를 조회하지 않아, 비활성 환경에서 성능이
* 떨어져도 운영자가 인지할 경로가 없었다. 정보 탭에 상태를 노출하고, 비활성일 때만
* 성능 저하 경고를 띄운다.
*
* 검증:
* 1. 정보 탭에 OPcache 행이 마운트되고 값이 raw 키가 아닌 번역문으로 표시된다
* 2. 경고 블록은 enabled=false 일 때만 뜬다 (활성/확인불가에서는 뜨지 않는다)
* — API 응답을 가로채 세 상태를 모두 주입해 조건부 렌더를 검증한다
*/
import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth';
const OPCACHE_ROW = '#opcache_row';
const OPCACHE_WARNING = '#opcache_warning';
const SYSTEM_INFO_API = '**/api/admin/settings/system-info*';
/** 관리자 환경설정 정보 탭 진입 */
async function gotoInfoTab(page: import('@playwright/test').Page): Promise<void> {
await page.goto('/admin/settings?tab=info');
await page.waitForLoadState('domcontentloaded', { timeout: 30_000 });
await expect(page.locator(OPCACHE_ROW)).toBeAttached({ timeout: 20_000 });
}
/**
* system-info 응답의 opcache 필드만 교체해 라우팅한다.
* 나머지 필드는 실제 서버 응답을 그대로 쓴다 (정보 탭 전체가 깨지지 않도록).
*/
async function stubOpcache(
page: import('@playwright/test').Page,
opcache: { loaded: boolean; enabled: boolean | null },
): Promise<void> {
await page.route(SYSTEM_INFO_API, async (route) => {
try {
const response = await route.fetch();
const body = await response.json();
if (body?.data) {
body.data.opcache = opcache;
}
await route.fulfill({ response, json: body });
} catch {
// 테스트가 끝난 뒤 도착한 요청에서 `route.fetch()` 는 "Test ended" 로 거부된다.
// 이 콜백은 테스트가 await 하지 않으므로 그 거부가 **어느 테스트에도 속하지 않는 에러**가
// 되고, 워커가 중단되어 남은 테스트가 "did not run" 으로 집계된다(실측: 전수 실행에서
// 에러 2건 → 미실행 2건). 종료 직후 요청은 조용히 통과시킨다.
await route.continue().catch(() => undefined);
}
});
}
// @scenario tab=info, permitted=yes
// @effects opcache_row_mounted
test('@smoke - 정보 탭에 OPcache 상태 행이 번역문과 함께 마운트된다', async ({ page }) => {
const token = issueToken('core.settings.read', 'core.settings.update');
await authenticatePage(page, token);
await gotoInfoTab(page);
expect(page.url()).not.toMatch(/\/admin\/login/);
const row = page.locator(OPCACHE_ROW);
await expect(row).toBeAttached();
// 다국어 키가 해석되지 않으면 "$t:admin.settings.info.opcache" 원문이 그대로 노출된다
const text = (await row.innerText()).trim();
expect(text).not.toContain('$t:');
expect(text).toContain('OPcache');
// 값이 비어 있으면 상태 표현식이 해석되지 않은 것이다 (바인딩 회귀 가드)
expect(text.replace('OPcache', '').trim().length).toBeGreaterThan(0);
});
// @scenario tab=info, opcache_enabled=false
// @effects opcache_warning_conditional
test('- OPcache 비활성이면 성능 저하 경고 블록이 표시된다', async ({ page }) => {
const token = issueToken('core.settings.read', 'core.settings.update');
await authenticatePage(page, token);
await stubOpcache(page, { loaded: true, enabled: false });
await gotoInfoTab(page);
await expect(page.locator(OPCACHE_WARNING)).toBeVisible({ timeout: 20_000 });
const warningText = (await page.locator(OPCACHE_WARNING).innerText()).trim();
expect(warningText).not.toContain('$t:');
expect(warningText.length).toBeGreaterThan(0);
});
// @scenario tab=info, opcache_enabled=false
// @effects opcache_warning_placed_at_tab_top
test('- 경고 블록이 정보 탭 최상단에 놓이고 아래 카드와 간격을 둔다', async ({ page }) => {
const token = issueToken('core.settings.read', 'core.settings.update');
await authenticatePage(page, token);
await stubOpcache(page, { loaded: true, enabled: false });
await gotoInfoTab(page);
const warning = page.locator(OPCACHE_WARNING);
await expect(warning).toBeVisible({ timeout: 20_000 });
// 경고는 카드 그리드보다 위에 있어야 한다. 그리드 안(세 번째 셀)에 들어가면
// 카드 사이에 끼어 배너로 읽히지 않는다 — 그 회귀를 좌표로 고정한다.
const grid = page.locator('#tab_content_info > .grid-2col-responsive').first();
await expect(grid).toBeVisible();
const warningBox = await warning.boundingBox();
const gridBox = await grid.boundingBox();
expect(warningBox).not.toBeNull();
expect(gridBox).not.toBeNull();
expect(warningBox!.y).toBeLessThan(gridBox!.y);
// 경고와 카드가 붙지 않아야 한다 (자산이 부여하는 하단 간격).
const gap = gridBox!.y - (warningBox!.y + warningBox!.height);
expect(gap).toBeGreaterThan(0);
// 경고가 그리드 셀이 아니라 탭의 직계 자식이어야 한다 (구조 단언)
await expect(page.locator('#tab_content_info > #opcache_warning')).toHaveCount(1);
});
// @scenario tab=info, opcache_enabled=true
// @effects opcache_warning_conditional
test('- OPcache 활성이면 경고 블록이 표시되지 않는다', async ({ page }) => {
const token = issueToken('core.settings.read', 'core.settings.update');
await authenticatePage(page, token);
await stubOpcache(page, { loaded: true, enabled: true });
await gotoInfoTab(page);
// 행이 떠 있음을 먼저 확인해야(긍정 앵커) 아래 부정 단언이 의미를 갖는다
await expect(page.locator(OPCACHE_ROW)).toBeVisible();
await expect(page.locator(OPCACHE_WARNING)).toHaveCount(0);
});
// @scenario tab=info, opcache_enabled=null
// @effects opcache_warning_conditional
test('- OPcache 확인 불가(null)면 경고 블록이 표시되지 않는다', async ({ page }) => {
const token = issueToken('core.settings.read', 'core.settings.update');
await authenticatePage(page, token);
// `!enabled` 로 조건을 쓰면 null 에서도 경고가 잘못 뜬다 — 엄격 비교 회귀 가드
await stubOpcache(page, { loaded: true, enabled: null });
await gotoInfoTab(page);
await expect(page.locator(OPCACHE_ROW)).toBeVisible();
await expect(page.locator(OPCACHE_WARNING)).toHaveCount(0);
});