Harden like result modal: kill mutation survivors and DRY test setup

Add unit coverage for the LikeTipPage initial state, deps seeding, the
dust-limit boundary, and empty/null tip parsing, and assert exact
validation/broadcast error messages. Add the NewPostPage initial
result-modal state assertion. Refresh the mutate4javascript footer
manifests for the touched services and the soft Gherkin
acceptance-mutation manifest for the like-result feature.

By architect.
This commit is contained in:
Chris Troutner
2026-09-16 12:29:53 -07:00
parent e978516a0f
commit e071881ea0
8 changed files with 69 additions and 21 deletions
@@ -1,3 +1,7 @@
# acceptance-mutation-manifest-begin
# {"version":1,"tested_at":"2026-09-16T19:27:51.931950466Z","feature_name":"Like Broadcast Result","feature_path":"/home/trout/work/psf-memo/.worktrees/architect/psf-memo-client/specs/like-broadcast-result.feature","background_hash":"1714896143ae0425b1c9938c1bc926ef61183ee3e842aa76d8bfd0ec751fcc9c","implementation_hash":"unknown","scenarios":[{"index":0,"name":"Like Broadcast Result - 1 a successful like keeps the modal open and shows the broadcast result","scenario_hash":"315b01b6a9c59504c9ecf91ec0229a957c7490387f69f246b33f899dd18a896f","mutation_count":2,"result":{"Total":2,"Killed":2,"Survived":0,"Errors":0},"tested_at":"2026-09-16T19:27:51.931950466Z"}]}
# acceptance-mutation-manifest-end
# Scenarios: Like Broadcast Result - 1, Like Broadcast Result - 2, Like Broadcast Result - 3
#
# After a like is broadcast from the like/tip modal, the modal no longer closes
@@ -38,3 +38,7 @@ function LikeResult ({ txid = '', message = '', explorerUrl = '' }) {
}
module.exports = LikeResult
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-09-16T19:24:39.237Z","module_hash":"53beb37c8b7aaa5c0291ff89be8444acd53240d70817b691a8eeb7def5d072f2","functions":[{"id":"func/LikeResult","name":"LikeResult","line":15,"end_line":38,"hash":"a9094a5a6f71e1142eab902750c33dded00411ae9627158589831aa3567c96a8"}]}
// mutate4javascript-manifest-end
@@ -18,3 +18,7 @@ module.exports = {
BLOCK_EXPLORER_TX_BASE,
blockExplorerTxUrl
}
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-09-16T19:07:27.216Z","module_hash":"941a53501bc90a2ae36e1c0dc3890fd24add133e651c868a1a8f4f6a31035cb7","functions":[{"id":"func/blockExplorerTxUrl","name":"blockExplorerTxUrl","line":12,"end_line":15,"hash":"a8d84886234e25a946eb3363e23583b621851a2778990aa57df7d32fb8ed949a"}]}
// mutate4javascript-manifest-end
@@ -149,3 +149,7 @@ module.exports = LikeTipPage
LikeTipPage.EXPLORER_TX_BASE = BLOCK_EXPLORER_TX_BASE
LikeTipPage.SUCCESS_MESSAGE = SUCCESS_MESSAGE
LikeTipPage.explorerUrl = blockExplorerTxUrl
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-09-16T19:21:51.249Z","module_hash":"df3f5a3cd43fba8d3b7267561b21c84b68b964a958b6b77f0fa4b12f3afffc8d","functions":[{"id":"func/LikeTipPage.constructor","name":"LikeTipPage.constructor","line":22,"end_line":35,"hash":"020fe0af9dd6885fb83ad2edc0e94ef5d28661eec2a138fa5576743014341124"},{"id":"func/LikeTipPage.open","name":"LikeTipPage.open","line":39,"end_line":66,"hash":"0bfb69e7e8f6d9fc8ec2c923c20578215e8765088c514f8398461e64108aaaff"},{"id":"func/LikeTipPage.close","name":"LikeTipPage.close","line":69,"end_line":74,"hash":"673843dc179c1a325e224d6ad235f945d14b7efbceefb26f1b1f69dccfa4e535"},{"id":"func/LikeTipPage.setTip","name":"LikeTipPage.setTip","line":78,"end_line":80,"hash":"cfa38ba8b9f799839393de98e83d5512699327d99dbcf3de11d322be3a027323"},{"id":"func/LikeTipPage._setBusy","name":"LikeTipPage._setBusy","line":83,"end_line":85,"hash":"8fe4b97a14cdbd3849bd6bf493738a2caf50f36002de1c96603290b98c83e511"},{"id":"func/LikeTipPage.submit","name":"LikeTipPage.submit","line":90,"end_line":100,"hash":"d7a255241f316400a9c1f30b057ae7c63e27421d76432c8e02ecc88121319982"},{"id":"func/LikeTipPage.getBroadcastMessage","name":"LikeTipPage.getBroadcastMessage","line":103,"end_line":106,"hash":"800d20ba3a50dcffcee063bc5cd6422b548713520530b3d159cf1980310000b2"},{"id":"func/LikeTipPage.explorerUrl","name":"LikeTipPage.explorerUrl","line":109,"end_line":111,"hash":"7822bf8055c5fc45eaa46919b8665c8954def38f92bed94b091796a6a9b5381d"},{"id":"func/LikeTipPage.dismissResult","name":"LikeTipPage.dismissResult","line":114,"end_line":117,"hash":"96d1421749b8e3a3c295ad071d4bd921d17edd7c5e8824b1b3c3196b009cfcab"},{"id":"func/LikeTipPage._parseTip","name":"LikeTipPage._parseTip","line":120,"end_line":128,"hash":"7db17db65fe02da8db7d4658aa2fc38d52f61754c6e3a92cca5db6ebcf8eda5a"},{"id":"func/LikeTipPage._perform","name":"LikeTipPage._perform","line":131,"end_line":137,"hash":"f6af26bbf3965455c3436dd1da28bf22e27b65d28d4e02a9fe423a909efca030"},{"id":"func/LikeTipPage._handleSubmitFailure","name":"LikeTipPage._handleSubmitFailure","line":141,"end_line":144,"hash":"a66731eb7050a3c87ff7d228de3661facb9c971e7611d7953f86ba1b88bb1110"}]}
// mutate4javascript-manifest-end
+1 -1
View File
@@ -101,5 +101,5 @@ NewPostPage.explorerUrl = blockExplorerTxUrl
module.exports = NewPostPage
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-09-13T13:36:17.184Z","module_hash":"65edafa07e390d316197347119a4d2a50c3fe73980c8e526f38fa0f66ee69689","functions":[{"id":"func/NewPostPage.constructor","name":"NewPostPage.constructor","line":24,"end_line":37,"hash":"1a42004b33ac28f396df02f70b2cec6a91f890d3a065aefe890c23cb1456ace3"},{"id":"func/NewPostPage.addMenuLink","name":"NewPostPage.addMenuLink","line":40,"end_line":43,"hash":"2bb99421f2cdbcf9a39f77244b250e4251b9bd3cc81a4f399589007f71486b79"},{"id":"func/NewPostPage.hasMenuLink","name":"NewPostPage.hasMenuLink","line":46,"end_line":48,"hash":"f2e167f20bd3040ebbcfaf3a3e95254333b9739e0fa93d7801ead542be2a15e3"},{"id":"func/NewPostPage.remainingCount","name":"NewPostPage.remainingCount","line":51,"end_line":53,"hash":"421d0af0e01278b479a5bb8d62d344498f875d188e8307eda420d637c23600d4"},{"id":"func/NewPostPage._setBusy","name":"NewPostPage._setBusy","line":56,"end_line":58,"hash":"1552af7690ee7a92b2a9ed84af1ba301c2468939b6994ca3942fe0b7004028fb"},{"id":"func/NewPostPage._perform","name":"NewPostPage._perform","line":61,"end_line":66,"hash":"c8b7394bcd5a2252278095e9af08cc5e76720b4fab08aa83218cb93fdb372407"},{"id":"func/NewPostPage.explorerUrl","name":"NewPostPage.explorerUrl","line":69,"end_line":71,"hash":"84a048fa78b38cef32573f1e31be11f870c765393213e59744d9799690da4d37"},{"id":"func/NewPostPage.submit","name":"NewPostPage.submit","line":75,"end_line":84,"hash":"1f15969b62987f29fbc310845716fe65c9ace8bec2f7bb2d8bee62e59b7cee95"},{"id":"func/NewPostPage.dismissResult","name":"NewPostPage.dismissResult","line":87,"end_line":93,"hash":"c155b35aedd68c7bbde87329ec086079621cf362ecf01472b65c7e442c9646b7"}]}
// {"version":1,"tested_at":"2026-09-16T19:19:23.763Z","module_hash":"a285c6a2a27006b4e90960ebfe9968651e6cfd7a4f12e600fca76d521eda2751","functions":[{"id":"func/NewPostPage.constructor","name":"NewPostPage.constructor","line":24,"end_line":37,"hash":"ea4edf077e9c3631366acadc62f27b8cf4d5087cc6d2e317db9086b1d0d18347"},{"id":"func/NewPostPage.addMenuLink","name":"NewPostPage.addMenuLink","line":40,"end_line":43,"hash":"bac97164d2d70bfdb946c4d54e67983c43cad093bac558f1d169e1739ca97137"},{"id":"func/NewPostPage.hasMenuLink","name":"NewPostPage.hasMenuLink","line":46,"end_line":48,"hash":"7e1abf5d0833aaf3b3da3a024de9eb2b80da900b2930e832c7e92d77e0e50344"},{"id":"func/NewPostPage.remainingCount","name":"NewPostPage.remainingCount","line":51,"end_line":53,"hash":"521ce4ed841f62099529b327f2245a9e94c09af2f67ed5d91839e52607bcea37"},{"id":"func/NewPostPage._setBusy","name":"NewPostPage._setBusy","line":56,"end_line":58,"hash":"af4c51d75a9bbfc4d8c2566414ee950704d83831ad915ee04eea8bf2fab65a3e"},{"id":"func/NewPostPage._perform","name":"NewPostPage._perform","line":61,"end_line":66,"hash":"e66f893a4913b5d6f3cc4fcc1ad58e21557b4a316f0150050c961e3ece237795"},{"id":"func/NewPostPage.explorerUrl","name":"NewPostPage.explorerUrl","line":69,"end_line":71,"hash":"947125deea2b19ae2dff9d9bd9449a23f10b6bf6075a9c9be4bc588cd211ff56"},{"id":"func/NewPostPage.submit","name":"NewPostPage.submit","line":75,"end_line":84,"hash":"56282a4dea0927a6ab8bd3e6452f1860b978bcf4598d17cdcf86f6083b561b17"},{"id":"func/NewPostPage.dismissResult","name":"NewPostPage.dismissResult","line":87,"end_line":93,"hash":"35cae53c6e851cd96e5b81dfe5a2b954f458bdacd60d8228676779c025820c73"}]}
// mutate4javascript-manifest-end
+1 -1
View File
@@ -108,5 +108,5 @@ module.exports = {
}
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-09-16T16:53:14.617Z","module_hash":"9564c40bbc416ef70c9b53f4dcdd849182f85734b9e48f90521b400921d3c6b3","functions":[{"id":"func/explorerTxUrl","name":"explorerTxUrl","line":18,"end_line":21,"hash":"b0284558de13bf4b63d70227a688add4739e577a8908593f3ced4ff557be6a16"},{"id":"func/postOptionsItems","name":"postOptionsItems","line":24,"end_line":34,"hash":"271f7b7971ae5d011c7014444b67d4e3f7cd25b85474f9a556e8bf8a8acb2b35"},{"id":"func/initialPostOptionsState","name":"initialPostOptionsState","line":37,"end_line":39,"hash":"1b6ab74976ac9fb54520f85e9fab7e61109572783da055866dcf64e36fc857f9"},{"id":"func/openPostOptions","name":"openPostOptions","line":42,"end_line":44,"hash":"4e91430330da69f5829a931fe6df67d2c6b7aa791f8092e1152e791c4935c384"},{"id":"func/closePostOptions","name":"closePostOptions","line":47,"end_line":49,"hash":"fe56a32123f3f5241891e22b551b0abba708bf182b2e04a9c1301d15e12fa466"},{"id":"func/togglePostOptions","name":"togglePostOptions","line":52,"end_line":54,"hash":"3bcb1a0d78907b5e83920c5f5e0d3d4fd55e0d62691d3c71bfc84bd1b9d8adac"},{"id":"func/focusFirstPostOption","name":"focusFirstPostOption","line":57,"end_line":60,"hash":"c3f174e7232a704c72bc79d84c4e5ef81ad1f32012a9c215d83c5a4eb7bd3291"},{"id":"func/handlePostOptionsEscape","name":"handlePostOptionsEscape","line":63,"end_line":65,"hash":"028896c476d907d1ca26361c5958c5996ac33a5455fa90ed8a7d48eaed9d2301"},{"id":"func/handlePostOptionsOutsideClick","name":"handlePostOptionsOutsideClick","line":68,"end_line":70,"hash":"66e71edaea9e5a93f14c2cb24026a338c02be5451f1ecdb00145705cc41e3ed0"},{"id":"func/isOutsidePostOptions","name":"isOutsidePostOptions","line":76,"end_line":78,"hash":"a35084f2e010d5e4726c25cac48b78c7e10fe331caf2ca3cabc663607cbf1962"},{"id":"func/postOptionsKeyCommand","name":"postOptionsKeyCommand","line":84,"end_line":97,"hash":"625fb7f69df3baf377fcd9a0b0d3d2bd129b31090e51591552f4eaa056fc120e"}]}
// {"version":1,"tested_at":"2026-09-16T19:14:16.172Z","module_hash":"01043ede064dd77b6d74651fdbd61874be779a060c600d48c400d28194fc0348","functions":[{"id":"func/postOptionsItems","name":"postOptionsItems","line":19,"end_line":29,"hash":"a50c66c4fcc7255a2142df15b8714a7831c184b3fb58d09ce6c6c4795a031b5b"},{"id":"func/initialPostOptionsState","name":"initialPostOptionsState","line":32,"end_line":34,"hash":"1b6ab74976ac9fb54520f85e9fab7e61109572783da055866dcf64e36fc857f9"},{"id":"func/openPostOptions","name":"openPostOptions","line":37,"end_line":39,"hash":"4e91430330da69f5829a931fe6df67d2c6b7aa791f8092e1152e791c4935c384"},{"id":"func/closePostOptions","name":"closePostOptions","line":42,"end_line":44,"hash":"fe56a32123f3f5241891e22b551b0abba708bf182b2e04a9c1301d15e12fa466"},{"id":"func/togglePostOptions","name":"togglePostOptions","line":47,"end_line":49,"hash":"3bcb1a0d78907b5e83920c5f5e0d3d4fd55e0d62691d3c71bfc84bd1b9d8adac"},{"id":"func/focusFirstPostOption","name":"focusFirstPostOption","line":52,"end_line":55,"hash":"c3f174e7232a704c72bc79d84c4e5ef81ad1f32012a9c215d83c5a4eb7bd3291"},{"id":"func/handlePostOptionsEscape","name":"handlePostOptionsEscape","line":58,"end_line":60,"hash":"028896c476d907d1ca26361c5958c5996ac33a5455fa90ed8a7d48eaed9d2301"},{"id":"func/handlePostOptionsOutsideClick","name":"handlePostOptionsOutsideClick","line":63,"end_line":65,"hash":"66e71edaea9e5a93f14c2cb24026a338c02be5451f1ecdb00145705cc41e3ed0"},{"id":"func/isOutsidePostOptions","name":"isOutsidePostOptions","line":71,"end_line":73,"hash":"a35084f2e010d5e4726c25cac48b78c7e10fe331caf2ca3cabc663607cbf1962"},{"id":"func/postOptionsKeyCommand","name":"postOptionsKeyCommand","line":79,"end_line":92,"hash":"625fb7f69df3baf377fcd9a0b0d3d2bd129b31090e51591552f4eaa056fc120e"}]}
// mutate4javascript-manifest-end
+48 -18
View File
@@ -37,16 +37,40 @@ function makeWallet () {
function makePage (opts = {}) {
const wallet = makeWallet()
if (opts.balance !== undefined) {
wallet.utxos = [{ txid: 'utxo', value: opts.balance }]
}
if (opts.failWith) wallet.failWith = opts.failWith
const memoLike = new MemoLike({ wallet })
return { wallet, page: new LikeTipPage({ memoLike }) }
}
// Open the modal, submit a like with no tip, and return the page and result.
async function submitLike (opts = {}) {
const { wallet, page } = makePage(opts)
page.open(SAMPLE_TXID, AUTHOR_ADDRESS)
page.setTip('')
const result = await page.submit()
return { wallet, page, result }
}
test('the result modal starts hidden', () => {
const { page } = makePage()
assert.equal(page.showResultModal, false)
assert.equal(page.lastResult, null)
assert.equal(page.modalOpen, false)
assert.equal(page.tipping, false)
})
test('the controller seeds the post txid and author from deps', () => {
const page = new LikeTipPage({
postTxid: SAMPLE_TXID,
authorAddress: AUTHOR_ADDRESS
})
assert.equal(page.postTxid, SAMPLE_TXID)
assert.equal(page.authorAddress, AUTHOR_ADDRESS)
})
test('explorerUrl builds a bch.loping.net transaction link', () => {
@@ -66,11 +90,7 @@ test('SUCCESS_MESSAGE announces the broadcast', () => {
})
test('a successful like keeps the modal open and shows the result', async () => {
const { page } = makePage()
page.open(SAMPLE_TXID, AUTHOR_ADDRESS)
page.setTip('')
const result = await page.submit()
const { page, result } = await submitLike()
assert.equal(result.ok, true)
assert.equal(result.txid, LIKE_TXID)
@@ -81,10 +101,7 @@ test('a successful like keeps the modal open and shows the result', async () =>
})
test('dismissing the result closes the like/tip modal', async () => {
const { page } = makePage()
page.open(SAMPLE_TXID, AUTHOR_ADDRESS)
page.setTip('')
await page.submit()
const { page } = await submitLike()
page.dismissResult()
@@ -93,10 +110,7 @@ test('dismissing the result closes the like/tip modal', async () => {
})
test('opening the modal clears any previous result', async () => {
const { page } = makePage()
page.open(SAMPLE_TXID, AUTHOR_ADDRESS)
page.setTip('')
await page.submit()
const { page } = await submitLike()
page.open(SAMPLE_TXID, AUTHOR_ADDRESS)
@@ -113,6 +127,7 @@ test('a validation failure stays on the form and opens no result', async () => {
assert.equal(result.ok, false)
assert.equal(result.error, 'like_validation')
assert.equal(page.broadcastError, 'Tip must be a valid number of satoshis.')
assert.equal(page.modalOpen, true)
assert.equal(page.showResultModal, false)
assert.equal(page.lastResult.ok, false)
@@ -127,7 +142,7 @@ test('a broadcast failure stays on the form and opens no result', async () => {
assert.equal(result.ok, false)
assert.equal(result.error, 'broadcast')
assert.match(page.broadcastError, /Insufficient balance/)
assert.equal(page.broadcastError, 'Insufficient balance')
assert.equal(page.modalOpen, true)
assert.equal(page.showResultModal, false)
assert.equal(page.getBroadcastMessage(), '')
@@ -146,10 +161,7 @@ test('open reports a validation error without a memo like handler', () => {
})
test('open reports an empty-balance error below the dust limit', () => {
const wallet = makeWallet()
wallet.utxos = [{ txid: 'utxo', value: 100 }]
const memoLike = new MemoLike({ wallet })
const page = new LikeTipPage({ memoLike })
const { page } = makePage({ balance: 100 })
const result = page.open(SAMPLE_TXID, AUTHOR_ADDRESS)
@@ -159,6 +171,16 @@ test('open reports an empty-balance error below the dust limit', () => {
assert.equal(page.showResultModal, false)
})
test('open accepts a balance exactly at the dust limit', () => {
const { page } = makePage({ balance: 3000 })
const result = page.open(SAMPLE_TXID, AUTHOR_ADDRESS)
assert.equal(result.ok, true)
assert.equal(page.submitError, null)
assert.equal(page.broadcastError, null)
})
test('submit fails without a memo like handler', async () => {
const page = new LikeTipPage({})
page.setTip('')
@@ -184,3 +206,11 @@ test('a like with a positive tip sends the tip to the author', async () => {
{ address: AUTHOR_ADDRESS, amountSat: 600 }
])
})
test('an empty, null, or undefined tip parses as zero', () => {
const { page } = makePage()
assert.equal(page._parseTip(''), 0)
assert.equal(page._parseTip(null), 0)
assert.equal(page._parseTip(undefined), 0)
})
@@ -38,10 +38,12 @@ function makeMemoPost (opts = {}) {
return { wallet, feed, memoPost: new MemoPost({ wallet, feed }) }
}
test('the in-flight flag starts false', () => {
test('the in-flight and result-modal flags start false', () => {
const page = new NewPostPage({ navigate: () => {} })
assert.equal(page.posting, false)
assert.equal(page.showResultModal, false)
assert.equal(page.lastResult, null)
})
test('NEW_POST_PATH and RECENT_FEED_PATH constants', () => {