위임 관리자(부관리자)가 권한·역할·표현식·비밀 콘텐츠 경계를 우회하던 결함군을 계층 대칭성 원칙으로 전건 차단한다. 약한 경로가 정상 응답을 내보내는 것이 유일한 증상이라, 게이트를 생산 지점 한 곳(SSoT)에 두고 같은 데이터를 내보내는 소비 경로 전부가 그 게이트를 경유하도록 맞췄다. - 등급 상한(rank ceiling): 슈퍼관리자 보호·역할/사용자 역할 배정(추가·제거 대칭)·일괄 상태변경·순서변경을 상세 경로와 동일 강도로 재적용. 가드는 DB 쓰기에 선행하여 거부 시 상태 불변. - 레이아웃 표현식: new Function/with 실행을 AST 화이트리스트 평가기로 교체. 비-문자열 computed 키 정규화(normalizeKey)·Object facade(리플렉션 static 제거)·legacy 접근자 차단. 저장측 검증·정적 검사와 3계층 동형. - secret 게이트: 비밀글의 댓글·첨부·문의 독립 경로 재적용, hash 파일서빙 소유권·비밀·발행 상태 검사 통일. - 신뢰 스크립트 호스트: 확장 선언 기반 + same-origin 브라우저 정규화를 런타임·저장측·정적검사 3층 동형화. - 회귀 감지: 단위·Feature·E2E·시나리오 매니페스트 전축 + audit 룰 4종 신설.
90 lines
4.4 KiB
TypeScript
90 lines
4.4 KiB
TypeScript
/**
|
|
* Layout Editor — 버전 히스토리 모달 + 실데이터 미리보기
|
|
*
|
|
* 기존 LayoutController 의 versions/restoreVersion/storePreview API 재사용(신규 백엔드 없음).
|
|
* 브라우저 통합 검증:
|
|
* - 🕘 버전 기록 버튼 활성 + 클릭 시 모달 열림 + 버전 목록(또는 빈 상태) 노출
|
|
* - 👁 미리보기 버튼 활성 + 클릭 시 /preview/{token} 새 창 열림
|
|
* - 라우트 미선택(layoutName 없음) 시 두 버튼 비활성
|
|
*
|
|
* 버전 목록·복원의 응답 정합/권한 가드는 LayoutControllerTest.php 가 커버 — 본 spec 은
|
|
* 위지윅 chrome 의 버튼 활성/모달 노출/새 창 열림 책임만.
|
|
*
|
|
* 축 요약(마커 아님 — 평문): toolbar_button, edit_mode, layout_name_present, versions_state, preview_result.
|
|
* 효과 요약(마커 아님 — 평문): preview_button_enabled_when_layout_selected, preview_creates_temp_record_and_opens_window_with_token, version_button_disabled_when_no_layout_name, version_modal_lists_saved_versions_with_change_summary, version_modal_empty_state_when_no_versions.
|
|
*/
|
|
import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth';
|
|
|
|
const EDITOR_URL_HOME = '/admin/layout-editor/sirsoft-basic?route=%2F';
|
|
const EDITOR_URL_NO_ROUTE = '/admin/layout-editor/sirsoft-basic';
|
|
|
|
async function enterEditor(
|
|
page: import('@playwright/test').Page,
|
|
url: string,
|
|
): Promise<void> {
|
|
const token = issueToken('core.templates.layouts.edit');
|
|
await authenticatePage(page, token);
|
|
await page.goto(url);
|
|
await page.waitForLoadState('domcontentloaded', { timeout: 30_000 });
|
|
await page.waitForSelector('[data-testid="g7le-toolbar"]', { timeout: 30_000 });
|
|
}
|
|
|
|
test.describe('@layout-editor version history + preview', () => {
|
|
/** @effects version_modal_lists_saved_versions_with_change_summary, version_modal_empty_state_when_no_versions */
|
|
test('🕘 버전 기록 버튼 클릭 시 모달 열림 + 목록 또는 빈 상태 노출', async ({ page }) => {
|
|
await enterEditor(page, EDITOR_URL_HOME);
|
|
await page.waitForSelector('[data-testid="g7le-preview-frame"]', { timeout: 30_000 });
|
|
|
|
const versionBtn = page.getByTestId('g7le-toolbar-versions');
|
|
await expect(versionBtn).toBeEnabled();
|
|
await versionBtn.click();
|
|
|
|
// 모달 본체 노출
|
|
await expect(page.getByTestId('g7le-version-history')).toBeVisible();
|
|
await expect(page.getByTestId('g7le-version-history-title')).toBeVisible();
|
|
|
|
// 로딩이 끝나면 목록(list) 또는 빈 상태(empty) 중 하나가 노출 — 둘 다 정상.
|
|
await expect
|
|
.poll(
|
|
async () =>
|
|
(await page.locator('[data-testid="g7le-version-history-list"]').count()) +
|
|
(await page.locator('[data-testid="g7le-version-history-empty"]').count()),
|
|
{ timeout: 15_000 },
|
|
)
|
|
.toBeGreaterThan(0);
|
|
|
|
// 닫기
|
|
await page.getByTestId('g7le-version-history-close').click();
|
|
await expect(page.getByTestId('g7le-version-history')).toHaveCount(0);
|
|
});
|
|
|
|
/** @effects preview_button_enabled_when_layout_selected, preview_creates_temp_record_and_opens_window_with_token */
|
|
test('👁 미리보기 버튼 클릭 시 /preview/{token} 새 창 열림', async ({ page, context }) => {
|
|
await enterEditor(page, EDITOR_URL_HOME);
|
|
await page.waitForSelector('[data-testid="g7le-preview-frame"]', { timeout: 30_000 });
|
|
|
|
const previewBtn = page.getByTestId('g7le-toolbar-preview');
|
|
await expect(previewBtn).toBeEnabled();
|
|
|
|
const popupPromise = context.waitForEvent('page', { timeout: 20_000 });
|
|
await previewBtn.click();
|
|
const popup = await popupPromise;
|
|
await popup.waitForLoadState('domcontentloaded', { timeout: 20_000 });
|
|
|
|
// 새 창 URL 은 /preview/{token}
|
|
expect(new URL(popup.url()).pathname).toMatch(/^\/preview\/[0-9a-f-]+$/);
|
|
|
|
// 원래 위지윅 편집 화면은 이탈하지 않고 유지
|
|
expect(page.url()).toMatch(/\/admin\/layout-editor\/sirsoft-basic/);
|
|
await popup.close();
|
|
});
|
|
|
|
/** @effects version_button_disabled_when_no_layout_name */
|
|
test('라우트 미선택 시 버전/미리보기 버튼 비활성', async ({ page }) => {
|
|
await enterEditor(page, EDITOR_URL_NO_ROUTE);
|
|
// 라우트 미선택 상태(layoutName 없음) — 두 버튼 모두 disabled
|
|
await expect(page.getByTestId('g7le-toolbar-versions')).toBeDisabled();
|
|
await expect(page.getByTestId('g7le-toolbar-preview')).toBeDisabled();
|
|
});
|
|
});
|