# 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:154:7

# Error details

```
Error: 영업점 계정에는 배송 이미지 등록 버튼이 보이면 안 됩니다.

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

Expected: false
Received: true
```

# Test source

```ts
  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");
  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);
      |                                                                                                                                  ^ Error: 영업점 계정에는 배송 이미지 등록 버튼이 보이면 안 됩니다.
  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 |         });
  188 |       }
  189 | 
  190 |       test(checklist.checklist[18], async ({ browser }, testInfo) => {
  191 |         testInfo.setTimeout(90_000);
  192 |         const [veSnapshot, coSnapshot, boSnapshot] = await Promise.all([
  193 |           collectStoreEvidence(browser, requiredVe, storePage, testInfo, "department-user-ve"),
  194 |           collectStoreEvidence(browser, coRequired, storePage, testInfo, "department-user-co"),
  195 |           collectStoreEvidence(browser, boRequired, storePage, testInfo, "department-user-bo")
  196 |         ]);
  197 |         const veLink = findTransaction(veSnapshot, { paymentType: "link", cancelled: false, approvalNo: REQUIRED_LINK_APPROVAL });
  198 |         const coLinks = coSnapshot.transactions.filter((transaction) => transaction.paymentType === "link");
  199 |         const boLinks = boSnapshot.transactions.filter((transaction) => transaction.paymentType === "link");
  200 |         expect(veLink, "가맹점 계정에서 링크결제 승인 거래가 보여야 합니다.").toBeTruthy();
  201 |         expect(hasDeliveryButton(veLink?.text ?? ""), "해당 거래의 가맹점 사용자에게는 이미지 등록/삭제 진입 버튼이 보여야 합니다.").toBe(true);
  202 |         expect(coLinks.some((transaction) => hasDeliveryButton(transaction.text)), "법인 계정에는 이미지 등록/삭제 버튼이 보이면 안 됩니다.").toBe(false);
  203 |         expect(boLinks.some((transaction) => hasDeliveryButton(transaction.text)), "영업점 계정에는 이미지 등록/삭제 버튼이 보이면 안 됩니다.").toBe(false);
  204 |       });
  205 |     });
  206 | 
  207 |     test.describe("[ADMIN] role / 어드민 거래내역", () => {
  208 |       test(checklist.checklist[19], async ({ browser }, testInfo) => {
  209 |         testInfo.setTimeout(75_000);
  210 |         const snapshot = await collectAdminEvidence(browser, adminEnv, adminPage, testInfo, "admin-required-link-image");
  211 |         const rowText = rowAround(snapshot.bodyText, REQUIRED_LINK_APPROVAL);
  212 |         expect(rowText, "필수 ON 링크결제 거래가 어드민 거래내역에 보여야 합니다.").toContain(REQUIRED_LINK_APPROVAL);
  213 |         expect(hasDeliveryButton(rowText) || hasDeliveryButton(snapshot.buttonTexts.join("\n")), "어드민 거래내역에서 필수 ON 링크결제 거래의 배송 이미지 확인이 가능해야 합니다.").toBe(true);
  214 |       });
  215 | 
  216 |       test(checklist.checklist[20], async ({ browser }, testInfo) => {
  217 |         testInfo.setTimeout(75_000);
  218 |         const snapshot = await collectAdminEvidence(browser, adminEnv, adminPage, testInfo, "admin-required-link-empty-image");
  219 |         const rowText = rowAround(snapshot.bodyText, REQUIRED_LINK_APPROVAL);
  220 |         expect(rowText, "필수 ON 링크결제 거래가 어드민 거래내역에 보여야 합니다.").toContain(REQUIRED_LINK_APPROVAL);
  221 |         expect(/이미지가\s*없|이미지\s*없|없습니다/.test(rowText) || hasDeliveryButton(rowText), "등록된 배송 이미지가 없으면 이미지가 없습니다 안내 또는 이미지 확인 진입이 보여야 합니다.").toBe(true);
  222 |       });
  223 | 
  224 |       test(checklist.checklist[21], async ({ browser }, testInfo) => {
  225 |         testInfo.setTimeout(75_000);
  226 |         const snapshot = await collectAdminEvidence(browser, adminEnv, adminPage, testInfo, "admin-optional-link-no-image");
  227 |         const rowText = rowAround(snapshot.bodyText, OPTIONAL_LINK_APPROVAL);
  228 |         expect(rowText, "필수 OFF 링크결제 거래가 어드민 거래내역에 보여야 합니다.").toContain(OPTIONAL_LINK_APPROVAL);
  229 |         expect(hasDeliveryButton(rowText), "필수 OFF 링크결제 거래에는 이미지 조회 버튼이 보이면 안 됩니다.").toBe(false);
  230 |       });
  231 |     });
  232 |   });
  233 | }
  234 | 
  235 | async function attachAppPushEvidence(
  236 |   testInfo: TestInfo,
  237 |   options: ScenarioOptions,
  238 |   slug: string
  239 | ): Promise<AppPushEvidence | undefined> {
  240 |   const evidenceRoot = path.join(process.cwd(), "qa", "issues", options.issueId, options.environment, "artifacts", slug);
  241 |   const evidencePath = path.join(evidenceRoot, "evidence.json");
  242 |   if (!fs.existsSync(evidencePath)) {
  243 |     return undefined;
  244 |   }
  245 | 
  246 |   const evidence = JSON.parse(fs.readFileSync(evidencePath, "utf8")) as AppPushEvidence;
  247 |   await testInfo.attach(`${slug}-evidence.md`, {
  248 |     body: [
  249 |       `# ${evidence.title}`,
  250 |       "",
  251 |       `- 기대 결과: ${evidence.expected}`,
  252 |       `- 실제 결과: ${evidence.actual}`,
  253 |       evidence.paymentUrl ? `- 결제 링크: ${evidence.paymentUrl}` : undefined,
  254 |       evidence.paymentResultUrl ? `- 결제 완료 URL: ${evidence.paymentResultUrl}` : undefined,
  255 |       evidence.finalUrl ? `- 최종 도착 URL: ${evidence.finalUrl}` : undefined,
  256 |       evidence.notification ? `- 수신 푸시: ${evidence.notification.title ?? ""} / ${evidence.notification.body ?? ""}` : undefined,
  257 |       "",
  258 |       "## 실제 진행",
  259 |       ...(evidence.steps ?? []).map((step, index) => `${index + 1}. ${step}`)
```