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 unit and property tests
Extract shared test helpers for the Memo post and Set Name behavior slices: fake wallet/profiles, MemoAction tests, page-controller tests, page build wiring, and shared property-test invariants. Behavior is preserved; all unit, property, and acceptance tests pass. By refactorer.
This commit is contained in:
@@ -2,5 +2,6 @@ node_modules/
|
||||
build/
|
||||
docs/
|
||||
tmp/
|
||||
target/
|
||||
|
||||
.gitsigners
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
'use strict'
|
||||
|
||||
// A fake profile store that records names set for addresses.
|
||||
function fakeProfiles () {
|
||||
const names = new Map()
|
||||
return {
|
||||
names,
|
||||
setName: (addr, name) => names.set(addr, name),
|
||||
getName: (addr) => names.get(addr) || null
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { fakeProfiles }
|
||||
@@ -0,0 +1,26 @@
|
||||
'use strict'
|
||||
|
||||
// A fake wallet that records every broadcast attempt and can be made to fail.
|
||||
// It satisfies the small adapter surface the Memo action modules need:
|
||||
// walletInfo, getUtxos(), sendOpReturn(). Set `wallet.failWith` to make
|
||||
// sendOpReturn throw.
|
||||
function fakeWallet ({
|
||||
cashAddress = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d',
|
||||
utxos = [{ txid: 'utxo1' }],
|
||||
txid = 'fake-txid'
|
||||
} = {}) {
|
||||
const broadcasts = []
|
||||
const wallet = {
|
||||
walletInfo: { cashAddress },
|
||||
getUtxos: async () => utxos,
|
||||
sendOpReturn: async function (msg, prefix) {
|
||||
broadcasts.push({ msg, prefix })
|
||||
if (this.failWith) throw new Error(this.failWith)
|
||||
return txid
|
||||
}
|
||||
}
|
||||
wallet.broadcasts = broadcasts
|
||||
return wallet
|
||||
}
|
||||
|
||||
module.exports = { fakeWallet }
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
Shared property tests for the Memo post and Set Name behavior slices.
|
||||
|
||||
Both slices validate a value against a length limit, expose a character/byte
|
||||
counter that conserves its relationship to input length, round-trip the draft
|
||||
text through setInput, and surface broadcast failures without navigating.
|
||||
These tests assert those invariants across a broad input range; the slice
|
||||
specifics are supplied through `cfg` so the two behavior slices share one
|
||||
implementation instead of duplicating it.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const test = require('node:test')
|
||||
|
||||
const { forAll, makeStringGen } = require('./harness')
|
||||
const { fakeWallet } = require('../helpers/fake-wallet')
|
||||
|
||||
// Register the property tests shared by a Memo behavior slice. `cfg` supplies
|
||||
// the slice-specific pieces:
|
||||
// Module - the action class under test (MemoPost or MemoSetName)
|
||||
// MAX - the length limit (characters or bytes)
|
||||
// label - a short label used in test names
|
||||
// rng - the seeded random generator
|
||||
// measure - (input) => length in the slice's unit (chars or bytes)
|
||||
// buildPage - () => a fresh page for counter and round-trip tests
|
||||
// buildBroadcastPage - ({ wallet, navigations }) => a page wired to broadcast
|
||||
function registerBehaviorProperties (cfg) {
|
||||
const { Module, MAX, label, rng, measure, buildPage, buildBroadcastPage } = cfg
|
||||
const stringOf = makeStringGen(rng)
|
||||
|
||||
test(`${label} validation: any non-blank string at or below the limit is valid`, async () => {
|
||||
await forAll(
|
||||
(i) => {
|
||||
const len = 1 + Math.floor(rng() * MAX) // 1..MAX
|
||||
return stringOf(len)
|
||||
},
|
||||
(input) => {
|
||||
// A random ASCII string may occasionally be all whitespace; whitespace-only
|
||||
// input is a validation error, so only assert for non-blank strings.
|
||||
if (input.trim().length === 0) return true
|
||||
const result = new Module({}).validate(input)
|
||||
return result.ok === true
|
||||
},
|
||||
{ label: 'valid length' }
|
||||
)
|
||||
})
|
||||
|
||||
test(`${label} validation: any string above the limit is a length error`, async () => {
|
||||
await forAll(
|
||||
(i) => stringOf(MAX + 1 + Math.floor(rng() * 100)), // > MAX
|
||||
(input) => {
|
||||
const result = new Module({}).validate(input)
|
||||
return result.ok === false && result.type === 'length'
|
||||
},
|
||||
{ label: 'over-long rejected as length' }
|
||||
)
|
||||
})
|
||||
|
||||
test(`${label} validation: blank and non-string input are validation errors`, async () => {
|
||||
await forAll(
|
||||
(i) => (i % 2 === 0 ? ' ' : null),
|
||||
(input) => {
|
||||
const result = new Module({}).validate(input)
|
||||
return result.ok === false && result.type === 'validation'
|
||||
},
|
||||
{ label: 'blank/non-string rejected as validation' }
|
||||
)
|
||||
})
|
||||
|
||||
test(`${label} counter conserves length: remaining === MAX - measure(input)`, async () => {
|
||||
await forAll(
|
||||
(i) => stringOf(Math.floor(rng() * (MAX + 8))),
|
||||
(input) => {
|
||||
const page = buildPage()
|
||||
page.setInput(input)
|
||||
return page.remainingCount() === MAX - measure(input)
|
||||
},
|
||||
{ label: 'counter conservation' }
|
||||
)
|
||||
})
|
||||
|
||||
test('setInput round-trips the draft text exactly', async () => {
|
||||
await forAll(
|
||||
(i) => stringOf(Math.floor(rng() * 50)),
|
||||
(input) => {
|
||||
const page = buildPage()
|
||||
page.setInput(input)
|
||||
return page.input === input
|
||||
},
|
||||
{ label: 'setInput round-trip' }
|
||||
)
|
||||
})
|
||||
|
||||
test('a broadcast failure surfaces the error and never navigates', async () => {
|
||||
await forAll(
|
||||
(i) => ({ text: stringOf(1 + Math.floor(rng() * 40)), failWith: `boom-${i % 97}` }),
|
||||
({ text, failWith }) => {
|
||||
const wallet = fakeWallet()
|
||||
wallet.failWith = failWith
|
||||
const navigations = []
|
||||
const page = buildBroadcastPage({ wallet, navigations })
|
||||
page.setInput(text)
|
||||
|
||||
return page.submit().then((result) => {
|
||||
if (result.ok) return false
|
||||
if (page.submitError !== 'broadcast') return false
|
||||
if (!page.broadcastError || !page.broadcastError.includes('boom')) return false
|
||||
return navigations.length === 0
|
||||
})
|
||||
},
|
||||
{ label: 'broadcast failure does not navigate' }
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { registerBehaviorProperties }
|
||||
@@ -2,21 +2,18 @@
|
||||
Property tests for the Memo post / New Post behavior slices.
|
||||
|
||||
These assert useful invariants that unit tests cover only at a few fixed
|
||||
points:
|
||||
- Validation classification is stable across a broad input range: any
|
||||
non-blank string up to the length limit is accepted, any longer string is
|
||||
rejected with a length error, and blank/non-string input is a validation
|
||||
error.
|
||||
- The New Post character counter conserves its relationship to input
|
||||
length: input.length + remainingCount() === MAX_MEMO_CHARS for any string.
|
||||
- setInput round-trips the exact draft text.
|
||||
points. The validation, counter-conservation, setInput round-trip, and
|
||||
broadcast-failure invariants are shared with the Set Name slice and live in
|
||||
behavior-helpers.js; this file supplies the Memo-post specifics and the
|
||||
menu-link idempotence property unique to the New Post page.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const test = require('node:test')
|
||||
|
||||
const { seededRandom, forAll, makeStringGen } = require('./harness')
|
||||
const { seededRandom, forAll } = require('./harness')
|
||||
const { registerBehaviorProperties } = require('./behavior-helpers')
|
||||
|
||||
const MemoPost = require('../../src/services/memo-post')
|
||||
const NewPostPage = require('../../src/services/new-post')
|
||||
@@ -24,7 +21,6 @@ const NewPostPage = require('../../src/services/new-post')
|
||||
const MAX = MemoPost.MAX_MEMO_CHARS // 217
|
||||
|
||||
const rng = seededRandom(20260826)
|
||||
const stringOf = makeStringGen(rng)
|
||||
|
||||
function buildPage () {
|
||||
return new NewPostPage({
|
||||
@@ -34,86 +30,26 @@ function buildPage () {
|
||||
})
|
||||
}
|
||||
|
||||
// A fake wallet recording broadcast attempts; fails when failWith is set.
|
||||
function fakeWallet () {
|
||||
const wallet = {
|
||||
walletInfo: { cashAddress: 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d' },
|
||||
utxos: [{ txid: 'utxo-fee' }],
|
||||
getUtxos: async function () { return this.utxos },
|
||||
sendOpReturn: async function (walletInfo, bchUtxos, msg, prefix) {
|
||||
if (this.failWith) throw new Error(this.failWith)
|
||||
return 'prop-txid'
|
||||
}
|
||||
}
|
||||
return wallet
|
||||
}
|
||||
|
||||
function fakeFeed () {
|
||||
const posts = []
|
||||
return { posts, addPost: (p) => posts.push(p) }
|
||||
}
|
||||
|
||||
test('memo validation: any non-blank string at or below the limit is valid', async () => {
|
||||
await forAll(
|
||||
(i) => {
|
||||
const len = 1 + Math.floor(rng() * MAX) // 1..MAX
|
||||
return stringOf(len)
|
||||
},
|
||||
(msg) => {
|
||||
// A random ASCII string may occasionally be all whitespace; whitespace-only
|
||||
// input is a validation error, so only assert for non-blank strings.
|
||||
if (msg.trim().length === 0) return true
|
||||
const result = new MemoPost({}).validate(msg)
|
||||
return result.ok === true
|
||||
},
|
||||
{ label: 'valid length' }
|
||||
)
|
||||
})
|
||||
function buildBroadcastPage ({ wallet, navigations }) {
|
||||
return new NewPostPage({
|
||||
memoPost: new MemoPost({ wallet, feed: fakeFeed() }),
|
||||
navigate: (p) => navigations.push(p)
|
||||
})
|
||||
}
|
||||
|
||||
test('memo validation: any string above the limit is a length error', async () => {
|
||||
await forAll(
|
||||
(i) => stringOf(MAX + 1 + Math.floor(rng() * 100)), // > MAX
|
||||
(msg) => {
|
||||
const result = new MemoPost({}).validate(msg)
|
||||
return result.ok === false && result.type === 'length'
|
||||
},
|
||||
{ label: 'over-long rejected as length' }
|
||||
)
|
||||
})
|
||||
|
||||
test('memo validation: blank and non-string input are validation errors', async () => {
|
||||
await forAll(
|
||||
(i) => (i % 2 === 0 ? ' ' : null),
|
||||
(msg) => {
|
||||
const result = new MemoPost({}).validate(msg)
|
||||
return result.ok === false && result.type === 'validation'
|
||||
},
|
||||
{ label: 'blank/non-string rejected as validation' }
|
||||
)
|
||||
})
|
||||
|
||||
test('character counter conserves length: remaining === MAX - input.length', async () => {
|
||||
await forAll(
|
||||
(i) => stringOf(Math.floor(rng() * (MAX + 8))),
|
||||
(msg) => {
|
||||
const page = buildPage()
|
||||
page.setInput(msg)
|
||||
return page.remainingCount() === MAX - msg.length
|
||||
},
|
||||
{ label: 'counter conservation' }
|
||||
)
|
||||
})
|
||||
|
||||
test('setInput round-trips the draft text exactly', async () => {
|
||||
await forAll(
|
||||
(i) => stringOf(Math.floor(rng() * 50)),
|
||||
(msg) => {
|
||||
const page = buildPage()
|
||||
page.setInput(msg)
|
||||
return page.input === msg
|
||||
},
|
||||
{ label: 'setInput round-trip' }
|
||||
)
|
||||
registerBehaviorProperties({
|
||||
Module: MemoPost,
|
||||
MAX,
|
||||
label: 'memo',
|
||||
rng,
|
||||
measure: (input) => input.length,
|
||||
buildPage,
|
||||
buildBroadcastPage
|
||||
})
|
||||
|
||||
test('menu link registration is idempotent', async () => {
|
||||
@@ -129,27 +65,3 @@ test('menu link registration is idempotent', async () => {
|
||||
{ label: 'menu link idempotence' }
|
||||
)
|
||||
})
|
||||
|
||||
test('a broadcast failure surfaces the error and never navigates', async () => {
|
||||
await forAll(
|
||||
(i) => ({ message: stringOf(1 + Math.floor(rng() * 40)), failWith: `boom-${i % 97}` }),
|
||||
({ message, failWith }) => {
|
||||
const wallet = fakeWallet()
|
||||
wallet.failWith = failWith
|
||||
const navigations = []
|
||||
const page = new NewPostPage({
|
||||
memoPost: new MemoPost({ wallet, feed: fakeFeed() }),
|
||||
navigate: (p) => navigations.push(p)
|
||||
})
|
||||
page.setInput(message)
|
||||
|
||||
return page.submit().then((result) => {
|
||||
if (result.ok) return false
|
||||
if (page.submitError !== 'broadcast') return false
|
||||
if (!page.broadcastError || !page.broadcastError.includes('boom')) return false
|
||||
return navigations.length === 0
|
||||
})
|
||||
},
|
||||
{ label: 'broadcast failure does not navigate' }
|
||||
)
|
||||
})
|
||||
|
||||
@@ -2,23 +2,16 @@
|
||||
Property tests for the Set Name behavior slice.
|
||||
|
||||
These assert useful invariants that unit tests cover only at a few fixed
|
||||
points:
|
||||
- Set-name validation classification is stable across a broad input range:
|
||||
any non-blank string up to the byte limit is accepted, any longer string
|
||||
is rejected with a length error, and blank/non-string input is a
|
||||
validation error.
|
||||
- The Set Name byte counter conserves its relationship to input byte
|
||||
length: Buffer.byteLength(input) + remainingCount() === MAX_NAME_BYTES
|
||||
for any string.
|
||||
- setInput round-trips the exact draft text.
|
||||
- A broadcast failure surfaces the error and never navigates.
|
||||
points. The validation, counter-conservation, setInput round-trip, and
|
||||
broadcast-failure invariants are shared with the Memo post slice and live in
|
||||
behavior-helpers.js; this file supplies the Set Name specifics.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const test = require('node:test')
|
||||
|
||||
const { seededRandom, forAll, makeStringGen } = require('./harness')
|
||||
const { seededRandom } = require('./harness')
|
||||
const { registerBehaviorProperties } = require('./behavior-helpers')
|
||||
const { fakeProfiles } = require('../helpers/fake-profiles')
|
||||
|
||||
const MemoSetName = require('../../src/services/memo-set-name')
|
||||
const SetNamePage = require('../../src/services/set-name-page')
|
||||
@@ -26,7 +19,6 @@ const SetNamePage = require('../../src/services/set-name-page')
|
||||
const MAX = MemoSetName.MAX_NAME_BYTES // 77
|
||||
|
||||
const rng = seededRandom(20260827)
|
||||
const stringOf = makeStringGen(rng)
|
||||
|
||||
function buildPage () {
|
||||
return new SetNamePage({
|
||||
@@ -35,112 +27,19 @@ function buildPage () {
|
||||
})
|
||||
}
|
||||
|
||||
// A fake wallet recording broadcast attempts; fails when failWith is set.
|
||||
function fakeWallet () {
|
||||
const wallet = {
|
||||
walletInfo: { cashAddress: 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d' },
|
||||
utxos: [{ txid: 'utxo-fee' }],
|
||||
getUtxos: async function () { return this.utxos },
|
||||
sendOpReturn: async function (msg, prefix) {
|
||||
if (this.failWith) throw new Error(this.failWith)
|
||||
return 'prop-txid'
|
||||
}
|
||||
}
|
||||
return wallet
|
||||
function buildBroadcastPage ({ wallet, navigations }) {
|
||||
return new SetNamePage({
|
||||
memoSetName: new MemoSetName({ wallet, profiles: fakeProfiles() }),
|
||||
navigate: (p) => navigations.push(p)
|
||||
})
|
||||
}
|
||||
|
||||
function fakeProfiles () {
|
||||
const names = new Map()
|
||||
return {
|
||||
names,
|
||||
setName: (addr, name) => names.set(addr, name),
|
||||
getName: (addr) => names.get(addr) || null
|
||||
}
|
||||
}
|
||||
|
||||
test('set-name validation: any non-blank string at or below the byte limit is valid', async () => {
|
||||
await forAll(
|
||||
(i) => {
|
||||
const len = 1 + Math.floor(rng() * MAX) // 1..MAX
|
||||
return stringOf(len)
|
||||
},
|
||||
(name) => {
|
||||
// A random ASCII string may occasionally be all whitespace; whitespace-only
|
||||
// input is a validation error, so only assert for non-blank strings.
|
||||
if (name.trim().length === 0) return true
|
||||
const result = new MemoSetName({}).validate(name)
|
||||
return result.ok === true
|
||||
},
|
||||
{ label: 'valid byte length' }
|
||||
)
|
||||
})
|
||||
|
||||
test('set-name validation: any string above the byte limit is a length error', async () => {
|
||||
await forAll(
|
||||
(i) => stringOf(MAX + 1 + Math.floor(rng() * 100)), // > MAX
|
||||
(name) => {
|
||||
const result = new MemoSetName({}).validate(name)
|
||||
return result.ok === false && result.type === 'length'
|
||||
},
|
||||
{ label: 'over-long rejected as length' }
|
||||
)
|
||||
})
|
||||
|
||||
test('set-name validation: blank and non-string input are validation errors', async () => {
|
||||
await forAll(
|
||||
(i) => (i % 2 === 0 ? ' ' : null),
|
||||
(name) => {
|
||||
const result = new MemoSetName({}).validate(name)
|
||||
return result.ok === false && result.type === 'validation'
|
||||
},
|
||||
{ label: 'blank/non-string rejected as validation' }
|
||||
)
|
||||
})
|
||||
|
||||
test('byte counter conserves byte length: remaining === MAX - byteLength(input)', async () => {
|
||||
await forAll(
|
||||
(i) => stringOf(Math.floor(rng() * (MAX + 8))),
|
||||
(name) => {
|
||||
const page = buildPage()
|
||||
page.setInput(name)
|
||||
return page.remainingCount() === MAX - Buffer.byteLength(name, 'utf8')
|
||||
},
|
||||
{ label: 'byte counter conservation' }
|
||||
)
|
||||
})
|
||||
|
||||
test('setInput round-trips the draft text exactly', async () => {
|
||||
await forAll(
|
||||
(i) => stringOf(Math.floor(rng() * 50)),
|
||||
(name) => {
|
||||
const page = buildPage()
|
||||
page.setInput(name)
|
||||
return page.input === name
|
||||
},
|
||||
{ label: 'setInput round-trip' }
|
||||
)
|
||||
})
|
||||
|
||||
test('a broadcast failure surfaces the error and never navigates', async () => {
|
||||
await forAll(
|
||||
(i) => ({ name: stringOf(1 + Math.floor(rng() * 40)), failWith: `boom-${i % 97}` }),
|
||||
({ name, failWith }) => {
|
||||
const wallet = fakeWallet()
|
||||
wallet.failWith = failWith
|
||||
const navigations = []
|
||||
const page = new SetNamePage({
|
||||
memoSetName: new MemoSetName({ wallet, profiles: fakeProfiles() }),
|
||||
navigate: (p) => navigations.push(p)
|
||||
})
|
||||
page.setInput(name)
|
||||
|
||||
return page.submit().then((result) => {
|
||||
if (result.ok) return false
|
||||
if (page.submitError !== 'broadcast') return false
|
||||
if (!page.broadcastError || !page.broadcastError.includes('boom')) return false
|
||||
return navigations.length === 0
|
||||
})
|
||||
},
|
||||
{ label: 'broadcast failure does not navigate' }
|
||||
)
|
||||
registerBehaviorProperties({
|
||||
Module: MemoSetName,
|
||||
MAX,
|
||||
label: 'set-name',
|
||||
rng,
|
||||
measure: (input) => Buffer.byteLength(input, 'utf8'),
|
||||
buildPage,
|
||||
buildBroadcastPage
|
||||
})
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
'use strict'
|
||||
|
||||
const test = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
|
||||
const { fakeWallet } = require('../helpers/fake-wallet')
|
||||
|
||||
// Register the MemoAction tests shared by the Memo post and Set Name slices.
|
||||
// Both slices broadcast a value through a wallet and reflect the result on an
|
||||
// injected store, so the maximum-length and over-length behaviors are
|
||||
// identical; only the slice-specific pieces differ. `cfg` supplies:
|
||||
// Action - the action class (MemoPost or MemoSetName)
|
||||
// method - the broadcast method name ('post' or 'setName')
|
||||
// MAX - the length limit
|
||||
// lengthCode - the length error code
|
||||
// validationCode - the validation error code
|
||||
// label - a short label for test names
|
||||
// storeKey - the action's store dependency key ('feed' or 'profiles')
|
||||
// storeFactory - () => a fresh store
|
||||
// assertStoreEmpty - (store, wallet) => asserts the store was not updated
|
||||
function registerMemoActionTests (cfg) {
|
||||
const { Action, method, MAX, lengthCode, validationCode, label, storeKey, storeFactory, assertStoreEmpty } = cfg
|
||||
|
||||
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)
|
||||
assert.equal(txid, 'fake-txid')
|
||||
assert.equal(wallet.broadcasts[0].msg, value)
|
||||
})
|
||||
|
||||
test(`${label} over the limit (${MAX + 1}) throws a length error and broadcasts nothing`, async () => {
|
||||
const wallet = fakeWallet()
|
||||
const store = storeFactory()
|
||||
const action = new Action({ wallet, [storeKey]: store })
|
||||
|
||||
await assert.rejects(
|
||||
action[method]('y'.repeat(MAX + 1)),
|
||||
(err) => err.code === lengthCode
|
||||
)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
assertStoreEmpty(store, wallet)
|
||||
})
|
||||
|
||||
test(`${label} that is whitespace-only or non-string throws a validation error and broadcasts nothing`, async () => {
|
||||
for (const invalid of [' ', 42]) {
|
||||
const wallet = fakeWallet()
|
||||
const action = new Action({ wallet })
|
||||
|
||||
await assert.rejects(
|
||||
action[method](invalid),
|
||||
(err) => err.code === validationCode
|
||||
)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { registerMemoActionTests }
|
||||
+14
-53
@@ -15,23 +15,8 @@ const test = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
|
||||
const MemoPost = require('../../src/services/memo-post')
|
||||
|
||||
// A fake wallet that records every broadcast attempt. It satisfies the small
|
||||
// adapter surface the MemoPost module needs: walletInfo, getUtxos(),
|
||||
// sendOpReturn().
|
||||
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
|
||||
}
|
||||
const { fakeWallet } = require('../helpers/fake-wallet')
|
||||
const { registerMemoActionTests } = require('./memo-action-helpers')
|
||||
|
||||
// A fake feed that records posts added to the recent posts feed.
|
||||
function fakeFeed () {
|
||||
@@ -39,6 +24,18 @@ function fakeFeed () {
|
||||
return { posts, addPost: (p) => posts.push(p) }
|
||||
}
|
||||
|
||||
registerMemoActionTests({
|
||||
Action: MemoPost,
|
||||
method: 'post',
|
||||
MAX: 217,
|
||||
lengthCode: 'memo_length',
|
||||
validationCode: 'memo_validation',
|
||||
label: 'posting a memo',
|
||||
storeKey: 'feed',
|
||||
storeFactory: fakeFeed,
|
||||
assertStoreEmpty: (feed) => assert.equal(feed.posts.length, 0)
|
||||
})
|
||||
|
||||
test('MEMO_POST_PREFIX is the Memo post action 0x6d02', () => {
|
||||
assert.equal(MemoPost.MEMO_POST_PREFIX, '6d02')
|
||||
})
|
||||
@@ -62,16 +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 a memo at the maximum length (217) is accepted', async () => {
|
||||
const wallet = fakeWallet()
|
||||
const memoPost = new MemoPost({ wallet })
|
||||
|
||||
const msg = 'x'.repeat(217)
|
||||
const txid = await memoPost.post(msg)
|
||||
assert.equal(txid, 'fake-txid')
|
||||
assert.equal(wallet.broadcasts[0].msg, msg)
|
||||
})
|
||||
|
||||
test('posting an empty memo throws a validation error and broadcasts nothing', async () => {
|
||||
const wallet = fakeWallet()
|
||||
const feed = fakeFeed()
|
||||
@@ -85,32 +72,6 @@ test('posting an empty memo throws a validation error and broadcasts nothing', a
|
||||
assert.equal(feed.posts.length, 0)
|
||||
})
|
||||
|
||||
test('posting a whitespace-only or non-string memo throws a validation error and broadcasts nothing', async () => {
|
||||
for (const invalid of [' ', 42]) {
|
||||
const wallet = fakeWallet()
|
||||
const memoPost = new MemoPost({ wallet })
|
||||
|
||||
await assert.rejects(
|
||||
memoPost.post(invalid),
|
||||
(err) => err.code === 'memo_validation'
|
||||
)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
}
|
||||
})
|
||||
|
||||
test('posting an over-long memo (218) throws a length error and broadcasts nothing', async () => {
|
||||
const wallet = fakeWallet()
|
||||
const feed = fakeFeed()
|
||||
const memoPost = new MemoPost({ wallet, feed })
|
||||
|
||||
await assert.rejects(
|
||||
memoPost.post('y'.repeat(218)),
|
||||
(err) => err.code === 'memo_length'
|
||||
)
|
||||
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(
|
||||
|
||||
@@ -15,34 +15,21 @@ const test = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
|
||||
const MemoSetName = require('../../src/services/memo-set-name')
|
||||
const { fakeWallet } = require('../helpers/fake-wallet')
|
||||
const { fakeProfiles } = require('../helpers/fake-profiles')
|
||||
const { registerMemoActionTests } = require('./memo-action-helpers')
|
||||
|
||||
// A fake wallet that records every broadcast attempt. It satisfies the small
|
||||
// adapter surface the MemoSetName module needs: walletInfo, getUtxos(),
|
||||
// sendOpReturn().
|
||||
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 profile store that records names set for addresses.
|
||||
function fakeProfiles () {
|
||||
const names = {}
|
||||
return {
|
||||
names,
|
||||
setName: (addr, name) => { names[addr] = name },
|
||||
getName: (addr) => names[addr] || null
|
||||
}
|
||||
}
|
||||
|
||||
registerMemoActionTests({
|
||||
Action: MemoSetName,
|
||||
method: 'setName',
|
||||
MAX: 77,
|
||||
lengthCode: 'name_length',
|
||||
validationCode: 'name_validation',
|
||||
label: 'setting a name',
|
||||
storeKey: 'profiles',
|
||||
storeFactory: fakeProfiles,
|
||||
assertStoreEmpty: (profiles, wallet) => assert.equal(profiles.getName(wallet.walletInfo.cashAddress), null)
|
||||
})
|
||||
test('MEMO_SET_NAME_PREFIX is the Memo set-name action 0x6d01', () => {
|
||||
assert.equal(MemoSetName.MEMO_SET_NAME_PREFIX, '6d01')
|
||||
})
|
||||
@@ -64,16 +51,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 (77) is accepted', async () => {
|
||||
const wallet = fakeWallet()
|
||||
const memoSetName = new MemoSetName({ wallet })
|
||||
|
||||
const name = 'x'.repeat(77)
|
||||
const txid = await memoSetName.setName(name)
|
||||
assert.equal(txid, 'fake-txid')
|
||||
assert.equal(wallet.broadcasts[0].msg, name)
|
||||
})
|
||||
|
||||
test('setting a name at the maximum byte length with multi-byte characters is accepted', async () => {
|
||||
const wallet = fakeWallet()
|
||||
const memoSetName = new MemoSetName({ wallet })
|
||||
@@ -112,32 +89,6 @@ test('setting an empty name throws a validation error and broadcasts nothing', a
|
||||
assert.equal(profiles.getName(wallet.walletInfo.cashAddress), null)
|
||||
})
|
||||
|
||||
test('setting a whitespace-only or non-string name throws a validation error and broadcasts nothing', async () => {
|
||||
for (const invalid of [' ', 42]) {
|
||||
const wallet = fakeWallet()
|
||||
const memoSetName = new MemoSetName({ wallet })
|
||||
|
||||
await assert.rejects(
|
||||
memoSetName.setName(invalid),
|
||||
(err) => err.code === 'name_validation'
|
||||
)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
}
|
||||
})
|
||||
|
||||
test('setting an over-long name (78) throws a length error and broadcasts nothing', async () => {
|
||||
const wallet = fakeWallet()
|
||||
const profiles = fakeProfiles()
|
||||
const memoSetName = new MemoSetName({ wallet, profiles })
|
||||
|
||||
await assert.rejects(
|
||||
memoSetName.setName('y'.repeat(78)),
|
||||
(err) => err.code === 'name_length'
|
||||
)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
assert.equal(profiles.getName(wallet.walletInfo.cashAddress), null)
|
||||
})
|
||||
|
||||
test('setting a name without a wallet reports a missing-wallet error', async () => {
|
||||
const memoSetName = new MemoSetName({})
|
||||
await assert.rejects(
|
||||
|
||||
+25
-89
@@ -17,41 +17,29 @@ 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 { buildPage } = require('./page-build-helpers')
|
||||
|
||||
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 (msg, prefix) {
|
||||
this.broadcasts.push({ msg, prefix })
|
||||
if (this.failWith) throw new Error(this.failWith)
|
||||
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 buildPage({
|
||||
Page: NewPostPage,
|
||||
Action: MemoPost,
|
||||
actionKey: 'memoPost',
|
||||
storeKey: 'feed',
|
||||
storeFactory: fakeFeed
|
||||
})
|
||||
return { wallet, feed, memoPost, page, navigations }
|
||||
}
|
||||
|
||||
function buildBarePage (navigations) {
|
||||
return new NewPostPage({ navigate: (p) => navigations.push(p) })
|
||||
}
|
||||
|
||||
test('NEW_POST_PATH and RECENT_FEED_PATH constants', () => {
|
||||
@@ -83,7 +71,7 @@ test('the character counter reaches zero at the memo limit', () => {
|
||||
})
|
||||
|
||||
test('posting a valid memo broadcasts the Memo post prefix and navigates to the feed', async () => {
|
||||
const { wallet, feed, page, navigations } = build()
|
||||
const { wallet, store, page, navigations } = build()
|
||||
page.setInput('hello memo')
|
||||
|
||||
const result = await page.submit()
|
||||
@@ -98,12 +86,12 @@ test('posting a valid memo broadcasts the Memo post prefix and navigates to the
|
||||
// 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')
|
||||
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, feed, page, navigations } = build()
|
||||
const { wallet, store, page, navigations } = build()
|
||||
page.setInput('')
|
||||
|
||||
const result = await page.submit()
|
||||
@@ -113,12 +101,12 @@ test('posting an empty memo is rejected with a validation error and nothing is b
|
||||
assert.equal(page.submitError, 'memo_validation')
|
||||
assert.equal(page.posting, false)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
assert.equal(feed.posts.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, feed, page, navigations } = build()
|
||||
const { wallet, store, page, navigations } = build()
|
||||
page.setInput('y'.repeat(MAX + 1))
|
||||
|
||||
const result = await page.submit()
|
||||
@@ -128,7 +116,7 @@ test('posting an over-long memo is rejected with a length error and nothing is b
|
||||
assert.equal(page.submitError, 'memo_length')
|
||||
assert.equal(page.posting, false)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
assert.equal(feed.posts.length, 0)
|
||||
assert.equal(store.posts.length, 0)
|
||||
assert.deepEqual(navigations, [])
|
||||
})
|
||||
|
||||
@@ -137,63 +125,11 @@ test('the new post page starts idle (not posting)', () => {
|
||||
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.
|
||||
await new Promise((resolve) => setImmediate(resolve))
|
||||
assert.equal(typeof resolveSend, 'function')
|
||||
resolveSend('in-flight-txid')
|
||||
await pending
|
||||
assert.equal(page.posting, false)
|
||||
})
|
||||
|
||||
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, [])
|
||||
registerPageControllerTests({
|
||||
buildPage: build,
|
||||
buildBarePage,
|
||||
busyFlag: 'posting',
|
||||
prefix: '6d02'
|
||||
})
|
||||
|
||||
test('a failed broadcast surfaces a different real error message', async () => {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
'use strict'
|
||||
|
||||
const { fakeWallet } = require('../helpers/fake-wallet')
|
||||
|
||||
// Build a page controller wired to a working action and a navigation recorder.
|
||||
// Both the New Post and Set Name pages extend PageController and take a single
|
||||
// action dependency plus a navigate callback, so the wiring is identical; only
|
||||
// the page-specific pieces differ. `cfg` supplies:
|
||||
// Page - the page controller class (NewPostPage or SetNamePage)
|
||||
// Action - the action class (MemoPost or MemoSetName)
|
||||
// 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 }) {
|
||||
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)
|
||||
})
|
||||
return { wallet, store, action, page, navigations }
|
||||
}
|
||||
|
||||
module.exports = { buildPage }
|
||||
@@ -0,0 +1,66 @@
|
||||
'use strict'
|
||||
|
||||
const test = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
|
||||
// Register the page-controller tests shared by the New Post and Set Name pages.
|
||||
// Both pages extend PageController, so the in-flight flag, missing-handler, and
|
||||
// broadcast-failure behaviors are identical; only the page-specific pieces
|
||||
// differ. `cfg` supplies:
|
||||
// buildPage - () => ({ wallet, page, navigations }) with a working handler
|
||||
// buildBarePage - (navigations) => a page with no action handler
|
||||
// busyFlag - the page's in-flight flag name ('posting' or 'settingName')
|
||||
// prefix - the broadcast prefix ('6d02' or '6d01')
|
||||
function registerPageControllerTests (cfg) {
|
||||
const { buildPage, buildBarePage, busyFlag, prefix } = cfg
|
||||
|
||||
test(`${busyFlag} is true while a submit is in flight and false once it settles`, async () => {
|
||||
const { wallet, page } = buildPage()
|
||||
|
||||
// Defer the broadcast so we can observe the in-flight state.
|
||||
let resolveSend
|
||||
wallet.sendOpReturn = async () => new Promise((resolve) => { resolveSend = resolve })
|
||||
page.setInput('hello')
|
||||
|
||||
assert.equal(page[busyFlag], false)
|
||||
const pending = page.submit()
|
||||
assert.equal(page[busyFlag], true)
|
||||
|
||||
// Yield until the async chain reaches the deferred sendOpReturn call.
|
||||
await new Promise((resolve) => setImmediate(resolve))
|
||||
assert.equal(typeof resolveSend, 'function')
|
||||
resolveSend('in-flight-txid')
|
||||
await pending
|
||||
assert.equal(page[busyFlag], false)
|
||||
})
|
||||
|
||||
test('submitting without a handler reports an error and does not navigate', async () => {
|
||||
const navigations = []
|
||||
const page = buildBarePage(navigations)
|
||||
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, page, navigations } = buildPage()
|
||||
wallet.failWith = 'BCH UTXO list is empty'
|
||||
page.setInput('hello')
|
||||
|
||||
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, prefix)
|
||||
// The user stays on the page.
|
||||
assert.deepEqual(navigations, [])
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { registerPageControllerTests }
|
||||
@@ -16,40 +16,24 @@ const assert = require('node:assert/strict')
|
||||
|
||||
const MemoSetName = require('../../src/services/memo-set-name')
|
||||
const SetNamePage = require('../../src/services/set-name-page')
|
||||
const { fakeProfiles } = require('../helpers/fake-profiles')
|
||||
const { registerPageControllerTests } = require('./page-controller-helpers')
|
||||
const { buildPage } = require('./page-build-helpers')
|
||||
|
||||
const MAX = MemoSetName.MAX_NAME_BYTES // 77
|
||||
|
||||
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 'setname-txid'
|
||||
}
|
||||
}
|
||||
wallet.broadcasts = broadcasts
|
||||
return wallet
|
||||
}
|
||||
|
||||
function fakeProfiles () {
|
||||
const names = {}
|
||||
return { names, setName: (addr, name) => { names[addr] = name }, getName: (addr) => names[addr] || null }
|
||||
}
|
||||
|
||||
function build () {
|
||||
const wallet = fakeWallet()
|
||||
const profiles = fakeProfiles()
|
||||
const memoSetName = new MemoSetName({ wallet, profiles })
|
||||
const navigations = []
|
||||
const page = new SetNamePage({
|
||||
memoSetName,
|
||||
navigate: (path) => navigations.push(path)
|
||||
return buildPage({
|
||||
Page: SetNamePage,
|
||||
Action: MemoSetName,
|
||||
actionKey: 'memoSetName',
|
||||
storeKey: 'profiles',
|
||||
storeFactory: fakeProfiles
|
||||
})
|
||||
return { wallet, profiles, memoSetName, page, navigations }
|
||||
}
|
||||
|
||||
function buildBarePage (navigations) {
|
||||
return new SetNamePage({ navigate: (p) => navigations.push(p) })
|
||||
}
|
||||
|
||||
test('SET_NAME_PATH and ACCOUNT_PATH constants', () => {
|
||||
@@ -88,7 +72,7 @@ test('the byte counter reaches zero at the name limit', () => {
|
||||
})
|
||||
|
||||
test('setting a valid name broadcasts the Memo set-name prefix and navigates to the account page', async () => {
|
||||
const { wallet, profiles, page, navigations } = build()
|
||||
const { wallet, store, page, navigations } = build()
|
||||
page.setInput('trout')
|
||||
|
||||
const result = await page.submit()
|
||||
@@ -99,11 +83,11 @@ test('setting a valid name broadcasts the Memo set-name prefix and navigates to
|
||||
assert.equal(wallet.broadcasts[0].prefix, '6d01')
|
||||
assert.equal(wallet.broadcasts[0].msg, 'trout')
|
||||
assert.deepEqual(navigations, ['/account'])
|
||||
assert.equal(profiles.getName(wallet.walletInfo.cashAddress), 'trout')
|
||||
assert.equal(store.getName(wallet.walletInfo.cashAddress), 'trout')
|
||||
})
|
||||
|
||||
test('setting an empty name is rejected with a validation error and nothing is broadcast', async () => {
|
||||
const { wallet, profiles, page, navigations } = build()
|
||||
const { wallet, store, page, navigations } = build()
|
||||
page.setInput('')
|
||||
|
||||
const result = await page.submit()
|
||||
@@ -113,12 +97,12 @@ test('setting an empty name is rejected with a validation error and nothing is b
|
||||
assert.equal(page.submitError, 'name_validation')
|
||||
assert.equal(page.settingName, false)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
assert.equal(profiles.getName(wallet.walletInfo.cashAddress), null)
|
||||
assert.equal(store.getName(wallet.walletInfo.cashAddress), null)
|
||||
assert.deepEqual(navigations, [])
|
||||
})
|
||||
|
||||
test('setting an over-long name is rejected with a length error and nothing is broadcast', async () => {
|
||||
const { wallet, profiles, page, navigations } = build()
|
||||
const { wallet, store, page, navigations } = build()
|
||||
page.setInput('y'.repeat(MAX + 1))
|
||||
|
||||
const result = await page.submit()
|
||||
@@ -128,7 +112,7 @@ test('setting an over-long name is rejected with a length error and nothing is b
|
||||
assert.equal(page.submitError, 'name_length')
|
||||
assert.equal(page.settingName, false)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
assert.equal(profiles.getName(wallet.walletInfo.cashAddress), null)
|
||||
assert.equal(store.getName(wallet.walletInfo.cashAddress), null)
|
||||
assert.deepEqual(navigations, [])
|
||||
})
|
||||
|
||||
@@ -137,57 +121,9 @@ test('the set name page starts idle (not setting name)', () => {
|
||||
assert.equal(page.settingName, false)
|
||||
})
|
||||
|
||||
test('setting name is true while a submit is in flight and false once it settles', async () => {
|
||||
const wallet = fakeWallet()
|
||||
const profiles = fakeProfiles()
|
||||
|
||||
let resolveSend
|
||||
wallet.sendOpReturn = async () => new Promise((resolve) => { resolveSend = resolve })
|
||||
const page = new SetNamePage({
|
||||
memoSetName: new MemoSetName({ wallet, profiles }),
|
||||
navigate: () => {}
|
||||
})
|
||||
page.setInput('trout')
|
||||
|
||||
assert.equal(page.settingName, false)
|
||||
const pending = page.submit()
|
||||
assert.equal(page.settingName, true)
|
||||
|
||||
await new Promise((resolve) => setImmediate(resolve))
|
||||
assert.equal(typeof resolveSend, 'function')
|
||||
resolveSend('in-flight-txid')
|
||||
await pending
|
||||
assert.equal(page.settingName, false)
|
||||
})
|
||||
|
||||
test('submitting without a memo set-name handler reports an error and does not navigate', async () => {
|
||||
const navigations = []
|
||||
const page = new SetNamePage({ navigate: (p) => navigations.push(p) })
|
||||
page.setInput('trout')
|
||||
|
||||
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 profiles = fakeProfiles()
|
||||
wallet.failWith = 'BCH UTXO list is empty'
|
||||
const navigations = []
|
||||
const page = new SetNamePage({
|
||||
memoSetName: new MemoSetName({ wallet, profiles }),
|
||||
navigate: (p) => navigations.push(p)
|
||||
})
|
||||
page.setInput('trout')
|
||||
|
||||
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, '6d01')
|
||||
assert.deepEqual(navigations, [])
|
||||
registerPageControllerTests({
|
||||
buildPage: build,
|
||||
buildBarePage,
|
||||
busyFlag: 'settingName',
|
||||
prefix: '6d01'
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user