mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo.git
synced 2026-09-21 16:52:01 -07:00
Review feed tabs: consume controller snapshot, kill mutation survivors
Route the posts view through FeedTabsPage.getState() instead of reaching into the controller's internal fields, and consolidate the default-mode unit tests. Add initial-state and unloaded-pagination tests that kill the four constructor/canLoadMore mutation survivors (22 killed, 0 survived, 0 uncovered). By architect.
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
# acceptance-mutation-manifest-begin
|
||||
# {"version":1,"tested_at":"2026-09-20T17:33:46.020711102Z","feature_name":"Feed Tabs","feature_path":"/home/trout/work/psf-memo/.worktrees/architect/psf-memo-client/specs/feed-tabs.feature","background_hash":"0d66780cb1b8e277f0ada40a8ffe336dec7a8eaf658f19d2ea344815fb9bf26c","implementation_hash":"unknown","scenarios":[]}
|
||||
# acceptance-mutation-manifest-end
|
||||
|
||||
# Scenarios: Feed Tabs - 1, Feed Tabs - 2, Feed Tabs - 3, Feed Tabs - 4, Feed Tabs - 5, Feed Tabs - 6, Feed Tabs - 7, Feed Tabs - 8
|
||||
#
|
||||
# The posts feed at /posts/recent is the single posts page. It shows a row of
|
||||
|
||||
@@ -43,16 +43,18 @@ function RecentPosts (props) {
|
||||
const pageRef = useRef(null)
|
||||
|
||||
// Reflect a loaded controller page into React state, including the author
|
||||
// profiles needed by the post cards.
|
||||
// profiles needed by the post cards. The controller snapshot is the single
|
||||
// source of feed state; the view never reaches into its internal fields.
|
||||
const showPage = async (page) => {
|
||||
const addrs = collectPostAddrs(page.posts)
|
||||
const state = page.getState()
|
||||
const addrs = collectPostAddrs(state.posts)
|
||||
const profileMap = await loadThreadProfiles(addrs, page.memoDb)
|
||||
|
||||
setPosts(page.posts)
|
||||
setPagination(page.pagination)
|
||||
setMode(page.mode)
|
||||
setEmptyBecauseNoFollows(page.emptyBecauseNoFollows)
|
||||
setOffset(page.offset)
|
||||
setPosts(state.posts)
|
||||
setPagination(state.pagination)
|
||||
setMode(state.mode)
|
||||
setEmptyBecauseNoFollows(state.emptyBecauseNoFollows)
|
||||
setOffset(state.offset)
|
||||
setProfiles(profileMap)
|
||||
}
|
||||
|
||||
|
||||
@@ -155,3 +155,7 @@ FeedTabsPage.RECENT_MODE = RECENT_MODE
|
||||
FeedTabsPage.FOLLOWING_MODE = FOLLOWING_MODE
|
||||
|
||||
module.exports = FeedTabsPage
|
||||
|
||||
// mutate4javascript-manifest-begin
|
||||
// {"version":1,"tested_at":"2026-09-20T17:31:02.596Z","module_hash":"df20b58c9b7744a421ffa2e1b457fd1416fb44e11e8e969647bebc96bde7c3bc","functions":[{"id":"func/FeedTabsPage.constructor","name":"FeedTabsPage.constructor","line":21,"end_line":34,"hash":"5383666103ae9a7a9bd4618280a48f5ffdbb8dd3b3f731ceb0d102f1f8487d6b"},{"id":"func/FeedTabsPage.getMyAddress","name":"FeedTabsPage.getMyAddress","line":36,"end_line":38,"hash":"3e5d4ac4df379300933a772020528b4ecf4ed83c7386a066f5c270df81adcddd"},{"id":"func/FeedTabsPage.open","name":"FeedTabsPage.open","line":42,"end_line":58,"hash":"23ed42e89c0fe89af20d936fd0175d9728a2a671379970ef862ad90941fdd374"},{"id":"func/FeedTabsPage.selectTab","name":"FeedTabsPage.selectTab","line":63,"end_line":74,"hash":"99ff87aedeb0019b22459e950e2a99df2648477e6ef3053921a487b636b95f86"},{"id":"func/FeedTabsPage._loadMode","name":"FeedTabsPage._loadMode","line":77,"end_line":84,"hash":"99d59c57f03b3ebb6cc8e1dd4aab8df019a34712bee72cc742d208b6b6a8b8c3"},{"id":"func/FeedTabsPage._pageFor","name":"FeedTabsPage._pageFor","line":86,"end_line":88,"hash":"2ecf46b5da42fb117b0fd4a2fcf7621a6609d99c8f2d7220a41a22c15000ede7"},{"id":"func/FeedTabsPage._isEmptyFollowing","name":"FeedTabsPage._isEmptyFollowing","line":92,"end_line":94,"hash":"baef393ae6c886f86edb26a15ddb582d16ee862df42b730ef94378c2c19df991"},{"id":"func/FeedTabsPage._normalizeTab","name":"FeedTabsPage._normalizeTab","line":96,"end_line":101,"hash":"3f9e921fd03e8a121d2c9d7db84ca96f8ab348098e5510d461b19e3d9d120f98"},{"id":"func/FeedTabsPage.isRecent","name":"FeedTabsPage.isRecent","line":103,"end_line":105,"hash":"dd7b7035d81fb710da80bef4db78e68620b9a6dbea652c1d2d7f9b2bbb1bf812"},{"id":"func/FeedTabsPage.isFollowing","name":"FeedTabsPage.isFollowing","line":107,"end_line":109,"hash":"2e05c1dad4d206852fb5d84784a2926b3beec11bcd3a1115798c79db70db6ec4"},{"id":"func/FeedTabsPage.canLoadMore","name":"FeedTabsPage.canLoadMore","line":111,"end_line":113,"hash":"634983bcc6bbe560daad8326db0dd4bf31d5cb9e45c40112565351dceaf8e5d5"},{"id":"func/FeedTabsPage.nextPage","name":"FeedTabsPage.nextPage","line":115,"end_line":124,"hash":"c4f4db842625b89422158164ed4ad1a67971e592ca3e8e7109790b3e09a1746e"},{"id":"func/FeedTabsPage.previousPage","name":"FeedTabsPage.previousPage","line":126,"end_line":136,"hash":"d0ac6bcd201e128a16b1404116b665c3edeb0849b8ae8de1014892d1410638da"},{"id":"func/FeedTabsPage.getPost","name":"FeedTabsPage.getPost","line":138,"end_line":140,"hash":"1a6ae1a02b0f79b5b62a2b2324a5f1edbd743bec004fe73cfef7970bead56885"},{"id":"func/FeedTabsPage.getState","name":"FeedTabsPage.getState","line":142,"end_line":150,"hash":"84595f8c1f3f00e0e410c96fe1084801d1469a606378a8b15d5a3b6ca03f1bc9"}]}
|
||||
// mutate4javascript-manifest-end
|
||||
|
||||
@@ -57,32 +57,54 @@ async function openWithOnePost ({ following, feed, key }) {
|
||||
return { page, memoDb, post }
|
||||
}
|
||||
|
||||
// Assert that open selected exactly the feed implied by the follow state and
|
||||
// loaded only that feed's single post.
|
||||
function assertOnlySelectedFeedLoaded ({ page, memoDb, post, following }) {
|
||||
const follows = following.length > 0
|
||||
assert.equal(page.isFollowing(), follows)
|
||||
assert.equal(page.isRecent(), !follows)
|
||||
assert.deepEqual(page.posts, [post])
|
||||
assert.equal(memoDb.calls.getFollowingFeed.length, follows ? 1 : 0)
|
||||
assert.equal(memoDb.calls.getRecentPosts.length, follows ? 0 : 1)
|
||||
}
|
||||
|
||||
test('exposes the Recent and Following tabs in order', () => {
|
||||
const page = new FeedTabsPage({ memoDb: makeMemoDb(), wallet: makeWallet() })
|
||||
assert.deepEqual(page.tabs, ['Recent', 'Following'])
|
||||
})
|
||||
|
||||
test('open selects Following and loads followed posts when the viewer follows an account', async () => {
|
||||
const { page, memoDb, post } = await openWithOnePost({ following: [ALICE], feed: 'followingFeed', key: 'a' })
|
||||
test('constructor starts in a neutral, unloaded state', () => {
|
||||
const page = new FeedTabsPage({ memoDb: makeMemoDb(), wallet: makeWallet() })
|
||||
|
||||
assert.equal(page.isFollowing(), true)
|
||||
assert.equal(page.isRecent(), false)
|
||||
assert.deepEqual(page.posts, [post])
|
||||
assert.equal(memoDb.calls.getFollowing.length, 1)
|
||||
assert.equal(memoDb.calls.getRecentPosts.length, 0)
|
||||
assert.equal(memoDb.calls.getFollowingFeed.length, 1)
|
||||
assert.equal(page.mode, null)
|
||||
assert.equal(page.offset, 0)
|
||||
assert.deepEqual(page.posts, [])
|
||||
assert.equal(page.pagination, null)
|
||||
assert.equal(page.hasFollows, false)
|
||||
assert.equal(page.emptyBecauseNoFollows, false)
|
||||
})
|
||||
|
||||
test('open selects Recent and loads recent posts when the viewer follows no one', async () => {
|
||||
const { page, memoDb, post } = await openWithOnePost({ following: [], feed: 'recent', key: 'b' })
|
||||
test('canLoadMore is false before any page has been loaded', () => {
|
||||
const page = new FeedTabsPage({ memoDb: makeMemoDb(), wallet: makeWallet() })
|
||||
|
||||
assert.equal(page.isRecent(), true)
|
||||
assert.equal(page.isFollowing(), false)
|
||||
assert.deepEqual(page.posts, [post])
|
||||
assert.equal(memoDb.calls.getRecentPosts.length, 1)
|
||||
assert.equal(memoDb.calls.getFollowingFeed.length, 0)
|
||||
assert.equal(page.canLoadMore(), false)
|
||||
})
|
||||
|
||||
// The two default-mode cases differ only in the follow state and the feed
|
||||
// that should be loaded, so they run as one table.
|
||||
const DEFAULT_MODE_CASES = [
|
||||
{ label: 'the viewer follows an account', following: [ALICE], feed: 'followingFeed', key: 'a' },
|
||||
{ label: 'the viewer follows no one', following: [], feed: 'recent', key: 'b' }
|
||||
]
|
||||
|
||||
for (const testCase of DEFAULT_MODE_CASES) {
|
||||
test(`open selects the default feed and loads its posts when ${testCase.label}`, async () => {
|
||||
const { page, memoDb, post } = await openWithOnePost(testCase)
|
||||
|
||||
assertOnlySelectedFeedLoaded({ page, memoDb, post, following: testCase.following })
|
||||
})
|
||||
}
|
||||
|
||||
test('open asks the memo db which accounts the viewer follows', async () => {
|
||||
const memoDb = makeMemoDb({ following: [ALICE] })
|
||||
const page = new FeedTabsPage({ memoDb, wallet: makeWallet() })
|
||||
|
||||
Reference in New Issue
Block a user