Skip to content

Commit

Permalink
Confirm following from banner (#6004)
Browse files Browse the repository at this point in the history
# Motivation

If there are inactive neurons, the user should be able to reset their
voting-power-refreshed timestamp. In this PR, we add a ["Confirm
Following" button](#5990) to the
modal that displays the user’s inactive neurons.

# Changes

- Add the `ConfirmFollowingButton` to the modal.
- Close the modal when all inactive neurons are refreshed.

# Tests

- Added.

# Todos

- [ ] Add entry to changelog (if necessary).
Not necessary.
  • Loading branch information
mstrasinskis authored Dec 15, 2024
1 parent 6c53255 commit 57c5974
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 12 deletions.
20 changes: 14 additions & 6 deletions frontend/src/lib/modals/neurons/LosingRewardNeuronsModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,23 @@
import { buildNeuronUrl } from "$lib/utils/navigation.utils";
import { OWN_CANISTER_ID_TEXT } from "$lib/constants/canister-ids.constants";
import type { NeuronInfo } from "@dfinity/nns";
import ConfirmFollowingButton from "$lib/components/neuron-detail/actions/ConfirmFollowingButton.svelte";
const dispatcher = createEventDispatcher<{ nnsClose: void }>();
// Load KnownNeurons which are used in the FollowNnsTopicSections
onMount(() => listKnownNeurons());
const confirm = () => {
// TBD
};
const close = () => dispatcher("nnsClose");
const onConfirmed = ({
detail: { successCount, totalCount },
}: {
detail: { successCount: number; totalCount: number };
}) => {
if (successCount === totalCount) {
close();
}
};
const navigateToNeuronDetail = async (neuron: NeuronInfo) => {
close();
Expand All @@ -32,6 +39,9 @@
})
);
};
let neuronIds: bigint[];
$: neuronIds = $soonLosingRewardNeuronsStore.map((neuron) => neuron.neuronId);
</script>

