mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo-client.git
synced 2026-09-21 16:52:02 -07:00
Surface broadcast errors on the new post page
Implements New Post Page scenario 6 (specs/memo-new.feature): when a memo broadcast fails, the app surfaces the real wallet error and the user stays on /posts/new instead of navigating to the feed. - src/services/new-post.js: distinguish local validation errors from broadcast failures; expose the real error message via broadcastError. - src/components/app-body/new-post/index.js: render the broadcast error. - acceptance/lib/handlers.js: support wallet-fail, attempts-to-broadcast, error-containing, and remain-on-path steps. - Unit tests for broadcast-failure surfacing. By coder.
This commit is contained in:
@@ -31,7 +31,9 @@ function makeWallet (address) {
|
||||
return this.utxos
|
||||
},
|
||||
sendOpReturn: async function (walletInfo, bchUtxos, msg, prefix) {
|
||||
// Record the broadcast attempt, then fail if configured to do so.
|
||||
this.broadcasts.push({ walletInfo, bchUtxos, msg, prefix })
|
||||
if (this.failWith) throw new Error(this.failWith)
|
||||
return 'aa'.repeat(32)
|
||||
}
|
||||
}
|
||||
@@ -95,6 +97,17 @@ const handlers = [
|
||||
world.currentPath = NewPostPage.RECENT_FEED_PATH
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'wallet fails to broadcast with error',
|
||||
pattern: /^the wallet fails to broadcast with the error "<([A-Za-z0-9_]+)>"$/,
|
||||
run (m, example, world) {
|
||||
const param = m[1]
|
||||
if (!(param in example)) {
|
||||
throw new Error(`Missing example value for "${param}"`)
|
||||
}
|
||||
world.wallet.failWith = example[param]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'navigate to path',
|
||||
pattern: /^I navigate to the path (.+)$/,
|
||||
@@ -109,6 +122,16 @@ const handlers = [
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'remain on path',
|
||||
pattern: /^I remain on the path (.+)$/,
|
||||
run (m, example, world) {
|
||||
const target = m[1].trim()
|
||||
if (world.currentPath !== target) {
|
||||
throw new Error(`Expected to remain on path ${target}, but current path is ${world.currentPath}.`)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'open navigation menu',
|
||||
pattern: /^I open the navigation menu$/,
|
||||
@@ -145,8 +168,8 @@ const handlers = [
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'broadcasts OP_RETURN with Memo post prefix',
|
||||
pattern: /^(?:the wallet|the app) broadcasts an OP_RETURN transaction with the Memo post prefix$/,
|
||||
name: 'broadcasts/attempts OP_RETURN with Memo post prefix',
|
||||
pattern: /^(?:the wallet|the app) (?:broadcasts|attempts to broadcast) an OP_RETURN transaction with the Memo post prefix$/,
|
||||
run (m, example, world) {
|
||||
const broadcasts = world.wallet.broadcasts
|
||||
if (!broadcasts.length) {
|
||||
@@ -176,6 +199,18 @@ const handlers = [
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'page shows error containing text',
|
||||
pattern: /^the new post page shows an error containing "<([A-Za-z0-9_]+)>"$/,
|
||||
run (m, example, world) {
|
||||
const param = m[1]
|
||||
const expected = example[param]
|
||||
const actual = world.newPage.broadcastError || ''
|
||||
if (!actual.includes(expected)) {
|
||||
throw new Error(`Expected an error containing "${expected}", got "${actual}".`)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'page shows validation/length error',
|
||||
pattern: /^the (?:app|new post page) shows a (validation|length) error$/,
|
||||
|
||||
@@ -36,11 +36,15 @@ function NewPost (props) {
|
||||
|
||||
const result = await page.submit()
|
||||
if (!result.ok) {
|
||||
setErr(
|
||||
result.error === 'memo_length'
|
||||
? `Memo is too long. Maximum is ${maxChars} characters.`
|
||||
: 'Memo must not be empty.'
|
||||
)
|
||||
if (result.error === 'memo_length') {
|
||||
setErr(`Memo is too long. Maximum is ${maxChars} characters.`)
|
||||
} else if (result.error === 'memo_validation') {
|
||||
setErr('Memo must not be empty.')
|
||||
} else if (result.message) {
|
||||
setErr(`Failed to broadcast: ${result.message}`)
|
||||
} else {
|
||||
setErr('Failed to post memo.')
|
||||
}
|
||||
}
|
||||
// On success page.submit() navigated to the recent feed.
|
||||
} catch (submitErr) {
|
||||
|
||||
@@ -26,6 +26,7 @@ class NewPostPage {
|
||||
|
||||
this.input = ''
|
||||
this.submitError = null
|
||||
this.broadcastError = null
|
||||
this.posting = false
|
||||
|
||||
// The navigation menu links to the new post page.
|
||||
@@ -55,10 +56,12 @@ class NewPostPage {
|
||||
}
|
||||
|
||||
// Validate and post the current draft. On success, navigate to the recent
|
||||
// feed. On failure, record the typed error. Resolves with a result object.
|
||||
// feed. On failure, record the typed error and stay on the page. Resolves
|
||||
// with a result object.
|
||||
async submit () {
|
||||
this.posting = true
|
||||
this.submitError = null
|
||||
this.broadcastError = null
|
||||
|
||||
try {
|
||||
if (!this.memoPost) {
|
||||
@@ -70,9 +73,16 @@ class NewPostPage {
|
||||
this.posting = false
|
||||
return { ok: true, txid }
|
||||
} catch (err) {
|
||||
this.submitError = err.code || 'memo_validation'
|
||||
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 }
|
||||
return { ok: false, error: this.submitError, message: this.broadcastError }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ function fakeWallet (cashAddress = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0
|
||||
getUtxos: async function () { return this.utxos },
|
||||
sendOpReturn: async function (walletInfo, bchUtxos, msg, prefix) {
|
||||
this.broadcasts.push({ walletInfo, bchUtxos, msg, prefix })
|
||||
if (this.failWith) throw new Error(this.failWith)
|
||||
return 'newpost-txid'
|
||||
}
|
||||
}
|
||||
@@ -171,3 +172,41 @@ test('submitting without a memo post handler reports an error and does not navig
|
||||
assert.equal(result.ok, false)
|
||||
assert.deepEqual(navigations, [])
|
||||
})
|
||||
|
||||
test('a failed broadcast surfaces the real error and does not navigate', async () => {
|
||||
const wallet = fakeWallet()
|
||||
const feed = fakeFeed()
|
||||
wallet.failWith = 'BCH UTXO list is empty'
|
||||
const navigations = []
|
||||
const page = new NewPostPage({
|
||||
memoPost: new MemoPost({ wallet, feed }),
|
||||
navigate: (p) => navigations.push(p)
|
||||
})
|
||||
page.setInput('hello memo')
|
||||
|
||||
const result = await page.submit()
|
||||
|
||||
assert.equal(result.ok, false)
|
||||
assert.equal(page.submitError, 'broadcast')
|
||||
assert.match(page.broadcastError, /BCH UTXO list is empty/)
|
||||
// The broadcast was attempted (recorded) before it failed.
|
||||
assert.equal(wallet.broadcasts.length, 1)
|
||||
assert.equal(wallet.broadcasts[0].prefix, '6d02')
|
||||
// The user stays on the page.
|
||||
assert.deepEqual(navigations, [])
|
||||
})
|
||||
|
||||
test('a failed broadcast surfaces a different real error message', async () => {
|
||||
const wallet = fakeWallet()
|
||||
wallet.failWith = 'Insufficient balance'
|
||||
const page = new NewPostPage({
|
||||
memoPost: new MemoPost({ wallet }),
|
||||
navigate: () => {}
|
||||
})
|
||||
page.setInput('hello memo')
|
||||
|
||||
const result = await page.submit()
|
||||
|
||||
assert.equal(result.ok, false)
|
||||
assert.match(page.broadcastError, /Insufficient balance/)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user