diff --git a/psf-memo-client/acceptance/lib/handlers.js b/psf-memo-client/acceptance/lib/handlers.js
index 57c4dce..6e60f3b 100644
--- a/psf-memo-client/acceptance/lib/handlers.js
+++ b/psf-memo-client/acceptance/lib/handlers.js
@@ -1686,36 +1686,21 @@ const handlers = [
name: 'like/tip modal shows a broadcast success message',
pattern: /^the like\/tip modal shows a broadcast success message$/,
run (m, example, world) {
- const { message, html } = renderLikeBroadcastResult(world)
- if (!html.includes(message)) {
- throw new Error(`The rendered like result does not show the message "${message}".`)
- }
+ assertBroadcastResultMessage(world, 'like')
}
},
{
name: 'like/tip modal shows the like transaction id',
pattern: /^the like\/tip modal shows the like transaction id$/,
run (m, example, world) {
- const { txid, html } = renderLikeBroadcastResult(world)
- if (!html.includes(txid)) {
- throw new Error(`The rendered like result does not show the transaction id ${txid}.`)
- }
+ assertBroadcastResultTxid(world, 'like')
}
},
{
name: 'like/tip modal shows a block explorer link',
pattern: /^the like\/tip modal shows a link to the block explorer for the like transaction$/,
run (m, example, world) {
- const { url, html } = renderLikeBroadcastResult(world)
- if (!url.startsWith('https://bch.loping.net/tx/')) {
- throw new Error(`Expected a bch.loping.net explorer link, got "${url}".`)
- }
- if (!html.includes(`href="${url}"`)) {
- throw new Error(`The rendered like result does not link to ${url}.`)
- }
- if (!html.includes('target="_blank"')) {
- throw new Error('The rendered like explorer link does not open in a new tab.')
- }
+ assertBroadcastResultExplorerLink(world, 'like')
}
},
{
@@ -1977,38 +1962,14 @@ const handlers = [
name: 'broadcasts OP_RETURN with Memo follow prefix for address',
pattern: /^the app broadcasts an OP_RETURN transaction with the Memo follow prefix for the address (.+)$/,
run (m, example, world) {
- const addr = resolveParam(m[1], example)
- const hash160 = world.wallet.bchjs.Address.toHash160(addr)
- const broadcasts = world.wallet.broadcasts
- if (!broadcasts.length) {
- throw new Error('No OP_RETURN transaction was broadcast.')
- }
- const last = broadcasts[broadcasts.length - 1]
- if (last.prefix !== MEMO_FOLLOW_PREFIX) {
- throw new Error(`Expected Memo follow prefix ${MEMO_FOLLOW_PREFIX}, got "${last.prefix}".`)
- }
- if (last.msg.toString('hex') !== hash160) {
- throw new Error(`Broadcast follow hash160 did not match ${addr}.`)
- }
+ assertMemoBroadcastPrefix(world, resolveParam(m[1], example), MEMO_FOLLOW_PREFIX, 'follow')
}
},
{
name: 'broadcasts OP_RETURN with Memo unfollow prefix for address',
pattern: /^the app broadcasts an OP_RETURN transaction with the Memo unfollow prefix for the address (.+)$/,
run (m, example, world) {
- const addr = resolveParam(m[1], example)
- const hash160 = world.wallet.bchjs.Address.toHash160(addr)
- const broadcasts = world.wallet.broadcasts
- if (!broadcasts.length) {
- throw new Error('No OP_RETURN transaction was broadcast.')
- }
- const last = broadcasts[broadcasts.length - 1]
- if (last.prefix !== MEMO_UNFOLLOW_PREFIX) {
- throw new Error(`Expected Memo unfollow prefix ${MEMO_UNFOLLOW_PREFIX}, got "${last.prefix}".`)
- }
- if (last.msg.toString('hex') !== hash160) {
- throw new Error(`Broadcast unfollow hash160 did not match ${addr}.`)
- }
+ assertMemoBroadcastPrefix(world, resolveParam(m[1], example), MEMO_UNFOLLOW_PREFIX, 'unfollow')
}
},
{
@@ -2089,38 +2050,14 @@ const handlers = [
name: 'broadcasts or attempts Memo mute prefix for address',
pattern: /^the app (?:broadcasts|attempts to broadcast) an OP_RETURN transaction with the Memo mute prefix for the address (.+)$/,
run (m, example, world) {
- const addr = resolveParam(m[1], example)
- const hash160 = world.wallet.bchjs.Address.toHash160(addr)
- const broadcasts = world.wallet.broadcasts
- if (!broadcasts.length) {
- throw new Error('No OP_RETURN transaction was broadcast.')
- }
- const last = broadcasts[broadcasts.length - 1]
- if (last.prefix !== MEMO_MUTE_PREFIX) {
- throw new Error(`Expected Memo mute prefix ${MEMO_MUTE_PREFIX}, got "${last.prefix}".`)
- }
- if (last.msg.toString('hex') !== hash160) {
- throw new Error(`Broadcast mute hash160 did not match ${addr}.`)
- }
+ assertMemoBroadcastPrefix(world, resolveParam(m[1], example), MEMO_MUTE_PREFIX, 'mute')
}
},
{
name: 'broadcasts OP_RETURN with Memo unmute prefix for address',
pattern: /^the app broadcasts an OP_RETURN transaction with the Memo unmute prefix for the address (.+)$/,
run (m, example, world) {
- const addr = resolveParam(m[1], example)
- const hash160 = world.wallet.bchjs.Address.toHash160(addr)
- const broadcasts = world.wallet.broadcasts
- if (!broadcasts.length) {
- throw new Error('No OP_RETURN transaction was broadcast.')
- }
- const last = broadcasts[broadcasts.length - 1]
- if (last.prefix !== MEMO_UNMUTE_PREFIX) {
- throw new Error(`Expected Memo unmute prefix ${MEMO_UNMUTE_PREFIX}, got "${last.prefix}".`)
- }
- if (last.msg.toString('hex') !== hash160) {
- throw new Error(`Broadcast unmute hash160 did not match ${addr}.`)
- }
+ assertMemoBroadcastPrefix(world, resolveParam(m[1], example), MEMO_UNMUTE_PREFIX, 'unmute')
}
},
{
@@ -2139,36 +2076,21 @@ const handlers = [
name: 'mute result modal shows a broadcast success message',
pattern: /^the mute result modal shows a broadcast success message$/,
run (m, example, world) {
- const { message, html } = renderMuteBroadcastResult(world)
- if (!html.includes(message)) {
- throw new Error(`The rendered mute result does not show the message "${message}".`)
- }
+ assertBroadcastResultMessage(world, 'mute')
}
},
{
name: 'mute result modal shows the mute transaction id',
pattern: /^the mute result modal shows the mute transaction id$/,
run (m, example, world) {
- const { txid, html } = renderMuteBroadcastResult(world)
- if (!html.includes(txid)) {
- throw new Error(`The rendered mute result does not show the transaction id ${txid}.`)
- }
+ assertBroadcastResultTxid(world, 'mute')
}
},
{
name: 'mute result modal shows a block explorer link',
pattern: /^the mute result modal shows a link to the block explorer for the mute transaction$/,
run (m, example, world) {
- const { url, html } = renderMuteBroadcastResult(world)
- if (!url.startsWith(`${ProfilePage.EXPLORER_TX_BASE}/`)) {
- throw new Error(`Expected a bch.loping.net explorer link, got "${url}".`)
- }
- if (!html.includes(`href="${url}"`)) {
- throw new Error(`The rendered mute result does not link to ${url}.`)
- }
- if (!html.includes('target="_blank"')) {
- throw new Error('The rendered mute explorer link does not open in a new tab.')
- }
+ assertBroadcastResultExplorerLink(world, 'mute')
}
},
{
@@ -4027,44 +3949,93 @@ function togglePostOptionsMenu (world, txid) {
world.activeMenuTxid = txid
}
-// Require a successful like broadcast result and render it to static HTML for
-// the like/tip acceptance assertions. The caller inspects the returned fields.
-function renderLikeBroadcastResult (world) {
- const page = world.likeTipPage
- if (!page.showResultModal || !page.lastResult || !page.lastResult.ok) {
- throw new Error('Expected a successful like broadcast result.')
+// Broadcast result plumbing shared by the like/tip and mute acceptance
+// assertions. Each feature names the world field holding its page, the page
+// fields/getters for the visible result, and the component renderer.
+const BROADCAST_RESULTS = {
+ like: {
+ page: (world) => world.likeTipPage,
+ showField: 'showResultModal',
+ resultField: 'lastResult',
+ getMessage: (page) => page.getBroadcastMessage(),
+ render: renderLikeResult
+ },
+ mute: {
+ page: (world) => world.profilePage,
+ showField: 'showMuteResultModal',
+ resultField: 'lastMuteResult',
+ getMessage: (page) => page.getMuteBroadcastMessage(),
+ render: renderMuteResult
}
- const txid = page.lastResult.txid
+}
+
+// Require a successful broadcast result for `feature` and render it to static
+// HTML for the acceptance assertions. The caller inspects the returned fields.
+function renderBroadcastResult (world, feature) {
+ const spec = BROADCAST_RESULTS[feature]
+ const page = spec.page(world)
+ if (!page || !page[spec.showField] || !page[spec.resultField] || !page[spec.resultField].ok) {
+ throw new Error(`Expected a successful ${feature} broadcast result.`)
+ }
+ const txid = page[spec.resultField].txid
if (!txid) {
- throw new Error('Expected the like result to include a transaction id.')
+ throw new Error(`Expected the ${feature} result to include a transaction id.`)
}
- const message = page.getBroadcastMessage()
+ const message = spec.getMessage(page)
if (!message) {
- throw new Error('Expected a like broadcast success message.')
+ throw new Error(`Expected a ${feature} broadcast success message.`)
}
const url = page.explorerUrl(txid)
- const html = renderLikeResult({ txid, message, explorerUrl: url })
+ const html = spec.render({ txid, message, explorerUrl: url })
return { txid, message, url, html }
}
-// Require a successful mute broadcast result and render it to static HTML for
-// the mute acceptance assertions. The caller inspects the returned fields.
-function renderMuteBroadcastResult (world) {
- const page = world.profilePage
- if (!page || !page.showMuteResultModal || !page.lastMuteResult || !page.lastMuteResult.ok) {
- throw new Error('Expected a successful mute broadcast result.')
+// Assert the rendered broadcast result shows its success message.
+function assertBroadcastResultMessage (world, feature) {
+ const { message, html } = renderBroadcastResult(world, feature)
+ if (!html.includes(message)) {
+ throw new Error(`The rendered ${feature} result does not show the message "${message}".`)
}
- const txid = page.lastMuteResult.txid
- if (!txid) {
- throw new Error('Expected the mute result to include a transaction id.')
+}
+
+// Assert the rendered broadcast result shows its transaction id.
+function assertBroadcastResultTxid (world, feature) {
+ const { txid, html } = renderBroadcastResult(world, feature)
+ if (!html.includes(txid)) {
+ throw new Error(`The rendered ${feature} result does not show the transaction id ${txid}.`)
}
- const message = page.getMuteBroadcastMessage()
- if (!message) {
- throw new Error('Expected a mute broadcast success message.')
+}
+
+// Assert the rendered broadcast result links to the block explorer in a new
+// tab.
+function assertBroadcastResultExplorerLink (world, feature) {
+ const { url, html } = renderBroadcastResult(world, feature)
+ if (!url.startsWith(`${ProfilePage.EXPLORER_TX_BASE}/`)) {
+ throw new Error(`Expected a bch.loping.net explorer link, got "${url}".`)
+ }
+ if (!html.includes(`href="${url}"`)) {
+ throw new Error(`The rendered ${feature} result does not link to ${url}.`)
+ }
+ if (!html.includes('target="_blank"')) {
+ throw new Error(`The rendered ${feature} explorer link does not open in a new tab.`)
+ }
+}
+
+// Assert that the wallet's most recent broadcast carries the Memo `label`
+// prefix and the 20-byte hash160 payload for `addr`.
+function assertMemoBroadcastPrefix (world, addr, prefix, label) {
+ const hash160 = world.wallet.bchjs.Address.toHash160(addr)
+ const broadcasts = world.wallet.broadcasts
+ if (!broadcasts.length) {
+ throw new Error('No OP_RETURN transaction was broadcast.')
+ }
+ const last = broadcasts[broadcasts.length - 1]
+ if (last.prefix !== prefix) {
+ throw new Error(`Expected Memo ${label} prefix ${prefix}, got "${last.prefix}".`)
+ }
+ if (last.msg.toString('hex') !== hash160) {
+ throw new Error(`Broadcast ${label} hash160 did not match ${addr}.`)
}
- const url = page.explorerUrl(txid)
- const html = renderMuteResult({ txid, message, explorerUrl: url })
- return { txid, message, url, html }
}
// The posts currently rendered by the page the scenario has opened.
diff --git a/psf-memo-client/src/components/app-body/profile/mute-result.js b/psf-memo-client/src/components/app-body/profile/mute-result.js
index 51fbd19..f0fe1f9 100644
--- a/psf-memo-client/src/components/app-body/profile/mute-result.js
+++ b/psf-memo-client/src/components/app-body/profile/mute-result.js
@@ -12,6 +12,7 @@
*/
const React = require('react')
+const ExplorerTxLink = require('../../explorer-tx-link')
function MuteResult ({ txid = '', message = '', error = '', explorerUrl = '' }) {
return React.createElement(
@@ -23,23 +24,11 @@ function MuteResult ({ txid = '', message = '', error = '', explorerUrl = '' })
React.Fragment,
null,
React.createElement('p', { className: 'mute-result-message' }, message),
- txid
- ? React.createElement(
- 'p',
- { className: 'mute-result-txid mb-0' },
- 'Transaction ID: ',
- React.createElement(
- 'a',
- {
- href: explorerUrl,
- target: '_blank',
- rel: 'noopener noreferrer',
- style: { wordBreak: 'break-all' }
- },
- txid
- )
- )
- : null
+ React.createElement(ExplorerTxLink, {
+ txid,
+ explorerUrl,
+ className: 'mute-result-txid'
+ })
)
)
}
diff --git a/psf-memo-client/src/components/explorer-tx-link.js b/psf-memo-client/src/components/explorer-tx-link.js
new file mode 100644
index 0000000..b2f38b9
--- /dev/null
+++ b/psf-memo-client/src/components/explorer-tx-link.js
@@ -0,0 +1,34 @@
+/*
+ Explorer link for a broadcast transaction.
+
+ Shown by the like/tip and mute broadcast result components: "Transaction ID:"
+ followed by the txid linked to its block explorer page, opening in a new tab.
+ The `className` is supplied by the caller so each result keeps its own
+ styling.
+
+ Written in plain React.createElement style so the same module can be used by
+ the JSX components in the browser build and by the Node acceptance adapters.
+*/
+
+const React = require('react')
+
+function ExplorerTxLink ({ txid = '', explorerUrl = '', className = '' }) {
+ if (!txid) return null
+ return React.createElement(
+ 'p',
+ { className: `${className} mb-0`.trim() },
+ 'Transaction ID: ',
+ React.createElement(
+ 'a',
+ {
+ href: explorerUrl,
+ target: '_blank',
+ rel: 'noopener noreferrer',
+ style: { wordBreak: 'break-all' }
+ },
+ txid
+ )
+ )
+}
+
+module.exports = ExplorerTxLink
diff --git a/psf-memo-client/src/components/post-feed/like-result.js b/psf-memo-client/src/components/post-feed/like-result.js
index 1f598fe..acb19f3 100644
--- a/psf-memo-client/src/components/post-feed/like-result.js
+++ b/psf-memo-client/src/components/post-feed/like-result.js
@@ -11,29 +11,18 @@
*/
const React = require('react')
+const ExplorerTxLink = require('../explorer-tx-link')
function LikeResult ({ txid = '', message = '', explorerUrl = '' }) {
return React.createElement(
'div',
{ className: 'like-result' },
React.createElement('p', { className: 'like-result-message' }, message),
- txid
- ? React.createElement(
- 'p',
- { className: 'like-result-txid mb-0' },
- 'Transaction ID: ',
- React.createElement(
- 'a',
- {
- href: explorerUrl,
- target: '_blank',
- rel: 'noopener noreferrer',
- style: { wordBreak: 'break-all' }
- },
- txid
- )
- )
- : null
+ React.createElement(ExplorerTxLink, {
+ txid,
+ explorerUrl,
+ className: 'like-result-txid'
+ })
)
}
diff --git a/psf-memo-client/test/property/mute-result.property.test.js b/psf-memo-client/test/property/mute-result.property.test.js
new file mode 100644
index 0000000..632832f
--- /dev/null
+++ b/psf-memo-client/test/property/mute-result.property.test.js
@@ -0,0 +1,241 @@
+/*
+ Property tests for the mute broadcast result.
+
+ The unit tests probe the mute result at a few fixed fixtures. These
+ properties pin down the invariants over broad random inputs:
+
+ - explorerUrl composes the shared block explorer base with the txid and
+ returns '' for every falsy input.
+ - The MuteResult component always shows the message, shows the txid and an
+ explorer link that opens in a new tab exactly when a txid is present,
+ shows only the error when an error is present, and is deterministic.
+ - The profile page mute result state machine mirrors the last broadcast: a
+ successful mute/unmute opens the modal with the matching success message,
+ a failure opens the failure modal with the error and leaves the button
+ state unchanged, a new broadcast replaces the previous result, and
+ dismissing closes the modal without changing the button.
+*/
+
+'use strict'
+
+const test = require('node:test')
+const React = require('react')
+const ReactDOMServer = require('react-dom/server')
+const { seededRandom, forAll, intGen } = require('./harness')
+const ProfilePage = require('../../src/services/profile-page')
+const MuteResult = require('../../src/components/app-body/profile/mute-result')
+
+const rng = seededRandom(20260918)
+
+const MY_ADDRESS = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d'
+const MUTE_ADDRESS = 'bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy'
+const MUTE_TXID = 'ab'.repeat(32)
+
+const HEX = '0123456789abcdef'
+const SAFE_WORDS = ['mute', 'unmute', 'broadcast', 'success', 'memo', 'network']
+const FALSY = [undefined, null, '', 0, false]
+
+function randomTxid () {
+ const n = intGen(rng, 1, 64)()
+ let out = ''
+ for (let i = 0; i < n; i++) out += HEX[Math.floor(rng() * HEX.length)]
+ return out
+}
+
+function randomMessage () {
+ const n = intGen(rng, 1, 6)()
+ let out = ''
+ for (let i = 0; i < n; i++) {
+ out += `${SAFE_WORDS[intGen(rng, 0, SAFE_WORDS.length - 1)()]} `
+ }
+ return out.trim()
+}
+
+function makeMemoDb () {
+ return {
+ async getPostsByAddr () {
+ return { posts: [], pagination: { total: 0 } }
+ },
+ async getFollowState () {
+ return false
+ },
+ async getMuteState () {
+ return false
+ }
+ }
+}
+
+// A profile page whose memo mute handler either succeeds with MUTE_TXID or
+// fails with the given message.
+function makePage ({ fail = null, txid = MUTE_TXID } = {}) {
+ const page = new ProfilePage({ memoDb: makeMemoDb(), addr: MUTE_ADDRESS, myAddr: MY_ADDRESS })
+ page.memoMute = {
+ async mute () {
+ if (fail) throw new Error(fail)
+ return txid
+ },
+ async unmute () {
+ if (fail) throw new Error(fail)
+ return txid
+ }
+ }
+ return page
+}
+
+function render (props) {
+ return ReactDOMServer.renderToStaticMarkup(
+ React.createElement(MuteResult, props)
+ )
+}
+
+test('explorerUrl composes the shared base and the txid', async () => {
+ await forAll(
+ () => randomTxid(),
+ async (txid) =>
+ ProfilePage.explorerUrl(txid) === `${ProfilePage.EXPLORER_TX_BASE}/${txid}`,
+ { label: 'mute explorer url composition', samples: 2000 }
+ )
+})
+
+test('explorerUrl returns an empty string for every falsy input', async () => {
+ await forAll(
+ () => FALSY[intGen(rng, 0, FALSY.length - 1)()],
+ async (value) => ProfilePage.explorerUrl(value) === '',
+ { label: 'mute explorer url falsy inputs', samples: 500 }
+ )
+})
+
+test('MuteResult shows the message and links only with a txid', async () => {
+ await forAll(
+ () => ({ txid: rng() < 0.2 ? '' : randomTxid(), message: randomMessage() }),
+ async ({ txid, message }) => {
+ const url = ProfilePage.explorerUrl(txid)
+ const html = render({ txid, message, explorerUrl: url })
+ if (!html.includes(message)) return false
+ if (txid) {
+ if (!html.includes(txid)) return false
+ if (!html.includes(`href="${url}"`)) return false
+ if (!html.includes('target="_blank"')) return false
+ } else if (html.includes(' {
+ await forAll(
+ () => ({ txid: randomTxid(), message: randomMessage(), error: randomMessage() }),
+ async ({ txid, message, error }) => {
+ const html = render({ txid, message, error, explorerUrl: ProfilePage.explorerUrl(txid) })
+ return html.includes('mute-result-error') &&
+ html.includes(error) &&
+ !html.includes('mute-result-message') &&
+ !html.includes(' {
+ await forAll(
+ () => ({ txid: randomTxid(), message: randomMessage() }),
+ async (props) => {
+ const first = render(props)
+ const second = render(props)
+ return first === second
+ },
+ { label: 'mute result render determinism', samples: 500 }
+ )
+})
+
+test('the mute result modal mirrors the last broadcast outcome', async () => {
+ await forAll(
+ () => ({ method: rng() < 0.5 ? 'mute' : 'unmute', fail: rng() < 0.4 }),
+ async ({ method, fail }) => {
+ const page = makePage({ fail: fail ? 'Insufficient balance' : null })
+ const result = await page[method]()
+
+ if (!page.showMuteResultModal) return false
+ if (result.ok) {
+ const expected = method === 'unmute' ? ProfilePage.UNMUTE_SUCCESS_MESSAGE : ProfilePage.MUTE_SUCCESS_MESSAGE
+ return page.getMuteBroadcastMessage() === expected && page.getMuteResultError() === ''
+ }
+ return page.getMuteBroadcastMessage() === '' && page.getMuteResultError() === 'Insufficient balance'
+ },
+ { label: 'mute result modal outcome', samples: 800 }
+ )
+})
+
+test('a failed mute broadcast leaves the button state unchanged', async () => {
+ await forAll(
+ () => ({ method: rng() < 0.5 ? 'mute' : 'unmute', initial: rng() < 0.5 }),
+ async ({ method, initial }) => {
+ const page = makePage({ fail: 'Insufficient balance' })
+ page.muteState = initial
+ const result = await page[method]()
+ return result.ok === false && page.isMuting() === initial
+ },
+ { label: 'mute failure leaves button state', samples: 500 }
+ )
+})
+
+test('a successful mute then unmute round-trips the reflected mute state', async () => {
+ await forAll(
+ () => rng() < 0.5,
+ async (startMuted) => {
+ const page = makePage()
+ page.muteState = startMuted
+ const muteResult = await page.mute()
+ const afterMute = page.isMuting()
+ const unmuteResult = await page.unmute()
+ const afterUnmute = page.isMuting()
+ return muteResult.ok && afterMute === true && unmuteResult.ok && afterUnmute === false
+ },
+ { label: 'mute reflected state round trip', samples: 400 }
+ )
+})
+
+test('a new broadcast replaces the previous result', async () => {
+ await forAll(
+ () => rng() < 0.5,
+ async (firstFails) => {
+ const page = makePage({ fail: firstFails ? 'Insufficient balance' : null })
+ await page.mute()
+ const before = { ok: page.lastMuteResult.ok, error: page.getMuteResultError() }
+ // Force the opposite outcome on the next broadcast.
+ page.memoMute = {
+ async mute () {
+ if (!firstFails) throw new Error('Insufficient balance')
+ return MUTE_TXID
+ },
+ async unmute () {
+ return MUTE_TXID
+ }
+ }
+ await page.mute()
+ return before.ok === !firstFails &&
+ page.lastMuteResult.ok === firstFails &&
+ page.getMuteResultError() === (firstFails ? '' : 'Insufficient balance')
+ },
+ { label: 'mute result replace', samples: 400 }
+ )
+})
+
+test('dismissing the mute result closes the modal without changing the button', async () => {
+ await forAll(
+ () => ({ method: rng() < 0.5 ? 'mute' : 'unmute', fail: rng() < 0.5 }),
+ async ({ method, fail }) => {
+ const page = makePage({ fail: fail ? 'Insufficient balance' : null })
+ const result = await page[method]()
+ const stateBeforeDismiss = page.muteState
+ page.dismissMuteResult()
+ return result.ok === !fail &&
+ page.showMuteResultModal === false &&
+ page.muteState === stateBeforeDismiss
+ },
+ { label: 'mute result dismiss', samples: 500 }
+ )
+})