Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Neuron confirm following confirmation #6115

Merged
merged 15 commits into from
Jan 8, 2025
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
import { secondsToDuration } from "@dfinity/utils";
import { replacePlaceholders } from "$lib/utils/i18n.utils";
import FollowNeuronsButton from "./actions/FollowNeuronsButton.svelte";
import ConfirmFollowingButton from "./actions/ConfirmFollowingButton.svelte";
import { secondsToDissolveDelayDuration } from "$lib/utils/date.utils";
import { START_REDUCING_VOTING_POWER_AFTER_SECONDS } from "$lib/constants/neurons.constants";
import ConfirmFollowingActionButton from "$lib/components/neuron-detail/actions/ConfirmFollowingActionButton.svelte";

export let neuron: NeuronInfo;

Expand Down Expand Up @@ -103,7 +103,7 @@
{#if isFollowingReset}
<FollowNeuronsButton variant="secondary" />
{:else}
<ConfirmFollowingButton neuronIds={[neuron.neuronId]} />
<ConfirmFollowingActionButton {neuron} />
{/if}
</CommonItemAction>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script lang="ts">
import { i18n } from "$lib/stores/i18n";
import { openNnsNeuronModal } from "$lib/utils/modals.utils";
import type { NeuronInfo } from "@dfinity/nns";

export let neuron: NeuronInfo;
</script>

<button
data-tid="confirm-following-action-button-component"
class="secondary"
on:click={() =>
openNnsNeuronModal({
type: "confirm-following",
data: { neuron },
})}>{$i18n.losing_rewards.confirm}</button
>
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
import { icpAccountsStore } from "$lib/derived/icp-accounts.derived";
import { i18n } from "$lib/stores/i18n";
import NeuronTag from "$lib/components/ui/NeuronTag.svelte";
import { createEventDispatcher } from "svelte";

const dispatch = createEventDispatcher<{ nnsClick: void }>();
import { nonNullish } from "@dfinity/utils";

export let neuron: NeuronInfo;
export let onClick: (() => void) | undefined;

let isClickable: boolean;
$: isClickable = nonNullish(onClick);
mstrasinskis marked this conversation as resolved.
Show resolved Hide resolved

let neuronTags: NeuronTagData[];
$: neuronTags = getNeuronTags({
Expand All @@ -32,10 +34,10 @@

<Card
testId="nns-loosing-rewards-neuron-card-component"
role="button"
role={isClickable ? "button" : undefined}
noMargin
ariaLabel={$i18n.losing_rewards_modal.goto_neuron}
on:click={() => dispatch("nnsClick")}
on:click={onClick}
>
<div class="wrapper">
<div class="header">
Expand All @@ -47,9 +49,11 @@
<NeuronTag {tag} />
{/each}
</div>
<div class="icon-right">
<IconRight />
</div>
{#if isClickable}
<div class="icon-right">
<IconRight />
</div>
{/if}
</div>

{#if followees.length > 0}
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/lib/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -405,8 +405,8 @@
},
"losing_rewards_modal": {
"goto_neuron": "Go to neuron details",
"title": "Review your following for neurons",
"label": "Neurons",
"title": "Review following",
"label": "Neuron(s)",
"no_following": "This neuron has no following configured. You need to vote manually to earn voting rewards."
},
"new_followee": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import ConfirmFollowingButton from "$lib/components/neuron-detail/actions/ConfirmFollowingButton.svelte";

export let neurons: NeuronInfo[];
export let withNeuronNavigation = true;

const dispatcher = createEventDispatcher<{ nnsClose: void }>();

Expand Down Expand Up @@ -65,7 +66,9 @@
<li>
<NnsLosingRewardsNeuronCard
{neuron}
on:nnsClick={() => navigateToNeuronDetail(neuron)}
onClick={withNeuronNavigation
? () => navigateToNeuronDetail(neuron)
: undefined}
/>
</li>
{/each}
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/lib/modals/neurons/NnsNeuronModals.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import ChangeNeuronVisibilityModal from "./ChangeNeuronVisibilityModal.svelte";
import type { NeuronInfo } from "@dfinity/nns";
import { nonNullish } from "@dfinity/utils";
import LosingRewardNeuronsModal from "./LosingRewardNeuronsModal.svelte";

let modal: NnsNeuronModal<NnsNeuronModalData> | undefined;
const close = () => (modal = undefined);
Expand Down Expand Up @@ -110,6 +111,14 @@
on:nnsClose={close}
/>
{/if}

{#if type === "confirm-following"}
<LosingRewardNeuronsModal
withNeuronNavigation={false}
neurons={[neuron]}
on:nnsClose={close}
/>
{/if}
{/if}

{#if type === "voting-history" && nonNullish(followee)}
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/lib/types/nns-neuron-detail.modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export type NnsNeuronModalType =
| "dev-add-maturity"
| "voting-history"
| "change-neuron-visibility"
| "dev-update-voting-power-refreshed";
| "dev-update-voting-power-refreshed"
| "confirm-following";
export interface NnsNeuronModalData {
neuron: NeuronInfo | undefined | null;
}
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/tests/fakes/governance-api.fake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ export const addNeuronWith = ({
}: {
identity?: Identity;
votingPowerRefreshedTimestampSeconds?: bigint | number;
} & Partial<FakeNeuronParams>) => {
} & Partial<FakeNeuronParams>): NeuronInfo => {
const neuron = { ...mockNeuron, fullNeuron: { ...mockNeuron.fullNeuron } };
if (neuronId) {
neuron.neuronId = neuronId;
Expand All @@ -230,6 +230,8 @@ export const addNeuronWith = ({
throw new Error(`A neuron with id ${neuron.neuronId} already exists`);
}
getNeurons(identity).push(neuron);

return { ...neuron };
};

export const addNeurons = ({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import * as governanceApi from "$lib/api/governance.api";
import {
SECONDS_IN_DAY,
SECONDS_IN_HALF_YEAR,
SECONDS_IN_MONTH,
} 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 { mockIdentity } from "$tests/mocks/auth.store.mock";
import { mockFullNeuron, mockNeuron } from "$tests/mocks/neurons.mock";
import { NnsNeuronRewardStatusActionPo } from "$tests/page-objects/NnsNeuronRewardStatusAction.page-object";
import { JestPageObjectElement } from "$tests/page-objects/jest.page-object";
import { runResolvedPromises } from "$tests/utils/timers.test-utils";
import { type NeuronInfo } from "@dfinity/nns";
import { render } from "@testing-library/svelte";
import NnsNeuronRewardStatusActionTest from "./NnsNeuronRewardStatusActionTest.svelte";
Expand Down Expand Up @@ -114,41 +111,4 @@ describe("NnsNeuronRewardStatusAction", () => {
true
);
});

it("should refresh voting power", async () => {
const testNeuron = {
...mockNeuron,
fullNeuron: {
...mockFullNeuron,
votingPowerRefreshedTimestampSeconds: BigInt(
nowInSeconds() - SECONDS_IN_HALF_YEAR + 10 * SECONDS_IN_DAY
),
controller: mockIdentity.getPrincipal().toText(),
},
};
resetIdentity();
neuronsStore.setNeurons({
neurons: [testNeuron],
certified: true,
});
vi.spyOn(governanceApi, "queryNeurons").mockResolvedValue([testNeuron]);
const spyRefreshVotingPower = vi
.spyOn(governanceApi, "refreshVotingPower")
.mockResolvedValue();
vi.spyOn(governanceApi, "queryKnownNeurons").mockResolvedValue([]);

const po = renderComponent(testNeuron);

expect(spyRefreshVotingPower).toHaveBeenCalledTimes(0);

expect(await po.getConfirmFollowingButtonPo().isPresent()).toBe(true);
await po.getConfirmFollowingButtonPo().click();
await runResolvedPromises();

expect(spyRefreshVotingPower).toHaveBeenCalledTimes(1);
expect(spyRefreshVotingPower).toHaveBeenCalledWith({
identity: mockIdentity,
neuronId: testNeuron.neuronId,
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,19 @@ describe("LosingRewardNeuronsModal", () => {
let spyRefreshVotingPower;

const renderComponent = ({
onClose,
neurons,
}: { onClose?: () => void; neurons?: NeuronInfo[] } = {}) => {
withNeuronNavigation,
onClose,
}: {
neurons?: NeuronInfo[];
withNeuronNavigation?: boolean;
onClose?: () => void;
} = {}) => {
const { container, component } = render(LosingRewardNeuronsModal, {
props: { neurons },
props: {
neurons,
withNeuronNavigation,
},
});

if (nonNullish(onClose)) {
Expand Down Expand Up @@ -167,6 +175,24 @@ describe("LosingRewardNeuronsModal", () => {
});
});

it("should not navigate to the neuron details when withNeuronNavigation=false", async () => {
const onClose = vi.fn();
const po = await renderComponent({
onClose,
neurons: [losingRewardsNeuron],
withNeuronNavigation: false,
});
const firstCards = (await po.getNnsLosingRewardsNeuronCardPos())[0];
expect(get(pageStore).path).toEqual("/staking");
expect(firstCards).not.toEqual(undefined);

await firstCards.click();
await runResolvedPromises();

expect(onClose).toHaveBeenCalledTimes(0);
expect(get(pageStore).path).toEqual("/staking");
});

it("should fetch known neurons", async () => {
const queryKnownNeuronsSpy = vi
.spyOn(governanceApi, "queryKnownNeurons")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { mockFullNeuron, mockNeuron } from "$tests/mocks/neurons.mock";
import { NnsLosingRewardsNeuronCardPo } from "$tests/page-objects/NnsLosingRewardsNeuronCard.page-object";
import { JestPageObjectElement } from "$tests/page-objects/jest.page-object";
import { Topic, type NeuronInfo } from "@dfinity/nns";
import { nonNullish } from "@dfinity/utils";
import { render } from "@testing-library/svelte";

describe("NnsLosingRewardsNeuronCard", () => {
Expand Down Expand Up @@ -37,16 +36,13 @@ describe("NnsLosingRewardsNeuronCard", () => {
neuron: NeuronInfo;
onClick?: () => void;
}) => {
const { container, component } = render(NnsLosingRewardsNeuronCard, {
const { container } = render(NnsLosingRewardsNeuronCard, {
props: {
neuron,
onClick,
},
});

if (nonNullish(onClick)) {
component.$on("nnsClick", onClick);
}

return NnsLosingRewardsNeuronCardPo.under(
new JestPageObjectElement(container)
);
Expand Down
51 changes: 51 additions & 0 deletions frontend/src/tests/lib/pages/NnsNeuronDetail.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,57 @@ describe("NeuronDetail", () => {

expect(await po.getConfirmFollowingBannerPo().isPresent()).toBe(false);
});

it("should call refreshVotingPower", async () => {
overrideFeatureFlagsStore.setFlag(
"ENABLE_PERIODIC_FOLLOWING_CONFIRMATION",
true
);
const testNeuron = fakeGovernanceApi.addNeuronWith({
neuronId,
votingPowerRefreshedTimestampSeconds:
nowInSeconds() - SECONDS_IN_HALF_YEAR - SECONDS_IN_DAY,
controller: mockIdentity.getPrincipal().toText(),
});

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

const po = await renderComponent(`${neuronId}`);

expect(
await po
.getNnsNeuronRewardStatusActionPo()
.getConfirmFollowingButtonPo()
.isPresent()
).toBe(true);

expect(spyRefreshVotingPower).toHaveBeenCalledTimes(0);
// open modal
await po
.getNnsNeuronRewardStatusActionPo()
.getConfirmFollowingButtonPo()
.click();
await runResolvedPromises();

const modal = po.getNnsNeuronModalsPo().getLosingRewardNeuronsModalPo();
expect(await modal.isPresent()).toEqual(true);
const cards = await modal.getNnsLosingRewardsNeuronCardPos();
expect(cards.length).toEqual(1);
expect(await cards[0].getNeuronId()).toEqual(`${neuronId}`);

await modal.clickConfirmFollowing();
await runResolvedPromises();

expect(spyRefreshVotingPower).toHaveBeenCalledTimes(1);
expect(spyRefreshVotingPower).toHaveBeenCalledWith({
identity: mockIdentity,
neuronId: testNeuron.neuronId,
});
});
});

it("should render nns project name", async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { BasePageObject } from "$tests/page-objects/base.page-object";
import type { PageObjectElement } from "$tests/types/page-object.types";

export class ConfirmFollowingActionButtonPo extends BasePageObject {
private static readonly TID = "confirm-following-action-button-component";

static under(element: PageObjectElement): ConfirmFollowingActionButtonPo {
return new ConfirmFollowingActionButtonPo(
element.byTestId(ConfirmFollowingActionButtonPo.TID)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { BasePageObject } from "$tests/page-objects/base.page-object";
import type { PageObjectElement } from "$tests/types/page-object.types";
import { ConfirmFollowingBannerPo } from "./ConfirmFollowingBanner.page-object";
import { NnsNeuronPageHeaderPo } from "./NnsNeuronPageHeader.page-object";
import { NnsNeuronRewardStatusActionPo } from "./NnsNeuronRewardStatusAction.page-object";

export class NnsNeuronDetailPo extends BasePageObject {
private static readonly TID = "nns-neuron-detail-component";
Expand Down Expand Up @@ -74,6 +75,10 @@ export class NnsNeuronDetailPo extends BasePageObject {
return ConfirmFollowingBannerPo.under(this.root);
}

getNnsNeuronRewardStatusActionPo(): NnsNeuronRewardStatusActionPo {
return NnsNeuronRewardStatusActionPo.under(this.root);
}

getVotingPowerSectionPo(): NnsNeuronVotingPowerSectionPo {
return NnsNeuronVotingPowerSectionPo.under(this.root);
}
Expand Down
Loading
Loading