diff --git a/CHANGELOG-Nns-Dapp-unreleased.md b/CHANGELOG-Nns-Dapp-unreleased.md index ef1dcee8afd..7bee4dc6d2a 100644 --- a/CHANGELOG-Nns-Dapp-unreleased.md +++ b/CHANGELOG-Nns-Dapp-unreleased.md @@ -22,6 +22,7 @@ proposal is successful, the changes it released will be moved from this file to - Migration functions. - Render pending and failed BTC withdrawal transaction as such. - Add `ENABLE_SNS_TYPES_FILTER` feature flag. +- Redesign proposal detail neurons block (collapsible). #### Changed diff --git a/frontend/src/lib/components/proposal-detail/IneligibleNeuronsCard.svelte b/frontend/src/lib/components/proposal-detail/IneligibleNeuronsCard.svelte index 42260fd3398..081e7750a9a 100644 --- a/frontend/src/lib/components/proposal-detail/IneligibleNeuronsCard.svelte +++ b/frontend/src/lib/components/proposal-detail/IneligibleNeuronsCard.svelte @@ -1,6 +1,5 @@ {#if visible} - -

{$i18n.proposal_detail__ineligible.headline}

-

- {replacePlaceholders($i18n.proposal_detail__ineligible.text, { - $minDissolveDelay: secondsToDissolveDelayDuration( - minSnsDissolveDelaySeconds - ), - })} -

