statuses unit tests

This commit is contained in:
Henry Jameson 2026-08-18 18:00:36 +03:00
commit 1396eeb71b
23 changed files with 860 additions and 742 deletions

View file

@ -1,447 +0,0 @@
import { createTestingPinia } from '@pinia/testing'
import {
defaultState,
mutations,
prepareStatus,
} from '../../../../src/modules/statuses.js'
createTestingPinia()
const makeMockStatus = ({ id, text, type = 'status' }) => {
return {
id,
user: { id: '0' },
name: 'status',
text: text || `Text number ${id}`,
fave_num: 0,
uri: '',
type,
attentions: [],
}
}
describe('Statuses module', () => {
describe('prepareStatus', () => {
it('sets deleted flag to false', () => {
const aStatus = makeMockStatus({ id: '1', text: 'Hello oniichan' })
expect(prepareStatus(aStatus).deleted).to.eq(false)
})
})
describe('addNewStatuses', () => {
it('adds the status to allStatuses and to the given timeline', () => {
const state = defaultState()
const status = makeMockStatus({ id: '1' })
mutations.addNewStatuses(state, {
statuses: [status],
timeline: 'public',
})
expect(state.allStatuses).to.eql([status])
expect(state.timelines.public.statuses).to.eql([status])
expect(state.timelines.public.visibleStatuses).to.eql([])
expect(state.timelines.public.newStatusCount).to.equal(1)
})
it('counts the status as new if it has not been seen on this timeline', () => {
const state = defaultState()
const status = makeMockStatus({ id: '1' })
mutations.addNewStatuses(state, {
statuses: [status],
timeline: 'public',
})
mutations.addNewStatuses(state, {
statuses: [status],
timeline: 'friends',
})
expect(state.allStatuses).to.eql([status])
expect(state.timelines.public.statuses).to.eql([status])
expect(state.timelines.public.visibleStatuses).to.eql([])
expect(state.timelines.public.newStatusCount).to.equal(1)
expect(state.allStatuses).to.eql([status])
expect(state.timelines.friends.statuses).to.eql([status])
expect(state.timelines.friends.visibleStatuses).to.eql([])
expect(state.timelines.friends.newStatusCount).to.equal(1)
})
it('add the statuses to allStatuses if no timeline is given', () => {
const state = defaultState()
const status = makeMockStatus({ id: '1' })
mutations.addNewStatuses(state, { statuses: [status] })
expect(state.allStatuses).to.eql([status])
expect(state.timelines.public.statuses).to.eql([])
expect(state.timelines.public.visibleStatuses).to.eql([])
expect(state.timelines.public.newStatusCount).to.equal(0)
})
it('adds the status to allStatuses and to the given timeline, directly visible', () => {
const state = defaultState()
const status = makeMockStatus({ id: '1' })
mutations.addNewStatuses(state, {
statuses: [status],
showImmediately: true,
timeline: 'public',
})
expect(state.allStatuses).to.eql([status])
expect(state.timelines.public.statuses).to.eql([status])
expect(state.timelines.public.visibleStatuses).to.eql([status])
expect(state.timelines.public.newStatusCount).to.equal(0)
})
it('does not update the maxId when the noIdUpdate flag is set', () => {
const state = defaultState()
const status = makeMockStatus({ id: '1' })
const secondStatus = makeMockStatus({ id: '2' })
mutations.addNewStatuses(state, {
statuses: [status],
showImmediately: true,
timeline: 'public',
})
expect(state.timelines.public.maxId).to.equal('1')
mutations.addNewStatuses(state, {
statuses: [secondStatus],
showImmediately: true,
timeline: 'public',
noIdUpdate: true,
})
expect(state.timelines.public.statuses).to.eql([secondStatus, status])
expect(state.timelines.public.visibleStatuses).to.eql([
secondStatus,
status,
])
expect(state.timelines.public.maxId).to.equal('1')
})
it('keeps a descending by id order in timeline.visibleStatuses and timeline.statuses', () => {
const state = defaultState()
const nonVisibleStatus = makeMockStatus({ id: '1' })
const status = makeMockStatus({ id: '3' })
const statusTwo = makeMockStatus({ id: '2' })
const statusThree = makeMockStatus({ id: '4' })
mutations.addNewStatuses(state, {
statuses: [nonVisibleStatus],
showImmediately: false,
timeline: 'public',
})
mutations.addNewStatuses(state, {
statuses: [status],
showImmediately: true,
timeline: 'public',
})
mutations.addNewStatuses(state, {
statuses: [statusTwo],
showImmediately: true,
timeline: 'public',
})
expect(state.timelines.public.minVisibleId).to.equal('2')
mutations.addNewStatuses(state, {
statuses: [statusThree],
showImmediately: true,
timeline: 'public',
})
expect(state.timelines.public.statuses).to.eql([
statusThree,
status,
statusTwo,
nonVisibleStatus,
])
expect(state.timelines.public.visibleStatuses).to.eql([
statusThree,
status,
statusTwo,
])
})
it('splits retweets from their status and links them', () => {
const state = defaultState()
const status = makeMockStatus({ id: '1' })
const retweet = makeMockStatus({ id: '2', type: 'retweet' })
const modStatus = makeMockStatus({ id: '1', text: 'something else' })
retweet.retweeted_status = status
// It adds both statuses, but only the retweet to visible.
mutations.addNewStatuses(state, {
statuses: [retweet],
timeline: 'public',
showImmediately: true,
})
expect(state.timelines.public.visibleStatuses).to.have.length(1)
expect(state.timelines.public.statuses).to.have.length(1)
expect(state.allStatuses).to.have.length(2)
expect(state.allStatuses[0].id).to.equal('1')
expect(state.allStatuses[1].id).to.equal('2')
// It refers to the modified status.
mutations.addNewStatuses(state, {
statuses: [modStatus],
timeline: 'public',
})
expect(state.allStatuses).to.have.length(2)
expect(state.allStatuses[0].id).to.equal('1')
expect(state.allStatuses[0].text).to.equal(modStatus.text)
expect(state.allStatuses[1].id).to.equal('2')
expect(retweet.retweeted_status.text).to.eql(modStatus.text)
})
it('replaces existing statuses with the same id', () => {
const state = defaultState()
const status = makeMockStatus({ id: '1' })
const modStatus = makeMockStatus({ id: '1', text: 'something else' })
// Add original status
mutations.addNewStatuses(state, {
statuses: [status],
showImmediately: true,
timeline: 'public',
})
expect(state.timelines.public.visibleStatuses).to.have.length(1)
expect(state.allStatuses).to.have.length(1)
// Add new version of status
mutations.addNewStatuses(state, {
statuses: [modStatus],
showImmediately: true,
timeline: 'public',
})
expect(state.timelines.public.visibleStatuses).to.have.length(1)
expect(state.allStatuses).to.have.length(1)
expect(state.allStatuses[0].text).to.eql(modStatus.text)
})
it('replaces existing statuses with the same id, coming from a retweet', () => {
const state = defaultState()
const status = makeMockStatus({ id: '1' })
const modStatus = makeMockStatus({ id: '1', text: 'something else' })
const retweet = makeMockStatus({ id: '2', type: 'retweet' })
retweet.retweeted_status = modStatus
// Add original status
mutations.addNewStatuses(state, {
statuses: [status],
showImmediately: true,
timeline: 'public',
})
expect(state.timelines.public.visibleStatuses).to.have.length(1)
expect(state.allStatuses).to.have.length(1)
// Add new version of status
mutations.addNewStatuses(state, {
statuses: [retweet],
showImmediately: false,
timeline: 'public',
})
expect(state.timelines.public.visibleStatuses).to.have.length(1)
// Don't add the retweet itself if the tweet is visible
expect(state.timelines.public.statuses).to.have.length(1)
expect(state.allStatuses).to.have.length(2)
expect(state.allStatuses[0].text).to.eql(modStatus.text)
})
it('handles favorite actions', () => {
const state = defaultState()
const status = makeMockStatus({ id: '1' })
const favorite = {
id: '2',
type: 'favorite',
in_reply_to_status_id: '1', // The API uses strings here...
uri: 'tag:shitposter.club,2016-08-21:fave:3895:note:773501:2016-08-21T16:52:15+00:00',
text: 'a favorited something by b',
user: { id: '99' },
}
mutations.addNewStatuses(state, {
statuses: [status],
showImmediately: true,
timeline: 'public',
})
mutations.addNewStatuses(state, {
statuses: [favorite],
showImmediately: true,
timeline: 'public',
})
expect(state.timelines.public.visibleStatuses).to.have.length(1)
expect(state.timelines.public.visibleStatuses[0].fave_num).to.eql(1)
expect(state.timelines.public.maxId).to.eq(favorite.id)
// Adding it again does nothing
mutations.addNewStatuses(state, {
statuses: [favorite],
showImmediately: true,
timeline: 'public',
})
expect(state.timelines.public.visibleStatuses).to.have.length(1)
expect(state.timelines.public.visibleStatuses[0].fave_num).to.eql(1)
expect(state.timelines.public.maxId).to.eq(favorite.id)
// If something is favorited by the current user, it also sets the 'favorited' property but does not increment counter to avoid over-counting. Counter is incremented (updated, really) via response to the favorite request.
const user = {
id: '1',
}
const ownFavorite = {
id: '3',
type: 'favorite',
in_reply_to_status_id: '1', // The API uses strings here...
uri: 'tag:shitposter.club,2016-08-21:fave:3895:note:773501:2016-08-21T16:52:15+00:00',
text: 'a favorited something by b',
user,
}
mutations.addNewStatuses(state, {
statuses: [ownFavorite],
showImmediately: true,
timeline: 'public',
user,
})
expect(state.timelines.public.visibleStatuses).to.have.length(1)
expect(state.timelines.public.visibleStatuses[0].fave_num).to.eql(1)
expect(state.timelines.public.visibleStatuses[0].favorited).to.eql(true)
})
})
describe('emojiReactions', () => {
it('increments count in existing reaction', () => {
const state = defaultState()
const status = makeMockStatus({ id: '1' })
status.emoji_reactions = [{ name: '😂', count: 1, accounts: [] }]
mutations.addNewStatuses(state, {
statuses: [status],
showImmediately: true,
timeline: 'public',
})
mutations.addOwnReaction(state, {
id: '1',
emoji: '😂',
currentUser: { id: 'me' },
})
expect(state.allStatusesObject['1'].emoji_reactions[0].count).to.eql(2)
expect(state.allStatusesObject['1'].emoji_reactions[0].me).to.eql(true)
expect(
state.allStatusesObject['1'].emoji_reactions[0].accounts[0].id,
).to.equal('me')
})
it('adds a new reaction', () => {
const state = defaultState()
const status = makeMockStatus({ id: '1' })
status.emoji_reactions = []
mutations.addNewStatuses(state, {
statuses: [status],
showImmediately: true,
timeline: 'public',
})
mutations.addOwnReaction(state, {
id: '1',
emoji: '😂',
currentUser: { id: 'me' },
})
expect(state.allStatusesObject['1'].emoji_reactions[0].count).to.eql(1)
expect(state.allStatusesObject['1'].emoji_reactions[0].me).to.eql(true)
expect(
state.allStatusesObject['1'].emoji_reactions[0].accounts[0].id,
).to.equal('me')
})
it('decreases count in existing reaction', () => {
const state = defaultState()
const status = makeMockStatus({ id: '1' })
status.emoji_reactions = [
{ name: '😂', count: 2, accounts: [{ id: 'me' }] },
]
mutations.addNewStatuses(state, {
statuses: [status],
showImmediately: true,
timeline: 'public',
})
mutations.removeOwnReaction(state, {
id: '1',
emoji: '😂',
currentUser: { id: 'me' },
})
expect(state.allStatusesObject['1'].emoji_reactions[0].count).to.eql(1)
expect(state.allStatusesObject['1'].emoji_reactions[0].me).to.eql(false)
expect(state.allStatusesObject['1'].emoji_reactions[0].accounts).to.eql(
[],
)
})
it('removes a reaction', () => {
const state = defaultState()
const status = makeMockStatus({ id: '1' })
status.emoji_reactions = [
{ name: '😂', count: 1, accounts: [{ id: 'me' }] },
]
mutations.addNewStatuses(state, {
statuses: [status],
showImmediately: true,
timeline: 'public',
})
mutations.removeOwnReaction(state, {
id: '1',
emoji: '😂',
currentUser: { id: 'me' },
})
expect(state.allStatusesObject['1'].emoji_reactions).to.have.length(0)
})
})
describe('showNewStatuses', () => {
it('resets the minId to the min of the visible statuses when adding new to visible statuses', () => {
const state = defaultState()
const status = makeMockStatus({ id: '10' })
mutations.addNewStatuses(state, {
statuses: [status],
showImmediately: true,
timeline: 'public',
})
const newStatus = makeMockStatus({ id: '20' })
mutations.addNewStatuses(state, {
statuses: [newStatus],
showImmediately: false,
timeline: 'public',
})
state.timelines.public.minId = '5'
mutations.showNewStatuses(state, { timeline: 'public' })
expect(state.timelines.public.visibleStatuses).to.have.length(2)
expect(state.timelines.public.minVisibleId).to.equal('10')
expect(state.timelines.public.minId).to.equal('10')
})
})
describe('clearTimeline', () => {
it('keeps userId when clearing user timeline when excludeUserId param is true', () => {
const state = defaultState()
state.timelines.user.userId = 123
mutations.clearTimeline(state, { timeline: 'user', excludeUserId: true })
expect(state.timelines.user.userId).to.eql(123)
})
})
})

