mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo.git
synced 2026-09-21 16:52:01 -07:00
Review notification entry display: wire SPA profile links, cover renderer
Wire the existing onProfileClick seam in the Notifications page so the actor avatar and display name navigate through the SPA router instead of a full-page reload (which 404s on the BrowserRouter/GH Pages deployment). Add a component unit test so the renderer's mutation sites are covered, and a fresh-state controller test to kill the constructor's empty=false mutant. Refresh the tool-written mutation manifests for the changed modules and the soft Gherkin mutation manifest for the feature. By architect.
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
# acceptance-mutation-manifest-begin
|
||||
# {"version":1,"tested_at":"2026-09-18T19:43:11.863411027Z","feature_name":"Notification Entry Display","feature_path":"/home/trout/work/psf-memo/.worktrees/architect/psf-memo-client/specs/notification-entry-display.feature","background_hash":"0d66780cb1b8e277f0ada40a8ffe336dec7a8eaf658f19d2ea344815fb9bf26c","implementation_hash":"unknown","scenarios":[{"index":1,"name":"Notification Entry Display - 2 the avatar and display name link to the actor's profile","scenario_hash":"6eac4433d642f99b7538085e60169c835655026202b1abee6b2264fe01d52711","mutation_count":4,"result":{"Total":4,"Killed":4,"Survived":0,"Errors":0},"tested_at":"2026-09-18T19:43:11.863411027Z"}]}
|
||||
# acceptance-mutation-manifest-end
|
||||
|
||||
# Scenarios: Notification Entry Display - 1, Notification Entry Display - 2, Notification Entry Display - 3, Notification Entry Display - 4, Notification Entry Display - 5, Notification Entry Display - 6, Notification Entry Display - 7, Notification Entry Display - 8
|
||||
#
|
||||
# Each Notifications entry names its actor with the actor's Memo display name
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
// Global npm libraries
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Container, Row, Col, Spinner, Button } from 'react-bootstrap'
|
||||
|
||||
// Local libraries
|
||||
@@ -24,6 +25,7 @@ const PAGE_SIZE = 50
|
||||
function Notifications (props) {
|
||||
const { appData } = props
|
||||
const wallet = appData?.wallet
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(null)
|
||||
@@ -127,6 +129,7 @@ function Notifications (props) {
|
||||
key={entry.txid}
|
||||
entry={entry}
|
||||
onViewPost={openThread}
|
||||
onProfileClick={navigate}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -95,3 +95,7 @@ function NotificationEntry ({ entry, onViewPost, onProfileClick }) {
|
||||
|
||||
module.exports = NotificationEntry
|
||||
module.exports.NotificationAvatar = NotificationAvatar
|
||||
|
||||
// mutate4javascript-manifest-begin
|
||||
// {"version":1,"tested_at":"2026-09-18T19:39:02.274Z","module_hash":"98bffd92542fbbee866abbc2775b60736c2ea8ba6fe36ec38690b22035fdd306","functions":[{"id":"func/NotificationAvatar","name":"NotificationAvatar","line":23,"end_line":39,"hash":"eaae01344873b8451a42943bd87478a7b4e8dc3fa5021efd2811c932950e40bf"},{"id":"func/NotificationEntry","name":"NotificationEntry","line":41,"end_line":94,"hash":"c17002fa1ff4602c2785990a95100f4ff782ebb1a5dc7c5573190d85a5bdfe4f"}]}
|
||||
// mutate4javascript-manifest-end
|
||||
|
||||
@@ -61,3 +61,7 @@ module.exports = {
|
||||
notificationMessage,
|
||||
buildNotificationEntry
|
||||
}
|
||||
|
||||
// mutate4javascript-manifest-begin
|
||||
// {"version":1,"tested_at":"2026-09-18T19:33:01.460Z","module_hash":"8043c8e67b2834a2e0667b5e9d528ef96bb0f25ac2d45cf87ecc00505e89171e","functions":[{"id":"func/profilePath","name":"profilePath","line":18,"end_line":20,"hash":"600b1f8066ecb8274c17a26c6ac2c753471c3ee4efc2de69558c6811a070a82c"},{"id":"func/displayName","name":"displayName","line":24,"end_line":26,"hash":"4c3a77ba28e6e2e1ec50c7bf6854a51f3fe6ed85f23faa8d48745898c1ee580a"},{"id":"func/notificationMessage","name":"notificationMessage","line":29,"end_line":40,"hash":"5dd8314d32ab92867f655414252398c224e804a71b83a0bd96a18fb32595297f"},{"id":"func/buildNotificationEntry","name":"buildNotificationEntry","line":44,"end_line":53,"hash":"4707908e71b73429351a2a283aaf4e68093be66dd0e06ee0af7188fe65bcea7d"}]}
|
||||
// mutate4javascript-manifest-end
|
||||
|
||||
@@ -114,5 +114,5 @@ NotificationsPage.NOTIFICATIONS_PATH = NOTIFICATIONS_PATH
|
||||
module.exports = NotificationsPage
|
||||
|
||||
// mutate4javascript-manifest-begin
|
||||
// {"version":1,"tested_at":"2026-09-04T17:20:15.460Z","module_hash":"302d668c6360824911c04786d5337002fe2870aed064fdb3ba85b3944e6c4f7b","functions":[{"id":"func/NotificationsPage.constructor","name":"NotificationsPage.constructor","line":13,"end_line":19,"hash":"52eca9540d0f0c524cd8b2c6c2a80c37cc1769c2a109bae7c97f080bede56130"},{"id":"func/NotificationsPage.getMyAddress","name":"NotificationsPage.getMyAddress","line":21,"end_line":23,"hash":"3e5d4ac4df379300933a772020528b4ecf4ed83c7386a066f5c270df81adcddd"},{"id":"func/NotificationsPage.load","name":"NotificationsPage.load","line":25,"end_line":45,"hash":"d99ba2df1db799b969e7fc4b8e3f0964c84f973d92e048df97c361e4659b0c9d"},{"id":"func/NotificationsPage.canLoadMore","name":"NotificationsPage.canLoadMore","line":47,"end_line":49,"hash":"634983bcc6bbe560daad8326db0dd4bf31d5cb9e45c40112565351dceaf8e5d5"},{"id":"func/NotificationsPage.getNotification","name":"NotificationsPage.getNotification","line":51,"end_line":53,"hash":"d01238c7fddb85e9ca7427d512c828ec6524657feb1156a820516641ed27105c"}]}
|
||||
// {"version":1,"tested_at":"2026-09-18T19:40:56.858Z","module_hash":"db9565ee912fd31eed870808332e98f57e4b9c01b573a03bd11fe36a66fb6989","functions":[{"id":"func/NotificationsPage.constructor","name":"NotificationsPage.constructor","line":15,"end_line":22,"hash":"e9f52dab100cd73a4149081fd1e1ac59bcfbfe58a3a3ef32f2f2bf83c613f9ab"},{"id":"func/NotificationsPage.getMyAddress","name":"NotificationsPage.getMyAddress","line":24,"end_line":26,"hash":"3e5d4ac4df379300933a772020528b4ecf4ed83c7386a066f5c270df81adcddd"},{"id":"func/NotificationsPage.load","name":"NotificationsPage.load","line":28,"end_line":50,"hash":"aeba628bcf9228754a2a57807a9bf8bb84236ed95a8d89c5493de9ef362cb937"},{"id":"func/NotificationsPage._loadProfiles","name":"NotificationsPage._loadProfiles","line":54,"end_line":60,"hash":"bf78658c8e7d10ed90d64ede96ccdc215f07045834fe1e44f54f25c0af4d54a6"},{"id":"func/NotificationsPage._loadProfile","name":"NotificationsPage._loadProfile","line":64,"end_line":77,"hash":"f5de92a5e6a20e82d365c30749d086d255b665f23b916aa2a2f03ad08798dd63"},{"id":"func/NotificationsPage._loadProfileField","name":"NotificationsPage._loadProfileField","line":79,"end_line":82,"hash":"8d3814b475e60aa26a45bb99c4f2d3ffc6cc78225e319c85c073afad0c611086"},{"id":"func/NotificationsPage.canLoadMore","name":"NotificationsPage.canLoadMore","line":84,"end_line":86,"hash":"634983bcc6bbe560daad8326db0dd4bf31d5cb9e45c40112565351dceaf8e5d5"},{"id":"func/NotificationsPage.getNotification","name":"NotificationsPage.getNotification","line":88,"end_line":90,"hash":"d01238c7fddb85e9ca7427d512c828ec6524657feb1156a820516641ed27105c"},{"id":"func/NotificationsPage.getEntries","name":"NotificationsPage.getEntries","line":93,"end_line":95,"hash":"9481b5ecf1a0f770df7d4eb764e695387d6362ee58a0936ea3914357292dec14"},{"id":"func/NotificationsPage.getEntry","name":"NotificationsPage.getEntry","line":98,"end_line":102,"hash":"5a259b305609fd6e8529588a571f4ba0397fb8d93c69569a68d8d5ad4b942033"},{"id":"func/NotificationsPage.getEntryByAddr","name":"NotificationsPage.getEntryByAddr","line":105,"end_line":109,"hash":"a99ae43f45702447dd3abfa5f8dd2ffa8acf79b655a24765299f84709bc98624"}]}
|
||||
// mutate4javascript-manifest-end
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
Unit tests for the notification entry renderer.
|
||||
|
||||
The renderer names its actor with the display name and avatar, links both to
|
||||
the actor's profile, shows the full address as plain text, and offers a
|
||||
"View Post" link only for like and reply notifications. It falls back to an
|
||||
identicon when the actor has no avatar.
|
||||
|
||||
Written against the same rendering adapter seam the acceptance suite uses so
|
||||
the module stays covered by the standard unit suite.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const test = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
const React = require('react')
|
||||
const ReactDOMServer = require('react-dom/server')
|
||||
const NotificationEntry = require('../../src/components/app-body/notifications/notification-entry')
|
||||
|
||||
const ALICE = 'bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy'
|
||||
const PROFILE_PATH = `/profile/${encodeURIComponent(ALICE)}`
|
||||
const VIEW_POST_LABEL = 'View Post'
|
||||
|
||||
function makeEntry (overrides = {}) {
|
||||
return {
|
||||
type: 'like',
|
||||
txid: 'a'.repeat(64),
|
||||
addr: ALICE,
|
||||
postTxid: 'b'.repeat(64),
|
||||
displayName: 'alice',
|
||||
avatarUrl: null,
|
||||
profilePath: PROFILE_PATH,
|
||||
showViewPost: true,
|
||||
message: 'liked your post',
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
function render (entry) {
|
||||
return ReactDOMServer.renderToStaticMarkup(
|
||||
React.createElement(NotificationEntry, { entry })
|
||||
)
|
||||
}
|
||||
|
||||
test('renders nothing without an entry', () => {
|
||||
assert.equal(render(null), '')
|
||||
})
|
||||
|
||||
test('renders the display name, full address, and message', () => {
|
||||
const html = render(makeEntry())
|
||||
|
||||
assert.ok(html.includes('alice'))
|
||||
assert.ok(html.includes(ALICE))
|
||||
assert.ok(html.includes('liked your post'))
|
||||
})
|
||||
|
||||
test('renders the avatar image when the actor has an avatar URL', () => {
|
||||
const html = render(makeEntry({ avatarUrl: 'https://example.com/alice.png' }))
|
||||
|
||||
assert.match(html, /<img[^>]+src="https:\/\/example\.com\/alice\.png"/)
|
||||
assert.doesNotMatch(html, /notification-entry-identicon/)
|
||||
})
|
||||
|
||||
test('renders an identicon when the actor has no avatar URL', () => {
|
||||
const html = render(makeEntry({ avatarUrl: null }))
|
||||
|
||||
assert.match(html, /notification-entry-identicon/)
|
||||
assert.match(html, /data-jdenticon-value/)
|
||||
assert.doesNotMatch(html, /<img/)
|
||||
})
|
||||
|
||||
test('links the avatar and display name to the actor profile', () => {
|
||||
const html = render(makeEntry())
|
||||
|
||||
assert.ok(html.includes('class="notification-entry-avatar-link"'))
|
||||
assert.ok(html.includes('class="notification-entry-name-link"'))
|
||||
assert.equal(html.split(`href="${PROFILE_PATH}"`).length - 1, 2)
|
||||
})
|
||||
|
||||
test('offers a View Post link when the notification references a post', () => {
|
||||
const html = render(makeEntry({ showViewPost: true }))
|
||||
|
||||
assert.ok(html.includes(VIEW_POST_LABEL))
|
||||
assert.ok(html.includes('class="notification-entry-view-post"'))
|
||||
})
|
||||
|
||||
test('offers no View Post link when the notification does not reference a post', () => {
|
||||
const html = render(makeEntry({ showViewPost: false }))
|
||||
|
||||
assert.ok(!html.includes(VIEW_POST_LABEL))
|
||||
assert.ok(!html.includes('notification-entry-view-post'))
|
||||
})
|
||||
|
||||
test('renders an empty message when the notification has no text', () => {
|
||||
const html = render(makeEntry({ message: undefined }))
|
||||
|
||||
assert.match(html, /class="notification-entry-text"[^>]*><\/p>/)
|
||||
})
|
||||
@@ -24,6 +24,15 @@ function makeMemoDb (notifications, pagination) {
|
||||
}
|
||||
}
|
||||
|
||||
test('a fresh page starts with unloaded defaults', () => {
|
||||
const page = new NotificationsPage({})
|
||||
|
||||
assert.equal(page.empty, false)
|
||||
assert.equal(page.pagination, null)
|
||||
assert.deepEqual(page.notifications, [])
|
||||
assert.deepEqual(page.profiles, {})
|
||||
})
|
||||
|
||||
test('load returns notifications', async () => {
|
||||
const notifications = [
|
||||
{ type: 'reply', txid: 'a'.repeat(64), addr: 'bitcoincash:other', text: 'hi' },
|
||||
|
||||
Reference in New Issue
Block a user