diff --git a/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/adminEcommerceSettingsOrder.test.tsx b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/adminEcommerceSettingsOrder.test.tsx index 5509a6f9..649bc59f 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/adminEcommerceSettingsOrder.test.tsx +++ b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/adminEcommerceSettingsOrder.test.tsx @@ -9,6 +9,9 @@ * - 폼 바인딩 및 핸들러 검증 * - 다국어 키 검증 * + * @scenario extension_payment_method method_kind=extension × capability_declared=declared × capability=pg_locked + * @effects admin_shows_pg_locked_badge, admin_hides_pg_select_for_locked, admin_shows_pg_select_for_unlocked + * * @vitest-environment node */ diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/payment-method-brand-mark.spec.ts b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/payment-method-brand-mark.spec.ts index ac07315a..27ee63bb 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/payment-method-brand-mark.spec.ts +++ b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/payment-method-brand-mark.spec.ts @@ -16,7 +16,10 @@ * data 속성을 **런타임 카운트**로 판정하고, 배지가 0인 환경(간편결제 플러그인 미설치)은 * skip 이 아니라 "행이 렌더됐는지" 를 먼저 확인해 거짓 통과를 만들지 않는다. * - * @scenario extension_payment_method method_kind=extension × capability=brand_mark + * 마킹은 쉼표로 축을 나눈다 — 마커 파서가 `,` 로만 분리하므로, 저장소에 흔한 `×` 표기는 + * 첫 축만 잡히고 나머지가 값 문자열에 묻혀 cross product 커버로 집계되지 않는다. + * + * @scenario extension_payment_method, method_kind=extension, capability_declared=declared, capability=brand_mark * @effects admin_shows_brand_mark, admin_rebinds_brand_mark_after_tab_return */ import { test, expect, authenticatePage } from '../../fixtures/ecommerce-auth'; @@ -24,13 +27,32 @@ import type { Page } from '@playwright/test'; const ORDER_SETTINGS_URL = '/admin/ecommerce/settings?tab=order_settings'; -/** 설치된 모든 PG 플러그인의 브랜드 배지 총합 + 렌더된 결제수단 행 수. */ -async function counts(page: Page): Promise<{ badges: number; rows: number }> { - return page.evaluate(() => ({ - badges: document.querySelectorAll('[data-testid], span').length - && document.querySelectorAll('span[class*="rounded-lg"][aria-hidden="true"]').length, - rows: document.querySelectorAll('[data-testid^="pg-locked-badge-"]').length, - })); +/** + * 렌더된 결제수단 행 수. + * + * 행 판정에 `pg-locked-badge-*` 하나만 쓰지 않는다 — 그 배지는 `pg_locked === true` 인 수단에만 + * 렌더되므로, PG 고정 수단이 하나도 없는 구성에서는 화면이 정상 렌더돼도 0 이 되어 준비 게이트가 + * 영원히 열리지 않는다(설치 구성을 하드코딩하지 않는다는 이 spec 의 규율과 어긋난다). + * + * PG 열은 비고아 행마다 정확히 하나의 분기를 그린다 — pg_locked / PG 셀렉트 / PG 미설치 / + * PG 불필요. 네 접두사를 합치면 설치 구성과 무관하게 비고아 행 수와 일치한다. + */ +const PG_CELL_TESTID_PREFIXES = [ + 'pg-locked-badge-', + 'pg-select-', + 'pg-not-installed-', + 'pg-not-required-', +] as const; + +async function rowCount(page: Page): Promise { + return page.evaluate( + (prefixes) => + prefixes.reduce( + (sum, prefix) => sum + document.querySelectorAll(`[data-testid^="${prefix}"]`).length, + 0, + ), + PG_CELL_TESTID_PREFIXES as unknown as string[], + ); } /** 플러그인별 브랜드 배지 카운트 (data 속성 접미사로 집계 — 플러그인명 하드코딩 회피). */ @@ -63,7 +85,7 @@ async function gotoOrderSettings(page: Page, token: string): Promise { await page.setViewportSize({ width: 1440, height: 1000 }); await page.goto(ORDER_SETTINGS_URL); await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); - await expect.poll(async () => (await counts(page)).rows, { timeout: 30_000 }).toBeGreaterThan(0); + await expect.poll(async () => await rowCount(page), { timeout: 30_000 }).toBeGreaterThan(0); } test.describe('@sirsoft-ecommerce 주문설정 결제수단 — 브랜드 마크', () => { @@ -91,10 +113,10 @@ test.describe('@sirsoft-ecommerce 주문설정 결제수단 — 브랜드 마크 // 탭 전환은 replaceState 로 URL 만 바꾼다 — 화면 전환을 실제 클릭으로 재현한다. // 탭은 접근성 이름이 라벨과 정확히 일치하지 않으므로(내부 텍스트 노드 구성) 텍스트로 찾는다. await clickTab(page, '배송설정'); - await expect.poll(async () => (await counts(page)).rows, { timeout: 20_000 }).toBe(0); + await expect.poll(async () => await rowCount(page), { timeout: 20_000 }).toBe(0); await clickTab(page, '주문설정'); - await expect.poll(async () => (await counts(page)).rows, { timeout: 20_000 }).toBeGreaterThan(0); + await expect.poll(async () => await rowCount(page), { timeout: 20_000 }).toBeGreaterThan(0); // 복귀 후 배지가 수정 전처럼 0 으로 남지 않아야 한다. await expect diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Models/OrderPaymentTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Models/OrderPaymentTest.php index 899deaa3..7cae0c18 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Models/OrderPaymentTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Models/OrderPaymentTest.php @@ -12,6 +12,9 @@ use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; /** * OrderPayment 모델 테스트 + * + * @scenario extension_payment_method method_kind=extension × capability_declared=declared × capability=refund_method + * @effects extension_id_persisted_as_is, builtin_capability_unchanged, refund_method_is_pg */ class OrderPaymentTest extends ModuleTestCase { diff --git a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/extension-payment-method.yaml b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/extension-payment-method.yaml index 311b375a..c7cb72e0 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/extension-payment-method.yaml +++ b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/extension-payment-method.yaml @@ -16,6 +16,7 @@ test_files: - modules/_bundled/sirsoft-ecommerce/tests/Unit/Models/OrderPaymentTest.php - modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/adminEcommerceSettingsOrder.test.tsx - modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/payment-method-pg-locked.spec.ts + - modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/payment-method-brand-mark.spec.ts - plugins/_bundled/sirsoft-pay_nhnkcp/tests/Unit/Listeners/RegisterEasyPayMethodsListenerTest.php - plugins/_bundled/sirsoft-pay_nhnkcp/tests/Unit/Upgrades/BackfillEasyPayPgProviderTest.php - plugins/_bundled/sirsoft-pay_nicepayments/tests/Unit/Listeners/RegisterEasyPayMethodsListenerTest.php @@ -30,12 +31,18 @@ axes: # 카탈로그가 능력을 선언했는가 (플러그인이 신규 키를 안 쓰는 하위호환 경로) capability_declared: [declared, undeclared] # 능력별 판정 표면 - capability: [needs_pg, label, refund_method, pg_locked] + capability: [needs_pg, label, refund_method, pg_locked, brand_mark] exclusions: - method_kind: builtin capability_declared: undeclared reason: "builtin 은 코어가 enum 에서 능력을 항상 파생하므로 '미선언' 상태가 존재하지 않는다" + - method_kind: builtin + capability: brand_mark + reason: "브랜드 마크는 PG 플러그인이 등록한 간편결제 수단의 표현이다. 코어 8종은 브랜드가 아니라 결제 형태(카드·가상계좌 등)이므로 brand_mark 를 선언하지 않는다" + - capability: brand_mark + capability_declared: undeclared + reason: "brand_mark 는 다른 능력과 달리 미선언 시 파생 경로가 없다 — 관리자 화면은 공급 플러그인이 직접 주입하는 배지로만 그리므로, 미선언은 곧 '배지 없음(기본 아이콘 유지)' 이고 판정할 후속 효과가 존재하지 않는다" # 후속 효과 체인 — 능력 오판이 낳는 실패들 effects: @@ -57,6 +64,8 @@ effects: - admin_shows_pg_locked_badge # 확장 결제수단이 "PG 고정 · {PG명}" 배지로 표시 - admin_hides_pg_select_for_locked # PG 고정 수단은 PG 선택 셀렉트 미노출 - admin_shows_pg_select_for_unlocked # 일반 PG 수단은 셀렉트 노출 (회귀 차단) + - admin_shows_brand_mark # 간편결제 행의 기본 아이콘이 공급 플러그인의 브랜드 배지로 치환 + - admin_rebinds_brand_mark_after_tab_return # 탭 전환(replaceState) 후 복귀해도 배지 재주입 — 새로고침 불요 # 기존 설치 환경 - saved_null_pg_provider_self_healed # 저장된 pg_provider=null 이 정의값으로 덮임 - upgrade_step_backfills_settings_file # upgrade step 이 저장 파일 자체도 정정 (멱등) diff --git a/plugins/_bundled/sirsoft-pay_kginicis/resources/js/__tests__/adminPaymentMethodBrandInjector.test.ts b/plugins/_bundled/sirsoft-pay_kginicis/resources/js/__tests__/adminPaymentMethodBrandInjector.test.ts index 01c959e8..8dad0f99 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/resources/js/__tests__/adminPaymentMethodBrandInjector.test.ts +++ b/plugins/_bundled/sirsoft-pay_kginicis/resources/js/__tests__/adminPaymentMethodBrandInjector.test.ts @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { + installAdminPaymentMethodBrandInjector, resetAdminPaymentMethodBrandInjectorForTests, syncRenderedAdminPaymentMethodBrands, } from '../adminPaymentMethodBrandInjector'; @@ -84,4 +85,32 @@ describe('adminPaymentMethodBrandInjector', () => { expect(syncRenderedAdminPaymentMethodBrands()).toBe(false); expect(document.querySelector('[data-kginicis-admin-payment-brand-mark="true"]')).toBeNull(); }); + + /** + * 탭 전환 후 복귀 시 배지 재주입 (브라우저 실측으로 발견한 회귀). + * + * 환경설정 화면의 탭 전환은 pushState 가 아니라 **replaceState** 로 URL 만 바꾼다 + * (탭 왕복 1회에 push 0 / replace 2 실측). pushState/popstate 만 후킹하던 기존 구현은 + * 다른 탭에 갔다 돌아왔을 때 onRouteChange 가 발화하지 않아, 행은 다시 렌더되는데 + * 배지만 사라진 채로 남았다 — 사용자에게는 아이콘이 회색으로 되돌아간 것으로 보인다. + */ + it('replaceState 로 탭을 전환했다 돌아와도 배지를 재주입한다', async () => { + installAdminPaymentMethodBrandInjector(); + expect(document.querySelectorAll('[data-kginicis-admin-payment-brand-mark="true"]').length).toBeGreaterThan(0); + + // 다른 탭으로 이동 (replaceState) — 화면이 갈아끼워지며 배지가 사라진 상태를 재현 + window.history.replaceState({}, '', '/admin/ecommerce/settings?tab=shipping'); + document.body.innerHTML = ''; + await new Promise((r) => setTimeout(r, 300)); + + // 주문설정으로 복귀 (replaceState) + 행 재렌더 + document.body.innerHTML = desktopItem('naverpay', '네이버페이 (KG이니시스)', '네이버페이로 결제'); + window.history.replaceState({}, '', '/admin/ecommerce/settings?tab=order_settings'); + await new Promise((r) => setTimeout(r, 600)); + + expect( + document.querySelectorAll('[data-kginicis-admin-payment-brand-mark="true"]').length, + 'replaceState 복귀 후 배지가 재주입되지 않았다', + ).toBeGreaterThan(0); + }); }); diff --git a/plugins/_bundled/sirsoft-pay_nhnkcp/resources/js/__tests__/adminPaymentMethodBrandInjector.test.ts b/plugins/_bundled/sirsoft-pay_nhnkcp/resources/js/__tests__/adminPaymentMethodBrandInjector.test.ts index b16f51d0..8f73307d 100644 --- a/plugins/_bundled/sirsoft-pay_nhnkcp/resources/js/__tests__/adminPaymentMethodBrandInjector.test.ts +++ b/plugins/_bundled/sirsoft-pay_nhnkcp/resources/js/__tests__/adminPaymentMethodBrandInjector.test.ts @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { + installAdminPaymentMethodBrandInjector, resetAdminPaymentMethodBrandInjectorForTests, syncRenderedAdminPaymentMethodBrands, } from '../adminPaymentMethodBrandInjector'; @@ -84,4 +85,32 @@ describe('adminPaymentMethodBrandInjector', () => { expect(syncRenderedAdminPaymentMethodBrands()).toBe(false); expect(document.querySelector('[data-nhnkcp-admin-payment-brand-mark="true"]')).toBeNull(); }); + + /** + * 탭 전환 후 복귀 시 배지 재주입 (브라우저 실측으로 발견한 회귀). + * + * 환경설정 화면의 탭 전환은 pushState 가 아니라 **replaceState** 로 URL 만 바꾼다 + * (탭 왕복 1회에 push 0 / replace 2 실측). pushState/popstate 만 후킹하던 기존 구현은 + * 다른 탭에 갔다 돌아왔을 때 onRouteChange 가 발화하지 않아, 행은 다시 렌더되는데 + * 배지만 사라진 채로 남았다 — 사용자에게는 아이콘이 회색으로 되돌아간 것으로 보인다. + */ + it('replaceState 로 탭을 전환했다 돌아와도 배지를 재주입한다', async () => { + installAdminPaymentMethodBrandInjector(); + expect(document.querySelectorAll('[data-nhnkcp-admin-payment-brand-mark="true"]').length).toBeGreaterThan(0); + + // 다른 탭으로 이동 (replaceState) — 화면이 갈아끼워지며 배지가 사라진 상태를 재현 + window.history.replaceState({}, '', '/admin/ecommerce/settings?tab=shipping'); + document.body.innerHTML = ''; + await new Promise((r) => setTimeout(r, 300)); + + // 주문설정으로 복귀 (replaceState) + 행 재렌더 + document.body.innerHTML = desktopItem('payco', 'PAYCO', 'PAYCO로 결제 (NHN KCP)'); + window.history.replaceState({}, '', '/admin/ecommerce/settings?tab=order_settings'); + await new Promise((r) => setTimeout(r, 600)); + + expect( + document.querySelectorAll('[data-nhnkcp-admin-payment-brand-mark="true"]').length, + 'replaceState 복귀 후 배지가 재주입되지 않았다', + ).toBeGreaterThan(0); + }); }); diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/resources/js/__tests__/adminPaymentMethodBrandInjector.test.ts b/plugins/_bundled/sirsoft-pay_nicepayments/resources/js/__tests__/adminPaymentMethodBrandInjector.test.ts index 2303f9ff..cd6ad2d8 100644 --- a/plugins/_bundled/sirsoft-pay_nicepayments/resources/js/__tests__/adminPaymentMethodBrandInjector.test.ts +++ b/plugins/_bundled/sirsoft-pay_nicepayments/resources/js/__tests__/adminPaymentMethodBrandInjector.test.ts @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { + installAdminPaymentMethodBrandInjector, resetAdminPaymentMethodBrandInjectorForTests, syncRenderedAdminPaymentMethodBrands, } from '../adminPaymentMethodBrandInjector'; @@ -84,4 +85,32 @@ describe('adminPaymentMethodBrandInjector', () => { expect(syncRenderedAdminPaymentMethodBrands()).toBe(false); expect(document.querySelector('[data-nicepay-admin-payment-brand-mark="true"]')).toBeNull(); }); + + /** + * 탭 전환 후 복귀 시 배지 재주입 (브라우저 실측으로 발견한 회귀). + * + * 환경설정 화면의 탭 전환은 pushState 가 아니라 **replaceState** 로 URL 만 바꾼다 + * (탭 왕복 1회에 push 0 / replace 2 실측). pushState/popstate 만 후킹하던 기존 구현은 + * 다른 탭에 갔다 돌아왔을 때 onRouteChange 가 발화하지 않아, 행은 다시 렌더되는데 + * 배지만 사라진 채로 남았다 — 사용자에게는 아이콘이 회색으로 되돌아간 것으로 보인다. + */ + it('replaceState 로 탭을 전환했다 돌아와도 배지를 재주입한다', async () => { + installAdminPaymentMethodBrandInjector(); + expect(document.querySelectorAll('[data-nicepay-admin-payment-brand-mark="true"]').length).toBeGreaterThan(0); + + // 다른 탭으로 이동 (replaceState) — 화면이 갈아끼워지며 배지가 사라진 상태를 재현 + window.history.replaceState({}, '', '/admin/ecommerce/settings?tab=shipping'); + document.body.innerHTML = ''; + await new Promise((r) => setTimeout(r, 300)); + + // 주문설정으로 복귀 (replaceState) + 행 재렌더 + document.body.innerHTML = desktopItem('naverpay', '네이버페이 (나이스페이먼츠)', '네이버페이로 결제 (나이스페이먼츠)'); + window.history.replaceState({}, '', '/admin/ecommerce/settings?tab=order_settings'); + await new Promise((r) => setTimeout(r, 600)); + + expect( + document.querySelectorAll('[data-nicepay-admin-payment-brand-mark="true"]').length, + 'replaceState 복귀 후 배지가 재주입되지 않았다', + ).toBeGreaterThan(0); + }); });