From de24180c4da73537eafac4d5c9dbf36f4e979bb8 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Wed, 26 Aug 2026 04:40:45 -0700 Subject: [PATCH] Extract optimistic reply shaping into testable module Move the optimistic reply object construction out of the ReplyThreadForm React component into a testable service module with unit tests, keeping the component a thin UI adapter. Behavior is preserved; all unit, property, and acceptance tests pass. By refactorer. --- .../post-thread-modal/reply-thread-form.js | 9 ++- src/services/optimistic-reply.js | 20 ++++++ test/unit/optimistic-reply.test.js | 66 +++++++++++++++++++ 3 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 src/services/optimistic-reply.js create mode 100644 test/unit/optimistic-reply.test.js diff --git a/src/components/post-thread-modal/reply-thread-form.js b/src/components/post-thread-modal/reply-thread-form.js index 2052567..1a16f13 100644 --- a/src/components/post-thread-modal/reply-thread-form.js +++ b/src/components/post-thread-modal/reply-thread-form.js @@ -15,6 +15,7 @@ import { Form, Button } from 'react-bootstrap' import MemoReply from '../../services/memo-reply' import ReplyThreadPage from '../../services/reply-thread-page' import { byteLength } from '../../services/utf8' +import { buildOptimisticReply } from '../../services/optimistic-reply' function ReplyThreadForm ({ parentTxid, rootPost, wallet, profiles, onOptimisticReply }) { const maxBytes = MemoReply.MAX_REPLY_BYTES @@ -42,16 +43,14 @@ function ReplyThreadForm ({ parentTxid, rootPost, wallet, profiles, onOptimistic if (typeof onOptimisticReply === 'function') { const cashAddress = wallet?.walletInfo?.cashAddress const displayName = profiles?.[cashAddress]?.name || null - onOptimisticReply({ + onOptimisticReply(buildOptimisticReply({ txid: result.txid, addr: cashAddress, text: input, seen: Date.now(), blockHeight: rootPost?.blockHeight, - replyCount: 0, - replies: [], - profile: displayName ? { name: displayName } : undefined - }) + displayName + })) } } else { if (result.error === 'reply_length') { diff --git a/src/services/optimistic-reply.js b/src/services/optimistic-reply.js new file mode 100644 index 0000000..fd5f6ae --- /dev/null +++ b/src/services/optimistic-reply.js @@ -0,0 +1,20 @@ +'use strict' + +// Build the optimistic reply object used by the reply form for immediate +// thread rendering before the thread is refreshed from the network. This is a +// pure data-shaping function so the reply object shape is unit-testable and +// the React form stays a thin adapter. +function buildOptimisticReply ({ txid, addr, text, seen, blockHeight, displayName }) { + return { + txid, + addr, + text, + seen, + blockHeight, + replyCount: 0, + replies: [], + profile: displayName ? { name: displayName } : undefined + } +} + +module.exports = { buildOptimisticReply } diff --git a/test/unit/optimistic-reply.test.js b/test/unit/optimistic-reply.test.js new file mode 100644 index 0000000..811acd2 --- /dev/null +++ b/test/unit/optimistic-reply.test.js @@ -0,0 +1,66 @@ +/* + Unit tests for the optimistic reply object builder + (src/services/optimistic-reply.js). + + The reply form renders an optimistic reply immediately after a successful + broadcast, before the thread is refreshed from the network. This module + shapes that reply object so the shape is covered by unit tests and the + React form stays a thin adapter. +*/ + +'use strict' + +const test = require('node:test') +const assert = require('node:assert/strict') + +const { buildOptimisticReply } = require('../../src/services/optimistic-reply') + +const TXID = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' +const ADDR = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d' + +test('builds a reply with a zero reply count and no child replies', () => { + const reply = buildOptimisticReply({ + txid: TXID, + addr: ADDR, + text: 'hello', + seen: 1234, + blockHeight: 800000, + displayName: null + }) + + assert.equal(reply.txid, TXID) + assert.equal(reply.addr, ADDR) + assert.equal(reply.text, 'hello') + assert.equal(reply.seen, 1234) + assert.equal(reply.blockHeight, 800000) + assert.equal(reply.replyCount, 0) + assert.deepEqual(reply.replies, []) + assert.equal(reply.profile, undefined) +}) + +test('attaches a profile when a display name is present', () => { + const reply = buildOptimisticReply({ + txid: TXID, + addr: ADDR, + text: 'hello', + seen: 1234, + blockHeight: undefined, + displayName: 'Trout' + }) + + assert.deepEqual(reply.profile, { name: 'Trout' }) + assert.equal(reply.blockHeight, undefined) +}) + +test('preserves an absent block height', () => { + const reply = buildOptimisticReply({ + txid: TXID, + addr: ADDR, + text: 'hello', + seen: 1234, + blockHeight: null, + displayName: null + }) + + assert.equal(reply.blockHeight, null) +})