Something went wrong. Try again.
Mirror of https://github.com/improsocial/improAn extensible Bluesky client for web
Something went wrong. Try again.
JavaScript
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596import { createPost } from "../shared/factories.js";import { bskyLabeler, userProfile } from "./testData.js";import { TEST_PLUGIN_ID, TEST_PLUGIN_MANIFEST, TEST_PLUGIN_RAW_MANIFEST, getTestPluginSource,} from "./testPlugin.js";
export class MockServer { constructor() { this.authorFeeds = new Map(); this.bookmarks = []; this.convos = []; this.convoMessages = new Map(); this.convoMemberLists = new Map(); this.convoDelays = new Map(); this.chatLogs = []; this.joinLinkPreviews = new Map(); this.joinLinkJoinedConvos = new Map(); this.failJoinLinkCodes = new Set(); this.createRecordCounter = 0; this.applyWritesCalls = []; this.drafts = []; this.draftCounter = 0; this.deletedDraftIds = []; this.draftLimitReached = false; this.interactionPayloads = []; this.blobCounter = 0; this.messageCounter = 0; this.sentMessageRequests = []; this.sendMessageFailure = null; this.convoForMembersError = null; this.typeaheadProfiles = []; this.typeaheadDelayMs = 0; this.externalLinkCards = new Map(); this.feedGenerators = []; this.feeds = new Map(); this.hiddenPostUris = []; this.labelerSubscriptions = []; this.labelerViews = [bskyLabeler]; this.mutedWords = []; this.blockedProfiles = []; this.mutedProfiles = []; this.contentLabelPrefs = []; this.notifications = []; this.notificationCursor = undefined; this.notificationsDelayMs = 0; this.pinnedFeedUris = []; this.pinnedListUris = []; this.lists = []; this.starterPacks = []; this.listMembers = new Map(); this.currentUserListItems = []; this.posts = []; this.postLikes = new Map(); this.reportPayloads = []; this.postQuotes = new Map(); this.postReposts = new Map(); this.postThreadOthers = new Map(); this.postThreads = new Map(); this.postThreadDelays = new Map(); this.postDelays = new Map(); this.profileFollowers = new Map(); this.profileFollows = new Map(); this.knownFollowers = new Map(); this.profiles = new Map(); this.savedFeedUris = []; this.actorFeeds = new Map(); this.actorLists = new Map(); this.searchFeedGenerators = []; this.searchPosts = []; this.searchPostsBySort = { top: [], latest: [] }; this.searchProfiles = []; this.searchRequestCounts = { profiles: 0, top: 0, latest: 0, feeds: 0, typeahead: 0, }; this.timelinePosts = []; this.timelineDelayMs = 0; this.pluginSettings = new Map(); this.installedPlugins = []; // Override the source served for the local test plugin's main.js; defaults // to the standard fixture when null. this.localPluginSource = null; this.registryEntries = []; this.liveManifest = null; // README markdown served for plugin repos; set to null to simulate a // plugin with no README (404). this.pluginReadme = "# Remote Themes\n\nA test readme for the plugin."; this.tokenRefreshShouldFail = false; }
// Make subsequent OAuth token refreshes fail with a non-retryable error, // simulating a stale/revoked refresh token. failTokenRefresh() { this.tokenRefreshShouldFail = true; }
addAuthorFeedPosts(did, filter, posts) { this.authorFeeds.set(`${did}-${filter}`, posts); }
addBookmarks(bookmarks) { this.bookmarks.push(...bookmarks); }
addActorFeeds(did, feedGenerators) { const existing = this.actorFeeds.get(did) || []; this.actorFeeds.set(did, [...existing, ...feedGenerators]); }
addActorLists(did, lists) { const existing = this.actorLists.get(did) || []; this.actorLists.set(did, [...existing, ...lists]); }
addFeedGenerators(feedGenerators) { this.feedGenerators.push(...feedGenerators); }
addLabelerSubscription(did) { this.labelerSubscriptions.push(did); }
addLabelerViews(views) { this.labelerViews.push(...views); }
addFeedItems(feedUri, posts) { this.feeds.set( feedUri, posts.map((post) => ({ post })), ); }
addLists(lists) { this.lists.push(...lists); }
addStarterPacks(starterPacks) { this.starterPacks.push(...starterPacks); }
addListMembers(listUri, profiles) { this.listMembers.set(listUri, profiles); }
addCurrentUserListItem({ uri, listUri, subjectDid }) { this.currentUserListItems.push({ uri, listUri, subjectDid }); }
addListFeedItems(listUri, posts) { this.feeds.set( listUri, posts.map((post) => ({ post })), ); }
setPinnedFeeds(feedUris) { this.pinnedFeedUris = feedUris; }
setPinnedLists(listUris) { this.pinnedListUris = listUris; }
setSavedFeeds(feedUris) { this.savedFeedUris = feedUris; }
addTimelinePosts(posts) { this.timelinePosts.push(...posts); }
// Delays apply to requests made after the setter is called, so a test can // let the initial load complete instantly and only slow down a reload. setTimelineDelay(delayMs) { this.timelineDelayMs = delayMs; }
setNotificationsDelay(delayMs) { this.notificationsDelayMs = delayMs; }
addSearchPosts(posts, { sort } = {}) { this.searchPosts.push(...posts); if (sort) { this.searchPostsBySort[sort].push(...posts); } else { this.searchPostsBySort.top.push(...posts); this.searchPostsBySort.latest.push(...posts); } }
addSearchProfiles(profiles) { this.searchProfiles.push(...profiles); }
addSearchFeedGenerators(feedGenerators) { this.searchFeedGenerators.push(...feedGenerators); }
addTypeaheadProfiles(profiles) { this.typeaheadProfiles.push(...profiles); }
setConvoForMembersError(errorName) { this.convoForMembersError = errorName; }
setExternalLinkCard(url, meta) { this.externalLinkCards.set(url, meta); }
addNotifications(notifications, { cursor } = {}) { this.notifications.push(...notifications); this.notificationCursor = cursor; }
addPosts(posts, { delayMs = 0 } = {}) { this.posts.push(...posts); for (const post of posts) { const preview = post?.embed?.joinLinkPreview; if (preview?.code) { this.joinLinkPreviews.set(preview.code, preview); } if (delayMs > 0) { this.postDelays.set(post.uri, delayMs); } } }
addPostLikes(postUri, likes) { this.postLikes.set(postUri, likes); }
addPostQuotes(postUri, quotes) { this.postQuotes.set(postUri, quotes); }
addPostReposts(postUri, reposts) { this.postReposts.set(postUri, reposts); }
setPostThread(postUri, thread, { delayMs = 0 } = {}) { this.postThreads.set(postUri, thread); if (delayMs > 0) { this.postThreadDelays.set(postUri, delayMs); } }
setPostThreadOther(postUri, threadOther) { this.postThreadOthers.set(postUri, threadOther); }
addProfileFollowers(did, followers) { this.profileFollowers.set(did, followers); }
addProfileFollows(did, follows) { this.profileFollows.set(did, follows); }
addKnownFollowers(did, followers) { this.knownFollowers.set(did, followers); }
addProfile(profile) { this.profiles.set(profile.did, profile); }
addConvos(convos) { for (const convo of convos) { if (!this.convoMessages.has(convo.id)) { this.convoMessages.set(convo.id, []); } } this.convos.push(...convos); }
addConvoMembers(convoId, members) { this.convoMemberLists.set(convoId, members); }
failConvoMembers({ status = 500, error = "InternalServerError", message }) { this.convoMembersFailure = { status, error, message }; }
setConvoDelay(convoId, delayMs) { this.convoDelays.set(convoId, delayMs); }
// Queued logs are returned (and drained) by the next chat.bsky.convo.getLog // poll, simulating events arriving while a conversation is open. addChatLogs(logs) { this.chatLogs.push(...logs); }
addConvoMessages(convoId, messages) { this.convoMessages.set(convoId, messages); for (const message of messages) { const preview = message?.embed?.joinLinkPreview; if (preview?.code) { this.joinLinkPreviews.set(preview.code, preview); } } }
setJoinLinkPreview(code, preview) { this.joinLinkPreviews.set(code, preview); }
failJoinLinkRequest(code) { this.failJoinLinkCodes.add(code); }
failSendMessage({ status = 400, error = "InvalidRequest", message }) { this.sendMessageFailure = { status, error, message }; }
setJoinLinkJoinedConvo(code, convo) { this.joinLinkJoinedConvos.set(code, convo); }
// Abort all subsequent document navigations (reloads, redirects) so the // page stays on the current document. UI states that only exist until a // navigation completes (e.g. an in-progress spinner) stay observable. async blockNavigations(page) { await page.route("**/*", async (route) => { if ( route.request().isNavigationRequest() && route.request().resourceType() === "document" ) { await route.abort("aborted"); return; } await route.fallback(); }); }
// Turns a submitted app.bsky.feed.post record into a hydrated post view and // threads it into the mock state (quote counts, reply threads, author // feeds). Shared by the createRecord and applyWrites routes. _ingestPostRecord(record, uri) { let embed; let quotedPostUri; const recordEmbed = record?.embed;
if (recordEmbed?.$type === "app.bsky.embed.images") { embed = { $type: "app.bsky.embed.images#view", images: recordEmbed.images.map((img) => ({ thumb: "", fullsize: "", alt: img.alt || "", aspectRatio: img.aspectRatio, })), }; } else if (recordEmbed?.$type === "app.bsky.embed.video") { embed = { $type: "app.bsky.embed.video#view", cid: recordEmbed.video.ref.$link, playlist: "", alt: recordEmbed.alt || "", aspectRatio: recordEmbed.aspectRatio, }; } else if (recordEmbed?.$type === "app.bsky.embed.external") { embed = { $type: "app.bsky.embed.external#view", external: { uri: recordEmbed.external.uri, title: recordEmbed.external.title || "", description: recordEmbed.external.description || "", }, }; } else if (recordEmbed?.$type === "app.bsky.embed.record") { const quotedRecordUri = recordEmbed.record.uri; const allQuotePosts = [ ...this.timelinePosts, ...this.bookmarks, ...this.searchPosts, ...this.posts, ]; const quotedPost = allQuotePosts.find((p) => p.uri === quotedRecordUri); const quotedGenerator = this.feedGenerators.find( (g) => g.uri === quotedRecordUri, ); const quotedList = this.lists.find((l) => l.uri === quotedRecordUri); const quotedStarterPack = this.starterPacks.find( (s) => s.uri === quotedRecordUri, ); if (quotedPost) { quotedPostUri = quotedRecordUri; embed = { $type: "app.bsky.embed.record#view", record: { $type: "app.bsky.embed.record#viewRecord", uri: quotedPost.uri, cid: quotedPost.cid, author: quotedPost.author, value: quotedPost.record, indexedAt: quotedPost.indexedAt, labels: [], embeds: [], }, }; quotedPost.quoteCount = (quotedPost.quoteCount || 0) + 1; } else if (quotedGenerator) { embed = { $type: "app.bsky.embed.record#view", record: { $type: "app.bsky.feed.defs#generatorView", ...quotedGenerator, }, }; } else if (quotedList) { embed = { $type: "app.bsky.embed.record#view", record: { $type: "app.bsky.graph.defs#listView", ...quotedList, }, }; } else if (quotedStarterPack) { embed = { $type: "app.bsky.embed.record#view", record: { $type: "app.bsky.graph.defs#starterPackViewBasic", ...quotedStarterPack, }, }; } }
const post = createPost({ uri, text: record?.text || "", authorHandle: userProfile.handle, authorDisplayName: userProfile.displayName, embed, }); this.posts.push(post);
if (quotedPostUri) { const existingQuotes = this.postQuotes.get(quotedPostUri) || []; existingQuotes.push(post); this.postQuotes.set(quotedPostUri, existingQuotes); }
const isReply = !!record?.reply;
if (isReply) { const parentUri = record.reply.parent.uri; const allReplyPosts = [ ...this.timelinePosts, ...this.bookmarks, ...this.searchPosts, ...this.posts, ]; const parentPost = allReplyPosts.find((p) => p.uri === parentUri); if (parentPost) { parentPost.replyCount = (parentPost.replyCount || 0) + 1; } const thread = this.postThreads.get(parentUri); if (thread) { thread.replies = thread.replies || []; thread.replies.push({ $type: "app.bsky.feed.defs#threadViewPost", post, replies: [], }); } }
const feedKey = `${userProfile.did}-posts_and_author_threads`; const existing = this.authorFeeds.get(feedKey) || []; this.authorFeeds.set(feedKey, [post, ...existing]); if (!isReply) { const noRepliesKey = `${userProfile.did}-posts_no_replies`; const existingNoReplies = this.authorFeeds.get(noRepliesKey) || []; this.authorFeeds.set(noRepliesKey, [post, ...existingNoReplies]); } return post; }
async setup(page, { welcomeModalSeen = true } = {}) { // Mark the welcome modal as already seen so it doesn't open over // logged-out tests; pass welcomeModalSeen: false to test the modal itself. if (welcomeModalSeen) { await page.addInitScript(() => { sessionStorage.setItem("welcome-modal-seen", "true"); }); }
// Stub gif proxy fetches (gif embeds stream from these CDNs). await page.route( /https:\/\/(t\.gifs\.bsky\.app|.*\.klipy\.com)\/.*/, (route) => route.fulfill({ status: 200, contentType: "video/webm", body: "", }), );
// Stub the YouTube embed player so tests don't hit the network. await page.route("https://www.youtube-nocookie.com/**", (route) => route.fulfill({ status: 200, contentType: "text/html", body: "<!doctype html><html><body></body></html>", }), );
// Stub emoji-picker-element's CDN data fetch so tests don't hit the // network. Returns a tiny fixture with one emoji per group the library // requires (its assertEmojiData rejects an empty list). await page.route("**/emoji-picker-element-data@*/**", (route) => route.fulfill({ status: 200, contentType: "application/json", headers: { etag: '"test-emoji-data"' }, body: JSON.stringify([ { annotation: "party popper", emoji: "🎉", group: 0, order: 1, version: 0.6, shortcodes: ["tada"], tags: ["celebration"], }, ]), }), );
// Plugin fixture routes — serve a self-contained test plugin so plugin // e2e tests don't depend on plugins-local/. await page.route("**/plugins-local/index.json", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([ { id: TEST_PLUGIN_MANIFEST.id, name: TEST_PLUGIN_MANIFEST.name, author: TEST_PLUGIN_MANIFEST.author, description: TEST_PLUGIN_MANIFEST.description, }, ]), }), ); await page.route( `**/plugins-local/${TEST_PLUGIN_ID}/manifest.json`, (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(TEST_PLUGIN_RAW_MANIFEST), }), ); await page.route(`**/plugins-local/${TEST_PLUGIN_ID}/main.js`, (route) => route.fulfill({ status: 200, contentType: "text/javascript", body: this.localPluginSource ?? getTestPluginSource(), }), );
// Remote plugin registry routes — serve a fake registry and matching // GitHub release assets so flow tests can install remote plugins. await page.route( "**/improsocial/impro-releases/main/community-plugins.json", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(this.registryEntries), }), ); await page.route( "**/raw.githubusercontent.com/*/*/refs/tags/*/manifest.json", (route) => { const match = route .request() .url() .match(/refs\/tags\/([^/]+)\/manifest\.json$/); const version = match?.[1] ?? "0.0.0"; const id = this.registryEntries[0]?.id ?? "remote-plugin"; route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ id, name: this.registryEntries[0]?.name ?? "Remote Plugin", version, description: this.registryEntries[0]?.description, }), }); }, ); await page.route( "**/raw.githubusercontent.com/*/*/refs/tags/*/main.js", (route) => route.fulfill({ status: 200, contentType: "text/javascript", body: getTestPluginSource(), }), ); await page.route( "**/raw.githubusercontent.com/*/*/refs/tags/*/styles.css", (route) => route.fulfill({ status: 404, body: "Not Found" }), ); await page.route( "**/raw.githubusercontent.com/*/*/refs/heads/main/manifest.json", (route) => { const live = this.liveManifest ?? { id: this.registryEntries[0]?.id ?? "remote-plugin", name: this.registryEntries[0]?.name ?? "Remote Plugin", version: "1.0.0", }; route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(live), }); }, ); // Plugin README, served from the repo's main branch (remote) or the // local plugin directory. const fulfillReadme = (route) => { if (this.pluginReadme === null) { route.fulfill({ status: 404, body: "Not Found" }); return; } route.fulfill({ status: 200, contentType: "text/markdown", body: this.pluginReadme, }); }; await page.route( "**/raw.githubusercontent.com/*/*/refs/heads/main/README.md", fulfillReadme, ); await page.route("**/plugins-local/*/README.md", fulfillReadme);
await page.route("**/.well-known/atproto-did*", (route) => route.fulfill({ status: 404, body: "Not Found" }), );
// OAuth token endpoint used by seeded test sessions. `invalid_request` // (not `invalid_grant`) fails immediately without the cross-tab // rotated-session recovery delay. await page.route("**/oauth/token*", (route) => { if (this.tokenRefreshShouldFail) { route.fulfill({ status: 400, contentType: "application/json", body: JSON.stringify({ error: "invalid_request" }), }); return; } route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ access_token: "mock-access-token-refreshed", refresh_token: "mock-refresh-token-refreshed", expires_in: 3600, }), }); });
await page.route("**/xrpc/blue.microcosm.links.getBacklinks*", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ records: [], cursor: "" }), }), );
await page.route("**/xrpc/com.atproto.server.getSession*", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ did: userProfile.did, handle: userProfile.handle, }), }), );
await page.route("**/xrpc/app.bsky.actor.getPreferences*", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ preferences: [ { $type: "app.bsky.actor.defs#savedFeedsPrefV2", items: [ { type: "timeline", value: "following", pinned: true, id: "timeline-following", }, ...this.pinnedFeedUris.map((uri) => ({ type: "feed", value: uri, pinned: true, id: uri, })), ...this.pinnedListUris.map((uri) => ({ type: "list", value: uri, pinned: true, id: uri, })), ...this.savedFeedUris.map((uri) => ({ type: "feed", value: uri, pinned: false, id: uri, })), ], }, ...(this.hiddenPostUris.length > 0 ? [ { $type: "app.bsky.actor.defs#improHiddenPostsPref", items: this.hiddenPostUris, }, ] : []), ...(this.labelerSubscriptions.length > 0 ? [ { $type: "app.bsky.actor.defs#labelersPref", labelers: this.labelerSubscriptions.map((did) => ({ did, })), }, ] : []), ...this.contentLabelPrefs, ...(this.mutedWords.length > 0 ? [ { $type: "app.bsky.actor.defs#mutedWordsPref", items: this.mutedWords, }, ] : []), ...[...this.pluginSettings.entries()].map(([pluginId, data]) => ({ $type: "app.bsky.actor.defs#improPluginSettingsPref", pluginId, data, })), ...(this.installedPlugins.length > 0 ? [ { $type: "app.bsky.actor.defs#improInstalledPluginsPref", plugins: this.installedPlugins, }, ] : []), ], }), }), );
await page.route("**/xrpc/app.bsky.labeler.getServices*", (route) => { const url = new URL(route.request().url()); const requestedDids = url.searchParams.getAll("dids"); const views = requestedDids.length > 0 ? this.labelerViews.filter((v) => requestedDids.includes(v.creator.did), ) : this.labelerViews; return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ views }), }); });
await page.route("**/xrpc/app.bsky.notification.getUnreadCount*", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ count: this.notifications.filter((n) => !n.isRead).length, }), }), );
await page.route( "**/xrpc/app.bsky.notification.listNotifications*", async (route) => { if (this.notificationsDelayMs > 0) { await new Promise((resolve) => setTimeout(resolve, this.notificationsDelayMs), ); } const url = new URL(route.request().url()); const cursor = url.searchParams.get("cursor") || ""; const limit = parseInt(url.searchParams.get("limit") || "0", 10); const offset = cursor ? parseInt(cursor, 10) : 0; const reasons = url.searchParams.getAll("reasons");
const filteredNotifications = reasons.length > 0 ? this.notifications.filter((n) => reasons.includes(n.reason)) : this.notifications;
let notifications, nextCursor; if (limit) { notifications = filteredNotifications.slice(offset, offset + limit); nextCursor = offset + limit < filteredNotifications.length ? String(offset + limit) : ""; } else { notifications = filteredNotifications; nextCursor = this.notificationCursor || ""; }
return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ notifications, cursor: nextCursor }), }); }, );
await page.route("**/xrpc/app.bsky.notification.updateSeen*", (route) => { for (const notification of this.notifications) { notification.isRead = true; } return route.fulfill({ status: 200, contentType: "application/json", body: "{}", }); });
await page.route("**/xrpc/app.bsky.feed.getPosts*", async (route) => { const url = new URL(route.request().url()); const uris = url.searchParams.getAll("uris"); const delayMs = Math.max( 0, ...uris.map((uri) => this.postDelays.get(uri) ?? 0), ); if (delayMs > 0) { await new Promise((resolve) => setTimeout(resolve, delayMs)); } const posts = uris .map((uri) => this.posts.find((p) => p.uri === uri)) .filter(Boolean); return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ posts }), }); });
await page.route("**/xrpc/chat.bsky.convo.getUnreadCounts*", (route) => { const unreadAcceptedConvos = this.convos.filter( (convo) => convo.unreadCount > 0 && convo.status !== "request", ).length; const unreadRequestConvos = this.convos.filter( (convo) => convo.unreadCount > 0 && convo.status === "request", ).length; return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ unreadAcceptedConvos: Math.min(100, unreadAcceptedConvos), unreadRequestConvos: Math.min(100, unreadRequestConvos), }), }); });
await page.route("**/xrpc/chat.bsky.convo.listConvos*", (route) => { const url = new URL(route.request().url()); const readState = url.searchParams.get("readState"); const status = url.searchParams.get("status"); const cursor = url.searchParams.get("cursor") || ""; const limit = parseInt(url.searchParams.get("limit") || "0", 10); const offset = cursor ? parseInt(cursor, 10) : 0; let allConvos = this.convos; if (readState === "unread") { allConvos = allConvos.filter((convo) => convo.unreadCount > 0); } if (status) { allConvos = allConvos.filter((convo) => convo.status === status); }
let convos, nextCursor; if (limit) { convos = allConvos.slice(offset, offset + limit); nextCursor = offset + limit < allConvos.length ? String(offset + limit) : ""; } else { convos = allConvos; nextCursor = ""; }
return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ convos, cursor: nextCursor }), }); });
await page.route("**/xrpc/chat.bsky.convo.getConvo*", async (route) => { const url = new URL(route.request().url()); const convoId = url.searchParams.get("convoId"); const delayMs = this.convoDelays.get(convoId); if (delayMs) { await new Promise((resolve) => setTimeout(resolve, delayMs)); } const convo = this.convos.find((c) => c.id === convoId); if (!convo) { return route.fulfill({ status: 400, contentType: "application/json", body: JSON.stringify({ error: "InvalidConvo", message: "Conversation not found", }), }); } return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ convo }), }); });
await page.route("**/xrpc/chat.bsky.convo.getConvoMembers*", (route) => { if (this.convoMembersFailure) { const { status, error, message } = this.convoMembersFailure; return route.fulfill({ status, contentType: "application/json", body: JSON.stringify({ error, message }), }); } const url = new URL(route.request().url()); const convoId = url.searchParams.get("convoId"); const convo = this.convos.find((c) => c.id === convoId); if (!convo) { return route.fulfill({ status: 400, contentType: "application/json", body: JSON.stringify({ error: "InvalidConvo", message: "Conversation not found", }), }); } const allMembers = this.convoMemberLists.get(convoId) ?? convo.members; const cursor = url.searchParams.get("cursor") || ""; const limit = parseInt(url.searchParams.get("limit") || "50", 10); const offset = cursor ? parseInt(cursor, 10) : 0; const members = allMembers.slice(offset, offset + limit); const nextCursor = offset + limit < allMembers.length ? String(offset + limit) : ""; return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ members, ...(nextCursor ? { cursor: nextCursor } : {}), }), }); });
await page.route("**/xrpc/chat.bsky.convo.getMessages*", (route) => { const url = new URL(route.request().url()); const convoId = url.searchParams.get("convoId"); const messages = this.convoMessages.get(convoId) || []; return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ messages }), }); });
await page.route("**/xrpc/chat.bsky.convo.sendMessage*", (route) => { const body = route.request().postDataJSON(); this.sentMessageRequests.push(body); if (this.sendMessageFailure) { const { status, error, message } = this.sendMessageFailure; return route.fulfill({ status, contentType: "application/json", body: JSON.stringify({ error, message }), }); } const msgId = ++this.messageCounter; const sentMessage = { id: `msg-sent-${msgId}`, rev: `rev-sent-${msgId}`, text: body?.message?.text || "", sender: { did: userProfile.did }, sentAt: new Date().toISOString(), }; const convo = this.convos.find((c) => c.id === body?.convoId); if (convo) { convo.lastMessage = { $type: "chat.bsky.convo.defs#messageView", ...sentMessage, }; } return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(sentMessage), }); });
await page.route("**/xrpc/chat.bsky.convo.addReaction*", (route) => { const body = route.request().postDataJSON(); const { convoId, messageId, value } = body || {}; const messages = this.convoMessages.get(convoId) || []; const message = messages.find((m) => m.id === messageId); if (message) { message.reactions.push({ createdAt: new Date().toISOString(), sender: { did: userProfile.did }, value, }); } return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ message: message || {} }), }); });
await page.route( "**/xrpc/chat.bsky.group.getJoinLinkPreviews*", (route) => { const url = new URL(route.request().url()); const codes = url.searchParams.getAll("codes"); const joinLinkPreviews = codes .map((code) => this.joinLinkPreviews.get(code)) .filter(Boolean); return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ joinLinkPreviews }), }); }, );
await page.route("**/xrpc/chat.bsky.group.requestJoin*", (route) => { const body = route.request().postDataJSON(); const code = body?.code; if (this.failJoinLinkCodes.has(code)) { return route.fulfill({ status: 500, contentType: "application/json", body: JSON.stringify({ error: "ServerError" }), }); } const preview = this.joinLinkPreviews.get(code); const joinedConvo = preview?.requireApproval === false ? this.joinLinkJoinedConvos.get(code) : null; if (preview?.$type === "chat.bsky.group.defs#joinLinkPreviewView") { this.joinLinkPreviews.set(code, { ...preview, ...(joinedConvo ? { convo: joinedConvo } : {}), viewer: { ...(preview.viewer ?? {}), ...(joinedConvo ? {} : { requestedAt: new Date().toISOString() }), }, }); } return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify( joinedConvo ? { status: "joined", convo: joinedConvo } : { status: "pending" }, ), }); });
await page.route("**/xrpc/chat.bsky.convo.removeReaction*", (route) => { const body = route.request().postDataJSON(); const { convoId, messageId, value } = body || {}; const messages = this.convoMessages.get(convoId) || []; const message = messages.find((m) => m.id === messageId); if (message) { message.reactions = message.reactions.filter( (r) => !(r.value === value && r.sender.did === userProfile.did), ); } return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ message: message || {} }), }); });
await page.route("**/xrpc/chat.bsky.convo.updateRead*", (route) => { const body = route.request().postDataJSON(); const convoId = body?.convoId; const convo = this.convos.find((c) => c.id === convoId); if (convo) { convo.unreadCount = 0; } return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ convo: convo || {} }), }); });
await page.route("**/xrpc/chat.bsky.convo.getLog*", (route) => { const logs = this.chatLogs.splice(0, this.chatLogs.length); return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ logs, cursor: "" }), }); });
await page.route("**/xrpc/chat.bsky.convo.acceptConvo*", (route) => { const body = route.request().postDataJSON(); const convo = this.convos.find((c) => c.id === body?.convoId); if (convo) { convo.status = "accepted"; } return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ rev: "rev-accepted" }), }); });
await page.route("**/xrpc/chat.bsky.convo.leaveConvo*", (route) => { const body = route.request().postDataJSON(); this.convos = this.convos.filter((c) => c.id !== body?.convoId); return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ rev: "rev-left" }), }); });
await page.route( "**/xrpc/chat.bsky.convo.getConvoAvailability*", (route) => { const url = new URL(route.request().url()); const members = url.searchParams.getAll("members"); const otherDid = members.find((m) => m !== userProfile.did); const profile = this.profiles.get(otherDid); return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ canChat: profile?.canChat ?? false }), }); }, );
await page.route("**/xrpc/chat.bsky.convo.getConvoForMembers*", (route) => { if (this.convoForMembersError) { return route.fulfill({ status: 400, contentType: "application/json", body: JSON.stringify({ error: this.convoForMembersError, message: this.convoForMembersError, }), }); } const url = new URL(route.request().url()); const members = url.searchParams.getAll("members"); const otherDid = members.find((m) => m !== userProfile.did); // Find existing convo with this member const existingConvo = this.convos.find((c) => c.members.some((m) => m.did === otherDid), ); if (existingConvo) { return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ convo: existingConvo }), }); } // Create a new convo const profile = this.profiles.get(otherDid); const newConvo = { id: `convo-new-${++this.messageCounter}`, rev: `rev-new-${this.messageCounter}`, members: [userProfile, profile || { did: otherDid }], status: "accepted", unreadCount: 0, lastMessage: undefined, }; this.convos.push(newConvo); this.convoMessages.set(newConvo.id, []); return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ convo: newConvo }), }); });
await page.route("**/xrpc/app.bsky.graph.muteActor*", (route) => { const body = route.request().postDataJSON(); const actor = body?.actor; const profile = this.profiles.get(actor); if (profile) { profile.viewer = { ...profile.viewer, muted: true }; } return route.fulfill({ status: 200, contentType: "application/json", body: "{}", }); });
await page.route("**/xrpc/app.bsky.graph.unmuteActor*", (route) => { const body = route.request().postDataJSON(); const actor = body?.actor; const profile = this.profiles.get(actor); if (profile) { profile.viewer = { ...profile.viewer, muted: false }; } return route.fulfill({ status: 200, contentType: "application/json", body: "{}", }); });
await page.route("**/xrpc/app.bsky.graph.muteActorList*", (route) => { const body = route.request().postDataJSON(); const listUri = body?.list; const list = this.lists.find((l) => l.uri === listUri); if (list) { list.viewer = { ...list.viewer, muted: true }; } return route.fulfill({ status: 200, contentType: "application/json", body: "{}", }); });
await page.route("**/xrpc/app.bsky.graph.unmuteActorList*", (route) => { const body = route.request().postDataJSON(); const listUri = body?.list; const list = this.lists.find((l) => l.uri === listUri); if (list) { list.viewer = { ...list.viewer, muted: false }; } return route.fulfill({ status: 200, contentType: "application/json", body: "{}", }); });
await page.route( "**/xrpc/app.bsky.notification.putActivitySubscription*", (route) => { const body = route.request().postDataJSON(); const subject = body?.subject; const activitySubscription = body?.activitySubscription; const profile = this.profiles.get(subject); if (profile) { profile.viewer = { ...profile.viewer, activitySubscription }; } return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ subject, activitySubscription }), }); }, );
await page.route("**/xrpc/app.bsky.feed.getActorLikes*", (route) => { const url = new URL(route.request().url()); const actor = url.searchParams.get("actor"); const cursor = url.searchParams.get("cursor") || ""; const limit = parseInt(url.searchParams.get("limit") || "0", 10); const offset = cursor ? parseInt(cursor, 10) : 0; const allPosts = this.authorFeeds.get(`${actor}-likes`) || [];
let posts, nextCursor; if (limit) { posts = allPosts.slice(offset, offset + limit); nextCursor = offset + limit < allPosts.length ? String(offset + limit) : ""; } else { posts = allPosts; nextCursor = ""; }
return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ feed: posts.map((post) => ({ post })), cursor: nextCursor, }), }); });
await page.route("**/xrpc/app.bsky.feed.getAuthorFeed*", (route) => { const url = new URL(route.request().url()); const actor = url.searchParams.get("actor"); const filter = url.searchParams.get("filter") || ""; const cursor = url.searchParams.get("cursor") || ""; const limit = parseInt(url.searchParams.get("limit") || "0", 10); const offset = cursor ? parseInt(cursor, 10) : 0; const allPosts = this.authorFeeds.get(`${actor}-${filter}`) || [];
let posts, nextCursor; if (limit) { posts = allPosts.slice(offset, offset + limit); nextCursor = offset + limit < allPosts.length ? String(offset + limit) : ""; } else { posts = allPosts; nextCursor = ""; }
return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ feed: posts.map((post) => ({ post })), cursor: nextCursor, }), }); });
await page.route("**/xrpc/app.bsky.actor.getProfile*", (route) => { const url = new URL(route.request().url()); const actor = url.searchParams.get("actor"); const profile = this.profiles.get(actor) || userProfile; return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(profile), }); });
await page.route("**/xrpc/app.bsky.actor.getProfiles*", (route) => { const url = new URL(route.request().url()); const actors = url.searchParams.getAll("actors"); const profiles = actors .map((actor) => this.profiles.get(actor)) .filter(Boolean); return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ profiles }), }); });
await page.route("**/xrpc/app.bsky.bookmark.getBookmarks*", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ bookmarks: this.bookmarks.map((post) => ({ item: post })), }), }), );
await page.route("**/xrpc/app.bsky.bookmark.createBookmark*", (route) => { const body = route.request().postDataJSON(); const postUri = body?.uri; const allPosts = [ ...this.timelinePosts, ...this.bookmarks, ...this.searchPosts, ...this.posts, ]; const post = allPosts.find((p) => p.uri === postUri); if (post) { post.viewer.bookmarked = true; if (!this.bookmarks.includes(post)) { this.bookmarks.push(post); } } return route.fulfill({ status: 200, contentType: "application/json", body: "{}", }); });
await page.route("**/xrpc/app.bsky.bookmark.deleteBookmark*", (route) => { const body = route.request().postDataJSON(); const postUri = body?.uri; const idx = this.bookmarks.findIndex((p) => p.uri === postUri); if (idx !== -1) { this.bookmarks[idx].viewer.bookmarked = false; this.bookmarks.splice(idx, 1); } return route.fulfill({ status: 200, contentType: "application/json", body: "{}", }); });
await page.route("**/xrpc/app.bsky.draft.getDrafts*", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ drafts: this.drafts }), }), );
await page.route("**/xrpc/app.bsky.draft.createDraft*", (route) => { if (this.draftLimitReached) { return route.fulfill({ status: 400, contentType: "application/json", body: JSON.stringify({ error: "DraftLimitReached", message: "Draft limit reached", }), }); } const body = route.request().postDataJSON(); const id = `draft-${++this.draftCounter}`; const now = new Date().toISOString(); this.drafts.unshift({ id, createdAt: now, updatedAt: now, draft: body.draft, }); return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ id }), }); });
await page.route("**/xrpc/app.bsky.draft.updateDraft*", (route) => { const body = route.request().postDataJSON(); const existing = this.drafts.find( (draftView) => draftView.id === body.draft.id, ); if (existing) { existing.draft = body.draft.draft; existing.updatedAt = new Date().toISOString(); } return route.fulfill({ status: 200, contentType: "application/json", body: "{}", }); });
await page.route("**/xrpc/app.bsky.draft.deleteDraft*", (route) => { const body = route.request().postDataJSON(); this.deletedDraftIds.push(body.id); this.drafts = this.drafts.filter((draftView) => draftView.id !== body.id); return route.fulfill({ status: 200, contentType: "application/json", body: "{}", }); });
await page.route("**/xrpc/app.bsky.actor.searchActors*", (route) => { this.searchRequestCounts.profiles += 1; const url = new URL(route.request().url()); const cursor = url.searchParams.get("cursor") || ""; const limit = parseInt(url.searchParams.get("limit") || "0", 10); const offset = cursor ? parseInt(cursor, 10) : 0;
let actors, nextCursor; if (limit) { actors = this.searchProfiles.slice(offset, offset + limit); nextCursor = offset + limit < this.searchProfiles.length ? String(offset + limit) : ""; } else { actors = this.searchProfiles; nextCursor = ""; }
return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ actors, cursor: nextCursor }), }); });
await page.route( "**/xrpc/app.bsky.unspecced.getPopularFeedGenerators*", (route) => { this.searchRequestCounts.feeds += 1; const url = new URL(route.request().url()); const cursor = url.searchParams.get("cursor") || ""; const limit = parseInt(url.searchParams.get("limit") || "0", 10); const offset = cursor ? parseInt(cursor, 10) : 0;
let feeds, nextCursor; if (limit) { feeds = this.searchFeedGenerators.slice(offset, offset + limit); nextCursor = offset + limit < this.searchFeedGenerators.length ? String(offset + limit) : ""; } else { feeds = this.searchFeedGenerators; nextCursor = ""; }
return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ feeds, cursor: nextCursor }), }); }, );
await page.route("**/xrpc/app.bsky.feed.getActorFeeds*", (route) => { const url = new URL(route.request().url()); const actor = url.searchParams.get("actor") || ""; const cursor = url.searchParams.get("cursor") || ""; const limit = parseInt(url.searchParams.get("limit") || "0", 10); const offset = cursor ? parseInt(cursor, 10) : 0; const allFeeds = this.actorFeeds.get(actor) || [];
let feeds, nextCursor; if (limit) { feeds = allFeeds.slice(offset, offset + limit); nextCursor = offset + limit < allFeeds.length ? String(offset + limit) : ""; } else { feeds = allFeeds; nextCursor = ""; }
return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ feeds, cursor: nextCursor }), }); });
await page.route("**/xrpc/app.bsky.feed.searchPosts*", (route) => { const url = new URL(route.request().url()); const cursor = url.searchParams.get("cursor") || ""; const limit = parseInt(url.searchParams.get("limit") || "0", 10); const offset = cursor ? parseInt(cursor, 10) : 0; const sort = url.searchParams.get("sort") || "top"; this.searchRequestCounts[sort] += 1; const sortedPosts = this.searchPostsBySort[sort] ?? this.searchPosts;
let posts, nextCursor; if (limit) { posts = sortedPosts.slice(offset, offset + limit); nextCursor = offset + limit < sortedPosts.length ? String(offset + limit) : ""; } else { posts = sortedPosts; nextCursor = ""; }
return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ posts, cursor: nextCursor }), }); });
await page.route("**/xrpc/app.bsky.feed.getTimeline*", async (route) => { if (this.timelineDelayMs > 0) { await new Promise((resolve) => setTimeout(resolve, this.timelineDelayMs), ); } const url = new URL(route.request().url()); const cursor = url.searchParams.get("cursor") || ""; const limit = parseInt(url.searchParams.get("limit") || "0", 10); const offset = cursor ? parseInt(cursor, 10) : 0;
const blockedDids = new Set(); for (const [did, profile] of this.profiles) { if (profile.viewer?.blocking || profile.viewer?.muted) { blockedDids.add(did); } } const allPosts = this.timelinePosts.filter( (post) => !blockedDids.has(post.author?.did), );
let posts, nextCursor; if (limit) { posts = allPosts.slice(offset, offset + limit); nextCursor = offset + limit < allPosts.length ? String(offset + limit) : ""; } else { posts = allPosts; nextCursor = ""; }
return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ feed: posts.map((post) => ({ post })), cursor: nextCursor, }), }); });
await page.route("**/xrpc/app.bsky.feed.getListFeed*", (route) => { const url = new URL(route.request().url()); const listUri = url.searchParams.get("list"); const feed = this.feeds.get(listUri) || []; return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ feed, cursor: "" }), }); });
// Order matters: Playwright checks routes in LIFO order, so register // the most general pattern first (checked last) and most specific last. await page.route("**/xrpc/app.bsky.feed.getFeed*", (route) => { const url = new URL(route.request().url()); const feedUri = url.searchParams.get("feed"); const feed = this.feeds.get(feedUri) || []; return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ feed, cursor: "" }), }); });
await page.route("**/xrpc/app.bsky.feed.getFeedGenerator*", (route) => { const url = new URL(route.request().url()); const feedUri = url.searchParams.get("feed"); const generator = this.feedGenerators.find((g) => g.uri === feedUri); return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ view: generator || {} }), }); });
await page.route("**/xrpc/app.bsky.feed.getFeedGenerators*", (route) => { const url = new URL(route.request().url()); const feedUris = url.searchParams.getAll("feeds"); const feeds = feedUris.map( (uri) => this.feedGenerators.find((g) => g.uri === uri) || {}, ); return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ feeds }), }); });
await page.route("**/xrpc/app.bsky.graph.getList*", (route) => { const url = new URL(route.request().url()); const listUri = url.searchParams.get("list"); const list = this.lists.find((l) => l.uri === listUri) || {}; const members = this.listMembers.get(listUri) || []; const items = members.map((profile) => ({ subject: profile })); return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ list, items, cursor: "" }), }); });
await page.route("https://ogcard.cdn.bsky.app/**", (route) => { return route.fulfill({ status: 200, contentType: "image/png", body: Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", "base64", ), }); });
await page.route("**/xrpc/app.bsky.graph.getStarterPack?*", (route) => { const url = new URL(route.request().url()); const starterPackUri = url.searchParams.get("starterPack"); const starterPack = this.starterPacks.find((s) => s.uri === starterPackUri) || {}; return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ starterPack }), }); });
await page.route("**/xrpc/app.bsky.graph.getLists*", (route) => { const url = new URL(route.request().url()); const actor = url.searchParams.get("actor") || ""; const cursor = url.searchParams.get("cursor") || ""; const limit = parseInt(url.searchParams.get("limit") || "0", 10); const offset = cursor ? parseInt(cursor, 10) : 0; const allLists = this.actorLists.get(actor) || [];
let lists, nextCursor; if (limit) { lists = allLists.slice(offset, offset + limit); nextCursor = offset + limit < allLists.length ? String(offset + limit) : ""; } else { lists = allLists; nextCursor = ""; }
return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ lists, cursor: nextCursor }), }); });
await page.route( "**/xrpc/app.bsky.graph.getListsWithMembership*", (route) => { const url = new URL(route.request().url()); const actor = url.searchParams.get("actor"); const limit = parseInt(url.searchParams.get("limit") || "50", 10); const cursorParam = url.searchParams.get("cursor") || ""; const start = cursorParam ? parseInt(cursorParam, 10) : 0; const lists = this.actorLists.get(userProfile.did) || []; const page = lists.slice(start, start + limit); const listsWithMembership = page.map((list) => { const item = this.currentUserListItems.find( (entry) => entry.listUri === list.uri && entry.subjectDid === actor, ); return item ? { list, listItem: { uri: item.uri, subject: actor } } : { list }; }); const nextStart = start + page.length; const cursor = nextStart < lists.length ? String(nextStart) : ""; return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ listsWithMembership, cursor }), }); }, );
await page.route("**/xrpc/app.bsky.feed.getLikes*", (route) => { const url = new URL(route.request().url()); const uri = url.searchParams.get("uri"); const likes = this.postLikes.get(uri) || []; return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ likes, cursor: "" }), }); });
await page.route("**/xrpc/app.bsky.feed.getQuotes*", (route) => { const url = new URL(route.request().url()); const uri = url.searchParams.get("uri"); const posts = this.postQuotes.get(uri) || []; return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ posts, cursor: "" }), }); });
await page.route("**/xrpc/app.bsky.feed.getRepostedBy*", (route) => { const url = new URL(route.request().url()); const uri = url.searchParams.get("uri"); const repostedBy = this.postReposts.get(uri) || []; return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ repostedBy, cursor: "" }), }); });
await page.route("**/xrpc/app.bsky.feed.getPostThread*", async (route) => { const url = new URL(route.request().url()); const uri = url.searchParams.get("uri"); const customThread = this.postThreads.get(uri); if (customThread) { const delayMs = this.postThreadDelays.get(uri); if (delayMs) { await new Promise((resolve) => setTimeout(resolve, delayMs)); } return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ thread: customThread }), }); } const allPosts = [ ...this.timelinePosts, ...this.bookmarks, ...this.searchPosts, ...this.posts, ]; const post = allPosts.find((p) => p.uri === uri); return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ thread: { $type: "app.bsky.feed.defs#threadViewPost", post: post || {}, replies: [], }, }), }); });
await page.route( "**/xrpc/app.bsky.unspecced.getPostThreadOtherV2*", (route) => { const url = new URL(route.request().url()); const anchor = url.searchParams.get("anchor"); const customThreadOther = this.postThreadOthers.get(anchor); return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ thread: customThreadOther || [] }), }); }, );
await page.route("**/xrpc/app.bsky.graph.getFollowers*", (route) => { const url = new URL(route.request().url()); const actor = url.searchParams.get("actor"); const cursor = url.searchParams.get("cursor") || ""; const limit = parseInt(url.searchParams.get("limit") || "0", 10); const offset = cursor ? parseInt(cursor, 10) : 0; const allFollowers = this.profileFollowers.get(actor) || [];
let followers, nextCursor; if (limit) { followers = allFollowers.slice(offset, offset + limit); nextCursor = offset + limit < allFollowers.length ? String(offset + limit) : ""; } else { followers = allFollowers; nextCursor = ""; }
return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ followers, cursor: nextCursor }), }); });
await page.route("**/xrpc/app.bsky.graph.getKnownFollowers*", (route) => { const url = new URL(route.request().url()); const actor = url.searchParams.get("actor"); const cursor = url.searchParams.get("cursor") || ""; const limit = parseInt(url.searchParams.get("limit") || "0", 10); const offset = cursor ? parseInt(cursor, 10) : 0; const allFollowers = this.knownFollowers.get(actor) || [];
let followers, nextCursor; if (limit) { followers = allFollowers.slice(offset, offset + limit); nextCursor = offset + limit < allFollowers.length ? String(offset + limit) : ""; } else { followers = allFollowers; nextCursor = ""; }
return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ followers, cursor: nextCursor }), }); });
await page.route("**/xrpc/app.bsky.graph.getFollows*", (route) => { const url = new URL(route.request().url()); const actor = url.searchParams.get("actor"); const cursor = url.searchParams.get("cursor") || ""; const limit = parseInt(url.searchParams.get("limit") || "0", 10); const offset = cursor ? parseInt(cursor, 10) : 0; const allFollows = this.profileFollows.get(actor) || [];
let follows, nextCursor; if (limit) { follows = allFollows.slice(offset, offset + limit); nextCursor = offset + limit < allFollows.length ? String(offset + limit) : ""; } else { follows = allFollows; nextCursor = ""; }
return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ follows, cursor: nextCursor }), }); });
await page.route("**/xrpc/app.bsky.graph.getBlocks*", (route) => { const url = new URL(route.request().url()); const cursor = url.searchParams.get("cursor") || ""; const limit = parseInt(url.searchParams.get("limit") || "0", 10); const offset = cursor ? parseInt(cursor, 10) : 0;
let blocks, nextCursor; if (limit) { blocks = this.blockedProfiles.slice(offset, offset + limit); nextCursor = offset + limit < this.blockedProfiles.length ? String(offset + limit) : ""; } else { blocks = this.blockedProfiles; nextCursor = ""; }
return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ blocks, cursor: nextCursor }), }); });
await page.route("**/xrpc/app.bsky.graph.getMutes*", (route) => { const url = new URL(route.request().url()); const cursor = url.searchParams.get("cursor") || ""; const limit = parseInt(url.searchParams.get("limit") || "0", 10); const offset = cursor ? parseInt(cursor, 10) : 0;
let mutes, nextCursor; if (limit) { mutes = this.mutedProfiles.slice(offset, offset + limit); nextCursor = offset + limit < this.mutedProfiles.length ? String(offset + limit) : ""; } else { mutes = this.mutedProfiles; nextCursor = ""; }
return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ mutes, cursor: nextCursor }), }); });
await page.route("**/xrpc/com.atproto.identity.resolveHandle*", (route) => { const url = new URL(route.request().url()); const handle = url.searchParams.get("handle"); const allPosts = [ ...this.timelinePosts, ...this.bookmarks, ...this.searchPosts, ...this.posts, ]; const postAuthor = allPosts.find( (p) => p.author?.handle === handle, )?.author; const generator = this.feedGenerators.find( (g) => g.creator.handle === handle, ); const list = this.lists.find((l) => l.creator?.handle === handle); const starterPack = this.starterPacks.find( (s) => s.creator?.handle === handle, ); const profileEntry = [...this.profiles.values()].find( (p) => p.handle === handle, ); const did = postAuthor?.did || generator?.creator?.did || list?.creator?.did || starterPack?.creator?.did || profileEntry?.did; if (!did) { return route.fulfill({ status: 404, body: JSON.stringify({ error: "NotFound" }), }); } return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ did }), }); });
await page.route("**/xrpc/com.atproto.repo.deleteRecord*", (route) => { const body = route.request().postDataJSON(); const collection = body?.collection; const rkey = body?.rkey;
if (collection === "app.bsky.feed.like") { const feedKey = `${userProfile.did}-likes`; const likes = this.authorFeeds.get(feedKey) || []; this.authorFeeds.set( feedKey, likes.filter((p) => p.viewer?.like?.split("/").pop() !== rkey), ); }
if (collection === "app.bsky.feed.repost") { const allPosts = [ ...this.timelinePosts, ...this.bookmarks, ...this.searchPosts, ...this.posts, ]; for (const post of allPosts) { if ( post.viewer?.repost && post.viewer.repost.split("/").pop() === rkey ) { delete post.viewer.repost; post.repostCount = Math.max(0, (post.repostCount || 0) - 1); const feedKey = `${userProfile.did}-posts_and_author_threads`; const existing = this.authorFeeds.get(feedKey) || []; this.authorFeeds.set( feedKey, existing.filter((p) => p !== post), ); break; } } }
if (collection === "app.bsky.graph.follow") { for (const [did, profile] of this.profiles) { if ( profile.viewer?.following && profile.viewer.following.split("/").pop() === rkey ) { profile.viewer = { ...profile.viewer, following: undefined }; if ( profile.followersCount !== undefined && profile.followersCount > 0 ) { profile.followersCount--; } const follows = this.profileFollows.get(userProfile.did) || []; this.profileFollows.set( userProfile.did, follows.filter((p) => p.did !== did), ); break; } } }
if (collection === "app.bsky.graph.block") { for (const [, profile] of this.profiles) { if ( profile.viewer?.blocking && profile.viewer.blocking.split("/").pop() === rkey ) { profile.viewer = { ...profile.viewer, blocking: undefined }; break; } } }
if (collection === "app.bsky.graph.listblock") { for (const list of this.lists) { if ( list.viewer?.blocked && list.viewer.blocked.split("/").pop() === rkey ) { list.viewer = { ...list.viewer, blocked: undefined }; break; } } }
if (collection === "app.bsky.feed.post") { const postUri = `at://${userProfile.did}/${collection}/${rkey}`; this.timelinePosts = this.timelinePosts.filter( (p) => p.uri !== postUri, ); this.posts = this.posts.filter((p) => p.uri !== postUri); for (const [key, posts] of this.authorFeeds) { this.authorFeeds.set( key, posts.filter((p) => p.uri !== postUri), ); } }
if (collection === "app.bsky.graph.listitem") { const itemUri = `at://${userProfile.did}/${collection}/${rkey}`; this.currentUserListItems = this.currentUserListItems.filter( (item) => item.uri !== itemUri, ); }
return route.fulfill({ status: 200, contentType: "application/json", body: "{}", }); });
await page.route("**/xrpc/com.atproto.repo.listRecords*", (route) => { const url = new URL(route.request().url()); const collection = url.searchParams.get("collection"); if (collection === "app.bsky.graph.listitem") { const records = this.currentUserListItems.map((item) => ({ uri: item.uri, cid: `bafyrei${item.uri.split("/").pop()}`, value: { $type: "app.bsky.graph.listitem", subject: item.subjectDid, list: item.listUri, createdAt: "2025-01-01T00:00:00.000Z", }, })); return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ records, cursor: "" }), }); } return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ records: [], cursor: "" }), }); });
await page.route("**/xrpc/com.atproto.repo.applyWrites*", (route) => { const body = route.request().postDataJSON(); const writes = body?.writes ?? []; this.applyWritesCalls.push(writes); for (const write of writes) { if (write.collection !== "app.bsky.feed.post") continue; const uri = `at://${userProfile.did}/app.bsky.feed.post/${write.rkey}`; this._ingestPostRecord(write.value, uri); } return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ results: [] }), }); });
await page.route("**/xrpc/com.atproto.repo.createRecord*", (route) => { const body = route.request().postDataJSON(); const collection = body?.collection; const rkey = `rkey-${++this.createRecordCounter}`; const uri = `at://${userProfile.did}/${collection}/${rkey}`; const cid = `bafyrei${rkey}`;
if (collection === "app.bsky.graph.listitem") { const record = body?.record || {}; this.currentUserListItems.push({ uri, listUri: record.list, subjectDid: record.subject, }); }
if (collection === "app.bsky.feed.post") { this._ingestPostRecord(body?.record, uri); }
if (collection === "app.bsky.feed.like") { const subjectUri = body?.record?.subject?.uri; const allPosts = [ ...this.timelinePosts, ...this.bookmarks, ...this.searchPosts, ...this.posts, ]; const post = allPosts.find((p) => p.uri === subjectUri); if (post) { post.viewer.like = uri; const feedKey = `${userProfile.did}-likes`; const existing = this.authorFeeds.get(feedKey) || []; this.authorFeeds.set(feedKey, [...existing, post]); } }
if (collection === "app.bsky.feed.repost") { const subjectUri = body?.record?.subject?.uri; const allPosts = [ ...this.timelinePosts, ...this.bookmarks, ...this.searchPosts, ...this.posts, ]; const post = allPosts.find((p) => p.uri === subjectUri); if (post) { post.viewer.repost = uri; post.repostCount = (post.repostCount || 0) + 1; const feedKey = `${userProfile.did}-posts_and_author_threads`; const existing = this.authorFeeds.get(feedKey) || []; this.authorFeeds.set(feedKey, [post, ...existing]); } }
if (collection === "app.bsky.graph.follow") { const subjectDid = body?.record?.subject; const profile = this.profiles.get(subjectDid); if (profile) { profile.viewer = { ...profile.viewer, following: uri }; if (profile.followersCount !== undefined) { profile.followersCount++; } const follows = this.profileFollows.get(userProfile.did) || []; if (!follows.find((p) => p.did === subjectDid)) { follows.push(profile); this.profileFollows.set(userProfile.did, follows); } } }
if (collection === "app.bsky.graph.block") { const subjectDid = body?.record?.subject; const profile = this.profiles.get(subjectDid); if (profile) { profile.viewer = { ...profile.viewer, blocking: uri }; } }
if (collection === "app.bsky.graph.listblock") { const subjectUri = body?.record?.subject; const list = this.lists.find((l) => l.uri === subjectUri); if (list) { list.viewer = { ...list.viewer, blocked: uri }; } }
return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ uri, cid }), }); });
await page.route("**/xrpc/app.bsky.actor.putPreferences*", (route) => { const body = route.request().postDataJSON(); const savedFeedsPref = body?.preferences?.find( (p) => p.$type === "app.bsky.actor.defs#savedFeedsPrefV2", ); if (savedFeedsPref) { this.pinnedFeedUris = savedFeedsPref.items .filter((item) => item.type === "feed" && item.pinned) .map((item) => item.value); this.savedFeedUris = savedFeedsPref.items .filter((item) => item.type === "feed" && !item.pinned) .map((item) => item.value); } const hiddenPostsPref = body?.preferences?.find( (p) => p.$type === "app.bsky.actor.defs#improHiddenPostsPref", ); if (hiddenPostsPref) { this.hiddenPostUris = hiddenPostsPref.items || []; } const labelersPref = body?.preferences?.find( (p) => p.$type === "app.bsky.actor.defs#labelersPref", ); if (labelersPref) { this.labelerSubscriptions = labelersPref.labelers.map((l) => l.did); } else { this.labelerSubscriptions = []; } this.contentLabelPrefs = (body?.preferences || []).filter( (p) => p.$type === "app.bsky.actor.defs#contentLabelPref", ); const mutedWordsPref = body?.preferences?.find( (p) => p.$type === "app.bsky.actor.defs#mutedWordsPref", ); if (mutedWordsPref) { this.mutedWords = mutedWordsPref.items || []; } const installedPluginsPref = body?.preferences?.find( (p) => p.$type === "app.bsky.actor.defs#improInstalledPluginsPref", ); if (installedPluginsPref) { this.installedPlugins = installedPluginsPref.plugins || []; } const pluginSettingsPrefs = (body?.preferences || []).filter( (p) => p.$type === "app.bsky.actor.defs#improPluginSettingsPref", ); this.pluginSettings = new Map( pluginSettingsPrefs.map((p) => [p.pluginId, p.data]), ); return route.fulfill({ status: 200, contentType: "application/json", body: "{}", }); });
await page.route( "**/xrpc/com.atproto.moderation.createReport*", (route) => { const body = route.request().postDataJSON(); this.reportPayloads.push(body); return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ id: this.reportPayloads.length, reasonType: body?.reasonType, subject: body?.subject, reportedBy: userProfile.did, createdAt: new Date().toISOString(), }), }); }, );
await page.route("**/xrpc/app.bsky.feed.sendInteractions*", (route) => { const body = route.request().postDataJSON(); this.interactionPayloads.push(body); return route.fulfill({ status: 200, contentType: "application/json", body: "{}", }); });
await page.route("**/xrpc/com.atproto.repo.getRecord*", (route) => { const url = new URL(route.request().url()); const collection = url.searchParams.get("collection"); const rkey = url.searchParams.get("rkey"); if (collection === "app.bsky.actor.profile" && rkey === "self") { const profile = this.profiles.get(userProfile.did) || userProfile; const value = { $type: "app.bsky.actor.profile", displayName: profile.displayName || "", description: profile.description || "", }; if (profile.pinnedPost) { value.pinnedPost = profile.pinnedPost; } return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ uri: `at://${userProfile.did}/${collection}/${rkey}`, cid: "bafyreiprofilerecord", value, }), }); } return route.fulfill({ status: 404, body: "{}" }); });
await page.route("**/xrpc/com.atproto.repo.putRecord*", (route) => { const body = route.request().postDataJSON(); const collection = body?.collection; if (collection === "app.bsky.actor.profile") { const record = body?.record || {}; const profile = this.profiles.get(userProfile.did) || { ...userProfile, }; profile.displayName = record.displayName || ""; profile.description = record.description || ""; if (record.avatar) { profile.avatar = "mock-avatar-url"; } else if (!record.avatar && record.avatar !== undefined) { profile.avatar = ""; } if (record.banner) { profile.banner = "mock-banner-url"; } else if (!record.banner && record.banner !== undefined) { profile.banner = ""; } if (record.pinnedPost) { profile.pinnedPost = record.pinnedPost; } else { delete profile.pinnedPost; } this.profiles.set(userProfile.did, profile); } return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ uri: `at://${userProfile.did}/${collection}/self`, cid: "bafyreiupdatedrecord", }), }); });
await page.route("**/xrpc/com.atproto.repo.uploadBlob*", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ blob: { $type: "blob", // suffix must stay within the base32 alphabet (computeRecordCid // rejects invalid CID characters like "0", "1", "8", "9") ref: { $link: `bafkreimockblob${String(++this.blobCounter) .split("") .map((digit) => "abcdefghij"[digit]) .join("")}`, }, mimeType: "image/jpeg", size: 50000, }, }), }), );
await page.route("**/xrpc/com.atproto.server.getServiceAuth*", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ token: "mock-service-auth-token" }), }), );
await page.route("**/xrpc/app.bsky.video.getUploadLimits*", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ canUpload: this.videoCanUpload !== false, remainingDailyVideos: 25, remainingDailyBytes: 100_000_000, message: this.videoUploadMessage || "", }), }), );
await page.route("**/xrpc/app.bsky.video.uploadVideo*", (route) => { this.videoJobCounter = (this.videoJobCounter || 0) + 1; const jobId = `mock-video-job-${this.videoJobCounter}`; this.videoJobPollCounts = this.videoJobPollCounts || new Map(); this.videoJobPollCounts.set(jobId, 0); return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ jobId, did: userProfile.did, state: "JOB_STATE_ENCODING", progress: 0, }), }); });
await page.route("**/xrpc/app.bsky.video.getJobStatus*", (route) => { const url = new URL(route.request().url()); const jobId = url.searchParams.get("jobId"); if (this.videoJobShouldFail) { return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ jobStatus: { jobId, did: userProfile.did, state: "JOB_STATE_FAILED", progress: 0, error: "mock failure", message: "mock failure", }, }), }); } this.videoJobPollCounts = this.videoJobPollCounts || new Map(); const count = (this.videoJobPollCounts.get(jobId) || 0) + 1; this.videoJobPollCounts.set(jobId, count); // Complete on the second poll if (count >= 2) { return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ jobStatus: { jobId, did: userProfile.did, state: "JOB_STATE_COMPLETED", progress: 1, blob: { $type: "blob", ref: { $link: `bafkreimockvideo${this.videoJobCounter}` }, mimeType: "video/mp4", size: 1024, }, }, }), }); } return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ jobStatus: { jobId, did: userProfile.did, state: "JOB_STATE_ENCODING", progress: 0.5, }, }), }); });
await page.route( (url) => url.toString().includes("cardyb.bsky.app/v1/extract"), (route) => { const url = new URL(route.request().url()); const targetUrl = url.searchParams.get("url"); const meta = this.externalLinkCards.get(targetUrl) || { title: targetUrl, description: "", }; return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(meta), }); }, );
await page.route( "**/xrpc/app.bsky.actor.searchActorsTypeahead*", async (route) => { this.searchRequestCounts.typeahead += 1; if (this.typeaheadDelayMs > 0) { await new Promise((resolve) => setTimeout(resolve, this.typeaheadDelayMs), ); } return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ actors: this.typeaheadProfiles }), }); }, ); }}
export class MockConstellation { constructor() { this.backlinks = new Map(); }
setBacklinks(subject, records) { this.backlinks.set(subject, records); }
async setup(page) { await page.route("**/xrpc/blue.microcosm.links.getBacklinks*", (route) => { const url = new URL(route.request().url()); const subject = url.searchParams.get("subject"); const records = this.backlinks.get(subject) || []; return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ records, cursor: "" }), }); }); }}