diff --git a/frontend/src/lib/constants/sns.constants.ts b/frontend/src/lib/constants/sns.constants.ts index 33f99965bb..c61917dd8f 100644 --- a/frontend/src/lib/constants/sns.constants.ts +++ b/frontend/src/lib/constants/sns.constants.ts @@ -8,3 +8,7 @@ export const WATCH_SALE_STATE_EVERY_MILLISECONDS = 10_000; * ref: https://github.com/dfinity/nns-dapp/blob/38c66bafd1130be921fe9b27296b9d8e5338b6ff/rs/sns_aggregator/src/upstream.rs#L19 */ export const AGGREGATOR_METRICS_TIME_WINDOW_SECONDS = 2 * 30 * 24 * 3600; + +// Should be in sync with the backend: +// https://github.com/dfinity/nns-dapp/blob/645f1bf762fed70230ed8779b5dbd92cc35ac023/rs/backend/src/accounts_store.rs#L35 +export const MAX_SNS_FAV_PROJECTS = 20; diff --git a/frontend/src/lib/i18n/en.json b/frontend/src/lib/i18n/en.json index 2c24c57ce4..8172959a55 100644 --- a/frontend/src/lib/i18n/en.json +++ b/frontend/src/lib/i18n/en.json @@ -912,6 +912,10 @@ "create_sns_proposal_title": "Create Service Nervous System (SNS)", "create_sns_proposal_vote": "Vote" }, + "fav_projects": { + "adding": "Adding project to favorites", + "removing": "Removing project from favorites" + }, "sns_project_detail": { "swap_proposal": "Swap Proposal", "link_to_dashboard": "ICP Dashboard", @@ -1062,7 +1066,9 @@ "invalid_canister_id": "Importing the token was unsuccessful because \"$canisterId\" is not a valid canister ID. Please verify the ID and retry." }, "error__fav_projects": { - "too_many": "You can't add more than $limit favorite projects." + "too_many": "You can't add more than $limit favorite projects.", + "adding_error": "There was an error while adding the project to favorites.", + "removing_error": "There was an error while removing the project from favorites." }, "error__sns": { "undefined_project": "The requested project is invalid or throws an error.", diff --git a/frontend/src/lib/services/app.services.ts b/frontend/src/lib/services/app.services.ts index 5a3e132bca..093b1d4338 100644 --- a/frontend/src/lib/services/app.services.ts +++ b/frontend/src/lib/services/app.services.ts @@ -6,6 +6,9 @@ import { loadImportedTokens } from "$lib/services/imported-tokens.services"; import { loadNetworkEconomicsParameters } from "$lib/services/network-economics.services"; import { loadNnsTotalVotingPower } from "$lib/services/nns-total-voting-power.service"; import { loadSnsProjects } from "$lib/services/public/sns.services"; +import { loadSnsFavProjects } from "$lib/services/sns.fav-projects.services"; +import { ENABLE_LAUNCHPAD_REDESIGN } from "$lib/stores/feature-flags.store"; +import { get } from "svelte/store"; export const initAppPrivateData = async (): Promise => { const initNetworkEconomicsParameters: Promise[] = [ @@ -15,9 +18,9 @@ export const initAppPrivateData = async (): Promise => { // Reload the SNS projects even if they were loaded. // Get latest data and create wrapper caches for the logged in identity. const initSns: Promise[] = [loadSnsProjects()]; - // Load imported tokens - const initImportedTokens: Promise[] = [ + const initNnsDappUserData = (): Promise[] => [ loadImportedTokens({ ignoreAccountNotFoundError: true }), + ...(get(ENABLE_LAUNCHPAD_REDESIGN) ? [loadSnsFavProjects()] : []), ]; const initGovernanceMetrics: Promise[] = [loadGovernanceMetrics()]; /** @@ -27,7 +30,7 @@ export const initAppPrivateData = async (): Promise => { Promise.all(initNetworkEconomicsParameters), Promise.all(initGovernanceMetrics), Promise.all(initNns), - Promise.all(initImportedTokens), + Promise.all(initNnsDappUserData()), Promise.all(initSns), ]); diff --git a/frontend/src/lib/services/sns.fav-projects.services.ts b/frontend/src/lib/services/sns.fav-projects.services.ts new file mode 100644 index 0000000000..0eea5d3b6e --- /dev/null +++ b/frontend/src/lib/services/sns.fav-projects.services.ts @@ -0,0 +1,145 @@ +import { getFavProjects, setFavProjects } from "$lib/api/fav-projects.api"; +import { AccountNotFoundError } from "$lib/canisters/nns-dapp/nns-dapp.errors"; +import type { FavProjects } from "$lib/canisters/nns-dapp/nns-dapp.types"; +import { FORCE_CALL_STRATEGY } from "$lib/constants/mockable.constants"; +import { getAuthenticatedIdentity } from "$lib/services/auth.services"; +import { queryAndUpdate } from "$lib/services/utils.services"; +import { startBusy, stopBusy } from "$lib/stores/busy.store"; +import { snsFavProjectsStore } from "$lib/stores/sns-fav-projects.store"; +import { toastsError } from "$lib/stores/toasts.store"; +import { isLastCall } from "$lib/utils/env.utils"; +import { + fromSnsFavProject, + toSnsFavProject, +} from "$lib/utils/sns-fav-projects.utils"; +import type { Principal } from "@dfinity/principal"; +import { isNullish } from "@dfinity/utils"; +import { get } from "svelte/store"; + +export const loadSnsFavProjects = async () => { + return queryAndUpdate({ + request: (options) => getFavProjects(options), + strategy: FORCE_CALL_STRATEGY, + onLoad: ({ response: { fav_projects: favProjects }, certified }) => { + snsFavProjectsStore.set({ + rootCanisterIds: favProjects.map(fromSnsFavProject), + certified, + }); + }, + onError: ({ error: err, certified, strategy }) => { + console.error(err); + + if (err instanceof AccountNotFoundError) { + // When you log in with a new account for the first time, the account is created in the NNS dapp. + // If you request favorite projects before the account is created, an `AccountNotFound` error will be thrown. + // In this case, we can be sure that the user has no favorite projects. + snsFavProjectsStore.set({ + rootCanisterIds: [], + certified, + }); + return; + } + + if (!isLastCall({ strategy, certified })) { + return; + } + + // Explicitly handle only UPDATE errors + // Reset the store to avoid showing stale data. + snsFavProjectsStore.reset(); + + // Failing to load favorite projects is not a critical error, + // so we just log in console. + }, + logMessage: "Get Favorite Projects", + }); +}; + +// Save favorite projects to the nns-dapp backend. +// Returns an error if the operation fails. +const saveSnsFavProject = async ({ + projects, +}: { + projects: Principal[]; +}): Promise<{ err: Error | undefined }> => { + try { + const identity = await getAuthenticatedIdentity(); + await setFavProjects({ + identity, + favProjects: projects.map(toSnsFavProject), + }); + } catch (err) { + return { err: err as Error }; + } + + return { err: undefined }; +}; + +/** + * Add new favorite project and reload favorite projects from the `nns-dapp` backend to update the `snsFavProjectsStore`. + * No success toast is shown, as the state will be reflected on the UI immediately. + */ +export const addSnsFavProject = async ( + projectToAdd: Principal +): Promise<{ success: boolean }> => { + try { + startBusy({ + initiator: "fav-project-adding", + labelKey: "fav_projects.adding", + }); + + const projects = [ + ...(get(snsFavProjectsStore).rootCanisterIds ?? []), + projectToAdd, + ]; + const { err } = await saveSnsFavProject({ projects }); + + if (isNullish(err)) { + // Update the store. + await loadSnsFavProjects(); + return { success: true }; + } + + toastsError({ + labelKey: "error__fav_projects.adding_error", + }); + + return { success: false }; + } finally { + stopBusy("fav-project-adding"); + } +}; + +/** + * Remove favorite project and reload favorite projects from the `nns-dapp` backend to update the `snsFavProjectsStore`. + * No success toast is shown, as the state will be reflected on the UI immediately. + */ +export const removeSnsFavProject = async ( + rootCanisterId: Principal +): Promise<{ success: boolean }> => { + try { + startBusy({ + initiator: "fav-project-removing", + labelKey: "fav_projects.removing", + }); + + const remainingProjects = ( + get(snsFavProjectsStore).rootCanisterIds ?? [] + ).filter((id) => id.toText() !== rootCanisterId.toText()); + const { err } = await saveSnsFavProject({ projects: remainingProjects }); + + if (isNullish(err)) { + snsFavProjectsStore.remove(rootCanisterId); + + return { success: true }; + } + + toastsError({ + labelKey: "error__fav_projects.removing_error", + }); + + return { success: false }; + } finally { + stopBusy("fav-project-removing"); + } +}; diff --git a/frontend/src/lib/stores/busy.store.ts b/frontend/src/lib/stores/busy.store.ts index f63b40c100..ed74b29f25 100644 --- a/frontend/src/lib/stores/busy.store.ts +++ b/frontend/src/lib/stores/busy.store.ts @@ -58,6 +58,8 @@ export type BusyStateInitiatorType = | "refresh-voting-power" | "change-neuron-visibility" | "reporting-neurons" + | "fav-project-adding" + | "fav-project-removing" | "reporting-transactions"; export interface BusyState { diff --git a/frontend/src/lib/stores/sns-fav-projects.store.ts b/frontend/src/lib/stores/sns-fav-projects.store.ts new file mode 100644 index 0000000000..fa5cc6e731 --- /dev/null +++ b/frontend/src/lib/stores/sns-fav-projects.store.ts @@ -0,0 +1,52 @@ +import { Principal } from "@dfinity/principal"; +import { writable } from "svelte/store"; + +export interface SnsFavProjectsStoreData { + rootCanisterIds: Principal[] | undefined; + certified: boolean | undefined; +} + +/** + * A store that contains user favorite projects + */ +const initSnsFavProjectsStore = () => { + const { update, subscribe, set } = writable({ + rootCanisterIds: undefined, + certified: undefined, + }); + + return { + subscribe, + + set({ + rootCanisterIds, + certified, + }: { + rootCanisterIds: Principal[]; + certified: boolean; + }) { + set({ + rootCanisterIds, + certified, + }); + }, + + remove(rootCanisterId: Principal) { + update(({ rootCanisterIds, certified }) => ({ + rootCanisterIds: rootCanisterIds?.filter( + (id) => id.toText() !== rootCanisterId.toText() + ), + certified, + })); + }, + + reset() { + set({ + rootCanisterIds: undefined, + certified: undefined, + }); + }, + }; +}; + +export const snsFavProjectsStore = initSnsFavProjectsStore(); diff --git a/frontend/src/lib/types/i18n.d.ts b/frontend/src/lib/types/i18n.d.ts index 2d56c6a60c..c0a6417b12 100644 --- a/frontend/src/lib/types/i18n.d.ts +++ b/frontend/src/lib/types/i18n.d.ts @@ -949,6 +949,11 @@ interface I18nLaunchpad_cards { create_sns_proposal_vote: string; } +interface I18nFav_projects { + adding: string; + removing: string; +} + interface I18nSns_project_detail { swap_proposal: string; link_to_dashboard: string; @@ -1111,6 +1116,8 @@ interface I18nError__imported_tokens { interface I18nError__fav_projects { too_many: string; + adding_error: string; + removing_error: string; } interface I18nError__sns { @@ -1684,6 +1691,7 @@ interface I18n { sns_launchpad: I18nSns_launchpad; launchpad: I18nLaunchpad; launchpad_cards: I18nLaunchpad_cards; + fav_projects: I18nFav_projects; sns_project_detail: I18nSns_project_detail; sns_sale: I18nSns_sale; sns_neuron_detail: I18nSns_neuron_detail; diff --git a/frontend/src/lib/utils/sns-fav-projects.utils.ts b/frontend/src/lib/utils/sns-fav-projects.utils.ts new file mode 100644 index 0000000000..6f836b785c --- /dev/null +++ b/frontend/src/lib/utils/sns-fav-projects.utils.ts @@ -0,0 +1,10 @@ +import type { FavProject } from "$lib/canisters/nns-dapp/nns-dapp.types"; +import type { Principal } from "@dfinity/principal"; + +export const toSnsFavProject = (rootCanisterId: Principal): FavProject => ({ + root_canister_id: rootCanisterId, +}); + +export const fromSnsFavProject = ({ + root_canister_id, +}: FavProject): Principal => root_canister_id; diff --git a/frontend/src/tests/lib/services/app.services.spec.ts b/frontend/src/tests/lib/services/app.services.spec.ts index 8f14e61d8b..2814a8b7d9 100644 --- a/frontend/src/tests/lib/services/app.services.spec.ts +++ b/frontend/src/tests/lib/services/app.services.spec.ts @@ -1,5 +1,6 @@ import { clearSnsAggregatorCache } from "$lib/api-services/sns-aggregator.api-service"; import * as agent from "$lib/api/agent.api"; +import * as favProjectsApi from "$lib/api/fav-projects.api"; import * as governanceApi from "$lib/api/governance.api"; import * as aggregatorApi from "$lib/api/sns-aggregator.api"; import { NNSDappCanister } from "$lib/canisters/nns-dapp/nns-dapp.canister"; @@ -19,6 +20,8 @@ import type { HttpAgent } from "@dfinity/agent"; import { toastsStore } from "@dfinity/gix-components"; import { LedgerCanister } from "@dfinity/ledger-icp"; import { get } from "svelte/store"; + +import { overrideFeatureFlagsStore } from "$lib/stores/feature-flags.store"; import { mock } from "vitest-mock-extended"; vi.mock("$lib/api/sns-aggregator.api"); @@ -148,6 +151,46 @@ describe("app-services", () => { }); }); + it("should load fav projects", async () => { + overrideFeatureFlagsStore.setFlag("ENABLE_LAUNCHPAD_REDESIGN", true); + const spyGetFavProjects = vi + .spyOn(favProjectsApi, "getFavProjects") + .mockResolvedValue({ + fav_projects: [], + }); + + expect(spyGetFavProjects).toBeCalledTimes(0); + + initAppPrivateData(); + await runResolvedPromises(); + + expect(spyGetFavProjects).toHaveBeenCalledTimes(2); + expect(spyGetFavProjects).toHaveBeenCalledWith({ + certified: false, + identity: mockIdentity, + }); + expect(spyGetFavProjects).toHaveBeenCalledWith({ + certified: true, + identity: mockIdentity, + }); + }); + + it("should not load fav projects without feature flag", async () => { + overrideFeatureFlagsStore.setFlag("ENABLE_LAUNCHPAD_REDESIGN", false); + const spyGetFavProjects = vi + .spyOn(favProjectsApi, "getFavProjects") + .mockResolvedValue({ + fav_projects: [], + }); + + expect(spyGetFavProjects).toBeCalledTimes(0); + + initAppPrivateData(); + await runResolvedPromises(); + + expect(spyGetFavProjects).toHaveBeenCalledTimes(0); + }); + it("should load network economics", async () => { const spyGetNetworkEconomicsParameters = vi .spyOn(governanceApi, "getNetworkEconomicsParameters") diff --git a/frontend/src/tests/lib/services/sns-fav-projects.services.spec.ts b/frontend/src/tests/lib/services/sns-fav-projects.services.spec.ts new file mode 100644 index 0000000000..e9c4961532 --- /dev/null +++ b/frontend/src/tests/lib/services/sns-fav-projects.services.spec.ts @@ -0,0 +1,306 @@ +import * as favProjectsApi from "$lib/api/fav-projects.api"; +import { AccountNotFoundError } from "$lib/canisters/nns-dapp/nns-dapp.errors"; +import type { FavProject } from "$lib/canisters/nns-dapp/nns-dapp.types"; +import { + addSnsFavProject, + loadSnsFavProjects, + removeSnsFavProject, +} from "$lib/services/sns.fav-projects.services"; +import { snsFavProjectsStore } from "$lib/stores/sns-fav-projects.store"; +import { mockIdentity, resetIdentity } from "$tests/mocks/auth.store.mock"; +import { principal } from "$tests/mocks/sns-projects.mock"; +import { runResolvedPromises } from "$tests/utils/timers.test-utils"; +import { busyStore, toastsStore } from "@dfinity/gix-components"; +import * as dfinityUtils from "@dfinity/utils"; +import { get } from "svelte/store"; + +describe("sns-fav-projects-services", () => { + const rootCanisterIdA = principal(1); + const rootCanisterIdB = principal(2); + const favProjectA: FavProject = { + root_canister_id: rootCanisterIdA, + }; + const favProjectB: FavProject = { + root_canister_id: rootCanisterIdB, + }; + const testError = new Error("test"); + + beforeEach(() => { + resetIdentity(); + vi.spyOn(dfinityUtils, "createAgent").mockReturnValue(undefined); + }); + + describe("loadSnsFavProjects", () => { + it("should call getFavProjects and load favorite projects in store", async () => { + const spyGetFavProjects = vi + .spyOn(favProjectsApi, "getFavProjects") + .mockResolvedValue({ + fav_projects: [favProjectA, favProjectB], + }); + + expect(spyGetFavProjects).toBeCalledTimes(0); + + expect(get(snsFavProjectsStore)).toEqual({ + rootCanisterIds: undefined, + certified: undefined, + }); + + await loadSnsFavProjects(); + + expect(spyGetFavProjects).toBeCalledTimes(2); + expect(spyGetFavProjects).toBeCalledWith({ + certified: false, + identity: mockIdentity, + }); + expect(spyGetFavProjects).toBeCalledWith({ + certified: true, + identity: mockIdentity, + }); + expect(get(snsFavProjectsStore)).toEqual({ + rootCanisterIds: [rootCanisterIdA, rootCanisterIdB], + certified: true, + }); + }); + + it("should not display toast on error", async () => { + vi.spyOn(console, "error").mockReturnValue(); + + const spyGetFavProjects = vi + .spyOn(favProjectsApi, "getFavProjects") + .mockRejectedValue(testError); + + expect(spyGetFavProjects).toBeCalledTimes(0); + expect(get(toastsStore)).toEqual([]); + + await loadSnsFavProjects(); + + expect(get(toastsStore)).toEqual([]); + // Should not display toast - it's logged to console only + expect(spyGetFavProjects).toBeCalledTimes(2); + }); + + it("should reset store on error", async () => { + vi.spyOn(console, "error").mockReturnValue(); + vi.spyOn(favProjectsApi, "getFavProjects").mockRejectedValue(testError); + + snsFavProjectsStore.set({ + rootCanisterIds: [rootCanisterIdA], + certified: true, + }); + + expect(get(snsFavProjectsStore)).toEqual({ + rootCanisterIds: [rootCanisterIdA], + certified: true, + }); + + await loadSnsFavProjects(); + + expect(get(snsFavProjectsStore)).toEqual({ + rootCanisterIds: undefined, + certified: undefined, + }); + }); + + it("should handle AccountNotFoundError (no error, empty favorite projects)", async () => { + const accountNotFoundError = new AccountNotFoundError("test"); + vi.spyOn(favProjectsApi, "getFavProjects").mockRejectedValue( + accountNotFoundError + ); + vi.spyOn(console, "error").mockReturnValue(); + + expect(get(toastsStore)).toEqual([]); + expect(get(snsFavProjectsStore)).toEqual({ + rootCanisterIds: undefined, + certified: undefined, + }); + + await loadSnsFavProjects(); + + expect(get(toastsStore)).toEqual([]); + expect(get(snsFavProjectsStore)).toEqual({ + rootCanisterIds: [], + certified: true, + }); + }); + }); + + describe("addSnsFavProject", () => { + it("should call setFavProjects with updated project list", async () => { + const spySetFavProjects = vi + .spyOn(favProjectsApi, "setFavProjects") + .mockResolvedValue(undefined); + const spyGetFavProjects = vi + .spyOn(favProjectsApi, "getFavProjects") + .mockResolvedValue({ + fav_projects: [favProjectA, favProjectB], + }); + + snsFavProjectsStore.set({ + rootCanisterIds: [rootCanisterIdA], + certified: true, + }); + + expect(spySetFavProjects).toBeCalledTimes(0); + + const { success } = await addSnsFavProject(principal(2)); + + expect(success).toEqual(true); + expect(spySetFavProjects).toBeCalledTimes(1); + expect(spySetFavProjects).toBeCalledWith({ + identity: mockIdentity, + favProjects: [favProjectA, favProjectB], + }); + // Should reload projects + expect(spyGetFavProjects).toBeCalledTimes(2); + }); + + it("should display busy store", async () => { + let resolveSetFavProjects; + const spyOnSetFavProjects = vi + .spyOn(favProjectsApi, "setFavProjects") + .mockImplementation( + () => + new Promise((resolve) => (resolveSetFavProjects = resolve)) + ); + vi.spyOn(favProjectsApi, "getFavProjects").mockResolvedValue({ + fav_projects: [favProjectA, favProjectB], + }); + + snsFavProjectsStore.set({ + rootCanisterIds: [rootCanisterIdA], + certified: true, + }); + + expect(spyOnSetFavProjects).toBeCalledTimes(0); + expect(get(busyStore)).toEqual([]); + + addSnsFavProject(rootCanisterIdB); + await runResolvedPromises(); + + expect(spyOnSetFavProjects).toBeCalledTimes(1); + expect(get(busyStore)).toEqual([ + { + initiator: "fav-project-adding", + text: "Adding project to favorites", + }, + ]); + + resolveSetFavProjects(); + await runResolvedPromises(); + + expect(get(busyStore)).toEqual([]); + }); + + it("should display toast on error", async () => { + vi.spyOn(favProjectsApi, "setFavProjects").mockRejectedValue(testError); + + snsFavProjectsStore.set({ + rootCanisterIds: [rootCanisterIdA], + certified: true, + }); + + expect(get(toastsStore)).toEqual([]); + + const { success } = await addSnsFavProject(rootCanisterIdB); + + expect(success).toEqual(false); + expect(get(toastsStore)).toMatchObject([ + { + level: "error", + text: "There was an error while adding the project to favorites.", + }, + ]); + }); + }); + + describe("removeSnsFavProject", () => { + it("should call setFavProjects with updated project list", async () => { + const spyGetFavProjects = vi + .spyOn(favProjectsApi, "getFavProjects") + .mockResolvedValue({ + fav_projects: [favProjectA, favProjectB], + }); + const spySetFavProjects = vi + .spyOn(favProjectsApi, "setFavProjects") + .mockResolvedValue(undefined); + + snsFavProjectsStore.set({ + rootCanisterIds: [rootCanisterIdA, rootCanisterIdB], + certified: true, + }); + + expect(spySetFavProjects).toBeCalledTimes(0); + + const { success } = await removeSnsFavProject(rootCanisterIdA); + + expect(success).toEqual(true); + expect(spySetFavProjects).toBeCalledTimes(1); + expect(spySetFavProjects).toBeCalledWith({ + identity: mockIdentity, + favProjects: [favProjectB], + }); + + expect(get(snsFavProjectsStore)).toEqual({ + rootCanisterIds: [rootCanisterIdB], + certified: true, + }); + // Should not reload projects + expect(spyGetFavProjects).toBeCalledTimes(0); + }); + + it("should display busy store", async () => { + let resolveSetFavProjects; + const spyOnSetFavProjects = vi + .spyOn(favProjectsApi, "setFavProjects") + .mockImplementation( + () => + new Promise((resolve) => (resolveSetFavProjects = resolve)) + ); + + snsFavProjectsStore.set({ + rootCanisterIds: [rootCanisterIdA, rootCanisterIdB], + certified: true, + }); + + expect(spyOnSetFavProjects).toBeCalledTimes(0); + expect(get(busyStore)).toEqual([]); + + removeSnsFavProject(rootCanisterIdA); + await runResolvedPromises(); + + expect(spyOnSetFavProjects).toBeCalledTimes(1); + expect(get(busyStore)).toEqual([ + { + initiator: "fav-project-removing", + text: "Removing project from favorites", + }, + ]); + + resolveSetFavProjects(); + await runResolvedPromises(); + + expect(get(busyStore)).toEqual([]); + }); + + it("should display toast on error", async () => { + vi.spyOn(favProjectsApi, "setFavProjects").mockRejectedValue(testError); + + snsFavProjectsStore.set({ + rootCanisterIds: [rootCanisterIdA, rootCanisterIdB], + certified: true, + }); + + expect(get(toastsStore)).toEqual([]); + + const { success } = await removeSnsFavProject(rootCanisterIdA); + + expect(success).toEqual(false); + expect(get(toastsStore)).toMatchObject([ + { + level: "error", + text: "There was an error while removing the project from favorites.", + }, + ]); + }); + }); +}); diff --git a/frontend/src/tests/lib/stores/sns-fav-projects.store.spec.ts b/frontend/src/tests/lib/stores/sns-fav-projects.store.spec.ts new file mode 100644 index 0000000000..4306f8f761 --- /dev/null +++ b/frontend/src/tests/lib/stores/sns-fav-projects.store.spec.ts @@ -0,0 +1,73 @@ +import { snsFavProjectsStore } from "$lib/stores/sns-fav-projects.store"; +import { principal } from "$tests/mocks/sns-projects.mock"; +import type { Principal } from "@dfinity/principal"; +import { get } from "svelte/store"; + +describe("sns-fav-projects-store", () => { + describe("snsFavProjectsStore", () => { + const projectA: Principal = principal(0); + const projectB: Principal = principal(2); + + it("should set fav projects", () => { + const rootCanisterIds = [projectA, projectB]; + expect(get(snsFavProjectsStore)).toEqual({ + rootCanisterIds: undefined, + certified: undefined, + }); + + snsFavProjectsStore.set({ rootCanisterIds, certified: true }); + + expect(get(snsFavProjectsStore)).toEqual({ + rootCanisterIds, + certified: true, + }); + }); + + it("should remove from fav projects", () => { + snsFavProjectsStore.set({ + rootCanisterIds: [projectA, projectB], + certified: true, + }); + expect(get(snsFavProjectsStore)).toEqual({ + rootCanisterIds: [projectA, projectB], + certified: true, + }); + + snsFavProjectsStore.remove(projectA); + expect(get(snsFavProjectsStore)).toEqual({ + rootCanisterIds: [projectB], + certified: true, + }); + + snsFavProjectsStore.remove(projectB); + expect(get(snsFavProjectsStore)).toEqual({ + rootCanisterIds: [], + certified: true, + }); + }); + + it("should reset fav projects", () => { + const favProjects = [projectA, projectB]; + expect(get(snsFavProjectsStore)).toEqual({ + rootCanisterIds: undefined, + certified: undefined, + }); + + snsFavProjectsStore.set({ + rootCanisterIds: favProjects, + certified: true, + }); + + expect(get(snsFavProjectsStore)).toEqual({ + rootCanisterIds: favProjects, + certified: true, + }); + snsFavProjectsStore.reset(); + + expect(get(snsFavProjectsStore)).toEqual({ + rootCanisterIds: undefined, + certified: undefined, + }); + }); + }); +});