Files
psf-memo-client/test/unit/new-post.test.js
T

213 lines
6.9 KiB
JavaScript
Raw Normal View History

2026-08-25 17:00:27 -07:00
/*
Unit tests for the New Post Page behavior slice (src/services/new-post.js).
Expresses the observable behavior described by specs/memo-new.feature:
- posting a valid memo broadcasts an OP_RETURN with the Memo post prefix and
navigates the user to the recent feed.
- an empty memo is rejected with a validation error; nothing is broadcast.
- an over-long memo is rejected with a length error; nothing is broadcast.
- the character counter counts down from the memo limit.
- the navigation menu links to /posts/new.
*/
'use strict'
const test = require('node:test')
const assert = require('node:assert/strict')
const MemoPost = require('../../src/services/memo-post')
const NewPostPage = require('../../src/services/new-post')
const MAX = MemoPost.MAX_MEMO_CHARS // 217
// A fake wallet recording broadcast attempts.
function fakeWallet (cashAddress = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d') {
const broadcasts = []
const wallet = {
walletInfo: { cashAddress },
utxos: [{ txid: 'utxo-fee' }],
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)
2026-08-25 17:00:27 -07:00
return 'newpost-txid'
}
}
wallet.broadcasts = broadcasts
return wallet
}
function fakeFeed () {
const posts = []
return { posts, addPost: (p) => posts.push(p) }
}
function build () {
const wallet = fakeWallet()
const feed = fakeFeed()
const memoPost = new MemoPost({ wallet, feed })
const navigations = []
const page = new NewPostPage({
memoPost,
navigate: (path) => navigations.push(path)
})
return { wallet, feed, memoPost, page, navigations }
}
test('NEW_POST_PATH and RECENT_FEED_PATH constants', () => {
assert.equal(NewPostPage.NEW_POST_PATH, '/posts/new')
assert.equal(NewPostPage.RECENT_FEED_PATH, '/posts/recent')
})
test('the new post page is linked from the navigation menu', () => {
const { page } = build()
assert.equal(page.hasMenuLink('/posts/new'), true)
})
test('the character counter counts down from the memo limit for an empty memo', () => {
const { page } = build()
page.setInput('')
assert.equal(page.remainingCount(), MAX)
})
test('the character counter counts down from the memo limit for a short memo', () => {
const { page } = build()
page.setInput('hello')
assert.equal(page.remainingCount(), MAX - 5)
})
test('the character counter reaches zero at the memo limit', () => {
const { page } = build()
page.setInput('x'.repeat(MAX))
assert.equal(page.remainingCount(), 0)
})
test('posting a valid memo broadcasts the Memo post prefix and navigates to the feed', async () => {
const { wallet, feed, page, navigations } = build()
page.setInput('hello memo')
const result = await page.submit()
assert.equal(result.ok, true)
// The page returns to an idle (not posting) state after success.
assert.equal(page.posting, false)
2026-08-25 17:00:27 -07:00
// Broadcast happened with the Memo post prefix and the exact message.
assert.equal(wallet.broadcasts.length, 1)
assert.equal(wallet.broadcasts[0].prefix, '6d02')
assert.equal(wallet.broadcasts[0].msg, 'hello memo')
// Navigated to the recent feed after posting.
assert.deepEqual(navigations, ['/posts/recent'])
// The feed reflects the new post from this address.
assert.equal(feed.posts.length, 1)
assert.equal(feed.posts[0].text, 'hello memo')
})
test('posting an empty memo is rejected with a validation error and nothing is broadcast', async () => {
const { wallet, feed, page, navigations } = build()
page.setInput('')
const result = await page.submit()
assert.equal(result.ok, false)
assert.equal(result.error, 'memo_validation')
assert.equal(page.submitError, 'memo_validation')
assert.equal(page.posting, false)
2026-08-25 17:00:27 -07:00
assert.equal(wallet.broadcasts.length, 0)
assert.equal(feed.posts.length, 0)
assert.deepEqual(navigations, [])
})
test('posting an over-long memo is rejected with a length error and nothing is broadcast', async () => {
const { wallet, feed, page, navigations } = build()
page.setInput('y'.repeat(MAX + 1))
const result = await page.submit()
assert.equal(result.ok, false)
assert.equal(result.error, 'memo_length')
assert.equal(page.submitError, 'memo_length')
assert.equal(page.posting, false)
2026-08-25 17:00:27 -07:00
assert.equal(wallet.broadcasts.length, 0)
assert.equal(feed.posts.length, 0)
assert.deepEqual(navigations, [])
})
test('the new post page starts idle (not posting)', () => {
const { page } = build()
assert.equal(page.posting, false)
})
test('posting is true while a submit is in flight and false once it settles', async () => {
const wallet = fakeWallet()
const feed = fakeFeed()
// Defer the broadcast so we can observe the in-flight posting state.
let resolveSend
wallet.sendOpReturn = async () => new Promise((resolve) => { resolveSend = resolve })
const page = new NewPostPage({
memoPost: new MemoPost({ wallet, feed }),
navigate: () => {}
})
page.setInput('hello memo')
assert.equal(page.posting, false)
const pending = page.submit()
assert.equal(page.posting, true)
// Yield until the async chain reaches the deferred sendOpReturn call.
2026-08-25 17:13:16 -07:00
await new Promise((resolve) => setImmediate(resolve))
assert.equal(typeof resolveSend, 'function')
resolveSend('in-flight-txid')
await pending
assert.equal(page.posting, false)
})
2026-08-25 17:00:27 -07:00
test('submitting without a memo post handler reports an error and does not navigate', async () => {
const navigations = []
const page = new NewPostPage({ navigate: (p) => navigations.push(p) })
page.setInput('hello')
const result = await page.submit()
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/)
})