diff --git a/crates/js_api/src/gui/deposits.rs b/crates/js_api/src/gui/deposits.rs index c4a291f34..c86364c47 100644 --- a/crates/js_api/src/gui/deposits.rs +++ b/crates/js_api/src/gui/deposits.rs @@ -63,7 +63,7 @@ impl DotrainOrderGui { let gui_deposit = self.get_gui_deposit(&token)?; if amount.is_empty() { - self.remove_deposit(token); + self.remove_deposit(token)?; return Ok(()); } @@ -85,12 +85,16 @@ impl DotrainOrderGui { }; self.deposits.insert(token, value); + + self.execute_state_update_callback()?; Ok(()) } #[wasm_bindgen(js_name = "removeDeposit")] - pub fn remove_deposit(&mut self, token: String) { + pub fn remove_deposit(&mut self, token: String) -> Result<(), GuiError> { self.deposits.remove(&token); + self.execute_state_update_callback()?; + Ok(()) } #[wasm_bindgen(js_name = "getDepositPresets")] diff --git a/crates/js_api/src/gui/field_values.rs b/crates/js_api/src/gui/field_values.rs index d763840ca..9c12c2adc 100644 --- a/crates/js_api/src/gui/field_values.rs +++ b/crates/js_api/src/gui/field_values.rs @@ -42,6 +42,8 @@ impl DotrainOrderGui { } } self.field_values.insert(binding, value); + + self.execute_state_update_callback()?; Ok(()) } @@ -54,8 +56,10 @@ impl DotrainOrderGui { } #[wasm_bindgen(js_name = "removeFieldValue")] - pub fn remove_field_value(&mut self, binding: String) { + pub fn remove_field_value(&mut self, binding: String) -> Result<(), GuiError> { self.field_values.remove(&binding); + self.execute_state_update_callback()?; + Ok(()) } #[wasm_bindgen(js_name = "getFieldValue")] diff --git a/crates/js_api/src/gui/mod.rs b/crates/js_api/src/gui/mod.rs index 0f7e607ce..3a0b54de9 100644 --- a/crates/js_api/src/gui/mod.rs +++ b/crates/js_api/src/gui/mod.rs @@ -53,6 +53,8 @@ pub struct DotrainOrderGui { selected_deployment: String, field_values: BTreeMap, deposits: BTreeMap, + #[serde(skip)] + state_update_callback: Option, } #[wasm_bindgen] impl DotrainOrderGui { @@ -67,6 +69,7 @@ impl DotrainOrderGui { pub async fn choose_deployment( dotrain: String, deployment_name: String, + state_update_callback: Option, ) -> Result { let dotrain_order = DotrainOrder::new(dotrain, None).await?; @@ -80,6 +83,7 @@ impl DotrainOrderGui { selected_deployment: deployment_name.clone(), field_values: BTreeMap::new(), deposits: BTreeMap::new(), + state_update_callback, }) } @@ -235,6 +239,8 @@ pub enum GuiError { BindingHasNoPresets(String), #[error("Token not in select tokens: {0}")] TokenNotInSelectTokens(String), + #[error("JavaScript error: {0}")] + JsError(String), #[error(transparent)] DotrainOrderError(#[from] DotrainOrderError), #[error(transparent)] diff --git a/crates/js_api/src/gui/order_operations.rs b/crates/js_api/src/gui/order_operations.rs index b92344932..7a302f17b 100644 --- a/crates/js_api/src/gui/order_operations.rs +++ b/crates/js_api/src/gui/order_operations.rs @@ -343,6 +343,8 @@ impl DotrainOrderGui { .dotrain_yaml() .get_order(&deployment.deployment.order.key)? .update_vault_id(is_input, index, vault_id)?; + + self.execute_state_update_callback()?; Ok(()) } diff --git a/crates/js_api/src/gui/select_tokens.rs b/crates/js_api/src/gui/select_tokens.rs index beafda022..d5032687f 100644 --- a/crates/js_api/src/gui/select_tokens.rs +++ b/crates/js_api/src/gui/select_tokens.rs @@ -96,6 +96,8 @@ impl DotrainOrderGui { Some(&token_info.name), Some(&token_info.symbol), )?; + + self.execute_state_update_callback()?; Ok(()) } @@ -122,6 +124,8 @@ impl DotrainOrderGui { } Token::remove_record_from_yaml(self.dotrain_order.orderbook_yaml().documents, &key)?; + + self.execute_state_update_callback()?; Ok(()) } diff --git a/crates/js_api/src/gui/state_management.rs b/crates/js_api/src/gui/state_management.rs index 6518f55a9..61851df4d 100644 --- a/crates/js_api/src/gui/state_management.rs +++ b/crates/js_api/src/gui/state_management.rs @@ -151,6 +151,7 @@ impl DotrainOrderGui { pub async fn deserialize_state( dotrain: String, serialized: String, + state_update_callback: Option, ) -> Result { let compressed = URL_SAFE.decode(serialized)?; @@ -183,6 +184,7 @@ impl DotrainOrderGui { field_values, deposits, selected_deployment: state.selected_deployment.clone(), + state_update_callback, }; let deployment_select_tokens = Gui::parse_select_tokens( @@ -251,4 +253,15 @@ impl DotrainOrderGui { pub fn is_deposit_preset(&self, token: String) -> Option { self.is_preset(token, &self.deposits) } + + #[wasm_bindgen(js_name = "executeStateUpdateCallback")] + pub fn execute_state_update_callback(&self) -> Result<(), GuiError> { + if let Some(callback) = &self.state_update_callback { + let state = to_value(&self.serialize_state()?)?; + callback.call1(&JsValue::UNDEFINED, &state).map_err(|e| { + GuiError::JsError(format!("Failed to execute state update callback: {:?}", e)) + })?; + } + Ok(()) + } } diff --git a/packages/orderbook/test/js_api/gui.test.ts b/packages/orderbook/test/js_api/gui.test.ts index 88b1b19f7..952f1acde 100644 --- a/packages/orderbook/test/js_api/gui.test.ts +++ b/packages/orderbook/test/js_api/gui.test.ts @@ -1,5 +1,5 @@ import assert from 'assert'; -import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, beforeEach, describe, expect, it, Mock, vi } from 'vitest'; import { DotrainOrderGui } from '../../dist/cjs/js_api.js'; import { AddOrderCalldataResult, @@ -370,6 +370,18 @@ describe('Rain Orderbook JS API Package Bindgen Tests - Gui', async function () assert.equal(guiConfig.description, 'Fixed limit order strategy'); }); + it('should initialize gui object with state update callback', async () => { + const stateUpdateCallback = vi.fn(); + const gui = await DotrainOrderGui.chooseDeployment( + dotrainWithGui, + 'some-deployment', + stateUpdateCallback + ); + + gui.executeStateUpdateCallback(); + assert.equal(stateUpdateCallback.mock.calls.length, 1); + }); + it('should get strategy details', async () => { const strategyDetails: NameAndDescription = await DotrainOrderGui.getStrategyDetails(dotrainWithGui); @@ -435,14 +447,20 @@ describe('Rain Orderbook JS API Package Bindgen Tests - Gui', async function () describe('deposit tests', async () => { let gui: DotrainOrderGui; - beforeAll(async () => { + let stateUpdateCallback: Mock; + beforeEach(async () => { + stateUpdateCallback = vi.fn(); mockServer .forPost('/rpc-url') .withBodyIncluding('0x82ad56cb') .thenSendJsonRpcResult( '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000007546f6b656e203100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000025431000000000000000000000000000000000000000000000000000000000000' ); - gui = await DotrainOrderGui.chooseDeployment(dotrainWithGui, 'some-deployment'); + gui = await DotrainOrderGui.chooseDeployment( + dotrainWithGui, + 'some-deployment', + stateUpdateCallback + ); }); it('should add deposit', async () => { @@ -453,6 +471,9 @@ describe('Rain Orderbook JS API Package Bindgen Tests - Gui', async function () assert.equal(deposits.length, 1); assert.equal(gui.hasAnyDeposit(), true); + + assert.equal(stateUpdateCallback.mock.calls.length, 1); + expect(stateUpdateCallback).toHaveBeenCalledWith(gui.serializeState()); }); it('should update deposit', async () => { @@ -461,6 +482,9 @@ describe('Rain Orderbook JS API Package Bindgen Tests - Gui', async function () const deposits: TokenDeposit[] = gui.getDeposits(); assert.equal(deposits.length, 1); assert.equal(deposits[0].amount, '100.6'); + + assert.equal(stateUpdateCallback.mock.calls.length, 2); + expect(stateUpdateCallback).toHaveBeenCalledWith(gui.serializeState()); }); it('should throw error if deposit token is not found in gui config', () => { @@ -482,6 +506,9 @@ describe('Rain Orderbook JS API Package Bindgen Tests - Gui', async function () assert.equal(gui.getDeposits().length, 1); gui.saveDeposit('token1', ''); assert.equal(gui.getDeposits().length, 0); + + assert.equal(stateUpdateCallback.mock.calls.length, 4); + expect(stateUpdateCallback).toHaveBeenCalledWith(gui.serializeState()); }); it('should get deposit presets', async () => { @@ -503,14 +530,20 @@ describe('Rain Orderbook JS API Package Bindgen Tests - Gui', async function () describe('field value tests', async () => { let gui: DotrainOrderGui; - beforeAll(async () => { + let stateUpdateCallback: Mock; + beforeEach(async () => { + stateUpdateCallback = vi.fn(); mockServer .forPost('/rpc-url') .withBodyIncluding('0x82ad56cb') .thenSendJsonRpcResult( '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000007546f6b656e203100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000025431000000000000000000000000000000000000000000000000000000000000' ); - gui = await DotrainOrderGui.chooseDeployment(dotrainWithGui, 'some-deployment'); + gui = await DotrainOrderGui.chooseDeployment( + dotrainWithGui, + 'some-deployment', + stateUpdateCallback + ); }); it('should save the field value as presets', async () => { @@ -530,6 +563,9 @@ describe('Rain Orderbook JS API Package Bindgen Tests - Gui', async function () value: allFieldDefinitions[0].presets[2].id }); assert.deepEqual(gui.getFieldValue('binding-1'), allFieldDefinitions[0].presets[2]); + + assert.equal(stateUpdateCallback.mock.calls.length, 3); + expect(stateUpdateCallback).toHaveBeenCalledWith(gui.serializeState()); }); it('should save field value as custom values', async () => { @@ -583,6 +619,9 @@ describe('Rain Orderbook JS API Package Bindgen Tests - Gui', async function () value: 'true' } }); + + assert.equal(stateUpdateCallback.mock.calls.length, 4); + expect(stateUpdateCallback).toHaveBeenCalledWith(gui.serializeState()); }); it('should throw error during save if preset is not found in field definition', () => { @@ -1163,6 +1202,7 @@ ${dotrainWithoutVaultIds}`; }); it('should set vault ids', async () => { + let stateUpdateCallback = vi.fn(); mockServer .forPost('/rpc-url') .withBodyIncluding('0x82ad56cb') @@ -1175,7 +1215,11 @@ ${dotrainWithoutVaultIds}`; ${dotrainWithoutVaultIds} `; - gui = await DotrainOrderGui.chooseDeployment(testDotrain, 'other-deployment'); + gui = await DotrainOrderGui.chooseDeployment( + testDotrain, + 'other-deployment', + stateUpdateCallback + ); let currentDeployment: GuiDeployment = gui.getCurrentDeployment(); assert.equal(currentDeployment.deployment.order.inputs[0].vaultId, undefined); @@ -1211,6 +1255,9 @@ ${dotrainWithoutVaultIds}`; expect(() => gui.setVaultId(true, 0, 'test')).toThrow( "Invalid value for field 'vault-id': Failed to parse vault id in index '0' of inputs in order 'some-order'" ); + + assert.equal(stateUpdateCallback.mock.calls.length, 4); + expect(stateUpdateCallback).toHaveBeenCalledWith(gui.serializeState()); }); it('should skip deposits with zero amount for deposit calldata', async () => { @@ -1294,13 +1341,19 @@ ${dotrainWithoutVaultIds}`; describe('select tokens tests', async () => { let gui: DotrainOrderGui; - beforeAll(async () => { + let stateUpdateCallback: Mock; + beforeEach(async () => { + stateUpdateCallback = vi.fn(); let dotrain3 = ` ${guiConfig3} ${dotrainWithoutTokens} `; - gui = await DotrainOrderGui.chooseDeployment(dotrain3, 'other-deployment'); + gui = await DotrainOrderGui.chooseDeployment( + dotrain3, + 'other-deployment', + stateUpdateCallback + ); }); it('should get select tokens', async () => { @@ -1376,12 +1429,12 @@ ${dotrainWithoutVaultIds}`; assert.equal(tokenInfo.name, 'Teken 2'); assert.equal(tokenInfo.symbol, 'T2'); assert.equal(tokenInfo.decimals, 18); + + assert.equal(stateUpdateCallback.mock.calls.length, 2); + expect(stateUpdateCallback).toHaveBeenCalledWith(gui.serializeState()); }); it('should replace select token', async () => { - gui.removeSelectToken('token1'); - gui.removeSelectToken('token2'); - mockServer .forPost('/rpc-url') .once() @@ -1410,15 +1463,23 @@ ${dotrainWithoutVaultIds}`; assert.equal(tokenInfo.name, 'Teken 2'); assert.equal(tokenInfo.symbol, 'T2'); assert.equal(tokenInfo.decimals, 18); + + assert.equal(stateUpdateCallback.mock.calls.length, 3); + expect(stateUpdateCallback).toHaveBeenCalledWith(gui.serializeState()); }); it('should remove select token', async () => { + stateUpdateCallback = vi.fn(); let dotrain3 = ` ${guiConfig3} ${dotrainWithoutTokens} `; - gui = await DotrainOrderGui.chooseDeployment(dotrain3, 'other-deployment'); + gui = await DotrainOrderGui.chooseDeployment( + dotrain3, + 'other-deployment', + stateUpdateCallback + ); mockServer .forPost('/rpc-url') @@ -1447,6 +1508,9 @@ ${dotrainWithoutVaultIds}`; await expect(async () => await gui.getTokenInfo('token1')).rejects.toThrow( "Missing required field 'tokens' in root" ); + + assert.equal(stateUpdateCallback.mock.calls.length, 2); + expect(stateUpdateCallback).toHaveBeenCalledWith(gui.serializeState()); }); it('should get network key', async () => { diff --git a/packages/ui-components/src/__tests__/DeploymentSteps.test.ts b/packages/ui-components/src/__tests__/DeploymentSteps.test.ts index 59638aca4..20e022f8e 100644 --- a/packages/ui-components/src/__tests__/DeploymentSteps.test.ts +++ b/packages/ui-components/src/__tests__/DeploymentSteps.test.ts @@ -630,7 +630,7 @@ const defaultProps: DeploymentStepsProps = { handleDeployModal: vi.fn() as unknown as (args: DeployModalProps) => void, handleDisclaimerModal: vi.fn() as unknown as (args: DisclaimerModalProps) => void, settings: writable({} as ConfigSource), - handleUpdateGuiState: vi.fn() + pushGuiStateToUrlHistory: vi.fn() }; describe('DeploymentSteps', () => { diff --git a/packages/ui-components/src/__tests__/DepositInput.test.ts b/packages/ui-components/src/__tests__/DepositInput.test.ts index cf9e72b67..a30d051d9 100644 --- a/packages/ui-components/src/__tests__/DepositInput.test.ts +++ b/packages/ui-components/src/__tests__/DepositInput.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'; import { render, fireEvent, waitFor } from '@testing-library/svelte'; import DepositInput from '../lib/components/deployment/DepositInput.svelte'; import type { GuiDeposit } from '@rainlanguage/orderbook/js_api'; @@ -7,6 +7,8 @@ import type { ComponentProps } from 'svelte'; type DepositInputProps = ComponentProps; describe('DepositInput', () => { + let mockStateUpdateCallback: Mock; + const mockGui = { getTokenInfo: vi.fn(), isDepositPreset: vi.fn(), @@ -20,6 +22,10 @@ describe('DepositInput', () => { } as unknown as GuiDeposit; beforeEach(() => { + mockStateUpdateCallback = vi.fn(); + mockGui.saveDeposit.mockImplementation(() => { + mockStateUpdateCallback(); + }); vi.clearAllMocks(); }); @@ -30,8 +36,7 @@ describe('DepositInput', () => { gui: { ...mockGui, getTokenInfo: vi.fn().mockReturnValue({ name: 'Test Token', symbol: 'TEST' }) - }, - handleUpdateGuiState: vi.fn() + } } as unknown as DepositInputProps }); await waitFor(() => { @@ -46,8 +51,7 @@ describe('DepositInput', () => { const { getByText } = render(DepositInput, { props: { deposit: mockDeposit, - gui: mockGui, - handleUpdateGuiState: vi.fn() + gui: mockGui } as unknown as DepositInputProps }); @@ -55,14 +59,14 @@ describe('DepositInput', () => { expect(mockGui.saveDeposit).toHaveBeenCalledWith('TEST', '100'); }); - it('handles custom input changes', async () => { + it('handles custom input changes and triggers state update', async () => { mockGui.isDepositPreset.mockReturnValue(false); const { getByPlaceholderText } = render(DepositInput, { props: { deposit: mockDeposit, gui: mockGui, - handleUpdateGuiState: vi.fn() + onStateUpdate: mockStateUpdateCallback } as unknown as DepositInputProps }); @@ -70,5 +74,6 @@ describe('DepositInput', () => { await fireEvent.input(input, { target: { value: '150' } }); expect(mockGui.saveDeposit).toHaveBeenCalledWith('TEST', '150'); + expect(mockStateUpdateCallback).toHaveBeenCalled(); }); }); diff --git a/packages/ui-components/src/__tests__/FieldDefinitionInput.test.ts b/packages/ui-components/src/__tests__/FieldDefinitionInput.test.ts index 3c0560a62..f5273f2c6 100644 --- a/packages/ui-components/src/__tests__/FieldDefinitionInput.test.ts +++ b/packages/ui-components/src/__tests__/FieldDefinitionInput.test.ts @@ -1,5 +1,5 @@ import { render, fireEvent } from '@testing-library/svelte'; -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'; import FieldDefinitionInput from '../lib/components/deployment/FieldDefinitionInput.svelte'; import { DotrainOrderGui } from '@rainlanguage/orderbook/js_api'; @@ -15,6 +15,8 @@ vi.mock('@rainlanguage/orderbook/js_api', () => ({ describe('FieldDefinitionInput', () => { let mockGui: DotrainOrderGui; + let mockStateUpdateCallback: Mock; + const mockFieldDefinition = { binding: 'test-binding', name: 'Test Field', @@ -26,15 +28,18 @@ describe('FieldDefinitionInput', () => { }; beforeEach(() => { + mockStateUpdateCallback = vi.fn(); mockGui = new DotrainOrderGui(); + mockGui.saveFieldValue = vi.fn().mockImplementation(() => { + mockStateUpdateCallback(); + }); }); it('renders field name and description', () => { const { getByText } = render(FieldDefinitionInput, { props: { fieldDefinition: mockFieldDefinition, - gui: mockGui, - handleUpdateGuiState: vi.fn() + gui: mockGui } }); @@ -46,8 +51,7 @@ describe('FieldDefinitionInput', () => { const { getByText } = render(FieldDefinitionInput, { props: { fieldDefinition: mockFieldDefinition, - gui: mockGui, - handleUpdateGuiState: vi.fn() + gui: mockGui } }); @@ -55,12 +59,11 @@ describe('FieldDefinitionInput', () => { expect(getByText('Preset 2')).toBeTruthy(); }); - it('handles preset button clicks', async () => { + it('handles preset button clicks and triggers state update', async () => { const { getByText } = render(FieldDefinitionInput, { props: { fieldDefinition: mockFieldDefinition, - gui: mockGui, - handleUpdateGuiState: vi.fn() + gui: mockGui } }); @@ -70,14 +73,14 @@ describe('FieldDefinitionInput', () => { isPreset: true, value: 'preset1' }); + expect(mockStateUpdateCallback).toHaveBeenCalled(); }); - it('handles custom input changes', async () => { + it('handles custom input changes and triggers state update', async () => { const { getByPlaceholderText } = render(FieldDefinitionInput, { props: { fieldDefinition: mockFieldDefinition, - gui: mockGui, - handleUpdateGuiState: vi.fn() + gui: mockGui } }); @@ -88,6 +91,7 @@ describe('FieldDefinitionInput', () => { isPreset: false, value: 'custom value' }); + expect(mockStateUpdateCallback).toHaveBeenCalled(); }); it('does not show Custom button for is-fast-exit binding', () => { @@ -99,8 +103,7 @@ describe('FieldDefinitionInput', () => { const { queryByText } = render(FieldDefinitionInput, { props: { fieldDefinition: fastExitFieldDef, - gui: mockGui, - handleUpdateGuiState: vi.fn() + gui: mockGui } }); diff --git a/packages/ui-components/src/__tests__/SelectToken.test.ts b/packages/ui-components/src/__tests__/SelectToken.test.ts index da76e4b9a..cf6366bd1 100644 --- a/packages/ui-components/src/__tests__/SelectToken.test.ts +++ b/packages/ui-components/src/__tests__/SelectToken.test.ts @@ -1,12 +1,13 @@ import { render, waitFor } from '@testing-library/svelte'; import userEvent from '@testing-library/user-event'; -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'; import SelectToken from '../lib/components/deployment/SelectToken.svelte'; import type { ComponentProps } from 'svelte'; import type { DotrainOrderGui } from '@rainlanguage/orderbook/js_api'; export type SelectTokenComponentProps = ComponentProps; describe('SelectToken', () => { + let mockStateUpdateCallback: Mock; const mockGui: DotrainOrderGui = { saveSelectToken: vi.fn(), replaceSelectToken: vi.fn(), @@ -25,10 +26,20 @@ describe('SelectToken', () => { name: 'test input', description: 'test description' }, - handleUpdateGuiState: vi.fn() + onSelectTokenSelect: vi.fn() }; beforeEach(() => { + mockStateUpdateCallback = vi.fn(); + mockGui.saveSelectToken = vi.fn().mockImplementation(() => { + mockStateUpdateCallback(); + return Promise.resolve(); + }); + mockGui.replaceSelectToken = vi.fn().mockImplementation(() => { + mockStateUpdateCallback(); + mockStateUpdateCallback(); + return Promise.resolve(); + }); vi.clearAllMocks(); }); @@ -55,11 +66,12 @@ describe('SelectToken', () => { const input = getByRole('textbox'); await userEvent.clear(input); - await user.type(input, '0x456'); + await user.paste('0x456'); await waitFor(() => { expect(mockGui.saveSelectToken).toHaveBeenCalledWith('input', '0x456'); }); + expect(mockStateUpdateCallback).toHaveBeenCalledTimes(1); }); it('shows error message for invalid address, and removes the selectToken', async () => { @@ -75,7 +87,8 @@ describe('SelectToken', () => { }); const input = screen.getByRole('textbox'); - await user.type(input, 'invalid'); + await userEvent.clear(input); + await user.paste('invalid'); await waitFor(() => { expect(screen.getByTestId('error')).toBeInTheDocument(); }); @@ -89,14 +102,15 @@ describe('SelectToken', () => { } as unknown as SelectTokenComponentProps); const input = getByRole('textbox'); - await user.type(input, '0x456'); + await userEvent.clear(input); + await user.paste('0x456'); await waitFor(() => { expect(mockGui.saveSelectToken).not.toHaveBeenCalled(); }); }); - it('replaces the token if the token is already set', async () => { + it('replaces the token and triggers state update twice if the token is already set', async () => { const mockGuiWithTokenSet = { ...mockGui, isSelectTokenSet: vi.fn().mockResolvedValue(true) @@ -110,9 +124,11 @@ describe('SelectToken', () => { }); const input = getByRole('textbox'); - await user.type(input, 'invalid'); + await userEvent.clear(input); + await user.paste('invalid'); await waitFor(() => { expect(mockGui.replaceSelectToken).toHaveBeenCalled(); + expect(mockStateUpdateCallback).toHaveBeenCalledTimes(2); }); }); }); diff --git a/packages/ui-components/src/__tests__/TokenIOInput.test.ts b/packages/ui-components/src/__tests__/TokenIOInput.test.ts index 523b855ef..9c25429ff 100644 --- a/packages/ui-components/src/__tests__/TokenIOInput.test.ts +++ b/packages/ui-components/src/__tests__/TokenIOInput.test.ts @@ -1,11 +1,13 @@ import { render, fireEvent, waitFor } from '@testing-library/svelte'; -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'; import TokenIOInput from '../lib/components/deployment/TokenIOInput.svelte'; import type { ComponentProps } from 'svelte'; export type TokenIOInputComponentProps = ComponentProps; describe('TokenInput', () => { + let mockStateUpdateCallback: Mock; + const mockInput = { token: { address: '0x123', @@ -40,20 +42,22 @@ describe('TokenInput', () => { i: 0, label: 'Input', vault: mockInput, - gui: mockGui, - handleUpdateGuiState: vi.fn() + gui: mockGui } as unknown as TokenIOInputComponentProps; const outputMockProps: TokenIOInputComponentProps = { i: 0, label: 'Output', vault: mockInput, - gui: mockGui, - handleUpdateGuiState: vi.fn() + gui: mockGui } as unknown as TokenIOInputComponentProps; beforeEach(() => { vi.clearAllMocks(); + mockStateUpdateCallback = vi.fn(); + mockGui.setVaultId.mockImplementation(() => { + mockStateUpdateCallback(); + }); mockGui.getTokenInfo = vi.fn().mockResolvedValue(mockTokenInfo); }); @@ -79,12 +83,14 @@ describe('TokenInput', () => { const input = render(TokenIOInput, mockProps).getByPlaceholderText('Enter vault ID'); await fireEvent.input(input, { target: { value: 'vault1' } }); expect(mockGui.setVaultId).toHaveBeenCalledWith(true, 0, 'vault1'); + expect(mockStateUpdateCallback).toHaveBeenCalledTimes(1); }); it('calls setVaultId on output vault when input changes', async () => { const input = render(TokenIOInput, outputMockProps).getByPlaceholderText('Enter vault ID'); await fireEvent.input(input, { target: { value: 'vault2' } }); expect(mockGui.setVaultId).toHaveBeenCalledWith(false, 0, 'vault2'); + expect(mockStateUpdateCallback).toHaveBeenCalledTimes(1); }); it('does not call setVaultId when gui is undefined', async () => { diff --git a/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte b/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte index d9540ad5b..1cfa21215 100644 --- a/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte +++ b/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte @@ -49,7 +49,7 @@ export let handleDeployModal: (args: DeployModalProps) => void; export let handleDisclaimerModal: (args: DisclaimerModalProps) => void; - export let handleUpdateGuiState: (gui: DotrainOrderGui) => void; + export let pushGuiStateToUrlHistory: (serializedState: string) => void; let selectTokens: SelectTokens | null = null; let allDepositFields: GuiDeposit[] = []; @@ -78,7 +78,7 @@ errorDetails = null; try { - gui = await DotrainOrderGui.chooseDeployment(dotrain, deployment); + gui = await DotrainOrderGui.chooseDeployment(dotrain, deployment, pushGuiStateToUrlHistory); if (gui) { networkKey = await gui.getNetworkKey(); @@ -177,16 +177,12 @@ if (!$page.url.searchParams.get('state')) return; gui = await DotrainOrderGui.deserializeState( dotrain, - $page.url.searchParams.get('state') || '' + $page.url.searchParams.get('state') || '', + pushGuiStateToUrlHistory ); areAllTokensSelected(); } - async function _handleUpdateGuiState(gui: DotrainOrderGui) { - await areAllTokensSelected(); - handleUpdateGuiState(gui); - } - async function handleDeployButtonClick() { error = null; errorDetails = null; @@ -251,7 +247,7 @@ }); } - const areAllTokensSelected = async () => { + const areAllTokensSelected = () => { if (gui) { try { allTokensSelected = gui.areAllTokensSelected(); @@ -297,22 +293,22 @@ {/if} {#if selectTokens && selectTokens.length > 0} - + {/if} {#if allTokensSelected || selectTokens?.length === 0} {#if allFieldDefinitions.length > 0} - + {/if} Show advanced options {#if allDepositFields.length > 0 && showAdvancedOptions} - + {/if} {#if allTokenInputs.length > 0 && allTokenOutputs.length > 0 && showAdvancedOptions} - + {/if} {#if error || errorDetails} diff --git a/packages/ui-components/src/lib/components/deployment/DepositInput.svelte b/packages/ui-components/src/lib/components/deployment/DepositInput.svelte index 7133181f0..5a539f6e8 100644 --- a/packages/ui-components/src/lib/components/deployment/DepositInput.svelte +++ b/packages/ui-components/src/lib/components/deployment/DepositInput.svelte @@ -13,7 +13,6 @@ export let deposit: GuiDeposit; export let gui: DotrainOrderGui; - export let handleUpdateGuiState: (gui: DotrainOrderGui) => void; let error: string = ''; let currentDeposit: TokenDeposit | undefined; let inputValue: string = ''; @@ -51,7 +50,6 @@ gui = gui; currentDeposit = gui?.getDeposits().find((d) => d.token === deposit.token?.key); } - handleUpdateGuiState(gui); } function handleInput(e: Event) { @@ -63,7 +61,6 @@ currentDeposit = gui?.getDeposits().find((d) => d.token === deposit.token?.key); } } - handleUpdateGuiState(gui); } $: if (deposit.token?.key) { diff --git a/packages/ui-components/src/lib/components/deployment/DepositsSection.svelte b/packages/ui-components/src/lib/components/deployment/DepositsSection.svelte index 3a0f548b3..2c11f375d 100644 --- a/packages/ui-components/src/lib/components/deployment/DepositsSection.svelte +++ b/packages/ui-components/src/lib/components/deployment/DepositsSection.svelte @@ -4,9 +4,8 @@ export let allDepositFields: GuiDeposit[]; export let gui: DotrainOrderGui; - export let handleUpdateGuiState: (gui: DotrainOrderGui) => void; {#each allDepositFields as deposit} - + {/each} diff --git a/packages/ui-components/src/lib/components/deployment/FieldDefinitionInput.svelte b/packages/ui-components/src/lib/components/deployment/FieldDefinitionInput.svelte index 5f04e6bfa..8cbae3552 100644 --- a/packages/ui-components/src/lib/components/deployment/FieldDefinitionInput.svelte +++ b/packages/ui-components/src/lib/components/deployment/FieldDefinitionInput.svelte @@ -12,7 +12,6 @@ export let fieldDefinition: GuiFieldDefinition; export let gui: DotrainOrderGui; - export let handleUpdateGuiState: (gui: DotrainOrderGui) => void; let currentValue: GuiPreset | undefined; let inputValue: string | null = currentValue?.value || null; @@ -33,7 +32,6 @@ value: preset.id }); currentValue = gui.getFieldValue(fieldDefinition.binding); - handleUpdateGuiState(gui); } async function handleCustomInputChange(value: string) { @@ -43,7 +41,6 @@ value: value }); currentValue = gui.getFieldValue(fieldDefinition.binding); - handleUpdateGuiState(gui); } diff --git a/packages/ui-components/src/lib/components/deployment/FieldDefinitionsSection.svelte b/packages/ui-components/src/lib/components/deployment/FieldDefinitionsSection.svelte index d2003c110..efd788183 100644 --- a/packages/ui-components/src/lib/components/deployment/FieldDefinitionsSection.svelte +++ b/packages/ui-components/src/lib/components/deployment/FieldDefinitionsSection.svelte @@ -4,9 +4,8 @@ export let allFieldDefinitions: GuiFieldDefinition[]; export let gui: DotrainOrderGui; - export let handleUpdateGuiState: (gui: DotrainOrderGui) => void; {#each allFieldDefinitions as fieldDefinition} - + {/each} diff --git a/packages/ui-components/src/lib/components/deployment/SelectToken.svelte b/packages/ui-components/src/lib/components/deployment/SelectToken.svelte index 3bf2efd17..23bdc6802 100644 --- a/packages/ui-components/src/lib/components/deployment/SelectToken.svelte +++ b/packages/ui-components/src/lib/components/deployment/SelectToken.svelte @@ -7,7 +7,7 @@ export let token: GuiSelectTokens; export let gui: DotrainOrderGui; - export let handleUpdateGuiState: (gui: DotrainOrderGui) => void; + export let onSelectTokenSelect: () => void; let inputValue: string | null = null; let tokenInfo: TokenInfo | null = null; let error = ''; @@ -58,7 +58,7 @@ } checking = false; - handleUpdateGuiState(gui); + onSelectTokenSelect(); } diff --git a/packages/ui-components/src/lib/components/deployment/SelectTokensSection.svelte b/packages/ui-components/src/lib/components/deployment/SelectTokensSection.svelte index a0f821237..530af6a0b 100644 --- a/packages/ui-components/src/lib/components/deployment/SelectTokensSection.svelte +++ b/packages/ui-components/src/lib/components/deployment/SelectTokensSection.svelte @@ -5,7 +5,7 @@ export let gui: DotrainOrderGui; export let selectTokens: SelectTokens; - export let handleUpdateGuiState: (gui: DotrainOrderGui) => void; + export let onSelectTokenSelect: () => void;
@@ -14,6 +14,6 @@ description="Select the tokens that you want to use in your order." /> {#each selectTokens as token} - + {/each}
diff --git a/packages/ui-components/src/lib/components/deployment/TokenIOInput.svelte b/packages/ui-components/src/lib/components/deployment/TokenIOInput.svelte index 55280ee78..18f92ef7a 100644 --- a/packages/ui-components/src/lib/components/deployment/TokenIOInput.svelte +++ b/packages/ui-components/src/lib/components/deployment/TokenIOInput.svelte @@ -12,7 +12,6 @@ export let label: 'Input' | 'Output'; export let vault: OrderIO; export let gui: DotrainOrderGui; - export let handleUpdateGuiState: (gui: DotrainOrderGui) => void; let tokenInfo: TokenInfo | null = null; let inputValue: string = ''; @@ -45,7 +44,6 @@ error = ''; try { gui?.setVaultId(isInput, i, inputValue); - handleUpdateGuiState(gui); } catch (e) { const errorMessage = (e as Error).message ? (e as Error).message : 'Error setting vault ID.'; error = errorMessage; diff --git a/packages/ui-components/src/lib/components/deployment/TokenIOSection.svelte b/packages/ui-components/src/lib/components/deployment/TokenIOSection.svelte index 97d3d8b70..df577d14d 100644 --- a/packages/ui-components/src/lib/components/deployment/TokenIOSection.svelte +++ b/packages/ui-components/src/lib/components/deployment/TokenIOSection.svelte @@ -5,17 +5,16 @@ export let allTokenInputs: OrderIO[] = []; export let allTokenOutputs: OrderIO[] = []; export let gui: DotrainOrderGui; - export let handleUpdateGuiState: (gui: DotrainOrderGui) => void; {#if allTokenInputs.length > 0} {#each allTokenInputs as input, i} - + {/each} {/if} {#if allTokenOutputs.length > 0} {#each allTokenOutputs as output, i} - + {/each} {/if} diff --git a/packages/webapp/src/lib/services/handleUpdateGuiState.ts b/packages/webapp/src/lib/services/handleUpdateGuiState.ts index ef558faf5..fb55b7823 100644 --- a/packages/webapp/src/lib/services/handleUpdateGuiState.ts +++ b/packages/webapp/src/lib/services/handleUpdateGuiState.ts @@ -1,16 +1,8 @@ import { pushState } from '$app/navigation'; -import type { DotrainOrderGui } from '@rainlanguage/orderbook/js_api'; import { debounce } from 'lodash'; -export function handleUpdateGuiState(gui: DotrainOrderGui) { - pushGuiStateToUrlHistory(gui); -} - -const pushGuiStateToUrlHistory = debounce((gui: DotrainOrderGui) => { - const serializedState = gui.serializeState(); - if (serializedState) { - pushState(`?state=${serializedState}`, { serializedState }); - } +export const pushGuiStateToUrlHistory = debounce((serializedState: string) => { + pushState(`?state=${serializedState}`, { serializedState }); }, 1000); if (import.meta.vitest) { @@ -33,11 +25,7 @@ if (import.meta.vitest) { it('should push state to URL history when serializedState exists', async () => { const mockSerializedState = 'mockSerializedState123'; - const mockGui = { - serializeState: vi.fn().mockReturnValue(mockSerializedState) - } as unknown as DotrainOrderGui; - - handleUpdateGuiState(mockGui); + pushGuiStateToUrlHistory(mockSerializedState); // Fast-forward timers to trigger debounced function await vi.advanceTimersByTimeAsync(1000); @@ -47,28 +35,13 @@ if (import.meta.vitest) { }); }); - it('should not push state when serializedState is falsy', async () => { - const mockGui = { - serializeState: vi.fn().mockReturnValue(null) - } as unknown as DotrainOrderGui; - - handleUpdateGuiState(mockGui); - - await vi.advanceTimersByTimeAsync(1000); - - expect(pushState).not.toHaveBeenCalled(); - }); - it('should debounce multiple calls', async () => { const mockSerializedState = 'mockSerializedState123'; - const mockGui = { - serializeState: vi.fn().mockReturnValue(mockSerializedState) - } as unknown as DotrainOrderGui; // Call multiple times in quick succession - handleUpdateGuiState(mockGui); - handleUpdateGuiState(mockGui); - handleUpdateGuiState(mockGui); + pushGuiStateToUrlHistory(mockSerializedState); + pushGuiStateToUrlHistory(mockSerializedState); + pushGuiStateToUrlHistory(mockSerializedState); await vi.advanceTimersByTimeAsync(1000); diff --git a/packages/webapp/src/routes/deploy/[strategyName]/[deploymentKey]/+page.svelte b/packages/webapp/src/routes/deploy/[strategyName]/[deploymentKey]/+page.svelte index 3b387c040..08d8d1ee5 100644 --- a/packages/webapp/src/routes/deploy/[strategyName]/[deploymentKey]/+page.svelte +++ b/packages/webapp/src/routes/deploy/[strategyName]/[deploymentKey]/+page.svelte @@ -4,7 +4,7 @@ import { DeploymentSteps, PageHeader } from '@rainlanguage/ui-components'; import { wagmiConfig, connected, appKitModal } from '$lib/stores/wagmi'; import { handleDeployModal, handleDisclaimerModal } from '$lib/services/modal'; - import { handleUpdateGuiState } from '$lib/services/handleUpdateGuiState'; + import { pushGuiStateToUrlHistory } from '$lib/services/handleUpdateGuiState'; const { settings } = $page.data.stores; const { dotrain, deployment, strategyDetail } = $page.data; @@ -33,7 +33,7 @@ {handleDeployModal} {settings} {stateFromUrl} - {handleUpdateGuiState} + {pushGuiStateToUrlHistory} {handleDisclaimerModal} /> {/if}