atproto/packages/api/tests/atp-agent.test.ts
dan a3ce23c4cc
Add hotness as a default thread sort option (#3082)
* Add hotness as a thread sorting option

* Changeset
2024-11-22 18:36:55 -08:00

3345 lines
93 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { TestNetworkNoAppView } from '@atproto/dev-env'
import { TID } from '@atproto/common-web'
import {
AppBskyActorDefs,
AppBskyActorProfile,
AtpAgent,
ComAtprotoRepoPutRecord,
DEFAULT_LABEL_SETTINGS,
} from '../src'
import {
getSavedFeedType,
savedFeedsToUriArrays,
validateSavedFeed,
} from '../src/util'
describe('agent', () => {
let network: TestNetworkNoAppView
beforeAll(async () => {
network = await TestNetworkNoAppView.create({
dbPostgresSchema: 'api_atp_agent',
})
})
afterAll(async () => {
await network.close()
})
const getProfileDisplayName = async (
agent: AtpAgent,
): Promise<string | undefined> => {
try {
const res = await agent.app.bsky.actor.profile.get({
repo: agent.accountDid,
rkey: 'self',
})
return res.value.displayName ?? ''
} catch (err) {
return undefined
}
}
it('clones correctly', () => {
const agent = new AtpAgent({ service: network.pds.url })
const agent2 = agent.clone()
expect(agent2 instanceof AtpAgent).toBeTruthy()
expect(agent.service).toEqual(agent2.service)
})
it('upsertProfile correctly creates and updates profiles.', async () => {
const agent = new AtpAgent({ service: network.pds.url })
await agent.createAccount({
handle: 'user1.test',
email: 'user1@test.com',
password: 'password',
})
const displayName1 = await getProfileDisplayName(agent)
expect(displayName1).toBeFalsy()
await agent.upsertProfile((existing) => {
expect(existing).toBeFalsy()
return {
displayName: 'Bob',
}
})
const displayName2 = await getProfileDisplayName(agent)
expect(displayName2).toBe('Bob')
await agent.upsertProfile((existing) => {
expect(existing).toBeTruthy()
return {
displayName: existing?.displayName?.toUpperCase(),
}
})
const displayName3 = await getProfileDisplayName(agent)
expect(displayName3).toBe('BOB')
})
it('upsertProfile correctly handles CAS failures.', async () => {
const agent = new AtpAgent({ service: network.pds.url })
await agent.createAccount({
handle: 'user2.test',
email: 'user2@test.com',
password: 'password',
})
const displayName1 = await getProfileDisplayName(agent)
expect(displayName1).toBeFalsy()
let hasConflicted = false
let ranTwice = false
await agent.upsertProfile(async (_existing) => {
if (!hasConflicted) {
await agent.com.atproto.repo.putRecord({
repo: agent.accountDid,
collection: 'app.bsky.actor.profile',
rkey: 'self',
record: {
$type: 'app.bsky.actor.profile',
displayName: String(Math.random()),
},
})
hasConflicted = true
} else {
ranTwice = true
}
return {
displayName: 'Bob',
}
})
expect(ranTwice).toBe(true)
const displayName2 = await getProfileDisplayName(agent)
expect(displayName2).toBe('Bob')
})
it('upsertProfile wont endlessly retry CAS failures.', async () => {
const agent = new AtpAgent({ service: network.pds.url })
await agent.createAccount({
handle: 'user3.test',
email: 'user3@test.com',
password: 'password',
})
const displayName1 = await getProfileDisplayName(agent)
expect(displayName1).toBeFalsy()
const p = agent.upsertProfile(async (_existing) => {
await agent.com.atproto.repo.putRecord({
repo: agent.accountDid,
collection: 'app.bsky.actor.profile',
rkey: 'self',
record: {
$type: 'app.bsky.actor.profile',
displayName: String(Math.random()),
},
})
return {
displayName: 'Bob',
}
})
await expect(p).rejects.toThrow(ComAtprotoRepoPutRecord.InvalidSwapError)
})
it('upsertProfile validates the record.', async () => {
const agent = new AtpAgent({ service: network.pds.url })
await agent.createAccount({
handle: 'user4.test',
email: 'user4@test.com',
password: 'password',
})
const p = agent.upsertProfile((_existing) => {
return {
displayName: { string: 'Bob' },
} as unknown as AppBskyActorProfile.Record
})
await expect(p).rejects.toThrow('Record/displayName must be a string')
})
describe('app', () => {
it('should retrieve the api app', () => {
const agent = new AtpAgent({ service: network.pds.url })
expect(agent.api).toBe(agent)
expect(agent.app).toBeDefined()
})
})
describe('post', () => {
it('should throw if no session', async () => {
const agent = new AtpAgent({ service: network.pds.url })
await expect(agent.post({ text: 'foo' })).rejects.toThrow('Not logged in')
})
})
describe('deletePost', () => {
it('should throw if no session', async () => {
const agent = new AtpAgent({ service: network.pds.url })
await expect(agent.deletePost('foo')).rejects.toThrow('Not logged in')
})
})
describe('like', () => {
it('should throw if no session', async () => {
const agent = new AtpAgent({ service: network.pds.url })
await expect(agent.like('foo', 'bar')).rejects.toThrow('Not logged in')
})
})
describe('deleteLike', () => {
it('should throw if no session', async () => {
const agent = new AtpAgent({ service: network.pds.url })
await expect(agent.deleteLike('foo')).rejects.toThrow('Not logged in')
})
})
describe('repost', () => {
it('should throw if no session', async () => {
const agent = new AtpAgent({ service: network.pds.url })
await expect(agent.repost('foo', 'bar')).rejects.toThrow('Not logged in')
})
})
describe('deleteRepost', () => {
it('should throw if no session', async () => {
const agent = new AtpAgent({ service: network.pds.url })
await expect(agent.deleteRepost('foo')).rejects.toThrow('Not logged in')
})
})
describe('follow', () => {
it('should throw if no session', async () => {
const agent = new AtpAgent({ service: network.pds.url })
await expect(agent.follow('foo')).rejects.toThrow('Not logged in')
})
})
describe('deleteFollow', () => {
it('should throw if no session', async () => {
const agent = new AtpAgent({ service: network.pds.url })
await expect(agent.deleteFollow('foo')).rejects.toThrow('Not logged in')
})
})
describe('preferences methods', () => {
it('gets and sets preferences correctly', async () => {
const agent = new AtpAgent({ service: network.pds.url })
await agent.createAccount({
handle: 'user5.test',
email: 'user5@test.com',
password: 'password',
})
const DEFAULT_LABELERS = AtpAgent.appLabelers.map((did) => ({
did,
labels: {},
}))
await expect(agent.getPreferences()).resolves.toStrictEqual({
feeds: { pinned: undefined, saved: undefined },
savedFeeds: [
{
id: expect.any(String),
pinned: true,
type: 'timeline',
value: 'following',
},
],
moderationPrefs: {
adultContentEnabled: false,
labels: DEFAULT_LABEL_SETTINGS,
labelers: DEFAULT_LABELERS,
mutedWords: [],
hiddenPosts: [],
},
birthDate: undefined,
feedViewPrefs: {
home: {
hideReplies: false,
hideRepliesByUnfollowed: true,
hideRepliesByLikeCount: 0,
hideReposts: false,
hideQuotePosts: false,
},
},
threadViewPrefs: {
sort: 'hotness',
prioritizeFollowedUsers: true,
},
interests: {
tags: [],
},
bskyAppState: {
activeProgressGuide: undefined,
queuedNudges: [],
nuxs: [],
},
})
await agent.setAdultContentEnabled(true)
await expect(agent.getPreferences()).resolves.toStrictEqual({
feeds: { pinned: undefined, saved: undefined },
savedFeeds: [
{
id: expect.any(String),
pinned: true,
type: 'timeline',
value: 'following',
},
],
moderationPrefs: {
adultContentEnabled: true,
labels: DEFAULT_LABEL_SETTINGS,
labelers: DEFAULT_LABELERS,
mutedWords: [],
hiddenPosts: [],
},
birthDate: undefined,
feedViewPrefs: {
home: {
hideReplies: false,
hideRepliesByUnfollowed: true,
hideRepliesByLikeCount: 0,
hideReposts: false,
hideQuotePosts: false,
},
},
threadViewPrefs: {
sort: 'hotness',
prioritizeFollowedUsers: true,
},
interests: {
tags: [],
},
bskyAppState: {
activeProgressGuide: undefined,
queuedNudges: [],
nuxs: [],
},
})
await agent.setAdultContentEnabled(false)
await expect(agent.getPreferences()).resolves.toStrictEqual({
feeds: { pinned: undefined, saved: undefined },
savedFeeds: [
{
id: expect.any(String),
pinned: true,
type: 'timeline',
value: 'following',
},
],
moderationPrefs: {
adultContentEnabled: false,
labels: DEFAULT_LABEL_SETTINGS,
labelers: DEFAULT_LABELERS,
mutedWords: [],
hiddenPosts: [],
},
birthDate: undefined,
feedViewPrefs: {
home: {
hideReplies: false,
hideRepliesByUnfollowed: true,
hideRepliesByLikeCount: 0,
hideReposts: false,
hideQuotePosts: false,
},
},
threadViewPrefs: {
sort: 'hotness',
prioritizeFollowedUsers: true,
},
interests: {
tags: [],
},
bskyAppState: {
activeProgressGuide: undefined,
queuedNudges: [],
nuxs: [],
},
})
await agent.setContentLabelPref('misinfo', 'hide')
await expect(agent.getPreferences()).resolves.toStrictEqual({
feeds: { pinned: undefined, saved: undefined },
savedFeeds: [
{
id: expect.any(String),
pinned: true,
type: 'timeline',
value: 'following',
},
],
moderationPrefs: {
adultContentEnabled: false,
labels: { ...DEFAULT_LABEL_SETTINGS, misinfo: 'hide' },
labelers: DEFAULT_LABELERS,
mutedWords: [],
hiddenPosts: [],
},
birthDate: undefined,
feedViewPrefs: {
home: {
hideReplies: false,
hideRepliesByUnfollowed: true,
hideRepliesByLikeCount: 0,
hideReposts: false,
hideQuotePosts: false,
},
},
threadViewPrefs: {
sort: 'hotness',
prioritizeFollowedUsers: true,
},
interests: {
tags: [],
},
bskyAppState: {
activeProgressGuide: undefined,
queuedNudges: [],
nuxs: [],
},
})
await agent.setContentLabelPref('spam', 'ignore')
await expect(agent.getPreferences()).resolves.toStrictEqual({
feeds: { pinned: undefined, saved: undefined },
savedFeeds: [
{
id: expect.any(String),
pinned: true,
type: 'timeline',
value: 'following',
},
],
moderationPrefs: {
adultContentEnabled: false,
labels: {
...DEFAULT_LABEL_SETTINGS,
misinfo: 'hide',
spam: 'ignore',
},
labelers: DEFAULT_LABELERS,
mutedWords: [],
hiddenPosts: [],
},
birthDate: undefined,
feedViewPrefs: {
home: {
hideReplies: false,
hideRepliesByUnfollowed: true,
hideRepliesByLikeCount: 0,
hideReposts: false,
hideQuotePosts: false,
},
},
threadViewPrefs: {
sort: 'hotness',
prioritizeFollowedUsers: true,
},
interests: {
tags: [],
},
bskyAppState: {
activeProgressGuide: undefined,
queuedNudges: [],
nuxs: [],
},
})
await agent.addSavedFeed('at://bob.com/app.bsky.feed.generator/fake')
await expect(agent.getPreferences()).resolves.toStrictEqual({
savedFeeds: [
{
id: expect.any(String),
pinned: true,
type: 'timeline',
value: 'following',
},
],
feeds: {
pinned: [],
saved: ['at://bob.com/app.bsky.feed.generator/fake'],
},
moderationPrefs: {
adultContentEnabled: false,
labels: {
...DEFAULT_LABEL_SETTINGS,
misinfo: 'hide',
spam: 'ignore',
},
labelers: DEFAULT_LABELERS,
mutedWords: [],
hiddenPosts: [],
},
birthDate: undefined,
feedViewPrefs: {
home: {
hideReplies: false,
hideRepliesByUnfollowed: true,
hideRepliesByLikeCount: 0,
hideReposts: false,
hideQuotePosts: false,
},
},
threadViewPrefs: {
sort: 'hotness',
prioritizeFollowedUsers: true,
},
interests: {
tags: [],
},
bskyAppState: {
activeProgressGuide: undefined,
queuedNudges: [],
nuxs: [],
},
})
await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake')
await expect(agent.getPreferences()).resolves.toStrictEqual({
savedFeeds: [
{
id: expect.any(String),
pinned: true,
type: 'timeline',
value: 'following',
},
],
feeds: {
pinned: ['at://bob.com/app.bsky.feed.generator/fake'],
saved: ['at://bob.com/app.bsky.feed.generator/fake'],
},
moderationPrefs: {
adultContentEnabled: false,
labels: {
...DEFAULT_LABEL_SETTINGS,
misinfo: 'hide',
spam: 'ignore',
},
labelers: DEFAULT_LABELERS,
mutedWords: [],
hiddenPosts: [],
},
birthDate: undefined,
feedViewPrefs: {
home: {
hideReplies: false,
hideRepliesByUnfollowed: true,
hideRepliesByLikeCount: 0,
hideReposts: false,
hideQuotePosts: false,
},
},
threadViewPrefs: {
sort: 'hotness',
prioritizeFollowedUsers: true,
},
interests: {
tags: [],
},
bskyAppState: {
activeProgressGuide: undefined,
queuedNudges: [],
nuxs: [],
},
})
await agent.removePinnedFeed('at://bob.com/app.bsky.feed.generator/fake')
await expect(agent.getPreferences()).resolves.toStrictEqual({
savedFeeds: [
{
id: expect.any(String),
pinned: true,
type: 'timeline',
value: 'following',
},
],
feeds: {
pinned: [],
saved: ['at://bob.com/app.bsky.feed.generator/fake'],
},
moderationPrefs: {
adultContentEnabled: false,
labels: {
...DEFAULT_LABEL_SETTINGS,
misinfo: 'hide',
spam: 'ignore',
},
labelers: DEFAULT_LABELERS,
mutedWords: [],
hiddenPosts: [],
},
birthDate: undefined,
feedViewPrefs: {
home: {
hideReplies: false,
hideRepliesByUnfollowed: true,
hideRepliesByLikeCount: 0,
hideReposts: false,
hideQuotePosts: false,
},
},
threadViewPrefs: {
sort: 'hotness',
prioritizeFollowedUsers: true,
},
interests: {
tags: [],
},
bskyAppState: {
activeProgressGuide: undefined,
queuedNudges: [],
nuxs: [],
},
})
await agent.removeSavedFeed('at://bob.com/app.bsky.feed.generator/fake')
await expect(agent.getPreferences()).resolves.toStrictEqual({
savedFeeds: [
{
id: expect.any(String),
pinned: true,
type: 'timeline',
value: 'following',
},
],
feeds: {
pinned: [],
saved: [],
},
moderationPrefs: {
adultContentEnabled: false,
labels: {
...DEFAULT_LABEL_SETTINGS,
misinfo: 'hide',
spam: 'ignore',
},
labelers: DEFAULT_LABELERS,
mutedWords: [],
hiddenPosts: [],
},
birthDate: undefined,
feedViewPrefs: {
home: {
hideReplies: false,
hideRepliesByUnfollowed: true,
hideRepliesByLikeCount: 0,
hideReposts: false,
hideQuotePosts: false,
},
},
threadViewPrefs: {
sort: 'hotness',
prioritizeFollowedUsers: true,
},
interests: {
tags: [],
},
bskyAppState: {
activeProgressGuide: undefined,
queuedNudges: [],
nuxs: [],
},
})
await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake')
await expect(agent.getPreferences()).resolves.toStrictEqual({
savedFeeds: [
{
id: expect.any(String),
pinned: true,
type: 'timeline',
value: 'following',
},
],
feeds: {
pinned: ['at://bob.com/app.bsky.feed.generator/fake'],
saved: ['at://bob.com/app.bsky.feed.generator/fake'],
},
moderationPrefs: {
adultContentEnabled: false,
labels: {
...DEFAULT_LABEL_SETTINGS,
misinfo: 'hide',
spam: 'ignore',
},
labelers: DEFAULT_LABELERS,
mutedWords: [],
hiddenPosts: [],
},
birthDate: undefined,
feedViewPrefs: {
home: {
hideReplies: false,
hideRepliesByUnfollowed: true,
hideRepliesByLikeCount: 0,
hideReposts: false,
hideQuotePosts: false,
},
},
threadViewPrefs: {
sort: 'hotness',
prioritizeFollowedUsers: true,
},
interests: {
tags: [],
},
bskyAppState: {
activeProgressGuide: undefined,
queuedNudges: [],
nuxs: [],
},
})
await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake2')
await expect(agent.getPreferences()).resolves.toStrictEqual({
savedFeeds: [
{
id: expect.any(String),
pinned: true,
type: 'timeline',
value: 'following',
},
],
feeds: {
pinned: [
'at://bob.com/app.bsky.feed.generator/fake',
'at://bob.com/app.bsky.feed.generator/fake2',
],
saved: [
'at://bob.com/app.bsky.feed.generator/fake',
'at://bob.com/app.bsky.feed.generator/fake2',
],
},
moderationPrefs: {
adultContentEnabled: false,
labels: {
...DEFAULT_LABEL_SETTINGS,
misinfo: 'hide',
spam: 'ignore',
},
labelers: DEFAULT_LABELERS,
mutedWords: [],
hiddenPosts: [],
},
birthDate: undefined,
feedViewPrefs: {
home: {
hideReplies: false,
hideRepliesByUnfollowed: true,
hideRepliesByLikeCount: 0,
hideReposts: false,
hideQuotePosts: false,
},
},
threadViewPrefs: {
sort: 'hotness',
prioritizeFollowedUsers: true,
},
interests: {
tags: [],
},
bskyAppState: {
activeProgressGuide: undefined,
queuedNudges: [],
nuxs: [],
},
})
await agent.removeSavedFeed('at://bob.com/app.bsky.feed.generator/fake')
await expect(agent.getPreferences()).resolves.toStrictEqual({
savedFeeds: [
{
id: expect.any(String),
pinned: true,
type: 'timeline',
value: 'following',
},
],
feeds: {
pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],
saved: ['at://bob.com/app.bsky.feed.generator/fake2'],
},
moderationPrefs: {
adultContentEnabled: false,
labels: {
...DEFAULT_LABEL_SETTINGS,
misinfo: 'hide',
spam: 'ignore',
},
labelers: DEFAULT_LABELERS,
mutedWords: [],
hiddenPosts: [],
},
birthDate: undefined,
feedViewPrefs: {
home: {
hideReplies: false,
hideRepliesByUnfollowed: true,
hideRepliesByLikeCount: 0,
hideReposts: false,
hideQuotePosts: false,
},
},
threadViewPrefs: {
sort: 'hotness',
prioritizeFollowedUsers: true,
},
interests: {
tags: [],
},
bskyAppState: {
activeProgressGuide: undefined,
queuedNudges: [],
nuxs: [],
},
})
await agent.setPersonalDetails({ birthDate: '2023-09-11T18:05:42.556Z' })
await expect(agent.getPreferences()).resolves.toStrictEqual({
savedFeeds: [
{
id: expect.any(String),
pinned: true,
type: 'timeline',
value: 'following',
},
],
feeds: {
pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],
saved: ['at://bob.com/app.bsky.feed.generator/fake2'],
},
moderationPrefs: {
adultContentEnabled: false,
labels: {
...DEFAULT_LABEL_SETTINGS,
misinfo: 'hide',
spam: 'ignore',
},
labelers: DEFAULT_LABELERS,
mutedWords: [],
hiddenPosts: [],
},
birthDate: new Date('2023-09-11T18:05:42.556Z'),
feedViewPrefs: {
home: {
hideReplies: false,
hideRepliesByUnfollowed: true,
hideRepliesByLikeCount: 0,
hideReposts: false,
hideQuotePosts: false,
},
},
threadViewPrefs: {
sort: 'hotness',
prioritizeFollowedUsers: true,
},
interests: {
tags: [],
},
bskyAppState: {
activeProgressGuide: undefined,
queuedNudges: [],
nuxs: [],
},
})
await agent.setFeedViewPrefs('home', { hideReplies: true })
await expect(agent.getPreferences()).resolves.toStrictEqual({
savedFeeds: [
{
id: expect.any(String),
pinned: true,
type: 'timeline',
value: 'following',
},
],
feeds: {
pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],
saved: ['at://bob.com/app.bsky.feed.generator/fake2'],
},
moderationPrefs: {
adultContentEnabled: false,
labels: {
...DEFAULT_LABEL_SETTINGS,
misinfo: 'hide',
spam: 'ignore',
},
labelers: DEFAULT_LABELERS,
mutedWords: [],
hiddenPosts: [],
},
birthDate: new Date('2023-09-11T18:05:42.556Z'),
feedViewPrefs: {
home: {
hideReplies: true,
hideRepliesByUnfollowed: true,
hideRepliesByLikeCount: 0,
hideReposts: false,
hideQuotePosts: false,
},
},
threadViewPrefs: {
sort: 'hotness',
prioritizeFollowedUsers: true,
},
interests: {
tags: [],
},
bskyAppState: {
activeProgressGuide: undefined,
queuedNudges: [],
nuxs: [],
},
})
await agent.setFeedViewPrefs('home', { hideReplies: false })
await expect(agent.getPreferences()).resolves.toStrictEqual({
savedFeeds: [
{
id: expect.any(String),
pinned: true,
type: 'timeline',
value: 'following',
},
],
feeds: {
pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],
saved: ['at://bob.com/app.bsky.feed.generator/fake2'],
},
moderationPrefs: {
adultContentEnabled: false,
labels: {
...DEFAULT_LABEL_SETTINGS,
misinfo: 'hide',
spam: 'ignore',
},
labelers: DEFAULT_LABELERS,
mutedWords: [],
hiddenPosts: [],
},
birthDate: new Date('2023-09-11T18:05:42.556Z'),
feedViewPrefs: {
home: {
hideReplies: false,
hideRepliesByUnfollowed: true,
hideRepliesByLikeCount: 0,
hideReposts: false,
hideQuotePosts: false,
},
},
threadViewPrefs: {
sort: 'hotness',
prioritizeFollowedUsers: true,
},
interests: {
tags: [],
},
bskyAppState: {
activeProgressGuide: undefined,
queuedNudges: [],
nuxs: [],
},
})
await agent.setFeedViewPrefs('other', { hideReplies: true })
await expect(agent.getPreferences()).resolves.toStrictEqual({
savedFeeds: [
{
id: expect.any(String),
pinned: true,
type: 'timeline',
value: 'following',
},
],
feeds: {
pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],
saved: ['at://bob.com/app.bsky.feed.generator/fake2'],
},
moderationPrefs: {
adultContentEnabled: false,
labels: {
...DEFAULT_LABEL_SETTINGS,
misinfo: 'hide',
spam: 'ignore',
},
labelers: DEFAULT_LABELERS,
mutedWords: [],
hiddenPosts: [],
},
birthDate: new Date('2023-09-11T18:05:42.556Z'),
feedViewPrefs: {
home: {
hideReplies: false,
hideRepliesByUnfollowed: true,
hideRepliesByLikeCount: 0,
hideReposts: false,
hideQuotePosts: false,
},
other: {
hideReplies: true,
hideRepliesByUnfollowed: true,
hideRepliesByLikeCount: 0,
hideReposts: false,
hideQuotePosts: false,
},
},
threadViewPrefs: {
sort: 'hotness',
prioritizeFollowedUsers: true,
},
interests: {
tags: [],
},
bskyAppState: {
activeProgressGuide: undefined,
queuedNudges: [],
nuxs: [],
},
})
await agent.setThreadViewPrefs({ sort: 'random' })
await expect(agent.getPreferences()).resolves.toStrictEqual({
savedFeeds: [
{
id: expect.any(String),
pinned: true,
type: 'timeline',
value: 'following',
},
],
feeds: {
pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],
saved: ['at://bob.com/app.bsky.feed.generator/fake2'],
},
moderationPrefs: {
adultContentEnabled: false,
labels: {
...DEFAULT_LABEL_SETTINGS,
misinfo: 'hide',
spam: 'ignore',
},
labelers: DEFAULT_LABELERS,
mutedWords: [],
hiddenPosts: [],
},
birthDate: new Date('2023-09-11T18:05:42.556Z'),
feedViewPrefs: {
home: {
hideReplies: false,
hideRepliesByUnfollowed: true,
hideRepliesByLikeCount: 0,
hideReposts: false,
hideQuotePosts: false,
},
other: {
hideReplies: true,
hideRepliesByUnfollowed: true,
hideRepliesByLikeCount: 0,
hideReposts: false,
hideQuotePosts: false,
},
},
threadViewPrefs: {
sort: 'random',
prioritizeFollowedUsers: true,
},
interests: {
tags: [],
},
bskyAppState: {
activeProgressGuide: undefined,
queuedNudges: [],
nuxs: [],
},
})
await agent.setThreadViewPrefs({ sort: 'oldest' })
await expect(agent.getPreferences()).resolves.toStrictEqual({
savedFeeds: [
{
id: expect.any(String),
pinned: true,
type: 'timeline',
value: 'following',
},
],
feeds: {
pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],
saved: ['at://bob.com/app.bsky.feed.generator/fake2'],
},
moderationPrefs: {
adultContentEnabled: false,
labels: {
...DEFAULT_LABEL_SETTINGS,
misinfo: 'hide',
spam: 'ignore',
},
labelers: DEFAULT_LABELERS,
mutedWords: [],
hiddenPosts: [],
},
birthDate: new Date('2023-09-11T18:05:42.556Z'),
feedViewPrefs: {
home: {
hideReplies: false,
hideRepliesByUnfollowed: true,
hideRepliesByLikeCount: 0,
hideReposts: false,
hideQuotePosts: false,
},
other: {
hideReplies: true,
hideRepliesByUnfollowed: true,
hideRepliesByLikeCount: 0,
hideReposts: false,
hideQuotePosts: false,
},
},
threadViewPrefs: {
sort: 'oldest',
prioritizeFollowedUsers: true,
},
interests: {
tags: [],
},
bskyAppState: {
activeProgressGuide: undefined,
queuedNudges: [],
nuxs: [],
},
})
await agent.setInterestsPref({ tags: ['foo', 'bar'] })
await expect(agent.getPreferences()).resolves.toStrictEqual({
savedFeeds: [
{
id: expect.any(String),
pinned: true,
type: 'timeline',
value: 'following',
},
],
feeds: {
pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],
saved: ['at://bob.com/app.bsky.feed.generator/fake2'],
},
moderationPrefs: {
adultContentEnabled: false,
labels: {
...DEFAULT_LABEL_SETTINGS,
misinfo: 'hide',
spam: 'ignore',
},
labelers: DEFAULT_LABELERS,
mutedWords: [],
hiddenPosts: [],
},
birthDate: new Date('2023-09-11T18:05:42.556Z'),
feedViewPrefs: {
home: {
hideReplies: false,
hideRepliesByUnfollowed: true,
hideRepliesByLikeCount: 0,
hideReposts: false,
hideQuotePosts: false,
},
other: {
hideReplies: true,
hideRepliesByUnfollowed: true,
hideRepliesByLikeCount: 0,
hideReposts: false,
hideQuotePosts: false,
},
},
threadViewPrefs: {
sort: 'oldest',
prioritizeFollowedUsers: true,
},
interests: {
tags: ['foo', 'bar'],
},
bskyAppState: {
activeProgressGuide: undefined,
queuedNudges: [],
nuxs: [],
},
})
})
it('resolves duplicates correctly', async () => {
const agent = new AtpAgent({ service: network.pds.url })
await agent.createAccount({
handle: 'user6.test',
email: 'user6@test.com',
password: 'password',
})
await agent.app.bsky.actor.putPreferences({
preferences: [
{
$type: 'app.bsky.actor.defs#contentLabelPref',
label: 'porn',
visibility: 'show',
},
{
$type: 'app.bsky.actor.defs#contentLabelPref',
label: 'porn',
visibility: 'hide',
},
{
$type: 'app.bsky.actor.defs#contentLabelPref',
label: 'porn',
visibility: 'show',
},
{
$type: 'app.bsky.actor.defs#contentLabelPref',
label: 'porn',
visibility: 'warn',
},
{
$type: 'app.bsky.actor.defs#labelersPref',
labelers: [
{
did: 'did:plc:first-labeler',
},
],
},
{
$type: 'app.bsky.actor.defs#labelersPref',
labelers: [
{
did: 'did:plc:first-labeler',
},
{
did: 'did:plc:other',
},
],
},
{
$type: 'app.bsky.actor.defs#adultContentPref',
enabled: true,
},
{
$type: 'app.bsky.actor.defs#adultContentPref',
enabled: false,
},
{
$type: 'app.bsky.actor.defs#adultContentPref',
enabled: true,
},
{
$type: 'app.bsky.actor.defs#savedFeedsPref',
pinned: [
'at://bob.com/app.bsky.feed.generator/fake',
'at://bob.com/app.bsky.feed.generator/fake2',
],
saved: [
'at://bob.com/app.bsky.feed.generator/fake',
'at://bob.com/app.bsky.feed.generator/fake2',
],
},
{
$type: 'app.bsky.actor.defs#savedFeedsPref',
pinned: [],
saved: [],
},
{
$type: 'app.bsky.actor.defs#personalDetailsPref',
birthDate: '2023-09-11T18:05:42.556Z',
},
{
$type: 'app.bsky.actor.defs#personalDetailsPref',
birthDate: '2021-09-11T18:05:42.556Z',
},
{
$type: 'app.bsky.actor.defs#feedViewPref',
feed: 'home',
hideReplies: false,
hideRepliesByUnfollowed: true,
hideRepliesByLikeCount: 0,
hideReposts: false,
hideQuotePosts: false,
},
{
$type: 'app.bsky.actor.defs#feedViewPref',
feed: 'home',
hideReplies: true,
hideRepliesByUnfollowed: false,
hideRepliesByLikeCount: 10,
hideReposts: true,
hideQuotePosts: true,
},
{
$type: 'app.bsky.actor.defs#threadViewPref',
sort: 'oldest',
prioritizeFollowedUsers: true,
},
{
$type: 'app.bsky.actor.defs#threadViewPref',
sort: 'newest',
prioritizeFollowedUsers: false,
},
{
$type: 'app.bsky.actor.defs#bskyAppStatePref',
queuedNudges: ['one'],
},
{
$type: 'app.bsky.actor.defs#bskyAppStatePref',
activeProgressGuide: undefined,
queuedNudges: ['two'],
},
],
})
await expect(agent.getPreferences()).resolves.toStrictEqual({
savedFeeds: [
{
id: expect.any(String),
type: 'timeline',
value: 'following',
pinned: true,
},
],
feeds: {
pinned: [],
saved: [],
},
moderationPrefs: {
adultContentEnabled: true,
labels: {
...DEFAULT_LABEL_SETTINGS,
porn: 'warn',
},
labelers: [
...AtpAgent.appLabelers.map((did) => ({ did, labels: {} })),
{
did: 'did:plc:first-labeler',
labels: {},
},
{
did: 'did:plc:other',
labels: {},
},
],
mutedWords: [],
hiddenPosts: [],
},
birthDate: new Date('2021-09-11T18:05:42.556Z'),
feedViewPrefs: {
home: {
hideReplies: true,
hideRepliesByUnfollowed: false,
hideRepliesByLikeCount: 10,
hideReposts: true,
hideQuotePosts: true,
},
},
threadViewPrefs: {
sort: 'newest',
prioritizeFollowedUsers: false,
},
interests: {
tags: [],
},
bskyAppState: {
activeProgressGuide: undefined,
queuedNudges: ['two'],
nuxs: [],
},
})
await agent.setAdultContentEnabled(false)
await expect(agent.getPreferences()).resolves.toStrictEqual({
savedFeeds: [
{
id: expect.any(String),
type: 'timeline',
value: 'following',
pinned: true,
},
],
feeds: {
pinned: [],
saved: [],
},
moderationPrefs: {
adultContentEnabled: false,
labels: {
...DEFAULT_LABEL_SETTINGS,
porn: 'warn',
},
labelers: [
...AtpAgent.appLabelers.map((did) => ({ did, labels: {} })),
{
did: 'did:plc:first-labeler',
labels: {},
},
{
did: 'did:plc:other',
labels: {},
},
],
mutedWords: [],
hiddenPosts: [],
},
birthDate: new Date('2021-09-11T18:05:42.556Z'),
feedViewPrefs: {
home: {
hideReplies: true,
hideRepliesByUnfollowed: false,
hideRepliesByLikeCount: 10,
hideReposts: true,
hideQuotePosts: true,
},
},
threadViewPrefs: {
sort: 'newest',
prioritizeFollowedUsers: false,
},
interests: {
tags: [],
},
bskyAppState: {
activeProgressGuide: undefined,
queuedNudges: ['two'],
nuxs: [],
},
})
await agent.setContentLabelPref('porn', 'ignore')
await expect(agent.getPreferences()).resolves.toStrictEqual({
savedFeeds: [
{
id: expect.any(String),
type: 'timeline',
value: 'following',
pinned: true,
},
],
feeds: {
pinned: [],
saved: [],
},
moderationPrefs: {
adultContentEnabled: false,
labels: {
...DEFAULT_LABEL_SETTINGS,
nsfw: 'ignore',
porn: 'ignore',
},
labelers: [
...AtpAgent.appLabelers.map((did) => ({ did, labels: {} })),
{
did: 'did:plc:first-labeler',
labels: {},
},
{
did: 'did:plc:other',
labels: {},
},
],
mutedWords: [],
hiddenPosts: [],
},
birthDate: new Date('2021-09-11T18:05:42.556Z'),
feedViewPrefs: {
home: {
hideReplies: true,
hideRepliesByUnfollowed: false,
hideRepliesByLikeCount: 10,
hideReposts: true,
hideQuotePosts: true,
},
},
threadViewPrefs: {
sort: 'newest',
prioritizeFollowedUsers: false,
},
interests: {
tags: [],
},
bskyAppState: {
activeProgressGuide: undefined,
queuedNudges: ['two'],
nuxs: [],
},
})
await agent.removeLabeler('did:plc:other')
await expect(agent.getPreferences()).resolves.toStrictEqual({
savedFeeds: [
{
id: expect.any(String),
type: 'timeline',
value: 'following',
pinned: true,
},
],
feeds: {
pinned: [],
saved: [],
},
moderationPrefs: {
adultContentEnabled: false,
labels: {
...DEFAULT_LABEL_SETTINGS,
nsfw: 'ignore',
porn: 'ignore',
},
labelers: [
...AtpAgent.appLabelers.map((did) => ({ did, labels: {} })),
{
did: 'did:plc:first-labeler',
labels: {},
},
],
mutedWords: [],
hiddenPosts: [],
},
birthDate: new Date('2021-09-11T18:05:42.556Z'),
feedViewPrefs: {
home: {
hideReplies: true,
hideRepliesByUnfollowed: false,
hideRepliesByLikeCount: 10,
hideReposts: true,
hideQuotePosts: true,
},
},
threadViewPrefs: {
sort: 'newest',
prioritizeFollowedUsers: false,
},
interests: {
tags: [],
},
bskyAppState: {
activeProgressGuide: undefined,
queuedNudges: ['two'],
nuxs: [],
},
})
await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake')
await expect(agent.getPreferences()).resolves.toStrictEqual({
feeds: {
pinned: ['at://bob.com/app.bsky.feed.generator/fake'],
saved: ['at://bob.com/app.bsky.feed.generator/fake'],
},
savedFeeds: [
{
id: expect.any(String),
pinned: true,
type: 'timeline',
value: 'following',
},
],
moderationPrefs: {
adultContentEnabled: false,
labels: {
...DEFAULT_LABEL_SETTINGS,
nsfw: 'ignore',
porn: 'ignore',
},
labelers: [
...AtpAgent.appLabelers.map((did) => ({ did, labels: {} })),
{
did: 'did:plc:first-labeler',
labels: {},
},
],
mutedWords: [],
hiddenPosts: [],
},
birthDate: new Date('2021-09-11T18:05:42.556Z'),
feedViewPrefs: {
home: {
hideReplies: true,
hideRepliesByUnfollowed: false,
hideRepliesByLikeCount: 10,
hideReposts: true,
hideQuotePosts: true,
},
},
threadViewPrefs: {
sort: 'newest',
prioritizeFollowedUsers: false,
},
interests: {
tags: [],
},
bskyAppState: {
activeProgressGuide: undefined,
queuedNudges: ['two'],
nuxs: [],
},
})
await agent.setPersonalDetails({ birthDate: '2023-09-11T18:05:42.556Z' })
await expect(agent.getPreferences()).resolves.toStrictEqual({
feeds: {
pinned: ['at://bob.com/app.bsky.feed.generator/fake'],
saved: ['at://bob.com/app.bsky.feed.generator/fake'],
},
savedFeeds: [
{
id: expect.any(String),
pinned: true,
type: 'timeline',
value: 'following',
},
],
moderationPrefs: {
adultContentEnabled: false,
labels: {
...DEFAULT_LABEL_SETTINGS,
nsfw: 'ignore',
porn: 'ignore',
},
labelers: [
...AtpAgent.appLabelers.map((did) => ({ did, labels: {} })),
{
did: 'did:plc:first-labeler',
labels: {},
},
],
mutedWords: [],
hiddenPosts: [],
},
birthDate: new Date('2023-09-11T18:05:42.556Z'),
feedViewPrefs: {
home: {
hideReplies: true,
hideRepliesByUnfollowed: false,
hideRepliesByLikeCount: 10,
hideReposts: true,
hideQuotePosts: true,
},
},
threadViewPrefs: {
sort: 'newest',
prioritizeFollowedUsers: false,
},
interests: {
tags: [],
},
bskyAppState: {
activeProgressGuide: undefined,
queuedNudges: ['two'],
nuxs: [],
},
})
await agent.setFeedViewPrefs('home', {
hideReplies: false,
hideRepliesByUnfollowed: true,
hideRepliesByLikeCount: 0,
hideReposts: false,
hideQuotePosts: false,
})
await agent.setThreadViewPrefs({
sort: 'oldest',
prioritizeFollowedUsers: true,
})
await agent.setPersonalDetails({ birthDate: '2023-09-11T18:05:42.556Z' })
await agent.bskyAppQueueNudges('three')
await expect(agent.getPreferences()).resolves.toStrictEqual({
feeds: {
pinned: ['at://bob.com/app.bsky.feed.generator/fake'],
saved: ['at://bob.com/app.bsky.feed.generator/fake'],
},
savedFeeds: [
{
id: expect.any(String),
pinned: true,
type: 'timeline',
value: 'following',
},
],
moderationPrefs: {
adultContentEnabled: false,
labels: {
...DEFAULT_LABEL_SETTINGS,
nsfw: 'ignore',
porn: 'ignore',
},
labelers: [
...AtpAgent.appLabelers.map((did) => ({ did, labels: {} })),
{
did: 'did:plc:first-labeler',
labels: {},
},
],
mutedWords: [],
hiddenPosts: [],
},
birthDate: new Date('2023-09-11T18:05:42.556Z'),
feedViewPrefs: {
home: {
hideReplies: false,
hideRepliesByUnfollowed: true,
hideRepliesByLikeCount: 0,
hideReposts: false,
hideQuotePosts: false,
},
},
threadViewPrefs: {
sort: 'oldest',
prioritizeFollowedUsers: true,
},
interests: {
tags: [],
},
bskyAppState: {
activeProgressGuide: undefined,
queuedNudges: ['two', 'three'],
nuxs: [],
},
})
const res = await agent.app.bsky.actor.getPreferences()
expect(res.data.preferences.sort(byType)).toStrictEqual(
[
{
$type: 'app.bsky.actor.defs#bskyAppStatePref',
queuedNudges: ['two', 'three'],
},
{
$type: 'app.bsky.actor.defs#adultContentPref',
enabled: false,
},
{
$type: 'app.bsky.actor.defs#contentLabelPref',
label: 'porn',
visibility: 'ignore',
},
{
$type: 'app.bsky.actor.defs#contentLabelPref',
label: 'nsfw',
visibility: 'ignore',
},
{
$type: 'app.bsky.actor.defs#labelersPref',
labelers: [
{
did: 'did:plc:first-labeler',
},
],
},
{
$type: 'app.bsky.actor.defs#savedFeedsPref',
pinned: ['at://bob.com/app.bsky.feed.generator/fake'],
saved: ['at://bob.com/app.bsky.feed.generator/fake'],
},
{
$type: 'app.bsky.actor.defs#savedFeedsPrefV2',
items: [
{
id: expect.any(String),
pinned: true,
type: 'timeline',
value: 'following',
},
],
},
{
$type: 'app.bsky.actor.defs#personalDetailsPref',
birthDate: '2023-09-11T18:05:42.556Z',
},
{
$type: 'app.bsky.actor.defs#feedViewPref',
feed: 'home',
hideReplies: false,
hideRepliesByUnfollowed: true,
hideRepliesByLikeCount: 0,
hideReposts: false,
hideQuotePosts: false,
},
{
$type: 'app.bsky.actor.defs#threadViewPref',
sort: 'oldest',
prioritizeFollowedUsers: true,
},
].sort(byType),
)
})
describe('muted words', () => {
let agent: AtpAgent
beforeAll(async () => {
agent = new AtpAgent({ service: network.pds.url })
await agent.createAccount({
handle: 'user7.test',
email: 'user7@test.com',
password: 'password',
})
})
afterEach(async () => {
const { moderationPrefs } = await agent.getPreferences()
await agent.removeMutedWords(moderationPrefs.mutedWords)
})
describe('addMutedWord', () => {
it('inserts', async () => {
const expiresAt = new Date(Date.now() + 6e3).toISOString()
await agent.addMutedWord({
value: 'word',
targets: ['content'],
actorTarget: 'all',
expiresAt,
})
const { moderationPrefs } = await agent.getPreferences()
const word = moderationPrefs.mutedWords.find(
(m) => m.value === 'word',
)
expect(word!.id).toBeTruthy()
expect(word!.targets).toEqual(['content'])
expect(word!.actorTarget).toEqual('all')
expect(word!.expiresAt).toEqual(expiresAt)
})
it('single-hash #, no insert', async () => {
await agent.addMutedWord({
value: '#',
targets: [],
actorTarget: 'all',
})
const { moderationPrefs } = await agent.getPreferences()
// sanitized to empty string, not inserted
expect(moderationPrefs.mutedWords.length).toEqual(0)
})
it('multi-hash ##, inserts #', async () => {
await agent.addMutedWord({
value: '##',
targets: [],
actorTarget: 'all',
})
const { moderationPrefs } = await agent.getPreferences()
expect(
moderationPrefs.mutedWords.find((m) => m.value === '#'),
).toBeTruthy()
})
it('multi-hash ##hashtag, inserts #hashtag', async () => {
await agent.addMutedWord({
value: '##hashtag',
targets: [],
actorTarget: 'all',
})
const { moderationPrefs } = await agent.getPreferences()
expect(
moderationPrefs.mutedWords.find((w) => w.value === '#hashtag'),
).toBeTruthy()
})
it('hash emoji #️⃣, inserts #️⃣', async () => {
await agent.addMutedWord({
value: '#️⃣',
targets: [],
actorTarget: 'all',
})
const { moderationPrefs } = await agent.getPreferences()
expect(
moderationPrefs.mutedWords.find((m) => m.value === '#️⃣'),
).toBeTruthy()
})
it('hash emoji w/leading hash ##️⃣, inserts #️⃣', async () => {
await agent.addMutedWord({
value: '##️⃣',
targets: [],
actorTarget: 'all',
})
const { moderationPrefs } = await agent.getPreferences()
expect(
moderationPrefs.mutedWords.find((m) => m.value === '#️⃣'),
).toBeTruthy()
})
it('hash emoji with double leading hash ###️⃣, inserts ##️⃣', async () => {
await agent.addMutedWord({
value: '###️⃣',
targets: [],
actorTarget: 'all',
})
const { moderationPrefs } = await agent.getPreferences()
expect(
moderationPrefs.mutedWords.find((m) => m.value === '##️⃣'),
).toBeTruthy()
})
it(`includes apostrophes e.g. Bluesky's`, async () => {
await agent.addMutedWord({
value: `Bluesky's`,
targets: [],
actorTarget: 'all',
})
const { mutedWords } = (await agent.getPreferences()).moderationPrefs
expect(mutedWords.find((m) => m.value === `Bluesky's`)).toBeTruthy()
})
describe(`invalid characters`, () => {
it('#<zws>, no insert', async () => {
await agent.addMutedWord({
value: '#',
targets: [],
actorTarget: 'all',
})
const { moderationPrefs } = await agent.getPreferences()
expect(moderationPrefs.mutedWords.length).toEqual(0)
})
it('#<zws>ab, inserts ab', async () => {
await agent.addMutedWord({
value: '#ab',
targets: [],
actorTarget: 'all',
})
const { moderationPrefs } = await agent.getPreferences()
expect(moderationPrefs.mutedWords.length).toEqual(1)
})
it('phrase with newline, inserts phrase without newline', async () => {
await agent.addMutedWord({
value: 'test value\n with newline',
targets: [],
actorTarget: 'all',
})
const { moderationPrefs } = await agent.getPreferences()
expect(
moderationPrefs.mutedWords.find(
(m) => m.value === 'test value with newline',
),
).toBeTruthy()
})
it('phrase with newlines, inserts phrase without newlines', async () => {
await agent.addMutedWord({
value: 'test value\n\r with newline',
targets: [],
actorTarget: 'all',
})
const { moderationPrefs } = await agent.getPreferences()
expect(
moderationPrefs.mutedWords.find(
(m) => m.value === 'test value with newline',
),
).toBeTruthy()
})
it('empty space, no insert', async () => {
await agent.addMutedWord({
value: ' ',
targets: [],
actorTarget: 'all',
})
const { moderationPrefs } = await agent.getPreferences()
expect(moderationPrefs.mutedWords.length).toEqual(0)
})
it(`' trim ', inserts 'trim'`, async () => {
await agent.addMutedWord({
value: ' trim ',
targets: [],
actorTarget: 'all',
})
const { moderationPrefs } = await agent.getPreferences()
expect(
moderationPrefs.mutedWords.find((m) => m.value === 'trim'),
).toBeTruthy()
})
})
})
describe('addMutedWords', () => {
it('inserts happen sequentially, no clobbering', async () => {
await agent.addMutedWords([
{ value: 'a', targets: ['content'], actorTarget: 'all' },
{ value: 'b', targets: ['content'], actorTarget: 'all' },
{ value: 'c', targets: ['content'], actorTarget: 'all' },
])
const { moderationPrefs } = await agent.getPreferences()
expect(moderationPrefs.mutedWords.length).toEqual(3)
})
})
describe('upsertMutedWords (deprecated)', () => {
it('no longer upserts, calls addMutedWords', async () => {
await agent.upsertMutedWords([
{ value: 'both', targets: ['content'], actorTarget: 'all' },
])
await agent.upsertMutedWords([
{ value: 'both', targets: ['tag'], actorTarget: 'all' },
])
const { moderationPrefs } = await agent.getPreferences()
expect(moderationPrefs.mutedWords.length).toEqual(2)
})
})
describe('updateMutedWord', () => {
it(`word doesn't exist, no update or insert`, async () => {
await agent.updateMutedWord({
value: 'word',
targets: ['tag', 'content'],
actorTarget: 'all',
})
const { moderationPrefs } = await agent.getPreferences()
expect(moderationPrefs.mutedWords.length).toEqual(0)
})
it('updates and sanitizes new value', async () => {
await agent.addMutedWord({
value: 'value',
targets: ['content'],
actorTarget: 'all',
})
const a = await agent.getPreferences()
const word = a.moderationPrefs.mutedWords.find(
(m) => m.value === 'value',
)
await agent.updateMutedWord({
...word!,
value: '#new value',
})
const b = await agent.getPreferences()
const updatedWord = b.moderationPrefs.mutedWords.find(
(m) => m.id === word!.id,
)
expect(updatedWord!.value).toEqual('new value')
expect(updatedWord).toHaveProperty('targets', ['content'])
})
it('updates targets', async () => {
await agent.addMutedWord({
value: 'word',
targets: ['tag'],
actorTarget: 'all',
})
const a = await agent.getPreferences()
const word = a.moderationPrefs.mutedWords.find(
(m) => m.value === 'word',
)
await agent.updateMutedWord({
...word!,
targets: ['content'],
})
const b = await agent.getPreferences()
expect(
b.moderationPrefs.mutedWords.find((m) => m.id === word!.id),
).toHaveProperty('targets', ['content'])
})
it('updates actorTarget', async () => {
await agent.addMutedWord({
value: 'value',
targets: ['content'],
actorTarget: 'all',
})
const a = await agent.getPreferences()
const word = a.moderationPrefs.mutedWords.find(
(m) => m.value === 'value',
)
await agent.updateMutedWord({
...word!,
actorTarget: 'exclude-following',
})
const b = await agent.getPreferences()
expect(
b.moderationPrefs.mutedWords.find((m) => m.id === word!.id),
).toHaveProperty('actorTarget', 'exclude-following')
})
it('updates expiresAt', async () => {
const expiresAt = new Date(Date.now() + 6e3).toISOString()
const expiresAt2 = new Date(Date.now() + 10e3).toISOString()
await agent.addMutedWord({
value: 'value',
targets: ['content'],
expiresAt,
actorTarget: 'all',
})
const a = await agent.getPreferences()
const word = a.moderationPrefs.mutedWords.find(
(m) => m.value === 'value',
)
await agent.updateMutedWord({
...word!,
expiresAt: expiresAt2,
})
const b = await agent.getPreferences()
expect(
b.moderationPrefs.mutedWords.find((m) => m.id === word!.id),
).toHaveProperty('expiresAt', expiresAt2)
})
it(`doesn't update if value is sanitized to be falsy`, async () => {
await agent.addMutedWord({
value: 'rug',
targets: ['content'],
actorTarget: 'all',
})
const a = await agent.getPreferences()
const word = a.moderationPrefs.mutedWords.find(
(m) => m.value === 'rug',
)
await agent.updateMutedWord({
...word!,
value: '',
})
const b = await agent.getPreferences()
expect(
b.moderationPrefs.mutedWords.find((m) => m.id === word!.id),
).toHaveProperty('value', 'rug')
})
})
describe('removeMutedWord', () => {
it('removes word', async () => {
await agent.addMutedWord({
value: 'word',
targets: ['tag'],
actorTarget: 'all',
})
const a = await agent.getPreferences()
const word = a.moderationPrefs.mutedWords.find(
(m) => m.value === 'word',
)
await agent.removeMutedWord(word!)
const b = await agent.getPreferences()
expect(
b.moderationPrefs.mutedWords.find((m) => m.id === word!.id),
).toBeFalsy()
})
it(`word doesn't exist, no action`, async () => {
await agent.addMutedWord({
value: 'word',
targets: ['tag'],
actorTarget: 'all',
})
const a = await agent.getPreferences()
const word = a.moderationPrefs.mutedWords.find(
(m) => m.value === 'word',
)
await agent.removeMutedWord({
value: 'another',
targets: [],
actorTarget: 'all',
})
const b = await agent.getPreferences()
expect(
b.moderationPrefs.mutedWords.find((m) => m.id === word!.id),
).toBeTruthy()
})
})
describe('removeMutedWords', () => {
it(`removes sequentially, no clobbering`, async () => {
await agent.addMutedWords([
{ value: 'a', targets: ['content'], actorTarget: 'all ' },
{ value: 'b', targets: ['content'], actorTarget: 'all ' },
{ value: 'c', targets: ['content'], actorTarget: 'all ' },
])
const a = await agent.getPreferences()
await agent.removeMutedWords(a.moderationPrefs.mutedWords)
const b = await agent.getPreferences()
expect(b.moderationPrefs.mutedWords.length).toEqual(0)
})
})
})
describe('legacy muted words', () => {
let agent: AtpAgent
async function updatePreferences(
agent: AtpAgent,
cb: (
prefs: AppBskyActorDefs.Preferences,
) => AppBskyActorDefs.Preferences | false,
) {
const res = await agent.app.bsky.actor.getPreferences({})
const newPrefs = cb(res.data.preferences)
if (newPrefs === false) {
return
}
await agent.app.bsky.actor.putPreferences({
preferences: newPrefs,
})
}
async function addLegacyMutedWord(mutedWord: AppBskyActorDefs.MutedWord) {
await updatePreferences(agent, (prefs) => {
let mutedWordsPref = prefs.findLast(
(pref) =>
AppBskyActorDefs.isMutedWordsPref(pref) &&
AppBskyActorDefs.validateMutedWordsPref(pref).success,
)
const newMutedWord: AppBskyActorDefs.MutedWord = {
value: mutedWord.value,
targets: mutedWord.targets,
actorTarget: 'all',
}
if (
mutedWordsPref &&
AppBskyActorDefs.isMutedWordsPref(mutedWordsPref)
) {
mutedWordsPref.items.push(newMutedWord)
} else {
// if the pref doesn't exist, create it
mutedWordsPref = {
items: [newMutedWord],
}
}
return prefs
.filter((p) => !AppBskyActorDefs.isMutedWordsPref(p))
.concat([
{
...mutedWordsPref,
$type: 'app.bsky.actor.defs#mutedWordsPref',
},
])
})
}
beforeAll(async () => {
agent = new AtpAgent({ service: network.pds.url })
await agent.createAccount({
handle: 'user7-1.test',
email: 'user7-1@test.com',
password: 'password',
})
})
afterEach(async () => {
const { moderationPrefs } = await agent.getPreferences()
await agent.removeMutedWords(moderationPrefs.mutedWords)
})
describe(`upsertMutedWords (and addMutedWord)`, () => {
it(`adds new word, migrates old words`, async () => {
await addLegacyMutedWord({
value: 'word',
targets: ['content'],
actorTarget: 'all',
})
{
const { moderationPrefs } = await agent.getPreferences()
const word = moderationPrefs.mutedWords.find(
(w) => w.value === 'word',
)
expect(word).toBeTruthy()
expect(word!.id).toBeFalsy()
}
await agent.upsertMutedWords([
{ value: 'word2', targets: ['tag'], actorTarget: 'all' },
])
{
const { moderationPrefs } = await agent.getPreferences()
const word = moderationPrefs.mutedWords.find(
(w) => w.value === 'word',
)
const word2 = moderationPrefs.mutedWords.find(
(w) => w.value === 'word2',
)
expect(word!.id).toBeTruthy()
expect(word2!.id).toBeTruthy()
}
})
})
describe(`updateMutedWord`, () => {
it(`updates legacy word, migrates old words`, async () => {
await addLegacyMutedWord({
value: 'word',
targets: ['content'],
actorTarget: 'all',
})
await addLegacyMutedWord({
value: 'word2',
targets: ['tag'],
actorTarget: 'all',
})
await agent.updateMutedWord({
value: 'word',
targets: ['tag'],
actorTarget: 'all',
})
{
const { moderationPrefs } = await agent.getPreferences()
const word = moderationPrefs.mutedWords.find(
(w) => w.value === 'word',
)
const word2 = moderationPrefs.mutedWords.find(
(w) => w.value === 'word2',
)
expect(moderationPrefs.mutedWords.length).toEqual(2)
expect(word!.id).toBeTruthy()
expect(word!.targets).toEqual(['tag'])
expect(word2!.id).toBeTruthy()
}
})
})
describe(`removeMutedWord`, () => {
it(`removes legacy word, migrates old words`, async () => {
await addLegacyMutedWord({
value: 'word',
targets: ['content'],
actorTarget: 'all',
})
await addLegacyMutedWord({
value: 'word2',
targets: ['tag'],
actorTarget: 'all',
})
await agent.removeMutedWord({
value: 'word',
targets: ['tag'],
actorTarget: 'all',
})
{
const { moderationPrefs } = await agent.getPreferences()
const word = moderationPrefs.mutedWords.find(
(w) => w.value === 'word',
)
const word2 = moderationPrefs.mutedWords.find(
(w) => w.value === 'word2',
)
expect(moderationPrefs.mutedWords.length).toEqual(1)
expect(word).toBeFalsy()
expect(word2!.id).toBeTruthy()
}
})
})
})
describe('hidden posts', () => {
let agent: AtpAgent
const postUri = 'at://did:plc:fake/app.bsky.feed.post/fake'
beforeAll(async () => {
agent = new AtpAgent({ service: network.pds.url })
await agent.createAccount({
handle: 'user8.test',
email: 'user8@test.com',
password: 'password',
})
})
it('hidePost', async () => {
await agent.hidePost(postUri)
await agent.hidePost(postUri) // double, should dedupe
await expect(agent.getPreferences()).resolves.toHaveProperty(
'moderationPrefs.hiddenPosts',
[postUri],
)
})
it('unhidePost', async () => {
await agent.unhidePost(postUri)
await expect(agent.getPreferences()).resolves.toHaveProperty(
'moderationPrefs.hiddenPosts',
[],
)
// no issues calling a second time
await agent.unhidePost(postUri)
await expect(agent.getPreferences()).resolves.toHaveProperty(
'moderationPrefs.hiddenPosts',
[],
)
})
})
describe(`saved feeds v2`, () => {
let agent: AtpAgent
let i = 0
const feedUri = () => `at://bob.com/app.bsky.feed.generator/${i++}`
const listUri = () => `at://bob.com/app.bsky.graph.list/${i++}`
beforeAll(async () => {
agent = new AtpAgent({ service: network.pds.url })
await agent.createAccount({
handle: 'user9.test',
email: 'user9@test.com',
password: 'password',
})
})
beforeEach(async () => {
await agent.app.bsky.actor.putPreferences({
preferences: [],
})
})
describe(`addSavedFeeds`, () => {
it('works', async () => {
const feed = {
type: 'feed',
value: feedUri(),
pinned: false,
}
await agent.addSavedFeeds([feed])
const prefs = await agent.getPreferences()
expect(prefs.savedFeeds).toStrictEqual([
{
...feed,
id: expect.any(String),
},
])
})
it('throws if feed is specified and list provided', async () => {
const list = listUri()
await expect(() =>
agent.addSavedFeeds([
{
type: 'feed',
value: list,
pinned: true,
},
]),
).rejects.toThrow()
})
it('throws if list is specified and feed provided', async () => {
const feed = feedUri()
await expect(() =>
agent.addSavedFeeds([
{
type: 'list',
value: feed,
pinned: true,
},
]),
).rejects.toThrow()
})
it(`timeline`, async () => {
const feeds = await agent.addSavedFeeds([
{
type: 'timeline',
value: 'following',
pinned: true,
},
])
const prefs = await agent.getPreferences()
expect(
prefs.savedFeeds.filter((f) => f.type === 'timeline'),
).toStrictEqual(feeds)
})
it(`allows duplicates`, async () => {
const feed = {
type: 'feed',
value: feedUri(),
pinned: false,
}
await agent.addSavedFeeds([feed])
await agent.addSavedFeeds([feed])
const prefs = await agent.getPreferences()
expect(prefs.savedFeeds).toStrictEqual([
{
...feed,
id: expect.any(String),
},
{
...feed,
id: expect.any(String),
},
])
})
it(`adds multiple`, async () => {
const a = {
type: 'feed',
value: feedUri(),
pinned: true,
}
const b = {
type: 'feed',
value: feedUri(),
pinned: false,
}
await agent.addSavedFeeds([a, b])
const prefs = await agent.getPreferences()
expect(prefs.savedFeeds).toStrictEqual([
{
...a,
id: expect.any(String),
},
{
...b,
id: expect.any(String),
},
])
})
it(`appends multiple`, async () => {
const a = {
type: 'feed',
value: feedUri(),
pinned: true,
}
const b = {
type: 'feed',
value: feedUri(),
pinned: false,
}
const c = {
type: 'feed',
value: feedUri(),
pinned: true,
}
const d = {
type: 'feed',
value: feedUri(),
pinned: false,
}
await agent.addSavedFeeds([a, b])
await agent.addSavedFeeds([c, d])
const prefs = await agent.getPreferences()
expect(prefs.savedFeeds).toStrictEqual([
{
...a,
id: expect.any(String),
},
{
...c,
id: expect.any(String),
},
{
...b,
id: expect.any(String),
},
{
...d,
id: expect.any(String),
},
])
})
})
describe(`removeSavedFeeds`, () => {
it('works', async () => {
const feed = {
type: 'feed',
value: feedUri(),
pinned: true,
}
const savedFeeds = await agent.addSavedFeeds([feed])
await agent.removeSavedFeeds([savedFeeds[0].id])
const prefs = await agent.getPreferences()
expect(prefs.savedFeeds).toStrictEqual([])
})
})
describe(`overwriteSavedFeeds`, () => {
it(`dedupes by id, takes last, preserves order based on last found`, async () => {
const a = {
id: TID.nextStr(),
type: 'feed',
value: feedUri(),
pinned: true,
}
const b = {
id: TID.nextStr(),
type: 'feed',
value: feedUri(),
pinned: true,
}
await agent.overwriteSavedFeeds([a, b, a])
const prefs = await agent.getPreferences()
expect(prefs.savedFeeds).toStrictEqual([b, a])
})
it(`preserves order`, async () => {
const a = feedUri()
const b = feedUri()
const c = feedUri()
const d = feedUri()
await agent.overwriteSavedFeeds([
{
id: TID.nextStr(),
type: 'timeline',
value: a,
pinned: true,
},
{
id: TID.nextStr(),
type: 'feed',
value: b,
pinned: false,
},
{
id: TID.nextStr(),
type: 'feed',
value: c,
pinned: true,
},
{
id: TID.nextStr(),
type: 'feed',
value: d,
pinned: false,
},
])
const { savedFeeds } = await agent.getPreferences()
expect(savedFeeds.filter((f) => f.pinned)).toStrictEqual([
{
id: expect.any(String),
type: 'timeline',
value: a,
pinned: true,
},
{
id: expect.any(String),
type: 'feed',
value: c,
pinned: true,
},
])
expect(savedFeeds.filter((f) => !f.pinned)).toEqual([
{
id: expect.any(String),
type: 'feed',
value: b,
pinned: false,
},
{
id: expect.any(String),
type: 'feed',
value: d,
pinned: false,
},
])
})
})
describe(`updateSavedFeeds`, () => {
it(`updates affect order, saved last, new pins last`, async () => {
const a = {
id: TID.nextStr(),
type: 'feed',
value: feedUri(),
pinned: true,
}
const b = {
id: TID.nextStr(),
type: 'feed',
value: feedUri(),
pinned: true,
}
const c = {
id: TID.nextStr(),
type: 'feed',
value: feedUri(),
pinned: true,
}
await agent.overwriteSavedFeeds([a, b, c])
await agent.updateSavedFeeds([
{
...b,
pinned: false,
},
])
const prefs1 = await agent.getPreferences()
expect(prefs1.savedFeeds).toStrictEqual([
a,
c,
{
...b,
pinned: false,
},
])
await agent.updateSavedFeeds([
{
...b,
pinned: true,
},
])
const prefs2 = await agent.getPreferences()
expect(prefs2.savedFeeds).toStrictEqual([a, c, b])
})
it(`cannot override original id`, async () => {
const a = {
id: TID.nextStr(),
type: 'feed',
value: feedUri(),
pinned: true,
}
await agent.overwriteSavedFeeds([a])
await agent.updateSavedFeeds([
{
...a,
pinned: false,
id: TID.nextStr(),
},
])
const prefs = await agent.getPreferences()
expect(prefs.savedFeeds).toStrictEqual([a])
})
it(`updates multiple`, async () => {
const a = {
id: TID.nextStr(),
type: 'feed',
value: feedUri(),
pinned: false,
}
const b = {
id: TID.nextStr(),
type: 'feed',
value: feedUri(),
pinned: false,
}
const c = {
id: TID.nextStr(),
type: 'feed',
value: feedUri(),
pinned: false,
}
await agent.overwriteSavedFeeds([a, b, c])
await agent.updateSavedFeeds([
{
...b,
pinned: true,
},
{
...c,
pinned: true,
},
])
const prefs1 = await agent.getPreferences()
expect(prefs1.savedFeeds).toStrictEqual([
{
...b,
pinned: true,
},
{
...c,
pinned: true,
},
a,
])
})
})
describe(`utils`, () => {
describe(`savedFeedsToUriArrays`, () => {
const { saved, pinned } = savedFeedsToUriArrays([
{
id: '',
type: 'feed',
value: 'a',
pinned: true,
},
{
id: '',
type: 'feed',
value: 'b',
pinned: false,
},
{
id: '',
type: 'feed',
value: 'c',
pinned: true,
},
])
expect(saved).toStrictEqual(['a', 'b', 'c'])
expect(pinned).toStrictEqual(['a', 'c'])
})
describe(`getSavedFeedType`, () => {
it(`works`, () => {
expect(getSavedFeedType('foo')).toBe('unknown')
expect(getSavedFeedType(feedUri())).toBe('feed')
expect(getSavedFeedType(listUri())).toBe('list')
expect(
getSavedFeedType('at://did:plc:fake/app.bsky.graph.follow/fake'),
).toBe('unknown')
})
})
describe(`validateSavedFeed`, () => {
it(`throws if invalid TID`, () => {
// really only checks length at time of writing
expect(() =>
validateSavedFeed({
id: 'a',
type: 'feed',
value: feedUri(),
pinned: false,
}),
).toThrow()
})
it(`throws if mismatched types`, () => {
expect(() =>
validateSavedFeed({
id: TID.nextStr(),
type: 'list',
value: feedUri(),
pinned: false,
}),
).toThrow()
expect(() =>
validateSavedFeed({
id: TID.nextStr(),
type: 'feed',
value: listUri(),
pinned: false,
}),
).toThrow()
})
it(`ignores values it can't validate`, () => {
expect(() =>
validateSavedFeed({
id: TID.nextStr(),
type: 'timeline',
value: 'following',
pinned: false,
}),
).not.toThrow()
expect(() =>
validateSavedFeed({
id: TID.nextStr(),
type: 'unknown',
value: 'could be @nyt4!ng',
pinned: false,
}),
).not.toThrow()
})
})
})
})
describe(`saved feeds v2: migration scenarios`, () => {
let agent: AtpAgent
let i = 0
const feedUri = () => `at://bob.com/app.bsky.feed.generator/${i++}`
beforeAll(async () => {
agent = new AtpAgent({ service: network.pds.url })
await agent.createAccount({
handle: 'user10.test',
email: 'user10@test.com',
password: 'password',
})
})
beforeEach(async () => {
await agent.app.bsky.actor.putPreferences({
preferences: [],
})
})
it('CRUD action before migration, no timeline inserted', async () => {
const feed = {
type: 'feed',
value: feedUri(),
pinned: false,
}
await agent.addSavedFeeds([feed])
const prefs = await agent.getPreferences()
expect(prefs.savedFeeds).toStrictEqual([
{
...feed,
id: expect.any(String),
},
])
})
it('CRUD action AFTER migration, timeline was inserted', async () => {
await agent.getPreferences()
const feed = {
type: 'feed',
value: feedUri(),
pinned: false,
}
await agent.addSavedFeeds([feed])
const prefs = await agent.getPreferences()
expect(prefs.savedFeeds).toStrictEqual([
{
id: expect.any(String),
type: 'timeline',
value: 'following',
pinned: true,
},
{
...feed,
id: expect.any(String),
},
])
})
// fresh account OR an old account with no v1 prefs to migrate from
it(`brand new user, v1 remains undefined`, async () => {
const prefs = await agent.getPreferences()
expect(prefs.savedFeeds).toStrictEqual([
{
id: expect.any(String),
type: 'timeline',
value: 'following',
pinned: true,
},
])
// no v1 prefs to populate from
expect(prefs.feeds).toStrictEqual({
saved: undefined,
pinned: undefined,
})
})
it(`brand new user, v2 does not write to v1`, async () => {
const a = feedUri()
// migration happens
await agent.getPreferences()
await agent.addSavedFeeds([
{
type: 'feed',
value: a,
pinned: false,
},
])
const prefs = await agent.getPreferences()
expect(prefs.savedFeeds).toStrictEqual([
{
id: expect.any(String),
type: 'timeline',
value: 'following',
pinned: true,
},
{
id: expect.any(String),
type: 'feed',
value: a,
pinned: false,
},
])
// no v1 prefs to populate from
expect(prefs.feeds).toStrictEqual({
saved: undefined,
pinned: undefined,
})
})
it(`existing user with v1 prefs, migrates`, async () => {
const one = feedUri()
const two = feedUri()
await agent.app.bsky.actor.putPreferences({
preferences: [
{
$type: 'app.bsky.actor.defs#savedFeedsPref',
pinned: [one],
saved: [one, two],
},
],
})
const prefs = await agent.getPreferences()
// deprecated interface receives what it normally would
expect(prefs.feeds).toStrictEqual({
pinned: [one],
saved: [one, two],
})
// new interface gets new timeline + old pinned feed
expect(prefs.savedFeeds).toStrictEqual([
{
id: expect.any(String),
type: 'timeline',
value: 'following',
pinned: true,
},
{
id: expect.any(String),
type: 'feed',
value: one,
pinned: true,
},
{
id: expect.any(String),
type: 'feed',
value: two,
pinned: false,
},
])
})
it('squashes duplicates during migration', async () => {
const one = feedUri()
const two = feedUri()
await agent.app.bsky.actor.putPreferences({
preferences: [
{
$type: 'app.bsky.actor.defs#savedFeedsPref',
pinned: [one, two],
saved: [one, two],
},
{
$type: 'app.bsky.actor.defs#savedFeedsPref',
pinned: [],
saved: [],
},
],
})
// performs migration
const prefs = await agent.getPreferences()
expect(prefs.feeds).toStrictEqual({
pinned: [],
saved: [],
})
expect(prefs.savedFeeds).toStrictEqual([
{
id: expect.any(String),
type: 'timeline',
value: 'following',
pinned: true,
},
])
const res = await agent.app.bsky.actor.getPreferences()
expect(res.data.preferences).toStrictEqual([
{
$type: 'app.bsky.actor.defs#savedFeedsPrefV2',
items: [
{
id: expect.any(String),
type: 'timeline',
value: 'following',
pinned: true,
},
],
},
{
$type: 'app.bsky.actor.defs#savedFeedsPref',
pinned: [],
saved: [],
},
])
})
it('v2 writes persist to v1, not the inverse', async () => {
const a = feedUri()
const b = feedUri()
const c = feedUri()
const d = feedUri()
const e = feedUri()
await agent.app.bsky.actor.putPreferences({
preferences: [
{
$type: 'app.bsky.actor.defs#savedFeedsPref',
pinned: [a, b],
saved: [a, b],
},
],
})
// client updates, migrates to v2
// a and b are both pinned
await agent.getPreferences()
// new write to v2, c is saved
await agent.addSavedFeeds([
{
type: 'feed',
value: c,
pinned: false,
},
])
// v2 write wrote to v1 also
const res1 = await agent.app.bsky.actor.getPreferences()
const v1Pref = res1.data.preferences.find((p) =>
AppBskyActorDefs.isSavedFeedsPref(p),
)
expect(v1Pref).toStrictEqual({
$type: 'app.bsky.actor.defs#savedFeedsPref',
pinned: [a, b],
saved: [a, b, c],
})
// v1 write occurs, d is added but not to v2
await agent.addSavedFeed(d)
const res3 = await agent.app.bsky.actor.getPreferences()
const v1Pref3 = res3.data.preferences.find((p) =>
AppBskyActorDefs.isSavedFeedsPref(p),
)
expect(v1Pref3).toStrictEqual({
$type: 'app.bsky.actor.defs#savedFeedsPref',
pinned: [a, b],
saved: [a, b, c, d],
})
// another new write to v2, pins e
await agent.addSavedFeeds([
{
type: 'feed',
value: e,
pinned: true,
},
])
const res4 = await agent.app.bsky.actor.getPreferences()
const v1Pref4 = res4.data.preferences.find((p) =>
AppBskyActorDefs.isSavedFeedsPref(p),
)
// v1 pref got v2 write
expect(v1Pref4).toStrictEqual({
$type: 'app.bsky.actor.defs#savedFeedsPref',
pinned: [a, b, e],
saved: [a, b, c, d, e],
})
const final = await agent.getPreferences()
// d not here bc it was written with v1
expect(final.savedFeeds).toStrictEqual([
{
id: expect.any(String),
type: 'timeline',
value: 'following',
pinned: true,
},
{ id: expect.any(String), type: 'feed', value: a, pinned: true },
{ id: expect.any(String), type: 'feed', value: b, pinned: true },
{ id: expect.any(String), type: 'feed', value: e, pinned: true },
{ id: expect.any(String), type: 'feed', value: c, pinned: false },
])
})
it(`filters out invalid values in v1 prefs`, async () => {
// v1 prefs must be valid AtUris, but they could be any type in theory
await agent.app.bsky.actor.putPreferences({
preferences: [
{
$type: 'app.bsky.actor.defs#savedFeedsPref',
pinned: ['at://did:plc:fake/app.bsky.graph.follow/fake'],
saved: ['at://did:plc:fake/app.bsky.graph.follow/fake'],
},
],
})
const prefs = await agent.getPreferences()
expect(prefs.savedFeeds).toStrictEqual([
{
id: expect.any(String),
type: 'timeline',
value: 'following',
pinned: true,
},
])
})
})
describe('queued nudges', () => {
it('queueNudges & dismissNudges', async () => {
const agent = new AtpAgent({ service: network.pds.url })
await agent.createAccount({
handle: 'user11.test',
email: 'user11@test.com',
password: 'password',
})
await agent.bskyAppQueueNudges('first')
await expect(agent.getPreferences()).resolves.toHaveProperty(
'bskyAppState.queuedNudges',
['first'],
)
await agent.bskyAppQueueNudges(['second', 'third'])
await expect(agent.getPreferences()).resolves.toHaveProperty(
'bskyAppState.queuedNudges',
['first', 'second', 'third'],
)
await agent.bskyAppDismissNudges('second')
await expect(agent.getPreferences()).resolves.toHaveProperty(
'bskyAppState.queuedNudges',
['first', 'third'],
)
await agent.bskyAppDismissNudges(['first', 'third'])
await expect(agent.getPreferences()).resolves.toHaveProperty(
'bskyAppState.queuedNudges',
[],
)
})
})
describe('guided tours', () => {
it('setActiveProgressGuide', async () => {
const agent = new AtpAgent({ service: network.pds.url })
await agent.createAccount({
handle: 'user12.test',
email: 'user12@test.com',
password: 'password',
})
await agent.bskyAppSetActiveProgressGuide({
guide: 'test-guide',
numThings: 0,
})
await expect(agent.getPreferences()).resolves.toHaveProperty(
'bskyAppState.activeProgressGuide.guide',
'test-guide',
)
await agent.bskyAppSetActiveProgressGuide({
guide: 'test-guide',
numThings: 1,
})
await expect(agent.getPreferences()).resolves.toHaveProperty(
'bskyAppState.activeProgressGuide.guide',
'test-guide',
)
await expect(agent.getPreferences()).resolves.toHaveProperty(
'bskyAppState.activeProgressGuide.numThings',
1,
)
await agent.bskyAppSetActiveProgressGuide(undefined)
await expect(agent.getPreferences()).resolves.toHaveProperty(
'bskyAppState.activeProgressGuide',
undefined,
)
})
})
describe('nuxs', () => {
let agent: AtpAgent
const nux = {
id: 'a',
completed: false,
data: '{}',
expiresAt: new Date(Date.now() + 6e3).toISOString(),
}
beforeAll(async () => {
agent = new AtpAgent({ service: network.pds.url })
await agent.createAccount({
handle: 'nuxs.test',
email: 'nuxs@test.com',
password: 'password',
})
})
it('bskyAppUpsertNux', async () => {
// never duplicates
await agent.bskyAppUpsertNux(nux)
await agent.bskyAppUpsertNux(nux)
await agent.bskyAppUpsertNux(nux)
const prefs = await agent.getPreferences()
const nuxs = prefs.bskyAppState.nuxs
expect(nuxs.length).toEqual(1)
expect(nuxs.find((n) => n.id === nux.id)).toEqual(nux)
})
it('bskyAppUpsertNux completed', async () => {
// never duplicates
await agent.bskyAppUpsertNux({
...nux,
completed: true,
})
const prefs = await agent.getPreferences()
const nuxs = prefs.bskyAppState.nuxs
expect(nuxs.length).toEqual(1)
expect(nuxs.find((n) => n.id === nux.id)?.completed).toEqual(true)
})
it('bskyAppRemoveNuxs', async () => {
await agent.bskyAppRemoveNuxs([nux.id])
const prefs = await agent.getPreferences()
const nuxs = prefs.bskyAppState.nuxs
expect(nuxs.length).toEqual(0)
})
it('bskyAppUpsertNux validates nux', async () => {
// @ts-expect-error
expect(() => agent.bskyAppUpsertNux({ name: 'a' })).rejects.toThrow()
expect(() =>
agent.bskyAppUpsertNux({ id: 'a', completed: false, foo: 'bar' }),
).rejects.toThrow()
})
})
// end
})
})
const byType = (a, b) => a.$type.localeCompare(b.$type)