# 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 >> [ADMIN] role / 어드민 거래내역 >> 어드민/비스킷링크 거래내역에서 접근 가능한 상품 배송 이미지 등록 필수 설정이 켜진 가맹점의 링크결제 거래는 배송 이미지 확인이 가능한지 확인
- Location: src/qa/scenarios/deliveryImagePush.ts:208:7

# Error details

```
Error: 어드민 거래내역에서 필수 ON 링크결제 거래의 배송 이미지 확인이 가능해야 합니다.

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

Expected: true
Received: false
```

# Test source

```ts
  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 |         });
  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);
      |                                                                                                                                                    ^ Error: 어드민 거래내역에서 필수 ON 링크결제 거래의 배송 이미지 확인이 가능해야 합니다.
  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}`)
  260 |     ].filter((line): line is string => typeof line === "string").join("\n"),
  261 |     contentType: "text/markdown"
  262 |   });
  263 | 
  264 |   for (const screenshot of evidence.screenshots ?? []) {
  265 |     const screenshotPath = path.join(evidenceRoot, screenshot);
  266 |     if (!fs.existsSync(screenshotPath)) {
  267 |       continue;
  268 |     }
  269 |     await testInfo.attach(`${slug}-${screenshot}`, {
  270 |       body: fs.readFileSync(screenshotPath),
  271 |       contentType: "image/png"
  272 |     });
  273 |   }
  274 | 
  275 |   return evidence;
  276 | }
  277 | 
  278 | async function collectStoreEvidence(
  279 |   browser: Browser,
  280 |   subject: SubjectRuntime,
  281 |   pageDefinition: QaPageDefinition,
  282 |   testInfo: TestInfo,
  283 |   suffix: string
  284 | ): Promise<StoreSnapshot> {
  285 |   return withRolePage(browser, subject.runtimeEnv, async (page) => {
  286 |     await openAuthenticatedPath(page, subject.runtimeEnv, pageDefinition.path || STORE_PATH);
  287 |     await settle(page);
  288 |     const snapshot = await storeSnapshot(page);
  289 |     await attachPageScreenshot(page, testInfo, pageDefinition.name, subject.envRole, suffix);
  290 |     await attachEvidenceLog(testInfo, pageDefinition.name, subject.role, suffix, [
  291 |       `${subject.label}(${subject.envRole}) 계정으로 ${page.url()} 화면을 조회했습니다.`,
  292 |       `거래 블록 수: ${snapshot.transactions.length}`,
  293 |       `버튼 텍스트: ${snapshot.buttonTexts.join(" / ") || "없음"}`,
  294 |       `링크결제 승인번호 후보: ${snapshot.transactions.filter((transaction) => transaction.paymentType === "link").map((transaction) => transaction.approvalNo ?? "번호 없음").join(", ") || "없음"}`,
  295 |       "화면 텍스트 일부:",
  296 |       snapshot.bodyText.slice(0, 2500)
  297 |     ]);
  298 |     return snapshot;
  299 |   });
  300 | }
  301 | 
  302 | async function collectAdminEvidence(
  303 |   browser: Browser,
  304 |   runtimeEnv: RuntimeQaEnv,
  305 |   pageDefinition: QaPageDefinition,
  306 |   testInfo: TestInfo,
  307 |   suffix: string
  308 | ): Promise<AdminSnapshot> {
  309 |   return withRolePage(browser, runtimeEnv, async (page) => {
  310 |     await openAuthenticatedPath(page, runtimeEnv, pageDefinition.path || ADMIN_PATH);
  311 |     await settle(page);
  312 |     const snapshot = await adminSnapshot(page);
  313 |     await attachPageScreenshot(page, testInfo, pageDefinition.name, runtimeEnv.role, suffix);
```