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)
+ );
+ }
+}