diff --git a/neode-ui/src/components/__tests__/ReceiveBitcoinModal.i18n.test.ts b/neode-ui/src/components/__tests__/ReceiveBitcoinModal.i18n.test.ts new file mode 100644 index 00000000..a5d036d9 --- /dev/null +++ b/neode-ui/src/components/__tests__/ReceiveBitcoinModal.i18n.test.ts @@ -0,0 +1,66 @@ +// Real vue-i18n instance (unlike ReceiveBitcoinModal.test.ts, which mocks +// `t` to a no-op and so cannot catch a bad message string). Operator report +// (2026-09-08): clicking the Ecash tab closed the whole Receive modal, in +// both the browser and the Android companion's WebView. Root cause: vue-i18n +// treats a bare `@` as the start of "linked message" syntax — `en.json`'s +// `receiveBitcoin.lnAddressLabel` ("Your @minibits.cash address:") isn't +// valid linked-message syntax, so *compiling* that message throws a +// SyntaxError the instant it's first rendered (i.e. the moment the address +// loads), and the uncaught render-function error blanks the whole teleported +// modal. Fixed by escaping it as `{'@'}` (the same pattern already used for +// `settings.domainNamePlaceholder`). This test uses the real compiler so a +// future bad interpolation string in this component fails fast in `npm test` +// instead of only in a live browser. +import { flushPromises, mount } from '@vue/test-utils' +import { describe, expect, it, vi } from 'vitest' +import ReceiveBitcoinModal from '../ReceiveBitcoinModal.vue' +import { rpcClient } from '@/api/rpc-client' +import i18n from '@/i18n' + +vi.mock('@/api/rpc-client', () => ({ + rpcClient: { call: vi.fn() }, +})) + +vi.mock('@/composables/useLightningRequired', () => ({ + useLightningRequired: () => ({ + requireLightningReady: vi.fn().mockResolvedValue(true), + handleLightningFailure: vi.fn().mockReturnValue(false), + }), +})) + +describe('ReceiveBitcoinModal — ecash tab with the real vue-i18n compiler', () => { + it('renders the Minibits address label without an uncaught render error', async () => { + vi.mocked(rpcClient.call).mockImplementation(async ({ method }: { method: string }) => { + if (method === 'wallet.ecash-lnaddress') { + return { address: 'someone@minibits.cash' } as never + } + return { claimed_count: 0, received_sats: 0, failed_count: 0 } as never + }) + + const wrapper = mount(ReceiveBitcoinModal, { + props: { show: true }, + attachTo: document.body, + global: { plugins: [i18n] }, + }) + let captured: unknown = null + wrapper.vm.$.appContext.app.config.errorHandler = (err) => { captured = err } + await flushPromises() + + const ecashTab = Array.from(document.body.querySelectorAll('button')).find((b) => + b.textContent?.toLowerCase().includes('ecash'), + ) + expect(ecashTab).toBeTruthy() + ecashTab!.dispatchEvent(new Event('click', { bubbles: true })) + await flushPromises() + await flushPromises() + + expect(captured).toBeNull() + expect(wrapper.emitted('close')).toBeFalsy() + const dialog = document.body.querySelector('[role="dialog"]') + expect(dialog).toBeTruthy() + expect(dialog?.textContent).toContain('minibits.cash') + expect(dialog?.textContent).toContain('someone@minibits.cash') + + wrapper.unmount() + }) +}) diff --git a/neode-ui/src/locales/en.json b/neode-ui/src/locales/en.json index c852de09..751bc17b 100644 --- a/neode-ui/src/locales/en.json +++ b/neode-ui/src/locales/en.json @@ -777,7 +777,7 @@ "pasteEcashToken": "Paste ecash token", "lnAddressTitle": "Or share your Minibits Lightning address", "lnAddressHint": "Anyone can pay you sats with any Lightning wallet by sending to this address — the sats arrive as ecash. Keep this screen open to receive them.", - "lnAddressLabel": "Your @minibits.cash address:", + "lnAddressLabel": "Your {'@'}minibits.cash address:", "lnAddressLoading": "Setting up your Lightning address…", "lnAddressUnavailable": "Lightning address unavailable — you can still paste a token below.", "lnAddressReceived": "Received {amount} sats to your Lightning address!", diff --git a/neode-ui/src/locales/es.json b/neode-ui/src/locales/es.json index ae1d7fdc..d1ceb5cd 100644 --- a/neode-ui/src/locales/es.json +++ b/neode-ui/src/locales/es.json @@ -758,7 +758,7 @@ "pasteEcashToken": "Pegar token Ecash", "lnAddressTitle": "O comparte tu direcci\u00f3n Lightning de Minibits", "lnAddressHint": "Cualquier persona puede pagarte sats con cualquier billetera Lightning enviando a esta direcci\u00f3n \u2014 los sats llegan como ecash. Mant\u00e9n esta pantalla abierta para recibirlos.", - "lnAddressLabel": "Su direcci\u00f3n @minibits.cash:", + "lnAddressLabel": "Su direcci\u00f3n {'@'}minibits.cash:", "lnAddressLoading": "Configurando su direcci\u00f3n Lightning\u2026", "lnAddressUnavailable": "Direcci\u00f3n Lightning no disponible \u2014 a\u00fan puede pegar un token abajo.", "lnAddressReceived": "\u00a1Recibi\u00f3 {amount} sats en su direcci\u00f3n Lightning!",