Files
psf-memo/psf-memo-client/test/unit/post-options.test.js
T
Chris Troutner d0882bbd82 Refactor post options menu and add property tests
Move the keyboard shortcut mapping into the pure post-options service so the
React component is a thin adapter and the Escape/ArrowDown decisions are unit
and property testable. DRY the duplicated post-options acceptance steps behind
two helpers, and add property tests for the explorer URL, menu-item shape,
state transitions, key command, and rendered markup.

The createElement post-options component and service stay scannable by
mutate4javascript; the JSX call sites (post-feed-item, profile) are unchanged
except for rendering the shared menu.

By refactorer.
2026-09-16 09:48:36 -07:00

167 lines
5.6 KiB
JavaScript

/*
Unit tests for the post options menu.
Every post card shows a three-dots "Post options" button whose menu's first
item is "See on block explorer", a link to the post transaction on
bch.loping.net that opens in a new tab. The menu opens on the button, closes
on the button again, an outside click, or Escape, and ArrowDown moves focus
to the first item.
The pure service owns the link construction and the open/close/focus state
transitions; the component renders that state to markup.
*/
'use strict'
const test = require('node:test')
const assert = require('node:assert/strict')
const React = require('react')
const ReactDOMServer = require('react-dom/server')
const PostOptions = require('../../src/services/post-options')
const PostOptionsMenu = require('../../src/components/post-feed/post-options-menu')
const TXID = 'c96a46c8b55657fe125115e3ddf5ad30ad587bb41baa952cb8d9be9334161875'
const EXPLORER_URL = `https://bch.loping.net/tx/${TXID}`
function renderMenu (props = {}) {
return ReactDOMServer.renderToStaticMarkup(
React.createElement(PostOptionsMenu, { txid: TXID, ...props })
)
}
test('explorerTxUrl builds the bch.loping.net transaction link', () => {
assert.equal(PostOptions.explorerTxUrl(TXID), EXPLORER_URL)
})
test('explorerTxUrl returns an empty string without a txid', () => {
assert.equal(PostOptions.explorerTxUrl(''), '')
assert.equal(PostOptions.explorerTxUrl(null), '')
assert.equal(PostOptions.explorerTxUrl(undefined), '')
})
test('postOptionsItems puts the block explorer link first', () => {
const items = PostOptions.postOptionsItems(TXID)
assert.equal(items[0].label, 'See on block explorer')
assert.equal(items[0].href, EXPLORER_URL)
assert.equal(items[0].target, '_blank')
assert.equal(items[0].rel, 'noopener noreferrer')
})
test('the menu starts hidden', () => {
const state = PostOptions.initialPostOptionsState()
assert.equal(state.open, false)
assert.equal(state.focusedIndex, -1)
})
test('toggling the menu opens it, then closes it again', () => {
let state = PostOptions.initialPostOptionsState()
state = PostOptions.togglePostOptions(state)
assert.equal(state.open, true)
state = PostOptions.togglePostOptions(state)
assert.equal(state.open, false)
})
test('closing the menu also clears item focus', () => {
const open = PostOptions.openPostOptions(PostOptions.initialPostOptionsState())
const focused = PostOptions.focusFirstPostOption(
open,
PostOptions.postOptionsItems(TXID)
)
const closed = PostOptions.closePostOptions(focused)
assert.equal(closed.open, false)
assert.equal(closed.focusedIndex, -1)
})
test('Escape and an outside click both close the menu', () => {
const open = PostOptions.openPostOptions(PostOptions.initialPostOptionsState())
assert.equal(PostOptions.handlePostOptionsEscape(open).open, false)
assert.equal(PostOptions.handlePostOptionsOutsideClick(open).open, false)
})
test('focusing the first item selects index 0 when items exist', () => {
const open = PostOptions.openPostOptions(PostOptions.initialPostOptionsState())
const focused = PostOptions.focusFirstPostOption(
open,
PostOptions.postOptionsItems(TXID)
)
assert.equal(focused.focusedIndex, 0)
})
test('focusing the first item is a no-op with no items', () => {
const open = PostOptions.openPostOptions(PostOptions.initialPostOptionsState())
const focused = PostOptions.focusFirstPostOption(open, [])
assert.equal(focused.focusedIndex, -1)
})
test('the Escape key command closes the menu without preventing default', () => {
const command = PostOptions.postOptionsKeyCommand(
'Escape',
PostOptions.postOptionsItems(TXID)
)
assert.equal(command.preventDefault, false)
const next = command.transition(
PostOptions.openPostOptions(PostOptions.initialPostOptionsState())
)
assert.equal(next.open, false)
assert.equal(next.focusedIndex, -1)
})
test('the ArrowDown key command focuses the first item and prevents default', () => {
const command = PostOptions.postOptionsKeyCommand(
'ArrowDown',
PostOptions.postOptionsItems(TXID)
)
assert.equal(command.preventDefault, true)
const next = command.transition(PostOptions.initialPostOptionsState())
assert.equal(next.open, true)
assert.equal(next.focusedIndex, 0)
})
test('the ArrowDown key command keeps the menu closed when there are no items', () => {
const command = PostOptions.postOptionsKeyCommand('ArrowDown', [])
const next = command.transition(PostOptions.initialPostOptionsState())
assert.equal(next.open, false)
assert.equal(next.focusedIndex, -1)
})
test('other keys have no menu command', () => {
assert.equal(PostOptions.postOptionsKeyCommand('Enter', PostOptions.postOptionsItems(TXID)), null)
assert.equal(PostOptions.postOptionsKeyCommand('Tab', []), null)
})
test('the closed menu renders the post options button but no items', () => {
const html = renderMenu()
assert.match(html, /aria-label="Post options"/)
assert.match(html, /aria-haspopup="menu"/)
assert.match(html, /aria-expanded="false"/)
assert.doesNotMatch(html, /See on block explorer/)
})
test('the open menu renders the block explorer link that opens in a new tab', () => {
const html = renderMenu({ initialOpen: true })
assert.match(html, /aria-expanded="true"/)
assert.match(html, /See on block explorer/)
assert.match(html, new RegExp(`href="${EXPLORER_URL.replace(/[.]/g, '\\.')}"`))
assert.match(html, /target="_blank"/)
assert.match(html, /rel="noopener noreferrer"/)
})
test('the focused first item is tabbable and the others are not', () => {
const html = renderMenu({ initialOpen: true, initialFocusedIndex: 0 })
assert.match(html, /tabindex="0"/)
})