mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo-client.git
synced 2026-09-21 16:52:02 -07:00
118 lines
4.2 KiB
JavaScript
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 }
|