Files
sam-scenarios/_global-crud-config.json

322 lines
16 KiB
JSON

{
"$schema": "E2E Global CRUD Testing Configuration",
"version": "1.0.0",
"description": "CRUD 전체 흐름 테스트를 위한 전역 설정 - 생성→조회→수정→삭제 완전 검증",
"lastUpdated": "2026-01-31",
"crudPolicy": {
"description": "CRUD 테스트 정책",
"rules": [
"테스트 데이터는 반드시 테스트 내에서 생성",
"기존 데이터 수정/삭제 절대 금지",
"삭제는 생성한 데이터에 대해서만 허용",
"테스트 실패 시에도 생성된 데이터 정리 시도",
"테스트 데이터는 고유 식별자로 구분"
],
"testDataPrefix": "E2E_TEST_",
"uniqueIdentifier": "timestamp",
"cleanupOnFailure": true
},
"phases": {
"CREATE": {
"order": 1,
"description": "테스트 데이터 생성",
"requiredValidations": [
"등록 폼/모달 열림 확인",
"필수 필드 입력",
"저장 버튼 클릭",
"API 호출 확인 (POST)",
"성공 토스트/메시지 확인",
"목록에 데이터 표시 확인"
],
"expectedApi": {
"method": "POST",
"successStatus": [200, 201],
"responseValidation": "id 또는 생성된 데이터 반환"
},
"onSuccess": "createdId 저장 → READ 단계 진행",
"onFailure": "스크린샷 + 오류 로그 → 테스트 중단"
},
"READ": {
"order": 2,
"description": "생성된 데이터 조회 확인",
"requiredValidations": [
"목록에서 생성된 데이터 검색",
"상세 페이지 진입",
"입력한 데이터가 정확히 표시되는지 확인",
"모든 필드 값 검증"
],
"expectedApi": {
"method": "GET",
"successStatus": [200],
"responseValidation": "생성 시 입력한 데이터와 일치"
},
"onSuccess": "UPDATE 단계 진행",
"onFailure": "스크린샷 + 데이터 불일치 기록 → DELETE 단계로 건너뛰기"
},
"UPDATE": {
"order": 3,
"description": "데이터 수정",
"requiredValidations": [
"수정 모드 진입 (URL: ?mode=edit 또는 수정 버튼)",
"필드 값 변경",
"저장 버튼 클릭",
"API 호출 확인 (PUT/PATCH)",
"성공 토스트/메시지 확인",
"변경된 데이터 표시 확인"
],
"expectedApi": {
"method": ["PUT", "PATCH"],
"successStatus": [200],
"responseValidation": "수정된 데이터 반환"
},
"onSuccess": "DELETE 단계 진행",
"onFailure": "스크린샷 + 오류 로그 → DELETE 단계 진행 (정리 필요)"
},
"DELETE": {
"order": 4,
"description": "테스트 데이터 삭제 (정리)",
"requiredValidations": [
"삭제 버튼 클릭",
"확인 다이얼로그 표시",
"삭제 확인 클릭",
"API 호출 확인 (DELETE)",
"성공 토스트/메시지 확인",
"목록에서 데이터 제거 확인"
],
"expectedApi": {
"method": "DELETE",
"successStatus": [200, 204],
"responseValidation": "삭제 성공 응답"
},
"onSuccess": "테스트 완료 (PASS)",
"onFailure": "수동 정리 필요 경고 + 테스트 데이터 정보 기록"
}
},
"testDataTemplates": {
"vendor": {
"entityName": "거래처",
"menuPath": "회계관리 > 거래처관리",
"url": "/accounting/vendors",
"fields": {
"vendorName": { "type": "text", "required": true, "testValue": "E2E_TEST_거래처_{timestamp}" },
"businessNumber": { "type": "text", "required": false, "testValue": "123-45-67890" },
"representative": { "type": "text", "required": false, "testValue": "테스트대표" },
"vendorType": { "type": "select", "required": true, "testValue": "매출" },
"phone": { "type": "text", "required": false, "testValue": "02-1234-5678" },
"email": { "type": "email", "required": false, "testValue": "e2e@test.com" }
},
"updateFields": {
"vendorName": "E2E_TEST_수정완료_{timestamp}",
"representative": "수정대표"
},
"searchField": "vendorName",
"api": {
"list": "GET /api/v1/clients",
"create": "POST /api/v1/clients",
"read": "GET /api/v1/clients/{id}",
"update": "PUT /api/v1/clients/{id}",
"delete": "DELETE /api/v1/clients/{id}"
}
},
"freeBoard": {
"entityName": "자유게시판 글",
"menuPath": "게시판 > 자유게시판",
"url": "/boards/free",
"fields": {
"title": { "type": "text", "required": true, "testValue": "E2E_TEST_게시글_{timestamp}" },
"content": { "type": "textarea", "required": true, "testValue": "E2E 테스트용 게시글 내용입니다. 자동 생성됨." }
},
"updateFields": {
"title": "E2E_TEST_수정됨_{timestamp}",
"content": "수정된 게시글 내용입니다."
},
"searchField": "title",
"api": {
"list": "GET /api/v1/boards/free",
"create": "POST /api/v1/boards/free",
"read": "GET /api/v1/boards/free/{id}",
"update": "PUT /api/v1/boards/free/{id}",
"delete": "DELETE /api/v1/boards/free/{id}"
}
},
"deposit": {
"entityName": "입금",
"menuPath": "회계관리 > 입금관리",
"url": "/accounting/deposits",
"fields": {
"vendorName": { "type": "select", "required": true, "testValue": "가우스전자" },
"amount": { "type": "number", "required": true, "testValue": "100000" },
"depositDate": { "type": "date", "required": true, "testValue": "today" },
"description": { "type": "text", "required": false, "testValue": "E2E_TEST_입금_{timestamp}" }
},
"updateFields": {
"amount": "200000",
"description": "E2E_TEST_수정입금_{timestamp}"
},
"searchField": "description"
}
},
"scripts": {
"generateTimestamp": "(function() { const now = new Date(); const pad = n => n.toString().padStart(2, '0'); return `${now.getFullYear()}${pad(now.getMonth()+1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`; })()",
"generateTestData": "(function(template, timestamp) { const result = {}; for (const [key, config] of Object.entries(template.fields)) { let value = config.testValue; if (typeof value === 'string' && value.includes('{timestamp}')) { value = value.replace('{timestamp}', timestamp); } if (value === 'today') { value = new Date().toISOString().split('T')[0]; } result[key] = value; } return result; })",
"findTestDataInTable": "(function(searchText) { const rows = document.querySelectorAll('table tbody tr'); for (const row of rows) { if (row.innerText.includes(searchText)) { return { found: true, row: row, text: row.innerText.substring(0, 100) }; } } return { found: false }; })",
"verifyDataCreated": "(async function(searchText, timeout = 5000) { const start = Date.now(); while (Date.now() - start < timeout) { const result = findTestDataInTable(searchText); if (result.found) return result; await new Promise(r => setTimeout(r, 500)); } return { found: false, error: 'timeout' }; })",
"clickCreateButton": "(function() { const buttons = Array.from(document.querySelectorAll('button')); const createBtn = buttons.find(b => ['등록', '추가', '신규', '작성'].some(t => b.innerText?.includes(t))); if (createBtn) { createBtn.click(); return { clicked: true, text: createBtn.innerText }; } return { clicked: false, error: 'Create button not found' }; })",
"clickEditButton": "(function() { const buttons = Array.from(document.querySelectorAll('button')); const editBtn = buttons.find(b => ['수정', '편집', 'Edit'].some(t => b.innerText?.includes(t))); if (editBtn) { editBtn.click(); return { clicked: true, text: editBtn.innerText }; } return { clicked: false, error: 'Edit button not found' }; })",
"clickDeleteButton": "(function() { const buttons = Array.from(document.querySelectorAll('button')); const deleteBtn = buttons.find(b => ['삭제', '제거', 'Delete'].some(t => b.innerText?.includes(t))); if (deleteBtn) { deleteBtn.click(); return { clicked: true, text: deleteBtn.innerText }; } return { clicked: false, error: 'Delete button not found' }; })",
"clickSaveButton": "(function() { const buttons = Array.from(document.querySelectorAll('button')); const saveBtn = buttons.find(b => ['저장', '등록', '확인', 'Save', 'Submit'].some(t => b.innerText?.trim() === t || b.innerText?.includes(t))); if (saveBtn) { saveBtn.click(); return { clicked: true, text: saveBtn.innerText }; } return { clicked: false, error: 'Save button not found' }; })",
"clickConfirmDialog": "(async function() { await new Promise(r => setTimeout(r, 500)); const dialog = document.querySelector('[role=\"alertdialog\"], [role=\"dialog\"], [class*=\"modal\"]'); if (dialog) { const confirmBtn = Array.from(dialog.querySelectorAll('button')).find(b => ['확인', '예', 'Yes', 'OK', '삭제'].some(t => b.innerText?.includes(t))); if (confirmBtn) { confirmBtn.click(); return { clicked: true, text: confirmBtn.innerText }; } } return { clicked: false, error: 'Confirm button not found' }; })",
"fillField": "(function(labelOrName, value) { const inputs = document.querySelectorAll('input, textarea, select'); for (const input of inputs) { const label = input.closest('label')?.innerText || input.placeholder || input.name || input.id; if (label?.includes(labelOrName)) { if (input.tagName === 'SELECT') { const option = Array.from(input.options).find(o => o.text.includes(value)); if (option) { input.value = option.value; input.dispatchEvent(new Event('change', { bubbles: true })); return { filled: true, field: labelOrName }; } } else { input.value = value; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); return { filled: true, field: labelOrName }; } } } return { filled: false, error: `Field '${labelOrName}' not found` }; })",
"getFieldValue": "(function(labelOrName) { const inputs = document.querySelectorAll('input, textarea, select'); for (const input of inputs) { const label = input.closest('label')?.innerText || input.placeholder || input.name || input.id; if (label?.includes(labelOrName)) { return { found: true, field: labelOrName, value: input.value }; } } return { found: false, error: `Field '${labelOrName}' not found` }; })",
"waitForToast": "(async function(expectedText, timeout = 5000) { const start = Date.now(); while (Date.now() - start < timeout) { const toasts = document.querySelectorAll('[class*=\"toast\"], [class*=\"Toast\"], [class*=\"notification\"], [class*=\"alert\"]:not([role=\"alertdialog\"])'); for (const toast of toasts) { if (toast.innerText?.includes(expectedText)) { return { found: true, text: toast.innerText }; } } await new Promise(r => setTimeout(r, 200)); } return { found: false, error: 'Toast not found', expected: expectedText }; })",
"captureTestContext": "(function() { return { url: window.location.href, timestamp: new Date().toISOString(), pageTitle: document.title, apiLogs: window.__API_LOGS__ || [], apiErrors: window.__API_ERRORS__ || [] }; })"
},
"flowTemplates": {
"standardCRUD": {
"description": "표준 CRUD 흐름 템플릿",
"steps": [
{
"phase": "SETUP",
"name": "테스트 준비",
"actions": [
{ "type": "generateTimestamp", "saveAs": "testTimestamp" },
{ "type": "initApiMonitoring" },
{ "type": "navigateToPage" }
]
},
{
"phase": "CREATE",
"name": "데이터 생성",
"actions": [
{ "type": "clickCreateButton" },
{ "type": "waitForModal", "timeout": 2000 },
{ "type": "fillTestData", "template": "{{entityTemplate}}" },
{ "type": "clickSaveButton" },
{ "type": "waitForToast", "contains": ["등록", "완료", "성공"] },
{ "type": "verifyApiCall", "method": "POST", "status": [200, 201] },
{ "type": "closeModalIfOpen" },
{ "type": "searchTestData" },
{ "type": "verifyDataInTable" }
],
"saveResult": "createdData"
},
{
"phase": "READ",
"name": "데이터 조회",
"actions": [
{ "type": "clickTestDataRow" },
{ "type": "waitForDetailPage" },
{ "type": "verifyFieldValues", "expected": "{{createdData}}" },
{ "type": "verifyApiCall", "method": "GET", "status": [200] }
]
},
{
"phase": "UPDATE",
"name": "데이터 수정",
"actions": [
{ "type": "clickEditButton" },
{ "type": "waitForEditMode" },
{ "type": "updateTestData", "template": "{{updateTemplate}}" },
{ "type": "clickSaveButton" },
{ "type": "waitForToast", "contains": ["수정", "완료", "성공"] },
{ "type": "verifyApiCall", "method": ["PUT", "PATCH"], "status": [200] },
{ "type": "verifyUpdatedValues" }
],
"saveResult": "updatedData"
},
{
"phase": "DELETE",
"name": "데이터 삭제",
"actions": [
{ "type": "clickDeleteButton" },
{ "type": "waitForConfirmDialog" },
{ "type": "clickConfirmDialog" },
{ "type": "waitForToast", "contains": ["삭제", "완료", "성공"] },
{ "type": "verifyApiCall", "method": "DELETE", "status": [200, 204] },
{ "type": "verifyRedirectToList" },
{ "type": "verifyDataNotInTable" }
]
},
{
"phase": "CLEANUP",
"name": "정리",
"actions": [
{ "type": "clearSearchFilter" },
{ "type": "captureApiSummary" },
{ "type": "generateReport" }
]
}
]
}
},
"errorHandling": {
"onCreateFailure": {
"actions": ["screenshot", "logError", "skipToCleanup"],
"testResult": "FAIL",
"reason": "CREATE 단계 실패 - 데이터 생성 불가"
},
"onReadFailure": {
"actions": ["screenshot", "logError", "continueToDelete"],
"testResult": "PARTIAL",
"reason": "READ 단계 실패 - 데이터 조회 불일치"
},
"onUpdateFailure": {
"actions": ["screenshot", "logError", "continueToDelete"],
"testResult": "PARTIAL",
"reason": "UPDATE 단계 실패 - 수정 저장 실패"
},
"onDeleteFailure": {
"actions": ["screenshot", "logError", "recordManualCleanup"],
"testResult": "FAIL",
"reason": "DELETE 단계 실패 - 수동 정리 필요",
"manualCleanupRequired": true
}
},
"reportTemplate": {
"sections": [
{
"name": "테스트 요약",
"fields": ["시나리오명", "실행시간", "전체결과", "소요시간"]
},
{
"name": "CRUD 단계별 결과",
"fields": ["CREATE", "READ", "UPDATE", "DELETE"],
"format": "table"
},
{
"name": "API 호출 검증",
"fields": ["총 호출수", "성공", "실패", "응답시간"],
"includeDetails": true
},
{
"name": "테스트 데이터",
"fields": ["생성 데이터", "수정 데이터", "삭제 확인"]
},
{
"name": "오류 및 경고",
"conditional": "hasErrors",
"fields": ["오류 메시지", "스크린샷", "수동 정리 필요 여부"]
}
]
}
}