mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo-client.git
synced 2026-09-21 16:52:02 -07:00
Reduce DRY duplication in reply-memo tests
Refactor the coder's new memo-reply and reply-thread-page tests to use the shared test helpers (fake wallet, MemoAction tests, page-controller tests, page submit tests, page build wiring). Extend the memo-action and page-build helpers to support the reply slice's extra parent-txid argument and byte-based multi-byte tests. Behavior is preserved; all unit, property, and acceptance tests pass. By refactorer.
This commit is contained in:
@@ -18,17 +18,21 @@ const { fakeWallet } = require('../helpers/fake-wallet')
|
||||
// storeKey - the action's store dependency key ('feed' or 'profiles')
|
||||
// storeFactory - () => a fresh store
|
||||
// assertStoreEmpty - (store, wallet) => asserts the store was not updated
|
||||
// assertBroadcastMsg - (broadcast, value) => asserts the broadcast message
|
||||
// byteBased - true when the slice counts bytes (registers multi-byte tests)
|
||||
// extraArgs - extra arguments passed to the broadcast method after the value
|
||||
function registerMemoActionTests (cfg) {
|
||||
const { Action, method, MAX, lengthCode, validationCode, label, storeKey, storeFactory, assertStoreEmpty } = cfg
|
||||
const { Action, method, MAX, lengthCode, validationCode, label, storeKey, storeFactory, assertStoreEmpty, assertBroadcastMsg, byteBased = false, extraArgs = [] } = cfg
|
||||
const checkBroadcastMsg = assertBroadcastMsg || ((broadcast, value) => assert.equal(broadcast.msg, value))
|
||||
|
||||
test(`${label} at the maximum length (${MAX}) is accepted`, async () => {
|
||||
const wallet = fakeWallet()
|
||||
const action = new Action({ wallet })
|
||||
|
||||
const value = 'x'.repeat(MAX)
|
||||
const txid = await action[method](value)
|
||||
const txid = await action[method](value, ...extraArgs)
|
||||
assert.equal(txid, 'fake-txid')
|
||||
assert.equal(wallet.broadcasts[0].msg, value)
|
||||
checkBroadcastMsg(wallet.broadcasts[0], value)
|
||||
})
|
||||
|
||||
test(`${label} over the limit (${MAX + 1}) throws a length error and broadcasts nothing`, async () => {
|
||||
@@ -37,7 +41,7 @@ function registerMemoActionTests (cfg) {
|
||||
const action = new Action({ wallet, [storeKey]: store })
|
||||
|
||||
await assert.rejects(
|
||||
action[method]('y'.repeat(MAX + 1)),
|
||||
action[method]('y'.repeat(MAX + 1), ...extraArgs),
|
||||
(err) => err.code === lengthCode
|
||||
)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
@@ -50,12 +54,54 @@ function registerMemoActionTests (cfg) {
|
||||
const action = new Action({ wallet })
|
||||
|
||||
await assert.rejects(
|
||||
action[method](invalid),
|
||||
action[method](invalid, ...extraArgs),
|
||||
(err) => err.code === validationCode
|
||||
)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
}
|
||||
})
|
||||
|
||||
test(`${label} that is empty throws a validation error and broadcasts nothing`, async () => {
|
||||
const wallet = fakeWallet()
|
||||
const store = storeFactory()
|
||||
const action = new Action({ wallet, [storeKey]: store })
|
||||
|
||||
await assert.rejects(
|
||||
action[method]('', ...extraArgs),
|
||||
(err) => err.code === validationCode
|
||||
)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
assertStoreEmpty(store, wallet)
|
||||
})
|
||||
|
||||
if (byteBased) {
|
||||
test(`${label} with multi-byte characters at the byte limit is accepted`, async () => {
|
||||
const wallet = fakeWallet()
|
||||
const action = new Action({ wallet })
|
||||
|
||||
// 'é' encodes to 2 UTF-8 bytes, so floor(MAX/2) characters reach the limit.
|
||||
const count = Math.floor(MAX / 2)
|
||||
const value = 'é'.repeat(count)
|
||||
assert.equal(Buffer.byteLength(value, 'utf8'), count * 2)
|
||||
const txid = await action[method](value, ...extraArgs)
|
||||
assert.equal(txid, 'fake-txid')
|
||||
})
|
||||
|
||||
test(`${label} with multi-byte characters that exceed the byte limit throws a length error`, async () => {
|
||||
const wallet = fakeWallet()
|
||||
const action = new Action({ wallet })
|
||||
|
||||
const count = Math.floor(MAX / 2) + 1
|
||||
const value = 'é'.repeat(count)
|
||||
assert.ok(Buffer.byteLength(value, 'utf8') > MAX)
|
||||
|
||||
await assert.rejects(
|
||||
action[method](value, ...extraArgs),
|
||||
(err) => err.code === lengthCode
|
||||
)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { registerMemoActionTests }
|
||||
|
||||
@@ -59,19 +59,6 @@ test('posting a valid memo broadcasts an OP_RETURN with the Memo post prefix and
|
||||
assert.equal(feed.posts[0].address, wallet.walletInfo.cashAddress)
|
||||
})
|
||||
|
||||
test('posting an empty memo throws a validation error and broadcasts nothing', async () => {
|
||||
const wallet = fakeWallet()
|
||||
const feed = fakeFeed()
|
||||
const memoPost = new MemoPost({ wallet, feed })
|
||||
|
||||
await assert.rejects(
|
||||
memoPost.post(''),
|
||||
(err) => err.code === 'memo_validation'
|
||||
)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
assert.equal(feed.posts.length, 0)
|
||||
})
|
||||
|
||||
test('posting without a wallet reports a missing-wallet error', async () => {
|
||||
const memoPost = new MemoPost({})
|
||||
await assert.rejects(
|
||||
|
||||
@@ -16,24 +16,11 @@ const test = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
|
||||
const MemoReply = require('../../src/services/memo-reply')
|
||||
const { fakeWallet } = require('../helpers/fake-wallet')
|
||||
const { registerMemoActionTests } = require('./memo-action-helpers')
|
||||
|
||||
const PARENT_TXID = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
|
||||
|
||||
// A fake wallet that records every broadcast attempt.
|
||||
function fakeWallet (cashAddress = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d', utxos = [{ txid: 'utxo1' }]) {
|
||||
const broadcasts = []
|
||||
const wallet = {
|
||||
walletInfo: { cashAddress },
|
||||
getUtxos: async () => utxos,
|
||||
sendOpReturn: async (msg, prefix) => {
|
||||
broadcasts.push({ msg, prefix })
|
||||
return 'fake-txid'
|
||||
}
|
||||
}
|
||||
wallet.broadcasts = broadcasts
|
||||
return wallet
|
||||
}
|
||||
|
||||
// A fake thread that records replies added to the thread view.
|
||||
function fakeThread (rootTxid = PARENT_TXID) {
|
||||
const replies = []
|
||||
@@ -55,6 +42,21 @@ function decodeParentTxid (raw) {
|
||||
return Buffer.from(raw.slice(0, 32)).toString('hex')
|
||||
}
|
||||
|
||||
registerMemoActionTests({
|
||||
Action: MemoReply,
|
||||
method: 'reply',
|
||||
MAX: 184,
|
||||
lengthCode: 'reply_length',
|
||||
validationCode: 'reply_validation',
|
||||
label: 'a reply',
|
||||
storeKey: 'thread',
|
||||
storeFactory: fakeThread,
|
||||
assertStoreEmpty: (thread) => assert.equal(thread.replies.length, 0),
|
||||
assertBroadcastMsg: (broadcast, value) => assert.equal(decodeReplyText(broadcast.msg), value),
|
||||
byteBased: true,
|
||||
extraArgs: [PARENT_TXID]
|
||||
})
|
||||
|
||||
test('MEMO_REPLY_PREFIX is the Memo reply action 0x6d03', () => {
|
||||
assert.equal(MemoReply.MEMO_REPLY_PREFIX, '6d03')
|
||||
})
|
||||
@@ -81,81 +83,6 @@ test('replying with a valid message broadcasts an OP_RETURN with the Memo reply
|
||||
assert.equal(thread.replies[0].parentTxid, PARENT_TXID)
|
||||
})
|
||||
|
||||
test('replying at the maximum byte length (184) is accepted', async () => {
|
||||
const wallet = fakeWallet()
|
||||
const memoReply = new MemoReply({ wallet })
|
||||
|
||||
const msg = 'x'.repeat(184)
|
||||
const txid = await memoReply.reply(msg, PARENT_TXID)
|
||||
assert.equal(txid, 'fake-txid')
|
||||
assert.equal(decodeReplyText(wallet.broadcasts[0].msg), msg)
|
||||
})
|
||||
|
||||
test('replying with a multi-byte character at the byte limit is accepted', async () => {
|
||||
const wallet = fakeWallet()
|
||||
const memoReply = new MemoReply({ wallet })
|
||||
|
||||
// 92 'é' characters encode to 184 UTF-8 bytes.
|
||||
const msg = 'é'.repeat(92)
|
||||
assert.equal(Buffer.byteLength(msg, 'utf8'), 184)
|
||||
const txid = await memoReply.reply(msg, PARENT_TXID)
|
||||
assert.equal(txid, 'fake-txid')
|
||||
})
|
||||
|
||||
test('replying with an empty message throws a validation error and broadcasts nothing', async () => {
|
||||
const wallet = fakeWallet()
|
||||
const thread = fakeThread()
|
||||
const memoReply = new MemoReply({ wallet, thread })
|
||||
|
||||
await assert.rejects(
|
||||
memoReply.reply('', PARENT_TXID),
|
||||
(err) => err.code === 'reply_validation'
|
||||
)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
assert.equal(thread.replies.length, 0)
|
||||
})
|
||||
|
||||
test('replying with a whitespace-only or non-string message throws a validation error and broadcasts nothing', async () => {
|
||||
for (const invalid of [' ', 42]) {
|
||||
const wallet = fakeWallet()
|
||||
const memoReply = new MemoReply({ wallet })
|
||||
|
||||
await assert.rejects(
|
||||
memoReply.reply(invalid, PARENT_TXID),
|
||||
(err) => err.code === 'reply_validation'
|
||||
)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
}
|
||||
})
|
||||
|
||||
test('replying with an over-long message (185 bytes) throws a length error and broadcasts nothing', async () => {
|
||||
const wallet = fakeWallet()
|
||||
const thread = fakeThread()
|
||||
const memoReply = new MemoReply({ wallet, thread })
|
||||
|
||||
await assert.rejects(
|
||||
memoReply.reply('y'.repeat(185), PARENT_TXID),
|
||||
(err) => err.code === 'reply_length'
|
||||
)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
assert.equal(thread.replies.length, 0)
|
||||
})
|
||||
|
||||
test('replying with a multi-byte character that exceeds the byte limit throws a length error', async () => {
|
||||
const wallet = fakeWallet()
|
||||
const memoReply = new MemoReply({ wallet })
|
||||
|
||||
// 93 'é' characters encode to 186 UTF-8 bytes, exceeding the 184-byte limit.
|
||||
const msg = 'é'.repeat(93)
|
||||
assert.ok(Buffer.byteLength(msg, 'utf8') > 184)
|
||||
|
||||
await assert.rejects(
|
||||
memoReply.reply(msg, PARENT_TXID),
|
||||
(err) => err.code === 'reply_length'
|
||||
)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
})
|
||||
|
||||
test('replying without a wallet reports a missing-wallet error', async () => {
|
||||
const memoReply = new MemoReply({})
|
||||
await assert.rejects(
|
||||
|
||||
@@ -28,7 +28,8 @@ registerMemoActionTests({
|
||||
label: 'setting a name',
|
||||
storeKey: 'profiles',
|
||||
storeFactory: fakeProfiles,
|
||||
assertStoreEmpty: (profiles, wallet) => assert.equal(profiles.getName(wallet.walletInfo.cashAddress), null)
|
||||
assertStoreEmpty: (profiles, wallet) => assert.equal(profiles.getName(wallet.walletInfo.cashAddress), null),
|
||||
byteBased: true
|
||||
})
|
||||
test('MEMO_SET_NAME_PREFIX is the Memo set-name action 0x6d01', () => {
|
||||
assert.equal(MemoSetName.MEMO_SET_NAME_PREFIX, '6d01')
|
||||
@@ -51,31 +52,6 @@ test('setting a valid name broadcasts an OP_RETURN with the Memo set-name prefix
|
||||
assert.equal(profiles.getName(wallet.walletInfo.cashAddress), 'trout')
|
||||
})
|
||||
|
||||
test('setting a name at the maximum byte length with multi-byte characters is accepted', async () => {
|
||||
const wallet = fakeWallet()
|
||||
const memoSetName = new MemoSetName({ wallet })
|
||||
|
||||
// 38 'é' characters are 76 bytes in UTF-8.
|
||||
const name = 'é'.repeat(38)
|
||||
assert.equal(Buffer.byteLength(name, 'utf8'), 76)
|
||||
const txid = await memoSetName.setName(name)
|
||||
assert.equal(txid, 'fake-txid')
|
||||
})
|
||||
|
||||
test('setting an over-long name in bytes (78) throws a length error even when char count is lower', async () => {
|
||||
const wallet = fakeWallet()
|
||||
const memoSetName = new MemoSetName({ wallet })
|
||||
|
||||
// 40 'é' characters are 80 bytes, exceeding the 77-byte limit.
|
||||
const name = 'é'.repeat(40)
|
||||
assert.ok(Buffer.byteLength(name, 'utf8') > 77)
|
||||
await assert.rejects(
|
||||
memoSetName.setName(name),
|
||||
(err) => err.code === 'name_length'
|
||||
)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
})
|
||||
|
||||
test('setting an empty name throws a validation error and broadcasts nothing', async () => {
|
||||
const wallet = fakeWallet()
|
||||
const profiles = fakeProfiles()
|
||||
|
||||
+14
-49
@@ -18,7 +18,7 @@ const assert = require('node:assert/strict')
|
||||
const MemoPost = require('../../src/services/memo-post')
|
||||
const NewPostPage = require('../../src/services/new-post')
|
||||
const { fakeWallet } = require('../helpers/fake-wallet')
|
||||
const { registerPageControllerTests } = require('./page-controller-helpers')
|
||||
const { registerPageControllerTests, registerPageSubmitTests } = require('./page-controller-helpers')
|
||||
const { buildPage } = require('./page-build-helpers')
|
||||
|
||||
const MAX = MemoPost.MAX_MEMO_CHARS // 217
|
||||
@@ -70,54 +70,19 @@ test('the character counter reaches zero at the memo limit', () => {
|
||||
assert.equal(page.remainingCount(), 0)
|
||||
})
|
||||
|
||||
test('posting a valid memo broadcasts the Memo post prefix and navigates to the feed', async () => {
|
||||
const { wallet, store, 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)
|
||||
// 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(store.posts.length, 1)
|
||||
assert.equal(store.posts[0].text, 'hello memo')
|
||||
})
|
||||
|
||||
test('posting an empty memo is rejected with a validation error and nothing is broadcast', async () => {
|
||||
const { wallet, store, 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)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
assert.equal(store.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, store, 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)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
assert.equal(store.posts.length, 0)
|
||||
assert.deepEqual(navigations, [])
|
||||
registerPageSubmitTests({
|
||||
buildPage: build,
|
||||
verb: 'posting',
|
||||
label: 'memo',
|
||||
busyFlag: 'posting',
|
||||
prefix: '6d02',
|
||||
validationCode: 'memo_validation',
|
||||
lengthCode: 'memo_length',
|
||||
MAX,
|
||||
successPath: '/posts/recent',
|
||||
assertBroadcastMsg: (broadcast) => assert.equal(broadcast.msg, 'hello memo'),
|
||||
assertStore: (store) => assert.equal(store.posts[0].text, 'hello memo'),
|
||||
assertStoreEmpty: (store) => assert.equal(store.posts.length, 0)
|
||||
})
|
||||
|
||||
test('the new post page starts idle (not posting)', () => {
|
||||
|
||||
@@ -11,14 +11,16 @@ const { fakeWallet } = require('../helpers/fake-wallet')
|
||||
// actionKey - the page's action dependency key ('memoPost' or 'memoSetName')
|
||||
// storeKey - the action's store dependency key ('feed' or 'profiles')
|
||||
// storeFactory - () => a fresh store
|
||||
function buildPage ({ Page, Action, actionKey, storeKey, storeFactory }) {
|
||||
// pageDeps - extra dependencies passed to the page constructor
|
||||
function buildPage ({ Page, Action, actionKey, storeKey, storeFactory, pageDeps = {} }) {
|
||||
const wallet = fakeWallet()
|
||||
const store = storeFactory()
|
||||
const action = new Action({ wallet, [storeKey]: store })
|
||||
const navigations = []
|
||||
const page = new Page({
|
||||
[actionKey]: action,
|
||||
navigate: (path) => navigations.push(path)
|
||||
navigate: (path) => navigations.push(path),
|
||||
...pageDeps
|
||||
})
|
||||
return { wallet, store, action, page, navigations }
|
||||
}
|
||||
|
||||
@@ -63,4 +63,69 @@ function registerPageControllerTests (cfg) {
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { registerPageControllerTests }
|
||||
// Register the page-submit tests shared by the New Post and Reply Thread pages.
|
||||
// Both pages extend PageController and submit a single action, so the valid,
|
||||
// empty, and over-long submit behaviors are identical; only the page-specific
|
||||
// pieces differ. `cfg` supplies:
|
||||
// buildPage - () => ({ wallet, store, page, navigations })
|
||||
// verb - the action verb for test names ('posting' or 'submitting')
|
||||
// label - the noun for test names ('memo' or 'reply')
|
||||
// busyFlag - the page's in-flight flag name ('posting' or 'replying')
|
||||
// prefix - the broadcast prefix ('6d02' or '6d03')
|
||||
// validationCode - the validation error code
|
||||
// lengthCode - the length error code
|
||||
// MAX - the length limit
|
||||
// successPath - the navigation path on success, or null for no navigation
|
||||
// assertBroadcastMsg - (broadcast) => asserts the broadcast message (optional)
|
||||
// assertStore - (store, wallet) => asserts the store reflects the value
|
||||
// assertStoreEmpty - (store, wallet) => asserts the store was not updated
|
||||
function registerPageSubmitTests (cfg) {
|
||||
const { buildPage, verb, label, busyFlag, prefix, validationCode, lengthCode, MAX, successPath, assertBroadcastMsg, assertStore, assertStoreEmpty } = cfg
|
||||
|
||||
test(`${verb} a valid ${label} broadcasts the ${prefix} prefix and reflects it`, async () => {
|
||||
const { wallet, store, page, navigations } = buildPage()
|
||||
page.setInput('hello memo')
|
||||
|
||||
const result = await page.submit()
|
||||
|
||||
assert.equal(result.ok, true)
|
||||
assert.equal(page[busyFlag], false)
|
||||
assert.equal(wallet.broadcasts.length, 1)
|
||||
assert.equal(wallet.broadcasts[0].prefix, prefix)
|
||||
if (assertBroadcastMsg) assertBroadcastMsg(wallet.broadcasts[0])
|
||||
assert.deepEqual(navigations, successPath ? [successPath] : [])
|
||||
assertStore(store, wallet)
|
||||
})
|
||||
|
||||
test(`${verb} an empty ${label} is rejected with a validation error and nothing is broadcast`, async () => {
|
||||
const { wallet, store, page, navigations } = buildPage()
|
||||
page.setInput('')
|
||||
|
||||
const result = await page.submit()
|
||||
|
||||
assert.equal(result.ok, false)
|
||||
assert.equal(result.error, validationCode)
|
||||
assert.equal(page.submitError, validationCode)
|
||||
assert.equal(page[busyFlag], false)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
assertStoreEmpty(store, wallet)
|
||||
assert.deepEqual(navigations, [])
|
||||
})
|
||||
|
||||
test(`${verb} an over-long ${label} is rejected with a length error and nothing is broadcast`, async () => {
|
||||
const { wallet, store, page, navigations } = buildPage()
|
||||
page.setInput('y'.repeat(MAX + 1))
|
||||
|
||||
const result = await page.submit()
|
||||
|
||||
assert.equal(result.ok, false)
|
||||
assert.equal(result.error, lengthCode)
|
||||
assert.equal(page.submitError, lengthCode)
|
||||
assert.equal(page[busyFlag], false)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
assertStoreEmpty(store, wallet)
|
||||
assert.deepEqual(navigations, [])
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { registerPageControllerTests, registerPageSubmitTests }
|
||||
|
||||
@@ -16,42 +16,30 @@ const assert = require('node:assert/strict')
|
||||
|
||||
const MemoReply = require('../../src/services/memo-reply')
|
||||
const ReplyThreadPage = require('../../src/services/reply-thread-page')
|
||||
const { registerPageControllerTests, registerPageSubmitTests } = require('./page-controller-helpers')
|
||||
const { buildPage } = require('./page-build-helpers')
|
||||
|
||||
const MAX = MemoReply.MAX_REPLY_BYTES // 184
|
||||
const PARENT_TXID = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
|
||||
|
||||
function fakeWallet (cashAddress = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d') {
|
||||
const broadcasts = []
|
||||
const wallet = {
|
||||
walletInfo: { cashAddress },
|
||||
utxos: [{ txid: 'utxo-fee' }],
|
||||
getUtxos: async function () { return this.utxos },
|
||||
sendOpReturn: async function (msg, prefix) {
|
||||
this.broadcasts.push({ msg, prefix })
|
||||
if (this.failWith) throw new Error(this.failWith)
|
||||
return 'reply-txid'
|
||||
}
|
||||
}
|
||||
wallet.broadcasts = broadcasts
|
||||
return wallet
|
||||
}
|
||||
|
||||
function fakeThread (rootTxid = PARENT_TXID) {
|
||||
const replies = []
|
||||
return { rootTxid, replies, addReply: (r) => replies.push(r) }
|
||||
}
|
||||
|
||||
function build (deps = {}) {
|
||||
const wallet = deps.wallet || fakeWallet()
|
||||
const thread = deps.thread || fakeThread()
|
||||
const memoReply = new MemoReply({ wallet, thread })
|
||||
const navigations = []
|
||||
const page = new ReplyThreadPage({
|
||||
memoReply,
|
||||
parentTxid: deps.parentTxid || PARENT_TXID,
|
||||
navigate: (path) => navigations.push(path)
|
||||
function build () {
|
||||
return buildPage({
|
||||
Page: ReplyThreadPage,
|
||||
Action: MemoReply,
|
||||
actionKey: 'memoReply',
|
||||
storeKey: 'thread',
|
||||
storeFactory: fakeThread,
|
||||
pageDeps: { parentTxid: PARENT_TXID }
|
||||
})
|
||||
return { wallet, thread, memoReply, page, navigations }
|
||||
}
|
||||
|
||||
function buildBarePage (navigations) {
|
||||
return new ReplyThreadPage({ navigate: (p) => navigations.push(p) })
|
||||
}
|
||||
|
||||
test('REPLY_THREAD_PATH constant', () => {
|
||||
@@ -82,50 +70,21 @@ test('the byte counter reaches zero at the reply byte limit', () => {
|
||||
assert.equal(page.remainingCount(), 0)
|
||||
})
|
||||
|
||||
test('submitting a valid reply broadcasts the Memo reply prefix and reflects it in the thread', async () => {
|
||||
const { wallet, thread, page, navigations } = build()
|
||||
page.setInput('hello memo')
|
||||
|
||||
const result = await page.submit()
|
||||
|
||||
assert.equal(result.ok, true)
|
||||
assert.equal(page.replying, false)
|
||||
assert.equal(wallet.broadcasts.length, 1)
|
||||
assert.equal(wallet.broadcasts[0].prefix, '6d03')
|
||||
assert.deepEqual(navigations, [])
|
||||
assert.equal(thread.replies.length, 1)
|
||||
assert.equal(thread.replies[0].text, 'hello memo')
|
||||
assert.equal(thread.replies[0].parentTxid, PARENT_TXID)
|
||||
})
|
||||
|
||||
test('submitting an empty reply is rejected with a validation error and nothing is broadcast', async () => {
|
||||
const { wallet, thread, page, navigations } = build()
|
||||
page.setInput('')
|
||||
|
||||
const result = await page.submit()
|
||||
|
||||
assert.equal(result.ok, false)
|
||||
assert.equal(result.error, 'reply_validation')
|
||||
assert.equal(page.submitError, 'reply_validation')
|
||||
assert.equal(page.replying, false)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
assert.equal(thread.replies.length, 0)
|
||||
assert.deepEqual(navigations, [])
|
||||
})
|
||||
|
||||
test('submitting an over-long reply is rejected with a length error and nothing is broadcast', async () => {
|
||||
const { wallet, thread, page, navigations } = build()
|
||||
page.setInput('y'.repeat(MAX + 1))
|
||||
|
||||
const result = await page.submit()
|
||||
|
||||
assert.equal(result.ok, false)
|
||||
assert.equal(result.error, 'reply_length')
|
||||
assert.equal(page.submitError, 'reply_length')
|
||||
assert.equal(page.replying, false)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
assert.equal(thread.replies.length, 0)
|
||||
assert.deepEqual(navigations, [])
|
||||
registerPageSubmitTests({
|
||||
buildPage: build,
|
||||
verb: 'submitting',
|
||||
label: 'reply',
|
||||
busyFlag: 'replying',
|
||||
prefix: '6d03',
|
||||
validationCode: 'reply_validation',
|
||||
lengthCode: 'reply_length',
|
||||
MAX,
|
||||
successPath: null,
|
||||
assertStore: (store) => {
|
||||
assert.equal(store.replies[0].text, 'hello memo')
|
||||
assert.equal(store.replies[0].parentTxid, PARENT_TXID)
|
||||
},
|
||||
assertStoreEmpty: (store) => assert.equal(store.replies.length, 0)
|
||||
})
|
||||
|
||||
test('the reply page starts idle (not replying)', () => {
|
||||
@@ -133,72 +92,22 @@ test('the reply page starts idle (not replying)', () => {
|
||||
assert.equal(page.replying, false)
|
||||
})
|
||||
|
||||
test('replying is true while a submit is in flight and false once it settles', async () => {
|
||||
const wallet = fakeWallet()
|
||||
const thread = fakeThread()
|
||||
|
||||
let resolveSend
|
||||
wallet.sendOpReturn = async () => new Promise((resolve) => { resolveSend = resolve })
|
||||
const page = new ReplyThreadPage({
|
||||
memoReply: new MemoReply({ wallet, thread }),
|
||||
parentTxid: PARENT_TXID,
|
||||
navigate: () => {}
|
||||
})
|
||||
page.setInput('hello memo')
|
||||
|
||||
assert.equal(page.replying, false)
|
||||
const pending = page.submit()
|
||||
assert.equal(page.replying, true)
|
||||
|
||||
await new Promise((resolve) => setImmediate(resolve))
|
||||
assert.equal(typeof resolveSend, 'function')
|
||||
resolveSend('in-flight-txid')
|
||||
await pending
|
||||
assert.equal(page.replying, false)
|
||||
})
|
||||
|
||||
test('submitting without a memo reply handler reports an error and does not navigate', async () => {
|
||||
const navigations = []
|
||||
const page = new ReplyThreadPage({ 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 thread = fakeThread()
|
||||
wallet.failWith = 'BCH UTXO list is empty'
|
||||
const navigations = []
|
||||
const page = new ReplyThreadPage({
|
||||
memoReply: new MemoReply({ wallet, thread }),
|
||||
parentTxid: PARENT_TXID,
|
||||
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/)
|
||||
assert.equal(wallet.broadcasts.length, 1)
|
||||
assert.equal(wallet.broadcasts[0].prefix, '6d03')
|
||||
assert.deepEqual(navigations, [])
|
||||
registerPageControllerTests({
|
||||
buildPage: build,
|
||||
buildBarePage,
|
||||
busyFlag: 'replying',
|
||||
prefix: '6d03'
|
||||
})
|
||||
|
||||
test('replying to a nested reply uses the selected parent txid', async () => {
|
||||
const nestedTxid = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
|
||||
const { thread, page } = build()
|
||||
const { store, page } = build()
|
||||
page.setParent(nestedTxid)
|
||||
page.setInput('hello nested')
|
||||
|
||||
const result = await page.submit()
|
||||
|
||||
assert.equal(result.ok, true)
|
||||
assert.equal(thread.replies[0].parentTxid, nestedTxid)
|
||||
assert.equal(thread.replies[0].text, 'hello nested')
|
||||
assert.equal(store.replies[0].parentTxid, nestedTxid)
|
||||
assert.equal(store.replies[0].text, 'hello nested')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user