View file

@ -0,0 +1,603 @@
import { createTestingPinia } from '@pinia/testing'
import { snakeCase } from 'lodash'
import { setActivePinia } from 'pinia'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useUsersStore } from 'src/stores/users.js'
import * as PUBLIC_API from 'src/api/public.js'
import * as USER_API from 'src/api/user.js'
const userId = '1'
const userScreenName = 'user'
const userName = 'Guy'
const userUrl = 'http://localhost/user'
const mockMastoAPIUser = ({
screen_name = userScreenName,
name = userName,
url = userUrl,
id = userId,
} = {}) => ({
id,
acct: screen_name,
display_name: name,
fields: [],
avatar: '',
url,
pleroma: {
emoji_reactions: [],
},
})
const mockUser = ({
screen_name = userScreenName,
id = userId,
name = userName,
url = userUrl,
} = {}) => ({
_original: mockMastoAPIUser({
screen_name,
id,
name,
url,
}),
id,
name,
screen_name,
url,
relationship: undefined,
})
const mockStatus = ({
id = '1',
text,
type = 'status',
statusUser = mockUser(),
} = {}) => ({
id,
user: statusUser,
name: 'status',
text: text ?? `Text number ${id}`,
uri: '',
type,
attentions: [],
statusnet_conversation_id: 'c1',
emoji_reactions: [],
})
const mockMastoAPIStatus = ({
id = '1',
text,
type = 'status',
statusUser = mockMastoAPIUser(),
} = {}) => ({
id,
account: statusUser,
name: 'status',
content: text ?? `Text number ${id}`,
uri: '',
type,
attentions: [],
statusnet_conversation_id: 'c1',
})
const DEFAULT_OPTIONS = (method = 'GET') => ({
method,
credentials: 'same-origin',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
})
describe('Statuses store', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
})
describe('addNewStatuses', () => {
beforeEach(() => {
const usersStore = useUsersStore()
usersStore.addNewUsers = vi.fn().mockReturnValue([mockUser()])
})
it('adds the status to allStatuses', () => {
const store = useStatusesStore()
const usersStore = useUsersStore()
const status = mockStatus({ id: '1' })
store.addNewStatuses({
statuses: [status],
timestamp: 1,
})
expect(store.allStatuses).to.eql(new Map([['1', status]]))
expect(store.conversations).to.eql(new Map([['c1', new Set(['1'])]]))
expect(usersStore.addNewUsers).to.have.callCount(1)
})
it('splits retweets from their status and links them', () => {
const store = useStatusesStore()
const usersStore = useUsersStore()
const status = mockStatus({ id: '1' })
const retweet = mockStatus({
id: '2',
type: 'retweet',
user: mockUser({ id: '2' }),
})
retweet.type = 'retweet'
retweet.retweeted_status = status
store.addNewStatuses({
statuses: [retweet],
timestamp: 1,
})
expect(store.allStatuses).to.eql(
new Map([
['1', status],
['2', retweet],
]),
)
expect(usersStore.addNewUsers).to.have.callCount(2)
})
it('splits quotes from their status and links them', () => {
const store = useStatusesStore()
const usersStore = useUsersStore()
const status = mockStatus({ id: '1' })
const quote = mockStatus({ id: '2' })
quote.quote = status
store.addNewStatuses({
statuses: [quote],
timestamp: 1,
})
expect(store.allStatuses).to.eql(
new Map([
['1', status],
['2', quote],
]),
)
expect(usersStore.addNewUsers).to.have.callCount(2)
})
it('replaces existing statuses with the same id', () => {
const store = useStatusesStore()
const usersStore = useUsersStore()
const status = mockStatus({ id: '1' })
const modStatus = mockStatus({ id: '1', text: 'something else' })
store.addNewStatuses({
statuses: [status],
timestamp: 1991,
})
expect(store.allStatuses).to.eql(new Map([['1', status]]))
store.addNewStatuses({
statuses: [modStatus],
timestamp: 2000,
})
expect(store.allStatuses).to.eql(new Map([['1', modStatus]]))
expect(usersStore.addNewUsers).to.have.callCount(2)
})
it('handles edits', () => {
const store = useStatusesStore()
const usersStore = useUsersStore()
const status = mockStatus({ id: '1' })
const modStatus = mockStatus({
id: '1',
text: 'something else',
type: 'edit',
})
store.addNewStatuses({
statuses: [status],
timestamp: 1991,
})
expect(store.allStatuses).to.eql(new Map([['1', status]]))
store.addNewStatuses({
statuses: [modStatus],
timestamp: 2000,
})
expect(store.allStatuses).to.eql(new Map([['1', modStatus]]))
expect(usersStore.addNewUsers).to.have.callCount(2)
})
it('ignores updates with older timestamp', () => {
const store = useStatusesStore()
const usersStore = useUsersStore()
const status = mockStatus({ id: '1' })
const modStatus = mockStatus({ id: '1', text: 'something else' })
store.addNewStatuses({
statuses: [status],
timestamp: 2000,
})
expect(store.allStatuses).to.eql(new Map([['1', status]]))
store.addNewStatuses({
statuses: [modStatus],
timestamp: 1991,
})
expect(store.allStatuses).to.eql(new Map([['1', status]]))
expect(usersStore.addNewUsers).to.have.callCount(2)
})
it('calls useUsersStore().addNewUsers() even on older timestamp', () => {
const store = useStatusesStore()
const usersStore = useUsersStore()
const status = mockStatus({ id: '1' })
const modStatus = mockStatus({ id: '1', text: 'something else' })
store.addNewStatuses({
statuses: [status],
timestamp: 2000,
})
store.addNewStatuses({
statuses: [modStatus],
timestamp: 1991,
})
expect(usersStore.addNewUsers).to.have.callCount(2)
})
})
describe('fetchers', () => {
it.each([
[
'StatusSource',
{
content_type: 'text/plain',
text: 'Text',
spoiler_text: 'Text',
},
],
[
'EmojiReactions',
[
{
accounts: [mockMastoAPIUser()],
count: 1,
me: false,
name: 'cofe',
url: '',
},
],
],
['Favs', [mockMastoAPIUser()]],
['Repeats', [mockMastoAPIUser()]],
])('fetch%s', async (group, mockedResponse) => {
const mockFetch = vi.fn()
mockFetch.mockResolvedValueOnce(
new Response(JSON.stringify(mockedResponse), {
headers: { 'Content-Type': 'application/json' },
}),
)
vi.stubGlobal('fetch', mockFetch)
let urlKey
let prefix = 'MASTODON'
if (group === 'Favs') {
urlKey = 'STATUS_FAVORITEDBY'
} else if (group === 'Repeats') {
urlKey = 'STATUS_REBLOGGEDBY'
} else {
urlKey = snakeCase(group).toUpperCase()
}
if (group === 'EmojiReactions') {
prefix = 'PLEROMA'
}
const url = PUBLIC_API[`${prefix}_${urlKey}_URL`]('id')
const store = useStatusesStore()
const status = mockStatus({ id: 'id' })
store.addNewStatuses({
statuses: [status],
timestamp: 2000,
})
const result = await store[`fetch${group}`]('id')
const updated = store.allStatuses.get('id')
expect(mockFetch).to.have.been.calledWith(url, DEFAULT_OPTIONS())
if (group === 'Favs') {
expect(updated.favoritedBy).to.have.length(1)
expect(updated.fave_num).to.eql(1)
} else if (group === 'Repeats') {
expect(updated.rebloggedBy).to.have.length(1)
expect(updated.repeat_num).to.eql(1)
} else if (group === 'EmojiReactions') {
expect(updated.emoji_reactions).to.have.length(mockedResponse.length)
expect(updated.emoji_reactions[0].name).to.eql(mockedResponse[0].name)
} else {
expect(result).to.eql(mockedResponse)
}
})
it('fetchFavsAndRepeats', async () => {
const store = useStatusesStore()
store.fetchFavs = vi.fn().mockResolvedValue(async () => {
/* no-op */
})
store.fetchRepeats = vi.fn().mockResolvedValue(async () => {
/* no-op */
})
await store.fetchFavsAndRepeats('id')
expect(store.fetchFavs).to.have.been.calledWith('id')
expect(store.fetchRepeats).to.have.been.calledWith('id')
})
it('fetchStatus', async () => {
const mockFetch = vi.fn()
mockFetch.mockResolvedValueOnce(
new Response(JSON.stringify(mockMastoAPIStatus()), {
headers: { 'Content-Type': 'application/json' },
}),
)
vi.stubGlobal('fetch', mockFetch)
const store = useStatusesStore()
await store.fetchStatus('id')
expect(mockFetch).to.have.been.calledWith(
PUBLIC_API.MASTODON_STATUS_URL('id'),
DEFAULT_OPTIONS(),
)
expect(store.allStatuses).to.have.length(1)
})
})
describe('interactions', () => {
it.each([
['favorite', 'MASTODON_FAVORITE_URL'],
['unfavorite', 'MASTODON_UNFAVORITE_URL'],
['retweet', 'MASTODON_RETWEET_URL'],
['unretweet', 'MASTODON_UNRETWEET_URL'],
['reactWithEmoji', 'PLEROMA_EMOJI_REACT_URL', 'PUT'],
['unreactWithEmoji', 'PLEROMA_EMOJI_UNREACT_URL', 'DELETE'],
[
'bookmark',
'MASTODON_BOOKMARK_STATUS_URL',
undefined,
{ folder_id: 'argument' },
],
['unbookmark', 'MASTODON_UNBOOKMARK_STATUS_URL'],
['pinStatus', 'MASTODON_PIN_OWN_STATUS_URL'],
['unpinStatus', 'MASTODON_UNPIN_OWN_STATUS_URL'],
['muteConversation', 'MASTODON_MUTE_CONVERSATION_URL'],
['unmuteConversation', 'MASTODON_UNMUTE_CONVERSATION_URL'],
['deleteStatus', 'MASTODON_DELETE_URL', 'DELETE'],
])('%s - api call', async (interaction, urlKey, method = 'POST', body) => {
const url = USER_API[urlKey]('1', 'argument')
const status = mockStatus()
const store = useStatusesStore()
store.addNewStatuses({
statuses: [status],
timestamp: 1,
})
const mockFetch = vi.fn()
mockFetch.mockResolvedValueOnce(
new Response(JSON.stringify(mockMastoAPIStatus({ text: 'Updated' })), {
headers: { 'Content-Type': 'application/json' },
}),
)
vi.stubGlobal('fetch', mockFetch)
await store[interaction]('1', 'argument')
const expectedArg = DEFAULT_OPTIONS(method)
if (body) {
expectedArg.body = JSON.stringify(body)
}
expect(mockFetch).to.have.been.calledWith(url, expectedArg)
if (interaction === 'deleteStatus') {
expect(store.allStatuses.get('1').deleted).to.be.true
} else {
expect(store.allStatuses.get('1').text).to.eql('Updated')
}
})
const optimismInteractions = [
['favorite', 'favorited', 'fave_num'],
['retweet', 'repeated', 'repeat_num'],
['bookmark', 'bookmarked'],
['muteConversation', 'thread_muted'],
]
.map(([method, property, count]) => [
[method, property, count],
['un' + method, property, count],
])
.flat()
it.each(
optimismInteractions,
)('%s - optimism call', async (method, property, count) => {
// Prepare our status
const status = mockStatus()
const negate = method.startsWith('un')
const oldCount = 9
const newCount = negate ? 8 : 10
status[property] = negate
if (count) {
status[count] = oldCount
}
if (method === 'unbookmark') {
status.bookmark_folder_id = 'argument'
}
// Insert it
const store = useStatusesStore()
store.addNewStatuses({
statuses: [status],
timestamp: 1,
})
const mockFetch = vi.fn()
mockFetch.mockResolvedValueOnce(
new Response(JSON.stringify(mockMastoAPIStatus({ text: 'Updated' })), {
headers: { 'Content-Type': 'application/json' },
}),
)
expect(store.allStatuses.get('1')).to.have.property(property, negate)
if (count) {
expect(store.allStatuses.get('1')).to.have.property(count, oldCount)
}
vi.stubGlobal('fetch', mockFetch)
store[method]('1', 'argument')
expect(store.allStatuses.get('1')).to.have.property(property, !negate)
if (count) {
expect(store.allStatuses.get('1')).to.have.property(count, newCount)
}
if (property === 'bookmarked') {
expect(store.allStatuses.get('1')).to.have.property(
'bookmark_folder_id',
'argument',
)
}
})
it.each(
optimismInteractions,
)('%s - optimism fail', async (method, property, count) => {
// Prepare our status
const status = mockStatus()
const negate = method.startsWith('un')
const oldCount = 9
status[property] = negate
if (count) {
status[count] = oldCount
}
if (method === 'unbookmark') {
status.bookmark_folder_id = 'argument'
}
// Insert it
const store = useStatusesStore()
store.addNewStatuses({
statuses: [status],
timestamp: 1,
})
const mockFetch = vi.fn()
mockFetch.mockRejectedValueOnce(new Error('Failure!'))
expect(store.allStatuses.get('1')).to.have.property(property, negate)
if (count) {
expect(store.allStatuses.get('1')).to.have.property(count, oldCount)
}
vi.stubGlobal('fetch', mockFetch)
await store[method]('1', 'argument')
expect(store.allStatuses.get('1')).to.have.property(property, negate)
if (count) {
expect(store.allStatuses.get('1')).to.have.property(count, oldCount)
}
// Failing 'bookmark' method SHOULD clear folder id
if (method === 'unbookmark') {
expect(store.allStatuses.get('1')).to.have.property(
'bookmark_folder_id',
'argument',
)
}
})
it.each([
['reactWithEmoji', 0],
['unreactWithEmoji', 1],
['reactWithEmoji', 1],
['unreactWithEmoji', 2],
])('%s count: %s - optimism call', async (method, oldCount) => {
// Prepare our status
const status = mockStatus()
const negate = method.startsWith('un')
const newCount = negate ? oldCount - 1 : oldCount + 1
useUsersStore().currentUser = mockUser()
if (oldCount > 0) {
const reactors = [...new Array(oldCount)].map((empty, index) =>
mockUser({ id: 'o' + index }),
)
if (negate) {
// Replace one of reactors with ourselves
reactors[0] = useUsersStore().currentUser
}
status.emoji_reactions = [
{
name: 'hyperlol',
count: oldCount,
accounts: reactors,
},
]
} else {
status.emoji_reactions = []
}
// Insert it
const store = useStatusesStore()
store.addNewStatuses({
statuses: [status],
timestamp: 1,
})
const mockFetch = vi.fn()
mockFetch.mockResolvedValueOnce(
new Response(JSON.stringify(mockMastoAPIStatus({ text: 'Updated' })), {
headers: { 'Content-Type': 'application/json' },
}),
)
vi.stubGlobal('fetch', mockFetch)
const expected = store.allStatuses.get('1')
expect(expected.emoji_reactions).to.have.length(oldCount === 0 ? 0 : 1)
if (oldCount !== 0) {
expect(expected.emoji_reactions[0].name).to.eql('hyperlol')
expect(expected.emoji_reactions[0].count).to.eql(oldCount)
expect(expected.emoji_reactions[0].accounts).to.have.length(oldCount)
}
store[method]('1', 'hyperlol')
expect(expected.emoji_reactions).to.have.length(newCount === 0 ? 0 : 1)
if (newCount !== 0) {
expect(expected.emoji_reactions[0].name).to.eql('hyperlol')
expect(expected.emoji_reactions[0].count).to.eql(newCount)
expect(expected.emoji_reactions[0].accounts).to.have.length(newCount)
}
})
})
it('wipeUserStatuses', () => {
const store = useStatusesStore()
const statuses = [...new Array(20)].map((empty, index) =>
mockStatus({
id: 's' + index,
statusUser: mockUser({ id: 'u' + index }),
}),
)
store.addNewStatuses({
statuses,
timestamp: 1,
})
store.wipeUserStatuses('u19')
expect(store.allStatuses).to.have.length(19)
})
})