- -
+ )} + + {reasonText(neuron)} + + {/each} + {/if} diff --git a/frontend/src/lib/components/proposal-detail/ProposalVotingSection.svelte b/frontend/src/lib/components/proposal-detail/ProposalVotingSection.svelte index ce65b32d3ad..af016948154 100644 --- a/frontend/src/lib/components/proposal-detail/ProposalVotingSection.svelte +++ b/frontend/src/lib/components/proposal-detail/ProposalVotingSection.svelte @@ -1,7 +1,6 @@ + +
+ +
+
+ + + + +
+ +
+ +
+
+ + +
+
+ + diff --git a/frontend/src/lib/components/proposal-detail/VotingCard/IneligibleNeuronList.svelte b/frontend/src/lib/components/proposal-detail/VotingCard/IneligibleNeuronList.svelte new file mode 100644 index 00000000000..a038a0ff11f --- /dev/null +++ b/frontend/src/lib/components/proposal-detail/VotingCard/IneligibleNeuronList.svelte @@ -0,0 +1,24 @@ + + +{#if ineligibleNeuronCount > 0} + + + {replacePlaceholders($i18n.proposal_detail__ineligible.headline, { + $count: `${ineligibleNeuronCount}`, + })} + + + +{/if} diff --git a/frontend/src/lib/components/proposal-detail/VotingCard/NnsVotingCard.svelte b/frontend/src/lib/components/proposal-detail/VotingCard/NnsVotingCard.svelte new file mode 100644 index 00000000000..e01f4fa822c --- /dev/null +++ b/frontend/src/lib/components/proposal-detail/VotingCard/NnsVotingCard.svelte @@ -0,0 +1,133 @@ + + + diff --git a/frontend/src/lib/components/proposal-detail/VotingCard/VotableNeuronList.svelte b/frontend/src/lib/components/proposal-detail/VotingCard/VotableNeuronList.svelte new file mode 100644 index 00000000000..f9325c5408d --- /dev/null +++ b/frontend/src/lib/components/proposal-detail/VotingCard/VotableNeuronList.svelte @@ -0,0 +1,47 @@ + + +{#if totalVotingNeurons > 0} + +
+ {replacePlaceholders($i18n.proposal_detail__vote.vote_with_neurons, { + $votable_count: `${selectedVotingNeurons}`, + $all_count: `${totalVotingNeurons}`, + })} +
+ + {$i18n.proposal_detail__vote.voting_power} + {formatVotingPower( + totalNeuronsVotingPower === undefined ? 0n : totalNeuronsVotingPower + )} + + +
+{/if} diff --git a/frontend/src/lib/components/proposal-detail/VotingCard/VotedNeuronList.svelte b/frontend/src/lib/components/proposal-detail/VotingCard/VotedNeuronList.svelte new file mode 100644 index 00000000000..d5f7684c13d --- /dev/null +++ b/frontend/src/lib/components/proposal-detail/VotingCard/VotedNeuronList.svelte @@ -0,0 +1,37 @@ + + +{#if votedNeuronCount > 0} + + + {replacePlaceholders($i18n.proposal_detail.neurons_voted, { + $count: `${votedNeuronCount}`, + })} + + + {$i18n.proposal_detail__vote.voting_power} + {formatVotingPower(votedVotingPower)} + + + +{/if} diff --git a/frontend/src/lib/components/proposal-detail/VotingCard/VotingCard.svelte b/frontend/src/lib/components/proposal-detail/VotingCard/VotingCard.svelte index b8779bf2809..cb95850cb18 100644 --- a/frontend/src/lib/components/proposal-detail/VotingCard/VotingCard.svelte +++ b/frontend/src/lib/components/proposal-detail/VotingCard/VotingCard.svelte @@ -1,128 +1,26 @@ @@ -132,27 +30,21 @@ data-tid="voting-card-component" > - {#if $definedNeuronsStore.length > 0} + {#if hasNeurons} {#if neuronsReady} {#if visible} - + {/if} - - - - - + + + - + {:else} -
+
{$i18n.proposal_detail.loading_neurons}
{/if} @@ -165,15 +57,52 @@ diff --git a/frontend/src/lib/components/proposal-detail/VotingCard/VotingNeuronSelect.svelte b/frontend/src/lib/components/proposal-detail/VotingCard/VotingNeuronSelect.svelte deleted file mode 100644 index 30b43024764..00000000000 --- a/frontend/src/lib/components/proposal-detail/VotingCard/VotingNeuronSelect.svelte +++ /dev/null @@ -1,112 +0,0 @@ - - - -
-
- {$i18n.proposal_detail__vote - .neurons}{#if displayNeuronsInfo} ({selectedVotingNeurons}/{totalVotingNeurons}) - {/if} - - -
- - {#if displayNeuronsInfo} -
- {$i18n.proposal_detail__vote.voting_power} - {formatVotingPower( - totalNeuronsVotingPower === undefined ? 0n : totalNeuronsVotingPower - )} -
- {/if} -
- - - - -
- - diff --git a/frontend/src/lib/components/proposal-detail/VotingCard/VotingNeuronSelectContainer.svelte b/frontend/src/lib/components/proposal-detail/VotingCard/VotingNeuronSelectContainer.svelte deleted file mode 100644 index 9a66e5b4389..00000000000 --- a/frontend/src/lib/components/proposal-detail/VotingCard/VotingNeuronSelectContainer.svelte +++ /dev/null @@ -1,45 +0,0 @@ - - -
- -
- - diff --git a/frontend/src/lib/components/proposal-detail/VotingCard/VotingNeuronSelectList.svelte b/frontend/src/lib/components/proposal-detail/VotingCard/VotingNeuronSelectList.svelte index 61fc023be54..8094831a150 100644 --- a/frontend/src/lib/components/proposal-detail/VotingCard/VotingNeuronSelectList.svelte +++ b/frontend/src/lib/components/proposal-detail/VotingCard/VotingNeuronSelectList.svelte @@ -1,11 +1,13 @@ {#if $votingNeuronSelectStore.neurons.length > 0} -
    + {#each $votingNeuronSelectStore.neurons as neuron} -
  • - toggleSelection(neuron.neuronIdString)} - text="block" - {disabled} - > - {shortenWithMiddleEllipsis( - neuron.neuronIdString, - SNS_NEURON_ID_DISPLAY_LENGTH - )} - {formatVotingPower(neuron.votingPower)} - +
  • + + + {shortenWithMiddleEllipsis( + neuron.neuronIdString, + SNS_NEURON_ID_DISPLAY_LENGTH + )} + + + toggleSelection(neuron.neuronIdString)} + {disabled} + > + {formatVotingPower(neuron.votingPower)} + + +
  • {/each} -
+ {/if} - - diff --git a/frontend/src/lib/components/sns-proposals/SnsVotingCard.svelte b/frontend/src/lib/components/sns-proposals/SnsVotingCard.svelte index fbde444d48f..edbcb775627 100644 --- a/frontend/src/lib/components/sns-proposals/SnsVotingCard.svelte +++ b/frontend/src/lib/components/sns-proposals/SnsVotingCard.svelte @@ -1,8 +1,4 @@ - - - -
- - {#if $sortedSnsUserNeuronsStore.length > 0} - {#if neuronsReady} - {#if visible} - - {/if} - - - - - - - {:else} -
- {$i18n.proposal_detail.loading_neurons} -
- {/if} - {/if} - {$i18n.proposal_detail.sign_in} -
-
-
-
- - + diff --git a/frontend/src/lib/i18n/en.json b/frontend/src/lib/i18n/en.json index d44bbaf91aa..e34fa0c095f 100644 --- a/frontend/src/lib/i18n/en.json +++ b/frontend/src/lib/i18n/en.json @@ -520,7 +520,7 @@ "proposer_prefix": "Proposer", "proposer_description": "The ID of the neuron that submitted the proposal.", "open_voting_prefix": "Voting open:", - "my_votes": "My Votes", + "neurons_voted": "$count neurons voted", "loading_neurons": "Loading your neurons...", "unknown_nns_function": "Unknown nnsFunction", "nns_function_name": "nnsFunctionName", @@ -553,8 +553,9 @@ }, "proposal_detail__vote": { "headline": "Cast Vote", - "neurons": "Neurons", - "voting_power": "Voting power", + "vote_with_neurons": "Vote with $votable_count/$all_count Neurons", + "voting_power_value": "Voting power: $value", + "voting_power": "Voting Power:", "vote_progress": "Vote Progress", "total": "Total", "adopt": "Adopt", @@ -565,7 +566,7 @@ "confirm_reject_text": "You are about to cast $votingPower votes against this proposal, are you sure you want to proceed?", "vote_status": "Neuron $neuronId has voted $vote", "cast_vote_neuronId": "Neuron ID $neuronId", - "cast_vote_votingPower": "Voting power of $votingPower", + "cast_vote_votingPower": "Voting Power: $votingPower", "vote_adopt_in_progress": "Adopting proposal $proposalType ($proposalId). $status", "vote_reject_in_progress": "Rejecting proposal $proposalType ($proposalId). $status", "vote_status_registering": "Neurons registered: $completed/$amount. Keep the dapp open until completed.", @@ -585,8 +586,8 @@ "cast_votes_needs": "(Yes needs $immediate_majority)" }, "proposal_detail__ineligible": { - "headline": "Ineligible Neurons", - "text": "The following neurons had a dissolve delay of less than $minDissolveDelay at the time the proposal was submitted, or were created after the proposal was submitted, and therefore are not eligible to vote on it:", + "headline": "$count Ineligible neurons", + "text": "The following neurons are not eligible to vote.", "reason_since": "created after the proposal", "reason_no_permission": "no voting permission", "reason_short": "dissolve delay < $minDissolveDelay" diff --git a/frontend/src/lib/types/i18n.d.ts b/frontend/src/lib/types/i18n.d.ts index f31a1344508..0d5347d406b 100644 --- a/frontend/src/lib/types/i18n.d.ts +++ b/frontend/src/lib/types/i18n.d.ts @@ -540,7 +540,7 @@ interface I18nProposal_detail { proposer_prefix: string; proposer_description: string; open_voting_prefix: string; - my_votes: string; + neurons_voted: string; loading_neurons: string; unknown_nns_function: string; nns_function_name: string; @@ -574,7 +574,8 @@ interface I18nProposal_detail { interface I18nProposal_detail__vote { headline: string; - neurons: string; + vote_with_neurons: string; + voting_power_value: string; voting_power: string; vote_progress: string; total: string; diff --git a/frontend/src/lib/utils/neuron.utils.ts b/frontend/src/lib/utils/neuron.utils.ts index 6bee6bb41e4..a0ed353f928 100644 --- a/frontend/src/lib/utils/neuron.utils.ts +++ b/frontend/src/lib/utils/neuron.utils.ts @@ -846,6 +846,9 @@ export const votedNeuronDetails = ({ (compactNeuronInfoMaybe) => compactNeuronInfoMaybe.vote !== undefined ) as CompactNeuronInfo[]; +export const neuronsVotingPower = (neurons?: CompactNeuronInfo[]): bigint => + neurons?.reduce((sum, { votingPower }) => sum + votingPower, 0n) ?? 0n; + export const hasEnoughMaturityToStake = ({ fullNeuron }: NeuronInfo): boolean => (fullNeuron?.maturityE8sEquivalent ?? 0n) > 0n; diff --git a/frontend/src/tests/lib/components/proposal-detail/IneligibleNeuronsCard.spec.ts b/frontend/src/tests/lib/components/proposal-detail/IneligibleNeuronsCard.spec.ts index f111d3ffba2..97a941b55ee 100644 --- a/frontend/src/tests/lib/components/proposal-detail/IneligibleNeuronsCard.spec.ts +++ b/frontend/src/tests/lib/components/proposal-detail/IneligibleNeuronsCard.spec.ts @@ -18,7 +18,7 @@ describe("IneligibleNeuronsCard", () => { }); it("should display texts", () => { - const { getByText } = render(IneligibleNeuronsCard, { + const { getByTestId } = render(IneligibleNeuronsCard, { props: { ineligibleNeurons: [ { @@ -29,18 +29,10 @@ describe("IneligibleNeuronsCard", () => { minSnsDissolveDelaySeconds: BigInt(NNS_MINIMUM_DISSOLVE_DELAY_TO_VOTE), }, }); - expect( - getByText(en.proposal_detail__ineligible.headline) - ).toBeInTheDocument(); - expect( - getByText( - replacePlaceholders(en.proposal_detail__ineligible.text, { - $minDissolveDelay: secondsToDissolveDelayDuration( - BigInt(NNS_MINIMUM_DISSOLVE_DELAY_TO_VOTE) - ), - }) - ) - ).toBeInTheDocument(); + expect(getByTestId("ineligible-neurons-description")).toBeInTheDocument(); + expect(getByTestId("ineligible-neurons-description").textContent).toEqual( + "The following neurons are not eligible to vote." + ); }); it("should display ineligible neurons with reason 'short'", () => { diff --git a/frontend/src/tests/lib/components/proposal-detail/MyVotes.spec.ts b/frontend/src/tests/lib/components/proposal-detail/MyVotes.spec.ts index 615976f7cb6..c0bfd6363a9 100644 --- a/frontend/src/tests/lib/components/proposal-detail/MyVotes.spec.ts +++ b/frontend/src/tests/lib/components/proposal-detail/MyVotes.spec.ts @@ -18,22 +18,22 @@ describe("MyVotes", () => { }; const neuronsVotedForProposal = [noVoted, yesVoted]; - it("should have title when proposal has been voted by some owned neuron", () => { - const { getByText } = render(MyVotes, { + it("should display voted neurons when proposal has been voted by some owned neuron", () => { + const { queryAllByTestId } = render(MyVotes, { props: { neuronsVotedForProposal, }, }); - expect(getByText(en.proposal_detail.my_votes)).toBeInTheDocument(); + expect(queryAllByTestId("neuron-data").length).toEqual(2); }); - it("should not have title when proposal has not been voted by some owned neuron", () => { - const { getByText } = render(MyVotes, { + it("should not have voted neurons when proposal has not been voted by some owned neuron", () => { + const { queryAllByTestId } = render(MyVotes, { props: { neuronsVotedForProposal: [], }, }); - expect(() => getByText(en.proposal_detail.my_votes)).toThrow(); + expect(queryAllByTestId("neuron-data").length).toEqual(0); }); it("should render an item per voted neuron", () => { @@ -101,4 +101,16 @@ describe("MyVotes", () => { expect(element?.getAttribute("aria-label")).toBeTruthy(); }); + + it("should add colour class", () => { + const rejectedVotedCount = (neurons: CompactNeuronInfo[]) => + render(MyVotes, { + props: { + neuronsVotedForProposal: neurons, + }, + }).container.querySelectorAll(".rejected").length; + + expect(rejectedVotedCount([yesVoted])).toBe(0); + expect(rejectedVotedCount([noVoted])).toBe(1); + }); }); diff --git a/frontend/src/tests/lib/components/proposal-detail/ProposalVotingSection.spec.ts b/frontend/src/tests/lib/components/proposal-detail/ProposalVotingSection.spec.ts index 78840db0ea5..513ded89e31 100644 --- a/frontend/src/tests/lib/components/proposal-detail/ProposalVotingSection.spec.ts +++ b/frontend/src/tests/lib/components/proposal-detail/ProposalVotingSection.spec.ts @@ -78,9 +78,7 @@ describe("ProposalVotingSection", () => { queryByText(en.proposal_detail.voting_results) ).toBeInTheDocument(); expect(getByTestId("voting-confirmation-toolbar")).toBeInTheDocument(); - expect( - queryByText(en.proposal_detail__ineligible.headline) - ).toBeInTheDocument(); + expect(getByTestId("voting-neuron-select")).toBeInTheDocument(); }); it("should not render vote blocks if reward status has settled", () => { diff --git a/frontend/src/tests/lib/components/proposal-detail/VotingCard/NnsVotingCard.spec.ts b/frontend/src/tests/lib/components/proposal-detail/VotingCard/NnsVotingCard.spec.ts index ad3c39e55e9..f3a0fcab078 100644 --- a/frontend/src/tests/lib/components/proposal-detail/VotingCard/NnsVotingCard.spec.ts +++ b/frontend/src/tests/lib/components/proposal-detail/VotingCard/NnsVotingCard.spec.ts @@ -1,5 +1,5 @@ import * as agent from "$lib/api/agent.api"; -import VotingCard from "$lib/components/proposal-detail/VotingCard/VotingCard.svelte"; +import NnsVotingCard from "$lib/components/proposal-detail/VotingCard/NnsVotingCard.svelte"; import { SECONDS_IN_YEAR } from "$lib/constants/constants"; import { authStore } from "$lib/stores/auth.store"; import { neuronsStore } from "$lib/stores/neurons.store"; @@ -58,7 +58,7 @@ describe("VotingCard", () => { proposal: proposalInfo, }), } as SelectedProposalContext, - Component: VotingCard, + Component: NnsVotingCard, }, }); diff --git a/frontend/src/tests/lib/components/proposal-detail/VotingCard/SnsVotingCard.spec.ts b/frontend/src/tests/lib/components/proposal-detail/VotingCard/SnsVotingCard.spec.ts index 22081176a1d..7a86bc4037a 100644 --- a/frontend/src/tests/lib/components/proposal-detail/VotingCard/SnsVotingCard.spec.ts +++ b/frontend/src/tests/lib/components/proposal-detail/VotingCard/SnsVotingCard.spec.ts @@ -11,7 +11,6 @@ import { mockAuthStoreSubscribe, mockIdentity, } from "$tests/mocks/auth.store.mock"; -import en from "$tests/mocks/i18n.mock"; import { createMockSnsNeuron, snsNervousSystemParametersMock, @@ -245,6 +244,26 @@ describe("SnsVotingCard", () => { expect(queryByTestId("vote-no")).toBeInTheDocument(); }); + it("should display votable neurons", async () => { + snsNeuronsStore.setNeurons({ + rootCanisterId: mockSnsCanisterId, + neurons: [ + ...testNeurons, + // voted neuron + { + ...createMockSnsNeuron({ + id: [3], + state: NeuronState.Locked, + }), + }, + ], + certified: true, + }); + + const { getByTestId } = renderVotingCard(); + expect(getByTestId("votable-neurons")).toBeInTheDocument(); + }); + it("should display my votes", async () => { snsNeuronsStore.setNeurons({ rootCanisterId: mockSnsCanisterId, @@ -261,8 +280,28 @@ describe("SnsVotingCard", () => { certified: true, }); - const { getByText } = renderVotingCard(); - expect(getByText(en.proposal_detail.my_votes)).toBeInTheDocument(); + const { getByTestId } = renderVotingCard(); + expect(getByTestId("voted-neurons")).toBeInTheDocument(); + }); + + it("should display ineligible neurons", async () => { + snsNeuronsStore.setNeurons({ + rootCanisterId: mockSnsCanisterId, + neurons: [ + ...testNeurons, + // voted neuron + { + ...createMockSnsNeuron({ + id: [3], + state: NeuronState.Unspecified, + }), + }, + ], + certified: true, + }); + + const { getByTestId } = renderVotingCard(); + expect(getByTestId("ineligible-neurons")).toBeInTheDocument(); }); it("should display my votes with ballot voting power", async () => { @@ -315,8 +354,8 @@ describe("SnsVotingCard", () => { certified: true, }); - const { getByText } = renderVotingCard(); - expect(getByText(en.proposal_detail.my_votes)).toBeInTheDocument(); + const { getByTestId } = renderVotingCard(); + expect(getByTestId("voted-neurons")).toBeInTheDocument(); }); describe("voting", () => { diff --git a/frontend/src/tests/lib/components/proposal-detail/VotingCard/VotingNeuronSelect.spec.ts b/frontend/src/tests/lib/components/proposal-detail/VotingCard/VotableNeuronList.spec.ts similarity index 60% rename from frontend/src/tests/lib/components/proposal-detail/VotingCard/VotingNeuronSelect.spec.ts rename to frontend/src/tests/lib/components/proposal-detail/VotingCard/VotableNeuronList.spec.ts index cfa6d37d279..44cc48e2a86 100644 --- a/frontend/src/tests/lib/components/proposal-detail/VotingCard/VotingNeuronSelect.spec.ts +++ b/frontend/src/tests/lib/components/proposal-detail/VotingCard/VotableNeuronList.spec.ts @@ -1,14 +1,13 @@ -import VotingNeuronSelect from "$lib/components/proposal-detail/VotingCard/VotingNeuronSelect.svelte"; +import VotableNeuronList from "$lib/components/proposal-detail/VotingCard/VotableNeuronList.svelte"; import { votingNeuronSelectStore } from "$lib/stores/vote-registration.store"; import { formatVotingPower } from "$lib/utils/neuron.utils"; import { nnsNeuronToVotingNeuron } from "$lib/utils/proposals.utils"; -import en from "$tests/mocks/i18n.mock"; import { mockNeuron } from "$tests/mocks/neurons.mock"; import { mockProposalInfo } from "$tests/mocks/proposal.mock"; import { Vote, type NeuronInfo } from "@dfinity/nns"; import { render, waitFor } from "@testing-library/svelte"; -describe("VotingNeuronSelect", () => { +describe("VotableNeuronList", () => { const neuron1 = { ...mockNeuron, neuronId: 111n, @@ -45,47 +44,44 @@ describe("VotingNeuronSelect", () => { }); it("should display total voting power of ballots not of neurons", async () => { - const { queryByText } = render(VotingNeuronSelect); - const ballotsVotingPower = formatVotingPower( - ballots[0].votingPower + ballots[1].votingPower + ballots[2].votingPower - ); - expect(queryByText(ballotsVotingPower)).toBeInTheDocument(); + const { getByTestId } = render(VotableNeuronList, { + props: { + voteRegistration: undefined, + }, + }); + expect( + getByTestId("voting-collapsible-toolbar-voting-power").textContent + ).toBe("897.00"); }); it("should not display total voting power of neurons", async () => { - const { queryByText } = render(VotingNeuronSelect); + const { getByTestId } = render(VotableNeuronList, { + props: { + voteRegistration: undefined, + }, + }); const neuronsVotingPower = formatVotingPower( neurons[0].votingPower + neurons[1].votingPower + neurons[2].votingPower ); - expect(queryByText(neuronsVotingPower)).toBeNull(); + expect( + getByTestId("voting-collapsible-toolbar-voting-power").textContent + ).not.toBe(neuronsVotingPower); }); it("should recalculate total voting power after selection", async () => { - const { getByText } = render(VotingNeuronSelect); - - votingNeuronSelectStore.toggleSelection(`${neurons[1].neuronId}`); - const total = formatVotingPower( - ballots[0].votingPower + ballots[2].votingPower - ); - - waitFor(() => expect(getByText(total)).toBeInTheDocument()); - }); - - describe("No selectable neurons", () => { - beforeEach(() => { - votingNeuronSelectStore.set([]); + const { getByTestId } = render(VotableNeuronList, { + props: { + voteRegistration: undefined, + }, }); - it("should display no neurons information", () => { - const { getByTestId } = render(VotingNeuronSelect); - + votingNeuronSelectStore.toggleSelection(`${neurons[1].neuronId}`); + // ballots[0].votingPower + ballots[2].votingPower + await waitFor(() => expect( - getByTestId("voting-collapsible-toolbar-neurons")?.textContent?.trim() - ).toEqual(en.proposal_detail__vote.neurons); - expect(() => - getByTestId("voting-collapsible-toolbar-voting-power") - ).toThrow(); - }); + getByTestId("voting-collapsible-toolbar-voting-power").textContent + ).toBe("598.00") + ); }); describe("Has selected neurons", () => { @@ -101,21 +97,27 @@ describe("VotingNeuronSelect", () => { ); it("should display voting power", () => { - const { getByTestId } = render(VotingNeuronSelect); + const { getByTestId } = render(VotableNeuronList, { + props: { + voteRegistration: undefined, + }, + }); expect( - getByTestId("voting-collapsible-toolbar-voting-power") - ).not.toBeNull(); + getByTestId("voting-collapsible-toolbar-voting-power").textContent + ).toBe("897.00"); }); it("should display selectable neurons for voting power", () => { - const { getByTestId } = render(VotingNeuronSelect); + const { getByTestId } = render(VotableNeuronList, { + props: { + voteRegistration: undefined, + }, + }); expect( - getByTestId("voting-collapsible-toolbar-neurons") - ?.textContent?.trim() - .includes(`(${neurons.length}/${neurons.length})`) - ).toBeTruthy(); + getByTestId("voting-collapsible-toolbar-neurons")?.textContent + ).toBe(`Vote with 3/3 Neurons`); }); }); }); diff --git a/frontend/src/tests/lib/components/proposal-detail/VotingCard/VotingCard.spec.ts b/frontend/src/tests/lib/components/proposal-detail/VotingCard/VotingCard.spec.ts new file mode 100644 index 00000000000..9665f0486a6 --- /dev/null +++ b/frontend/src/tests/lib/components/proposal-detail/VotingCard/VotingCard.spec.ts @@ -0,0 +1,145 @@ +import VotingCard from "$lib/components/proposal-detail/VotingCard/VotingCard.svelte"; +import { authStore } from "$lib/stores/auth.store"; +import { votingNeuronSelectStore } from "$lib/stores/vote-registration.store"; +import type { CompactNeuronInfo } from "$lib/utils/neuron.utils"; +import { nnsNeuronToVotingNeuron } from "$lib/utils/proposals.utils"; +import { mockAuthStoreSubscribe } from "$tests/mocks/auth.store.mock"; +import { mockNeuron } from "$tests/mocks/neurons.mock"; +import { mockProposalInfo } from "$tests/mocks/proposal.mock"; +import { VotingCardPo } from "$tests/page-objects/VotingCard.page-object"; +import { JestPageObjectElement } from "$tests/page-objects/jest.page-object"; +import { runResolvedPromises } from "$tests/utils/timers.test-utils"; +import { Vote, type NeuronInfo } from "@dfinity/nns"; +import { render } from "@testing-library/svelte"; +import { describe } from "vitest"; + +describe("VotingCard", () => { + const neuron1 = { + ...mockNeuron, + neuronId: 111n, + votingPower: 10_000_000_000n, + }; + const neuron2 = { + ...mockNeuron, + neuronId: 222n, + votingPower: 30_000_000_000n, + }; + const neuron3 = { + ...mockNeuron, + neuronId: 333n, + votingPower: 50_000_000_000n, + }; + const neurons: NeuronInfo[] = [neuron1, neuron2, neuron3]; + const ballots = neurons.map(({ neuronId, votingPower }) => ({ + neuronId, + // Ballots and neurons have different voting power + votingPower: votingPower - 100_000_000n, + vote: Vote.No, + })); + const proposalInfo = { + ...mockProposalInfo, + ballots, + }; + const yesVoted: CompactNeuronInfo = { + idString: "200", + votingPower: 200n, + vote: Vote.Yes, + }; + + const renderComponent = async (props = {}) => { + const { container } = render(VotingCard, { + props: { + hasNeurons: true, + visible: false, + neuronsReady: true, + voteRegistration: undefined, + neuronsVotedForProposal: [yesVoted], + ineligibleNeurons: [ + { + neuronIdString: "111", + reason: "since", + }, + ], + minSnsDissolveDelaySeconds: 100n, + ...props, + }, + }); + + await runResolvedPromises(); + + return VotingCardPo.under(new JestPageObjectElement(container)); + }; + + beforeEach(() => { + votingNeuronSelectStore.set( + neurons.map((neuron) => + nnsNeuronToVotingNeuron({ neuron, proposal: proposalInfo }) + ) + ); + }); + + it("should display SignIn button", async () => { + const po = await renderComponent(); + + expect(await po.getSignInButtonPo().isPresent()).toBe(true); + }); + + describe("Signed in", () => { + beforeEach(() => { + vi.spyOn(authStore, "subscribe").mockImplementation( + mockAuthStoreSubscribe + ); + }); + + it("should spinner when neurons not ready", async () => { + const po = await renderComponent({ + neuronsReady: false, + }); + expect(await po.getSpinnerPo().isPresent()).toBe(true); + }); + + it("should hide spinner when neurons are ready", async () => { + const po = await renderComponent({ + neuronsReady: true, + }); + expect(await po.getSpinnerPo().isPresent()).toBe(false); + }); + + it("should display all neuron blocks", async () => { + const po = await renderComponent(); + + expect(await po.getVotableNeurons().isPresent()).toBe(true); + expect(await po.getVotedNeurons().isPresent()).toBe(true); + expect(await po.getIneligibleNeurons().isPresent()).toBe(true); + }); + + it("should not display votable neurons block", async () => { + votingNeuronSelectStore.reset(); + const po = await renderComponent({}); + + expect(await po.getVotableNeurons().isPresent()).toBe(false); + expect(await po.getVotedNeurons().isPresent()).toBe(true); + expect(await po.getIneligibleNeurons().isPresent()).toBe(true); + }); + + it("should not display voted neurons block", async () => { + const po = await renderComponent({ + neuronsVotedForProposal: [], + }); + + expect(await po.getVotableNeurons().isPresent()).toBe(true); + expect(await po.getVotedNeurons().isPresent()).toBe(false); + expect(await po.getIneligibleNeurons().isPresent()).toBe(true); + }); + + it("should not display ineligible neurons block", async () => { + const po = await renderComponent({ + ineligibleNeurons: [], + }); + + expect(await po.getVotableNeurons().isPresent()).toBe(true); + expect(await po.getVotedNeurons().isPresent()).toBe(true); + expect(await po.getIneligibleNeurons().isPresent()).toBe(false); + }); + }); +}); diff --git a/frontend/src/tests/lib/utils/neuron.utils.spec.ts b/frontend/src/tests/lib/utils/neuron.utils.spec.ts index ddd4bc601b1..91b2af85c80 100644 --- a/frontend/src/tests/lib/utils/neuron.utils.spec.ts +++ b/frontend/src/tests/lib/utils/neuron.utils.spec.ts @@ -57,11 +57,13 @@ import { neuronCanBeSplit, neuronStake, neuronVotingPower, + neuronsVotingPower, sortNeuronsByCreatedTimestamp, topicsToFollow, userAuthorizedNeuron, validTopUpAmount, votedNeuronDetails, + type CompactNeuronInfo, type IneligibleNeuronData, type InvalidState, type NeuronTagData, @@ -2138,6 +2140,24 @@ describe("neuron-utils", () => { }); }); + describe("votedNeuronDetails", () => { + it("should return neurons voting power", () => { + const neurons = [ + { + idString: "100", + votingPower: 100n, + vote: Vote.No, + }, + { + idString: "200", + votingPower: 200n, + vote: Vote.Yes, + }, + ] as CompactNeuronInfo[]; + expect(neuronsVotingPower(neurons)).toBe(300n); + }); + }); + describe("neuronCanBeSplit", () => { it("should return true if neuron has enough stake to be splitted", () => { const neuron = { diff --git a/frontend/src/tests/page-objects/SnsVotingCard.page-object.ts b/frontend/src/tests/page-objects/SnsVotingCard.page-object.ts index 2283e8c09fd..135811ef9ac 100644 --- a/frontend/src/tests/page-objects/SnsVotingCard.page-object.ts +++ b/frontend/src/tests/page-objects/SnsVotingCard.page-object.ts @@ -3,7 +3,7 @@ import type { PageObjectElement } from "$tests/types/page-object.types"; import { VotingConfirmationToolbarPo } from "./VotingConfirmationToolbar.page-object"; export class SnsVotingCardPo extends BasePageObject { - private static readonly TID = "sns-voting-card-component"; + private static readonly TID = "voting-card-component"; private constructor(root: PageObjectElement) { super(root); diff --git a/frontend/src/tests/page-objects/VotingCard.page-object.ts b/frontend/src/tests/page-objects/VotingCard.page-object.ts index 14f0110628c..9365341a8b3 100644 --- a/frontend/src/tests/page-objects/VotingCard.page-object.ts +++ b/frontend/src/tests/page-objects/VotingCard.page-object.ts @@ -13,12 +13,28 @@ export class VotingCardPo extends BasePageObject { return new VotingCardPo(element.byTestId(VotingCardPo.TID)); } + getVotableNeurons(): PageObjectElement { + return this.root.byTestId("votable-neurons"); + } + + getVotedNeurons(): PageObjectElement { + return this.root.byTestId("voted-neurons"); + } + + getIneligibleNeurons(): PageObjectElement { + return this.root.byTestId("ineligible-neurons"); + } + getVoteYesButtonPo(): ButtonPo { return this.getButton("vote-yes"); } - getVoteNoButtonPo(): ButtonPo { - return this.getButton("vote-no"); + getSignInButtonPo(): ButtonPo { + return this.getButton("login-button"); + } + + getSpinnerPo(): PageObjectElement { + return this.root.byTestId("loading-neurons-spinner"); } getConfirmYesButtonPo(): ButtonPo {