# 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: 3318/dev/delivery-image-push.spec.ts >> [3318][dev] 상품 배송 이미지 등록 필수 설정 링크결제/푸시 QA >> [VE] role / 앱 푸시 >> 스토어 환경에서 상품 배송 이미지 등록 필수 설정이 켜진 가맹점의 링크결제 완료 후, 앱 푸시 클릭 시 상품 배송 이미지 등록 화면으로 이동하는지 확인
- Location: src/qa/scenarios/deliveryImagePush.ts:79:7

# Error details

```
Error: 푸시는 도착했지만, 푸시 탭 후 PIN 인증과 로그인/2FA를 거친 뒤 최종 화면이 홈 화면으로 이동했습니다. 상품 배송 이미지 등록 화면으로 이어지지 않았습니다.

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

Expected: "pass"
Received: "fail"
```

# Test source

```ts
  1   | import fs from "node:fs";
  2   | import path from "node:path";
  3   | import { expect, test, type Browser, type BrowserContext, type Page, type TestInfo } from "@playwright/test";
  4   | import { isLoginPage, loginWithCredentials } from "../auth";
  5   | import { loadChecklist, loadIssueMetadata } from "../checklist";
  6   | import { getRuntimeQaEnv, type RuntimeQaEnv } from "../env";
  7   | import type { QaEnvironment, QaPageDefinition } from "../types";
  8   | 
  9   | interface ScenarioOptions {
  10  |   issueId: string;
  11  |   environment: QaEnvironment;
  12  | }
  13  | 
  14  | interface StoreSnapshot {
  15  |   bodyText: string;
  16  |   buttonTexts: string[];
  17  |   linkText: string[];
  18  |   transactions: TransactionBlock[];
  19  | }
  20  | 
  21  | interface TransactionBlock {
  22  |   status: string;
  23  |   text: string;
  24  |   paymentType: "link" | "manual" | "unknown";
  25  |   isCancelled: boolean;
  26  |   approvalNo?: string;
  27  | }
  28  | 
  29  | interface AdminSnapshot {
  30  |   bodyText: string;
  31  |   buttonTexts: string[];
  32  | }
  33  | 
  34  | interface SubjectRuntime {
  35  |   role: string;
  36  |   label: string;
  37  |   envRole: string;
  38  |   runtimeEnv: RuntimeQaEnv;
  39  | }
  40  | 
  41  | interface AppPushEvidence {
  42  |   status: "pass" | "fail" | "blocked";
  43  |   title: string;
  44  |   expected: string;
  45  |   actual: string;
  46  |   paymentUrl?: string;
  47  |   paymentResultUrl?: string;
  48  |   finalUrl?: string;
  49  |   notification?: {
  50  |     title?: string;
  51  |     body?: string;
  52  |   };
  53  |   steps?: string[];
  54  |   screenshots?: string[];
  55  | }
  56  | 
  57  | const STORE_PATH = "/transactions";
  58  | const ADMIN_PATH = "/trade-history-v2";
  59  | const DELIVERY_BUTTON_PATTERN = /배송\s*(이미지|사진)|상품\s*배송|이미지\s*(등록|업로드|보기|확인)|사진\s*(등록|업로드|보기|확인)/;
  60  | const DELIVERY_STATUS_PATTERN = /미등록|등록\s*완료|이미지\s*없|배송\s*(이미지|사진)/;
  61  | const REQUIRED_LINK_APPROVAL = process.env.QA_3318_REQUIRED_LINK_APPROVAL_NO ?? "15834237";
  62  | const OPTIONAL_LINK_APPROVAL = process.env.QA_3318_OPTIONAL_LINK_APPROVAL_NO ?? "47369256";
  63  | const REQUIRED_MANUAL_APPROVAL = process.env.QA_3318_REQUIRED_MANUAL_APPROVAL_NO ?? "15850408";
  64  | 
  65  | export function runDeliveryImagePushSuite(options: ScenarioOptions): void {
  66  |   const metadata = loadIssueMetadata(options.issueId);
  67  |   const checklist = loadChecklist(options.issueId, options.environment);
  68  |   const storePage = findPage(checklist.pages, "스토어 거래내역", STORE_PATH);
  69  |   const deliveryPage = findPage(checklist.pages, "상품 배송 이미지 등록", STORE_PATH);
  70  |   const adminPage = findPage(checklist.pages, "어드민 거래내역", ADMIN_PATH);
  71  |   const requiredVe = subjectRuntime(options.environment, "VE", "VE_DELIVERY_REQUIRED", "상품 배송 이미지 필수 가맹점");
  72  |   const optionalVe = subjectRuntime(options.environment, "VE", "VE_DELIVERY_OPTIONAL", "상품 배송 이미지 미사용 가맹점");
  73  |   const boRequired = subjectRuntime(options.environment, "BO", "BO_DELIVERY_REQUIRED", "필수 설정 연결 영업점");
  74  |   const coRequired = subjectRuntime(options.environment, "CO", "CO_DELIVERY_REQUIRED", "필수 설정 연결 법인");
  75  |   const adminEnv = getRuntimeQaEnv(options.environment, "admin", "ADMIN");
  76  | 
  77  |   test.describe(`[${options.issueId}][${options.environment}] ${metadata.subject}`, () => {
  78  |     test.describe("[VE] role / 앱 푸시", () => {
  79  |       test(checklist.checklist[0], async ({}, testInfo) => {
  80  |         const evidence = await attachAppPushEvidence(testInfo, options, "app-push-required-link");
  81  |         if (!evidence) {
  82  |           blocked("필수 ON 가맹점의 앱 푸시 실기기 증거가 없어 이동 결과를 확인할 수 없습니다.");
  83  |         }
  84  |         if (evidence.status === "blocked") {
  85  |           blocked(evidence.actual);
  86  |         }
> 87  |         expect(evidence.status, evidence.actual).toBe("pass");
      |                                                  ^ Error: 푸시는 도착했지만, 푸시 탭 후 PIN 인증과 로그인/2FA를 거친 뒤 최종 화면이 홈 화면으로 이동했습니다. 상품 배송 이미지 등록 화면으로 이어지지 않았습니다.
  88  |       });
  89  | 
  90  |       test(checklist.checklist[1], async ({}, testInfo) => {
  91  |         const evidence = await attachAppPushEvidence(testInfo, options, "app-push-optional-link");
  92  |         if (!evidence) {
  93  |           blocked("필수 OFF 가맹점의 앱 푸시 실기기 증거가 없어 기존 거래내역 이동 결과를 확인할 수 없습니다.");
  94  |         }
  95  |         if (evidence.status === "blocked") {
  96  |           blocked(evidence.actual);
  97  |         }
  98  |         expect(evidence.status, evidence.actual).toBe("pass");
  99  |       });
  100 | 
  101 |       test(checklist.checklist[2], async ({}, testInfo) => {
  102 |         const evidence = await attachAppPushEvidence(testInfo, options, "app-push-required-manual");
  103 |         if (!evidence) {
  104 |           blocked("필수 ON 가맹점의 수기결제 앱 푸시 실기기 증거가 없어 기존 거래내역 이동 결과를 확인할 수 없습니다.");
  105 |         }
  106 |         if (evidence.status === "blocked") {
  107 |           blocked(evidence.actual);
  108 |         }
  109 |         expect(evidence.status, evidence.actual).toBe("pass");
  110 |       });
  111 |     });
  112 | 
  113 |     test.describe("[VE] role / 스토어 거래내역", () => {
  114 |       test(checklist.checklist[3], async ({ browser }, testInfo) => {
  115 |         testInfo.setTimeout(75_000);
  116 |         const snapshot = await collectStoreEvidence(browser, requiredVe, storePage, testInfo, "required-link-list");
  117 |         const target = findTransaction(snapshot, { paymentType: "link", cancelled: false, approvalNo: REQUIRED_LINK_APPROVAL });
  118 |         expect(target, "필수 ON 가맹점의 링크결제 승인 거래가 목록에 보여야 합니다.").toBeTruthy();
  119 |         expect(hasDeliveryButton(target?.text ?? ""), "필수 ON 가맹점의 링크결제 거래에는 배송 이미지 버튼이 보여야 합니다.").toBe(true);
  120 |       });
  121 | 
  122 |       test(checklist.checklist[4], async ({ browser }, testInfo) => {
  123 |         testInfo.setTimeout(75_000);
  124 |         const snapshot = await collectStoreEvidence(browser, optionalVe, storePage, testInfo, "optional-link-list");
  125 |         const target = findTransaction(snapshot, { paymentType: "link", cancelled: false, approvalNo: OPTIONAL_LINK_APPROVAL });
  126 |         expect(target, "필수 OFF 가맹점의 링크결제 승인 거래가 목록에 보여야 합니다.").toBeTruthy();
  127 |         expect(hasDeliveryButton(target?.text ?? ""), "필수 OFF 가맹점의 링크결제 거래에는 배송 이미지 버튼이 보이면 안 됩니다.").toBe(false);
  128 |       });
  129 | 
  130 |       test(checklist.checklist[5], async ({ browser }, testInfo) => {
  131 |         testInfo.setTimeout(75_000);
  132 |         const snapshot = await collectStoreEvidence(browser, requiredVe, storePage, testInfo, "required-manual-list");
  133 |         const target = findTransaction(snapshot, { paymentType: "manual", cancelled: false, approvalNo: REQUIRED_MANUAL_APPROVAL });
  134 |         expect(target, "필수 ON 가맹점의 수기결제 승인 거래가 목록에 보여야 합니다.").toBeTruthy();
  135 |         expect(hasDeliveryButton(target?.text ?? ""), "수기결제 거래에는 배송 이미지 버튼이 보이면 안 됩니다.").toBe(false);
  136 |       });
  137 | 
  138 |       test(checklist.checklist[6], async ({ browser }, testInfo) => {
  139 |         testInfo.setTimeout(75_000);
  140 |         const snapshot = await collectStoreEvidence(browser, requiredVe, storePage, testInfo, "required-cancel-list");
  141 |         const target = findTransaction(snapshot, { paymentType: "link", cancelled: true, approvalNo: REQUIRED_LINK_APPROVAL });
  142 |         expect(target, "취소 상태 링크결제 거래가 목록에 보여야 합니다.").toBeTruthy();
  143 |         expect(hasDeliveryButton(target?.text ?? ""), "취소 상태 거래에는 배송 이미지 업로드/보기 버튼이 보이면 안 됩니다.").toBe(false);
  144 |       });
  145 | 
  146 |       test(checklist.checklist[7], async ({ browser }, testInfo) => {
  147 |         testInfo.setTimeout(75_000);
  148 |         const snapshot = await collectStoreEvidence(browser, coRequired, storePage, testInfo, "co-required-list");
  149 |         const linkTransactions = snapshot.transactions.filter((transaction) => transaction.paymentType === "link");
  150 |         expect(linkTransactions.length, "법인 계정에서 링크결제 거래가 조회되어야 권한 제한을 확인할 수 있습니다.").toBeGreaterThan(0);
  151 |         expect(linkTransactions.some((transaction) => hasDeliveryButton(transaction.text)), "법인 계정에는 배송 이미지 등록 버튼이 보이면 안 됩니다.").toBe(false);
  152 |       });
  153 | 
  154 |       test(checklist.checklist[8], async ({ browser }, testInfo) => {
  155 |         testInfo.setTimeout(75_000);
  156 |         const snapshot = await collectStoreEvidence(browser, boRequired, storePage, testInfo, "bo-required-list");
  157 |         const linkTransactions = snapshot.transactions.filter((transaction) => transaction.paymentType === "link");
  158 |         expect(linkTransactions.length, "영업점 계정에서 링크결제 거래가 조회되어야 권한 제한을 확인할 수 있습니다.").toBeGreaterThan(0);
  159 |         expect(linkTransactions.some((transaction) => hasDeliveryButton(transaction.text)), "영업점 계정에는 배송 이미지 등록 버튼이 보이면 안 됩니다.").toBe(false);
  160 |       });
  161 | 
  162 |       test(checklist.checklist[9], async ({ browser }, testInfo) => {
  163 |         testInfo.setTimeout(75_000);
  164 |         const snapshot = await collectStoreEvidence(browser, requiredVe, storePage, testInfo, "required-only-register");
  165 |         const linkTransaction = findTransaction(snapshot, { paymentType: "link", cancelled: false, approvalNo: REQUIRED_LINK_APPROVAL });
  166 |         expect(linkTransaction, "가맹점 계정의 링크결제 승인 거래가 목록에 보여야 합니다.").toBeTruthy();
  167 |         expect(hasDeliveryButton(linkTransaction?.text ?? ""), "가맹점 계정의 링크결제 거래에는 배송 이미지 등록 버튼이 보여야 합니다.").toBe(true);
  168 |       });
  169 | 
  170 |       test(checklist.checklist[10], async ({ browser }, testInfo) => {
  171 |         testInfo.setTimeout(75_000);
  172 |         const snapshot = await collectStoreEvidence(browser, requiredVe, storePage, testInfo, "delivery-status");
  173 |         expect(DELIVERY_STATUS_PATTERN.test(snapshot.bodyText), "스토어 거래내역에서 미등록/등록완료 또는 이미지 상태가 구분되어야 합니다.").toBe(true);
  174 |       });
  175 |     });
  176 | 
  177 |     test.describe("[VE] role / 상품 배송 이미지 등록", () => {
  178 |       for (let index = 11; index <= 17; index += 1) {
  179 |         test(checklist.checklist[index], async ({ browser }, testInfo) => {
  180 |           testInfo.setTimeout(75_000);
  181 |           const snapshot = await collectStoreEvidence(browser, requiredVe, deliveryPage, testInfo, `delivery-page-${index + 1}`);
  182 |           const linkTransaction = findTransaction(snapshot, { paymentType: "link", cancelled: false, approvalNo: REQUIRED_LINK_APPROVAL });
  183 |           if (!linkTransaction) {
  184 |             blocked("가맹점 계정의 링크결제 승인 거래가 목록에 없어 상품 배송 이미지 등록 화면으로 진입할 수 없습니다.");
  185 |           }
  186 |           expect(hasDeliveryButton(linkTransaction.text), "배송 이미지 등록 화면으로 진입할 버튼이 보여야 합니다.").toBe(true);
  187 |         });
```