View file

@ -43,47 +43,47 @@ const actionKeys = (action) => {
return result
}
describe('The users store', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
})
const userId = '1'
const userScreenName = 'user'
const userName = 'Guy'
const userUrl = 'http://localhost/user'
const userId = '1'
const userScreenName = 'user'
const userName = 'Guy'
const userUrl = 'http://localhost/user'
const mockMastoAPIUser = ({
screen_name = userScreenName,
name = userName,
url = userUrl,
id = userId,
} = {}) => ({
id,
acct: screen_name,
display_name: name,
fields: [],
avatar: '',
url,
})
const mastoApiUser = ({
screen_name = userScreenName,
name = userName,
url = userUrl,
id = userId,
} = {}) => ({
id,
acct: screen_name,
display_name: name,
fields: [],
avatar: '',
url,
})
const user = ({
screen_name = userScreenName,
id = userId,
name = userName,
url = userUrl,
} = {}) => ({
_original: mastoApiUser({
screen_name,
id,
name,
url,
}),
const mockUser = ({
screen_name = userScreenName,
id = userId,
name = userName,
url = userUrl,
} = {}) => ({
_original: mockMastoAPIUser({
screen_name,
id,
name,
screen_name,
url,
relationship: undefined,
}),
id,
name,
screen_name,
url,
relationship: undefined,
})
describe('Users store', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
})
describe('addNewUsers', () => {
@ -91,9 +91,9 @@ describe('The users store', () => {
it('adds new users to the set, merging in new information for old users', () => {
const store = useUsersStore()
const modUser = user({ name: 'Dude' })
const modUser = mockUser({ name: 'Dude' })
store.addNewUsers({ data: [user()], timestamp: 1 })
store.addNewUsers({ data: [mockUser()], timestamp: 1 })
expect(store.users).to.have.length(1)
expect(store.users).to.have.all.keys(userId)
@ -106,9 +106,9 @@ describe('The users store', () => {
it('ignores new users if timestamp is older', () => {
const store = useUsersStore()
const modUser = user({ name: 'Old guy' })
const modUser = mockUser({ name: 'Old guy' })
store.addNewUsers({ data: [user()], timestamp: 2000 })
store.addNewUsers({ data: [mockUser()], timestamp: 2000 })
expect(store.users).to.have.length(1)
expect(store.users).to.have.all.keys(userId)
expect(store.users.get(userId).name).to.eql('Guy')
@ -123,18 +123,18 @@ describe('The users store', () => {
const store = useUsersStore()
const userFields = {
...user(),
...mockUser(),
fields: [{ name: 'Label 1', value: 'Content 1' }],
}
const firstModUser = {
...user(),
...mockUser(),
fields: [
{ name: 'Label 2', value: 'Content 2' },
{ name: 'Label 3', value: 'Content 3' },
],
}
const secondModUser = {
...user(),
...mockUser(),
fields: [{ name: 'Label 4', value: 'Content 4' }],
}
@ -159,14 +159,14 @@ describe('The users store', () => {
const store = useUsersStore()
const modUser = {
...user(),
...mockUser(),
relationship: {
id: userId,
following: true,
},
}
store.addNewUsers({ data: [user()], timestamp: 1 })
store.addNewUsers({ data: [mockUser()], timestamp: 1 })
store.addNewUsers({ data: [modUser], timestamp: 2 })
expect(store.relationships).to.have.length(1)
@ -179,14 +179,14 @@ describe('The users store', () => {
const store = useUsersStore()
const modUser = {
...user({ name: 'Old Dude' }),
...mockUser({ name: 'Old Dude' }),
relationship: {
id: userId,
following: true,
},
}
store.addNewUsers({ data: [user()], timestamp: 2000 })
store.addNewUsers({ data: [mockUser()], timestamp: 2000 })
store.addNewUsers({ data: [modUser], timestamp: 1991 })
expect(store.relationships).to.have.length(1)
@ -198,10 +198,12 @@ describe('The users store', () => {
it("doesn't erase relationship information if new data has it missing", () => {
const store = useUsersStore()
const modUser = user({ name: 'Dude' })
const modUser = mockUser({ name: 'Dude' })
store.addNewUsers({
data: [{ ...user(), relationship: { id: userId, following: true } }],
data: [
{ ...mockUser(), relationship: { id: userId, following: true } },
],
timestamp: 1,
})
store.addNewUsers({ data: [modUser], timestamp: 2 })
@ -219,7 +221,7 @@ describe('The users store', () => {
const store = useUsersStore()
const relationship = { id: userId, following: true }
store.addNewUsers({ data: [user()], timestamp: 1 })
store.addNewUsers({ data: [mockUser()], timestamp: 1 })
store.updateUserRelationships({ data: relationship, timestamp: 2 })
expect(store.relationship(userId)).to.eql({ id: userId, following: true })
@ -240,7 +242,7 @@ describe('The users store', () => {
const relationship = { id: userId, following: true }
store.updateUserRelationships({ data: relationship, timestamp: 1 })
store.addNewUsers({ data: [user()], timestamp: 2 })
store.addNewUsers({ data: [mockUser()], timestamp: 2 })
expect(store.relationship(userId)).to.eql({ id: userId, following: true })
expect(store.findUser(userId).relationship).to.eql({
@ -255,7 +257,7 @@ describe('The users store', () => {
const newRelationship = { id: userId, following: false }
store.addNewUsers({
data: [{ ...user(), relationship: newRelationship }],
data: [{ ...mockUser(), relationship: newRelationship }],
timestamp: 2000,
})
store.updateUserRelationships({ data: oldRelationship, timestamp: 1991 })
@ -274,13 +276,13 @@ describe('The users store', () => {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValueOnce(
new Response(JSON.stringify(mastoApiUser()), {
new Response(JSON.stringify(mockMastoAPIUser()), {
headers: { 'Content-Type': 'application/json' },
}),
),
)
const expected = user()
const expected = mockUser()
const store = useUsersStore()
const resultUser = await store.fetchUserIfMissing({ id: '1' })
@ -300,14 +302,14 @@ describe('The users store', () => {
)
// fetch by name yields user id which we request next
.mockResolvedValueOnce(
new Response(JSON.stringify(mastoApiUser()), {
new Response(JSON.stringify(mockMastoAPIUser()), {
headers: { 'Content-Type': 'application/json' },
}),
)
.mockThrowOnce(new Error("Shouldn't be called more than once")),
)
const expected = user()
const expected = mockUser()
const store = useUsersStore()
const resultUser1 = await store.fetchUserIfMissing({ name: 'user' })
const resultUser2 = await store.fetchUserIfMissing({ id: '1' })
@ -323,11 +325,11 @@ describe('The users store', () => {
vi.fn().mockThrowOnce(new Error("Shouldn't be called at all")),
)
const store = useUsersStore()
store.addNewUsers({ data: [user()], timestamp: 1 })
store.addNewUsers({ data: [mockUser()], timestamp: 1 })
const resultUser1 = await store.fetchUserIfMissing({ id: '1' })
const resultUser2 = await store.fetchUserIfMissing({ name: 'user' })
const expected = user()
const expected = mockUser()
expect(resultUser1).to.deep.include(expected)
expect(resultUser2).to.deep.include(expected)
@ -338,7 +340,7 @@ describe('The users store', () => {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValueOnce(
new Response(JSON.stringify(mastoApiUser()), {
new Response(JSON.stringify(mockMastoAPIUser()), {
status: 404,
statusText: 'Not Found',
headers: { 'Content-Type': 'application/json' },
@ -371,8 +373,12 @@ describe('The users store', () => {
.mockResolvedValueOnce(
new Response(
JSON.stringify([
mastoApiUser({ screen_name: 'snake', name: 'John', id: '2' }),
mastoApiUser({
mockMastoAPIUser({
screen_name: 'snake',
name: 'John',
id: '2',
}),
mockMastoAPIUser({
screen_name: 'zero',
name: 'David Oh',
id: '3',
@ -386,12 +392,12 @@ describe('The users store', () => {
.mockResolvedValueOnce(
new Response(
JSON.stringify([
mastoApiUser({
mockMastoAPIUser({
screen_name: 'sigint',
name: 'Mr.Anderson',
id: '4',
}),
mastoApiUser({
mockMastoAPIUser({
screen_name: 'paramedic',
name: 'Dr.Clark',
id: '5',
@ -406,7 +412,7 @@ describe('The users store', () => {
vi.stubGlobal('fetch', mockFetch)
const store = useUsersStore()
store.addNewUsers({ timestamp: 1, data: user() })
store.addNewUsers({ timestamp: 1, data: mockUser() })
const urlGroup = group === 'Friends' ? 'Following' : group
const us = store.users.get(userId)
@ -443,8 +449,12 @@ describe('The users store', () => {
.mockResolvedValueOnce(
new Response(
JSON.stringify([
mastoApiUser({ screen_name: 'snake', name: 'John', id: '2' }),
mastoApiUser({
mockMastoAPIUser({
screen_name: 'snake',
name: 'John',
id: '2',
}),
mockMastoAPIUser({
screen_name: 'zero',
name: 'David Oh',
id: '3',
@ -458,12 +468,12 @@ describe('The users store', () => {
.mockResolvedValueOnce(
new Response(
JSON.stringify([
mastoApiUser({
mockMastoAPIUser({
screen_name: 'sigint',
name: 'Mr.Anderson',
id: '4',
}),
mastoApiUser({
mockMastoAPIUser({
screen_name: 'paramedic',
name: 'Dr.Clark',
id: '5',
@ -478,7 +488,7 @@ describe('The users store', () => {
vi.stubGlobal('fetch', mockFetch)
const store = useUsersStore()
store.addNewUsers({ timestamp: 1, data: user() })
store.addNewUsers({ timestamp: 1, data: mockUser() })
const us = store.users.get(userId)
@ -514,7 +524,7 @@ describe('The users store', () => {
vi.stubGlobal('fetch', mockFetch)
const store = useUsersStore()
store.addNewUsers({ timestamp: 1, data: user() })
store.addNewUsers({ timestamp: 1, data: mockUser() })
const us = store.users.get(userId)
store.currentUser = us
@ -548,9 +558,9 @@ describe('The users store', () => {
store.addNewUsers({
timestamp: 1,
data: [
user(),
{ ...user({ name: 'John', screen_name: 'snake', id: '2' }) },
{ ...user({ name: 'David Oh', screen_name: 'zero', id: '3' }) },
mockUser(),
{ ...mockUser({ name: 'John', screen_name: 'snake', id: '2' }) },
{ ...mockUser({ name: 'David Oh', screen_name: 'zero', id: '3' }) },
],
})
@ -572,7 +582,7 @@ describe('The users store', () => {
describe('misc updates', () => {
it('updateUserAdminData', () => {
const store = useUsersStore()
store.addNewUsers({ data: [user()], timestamp: 1 })
store.addNewUsers({ data: [mockUser()], timestamp: 1 })
const adminData = { is_active: true, tags: ['one', 'two'] }
store.updateUserAdminData(userId, adminData)
@ -583,7 +593,7 @@ describe('The users store', () => {
it('updateRight', () => {
const store = useUsersStore()
store.addNewUsers({ data: [user()], timestamp: 1 })
store.addNewUsers({ data: [mockUser()], timestamp: 1 })
store.updateRight(userId, 'right1', true)
store.updateRight(userId, 'right2', false)
@ -594,7 +604,7 @@ describe('The users store', () => {
it('clearFollowLists', () => {
const store = useUsersStore()
store.addNewUsers({ data: [user()], timestamp: 1 })
store.addNewUsers({ data: [mockUser()], timestamp: 1 })
const userData = store.users.get(userId)
store.relationshipsLists.friends.get(userData).add('2')
store.relationshipsLists.friends.get(userData).add('3')
@ -613,7 +623,7 @@ describe('The users store', () => {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValueOnce(
new Response(JSON.stringify(mastoApiUser()), {
new Response(JSON.stringify(mockMastoAPIUser()), {
headers: { 'Content-Type': 'application/json' },
}),
),
@ -653,9 +663,9 @@ describe('The users store', () => {
// Adding some users to verify they are getting cleaned afterwards
store.addNewUsers({
data: [
user(),
{ ...user({ name: 'John', screen_name: 'snake' }) },
{ ...user({ name: 'David Oh', screen_name: 'zero' }) },
mockUser(),
{ ...mockUser({ name: 'John', screen_name: 'snake' }) },
{ ...mockUser({ name: 'David Oh', screen_name: 'zero' }) },
],
timestamp: 2000,
})
@ -679,7 +689,7 @@ describe('The users store', () => {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValueOnce(
new Response(JSON.stringify(mastoApiUser()), {
new Response(JSON.stringify(mockMastoAPIUser()), {
status: 403,
statusText: 'Forbidden',
headers: { 'Content-Type': 'application/json' },
@ -756,17 +766,17 @@ describe('The users store', () => {
useMergedConfigStore().mergedConfig = { useStreamingApi: true }
const store = useUsersStore()
store.currentUser = user()
store.currentUser = mockUser()
// Adding some users to verify they are getting cleaned afterwards
store.addNewUsers({
data: [
user(),
mockUser(),
{
...user({ name: 'John', screen_name: 'snake' }),
...mockUser({ name: 'John', screen_name: 'snake' }),
relationship: { id: userId, following: true },
},
{ ...user({ name: 'David Oh', screen_name: 'zero' }) },
{ ...mockUser({ name: 'David Oh', screen_name: 'zero' }) },
],
timestamp: 2000,
})
@ -1168,7 +1178,7 @@ describe('The users store', () => {
vi.stubGlobal('fetch', mockFetch)
const store = useUsersStore()
store.currentUser = user()
store.currentUser = mockUser()
store.currentUser.domainMutes = new Set()
if (action === 'unmute') {
store.currentUser.domainMutes.add('example.com')
@ -1198,14 +1208,6 @@ describe('The users store', () => {
'blockUser',
'unblockUser',
])('%ss', async (action) => {
const mockFetch = vi.fn().mockResolvedValueOnce(
new Response(JSON.stringify({ id: userId }), {
headers: { 'Content-Type': 'application/json' },
}),
)
vi.stubGlobal('fetch', mockFetch)
const store = useUsersStore()
store[action] = vi.fn().mockResolvedValue(async () => {
/* no-op */
@ -1226,30 +1228,30 @@ describe('The users store', () => {
it('relationship returns a placeholder if relationship info is missing while user is present', () => {
const store = useUsersStore()
store.addNewUsers({ data: [user()], timestamp: 1 })
store.addNewUsers({ data: [mockUser()], timestamp: 1 })
expect(store.relationship(userId)).to.eql({ id: userId, loading: true })
})
it('findUser returns user with matching id', () => {
const store = useUsersStore()
store.addNewUsers({ data: [user()], timestamp: 1 })
store.addNewUsers({ data: [mockUser()], timestamp: 1 })
expect(store.findUser(userId).id).to.eql(userId)
})
it('findUserByName returns user with matching screen_name', () => {
const store = useUsersStore()
store.addNewUsers({ data: [user()], timestamp: 1 })
store.addNewUsers({ data: [mockUser()], timestamp: 1 })
expect(store.findUserByName(user().screen_name).id).to.eql(userId)
expect(store.findUserByName(mockUser().screen_name).id).to.eql(userId)
})
it('findUserByName returns user with matching url', () => {
const store = useUsersStore()
store.addNewUsers({ data: [user()], timestamp: 1 })
store.addNewUsers({ data: [mockUser()], timestamp: 1 })
expect(store.findUserByUrl(user().url).id).to.eql(userId)
expect(store.findUserByUrl(mockUser().url).id).to.eql(userId)
})
})
})