From 6ad5330caeb27cf40a00861e66d30981d599717b Mon Sep 17 00:00:00 2001 From: Max Strasinsky <98811342+mstrasinskis@users.noreply.github.com> Date: Fri, 4 Oct 2024 14:46:14 +0200 Subject: [PATCH] Use proposal ballots for voting state (#5559) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Motivation There are multiple reports from users who can’t vote. We believe the root cause of this issue is the 100-ballot limit for neurons. If a user has voted on more than 100 proposals (a likely scenario over a 4-day period), they don’t receive ballots for proposals they’ve already voted on, which remain technically votable due to this limit. The suggested solution is to switch from using neuron ballots to proposal ballots when calculating the votable state. The utilities have already been already updated in `ic-js` - https://github.com/dfinity/ic-js/pull/725 # Changes - `npm run upgrade:next` - Update the displayed votable neurons on proposal changes as well, since we use the proposal ballots to determine the neurons’ voting state. # Tests - Updated. - Manually 1. Created 100+ proposals. 2. Voted on them. 3. Ensured that some of them were still displayed as votable (due to the recentBallots limit). 4. Switched to the utilities that rely on the proposal ballots. 5. Verified that all the proposals previously displayed as votable (despite having already been voted on) were now displayed as voted. # Todos - [x] Add entry to changelog (if necessary). --- CHANGELOG-Nns-Dapp-unreleased.md | 1 + frontend/package-lock.json | 109 ++++++++--------- .../VotingCard/NnsVotingCard.svelte | 44 +++++-- .../VotingCard/NnsVotingCard.spec.ts | 5 +- .../tests/lib/pages/NnsProposalDetail.spec.ts | 111 +++++++++++++++++- .../actionable-proposals.services.spec.ts | 42 +++++++ .../vote-registration.services.spec.ts | 5 + .../page-objects/VotingCard.page-object.ts | 5 + 8 files changed, 249 insertions(+), 73 deletions(-) diff --git a/CHANGELOG-Nns-Dapp-unreleased.md b/CHANGELOG-Nns-Dapp-unreleased.md index 8cd5abad371..7d4fe8dbdd3 100644 --- a/CHANGELOG-Nns-Dapp-unreleased.md +++ b/CHANGELOG-Nns-Dapp-unreleased.md @@ -36,6 +36,7 @@ proposal is successful, the changes it released will be moved from this file to * Fixed a bug where a performance counter in `init` is wiped during state initialization. * Bug with parsing nervous system parameters from aborted SNSes. +* Bug where neurons are displayed as eligible to vote, even though they have already voted. #### Security diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f722ab74b6d..2b7f2c51123 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -249,10 +249,9 @@ } }, "node_modules/@dfinity/ckbtc": { - "version": "3.1.0-next-2024-09-30", - "resolved": "https://registry.npmjs.org/@dfinity/ckbtc/-/ckbtc-3.1.0-next-2024-09-30.tgz", - "integrity": "sha512-FlVouuKVSaiWWwfhB5CrhZInzZWKfTSEyKjiQ7DvYvemS0nhh67KeqPz4a+WVuh89iqNQAqbSHeYxvF6y1aYIw==", - "license": "Apache-2.0", + "version": "3.1.0-next-2024-10-02", + "resolved": "https://registry.npmjs.org/@dfinity/ckbtc/-/ckbtc-3.1.0-next-2024-10-02.tgz", + "integrity": "sha512-4Ixs29AWMy1HgQz11z1rSZyoUu3EO48+QQcfHSGIsHL09saoWiqaQi0Lyac8yd0mXzawyFZEmGW2XTKqFFFkGA==", "dependencies": { "@noble/hashes": "^1.3.2", "base58-js": "^1.0.5", @@ -266,10 +265,9 @@ } }, "node_modules/@dfinity/cmc": { - "version": "3.2.1-next-2024-09-30", - "resolved": "https://registry.npmjs.org/@dfinity/cmc/-/cmc-3.2.1-next-2024-09-30.tgz", - "integrity": "sha512-Ht88gCwQ/uiy2l6vMSU6ZBu5GniiWaUSNRxMqVqln1guAa4DvZBcVfx3pdM7QbFI7RmNBr5VIpuMY2AWa8ExYg==", - "license": "Apache-2.0", + "version": "3.2.1-next-2024-10-02", + "resolved": "https://registry.npmjs.org/@dfinity/cmc/-/cmc-3.2.1-next-2024-10-02.tgz", + "integrity": "sha512-Mwg7JPSi+vlii2+rj783rnQhSslh2+4T9/8tK5+ZQ6TVSiA6PXU8g3dvNM7EJ9G0VVO4uhv7qkYrxjXomjtQAw==", "peerDependencies": { "@dfinity/agent": "*", "@dfinity/candid": "*", @@ -294,10 +292,9 @@ } }, "node_modules/@dfinity/ic-management": { - "version": "5.2.1-next-2024-09-30", - "resolved": "https://registry.npmjs.org/@dfinity/ic-management/-/ic-management-5.2.1-next-2024-09-30.tgz", - "integrity": "sha512-mxLgdCoXoyd19+eiuriSPMdAu9d995xg7NuDd4GnXr/XDbAellCcGLA7tn8UumMptzYCGDdJq6NfnnXoldcvXg==", - "license": "Apache-2.0", + "version": "5.2.1-next-2024-10-02", + "resolved": "https://registry.npmjs.org/@dfinity/ic-management/-/ic-management-5.2.1-next-2024-10-02.tgz", + "integrity": "sha512-Q7C2rupyZx8JBciOLU7eq+GWs9AQwSrVBa16QstKq5S/oG84bwjif+3DaN9PgQhHnm6CasWZcShlC79a6SXxVw==", "peerDependencies": { "@dfinity/agent": "*", "@dfinity/candid": "*", @@ -321,10 +318,9 @@ } }, "node_modules/@dfinity/ledger-icp": { - "version": "2.6.0-next-2024-09-30", - "resolved": "https://registry.npmjs.org/@dfinity/ledger-icp/-/ledger-icp-2.6.0-next-2024-09-30.tgz", - "integrity": "sha512-la+XmcMYeJBQ8E+dsTShMhLA4BXoMDGM5pzXI7Yd6j6d7wL4ZJvkjw8UHAJaNaByuLeAusCgYG5PwgREb6rJZQ==", - "license": "Apache-2.0", + "version": "2.6.0-next-2024-10-02", + "resolved": "https://registry.npmjs.org/@dfinity/ledger-icp/-/ledger-icp-2.6.0-next-2024-10-02.tgz", + "integrity": "sha512-bmFhbwQ9OPWbUQuX+BwJP2ma6MGhhnIthsSNZu0YxxMXEgqLzjj83kBZPoji8TUg6soxaX3d6LfmD8Szv+X1dg==", "peerDependencies": { "@dfinity/agent": "*", "@dfinity/candid": "*", @@ -333,10 +329,9 @@ } }, "node_modules/@dfinity/ledger-icrc": { - "version": "2.6.0-next-2024-09-30", - "resolved": "https://registry.npmjs.org/@dfinity/ledger-icrc/-/ledger-icrc-2.6.0-next-2024-09-30.tgz", - "integrity": "sha512-KDPHVUujUUOS2WaAJXrja1vgX4iPOZ8cxSO/Q0zdrYrZo7Q0tSMoxC+t2mywxVuF2whhJUgQLIEzC8TJMbkiFQ==", - "license": "Apache-2.0", + "version": "2.6.0-next-2024-10-02", + "resolved": "https://registry.npmjs.org/@dfinity/ledger-icrc/-/ledger-icrc-2.6.0-next-2024-10-02.tgz", + "integrity": "sha512-J2GVKJLlFzUdQoX0M008V73g6ZSmsUC+ogiqgilU+KCP1i9vY/7GVuMhBa2g7k8wrwSxn/v4H70WC5KiJzfMMA==", "peerDependencies": { "@dfinity/agent": "*", "@dfinity/candid": "*", @@ -345,10 +340,9 @@ } }, "node_modules/@dfinity/nns": { - "version": "7.0.0-next-2024-09-30", - "resolved": "https://registry.npmjs.org/@dfinity/nns/-/nns-7.0.0-next-2024-09-30.tgz", - "integrity": "sha512-biigcsSW9bXNt0zleNsARnKFozxAYkUC4FrfgtG0LzXd7t+Lh3owh8MK4nVaSLkoEiSvE+d1obhrDrAW27PYmQ==", - "license": "Apache-2.0", + "version": "7.0.0-next-2024-10-02", + "resolved": "https://registry.npmjs.org/@dfinity/nns/-/nns-7.0.0-next-2024-10-02.tgz", + "integrity": "sha512-yHoRcppJ0Ktm+SIueG4w27MtKuYMKVGUqdWVcOtoSgZKOfHAjJAS0lUko916YwFvz5b7twh9OMBh6bDYtyHBJw==", "dependencies": { "@noble/hashes": "^1.3.2", "randombytes": "^2.1.0" @@ -370,10 +364,9 @@ } }, "node_modules/@dfinity/sns": { - "version": "3.2.1-next-2024-09-30", - "resolved": "https://registry.npmjs.org/@dfinity/sns/-/sns-3.2.1-next-2024-09-30.tgz", - "integrity": "sha512-fhNhk6WaES10y4oiXU3VQP+jq5fOnMpSLerGSpWNYDABTS7LT4qs5vNa/Z33mAbfyK/euuJO50sUYCw2Y6NIJg==", - "license": "Apache-2.0", + "version": "3.2.1-next-2024-10-02", + "resolved": "https://registry.npmjs.org/@dfinity/sns/-/sns-3.2.1-next-2024-10-02.tgz", + "integrity": "sha512-I/+b4Cz1GBn4c06NKVw3uywN5EAsN4178Y11H9XPJOM1nRtc03roewkAt0kfpHXyaO5hbOKWq3UYJAdUJFU9JA==", "dependencies": { "@noble/hashes": "^1.3.2" }, @@ -386,10 +379,9 @@ } }, "node_modules/@dfinity/utils": { - "version": "2.5.1-next-2024-09-30", - "resolved": "https://registry.npmjs.org/@dfinity/utils/-/utils-2.5.1-next-2024-09-30.tgz", - "integrity": "sha512-fV9N5DCS6jAtUAOCHNRvMqzjhs6Uq3IEIjZ/c2Nrj+4UPxB9vczthniMaghINJZq4xv5LQwpKYN3C8ZpwLz8QQ==", - "license": "Apache-2.0", + "version": "2.5.1-next-2024-10-02", + "resolved": "https://registry.npmjs.org/@dfinity/utils/-/utils-2.5.1-next-2024-10-02.tgz", + "integrity": "sha512-Q/2Gt0Z6DuwybmfCewszb0cQvcvGts7cV88kOcE41bIPyfLTvzK/B/TnrBW2KS2jHv8MZluBrPZJi73zVhHTbQ==", "peerDependencies": { "@dfinity/agent": "*", "@dfinity/candid": "*", @@ -2241,7 +2233,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/base58-js/-/base58-js-1.0.5.tgz", "integrity": "sha512-LkkAPP8Zu+c0SVNRTRVDyMfKVORThX+rCViget00xdgLRrKkClCTz1T7cIrpr69ShwV5XJuuoZvMvJ43yURwkA==", - "license": "MIT", "engines": { "node": ">= 8" } @@ -2276,8 +2267,7 @@ "node_modules/bech32": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", - "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", - "license": "MIT" + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" }, "node_modules/bignumber.js": { "version": "9.1.1", @@ -5567,7 +5557,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" } @@ -7221,9 +7210,9 @@ "requires": {} }, "@dfinity/ckbtc": { - "version": "3.1.0-next-2024-09-30", - "resolved": "https://registry.npmjs.org/@dfinity/ckbtc/-/ckbtc-3.1.0-next-2024-09-30.tgz", - "integrity": "sha512-FlVouuKVSaiWWwfhB5CrhZInzZWKfTSEyKjiQ7DvYvemS0nhh67KeqPz4a+WVuh89iqNQAqbSHeYxvF6y1aYIw==", + "version": "3.1.0-next-2024-10-02", + "resolved": "https://registry.npmjs.org/@dfinity/ckbtc/-/ckbtc-3.1.0-next-2024-10-02.tgz", + "integrity": "sha512-4Ixs29AWMy1HgQz11z1rSZyoUu3EO48+QQcfHSGIsHL09saoWiqaQi0Lyac8yd0mXzawyFZEmGW2XTKqFFFkGA==", "requires": { "@noble/hashes": "^1.3.2", "base58-js": "^1.0.5", @@ -7231,9 +7220,9 @@ } }, "@dfinity/cmc": { - "version": "3.2.1-next-2024-09-30", - "resolved": "https://registry.npmjs.org/@dfinity/cmc/-/cmc-3.2.1-next-2024-09-30.tgz", - "integrity": "sha512-Ht88gCwQ/uiy2l6vMSU6ZBu5GniiWaUSNRxMqVqln1guAa4DvZBcVfx3pdM7QbFI7RmNBr5VIpuMY2AWa8ExYg==", + "version": "3.2.1-next-2024-10-02", + "resolved": "https://registry.npmjs.org/@dfinity/cmc/-/cmc-3.2.1-next-2024-10-02.tgz", + "integrity": "sha512-Mwg7JPSi+vlii2+rj783rnQhSslh2+4T9/8tK5+ZQ6TVSiA6PXU8g3dvNM7EJ9G0VVO4uhv7qkYrxjXomjtQAw==", "requires": {} }, "@dfinity/gix-components": { @@ -7248,9 +7237,9 @@ } }, "@dfinity/ic-management": { - "version": "5.2.1-next-2024-09-30", - "resolved": "https://registry.npmjs.org/@dfinity/ic-management/-/ic-management-5.2.1-next-2024-09-30.tgz", - "integrity": "sha512-mxLgdCoXoyd19+eiuriSPMdAu9d995xg7NuDd4GnXr/XDbAellCcGLA7tn8UumMptzYCGDdJq6NfnnXoldcvXg==", + "version": "5.2.1-next-2024-10-02", + "resolved": "https://registry.npmjs.org/@dfinity/ic-management/-/ic-management-5.2.1-next-2024-10-02.tgz", + "integrity": "sha512-Q7C2rupyZx8JBciOLU7eq+GWs9AQwSrVBa16QstKq5S/oG84bwjif+3DaN9PgQhHnm6CasWZcShlC79a6SXxVw==", "requires": {} }, "@dfinity/identity": { @@ -7264,21 +7253,21 @@ } }, "@dfinity/ledger-icp": { - "version": "2.6.0-next-2024-09-30", - "resolved": "https://registry.npmjs.org/@dfinity/ledger-icp/-/ledger-icp-2.6.0-next-2024-09-30.tgz", - "integrity": "sha512-la+XmcMYeJBQ8E+dsTShMhLA4BXoMDGM5pzXI7Yd6j6d7wL4ZJvkjw8UHAJaNaByuLeAusCgYG5PwgREb6rJZQ==", + "version": "2.6.0-next-2024-10-02", + "resolved": "https://registry.npmjs.org/@dfinity/ledger-icp/-/ledger-icp-2.6.0-next-2024-10-02.tgz", + "integrity": "sha512-bmFhbwQ9OPWbUQuX+BwJP2ma6MGhhnIthsSNZu0YxxMXEgqLzjj83kBZPoji8TUg6soxaX3d6LfmD8Szv+X1dg==", "requires": {} }, "@dfinity/ledger-icrc": { - "version": "2.6.0-next-2024-09-30", - "resolved": "https://registry.npmjs.org/@dfinity/ledger-icrc/-/ledger-icrc-2.6.0-next-2024-09-30.tgz", - "integrity": "sha512-KDPHVUujUUOS2WaAJXrja1vgX4iPOZ8cxSO/Q0zdrYrZo7Q0tSMoxC+t2mywxVuF2whhJUgQLIEzC8TJMbkiFQ==", + "version": "2.6.0-next-2024-10-02", + "resolved": "https://registry.npmjs.org/@dfinity/ledger-icrc/-/ledger-icrc-2.6.0-next-2024-10-02.tgz", + "integrity": "sha512-J2GVKJLlFzUdQoX0M008V73g6ZSmsUC+ogiqgilU+KCP1i9vY/7GVuMhBa2g7k8wrwSxn/v4H70WC5KiJzfMMA==", "requires": {} }, "@dfinity/nns": { - "version": "7.0.0-next-2024-09-30", - "resolved": "https://registry.npmjs.org/@dfinity/nns/-/nns-7.0.0-next-2024-09-30.tgz", - "integrity": "sha512-biigcsSW9bXNt0zleNsARnKFozxAYkUC4FrfgtG0LzXd7t+Lh3owh8MK4nVaSLkoEiSvE+d1obhrDrAW27PYmQ==", + "version": "7.0.0-next-2024-10-02", + "resolved": "https://registry.npmjs.org/@dfinity/nns/-/nns-7.0.0-next-2024-10-02.tgz", + "integrity": "sha512-yHoRcppJ0Ktm+SIueG4w27MtKuYMKVGUqdWVcOtoSgZKOfHAjJAS0lUko916YwFvz5b7twh9OMBh6bDYtyHBJw==", "requires": { "@noble/hashes": "^1.3.2", "randombytes": "^2.1.0" @@ -7293,17 +7282,17 @@ } }, "@dfinity/sns": { - "version": "3.2.1-next-2024-09-30", - "resolved": "https://registry.npmjs.org/@dfinity/sns/-/sns-3.2.1-next-2024-09-30.tgz", - "integrity": "sha512-fhNhk6WaES10y4oiXU3VQP+jq5fOnMpSLerGSpWNYDABTS7LT4qs5vNa/Z33mAbfyK/euuJO50sUYCw2Y6NIJg==", + "version": "3.2.1-next-2024-10-02", + "resolved": "https://registry.npmjs.org/@dfinity/sns/-/sns-3.2.1-next-2024-10-02.tgz", + "integrity": "sha512-I/+b4Cz1GBn4c06NKVw3uywN5EAsN4178Y11H9XPJOM1nRtc03roewkAt0kfpHXyaO5hbOKWq3UYJAdUJFU9JA==", "requires": { "@noble/hashes": "^1.3.2" } }, "@dfinity/utils": { - "version": "2.5.1-next-2024-09-30", - "resolved": "https://registry.npmjs.org/@dfinity/utils/-/utils-2.5.1-next-2024-09-30.tgz", - "integrity": "sha512-fV9N5DCS6jAtUAOCHNRvMqzjhs6Uq3IEIjZ/c2Nrj+4UPxB9vczthniMaghINJZq4xv5LQwpKYN3C8ZpwLz8QQ==", + "version": "2.5.1-next-2024-10-02", + "resolved": "https://registry.npmjs.org/@dfinity/utils/-/utils-2.5.1-next-2024-10-02.tgz", + "integrity": "sha512-Q/2Gt0Z6DuwybmfCewszb0cQvcvGts7cV88kOcE41bIPyfLTvzK/B/TnrBW2KS2jHv8MZluBrPZJi73zVhHTbQ==", "requires": {} }, "@esbuild/aix-ppc64": { diff --git a/frontend/src/lib/components/proposal-detail/VotingCard/NnsVotingCard.svelte b/frontend/src/lib/components/proposal-detail/VotingCard/NnsVotingCard.svelte index a99a93ed2db..63953109db6 100644 --- a/frontend/src/lib/components/proposal-detail/VotingCard/NnsVotingCard.svelte +++ b/frontend/src/lib/components/proposal-detail/VotingCard/NnsVotingCard.svelte @@ -29,17 +29,22 @@ type Vote, votableNeurons as getVotableNeurons, } from "@dfinity/nns"; + import type { NeuronInfo } from "@dfinity/nns"; import { getContext } from "svelte"; export let proposalInfo: ProposalInfo; - const votableNeurons = () => + const votableNeurons = ({ + neurons, + proposal, + }: { + neurons: NeuronInfo[]; + proposal: ProposalInfo; + }) => getVotableNeurons({ - neurons: $definedNeuronsStore, - proposal: proposalInfo, - }).map((neuron) => - nnsNeuronToVotingNeuron({ neuron, proposal: proposalInfo }) - ); + neurons, + proposal, + }).map((neuron) => nnsNeuronToVotingNeuron({ neuron, proposal })); let visible = false; /** Signals that the initial checkbox preselection was done. To avoid removing of user selection after second queryAndUpdate callback. */ @@ -53,17 +58,36 @@ $: $definedNeuronsStore, (visible = isProposalDeadlineInTheFuture(proposalInfo)); - const updateVotingNeuronSelectedStore = () => { + const updateVotingNeuronSelectedStore = ({ + neurons, + proposal, + }: { + neurons: NeuronInfo[]; + proposal: ProposalInfo; + }) => { if (!initialSelectionDone) { initialSelectionDone = true; - votingNeuronSelectStore.set(votableNeurons()); + votingNeuronSelectStore.set( + votableNeurons({ + neurons, + proposal, + }) + ); } else { // preserve user selection after neurons update (e.g. queryAndUpdate second callback) - votingNeuronSelectStore.updateNeurons(votableNeurons()); + votingNeuronSelectStore.updateNeurons( + votableNeurons({ + neurons, + proposal, + }) + ); } }; - $: $definedNeuronsStore, updateVotingNeuronSelectedStore(); + $: updateVotingNeuronSelectedStore({ + neurons: $definedNeuronsStore, + proposal: proposalInfo, + }); const { store } = getContext( SELECTED_PROPOSAL_CONTEXT_KEY 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 f57e8298beb..7f73b3eaf0c 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 @@ -32,7 +32,10 @@ describe("VotingCard", () => { const neuronIds = [111, 222].map(BigInt); const proposalInfo: ProposalInfo = { ...mockProposalInfo, - ballots: neuronIds.map((neuronId) => ({ neuronId }) as Ballot), + ballots: neuronIds.map( + (neuronId) => + ({ neuronId, vote: Vote.Unspecified, votingPower: 1n }) as Ballot + ), proposalTimestampSeconds: 2_000n, status: ProposalStatus.Open, }; diff --git a/frontend/src/tests/lib/pages/NnsProposalDetail.spec.ts b/frontend/src/tests/lib/pages/NnsProposalDetail.spec.ts index 07fa507d20d..0343f1538d3 100644 --- a/frontend/src/tests/lib/pages/NnsProposalDetail.spec.ts +++ b/frontend/src/tests/lib/pages/NnsProposalDetail.spec.ts @@ -1,33 +1,75 @@ import { resetNeuronsApiService } from "$lib/api-services/governance.api-service"; import * as governanceApi from "$lib/api/governance.api"; import * as proposalsApi from "$lib/api/proposals.api"; +import { OWN_CANISTER_ID_TEXT } from "$lib/constants/canister-ids.constants"; +import { AppPath } from "$lib/constants/routes.constants"; import NnsProposalDetail from "$lib/pages/NnsProposalDetail.svelte"; import { actionableProposalsSegmentStore } from "$lib/stores/actionable-proposals-segment.store"; +import { page } from "$mocks/$app/stores"; import { mockIdentity, resetIdentity, setNoIdentity, } from "$tests/mocks/auth.store.mock"; +import { mockNeuron } from "$tests/mocks/neurons.mock"; import { mockProposalInfo } from "$tests/mocks/proposal.mock"; import { NnsProposalPo } from "$tests/page-objects/NnsProposal.page-object"; import { JestPageObjectElement } from "$tests/page-objects/jest.page-object"; import { runResolvedPromises } from "$tests/utils/timers.test-utils"; import { toastsStore } from "@dfinity/gix-components"; +import { ProposalRewardStatus, Vote, type NeuronInfo } from "@dfinity/nns"; import { render } from "@testing-library/svelte"; import { get } from "svelte/store"; vi.mock("$lib/api/governance.api"); describe("NnsProposalDetail", () => { + const neuronId1 = 0n; + const neuronId2 = 1n; + const testNeurons = [ + { + ...mockNeuron, + neuronId: neuronId1, + }, + { + ...mockNeuron, + neuronId: neuronId2, + }, + ] as NeuronInfo[]; + const testProposal = { + ...mockProposalInfo, + rewardStatus: ProposalRewardStatus.AcceptVotes, + ballots: [ + { + neuronId: BigInt(0), + vote: Vote.Unspecified, + votingPower: BigInt(1), + }, + { + neuronId: BigInt(1), + vote: Vote.Unspecified, + votingPower: BigInt(1), + }, + ], + }; + beforeEach(() => { resetIdentity(); vi.restoreAllMocks(); resetNeuronsApiService(); toastsStore.reset(); - vi.spyOn(governanceApi, "queryNeurons").mockResolvedValue([]); + vi.spyOn(governanceApi, "queryNeurons").mockResolvedValue(testNeurons); actionableProposalsSegmentStore.set("all"); - vi.spyOn(proposalsApi, "queryProposal").mockResolvedValue(mockProposalInfo); + vi.spyOn(proposalsApi, "queryProposal").mockResolvedValue(testProposal); + vi.spyOn(proposalsApi, "queryProposals").mockResolvedValue([testProposal]); // actionable proposals update + page.mock({ + routeId: AppPath.Proposal, + data: { + universe: OWN_CANISTER_ID_TEXT, + proposal: `${testProposal.id}`, + }, + }); }); const props = { @@ -92,6 +134,71 @@ describe("NnsProposalDetail", () => { }); expect(governanceApi.queryNeurons).toHaveBeenCalledTimes(2); }); + + it("should update votable neurons after voting", async () => { + let beforeVoting = true; + const spyOnQueryProposal = vi + .spyOn(proposalsApi, "queryProposal") + .mockImplementation(() => + Promise.resolve( + beforeVoting + ? testProposal + : { + ...testProposal, + ballots: [ + { + neuronId: neuronId1, + vote: Vote.Yes, + votingPower: BigInt(1), + }, + { + neuronId: neuronId2, + vote: Vote.Yes, + votingPower: BigInt(1), + }, + ], + } + ) + ); + + const po = renderComponent(); + const votingCardPo = po.getVotingCardPo(); + await runResolvedPromises(); + + expect(await votingCardPo.isPresent()).toBe(true); + expect( + await po.getVotingCardPo().getVotingNeuronSelectListPo().isPresent() + ).toBe(true); + expect(await votingCardPo.getVoteYesButtonPo().isDisabled()).toBe(false); + expect(await votingCardPo.getVoteNoButtonPo().isDisabled()).toBe(false); + + const votingNeuronListItemPos = await po + .getVotingCardPo() + .getVotingNeuronSelectListPo() + .getVotingNeuronListItemPos(); + expect(votingNeuronListItemPos.length).toBe(testNeurons.length); + expect(await votingNeuronListItemPos[0].getNeuronId()).toBe( + `${neuronId1}` + ); + expect(await votingNeuronListItemPos[1].getNeuronId()).toBe( + `${neuronId2}` + ); + expect(spyOnQueryProposal).toBeCalledTimes(2); + + beforeVoting = false; + await votingCardPo.voteYes(); + await runResolvedPromises(); + + expect(spyOnQueryProposal).toBeCalledTimes(3); + expect( + ( + await po + .getVotingCardPo() + .getVotingNeuronSelectListPo() + .getVotingNeuronListItemPos() + ).length + ).toBe(0); + }); }); describe("logged out user", () => { diff --git a/frontend/src/tests/lib/services/actionable-proposals.services.spec.ts b/frontend/src/tests/lib/services/actionable-proposals.services.spec.ts index bc3af0b3a42..f7789386bc6 100644 --- a/frontend/src/tests/lib/services/actionable-proposals.services.spec.ts +++ b/frontend/src/tests/lib/services/actionable-proposals.services.spec.ts @@ -67,16 +67,37 @@ describe("actionable-proposals.services", () => { }; const votableProposal: ProposalInfo = { ...mockProposalInfo, + ballots: [ + { + neuronId, + vote: Vote.Unspecified, + votingPower: 1n, + }, + ], id: 0n, }; const votedProposal: ProposalInfo = { ...mockProposalInfo, + ballots: [ + { + neuronId, + vote: Vote.Yes, + votingPower: 1n, + }, + ], id: votedProposalId, }; const fiveHundredsProposal = Array.from(Array(500)) .map((_, index) => ({ ...mockProposalInfo, id: BigInt(index), + ballots: [ + { + neuronId, + vote: Vote.Unspecified, + votingPower: 1n, + }, + ], })) .reverse(); let spyQueryProposals; @@ -227,14 +248,35 @@ describe("actionable-proposals.services", () => { const proposal0 = { ...mockProposalInfo, id: 0n, + ballots: [ + { + neuronId, + vote: Vote.Unspecified, + votingPower: 1n, + }, + ], } as ProposalInfo; const proposal1 = { ...mockProposalInfo, id: 1n, + ballots: [ + { + neuronId, + vote: Vote.Unspecified, + votingPower: 1n, + }, + ], } as ProposalInfo; const proposal2 = { ...mockProposalInfo, id: 2n, + ballots: [ + { + neuronId, + vote: Vote.Unspecified, + votingPower: 1n, + }, + ], } as ProposalInfo; it("should query list proposals also with ManageNeurons payload", async () => { diff --git a/frontend/src/tests/lib/services/vote-registration.services.spec.ts b/frontend/src/tests/lib/services/vote-registration.services.spec.ts index 13941b77158..c4ad8a5aa4f 100644 --- a/frontend/src/tests/lib/services/vote-registration.services.spec.ts +++ b/frontend/src/tests/lib/services/vote-registration.services.spec.ts @@ -59,6 +59,11 @@ describe("vote-registration-services", () => { const votableProposal: ProposalInfo = { ...mockProposalInfo, id: 0n, + ballots: [ + { neuronId: 0n, vote: Vote.Unspecified, votingPower: 1n }, + { neuronId: 1n, vote: Vote.Unspecified, votingPower: 1n }, + { neuronId: 2n, vote: Vote.Unspecified, votingPower: 1n }, + ], }; let resolveSpyQueryProposals; const spyQueryProposals = vi diff --git a/frontend/src/tests/page-objects/VotingCard.page-object.ts b/frontend/src/tests/page-objects/VotingCard.page-object.ts index 38b25ca29ef..18db44b8b37 100644 --- a/frontend/src/tests/page-objects/VotingCard.page-object.ts +++ b/frontend/src/tests/page-objects/VotingCard.page-object.ts @@ -3,6 +3,7 @@ import { StakeNeuronToVotePo } from "$tests/page-objects/StakeNeuronToVote.page- import { VotingConfirmationToolbarPo } from "$tests/page-objects/VotingConfirmationToolbar.page-object"; import { BasePageObject } from "$tests/page-objects/base.page-object"; import type { PageObjectElement } from "$tests/types/page-object.types"; +import { VotingNeuronSelectListPo } from "./VotingNeuronSelectList.page-object"; export class VotingCardPo extends BasePageObject { private static readonly TID = "voting-card-component"; @@ -35,6 +36,10 @@ export class VotingCardPo extends BasePageObject { return this.root.byTestId("votable-neurons"); } + getVotingNeuronSelectListPo(): VotingNeuronSelectListPo { + return VotingNeuronSelectListPo.under(this.root); + } + getVotedNeurons(): PageObjectElement { return this.root.byTestId("voted-neurons"); }