diff --git a/src/frontend/src/btc/components/convert/BtcConvertTokenWizard.svelte b/src/frontend/src/btc/components/convert/BtcConvertTokenWizard.svelte
new file mode 100644
index 0000000000..6fcdc6b0d7
--- /dev/null
+++ b/src/frontend/src/btc/components/convert/BtcConvertTokenWizard.svelte
@@ -0,0 +1,150 @@
+
+
+
+ {#if currentStep?.name === WizardStepsConvert.CONVERT}
+
+
+ {#if formCancelAction === 'back'}
+
+ {:else}
+
+ {/if}
+
+
+ {:else if currentStep?.name === WizardStepsConvert.REVIEW}
+
+ {:else if currentStep?.name === WizardStepsConvert.CONVERTING}
+
+ {:else}
+
+ {/if}
+
diff --git a/src/frontend/src/lib/components/convert/ConvertReview.svelte b/src/frontend/src/lib/components/convert/ConvertReview.svelte
index f809cdca0c..754bb498de 100644
--- a/src/frontend/src/lib/components/convert/ConvertReview.svelte
+++ b/src/frontend/src/lib/components/convert/ConvertReview.svelte
@@ -31,7 +31,7 @@
-
diff --git a/src/frontend/src/lib/i18n/en.json b/src/frontend/src/lib/i18n/en.json
index 2b0c16f827..108460ec6d 100644
--- a/src/frontend/src/lib/i18n/en.json
+++ b/src/frontend/src/lib/i18n/en.json
@@ -403,7 +403,9 @@
"insufficient_funds_for_fee": "Insufficient balance to cover the fees"
},
"error": {
- "loading_cketh_helper": "Error while loading the ckETH helper contract address. No minter canister ID has been initialized."
+ "loading_cketh_helper": "Error while loading the ckETH helper contract address. No minter canister ID has been initialized.",
+ "unexpected": "Something went wrong while converting tokens.",
+ "unexpected_missing_data": "Something went wrong while initiating a convert transaction."
}
},
"buy": {
diff --git a/src/frontend/src/lib/types/i18n.d.ts b/src/frontend/src/lib/types/i18n.d.ts
index af85dd033a..1a042b07ff 100644
--- a/src/frontend/src/lib/types/i18n.d.ts
+++ b/src/frontend/src/lib/types/i18n.d.ts
@@ -369,7 +369,7 @@ interface I18nConvert {
refreshing_ui: string;
};
assertion: { insufficient_funds: string; insufficient_funds_for_fee: string };
- error: { loading_cketh_helper: string };
+ error: { loading_cketh_helper: string; unexpected: string; unexpected_missing_data: string };
}
interface I18nBuy {
diff --git a/src/frontend/src/tests/btc/components/convert/BtcConvertTokenWizard.spec.ts b/src/frontend/src/tests/btc/components/convert/BtcConvertTokenWizard.spec.ts
new file mode 100644
index 0000000000..31e75c363a
--- /dev/null
+++ b/src/frontend/src/tests/btc/components/convert/BtcConvertTokenWizard.spec.ts
@@ -0,0 +1,193 @@
+import BtcConvertTokenWizard from '$btc/components/convert/BtcConvertTokenWizard.svelte';
+import * as btcSendServices from '$btc/services/btc-send.services';
+import * as utxosFeeStore from '$btc/stores/utxos-fee.store';
+import type { UtxosFee } from '$btc/types/btc-send';
+import { BTC_MAINNET_TOKEN } from '$env/tokens.btc.env';
+import { ETHEREUM_TOKEN, ICP_TOKEN } from '$env/tokens.env';
+import { btcAddressStore } from '$icp/stores/btc.store';
+import * as addressesStore from '$lib/derived/address.derived';
+import * as authStore from '$lib/derived/auth.derived';
+import { ProgressStepsConvert } from '$lib/enums/progress-steps';
+import { WizardStepsConvert } from '$lib/enums/wizard-steps';
+import { CONVERT_CONTEXT_KEY } from '$lib/stores/convert.store';
+import type { Token } from '$lib/types/token';
+import { mockBtcAddress, mockUtxosFee } from '$tests/mocks/btc.mock';
+import { mockIdentity } from '$tests/mocks/identity.mock';
+import { mockPage } from '$tests/mocks/page.store.mock';
+import type { Identity } from '@dfinity/agent';
+import { assertNonNullish } from '@dfinity/utils';
+import { fireEvent, render } from '@testing-library/svelte';
+import { readable } from 'svelte/store';
+
+describe('BtcConvertTokenWizard', () => {
+ const sendAmount = 0.001;
+ const mockContext = (sourceToken: Token | undefined = BTC_MAINNET_TOKEN) =>
+ new Map([
+ [
+ CONVERT_CONTEXT_KEY,
+ {
+ sourceToken: readable(sourceToken),
+ destinationToken: readable(ICP_TOKEN)
+ }
+ ]
+ ]);
+ const props = {
+ currentStep: {
+ name: WizardStepsConvert.REVIEW,
+ title: 'title'
+ },
+ convertProgressStep: ProgressStepsConvert.INITIALIZATION,
+ sendAmount: sendAmount,
+ receiveAmount: sendAmount
+ };
+ const mockBtcSendServices = () => vi.spyOn(btcSendServices, 'sendBtc').mockResolvedValue();
+ const mockAuthStore = (value: Identity | null = mockIdentity) =>
+ vi.spyOn(authStore, 'authIdentity', 'get').mockImplementation(() => readable(value));
+ const mockBtcAddressStore = (address: string | undefined = mockBtcAddress) => {
+ btcAddressStore.set({
+ tokenId: ICP_TOKEN.id,
+ data: {
+ certified: true,
+ data: address
+ }
+ });
+ };
+ const mockAddressesStore = () =>
+ vi
+ .spyOn(addressesStore, 'btcAddressMainnet', 'get')
+ .mockImplementation(() => readable(mockBtcAddress));
+ const mockUtxosFeeStore = (utxosFee?: UtxosFee) => {
+ const store = utxosFeeStore.initUtxosFeeStore();
+
+ vi.spyOn(utxosFeeStore, 'initUtxosFeeStore').mockImplementation(() => {
+ store.setUtxosFee({ utxosFee });
+ return store;
+ });
+ };
+ const clickConvertButton = async (container: HTMLElement) => {
+ const convertButtonSelector = '[data-tid="convert-review-button-next"]';
+ const button: HTMLButtonElement | null = container.querySelector(convertButtonSelector);
+ assertNonNullish(button, 'Button not found');
+ await fireEvent.click(button);
+ };
+
+ beforeEach(() => {
+ mockPage.reset();
+ });
+
+ it('should call sendBtc if all requirements are met', async () => {
+ const spy = mockBtcSendServices();
+ mockAuthStore();
+ mockBtcAddressStore();
+ mockAddressesStore();
+ mockUtxosFeeStore(mockUtxosFee);
+
+ const { container } = render(BtcConvertTokenWizard, {
+ props,
+ context: mockContext()
+ });
+
+ await clickConvertButton(container);
+
+ expect(spy).toHaveBeenCalledWith(
+ // all params except "onProgress"
+ expect.objectContaining({
+ amount: sendAmount,
+ destination: mockBtcAddress,
+ identity: mockIdentity,
+ network: BTC_MAINNET_TOKEN.network.env,
+ source: mockBtcAddress,
+ utxosFee: expect.objectContaining(mockUtxosFee)
+ })
+ );
+ expect(spy).toHaveBeenCalledOnce();
+ });
+
+ it('should not call sendBtc if authIdentity is not defined', async () => {
+ const spy = mockBtcSendServices();
+ mockAuthStore(null);
+ mockBtcAddressStore();
+ mockAddressesStore();
+ mockUtxosFeeStore(mockUtxosFee);
+
+ const { container } = render(BtcConvertTokenWizard, {
+ props,
+ context: mockContext()
+ });
+
+ await clickConvertButton(container);
+
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ it('should not call sendBtc if network is not BTC', async () => {
+ const spy = mockBtcSendServices();
+ mockAuthStore();
+ mockAddressesStore();
+ mockBtcAddressStore();
+ mockUtxosFeeStore(mockUtxosFee);
+
+ const { container } = render(BtcConvertTokenWizard, {
+ props,
+ context: mockContext(ETHEREUM_TOKEN)
+ });
+
+ await clickConvertButton(container);
+
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ it('should not call sendBtc if destination address is not defined', async () => {
+ const spy = mockBtcSendServices();
+ mockAuthStore();
+ mockAddressesStore();
+ mockBtcAddressStore('');
+ mockUtxosFeeStore(mockUtxosFee);
+
+ const { container } = render(BtcConvertTokenWizard, {
+ props,
+ context: mockContext()
+ });
+
+ await clickConvertButton(container);
+
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ it('should not call sendBtc if sendAmount is not defined', async () => {
+ const spy = mockBtcSendServices();
+ mockAuthStore();
+ mockAddressesStore();
+ mockBtcAddressStore();
+ mockUtxosFeeStore(mockUtxosFee);
+
+ const { container } = render(BtcConvertTokenWizard, {
+ props: {
+ ...props,
+ sendAmount: undefined
+ },
+ context: mockContext()
+ });
+
+ await clickConvertButton(container);
+
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ it('should not call sendBtc if utxos are not defined', async () => {
+ const spy = mockBtcSendServices();
+ mockAuthStore();
+ mockAddressesStore();
+ mockBtcAddressStore();
+ mockUtxosFeeStore(undefined);
+
+ const { container } = render(BtcConvertTokenWizard, {
+ props,
+ context: mockContext()
+ });
+
+ await clickConvertButton(container);
+
+ expect(spy).not.toHaveBeenCalled();
+ });
+});