# Instructions

- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.

# Test info

- Name: 3811/dev/transfer-details-action-button.spec.ts >> [3811][dev] [QA] 이체내역 이체구분·상태별 작업 버튼 노출 검증 >> [ADMIN] role >> 상태가 성공인 항목은 이체구분과 관계없이 작업 버튼이 노출되지 않는지 확인
- Location: src/qa/scenarios/transferDetailsActionButton.ts:101:7

# Error details

```
Error: 성공 상태 거래 ID 241535479에는 작업 버튼이 노출되면 안 됩니다.

expect(received).toBe(expected) // Object.is equality

Expected: false
Received: true
```

# Test source

```ts
  17  |   transferType?: string | null;
  18  |   isCompleted?: boolean;
  19  | }
  20  |
  21  | interface TransferItem {
  22  |   transaction?: TransferTransaction;
  23  | }
  24  |
  25  | interface TransferData {
  26  |   rows: TransferItem[];
  27  |   requestUrl: string;
  28  | }
  29  |
  30  | interface QaState {
  31  |   data?: TransferData;
  32  |   actionableByType?: Record<TransferType, TransferItem>;
  33  |   successfulRows?: TransferItem[];
  34  | }
  35  |
  36  | type TransferType = "store" | "manual" | "auto";
  37  |
  38  | const ROLE = "ADMIN";
  39  | const transferTypeLabels: Record<TransferType, string> = {
  40  |   store: "스토어이체",
  41  |   manual: "수동이체",
  42  |   auto: "자동이체"
  43  | };
  44  |
  45  | export function runTransferDetailsActionButtonSuite(options: ScenarioOptions): void {
  46  |   const metadata = loadIssueMetadata(options.issueId);
  47  |   const checklist = loadChecklist(options.issueId, options.environment);
  48  |   const pageDefinition = findPageDefinition(checklist.pages, "이체내역");
  49  |   const runtimeEnv = getRuntimeQaEnv(options.environment, pageDefinition.application ?? "admin", ROLE);
  50  |   const blockReason = getRuntimeBlockReason(runtimeEnv);
  51  |   const state: QaState = {};
  52  |
  53  |   test.describe(`[${options.issueId}][${options.environment}] ${metadata.subject}`, () => {
  54  |     test.describe(`[${ROLE}] role`, () => {
  55  |       test.skip(!!blockReason, blockReason);
  56  |
  57  |       test(checklist.checklist[0], async ({ browser }, testInfo) => {
  58  |         test.setTimeout(90_000);
  59  |         await withRolePage(browser, runtimeEnv, async (page) => {
  60  |           await openTransferPage(page, runtimeEnv, pageDefinition);
  61  |           state.data = await loadTransferData(runtimeEnv);
  62  |           state.actionableByType = findActionableRows(state.data.rows);
  63  |           state.successfulRows = state.data.rows.filter(isSuccessful);
  64  |
  65  |           await attachPageScreenshot(page, testInfo, pageDefinition.name, ROLE, "current-month-list");
  66  |           await attachEvidenceLog(testInfo, pageDefinition.name, ROLE, "current-month-list", [
  67  |             `조회 기간: ${currentMonthRange().from} ~ ${currentMonthRange().to}`,
  68  |             `API 요청: ${redactUrl(state.data.requestUrl)}`,
  69  |             `전체 목록: ${state.data.rows.length}건`,
  70  |             ...Object.entries(state.actionableByType).map(([type, row]) => `${transferTypeLabels[type as TransferType]} 작업 대상 거래 ID: ${transactionId(row)} / 상태: ${row.transaction?.status ?? "-"}`),
  71  |             `성공 상태 거래 수: ${state.successfulRows.length}`
  72  |           ]);
  73  |
  74  |           expect(state.data.rows.length, "당월 이체내역 목록이 조회되어야 합니다.").toBeGreaterThan(0);
  75  |           for (const type of Object.keys(transferTypeLabels) as TransferType[]) {
  76  |             expect(state.actionableByType[type], `${transferTypeLabels[type]}의 성공 이외 작업 대상 데이터가 필요합니다.`).toBeTruthy();
  77  |           }
  78  |         });
  79  |       });
  80  |
  81  |       test(checklist.checklist[1], async ({ browser }, testInfo) => {
  82  |         test.setTimeout(90_000);
  83  |         await withRolePage(browser, runtimeEnv, async (page) => {
  84  |           const actionable = await ensureActionableRows(page, runtimeEnv, pageDefinition, state);
  85  |           await openTransferPage(page, runtimeEnv, pageDefinition);
  86  |           const evidence: string[] = [];
  87  |
  88  |           for (const type of Object.keys(transferTypeLabels) as TransferType[]) {
  89  |             const row = requireRow(actionable[type], `${transferTypeLabels[type]} 작업 대상`);
  90  |             const screenRow = await findScreenRow(page, transactionId(row));
  91  |             const actionButton = await findActionButton(screenRow);
  92  |             evidence.push(`${transferTypeLabels[type]} / 거래 ID ${transactionId(row)} / 상태 ${row.transaction?.status ?? "-"} / 작업 버튼 ${await actionButton.isVisible().catch(() => false) ? "노출" : "미노출"}`);
  93  |             await expect(actionButton, `${transferTypeLabels[type]} 항목에는 작업 버튼이 보여야 합니다.`).toBeVisible({ timeout: 10_000 });
  94  |           }
  95  |
  96  |           await attachPageScreenshot(page, testInfo, pageDefinition.name, ROLE, "actionable-types");
  97  |           await attachEvidenceLog(testInfo, pageDefinition.name, ROLE, "actionable-types", evidence);
  98  |         });
  99  |       });
  100 |
  101 |       test(checklist.checklist[2], async ({ browser }, testInfo) => {
  102 |         test.setTimeout(90_000);
  103 |         await withRolePage(browser, runtimeEnv, async (page) => {
  104 |           const successfulRows = await ensureSuccessfulRows(page, runtimeEnv, pageDefinition, state);
  105 |           await openTransferPage(page, runtimeEnv, pageDefinition);
  106 |           const verified = successfulRows.slice(0, 10);
  107 |           if (verified.length === 0) {
  108 |             blocked("당월 성공 상태 이체내역이 없어 작업 버튼 미노출을 검증할 수 없습니다.");
  109 |           }
  110 |
  111 |           const evidence: string[] = [];
  112 |           for (const row of verified) {
  113 |             const screenRow = await findScreenRow(page, transactionId(row));
  114 |             const actionButton = await findActionButton(screenRow);
  115 |             const visible = await actionButton.isVisible({ timeout: 1_500 }).catch(() => false);
  116 |             evidence.push(`성공 / 거래 ID ${transactionId(row)} / 이체구분 ${row.transaction?.transferType ?? "-"} / 작업 버튼 ${visible ? "노출됨" : "미노출"}`);
> 117 |             expect(visible, `성공 상태 거래 ID ${transactionId(row)}에는 작업 버튼이 노출되면 안 됩니다.`).toBe(false);
      |                                                                                       ^ Error: 성공 상태 거래 ID 241535479에는 작업 버튼이 노출되면 안 됩니다.
  118 |           }
  119 |
  120 |           await attachPageScreenshot(page, testInfo, pageDefinition.name, ROLE, "success-no-action");
  121 |           await attachEvidenceLog(testInfo, pageDefinition.name, ROLE, "success-no-action", evidence);
  122 |         });
  123 |       });
  124 |
  125 |       test(checklist.checklist[3], async ({ browser }, testInfo) => {
  126 |         test.setTimeout(90_000);
  127 |         await withRolePage(browser, runtimeEnv, async (page) => {
  128 |           const actionable = await ensureActionableRows(page, runtimeEnv, pageDefinition, state);
  129 |           await openTransferPage(page, runtimeEnv, pageDefinition);
  130 |           const row = requireRow(actionable.store ?? actionable.manual ?? actionable.auto, "작업 창 확인 대상");
  131 |           const screenRow = await findScreenRow(page, transactionId(row));
  132 |           const actionButton = await findActionButton(screenRow);
  133 |
  134 |           await expect(actionButton, "작업 창 확인 대상의 작업 버튼이 보여야 합니다.").toBeVisible({ timeout: 10_000 });
  135 |           await actionButton.dispatchEvent("click");
  136 |           const dialog = page.locator("[role='dialog']:visible, [data-radix-popper-content-wrapper]:visible, .swal2-container:visible").last();
  137 |           await expect(dialog, "작업 버튼을 클릭하면 이체내역 작업 창이 열려야 합니다.").toBeVisible({ timeout: 10_000 });
  138 |
  139 |           await attachPageScreenshot(page, testInfo, pageDefinition.name, ROLE, "action-dialog");
  140 |           await attachEvidenceLog(testInfo, pageDefinition.name, ROLE, "action-dialog", [
  141 |             `작업 창 대상: ${transactionId(row)}`,
  142 |             `이체구분: ${row.transaction?.transferType ?? "-"}`,
  143 |             `상태: ${row.transaction?.status ?? "-"}`,
  144 |             `작업 창 텍스트: ${snippet(await dialog.innerText().catch(() => ""), 1200)}`
  145 |           ]);
  146 |         });
  147 |       });
  148 |     });
  149 |   });
  150 | }
  151 |
  152 | async function ensureActionableRows(page: Page, runtimeEnv: RuntimeQaEnv, pageDefinition: QaPageDefinition, state: QaState): Promise<Record<TransferType, TransferItem>> {
  153 |   if (!state.actionableByType) {
  154 |     await openTransferPage(page, runtimeEnv, pageDefinition);
  155 |     state.data = await loadTransferData(runtimeEnv);
  156 |     state.actionableByType = findActionableRows(state.data.rows);
  157 |   }
  158 |   return state.actionableByType;
  159 | }
  160 |
  161 | async function ensureSuccessfulRows(page: Page, runtimeEnv: RuntimeQaEnv, pageDefinition: QaPageDefinition, state: QaState): Promise<TransferItem[]> {
  162 |   if (!state.successfulRows) {
  163 |     await openTransferPage(page, runtimeEnv, pageDefinition);
  164 |     state.data = await loadTransferData(runtimeEnv);
  165 |     state.successfulRows = state.data.rows.filter(isSuccessful);
  166 |   }
  167 |   return state.successfulRows;
  168 | }
  169 |
  170 | async function withRolePage(browser: Browser, runtimeEnv: RuntimeQaEnv, callback: (page: Page) => Promise<void>): Promise<void> {
  171 |   const context = await browser.newContext(runtimeEnv.storageState ? { storageState: runtimeEnv.storageState } : {});
  172 |   const page = await context.newPage();
  173 |   try {
  174 |     await callback(page);
  175 |   } finally {
  176 |     await context.close();
  177 |   }
  178 | }
  179 |
  180 | async function openTransferPage(page: Page, runtimeEnv: RuntimeQaEnv, pageDefinition: QaPageDefinition): Promise<void> {
  181 |   const range = currentMonthRange();
  182 |   const targetUrl = new URL(buildPageUrl(runtimeEnv.baseUrl!, pageDefinition, runtimeEnv.tenantId!));
  183 |   targetUrl.searchParams.set("from", range.from);
  184 |   targetUrl.searchParams.set("to", range.to);
  185 |   targetUrl.searchParams.set("exactMatch", "false");
  186 |
  187 |   if (!runtimeEnv.storageState) {
  188 |     await loginWithCredentials(page, runtimeEnv);
  189 |   }
  190 |   await page.goto(targetUrl.toString(), { waitUntil: "domcontentloaded" });
  191 |   await page.waitForLoadState("networkidle", { timeout: 8_000 }).catch(() => undefined);
  192 |   if (await isLoginPage(page, runtimeEnv)) {
  193 |     await loginWithCredentials(page, runtimeEnv);
  194 |     await page.goto(targetUrl.toString(), { waitUntil: "domcontentloaded" });
  195 |   }
  196 |   expect(await isLoginPage(page, runtimeEnv), "이체내역 화면 진입 전 로그인 화면을 벗어나야 합니다.").toBe(false);
  197 |   await expect(page.locator("body"), "이체내역 화면이 표시되어야 합니다.").toContainText(/이체내역|이체일시/, { timeout: 15_000 });
  198 |   await page.waitForTimeout(900);
  199 | }
  200 |
  201 | async function loadTransferData(runtimeEnv: RuntimeQaEnv): Promise<TransferData> {
  202 |   const range = currentMonthRange();
  203 |   const requestUrl = new URL(`${apiBaseFromRuntime(runtimeEnv)}/api/v1/transfer/transaction`);
  204 |   requestUrl.searchParams.set("from", range.from);
  205 |   requestUrl.searchParams.set("to", range.to);
  206 |   requestUrl.searchParams.set("exactMatch", "false");
  207 |   requestUrl.searchParams.set("size", "500");
  208 |   requestUrl.searchParams.set("page", "0");
  209 |
  210 |   const response = await fetch(requestUrl, { headers: nodeApiHeaders(runtimeEnv) });
  211 |   const body = await response.json().catch(() => undefined);
  212 |   if (!response.ok) {
  213 |     throw new Error(`이체내역 목록 조회 실패: ${response.status} ${JSON.stringify(body)}`);
  214 |   }
  215 |   return { rows: Array.isArray(body?.content) ? body.content : [], requestUrl: requestUrl.toString() };
  216 | }
  217 |
```