fix(sirsoft-gdpr): 정책 버전 본문 페이지 링크 404 수정
정책 버전 snapshot 모달의 본문 페이지 링크가 /{{slug}} 로 조합되어
페이지 모듈(sirsoft-page) URL 패턴 /page/{slug} 와 불일치해 404가
발생하던 문제를 수정. 쿠키 배너의 동일 링크와 조합 방식을 통일했다.
회귀 검증을 위해 GDPR 플러그인에 Playwright E2E 인프라를 최초 도입.
This commit is contained in:
1 parent
d5b6f4b457
commit
a0cc36a9db
8 files changed
+193
-3
No files matched your search
@@ -27,6 +27,7 @@
|
||||
- 관리자 환경설정의 카테고리 「필수」 배지가 다크 모드에서 배경·글자색이 적용되지 않던 문제를 바로잡았습니다.
|
||||
- 관리자 환경설정의 「운영 주체 / 개인정보처리방침」 섹션 제목 영역이 다른 카드와 다르게 표시되던 문제를 바로잡았습니다.
|
||||
- 관리자 환경설정의 저장/취소 버튼을 다른 관리자 화면과 동일하게 화면 하단 고정 위치로 옮기고, 상단 탭 영역의 불필요한 구분선과 좌우 여백 어긋남을 바로잡았습니다.
|
||||
- 관리자 환경설정 「정책 버전 보기」에서 본문 페이지 링크를 클릭하면 페이지를 찾을 수 없던 문제를 바로잡았습니다.
|
||||
|
||||
## [1.0.1] - 2026-07-16
|
||||
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
"build": "vite build",
|
||||
"dev": "vite build --watch",
|
||||
"test": "vitest",
|
||||
"test:run": "vitest run"
|
||||
"test:run": "vitest run",
|
||||
"test:e2e": "playwright test --config=tests/Playwright/playwright.config.ts",
|
||||
"test:e2e:ui": "playwright test --config=tests/Playwright/playwright.config.ts --ui"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^6.5.0",
|
||||
|
||||
+24
@@ -14,6 +14,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import layout from '../../../layouts/admin/plugin_settings.json';
|
||||
import publishModal from '../../../layouts/admin/partials/plugin_settings/_policy_version_publish_modal.json';
|
||||
import snapshotModal from '../../../layouts/admin/partials/_shared/_policy_version_snapshot_modal.json';
|
||||
import { findById, type AnyNode } from './helpers';
|
||||
|
||||
describe('admin/plugin_settings.json — 3 카드 (operator / cookie_banner / auto_blocking_policy)', () => {
|
||||
@@ -698,3 +699,26 @@ describe('admin/plugin_settings.json — 3 카드 (operator / cookie_banner / au
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 이슈 #509 체크리스트 12번 회귀 테스트 — 정책 버전 snapshot 모달의 본문 페이지 링크가
|
||||
* `/{{slug}}` 로 조합되어 페이지 모듈(sirsoft-page) 실제 URL 패턴 `/page/{slug}` 와
|
||||
* 불일치, 클릭 시 404 발생하던 결함. 쿠키 배너의 '변경된 정책 본문 보기' 링크와
|
||||
* 동일하게 `/page/{{slug}}` 로 조합되어야 한다.
|
||||
*/
|
||||
describe('admin/partials/_shared/_policy_version_snapshot_modal.json — 본문 페이지 링크', () => {
|
||||
const layoutJson = JSON.stringify(snapshotModal);
|
||||
|
||||
it('본문 페이지 링크 href 가 /page/{{slug}} 형식으로 조합된다 (페이지 모듈 URL 패턴 일치)', () => {
|
||||
expect(layoutJson).toContain('"href":"/page/{{gdprPolicyVersionSnapshot?.data?.data?.snapshot?.privacy_policy_slug ?? \'\'}}"');
|
||||
});
|
||||
|
||||
it('href 에 /page/ 프리픽스 없이 슬러그만 조합하는 결함 패턴이 없다', () => {
|
||||
expect(layoutJson).not.toContain('"href":"/{{gdprPolicyVersionSnapshot?.data?.data?.snapshot?.privacy_policy_slug}}"');
|
||||
});
|
||||
|
||||
it('본문 페이지 링크는 새 탭(target=_blank) + noopener 로 연다', () => {
|
||||
expect(layoutJson).toContain('"target":"_blank"');
|
||||
expect(layoutJson).toContain('"rel":"noopener"');
|
||||
});
|
||||
});
|
||||
+3
-2
@@ -172,11 +172,12 @@
|
||||
"text": "{{gdprPolicyVersionSnapshot?.data?.data?.snapshot?.privacy_policy_slug ?? '-'}}"
|
||||
},
|
||||
{
|
||||
"comment": "본문 페이지 새 탭 열기 — 슬러그를 그대로 path 로 사용 (예: 'privacy-policy' → '/privacy-policy'). 운영자가 검증한 슬러그라 사용자 입력 검증 불요.",
|
||||
"id": "policy_version_snapshot_open_policy_page_link",
|
||||
"comment": "본문 페이지 새 탭 열기 — 페이지 모듈(sirsoft-page) URL 패턴 /page/{slug} 로 조합 (예: 'privacy-policy' → '/page/privacy-policy'). 쿠키 배너의 '변경된 정책 본문 보기' 링크(cookie_banner.json)와 동일한 조합 방식. 운영자가 검증한 슬러그라 사용자 입력 검증 불요.",
|
||||
"type": "basic",
|
||||
"name": "A",
|
||||
"props": {
|
||||
"href": "/{{gdprPolicyVersionSnapshot?.data?.data?.snapshot?.privacy_policy_slug}}",
|
||||
"href": "/page/{{gdprPolicyVersionSnapshot?.data?.data?.snapshot?.privacy_policy_slug ?? ''}}",
|
||||
"target": "_blank",
|
||||
"rel": "noopener",
|
||||
"className": "text-info-soft inline-flex items-center gap-1 px-3 py-2 font-medium bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-700 rounded hover:bg-blue-100 dark:hover:bg-blue-900/50 transition-colors flex-shrink-0"
|
||||
|
||||
@@ -755,6 +755,7 @@
|
||||
"text": "v{{gdprPolicyVersionCurrent?.data?.data?.version ?? '?'}}"
|
||||
},
|
||||
{
|
||||
"id": "policy_version_current_snapshot_view_button",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"if": "{{gdprPolicyVersionCurrent?.data?.data?.version}}",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* GDPR 플러그인 권한 fixture.
|
||||
*
|
||||
* 코어 `tests/Playwright/fixtures/auth.ts` 의 헬퍼를 재사용하되, 플러그인 권한 토큰 fixture 를
|
||||
* 자체적으로 정의한다. 권한 식별자는 임의 string 이므로 코어 PlaywrightIssueToken 커맨드가
|
||||
* 그대로 동작한다 (Permission::firstOrCreate 가 자동 생성).
|
||||
*/
|
||||
import { test as base } from '@playwright/test';
|
||||
// 6단계 상위 = 코어 루트의 fixtures/auth.ts
|
||||
import { issueToken, authenticatePage } from '../../../../../../tests/Playwright/fixtures/auth';
|
||||
|
||||
type GdprAuthFixtures = {
|
||||
/** GDPR 개인정보 조회 + 설정 변경 권한 보유 토큰 (환경설정 화면 접근용) */
|
||||
privacyManageToken: string;
|
||||
};
|
||||
|
||||
export const test = base.extend<GdprAuthFixtures>({
|
||||
privacyManageToken: async ({}, use) => {
|
||||
await use(issueToken('sirsoft-gdpr.privacy.view', 'sirsoft-gdpr.privacy.update'));
|
||||
},
|
||||
});
|
||||
|
||||
export { authenticatePage };
|
||||
export { expect } from '@playwright/test';
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* GDPR 플러그인 Playwright E2E 설정.
|
||||
*
|
||||
* 코어 `playwright.config.ts` 와 동일한 base URL 해석 우선순위를 따른다 — 플러그인도 활성 호스트가
|
||||
* 가변(개발자/CI/PO 환경별로 다른 도메인)이므로 하드코딩 회피.
|
||||
*
|
||||
* Base URL 해석:
|
||||
* 1. PLAYWRIGHT_BASE_URL 환경변수 (CI/명시적 오버라이드)
|
||||
* 2. .env (코어 루트) 의 APP_URL — 단 localhost 류는 fallback 부적합
|
||||
* 3. 그 외 — 명시 에러
|
||||
*
|
||||
* 실행 예시:
|
||||
* PowerShell — $env:PLAYWRIGHT_BASE_URL='https://g7.dev'; npm run test:e2e
|
||||
* Bash — PLAYWRIGHT_BASE_URL=https://g7.dev npm run test:e2e
|
||||
*
|
||||
* 플러그인은 코어 fixture 의 `issueToken` / `authenticatePage` 헬퍼를 재사용 — 권한 식별자는
|
||||
* `sirsoft-gdpr.*` 등 임의 string.
|
||||
*/
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
// ESM 환경(package.json "type": "module")에서는 __dirname 이 정의되지 않으므로
|
||||
// import.meta.url 로 재구성한다.
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
/**
|
||||
* 코어 루트 (artisan / .env / Playwright 산출물의 기준 경로).
|
||||
*
|
||||
* 확장 config 는 확장 디렉토리에서 실행되지만, 산출물을 그 안에 쓰면 Windows 에서
|
||||
* `plugin:update` 의 디렉토리 이동이 열린 핸들에 걸려 실패한다.
|
||||
* 산출물은 코어 루트 아래로 모아 update 경로와 분리한다 (.gitignore 가 이미 덮는 위치).
|
||||
*/
|
||||
const CORE_ROOT = process.env.G7_ROOT || resolve(__dirname, '../../../../../');
|
||||
|
||||
/** 확장별 산출물 격리 — 확장끼리 리포트를 덮어쓰지 않도록 slug 로 네임스페이스. */
|
||||
const ARTIFACT_SLUG = 'plugins/sirsoft-gdpr';
|
||||
|
||||
function readEnvFile(filePath: string, key: string): string | null {
|
||||
if (!existsSync(filePath)) return null;
|
||||
const content = readFileSync(filePath, { encoding: 'utf-8' });
|
||||
const pattern = new RegExp(`^${key}=(.*)$`, 'm');
|
||||
const match = content.match(pattern);
|
||||
if (!match) return null;
|
||||
let value = match[1].trim();
|
||||
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
return value || null;
|
||||
}
|
||||
|
||||
function resolveBaseUrl(): string {
|
||||
if (process.env.PLAYWRIGHT_BASE_URL) {
|
||||
return process.env.PLAYWRIGHT_BASE_URL;
|
||||
}
|
||||
const appUrl = readEnvFile(resolve(CORE_ROOT, '.env'), 'APP_URL');
|
||||
if (appUrl && !/^https?:\/\/localhost(:\d+)?\/?$/i.test(appUrl)) {
|
||||
return appUrl;
|
||||
}
|
||||
throw new Error(
|
||||
'GDPR 플러그인 E2E base URL 미설정. PLAYWRIGHT_BASE_URL 환경변수를 지정하거나 코어 .env 의 APP_URL 을 활성 호스트로 설정하세요.'
|
||||
);
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './specs',
|
||||
outputDir: resolve(CORE_ROOT, 'test-results', ARTIFACT_SLUG),
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: [
|
||||
['html', { outputFolder: resolve(CORE_ROOT, 'playwright-report', ARTIFACT_SLUG), open: 'never' }],
|
||||
['list'],
|
||||
],
|
||||
use: {
|
||||
baseURL: resolveBaseUrl(),
|
||||
trace: 'retain-on-failure',
|
||||
screenshot: 'only-on-failure',
|
||||
ignoreHTTPSErrors: true,
|
||||
},
|
||||
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
|
||||
});
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* E2E: 관리자 환경설정 > 쿠키 배너 > 정책 버전 > 보기 — 본문 페이지 링크 (#509 체크리스트 12번)
|
||||
*
|
||||
* @scenario admin_gdpr_policy_version_snapshot_open_policy_page_link
|
||||
* @effects snapshot_modal_opens, policy_page_link_href_matches_page_module_url_pattern
|
||||
*
|
||||
* 배경: 정책 버전 snapshot 모달의 본문 페이지 링크가 `/{{slug}}` 로 조합되어 페이지 모듈
|
||||
* (sirsoft-page) 의 실제 URL 패턴 `/page/{slug}` 와 불일치, 클릭 시 404 가 발생하던 결함.
|
||||
* 마이그레이션이 시드하는 v1 initial 정책 버전의 snapshot 에는 항상 기본 슬러그 'privacy' 가
|
||||
* 포함되므로 (GdprPolicyVersionMigrationSmokeTest 참조), 별도 설정 저장 없이도 v배지 클릭 →
|
||||
* 모달의 본문 페이지 링크 href 를 검증할 수 있다.
|
||||
*
|
||||
* 검증:
|
||||
* 1. 환경설정 화면에서 v배지 옆 "본문 보기" 버튼 클릭 시 snapshot 모달이 열린다
|
||||
* 2. 모달의 본문 페이지 링크 href 가 `/page/{slug}` 형식으로 조합되어 페이지 모듈 URL 패턴과 일치한다
|
||||
* (프리픽스 누락으로 `/{slug}` 형태가 되는 결함 패턴이 재발하지 않았는지 회귀 가드)
|
||||
*/
|
||||
import { test, expect, authenticatePage } from '../../fixtures/gdpr-auth';
|
||||
|
||||
const CARD_COOKIE_BANNER = '#card_cookie_banner';
|
||||
const SNAPSHOT_VIEW_BUTTON = '#policy_version_current_snapshot_view_button';
|
||||
const OPEN_POLICY_PAGE_LINK = '#policy_version_snapshot_open_policy_page_link';
|
||||
|
||||
/** 관리자 GDPR 환경설정 진입 후 쿠키 배너 탭(정책 버전 UI 포함)으로 스크롤 이동 */
|
||||
async function gotoGdprSettings(page: import('@playwright/test').Page): Promise<void> {
|
||||
await page.goto('/admin/plugins/sirsoft-gdpr/settings');
|
||||
await page.waitForLoadState('domcontentloaded', { timeout: 30_000 });
|
||||
await expect(page.locator('#settings_tab_navigation')).toBeAttached({ timeout: 20_000 });
|
||||
|
||||
// TabNavigationScroll 은 URL query 가 아니라 클릭 시 해당 카드로 스크롤하는 방식이다.
|
||||
await page.locator(CARD_COOKIE_BANNER).scrollIntoViewIfNeeded();
|
||||
await expect(page.locator(SNAPSHOT_VIEW_BUTTON)).toBeAttached({ timeout: 20_000 });
|
||||
}
|
||||
|
||||
// @scenario tab=card_cookie_banner, permitted=yes
|
||||
// @effects snapshot_modal_opens, policy_page_link_href_matches_page_module_url_pattern
|
||||
test('#509 - 정책 버전 snapshot 모달의 본문 페이지 링크가 /page/{slug} 로 조합된다', async ({ page, privacyManageToken }) => {
|
||||
await authenticatePage(page, privacyManageToken);
|
||||
|
||||
await gotoGdprSettings(page);
|
||||
expect(page.url()).not.toMatch(/\/admin\/login/);
|
||||
|
||||
await page.locator(SNAPSHOT_VIEW_BUTTON).click();
|
||||
|
||||
const link = page.locator(OPEN_POLICY_PAGE_LINK);
|
||||
await expect(link).toBeAttached({ timeout: 10_000 });
|
||||
|
||||
const href = await link.getAttribute('href');
|
||||
expect(href).not.toBeNull();
|
||||
// 결함 패턴(/{slug}, /page/ 프리픽스 누락) 재발 방지 — 페이지 모듈 URL 패턴 /page/{slug} 와 일치해야 한다.
|
||||
expect(href).toMatch(/^\/page\/.+/);
|
||||
expect(href).not.toMatch(/^\/(?!page\/)/);
|
||||
});
|
||||
Reference in new issue
Block a user