diff --git a/src/css/style.css b/src/css/style.css index 8ef057ad..3dac0e62 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -6137,6 +6137,17 @@ emoji-picker-dialog emoji-picker, flex: none; } +.profile-reposts-hidden { + display: inline-flex; + align-items: center; + gap: 4px; +} + +.profile-reposts-hidden app-icon { + width: 14px; + height: 14px; +} + .profile-blocked-badge { font-size: 13px; background: var(--secondary-button-color); diff --git a/src/img/icons/custom/repost-off.svg b/src/img/icons/custom/repost-off.svg new file mode 100644 index 00000000..57dd1592 --- /dev/null +++ b/src/img/icons/custom/repost-off.svg @@ -0,0 +1 @@ + diff --git a/src/js/api.js b/src/js/api.js index fb930ae2..da182c7d 100644 --- a/src/js/api.js +++ b/src/js/api.js @@ -1118,6 +1118,18 @@ export class Api { return res; } + async muteActorReposts(did) { + const res = await this.appViewRequest("app.bsky.graph.muteActor", { + method: "POST", + body: { + actor: did, + onlyReposts: true, + }, + parseJson: false, + }); + return res; + } + async unmuteActor(did) { const res = await this.appViewRequest("app.bsky.graph.unmuteActor", { method: "POST", diff --git a/src/js/dataLayer/derived.js b/src/js/dataLayer/derived.js index 50592b80..92dc561f 100644 --- a/src/js/dataLayer/derived.js +++ b/src/js/dataLayer/derived.js @@ -167,6 +167,8 @@ export class Derived extends ReactiveStore { this.patchStore.hasPendingProfilePatch(did, [ "muteProfile", "unmuteProfile", + "muteProfileReposts", + "unmuteProfileReposts", ]), ); this.$patchedPosts = new ComputedMap((uri) => { diff --git a/src/js/dataLayer/mutations.js b/src/js/dataLayer/mutations.js index 79e80227..79e8e5e3 100644 --- a/src/js/dataLayer/mutations.js +++ b/src/js/dataLayer/mutations.js @@ -775,17 +775,8 @@ export class Mutations { await this.api.muteActor(profile.did); this._updateStoredProfile(profile, (stored) => ({ ...stored, - viewer: { ...stored.viewer, muted: true }, + viewer: { ...stored.viewer, muted: true, mutedOnlyReposts: false }, })); - this._updatePostsByAuthor(profile.did, (post) => { - return { - ...post, - author: { - ...post.author, - viewer: { ...post.author.viewer, muted: true }, - }, - }; - }); const mutedProfiles = this.dataStore.$mutedProfiles.get(); if (mutedProfiles) { const alreadyListed = mutedProfiles.mutes.some( @@ -814,17 +805,8 @@ export class Mutations { await this.api.unmuteActor(profile.did); this._updateStoredProfile(profile, (stored) => ({ ...stored, - viewer: { ...stored.viewer, muted: false }, + viewer: { ...stored.viewer, muted: false, mutedOnlyReposts: false }, })); - this._updatePostsByAuthor(profile.did, (post) => { - return { - ...post, - author: { - ...post.author, - viewer: { ...post.author.viewer, muted: false }, - }, - }; - }); const mutedProfiles = this.dataStore.$mutedProfiles.get(); if (mutedProfiles) { this.dataStore.$mutedProfiles.set({ @@ -842,6 +824,42 @@ export class Mutations { } } + async muteProfileReposts(profile) { + const patchId = this.patchStore.addProfilePatch(profile.did, { + type: "muteProfileReposts", + }); + try { + await this.api.muteActorReposts(profile.did); + this._updateStoredProfile(profile, (stored) => ({ + ...stored, + viewer: { ...stored.viewer, mutedOnlyReposts: true }, + })); + } catch (error) { + console.error(error); + throw error; + } finally { + this.patchStore.removeProfilePatch(profile.did, patchId); + } + } + + async unmuteProfileReposts(profile) { + const patchId = this.patchStore.addProfilePatch(profile.did, { + type: "unmuteProfileReposts", + }); + try { + await this.api.unmuteActor(profile.did); + this._updateStoredProfile(profile, (stored) => ({ + ...stored, + viewer: { ...stored.viewer, mutedOnlyReposts: false }, + })); + } catch (error) { + console.error(error); + throw error; + } finally { + this.patchStore.removeProfilePatch(profile.did, patchId); + } + } + async blockProfile(profile) { const patchId = this.patchStore.addProfilePatch(profile.did, { type: "blockProfile", @@ -852,15 +870,6 @@ export class Mutations { ...stored, viewer: { ...stored.viewer, blocking: block.uri }, })); - this._updatePostsByAuthor(profile.did, (post) => { - return { - ...post, - author: { - ...post.author, - viewer: { ...post.author.viewer, blocking: block.uri }, - }, - }; - }); const blockedProfiles = this.dataStore.$blockedProfiles.get(); if (blockedProfiles) { const alreadyListed = blockedProfiles.blocks.some( @@ -910,15 +919,6 @@ export class Mutations { ...stored, viewer: { ...stored.viewer, blocking: null }, })); - this._updatePostsByAuthor(profile.did, (post) => { - return { - ...post, - author: { - ...post.author, - viewer: { ...post.author.viewer, blocking: null }, - }, - }; - }); const blockedProfiles = this.dataStore.$blockedProfiles.get(); if (blockedProfiles) { this.dataStore.$blockedProfiles.set({ @@ -2358,14 +2358,6 @@ export class Mutations { } } - _updatePostsByAuthor(profileDid, updateFunc) { - for (const post of this.dataStore.$posts.values()) { - if (post?.author?.did === profileDid) { - this.dataStore.$posts.set(post.uri, updateFunc(post)); - } - } - } - _updatePostsByThreadRoot(rootUri, updateFunc) { for (const post of this.dataStore.$posts.values()) { if (post?.viewer && getReplyRootFromPost(post).uri === rootUri) { diff --git a/src/js/dataLayer/patchStore.js b/src/js/dataLayer/patchStore.js index f00824e0..55e531be 100644 --- a/src/js/dataLayer/patchStore.js +++ b/src/js/dataLayer/patchStore.js @@ -216,6 +216,7 @@ export class PatchStore extends ReactiveStore { viewer: { ...profile.viewer, muted: true, + mutedOnlyReposts: false, }, }; case "unmuteProfile": @@ -224,6 +225,23 @@ export class PatchStore extends ReactiveStore { viewer: { ...profile.viewer, muted: false, + mutedOnlyReposts: false, + }, + }; + case "muteProfileReposts": + return { + ...profile, + viewer: { + ...profile.viewer, + mutedOnlyReposts: true, + }, + }; + case "unmuteProfileReposts": + return { + ...profile, + viewer: { + ...profile.viewer, + mutedOnlyReposts: false, }, }; case "blockProfile": diff --git a/src/js/profileInteractionHandler.js b/src/js/profileInteractionHandler.js index 8280d4cb..4daa459d 100644 --- a/src/js/profileInteractionHandler.js +++ b/src/js/profileInteractionHandler.js @@ -60,6 +60,26 @@ export class ProfileInteractionHandler { } } + async handleMuteReposts(profile, doMute) { + if (doMute) { + try { + await this.dataLayer.mutations.muteProfileReposts(profile); + showToast("Reposts will be hidden in feeds"); + } catch (error) { + console.error(error); + showToast("Failed to hide reposts", { style: "error" }); + } + } else { + try { + await this.dataLayer.mutations.unmuteProfileReposts(profile); + showToast("Reposts will be shown in feeds"); + } catch (error) { + console.error(error); + showToast("Failed to show reposts", { style: "error" }); + } + } + } + async handleBlock(profile, doBlock) { if (doBlock) { const confirmed = await confirmModal( diff --git a/src/js/templates/profileCard.template.js b/src/js/templates/profileCard.template.js index 5a0a6e46..28516b3a 100644 --- a/src/js/templates/profileCard.template.js +++ b/src/js/templates/profileCard.template.js @@ -129,6 +129,7 @@ function profileContextMenuTemplate({ pluginItems, onClickFollow, onClickMute, + onClickMuteReposts, onClickBlock, onClickAddToLists, onClickReport, @@ -137,6 +138,11 @@ function profileContextMenuTemplate({ liveStatus, }) { const isFollowing = profile.viewer?.following; + const mutedOnlyReposts = !!profile.viewer?.mutedOnlyReposts; + const canMuteReposts = + !profile.viewer?.blocking && + !profile.viewer?.mutedByList && + !profile.viewer?.muted; const pluginGroups = [...groupBy(pluginItems, "pluginId").values()]; return html` @@ -201,6 +207,20 @@ function profileContextMenuTemplate({ ` : null} + ${canMuteReposts + ? html` { + onClickMuteReposts(profile, !mutedOnlyReposts); + }} + > + ${mutedOnlyReposts + ? "Show reposts in feeds" + : "Hide reposts in feeds"} + ` + : null} ` : ""} + ${profile.viewer?.mutedOnlyReposts + ? html`
+ + Reposts hidden +
` + : ""}
@${profile.handle}
diff --git a/src/js/views/profile.view.js b/src/js/views/profile.view.js index d35eb486..4f693f00 100644 --- a/src/js/views/profile.view.js +++ b/src/js/views/profile.view.js @@ -343,6 +343,8 @@ export default async function profileView({ profileInteractionHandler.handleFollow(profile, doFollow), onClickMute: (profile, doMute) => profileInteractionHandler.handleMute(profile, doMute), + onClickMuteReposts: (profile, doMute) => + profileInteractionHandler.handleMuteReposts(profile, doMute), onClickBlock: async (profile, doBlock) => { await profileInteractionHandler.handleBlock(profile, doBlock); if (!doBlock) { diff --git a/tests/e2e/mockServer.js b/tests/e2e/mockServer.js index 25f499e2..7a1b73d7 100644 --- a/tests/e2e/mockServer.js +++ b/tests/e2e/mockServer.js @@ -1830,7 +1830,11 @@ export class MockServer { const actor = body?.actor; const profile = this.profiles.get(actor); if (profile) { - profile.viewer = { ...profile.viewer, muted: true }; + profile.viewer = { + ...profile.viewer, + muted: !body.onlyReposts, + mutedOnlyReposts: !!body.onlyReposts, + }; } return route.fulfill({ status: 200, @@ -1844,7 +1848,11 @@ export class MockServer { const actor = body?.actor; const profile = this.profiles.get(actor); if (profile) { - profile.viewer = { ...profile.viewer, muted: false }; + profile.viewer = { + ...profile.viewer, + muted: false, + mutedOnlyReposts: false, + }; } return route.fulfill({ status: 200, diff --git a/tests/e2e/specs/views/profile.view.test.js b/tests/e2e/specs/views/profile.view.test.js index daca683c..e44d26f8 100644 --- a/tests/e2e/specs/views/profile.view.test.js +++ b/tests/e2e/specs/views/profile.view.test.js @@ -841,12 +841,15 @@ test.describe("Profile view", () => { await view.locator(".ellipsis-button").click(); const menu = page.locator(".profile-context-menu"); - await expect(menu.locator("context-menu-item")).toHaveCount(7, { + await expect(menu.locator("context-menu-item")).toHaveCount(8, { timeout: 5000, }); await expect( menu.locator('[data-testid="menu-action-profile-open-in-bsky"]'), ).toBeVisible(); + await expect( + menu.locator('[data-testid="menu-action-profile-mute-reposts"]'), + ).toBeVisible(); await expect( menu.locator('[data-testid="menu-action-profile-copy-link"]'), ).toBeVisible(); @@ -905,6 +908,77 @@ test.describe("Profile view", () => { ).not.toBeVisible(); }); + test("should hide and show reposts from the context menu", async ({ + page, + }) => { + const mockServer = new MockServer(); + mockServer.addProfile(otherUser); + await mockServer.setup(page); + await login(page); + await page.goto(`/profile/${otherUser.did}`); + + const view = page.locator("#profile-view"); + await expect(view.locator('[data-testid="profile-name"]')).toContainText( + "Other User", + { timeout: 10000 }, + ); + await expect( + view.locator('[data-testid="reposts-hidden-badge"]'), + ).not.toBeVisible(); + + const menu = page.locator(".profile-context-menu"); + await view.locator(".ellipsis-button").click(); + await menu + .locator( + '[data-testid="menu-action-profile-mute-reposts"][data-teststate="unmuted"]', + ) + .click(); + + await expect( + view.locator('[data-testid="reposts-hidden-badge"]'), + ).toBeVisible({ timeout: 5000 }); + + await view.locator(".ellipsis-button").click(); + await menu + .locator( + '[data-testid="menu-action-profile-mute-reposts"][data-teststate="muted"]', + ) + .click(); + + await expect( + view.locator('[data-testid="reposts-hidden-badge"]'), + ).not.toBeVisible({ timeout: 5000 }); + }); + + test("should not offer hiding reposts for fully muted profiles", async ({ + page, + }) => { + const mutedUser = { + ...otherUser, + viewer: { ...otherUser.viewer, muted: true }, + }; + const mockServer = new MockServer(); + mockServer.addProfile(mutedUser); + await mockServer.setup(page); + await login(page); + await page.goto(`/profile/${mutedUser.did}`); + + const view = page.locator("#profile-view"); + await expect(view.locator('[data-testid="profile-name"]')).toContainText( + "Other User", + { timeout: 10000 }, + ); + await view.locator(".ellipsis-button").click(); + + const menu = page.locator(".profile-context-menu"); + await expect( + menu.locator('[data-testid="menu-action-profile-mute"]'), + ).toBeVisible({ timeout: 5000 }); + await expect( + menu.locator('[data-testid="menu-action-profile-mute-reposts"]'), + ).toHaveCount(0); + }); + test("should show 'Unblock Account' in context menu for blocked profiles", async ({ page, }) => { diff --git a/tests/unit/specs/api.test.js b/tests/unit/specs/api.test.js index 1707f11d..b96b6145 100644 --- a/tests/unit/specs/api.test.js +++ b/tests/unit/specs/api.test.js @@ -1543,6 +1543,23 @@ describe("muteActor", () => { }); }); +describe("muteActorReposts", () => { + it("should mute only the actor's reposts", async () => { + const session = createMockSession({}); + const api = new Api(session); + + await api.muteActorReposts("did:plc:target"); + + const { url, options } = session.getLastFetchOptions(); + assert(url.includes("app.bsky.graph.muteActor")); + assert.deepEqual(options.method, "POST"); + assert.deepEqual(JSON.parse(options.body), { + actor: "did:plc:target", + onlyReposts: true, + }); + }); +}); + describe("unmuteActor", () => { it("should unmute actor", async () => { const session = createMockSession({}); diff --git a/tests/unit/specs/dataLayer/derived.test.js b/tests/unit/specs/dataLayer/derived.test.js index 31890731..54531d32 100644 --- a/tests/unit/specs/dataLayer/derived.test.js +++ b/tests/unit/specs/dataLayer/derived.test.js @@ -2817,6 +2817,18 @@ describe("$isFollowPending / $isBlockPending / $isMutePending", () => { assert.deepEqual(derived.$isMutePending.get(did), true); }); + it("$isMutePending covers reposts-only mute patches", () => { + const dataStore = new DataStore(createSessionState(null)); + const { derived, patchStore } = makeDerived(dataStore); + const muteId = patchStore.addProfilePatch(did, { + type: "muteProfileReposts", + }); + assert.deepEqual(derived.$isMutePending.get(did), true); + patchStore.removeProfilePatch(did, muteId); + patchStore.addProfilePatch(did, { type: "unmuteProfileReposts" }); + assert.deepEqual(derived.$isMutePending.get(did), true); + }); + it("pending signals are keyed per profile", () => { const dataStore = new DataStore(createSessionState(null)); const { derived, patchStore } = makeDerived(dataStore); diff --git a/tests/unit/specs/dataLayer/mutations.test.js b/tests/unit/specs/dataLayer/mutations.test.js index 99827f84..c83381c1 100644 --- a/tests/unit/specs/dataLayer/mutations.test.js +++ b/tests/unit/specs/dataLayer/mutations.test.js @@ -12,6 +12,7 @@ import { HiddenFeedItemsStore } from "/js/dataLayer/hiddenFeedItemsStore.js"; import { CDN_URL } from "/js/config.js"; import { ApiError } from "/js/api.js"; import { trackDisposable } from "../../testHelpers.js"; +import { isBlockedPost } from "/js/dataHelpers.js"; import { createPost } from "../../../shared/factories.js"; function escapeRegExp(value) { @@ -1883,6 +1884,85 @@ describe("unmuteProfile", () => { }); }); +describe("muteProfileReposts / unmuteProfileReposts", () => { + const did = "did:plc:target"; + + function setup(mockApi = {}) { + const dataStore = new DataStore(createSessionState(null)); + const patchStore = new PatchStore(); + const mockPreferencesProvider = { + requirePreferences: () => Preferences.createLoggedOutPreferences(), + }; + const mutations = makeMutations( + { + muteActorReposts: async () => ({}), + unmuteActor: async () => ({}), + ...mockApi, + }, + dataStore, + patchStore, + mockPreferencesProvider, + ); + return { mutations, dataStore, patchStore }; + } + + it("muteProfileReposts calls muteActorReposts and sets viewer.mutedOnlyReposts", async () => { + const muteActorReposts = mock.fn(async () => ({})); + const { mutations, dataStore } = setup({ muteActorReposts }); + + await mutations.muteProfileReposts({ did, viewer: {} }); + + assert.deepEqual(muteActorReposts.mock.calls[0].arguments, [did]); + assert.deepEqual( + dataStore.$profiles.get(did).viewer.mutedOnlyReposts, + true, + ); + }); + + it("unmuteProfileReposts calls unmuteActor and clears viewer.mutedOnlyReposts", async () => { + const unmuteActor = mock.fn(async () => ({})); + const { mutations, dataStore } = setup({ unmuteActor }); + + await mutations.unmuteProfileReposts({ + did, + viewer: { mutedOnlyReposts: true }, + }); + + assert.deepEqual(unmuteActor.mock.calls[0].arguments, [did]); + assert.deepEqual( + dataStore.$profiles.get(did).viewer.mutedOnlyReposts, + false, + ); + }); + + it("muteProfile replaces a reposts-only mute", async () => { + const { mutations, dataStore } = setup({ muteActor: async () => ({}) }); + + await mutations.muteProfile({ did, viewer: { mutedOnlyReposts: true } }); + + const viewer = dataStore.$profiles.get(did).viewer; + assert.deepEqual(viewer.muted, true); + assert.deepEqual(viewer.mutedOnlyReposts, false); + }); + + it("rethrows and clears the pending patch on failure", async (t) => { + t.mock.method(console, "error", () => {}); + const { mutations, patchStore } = setup({ + muteActorReposts: async () => { + throw new Error("boom"); + }, + }); + await assert.rejects( + mutations.muteProfileReposts({ did, viewer: {} }), + /boom/, + ); + assert.deepEqual( + patchStore.hasPendingProfilePatch(did, ["muteProfileReposts"]), + false, + ); + }); +}); + describe("blockProfile", () => { const profile = { did: "did:plc:target", @@ -1897,13 +1977,14 @@ describe("blockProfile", () => { const mockPreferencesProvider = { requirePreferences: () => Preferences.createLoggedOutPreferences(), }; + const derived = makeDerived(dataStore, patchStore, mockPreferencesProvider); const mutations = makeMutations( { blockActor: async () => ({ uri: blockUri }), ...mockApi }, dataStore, patchStore, mockPreferencesProvider, ); - return { mutations, dataStore }; + return { mutations, dataStore, derived }; } it("should set viewer.blocking on the profile", async () => { @@ -1950,29 +2031,22 @@ describe("blockProfile", () => { assert.deepEqual(dataStore.$blockedProfiles.get(), null); }); - it("should update author viewer.blocking on cached posts by that author", async () => { - const { mutations, dataStore } = setup(); - const post = { + it("should render cached posts by that author as blocked", async () => { + const { mutations, dataStore, derived } = setup(); + const post = createPost({ uri: "at://did:plc:target/app.bsky.feed.post/1", - author: { did: profile.did, viewer: {} }, - }; - const otherPost = { + authorHandle: profile.handle, + }); + const otherPost = createPost({ uri: "at://did:plc:someone/app.bsky.feed.post/1", - author: { did: "did:plc:someone", viewer: {} }, - }; - dataStore.$posts.set(post.uri, post); - dataStore.$posts.set(otherPost.uri, otherPost); + authorHandle: "someone.test", + }); + dataStore.setPosts([post, otherPost]); await mutations.blockProfile(profile); - assert.deepEqual( - dataStore.$posts.get(post.uri).author.viewer.blocking, - blockUri, - ); - assert.deepEqual( - dataStore.$posts.get(otherPost.uri).author.viewer.blocking, - undefined, - ); + assert(isBlockedPost(derived.$hydratedPosts.get(post.uri))); + assert(!isBlockedPost(derived.$hydratedPosts.get(otherPost.uri))); }); }); @@ -1989,13 +2063,14 @@ describe("unblockProfile", () => { const mockPreferencesProvider = { requirePreferences: () => Preferences.createLoggedOutPreferences(), }; + const derived = makeDerived(dataStore, patchStore, mockPreferencesProvider); const mutations = makeMutations( { unblockActor: async () => ({}), ...mockApi }, dataStore, patchStore, mockPreferencesProvider, ); - return { mutations, dataStore }; + return { mutations, dataStore, derived }; } it("should clear viewer.blocking on the profile", async () => { @@ -2039,20 +2114,19 @@ describe("unblockProfile", () => { assert.deepEqual(dataStore.$blockedProfiles.get().blocks.length, 1); }); - it("should clear author viewer.blocking on cached posts by that author", async () => { - const { mutations, dataStore } = setup(); - const post = { + it("should stop rendering cached posts by that author as blocked", async () => { + const { mutations, dataStore, derived } = setup(); + const post = createPost({ uri: "at://did:plc:target/app.bsky.feed.post/1", - author: { did: profile.did, viewer: { blocking: "at://old" } }, - }; - dataStore.$posts.set(post.uri, post); + authorHandle: profile.handle, + authorViewer: profile.viewer, + }); + dataStore.setPosts([post]); + assert(isBlockedPost(derived.$hydratedPosts.get(post.uri))); await mutations.unblockProfile(profile); - assert.deepEqual( - dataStore.$posts.get(post.uri).author.viewer.blocking, - null, - ); + assert(!isBlockedPost(derived.$hydratedPosts.get(post.uri))); }); }); diff --git a/tests/unit/specs/dataLayer/patchStore.test.js b/tests/unit/specs/dataLayer/patchStore.test.js index b6f37eb4..6d82fdeb 100644 --- a/tests/unit/specs/dataLayer/patchStore.test.js +++ b/tests/unit/specs/dataLayer/patchStore.test.js @@ -1094,6 +1094,33 @@ describe("Profile Patches - Mute/Block/NotificationSubscription", () => { assert.deepEqual(result.viewer.muted, false); }); + it("should apply muteProfileReposts / unmuteProfileReposts patches", () => { + const patchStore = new PatchStore(); + patchStore.addProfilePatch(did, { type: "muteProfileReposts" }); + let result = applyProfilePatchesForDid(patchStore, baseProfile); + assert.deepEqual(result.viewer.mutedOnlyReposts, true); + assert.deepEqual(result.viewer.muted, false); + + const patchStore2 = new PatchStore(); + patchStore2.addProfilePatch(did, { type: "unmuteProfileReposts" }); + result = applyProfilePatchesForDid(patchStore2, { + ...baseProfile, + viewer: { ...baseProfile.viewer, mutedOnlyReposts: true }, + }); + assert.deepEqual(result.viewer.mutedOnlyReposts, false); + }); + + it("should clear mutedOnlyReposts when applying a full muteProfile patch", () => { + const patchStore = new PatchStore(); + patchStore.addProfilePatch(did, { type: "muteProfile" }); + const result = applyProfilePatchesForDid(patchStore, { + ...baseProfile, + viewer: { ...baseProfile.viewer, mutedOnlyReposts: true }, + }); + assert.deepEqual(result.viewer.muted, true); + assert.deepEqual(result.viewer.mutedOnlyReposts, false); + }); + it("should apply blockProfile / unblockProfile patches", () => { const patchStore = new PatchStore(); patchStore.addProfilePatch(did, { type: "blockProfile" }); diff --git a/tests/unit/specs/templates/profileCard.template.test.js b/tests/unit/specs/templates/profileCard.template.test.js index aeb33a3d..8c6ec1fd 100644 --- a/tests/unit/specs/templates/profileCard.template.test.js +++ b/tests/unit/specs/templates/profileCard.template.test.js @@ -210,6 +210,28 @@ describe("profileCardTemplate", () => { ); }); + it("should render reposts hidden badge only for reposts-only mutes", () => { + const container = document.createElement("div"); + render( + profileCardTemplate({ + profile: { ...mockProfile, viewer: { mutedOnlyReposts: true } }, + }), + container, + ); + assert( + container.querySelector("[data-testid='reposts-hidden-badge']") !== null, + ); + + render( + profileCardTemplate({ profile: { ...mockProfile, viewer: {} } }), + container, + ); + assert.deepEqual( + container.querySelector("[data-testid='reposts-hidden-badge']"), + null, + ); + }); + it("should call onClickFollow when follow button clicked", () => { let followCallArgs = null; const profile = {