634 lines
17 KiB
JavaScript
634 lines
17 KiB
JavaScript
import { createTestingPinia } from '@pinia/testing'
|
|
import { snakeCase } from 'lodash'
|
|
import { setActivePinia } from 'pinia'
|
|
|
|
import { useStatusesStore } from 'src/stores/statuses.js'
|
|
import { useStreamingStore } from 'src/stores/streaming.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 }))
|
|
})
|
|
|
|
it('init', () => {
|
|
const store = useStatusesStore()
|
|
const sub = vi.fn()
|
|
useStreamingStore().addSubscriber = sub
|
|
|
|
store.attachSocket()
|
|
|
|
expect(store.socket).to.not.be.null
|
|
expect(sub).to.have.been.called
|
|
})
|
|
|
|
it('resetStatuses', () => {
|
|
const store = useStatusesStore()
|
|
const statuses = [...new Array(20)].map((empty, index) =>
|
|
mockStatus({
|
|
id: 's' + index,
|
|
statusUser: mockUser({ id: 'u' + index }),
|
|
}),
|
|
)
|
|
|
|
store.attachSocket()
|
|
store.addNewStatuses({
|
|
statuses,
|
|
timestamp: 1,
|
|
})
|
|
store.resetStatuses()
|
|
expect(store.allStatuses).to.have.length(0)
|
|
})
|
|
|
|
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,
|
|
})
|
|
|
|
const result = store.wipeUserStatuses('u19')
|
|
|
|
expect(store.allStatuses).to.have.length(19)
|
|
expect(result).to.eql(new Set(['s19']))
|
|
})
|
|
})
|