From 48f571ed187b854fb884c77eb196244358b5e172 Mon Sep 17 00:00:00 2001 From: Kieran Hall Date: Mon, 23 Sep 2024 09:12:24 +0200 Subject: [PATCH] Introduce Stepper and allocation to Send flow Resolves #2110, #2420. --- web-wallet/CHANGELOG.md | 11 +- .../lib/components/Allocate/Allocate.svelte | 6 +- .../ContractStatusesList.svelte | 46 +- .../OperationResult/OperationResult.svelte | 5 +- .../src/lib/components/Send/Send.svelte | 126 +++-- .../src/lib/components/__tests__/Send.spec.js | 151 +++--- .../ContractStatusesList.spec.js.snap | 176 +++---- .../__tests__/__snapshots__/Send.spec.js.snap | 432 +++++++++++++----- .../__snapshots__/Stake.spec.js.snap | 333 +++++++------- .../dusk/components/Stepper/Stepper.svelte | 16 +- .../lib/dusk/components/Wizard/Wizard.svelte | 1 + .../__snapshots__/Stepper.spec.js.snap | 90 ++-- .../__tests__/__snapshots__/page.spec.js.snap | 1 + .../routes/(app)/dashboard/send/+page.svelte | 2 + .../__tests__/__snapshots__/page.spec.js.snap | 212 ++++----- .../dashboard/send/__tests__/page.spec.js | 10 + .../__tests__/__snapshots__/page.spec.js.snap | 11 + .../__tests__/__snapshots__/page.spec.js.snap | 1 + .../src/style/dusk-components/stepper.css | 4 + 19 files changed, 985 insertions(+), 649 deletions(-) diff --git a/web-wallet/CHANGELOG.md b/web-wallet/CHANGELOG.md index 12001ffe7..97f4999d8 100644 --- a/web-wallet/CHANGELOG.md +++ b/web-wallet/CHANGELOG.md @@ -18,7 +18,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Receive screen design updated, added UI support for displaying shielded/unshielded address [#2421] - Restrict mnemonic step input to alphabetical characters (Restore Flow) [#2355] - Newly created Wallet does not sync from genesis [#1567] - Update font-display to swap for custom fonts to improve performance [#2026] @@ -29,7 +28,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update dashboard to use routes instead of `Tabs` for navigation pattern [#2075] - Update dashboard by splitting the transfer operations into send and receive operations [#2175] - Update decimals shown for migration balance [#2406] -- Make address field only vertically resizable [#2435] +- Update `Balance` UI to include an optional `UsageIndicator` for Moonlight tokens [#2234] +- Receive screen design updated, added UI support for displaying shielded/unshielded address [#2421] +- Update `Send` to use `Stepper` [#2110] +- Update `Send` to include allocation button [#2420] ### Fixed @@ -262,8 +264,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#2310]: https://github.com/dusk-network/rusk/issues/2310 [#2355]: https://github.com/dusk-network/rusk/issues/2355 [#2406]: https://github.com/dusk-network/rusk/issues/2406 -[#2421]: https://github.com/dusk-network/rusk/issues/2421 -[#2421]: https://github.com/dusk-network/rusk/issues/2435 +[#2234]: https://github.com/dusk-network/rusk/issues/2234 +[#2110]: https://github.com/dusk-network/rusk/issues/2110 +[#2420]: https://github.com/dusk-network/rusk/issues/2420 diff --git a/web-wallet/src/lib/components/Allocate/Allocate.svelte b/web-wallet/src/lib/components/Allocate/Allocate.svelte index 53af14e37..bf5fc1c26 100644 --- a/web-wallet/src/lib/components/Allocate/Allocate.svelte +++ b/web-wallet/src/lib/components/Allocate/Allocate.svelte @@ -2,7 +2,7 @@ -
- {#each items as status (status.label)} -
- {status.label} -
-
- {status.value} - -
- {/each} -
+
+
+ {#each items as status (status.label)} +
+ {status.label} +
+
+ {status.value} + +
+ {/each} +
+ +
diff --git a/web-wallet/src/lib/components/__tests__/Send.spec.js b/web-wallet/src/lib/components/__tests__/Send.spec.js index c13d1a2d6..d3f04b350 100644 --- a/web-wallet/src/lib/components/__tests__/Send.spec.js +++ b/web-wallet/src/lib/components/__tests__/Send.spec.js @@ -7,6 +7,16 @@ import { getAsHTMLElement } from "$lib/dusk/test-helpers"; import { Send } from ".."; import { tick } from "svelte"; +vi.mock("$lib/dusk/string", async (importOriginal) => { + /** @type {typeof import("$lib/dusk/string")} */ + const original = await importOriginal(); + + return { + ...original, + randomUUID: () => "some-generated-id", + }; +}); + describe("Send", () => { const formatter = createCurrencyFormatter("en", "DUSK", 9); const lastTxId = "some-id"; @@ -42,13 +52,45 @@ describe("Send", () => { baseProps.execute.mockClear(); }); - describe("Amount step", () => { - it("should render the Send component Amount step", () => { + describe("Address step", () => { + it("should render the Send component Address step", () => { const { container } = render(Send, baseProps); expect(container.firstChild).toMatchSnapshot(); }); + it("should disable the next button if the address is empty", () => { + const { getByRole } = render(Send, baseProps); + const nextButton = getByRole("button", { name: "Next" }); + const addressInput = getByRole("textbox"); + + expect(addressInput).toHaveValue(""); + expect(nextButton).toBeDisabled(); + }); + + it("should disable the next button if the address is invalid empty", async () => { + const { getByRole } = render(Send, baseProps); + const nextButton = getByRole("button", { name: "Next" }); + const addressInput = getByRole("textbox"); + + await fireEvent.input(addressInput, { + target: { value: invalidAddress }, + }); + + expect(addressInput).toHaveValue(invalidAddress); + expect(nextButton).toBeDisabled(); + }); + }); + + describe("Amount step", () => { + it("should render the Send component Amount step", async () => { + const { container, getByRole } = render(Send, baseProps); + + await fireEvent.click(getByRole("button", { name: "Next" })); + + expect(container.firstChild).toMatchSnapshot(); + }); + it("should disable the next button if the amount is invalid on mount", async () => { const props = { ...baseProps, @@ -59,9 +101,13 @@ describe("Send", () => { }, }; const { getByRole } = render(Send, props); + + await fireEvent.click(getByRole("button", { name: "Next" })); + const next = getByRole("button", { name: "Next" }); await tick(); + expect(next).toBeDisabled(); }); @@ -71,6 +117,9 @@ describe("Send", () => { baseProps.gasSettings.gasPrice * baseProps.gasSettings.gasLimit ); const { getByRole } = render(Send, baseProps); + + await fireEvent.click(getByRole("button", { name: "Next" })); + const useMaxButton = getByRole("button", { name: "USE MAX" }); const nextButton = getByRole("button", { name: "Next" }); const amountInput = getByRole("spinbutton"); @@ -88,6 +137,8 @@ describe("Send", () => { }; const { getByRole } = render(Send, props); + await fireEvent.click(getByRole("button", { name: "Next" })); + const useMaxButton = getByRole("button", { name: "USE MAX" }); const amountInput = getByRole("spinbutton"); @@ -109,6 +160,9 @@ describe("Send", () => { }; const { getByRole } = render(Send, props); + + await fireEvent.click(getByRole("button", { name: "Next" })); + const useMaxButton = getByRole("button", { name: "USE MAX" }); const amountInput = getByRole("spinbutton"); @@ -121,6 +175,9 @@ describe("Send", () => { it("should disable the next button if the user enters an invalid amount", async () => { const { getByRole } = render(Send, baseProps); + + await fireEvent.click(getByRole("button", { name: "Next" })); + const nextButton = getByRole("button", { name: "Next" }); const amountInput = getByRole("spinbutton"); @@ -133,74 +190,18 @@ describe("Send", () => { }); }); - describe("Address step", () => { - it("should render the Send component Address step", async () => { - const { container, getByRole } = render(Send, baseProps); - const nextButton = getByRole("button", { name: "Next" }); - - await fireEvent.click(nextButton); - - expect(container.firstChild).toMatchSnapshot(); - }); - - it("should disable the next button if the address is empty", async () => { - const { getByRole } = render(Send, baseProps); - - await fireEvent.click(getByRole("button", { name: "Next" })); - - const nextButton = getByRole("button", { name: "Next" }); - const addressInput = getByRole("textbox"); - - expect(addressInput).toHaveValue(""); - expect(nextButton).toBeDisabled(); - }); - - it("should disable the next button if the address is invalid empty", async () => { - const { getByRole } = render(Send, baseProps); - - await fireEvent.click(getByRole("button", { name: "Next" })); - - const nextButton = getByRole("button", { name: "Next" }); - const addressInput = getByRole("textbox"); - - await fireEvent.input(addressInput, { - target: { value: invalidAddress }, - }); - - expect(addressInput).toHaveValue(invalidAddress); - - expect(nextButton).toBeDisabled(); - }); - - it("should enable the next button if the user inputs a valid address", async () => { - const { getByRole } = render(Send, baseProps); - - await fireEvent.click(getByRole("button", { name: "Next" })); - - const nextButton = getByRole("button", { name: "Next" }); - const addressInput = getByRole("textbox"); - - expect(nextButton).toBeDisabled(); - - await fireEvent.input(addressInput, { target: { value: address } }); - - expect(addressInput).toHaveValue(address); - expect(nextButton).toBeEnabled(); - }); - }); - describe("Review step", () => { it("should render the Send component Review step", async () => { const amount = 2345; const { container, getByRole } = render(Send, baseProps); - const amountInput = getByRole("spinbutton"); + const addressInput = getByRole("textbox"); - await fireEvent.input(amountInput, { target: { value: amount } }); + await fireEvent.input(addressInput, { target: { value: address } }); await fireEvent.click(getByRole("button", { name: "Next" })); - const addressInput = getByRole("textbox"); + const amountInput = getByRole("spinbutton"); - await fireEvent.input(addressInput, { target: { value: address } }); + await fireEvent.input(amountInput, { target: { value: amount } }); await fireEvent.click(getByRole("button", { name: "Next" })); const value = getAsHTMLElement( @@ -230,14 +231,14 @@ describe("Send", () => { it("should perform a transfer for the desired amount, give a success message and supply a link to see the transaction in the explorer", async () => { const { getByRole, getByText } = render(Send, baseProps); - const amountInput = getByRole("spinbutton"); + const addressInput = getByRole("textbox"); - await fireEvent.input(amountInput, { target: { value: amount } }); + await fireEvent.input(addressInput, { target: { value: address } }); await fireEvent.click(getByRole("button", { name: "Next" })); - const input = getByRole("textbox"); + const amountInput = getByRole("spinbutton"); - await fireEvent.input(input, { target: { value: address } }); + await fireEvent.input(amountInput, { target: { value: amount } }); await fireEvent.click(getByRole("button", { name: "Next" })); await fireEvent.click(getByRole("button", { name: "SEND" })); @@ -264,17 +265,16 @@ describe("Send", () => { baseProps.execute.mockRejectedValueOnce(new Error(errorMessage)); const { getByRole, getByText } = render(Send, baseProps); - const amountInput = getByRole("spinbutton"); + const addressInput = getByRole("textbox"); - await fireEvent.input(amountInput, { target: { value: amount } }); + await fireEvent.input(addressInput, { target: { value: address } }); await fireEvent.click(getByRole("button", { name: "Next" })); - const input = getByRole("textbox"); + const amountInput = getByRole("spinbutton"); - await fireEvent.input(input, { target: { value: address } }); + await fireEvent.input(amountInput, { target: { value: amount } }); await fireEvent.click(getByRole("button", { name: "Next" })); await fireEvent.click(getByRole("button", { name: "SEND" })); - await vi.advanceTimersToNextTimerAsync(); expect(baseProps.execute).toHaveBeenCalledTimes(1); @@ -292,17 +292,16 @@ describe("Send", () => { baseProps.execute.mockResolvedValueOnce(void 0); const { getByRole, getByText } = render(Send, baseProps); - const amountInput = getByRole("spinbutton"); + const addressInput = getByRole("textbox"); - await fireEvent.input(amountInput, { target: { value: amount } }); + await fireEvent.input(addressInput, { target: { value: address } }); await fireEvent.click(getByRole("button", { name: "Next" })); - const input = getByRole("textbox"); + const amountInput = getByRole("spinbutton"); - await fireEvent.input(input, { target: { value: address } }); + await fireEvent.input(amountInput, { target: { value: amount } }); await fireEvent.click(getByRole("button", { name: "Next" })); await fireEvent.click(getByRole("button", { name: "SEND" })); - await vi.advanceTimersToNextTimerAsync(); expect(baseProps.execute).toHaveBeenCalledTimes(1); diff --git a/web-wallet/src/lib/components/__tests__/__snapshots__/ContractStatusesList.spec.js.snap b/web-wallet/src/lib/components/__tests__/__snapshots__/ContractStatusesList.spec.js.snap index 48f0e50f3..3656d48ba 100644 --- a/web-wallet/src/lib/components/__tests__/__snapshots__/ContractStatusesList.spec.js.snap +++ b/web-wallet/src/lib/components/__tests__/__snapshots__/ContractStatusesList.spec.js.snap @@ -1,95 +1,105 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`ContractStatusesList > should be able to render the component without items 1`] = ` -
+
+
+ +
`; exports[`ContractStatusesList > should render the \`ContractStatusesList\` component 1`] = ` -
-
- Spendable - -
-
- - 99,899.999724165 - - - - - - - -
-
- Total Locked - -
-
- - 1,000.000000000 - - - +
- - - - -
-
- Rewards - -
-
- - 99,288.000000000 - - - + 99,899.999724165 + + + + + + + +
+
+ Total Locked + +
+
+ + 1,000.000000000 + + + + + + + +
+
+ Rewards + +
+
- - - - -
-
+ + 99,288.000000000 + + + + + + + + +
+ + `; diff --git a/web-wallet/src/lib/components/__tests__/__snapshots__/Send.spec.js.snap b/web-wallet/src/lib/components/__tests__/__snapshots__/Send.spec.js.snap index 95a0de5aa..14c1cd4e3 100644 --- a/web-wallet/src/lib/components/__tests__/__snapshots__/Send.spec.js.snap +++ b/web-wallet/src/lib/components/__tests__/__snapshots__/Send.spec.js.snap @@ -2,53 +2,87 @@ exports[`Send > Address step > should render the Send component Address step 1`] = `
- - - -
-
-
+ 1 + + + - Spendable + Address -
-
+ - - 1,000.000000000 - + 2 + + + + Amount - - - - + + + 3 + + + + Review -
-
- + + + 4 + + + + Done + + + +
+ +
+ + + +

Enter address: @@ -69,7 +103,7 @@ exports[`Send > Address step > should render the Send component Address step 1`]