From ac1e65b6f9ae99655cfbb347e931bc6df9cd5d1e Mon Sep 17 00:00:00 2001 From: Max Strasinsky <98811342+mstrasinskis@users.noreply.github.com> Date: Wed, 11 Dec 2024 22:41:12 +0100 Subject: [PATCH] Refresh voting power button (#5990) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Motivation If there are inactive neurons, the user should be able to reset their voting-power-refreshed timestamp. In this PR, we add a button that makes the necessary service calls to refresh the user’s neurons. # Changes - Add `ConfirmFollowingButton` component. # Tests - Added. - Tested locally: https://github.com/user-attachments/assets/6e71aace-4f5f-4426-9896-2de33bec290e # Todos - [ ] Add entry to changelog (if necessary). Not necessary. --- .../actions/ConfirmFollowingButton.svelte | 32 +++++ frontend/src/lib/i18n/en.json | 1 + frontend/src/lib/stores/busy.store.ts | 1 + frontend/src/lib/types/i18n.d.ts | 1 + .../actions/ConfirmFollowingButton.spec.ts | 109 ++++++++++++++++++ .../ConfirmFollowingButton.page-object.ts | 12 ++ 6 files changed, 156 insertions(+) create mode 100644 frontend/src/lib/components/neuron-detail/actions/ConfirmFollowingButton.svelte create mode 100644 frontend/src/tests/lib/components/neuron-detail/actions/ConfirmFollowingButton.spec.ts create mode 100644 frontend/src/tests/page-objects/ConfirmFollowingButton.page-object.ts diff --git a/frontend/src/lib/components/neuron-detail/actions/ConfirmFollowingButton.svelte b/frontend/src/lib/components/neuron-detail/actions/ConfirmFollowingButton.svelte new file mode 100644 index 00000000000..e91a035eb57 --- /dev/null +++ b/frontend/src/lib/components/neuron-detail/actions/ConfirmFollowingButton.svelte @@ -0,0 +1,32 @@ + + + diff --git a/frontend/src/lib/i18n/en.json b/frontend/src/lib/i18n/en.json index 6a2d4528a1d..22235528777 100644 --- a/frontend/src/lib/i18n/en.json +++ b/frontend/src/lib/i18n/en.json @@ -389,6 +389,7 @@ }, "losing_rewards": { "description": "ICP neurons that are inactive for $period start losing voting rewards. In order to avoid losing rewards, vote manually, edit or confirm your following.", + "confirming": "Confirming following. This may take a moment.", "confirm": "Confirm Following" }, "losing_rewards_banner": { diff --git a/frontend/src/lib/stores/busy.store.ts b/frontend/src/lib/stores/busy.store.ts index df40d31fb26..d480afba808 100644 --- a/frontend/src/lib/stores/busy.store.ts +++ b/frontend/src/lib/stores/busy.store.ts @@ -51,6 +51,7 @@ export type BusyStateInitiatorType = | "import-token-importing" | "import-token-removing" | "import-token-updating" + | "refresh-voting-power" | "change-neuron-visibility" | "reporting-neurons" | "reporting-transactions"; diff --git a/frontend/src/lib/types/i18n.d.ts b/frontend/src/lib/types/i18n.d.ts index b5c50f9dc37..5e896a7728f 100644 --- a/frontend/src/lib/types/i18n.d.ts +++ b/frontend/src/lib/types/i18n.d.ts @@ -405,6 +405,7 @@ interface I18nNeurons { interface I18nLosing_rewards { description: string; + confirming: string; confirm: string; } diff --git a/frontend/src/tests/lib/components/neuron-detail/actions/ConfirmFollowingButton.spec.ts b/frontend/src/tests/lib/components/neuron-detail/actions/ConfirmFollowingButton.spec.ts new file mode 100644 index 00000000000..30823adec8b --- /dev/null +++ b/frontend/src/tests/lib/components/neuron-detail/actions/ConfirmFollowingButton.spec.ts @@ -0,0 +1,109 @@ +import * as api from "$lib/api/governance.api"; +import ConfirmFollowingButton from "$lib/components/neuron-detail/actions/ConfirmFollowingButton.svelte"; +import { neuronsStore } from "$lib/stores/neurons.store"; +import { mockIdentity, resetIdentity } from "$tests/mocks/auth.store.mock"; +import { mockFullNeuron, mockNeuron } from "$tests/mocks/neurons.mock"; +import { ConfirmFollowingButtonPo } from "$tests/page-objects/ConfirmFollowingButton.page-object"; +import { JestPageObjectElement } from "$tests/page-objects/jest.page-object"; +import { allowLoggingInOneTestForDebugging } from "$tests/utils/console.test-utils"; +import { runResolvedPromises } from "$tests/utils/timers.test-utils"; +import { busyStore } from "@dfinity/gix-components"; +import { nonNullish } from "@dfinity/utils"; +import { render } from "@testing-library/svelte"; +import { get } from "svelte/store"; + +describe("ConfirmFollowingButton", () => { + const neuronId1 = 1n; + const neuronId2 = 22n; + const neuronIds = [neuronId1, neuronId2]; + const neurons = neuronIds.map((neuronId) => ({ + ...mockNeuron, + neuronId, + fullNeuron: { + ...mockFullNeuron, + neuronId, + controller: mockIdentity.getPrincipal().toText(), + }, + })); + + let spyQueryNeurons; + + beforeEach(() => { + resetIdentity(); + allowLoggingInOneTestForDebugging(); + + neuronsStore.pushNeurons({ neurons, certified: true }); + spyQueryNeurons = vi.spyOn(api, "queryNeurons").mockResolvedValue(neurons); + vi.spyOn(api, "refreshVotingPower").mockResolvedValue(); + }); + + const renderComponent = async ({ nnsComplete = undefined, ...props }) => { + const { container, component } = render(ConfirmFollowingButton, props); + + if (nonNullish(nnsComplete)) component.$on("nnsComplete", nnsComplete); + + return ConfirmFollowingButtonPo.under(new JestPageObjectElement(container)); + }; + + it("displays the button", async () => { + const po = await renderComponent({ + neuronIds: [neuronId1, neuronId2], + }); + + expect(await po.getText()).toBe("Confirm Following"); + }); + + it("returns the refreshed neuron count", async () => { + vi.spyOn(console, "error").mockReturnValue(); + + neuronsStore.pushNeurons({ neurons, certified: true }); + let resolveRefreshVotingPower; + const spyRefreshVotingPower = vi + .spyOn(api, "refreshVotingPower") + .mockRejectedValueOnce(new Error()) + .mockImplementationOnce( + () => new Promise((resolve) => (resolveRefreshVotingPower = resolve)) + ); + + expect(spyQueryNeurons).toBeCalledTimes(0); + expect(spyRefreshVotingPower).toBeCalledTimes(0); + + const nnsComplete = vi.fn(); + const po = await renderComponent({ + neuronIds: [neuronId1, neuronId2], + nnsComplete, + }); + + expect(get(busyStore)).toEqual([]); + + await po.click(); + + // Wait for busy screen to show + await runResolvedPromises(); + expect(get(busyStore)).toEqual([ + { + initiator: "refresh-voting-power", + text: "Confirming following. This may take a moment.", + }, + ]); + + resolveRefreshVotingPower(); + + // Wait for busy screen to hide + await runResolvedPromises(); + expect(get(busyStore)).toEqual([]); + + expect(spyQueryNeurons).toBeCalledTimes(2); + expect(spyRefreshVotingPower).toBeCalledTimes(2); + + expect(nnsComplete).toBeCalledTimes(1); + expect(nnsComplete).toBeCalledWith( + expect.objectContaining({ + detail: { + successCount: 1, + totalCount: 2, + }, + }) + ); + }); +}); diff --git a/frontend/src/tests/page-objects/ConfirmFollowingButton.page-object.ts b/frontend/src/tests/page-objects/ConfirmFollowingButton.page-object.ts new file mode 100644 index 00000000000..ebfd986b228 --- /dev/null +++ b/frontend/src/tests/page-objects/ConfirmFollowingButton.page-object.ts @@ -0,0 +1,12 @@ +import { BasePageObject } from "$tests/page-objects/base.page-object"; +import type { PageObjectElement } from "$tests/types/page-object.types"; + +export class ConfirmFollowingButtonPo extends BasePageObject { + private static readonly TID = "confirm-following-button-component"; + + static under(element: PageObjectElement): ConfirmFollowingButtonPo { + return new ConfirmFollowingButtonPo( + element.byTestId(ConfirmFollowingButtonPo.TID) + ); + } +}