Files
psf-memo/test/property/behavior-helpers.js
T

118 lines
4.2 KiB
JavaScript

/*
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 }