mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo.git
synced 2026-09-23 01:32:00 -07:00
Cover the address-copy confirmation and add property tests
Extract the fake-timer and clipboard test helpers to test/support so the address-copy unit and property tests share one definition, cover ProfilePage.destroy clearing a pending confirmation timer, and add seeded property tests for the copy write, timeout, elapse, destroy, and markup invariants. No production behavior changed. By refactorer.
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
Property tests for the profile page address copy behavior.
|
||||
|
||||
The unit tests probe the confirmation flow at fixed fixtures. These
|
||||
properties pin the address-copy invariants over broad random addresses:
|
||||
|
||||
- write: copyAddress writes exactly the profile address once, returns it,
|
||||
and shows the confirmation.
|
||||
- timeout: the scheduled confirmation timer hides the confirmation exactly
|
||||
once and notifies the change listener true then false.
|
||||
- elapse: addressCopyTimeoutElapsed hides the confirmation, clears the
|
||||
pending timer once, and is safe to call again.
|
||||
- destroy: destroy clears the pending timer without changing the visible
|
||||
confirmation, and is safe when nothing is pending.
|
||||
- markup: ProfileAddress always shows the address and shows the confirmation
|
||||
text exactly when copied, with deterministic rendering.
|
||||
|
||||
All generation is seeded, so runs are reproducible.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const test = require('node:test')
|
||||
const React = require('react')
|
||||
const ReactDOMServer = require('react-dom/server')
|
||||
const { seededRandom, forAll } = require('./harness')
|
||||
const {
|
||||
makeAddressCopyPage,
|
||||
makeFakeTimers
|
||||
} = require('../support/address-copy')
|
||||
const { randomRecentProfileAddr } = require('../support/recent-profiles')
|
||||
const ProfileAddress = require('../../src/components/app-body/profile/profile-address')
|
||||
|
||||
const rng = seededRandom(20260923)
|
||||
const COPIED_TEXT = 'Copied to clipboard'
|
||||
|
||||
function randomAddr () {
|
||||
return randomRecentProfileAddr(rng)
|
||||
}
|
||||
|
||||
function renderAddress (props) {
|
||||
return ReactDOMServer.renderToStaticMarkup(
|
||||
React.createElement(ProfileAddress, props)
|
||||
)
|
||||
}
|
||||
|
||||
test('copyAddress writes the address once, confirms, and returns it', async () => {
|
||||
await forAll(
|
||||
() => randomAddr(),
|
||||
async (addr) => {
|
||||
const writes = []
|
||||
const page = makeAddressCopyPage({
|
||||
addr,
|
||||
copyToClipboard: async (text) => { writes.push(text) }
|
||||
})
|
||||
|
||||
const returned = await page.copyAddress()
|
||||
|
||||
return writes.length === 1 &&
|
||||
writes[0] === addr &&
|
||||
returned === addr &&
|
||||
page.isShowingAddressCopyConfirmation() === true
|
||||
},
|
||||
{ label: 'profile address copy write', samples: 500 }
|
||||
)
|
||||
})
|
||||
|
||||
test('the scheduled confirmation timeout hides the confirmation once', async () => {
|
||||
await forAll(
|
||||
() => randomAddr(),
|
||||
async (addr) => {
|
||||
const timers = makeFakeTimers()
|
||||
const changes = []
|
||||
const page = makeAddressCopyPage({
|
||||
addr,
|
||||
onAddressCopyChange: (copied) => changes.push(copied),
|
||||
setTimer: timers.setTimer,
|
||||
clearTimer: timers.clearTimer
|
||||
})
|
||||
|
||||
await page.copyAddress()
|
||||
const scheduled = timers.scheduled.length
|
||||
timers.scheduled[0].fn()
|
||||
|
||||
return scheduled === 1 &&
|
||||
page.isShowingAddressCopyConfirmation() === false &&
|
||||
changes.length === 2 &&
|
||||
changes[0] === true &&
|
||||
changes[1] === false
|
||||
},
|
||||
{ label: 'profile address copy timeout', samples: 500 }
|
||||
)
|
||||
})
|
||||
|
||||
test('elapsing the confirmation clears the pending timer exactly once', async () => {
|
||||
await forAll(
|
||||
() => randomAddr(),
|
||||
async (addr) => {
|
||||
const timers = makeFakeTimers()
|
||||
const page = makeAddressCopyPage({
|
||||
addr,
|
||||
setTimer: timers.setTimer,
|
||||
clearTimer: timers.clearTimer
|
||||
})
|
||||
|
||||
await page.copyAddress()
|
||||
const returned = page.addressCopyTimeoutElapsed()
|
||||
page.addressCopyTimeoutElapsed()
|
||||
|
||||
return returned === page &&
|
||||
page.isShowingAddressCopyConfirmation() === false &&
|
||||
timers.cleared.length === 1
|
||||
},
|
||||
{ label: 'profile address copy elapse', samples: 400 }
|
||||
)
|
||||
})
|
||||
|
||||
test('destroy clears the pending timer without hiding the confirmation', async () => {
|
||||
await forAll(
|
||||
() => randomAddr(),
|
||||
async (addr) => {
|
||||
const timers = makeFakeTimers()
|
||||
const page = makeAddressCopyPage({
|
||||
addr,
|
||||
setTimer: timers.setTimer,
|
||||
clearTimer: timers.clearTimer
|
||||
})
|
||||
|
||||
await page.copyAddress()
|
||||
const returned = page.destroy()
|
||||
|
||||
return returned === page &&
|
||||
page.isShowingAddressCopyConfirmation() === true &&
|
||||
timers.scheduled.length === 1 &&
|
||||
timers.cleared.length === 1
|
||||
},
|
||||
{ label: 'profile address copy destroy', samples: 400 }
|
||||
)
|
||||
})
|
||||
|
||||
test('destroy is safe when no confirmation timer is pending', async () => {
|
||||
await forAll(
|
||||
() => randomAddr(),
|
||||
async (addr) => {
|
||||
const timers = makeFakeTimers()
|
||||
const page = makeAddressCopyPage({
|
||||
addr,
|
||||
setTimer: timers.setTimer,
|
||||
clearTimer: timers.clearTimer
|
||||
})
|
||||
|
||||
const returned = page.destroy()
|
||||
|
||||
return returned === page &&
|
||||
page.isShowingAddressCopyConfirmation() === false &&
|
||||
timers.cleared.length === 0
|
||||
},
|
||||
{ label: 'profile address copy destroy idle', samples: 300 }
|
||||
)
|
||||
})
|
||||
|
||||
test('ProfileAddress shows the confirmation exactly when copied', async () => {
|
||||
await forAll(
|
||||
() => ({ addr: randomAddr(), copied: rng() < 0.5 }),
|
||||
({ addr, copied }) => {
|
||||
const html = renderAddress({ address: addr, copied })
|
||||
return html.includes(addr) && html.includes(COPIED_TEXT) === copied
|
||||
},
|
||||
{ label: 'profile address markup confirmation', samples: 500 }
|
||||
)
|
||||
})
|
||||
|
||||
test('rendering the same ProfileAddress props twice yields the same markup', async () => {
|
||||
await forAll(
|
||||
() => ({ addr: randomAddr(), copied: rng() < 0.5 }),
|
||||
({ addr, copied }) => {
|
||||
const first = renderAddress({ address: addr, copied })
|
||||
const second = renderAddress({ address: addr, copied })
|
||||
return first === second
|
||||
},
|
||||
{ label: 'profile address render determinism', samples: 300 }
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
Test helpers for the profile address copy behavior.
|
||||
|
||||
`makeAddressCopyPage` builds a ProfilePage wired to a no-op clipboard adapter
|
||||
(override `copyToClipboard` to record writes). `makeFakeTimers` captures the
|
||||
confirmation timer callbacks and clear calls so a test can elapse the
|
||||
confirmation deterministically without real time.
|
||||
Shared by the address-copy unit and property tests.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const ProfilePage = require('../../src/services/profile-page')
|
||||
|
||||
const PROFILE_ADDR = 'bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy'
|
||||
|
||||
function makeAddressCopyPage (deps = {}) {
|
||||
return new ProfilePage({
|
||||
addr: PROFILE_ADDR,
|
||||
copyToClipboard: async () => {},
|
||||
...deps
|
||||
})
|
||||
}
|
||||
|
||||
function makeFakeTimers () {
|
||||
const scheduled = []
|
||||
const cleared = []
|
||||
let nextId = 0
|
||||
return {
|
||||
scheduled,
|
||||
cleared,
|
||||
setTimer (fn) {
|
||||
nextId += 1
|
||||
scheduled.push({ id: nextId, fn })
|
||||
return nextId
|
||||
},
|
||||
clearTimer (id) {
|
||||
cleared.push(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { PROFILE_ADDR, makeAddressCopyPage, makeFakeTimers }
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
Clicking the profile address copies it to the clipboard through an injected
|
||||
adapter and shows a transient "Copied to clipboard" confirmation. The
|
||||
confirmation clears when the injected timer elapses.
|
||||
confirmation clears when the injected timer elapses, and destroying the page
|
||||
stops the pending timer without changing the visible confirmation.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
@@ -11,30 +12,11 @@
|
||||
const test = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
const ProfilePage = require('../../src/services/profile-page')
|
||||
|
||||
const ADDR = 'bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy'
|
||||
|
||||
function makePage (deps = {}) {
|
||||
return new ProfilePage({ addr: ADDR, copyToClipboard: async () => {}, ...deps })
|
||||
}
|
||||
|
||||
function makeTimers () {
|
||||
const scheduled = []
|
||||
const cleared = []
|
||||
let nextId = 0
|
||||
return {
|
||||
scheduled,
|
||||
cleared,
|
||||
setTimer (fn) {
|
||||
nextId += 1
|
||||
scheduled.push({ id: nextId, fn })
|
||||
return nextId
|
||||
},
|
||||
clearTimer (id) {
|
||||
cleared.push(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
const {
|
||||
PROFILE_ADDR: ADDR,
|
||||
makeAddressCopyPage: makePage,
|
||||
makeFakeTimers: makeTimers
|
||||
} = require('../support/address-copy')
|
||||
|
||||
test('copyAddress writes the profile address to the clipboard adapter', async () => {
|
||||
const writes = []
|
||||
@@ -113,6 +95,26 @@ test('copyAddress notifies the change listener as the confirmation toggles', asy
|
||||
assert.deepEqual(changes, [true, false])
|
||||
})
|
||||
|
||||
test('destroy clears the pending timeout without hiding the confirmation', async () => {
|
||||
const timers = makeTimers()
|
||||
const page = makePage({ setTimer: timers.setTimer, clearTimer: timers.clearTimer })
|
||||
await page.copyAddress()
|
||||
|
||||
const returned = page.destroy()
|
||||
|
||||
assert.equal(returned, page)
|
||||
assert.deepEqual(timers.cleared, [1])
|
||||
assert.equal(page.isShowingAddressCopyConfirmation(), true)
|
||||
})
|
||||
|
||||
test('destroy is safe when no confirmation timer is pending', () => {
|
||||
const timers = makeTimers()
|
||||
const page = makePage({ setTimer: timers.setTimer, clearTimer: timers.clearTimer })
|
||||
|
||||
assert.doesNotThrow(() => page.destroy())
|
||||
assert.deepEqual(timers.cleared, [])
|
||||
})
|
||||
|
||||
test('copyAddress throws when the profile has no address', async () => {
|
||||
const page = new ProfilePage({ copyToClipboard: async () => {} })
|
||||
|
||||
|
||||
Reference in New Issue
Block a user