mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo.git
synced 2026-09-21 16:52:01 -07:00
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.
167 lines
5.6 KiB
JavaScript
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"/)
|
|
})
|