test(ecommerce,pay_kginicis,pay_nhnkcp,pay_nicepayments): 브랜드 배지 회귀 테스트 전 플러그인 확대 + 시나리오 마커 정합

브랜치 통합 검증에서 정적 룰이 잡지 못한 결함 4건을 해소한다.

replaceState 후킹은 4개 결제 플러그인에 동일하게 적용됐는데 단위 회귀 테스트는
tosspayments 에만 있었다. 나머지 3개는 기존 injector 테스트가 있는데도 변경이
반영되지 않아, 앞으로 한 플러그인만 고쳐도 회귀를 잡을 수단이 없었다.

브랜드 마크 E2E 의 준비 게이트가 pg-locked-badge 만 세고 있었다. 그 배지는
pg_locked 인 수단에만 렌더되므로 PG 고정 수단이 없는 구성에서는 화면이 정상
렌더돼도 0 이 되어 타임아웃으로 오판 실패한다. 스펙이 선언한 "설치 구성을
하드코딩하지 않는다" 규율과 어긋나는 숨은 결합이라, PG 열의 상호배타 4분기
합계로 바꿔 구성과 무관하게 비고아 행 수와 일치시킨다. 읽히지 않던 badges
헬퍼도 함께 걷어낸다.

brand_mark 축과 두 효과가 매니페스트에 없어 마커가 가리킬 대상이 없었다.
축·효과·제외 규칙을 추가하며, 마커 파서가 쉼표로만 축을 분리한다는 사실을
확인해(× 는 첫 축만 잡힌다) 해당 spec 을 쉼표 형식으로 정정했다. 매니페스트를
건드리자 test_files 에 선언됐으나 마킹이 0건이던 파일 2건도 함께 드러나 마킹했다.
This commit is contained in:
HeuJung committed 2026-08-10 17:17:53 +09:00
1 parent 1b0be674aa
commit 2ba4a760fb
7 files changed
+136 -12

No files matched your search

@@ -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
*/
@@ -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<number> {
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<void> {
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
@@ -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
{
@@ -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 이 저장 파일 자체도 정정 (멱등)
@@ -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);
});
});
@@ -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);
});
});
@@ -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);
});
});