Repository navigation
Test Specs CL
dogfootman edited this page Oct 16, 2026
·
5 revisions
생성: 2026-10-16 · 기준: v07 설치 후 통합 검증(IV) · 원본: 여정별 계획서(Excel) ← Test Scenarios · IV 허브 · 기능 TC
Playwright(TypeScript) spec 소스입니다. 시나리오마다 ## <시나리오번호> 섹션이 있고 앵커는 #<시나리오번호 소문자> 입니다. 공용 헬퍼는 Test-Specs-Common. 비밀번호 등은 마스킹했습니다.
| 시나리오 | 파일 | 줄 | 해시 |
|---|---|---|---|
| CL_S01 | tests/cl/CL_S01.spec.ts |
49 | c3e26c64 |
| CL_S02 | tests/cl/CL_S02.spec.ts |
60 | e5ce6dfb |
tests/cl/CL_S01.spec.ts · 49줄 · 해시 c3e26c64 · 시나리오 보기
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid, k8sName, k8sProject } from '../../lib/data';
import { openK8sList, deleteClusterFromList, clusterRow, waitList } from '../../lib/k8s';
// CL_S01 : CSP 테스트 자원 전체 삭제 (비용 방지, 녹화 선택) [CSP별]
// **현재는 5번(Project p{n} 의 K8s 삭제)만 구현**: 다른 단계(NLB/MyImages/Disks/Infra/Projects/SSH Key/SG/vNet)는 미구현.
// K8s 삭제: K8s Workloads 클러스터 목록 → 행의 action item → Delete (사용자 안내 2026-10-05).
// 대상 클러스터는 환경변수 CL_K8S(쉼표 구분)로 지정, 없으면 k{n}d. 예) CL_K8S=k1d,k1f
for (const c of activeCsps()) {
test(`${tid('CL_S01', c)} CSP 테스트 자원 전체 삭제 (K8s 단계만 구현)`, async ({ page }) => {
test.skip(c.code === 'KT', 'KT Credential 없음');
const project = k8sProject(c);
const targets = (process.env.CL_K8S ?? `${k8sName(c)}d`).split(',').map((s) => s.trim()).filter(Boolean);
test.info().annotations.push({ type: 'scope', description: `ws01 / ${project} / K8s=${targets.join(',')} (CL_S01 5번만)` });
await openK8sList(page, project);
for (const name of targets) {
// 목록은 자동 갱신되지 않고 로딩 중 일시적으로 비어 보일 수 있다(직전 삭제 직후 'k1d 없음'으로 오판, 2026-10-08) →
// 메뉴로 다시 진입해 행이 나타나는지 최대 ~60초 재확인한 뒤에도 없을 때만 '없음'으로 본다
let found = (await clusterRow(page, name).count()) > 0;
for (let i = 0; i < 4 && !found; i++) {
await page.waitForTimeout(10_000);
await page.getByRole('link', { name: 'K8s Workloads' }).first().click();
await waitList(page);
found = (await clusterRow(page, name).count()) > 0;
}
if (!found) {
test.info().annotations.push({ type: 'skip-not-found', description: `${name} 목록에 없음 → 건너뜀(존재하는 것만 삭제)` });
continue;
}
await deleteClusterFromList(page, name); // action item → Delete → 확인 팝업 Confirm (요청 직후 상태는 어노테이션)
// 삭제 요청 후 행이 사라지거나 삭제 진행 상태(Deleting 등)로 바뀔 때까지 최대 ~2분 확인.
// 목록은 자동 갱신되지 않아 사이드 메뉴로 다시 진입(Workspace/Project 선택 유지)해서 본다.
const started = Date.now();
let state = '';
for (let i = 0; i < 8; i++) {
await page.getByRole('link', { name: 'K8s Workloads' }).first().click();
await waitList(page);
const row = clusterRow(page, name);
state = (await row.count()) ? (await row.first().innerText()).replace(/\s+/g, ' ') : '(행 없음)';
if (/\(행 없음\)|Delet|Terminat/i.test(state)) break;
await page.waitForTimeout(12_000);
}
test.info().annotations.push({ type: 'k8s-state-after', description: `${name}: ${state} (요청 후 ${Math.round((Date.now() - started) / 1000)}s)` });
expect(state, `${name} 삭제 요청 후 상태(제거 또는 Deleting)`).toMatch(/\(행 없음\)|Delet|Terminat/i);
}
});
}tests/cl/CL_S02.spec.ts · 60줄 · 해시 e5ce6dfb · 시나리오 보기
// CL_S02: 멀티 CSP Infra mc10 삭제 (비용 방지, 녹화 선택) [정상, 공통] — TC-INFRA-056
// 화면: Infra Workloads 목록 → mc10 행 선택 → 목록 카드 'List of Infra' ⌄ Terminate → Confirm → (Terminated) → ⌄ Delete → Confirm → 목록에서 제거
// 시작 상태: admin(mcmp) 로그인 상태, Workspace=ws01 / Project=default 선택된 상태에서 시작. IV 종료(IV3_S11 로 만든 mc10 을 쓰는 IV4_S03·S06·S08 완료)
// 대상: Workspace ws01 / Project default(namespace default) 의 Infra mc10 하나(IV3_S11 이 Workflow 로 만든 8개 CSP NodeGroup g10-<csp>, node 각 1개)
// — 다른 Infra(mc1~mc9, mw*, mt1)·공용 vNet/SG/SSH Key(default-shared-*)는 삭제하지 않음
// 절차: 1.목록에서 mc10 선택 → ⌄ Terminate → Confirm → Status Terminated 대기 2.⌄ Delete → Confirm → 삭제 응답(DelInfra) 기록 3.목록에서 mc10 제거 확인
// 4.잔여 자원 확인: 삭제 응답에 '[WARNING] … node(s) may remain on the CSP' 가 없어야 함(Running 상태 force Delete 시 잔존 경고 사례 2026-10-02 → Terminate 먼저)
// 입력값: Infra=mc10 / Workspace=ws01 / Project=default
// 기대결과: 과금 자원 잔존 없음(mc10 목록 제거, 8개 CSP node 잔존 경고 없음). 화면으로 확인할 수 없는 CSP 콘솔 잔존은 결과 문서에 사용자 확인 요청으로 남김
// 실행: 사용자/메인 세션 지시가 있을 때만(2026-10-16 작성 시점에는 실행하지 않음)
// 비고: 2026-10-16 구현(stub 대체). Infra 단위 제어는 lib/iv3-mc10.ts infraListAction
import { test, expect } from '../../lib/fixtures';
import { tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { gotoInfraList, refreshInfraList, waitInfraListed, infraRow, infraRowText, waitInfraStatus } from '../../lib/iv4-infra';
import { MC10_NAME, infraListAction } from '../../lib/iv3-mc10';
test(`${tid('CL_S02')} 멀티 CSP Infra mc10 삭제 (비용 방지, 녹화 선택)`, async ({ page }) => {
test.setTimeout(60 * 60_000);
const name = process.env.CL_S02_INFRA ?? MC10_NAME;
test.info().annotations.push({ type: 'scope', description: `ws01 / default(namespace default) / Infra=${name} 만 삭제` });
await loginAsAdmin(page);
await gotoInfraList(page);
test.skip(!(await waitInfraListed(page, name)), `${name} 목록에 없음(이미 삭제됐거나 IV3_S11 미실행)`);
const before = await infraRowText(page, name);
test.info().annotations.push({ type: 'list-before', description: before });
// 1. Terminate → Terminated
if (!/^Terminated/i.test(before)) {
await infraListAction(page, name, 'Terminate');
await gotoInfraList(page);
const t = await waitInfraStatus(page, name, /^Terminated/i, 30 * 60_000);
test.info().annotations.push({ type: 'terminated', description: t });
}
// 2. Delete → 응답 기록
const del: string[] = [];
const onResp = async (r: import('@playwright/test').Response) => {
if (/DelInfra|DeleteInfra|DelMci/i.test(r.url())) del.push(`${r.status()} ${(await r.text().catch(() => '')).replace(/\s+/g, ' ').slice(0, 1500)}`);
};
page.on('response', onResp);
await infraListAction(page, name, 'Delete');
// 3. 목록에서 제거(최대 10분)
let row = 'x';
for (let i = 0; i < 20 && row; i++) {
await page.waitForTimeout(30_000);
await refreshInfraList(page);
await page.waitForTimeout(3000);
row = (await infraRow(page, name).count()) ? await infraRowText(page, name) : '';
}
page.off('response', onResp);
test.info().annotations.push({ type: 'delete-response', description: del.join(' || ') || '(응답 미수집)' });
test.info().annotations.push({ type: 'list-after', description: row || `${name} 목록에서 제거` });
expect(row, `${name} 목록에서 제거`).toBe('');
// 4. 잔여 자원 경고 없음
expect(del.join(' '), '삭제 응답에 CSP 잔존 경고 없음').not.toMatch(/may remain|WARNING/i);
await page.waitForTimeout(3000); // 녹화 마무리
});