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

# Error details

```
Error: BLOCKED: 필수 OFF 가맹점의 앱 푸시 실기기 증거가 없어 기존 거래내역 이동 결과를 확인할 수 없습니다.
```

# Test source

```ts
  423 |       return false;
  424 |     }
  425 |     if (options.approvalNo && transaction.approvalNo !== options.approvalNo) {
  426 |       return false;
  427 |     }
  428 |     return true;
  429 |   }) ?? snapshot.transactions.find((transaction) => {
  430 |     if (transaction.paymentType !== options.paymentType) {
  431 |       return false;
  432 |     }
  433 |     if (typeof options.cancelled === "boolean" && transaction.isCancelled !== options.cancelled) {
  434 |       return false;
  435 |     }
  436 |     return true;
  437 |   });
  438 | }
  439 |
  440 | function rowAround(bodyText: string, needle: string): string {
  441 |   const index = bodyText.indexOf(needle);
  442 |   if (index < 0) {
  443 |     return "";
  444 |   }
  445 |   return bodyText.slice(Math.max(0, index - 500), Math.min(bodyText.length, index + 800));
  446 | }
  447 |
  448 | function hasDeliveryButton(text: string): boolean {
  449 |   return DELIVERY_BUTTON_PATTERN.test(text);
  450 | }
  451 |
  452 | async function visibleTexts(page: Page, selector: string): Promise<string[]> {
  453 |   return page.locator(selector).evaluateAll((nodes) =>
  454 |     nodes
  455 |       .map((node) => (node.textContent ?? "").replace(/\s+/g, " ").trim())
  456 |       .filter(Boolean)
  457 |   ).catch(() => []);
  458 | }
  459 |
  460 | async function settle(page: Page): Promise<void> {
  461 |   await page.waitForLoadState("networkidle", { timeout: 8_000 }).catch(() => undefined);
  462 |   await page.waitForTimeout(1_000);
  463 | }
  464 |
  465 | async function attachPageScreenshot(
  466 |   page: Page,
  467 |   testInfo: TestInfo,
  468 |   pageName: string,
  469 |   role: string,
  470 |   suffix: string
  471 | ): Promise<void> {
  472 |   const image = await page.screenshot({ fullPage: true }).catch(() => undefined);
  473 |   if (!image) {
  474 |     return;
  475 |   }
  476 |   await testInfo.attach(`${safeName(pageName)}-${safeName(role)}-${suffix}.png`, {
  477 |     body: image,
  478 |     contentType: "image/png"
  479 |   });
  480 | }
  481 |
  482 | async function attachEvidenceLog(
  483 |   testInfo: TestInfo,
  484 |   pageName: string,
  485 |   role: string,
  486 |   suffix: string,
  487 |   lines: string[]
  488 | ): Promise<void> {
  489 |   await testInfo.attach(`${safeName(pageName)}-${safeName(role)}-${suffix}.md`, {
  490 |     body: [`# ${pageName} / ${role}`, "", ...lines].join("\n"),
  491 |     contentType: "text/markdown"
  492 |   });
  493 | }
  494 |
  495 | async function attachBlockedEvidence(
  496 |   testInfo: TestInfo,
  497 |   pageName: string,
  498 |   role: string,
  499 |   suffix: string,
  500 |   lines: string[]
  501 | ): Promise<void> {
  502 |   await attachEvidenceLog(testInfo, pageName, role, suffix, lines);
  503 | }
  504 |
  505 | function subjectRuntime(environment: QaEnvironment, role: string, envRole: string, label: string): SubjectRuntime {
  506 |   return {
  507 |     role,
  508 |     label,
  509 |     envRole,
  510 |     runtimeEnv: getRuntimeQaEnv(environment, "store", envRole)
  511 |   };
  512 | }
  513 |
  514 | function findPage(pages: QaPageDefinition[], name: string, fallbackPath: string): QaPageDefinition {
  515 |   return pages.find((page) => page.name === name) ?? {
  516 |     name,
  517 |     application: name.includes("어드민") ? "admin" : "store",
  518 |     path: fallbackPath
  519 |   };
  520 | }
  521 |
  522 | function blocked(message: string): never {
> 523 |   throw new Error(`BLOCKED: ${message}`);
      |         ^ Error: BLOCKED: 필수 OFF 가맹점의 앱 푸시 실기기 증거가 없어 기존 거래내역 이동 결과를 확인할 수 없습니다.
  524 | }
  525 |
  526 | function safeName(value: string): string {
  527 |   return value.replace(/[^a-zA-Z0-9가-힣._-]+/g, "-").replace(/^-+|-+$/g, "");
  528 | }
  529 |
```