From 79ff2f13396fb729b9f1e22859e21b6f6dc39def Mon Sep 17 00:00:00 2001 From: dxfareed Date: Sat, 5 Sep 2026 18:39:26 +0100 Subject: [PATCH 1/2] test(wallet): cover truncateAddress edge cases truncateAddress had two tests covering a C-address, a short input and an empty string. Adds the G-address case the issue asks for plus the boundaries that were unpinned: an input exactly as long as the elision, one character past it, a nullish address reaching the `|| ''` guard, the 6/6 defaults, and custom head/tail lengths. Also asserts both ends survive truncation and that two addresses sharing a 49-character prefix still render differently, so a head-only regression cannot pass. Verified by mutation: dropping the tail fails 7 tests, changing `<=` to `<` fails 1, removing the nullish guard fails 1, and altering the 6/6 defaults fails 4. Tests only; lib/nfts.ts is unchanged. --- frontend/wallet/lib/__tests__/nfts.test.ts | 54 ++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/frontend/wallet/lib/__tests__/nfts.test.ts b/frontend/wallet/lib/__tests__/nfts.test.ts index 6d79c0d35..239bfa04c 100644 --- a/frontend/wallet/lib/__tests__/nfts.test.ts +++ b/frontend/wallet/lib/__tests__/nfts.test.ts @@ -49,10 +49,64 @@ describe('nfts', () => { expect(truncateAddress(NFT_C, 6, 6)).toBe('CAAAAA…AAANFT') }) + it('elides the middle of a G-address', () => { + expect(WALLET).toHaveLength(56) + expect(truncateAddress(WALLET, 6, 6)).toBe('GAAAAA…AAAWLT') + }) + + it('keeps both ends so the address stays checkable', () => { + const out = truncateAddress(WALLET) + + // Both ends are what a user compares against a pasted address, so the + // prefix and suffix must survive verbatim. + expect(out.startsWith(WALLET.slice(0, 6))).toBe(true) + expect(out.endsWith(WALLET.slice(-6))).toBe(true) + expect(out).toContain('…') + }) + + it('does not confuse two addresses that share a prefix', () => { + // G-addresses share a leading character, so a head-only truncation would + // render distinct wallets identically. + const a = `G${'A'.repeat(49)}FIRST` + const b = `G${'A'.repeat(49)}OTHER` + + expect(truncateAddress(a)).not.toBe(truncateAddress(b)) + }) + + it('defaults to six characters at each end', () => { + expect(truncateAddress(WALLET)).toBe(truncateAddress(WALLET, 6, 6)) + }) + + it('honours custom head and tail lengths', () => { + // The NFT list renders contract ids at 5/5. + expect(truncateAddress(NFT_C, 5, 5)).toBe('CAAAA…AANFT') + expect(truncateAddress(NFT_C, 4, 8)).toBe('CAAA…AAAAANFT') + }) + it('leaves an address shorter than the elision alone', () => { expect(truncateAddress('C123')).toBe('C123') expect(truncateAddress('')).toBe('') }) + + it('leaves an address exactly as long as the elision alone', () => { + const exact = 'ABCDEFGHIJKL' // 12 === 6 + 6 + + expect(exact).toHaveLength(12) + expect(truncateAddress(exact)).toBe(exact) + }) + + it('truncates one character past the boundary', () => { + // Truncation kicks in here, though the ellipsis means the result is no + // shorter than the input. Pinned so the off-by-one stays deliberate. + expect(truncateAddress('ABCDEFGHIJKLM')).toBe('ABCDEF…HIJKLM') + }) + + it('returns an empty string for a nullish address', () => { + // Callers pass addresses straight from API payloads, where a missing + // field arrives as null rather than ''. + expect(truncateAddress(null as unknown as string)).toBe('') + expect(truncateAddress(undefined as unknown as string)).toBe('') + }) }) describe('formatTokenId', () => { From 53ce8bb7d2892d52df20567b5056e11dd78d61c2 Mon Sep 17 00:00:00 2001 From: dxfareed Date: Sat, 5 Sep 2026 18:46:49 +0100 Subject: [PATCH 2/2] test(wallet): polyfill TextEncoder so the feeBump suite runs lib/__tests__/feeBump.test.ts has been failing to load with "ReferenceError: TextEncoder is not defined", taking the wallet suite to 20/21. The suite mocks @stellar/stellar-sdk, but the failing import arrives through lib/fees -> @veil/sdk -> sdk/src/core.ts, which resolves a different physical copy of the package (sdk/node_modules, or the repo root when that is absent). Jest keys mocks by resolved path, so the mock covers the wallet's copy only and the real bundle loads, reaching for TextEncoder at module scope. jsdom does not define it. Polyfills TextEncoder/TextDecoder from node:util in setupFiles, which runs before modules load. Not moduleNameMapper: the note at jest.config.js:18 records that forcing a single copy breaks jsdom's browser-field resolution. Verified 21/21 suites and 260 tests both with and without sdk/node_modules present, since the two shapes resolve different copies. --- frontend/wallet/jest.config.js | 1 + frontend/wallet/jest.setup.js | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 frontend/wallet/jest.setup.js diff --git a/frontend/wallet/jest.config.js b/frontend/wallet/jest.config.js index 45f23a0be..4d49f48c1 100644 --- a/frontend/wallet/jest.config.js +++ b/frontend/wallet/jest.config.js @@ -34,6 +34,7 @@ const config = { '^@veil/sep7$': '/../../sdk/src/sep7', '^@veil/prf$': '/../../sdk/src/crypto/prf', }, + setupFiles: ['/jest.setup.js'], setupFilesAfterEnv: [], collectCoverageFrom: [ 'lib/**/*.ts', diff --git a/frontend/wallet/jest.setup.js b/frontend/wallet/jest.setup.js new file mode 100644 index 000000000..a7c34e26d --- /dev/null +++ b/frontend/wallet/jest.setup.js @@ -0,0 +1,6 @@ +// jsdom does not expose TextEncoder/TextDecoder, which @stellar/stellar-sdk +// reaches for at module load. Node has had both since v11. +const { TextEncoder, TextDecoder } = require('node:util') + +if (typeof globalThis.TextEncoder === 'undefined') globalThis.TextEncoder = TextEncoder +if (typeof globalThis.TextDecoder === 'undefined') globalThis.TextDecoder = TextDecoder