# 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: 4147/dev/privacy-masking-regression.spec.ts >> [4147][dev] [QA] 개인정보 마스킹 및 원문 조회 역할별 회귀 검증 >> [TA] 카드관리 / 상세 마스킹 및 PIN 원문 조회
- Location: src/qa/scenarios/privacyMaskingRegression.ts:74:9

# Error details

```
Error: BLOCKED: TA 카드관리 상세 데이터에 마스킹 검증이 가능한 개인정보 값이 없습니다.
```

# Test source

```ts
  634 |
  635 | async function closeVisibleDialogs(page: Page): Promise<void> {
  636 |   for (let attempt = 0; attempt < 3; attempt += 1) {
  637 |     const dialog = await firstVisible(page.locator("[role='dialog']:visible"));
  638 |     if (!dialog) return;
  639 |     const close = await firstVisible(dialog.getByRole("button", { name: /^(확인|닫기|취소)$/ }));
  640 |     if (close) await domClick(close).catch(() => undefined);
  641 |     else await page.keyboard.press("Escape").catch(() => undefined);
  642 |     await page.waitForTimeout(200);
  643 |   }
  644 | }
  645 |
  646 | async function setInputValue(input: Locator, value: string): Promise<void> {
  647 |   await input.evaluate((element, nextValue) => {
  648 |     const target = element as HTMLInputElement;
  649 |     const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
  650 |     setter?.call(target, nextValue);
  651 |     target.dispatchEvent(new Event("input", { bubbles: true }));
  652 |     target.dispatchEvent(new Event("change", { bubbles: true }));
  653 |   }, value);
  654 | }
  655 |
  656 | async function firstVisible(locator: Locator, timeoutMs = 1_000): Promise<Locator | undefined> {
  657 |   const deadline = Date.now() + timeoutMs;
  658 |   do {
  659 |     const count = await locator.count().catch(() => 0);
  660 |     for (let index = 0; index < count; index += 1) {
  661 |       const candidate = locator.nth(index);
  662 |       const visible = await candidate.isVisible().catch(() => false);
  663 |       if (!visible) continue;
  664 |       const active = await candidate.evaluate((element) => {
  665 |         const hiddenAncestor = element.closest("[inert], [aria-hidden='true']");
  666 |         return !hiddenAncestor;
  667 |       }).catch(() => false);
  668 |       if (active) return candidate;
  669 |     }
  670 |     if (Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 100));
  671 |   } while (Date.now() < deadline);
  672 |   return undefined;
  673 | }
  674 |
  675 | async function domClick(locator: Locator): Promise<void> {
  676 |   await locator.evaluate((element) => (element as HTMLElement).click());
  677 | }
  678 |
  679 | async function isActiveElement(locator: Locator): Promise<boolean> {
  680 |   return locator.evaluate((element) => !element.closest("[inert], [aria-hidden='true']")).catch(() => false);
  681 | }
  682 |
  683 | async function settle(page: Page): Promise<void> {
  684 |   await page.waitForLoadState("domcontentloaded").catch(() => undefined);
  685 |   await page.waitForLoadState("networkidle", { timeout: 5_000 }).catch(() => undefined);
  686 |   await page.waitForTimeout(500);
  687 | }
  688 |
  689 | async function attachEvidence(
  690 |   testInfo: TestInfo,
  691 |   pageName: string,
  692 |   role: string,
  693 |   suffix: string,
  694 |   lines: string[]
  695 | ): Promise<void> {
  696 |   await testInfo.attach(`${safe(pageName)}-${safe(role)}-${safe(suffix)}.md`, {
  697 |     body: `${lines.join("\n")}\n`,
  698 |     contentType: "text/markdown"
  699 |   });
  700 | }
  701 |
  702 | function assertRuntime(runtime: RuntimeQaEnv): void {
  703 |   if (!runtime.baseUrl) blocked(`${runtime.application}/${runtime.environment}/${runtime.role} baseUrl이 없습니다.`);
  704 |   if (!runtime.storageState && (!runtime.loginUrl || !runtime.username || !runtime.password)) {
  705 |     blocked(`${runtime.application}/${runtime.environment}/${runtime.role} 로그인 상태 또는 계정 정보가 없습니다.`);
  706 |   }
  707 | }
  708 |
  709 | function countMaskCharacters(value: string): number {
  710 |   return (value.match(/[＊*•]/g) ?? []).length;
  711 | }
  712 |
  713 | function normalizeMode(value: string | undefined): string {
  714 |   return String(value ?? "").trim().toUpperCase();
  715 | }
  716 |
  717 | function normalize(value: string): string {
  718 |   return value.replace(/\u200b/g, "").replace(/\s+/g, " ").trim();
  719 | }
  720 |
  721 | function ensureTrailingSlash(value: string): string {
  722 |   return value.endsWith("/") ? value : `${value}/`;
  723 | }
  724 |
  725 | function cssEscape(value: string): string {
  726 |   return value.replace(/['\\]/g, "\\$&");
  727 | }
  728 |
  729 | function safe(value: string): string {
  730 |   return value.replace(/[^a-zA-Z0-9가-힣._-]+/g, "-").slice(0, 120);
  731 | }
  732 |
  733 | function blocked(message: string): never {
> 734 |   throw new Error(`BLOCKED: ${message}`);
      |         ^ Error: BLOCKED: TA 카드관리 상세 데이터에 마스킹 검증이 가능한 개인정보 값이 없습니다.
  735 | }
  736 |
```