# 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: 4104/dev/duplicate-login-second-factor.spec.ts >> [4104][dev] SMS/Email 2차 인증 중복 로그인 강제로그인 모달 검증 >> Email/SMS 강제로그인 확인 모달의 화면 및 문구가 기존 PIN 모달과 동일한지 확인
- Location: src/qa/scenarios/duplicateLoginSecondFactor.ts:80:5

# Error details

```
Error: BLOCKED: Email/SMS/PIN 중 하나 이상에서 강제로그인 모달이 노출되지 않아 화면 및 문구를 비교할 수 없습니다.
```

# Test source

```ts
  198 |   }
  199 |   return value;
  200 | }
  201 |
  202 | async function expectForceModalOnly(page: Page, account: Account): Promise<void> {
  203 |   const surface = await inspectSurface(page, account);
  204 |   expect(surface.forceVisible, `${account.label} 중복 로그인 시 강제로그인 확인 모달이 단독으로 표시되어야 합니다.`).toBe(true);
  205 |   expect(surface.factorVisible, `${account.label} 강제로그인 확인 모달과 2차 인증 모달이 겹치면 안 됩니다.`).toBe(false);
  206 | }
  207 |
  208 | async function clickForceButton(page: Page, name: RegExp): Promise<void> {
  209 |   const modal = forceModal(page);
  210 |   const button = modal.getByRole("button", { name }).last();
  211 |   await expect(button).toBeVisible({ timeout: 5_000 });
  212 |   await button.click();
  213 | }
  214 |
  215 | async function forceModalText(page: Page): Promise<string> {
  216 |   const modal = forceModal(page);
  217 |   await expect(modal).toBeVisible({ timeout: 10_000 });
  218 |   return normalize(await modal.innerText());
  219 | }
  220 |
  221 | function forceModal(page: Page) {
  222 |   return page.locator("[role='dialog'], .fixed.inset-0").filter({ hasText: FORCE_MODAL }).last();
  223 | }
  224 |
  225 | interface LoginSurface {
  226 |   forceVisible: boolean;
  227 |   factorVisible: boolean;
  228 |   path: string;
  229 |   body: string;
  230 | }
  231 |
  232 | async function inspectSurface(page: Page, account: Account): Promise<LoginSurface> {
  233 |   const body = normalize(await page.locator("body").innerText().catch(() => ""));
  234 |   const factorVisible = await secondFactorInput(page, account).isVisible().catch(() => false);
  235 |   return {
  236 |     forceVisible: await forceModal(page).isVisible().catch(() => false),
  237 |     factorVisible,
  238 |     path: new URL(page.url()).pathname,
  239 |     body: body.slice(-700)
  240 |   };
  241 | }
  242 |
  243 | function describeLoginSurface(surface: LoginSurface): string[] {
  244 |   return [
  245 |     `강제로그인 확인 모달: ${surface.forceVisible ? "노출" : "미노출"}`,
  246 |     `2차 인증 입력 모달: ${surface.factorVisible ? "노출" : "미노출"}`,
  247 |     `현재 경로: ${surface.path}`,
  248 |     `화면 일부: ${surface.body || "-"}`
  249 |   ];
  250 | }
  251 |
  252 | async function clickExactButton(page: Page, name: string): Promise<void> {
  253 |   const buttons = page.getByRole("button", { name, exact: true });
  254 |   const count = await buttons.count();
  255 |   for (let index = count - 1; index >= 0; index -= 1) {
  256 |     const button = buttons.nth(index);
  257 |     if (await button.isVisible().catch(() => false)) {
  258 |       await button.click();
  259 |       return;
  260 |     }
  261 |   }
  262 |   await buttons.last().click();
  263 | }
  264 |
  265 | async function clickFirstVisible(locator: ReturnType<Page["locator"]>): Promise<void> {
  266 |   const count = await locator.count();
  267 |   for (let index = 0; index < count; index += 1) {
  268 |     const candidate = locator.nth(index);
  269 |     if (await candidate.isVisible().catch(() => false)) {
  270 |       await candidate.click();
  271 |       return;
  272 |     }
  273 |   }
  274 |   await locator.first().click();
  275 | }
  276 |
  277 | async function attachEvidence(testInfo: TestInfo, page: Page, slug: string, lines: string[]): Promise<void> {
  278 |   await testInfo.attach(`${slug}.md`, { body: `${lines.join("\n")}\n`, contentType: "text/markdown" });
  279 |   const screenshot = testInfo.outputPath(`${slug}.png`);
  280 |   await page.screenshot({ path: screenshot, fullPage: true });
  281 |   await testInfo.attach(`${slug}.png`, { path: screenshot, contentType: "image/png" });
  282 | }
  283 |
  284 | async function closeDuplicateState(state: DuplicateState): Promise<void> {
  285 |   await state.firstContext.close();
  286 |   await state.secondContext.close();
  287 | }
  288 |
  289 | function requiredEnv(key: string): string {
  290 |   const value = process.env[key];
  291 |   if (!value) {
  292 |     blocked(`${key} 런타임 환경변수가 필요합니다. 실제 비밀번호와 인증번호는 저장소 및 QA 산출물에 기록하지 않습니다.`);
  293 |   }
  294 |   return value;
  295 | }
  296 |
  297 | function blocked(message: string): never {
> 298 |   throw new Error(`BLOCKED: ${message}`);
      |         ^ Error: BLOCKED: Email/SMS/PIN 중 하나 이상에서 강제로그인 모달이 노출되지 않아 화면 및 문구를 비교할 수 없습니다.
  299 | }
  300 |
  301 | function normalize(value: string): string {
  302 |   return value.replace(/\s+/g, " ").trim();
  303 | }
  304 |
  305 | function positiveNumber(value: string | undefined, fallback: number): number {
  306 |   const parsed = Number(value);
  307 |   return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
  308 | }
  309 |
```