import { test, expect } from '@playwright/test'; import * as fs from 'fs'; import * as path from 'path'; test.describe('QE-M1-02: Collection Run List & Detail Verification', () => { // Login before each test test.beforeEach(async ({ page }) => { await page.goto('/Account/Login'); await page.waitForLoadState('domcontentloaded'); // Fill login form with credentials (admin/quant123! — see CLAUDE.md // "Mandatory Pre-Deployment Checklist"; "admin/admin" does not match the // seeded password from V4 migration and silently fails login) const usernameInput = page.locator('#username'); const passwordInput = page.locator('#password'); const loginButton = page.locator('#loginBtn'); await usernameInput.fill('admin'); await passwordInput.fill('quant123!'); await loginButton.click(); // Wait for login to complete await page.waitForLoadState('domcontentloaded'); }); test('QE-M1-02: Collection run renders in list with API-derived expected values', async ({ page }) => { // Step 1: Fetch expected values from API (source of truth) const apiResponse = await page.request.get('/api/collection/runs?limit=20'); expect(apiResponse.ok()).toBeTruthy(); const responseJson = await apiResponse.json(); const runs = (responseJson as any).runs || []; // Fail if no collection runs exist in database if (runs.length === 0) { throw new Error( 'No collection runs in DB — run the daily-collection job first. ' + 'Expected at least 1 run from kis_collection_runs table.' ); } // Extract expected values from most recent run (first in list) const expectedRun = runs[0]; const expectedRunId = expectedRun.runId; const expectedTotalSnapshots = expectedRun.totalSnapshots ?? 0; const expectedStatus = expectedRun.status; // e.g., "completed", "running", "failed" // Map status to Korean text (same logic as Index.cshtml — unknown statuses // like COMPLETED_WITH_ERRORS render the raw status string in a secondary badge) let expectedStatusText = String(expectedStatus ?? ''); if (expectedStatus?.toLowerCase() === 'completed') { expectedStatusText = '완료'; } else if (expectedStatus?.toLowerCase() === 'running') { expectedStatusText = '진행 중'; } else if (expectedStatus?.toLowerCase() === 'failed') { expectedStatusText = '실패'; } console.log( `\n=== QE-M1-02 Test Started ===\n` + `Expected RunId: ${expectedRunId}\n` + `Expected TotalSnapshots: ${expectedTotalSnapshots}\n` + `Expected Status: ${expectedStatus} (rendered as: ${expectedStatusText})\n` ); // Step 2: Navigate to Collection admin page await page.goto('/Admin/Collection'); await page.waitForLoadState('domcontentloaded'); // Step 3: Verify page title contains "데이터 수집" (collection) const pageTitle = await page.title(); expect(pageTitle).toContain('데이터 수집'); // Step 4: Assert that a row containing the expected runId is visible const runIdCell = page.locator(`td:has-text("${expectedRunId}")`); await expect(runIdCell).toBeVisible(); console.log(`✓ RunId row found and visible: ${expectedRunId}`); // Step 5: Find the row containing this runId and verify the snapshot count const tableRow = runIdCell.locator('xpath=ancestor::tr'); // Within the row, find all td elements and map to columns // Columns: 실행 ID (0), 시작 시간 (1), 종료 시간 (2), 상태 (3), 스냅샷 수 (4), 오류 수 (5) const cells = tableRow.locator('td'); const cellCount = await cells.count(); expect(cellCount).toBeGreaterThanOrEqual(5); // At least 5 columns // Cell 4 (index 4) is "스냅샷 수" (total snapshots) const snapshotCell = cells.nth(4); const snapshotText = await snapshotCell.textContent(); expect(snapshotText?.trim()).toBe(String(expectedTotalSnapshots)); console.log(`✓ Snapshot count matches: ${snapshotText?.trim()} == ${expectedTotalSnapshots}`); // Cell 3 (index 3) is "상태" (status badge) const statusCell = cells.nth(3); const statusBadge = statusCell.locator('span.badge'); const statusBadgeText = await statusBadge.textContent(); expect(statusBadgeText?.trim()).toBe(expectedStatusText); console.log(`✓ Status badge matches: ${statusBadgeText?.trim()} == ${expectedStatusText}`); // Step 6: Create screenshot directory and take screenshot of collection list const screenshotDir = path.join(process.cwd(), 'Temp', 'evidence', 'QE-M1-02', 'screenshots'); fs.mkdirSync(screenshotDir, { recursive: true }); await page.screenshot({ path: path.join(screenshotDir, '01-collection-page.png'), fullPage: true, }); console.log(`✓ Screenshot saved: 01-collection-page.png`); // Step 7: Navigate to the run detail page // Detail.cshtml declares @page "{runId?}" under Pages/Admin/Collection/, so the // route is /Admin/Collection/Detail/{runId} (Razor Pages route = folder + page name + template) await page.goto(`/Admin/Collection/Detail/${expectedRunId}`); await page.waitForLoadState('domcontentloaded'); // Step 8: Verify detail page title contains the runId const detailPageTitle = await page.title(); expect(detailPageTitle).toContain('수집 실행 상세'); // Step 9: Verify that the RunId is displayed on the detail page // The page title shows: "수집 실행 상세 - {runId}" const pageHeading = page.locator('h2.page-title'); const headingText = await pageHeading.textContent(); expect(headingText).toContain(expectedRunId); console.log(`✓ Detail page title contains RunId: ${headingText}`); // Step 10: Verify snapshots count is displayed on detail page // The snapshot count appears in a card with "스냅샷 수" as the title const snapshotCountCard = page.locator('h4.card-title:has-text("스냅샷 수")'); await expect(snapshotCountCard).toBeVisible(); // The value is in a div with class h6 after the title const snapshotCountValue = snapshotCountCard.locator('xpath=following-sibling::div[1]'); const countText = await snapshotCountValue.textContent(); expect(countText?.trim()).toBe(String(expectedTotalSnapshots)); console.log(`✓ Detail page snapshot count matches: ${countText?.trim()} == ${expectedTotalSnapshots}`); // Step 11: Take screenshot of detail page await page.screenshot({ path: path.join(screenshotDir, '02-run-detail.png'), fullPage: true, }); console.log(`✓ Screenshot saved: 02-run-detail.png`); console.log( `\n=== QE-M1-02 Test Completed Successfully ===\n` + `Evidence files saved to: ${screenshotDir}\n` ); }); });