diff --git a/src/services/new-post.js b/src/services/new-post.js index a52b38b..8e066c1 100644 --- a/src/services/new-post.js +++ b/src/services/new-post.js @@ -73,18 +73,23 @@ class NewPostPage { this.posting = false return { ok: true, txid } } catch (err) { - if (err.code === 'memo_validation' || err.code === 'memo_length') { - // Local validation failure: record the typed validation error. - this.submitError = err.code - } else { - // Broadcast (or handler) failure: surface the real error message. - this.broadcastError = err.message || String(err) - this.submitError = 'broadcast' - } - this.posting = false - return { ok: false, error: this.submitError, message: this.broadcastError } + return this._handleSubmitFailure(err) } } + + // Classify a submit failure, record the typed state, and return the failure + // result. Local validation failures set submitError; broadcast or handler + // failures surface the real error message via broadcastError. + _handleSubmitFailure (err) { + if (err.code === 'memo_validation' || err.code === 'memo_length') { + this.submitError = err.code + } else { + this.broadcastError = err.message || String(err) + this.submitError = 'broadcast' + } + this.posting = false + return { ok: false, error: this.submitError, message: this.broadcastError } + } } NewPostPage.NEW_POST_PATH = NEW_POST_PATH @@ -93,5 +98,5 @@ NewPostPage.RECENT_FEED_PATH = RECENT_FEED_PATH module.exports = NewPostPage // mutate4javascript-manifest-begin -// {"version":1,"tested_at":"2026-08-26T00:07:51.843Z","module_hash":"469dfd90342f6bcacf5b89ed819620663e221e20832f6936c35c46f9681ebfdc","functions":[{"id":"func/NewPostPage.constructor","name":"NewPostPage.constructor","line":22,"end_line":33,"hash":"7c957fbaa2b8d4adb62c1bbf240749243696a68e71e7896aefdbb68437432cd8"},{"id":"func/NewPostPage.addMenuLink","name":"NewPostPage.addMenuLink","line":36,"end_line":39,"hash":"bac97164d2d70bfdb946c4d54e67983c43cad093bac558f1d169e1739ca97137"},{"id":"func/NewPostPage.hasMenuLink","name":"NewPostPage.hasMenuLink","line":42,"end_line":44,"hash":"7e1abf5d0833aaf3b3da3a024de9eb2b80da900b2930e832c7e92d77e0e50344"},{"id":"func/NewPostPage.setInput","name":"NewPostPage.setInput","line":47,"end_line":50,"hash":"595484662b7ca07ef5eef5cebbff06309242552d9b4d15687df02f260ba88244"},{"id":"func/NewPostPage.remainingCount","name":"NewPostPage.remainingCount","line":53,"end_line":55,"hash":"521ce4ed841f62099529b327f2245a9e94c09af2f67ed5d91839e52607bcea37"},{"id":"func/NewPostPage.submit","name":"NewPostPage.submit","line":59,"end_line":77,"hash":"f4ce743f5a4bb139615086165b25173641a9388af16b7527f2db243a1c6b596c"}]} +// {"version":1,"tested_at":"2026-08-26T00:35:50.421Z","module_hash":"8d50d002e9c6094a1bd2d6e764023c942b1eb0f085b019255dc80f0a72ab1ec6","functions":[{"id":"func/NewPostPage.constructor","name":"NewPostPage.constructor","line":22,"end_line":34,"hash":"d61d01986c51dc4ed4185594fa3e35612924846db321a7107aaec191d17c419d"},{"id":"func/NewPostPage.addMenuLink","name":"NewPostPage.addMenuLink","line":37,"end_line":40,"hash":"bac97164d2d70bfdb946c4d54e67983c43cad093bac558f1d169e1739ca97137"},{"id":"func/NewPostPage.hasMenuLink","name":"NewPostPage.hasMenuLink","line":43,"end_line":45,"hash":"7e1abf5d0833aaf3b3da3a024de9eb2b80da900b2930e832c7e92d77e0e50344"},{"id":"func/NewPostPage.setInput","name":"NewPostPage.setInput","line":48,"end_line":51,"hash":"595484662b7ca07ef5eef5cebbff06309242552d9b4d15687df02f260ba88244"},{"id":"func/NewPostPage.remainingCount","name":"NewPostPage.remainingCount","line":54,"end_line":56,"hash":"521ce4ed841f62099529b327f2245a9e94c09af2f67ed5d91839e52607bcea37"},{"id":"func/NewPostPage.submit","name":"NewPostPage.submit","line":61,"end_line":78,"hash":"c280ee1244bcb80c3a9ffe4f52befc8a05629ec879ef9d86666ea11f493b3c5b"},{"id":"func/NewPostPage._handleSubmitFailure","name":"NewPostPage._handleSubmitFailure","line":83,"end_line":92,"hash":"b13d8cd6b48b1f42d72e0031cabcaa00bb78b813f42250517d711f0d6fb23126"}]} // mutate4javascript-manifest-end diff --git a/test/property/memo-post.property.test.js b/test/property/memo-post.property.test.js index b848f56..66829b2 100644 --- a/test/property/memo-post.property.test.js +++ b/test/property/memo-post.property.test.js @@ -34,6 +34,25 @@ function buildPage () { }) } +// A fake wallet recording broadcast attempts; fails when failWith is set. +function fakeWallet () { + const wallet = { + walletInfo: { cashAddress: 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d' }, + utxos: [{ txid: 'utxo-fee' }], + getUtxos: async function () { return this.utxos }, + sendOpReturn: async function (walletInfo, bchUtxos, msg, prefix) { + if (this.failWith) throw new Error(this.failWith) + return 'prop-txid' + } + } + return wallet +} + +function fakeFeed () { + const posts = [] + return { posts, addPost: (p) => posts.push(p) } +} + test('memo validation: any non-blank string at or below the limit is valid', async () => { await forAll( (i) => { @@ -110,3 +129,27 @@ test('menu link registration is idempotent', async () => { { label: 'menu link idempotence' } ) }) + +test('a broadcast failure surfaces the error and never navigates', async () => { + await forAll( + (i) => ({ message: stringOf(1 + Math.floor(rng() * 40)), failWith: `boom-${i % 97}` }), + ({ message, failWith }) => { + const wallet = fakeWallet() + wallet.failWith = failWith + const navigations = [] + const page = new NewPostPage({ + memoPost: new MemoPost({ wallet, feed: fakeFeed() }), + navigate: (p) => navigations.push(p) + }) + page.setInput(message) + + return page.submit().then((result) => { + if (result.ok) return false + if (page.submitError !== 'broadcast') return false + if (!page.broadcastError || !page.broadcastError.includes('boom')) return false + return navigations.length === 0 + }) + }, + { label: 'broadcast failure does not navigate' } + ) +})