Files
psf-memo/psf-memo-client/test/unit/topic-discovery-page.test.js
T
Chris Troutner dd11964174 Refactor topic metadata: cut CRAP, share test doubles, harden property tests
- Replace the ternary follower delta in recordTopicFollow with
  Number(isActive) - Number(wasActive), dropping its cyclomatic complexity
  from 8 to 6 (CRAP 8.0 -> 6.0). No behavior change.
- Extend the indexer topic property test to conserve lastSeen (newest post
  time) and followerCount (each address's final active follow state)
  alongside postCount/lastHeight, with varying post seen times.
- Extend the DB backfill and topic-query property tests to conserve lastSeen
  and followerCount; generated follows now include unfollows.
- Add a client topic-metadata property test covering relativeTime bucket
  classification, pluralization, monotonicity, and getLastSeenLabel
  consistency, and cover ensureTopicRoom's existing-summary path plus
  recordTopicFollow's legacy-follower-count fallback.
- Share one in-memory DB double per component (indexer test/support/
  memory-db.js, DB test/support/level-double.js) and table-drive the
  parallel topic-follow/topic-message/topic-query cases, clearing every
  dry4javascript candidate in the changed files.

Verification: indexer 113 unit / 12 property / 7 acceptance; db 387 unit /
57 property / 16 acceptance; client 441 unit / 90 property / 33 acceptance
+ build; lint clean. CRAP max 6.0; mutation scans under 100 sites/file.

By refactorer.
2026-09-18 06:02:13 -07:00

160 lines
4.6 KiB
JavaScript

/*
Unit tests for the topic discovery page controller.
*/
'use strict'
const test = require('node:test')
const assert = require('node:assert/strict')
const TopicDiscoveryPage = require('../../src/services/topic-discovery-page')
function makeMemoDb ({ topics = [], pagination = null } = {}) {
const calls = []
return {
calls,
async getTopics (opts) {
calls.push(opts)
return { topics, pagination }
}
}
}
// Build a controller over a fixed topic list and run its initial load.
async function loadPage (topics) {
const page = new TopicDiscoveryPage({ memoDb: makeMemoDb({ topics }) })
await page.load()
return page
}
test('load returns topics with post counts', async () => {
const topics = [
{ room: 'bitcoin', postCount: 2 },
{ room: 'cash', postCount: 1 }
]
const page = new TopicDiscoveryPage({ memoDb: makeMemoDb({ topics }) })
const result = await page.load()
assert.deepEqual(result.topics, topics)
})
test('load defaults to a page of 50 from offset 0', async () => {
const memoDb = makeMemoDb({ topics: [] })
const page = new TopicDiscoveryPage({ memoDb })
await page.load()
assert.deepEqual(memoDb.calls[0], { limit: 50, offset: 0 })
})
test('load forwards limit and offset and stores pagination', async () => {
const memoDb = makeMemoDb({
topics: [{ room: 'bitcoin', postCount: 2 }],
pagination: { limit: 50, offset: 50, total: 60, hasMore: false }
})
const page = new TopicDiscoveryPage({ memoDb })
const result = await page.load({ limit: 50, offset: 50 })
assert.deepEqual(memoDb.calls[0], { limit: 50, offset: 50 })
assert.deepEqual(result.pagination, { limit: 50, offset: 50, total: 60, hasMore: false })
assert.equal(page.pagination.offset, 50)
})
test('canLoadMore reflects the pagination hasMore flag', async () => {
const more = new TopicDiscoveryPage({
memoDb: makeMemoDb({
topics: [{ room: 'a', postCount: 0 }],
pagination: { limit: 50, offset: 0, total: 60, hasMore: true }
})
})
await more.load()
assert.equal(more.canLoadMore(), true)
const last = new TopicDiscoveryPage({
memoDb: makeMemoDb({
topics: [{ room: 'a', postCount: 0 }],
pagination: { limit: 50, offset: 50, total: 60, hasMore: false }
})
})
await last.load()
assert.equal(last.canLoadMore(), false)
})
test('load throws when no memo db client is provided', async () => {
const page = new TopicDiscoveryPage({})
await assert.rejects(
() => page.load(),
/requires a memo db client/
)
})
test('stores the provided navigate function', () => {
const navigate = () => {}
const page = new TopicDiscoveryPage({ memoDb: makeMemoDb({ topics: [] }), navigate })
assert.equal(page.navigate, navigate)
})
test('getTopic returns the matching topic', async () => {
const topics = [
{ room: 'bitcoin', postCount: 2 },
{ room: 'cash', postCount: 1 }
]
const page = await loadPage(topics)
assert.deepEqual(page.getTopic('bitcoin'), { room: 'bitcoin', postCount: 2 })
})
test('getTopic returns null when the topic is not loaded', async () => {
const page = await loadPage([])
assert.equal(page.getTopic('bitcoin'), null)
})
test('getLastSeenLabel formats the topic last-seen time', async () => {
const page = await loadPage([{ room: 'bitcoin', postCount: 1, lastSeen: 1799998200000 }])
assert.equal(page.getLastSeenLabel('bitcoin', 1800000000000), 'Less than an hour ago')
})
test('getLastSeenLabel returns "No posts" for a topic with no posts', async () => {
const page = await loadPage([{ room: 'lone', postCount: 0, lastSeen: 0 }])
assert.equal(page.getLastSeenLabel('lone', 1800000000000), 'No posts')
})
test('getLastSeenLabel returns null for an unknown topic', async () => {
const page = await loadPage([])
assert.equal(page.getLastSeenLabel('missing', 1800000000000), null)
})
test('openTopic navigates to the encoded topic feed path', () => {
const calls = []
const page = new TopicDiscoveryPage({
memoDb: makeMemoDb({ topics: [] }),
navigate: (path) => calls.push(path)
})
const result = page.openTopic('space room')
assert.deepEqual(result, { path: '/topics/space%20room' })
assert.deepEqual(calls, ['/topics/space%20room'])
})
test('topicFeedPath percent-encodes the room name', () => {
assert.equal(TopicDiscoveryPage.topicFeedPath('a/b'), '/topics/a%2Fb')
})
test('openTopic uses a no-op navigate by default', () => {
const page = new TopicDiscoveryPage({ memoDb: makeMemoDb({ topics: [] }) })
assert.deepEqual(page.openTopic('bitcoin'), { path: '/topics/bitcoin' })
})
test('exposes the topics page path', () => {
assert.equal(TopicDiscoveryPage.TOPICS_PATH, '/topics')
})