# 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: 2911/dev/lucypay-payment-log.spec.ts >> [2911][dev] [QA] 루시페이먼츠 ISP 결제/취소 및 거래 로그 재검증 >> [VE] role >> [ve] 가맹점 결제 테스트 화면에서 스토어의 결제하기 버튼 클릭 후, 루시페이먼츠 ISP 모달창에서는 별도 결제하기 버튼 클릭 없이 ISP 창이 바로 열리는지 확인
- Location: src/qa/scenarios/lucypayPaymentLog.ts:148:7

# Error details

```
Error: ISP 결제창은 결제 시작 직후 새 창으로 열려야 합니다.

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

Expected: true
Received: false
```

# Test source

```ts
  71  |   role: string;
  72  |   manualPayment?: CreatedPayment;
  73  |   ispPayment?: CreatedPayment;
  74  |   paymentLogPayment?: CreatedPayment;
  75  |   cancelPayment?: CreatedPayment;
  76  |   receiptUrl?: string;
  77  |   popupUrl?: string;
  78  |   cancelStatuses: string[];
  79  |   partialCancelLines: string[];
  80  |   idPairs: string[];
  81  | }
  82  |
  83  | interface ManualPaymentResponsePayload {
  84  |   resultCode?: string;
  85  |   resultMessage?: string;
  86  |   isSuccess?: boolean;
  87  |   transactionId?: string;
  88  | }
  89  |
  90  | interface IspStartResult {
  91  |   popupOpened: boolean;
  92  |   loggingStatus?: number;
  93  |   paymentStatus?: number;
  94  |   completed: boolean;
  95  |   completedStatus?: string;
  96  |   resultText?: string;
  97  |   extraConfirmButtonVisible?: boolean;
  98  |   created?: CreatedPayment;
  99  |   note: string;
  100 | }
  101 |
  102 | interface CancelEvidence {
  103 |   cancelAmount: number;
  104 |   responseStatus: number;
  105 |   cancelInfoText: string;
  106 | }
  107 |
  108 | const adminRoles = ["ADMIN", "TA", "CO", "BO"] as const;
  109 | type AdminRole = (typeof adminRoles)[number];
  110 |
  111 | const runState: {
  112 |   manualPrimary?: CreatedPayment;
  113 |   ispPrimary?: CreatedPayment;
  114 |   ispAutoCompletes: boolean;
  115 |   partialCancelPayment?: CreatedPayment;
  116 |   roleRuns: Record<string, RoleRunEvidence>;
  117 |   createdPayments: CreatedPayment[];
  118 | } = {
  119 |   ispAutoCompletes: false,
  120 |   roleRuns: {},
  121 |   createdPayments: []
  122 | };
  123 |
  124 | export function runLucypayPaymentLogSuite(options: ScenarioOptions): void {
  125 |   const metadata = loadIssueMetadata(options.issueId);
  126 |   const checklist = loadChecklist(options.issueId, options.environment);
  127 |   const storePaymentPage = findPageDefinition(checklist.pages, "가맹점 결제 테스트");
  128 |   const transactionPage = findPageDefinition(checklist.pages, "거래로그");
  129 |   const storeEnv = getRuntimeQaEnv(options.environment, "store", "LUCY_VE");
  130 |   const adminEnv = getRuntimeQaEnv(options.environment, "admin", "ADMIN");
  131 |   const roleEnvs = Object.fromEntries(
  132 |     adminRoles.map((role) => [role, getRuntimeQaEnv(options.environment, "admin", role)])
  133 |   ) as Record<AdminRole, RuntimeQaEnv>;
  134 |   const paymentConfig = getPaymentConfig();
  135 |   const baseBlockReason =
  136 |     getRuntimeBlockReason(storeEnv) ??
  137 |     getRuntimeBlockReason(adminEnv) ??
  138 |     getPaymentBlockReason(paymentConfig);
  139 |
  140 |   test.describe(`[${options.issueId}][${options.environment}] ${metadata.subject}`, () => {
  141 |     test.afterAll(async ({ browser }) => {
  142 |       await cleanupCreatedPayments(browser, adminEnv, transactionPage);
  143 |     });
  144 |
  145 |     test.describe("[VE] role", () => {
  146 |       test.skip(!!baseBlockReason, baseBlockReason);
  147 |
  148 |       test(checklist.checklist[0], async ({ browser }, testInfo) => {
  149 |         test.setTimeout(90_000);
  150 |         const result = await startIspPayment(
  151 |           browser,
  152 |           storeEnv,
  153 |           adminEnv,
  154 |           storePaymentPage,
  155 |           paymentConfig,
  156 |           options.issueId,
  157 |           "isp-popup",
  158 |           false,
  159 |           testInfo
  160 |         );
  161 |
  162 |         await attachEvidenceLog(testInfo, storePaymentPage.name, "VE", "isp-popup-opened", [
  163 |           "스토어 결제하기 버튼 클릭 후 루시페이먼츠 ISP 모달에서 추가 결제 버튼 클릭 없이 ISP 창이 바로 열리는지 확인했습니다.",
  164 |           `결제창 열림: ${result.popupOpened ? "예" : "아니오"}`,
  165 |           `추가 결제 버튼 노출: ${result.extraConfirmButtonVisible ? "예" : "아니오"}`,
  166 |           `ISP logging 응답 status: ${result.loggingStatus ?? "-"}`,
  167 |           `ISP payment HTML 응답 status: ${result.paymentStatus ?? "-"}`,
  168 |           `비고: ${result.note}`
  169 |         ]);
  170 |
> 171 |         expect(result.popupOpened, "ISP 결제창은 결제 시작 직후 새 창으로 열려야 합니다.").toBe(true);
      |                                                                        ^ Error: ISP 결제창은 결제 시작 직후 새 창으로 열려야 합니다.
  172 |         expect(result.extraConfirmButtonVisible, "루시페이먼츠 ISP 모달에 추가 결제 버튼이 남아 있으면 안 됩니다.").toBe(false);
  173 |         expect(result.paymentStatus, "ISP 결제 HTML 응답은 성공이어야 합니다.").toBeGreaterThanOrEqual(200);
  174 |         expect(result.paymentStatus, "ISP 결제 HTML 응답은 성공이어야 합니다.").toBeLessThan(400);
  175 |       });
  176 |
  177 |       test(checklist.checklist[1], async ({ browser }, testInfo) => {
  178 |         test.setTimeout(180_000);
  179 |         const completedIspPayment = await createCompletedIspOrBlock(
  180 |           browser,
  181 |           storeEnv,
  182 |           adminEnv,
  183 |           storePaymentPage,
  184 |           paymentConfig,
  185 |           options.issueId,
  186 |           "isp-primary",
  187 |           testInfo
  188 |         );
  189 |         runState.ispPrimary = completedIspPayment;
  190 |         runState.ispAutoCompletes = true;
  191 |         await attachEvidenceLog(testInfo, storePaymentPage.name, "VE", "isp-complete", [
  192 |           "루시페이먼츠 ISP 결제가 완료되어 스토어 결과 화면의 성공 안내와 운영자 거래 생성을 확인했습니다.",
  193 |           `거래 ID: ${redactId(completedIspPayment.transactionId ?? "")}`,
  194 |           `승인번호: ${completedIspPayment.approvalNo ?? "-"}`,
  195 |           `PG사: ${completedIspPayment.pgName ?? "-"}`,
  196 |           `payType: ${completedIspPayment.payType}`
  197 |         ]);
  198 |       });
  199 |     });
  200 |
  201 |     for (const [index, role] of adminRoles.entries()) {
  202 |       test.describe(`[${role}] role`, () => {
  203 |         const roleEnv = roleEnvs[role];
  204 |         const roleBlockReason = getSkippedRoleReason(role) ?? getRuntimeBlockReason(roleEnv) ?? baseBlockReason;
  205 |
  206 |         test.skip(!!roleBlockReason, roleBlockReason);
  207 |
  208 |         test(checklist.checklist[2 + index], async ({ browser }, testInfo) => {
  209 |           test.setTimeout(120_000);
  210 |           const evidence = getRoleEvidence(role);
  211 |           const payment = runState.ispPrimary;
  212 |           if (!payment?.approvalNo) {
  213 |             await attachEvidenceLog(testInfo, transactionPage.name, role, "payment-log-blocked", [
  214 |               `${role} 거래 로그 검증에 사용할 완료 ISP 거래가 없습니다.`,
  215 |               "선행 VE ISP 결제 완료 항목의 외부 결제창 승인 결과를 확인하세요."
  216 |             ]);
  217 |             blocked(`${role} 거래 로그 검증에 사용할 완료 ISP 거래가 없습니다.`);
  218 |           }
  219 |
  220 |           await verifyIspPaymentVisibleFromRole(browser, roleEnv, transactionPage, payment, role, testInfo);
  221 |           evidence.paymentLogPayment = payment;
  222 |         });
  223 |
  224 |         test(checklist.checklist[6 + index], async ({ browser }, testInfo) => {
  225 |           test.setTimeout(210_000);
  226 |           const evidence = getRoleEvidence(role);
  227 |           if (!runState.ispAutoCompletes) {
  228 |             await attachEvidenceLog(testInfo, transactionPage.name, role, "cancel-isp-blocked", [
  229 |               `${role} 취소 검증에 사용할 완료 ISP 거래가 없습니다.`,
  230 |               "선행 VE ISP 결제 완료 항목에서 외부 ISP 승인 완료 또는 거래 통보 반영이 발생하지 않았습니다.",
  231 |               "동일 외부 조건을 role별로 반복하지 않고 확인불가로 기록합니다."
  232 |             ]);
  233 |             blocked(`${role} 계정의 ISP 결제 취소 검증에 사용할 완료 ISP 거래가 없습니다.`);
  234 |           }
  235 |
  236 |           const ispPayment = await createCompletedIspOrBlock(
  237 |             browser,
  238 |             storeEnv,
  239 |             adminEnv,
  240 |             storePaymentPage,
  241 |             paymentConfig,
  242 |             options.issueId,
  243 |             `${role.toLowerCase()}-isp-cancel`,
  244 |             testInfo
  245 |           );
  246 |
  247 |           evidence.cancelPayment = ispPayment;
  248 |           const cancelEvidence = await cancelPaymentFromRole(browser, roleEnv, transactionPage, ispPayment, role, testInfo);
  249 |           evidence.cancelStatuses.push(`${ispPayment.payType}:${ispPayment.status ?? "-"}`);
  250 |           evidence.idPairs.push(formatIdPair(ispPayment));
  251 |           await attachEvidenceLog(testInfo, transactionPage.name, role, "cancel-manual-isp", [
  252 |             `${role} 계정으로 루시페이먼츠 ISP 결제 취소와 취소 금액 표시를 확인했습니다.`,
  253 |             `ISP 승인번호/상태: ${ispPayment.approvalNo ?? "-"} / ${ispPayment.status ?? "-"}`,
  254 |             `취소 요청 금액: ${formatNumber(cancelEvidence.cancelAmount)}원`,
  255 |             `취소 응답 status: ${cancelEvidence.responseStatus}`,
  256 |             `화면/API 취소 정보: ${cancelEvidence.cancelInfoText}`
  257 |           ]);
  258 |         });
  259 |       });
  260 |     }
  261 |
  262 |     test.describe("[ADMIN/TA/CO/BO] role", () => {
  263 |       test.skip(!!baseBlockReason, baseBlockReason);
  264 |
  265 |       test(checklist.checklist[10], async ({ browser }, testInfo) => {
  266 |         test.setTimeout(240_000);
  267 |         if (!runState.ispAutoCompletes) {
  268 |           await attachEvidenceLog(testInfo, transactionPage.name, "ALL", "partial-cancel-blocked", [
  269 |             "다중 부분취소 검증에 사용할 완료 ISP 거래가 없습니다.",
  270 |             "선행 VE ISP 결제 완료 항목에서 외부 ISP 승인 완료 또는 거래 통보 반영이 발생하지 않았습니다.",
  271 |             "부분취소는 완료 승인 거래가 필요한 항목이므로 확인불가로 기록합니다."
```