fix(admin-template): 사이트맵 상태 wait_for 누락 + stale 레이아웃 테스트 5건 교정
작업 중 전체 Vitest 스위트에서 사전 결함 6건을 발견해 같은 세션에서 종결한다. SEO 탭의 sitemap_status 가 transition_overlay.wait_for 에 빠져 있어, 오버레이가 먼저 걷히며 진행상황 블록이 빈 상태로 잠깐 노출될 수 있었다. 구조가 동일한 policies 는 등록돼 있어 설계 의도가 아닌 누락으로 판정했다. 나머지 5건은 로직이 아니라 테스트가 낡은 것이었다. 사이드바 테스트는 프로필 UI 부재라는 의도 대신 컨테이너 id 부재로 단언해 이후 재도입된 크로스 이동 링크에 걸렸고, 대시보드 테스트는 iteration id 보간 도입 후에도 정확 일치로만 노드를 찾고 있었다. 각각 의도에 맞게 교정하고 반대 방향 회귀 가드를 보강했다.
This commit is contained in:
1 parent
062a88ca04
commit
cbf8ce1555
5 files changed
+81
-9
No files matched your search
+34
-4
@@ -71,12 +71,42 @@ describe('어드민 베이스 transition_overlay (이슈 #245)', () => {
|
||||
});
|
||||
|
||||
describe('어드민 베이스 사이드바 프로필 영역 (이슈 #384)', () => {
|
||||
it('좌측 사이드바 하단 프로필/링크 영역을 렌더링하지 않는다', () => {
|
||||
const sidebarFooter = findById((adminBase as any).components, 'sidebar_footer');
|
||||
/**
|
||||
* #384 가 막으려는 것은 "사이드바 하단 **프로필/계정 UI**" 이지 sidebar_footer
|
||||
* 컨테이너 자체가 아니다. #450 이 같은 컨테이너에 "사이트 보기" 크로스 이동 링크를
|
||||
* 재도입했으므로, 컨테이너 id 부재로 단언하면 그 정상 기능에 걸린다.
|
||||
* 의도대로 계정 UI 부재를 단언한다.
|
||||
*/
|
||||
it('좌측 사이드바에 프로필/계정 UI 를 렌더링하지 않는다', () => {
|
||||
const sidebarProfile = findById((adminBase as any).components, 'user_profile');
|
||||
|
||||
expect(sidebarFooter).toBeUndefined();
|
||||
expect(sidebarProfile).toBeUndefined();
|
||||
|
||||
const sidebarFooter = findById((adminBase as any).components, 'sidebar_footer');
|
||||
if (sidebarFooter) {
|
||||
// 컨테이너가 있어도 그 안에 계정/프로필 컴포넌트가 있으면 안 된다.
|
||||
const names: string[] = [];
|
||||
const walk = (node: any): void => {
|
||||
if (!node || typeof node !== 'object') return;
|
||||
if (Array.isArray(node)) return node.forEach(walk);
|
||||
if (node.name) names.push(node.name);
|
||||
Object.values(node).forEach(walk);
|
||||
};
|
||||
walk(sidebarFooter);
|
||||
|
||||
expect(names).not.toContain('UserProfile');
|
||||
expect(names).not.toContain('Avatar');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* #450 크로스 이동 링크는 사이드바 하단에 유지되어야 한다 (제거 회귀 차단).
|
||||
*/
|
||||
it('사이드바 하단 "사이트 보기" 크로스 이동 링크는 유지한다', () => {
|
||||
const viewSiteLink = findById((adminBase as any).components, 'sidebar_view_site_link');
|
||||
|
||||
expect(viewSiteLink).toBeDefined();
|
||||
expect(viewSiteLink?.name).toBe('A');
|
||||
expect(viewSiteLink?.props).toMatchObject({ href: '/', target: '_blank' });
|
||||
});
|
||||
|
||||
it('계정 기능은 상단 헤더 드롭다운으로 유지한다', () => {
|
||||
|
||||
+6
-2
@@ -236,9 +236,11 @@ describe('어드민 대시보드 - 시스템 알림 (incompatible_core)', () =>
|
||||
|
||||
it('dismiss 버튼 액션이 POST /api/admin/extensions/{type}/{id}/dismiss 로 정의되어 있다', () => {
|
||||
// JSON 구조 검증 — alert_dismiss_button 의 actions[0].target
|
||||
// iteration template 안의 노드는 HTML id 유일성을 위해 `{{$idx}}` 접미가 붙는다(#421).
|
||||
// 정확 일치만 보면 보간 도입 시 조용히 null 이 되므로 접미 형태도 인정한다.
|
||||
function findById(node: any, id: string): any | null {
|
||||
if (!node || typeof node !== 'object') return null;
|
||||
if (node.id === id) return node;
|
||||
if (node.id === id || node.id === `${id}_{{$idx}}`) return node;
|
||||
const keys = ['children'];
|
||||
for (const k of keys) {
|
||||
const arr = node[k];
|
||||
@@ -284,9 +286,11 @@ describe('어드민 대시보드 - 시스템 알림 (incompatible_core)', () =>
|
||||
});
|
||||
|
||||
it('system_alerts_card 의 if 조건이 if:false 가 아닌 dashboard_alerts.length 기반이다 (회귀)', () => {
|
||||
// iteration template 안의 노드는 HTML id 유일성을 위해 `{{$idx}}` 접미가 붙는다(#421).
|
||||
// 정확 일치만 보면 보간 도입 시 조용히 null 이 되므로 접미 형태도 인정한다.
|
||||
function findById(node: any, id: string): any | null {
|
||||
if (!node || typeof node !== 'object') return null;
|
||||
if (node.id === id) return node;
|
||||
if (node.id === id || node.id === `${id}_{{$idx}}`) return node;
|
||||
const arr = node.children;
|
||||
if (Array.isArray(arr)) {
|
||||
for (const c of arr) {
|
||||
|
||||
+6
-2
@@ -159,10 +159,14 @@ function setupDefaultMocks(testUtils: ReturnType<typeof createLayoutTest>) {
|
||||
});
|
||||
}
|
||||
|
||||
// 레이아웃 트리에서 id 로 노드 찾기
|
||||
// 레이아웃 트리에서 id 로 노드 찾기.
|
||||
//
|
||||
// iteration template 안의 노드는 HTML id 유일성을 위해 `{{$idx}}` 접미가 붙는다(#421).
|
||||
// 따라서 정확 일치뿐 아니라 `<id>_{{$idx}}` 형태도 같은 노드로 인정한다.
|
||||
// 정확 일치만 쓰면 iteration id 보간 도입 시 조용히 null 이 되어 테스트가 깨진다.
|
||||
function findById(node: any, id: string): any | null {
|
||||
if (!node || typeof node !== 'object') return null;
|
||||
if (node.id === id) return node;
|
||||
if (node.id === id || node.id === `${id}_{{$idx}}`) return node;
|
||||
if (Array.isArray(node.children)) {
|
||||
for (const c of node.children) {
|
||||
const r = findById(c, id);
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
"identityMessages",
|
||||
"adminIdentityPolicies",
|
||||
"policies",
|
||||
"language_packs"
|
||||
"language_packs",
|
||||
"sitemap_status"
|
||||
]
|
||||
},
|
||||
"data_sources": [
|
||||
|
||||
@@ -295,3 +295,36 @@ test('#481 - 재생성 후 실시간/폴링 모드에 맞게 진행상황이 갱
|
||||
expect(statusGets).toBeLessThanOrEqual(2);
|
||||
}
|
||||
});
|
||||
|
||||
// @scenario tab=seo, transition_overlay=wait_for
|
||||
// @effects sitemap_status_awaited_before_reveal
|
||||
test('#481 - SEO 탭 콘텐츠 노출 시점에 sitemap 상태가 이미 채워져 있다', async ({ page }) => {
|
||||
const token = issueToken('core.settings.read', 'core.settings.update');
|
||||
await authenticatePage(page, token);
|
||||
|
||||
// 상태 API 를 의도적으로 지연시켜, 전환 오버레이가 이 데이터소스를 기다리는지 드러낸다.
|
||||
// wait_for 에 sitemap_status 가 빠져 있으면 오버레이가 먼저 걷히고 빈 상태로 노출된다.
|
||||
let statusResolvedAt = 0;
|
||||
await page.route('**/api/admin/seo/sitemap/status*', async (route) => {
|
||||
await new Promise((r) => setTimeout(r, 1_500));
|
||||
statusResolvedAt = Date.now();
|
||||
await route.continue();
|
||||
});
|
||||
|
||||
await page.goto('/admin/settings?tab=seo');
|
||||
await page.waitForLoadState('domcontentloaded', { timeout: 30_000 });
|
||||
|
||||
// 진행상황 블록이 보이는 시점 = 오버레이가 걷힌 시점
|
||||
const statusBlock = page.locator('#sitemap_last_updated_block');
|
||||
await expect(statusBlock).toBeVisible({ timeout: 30_000 });
|
||||
const revealedAt = Date.now();
|
||||
|
||||
// 상대 비교: 노출 시점이 상태 응답 이후여야 한다 (절대 임계값 사용 안 함)
|
||||
expect(statusResolvedAt).toBeGreaterThan(0);
|
||||
expect(revealedAt).toBeGreaterThanOrEqual(statusResolvedAt);
|
||||
|
||||
// 노출된 내용이 빈 껍데기가 아니어야 한다 (미해석 바인딩/빈 문자열 회귀 가드)
|
||||
const text = (await statusBlock.innerText()).trim();
|
||||
expect(text).not.toContain('$t:');
|
||||
expect(text.length).toBeGreaterThan(0);
|
||||
});
|
||||
Reference in new issue
Block a user