From 27e2ff8ea79b6a0267e5e8208e49d8a2f6c6e278 Mon Sep 17 00:00:00 2001 From: Hoang Dinh Date: Thu, 5 Sep 2024 08:39:33 +1000 Subject: [PATCH] chore: test the deploy app flow (#252) --- .../accounts/pages/account-page.test.tsx | 12 ++ .../create-app-interface-dialog-body.test.tsx | 110 ++++++++++++++++++ .../create-app-interface-form.test.tsx | 68 +++++++++++ .../components/create-app-interface-form.tsx | 6 +- .../components/deploy-app-form.tsx | 15 +-- .../app-interfaces/components/labels.ts | 3 +- src/features/app-interfaces/data/write.ts | 4 +- .../components/application-program.test.tsx | 12 ++ .../pages/application-page.test.tsx | 28 +++++ .../asset-transaction-history.test.tsx | 10 ++ src/features/assets/pages/asset-page.test.tsx | 21 ++++ src/features/blocks/pages/block-page.test.tsx | 12 ++ .../forms/components/file-form-item.tsx | 7 +- src/features/forms/components/file-input.tsx | 52 +++++---- src/features/forms/components/form.tsx | 6 +- .../components/readonly-file-form-item.tsx | 3 +- src/features/groups/pages/group-page.test.tsx | 12 ++ .../transaction-wizard-page.test.tsx | 53 +-------- .../pages/transaction-page.test.tsx | 14 ++- src/tests/setup/mocks/index.ts | 51 -------- .../test-app-specs/sample-six.arc32.json | 73 ++++++++++++ src/tests/utils/select-option.ts | 21 ++++ .../utils/set-wallet-address-and-signer.ts | 19 +++ vite.config.ts | 2 +- 24 files changed, 472 insertions(+), 142 deletions(-) create mode 100644 src/features/app-interfaces/components/create-app-interface-dialog-body.test.tsx create mode 100644 src/features/app-interfaces/components/create-app-interface-form.test.tsx create mode 100644 src/tests/test-app-specs/sample-six.arc32.json create mode 100644 src/tests/utils/select-option.ts create mode 100644 src/tests/utils/set-wallet-address-and-signer.ts diff --git a/src/features/accounts/pages/account-page.test.tsx b/src/features/accounts/pages/account-page.test.tsx index 6f1c7c79..23f669da 100644 --- a/src/features/accounts/pages/account-page.test.tsx +++ b/src/features/accounts/pages/account-page.test.tsx @@ -28,6 +28,18 @@ import { assetResultMother } from '@/tests/object-mother/asset-result' import { refreshButtonLabel } from '@/features/common/components/refresh-button' import { algod } from '@/features/common/data/algo-client' +vi.mock('@/features/common/data/algo-client', async () => { + const original = await vi.importActual('@/features/common/data/algo-client') + return { + ...original, + algod: { + accountInformation: vi.fn().mockReturnValue({ + do: vi.fn().mockReturnValue({ then: vi.fn() }), + }), + }, + } +}) + describe('account-page', () => { describe('when rendering an account using a invalid address', () => { it('should render an error message', () => { diff --git a/src/features/app-interfaces/components/create-app-interface-dialog-body.test.tsx b/src/features/app-interfaces/components/create-app-interface-dialog-body.test.tsx new file mode 100644 index 00000000..6c6763f3 --- /dev/null +++ b/src/features/app-interfaces/components/create-app-interface-dialog-body.test.tsx @@ -0,0 +1,110 @@ +import { afterEach, beforeEach, describe, expect, it, vitest } from 'vitest' +import { executeComponentTest } from '@/tests/test-component' +import SampleSixAppSpec from '@/tests/test-app-specs/sample-six.arc32.json' +import { fireEvent, getByLabelText, getByText, render, waitFor } from '@/tests/testing-library' +import { Arc32AppSpec } from '../data/types' +import { deployAppLabel, deployButtonLabel } from '@/features/app-interfaces/components/labels' +import { algorandFixture } from '@algorandfoundation/algokit-utils/testing' +import { CreateAppInterfaceDialogBody } from '@/features/app-interfaces/components/create-app-interface-dialog-body' +import { selectOption } from '@/tests/utils/select-option' +import { setWalletAddressAndSigner } from '@/tests/utils/set-wallet-address-and-signer' + +describe('create-app-interface-dialog-body', () => { + const localnet = algorandFixture() + beforeEach(localnet.beforeEach, 10e6) + afterEach(() => { + vitest.clearAllMocks() + }) + + describe('when deploying an app spec that requires template parameters', () => { + const appSpec = SampleSixAppSpec as Arc32AppSpec + + beforeEach(async () => { + await setWalletAddressAndSigner(localnet) + }) + + it('succeeds when all fields have been correctly supplied', () => { + return executeComponentTest( + () => { + return render( {}} />) + }, + async (component, user) => { + const appSpecFileInput = await component.findByLabelText(/JSON app spec file/) + await user.upload(appSpecFileInput, new File([JSON.stringify(appSpec)], 'app.json', { type: 'application/json' })) + + const deployAppButton = await waitFor(() => { + const button = component.getByRole('button', { name: deployAppLabel }) + expect(button).toBeDefined() + expect(button).not.toBeDisabled() + return button! + }) + await user.click(deployAppButton) + + const versionInput = await waitFor(() => { + const input = component.getByLabelText(/Version/) + expect(input).toBeDefined() + return input! + }) + fireEvent.input(versionInput, { + target: { value: '1.0.0' }, + }) + + await selectOption(component.container, user, /On Update/, 'Fail') + await selectOption(component.container, user, /On Schema Break/, 'Fail') + + const someStringTemplateParamDiv = await findParentDiv(component.container, 'SOME_STRING') + await selectOption(someStringTemplateParamDiv, user, /Type/, 'String') + const someStringInput = getByLabelText(someStringTemplateParamDiv, /Value/) + fireEvent.input(someStringInput, { + target: { value: 'some-string' }, + }) + + const someBytesTemplateParamDiv = await findParentDiv(component.container, 'SOME_BYTES') + await selectOption(someBytesTemplateParamDiv, user, /Type/, 'Uint8Array') + const someBytesInput = getByLabelText(someBytesTemplateParamDiv!, /Value/) + fireEvent.input(someBytesInput, { + target: { value: 'AQIDBA==' }, + }) + + const someNumberTemplateParamDiv = await findParentDiv(component.container, 'SOME_NUMBER') + await selectOption(someNumberTemplateParamDiv, user, /Type/, 'Number') + const someNumberInput = getByLabelText(someNumberTemplateParamDiv!, /Value/) + fireEvent.input(someNumberInput, { + target: { value: '3' }, + }) + + const deployButton = await waitFor(() => { + const button = component.queryByRole('button', { name: deployButtonLabel }) + expect(button).toBeDefined() + return button! + }) + await user.click(deployButton) + + await waitFor(() => { + const requiredValidationMessages = component.queryAllByText('Required') + expect(requiredValidationMessages.length).toBe(0) + }) + + await waitFor(() => { + const errorMessage = component.queryByRole('alert', { name: 'error-message' }) + expect(errorMessage).toBeNull() + }) + + await waitFor(() => { + const input = component.getByLabelText(/Application ID/) + expect(input).toBeDefined() + expect(input).toHaveValue() + return input! + }) + } + ) + }) + }) +}) + +const findParentDiv = async (component: HTMLElement, label: string) => { + return await waitFor(() => { + const div = getByText(component, label) + return div.parentElement! + }) +} diff --git a/src/features/app-interfaces/components/create-app-interface-form.test.tsx b/src/features/app-interfaces/components/create-app-interface-form.test.tsx new file mode 100644 index 00000000..6e9561f2 --- /dev/null +++ b/src/features/app-interfaces/components/create-app-interface-form.test.tsx @@ -0,0 +1,68 @@ +import { afterEach, beforeEach, describe, expect, it, vi, vitest } from 'vitest' +import { algorandFixture } from '@algorandfoundation/algokit-utils/testing' +import SampleSixAppSpec from '@/tests/test-app-specs/sample-six.arc32.json' +import { Arc32AppSpec } from '@/features/app-interfaces/data/types' +import { executeComponentTest } from '@/tests/test-component' +import { render, waitFor } from '@/tests/testing-library' +import { CreateAppInterfaceForm } from '@/features/app-interfaces/components/create-app-interface-form' +import { deployAppLabel } from '@/features/app-interfaces/components/labels' +import { setWalletAddressAndSigner } from '@/tests/utils/set-wallet-address-and-signer' +import { useWallet } from '@txnlab/use-wallet' + +describe('create-app-interface-form', () => { + const appSpec = SampleSixAppSpec as Arc32AppSpec + + const localnet = algorandFixture() + beforeEach(localnet.beforeEach, 10e6) + afterEach(() => { + vitest.clearAllMocks() + }) + + describe('when a wallet is connected', () => { + beforeEach(async () => { + await setWalletAddressAndSigner(localnet) + }) + + it('the button to deploy the app is enabled', () => { + return executeComponentTest( + () => { + return render( {}} />) + }, + async (component) => { + await waitFor(() => { + const deployAppButton = component.getByRole('button', { name: deployAppLabel }) + expect(deployAppButton).toBeEnabled() + }) + } + ) + }) + }) + + describe('when a wallet is not connected', () => { + beforeEach(async () => { + const original = await vi.importActual<{ useWallet: () => ReturnType }>('@txnlab/use-wallet') + vi.mocked(useWallet).mockImplementation(() => { + return { + ...original.useWallet(), + activeAddress: undefined, + isActive: false, + isReady: true, + } + }) + }) + + it('the button to deploy the app is disabled', () => { + return executeComponentTest( + () => { + return render( {}} />) + }, + async (component) => { + await waitFor(() => { + const deployAppButton = component.getByRole('button', { name: deployAppLabel }) + expect(deployAppButton).toBeDisabled() + }) + } + ) + }) + }) +}) diff --git a/src/features/app-interfaces/components/create-app-interface-form.tsx b/src/features/app-interfaces/components/create-app-interface-form.tsx index ca815cbe..4f680f24 100644 --- a/src/features/app-interfaces/components/create-app-interface-form.tsx +++ b/src/features/app-interfaces/components/create-app-interface-form.tsx @@ -12,7 +12,7 @@ import { FormFieldHelper } from '@/features/forms/components/form-field-helper' import { useFormContext, useWatch } from 'react-hook-form' import { useCreateAppInterfaceStateMachine } from '@/features/app-interfaces/data' import { Button } from '@/features/common/components/button' -import { deployToNetworkLabel } from '@/features/app-interfaces/components/labels' +import { deployAppLabel } from '@/features/app-interfaces/components/labels' import { isArc32AppSpec, isArc4AppSpec } from '@/features/common/utils' import { useLoadableActiveWalletAccount } from '@/features/wallet/data/active-wallet' import { numberSchema } from '@/features/forms/data/common' @@ -166,10 +166,10 @@ function FormInner({ helper, appSpec }: FormInnerProps) { disabled={deployButtonStatus.disabled} disabledReason={deployButtonStatus.reason} className="w-fit" - aria-label={deployToNetworkLabel} + aria-label={deployAppLabel} onClick={onDeployButtonClick} > - {deployToNetworkLabel} + {deployAppLabel} diff --git a/src/features/app-interfaces/components/deploy-app-form.tsx b/src/features/app-interfaces/components/deploy-app-form.tsx index 704a6e31..06e0c3b6 100644 --- a/src/features/app-interfaces/components/deploy-app-form.tsx +++ b/src/features/app-interfaces/components/deploy-app-form.tsx @@ -19,6 +19,7 @@ import { FormFieldHelper } from '@/features/forms/components/form-field-helper' import { Label } from '@/features/common/components/label' import { Fieldset } from '@/features/forms/components/fieldset' import { base64ToBytes } from '@/utils/base64-to-bytes' +import { deployButtonLabel } from '@/features/app-interfaces/components/labels' type Props = { className?: string @@ -80,7 +81,7 @@ const getTealTemplateParams = (names: string[], formData: DeployAppFormData) => export function DeployAppForm({ className, appSpec }: Props) { const [_, send] = useCreateAppInterfaceStateMachine() - const { signer, activeAccount } = useWallet() + const { signer, activeAddress } = useWallet() const templateParamNames = useMemo(() => { const approvalTemplateParams = getTemplateParamNames(appSpec.source?.approval ?? '') @@ -92,10 +93,10 @@ export function DeployAppForm({ className, appSpec }: Props) { async (values: DeployAppFormData) => { invariant(appSpec.source.approval, 'Approval program is not set') invariant(appSpec.source.clear, 'Clear program is not set') - invariant(activeAccount, 'No active wallet account is available') + invariant(activeAddress, 'No active wallet account is available') const signerAccount = { - addr: activeAccount.address, + addr: activeAddress, signer, } @@ -126,7 +127,7 @@ export function DeployAppForm({ className, appSpec }: Props) { return Number(deployAppResult.appId) }, [ - activeAccount, + activeAddress, appSpec.source.approval, appSpec.source.clear, appSpec.state.global.num_byte_slices, @@ -169,7 +170,7 @@ export function DeployAppForm({ className, appSpec }: Props) { formAction={ - Deploy + {deployButtonLabel} } defaultValues={{ @@ -257,7 +258,7 @@ export function TemplateParamForm({ className, name, index }: TemplateParamFormP {helper.selectField({ field: 'type', label: 'Type', - className: ' content-start', + className: 'content-start', options: [ { value: TemplateParamType.String, label: 'String' }, { value: TemplateParamType.Number, label: 'Number' }, @@ -267,7 +268,7 @@ export function TemplateParamForm({ className, name, index }: TemplateParamFormP {helper.textField({ field: 'value', label: 'Value', - className: ' content-start', + className: 'content-start', helpText: helpText, })} diff --git a/src/features/app-interfaces/components/labels.ts b/src/features/app-interfaces/components/labels.ts index 62138ded..4cf07cc0 100644 --- a/src/features/app-interfaces/components/labels.ts +++ b/src/features/app-interfaces/components/labels.ts @@ -3,4 +3,5 @@ export const methodsLabel = 'Methods' export const appIdLabel = 'App ID' export const createAppInterfaceLabel = 'Create App Interface' export const deleteAppInterfaceLabel = 'Delete App Interface' -export const deployToNetworkLabel = 'Deploy App' +export const deployAppLabel = 'Deploy App' +export const deployButtonLabel = 'Deploy' diff --git a/src/features/app-interfaces/data/write.ts b/src/features/app-interfaces/data/write.ts index a12b81f1..2931c1ef 100644 --- a/src/features/app-interfaces/data/write.ts +++ b/src/features/app-interfaces/data/write.ts @@ -8,7 +8,7 @@ import { useAtomCallback } from 'jotai/utils' import { useCallback } from 'react' import { invariant } from '@/utils/invariant' import { createTimestamp } from '@/features/common/data' -import { getAppInterfaces } from '@/features/app-interfaces/data/index' +import { getAppInterfaces } from '@/features/app-interfaces/data' export type AppSpecDetails = { applicationId: ApplicationId @@ -56,7 +56,7 @@ export const useCreateAppInterface = () => { ) } -const createMachine = () => +export const createMachine = () => setup({ types: { context: {} as { diff --git a/src/features/applications/components/application-program.test.tsx b/src/features/applications/components/application-program.test.tsx index 0593fbaa..21a6ae80 100644 --- a/src/features/applications/components/application-program.test.tsx +++ b/src/features/applications/components/application-program.test.tsx @@ -4,6 +4,18 @@ import { ApplicationProgram, base64ProgramTabLabel, tealProgramTabLabel } from ' import { executeComponentTest } from '@/tests/test-component' import { algod } from '@/features/common/data/algo-client' +vi.mock('@/features/common/data/algo-client', async () => { + const original = await vi.importActual('@/features/common/data/algo-client') + return { + ...original, + algod: { + disassemble: vi.fn().mockReturnValue({ + do: vi.fn(), + }), + }, + } +}) + describe('application-program', () => { describe('when rendering an application program', () => { const tabListName = 'test' diff --git a/src/features/applications/pages/application-page.test.tsx b/src/features/applications/pages/application-page.test.tsx index 6e961ef7..abe3bf7b 100644 --- a/src/features/applications/pages/application-page.test.tsx +++ b/src/features/applications/pages/application-page.test.tsx @@ -39,6 +39,34 @@ import { AppInterfaceEntity, dbConnectionAtom } from '@/features/common/data/ind import { writeAppInterface } from '@/features/app-interfaces/data' import SampleSevenAppSpec from '@/tests/test-app-specs/sample-seven.arc32.json' import { AppSpecStandard, Arc32AppSpec } from '@/features/app-interfaces/data/types' +import { searchTransactionsMock } from '@/tests/setup/mocks' + +vi.mock('@/features/common/data/algo-client', async () => { + const original = await vi.importActual('@/features/common/data/algo-client') + return { + ...original, + algod: { + getApplicationByID: vi.fn().mockReturnValue({ + do: vi.fn().mockReturnValue({ then: vi.fn() }), + }), + }, + indexer: { + lookupApplications: vi.fn().mockReturnValue({ + includeAll: vi.fn().mockReturnValue({ + do: vi.fn().mockReturnValue({ then: vi.fn() }), + }), + }), + searchForApplicationBoxes: vi.fn().mockReturnValue({ + nextToken: vi.fn().mockReturnValue({ + limit: vi.fn().mockReturnValue({ + do: vi.fn().mockReturnValue({ then: vi.fn() }), + }), + }), + }), + searchForTransactions: vi.fn().mockImplementation(() => searchTransactionsMock), + }, + } +}) describe('application-page', () => { describe('when rendering an application using an invalid application Id', () => { diff --git a/src/features/assets/components/asset-transaction-history.test.tsx b/src/features/assets/components/asset-transaction-history.test.tsx index d3ca9376..3108fe0e 100644 --- a/src/features/assets/components/asset-transaction-history.test.tsx +++ b/src/features/assets/components/asset-transaction-history.test.tsx @@ -12,6 +12,16 @@ import { getAllByRole } from '@testing-library/dom' import { ANY_NUMBER, ANY_STRING, searchTransactionsMock } from '@/tests/setup/mocks' import { RenderResult } from '@testing-library/react' +vi.mock('@/features/common/data/algo-client', async () => { + const original = await vi.importActual('@/features/common/data/algo-client') + return { + ...original, + indexer: { + searchForTransactions: vi.fn().mockImplementation(() => searchTransactionsMock), + }, + } +}) + describe('asset-transaction-history', () => { const asset = assetResultMother['testnet-642327435']().build() diff --git a/src/features/assets/pages/asset-page.test.tsx b/src/features/assets/pages/asset-page.test.tsx index 856aff58..6f7ad529 100644 --- a/src/features/assets/pages/asset-page.test.tsx +++ b/src/features/assets/pages/asset-page.test.tsx @@ -34,6 +34,7 @@ import { refreshButtonLabel } from '@/features/common/components/refresh-button' import { algod, indexer } from '@/features/common/data/algo-client' import { setupServer } from 'msw/node' import { http, HttpResponse } from 'msw' +import { searchTransactionsMock } from '@/tests/setup/mocks' const server = setupServer() @@ -41,6 +42,26 @@ beforeAll(() => server.listen()) afterAll(() => server.close()) afterEach(() => server.resetHandlers()) +vi.mock('@/features/common/data/algo-client', async () => { + const original = await vi.importActual('@/features/common/data/algo-client') + return { + ...original, + algod: { + getAssetByID: vi.fn().mockReturnValue({ + do: vi.fn().mockReturnValue({ then: vi.fn() }), + }), + }, + indexer: { + lookupAssetByID: vi.fn().mockReturnValue({ + includeAll: vi.fn().mockReturnValue({ + do: vi.fn().mockReturnValue({ then: vi.fn() }), + }), + }), + searchForTransactions: vi.fn().mockImplementation(() => searchTransactionsMock), + }, + } +}) + describe('asset-page', () => { describe('when rendering an asset using an invalid asset Id', () => { it('should display invalid asset Id message', () => { diff --git a/src/features/blocks/pages/block-page.test.tsx b/src/features/blocks/pages/block-page.test.tsx index 626091e7..db4e7ba3 100644 --- a/src/features/blocks/pages/block-page.test.tsx +++ b/src/features/blocks/pages/block-page.test.tsx @@ -19,6 +19,18 @@ import { descriptionListAssertion } from '@/tests/assertions/description-list-as import { assetResultsAtom } from '@/features/assets/data' import { indexer } from '@/features/common/data/algo-client' +vi.mock('@/features/common/data/algo-client', async () => { + const original = await vi.importActual('@/features/common/data/algo-client') + return { + ...original, + indexer: { + lookupBlock: vi.fn().mockReturnValue({ + do: vi.fn(), + }), + }, + } +}) + describe('block-page', () => { describe('when rendering a block using an invalid round number', () => { it('should display invalid round message', () => { diff --git a/src/features/forms/components/file-form-item.tsx b/src/features/forms/components/file-form-item.tsx index e7910929..67180638 100644 --- a/src/features/forms/components/file-form-item.tsx +++ b/src/features/forms/components/file-form-item.tsx @@ -2,7 +2,8 @@ import { Controller, FieldPath } from 'react-hook-form' import { FormItem } from '@/features/forms/components/form-item' import { FileInput, FileInputProps } from '@/features/forms/components/file-input' -export interface FileFormItemProps> extends Omit { +export interface FileFormItemProps> + extends Omit { label: string field: FieldPath } @@ -18,8 +19,8 @@ export function FileFormItem>({ ( - + render={({ field: { value, onChange, name } }) => ( + )} /> diff --git a/src/features/forms/components/file-input.tsx b/src/features/forms/components/file-input.tsx index a405dc01..5281ea73 100644 --- a/src/features/forms/components/file-input.tsx +++ b/src/features/forms/components/file-input.tsx @@ -10,9 +10,10 @@ export type FileInputProps = { value?: File onChange: (value: File) => void helpText?: string | ReactElement + fieldName: string } -export function FileInput({ accept, placeholder, value, disabled, onChange }: FileInputProps) { +export function FileInput({ accept, placeholder, value, disabled, onChange, fieldName }: FileInputProps) { const inputRef = useRef(null) const onFilesAdded = useCallback( @@ -35,36 +36,39 @@ export function FileInput({ accept, placeholder, value, disabled, onChange }: Fi }, []) return ( -
-
- {(() => { - if (value) { - return {value.name} - } else if (placeholder) { - return ( - <> - - {placeholder} - - ) - } else { - return ( - <> - - <>  - - ) - } - })()} -
+ <> onFilesAdded(Array.from(e.target.files ?? []))} disabled={disabled} accept={accept} /> -
+
+
+ {(() => { + if (value) { + return {value.name} + } else if (placeholder) { + return ( + <> + + {placeholder} + + ) + } else { + return ( + <> + + <>  + + ) + } + })()} +
+
+ ) } diff --git a/src/features/forms/components/form.tsx b/src/features/forms/components/form.tsx index cb6b339b..c7fd0c1f 100644 --- a/src/features/forms/components/form.tsx +++ b/src/features/forms/components/form.tsx @@ -82,7 +82,11 @@ export function Form>({
{typeof children === 'function' ? children(new FormFieldHelper(), handleSubmit) : children} - {errorMessage &&
{errorMessage}
} + {errorMessage && ( +
+ {errorMessage} +
+ )} {typeof formAction === 'function' ? formAction(formCtx, resetLocalState) : formAction}
diff --git a/src/features/forms/components/readonly-file-form-item.tsx b/src/features/forms/components/readonly-file-form-item.tsx index 2a15822e..febf3832 100644 --- a/src/features/forms/components/readonly-file-form-item.tsx +++ b/src/features/forms/components/readonly-file-form-item.tsx @@ -2,7 +2,8 @@ import { Controller, FieldPath } from 'react-hook-form' import { FormItem } from '@/features/forms/components/form-item' import { FileInputProps } from '@/features/forms/components/file-input' -export interface ReadonlyFileFormItemProps> extends Omit { +export interface ReadonlyFileFormItemProps> + extends Omit { label: string field: FieldPath } diff --git a/src/features/groups/pages/group-page.test.tsx b/src/features/groups/pages/group-page.test.tsx index 9749e242..df91f0bc 100644 --- a/src/features/groups/pages/group-page.test.tsx +++ b/src/features/groups/pages/group-page.test.tsx @@ -20,6 +20,18 @@ import { assetResultsAtom } from '@/features/assets/data' import { indexer } from '@/features/common/data/algo-client' import { genesisHashAtom } from '@/features/blocks/data' +vi.mock('@/features/common/data/algo-client', async () => { + const original = await vi.importActual('@/features/common/data/algo-client') + return { + ...original, + indexer: { + lookupBlock: vi.fn().mockReturnValue({ + do: vi.fn(), + }), + }, + } +}) + describe('group-page', () => { describe('when rendering a group using an invalid round number', () => { it('should display invalid round message', () => { diff --git a/src/features/transaction-wizard/transaction-wizard-page.test.tsx b/src/features/transaction-wizard/transaction-wizard-page.test.tsx index 4dee9c5a..b619d4f4 100644 --- a/src/features/transaction-wizard/transaction-wizard-page.test.tsx +++ b/src/features/transaction-wizard/transaction-wizard-page.test.tsx @@ -1,13 +1,15 @@ import { afterEach, beforeEach, describe, expect, vitest, it, vi } from 'vitest' import { algorandFixture } from '@algorandfoundation/algokit-utils/testing' import { executeComponentTest } from '@/tests/test-component' -import { fireEvent, render, waitFor, within } from '@/tests/testing-library' +import { fireEvent, render, waitFor } from '@/tests/testing-library' import { useWallet } from '@txnlab/use-wallet' import { algo } from '@algorandfoundation/algokit-utils' import { transactionIdLabel } from '../transactions/components/transaction-info' import { getByDescriptionTerm } from '@/tests/custom-queries/get-description' import { accountCloseTransaction } from './data/payment-transactions' import { sendButtonLabel, transactionTypeLabel, TransactionWizardPage } from './transaction-wizard-page' +import { selectOption } from '@/tests/utils/select-option' +import { setWalletAddressAndSigner } from '@/tests/utils/set-wallet-address-and-signer' describe('transaction-wizard-page', () => { const localnet = algorandFixture() @@ -46,18 +48,7 @@ describe('transaction-wizard-page', () => { describe('when a wallet is connected', () => { beforeEach(async () => { - const { testAccount } = localnet.context - const original = await vi.importActual<{ useWallet: () => ReturnType }>('@txnlab/use-wallet') - vi.mocked(useWallet).mockImplementation(() => { - return { - ...original.useWallet(), - activeAddress: testAccount.addr, - signer: testAccount.signer, - status: 'active', - isActive: true, - isReady: true, - } - }) + await setWalletAddressAndSigner(localnet) }) describe('and a payment transaction is being sent', () => { @@ -146,23 +137,7 @@ describe('transaction-wizard-page', () => { return render() }, async (component, user) => { - const transactionTypeSelect = await waitFor(() => { - const transactionTypeSelect = component.getByRole('combobox', { name: transactionTypeLabel }) - expect(transactionTypeSelect).toBeDefined() - return transactionTypeSelect! - }) - - await user.click(transactionTypeSelect) - - const accountCloseOption = await waitFor(() => { - return component.getByRole('option', { name: accountCloseTransaction.label }) - }) - - await user.click(accountCloseOption) - - await waitFor(() => { - expect(within(transactionTypeSelect).getByText(accountCloseTransaction.label)).toBeInTheDocument() - }) + await selectOption(component.container, user, transactionTypeLabel, accountCloseTransaction.label) const sendButton = await waitFor(() => { const sendButton = component.getByRole('button', { name: sendButtonLabel }) @@ -189,23 +164,7 @@ describe('transaction-wizard-page', () => { return render() }, async (component, user) => { - const transactionTypeSelect = await waitFor(() => { - const transactionTypeSelect = component.getByRole('combobox', { name: transactionTypeLabel }) - expect(transactionTypeSelect).toBeDefined() - return transactionTypeSelect! - }) - - await user.click(transactionTypeSelect) - - const accountCloseOption = await waitFor(() => { - return component.getByRole('option', { name: accountCloseTransaction.label }) - }) - - await user.click(accountCloseOption) - - await waitFor(() => { - expect(within(transactionTypeSelect).getByText(accountCloseTransaction.label)).toBeInTheDocument() - }) + await selectOption(component.container, user, transactionTypeLabel, accountCloseTransaction.label) const sendButton = await waitFor(() => { const sendButton = component.getByRole('button', { name: sendButtonLabel }) diff --git a/src/features/transactions/pages/transaction-page.test.tsx b/src/features/transactions/pages/transaction-page.test.tsx index bf296156..e248b0d4 100644 --- a/src/features/transactions/pages/transaction-page.test.tsx +++ b/src/features/transactions/pages/transaction-page.test.tsx @@ -80,12 +80,24 @@ import { base64ProgramTabLabel, tealProgramTabLabel } from '@/features/applicati import { transactionAmountLabel } from '../components/transactions-table-columns' import { transactionReceiverLabel, transactionSenderLabel } from '../components/labels' import { applicationIdLabel } from '@/features/applications/components/labels' -import { algod } from '@/features/common/data/algo-client' import SampleFiveAppSpec from '@/tests/test-app-specs/sample-five.arc32.json' import { AppSpecStandard, Arc32AppSpec, Arc4AppSpec } from '@/features/app-interfaces/data/types' import { AppInterfaceEntity, dbConnectionAtom } from '@/features/common/data/indexed-db' import { genesisHashAtom } from '@/features/blocks/data' import { writeAppInterface } from '@/features/app-interfaces/data' +import { algod } from '@/features/common/data/algo-client' + +vi.mock('@/features/common/data/algo-client', async () => { + const original = await vi.importActual('@/features/common/data/algo-client') + return { + ...original, + algod: { + disassemble: vi.fn().mockReturnValue({ + do: vi.fn(), + }), + }, + } +}) describe('transaction-page', () => { describe('when rendering a transaction with an invalid id', () => { diff --git a/src/tests/setup/mocks/index.ts b/src/tests/setup/mocks/index.ts index 1b7ced01..7f837099 100644 --- a/src/tests/setup/mocks/index.ts +++ b/src/tests/setup/mocks/index.ts @@ -1,5 +1,4 @@ import { vi } from 'vitest' -import algosdk from 'algosdk' import { PROVIDER_ID, useWallet } from '@txnlab/use-wallet' import { SearchTransactionsMock } from '@/tests/setup/mocks/search-transactions' @@ -18,56 +17,6 @@ vi.mock('@algorandfoundation/algokit-utils', async () => ({ lookupTransactionById: vi.fn(), })) -vi.mock('@/features/common/data/algo-client', async () => { - const original = (await vi.importActual('@/features/common/data/algo-client')) satisfies { - algod: algosdk.Algodv2 - indexer: algosdk.Indexer - } - return { - ...original, - algod: { - ...(original.algod as algosdk.Algodv2), - disassemble: vi.fn().mockReturnValue({ - do: vi.fn(), - }), - getAssetByID: vi.fn().mockReturnValue({ - do: vi.fn().mockReturnValue({ then: vi.fn() }), - }), - accountInformation: vi.fn().mockReturnValue({ - do: vi.fn().mockReturnValue({ then: vi.fn() }), - }), - getApplicationByID: vi.fn().mockReturnValue({ - do: vi.fn().mockReturnValue({ then: vi.fn() }), - }), - }, - indexer: { - ...(original.indexer as algosdk.Indexer), - lookupBlock: vi.fn().mockReturnValue({ - do: vi.fn(), - }), - lookupAssetByID: vi.fn().mockReturnValue({ - includeAll: vi.fn().mockReturnValue({ - do: vi.fn().mockReturnValue({ then: vi.fn() }), - }), - }), - searchForTransactions: vi.fn().mockImplementation(() => searchTransactionsMock), - lookupApplications: vi.fn().mockReturnValue({ - includeAll: vi.fn().mockReturnValue({ - do: vi.fn().mockReturnValue({ then: vi.fn() }), - }), - }), - searchForApplicationBoxes: vi.fn().mockReturnValue({ - nextToken: vi.fn().mockReturnValue({ - limit: vi.fn().mockReturnValue({ - do: vi.fn().mockReturnValue({ then: vi.fn() }), - }), - }), - }), - }, - updateClientConfig: vi.fn(), - } -}) - vi.mock('@txnlab/use-wallet', async () => { const original = await vi.importActual<{ useWallet: () => ReturnType }>('@txnlab/use-wallet') return { diff --git a/src/tests/test-app-specs/sample-six.arc32.json b/src/tests/test-app-specs/sample-six.arc32.json new file mode 100644 index 00000000..1b139d74 --- /dev/null +++ b/src/tests/test-app-specs/sample-six.arc32.json @@ -0,0 +1,73 @@ +{ + "hints": { + "get_string()string": { + "call_config": { + "no_op": "CALL" + } + }, + "get_bytes()byte[]": { + "call_config": { + "no_op": "CALL" + } + }, + "get_number()uint64": { + "call_config": { + "no_op": "CALL" + } + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMuc2FtcGxlX3NpeC5jb250cmFjdC5TYW1wbGVTaXguYXBwcm92YWxfcHJvZ3JhbToKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9zYW1wbGVfc2l4L2NvbnRyYWN0LnB5OjE1CiAgICAvLyBjbGFzcyBTYW1wbGVTaXgoQVJDNENvbnRyYWN0KToKICAgIHR4biBOdW1BcHBBcmdzCiAgICBieiBtYWluX2JhcmVfcm91dGluZ0A3CiAgICBtZXRob2QgImdldF9zdHJpbmcoKXN0cmluZyIKICAgIG1ldGhvZCAiZ2V0X2J5dGVzKClieXRlW10iCiAgICBtZXRob2QgImdldF9udW1iZXIoKXVpbnQ2NCIKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDAKICAgIG1hdGNoIG1haW5fZ2V0X3N0cmluZ19yb3V0ZUAyIG1haW5fZ2V0X2J5dGVzX3JvdXRlQDMgbWFpbl9nZXRfbnVtYmVyX3JvdXRlQDQKICAgIGVyciAvLyByZWplY3QgdHJhbnNhY3Rpb24KCm1haW5fZ2V0X3N0cmluZ19yb3V0ZUAyOgogICAgLy8gc21hcnRfY29udHJhY3RzL3NhbXBsZV9zaXgvY29udHJhY3QucHk6MTYKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgdHhuIE9uQ29tcGxldGlvbgogICAgIQogICAgYXNzZXJ0IC8vIE9uQ29tcGxldGlvbiBpcyBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGlzIG5vdCBjcmVhdGluZwogICAgY2FsbHN1YiBnZXRfc3RyaW5nCiAgICBkdXAKICAgIGxlbgogICAgaXRvYgogICAgZXh0cmFjdCA2IDIKICAgIHN3YXAKICAgIGNvbmNhdAogICAgYnl0ZSAweDE1MWY3Yzc1CiAgICBzd2FwCiAgICBjb25jYXQKICAgIGxvZwogICAgaW50IDEKICAgIHJldHVybgoKbWFpbl9nZXRfYnl0ZXNfcm91dGVAMzoKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9zYW1wbGVfc2l4L2NvbnRyYWN0LnB5OjIwCiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgTm9PcAogICAgdHhuIEFwcGxpY2F0aW9uSUQKICAgIGFzc2VydCAvLyBpcyBub3QgY3JlYXRpbmcKICAgIGNhbGxzdWIgZ2V0X2J5dGVzCiAgICBkdXAKICAgIGxlbgogICAgaXRvYgogICAgZXh0cmFjdCA2IDIKICAgIHN3YXAKICAgIGNvbmNhdAogICAgYnl0ZSAweDE1MWY3Yzc1CiAgICBzd2FwCiAgICBjb25jYXQKICAgIGxvZwogICAgaW50IDEKICAgIHJldHVybgoKbWFpbl9nZXRfbnVtYmVyX3JvdXRlQDQ6CiAgICAvLyBzbWFydF9jb250cmFjdHMvc2FtcGxlX3NpeC9jb250cmFjdC5weToyNAogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gT25Db21wbGV0aW9uIGlzIE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gaXMgbm90IGNyZWF0aW5nCiAgICBjYWxsc3ViIGdldF9udW1iZXIKICAgIGl0b2IKICAgIGJ5dGUgMHgxNTFmN2M3NQogICAgc3dhcAogICAgY29uY2F0CiAgICBsb2cKICAgIGludCAxCiAgICByZXR1cm4KCm1haW5fYmFyZV9yb3V0aW5nQDc6CiAgICAvLyBzbWFydF9jb250cmFjdHMvc2FtcGxlX3NpeC9jb250cmFjdC5weToxNQogICAgLy8gY2xhc3MgU2FtcGxlU2l4KEFSQzRDb250cmFjdCk6CiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gcmVqZWN0IHRyYW5zYWN0aW9uCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgIQogICAgYXNzZXJ0IC8vIGlzIGNyZWF0aW5nCiAgICBpbnQgMQogICAgcmV0dXJuCgoKLy8gc21hcnRfY29udHJhY3RzLnNhbXBsZV9zaXguY29udHJhY3QuU2FtcGxlU2l4LmdldF9zdHJpbmcoKSAtPiBieXRlczoKZ2V0X3N0cmluZzoKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9zYW1wbGVfc2l4L2NvbnRyYWN0LnB5OjE2LTE3CiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIC8vIGRlZiBnZXRfc3RyaW5nKHNlbGYpIC0+IFN0cmluZzoKICAgIHByb3RvIDAgMQogICAgLy8gc21hcnRfY29udHJhY3RzL3NhbXBsZV9zaXgvY29udHJhY3QucHk6MTgKICAgIC8vIHJldHVybiBUZW1wbGF0ZVZhcltTdHJpbmddKCJTT01FX1NUUklORyIpCiAgICBieXRlIFRNUExfU09NRV9TVFJJTkcKICAgIHJldHN1YgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy5zYW1wbGVfc2l4LmNvbnRyYWN0LlNhbXBsZVNpeC5nZXRfYnl0ZXMoKSAtPiBieXRlczoKZ2V0X2J5dGVzOgogICAgLy8gc21hcnRfY29udHJhY3RzL3NhbXBsZV9zaXgvY29udHJhY3QucHk6MjAtMjEKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgLy8gZGVmIGdldF9ieXRlcyhzZWxmKSAtPiBCeXRlczoKICAgIHByb3RvIDAgMQogICAgLy8gc21hcnRfY29udHJhY3RzL3NhbXBsZV9zaXgvY29udHJhY3QucHk6MjIKICAgIC8vIHJldHVybiBUZW1wbGF0ZVZhcltCeXRlc10oIlNPTUVfQllURVMiKQogICAgYnl0ZSBUTVBMX1NPTUVfQllURVMKICAgIHJldHN1YgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy5zYW1wbGVfc2l4LmNvbnRyYWN0LlNhbXBsZVNpeC5nZXRfbnVtYmVyKCkgLT4gdWludDY0OgpnZXRfbnVtYmVyOgogICAgLy8gc21hcnRfY29udHJhY3RzL3NhbXBsZV9zaXgvY29udHJhY3QucHk6MjQtMjUKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgLy8gZGVmIGdldF9udW1iZXIoc2VmdCkgLT4gVUludDY0OgogICAgcHJvdG8gMCAxCiAgICAvLyBzbWFydF9jb250cmFjdHMvc2FtcGxlX3NpeC9jb250cmFjdC5weToyNgogICAgLy8gcmV0dXJuIFRlbXBsYXRlVmFyW1VJbnQ2NF0oIlNPTUVfTlVNQkVSIikKICAgIGludCBUTVBMX1NPTUVfTlVNQkVSCiAgICByZXRzdWIK", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMuc2FtcGxlX3NpeC5jb250cmFjdC5TYW1wbGVTaXguY2xlYXJfc3RhdGVfcHJvZ3JhbToKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9zYW1wbGVfc2l4L2NvbnRyYWN0LnB5OjE1CiAgICAvLyBjbGFzcyBTYW1wbGVTaXgoQVJDNENvbnRyYWN0KToKICAgIGludCAxCiAgICByZXR1cm4K" + }, + "state": { + "global": { + "num_byte_slices": 0, + "num_uints": 0 + }, + "local": { + "num_byte_slices": 0, + "num_uints": 0 + } + }, + "schema": { + "global": { + "declared": {}, + "reserved": {} + }, + "local": { + "declared": {}, + "reserved": {} + } + }, + "contract": { + "name": "SampleSix", + "methods": [ + { + "name": "get_string", + "args": [], + "returns": { + "type": "string" + } + }, + { + "name": "get_bytes", + "args": [], + "returns": { + "type": "byte[]" + } + }, + { + "name": "get_number", + "args": [], + "returns": { + "type": "uint64" + } + } + ], + "networks": {} + }, + "bare_call_config": { + "no_op": "CREATE" + } +} \ No newline at end of file diff --git a/src/tests/utils/select-option.ts b/src/tests/utils/select-option.ts new file mode 100644 index 00000000..d8a952db --- /dev/null +++ b/src/tests/utils/select-option.ts @@ -0,0 +1,21 @@ +import { UserEvent } from '@testing-library/user-event' +import { getByRole, screen, waitFor, within } from '@/tests/testing-library' +import { expect } from 'vitest' + +export const selectOption = async (parentComponent: HTMLElement, user: UserEvent, name: string | RegExp, value: string) => { + const select = await waitFor(() => { + const select = getByRole(parentComponent, 'combobox', { name: name }) + expect(select).toBeDefined() + return select! + }) + await user.click(select) + + const option = await waitFor(() => { + return screen.getByRole('option', { name: value }) + }) + await user.click(option) + + await waitFor(() => { + expect(within(select).getByText(value)).toBeInTheDocument() + }) +} diff --git a/src/tests/utils/set-wallet-address-and-signer.ts b/src/tests/utils/set-wallet-address-and-signer.ts new file mode 100644 index 00000000..16897a55 --- /dev/null +++ b/src/tests/utils/set-wallet-address-and-signer.ts @@ -0,0 +1,19 @@ +import { AlgorandFixture } from '@algorandfoundation/algokit-utils/types/testing' +import { vi } from 'vitest' +import { useWallet } from '@txnlab/use-wallet' + +export const setWalletAddressAndSigner = async (localnet: AlgorandFixture) => { + const { testAccount } = localnet.context + + const original = await vi.importActual<{ useWallet: () => ReturnType }>('@txnlab/use-wallet') + vi.mocked(useWallet).mockImplementation(() => { + return { + ...original.useWallet(), + activeAddress: testAccount.addr, + signer: testAccount.signer, + status: 'active', + isActive: true, + isReady: true, + } + }) +} diff --git a/vite.config.ts b/vite.config.ts index 3d80f5b6..c8e6db1f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -17,7 +17,7 @@ export default defineConfig({ test: { testTimeout: 20_000, environment: 'happy-dom', - setupFiles: ['src/tests/setup/index.ts', 'src/tests/setup/mocks/index.ts', 'fake-indexeddb/auto'], + setupFiles: ['src/tests/setup/mocks/index.ts', 'src/tests/setup/index.ts', 'fake-indexeddb/auto'], globals: true, globalSetup: ['src/tests/setup/setup-timezone.ts'], env: {