Implement recent profile identity join and Account column

DB: GET /profile/recent joins each profile's display name (names store)
and avatar URL (profilePics store) by address, reporting null when a
record is absent. The join runs on the requested page only and leaves the
profile order and pagination unchanged.

Client: the /profile/recent table gains a leftmost Account column showing
the display name and avatar, both linking to the profile, with a
truncated-address fallback when the name is absent and an identicon
fallback when the avatar is absent. The existing columns are preserved.

Add focused unit tests, acceptance fixtures, and regex step handlers for
both components.

By coder.
This commit is contained in:
Chris Troutner
2026-09-20 12:46:12 -07:00
parent dda94b1a81
commit 9fb092e845
14 changed files with 639 additions and 13 deletions
@@ -0,0 +1,68 @@
/*
Unit tests for the Recent Profile account cell renderer.
The account cell shows the profile's display name and avatar, links both to
the profile, and falls back to an identicon when the profile has no avatar.
Written against the same rendering 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 RecentProfileAccount = require('../../src/components/app-body/recent-profiles/recent-profile-account')
const ALICE = 'bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy'
const PROFILE_PATH = `/profile/${encodeURIComponent(ALICE)}`
function makeAccount (overrides = {}) {
return {
addr: ALICE,
displayName: 'alice',
avatarUrl: null,
profilePath: PROFILE_PATH,
...overrides
}
}
function render (account) {
return ReactDOMServer.renderToStaticMarkup(
React.createElement(RecentProfileAccount, { account })
)
}
test('renders nothing without an account', () => {
assert.equal(render(null), '')
})
test('renders the display name', () => {
const html = render(makeAccount())
assert.ok(html.includes('alice'))
})
test('renders the avatar image when the profile has an avatar URL', () => {
const html = render(makeAccount({ avatarUrl: 'https://example.com/alice.png' }))
assert.match(html, /<img[^>]+src="https:\/\/example\.com\/alice\.png"/)
assert.doesNotMatch(html, /recent-profile-identicon/)
})
test('renders an identicon when the profile has no avatar URL', () => {
const html = render(makeAccount({ avatarUrl: null }))
assert.match(html, /recent-profile-identicon/)
assert.match(html, /data-jdenticon-value/)
assert.doesNotMatch(html, /<img/)
})
test('links the avatar and display name to the profile', () => {
const html = render(makeAccount())
assert.ok(html.includes('class="recent-profile-avatar-link"'))
assert.ok(html.includes('class="recent-profile-name-link"'))
assert.equal(html.split(`href="${PROFILE_PATH}"`).length - 1, 2)
})
@@ -0,0 +1,73 @@
/*
Unit tests for the Recent Profiles table view model.
The /profile/recent response already carries each profile's display name
(name) and avatar URL (profilePicUrl), joined by the DB. These tests pin the
pure table/account view model independent of the React shell: the leftmost
Account column, its truncated-address fallback, and the preserved columns.
*/
'use strict'
const test = require('node:test')
const assert = require('node:assert/strict')
const {
PROFILE_PATH_PREFIX,
RECENT_PROFILES_TABLE_HEADERS,
profilePath,
accountDisplayName,
buildRecentProfileAccount,
buildRecentProfilesTable
} = require('../../src/services/recent-profiles-table')
const ALICE = 'bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy'
const DAVE = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d'
test('profilePath encodes the address for the profile route', () => {
assert.equal(profilePath(ALICE), `${PROFILE_PATH_PREFIX}/${encodeURIComponent(ALICE)}`)
})
test('accountDisplayName prefers the display name', () => {
assert.equal(accountDisplayName(ALICE, 'alice'), 'alice')
})
test('accountDisplayName falls back to the truncated address without a name', () => {
assert.equal(accountDisplayName(DAVE, null), 'bitcoincas...py26r63g3d')
assert.equal(accountDisplayName(DAVE, ''), 'bitcoincas...py26r63g3d')
})
test('buildRecentProfileAccount carries the name, avatar, and profile path', () => {
const account = buildRecentProfileAccount({
addr: ALICE,
name: 'alice',
profilePicUrl: 'https://example.com/alice.png'
})
assert.equal(account.addr, ALICE)
assert.equal(account.displayName, 'alice')
assert.equal(account.avatarUrl, 'https://example.com/alice.png')
assert.equal(account.profilePath, `/profile/${encodeURIComponent(ALICE)}`)
})
test('buildRecentProfileAccount reports a null avatar when the profile has no picture', () => {
const account = buildRecentProfileAccount({ addr: ALICE, name: 'alice', profilePicUrl: null })
assert.equal(account.avatarUrl, null)
})
test('the table headers put Account first and preserve the existing columns', () => {
assert.deepEqual(RECENT_PROFILES_TABLE_HEADERS, ['Account', 'Address', 'Bio', 'Block', 'Seen', 'TXID'])
})
test('buildRecentProfilesTable builds one row per profile with its account', () => {
const table = buildRecentProfilesTable([
{ addr: ALICE, name: 'alice', profilePicUrl: 'https://example.com/alice.png' },
{ addr: DAVE, name: null, profilePicUrl: 'https://example.com/dave.png' }
])
assert.deepEqual(table.headers, RECENT_PROFILES_TABLE_HEADERS)
assert.equal(table.rows.length, 2)
assert.equal(table.rows[0].addr, ALICE)
assert.equal(table.rows[0].account.displayName, 'alice')
assert.equal(table.rows[1].account.displayName, 'bitcoincas...py26r63g3d')
})