From 50f9f56579e9a0e94049f257050400add9c9f62d Mon Sep 17 00:00:00 2001 From: Gaubee Date: Sat, 10 Jan 2026 10:00:01 +0800 Subject: [PATCH 1/4] chore(e2e): add Mobile Safari project and fix guide test --- e2e/guide.spec.ts | 4 ++-- playwright.config.ts | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/e2e/guide.spec.ts b/e2e/guide.spec.ts index a4de376f4..6db7c9639 100644 --- a/e2e/guide.spec.ts +++ b/e2e/guide.spec.ts @@ -63,7 +63,7 @@ test.describe('Guide 页面', () => { await page.click('[data-testid="get-started-button"]', { force: true }) // 应该导航到创建钱包页 - await expect(page).toHaveURL(/\/#\/wallet\/create\/?/) + await expect(page).toHaveURL(/(\/#\/wallet\/create)|(\/#\/onboarding\/create)\/?/) }) test('我有钱包按钮导航到导入钱包页', async ({ page }) => { @@ -79,6 +79,6 @@ test.describe('Guide 页面', () => { await page.click('[data-testid="have-wallet-button"]', { force: true }) // 应该导航到导入钱包页 - await expect(page).toHaveURL(/\/#\/wallet\/import\/?/) + await expect(page).toHaveURL(/(\/#\/wallet\/import)|(\/#\/onboarding\/recover)\/?/) }) }) diff --git a/playwright.config.ts b/playwright.config.ts index df569f833..ccca30610 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -85,6 +85,12 @@ export default defineConfig({ viewport: { width: 1280, height: 720 }, }, }, + { + name: 'Mobile Safari', + use: { + ...devices['iPhone 12'], + }, + }, ], // 使用标准 dev 服务器(端口 11173,避免与 vite 默认 517x 冲突) From ae946a48ff1b7b40332c8450232a33d659da0192 Mon Sep 17 00:00:00 2001 From: Gaubee Date: Sat, 10 Jan 2026 10:32:55 +0800 Subject: [PATCH 2/4] fix(ecosystem): sync my-apps state via global store - Fix file formatting (convert escaped \n to actual newlines) - Add ecosystem store unit tests for myApps management - Remove deprecated empty functions from my-apps.ts - Use ecosystemStore as single source of truth for myApps state --- src/services/ecosystem/my-apps.ts | 41 +-- src/services/ecosystem/types.ts | 9 + .../activities/MiniappDetailActivity.tsx | 19 +- .../activities/tabs/EcosystemTab.tsx | 22 +- src/stores/ecosystem.test.ts | 264 ++++++++++++++++++ src/stores/ecosystem.ts | 64 ++++- 6 files changed, 362 insertions(+), 57 deletions(-) create mode 100644 src/stores/ecosystem.test.ts diff --git a/src/services/ecosystem/my-apps.ts b/src/services/ecosystem/my-apps.ts index 023ffb842..7c2e3b6b6 100644 --- a/src/services/ecosystem/my-apps.ts +++ b/src/services/ecosystem/my-apps.ts @@ -1,21 +1,19 @@ /** - * My Apps - 本地已安装应用管理 + * My Apps - Local installed app persistence layer + * + * NOTE: Do not use this directly for state management. Use ecosystemStore instead. */ -const MY_APPS_KEY = 'ecosystem_my_apps' +import type { MyAppRecord } from './types' -export interface MyAppRecord { - appId: string - installedAt: number - lastUsedAt: number -} +const MY_APPS_KEY = 'ecosystem_my_apps' const LEGACY_APP_ID_MAP: Record = { teleport: 'xin.dweb.teleport', forge: 'xin.dweb.forge', } -function normalizeAppId(appId: string): string { +export function normalizeAppId(appId: string): string { return LEGACY_APP_ID_MAP[appId] ?? appId } @@ -44,31 +42,4 @@ export function saveMyApps(apps: MyAppRecord[]): void { localStorage.setItem(MY_APPS_KEY, JSON.stringify(apps)) } -export function isInMyApps(appId: string): boolean { - return loadMyApps().some((a) => a.appId === normalizeAppId(appId)) -} -export function addToMyApps(appId: string): void { - appId = normalizeAppId(appId) - const apps = loadMyApps() - if (!apps.some((a) => a.appId === appId)) { - apps.unshift({ appId, installedAt: Date.now(), lastUsedAt: Date.now() }) - saveMyApps(apps) - } -} - -export function updateLastUsed(appId: string): void { - appId = normalizeAppId(appId) - const apps = loadMyApps() - const app = apps.find((a) => a.appId === appId) - if (app) { - app.lastUsedAt = Date.now() - saveMyApps(apps) - } -} - -export function removeFromMyApps(appId: string): void { - appId = normalizeAppId(appId) - const apps = loadMyApps().filter((a) => a.appId !== appId) - saveMyApps(apps) -} diff --git a/src/services/ecosystem/types.ts b/src/services/ecosystem/types.ts index 11730bb18..719f4833e 100644 --- a/src/services/ecosystem/types.ts +++ b/src/services/ecosystem/types.ts @@ -270,6 +270,15 @@ export interface SourceRecord { builtin?: boolean } +/** + * My Apps - Local installed app record + */ +export interface MyAppRecord { + appId: string + installedAt: number + lastUsedAt: number +} + /** Method handler */ export type MethodHandler = ( params: unknown, diff --git a/src/stackflow/activities/MiniappDetailActivity.tsx b/src/stackflow/activities/MiniappDetailActivity.tsx index 9c806c2c6..2eaba2f10 100644 --- a/src/stackflow/activities/MiniappDetailActivity.tsx +++ b/src/stackflow/activities/MiniappDetailActivity.tsx @@ -4,6 +4,7 @@ import { useEffect, useState, useCallback } from 'react' import type { ActivityComponentType } from '@stackflow/react' +import { useStore } from '@tanstack/react-store' import { AppScreen } from '@stackflow/plugin-basic-ui' import { useTranslation } from 'react-i18next' import { useFlow } from '../stackflow' @@ -11,9 +12,6 @@ import { getAppById, initRegistry, refreshSources, - addToMyApps, - isInMyApps, - updateLastUsed, type MiniappManifest, KNOWN_PERMISSIONS } from '@/services/ecosystem' @@ -32,7 +30,7 @@ import { } from '@tabler/icons-react' import { cn } from '@/lib/utils' import { launchApp } from '@/services/miniapp-runtime' -import { ecosystemActions } from '@/stores/ecosystem' +import { ecosystemActions, ecosystemStore, ecosystemSelectors } from '@/stores/ecosystem' type MiniappDetailActivityParams = { appId: string @@ -118,7 +116,11 @@ export const MiniappDetailActivity: ActivityComponentType(null) const [loading, setLoading] = useState(true) const [descExpanded, setDescExpanded] = useState(false) - const [installed, setInstalled] = useState(false) + + // Use store selector for reactivity (Single Source of Truth) + const installed = useStore(ecosystemStore, (state) => + ecosystemSelectors.isAppInstalled(state, params.appId) + ) useEffect(() => { let disposed = false @@ -137,7 +139,6 @@ export const MiniappDetailActivity: ActivityComponentType { if (!app) return - addToMyApps(app.id) - setInstalled(true) + ecosystemActions.installApp(app.id) }, [app]) const handleOpen = useCallback(() => { if (!app) return - updateLastUsed(app.id) + ecosystemActions.updateAppLastUsed(app.id) ecosystemActions.setActiveSubPage('mine') launchApp(app.id, { ...app, targetDesktop: 'mine' }) pop() @@ -414,3 +414,4 @@ export const MiniappDetailActivity: ActivityComponentType ) } + diff --git a/src/stackflow/activities/tabs/EcosystemTab.tsx b/src/stackflow/activities/tabs/EcosystemTab.tsx index b8bba8b3a..e805839c5 100644 --- a/src/stackflow/activities/tabs/EcosystemTab.tsx +++ b/src/stackflow/activities/tabs/EcosystemTab.tsx @@ -1,28 +1,27 @@ import { useEffect, useState, useCallback, useRef, useMemo } from 'react'; +import { useStore } from '@tanstack/react-store'; import { useFlow } from '../../stackflow'; import { LoadingSpinner } from '@/components/common/loading-spinner'; import { initRegistry, getApps, subscribeApps, - loadMyApps, - addToMyApps, - updateLastUsed, - removeFromMyApps, type MyAppRecord, } from '@/services/ecosystem'; import type { MiniappManifest } from '@/services/ecosystem'; import { EcosystemDesktop, type EcosystemDesktopHandle } from '@/components/ecosystem'; import { computeFeaturedScore } from '@/services/ecosystem/scoring'; import { launchApp, useMiniappVisibilityRestore } from '@/services/miniapp-runtime'; -import { ecosystemActions } from '@/stores/ecosystem'; +import { ecosystemActions, ecosystemStore } from '@/stores/ecosystem'; export function EcosystemTab() { const { push } = useFlow(); const desktopRef = useRef(null); const [apps, setApps] = useState([]); - const [myAppRecords, setMyAppRecords] = useState([]); const [loading, setLoading] = useState(true); + + // Subscribe to myApps from the store (Single Source of Truth) + const myAppRecords = useStore(ecosystemStore, (state) => state.myApps); // 初始化数据 useEffect(() => { @@ -30,7 +29,6 @@ export function EcosystemTab() { initRegistry({ refresh: true }).then(() => { setApps(getApps()); - setMyAppRecords(loadMyApps()); setLoading(false); }); @@ -50,9 +48,9 @@ export function EcosystemTab() { const handleAppOpen = useCallback( (app: MiniappManifest) => { - addToMyApps(app.id); - updateLastUsed(app.id); - setMyAppRecords(loadMyApps()); + // Use store actions instead of direct service calls + ecosystemActions.installApp(app.id); + ecosystemActions.updateAppLastUsed(app.id); // 为了保证 shared layout 捕获到 icon 的起点位置,先固定到 mine ecosystemActions.setActiveSubPage('mine'); @@ -65,8 +63,7 @@ export function EcosystemTab() { ); const handleAppRemove = useCallback((appId: string) => { - removeFromMyApps(appId); - setMyAppRecords(loadMyApps()); + ecosystemActions.uninstallApp(appId); }, []); // 计算数据 @@ -121,3 +118,4 @@ export function EcosystemTab() { /> ); } + diff --git a/src/stores/ecosystem.test.ts b/src/stores/ecosystem.test.ts new file mode 100644 index 000000000..6956dc2f9 --- /dev/null +++ b/src/stores/ecosystem.test.ts @@ -0,0 +1,264 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { ecosystemStore, ecosystemActions, ecosystemSelectors } from './ecosystem' + +// Mock localStorage +const localStorageMock = (() => { + let store: Record = {} + return { + getItem: vi.fn((key: string) => store[key] ?? null), + setItem: vi.fn((key: string, value: string) => { + store[key] = value + }), + removeItem: vi.fn((key: string) => { + delete store[key] + }), + clear: vi.fn(() => { + store = {} + }), + } +})() + +Object.defineProperty(global, 'localStorage', { value: localStorageMock }) + +// Mock import.meta.env +vi.stubGlobal('import', { meta: { env: { BASE_URL: '/' } } }) + +describe('ecosystemStore - myApps management', () => { + beforeEach(() => { + vi.clearAllMocks() + localStorageMock.clear() + // Reset store state + ecosystemStore.setState(() => ({ + permissions: [], + sources: [], + myApps: [], + availableSubPages: ['discover', 'mine'], + activeSubPage: 'discover', + swiperProgress: 0, + syncSource: null, + })) + }) + + afterEach(() => { + vi.clearAllMocks() + }) + + describe('installApp', () => { + it('adds app to myApps list', () => { + ecosystemActions.installApp('test.app.id') + + expect(ecosystemStore.state.myApps).toHaveLength(1) + expect(ecosystemStore.state.myApps[0]?.appId).toBe('test.app.id') + }) + + it('sets installedAt and lastUsedAt timestamps', () => { + const before = Date.now() + ecosystemActions.installApp('test.app.id') + const after = Date.now() + + const app = ecosystemStore.state.myApps[0] + expect(app?.installedAt).toBeGreaterThanOrEqual(before) + expect(app?.installedAt).toBeLessThanOrEqual(after) + expect(app?.lastUsedAt).toBeGreaterThanOrEqual(before) + expect(app?.lastUsedAt).toBeLessThanOrEqual(after) + }) + + it('adds new app to the front of the list', () => { + ecosystemActions.installApp('app.first') + ecosystemActions.installApp('app.second') + + expect(ecosystemStore.state.myApps[0]?.appId).toBe('app.second') + expect(ecosystemStore.state.myApps[1]?.appId).toBe('app.first') + }) + + it('does not duplicate already installed app', () => { + ecosystemActions.installApp('test.app.id') + ecosystemActions.installApp('test.app.id') + + expect(ecosystemStore.state.myApps).toHaveLength(1) + }) + + it('normalizes legacy app IDs', () => { + ecosystemActions.installApp('teleport') + + expect(ecosystemStore.state.myApps[0]?.appId).toBe('xin.dweb.teleport') + }) + + it('persists to localStorage', () => { + ecosystemActions.installApp('test.app.id') + + expect(localStorageMock.setItem).toHaveBeenCalled() + // Find the last call to ecosystem_my_apps + const myAppsCalls = localStorageMock.setItem.mock.calls.filter( + (call) => call[0] === 'ecosystem_my_apps' + ) + expect(myAppsCalls.length).toBeGreaterThan(0) + const lastCall = myAppsCalls[myAppsCalls.length - 1] + const savedApps = JSON.parse(lastCall![1]) + expect(savedApps.some((a: { appId: string }) => a.appId === 'test.app.id')).toBe(true) + }) + }) + + describe('uninstallApp', () => { + beforeEach(() => { + ecosystemActions.installApp('app.one') + ecosystemActions.installApp('app.two') + vi.clearAllMocks() + }) + + it('removes app from myApps list', () => { + ecosystemActions.uninstallApp('app.one') + + expect(ecosystemStore.state.myApps).toHaveLength(1) + expect(ecosystemStore.state.myApps[0]?.appId).toBe('app.two') + }) + + it('does nothing if app not found', () => { + ecosystemActions.uninstallApp('app.nonexistent') + + expect(ecosystemStore.state.myApps).toHaveLength(2) + }) + + it('normalizes legacy app IDs', () => { + ecosystemActions.installApp('forge') + ecosystemActions.uninstallApp('forge') + + expect(ecosystemStore.state.myApps.find((a) => a.appId === 'xin.dweb.forge')).toBeUndefined() + }) + + it('persists to localStorage', () => { + ecosystemActions.uninstallApp('app.one') + + expect(localStorageMock.setItem).toHaveBeenCalled() + }) + }) + + describe('updateAppLastUsed', () => { + beforeEach(() => { + ecosystemActions.installApp('test.app.id') + vi.clearAllMocks() + }) + + it('updates lastUsedAt timestamp', async () => { + const originalLastUsed = ecosystemStore.state.myApps[0]?.lastUsedAt ?? 0 + + // Wait a bit to ensure timestamp difference + await new Promise((r) => setTimeout(r, 10)) + + ecosystemActions.updateAppLastUsed('test.app.id') + + const updatedLastUsed = ecosystemStore.state.myApps[0]?.lastUsedAt ?? 0 + expect(updatedLastUsed).toBeGreaterThan(originalLastUsed) + }) + + it('does nothing if app not installed', () => { + const stateBefore = { ...ecosystemStore.state } + ecosystemActions.updateAppLastUsed('nonexistent.app') + + expect(ecosystemStore.state.myApps).toEqual(stateBefore.myApps) + }) + + it('persists to localStorage', () => { + ecosystemActions.updateAppLastUsed('test.app.id') + + expect(localStorageMock.setItem).toHaveBeenCalled() + }) + }) + + describe('isAppInstalled selector', () => { + beforeEach(() => { + ecosystemActions.installApp('installed.app') + }) + + it('returns true for installed app', () => { + const result = ecosystemSelectors.isAppInstalled(ecosystemStore.state, 'installed.app') + expect(result).toBe(true) + }) + + it('returns false for non-installed app', () => { + const result = ecosystemSelectors.isAppInstalled(ecosystemStore.state, 'not.installed') + expect(result).toBe(false) + }) + + it('normalizes legacy app IDs', () => { + ecosystemActions.installApp('teleport') + + const result = ecosystemSelectors.isAppInstalled(ecosystemStore.state, 'teleport') + expect(result).toBe(true) + + const resultNormalized = ecosystemSelectors.isAppInstalled( + ecosystemStore.state, + 'xin.dweb.teleport' + ) + expect(resultNormalized).toBe(true) + }) + }) + + describe('store subscription reactivity', () => { + it('triggers subscribers on installApp', () => { + const subscriber = vi.fn() + const unsubscribe = ecosystemStore.subscribe(subscriber) + + ecosystemActions.installApp('new.app') + + expect(subscriber).toHaveBeenCalled() + unsubscribe() + }) + + it('triggers subscribers on uninstallApp', () => { + ecosystemActions.installApp('app.to.remove') + + const subscriber = vi.fn() + const unsubscribe = ecosystemStore.subscribe(subscriber) + + ecosystemActions.uninstallApp('app.to.remove') + + expect(subscriber).toHaveBeenCalled() + unsubscribe() + }) + + it('triggers subscribers on updateAppLastUsed', () => { + ecosystemActions.installApp('app.to.update') + + const subscriber = vi.fn() + const unsubscribe = ecosystemStore.subscribe(subscriber) + + ecosystemActions.updateAppLastUsed('app.to.update') + + expect(subscriber).toHaveBeenCalled() + unsubscribe() + }) + }) +}) + +describe('ecosystemStore - activeSubPage', () => { + beforeEach(() => { + ecosystemStore.setState(() => ({ + permissions: [], + sources: [], + myApps: [], + availableSubPages: ['discover', 'mine'], + activeSubPage: 'discover', + swiperProgress: 0, + syncSource: null, + })) + }) + + it('setActiveSubPage updates the active sub page', () => { + ecosystemActions.setActiveSubPage('mine') + expect(ecosystemStore.state.activeSubPage).toBe('mine') + }) + + it('persists activeSubPage to localStorage', () => { + ecosystemActions.setActiveSubPage('mine') + + // Find the last call to ecosystem_store + const storeCalls = localStorageMock.setItem.mock.calls.filter( + (call) => call[0] === 'ecosystem_store' + ) + expect(storeCalls.length).toBeGreaterThan(0) + const lastCall = storeCalls[storeCalls.length - 1] + const savedState = JSON.parse(lastCall![1]) + expect(savedState.activeSubPage).toBe('mine') + }) +}) diff --git a/src/stores/ecosystem.ts b/src/stores/ecosystem.ts index 8554f9da8..f58b2b021 100644 --- a/src/stores/ecosystem.ts +++ b/src/stores/ecosystem.ts @@ -4,6 +4,8 @@ */ import { Store } from '@tanstack/react-store' +import { type MyAppRecord } from '@/services/ecosystem/types' +import { loadMyApps, normalizeAppId, saveMyApps } from '@/services/ecosystem/my-apps' /** 权限记录 */ export interface PermissionRecord { @@ -43,6 +45,7 @@ export type SyncSource = 'swiper' | 'indicator' | null export interface EcosystemState { permissions: PermissionRecord[] sources: SourceRecord[] + myApps: MyAppRecord[] /** 当前可用子页面(由 EcosystemDesktop 根据配置/运行态写入) */ availableSubPages: EcosystemSubPage[] /** 当前子页面(发现/我的) */ @@ -81,6 +84,7 @@ function loadState(): EcosystemState { enabled: true, }, ], + myApps: loadMyApps(), availableSubPages: fixedAvailableSubPages, activeSubPage, swiperProgress: 0, @@ -100,6 +104,7 @@ function loadState(): EcosystemState { enabled: true, }, ], + myApps: loadMyApps(), availableSubPages: DEFAULT_AVAILABLE_SUBPAGES, activeSubPage: 'discover', swiperProgress: 0, @@ -110,7 +115,14 @@ function loadState(): EcosystemState { /** 保存状态到 localStorage */ function saveState(state: EcosystemState): void { try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(state)) + localStorage.setItem(STORAGE_KEY, JSON.stringify({ + permissions: state.permissions, + sources: state.sources, + availableSubPages: state.availableSubPages, + activeSubPage: state.activeSubPage, + // myApps is saved separately + })) + saveMyApps(state.myApps) } catch { // ignore } @@ -141,10 +153,60 @@ export const ecosystemSelectors = { getEnabledSources: (state: EcosystemState): SourceRecord[] => { return state.sources.filter((s) => s.enabled) }, + + /** 检查应用是否已安装 */ + isAppInstalled: (state: EcosystemState, appId: string): boolean => { + const normalized = normalizeAppId(appId) + return state.myApps.some((a) => a.appId === normalized) + }, } /** Actions */ export const ecosystemActions = { + /** 安装应用 */ + installApp: (appId: string): void => { + ecosystemStore.setState((state) => { + const normalized = normalizeAppId(appId) + if (state.myApps.some((a) => a.appId === normalized)) { + return state // 已安装 + } + return { + ...state, + myApps: [ + { appId: normalized, installedAt: Date.now(), lastUsedAt: Date.now() }, + ...state.myApps, + ], + } + }) + }, + + /** 卸载应用 */ + uninstallApp: (appId: string): void => { + ecosystemStore.setState((state) => { + const normalized = normalizeAppId(appId) + return { + ...state, + myApps: state.myApps.filter((a) => a.appId !== normalized), + } + }) + }, + + /** 更新应用最后使用时间 */ + updateAppLastUsed: (appId: string): void => { + ecosystemStore.setState((state) => { + const normalized = normalizeAppId(appId) + const existing = state.myApps.find((a) => a.appId === normalized) + if (!existing) return state + + return { + ...state, + myApps: state.myApps.map((a) => + a.appId === normalized ? { ...a, lastUsedAt: Date.now() } : a + ), + } + }) + }, + /** 授予权限 */ grantPermissions: (appId: string, permissions: string[]): void => { ecosystemStore.setState((state) => { From 0eee37c7cebbc7b4eb061ba846cf3e945024a4e2 Mon Sep 17 00:00:00 2001 From: Gaubee Date: Sat, 10 Jan 2026 13:29:24 +0800 Subject: [PATCH 3/4] chore: add missing i18n translations and fix AmountDisplay tests - Add ecosystem.json translations for en, ar, zh-TW - Fix missing i18n keys across multiple locales - Update tests to use aria-label for NumberFlow dual-span structure --- src/components/asset/asset-item.test.tsx | 3 +- src/components/asset/asset-list.test.tsx | 7 +- src/components/common/amount-display.test.tsx | 38 +-- src/i18n/locales/ar/common.json | 75 +++--- src/i18n/locales/ar/ecosystem.json | 24 ++ src/i18n/locales/ar/home.json | 21 +- src/i18n/locales/ar/settings.json | 231 +++++++++--------- src/i18n/locales/ar/transaction.json | 98 ++++---- src/i18n/locales/en/ecosystem.json | 24 ++ src/i18n/locales/zh-TW/authorize.json | 89 +++---- src/i18n/locales/zh-TW/common.json | 55 +++-- src/i18n/locales/zh-TW/ecosystem.json | 24 ++ src/i18n/locales/zh-TW/home.json | 21 +- src/i18n/locales/zh-TW/settings.json | 231 +++++++++--------- src/i18n/locales/zh-TW/transaction.json | 98 ++++---- 15 files changed, 578 insertions(+), 461 deletions(-) create mode 100644 src/i18n/locales/ar/ecosystem.json create mode 100644 src/i18n/locales/en/ecosystem.json create mode 100644 src/i18n/locales/zh-TW/ecosystem.json diff --git a/src/components/asset/asset-item.test.tsx b/src/components/asset/asset-item.test.tsx index 10473cf09..6dbe19e12 100644 --- a/src/components/asset/asset-item.test.tsx +++ b/src/components/asset/asset-item.test.tsx @@ -21,7 +21,8 @@ describe('AssetItem', () => { it('renders formatted balance', () => { render() - expect(screen.getByText('1.5')).toBeInTheDocument() + // AmountDisplay uses aria-label due to NumberFlow dual-span structure + expect(screen.getByLabelText('1.5')).toBeInTheDocument() }) it('uses assetType as name when name is not provided', () => { diff --git a/src/components/asset/asset-list.test.tsx b/src/components/asset/asset-list.test.tsx index 6b7ef40c4..f6edf917e 100644 --- a/src/components/asset/asset-list.test.tsx +++ b/src/components/asset/asset-list.test.tsx @@ -62,9 +62,10 @@ describe('AssetList', () => { it('renders asset balances', () => { renderWithI18n() - expect(screen.getByText('1.5')).toBeInTheDocument() // ETH - expect(screen.getByText('100')).toBeInTheDocument() // USDT - expect(screen.getByText('0.5')).toBeInTheDocument() // BTC + // AmountDisplay uses aria-label due to NumberFlow dual-span structure + expect(screen.getByLabelText('1.5')).toBeInTheDocument() + expect(screen.getByLabelText('100')).toBeInTheDocument() + expect(screen.getByLabelText('0.5')).toBeInTheDocument() }) it('applies custom className', () => { diff --git a/src/components/common/amount-display.test.tsx b/src/components/common/amount-display.test.tsx index d9b74b255..6a56af173 100644 --- a/src/components/common/amount-display.test.tsx +++ b/src/components/common/amount-display.test.tsx @@ -50,79 +50,85 @@ describe('formatAmount', () => { }) describe('AmountDisplay', () => { + // Helper to get amount element (uses aria-label due to NumberFlow dual-span structure) + const getAmount = (value: string) => screen.getByLabelText(value) + const queryAmount = (value: string) => screen.queryByLabelText(value) + it('renders value', () => { render() - expect(screen.getByText('100')).toBeInTheDocument() + expect(getAmount('100')).toBeInTheDocument() }) it('renders with symbol', () => { render() - expect(screen.getByText('100')).toBeInTheDocument() - expect(screen.getByText('USDT')).toBeInTheDocument() + expect(getAmount('100 USDT')).toBeInTheDocument() }) it('shows hidden state', () => { render(