<Modal on:nnsClose testId="losing-reward-neurons-modal-component">
Expand Down Expand Up @@ -63,9 +73,7 @@
<button on:click={close} class="secondary" data-tid="cancel-button"
>{$i18n.core.cancel}</button
>
<button on:click={confirm} class="primary" data-tid="confirm-button"
>{$i18n.losing_rewards.confirm}</button
>
<ConfirmFollowingButton {neuronIds} on:nnsComplete={onConfirmed} />
</div>
</div>
</Modal>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ 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";
Expand All @@ -30,7 +29,6 @@ describe("ConfirmFollowingButton", () => {

beforeEach(() => {
resetIdentity();
allowLoggingInOneTestForDebugging();

neuronsStore.pushNeurons({ neurons, certified: true });
spyQueryNeurons = vi.spyOn(api, "queryNeurons").mockResolvedValue(neurons);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe("LosingRewardNeuronsModal", () => {
fullNeuron: {
...mockFullNeuron,
votingPowerRefreshedTimestampSeconds: BigInt(nowSeconds),
controller: mockIdentity.getPrincipal().toText(),
},
};
const in10DaysLosingRewardsNeuron = {
Expand All @@ -35,6 +36,7 @@ describe("LosingRewardNeuronsModal", () => {
votingPowerRefreshedTimestampSeconds: BigInt(
nowSeconds - SECONDS_IN_HALF_YEAR + 10 * SECONDS_IN_DAY
),
controller: mockIdentity.getPrincipal().toText(),
},
};
const losingRewardsNeuron = {
Expand All @@ -45,8 +47,22 @@ describe("LosingRewardNeuronsModal", () => {
votingPowerRefreshedTimestampSeconds: BigInt(
nowSeconds - SECONDS_IN_HALF_YEAR
),
controller: mockIdentity.getPrincipal().toText(),
},
};
const neurons = [
activeNeuron,
in10DaysLosingRewardsNeuron,
losingRewardsNeuron,
];
const refreshedNeurons = neurons.map((neuron) => ({
...neuron,
fullNeuron: {
...neuron.fullNeuron,
votingPowerRefreshedTimestampSeconds: BigInt(nowSeconds),
},
}));
let spyRefreshVotingPower;

const renderComponent = ({ onClose }: { onClose?: () => void } = {}) => {
const { container, component } = render(LosingRewardNeuronsModal);
Expand Down Expand Up @@ -75,11 +91,15 @@ describe("LosingRewardNeuronsModal", () => {
});

vi.spyOn(governanceApi, "queryKnownNeurons").mockResolvedValue([]);
vi.spyOn(governanceApi, "queryNeurons").mockResolvedValue(refreshedNeurons);
spyRefreshVotingPower = vi
.spyOn(governanceApi, "refreshVotingPower")
.mockResolvedValue();
});

it("should not display active neurons", async () => {
neuronsStore.setNeurons({
neurons: [activeNeuron, in10DaysLosingRewardsNeuron, losingRewardsNeuron],
neurons,
certified: true,
});
const po = await renderComponent();
Expand All @@ -96,7 +116,7 @@ describe("LosingRewardNeuronsModal", () => {

it("should dispatch on close", async () => {
neuronsStore.setNeurons({
neurons: [activeNeuron, in10DaysLosingRewardsNeuron, losingRewardsNeuron],
neurons,
certified: true,
});
const onClose = vi.fn();
Expand All @@ -109,6 +129,30 @@ describe("LosingRewardNeuronsModal", () => {
expect(onClose).toHaveBeenCalledTimes(1);
});

it("should confirm following", async () => {
neuronsStore.setNeurons({
neurons: [in10DaysLosingRewardsNeuron, losingRewardsNeuron],
certified: true,
});
const po = await renderComponent({});

expect((await po.getNnsLosingRewardsNeuronCardPos()).length).toEqual(2);

await po.clickConfirmFollowing();
await runResolvedPromises();
expect((await po.getNnsLosingRewardsNeuronCardPos()).length).toEqual(0);

expect(spyRefreshVotingPower).toHaveBeenCalledTimes(2);
expect(spyRefreshVotingPower).toHaveBeenCalledWith({
identity: mockIdentity,
neuronId: in10DaysLosingRewardsNeuron.neuronId,
});
expect(spyRefreshVotingPower).toHaveBeenCalledWith({
identity: mockIdentity,
neuronId: losingRewardsNeuron.neuronId,
});
});

it("should navigate to the neuron details", async () => {
neuronsStore.setNeurons({
neurons: [losingRewardsNeuron],
Expand Down Expand Up @@ -143,7 +187,7 @@ describe("LosingRewardNeuronsModal", () => {
.spyOn(governanceApi, "queryKnownNeurons")
.mockResolvedValue([]);
neuronsStore.setNeurons({
neurons: [activeNeuron, in10DaysLosingRewardsNeuron, losingRewardsNeuron],
neurons,
certified: true,
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,49 +1,75 @@
import * as governanceApi from "$lib/api/governance.api";
import LosingRewardsBanner from "$lib/components/neurons/LosingRewardsBanner.svelte";
import { SECONDS_IN_DAY, SECONDS_IN_HALF_YEAR } from "$lib/constants/constants";
import { neuronsStore } from "$lib/stores/neurons.store";
import { nowInSeconds } from "$lib/utils/date.utils";
import { mockIdentity, resetIdentity } from "$tests/mocks/auth.store.mock";
import { mockFullNeuron, mockNeuron } from "$tests/mocks/neurons.mock";
import { LosingRewardsBannerPo } from "$tests/page-objects/LosingRewardsBanner.page-object";
import { JestPageObjectElement } from "$tests/page-objects/jest.page-object";
import { render } from "$tests/utils/svelte.test-utils";
import { runResolvedPromises } from "$tests/utils/timers.test-utils";

describe("LosingRewardsBanner", () => {
const nowSeconds = nowInSeconds();
const activeNeuron = {
...mockNeuron,
neuronId: 0n,
fullNeuron: {
...mockFullNeuron,
votingPowerRefreshedTimestampSeconds: BigInt(nowSeconds),
controller: mockIdentity.getPrincipal().toText(),
},
};
const in10DaysLosingRewardsNeuron = {
...mockNeuron,
neuronId: 1n,
fullNeuron: {
...mockFullNeuron,
votingPowerRefreshedTimestampSeconds: BigInt(
nowSeconds - SECONDS_IN_HALF_YEAR + 10 * SECONDS_IN_DAY
),
controller: mockIdentity.getPrincipal().toText(),
},
};
const losingRewardsNeuron = {
...mockNeuron,
neuronId: 2n,
fullNeuron: {
...mockFullNeuron,
votingPowerRefreshedTimestampSeconds: BigInt(
nowSeconds - SECONDS_IN_HALF_YEAR
),
controller: mockIdentity.getPrincipal().toText(),
},
};
const neurons = [
activeNeuron,
in10DaysLosingRewardsNeuron,
losingRewardsNeuron,
];
const refreshedNeurons = neurons.map((neuron) => ({
...neuron,
fullNeuron: {
...neuron.fullNeuron,
votingPowerRefreshedTimestampSeconds: BigInt(nowSeconds),
},
}));

const renderComponent = () => {
const { container } = render(LosingRewardsBanner);
return LosingRewardsBannerPo.under(new JestPageObjectElement(container));
};

beforeEach(() => {
resetIdentity();
vi.useFakeTimers({
now: nowSeconds * 1000,
});

vi.spyOn(governanceApi, "queryKnownNeurons").mockResolvedValue([]);
vi.spyOn(governanceApi, "queryNeurons").mockResolvedValue(refreshedNeurons);
vi.spyOn(governanceApi, "refreshVotingPower").mockResolvedValue();
});

it("should not display banner when all neurons are active", async () => {
Expand Down Expand Up @@ -77,7 +103,7 @@ describe("LosingRewardsBanner", () => {

it("displays losing rewards title ", async () => {
neuronsStore.setNeurons({
neurons: [activeNeuron, in10DaysLosingRewardsNeuron, losingRewardsNeuron],
neurons,
certified: true,
});
const po = await renderComponent();
Expand Down Expand Up @@ -109,4 +135,49 @@ describe("LosingRewardsBanner", () => {
await po.clickConfirm();
expect(await po.getLosingRewardNeuronsModalPo().isPresent()).toEqual(true);
});

it("should close modal when all refreshed", async () => {
const spyRefreshVotingPower = vi
.spyOn(governanceApi, "refreshVotingPower")
.mockResolvedValue();
neuronsStore.setNeurons({
neurons: [losingRewardsNeuron],
certified: true,
});
const po = await renderComponent();

expect(await po.getLosingRewardNeuronsModalPo().isPresent()).toEqual(false);
await po.clickConfirm();
expect(await po.getLosingRewardNeuronsModalPo().isPresent()).toEqual(true);

await po.getLosingRewardNeuronsModalPo().clickConfirmFollowing();
await runResolvedPromises();

vi.advanceTimersToNextFrame();
expect(spyRefreshVotingPower).toBeCalledTimes(1);
expect(await po.getLosingRewardNeuronsModalPo().isPresent()).toEqual(false);
});

it("should not close the modal on error", async () => {
const spyConsoleError = vi.spyOn(console, "error").mockReturnValue();
const spyRefreshVotingPower = vi
.spyOn(governanceApi, "refreshVotingPower")
.mockRejectedValueOnce(new Error());

neuronsStore.setNeurons({
neurons: [losingRewardsNeuron],
certified: true,
});
const po = await renderComponent();

await po.clickConfirm();
await runResolvedPromises();
await po.getLosingRewardNeuronsModalPo().clickConfirmFollowing();
await runResolvedPromises();
vi.advanceTimersToNextFrame();

expect(spyRefreshVotingPower).toBeCalledTimes(1);
expect(spyConsoleError).toBeCalledTimes(1);
expect(await po.getLosingRewardNeuronsModalPo().isPresent()).toEqual(true);
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ModalPo } from "$tests/page-objects/Modal.page-object";
import type { PageObjectElement } from "$tests/types/page-object.types";
import { ConfirmFollowingButtonPo } from "./ConfirmFollowingButton.page-object";
import { NnsLosingRewardsNeuronCardPo } from "./NnsLosingRewardsNeuronCard.page-object";

export class LosingRewardNeuronsModalPo extends ModalPo {
Expand All @@ -15,6 +16,14 @@ export class LosingRewardNeuronsModalPo extends ModalPo {
return NnsLosingRewardsNeuronCardPo.allUnder(this.root);
}

getConfirmFollowingButtonPo(): ConfirmFollowingButtonPo {
return ConfirmFollowingButtonPo.under(this.root);
}

async clickConfirmFollowing(): Promise<void> {
return this.getConfirmFollowingButtonPo().click();
}

async clickCancel(): Promise<void> {
return this.getButton("cancel-button").click();
}
Expand Down

0 comments on commit 57c5974

Please sign in to comment.