From faa73b5e82b16f9585d79f4ec86db57de2c15d02 Mon Sep 17 00:00:00 2001 From: dwebxr Date: Tue, 22 Sep 2026 04:16:12 +0900 Subject: [PATCH 1/3] =?UTF-8?q?test(header):=20WalletBadge=20=E3=81=AE=20S?= =?UTF-8?q?IWE=20flag=20holder=20=E3=82=92=E5=AE=9F=E8=A3=85=E3=81=A8?= =?UTF-8?q?=E4=B8=80=E8=87=B4=E3=81=95=E3=81=9B=E3=80=81=E3=81=9A=E3=82=8C?= =?UTF-8?q?=E3=82=92=E6=A4=9C=E5=87=BA=E3=81=99=E3=82=8B=E3=83=95=E3=82=A7?= =?UTF-8?q?=E3=83=B3=E3=82=B9=E3=82=92=E8=B6=B3=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 旧 holder は実在しない enableBilling を持ち、実装が読む enableUsageFee / enableHandles を 欠いていた (mock されない flag は実 env の false 固定 = 掟 7 のガードが 2 flag で無効)。 holder のキーを siweEnabled のソースと突き合わせるフェンスと、flag 1 本ずつの網羅テストを追加。 Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01HeizmagJBgL5peL5mQpxkc --- tests/components/WalletBadge.test.tsx | 55 ++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/tests/components/WalletBadge.test.tsx b/tests/components/WalletBadge.test.tsx index 66d0408f..208ed710 100644 --- a/tests/components/WalletBadge.test.tsx +++ b/tests/components/WalletBadge.test.tsx @@ -1,3 +1,5 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { fireEvent, screen, within } from '@testing-library/react'; import { renderWithIntl } from '../_helpers/i18n'; @@ -30,12 +32,13 @@ vi.mock('@/hooks/useSiweSession', () => ({ // (既定 = freee ON で SIWE UI を出す。両 OFF で隠れることは専用 test で検証)。 const flags = vi.hoisted(() => ({ enableFreeeSync: true, - enableBilling: false, + enableUsageFee: false, enablePro: false, enableCsvPass: false, enablePushNotify: false, enableTipMessage: false, enableCreatorStoreUi: false, + enableHandles: false, })); vi.mock('@/lib/env', async (importOriginal) => { const actual = await importOriginal(); @@ -46,8 +49,8 @@ vi.mock('@/lib/env', async (importOriginal) => { get enableFreeeSync() { return flags.enableFreeeSync; }, - get enableBilling() { - return flags.enableBilling; + get enableUsageFee() { + return flags.enableUsageFee; }, get enablePro() { return flags.enablePro; @@ -64,6 +67,9 @@ vi.mock('@/lib/env', async (importOriginal) => { get enableCreatorStoreUi() { return flags.enableCreatorStoreUi; }, + get enableHandles() { + return flags.enableHandles; + }, }, }; }); @@ -148,12 +154,13 @@ beforeEach(() => { visibleConnectorsMock.mockReturnValue([]); setSiwe(); flags.enableFreeeSync = true; // 既定: SIWE 機能 ON → ログイン UI を出す - flags.enableBilling = false; + flags.enableUsageFee = false; flags.enablePro = false; flags.enableCsvPass = false; flags.enablePushNotify = false; flags.enableTipMessage = false; flags.enableCreatorStoreUi = false; + flags.enableHandles = false; }); describe('WalletBadge: 接続済 branch', () => { @@ -285,12 +292,13 @@ describe('WalletBadge: SIWE サインイン', () => { it('SIWE 機能 (freee/利用権/Pro/CSVパス/push通知/tip質問) が全 OFF → ログイン UI を出さない (切断のみ)', () => { flags.enableFreeeSync = false; - flags.enableBilling = false; + flags.enableUsageFee = false; flags.enablePro = false; flags.enableCsvPass = false; flags.enablePushNotify = false; flags.enableTipMessage = false; flags.enableCreatorStoreUi = false; + flags.enableHandles = false; setConnected(); setSiwe({ isSignedIn: false }); // 仮にサインインしていなくてもログイン導線を出さない renderWithIntl(); @@ -310,7 +318,7 @@ describe('WalletBadge: SIWE サインイン', () => { // 回帰防止: siweEnabled に enablePushNotify を含めないと、push 単独構成でヘッダーから // サインインできず PushNotifyPanel が不到達になる (CsvPassPaywall と同型の教訓)。 flags.enableFreeeSync = false; - flags.enableBilling = false; + flags.enableUsageFee = false; flags.enablePro = false; flags.enableCsvPass = false; flags.enablePushNotify = true; @@ -325,7 +333,7 @@ describe('WalletBadge: SIWE サインイン', () => { it('チップ質問 inbox のみ ON → ログイン UI を出す (閲覧にサインインが要る)', () => { flags.enableFreeeSync = false; - flags.enableBilling = false; + flags.enableUsageFee = false; flags.enablePro = false; flags.enableCsvPass = false; flags.enablePushNotify = false; @@ -341,7 +349,7 @@ describe('WalletBadge: SIWE サインイン', () => { it('Creator Store UI のみ ON → 出品管理用のログイン UI を出す', () => { flags.enableFreeeSync = false; - flags.enableBilling = false; + flags.enableUsageFee = false; flags.enablePro = false; flags.enableCsvPass = false; flags.enablePushNotify = false; @@ -360,7 +368,7 @@ describe('WalletBadge: SIWE サインイン', () => { // 回帰防止: siweEnabled に enablePro を含めないと、Pro 単独構成でヘッダーからサインイン // できず、Pro ゲート (ProPaywall) が不到達になる。Pro だけでもログイン導線を出す。 flags.enableFreeeSync = false; - flags.enableBilling = false; + flags.enableUsageFee = false; flags.enablePro = true; setConnected(); setSiwe({ isSignedIn: false }); @@ -375,7 +383,7 @@ describe('WalletBadge: SIWE サインイン', () => { // 回帰防止: siweEnabled に enableCsvPass を含めないと、CSV パス単独構成 (= 現行の CSV ゲート) // でヘッダーからサインインできず、CSV パスゲート (CsvPassPaywall) が不到達になる。 flags.enableFreeeSync = false; - flags.enableBilling = false; + flags.enableUsageFee = false; flags.enablePro = false; flags.enableCsvPass = true; setConnected(); @@ -518,3 +526,30 @@ describe('WalletBadge: 接続状態の切り替わり', () => { expect(container.querySelector('details')?.open).toBe(false); }); }); + +describe('WalletBadge: siweEnabled の flag 網羅 (掟 7)', () => { + const FLAG_NAMES = Object.keys(flags) as (keyof typeof flags)[]; + + it('このテストの flag holder は WalletBadge の siweEnabled が読む env キーと一致する', () => { + // holder が実装とずれると、mock されない flag は実 env の false 固定になり、 + // 「足し忘れるとヘッダからサインインできない」を守るテストが黙って効かなくなる + // (旧 holder は実在しない enableBilling を持ち、enableUsageFee / enableHandles を欠いていた)。 + const src = readFileSync(join(process.cwd(), 'components/WalletBadge.tsx'), 'utf8'); + const block = src.slice(src.indexOf('const siweEnabled ='), src.indexOf('const handleSignIn')); + const used = [...block.matchAll(/env\.(enable\w+)/g)].map((m) => m[1]).sort(); + expect(used.length).toBeGreaterThan(0); + expect([...FLAG_NAMES].sort()).toEqual(used); + }); + + it.each(FLAG_NAMES)('%s のみ ON → ヘッダにログイン UI を出す', (name) => { + for (const k of FLAG_NAMES) flags[k] = false; + flags[name] = true; + setConnected(); + setSiwe({ isSignedIn: false }); + renderWithIntl(); + const details = openDropdown('0x52d4…cA81'); + expect( + within(details).getByRole('menuitem', { name: 'ログイン (署名)' }), + ).toBeInTheDocument(); + }); +}); From dc02b7132f6941e90a09a1b04b31590feb7a3bf8 Mon Sep 17 00:00:00 2001 From: dwebxr Date: Tue, 22 Sep 2026 04:38:07 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix(header):=20=E3=83=A1=E3=83=8B=E3=83=A5?= =?UTF-8?q?=E3=83=BC=E3=81=8B=E3=82=89=E6=8E=A5=E7=B6=9A=E3=83=BB=E5=88=87?= =?UTF-8?q?=E6=96=AD=E3=81=97=E3=81=9F=E3=81=82=E3=81=A8=E3=80=81=E3=83=95?= =?UTF-8?q?=E3=82=A9=E3=83=BC=E3=82=AB=E3=82=B9=E3=82=92=E6=96=B0=E3=81=97?= =?UTF-8?q?=E3=81=84=20summary=20=E3=81=B8=E6=88=BB=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 接続 / 切断で branch が入れ替わると、押したボタンごと
が作り直され、フォーカスが body へ落ちていた (キーボード利用者は位置を失う)。メニュー内の操作を起点にしたときだけ、 作り直された summary へフォーカスを戻す。 - ページ読み込み時の自動再接続・ページ側の接続ボタンではフォーカスを奪わない - 接続が失敗・拒否で終わったら予約を捨てる Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01HeizmagJBgL5peL5mQpxkc --- components/WalletBadge.tsx | 26 +++++++++++++-- tests/components/WalletBadge.test.tsx | 47 +++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/components/WalletBadge.tsx b/components/WalletBadge.tsx index b664d91e..ee79ecce 100644 --- a/components/WalletBadge.tsx +++ b/components/WalletBadge.tsx @@ -5,6 +5,7 @@ // 未接続時: 「接続」ボタン + ▾ で開く dropdown (利用可能 connector 一覧)。 // dropdown は native
で実装し、JS state を最小化する。 +import { useEffect, useRef } from 'react'; import { useTranslations } from 'next-intl'; import { useAccount, useConnect, useDisconnect } from 'wagmi'; import { Check, ChevronDown } from 'lucide-react'; @@ -48,8 +49,24 @@ export function WalletBadge() { const handleSignIn = () => { void signIn(t('siweStatement')).catch(() => undefined); }; + // メニュー内の操作 (接続 / 切断) で branch が入れ替わると、押したボタンごと
が作り直され + // フォーカスが body へ落ちる。メニューから操作したときだけ新しい summary へ戻す + // (ページ読み込み時の自動再接続ではフォーカスを奪わない)。 + const summaryRef = useRef(null); + const restoreFocusRef = useRef(false); + useEffect(() => { + if (!restoreFocusRef.current) return; + restoreFocusRef.current = false; + summaryRef.current?.focus(); + }, [isConnected]); + // 接続が失敗・拒否で終わったら予約を捨てる (後で別の場所から接続したときにフォーカスを奪わない)。 + useEffect(() => { + if (error) restoreFocusRef.current = false; + }, [error]); + // 切断時はセッション cookie も破棄して「ログイン済だが未接続」の宙ぶらりんを残さない。 const handleDisconnect = () => { + restoreFocusRef.current = true; void signOut().catch(() => undefined); disconnect(); }; @@ -59,7 +76,7 @@ export function WalletBadge() { // key: 未接続 branch と同じ位置の
なので、無いと React が DOM を使い回し open が // 引き継がれる (接続直後にメニューが開いたまま本文へ被さる)。
- + {siweEnabled && isSignedIn && ( @@ -127,7 +144,7 @@ export function WalletBadge() { return (
- + {t('connect')} connect({ connector: c })} + onClick={() => { + restoreFocusRef.current = true; + connect({ connector: c }); + }} className="flex w-full items-center gap-2 rounded-md px-3 py-1.5 text-left text-slate-700 hover:bg-slate-100 disabled:opacity-50" > {/* ウォレットアイコン (EIP-6963 data URI or 同梱 SVG)。装飾なので alt は空。 */} diff --git a/tests/components/WalletBadge.test.tsx b/tests/components/WalletBadge.test.tsx index 208ed710..4fd13a25 100644 --- a/tests/components/WalletBadge.test.tsx +++ b/tests/components/WalletBadge.test.tsx @@ -527,6 +527,53 @@ describe('WalletBadge: 接続状態の切り替わり', () => { }); }); +describe('WalletBadge: 接続・切断後のフォーカス', () => { + it('メニューから接続 → 作り直された summary へフォーカスを戻す (body に落とさない)', () => { + setDisconnected(); + visibleConnectorsMock.mockReturnValue([{ uid: '1', name: 'MetaMask' }]); + const { rerender } = renderWithIntl(); + const details = openDropdown('接続'); + fireEvent.click(within(details).getByRole('menuitem', { name: 'MetaMask' })); + + setConnected(); + rerender(); + expect(document.activeElement).toBe(screen.getByText(/0x52d4/i).closest('summary')); + }); + + it('メニューから切断 → 「接続」の summary へフォーカスを戻す', () => { + setConnected(); + const { rerender } = renderWithIntl(); + const details = openDropdown(/0x52d4/i); + fireEvent.click(within(details).getByRole('menuitem', { name: '切断' })); + + setDisconnected(); + rerender(); + expect(document.activeElement).toBe(screen.getByText('接続').closest('summary')); + }); + + it('メニューを使わない接続 (自動再接続・ページ側のボタン) ではフォーカスを奪わない', () => { + setDisconnected(); + const { rerender } = renderWithIntl(); + setConnected(); + rerender(); + expect(document.activeElement).toBe(document.body); + }); + + it('接続が拒否で終わったあと、別経路で接続してもフォーカスを奪わない', () => { + setDisconnected(); + visibleConnectorsMock.mockReturnValue([{ uid: '1', name: 'MetaMask' }]); + const { rerender } = renderWithIntl(); + const details = openDropdown('接続'); + fireEvent.click(within(details).getByRole('menuitem', { name: 'MetaMask' })); + setDisconnected({ error: new Error('User rejected the request') }); + rerender(); + + setConnected(); + rerender(); + expect(document.activeElement).toBe(document.body); + }); +}); + describe('WalletBadge: siweEnabled の flag 網羅 (掟 7)', () => { const FLAG_NAMES = Object.keys(flags) as (keyof typeof flags)[]; From cc7290ec3dccea9feb982cf47f350bbbcca3f9f9 Mon Sep 17 00:00:00 2001 From: dwebxr Date: Tue, 22 Sep 2026 04:51:09 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix(header):=20=E3=83=95=E3=82=A9=E3=83=BC?= =?UTF-8?q?=E3=82=AB=E3=82=B9=E3=81=AF=20body=20=E3=81=AB=E8=90=BD?= =?UTF-8?q?=E3=81=A1=E3=81=A6=E3=81=84=E3=82=8B=E3=81=A8=E3=81=8D=E3=81=A0?= =?UTF-8?q?=E3=81=91=E6=88=BB=E3=81=99=E3=83=BBflag=20=E3=83=95=E3=82=A7?= =?UTF-8?q?=E3=83=B3=E3=82=B9=E3=81=AB=E6=9B=B8=E3=81=8D=E6=96=B9=E3=81=AE?= =?UTF-8?q?=E5=9B=BA=E5=AE=9A=E3=82=92=E8=B6=B3=E3=81=99=20(Opus=205=20?= =?UTF-8?q?=E3=83=81=E3=82=A7=E3=83=83=E3=82=AF=E5=8F=8D=E6=98=A0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 承認待ちの間に利用者が別の場所へフォーカスを移していた場合、ヘッダへ奪っていた。 予約が残る 3 経路 (長い承認待ち・同一 error の再発・state を変えない disconnect) を 「落ちたフォーカスを拾うだけ」の 1 条件でまとめて塞ぐ - フェンスは env.enableX という書き方にしか反応せず、分割代入や helper 経由で flag を足すと holder にも無いまま一致して通っていた → ブロック内の enable* は env. 直読みであることも検査 - 全 OFF テストの題が実態 (8 flag) とずれていたのを直す Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01HeizmagJBgL5peL5mQpxkc --- components/WalletBadge.tsx | 3 +++ tests/components/WalletBadge.test.tsx | 26 +++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/components/WalletBadge.tsx b/components/WalletBadge.tsx index ee79ecce..8d85be54 100644 --- a/components/WalletBadge.tsx +++ b/components/WalletBadge.tsx @@ -57,6 +57,9 @@ export function WalletBadge() { useEffect(() => { if (!restoreFocusRef.current) return; restoreFocusRef.current = false; + // 落ちたフォーカスを拾うだけ: ウォレットの承認待ちの間に利用者が別の場所へ移していたら奪わない。 + const active = document.activeElement; + if (active && active !== document.body) return; summaryRef.current?.focus(); }, [isConnected]); // 接続が失敗・拒否で終わったら予約を捨てる (後で別の場所から接続したときにフォーカスを奪わない)。 diff --git a/tests/components/WalletBadge.test.tsx b/tests/components/WalletBadge.test.tsx index 4fd13a25..a4fb05e7 100644 --- a/tests/components/WalletBadge.test.tsx +++ b/tests/components/WalletBadge.test.tsx @@ -290,7 +290,7 @@ describe('WalletBadge: SIWE サインイン', () => { expect(disconnect).toHaveBeenCalledTimes(1); }); - it('SIWE 機能 (freee/利用権/Pro/CSVパス/push通知/tip質問) が全 OFF → ログイン UI を出さない (切断のみ)', () => { + it('SIWE 機能の flag が全 OFF → ログイン UI を出さない (切断のみ)', () => { flags.enableFreeeSync = false; flags.enableUsageFee = false; flags.enablePro = false; @@ -551,6 +551,25 @@ describe('WalletBadge: 接続・切断後のフォーカス', () => { expect(document.activeElement).toBe(screen.getByText('接続').closest('summary')); }); + it('承認待ちの間に利用者が別の場所へフォーカスを移していたら奪わない', () => { + setDisconnected(); + visibleConnectorsMock.mockReturnValue([{ uid: '1', name: 'MetaMask' }]); + const other = document.createElement('input'); + document.body.appendChild(other); + try { + const { rerender } = renderWithIntl(); + const details = openDropdown('接続'); + fireEvent.click(within(details).getByRole('menuitem', { name: 'MetaMask' })); + other.focus(); + + setConnected(); + rerender(); + expect(document.activeElement).toBe(other); + } finally { + other.remove(); + } + }); + it('メニューを使わない接続 (自動再接続・ページ側のボタン) ではフォーカスを奪わない', () => { setDisconnected(); const { rerender } = renderWithIntl(); @@ -585,6 +604,11 @@ describe('WalletBadge: siweEnabled の flag 網羅 (掟 7)', () => { const block = src.slice(src.indexOf('const siweEnabled ='), src.indexOf('const handleSignIn')); const used = [...block.matchAll(/env\.(enable\w+)/g)].map((m) => m[1]).sort(); expect(used.length).toBeGreaterThan(0); + // 書き方も固定する: 分割代入や helper 経由で flag を足すと上の正規表現に掛からず、holder にも + // 無いまま「一致」して通ってしまう。ブロック内の enable* はすべて `env.` 直読みであること。 + const bare = [...block.matchAll(/(? m[0]); + expect(bare).toEqual([]); + // 下の it.each の網羅は holder 由来なので、このフェンスが通っていることが前提。 expect([...FLAG_NAMES].sort()).toEqual(used); });