From 92169f3e58fa81c10cf7b098a63d3a03d775dff0 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Sat, 31 Aug 2024 02:32:07 -0700 Subject: [PATCH] eslint/prettier integration --- .eslintrc.json | 8 +- .prettierrc | 11 +- .vscode/settings.json | 13 +- bun.lockb | Bin 581330 -> 583682 bytes .../__tests__/adminOptions.test.tsx | 58 ++- .../__tests__/stakingParams.test.tsx | 92 ++-- .../__tests__/validatorList.test.tsx | 62 ++- components/admins/components/adminOptions.tsx | 72 ++- components/admins/components/index.tsx | 6 +- .../admins/components/stakingParams.tsx | 31 +- .../admins/components/validatorList.tsx | 71 ++- components/admins/index.tsx | 4 +- .../__tests__/descriptionModal.test.tsx | 28 +- .../__tests__/updateAdminModal.test.tsx | 56 +-- .../updateStakingParamsModal.test.tsx | 58 +-- .../modals/__tests__/validatorModal.test.tsx | 49 +- .../modals/__tests__/warningModal.test.tsx | 38 +- components/admins/modals/descriptionModal.tsx | 14 +- components/admins/modals/index.tsx | 6 +- components/admins/modals/updateAdminModal.tsx | 45 +- .../modals/updateStakingParamsModal.tsx | 119 +++-- components/admins/modals/validatorModal.tsx | 91 ++-- components/admins/modals/warningModal.tsx | 42 +- .../components/__tests__/historyBox.test.tsx | 103 ++--- .../components/__tests__/sendBox.test.tsx | 72 ++- .../components/__tests__/tokenList.test.tsx | 56 +-- components/bank/components/historyBox.tsx | 41 +- components/bank/components/index.ts | 10 +- components/bank/components/sendBox.tsx | 39 +- components/bank/components/tokenList.tsx | 32 +- .../bank/forms/__tests__/ibcSendForm.test.tsx | 78 ++-- .../bank/forms/__tests__/sendForm.test.tsx | 83 ++-- components/bank/forms/ibcSendForm.tsx | 74 ++- components/bank/forms/index.ts | 4 +- components/bank/forms/sendForm.tsx | 71 ++- components/bank/index.ts | 6 +- components/bank/modals/index.ts | 2 +- components/bank/modals/txInfo.tsx | 39 +- components/factory/components/DenomImage.tsx | 53 ++- components/factory/components/DenomInfo.tsx | 67 +-- components/factory/components/MyDenoms.tsx | 33 +- .../components/__tests__/DenomImage.test.tsx | 64 ++- .../components/__tests__/DenomInfo.test.tsx | 76 ++- .../components/__tests__/MyDenoms.test.tsx | 44 +- .../components/__tests__/metaBox.test.tsx | 58 +-- components/factory/components/index.ts | 8 +- components/factory/components/metaBox.tsx | 78 ++-- components/factory/forms/BurnForm.tsx | 126 ++--- components/factory/forms/ConfirmationForm.tsx | 69 ++- components/factory/forms/CreateDenom.tsx | 53 +-- components/factory/forms/MintForm.tsx | 130 ++---- components/factory/forms/Success.tsx | 29 +- components/factory/forms/TokenDetailsForm.tsx | 106 ++--- components/factory/forms/TransferForm.tsx | 52 +-- .../factory/forms/__tests__/BurnForm.test.tsx | 68 +-- .../forms/__tests__/ConfirmationForm.test.tsx | 66 ++- .../forms/__tests__/CreateDenom.test.tsx | 36 +- .../factory/forms/__tests__/MintForm.test.tsx | 60 +-- .../factory/forms/__tests__/Success.test.tsx | 52 +-- .../forms/__tests__/TokenDetailsForm.test.tsx | 120 +++-- .../forms/__tests__/TransferForm.test.tsx | 64 +-- components/factory/forms/index.ts | 14 +- components/factory/index.ts | 6 +- components/factory/modals/denomInfo.tsx | 36 +- components/factory/modals/index.ts | 4 +- .../factory/modals/multiMfxBurnModal.tsx | 41 +- .../factory/modals/multiMfxMintModal.tsx | 45 +- .../factory/modals/updateDenomMetadata.tsx | 101 ++-- .../groups/components/CountdownTimer.tsx | 14 +- .../groups/components/Notifications.tsx | 14 +- .../groups/components/StepIndicator.tsx | 7 +- .../__tests__/CountdownTimer.test.tsx | 70 ++- .../__tests__/StepIndicator.test.tsx | 44 +- .../components/__tests__/groupInfo.test.tsx | 76 ++- .../__tests__/groupProposals.test.tsx | 60 +-- .../components/__tests__/myGroups.test.tsx | 50 +- components/groups/components/groupInfo.tsx | 90 ++-- .../groups/components/groupProposals.tsx | 384 +++++++--------- components/groups/components/index.tsx | 10 +- components/groups/components/myGroups.tsx | 64 +-- .../groups/forms/groups/ConfirmationForm.tsx | 97 ++-- .../groups/forms/groups/GroupDetailsForm.tsx | 58 +-- .../groups/forms/groups/GroupPolicyForm.tsx | 47 +- .../groups/forms/groups/MemberInfoForm.tsx | 60 +-- components/groups/forms/groups/Success.tsx | 34 +- .../__tests__/ConfirmationForm.test.tsx | 62 +-- .../__tests__/GroupDetailsForm.test.tsx | 104 ++--- .../groups/__tests__/GroupPolicyForm.test.tsx | 62 ++- .../groups/__tests__/MemberInfoForm.test.tsx | 112 +++-- .../forms/groups/__tests__/Success.test.tsx | 32 +- components/groups/forms/groups/index.tsx | 8 +- components/groups/forms/index.tsx | 4 +- .../forms/proposals/ConfirmationForm.tsx | 149 +++--- .../forms/proposals/ProposalDetailsForm.tsx | 46 +- .../forms/proposals/ProposalMessages.tsx | 270 +++++------ .../forms/proposals/ProposalMetadataForm.tsx | 55 +-- .../groups/forms/proposals/SuccessForm.tsx | 35 +- .../__tests__/ConfirmationForm.test.tsx | 72 ++- .../__tests__/ProposalDetailsForm.test.tsx | 102 ++--- .../__tests__/ProposalMessages.test.tsx | 82 ++-- .../__tests__/ProposalMetadataForm.test.tsx | 102 ++--- .../proposals/__tests__/SuccessForm.test.tsx | 54 +-- components/groups/forms/proposals/index.tsx | 10 +- components/groups/forms/proposals/messages.ts | 138 +++--- components/groups/index.tsx | 6 +- .../groups/modals/groupDetailsModal.tsx | 82 ++-- components/groups/modals/index.tsx | 10 +- components/groups/modals/updateGroupModal.tsx | 280 ++++-------- components/groups/modals/voteDetailsModal.tsx | 311 +++++-------- components/groups/modals/voteModal.tsx | 26 +- components/index.tsx | 14 +- components/react/addressCopy.tsx | 26 +- components/react/authSignerModal.tsx | 77 ++-- components/react/chain-card.tsx | 2 +- components/react/endpointSelector.tsx | 129 +++--- components/react/header.tsx | 2 +- components/react/index.ts | 10 +- components/react/inputs/BaseInput.tsx | 11 +- components/react/inputs/NumberInput.tsx | 10 +- components/react/inputs/TextArea.tsx | 4 +- components/react/inputs/TextInput.tsx | 10 +- .../inputs/__tests__/NumberInput.test.tsx | 44 +- .../react/inputs/__tests__/TextArea.test.tsx | 44 +- .../react/inputs/__tests__/TextInput.test.tsx | 46 +- components/react/inputs/index.ts | 6 +- components/react/mobileNav.tsx | 42 +- components/react/modal.tsx | 49 +- components/react/scrollableFade.tsx | 20 +- components/react/settingsModal.tsx | 4 +- components/react/sideNav.tsx | 51 +-- components/react/views/Connected.tsx | 33 +- components/react/views/Connecting.tsx | 24 +- components/react/views/Error.tsx | 14 +- components/react/views/NotExist.tsx | 16 +- components/react/views/QRCode.tsx | 16 +- components/react/views/WalletList.tsx | 49 +- components/react/views/index.ts | 12 +- components/toast.tsx | 44 +- components/types.tsx | 12 +- components/wallet.tsx | 38 +- config/defaults.ts | 156 +++---- config/index.ts | 2 +- contexts/advancedModeContext.tsx | 30 +- contexts/index.ts | 6 +- contexts/theme.tsx | 41 +- contexts/toastContext.tsx | 10 +- happydom.ts | 2 +- helpers/formReducer.tsx | 105 ++--- helpers/index.tsx | 4 +- hooks/index.tsx | 12 +- hooks/useAbly.ts | 8 +- hooks/useFeeEstimation.ts | 22 +- hooks/useIpfs.ts | 31 +- hooks/useLcdQueryClient.ts | 16 +- hooks/useManifestLcdQueryClient.ts | 16 +- hooks/usePoaLcdQueryClient.ts | 16 +- hooks/useQueries.ts | 270 +++++------ hooks/useTx.tsx | 65 ++- next.config.js | 70 +-- package.json | 5 +- pages/404.tsx | 88 ++-- pages/_app.tsx | 145 +++--- pages/admins.tsx | 114 ++--- pages/bank.tsx | 120 +++-- pages/factory/create.tsx | 105 ++--- pages/factory/index.tsx | 69 ++- pages/governance.tsx | 2 +- pages/groups/create.tsx | 100 ++-- pages/groups/index.tsx | 155 +++---- .../submit-proposal/[policyAddress].tsx | 113 ++--- pages/index.tsx | 4 +- postcss.config.js | 2 +- store/endpointStore.ts | 68 ++- styles/Home.module.css | 12 +- styles/globals.css | 8 +- tailwind.config.js | 302 ++++++------ tests/mock.ts | 432 +++++++++--------- tests/render.tsx | 19 +- utils/ibc.ts | 30 +- utils/identicon.tsx | 25 +- utils/index.ts | 14 +- utils/logos.ts | 72 ++- utils/maths.ts | 41 +- utils/staking.ts | 93 ++-- utils/string.ts | 6 +- utils/voting.ts | 40 +- 186 files changed, 4634 insertions(+), 6127 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index bffb357a..103ced7a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,3 +1,9 @@ { - "extends": "next/core-web-vitals" + "extends": ["next/core-web-vitals", "prettier"], + "plugins": ["prettier"], + "rules": { + "prettier/prettier": "error", + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": "off" + } } diff --git a/.prettierrc b/.prettierrc index 0967ef42..81dcf5c2 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1 +1,10 @@ -{} +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "bracketSpacing": true, + "arrowParens": "avoid" +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 25fa6215..556a9217 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,14 @@ { - "typescript.tsdk": "node_modules/typescript/lib" + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + }, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "eslint.validate": ["typescript", "typescriptreact"] } diff --git a/bun.lockb b/bun.lockb index 31c1d42e59fe154234e2ef3c9a37f5d0641d42b2..9d2338b86da8aae9f4822ee236c7566f048e3227 100755 GIT binary patch delta 96657 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 a{%Iw?!!_+?@y`lX><*0eHl9{Y`hNj2Bj0QQ delta 95741 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¬qw%=OtoawFuVcWxGcOwRs75a!{R@@NdD% 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 { poaParams: mockPoaParams, group: mockGroup, isLoading: false, - address: "test_address", - admin: "admin1", + address: 'test_address', + admin: 'admin1', }; return renderWithChainProvider(); }; -describe("AdminOptions", () => { +describe('AdminOptions', () => { afterEach(cleanup); - test("renders loading state correctly", () => { + test('renders loading state correctly', () => { renderWithProps({ isLoading: true }); - expect(screen.getByText("Admin")).toBeInTheDocument(); + expect(screen.getByText('Admin')).toBeInTheDocument(); }); - test("renders admin details correctly when not loading", () => { + test('renders admin details correctly when not loading', () => { renderWithProps(); - expect(screen.getByText("Admin")).toBeInTheDocument(); - expect(screen.getByAltText("Profile Avatar")).toBeInTheDocument(); - const titleContainer = screen.getByLabelText("title"); - expect(within(titleContainer).getByText("title1")).toBeInTheDocument(); - const detailsContainer = screen.getByLabelText("details"); - expect(within(detailsContainer).getByText("details1")).toBeInTheDocument(); + expect(screen.getByText('Admin')).toBeInTheDocument(); + expect(screen.getByAltText('Profile Avatar')).toBeInTheDocument(); + const titleContainer = screen.getByLabelText('title'); + expect(within(titleContainer).getByText('title1')).toBeInTheDocument(); + const detailsContainer = screen.getByLabelText('details'); + expect(within(detailsContainer).getByText('details1')).toBeInTheDocument(); }); - test("opens update modal on button click", () => { + test('opens update modal on button click', () => { renderWithProps(); - const updateAdminButtonContainer = screen.getByLabelText("update admin"); - fireEvent.click( - within(updateAdminButtonContainer).getByText("Update Admin"), - ); - const modal = document.getElementById( - "update-admin-modal", - ) as HTMLDialogElement; + const updateAdminButtonContainer = screen.getByLabelText('update admin'); + fireEvent.click(within(updateAdminButtonContainer).getByText('Update Admin')); + const modal = document.getElementById('update-admin-modal') as HTMLDialogElement; expect(modal).toBeInTheDocument(); expect(modal.open).toBe(true); }); - test("opens description modal on button click", () => { + test('opens description modal on button click', () => { renderWithProps(); - fireEvent.click(screen.getByLabelText("three-dots")); - const modal = document.getElementById( - "description-modal", - ) as HTMLDialogElement; + fireEvent.click(screen.getByLabelText('three-dots')); + const modal = document.getElementById('description-modal') as HTMLDialogElement; expect(modal).toBeInTheDocument(); expect(modal.open).toBe(true); }); diff --git a/components/admins/components/__tests__/stakingParams.test.tsx b/components/admins/components/__tests__/stakingParams.test.tsx index 80c421db..ad214d84 100644 --- a/components/admins/components/__tests__/stakingParams.test.tsx +++ b/components/admins/components/__tests__/stakingParams.test.tsx @@ -1,10 +1,10 @@ -import { afterEach, describe, expect, test } from "bun:test"; -import React from "react"; -import { screen, cleanup, within, fireEvent } from "@testing-library/react"; -import StakingParams from "@/components/admins/components/stakingParams"; -import matchers from "@testing-library/jest-dom/matchers"; -import { mockStakingParams } from "@/tests/mock"; -import { renderWithChainProvider } from "@/tests/render"; +import { afterEach, describe, expect, test } from 'bun:test'; +import React from 'react'; +import { screen, cleanup, within, fireEvent } from '@testing-library/react'; +import StakingParams from '@/components/admins/components/stakingParams'; +import matchers from '@testing-library/jest-dom/matchers'; +import { mockStakingParams } from '@/tests/mock'; +import { renderWithChainProvider } from '@/tests/render'; expect.extend(matchers); @@ -12,70 +12,44 @@ const renderWithProps = (props = {}) => { const defaultProps = { stakingParams: mockStakingParams, isLoading: false, - address: "test_address", - admin: "admin1", + address: 'test_address', + admin: 'admin1', }; - return renderWithChainProvider( - , - ); + return renderWithChainProvider(); }; -describe("StakingParams", () => { +describe('StakingParams', () => { afterEach(cleanup); - test("renders correctly when not loading", () => { + test('renders correctly when not loading', () => { renderWithProps(); - const stakingParamsContainer = screen.getByLabelText("Staking Params"); - expect( - within(stakingParamsContainer).getByText("UNBONDING TIME"), - ).toBeInTheDocument(); - expect(within(stakingParamsContainer).getByText("1")).toBeInTheDocument(); - expect( - within(stakingParamsContainer).getByText("MAX VALIDATORS"), - ).toBeInTheDocument(); - expect(within(stakingParamsContainer).getByText("100")).toBeInTheDocument(); - expect( - within(stakingParamsContainer).getByText("BOND DENOM"), - ).toBeInTheDocument(); - expect( - within(stakingParamsContainer).getByText("upoa"), - ).toBeInTheDocument(); - expect( - within(stakingParamsContainer).getByText("MINIMUM COMMISSION"), - ).toBeInTheDocument(); - expect(within(stakingParamsContainer).getByText("5 %")).toBeInTheDocument(); - expect( - within(stakingParamsContainer).getByText("MAX ENTRIES"), - ).toBeInTheDocument(); - expect(within(stakingParamsContainer).getByText("7")).toBeInTheDocument(); - expect( - within(stakingParamsContainer).getByText("HISTORICAL ENTRIES"), - ).toBeInTheDocument(); - expect(within(stakingParamsContainer).getByText("200")).toBeInTheDocument(); + const stakingParamsContainer = screen.getByLabelText('Staking Params'); + expect(within(stakingParamsContainer).getByText('UNBONDING TIME')).toBeInTheDocument(); + expect(within(stakingParamsContainer).getByText('1')).toBeInTheDocument(); + expect(within(stakingParamsContainer).getByText('MAX VALIDATORS')).toBeInTheDocument(); + expect(within(stakingParamsContainer).getByText('100')).toBeInTheDocument(); + expect(within(stakingParamsContainer).getByText('BOND DENOM')).toBeInTheDocument(); + expect(within(stakingParamsContainer).getByText('upoa')).toBeInTheDocument(); + expect(within(stakingParamsContainer).getByText('MINIMUM COMMISSION')).toBeInTheDocument(); + expect(within(stakingParamsContainer).getByText('5 %')).toBeInTheDocument(); + expect(within(stakingParamsContainer).getByText('MAX ENTRIES')).toBeInTheDocument(); + expect(within(stakingParamsContainer).getByText('7')).toBeInTheDocument(); + expect(within(stakingParamsContainer).getByText('HISTORICAL ENTRIES')).toBeInTheDocument(); + expect(within(stakingParamsContainer).getByText('200')).toBeInTheDocument(); }); - test("renders loading state correctly", () => { + test('renders loading state correctly', () => { renderWithProps({ isLoading: true }); - const stakingParamsContainer = screen.getByLabelText( - "Skeleton Staking Params", - ); - expect( - within(stakingParamsContainer).getByText("Staking Params"), - ).toBeInTheDocument(); - expect( - within(stakingParamsContainer).getByText("Update"), - ).toBeInTheDocument(); + const stakingParamsContainer = screen.getByLabelText('Skeleton Staking Params'); + expect(within(stakingParamsContainer).getByText('Staking Params')).toBeInTheDocument(); + expect(within(stakingParamsContainer).getByText('Update')).toBeInTheDocument(); }); - test("opens update modal on button click", () => { + test('opens update modal on button click', () => { renderWithProps(); - const stakingParamsContainer = screen.getByLabelText( - "Skeleton Staking Params", - ); - fireEvent.click(within(stakingParamsContainer).getByText("Update")); - const modal = document.getElementById( - "update-params-modal", - ) as HTMLDialogElement; + const stakingParamsContainer = screen.getByLabelText('Skeleton Staking Params'); + fireEvent.click(within(stakingParamsContainer).getByText('Update')); + const modal = document.getElementById('update-params-modal') as HTMLDialogElement; expect(modal).toBeInTheDocument(); expect(modal.open).toBe(true); }); diff --git a/components/admins/components/__tests__/validatorList.test.tsx b/components/admins/components/__tests__/validatorList.test.tsx index 7e828dc5..747171c6 100644 --- a/components/admins/components/__tests__/validatorList.test.tsx +++ b/components/admins/components/__tests__/validatorList.test.tsx @@ -1,65 +1,61 @@ -import { afterEach, describe, expect, test } from "bun:test"; -import React from "react"; -import ValidatorList from "@/components/admins/components/validatorList"; -import { fireEvent, screen, cleanup, waitFor } from "@testing-library/react"; -import matchers from "@testing-library/jest-dom/matchers"; -import { mockActiveValidators, mockPendingValidators } from "@/tests/mock"; -import { renderWithChainProvider } from "@/tests/render"; +import { afterEach, describe, expect, test } from 'bun:test'; +import React from 'react'; +import ValidatorList from '@/components/admins/components/validatorList'; +import { fireEvent, screen, cleanup, waitFor } from '@testing-library/react'; +import matchers from '@testing-library/jest-dom/matchers'; +import { mockActiveValidators, mockPendingValidators } from '@/tests/mock'; +import { renderWithChainProvider } from '@/tests/render'; expect.extend(matchers); const renderWithProps = (props = {}) => { const defaultProps = { - admin: "admin1", + admin: 'admin1', activeValidators: mockActiveValidators, pendingValidators: mockPendingValidators, isLoading: false, }; - return renderWithChainProvider( - , - ); + return renderWithChainProvider(); }; -describe("ValidatorList", () => { +describe('ValidatorList', () => { afterEach(cleanup); - test("renders correctly", () => { + test('renders correctly', () => { renderWithProps(); - expect(screen.getByText("Active Validators")).toBeInTheDocument(); - expect(screen.getByText("Validator One")).toBeInTheDocument(); - expect(screen.getByText("Validator Two")).toBeInTheDocument(); + expect(screen.getByText('Active Validators')).toBeInTheDocument(); + expect(screen.getByText('Validator One')).toBeInTheDocument(); + expect(screen.getByText('Validator Two')).toBeInTheDocument(); }); - test("search functionality works", () => { + test('search functionality works', () => { renderWithProps(); - fireEvent.change(screen.getByPlaceholderText("Search for a validator..."), { - target: { value: "Validator One" }, + fireEvent.change(screen.getByPlaceholderText('Search for a validator...'), { + target: { value: 'Validator One' }, }); - expect(screen.getByText("Validator One")).toBeInTheDocument(); - expect(screen.queryByText("Validator Two")).not.toBeInTheDocument(); + expect(screen.getByText('Validator One')).toBeInTheDocument(); + expect(screen.queryByText('Validator Two')).not.toBeInTheDocument(); }); - test("active/pending toggle works", () => { + test('active/pending toggle works', () => { renderWithProps(); - fireEvent.click(screen.getByText("Pending")); - expect(screen.getByText("Pending Validators")).toBeInTheDocument(); - expect(screen.getByText("Validator Three")).toBeInTheDocument(); + fireEvent.click(screen.getByText('Pending')); + expect(screen.getByText('Pending Validators')).toBeInTheDocument(); + expect(screen.getByText('Validator Three')).toBeInTheDocument(); }); - test("clicking on a validator row opens the modal", async () => { + test('clicking on a validator row opens the modal', async () => { renderWithProps(); - fireEvent.click(screen.getByText("Validator One")); - await waitFor(() => expect(screen.getByRole("dialog")).toBeInTheDocument()); + fireEvent.click(screen.getByText('Validator One')); + await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); }); - test("remove button works and shows the warning modal", async () => { + test('remove button works and shows the warning modal', async () => { renderWithProps(); - const allRemoveButtons = screen.getAllByText("Remove"); + const allRemoveButtons = screen.getAllByText('Remove'); fireEvent.click(allRemoveButtons[0]); await waitFor(() => - expect( - screen.getByText("Are you sure you want to remove the validator"), - ).toBeInTheDocument(), + expect(screen.getByText('Are you sure you want to remove the validator')).toBeInTheDocument() ); }); }); diff --git a/components/admins/components/adminOptions.tsx b/components/admins/components/adminOptions.tsx index c54c0db6..415aa92e 100644 --- a/components/admins/components/adminOptions.tsx +++ b/components/admins/components/adminOptions.tsx @@ -1,16 +1,16 @@ -import React, { useEffect, useState } from "react"; -import { ExtendedGroupType, useFeeEstimation, useTx } from "@/hooks"; -import { ParamsSDKType } from "@chalabi/manifestjs/dist/codegen/strangelove_ventures/poa/v1/params"; -import { UpdateAdminModal } from "../modals/updateAdminModal"; +import React, { useEffect, useState } from 'react'; +import { ExtendedGroupType, useFeeEstimation, useTx } from '@/hooks'; +import { ParamsSDKType } from '@chalabi/manifestjs/dist/codegen/strangelove_ventures/poa/v1/params'; +import { UpdateAdminModal } from '../modals/updateAdminModal'; -import { BsThreeDots } from "react-icons/bs"; -import { DescriptionModal } from "../modals/descriptionModal"; -import ProfileAvatar from "@/utils/identicon"; +import { BsThreeDots } from 'react-icons/bs'; +import { DescriptionModal } from '../modals/descriptionModal'; +import ProfileAvatar from '@/utils/identicon'; -import { chainName } from "@/config"; -import { strangelove_ventures, cosmos } from "@chalabi/manifestjs"; -import { MsgUpdateParams } from "@chalabi/manifestjs/dist/codegen/strangelove_ventures/poa/v1/tx"; -import { Any } from "@chalabi/manifestjs/dist/codegen/google/protobuf/any"; +import { chainName } from '@/config'; +import { strangelove_ventures, cosmos } from '@chalabi/manifestjs'; +import { MsgUpdateParams } from '@chalabi/manifestjs/dist/codegen/strangelove_ventures/poa/v1/tx'; +import { Any } from '@chalabi/manifestjs/dist/codegen/google/protobuf/any'; interface AdminOptionsProps { poaParams: ParamsSDKType; @@ -30,32 +30,26 @@ export default function AdminOptions({ const exitEnabled = true; const handleOpen = () => { - const modal = document.getElementById( - `update-admin-modal`, - ) as HTMLDialogElement; + const modal = document.getElementById(`update-admin-modal`) as HTMLDialogElement; modal?.showModal(); }; const handleDescription = () => { - const modal = document.getElementById( - `description-modal`, - ) as HTMLDialogElement; + const modal = document.getElementById(`description-modal`) as HTMLDialogElement; modal?.showModal(); }; const { estimateFee } = useFeeEstimation(chainName); const { tx } = useTx(chainName); - const { updateParams } = - strangelove_ventures.poa.v1.MessageComposer.withTypeUrl; + const { updateParams } = strangelove_ventures.poa.v1.MessageComposer.withTypeUrl; const { submitProposal } = cosmos.group.v1.MessageComposer.withTypeUrl; const handleUpdate = async () => { const msgUpdateAdmin = updateParams({ - sender: admin ?? "", + sender: admin ?? '', params: { admins: poaParams.admins, - allowValidatorSelfExit: - poaParams.allow_validator_self_exit === true ? false : true, + allowValidatorSelfExit: poaParams.allow_validator_self_exit === true ? false : true, }, }); @@ -67,16 +61,16 @@ export default function AdminOptions({ const groupProposalMsg = submitProposal({ groupPolicyAddress: admin, messages: [anyMessage], - metadata: "", - proposers: [address ?? ""], + metadata: '', + proposers: [address ?? ''], title: `Update Self Exit`, summary: `This proposal will ${ - poaParams.allow_validator_self_exit === true ? "enable" : "disable" + poaParams.allow_validator_self_exit === true ? 'enable' : 'disable' } the ability to leave the active set.`, exec: 0, }); - const fee = await estimateFee(address ?? "", [groupProposalMsg]); + const fee = await estimateFee(address ?? '', [groupProposalMsg]); await tx([groupProposalMsg], { fee, onSuccess: () => {}, @@ -92,10 +86,7 @@ export default function AdminOptions({ {!isLoading && ( )} ); diff --git a/components/admins/components/index.tsx b/components/admins/components/index.tsx index a92e5627..c20c7a2f 100644 --- a/components/admins/components/index.tsx +++ b/components/admins/components/index.tsx @@ -1,3 +1,3 @@ -export * from "./adminOptions"; -export * from "./stakingParams"; -export * from "./validatorList"; +export * from './adminOptions'; +export * from './stakingParams'; +export * from './validatorList'; diff --git a/components/admins/components/stakingParams.tsx b/components/admins/components/stakingParams.tsx index ad625954..e4cd7dbb 100644 --- a/components/admins/components/stakingParams.tsx +++ b/components/admins/components/stakingParams.tsx @@ -1,6 +1,6 @@ -import React from "react"; -import { ParamsSDKType } from "@chalabi/manifestjs/dist/codegen/cosmos/staking/v1beta1/staking"; -import { UpdateStakingParamsModal } from "../modals/updateStakingParamsModal"; +import React from 'react'; +import { ParamsSDKType } from '@chalabi/manifestjs/dist/codegen/cosmos/staking/v1beta1/staking'; +import { UpdateStakingParamsModal } from '../modals/updateStakingParamsModal'; interface StakingParamsProps { stakingParams: ParamsSDKType; @@ -16,9 +16,7 @@ export default function StakingParams({ admin, }: StakingParamsProps) { const openParamsModal = () => { - const modal = document.getElementById( - `update-params-modal`, - ) as HTMLDialogElement; + const modal = document.getElementById(`update-params-modal`) as HTMLDialogElement; modal?.showModal(); }; @@ -30,10 +28,7 @@ export default function StakingParams({ >

Staking Params

- @@ -50,8 +45,7 @@ export default function StakingParams({ {Number( - BigInt(stakingParams.unbonding_time?.seconds ?? 1) / - BigInt(86400), + BigInt(stakingParams.unbonding_time?.seconds ?? 1) / BigInt(86400) ).toString()} @@ -71,15 +65,10 @@ export default function StakingParams({
- - MINIMUM COMMISSION - + MINIMUM COMMISSION - {(Number(stakingParams.min_commission_rate) * 100) - .toFixed(0) - .toString()}{" "} - % + {(Number(stakingParams.min_commission_rate) * 100).toFixed(0).toString()} %
@@ -92,9 +81,7 @@ export default function StakingParams({
- - HISTORICAL ENTRIES - + HISTORICAL ENTRIES {stakingParams.historical_entries} diff --git a/components/admins/components/validatorList.tsx b/components/admins/components/validatorList.tsx index 99282497..4ec262b5 100644 --- a/components/admins/components/validatorList.tsx +++ b/components/admins/components/validatorList.tsx @@ -1,9 +1,9 @@ -import React, { useState, useEffect } from "react"; -import { ValidatorDetailsModal } from "../modals/validatorModal"; -import { WarningModal } from "../modals/warningModal"; -import { ValidatorSDKType } from "@chalabi/manifestjs/dist/codegen/cosmos/staking/v1beta1/staking"; +import React, { useState, useEffect } from 'react'; +import { ValidatorDetailsModal } from '../modals/validatorModal'; +import { WarningModal } from '../modals/warningModal'; +import { ValidatorSDKType } from '@chalabi/manifestjs/dist/codegen/cosmos/staking/v1beta1/staking'; -import ProfileAvatar from "@/utils/identicon"; +import ProfileAvatar from '@/utils/identicon'; export interface ExtendedValidatorSDKType extends ValidatorSDKType { consensus_power?: bigint; logo_url?: string; @@ -22,13 +22,11 @@ export default function ValidatorList({ isLoading, }: ValidatorListProps) { const [active, setActive] = useState(true); - const [searchTerm, setSearchTerm] = useState(""); - const [selectedValidator, setSelectedValidator] = - useState(null); + const [searchTerm, setSearchTerm] = useState(''); + const [selectedValidator, setSelectedValidator] = useState(null); const [modalId, setModalId] = useState(null); const [warningVisible, setWarningVisible] = useState(false); - const [validatorToRemove, setValidatorToRemove] = - useState(null); + const [validatorToRemove, setValidatorToRemove] = useState(null); useEffect(() => { if (modalId) { @@ -45,17 +43,11 @@ export default function ValidatorList({ }; const filteredValidators = active - ? (Array.isArray(activeValidators) ? activeValidators : []).filter( - (validator) => - validator.description.moniker - .toLowerCase() - .includes(searchTerm.toLowerCase()), + ? (Array.isArray(activeValidators) ? activeValidators : []).filter(validator => + validator.description.moniker.toLowerCase().includes(searchTerm.toLowerCase()) ) - : (Array.isArray(pendingValidators) ? pendingValidators : []).filter( - (validator) => - validator.description.moniker - .toLowerCase() - .includes(searchTerm.toLowerCase()), + : (Array.isArray(pendingValidators) ? pendingValidators : []).filter(validator => + validator.description.moniker.toLowerCase().includes(searchTerm.toLowerCase()) ); const [modalKey, setModalKey] = useState(0); @@ -69,7 +61,7 @@ export default function ValidatorList({ const handleRowClick = (validator: ExtendedValidatorSDKType) => { setSelectedValidator(validator); - setModalKey((prevKey) => prevKey + 1); + setModalKey(prevKey => prevKey + 1); setModalId(`validator-modal-${validator.operator_address}-${Date.now()}`); }; @@ -79,17 +71,17 @@ export default function ValidatorList({

- {active ? "Active" : "Pending"} + {active ? 'Active' : 'Pending'}

@@ -98,15 +90,13 @@ export default function ValidatorList({ placeholder="Search for a validator..." className="input input-bordered input-xs ml-4" value={searchTerm} - onChange={(e) => setSearchTerm(e.target.value)} + onChange={e => setSearchTerm(e.target.value)} />
@@ -125,7 +115,7 @@ export default function ValidatorList({ - {filteredValidators.map((validator) => ( + {filteredValidators.map(validator => ( ) : ( - + )} @@ -155,11 +142,11 @@ export default function ValidatorList({ - {validator.consensus_power?.toString() ?? "Inactive"} + {validator.consensus_power?.toString() ?? 'Inactive'} - {!active && ( - - )} + {!active && } @@ -187,14 +172,14 @@ export default function ValidatorList({ diff --git a/components/admins/index.tsx b/components/admins/index.tsx index 520fcc33..61ea42e9 100644 --- a/components/admins/index.tsx +++ b/components/admins/index.tsx @@ -1,2 +1,2 @@ -export * from "./components"; -export * from "./modals"; +export * from './components'; +export * from './modals'; diff --git a/components/admins/modals/__tests__/descriptionModal.test.tsx b/components/admins/modals/__tests__/descriptionModal.test.tsx index e994965b..a948fe24 100644 --- a/components/admins/modals/__tests__/descriptionModal.test.tsx +++ b/components/admins/modals/__tests__/descriptionModal.test.tsx @@ -1,28 +1,26 @@ -import { describe, test, afterEach, expect } from "bun:test"; -import React from "react"; -import { render, screen, cleanup } from "@testing-library/react"; -import { DescriptionModal } from "@/components/admins/modals/descriptionModal"; -import matchers from "@testing-library/jest-dom/matchers"; +import { describe, test, afterEach, expect } from 'bun:test'; +import React from 'react'; +import { render, screen, cleanup } from '@testing-library/react'; +import { DescriptionModal } from '@/components/admins/modals/descriptionModal'; +import matchers from '@testing-library/jest-dom/matchers'; expect.extend(matchers); -describe("DescriptionModal Component", () => { - const modalId = "test-modal"; - const details = "This is a test description."; +describe('DescriptionModal Component', () => { + const modalId = 'test-modal'; + const details = 'This is a test description.'; afterEach(cleanup); - test("renders modal with correct details", () => { + test('renders modal with correct details', () => { render(); - expect(screen.getByText("Group Description")).toBeInTheDocument(); + expect(screen.getByText('Group Description')).toBeInTheDocument(); expect(screen.getByText(details)).toBeInTheDocument(); }); - test("displays correct title for validator type", () => { - render( - , - ); - expect(screen.getByText("Validator Description")).toBeInTheDocument(); + test('displays correct title for validator type', () => { + render(); + expect(screen.getByText('Validator Description')).toBeInTheDocument(); }); // TODO: Why is this test failing? diff --git a/components/admins/modals/__tests__/updateAdminModal.test.tsx b/components/admins/modals/__tests__/updateAdminModal.test.tsx index 8bb731e8..bad9308d 100644 --- a/components/admins/modals/__tests__/updateAdminModal.test.tsx +++ b/components/admins/modals/__tests__/updateAdminModal.test.tsx @@ -1,16 +1,16 @@ -import { describe, test, afterEach, expect } from "bun:test"; -import React from "react"; -import { screen, fireEvent, cleanup } from "@testing-library/react"; -import { UpdateAdminModal } from "@/components/admins/modals/updateAdminModal"; -import matchers from "@testing-library/jest-dom/matchers"; -import { renderWithChainProvider } from "@/tests/render"; +import { describe, test, afterEach, expect } from 'bun:test'; +import React from 'react'; +import { screen, fireEvent, cleanup } from '@testing-library/react'; +import { UpdateAdminModal } from '@/components/admins/modals/updateAdminModal'; +import matchers from '@testing-library/jest-dom/matchers'; +import { renderWithChainProvider } from '@/tests/render'; expect.extend(matchers); -const modalId = "test-modal"; -const admin = "manifest1adminaddress"; -const userAddress = "manifest1useraddress"; -const validAddress = "manifest1hj5fveer5cjtn4wd6wstzugjfdxzl0xp8ws9ct"; +const modalId = 'test-modal'; +const admin = 'manifest1adminaddress'; +const userAddress = 'manifest1useraddress'; +const validAddress = 'manifest1hj5fveer5cjtn4wd6wstzugjfdxzl0xp8ws9ct'; const allowExit = true; function renderWithProps(props = {}) { @@ -21,45 +21,45 @@ function renderWithProps(props = {}) { userAddress={userAddress} allowExit={allowExit} {...props} - />, + /> ); } -describe("UpdateAdminModal Component", () => { +describe('UpdateAdminModal Component', () => { afterEach(cleanup); - test("renders modal with correct details", () => { + test('renders modal with correct details', () => { renderWithProps(); - expect(screen.getByText("Update Admin")).toBeInTheDocument(); - expect(screen.getByText("Warning")).toBeInTheDocument(); + expect(screen.getByText('Update Admin')).toBeInTheDocument(); + expect(screen.getByText('Warning')).toBeInTheDocument(); expect( screen.getByText( - "Currently, the admin is set to a group policy address. While the admin can be any manifest1 address, it is recommended to set the new admin to another group policy address.", - ), + 'Currently, the admin is set to a group policy address. While the admin can be any manifest1 address, it is recommended to set the new admin to another group policy address.' + ) ).toBeInTheDocument(); }); - test("updates input field correctly", () => { + test('updates input field correctly', () => { renderWithProps(); - const input = screen.getByPlaceholderText("manifest123..."); - fireEvent.change(input, { target: { value: "manifest1newadminaddress" } }); - expect(input).toHaveValue("manifest1newadminaddress"); + const input = screen.getByPlaceholderText('manifest123...'); + fireEvent.change(input, { target: { value: 'manifest1newadminaddress' } }); + expect(input).toHaveValue('manifest1newadminaddress'); }); - test("disables update button when input is invalid", () => { + test('disables update button when input is invalid', () => { renderWithProps(); - const input = screen.getByPlaceholderText("manifest123..."); - const updateButton = screen.getByText("Update"); + const input = screen.getByPlaceholderText('manifest123...'); + const updateButton = screen.getByText('Update'); expect(updateButton).toBeDisabled(); - fireEvent.change(input, { target: { value: "invalidaddress" } }); + fireEvent.change(input, { target: { value: 'invalidaddress' } }); expect(updateButton).toBeDisabled(); }); - test("enables update button when input is valid", () => { + test('enables update button when input is valid', () => { renderWithProps(); - const updateButton = screen.getByText("Update"); + const updateButton = screen.getByText('Update'); expect(updateButton).toBeDisabled(); - const input = screen.getByPlaceholderText("manifest123..."); + const input = screen.getByPlaceholderText('manifest123...'); fireEvent.change(input, { target: { value: validAddress } }); expect(updateButton).toBeEnabled(); }); diff --git a/components/admins/modals/__tests__/updateStakingParamsModal.test.tsx b/components/admins/modals/__tests__/updateStakingParamsModal.test.tsx index 8fb750b8..207da39c 100644 --- a/components/admins/modals/__tests__/updateStakingParamsModal.test.tsx +++ b/components/admins/modals/__tests__/updateStakingParamsModal.test.tsx @@ -1,14 +1,14 @@ -import { describe, test, afterEach, expect } from "bun:test"; -import React from "react"; -import { screen, fireEvent, cleanup } from "@testing-library/react"; -import { UpdateStakingParamsModal } from "@/components/admins/modals/updateStakingParamsModal"; -import matchers from "@testing-library/jest-dom/matchers"; -import { renderWithChainProvider } from "@/tests/render"; -import { DurationSDKType } from "@chalabi/manifestjs/src/codegen/google/protobuf/duration"; +import { describe, test, afterEach, expect } from 'bun:test'; +import React from 'react'; +import { screen, fireEvent, cleanup } from '@testing-library/react'; +import { UpdateStakingParamsModal } from '@/components/admins/modals/updateStakingParamsModal'; +import matchers from '@testing-library/jest-dom/matchers'; +import { renderWithChainProvider } from '@/tests/render'; +import { DurationSDKType } from '@chalabi/manifestjs/src/codegen/google/protobuf/duration'; expect.extend(matchers); -const modalId = "test-modal"; +const modalId = 'test-modal'; const stakingParams: { unbonding_time: DurationSDKType; max_validators: number; @@ -19,13 +19,13 @@ const stakingParams: { } = { unbonding_time: { seconds: BigInt(86400), nanos: 0 }, max_validators: 100, - bond_denom: "stake", - min_commission_rate: "0.05", + bond_denom: 'stake', + min_commission_rate: '0.05', max_entries: 7, historical_entries: 100, }; -const admin = "manifest1adminaddress"; -const address = "manifest1useraddress"; +const admin = 'manifest1adminaddress'; +const address = 'manifest1useraddress'; function renderWithProps(props = {}) { renderWithChainProvider( @@ -35,42 +35,42 @@ function renderWithProps(props = {}) { admin={admin} address={address} {...props} - />, + /> ); } -describe("UpdateStakingParamsModal Component", () => { +describe('UpdateStakingParamsModal Component', () => { afterEach(cleanup); - test("renders modal with correct details", () => { + test('renders modal with correct details', () => { renderWithProps(); - expect(screen.getByText("Update Staking Parameters")).toBeInTheDocument(); - expect(screen.getByText("UNBONDING TIME")).toBeInTheDocument(); - expect(screen.getByText("MAX VALIDATORS")).toBeInTheDocument(); - expect(screen.getByText("BOND DENOM")).toBeInTheDocument(); - expect(screen.getByText("MINIMUM COMMISSION")).toBeInTheDocument(); - expect(screen.getByText("MAX ENTRIES")).toBeInTheDocument(); - expect(screen.getByText("HISTORICAL ENTRIES")).toBeInTheDocument(); + expect(screen.getByText('Update Staking Parameters')).toBeInTheDocument(); + expect(screen.getByText('UNBONDING TIME')).toBeInTheDocument(); + expect(screen.getByText('MAX VALIDATORS')).toBeInTheDocument(); + expect(screen.getByText('BOND DENOM')).toBeInTheDocument(); + expect(screen.getByText('MINIMUM COMMISSION')).toBeInTheDocument(); + expect(screen.getByText('MAX ENTRIES')).toBeInTheDocument(); + expect(screen.getByText('HISTORICAL ENTRIES')).toBeInTheDocument(); }); - test("updates input fields correctly", () => { + test('updates input fields correctly', () => { renderWithProps(); - const unbondingTimeInput = screen.getByPlaceholderText("1"); + const unbondingTimeInput = screen.getByPlaceholderText('1'); fireEvent.change(unbondingTimeInput, { target: { value: 2 } }); expect(unbondingTimeInput).toHaveValue(2); }); - test("disables update button when no changes are made", () => { + test('disables update button when no changes are made', () => { renderWithProps(); - const updateButton = screen.getByText("Update"); + const updateButton = screen.getByText('Update'); expect(updateButton).toBeDisabled(); }); - test("enables update button when changes are made", () => { + test('enables update button when changes are made', () => { renderWithProps(); - const unbondingTimeInput = screen.getByPlaceholderText("1"); + const unbondingTimeInput = screen.getByPlaceholderText('1'); fireEvent.change(unbondingTimeInput, { target: { value: 2 } }); - const updateButton = screen.getByText("Update"); + const updateButton = screen.getByText('Update'); expect(updateButton).toBeEnabled(); }); diff --git a/components/admins/modals/__tests__/validatorModal.test.tsx b/components/admins/modals/__tests__/validatorModal.test.tsx index 8217bc23..023d5277 100644 --- a/components/admins/modals/__tests__/validatorModal.test.tsx +++ b/components/admins/modals/__tests__/validatorModal.test.tsx @@ -1,52 +1,47 @@ -import { describe, test, afterEach, expect } from "bun:test"; -import React from "react"; -import { screen, fireEvent, cleanup, within } from "@testing-library/react"; -import { ValidatorDetailsModal } from "@/components/admins/modals/validatorModal"; -import matchers from "@testing-library/jest-dom/matchers"; -import { mockActiveValidators } from "@/tests/mock"; -import { renderWithChainProvider } from "@/tests/render"; +import { describe, test, afterEach, expect } from 'bun:test'; +import React from 'react'; +import { screen, fireEvent, cleanup, within } from '@testing-library/react'; +import { ValidatorDetailsModal } from '@/components/admins/modals/validatorModal'; +import matchers from '@testing-library/jest-dom/matchers'; +import { mockActiveValidators } from '@/tests/mock'; +import { renderWithChainProvider } from '@/tests/render'; expect.extend(matchers); const validator = mockActiveValidators[0]; -const modalId = "test-modal"; -const admin = "manifest1adminaddress"; +const modalId = 'test-modal'; +const admin = 'manifest1adminaddress'; function renderWithProps(props = {}) { return renderWithChainProvider( - , + ); } -describe("ValidatorDetailsModal Component", () => { +describe('ValidatorDetailsModal Component', () => { afterEach(cleanup); - test("renders modal with correct details", () => { + test('renders modal with correct details', () => { renderWithProps(); - expect(screen.getByText("Validator Details")).toBeInTheDocument(); - expect(screen.getByText("Validator One")).toBeInTheDocument(); - expect(screen.getByText("security1@foobar.com")).toBeInTheDocument(); - const detailsContainer = screen.getByLabelText("details"); - expect(within(detailsContainer).getByText("details1")).toBeInTheDocument(); + expect(screen.getByText('Validator Details')).toBeInTheDocument(); + expect(screen.getByText('Validator One')).toBeInTheDocument(); + expect(screen.getByText('security1@foobar.com')).toBeInTheDocument(); + const detailsContainer = screen.getByLabelText('details'); + expect(within(detailsContainer).getByText('details1')).toBeInTheDocument(); }); - test("updates input field correctly", () => { + test('updates input field correctly', () => { renderWithProps(); - const input = screen.getByPlaceholderText("1000"); + const input = screen.getByPlaceholderText('1000'); fireEvent.change(input, { target: { value: 2000 } }); expect(input).toHaveValue(2000); }); - test("enables update button when input is valid", () => { + test('enables update button when input is valid', () => { renderWithProps(); - const input = screen.getByPlaceholderText("1000"); + const input = screen.getByPlaceholderText('1000'); fireEvent.change(input, { target: { value: 2000 } }); - const updateButton = screen.getByText("update"); + const updateButton = screen.getByText('update'); expect(updateButton).toBeEnabled(); }); diff --git a/components/admins/modals/__tests__/warningModal.test.tsx b/components/admins/modals/__tests__/warningModal.test.tsx index 00e5ee29..81643301 100644 --- a/components/admins/modals/__tests__/warningModal.test.tsx +++ b/components/admins/modals/__tests__/warningModal.test.tsx @@ -1,16 +1,16 @@ -import { describe, test, afterEach, expect } from "bun:test"; -import React from "react"; -import { screen, cleanup } from "@testing-library/react"; -import { WarningModal } from "@/components/admins/modals/warningModal"; -import matchers from "@testing-library/jest-dom/matchers"; -import { renderWithChainProvider } from "@/tests/render"; +import { describe, test, afterEach, expect } from 'bun:test'; +import React from 'react'; +import { screen, cleanup } from '@testing-library/react'; +import { WarningModal } from '@/components/admins/modals/warningModal'; +import matchers from '@testing-library/jest-dom/matchers'; +import { renderWithChainProvider } from '@/tests/render'; expect.extend(matchers); -const admin = "manifest1adminaddress"; -const address = "manifest1validatoraddress"; -const moniker = "Validator Moniker"; -const modalId = "test-modal"; +const admin = 'manifest1adminaddress'; +const address = 'manifest1validatoraddress'; +const moniker = 'Validator Moniker'; +const modalId = 'test-modal'; function renderWithProps(props = {}) { return renderWithChainProvider( @@ -21,30 +21,28 @@ function renderWithProps(props = {}) { modalId={modalId} isActive={true} {...props} - />, + /> ); } -describe("WarningModal Component", () => { +describe('WarningModal Component', () => { afterEach(cleanup); - test("renders modal with correct details", () => { + test('renders modal with correct details', () => { renderWithProps(); - expect( - screen.getByText("Are you sure you want to remove the validator"), - ).toBeInTheDocument(); + expect(screen.getByText('Are you sure you want to remove the validator')).toBeInTheDocument(); expect(screen.getByText(moniker)).toBeInTheDocument(); - expect(screen.getByText("from the active set?")).toBeInTheDocument(); + expect(screen.getByText('from the active set?')).toBeInTheDocument(); }); - test("displays correct text based on isActive prop", () => { + test('displays correct text based on isActive prop', () => { renderWithProps(); - expect(screen.getByText("Remove From Active Set")).toBeInTheDocument(); + expect(screen.getByText('Remove From Active Set')).toBeInTheDocument(); cleanup(); renderWithProps({ isActive: false }); - expect(screen.getByText("Remove From Pending List")).toBeInTheDocument(); + expect(screen.getByText('Remove From Pending List')).toBeInTheDocument(); }); // // TODO: Why is this test failing? diff --git a/components/admins/modals/descriptionModal.tsx b/components/admins/modals/descriptionModal.tsx index 7cbbbfd6..36a53d79 100644 --- a/components/admins/modals/descriptionModal.tsx +++ b/components/admins/modals/descriptionModal.tsx @@ -1,17 +1,13 @@ -import React from "react"; -import { PiWarning } from "react-icons/pi"; +import React from 'react'; +import { PiWarning } from 'react-icons/pi'; interface DescriptionModalProps { modalId: string; details: string; - type?: "group" | "validator"; + type?: 'group' | 'validator'; } -export function DescriptionModal({ - modalId, - details, - type, -}: Readonly) { +export function DescriptionModal({ modalId, details, type }: Readonly) { return (
@@ -22,7 +18,7 @@ export function DescriptionModal({ ✕

- {type === "validator" ? "Validator" : "Group"} Description + {type === 'validator' ? 'Validator' : 'Group'} Description

diff --git a/components/admins/modals/index.tsx b/components/admins/modals/index.tsx index bde28273..ec2dab2c 100644 --- a/components/admins/modals/index.tsx +++ b/components/admins/modals/index.tsx @@ -1,3 +1,3 @@ -export * from "./updateAdminModal"; -export * from "./validatorModal"; -export * from "./warningModal"; +export * from './updateAdminModal'; +export * from './validatorModal'; +export * from './warningModal'; diff --git a/components/admins/modals/updateAdminModal.tsx b/components/admins/modals/updateAdminModal.tsx index bc0807dd..2a1b19ab 100644 --- a/components/admins/modals/updateAdminModal.tsx +++ b/components/admins/modals/updateAdminModal.tsx @@ -1,10 +1,10 @@ -import { chainName } from "@/config"; -import { useFeeEstimation, useTx } from "@/hooks"; -import { cosmos, strangelove_ventures } from "@chalabi/manifestjs"; -import { Any } from "@chalabi/manifestjs/dist/codegen/google/protobuf/any"; -import { MsgUpdateParams } from "@chalabi/manifestjs/dist/codegen/strangelove_ventures/poa/v1/tx"; -import React, { useState, useEffect } from "react"; -import { PiWarning } from "react-icons/pi"; +import { chainName } from '@/config'; +import { useFeeEstimation, useTx } from '@/hooks'; +import { cosmos, strangelove_ventures } from '@chalabi/manifestjs'; +import { Any } from '@chalabi/manifestjs/dist/codegen/google/protobuf/any'; +import { MsgUpdateParams } from '@chalabi/manifestjs/dist/codegen/strangelove_ventures/poa/v1/tx'; +import React, { useState, useEffect } from 'react'; +import { PiWarning } from 'react-icons/pi'; interface UpdateModalProps { modalId: string; @@ -19,13 +19,12 @@ export function UpdateAdminModal({ userAddress, allowExit, }: Readonly) { - const [newAdmin, setNewAdmin] = useState(""); + const [newAdmin, setNewAdmin] = useState(''); const [isValidAddress, setIsValidAddress] = useState(false); const { estimateFee } = useFeeEstimation(chainName); const { tx } = useTx(chainName); - const { updateParams } = - strangelove_ventures.poa.v1.MessageComposer.withTypeUrl; + const { updateParams } = strangelove_ventures.poa.v1.MessageComposer.withTypeUrl; const { submitProposal } = cosmos.group.v1.MessageComposer.withTypeUrl; useEffect(() => { @@ -37,7 +36,7 @@ export function UpdateAdminModal({ if (!isValidAddress) return; const msgUpdateAdmin = updateParams({ - sender: admin ?? "", + sender: admin ?? '', params: { admins: [newAdmin], allowValidatorSelfExit: allowExit ?? false, @@ -52,14 +51,14 @@ export function UpdateAdminModal({ const groupProposalMsg = submitProposal({ groupPolicyAddress: admin, messages: [anyMessage], - metadata: "", - proposers: [userAddress ?? ""], + metadata: '', + proposers: [userAddress ?? ''], title: `Update PoA Admin`, summary: `This proposal will update the administrator of the PoA module to ${newAdmin}`, exec: 0, }); - const fee = await estimateFee(userAddress ?? "", [groupProposalMsg]); + const fee = await estimateFee(userAddress ?? '', [groupProposalMsg]); await tx([groupProposalMsg], { fee, onSuccess: () => {}, @@ -69,9 +68,7 @@ export function UpdateAdminModal({ return ( - +

Update Admin

@@ -81,9 +78,9 @@ export function UpdateAdminModal({ Warning

- Currently, the admin is set to a group policy address. While the - admin can be any manifest1 address, it is recommended to set the - new admin to another group policy address. + Currently, the admin is set to a group policy address. While the admin can be any + manifest1 address, it is recommended to set the new admin to another group policy + address.

@@ -92,15 +89,13 @@ export function UpdateAdminModal({ type="text" placeholder="manifest123..." className={`input input-bordered input-md w-full ${ - newAdmin && !isValidAddress ? "input-error" : "" + newAdmin && !isValidAddress ? 'input-error' : '' }`} value={newAdmin} - onChange={(e) => setNewAdmin(e.target.value)} + onChange={e => setNewAdmin(e.target.value)} /> {newAdmin && !isValidAddress && ( -

- Please enter a valid manifest1 address -

+

Please enter a valid manifest1 address

)}
diff --git a/components/admins/modals/updateStakingParamsModal.tsx b/components/admins/modals/updateStakingParamsModal.tsx index 3ed883ea..e13b145f 100644 --- a/components/admins/modals/updateStakingParamsModal.tsx +++ b/components/admins/modals/updateStakingParamsModal.tsx @@ -1,10 +1,10 @@ -import { chainName } from "@/config"; -import { useFeeEstimation, useTx } from "@/hooks"; -import { strangelove_ventures, cosmos, manifest } from "@chalabi/manifestjs"; -import { ParamsSDKType } from "@chalabi/manifestjs/dist/codegen/cosmos/staking/v1beta1/staking"; -import { MsgUpdateStakingParams } from "@chalabi/manifestjs/dist/codegen/strangelove_ventures/poa/v1/tx"; -import { Any } from "@chalabi/manifestjs/dist/codegen/google/protobuf/any"; -import React, { useState, useEffect } from "react"; +import { chainName } from '@/config'; +import { useFeeEstimation, useTx } from '@/hooks'; +import { strangelove_ventures, cosmos, manifest } from '@chalabi/manifestjs'; +import { ParamsSDKType } from '@chalabi/manifestjs/dist/codegen/cosmos/staking/v1beta1/staking'; +import { MsgUpdateStakingParams } from '@chalabi/manifestjs/dist/codegen/strangelove_ventures/poa/v1/tx'; +import { Any } from '@chalabi/manifestjs/dist/codegen/google/protobuf/any'; +import React, { useState, useEffect } from 'react'; interface UpdateStakingParamsModalProps { modalId: string; @@ -19,55 +19,42 @@ export function UpdateStakingParamsModal({ admin, address, }: Readonly) { - const [unbondingTime, setUnbondingTime] = useState(""); - const [maxValidators, setMaxValidators] = useState(""); - const [bondDenom, setBondDenom] = useState(""); - const [minCommissionRate, setMinCommissionRate] = useState(""); - const [maxEntries, setMaxEntries] = useState(""); - const [historicalEntries, setHistoricalEntries] = useState(""); + const [unbondingTime, setUnbondingTime] = useState(''); + const [maxValidators, setMaxValidators] = useState(''); + const [bondDenom, setBondDenom] = useState(''); + const [minCommissionRate, setMinCommissionRate] = useState(''); + const [maxEntries, setMaxEntries] = useState(''); + const [historicalEntries, setHistoricalEntries] = useState(''); const [isChanged, setIsChanged] = useState(false); const { estimateFee } = useFeeEstimation(chainName); const { tx } = useTx(chainName); - const { updateStakingParams } = - strangelove_ventures.poa.v1.MessageComposer.withTypeUrl; + const { updateStakingParams } = strangelove_ventures.poa.v1.MessageComposer.withTypeUrl; const { submitProposal } = cosmos.group.v1.MessageComposer.withTypeUrl; useEffect(() => { setIsChanged( - unbondingTime !== "" || - maxValidators !== "" || - bondDenom !== "" || - minCommissionRate !== "" || - maxEntries !== "" || - historicalEntries !== "", + unbondingTime !== '' || + maxValidators !== '' || + bondDenom !== '' || + minCommissionRate !== '' || + maxEntries !== '' || + historicalEntries !== '' ); - }, [ - unbondingTime, - maxValidators, - bondDenom, - minCommissionRate, - maxEntries, - historicalEntries, - ]); + }, [unbondingTime, maxValidators, bondDenom, minCommissionRate, maxEntries, historicalEntries]); const handleUpdate = async () => { const msgUpdateStakingParams = updateStakingParams({ - sender: admin ?? "", + sender: admin ?? '', params: { unbondingTime: unbondingTime ? { seconds: BigInt(parseInt(unbondingTime) * 86400), nanos: 0 } : stakingParams.unbonding_time, - maxValidators: maxValidators - ? parseInt(maxValidators) - : stakingParams.max_validators, + maxValidators: maxValidators ? parseInt(maxValidators) : stakingParams.max_validators, bondDenom: bondDenom || stakingParams.bond_denom, - minCommissionRate: - minCommissionRate || stakingParams.min_commission_rate, - maxEntries: maxEntries - ? parseInt(maxEntries) - : stakingParams.max_entries, + minCommissionRate: minCommissionRate || stakingParams.min_commission_rate, + maxEntries: maxEntries ? parseInt(maxEntries) : stakingParams.max_entries, historicalEntries: historicalEntries ? parseInt(historicalEntries) : stakingParams.historical_entries, @@ -76,22 +63,20 @@ export function UpdateStakingParamsModal({ const anyMessage = Any.fromPartial({ typeUrl: msgUpdateStakingParams.typeUrl, - value: MsgUpdateStakingParams.encode( - msgUpdateStakingParams.value, - ).finish(), + value: MsgUpdateStakingParams.encode(msgUpdateStakingParams.value).finish(), }); const groupProposalMsg = submitProposal({ groupPolicyAddress: admin, messages: [anyMessage], - metadata: "", - proposers: [address ?? ""], + metadata: '', + proposers: [address ?? ''], title: `Update Staking Params`, summary: `This proposal will update various staking parameters.`, exec: 0, }); - const fee = await estimateFee(address ?? "", [groupProposalMsg]); + const fee = await estimateFee(address ?? '', [groupProposalMsg]); await tx([groupProposalMsg], { fee, onSuccess: () => {}, @@ -104,7 +89,7 @@ export function UpdateStakingParamsModal({ setter: React.Dispatch>, tip: string, placeholder: string, - type: string = "text", + type: string = 'text' ) => (
{label} @@ -112,7 +97,7 @@ export function UpdateStakingParamsModal({ className="input input-bordered input-sm" type={type} value={value} - onChange={(e) => setter(e.target.value)} + onChange={e => setter(e.target.value)} placeholder={placeholder} /> {tip} @@ -122,63 +107,61 @@ export function UpdateStakingParamsModal({ return ( - +

Update Staking Parameters

{renderInput( - "UNBONDING TIME", + 'UNBONDING TIME', unbondingTime, setUnbondingTime, - "Enter time in days", - "1", - "number", + 'Enter time in days', + '1', + 'number' )} {renderInput( - "MAX VALIDATORS", + 'MAX VALIDATORS', maxValidators, setMaxValidators, - "Maximum number of validators", + 'Maximum number of validators', stakingParams.max_validators.toString(), - "number", + 'number' )}
{renderInput( - "BOND DENOM", + 'BOND DENOM', bondDenom, setBondDenom, - "Token denomination for bonding", - stakingParams.bond_denom, + 'Token denomination for bonding', + stakingParams.bond_denom )} {renderInput( - "MINIMUM COMMISSION", + 'MINIMUM COMMISSION', minCommissionRate, setMinCommissionRate, - "Commission rate (e.g., 0.05 for 5%)", - stakingParams.min_commission_rate, + 'Commission rate (e.g., 0.05 for 5%)', + stakingParams.min_commission_rate )}
{renderInput( - "MAX ENTRIES", + 'MAX ENTRIES', maxEntries, setMaxEntries, - "Maximum entries for either unbonding delegation or redelegation", + 'Maximum entries for either unbonding delegation or redelegation', stakingParams.max_entries.toString(), - "number", + 'number' )} {renderInput( - "HISTORICAL ENTRIES", + 'HISTORICAL ENTRIES', historicalEntries, setHistoricalEntries, - "Number of historical entries to persist", + 'Number of historical entries to persist', stakingParams.historical_entries.toString(), - "number", + 'number' )}
diff --git a/components/admins/modals/validatorModal.tsx b/components/admins/modals/validatorModal.tsx index e28d1da4..4bec5887 100644 --- a/components/admins/modals/validatorModal.tsx +++ b/components/admins/modals/validatorModal.tsx @@ -1,20 +1,20 @@ -import React, { useState } from "react"; -import { TruncatedAddressWithCopy } from "@/components/react/addressCopy"; -import { ExtendedValidatorSDKType } from "../components"; -import ProfileAvatar from "@/utils/identicon"; -import { BsThreeDots } from "react-icons/bs"; -import { DescriptionModal } from "./descriptionModal"; -import { chainName } from "@/config"; -import { useTx, useFeeEstimation } from "@/hooks"; -import { strangelove_ventures } from "@chalabi/manifestjs"; -import { useChain } from "@cosmos-kit/react"; -import { cosmos } from "@chalabi/manifestjs"; -import { Any } from "@chalabi/manifestjs/dist/codegen/google/protobuf/any"; -import { MsgSetPower } from "@chalabi/manifestjs/dist/codegen/strangelove_ventures/poa/v1/tx"; +import React, { useState } from 'react'; +import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; +import { ExtendedValidatorSDKType } from '../components'; +import ProfileAvatar from '@/utils/identicon'; +import { BsThreeDots } from 'react-icons/bs'; +import { DescriptionModal } from './descriptionModal'; +import { chainName } from '@/config'; +import { useTx, useFeeEstimation } from '@/hooks'; +import { strangelove_ventures } from '@chalabi/manifestjs'; +import { useChain } from '@cosmos-kit/react'; +import { cosmos } from '@chalabi/manifestjs'; +import { Any } from '@chalabi/manifestjs/dist/codegen/google/protobuf/any'; +import { MsgSetPower } from '@chalabi/manifestjs/dist/codegen/strangelove_ventures/poa/v1/tx'; import { Cosmos_basev1beta1Msg_ToAmino, Cosmos_basev1beta1Msg_InterfaceDecoder, -} from "@chalabi/manifestjs/dist/codegen/cosmos/group/v1/tx"; +} from '@chalabi/manifestjs/dist/codegen/cosmos/group/v1/tx'; export function ValidatorDetailsModal({ validator, @@ -25,9 +25,7 @@ export function ValidatorDetailsModal({ modalId: string; admin: string; }>) { - const [power, setPowerInput] = useState( - validator?.consensus_power?.toString() || "", - ); + const [power, setPowerInput] = useState(validator?.consensus_power?.toString() || ''); const { tx } = useTx(chainName); const { estimateFee } = useFeeEstimation(chainName); const { address: userAddress } = useChain(chainName); @@ -46,15 +44,13 @@ export function ValidatorDetailsModal({ const handleDescription = (event: React.MouseEvent) => { event.preventDefault(); event.stopPropagation(); - const modal = document.getElementById( - `validator-description-modal`, - ) as HTMLDialogElement; + const modal = document.getElementById(`validator-description-modal`) as HTMLDialogElement; modal?.showModal(); }; const handleUpdate = async () => { const msgSetPower = setPower({ - sender: admin ?? "", + sender: admin ?? '', validatorAddress: validator.operator_address, power: BigInt(power), unsafe: false, @@ -68,14 +64,14 @@ export function ValidatorDetailsModal({ const groupProposalMsg = submitProposal({ groupPolicyAddress: admin, messages: [anyMessage], - metadata: "", - proposers: [userAddress ?? ""], + metadata: '', + proposers: [userAddress ?? ''], title: `Update the Voting Power of ${validator.description.moniker}`, summary: `This proposal will update the voting power of ${validator.description.moniker} to ${power}`, exec: 0, }); - const fee = await estimateFee(userAddress ?? "", [groupProposalMsg]); + const fee = await estimateFee(userAddress ?? '', [groupProposalMsg]); await tx([groupProposalMsg], { fee, onSuccess: () => {}, @@ -85,28 +81,17 @@ export function ValidatorDetailsModal({ return ( - +

Validator Details

- - VALIDATOR - + VALIDATOR
- {validator.logo_url !== "" && ( - + {validator.logo_url !== '' && ( + )} - {validator.logo_url === "" && ( - + {validator.logo_url === '' && ( + )} {validator.description.moniker}
@@ -118,7 +103,7 @@ export function ValidatorDetailsModal({ {isEmail(validator.description.security_contact) ? validator.description.security_contact - : "No Security Contact"} + : 'No Security Contact'}
@@ -126,17 +111,12 @@ export function ValidatorDetailsModal({
setPowerInput(e.target.value)} - placeholder={ - validator.consensus_power?.toString() ?? "Inactive" - } + onChange={e => setPowerInput(e.target.value)} + placeholder={validator.consensus_power?.toString() ?? 'Inactive'} className="input input-bordered input-xs w-2/3" type="number" /> -
@@ -144,10 +124,7 @@ export function ValidatorDetailsModal({
OPERATOR ADDRESS - +
@@ -166,8 +143,8 @@ export function ValidatorDetailsModal({ {validator.description.details ? validator.description.details.substring(0, 50) + - (validator.description.details.length > 50 ? "..." : "") - : "No Details"} + (validator.description.details.length > 50 ? '...' : '') + : 'No Details'}
@@ -175,7 +152,7 @@ export function ValidatorDetailsModal({ diff --git a/components/admins/modals/warningModal.tsx b/components/admins/modals/warningModal.tsx index cb57bf0f..3cfbd7b1 100644 --- a/components/admins/modals/warningModal.tsx +++ b/components/admins/modals/warningModal.tsx @@ -1,11 +1,11 @@ -import { chainName } from "@/config"; -import { useFeeEstimation, useTx } from "@/hooks"; -import { cosmos, strangelove_ventures } from "@chalabi/manifestjs"; -import { Any } from "@chalabi/manifestjs/dist/codegen/google/protobuf/any"; -import { MsgRemoveValidator } from "@chalabi/manifestjs/dist/codegen/strangelove_ventures/poa/v1/tx"; -import { useChain } from "@cosmos-kit/react"; -import React from "react"; -import { PiWarning } from "react-icons/pi"; +import { chainName } from '@/config'; +import { useFeeEstimation, useTx } from '@/hooks'; +import { cosmos, strangelove_ventures } from '@chalabi/manifestjs'; +import { Any } from '@chalabi/manifestjs/dist/codegen/google/protobuf/any'; +import { MsgRemoveValidator } from '@chalabi/manifestjs/dist/codegen/strangelove_ventures/poa/v1/tx'; +import { useChain } from '@cosmos-kit/react'; +import React from 'react'; +import { PiWarning } from 'react-icons/pi'; interface WarningModalProps { admin: string; @@ -31,12 +31,12 @@ export function WarningModal({ const handleAccept = async () => { const msgRemoveActive = removeValidator({ - sender: admin ?? "", + sender: admin ?? '', validatorAddress: address, }); const msgRemovePending = removePending({ - sender: admin ?? "", + sender: admin ?? '', validatorAddress: address, }); @@ -50,16 +50,14 @@ export function WarningModal({ const groupProposalMsg = submitProposal({ groupPolicyAddress: admin, messages: [anyMessage], - metadata: "", - proposers: [userAddress ?? ""], - title: `Remove ${isActive ? "Active" : "Pending"} Validator ${moniker}`, - summary: `Proposal to remove ${moniker} from the ${ - isActive ? "active" : "pending" - } set.`, + metadata: '', + proposers: [userAddress ?? ''], + title: `Remove ${isActive ? 'Active' : 'Pending'} Validator ${moniker}`, + summary: `Proposal to remove ${moniker} from the ${isActive ? 'active' : 'pending'} set.`, exec: 0, }); - const fee = await estimateFee(userAddress ?? "", [groupProposalMsg]); + const fee = await estimateFee(userAddress ?? '', [groupProposalMsg]); await tx([groupProposalMsg], { fee, onSuccess: () => {}, @@ -69,19 +67,17 @@ export function WarningModal({ return ( - +

- Are you sure you want to remove the validator{" "} + Are you sure you want to remove the validator{' '}

{moniker}

- from the {isActive ? "active set" : "pending list"}? + from the {isActive ? 'active set' : 'pending list'}?

@@ -91,7 +87,7 @@ export function WarningModal({ className="btn btn-secondary w-1/2 mx-auto -mt-2" onClick={handleAccept} > - {isActive ? "Remove From Active Set" : "Remove From Pending List"} + {isActive ? 'Remove From Active Set' : 'Remove From Pending List'}
diff --git a/components/bank/components/__tests__/historyBox.test.tsx b/components/bank/components/__tests__/historyBox.test.tsx index 76f95b56..15d80b0b 100644 --- a/components/bank/components/__tests__/historyBox.test.tsx +++ b/components/bank/components/__tests__/historyBox.test.tsx @@ -1,97 +1,54 @@ -import { test, expect, afterEach, describe } from "bun:test"; -import React from "react"; -import matchers from "@testing-library/jest-dom/matchers"; -import { - render, - screen, - cleanup, - waitFor, - fireEvent, - within, -} from "@testing-library/react"; -import { HistoryBox } from "@/components/bank/components/historyBox"; -import { mockTransactions } from "@/tests/mock"; +import { test, expect, afterEach, describe } from 'bun:test'; +import React from 'react'; +import matchers from '@testing-library/jest-dom/matchers'; +import { render, screen, cleanup, waitFor, fireEvent, within } from '@testing-library/react'; +import { HistoryBox } from '@/components/bank/components/historyBox'; +import { mockTransactions } from '@/tests/mock'; expect.extend(matchers); -describe("HistoryBox", () => { +describe('HistoryBox', () => { afterEach(() => { cleanup(); }); - test("renders correctly", () => { - render( - , - ); - expect(screen.getByText("Tx History")).toBeInTheDocument(); + test('renders correctly', () => { + render(); + expect(screen.getByText('Tx History')).toBeInTheDocument(); }); - test("displays transactions", () => { - render( - , - ); - expect(screen.getByText("Send")).toBeInTheDocument(); - expect(screen.getByText("Receive")).toBeInTheDocument(); + test('displays transactions', () => { + render(); + expect(screen.getByText('Send')).toBeInTheDocument(); + expect(screen.getByText('Receive')).toBeInTheDocument(); }); test("displays 'No transactions found' message when there are no transactions", () => { render(); - expect( - screen.getByText("No transactions found for this account!"), - ).toBeInTheDocument(); + expect(screen.getByText('No transactions found for this account!')).toBeInTheDocument(); }); - test("opens modal when clicking on a transaction", async () => { - render( - , - ); - fireEvent.click(screen.getByText("Send")); + test('opens modal when clicking on a transaction', async () => { + render(); + fireEvent.click(screen.getByText('Send')); await waitFor(() => { - expect(screen.getByLabelText("tx info")).toBeInTheDocument(); - expect(screen.getByText("Transaction Details")).toBeInTheDocument(); + expect(screen.getByLabelText('tx info')).toBeInTheDocument(); + expect(screen.getByText('Transaction Details')).toBeInTheDocument(); - const fromContainer = screen.getByLabelText("from"); - expect( - within(fromContainer).getByText("addres...dress1"), - ).toBeInTheDocument(); - const toContainer = screen.getByLabelText("to"); - expect( - within(toContainer).getByText("addres...dress2"), - ).toBeInTheDocument(); + const fromContainer = screen.getByLabelText('from'); + expect(within(fromContainer).getByText('addres...dress1')).toBeInTheDocument(); + const toContainer = screen.getByLabelText('to'); + expect(within(toContainer).getByText('addres...dress2')).toBeInTheDocument(); }); }); - test("formats date correctly", () => { - render( - , - ); - expect(screen.getByText("May 1, 2023")).toBeInTheDocument(); + test('formats date correctly', () => { + render(); + expect(screen.getByText('May 1, 2023')).toBeInTheDocument(); }); - test("formats amount correctly", () => { - render( - , - ); - expect(screen.getByText("1 TOKEN")).toBeInTheDocument(); + test('formats amount correctly', () => { + render(); + expect(screen.getByText('1 TOKEN')).toBeInTheDocument(); }); }); diff --git a/components/bank/components/__tests__/sendBox.test.tsx b/components/bank/components/__tests__/sendBox.test.tsx index 05c4b14d..78294938 100644 --- a/components/bank/components/__tests__/sendBox.test.tsx +++ b/components/bank/components/__tests__/sendBox.test.tsx @@ -1,22 +1,16 @@ -import { test, expect, afterEach, describe } from "bun:test"; -import React from "react"; -import matchers from "@testing-library/jest-dom/matchers"; -import { - screen, - cleanup, - waitFor, - fireEvent, - within, -} from "@testing-library/react"; -import SendBox from "@/components/bank/components/sendBox"; -import { mockBalances } from "@/tests/mock"; -import { renderWithChainProvider } from "@/tests/render"; +import { test, expect, afterEach, describe } from 'bun:test'; +import React from 'react'; +import matchers from '@testing-library/jest-dom/matchers'; +import { screen, cleanup, waitFor, fireEvent, within } from '@testing-library/react'; +import SendBox from '@/components/bank/components/sendBox'; +import { mockBalances } from '@/tests/mock'; +import { renderWithChainProvider } from '@/tests/render'; expect.extend(matchers); const renderWithProps = (props = {}) => { const defaultProps = { - address: "test_address", + address: 'test_address', balances: mockBalances, isBalancesLoading: false, refetchBalances: () => {}, @@ -24,52 +18,44 @@ const renderWithProps = (props = {}) => { return renderWithChainProvider(); }; -describe("SendBox", () => { +describe('SendBox', () => { afterEach(() => { cleanup(); }); - test("renders correctly", () => { + test('renders correctly', () => { renderWithProps(); - expect(screen.getByText("Send Tokens")).toBeInTheDocument(); + expect(screen.getByText('Send Tokens')).toBeInTheDocument(); }); - test("toggles between Send and IBC Transfer", async () => { + test('toggles between Send and IBC Transfer', async () => { renderWithProps(); - const buttonContainer = screen.getByLabelText("buttons"); - expect(within(buttonContainer).getByText("Send")).toBeInTheDocument(); - expect( - within(buttonContainer).getByText("IBC Transfer"), - ).toBeInTheDocument(); + const buttonContainer = screen.getByLabelText('buttons'); + expect(within(buttonContainer).getByText('Send')).toBeInTheDocument(); + expect(within(buttonContainer).getByText('IBC Transfer')).toBeInTheDocument(); - const tabsContainer = screen.getByLabelText("tabs"); - expect(within(tabsContainer).getByText("Send Tokens")).toBeInTheDocument(); + const tabsContainer = screen.getByLabelText('tabs'); + expect(within(tabsContainer).getByText('Send Tokens')).toBeInTheDocument(); - fireEvent.click(within(buttonContainer).getByText("IBC Transfer")); + fireEvent.click(within(buttonContainer).getByText('IBC Transfer')); await waitFor(() => - expect( - within(tabsContainer).getByText("IBC Transfer"), - ).toBeInTheDocument(), + expect(within(tabsContainer).getByText('IBC Transfer')).toBeInTheDocument() ); }); - test("displays chain selection dropdown when in IBC Transfer mode", async () => { + test('displays chain selection dropdown when in IBC Transfer mode', async () => { renderWithProps(); - fireEvent.click(screen.getByText("IBC Transfer")); - await waitFor(() => expect(screen.getByText("Chain")).toBeInTheDocument()); + fireEvent.click(screen.getByText('IBC Transfer')); + await waitFor(() => expect(screen.getByText('Chain')).toBeInTheDocument()); }); - test("selects a chain in IBC Transfer mode", async () => { + test('selects a chain in IBC Transfer mode', async () => { renderWithProps(); - const buttonContainer = screen.getByLabelText("buttons"); - expect( - within(buttonContainer).getByText("IBC Transfer"), - ).toBeInTheDocument(); - fireEvent.click(within(buttonContainer).getByText("IBC Transfer")); - fireEvent.click(screen.getByText("Chain")); - fireEvent.click(screen.getByLabelText("Osmosis")); - await waitFor(() => - expect(screen.getByAltText("Osmosis")).toBeInTheDocument(), - ); + const buttonContainer = screen.getByLabelText('buttons'); + expect(within(buttonContainer).getByText('IBC Transfer')).toBeInTheDocument(); + fireEvent.click(within(buttonContainer).getByText('IBC Transfer')); + fireEvent.click(screen.getByText('Chain')); + fireEvent.click(screen.getByLabelText('Osmosis')); + await waitFor(() => expect(screen.getByAltText('Osmosis')).toBeInTheDocument()); }); }); diff --git a/components/bank/components/__tests__/tokenList.test.tsx b/components/bank/components/__tests__/tokenList.test.tsx index 712bc967..753a4594 100644 --- a/components/bank/components/__tests__/tokenList.test.tsx +++ b/components/bank/components/__tests__/tokenList.test.tsx @@ -1,57 +1,57 @@ -import { test, expect, afterEach, describe } from "bun:test"; -import React from "react"; -import matchers from "@testing-library/jest-dom/matchers"; -import { fireEvent, render, screen, cleanup } from "@testing-library/react"; -import TokenList from "@/components/bank/components/tokenList"; -import { mockBalances } from "@/tests/mock"; +import { test, expect, afterEach, describe } from 'bun:test'; +import React from 'react'; +import matchers from '@testing-library/jest-dom/matchers'; +import { fireEvent, render, screen, cleanup } from '@testing-library/react'; +import TokenList from '@/components/bank/components/tokenList'; +import { mockBalances } from '@/tests/mock'; expect.extend(matchers); -describe("TokenList", () => { +describe('TokenList', () => { afterEach(() => { cleanup(); }); - test("renders correctly", () => { + test('renders correctly', () => { render(); - expect(screen.getByText("Your Balances")).toBeInTheDocument(); + expect(screen.getByText('Your Balances')).toBeInTheDocument(); }); - test("displays loading skeleton when isLoading is true", () => { + test('displays loading skeleton when isLoading is true', () => { render(); - expect(screen.getByText("Your wallet is empty!")).toBeInTheDocument(); + expect(screen.getByText('Your wallet is empty!')).toBeInTheDocument(); }); - test("displays empty state message when there are no balances", () => { + test('displays empty state message when there are no balances', () => { render(); - expect(screen.getByText("Your wallet is empty!")).toBeInTheDocument(); + expect(screen.getByText('Your wallet is empty!')).toBeInTheDocument(); }); - test("filters balances based on search term", () => { + test('filters balances based on search term', () => { render(); - const searchInput = screen.getByPlaceholderText("Search for a token..."); - fireEvent.change(searchInput, { target: { value: "token1" } }); - expect(screen.getByText("TOKEN 1")).toBeInTheDocument(); - expect(screen.queryByText("TOKEN 2")).not.toBeInTheDocument(); + const searchInput = screen.getByPlaceholderText('Search for a token...'); + fireEvent.change(searchInput, { target: { value: 'token1' } }); + expect(screen.getByText('TOKEN 1')).toBeInTheDocument(); + expect(screen.queryByText('TOKEN 2')).not.toBeInTheDocument(); }); - test("opens modal with correct denomination information", () => { + test('opens modal with correct denomination information', () => { render(); - const balanceRow = screen.getByText("TOKEN 1").closest("tr"); - if (!balanceRow) throw new Error("Balance row not found"); + const balanceRow = screen.getByText('TOKEN 1').closest('tr'); + if (!balanceRow) throw new Error('Balance row not found'); fireEvent.click(balanceRow); - expect(screen.getByText("TOKEN 1")).toBeInTheDocument(); + expect(screen.getByText('TOKEN 1')).toBeInTheDocument(); }); - test("displays correct balance for each token", () => { + test('displays correct balance for each token', () => { render(); - expect(screen.getByText("0.001")).toBeInTheDocument(); - expect(screen.getByText("0.002")).toBeInTheDocument(); + expect(screen.getByText('0.001')).toBeInTheDocument(); + expect(screen.getByText('0.002')).toBeInTheDocument(); }); - test("displays correct base denomination for each token", () => { + test('displays correct base denomination for each token', () => { render(); - expect(screen.getByText("token1")).toBeInTheDocument(); - expect(screen.getByText("token2")).toBeInTheDocument(); + expect(screen.getByText('token1')).toBeInTheDocument(); + expect(screen.getByText('token2')).toBeInTheDocument(); }); }); diff --git a/components/bank/components/historyBox.tsx b/components/bank/components/historyBox.tsx index fe042ea1..d99a508b 100644 --- a/components/bank/components/historyBox.tsx +++ b/components/bank/components/historyBox.tsx @@ -1,8 +1,8 @@ -import React, { useState } from "react"; -import { TruncatedAddressWithCopy } from "@/components/react/addressCopy"; -import TxInfoModal from "../modals/txInfo"; -import { shiftDigits } from "@/utils"; -import { formatDenom } from "@/components"; +import React, { useState } from 'react'; +import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; +import TxInfoModal from '../modals/txInfo'; +import { shiftDigits } from '@/utils'; +import { formatDenom } from '@/components'; interface Transaction { from_address: string; @@ -30,10 +30,10 @@ export function HistoryBox({ function formatDateShort(dateString: string): string { const date = new Date(dateString); - return date.toLocaleString("en-US", { - year: "numeric", - month: "short", - day: "numeric", + return date.toLocaleString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', }); } @@ -71,21 +71,14 @@ export function HistoryBox({ onClick={() => openModal(tx)} className="cursor-pointer hover:bg-base-200" > + {formatDateShort(tx.formatted_date)} - {formatDateShort(tx.formatted_date)} - - - {tx.data.from_address === address ? "Send" : "Receive"} + {tx.data.from_address === address ? 'Send' : 'Receive'} {tx.data.amount - .map( - (amt) => - `${shiftDigits(amt.amount, -6)} ${formatDenom( - amt.denom, - )}`, - ) - .join(", ")} + .map(amt => `${shiftDigits(amt.amount, -6)} ${formatDenom(amt.denom)}`) + .join(', ')}
- {selectedTx && ( - - )} + {selectedTx && } ); } diff --git a/components/bank/components/index.ts b/components/bank/components/index.ts index 962477b1..ea097264 100644 --- a/components/bank/components/index.ts +++ b/components/bank/components/index.ts @@ -1,11 +1,11 @@ -export * from "./sendBox"; -export * from "./tokenList"; -export * from "./historyBox"; +export * from './sendBox'; +export * from './tokenList'; +export * from './historyBox'; export function formatDenom(denom: string): string { - const cleanDenom = denom.replace(/^factory\/[^/]+\//, ""); + const cleanDenom = denom.replace(/^factory\/[^/]+\//, ''); - if (cleanDenom.startsWith("u")) { + if (cleanDenom.startsWith('u')) { return cleanDenom.slice(1).toUpperCase(); } diff --git a/components/bank/components/sendBox.tsx b/components/bank/components/sendBox.tsx index 137fd5b7..c541d6a1 100644 --- a/components/bank/components/sendBox.tsx +++ b/components/bank/components/sendBox.tsx @@ -1,10 +1,10 @@ -import { useState } from "react"; -import SendForm from "../forms/sendForm"; -import IbcSendForm from "../forms/ibcSendForm"; -import { PiCaretDownBold } from "react-icons/pi"; -import Image from "next/image"; -import { CoinSDKType } from "@chalabi/manifestjs/dist/codegen/cosmos/base/v1beta1/coin"; -import { CombinedBalanceInfo } from "@/pages/bank"; +import { useState } from 'react'; +import SendForm from '../forms/sendForm'; +import IbcSendForm from '../forms/ibcSendForm'; +import { PiCaretDownBold } from 'react-icons/pi'; +import Image from 'next/image'; +import { CoinSDKType } from '@chalabi/manifestjs/dist/codegen/cosmos/base/v1beta1/coin'; +import { CombinedBalanceInfo } from '@/pages/bank'; export default function SendBox({ address, @@ -18,12 +18,12 @@ export default function SendBox({ refetchBalances: () => void; }) { const [isIbcTransfer, setIsIbcTransfer] = useState(false); - const [selectedChain, setSelectedChain] = useState(""); + const [selectedChain, setSelectedChain] = useState(''); const ibcChains = [ { - id: "osmosis", - name: "Osmosis", - icon: "https://osmosis.zone/assets/icons/osmo-logo-icon.svg", + id: 'osmosis', + name: 'Osmosis', + icon: 'https://osmosis.zone/assets/icons/osmo-logo-icon.svg', }, ]; @@ -32,25 +32,24 @@ export default function SendBox({

- {isIbcTransfer ? "IBC Transfer" : "Send Tokens"} + {isIbcTransfer ? 'IBC Transfer' : 'Send Tokens'}

); } diff --git a/components/bank/forms/__tests__/ibcSendForm.test.tsx b/components/bank/forms/__tests__/ibcSendForm.test.tsx index 10628757..2451357f 100644 --- a/components/bank/forms/__tests__/ibcSendForm.test.tsx +++ b/components/bank/forms/__tests__/ibcSendForm.test.tsx @@ -1,17 +1,17 @@ -import { describe, test, afterEach, expect, jest } from "bun:test"; -import React from "react"; -import { screen, cleanup, fireEvent, within } from "@testing-library/react"; -import IbcSendForm from "@/components/bank/forms/ibcSendForm"; -import matchers from "@testing-library/jest-dom/matchers"; -import { mockBalances } from "@/tests/mock"; -import { renderWithChainProvider } from "@/tests/render"; +import { describe, test, afterEach, expect, jest } from 'bun:test'; +import React from 'react'; +import { screen, cleanup, fireEvent, within } from '@testing-library/react'; +import IbcSendForm from '@/components/bank/forms/ibcSendForm'; +import matchers from '@testing-library/jest-dom/matchers'; +import { mockBalances } from '@/tests/mock'; +import { renderWithChainProvider } from '@/tests/render'; expect.extend(matchers); function renderWithProps(props = {}) { const defaultProps = { - address: "manifest1address", - destinationChain: "osmosis", + address: 'manifest1address', + destinationChain: 'osmosis', balances: mockBalances, isBalancesLoading: false, refetchBalances: jest.fn(), @@ -21,45 +21,43 @@ function renderWithProps(props = {}) { } // TODO: Validate form inputs in component -describe("IbcSendForm Component", () => { +describe('IbcSendForm Component', () => { afterEach(cleanup); - test("renders form with correct details", () => { + test('renders form with correct details', () => { renderWithProps(); - expect(screen.getByText("Token")).toBeInTheDocument(); - expect(screen.getByText("Recipient")).toBeInTheDocument(); - expect(screen.getByText("Amount")).toBeInTheDocument(); + expect(screen.getByText('Token')).toBeInTheDocument(); + expect(screen.getByText('Recipient')).toBeInTheDocument(); + expect(screen.getByText('Amount')).toBeInTheDocument(); }); - test("empty balances", () => { + test('empty balances', () => { renderWithProps({ balances: [] }); - expect(screen.getByText("Select Token")).toBeInTheDocument(); + expect(screen.getByText('Select Token')).toBeInTheDocument(); }); - test("updates token dropdown correctly", () => { + test('updates token dropdown correctly', () => { renderWithProps(); - const dropdownLabelContainer = screen.getByLabelText("dropdown-label"); - fireEvent.click(within(dropdownLabelContainer).getByText("TOKEN 1")); + const dropdownLabelContainer = screen.getByLabelText('dropdown-label'); + fireEvent.click(within(dropdownLabelContainer).getByText('TOKEN 1')); - const balanceContainer = screen.getByLabelText("Token 1"); - expect(within(balanceContainer).getByText("TOKEN 1")).toBeInTheDocument(); - expect( - within(balanceContainer).queryByText("TOKEN 2"), - ).not.toBeInTheDocument(); + const balanceContainer = screen.getByLabelText('Token 1'); + expect(within(balanceContainer).getByText('TOKEN 1')).toBeInTheDocument(); + expect(within(balanceContainer).queryByText('TOKEN 2')).not.toBeInTheDocument(); }); - test("updates recipient input correctly", () => { + test('updates recipient input correctly', () => { renderWithProps(); - const recipientInput = screen.getByPlaceholderText("Recipient address"); - fireEvent.change(recipientInput, { target: { value: "cosmos1recipient" } }); - expect(recipientInput).toHaveValue("cosmos1recipient"); + const recipientInput = screen.getByPlaceholderText('Recipient address'); + fireEvent.change(recipientInput, { target: { value: 'cosmos1recipient' } }); + expect(recipientInput).toHaveValue('cosmos1recipient'); }); - test("updates amount input correctly", () => { + test('updates amount input correctly', () => { renderWithProps(); - const amountInput = screen.getByPlaceholderText("Enter amount"); - fireEvent.change(amountInput, { target: { value: "100" } }); - expect(amountInput).toHaveValue("100"); + const amountInput = screen.getByPlaceholderText('Enter amount'); + fireEvent.change(amountInput, { target: { value: '100' } }); + expect(amountInput).toHaveValue('100'); }); // // TODO: Make this test pass @@ -70,17 +68,17 @@ describe("IbcSendForm Component", () => { // }); // TODO: Fix inputs to be valid - test("send button is enabled when inputs are valid", () => { + test('send button is enabled when inputs are valid', () => { renderWithProps(); - fireEvent.change(screen.getByPlaceholderText("Recipient address"), { - target: { value: "cosmos1recipient" }, + fireEvent.change(screen.getByPlaceholderText('Recipient address'), { + target: { value: 'cosmos1recipient' }, }); - fireEvent.change(screen.getByPlaceholderText("Enter amount"), { - target: { value: "100" }, + fireEvent.change(screen.getByPlaceholderText('Enter amount'), { + target: { value: '100' }, }); - const dropdownLabelContainer = screen.getByLabelText("dropdown-label"); - fireEvent.click(within(dropdownLabelContainer).getByText("TOKEN 1")); - const sendButton = screen.getByText("Send"); + const dropdownLabelContainer = screen.getByLabelText('dropdown-label'); + fireEvent.click(within(dropdownLabelContainer).getByText('TOKEN 1')); + const sendButton = screen.getByText('Send'); expect(sendButton).toBeEnabled(); }); }); diff --git a/components/bank/forms/__tests__/sendForm.test.tsx b/components/bank/forms/__tests__/sendForm.test.tsx index d61cb8c4..577f15df 100644 --- a/components/bank/forms/__tests__/sendForm.test.tsx +++ b/components/bank/forms/__tests__/sendForm.test.tsx @@ -1,23 +1,16 @@ -import { describe, test, afterEach, expect, jest } from "bun:test"; -import React from "react"; -import { - render, - screen, - fireEvent, - cleanup, - waitFor, - within, -} from "@testing-library/react"; -import SendForm from "@/components/bank/forms/sendForm"; -import matchers from "@testing-library/jest-dom/matchers"; -import { mockBalances } from "@/tests/mock"; -import { renderWithChainProvider } from "@/tests/render"; +import { describe, test, afterEach, expect, jest } from 'bun:test'; +import React from 'react'; +import { render, screen, fireEvent, cleanup, waitFor, within } from '@testing-library/react'; +import SendForm from '@/components/bank/forms/sendForm'; +import matchers from '@testing-library/jest-dom/matchers'; +import { mockBalances } from '@/tests/mock'; +import { renderWithChainProvider } from '@/tests/render'; expect.extend(matchers); function renderWithProps(props = {}) { const defaultProps = { - address: "manifest1address", + address: 'manifest1address', balances: mockBalances, isBalancesLoading: false, refetchBalances: jest.fn(), @@ -27,45 +20,43 @@ function renderWithProps(props = {}) { } // TODO: Validate form inputs in component -describe("SendForm Component", () => { +describe('SendForm Component', () => { afterEach(cleanup); - test("renders form with correct details", () => { + test('renders form with correct details', () => { renderWithProps(); - expect(screen.getByText("Token")).toBeInTheDocument(); - expect(screen.getByText("Recipient")).toBeInTheDocument(); - expect(screen.getByText("Amount")).toBeInTheDocument(); + expect(screen.getByText('Token')).toBeInTheDocument(); + expect(screen.getByText('Recipient')).toBeInTheDocument(); + expect(screen.getByText('Amount')).toBeInTheDocument(); }); - test("empty balances", () => { + test('empty balances', () => { renderWithProps({ balances: [] }); - expect(screen.getByText("Select Token")).toBeInTheDocument(); + expect(screen.getByText('Select Token')).toBeInTheDocument(); }); - test("updates token dropdown correctly", () => { + test('updates token dropdown correctly', () => { renderWithProps(); - const dropdownLabelContainer = screen.getByLabelText("dropdown-label"); - fireEvent.click(within(dropdownLabelContainer).getByText("TOKEN 1")); + const dropdownLabelContainer = screen.getByLabelText('dropdown-label'); + fireEvent.click(within(dropdownLabelContainer).getByText('TOKEN 1')); - const balanceContainer = screen.getByLabelText("Token 1"); - expect(within(balanceContainer).getByText("TOKEN 1")).toBeInTheDocument(); - expect( - within(balanceContainer).queryByText("TOKEN 2"), - ).not.toBeInTheDocument(); + const balanceContainer = screen.getByLabelText('Token 1'); + expect(within(balanceContainer).getByText('TOKEN 1')).toBeInTheDocument(); + expect(within(balanceContainer).queryByText('TOKEN 2')).not.toBeInTheDocument(); }); - test("updates recipient input correctly", () => { + test('updates recipient input correctly', () => { renderWithProps(); - const recipientInput = screen.getByPlaceholderText("Recipient address"); - fireEvent.change(recipientInput, { target: { value: "cosmos1recipient" } }); - expect(recipientInput).toHaveValue("cosmos1recipient"); + const recipientInput = screen.getByPlaceholderText('Recipient address'); + fireEvent.change(recipientInput, { target: { value: 'cosmos1recipient' } }); + expect(recipientInput).toHaveValue('cosmos1recipient'); }); - test("updates amount input correctly", () => { + test('updates amount input correctly', () => { renderWithProps(); - const amountInput = screen.getByPlaceholderText("Enter amount"); - fireEvent.change(amountInput, { target: { value: "100" } }); - expect(amountInput).toHaveValue("100"); + const amountInput = screen.getByPlaceholderText('Enter amount'); + fireEvent.change(amountInput, { target: { value: '100' } }); + expect(amountInput).toHaveValue('100'); }); // TODO: Make this test pass @@ -76,17 +67,17 @@ describe("SendForm Component", () => { // }); // TODO: Fix inputs to be valid - test("send button is enabled when inputs are valid", () => { + test('send button is enabled when inputs are valid', () => { renderWithProps(); - fireEvent.change(screen.getByPlaceholderText("Recipient address"), { - target: { value: "cosmos1recipient" }, + fireEvent.change(screen.getByPlaceholderText('Recipient address'), { + target: { value: 'cosmos1recipient' }, }); - fireEvent.change(screen.getByPlaceholderText("Enter amount"), { - target: { value: "100" }, + fireEvent.change(screen.getByPlaceholderText('Enter amount'), { + target: { value: '100' }, }); - const dropdownLabelContainer = screen.getByLabelText("dropdown-label"); - fireEvent.click(within(dropdownLabelContainer).getByText("TOKEN 1")); - const sendButton = screen.getByText("Send"); + const dropdownLabelContainer = screen.getByLabelText('dropdown-label'); + fireEvent.click(within(dropdownLabelContainer).getByText('TOKEN 1')); + const sendButton = screen.getByText('Send'); expect(sendButton).toBeEnabled(); }); }); diff --git a/components/bank/forms/ibcSendForm.tsx b/components/bank/forms/ibcSendForm.tsx index 1244e3a4..479c1b3e 100644 --- a/components/bank/forms/ibcSendForm.tsx +++ b/components/bank/forms/ibcSendForm.tsx @@ -1,13 +1,13 @@ -import { useState, useEffect } from "react"; -import { chainName } from "@/config"; -import { useFeeEstimation, useTx, useBalance, useTokenBalances } from "@/hooks"; -import { cosmos, ibc } from "@chalabi/manifestjs"; +import { useState, useEffect } from 'react'; +import { chainName } from '@/config'; +import { useFeeEstimation, useTx, useBalance, useTokenBalances } from '@/hooks'; +import { cosmos, ibc } from '@chalabi/manifestjs'; -import { getIbcInfo, shiftDigits } from "@/utils"; -import { PiAddressBook, PiCaretDownBold } from "react-icons/pi"; -import { CoinSDKType } from "@chalabi/manifestjs/dist/codegen/cosmos/base/v1beta1/coin"; -import { CombinedBalanceInfo } from "@/pages/bank"; -import { DenomImage } from "@/components/factory"; +import { getIbcInfo, shiftDigits } from '@/utils'; +import { PiAddressBook, PiCaretDownBold } from 'react-icons/pi'; +import { CoinSDKType } from '@chalabi/manifestjs/dist/codegen/cosmos/base/v1beta1/coin'; +import { CombinedBalanceInfo } from '@/pages/bank'; +import { DenomImage } from '@/components/factory'; export default function IbcSendForm({ address, @@ -22,10 +22,9 @@ export default function IbcSendForm({ isBalancesLoading: boolean; refetchBalances: () => void; }>) { - const [recipient, setRecipient] = useState(""); - const [amount, setAmount] = useState(""); - const [selectedToken, setSelectedToken] = - useState(null); + const [recipient, setRecipient] = useState(''); + const [amount, setAmount] = useState(''); + const [selectedToken, setSelectedToken] = useState(null); const [isSending, setIsSending] = useState(false); const { tx } = useTx(chainName); @@ -46,15 +45,11 @@ export default function IbcSendForm({ setIsSending(true); try { const exponent = - selectedToken.metadata?.denom_units.find( - (unit) => unit.denom === selectedToken.denom, - )?.exponent ?? 6; + selectedToken.metadata?.denom_units.find(unit => unit.denom === selectedToken.denom) + ?.exponent ?? 6; const amountInBaseUnits = shiftDigits(amount, exponent); - const { source_port, source_channel } = getIbcInfo( - chainName ?? "", - destinationChain ?? "", - ); + const { source_port, source_channel } = getIbcInfo(chainName ?? '', destinationChain ?? ''); const token = { denom: selectedToken.coreDenom, @@ -67,8 +62,8 @@ export default function IbcSendForm({ const msg = transfer({ sourcePort: source_port, sourceChannel: source_channel, - sender: address ?? "", - receiver: recipient ?? "", + sender: address ?? '', + receiver: recipient ?? '', token, //@ts-ignore timeoutHeight: undefined, @@ -80,22 +75,22 @@ export default function IbcSendForm({ await tx([msg], { fee, onSuccess: () => { - setAmount(""); - setRecipient(""); + setAmount(''); + setRecipient(''); refetchBalances(); }, }); } catch (error) { - console.error("Error during sending:", error); + console.error('Error during sending:', error); } finally { setIsSending(false); } }; - const [searchTerm, setSearchTerm] = useState(""); + const [searchTerm, setSearchTerm] = useState(''); - const filteredBalances = balances?.filter((token) => - token.metadata?.display.toLowerCase().includes(searchTerm.toLowerCase()), + const filteredBalances = balances?.filter(token => + token.metadata?.display.toLowerCase().includes(searchTerm.toLowerCase()) ); return ( @@ -111,7 +106,7 @@ export default function IbcSendForm({ className="btn btn-sm bg-base-300 w-full justify-between" aria-label="dropdown-label" > - {selectedToken?.metadata?.display.toUpperCase() ?? "Select Token"} + {selectedToken?.metadata?.display.toUpperCase() ?? 'Select Token'}
@@ -195,7 +187,7 @@ export default function IbcSendForm({ placeholder="Enter amount" className="input input-bordered input-sm w-full" value={amount} - onChange={(e) => setAmount(e.target.value)} + onChange={e => setAmount(e.target.value)} />
@@ -206,11 +198,7 @@ export default function IbcSendForm({ disabled={isSending} aria-label="send-btn" > - {isSending ? ( - - ) : ( - "Send" - )} + {isSending ? : 'Send'} diff --git a/components/bank/forms/index.ts b/components/bank/forms/index.ts index dc6da477..7a79b4ad 100644 --- a/components/bank/forms/index.ts +++ b/components/bank/forms/index.ts @@ -1,2 +1,2 @@ -export * from "./sendForm"; -export * from "./ibcSendForm"; +export * from './sendForm'; +export * from './ibcSendForm'; diff --git a/components/bank/forms/sendForm.tsx b/components/bank/forms/sendForm.tsx index 61747a1e..7358998a 100644 --- a/components/bank/forms/sendForm.tsx +++ b/components/bank/forms/sendForm.tsx @@ -1,11 +1,11 @@ -import { useState, useEffect } from "react"; -import { chainName } from "@/config"; -import { useFeeEstimation, useTx } from "@/hooks"; -import { cosmos } from "@chalabi/manifestjs"; -import { PiAddressBook, PiCaretDownBold } from "react-icons/pi"; -import { shiftDigits } from "@/utils"; -import { CombinedBalanceInfo } from "@/pages/bank"; -import { DenomImage } from "@/components/factory"; +import { useState, useEffect } from 'react'; +import { chainName } from '@/config'; +import { useFeeEstimation, useTx } from '@/hooks'; +import { cosmos } from '@chalabi/manifestjs'; +import { PiAddressBook, PiCaretDownBold } from 'react-icons/pi'; +import { shiftDigits } from '@/utils'; +import { CombinedBalanceInfo } from '@/pages/bank'; +import { DenomImage } from '@/components/factory'; export default function SendForm({ address, @@ -18,10 +18,9 @@ export default function SendForm({ isBalancesLoading: boolean; refetchBalances: () => void; }>) { - const [recipient, setRecipient] = useState(""); - const [amount, setAmount] = useState(""); - const [selectedToken, setSelectedToken] = - useState(null); + const [recipient, setRecipient] = useState(''); + const [amount, setAmount] = useState(''); + const [selectedToken, setSelectedToken] = useState(null); const [isSending, setIsSending] = useState(false); const { tx } = useTx(chainName); @@ -42,9 +41,8 @@ export default function SendForm({ setIsSending(true); try { const exponent = - selectedToken.metadata?.denom_units.find( - (unit) => unit.denom === selectedToken.denom, - )?.exponent ?? 6; + selectedToken.metadata?.denom_units.find(unit => unit.denom === selectedToken.denom) + ?.exponent ?? 6; const amountInBaseUnits = shiftDigits(amount, exponent); const msg = send({ @@ -57,22 +55,22 @@ export default function SendForm({ await tx([msg], { fee, onSuccess: () => { - setAmount(""); - setRecipient(""); + setAmount(''); + setRecipient(''); refetchBalances(); }, }); } catch (error) { - console.error("Error during sending:", error); + console.error('Error during sending:', error); } finally { setIsSending(false); } }; - const [searchTerm, setSearchTerm] = useState(""); + const [searchTerm, setSearchTerm] = useState(''); - const filteredBalances = balances?.filter((token) => - token.metadata?.display.toLowerCase().includes(searchTerm.toLowerCase()), + const filteredBalances = balances?.filter(token => + token.metadata?.display.toLowerCase().includes(searchTerm.toLowerCase()) ); return ( @@ -86,9 +84,9 @@ export default function SendForm({ @@ -256,11 +232,7 @@ export default function TokenDetails({ - diff --git a/components/factory/forms/TransferForm.tsx b/components/factory/forms/TransferForm.tsx index f28bc9dd..7a7ed5d5 100644 --- a/components/factory/forms/TransferForm.tsx +++ b/components/factory/forms/TransferForm.tsx @@ -1,9 +1,9 @@ -import React, { useState } from "react"; -import { chainName } from "@/config"; -import { useFeeEstimation, useTx } from "@/hooks"; -import { osmosis } from "@chalabi/manifestjs"; -import { MetadataSDKType } from "@chalabi/manifestjs/dist/codegen/cosmos/bank/v1beta1/bank"; -import { PiAddressBook, PiSwap } from "react-icons/pi"; +import React, { useState } from 'react'; +import { chainName } from '@/config'; +import { useFeeEstimation, useTx } from '@/hooks'; +import { osmosis } from '@chalabi/manifestjs'; +import { MetadataSDKType } from '@chalabi/manifestjs/dist/codegen/cosmos/bank/v1beta1/bank'; +import { PiAddressBook, PiSwap } from 'react-icons/pi'; export default function TransferForm({ denom, @@ -16,14 +16,13 @@ export default function TransferForm({ refetch: () => void; balance: string; }) { - const [amount, setAmount] = useState(""); + const [amount, setAmount] = useState(''); const [fromAddress, setFromAddress] = useState(address); - const [toAddress, setToAddress] = useState(""); + const [toAddress, setToAddress] = useState(''); const [isSigning, setIsSigning] = useState(false); const { tx } = useTx(chainName); const { estimateFee } = useFeeEstimation(chainName); - const { forceTransfer } = - osmosis.tokenfactory.v1beta1.MessageComposer.withTypeUrl; + const { forceTransfer } = osmosis.tokenfactory.v1beta1.MessageComposer.withTypeUrl; const handleTransfer = async () => { if (!amount || isNaN(Number(amount))) { @@ -32,11 +31,8 @@ export default function TransferForm({ setIsSigning(true); try { const exponent = - denom?.denom_units?.find((unit) => unit.denom === denom.display) - ?.exponent || 0; - const amountInBaseUnits = BigInt( - parseFloat(amount) * Math.pow(10, exponent), - ).toString(); + denom?.denom_units?.find(unit => unit.denom === denom.display)?.exponent || 0; + const amountInBaseUnits = BigInt(parseFloat(amount) * Math.pow(10, exponent)).toString(); const msg = forceTransfer({ sender: address, amount: { @@ -46,16 +42,16 @@ export default function TransferForm({ transferFromAddress: fromAddress, transferToAddress: toAddress, }); - const fee = await estimateFee(address ?? "", [msg]); + const fee = await estimateFee(address ?? '', [msg]); await tx([msg], { fee, onSuccess: () => { - setAmount(""); + setAmount(''); refetch(); }, }); } catch (error) { - console.error("Error during transfer:", error); + console.error('Error during transfer:', error); } finally { setIsSigning(false); } @@ -82,15 +78,11 @@ export default function TransferForm({

CIRCULATING SUPPLY

-

- {denom.symbol} -

+

{denom.symbol}

EXPONENT

-

- {denom?.denom_units[1]?.exponent} -

+

{denom?.denom_units[1]?.exponent}

@@ -105,7 +97,7 @@ export default function TransferForm({ placeholder="Enter amount" className="input input-bordered h-10 input-sm w-full" value={amount} - onChange={(e) => setAmount(e.target.value)} + onChange={e => setAmount(e.target.value)} /> @@ -120,7 +112,7 @@ export default function TransferForm({ placeholder="From address" className="input input-bordered input-sm h-10 rounded-tl-lg rounded-bl-lg rounded-tr-none rounded-br-none w-full " value={fromAddress} - onChange={(e) => setFromAddress(e.target.value)} + onChange={e => setFromAddress(e.target.value)} /> diff --git a/components/factory/forms/__tests__/BurnForm.test.tsx b/components/factory/forms/__tests__/BurnForm.test.tsx index 74b2c631..ed133019 100644 --- a/components/factory/forms/__tests__/BurnForm.test.tsx +++ b/components/factory/forms/__tests__/BurnForm.test.tsx @@ -1,61 +1,61 @@ -import { describe, test, afterEach, expect, jest } from "bun:test"; -import React from "react"; -import { screen, fireEvent, cleanup } from "@testing-library/react"; -import BurnForm from "@/components/factory/forms/BurnForm"; -import matchers from "@testing-library/jest-dom/matchers"; -import { mockDenomMeta1, mockMfxDenom } from "@/tests/mock"; -import { renderWithChainProvider } from "@/tests/render"; +import { describe, test, afterEach, expect, jest } from 'bun:test'; +import React from 'react'; +import { screen, fireEvent, cleanup } from '@testing-library/react'; +import BurnForm from '@/components/factory/forms/BurnForm'; +import matchers from '@testing-library/jest-dom/matchers'; +import { mockDenomMeta1, mockMfxDenom } from '@/tests/mock'; +import { renderWithChainProvider } from '@/tests/render'; expect.extend(matchers); const mockProps = { isAdmin: true, - admin: "cosmos1adminaddress", + admin: 'cosmos1adminaddress', denom: mockDenomMeta1, - address: "cosmos1address", + address: 'cosmos1address', refetch: jest.fn(), - balance: "1000000", + balance: '1000000', }; function renderWithProps(props = {}) { return renderWithChainProvider(); } -describe("BurnForm Component", () => { +describe('BurnForm Component', () => { afterEach(cleanup); - test("renders form with correct details", () => { + test('renders form with correct details', () => { renderWithProps(); - expect(screen.getByText("NAME")).toBeInTheDocument(); - expect(screen.getByText("YOUR BALANCE")).toBeInTheDocument(); - expect(screen.getByText("EXPONENT")).toBeInTheDocument(); - expect(screen.getByText("CIRCULATING SUPPLY")).toBeInTheDocument(); + expect(screen.getByText('NAME')).toBeInTheDocument(); + expect(screen.getByText('YOUR BALANCE')).toBeInTheDocument(); + expect(screen.getByText('EXPONENT')).toBeInTheDocument(); + expect(screen.getByText('CIRCULATING SUPPLY')).toBeInTheDocument(); }); - test("renders multi burn when token is mfx", () => { + test('renders multi burn when token is mfx', () => { renderWithProps({ denom: mockMfxDenom }); - expect(screen.getByLabelText("multi-burn-btn")).toBeInTheDocument(); + expect(screen.getByLabelText('multi-burn-btn')).toBeInTheDocument(); }); - test("renders not affiliated message when not admin and token is mfx", () => { + test('renders not affiliated message when not admin and token is mfx', () => { renderWithProps({ isAdmin: false, denom: mockMfxDenom }); expect( - screen.getByText("You are not affiliated with any PoA Admin entity."), + screen.getByText('You are not affiliated with any PoA Admin entity.') ).toBeInTheDocument(); }); - test("updates amount input correctly", () => { + test('updates amount input correctly', () => { renderWithProps(); - const amountInput = screen.getByPlaceholderText("Enter amount"); - fireEvent.change(amountInput, { target: { value: "100" } }); - expect(amountInput).toHaveValue("100"); + const amountInput = screen.getByPlaceholderText('Enter amount'); + fireEvent.change(amountInput, { target: { value: '100' } }); + expect(amountInput).toHaveValue('100'); }); - test("updates recipient input correctly", () => { + test('updates recipient input correctly', () => { renderWithProps(); - const recipientInput = screen.getByPlaceholderText("Target address"); - fireEvent.change(recipientInput, { target: { value: "cosmos1recipient" } }); - expect(recipientInput).toHaveValue("cosmos1recipient"); + const recipientInput = screen.getByPlaceholderText('Target address'); + fireEvent.change(recipientInput, { target: { value: 'cosmos1recipient' } }); + expect(recipientInput).toHaveValue('cosmos1recipient'); }); // // TODO: Make this test pass @@ -66,15 +66,15 @@ describe("BurnForm Component", () => { // }); // TODO: Validate form inputs in component - test("burn button is enabled when inputs are valid", () => { + test('burn button is enabled when inputs are valid', () => { renderWithProps(); - fireEvent.change(screen.getByPlaceholderText("Enter amount"), { - target: { value: "100" }, + fireEvent.change(screen.getByPlaceholderText('Enter amount'), { + target: { value: '100' }, }); - fireEvent.change(screen.getByPlaceholderText("Target address"), { - target: { value: "cosmos1recipient" }, + fireEvent.change(screen.getByPlaceholderText('Target address'), { + target: { value: 'cosmos1recipient' }, }); - const burnButton = screen.getByText("Burn"); + const burnButton = screen.getByText('Burn'); expect(burnButton).toBeEnabled(); }); }); diff --git a/components/factory/forms/__tests__/ConfirmationForm.test.tsx b/components/factory/forms/__tests__/ConfirmationForm.test.tsx index fa1625c1..bf24c3fc 100644 --- a/components/factory/forms/__tests__/ConfirmationForm.test.tsx +++ b/components/factory/forms/__tests__/ConfirmationForm.test.tsx @@ -1,10 +1,10 @@ -import { afterEach, describe, expect, jest, test } from "bun:test"; -import React from "react"; -import { cleanup, fireEvent, screen } from "@testing-library/react"; -import ConfirmationForm from "@/components/factory/forms/ConfirmationForm"; -import matchers from "@testing-library/jest-dom/matchers"; -import { renderWithChainProvider } from "@/tests/render"; -import { mockTokenFormData } from "@/tests/mock"; +import { afterEach, describe, expect, jest, test } from 'bun:test'; +import React from 'react'; +import { cleanup, fireEvent, screen } from '@testing-library/react'; +import ConfirmationForm from '@/components/factory/forms/ConfirmationForm'; +import matchers from '@testing-library/jest-dom/matchers'; +import { renderWithChainProvider } from '@/tests/render'; +import { mockTokenFormData } from '@/tests/mock'; expect.extend(matchers); @@ -13,79 +13,77 @@ function renderWithProps(props = {}) { nextStep: jest.fn(), prevStep: jest.fn(), formData: mockTokenFormData, - address: "cosmos1address", + address: 'cosmos1address', }; - return renderWithChainProvider( - , - ); + return renderWithChainProvider(); } -describe("ConfirmationForm Component", () => { +describe('ConfirmationForm Component', () => { afterEach(cleanup); // TODO: Fix hardcoded values in component - test("renders form with correct details", () => { + test('renders form with correct details', () => { renderWithProps(); - expect(screen.getByText("Token Information")).toBeInTheDocument(); - expect(screen.getByText("Token Name")).toBeInTheDocument(); + expect(screen.getByText('Token Information')).toBeInTheDocument(); + expect(screen.getByText('Token Name')).toBeInTheDocument(); expect(screen.getByText(mockTokenFormData.name)).toBeInTheDocument(); - expect(screen.getByText("Symbol")).toBeInTheDocument(); + expect(screen.getByText('Symbol')).toBeInTheDocument(); expect(screen.getByText(mockTokenFormData.symbol)).toBeInTheDocument(); - expect(screen.getByText("Display")).toBeInTheDocument(); + expect(screen.getByText('Display')).toBeInTheDocument(); expect(screen.getByText(mockTokenFormData.display)).toBeInTheDocument(); - expect(screen.getByText("Subdenom")).toBeInTheDocument(); + expect(screen.getByText('Subdenom')).toBeInTheDocument(); expect(screen.getByText(mockTokenFormData.subdenom)).toBeInTheDocument(); - expect(screen.getByText("Description")).toBeInTheDocument(); + expect(screen.getByText('Description')).toBeInTheDocument(); expect(screen.getByText(mockTokenFormData.description)).toBeInTheDocument(); - expect(screen.getByText("Denom Units")).toBeInTheDocument(); - expect(screen.getByText("Base Denom")).toBeInTheDocument(); + expect(screen.getByText('Denom Units')).toBeInTheDocument(); + expect(screen.getByText('Base Denom')).toBeInTheDocument(); // TODO: Fix the following. This is hardcoded to `turd` at the moment. // expect(screen.getByText(mockFormData.denomUnits[0].denom)).toBeInTheDocument(); - expect(screen.getByText("Base Exponent")).toBeInTheDocument(); + expect(screen.getByText('Base Exponent')).toBeInTheDocument(); // TODO: Fix the following. This is hardcoded to `0` at the moment. // expect(screen.getByText(mockFormData.denomUnits[0].exponent.toString())).toBeInTheDocument(); - expect(screen.getByText("Full Denom")).toBeInTheDocument(); + expect(screen.getByText('Full Denom')).toBeInTheDocument(); expect( - screen.getByText(`factory/cosmos1address/${mockTokenFormData.subdenom}`), + screen.getByText(`factory/cosmos1address/${mockTokenFormData.subdenom}`) ).toBeInTheDocument(); - expect(screen.getByText("Full Denom Exponent")).toBeInTheDocument(); + expect(screen.getByText('Full Denom Exponent')).toBeInTheDocument(); expect( - screen.getByText(mockTokenFormData.denomUnits[1].exponent.toString()), + screen.getByText(mockTokenFormData.denomUnits[1].exponent.toString()) ).toBeInTheDocument(); }); // TODO: Fix advanced details in component - test("toggles advanced details correctly", () => { + test('toggles advanced details correctly', () => { renderWithProps(); - const toggleButton = screen.getByText("Show Advanced Details"); + const toggleButton = screen.getByText('Show Advanced Details'); fireEvent.click(toggleButton); - expect(screen.getByText("URI")).toBeInTheDocument(); + expect(screen.getByText('URI')).toBeInTheDocument(); expect(screen.getByText(mockTokenFormData.uri)).toBeInTheDocument(); - expect(screen.getByText("URI Hash")).toBeInTheDocument(); + expect(screen.getByText('URI Hash')).toBeInTheDocument(); expect(screen.getByText(mockTokenFormData.uriHash)).toBeInTheDocument(); - expect(screen.getByText("Base Denom Alias")).toBeInTheDocument(); + expect(screen.getByText('Base Denom Alias')).toBeInTheDocument(); // TODO: Fix the following in component. This should be the alias, not the subdenom. // expect(screen.getByText(mockFormData.subdenom)).toBeInTheDocument(); - expect(screen.getByText("Full Denom Alias")).toBeInTheDocument(); + expect(screen.getByText('Full Denom Alias')).toBeInTheDocument(); // TODO: Fix the following in component. This should be the alias, not the display. // expect(screen.getByText(mockFormData.display)).toBeInTheDocument(); fireEvent.click(toggleButton); - expect(screen.queryByText("URI")).not.toBeInTheDocument(); - expect(screen.queryByText("URI Hash")).not.toBeInTheDocument(); + expect(screen.queryByText('URI')).not.toBeInTheDocument(); + expect(screen.queryByText('URI Hash')).not.toBeInTheDocument(); }); }); diff --git a/components/factory/forms/__tests__/CreateDenom.test.tsx b/components/factory/forms/__tests__/CreateDenom.test.tsx index f663d47a..eeabf2e7 100644 --- a/components/factory/forms/__tests__/CreateDenom.test.tsx +++ b/components/factory/forms/__tests__/CreateDenom.test.tsx @@ -1,10 +1,10 @@ -import { describe, test, afterEach, expect, jest } from "bun:test"; -import React from "react"; -import { screen, cleanup, fireEvent, waitFor } from "@testing-library/react"; -import CreateDenom from "@/components/factory/forms/CreateDenom"; -import matchers from "@testing-library/jest-dom/matchers"; -import { renderWithChainProvider } from "@/tests/render"; -import { mockTokenFormData } from "@/tests/mock"; +import { describe, test, afterEach, expect, jest } from 'bun:test'; +import React from 'react'; +import { screen, cleanup, fireEvent, waitFor } from '@testing-library/react'; +import CreateDenom from '@/components/factory/forms/CreateDenom'; +import matchers from '@testing-library/jest-dom/matchers'; +import { renderWithChainProvider } from '@/tests/render'; +import { mockTokenFormData } from '@/tests/mock'; expect.extend(matchers); @@ -12,26 +12,26 @@ const mockProps = { nextStep: jest.fn(), formData: mockTokenFormData, dispatch: jest.fn(), - address: "cosmos1address", + address: 'cosmos1address', }; -describe("CreateDenom Component", () => { +describe('CreateDenom Component', () => { afterEach(cleanup); - test("renders form with correct details", () => { + test('renders form with correct details', () => { renderWithChainProvider(); - expect(screen.getByText("Create Denom")).toBeInTheDocument(); - expect(screen.getByText("Token Sub Denom")).toBeInTheDocument(); + expect(screen.getByText('Create Denom')).toBeInTheDocument(); + expect(screen.getByText('Token Sub Denom')).toBeInTheDocument(); }); - test("updates subdenom input correctly", () => { + test('updates subdenom input correctly', () => { renderWithChainProvider(); - const subdenomInput = screen.getByPlaceholderText("udenom"); - fireEvent.change(subdenomInput, { target: { value: "utest" } }); + const subdenomInput = screen.getByPlaceholderText('udenom'); + fireEvent.change(subdenomInput, { target: { value: 'utest' } }); expect(mockProps.dispatch).toHaveBeenCalledWith({ - type: "UPDATE_FIELD", - field: "subdenom", - value: "utest", + type: 'UPDATE_FIELD', + field: 'subdenom', + value: 'utest', }); }); diff --git a/components/factory/forms/__tests__/MintForm.test.tsx b/components/factory/forms/__tests__/MintForm.test.tsx index 55bd570b..fe666ca4 100644 --- a/components/factory/forms/__tests__/MintForm.test.tsx +++ b/components/factory/forms/__tests__/MintForm.test.tsx @@ -1,45 +1,45 @@ -import { describe, test, afterEach, expect, jest } from "bun:test"; -import React from "react"; -import { screen, fireEvent, cleanup } from "@testing-library/react"; -import MintForm from "@/components/factory/forms/MintForm"; -import matchers from "@testing-library/jest-dom/matchers"; -import { renderWithChainProvider } from "@/tests/render"; -import { mockDenomMeta1 } from "@/tests/mock"; +import { describe, test, afterEach, expect, jest } from 'bun:test'; +import React from 'react'; +import { screen, fireEvent, cleanup } from '@testing-library/react'; +import MintForm from '@/components/factory/forms/MintForm'; +import matchers from '@testing-library/jest-dom/matchers'; +import { renderWithChainProvider } from '@/tests/render'; +import { mockDenomMeta1 } from '@/tests/mock'; expect.extend(matchers); const mockProps = { isAdmin: true, - admin: "cosmos1adminaddress", + admin: 'cosmos1adminaddress', denom: mockDenomMeta1, - address: "cosmos1address", + address: 'cosmos1address', refetch: jest.fn(), - balance: "1000000", + balance: '1000000', }; -describe("MintForm Component", () => { +describe('MintForm Component', () => { afterEach(cleanup); - test("renders form with correct details", () => { + test('renders form with correct details', () => { renderWithChainProvider(); - expect(screen.getByText("NAME")).toBeInTheDocument(); - expect(screen.getByText("YOUR BALANCE")).toBeInTheDocument(); - expect(screen.getByText("EXPONENT")).toBeInTheDocument(); - expect(screen.getByText("CIRCULATING SUPPLY")).toBeInTheDocument(); + expect(screen.getByText('NAME')).toBeInTheDocument(); + expect(screen.getByText('YOUR BALANCE')).toBeInTheDocument(); + expect(screen.getByText('EXPONENT')).toBeInTheDocument(); + expect(screen.getByText('CIRCULATING SUPPLY')).toBeInTheDocument(); }); - test("updates amount input correctly", () => { + test('updates amount input correctly', () => { renderWithChainProvider(); - const amountInput = screen.getByLabelText("mint-amount-input"); - fireEvent.change(amountInput, { target: { value: "100" } }); - expect(amountInput).toHaveValue("100"); + const amountInput = screen.getByLabelText('mint-amount-input'); + fireEvent.change(amountInput, { target: { value: '100' } }); + expect(amountInput).toHaveValue('100'); }); - test("updates recipient input correctly", () => { + test('updates recipient input correctly', () => { renderWithChainProvider(); - const recipientInput = screen.getByLabelText("mint-recipient-input"); - fireEvent.change(recipientInput, { target: { value: "cosmos1recipient" } }); - expect(recipientInput).toHaveValue("cosmos1recipient"); + const recipientInput = screen.getByLabelText('mint-recipient-input'); + fireEvent.change(recipientInput, { target: { value: 'cosmos1recipient' } }); + expect(recipientInput).toHaveValue('cosmos1recipient'); }); // TODO: Button is disabled when inputs are invalid @@ -51,15 +51,15 @@ describe("MintForm Component", () => { // // TODO: Button is enabled when inputs are valid // Fix values validation in the component, this test should not pass as-is - test("mint button is enabled when inputs are valid", () => { + test('mint button is enabled when inputs are valid', () => { renderWithChainProvider(); - fireEvent.change(screen.getByLabelText("mint-amount-input"), { - target: { value: "100" }, + fireEvent.change(screen.getByLabelText('mint-amount-input'), { + target: { value: '100' }, }); - fireEvent.change(screen.getByLabelText("mint-recipient-input"), { - target: { value: "cosmos1recipient" }, + fireEvent.change(screen.getByLabelText('mint-recipient-input'), { + target: { value: 'cosmos1recipient' }, }); - const mintButton = screen.getByText("Mint"); + const mintButton = screen.getByText('Mint'); expect(mintButton).toBeEnabled(); }); }); diff --git a/components/factory/forms/__tests__/Success.test.tsx b/components/factory/forms/__tests__/Success.test.tsx index 4da9deaf..f7281b62 100644 --- a/components/factory/forms/__tests__/Success.test.tsx +++ b/components/factory/forms/__tests__/Success.test.tsx @@ -1,52 +1,48 @@ -import { describe, test, afterEach, expect } from "bun:test"; -import React from "react"; -import { screen, cleanup } from "@testing-library/react"; -import Success from "@/components/factory/forms/Success"; -import matchers from "@testing-library/jest-dom/matchers"; -import { renderWithChainProvider } from "@/tests/render"; -import { mockTokenFormData } from "@/tests/mock"; +import { describe, test, afterEach, expect } from 'bun:test'; +import React from 'react'; +import { screen, cleanup } from '@testing-library/react'; +import Success from '@/components/factory/forms/Success'; +import matchers from '@testing-library/jest-dom/matchers'; +import { renderWithChainProvider } from '@/tests/render'; +import { mockTokenFormData } from '@/tests/mock'; expect.extend(matchers); const mockProps = { formData: mockTokenFormData, - address: "cosmos1address", + address: 'cosmos1address', }; -describe("Success Component", () => { +describe('Success Component', () => { afterEach(cleanup); - test("renders component with correct details", () => { + test('renders component with correct details', () => { renderWithChainProvider(); - expect(screen.getByText("Success!")).toBeInTheDocument(); + expect(screen.getByText('Success!')).toBeInTheDocument(); expect( - screen.getByText( - "Your token was successfully created and the metadata was set.", - ), + screen.getByText('Your token was successfully created and the metadata was set.') ).toBeInTheDocument(); - expect( - screen.getByText("The full denom of your token is:"), - ).toBeInTheDocument(); - expect(screen.getByText("Token Details")).toBeInTheDocument(); + expect(screen.getByText('The full denom of your token is:')).toBeInTheDocument(); + expect(screen.getByText('Token Details')).toBeInTheDocument(); }); - test("displays token details correctly", () => { + test('displays token details correctly', () => { renderWithChainProvider(); - expect(screen.getByText("NAME")).toBeInTheDocument(); + expect(screen.getByText('NAME')).toBeInTheDocument(); expect(screen.getByText(mockTokenFormData.name)).toBeInTheDocument(); - expect(screen.getByText("SYMBOL")).toBeInTheDocument(); + expect(screen.getByText('SYMBOL')).toBeInTheDocument(); expect(screen.getByText(mockTokenFormData.symbol)).toBeInTheDocument(); - expect(screen.getByText("DISPLAY")).toBeInTheDocument(); + expect(screen.getByText('DISPLAY')).toBeInTheDocument(); expect(screen.getByText(mockTokenFormData.display)).toBeInTheDocument(); - expect(screen.getByText("SUBDENOM")).toBeInTheDocument(); + expect(screen.getByText('SUBDENOM')).toBeInTheDocument(); expect(screen.getByText(mockTokenFormData.subdenom)).toBeInTheDocument(); - expect(screen.getByText("DESCRIPTION")).toBeInTheDocument(); + expect(screen.getByText('DESCRIPTION')).toBeInTheDocument(); expect(screen.getByText(mockTokenFormData.description)).toBeInTheDocument(); - expect(screen.getByText("BASE EXPONENT")).toBeInTheDocument(); - expect(screen.getByText("0")).toBeInTheDocument(); - expect(screen.getByText("DISPLAY EXPONENT")).toBeInTheDocument(); + expect(screen.getByText('BASE EXPONENT')).toBeInTheDocument(); + expect(screen.getByText('0')).toBeInTheDocument(); + expect(screen.getByText('DISPLAY EXPONENT')).toBeInTheDocument(); expect( - screen.getByText(mockTokenFormData.denomUnits[1].exponent.toString()), + screen.getByText(mockTokenFormData.denomUnits[1].exponent.toString()) ).toBeInTheDocument(); }); }); diff --git a/components/factory/forms/__tests__/TokenDetailsForm.test.tsx b/components/factory/forms/__tests__/TokenDetailsForm.test.tsx index 9860a7a2..74538a67 100644 --- a/components/factory/forms/__tests__/TokenDetailsForm.test.tsx +++ b/components/factory/forms/__tests__/TokenDetailsForm.test.tsx @@ -1,10 +1,10 @@ -import { describe, test, afterEach, expect, jest } from "bun:test"; -import React from "react"; -import { screen, cleanup, fireEvent } from "@testing-library/react"; -import TokenDetailsForm from "@/components/factory/forms/TokenDetailsForm"; -import matchers from "@testing-library/jest-dom/matchers"; -import { renderWithChainProvider } from "@/tests/render"; -import { mockTokenFormData } from "@/tests/mock"; +import { describe, test, afterEach, expect, jest } from 'bun:test'; +import React from 'react'; +import { screen, cleanup, fireEvent } from '@testing-library/react'; +import TokenDetailsForm from '@/components/factory/forms/TokenDetailsForm'; +import matchers from '@testing-library/jest-dom/matchers'; +import { renderWithChainProvider } from '@/tests/render'; +import { mockTokenFormData } from '@/tests/mock'; expect.extend(matchers); @@ -13,96 +13,94 @@ const mockProps = { prevStep: jest.fn(), formData: mockTokenFormData, dispatch: jest.fn(), - address: "cosmos1address", + address: 'cosmos1address', }; -describe("TokenDetailsForm Component", () => { +describe('TokenDetailsForm Component', () => { afterEach(cleanup); - test("renders form with correct details", () => { + test('renders form with correct details', () => { renderWithChainProvider(); - expect(screen.getByText("Subdenom")).toBeInTheDocument(); - expect(screen.getByText("Display")).toBeInTheDocument(); - expect(screen.getByText("Name")).toBeInTheDocument(); - expect(screen.getByText("Symbol")).toBeInTheDocument(); - expect(screen.getByText("Description")).toBeInTheDocument(); - expect(screen.getByText("URI")).toBeInTheDocument(); - expect(screen.getByText("URI Hash")).toBeInTheDocument(); + expect(screen.getByText('Subdenom')).toBeInTheDocument(); + expect(screen.getByText('Display')).toBeInTheDocument(); + expect(screen.getByText('Name')).toBeInTheDocument(); + expect(screen.getByText('Symbol')).toBeInTheDocument(); + expect(screen.getByText('Description')).toBeInTheDocument(); + expect(screen.getByText('URI')).toBeInTheDocument(); + expect(screen.getByText('URI Hash')).toBeInTheDocument(); }); - test("updates form fields correctly", () => { + test('updates form fields correctly', () => { renderWithChainProvider(); - const subdenomInput = screen.getByLabelText("subdenom-input"); - fireEvent.change(subdenomInput, { target: { value: "newsubdenom" } }); + const subdenomInput = screen.getByLabelText('subdenom-input'); + fireEvent.change(subdenomInput, { target: { value: 'newsubdenom' } }); expect(mockProps.dispatch).toHaveBeenCalledWith({ - type: "UPDATE_FIELD", - field: "subdenom", - value: "newsubdenom", + type: 'UPDATE_FIELD', + field: 'subdenom', + value: 'newsubdenom', }); - const displayInput = screen.getByLabelText("display-input"); - fireEvent.change(displayInput, { target: { value: "New Display" } }); + const displayInput = screen.getByLabelText('display-input'); + fireEvent.change(displayInput, { target: { value: 'New Display' } }); expect(mockProps.dispatch).toHaveBeenCalledWith({ - type: "UPDATE_FIELD", - field: "display", - value: "New Display", + type: 'UPDATE_FIELD', + field: 'display', + value: 'New Display', }); - const nameInput = screen.getByLabelText("name-input"); - fireEvent.change(nameInput, { target: { value: "New Name" } }); + const nameInput = screen.getByLabelText('name-input'); + fireEvent.change(nameInput, { target: { value: 'New Name' } }); expect(mockProps.dispatch).toHaveBeenCalledWith({ - type: "UPDATE_FIELD", - field: "name", - value: "New Name", + type: 'UPDATE_FIELD', + field: 'name', + value: 'New Name', }); - const symbolInput = screen.getByLabelText("symbol-input"); - fireEvent.change(symbolInput, { target: { value: "NS" } }); + const symbolInput = screen.getByLabelText('symbol-input'); + fireEvent.change(symbolInput, { target: { value: 'NS' } }); expect(mockProps.dispatch).toHaveBeenCalledWith({ - type: "UPDATE_FIELD", - field: "symbol", - value: "NS", + type: 'UPDATE_FIELD', + field: 'symbol', + value: 'NS', }); - const descriptionInput = screen.getByLabelText("description-input"); + const descriptionInput = screen.getByLabelText('description-input'); fireEvent.change(descriptionInput, { - target: { value: "New Description" }, + target: { value: 'New Description' }, }); expect(mockProps.dispatch).toHaveBeenCalledWith({ - type: "UPDATE_FIELD", - field: "description", - value: "New Description", + type: 'UPDATE_FIELD', + field: 'description', + value: 'New Description', }); - const uriInput = screen.getByLabelText("uri-input"); - fireEvent.change(uriInput, { target: { value: "http://newuri.com" } }); + const uriInput = screen.getByLabelText('uri-input'); + fireEvent.change(uriInput, { target: { value: 'http://newuri.com' } }); expect(mockProps.dispatch).toHaveBeenCalledWith({ - type: "UPDATE_FIELD", - field: "uri", - value: "http://newuri.com", + type: 'UPDATE_FIELD', + field: 'uri', + value: 'http://newuri.com', }); - const uriHashInput = screen.getByLabelText("uri-hash-input"); - fireEvent.change(uriHashInput, { target: { value: "newurihash" } }); + const uriHashInput = screen.getByLabelText('uri-hash-input'); + fireEvent.change(uriHashInput, { target: { value: 'newurihash' } }); expect(mockProps.dispatch).toHaveBeenCalledWith({ - type: "UPDATE_FIELD", - field: "uriHash", - value: "newurihash", + type: 'UPDATE_FIELD', + field: 'uriHash', + value: 'newurihash', }); }); - test("next button is disabled when form is invalid", () => { - const invalidFormData = { ...mockTokenFormData, subdenom: "" }; - renderWithChainProvider( - , - ); - const nextButton = screen.getByText("Next: Confirmation"); + test('next button is disabled when form is invalid', () => { + const invalidFormData = { ...mockTokenFormData, subdenom: '' }; + renderWithChainProvider(); + const nextButton = screen.getByText('Next: Confirmation'); expect(nextButton).toBeDisabled(); }); - test("next button is enabled when form is valid", () => { + test('next button is enabled when form is valid', () => { renderWithChainProvider(); - const nextButton = screen.getByText("Next: Confirmation"); + const nextButton = screen.getByText('Next: Confirmation'); expect(nextButton).toBeEnabled(); }); }); diff --git a/components/factory/forms/__tests__/TransferForm.test.tsx b/components/factory/forms/__tests__/TransferForm.test.tsx index 7e573fb8..05843a11 100644 --- a/components/factory/forms/__tests__/TransferForm.test.tsx +++ b/components/factory/forms/__tests__/TransferForm.test.tsx @@ -1,47 +1,47 @@ -import { describe, test, afterEach, expect, jest } from "bun:test"; -import React from "react"; -import { render, screen, fireEvent, cleanup } from "@testing-library/react"; -import TransferForm from "@/components/factory/forms/TransferForm"; -import matchers from "@testing-library/jest-dom/matchers"; -import { renderWithChainProvider } from "@/tests/render"; -import { mockDenomMeta1 } from "@/tests/mock"; +import { describe, test, afterEach, expect, jest } from 'bun:test'; +import React from 'react'; +import { render, screen, fireEvent, cleanup } from '@testing-library/react'; +import TransferForm from '@/components/factory/forms/TransferForm'; +import matchers from '@testing-library/jest-dom/matchers'; +import { renderWithChainProvider } from '@/tests/render'; +import { mockDenomMeta1 } from '@/tests/mock'; expect.extend(matchers); const mockProps = { denom: mockDenomMeta1, - address: "cosmos1address", + address: 'cosmos1address', refetch: jest.fn(), - balance: "1000", + balance: '1000', }; -describe("TransferForm Component", () => { +describe('TransferForm Component', () => { afterEach(cleanup); - test("renders form with correct details", () => { + test('renders form with correct details', () => { renderWithChainProvider(); - expect(screen.getByText("CIRCULATING SUPPLY")).toBeInTheDocument(); - expect(screen.getByText("EXPONENT")).toBeInTheDocument(); - expect(screen.getByText("AMOUNT")).toBeInTheDocument(); - expect(screen.getByText("FROM")).toBeInTheDocument(); - expect(screen.getByText("TO")).toBeInTheDocument(); + expect(screen.getByText('CIRCULATING SUPPLY')).toBeInTheDocument(); + expect(screen.getByText('EXPONENT')).toBeInTheDocument(); + expect(screen.getByText('AMOUNT')).toBeInTheDocument(); + expect(screen.getByText('FROM')).toBeInTheDocument(); + expect(screen.getByText('TO')).toBeInTheDocument(); }); - test("updates form fields correctly", () => { + test('updates form fields correctly', () => { renderWithChainProvider(); - const amountInput = screen.getByPlaceholderText("Enter amount"); - fireEvent.change(amountInput, { target: { value: "100" } }); - expect(amountInput).toHaveValue("100"); + const amountInput = screen.getByPlaceholderText('Enter amount'); + fireEvent.change(amountInput, { target: { value: '100' } }); + expect(amountInput).toHaveValue('100'); - const fromAddressInput = screen.getByPlaceholderText("From address"); + const fromAddressInput = screen.getByPlaceholderText('From address'); fireEvent.change(fromAddressInput, { - target: { value: "cosmos1fromaddress" }, + target: { value: 'cosmos1fromaddress' }, }); - expect(fromAddressInput).toHaveValue("cosmos1fromaddress"); + expect(fromAddressInput).toHaveValue('cosmos1fromaddress'); - const toAddressInput = screen.getByPlaceholderText("To address"); - fireEvent.change(toAddressInput, { target: { value: "cosmos1toaddress" } }); - expect(toAddressInput).toHaveValue("cosmos1toaddress"); + const toAddressInput = screen.getByPlaceholderText('To address'); + fireEvent.change(toAddressInput, { target: { value: 'cosmos1toaddress' } }); + expect(toAddressInput).toHaveValue('cosmos1toaddress'); }); // TODO: Make this test pass @@ -52,13 +52,13 @@ describe("TransferForm Component", () => { // }); // TODO: Fix values validation in the component, this test should not pass as-is - test("transfer button is enabled when form is valid", () => { + test('transfer button is enabled when form is valid', () => { renderWithChainProvider(); - const amountInput = screen.getByPlaceholderText("Enter amount"); - fireEvent.change(amountInput, { target: { value: "100" } }); - const toAddressInput = screen.getByPlaceholderText("To address"); - fireEvent.change(toAddressInput, { target: { value: "cosmos1toaddress" } }); - const transferButton = screen.getByText("Transfer"); + const amountInput = screen.getByPlaceholderText('Enter amount'); + fireEvent.change(amountInput, { target: { value: '100' } }); + const toAddressInput = screen.getByPlaceholderText('To address'); + fireEvent.change(toAddressInput, { target: { value: 'cosmos1toaddress' } }); + const transferButton = screen.getByText('Transfer'); expect(transferButton).toBeEnabled(); }); }); diff --git a/components/factory/forms/index.ts b/components/factory/forms/index.ts index ae204ce7..da036c66 100644 --- a/components/factory/forms/index.ts +++ b/components/factory/forms/index.ts @@ -1,7 +1,7 @@ -export * from "./ConfirmationForm"; -export * from "./CreateDenom"; -export * from "./Success"; -export * from "./TokenDetailsForm"; -export * from "./MintForm"; -export * from "./BurnForm"; -export * from "./TransferForm"; +export * from './ConfirmationForm'; +export * from './CreateDenom'; +export * from './Success'; +export * from './TokenDetailsForm'; +export * from './MintForm'; +export * from './BurnForm'; +export * from './TransferForm'; diff --git a/components/factory/index.ts b/components/factory/index.ts index a785377a..a9de1fa4 100644 --- a/components/factory/index.ts +++ b/components/factory/index.ts @@ -1,3 +1,3 @@ -export * from "./components"; -export * from "./forms"; -export * from "./modals"; +export * from './components'; +export * from './forms'; +export * from './modals'; diff --git a/components/factory/modals/denomInfo.tsx b/components/factory/modals/denomInfo.tsx index 2afe5ff2..6ea61d4e 100644 --- a/components/factory/modals/denomInfo.tsx +++ b/components/factory/modals/denomInfo.tsx @@ -1,22 +1,14 @@ -import { TruncatedAddressWithCopy } from "@/components/react/addressCopy"; +import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; -type MessageType = "payout" | "burn"; +type MessageType = 'payout' | 'burn'; -export function DenomInfoModal({ - denom, - modalId, -}: { - denom: any; - modalId: string; -}) { +export function DenomInfoModal({ denom, modalId }: { denom: any; modalId: string }) { return ( <>
- +
@@ -25,22 +17,20 @@ export function DenomInfoModal({

NAME

-

{denom.name ?? "No name available"}

+

{denom.name ?? 'No name available'}

SYMBOL

-

- {denom.symbol ?? "No symbol available"} -

+

{denom.symbol ?? 'No symbol available'}

DESCRIPTION

- {denom.description ?? "No description available"} + {denom.description ?? 'No description available'}

@@ -48,9 +38,7 @@ export function DenomInfoModal({

EXPONENT

-

- {denom?.denom_units[1]?.exponent ?? "0"} -

+

{denom?.denom_units[1]?.exponent ?? '0'}

@@ -70,9 +58,7 @@ export function DenomInfoModal({

ALIASES

-

- {unit.aliases.join(", ") || "No aliases"} -

+

{unit.aliases.join(', ') || 'No aliases'}

@@ -93,9 +79,7 @@ export function DenomInfoModal({

DISPLAY

-

- {denom.display ?? "No display available"} -

+

{denom.display ?? 'No display available'}

diff --git a/components/factory/modals/index.ts b/components/factory/modals/index.ts index 91ad2ef5..0e511c5e 100644 --- a/components/factory/modals/index.ts +++ b/components/factory/modals/index.ts @@ -1,2 +1,2 @@ -export * from "./denomInfo"; -export * from "./updateDenomMetadata"; +export * from './denomInfo'; +export * from './updateDenomMetadata'; diff --git a/components/factory/modals/multiMfxBurnModal.tsx b/components/factory/modals/multiMfxBurnModal.tsx index 6d04a15f..92c341eb 100644 --- a/components/factory/modals/multiMfxBurnModal.tsx +++ b/components/factory/modals/multiMfxBurnModal.tsx @@ -1,5 +1,5 @@ -import React from "react"; -import { PiPlusCircle, PiMinusCircle } from "react-icons/pi"; +import React from 'react'; +import { PiPlusCircle, PiMinusCircle } from 'react-icons/pi'; interface BurnPair { address: string; @@ -10,11 +10,7 @@ interface MultiBurnModalProps { isOpen: boolean; onClose: () => void; burnPairs: BurnPair[]; - updateBurnPair: ( - index: number, - field: "address" | "amount", - value: string, - ) => void; + updateBurnPair: (index: number, field: 'address' | 'amount', value: string) => void; addBurnPair: () => void; removeBurnPair: (index: number) => void; handleMultiBurn: () => void; @@ -32,10 +28,7 @@ export function MultiBurnModal({ isSigning, }: MultiBurnModalProps) { return ( - +

Multi Burn MFX

@@ -54,9 +47,7 @@ export function MultiBurnModal({ placeholder="Enter address" className="input input-bordered input-sm w-full mb-4" value={pair.address} - onChange={(e) => - updateBurnPair(index, "address", e.target.value) - } + onChange={e => updateBurnPair(index, 'address', e.target.value)} />
@@ -68,19 +59,15 @@ export function MultiBurnModal({ placeholder="Enter amount" className="input input-bordered input-sm w-full mb-4" value={pair.amount} - onChange={(e) => - updateBurnPair(index, "amount", e.target.value) - } + onChange={e => updateBurnPair(index, 'amount', e.target.value)} />
- diff --git a/components/factory/modals/multiMfxMintModal.tsx b/components/factory/modals/multiMfxMintModal.tsx index 68210cbc..7e4c608a 100644 --- a/components/factory/modals/multiMfxMintModal.tsx +++ b/components/factory/modals/multiMfxMintModal.tsx @@ -1,6 +1,6 @@ // MultiMintModal.tsx -import React from "react"; -import { PiPlusCircle, PiMinusCircle } from "react-icons/pi"; +import React from 'react'; +import { PiPlusCircle, PiMinusCircle } from 'react-icons/pi'; interface PayoutPair { address: string; @@ -11,11 +11,7 @@ interface MultiMintModalProps { isOpen: boolean; onClose: () => void; payoutPairs: PayoutPair[]; - updatePayoutPair: ( - index: number, - field: "address" | "amount", - value: string, - ) => void; + updatePayoutPair: (index: number, field: 'address' | 'amount', value: string) => void; addPayoutPair: () => void; removePayoutPair: (index: number) => void; handleMultiMint: () => void; @@ -33,10 +29,7 @@ export function MultiMintModal({ isSigning, }: MultiMintModalProps) { return ( - +

Multi Mint MFX

@@ -48,18 +41,14 @@ export function MultiMintModal({ >
- updatePayoutPair(index, "address", e.target.value) - } + onChange={e => updatePayoutPair(index, 'address', e.target.value)} />
@@ -71,19 +60,15 @@ export function MultiMintModal({ placeholder="Enter amount" className="input input-bordered input-sm w-full mb-4" value={pair.amount} - onChange={(e) => - updatePayoutPair(index, "amount", e.target.value) - } + onChange={e => updatePayoutPair(index, 'amount', e.target.value)} />
-
diff --git a/components/factory/modals/updateDenomMetadata.tsx b/components/factory/modals/updateDenomMetadata.tsx index f18e77c0..74ed0c10 100644 --- a/components/factory/modals/updateDenomMetadata.tsx +++ b/components/factory/modals/updateDenomMetadata.tsx @@ -1,10 +1,10 @@ -import { useState } from "react"; -import { TokenFormData } from "@/helpers/formReducer"; -import { useFeeEstimation } from "@/hooks/useFeeEstimation"; -import { useTx } from "@/hooks/useTx"; -import { osmosis } from "@chalabi/manifestjs"; -import { chainName } from "@/config"; -import { DenomUnit } from "@chalabi/manifestjs/dist/codegen/cosmos/bank/v1beta1/bank"; +import { useState } from 'react'; +import { TokenFormData } from '@/helpers/formReducer'; +import { useFeeEstimation } from '@/hooks/useFeeEstimation'; +import { useTx } from '@/hooks/useTx'; +import { osmosis } from '@chalabi/manifestjs'; +import { chainName } from '@/config'; +import { DenomUnit } from '@chalabi/manifestjs/dist/codegen/cosmos/bank/v1beta1/bank'; export function UpdateDenomMetadataModal({ denom, @@ -26,16 +26,15 @@ export function UpdateDenomMetadataModal({ denomUnits: denom.denom_units, uri: denom.uri, uriHash: denom.uri_hash, - subdenom: denom.base.split("/").pop() || "", - exponent: denom?.denom_units[1]?.exponent?.toString() ?? "6", - label: denom?.denom_units[1]?.denom ?? "mfx", + subdenom: denom.base.split('/').pop() || '', + exponent: denom?.denom_units[1]?.exponent?.toString() ?? '6', + label: denom?.denom_units[1]?.denom ?? 'mfx', }); const [isSigning, setIsSigning] = useState(false); const { tx } = useTx(chainName); const { estimateFee } = useFeeEstimation(chainName); - const { setDenomMetadata } = - osmosis.tokenfactory.v1beta1.MessageComposer.withTypeUrl; + const { setDenomMetadata } = osmosis.tokenfactory.v1beta1.MessageComposer.withTypeUrl; const handleUpdate = async () => { setIsSigning(true); @@ -64,24 +63,20 @@ export function UpdateDenomMetadataModal({ }, }); } catch (error) { - console.error("Error during transaction setup:", error); + console.error('Error during transaction setup:', error); } finally { setIsSigning(false); } }; const updateField = (field: keyof TokenFormData, value: any) => { - setFormData((prev) => ({ ...prev, [field]: value })); + setFormData(prev => ({ ...prev, [field]: value })); }; - const updateDenomUnit = ( - index: number, - field: keyof DenomUnit, - value: any, - ) => { + const updateDenomUnit = (index: number, field: keyof DenomUnit, value: any) => { const updatedDenomUnits = [...formData.denomUnits]; updatedDenomUnits[index] = { ...updatedDenomUnits[index], [field]: value }; - updateField("denomUnits", updatedDenomUnits); + updateField('denomUnits', updatedDenomUnits); }; const isFormValid = () => { @@ -105,25 +100,20 @@ export function UpdateDenomMetadataModal({

Update Denom Metadata

- +
updateField("display", e.target.value)} + onChange={e => updateField('display', e.target.value)} required />
@@ -136,21 +126,20 @@ export function UpdateDenomMetadataModal({ id="name" className="input input-bordered w-full mt-1" value={formData.name} - onChange={(e) => updateField("name", e.target.value)} + onChange={e => updateField('name', e.target.value)} required />
updateField("symbol", e.target.value)} + onChange={e => updateField('symbol', e.target.value)} required />
@@ -158,15 +147,14 @@ export function UpdateDenomMetadataModal({
@@ -174,28 +162,26 @@ export function UpdateDenomMetadataModal({
updateField("uri", e.target.value)} + onChange={e => updateField('uri', e.target.value)} />
updateField("uriHash", e.target.value)} + onChange={e => updateField('uriHash', e.target.value)} />
@@ -205,10 +191,7 @@ export function UpdateDenomMetadataModal({
- updateDenomUnit(1, "denom", e.target.value) - } + value={formData.denomUnits[1]?.denom || ''} + onChange={e => updateDenomUnit(1, 'denom', e.target.value)} required />
  • - updateDenomUnit(1, "exponent", exp)} - > - {exp} - + updateDenomUnit(1, 'exponent', exp)}>{exp}
  • ))} @@ -291,11 +264,7 @@ export function UpdateDenomMetadataModal({ onClick={handleUpdate} disabled={isSigning || !isFormValid()} > - {isSigning ? ( - - ) : ( - "Update" - )} + {isSigning ? : 'Update'}
    diff --git a/components/groups/components/CountdownTimer.tsx b/components/groups/components/CountdownTimer.tsx index aede1308..d8d95cef 100644 --- a/components/groups/components/CountdownTimer.tsx +++ b/components/groups/components/CountdownTimer.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState } from 'react'; export default function CountdownTimer({ endTime }: { endTime: Date }) { const calculateTimeLeft = () => { @@ -6,9 +6,7 @@ export default function CountdownTimer({ endTime }: { endTime: Date }) { const timeDiff = endTime.getTime() - now.getTime(); if (timeDiff > 0) { const days = Math.floor(timeDiff / (1000 * 60 * 60 * 24)); - const hours = Math.floor( - (timeDiff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60), - ); + const hours = Math.floor((timeDiff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const min = Math.floor((timeDiff % (1000 * 60 * 60)) / (1000 * 60)); const sec = Math.floor((timeDiff % (1000 * 60)) / 1000); return { days, hours, min, sec }; @@ -32,7 +30,7 @@ export default function CountdownTimer({ endTime }: { endTime: Date }) {
    @@ -43,7 +41,7 @@ export default function CountdownTimer({ endTime }: { endTime: Date }) { = ({ - groupPolicyAddress, -}) => { +const NotificationsComponent: React.FC = ({ groupPolicyAddress }) => { const { messages } = useAbly(`group-${groupPolicyAddress}`); const [isOpen, setIsOpen] = useState(false); @@ -44,9 +42,7 @@ const NotificationsComponent: React.FC = ({ ); }; -const Notifications: React.FC = ({ - groupPolicyAddress, -}) => { +const Notifications: React.FC = ({ groupPolicyAddress }) => { return ( diff --git a/components/groups/components/StepIndicator.tsx b/components/groups/components/StepIndicator.tsx index 97241bc9..150985bd 100644 --- a/components/groups/components/StepIndicator.tsx +++ b/components/groups/components/StepIndicator.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from "react"; +import React, { ReactNode } from 'react'; export default function StepIndicator({ currentStep, @@ -10,10 +10,7 @@ export default function StepIndicator({ return (
      {steps.map(({ label, step }) => ( -
    • +
    • {label}
    • ))} diff --git a/components/groups/components/__tests__/CountdownTimer.test.tsx b/components/groups/components/__tests__/CountdownTimer.test.tsx index bdd18380..93c99551 100644 --- a/components/groups/components/__tests__/CountdownTimer.test.tsx +++ b/components/groups/components/__tests__/CountdownTimer.test.tsx @@ -1,65 +1,63 @@ -import { afterEach, describe, expect, test, jest } from "bun:test"; -import React from "react"; -import { screen, cleanup, render } from "@testing-library/react"; -import CountdownTimer from "@/components/groups/components/CountdownTimer"; -import matchers from "@testing-library/jest-dom/matchers"; +import { afterEach, describe, expect, test, jest } from 'bun:test'; +import React from 'react'; +import { screen, cleanup, render } from '@testing-library/react'; +import CountdownTimer from '@/components/groups/components/CountdownTimer'; +import matchers from '@testing-library/jest-dom/matchers'; expect.extend(matchers); -describe("CountdownTimer", () => { +describe('CountdownTimer', () => { afterEach(cleanup); - test("renders initial state correctly", () => { + test('renders initial state correctly', () => { jest.useFakeTimers(); - jest.setSystemTime(new Date("1992-01-01T00:00:00.000Z")); + jest.setSystemTime(new Date('1992-01-01T00:00:00.000Z')); const oneSecond = 1000; const oneMinute = oneSecond * 60; const oneHour = oneMinute * 60; const oneDay = oneHour * 24; // Now + 2 days - 1 hour - 2 minutes - 1 second - const endTime = new Date( - Date.now() + 2 * oneDay - oneHour - 2 * oneMinute - oneSecond, - ); + const endTime = new Date(Date.now() + 2 * oneDay - oneHour - 2 * oneMinute - oneSecond); render(); - expect(screen.getByText("days")).toBeInTheDocument(); - const daysSpan = screen.getByLabelText("days"); - expect(daysSpan).toHaveStyle("--value: 1"); + expect(screen.getByText('days')).toBeInTheDocument(); + const daysSpan = screen.getByLabelText('days'); + expect(daysSpan).toHaveStyle('--value: 1'); - expect(screen.getByText("hours")).toBeInTheDocument(); - const hoursSpan = screen.getByLabelText("hours"); - expect(hoursSpan).toHaveStyle("--value: 22"); + expect(screen.getByText('hours')).toBeInTheDocument(); + const hoursSpan = screen.getByLabelText('hours'); + expect(hoursSpan).toHaveStyle('--value: 22'); - expect(screen.getByText("min")).toBeInTheDocument(); - const minSpan = screen.getByLabelText("mins"); - expect(minSpan).toHaveStyle("--value: 57"); + expect(screen.getByText('min')).toBeInTheDocument(); + const minSpan = screen.getByLabelText('mins'); + expect(minSpan).toHaveStyle('--value: 57'); - expect(screen.getByText("sec")).toBeInTheDocument(); - const secSpan = screen.getByLabelText("secs"); - expect(secSpan).toHaveStyle("--value: 59"); + expect(screen.getByText('sec')).toBeInTheDocument(); + const secSpan = screen.getByLabelText('secs'); + expect(secSpan).toHaveStyle('--value: 59'); jest.useRealTimers(); }); - test("shows zero values when countdown is complete", () => { + test('shows zero values when countdown is complete', () => { const endTime = new Date(Date.now() - 1000); // 1 second ago render(); - expect(screen.getByText("days")).toBeInTheDocument(); - const daysSpan = screen.getByLabelText("days"); - expect(daysSpan).toHaveStyle("--value: 0"); + expect(screen.getByText('days')).toBeInTheDocument(); + const daysSpan = screen.getByLabelText('days'); + expect(daysSpan).toHaveStyle('--value: 0'); - expect(screen.getByText("hours")).toBeInTheDocument(); - const hoursSpan = screen.getByLabelText("hours"); - expect(hoursSpan).toHaveStyle("--value: 0"); + expect(screen.getByText('hours')).toBeInTheDocument(); + const hoursSpan = screen.getByLabelText('hours'); + expect(hoursSpan).toHaveStyle('--value: 0'); - expect(screen.getByText("min")).toBeInTheDocument(); - const minSpan = screen.getByLabelText("mins"); - expect(minSpan).toHaveStyle("--value: 0"); + expect(screen.getByText('min')).toBeInTheDocument(); + const minSpan = screen.getByLabelText('mins'); + expect(minSpan).toHaveStyle('--value: 0'); - expect(screen.getByText("sec")).toBeInTheDocument(); - const secSpan = screen.getByLabelText("secs"); - expect(secSpan).toHaveStyle("--value: 0"); + expect(screen.getByText('sec')).toBeInTheDocument(); + const secSpan = screen.getByLabelText('secs'); + expect(secSpan).toHaveStyle('--value: 0'); }); }); diff --git a/components/groups/components/__tests__/StepIndicator.test.tsx b/components/groups/components/__tests__/StepIndicator.test.tsx index c5c404d3..7cfbd7bb 100644 --- a/components/groups/components/__tests__/StepIndicator.test.tsx +++ b/components/groups/components/__tests__/StepIndicator.test.tsx @@ -1,42 +1,42 @@ -import { describe, test, expect, afterEach } from "bun:test"; -import React from "react"; -import { render, screen, cleanup } from "@testing-library/react"; -import StepIndicator from "@/components/groups/components/StepIndicator"; -import matchers from "@testing-library/jest-dom/matchers"; +import { describe, test, expect, afterEach } from 'bun:test'; +import React from 'react'; +import { render, screen, cleanup } from '@testing-library/react'; +import StepIndicator from '@/components/groups/components/StepIndicator'; +import matchers from '@testing-library/jest-dom/matchers'; expect.extend(matchers); -describe("StepIndicator Component", () => { +describe('StepIndicator Component', () => { afterEach(cleanup); const steps = [ - { label: "Step 1", step: 1 }, - { label: "Step 2", step: 2 }, - { label: "Step 3", step: 3 }, + { label: 'Step 1', step: 1 }, + { label: 'Step 2', step: 2 }, + { label: 'Step 3', step: 3 }, ]; - test("renders steps correctly", () => { + test('renders steps correctly', () => { render(); - expect(screen.getByText("Step 1")).toBeInTheDocument(); - expect(screen.getByText("Step 2")).toBeInTheDocument(); - expect(screen.getByText("Step 3")).toBeInTheDocument(); + expect(screen.getByText('Step 1')).toBeInTheDocument(); + expect(screen.getByText('Step 2')).toBeInTheDocument(); + expect(screen.getByText('Step 3')).toBeInTheDocument(); }); - test("highlights the current step correctly", () => { + test('highlights the current step correctly', () => { render(); - const currentStep = screen.getByText("Step 2"); - expect(currentStep).toHaveClass("step-primary"); + const currentStep = screen.getByText('Step 2'); + expect(currentStep).toHaveClass('step-primary'); }); - test("highlights the steps before the current step correctly", () => { + test('highlights the steps before the current step correctly', () => { render(); - const previousStep = screen.getByText("Step 1"); - expect(previousStep).toHaveClass("step-primary"); + const previousStep = screen.getByText('Step 1'); + expect(previousStep).toHaveClass('step-primary'); }); - test("does not highlight the steps after the current step", () => { + test('does not highlight the steps after the current step', () => { render(); - const nextStep = screen.getByText("Step 3"); - expect(nextStep).not.toHaveClass("step-primary"); + const nextStep = screen.getByText('Step 3'); + expect(nextStep).not.toHaveClass('step-primary'); }); }); diff --git a/components/groups/components/__tests__/groupInfo.test.tsx b/components/groups/components/__tests__/groupInfo.test.tsx index 3da338d6..1d21312c 100644 --- a/components/groups/components/__tests__/groupInfo.test.tsx +++ b/components/groups/components/__tests__/groupInfo.test.tsx @@ -1,31 +1,23 @@ -import { - afterEach, - describe, - expect, - test, - jest, - mock, - beforeAll, -} from "bun:test"; -import React from "react"; -import { screen, cleanup, fireEvent } from "@testing-library/react"; -import { GroupInfo } from "@/components/groups/components/groupInfo"; -import matchers from "@testing-library/jest-dom/matchers"; -import { renderWithChainProvider } from "@/tests/render"; -import { mockGroup } from "@/tests/mock"; +import { afterEach, describe, expect, test, jest, mock, beforeAll } from 'bun:test'; +import React from 'react'; +import { screen, cleanup, fireEvent } from '@testing-library/react'; +import { GroupInfo } from '@/components/groups/components/groupInfo'; +import matchers from '@testing-library/jest-dom/matchers'; +import { renderWithChainProvider } from '@/tests/render'; +import { mockGroup } from '@/tests/mock'; expect.extend(matchers); // Mock the useBalance hook const m = jest.fn(); -mock.module("@/hooks/useQueries", () => ({ +mock.module('@/hooks/useQueries', () => ({ useBalance: m, })); const defaultProps = { group: mockGroup, - address: "test_address", - policyAddress: "test_policy_address", + address: 'test_address', + policyAddress: 'test_policy_address', }; const renderWithProps = (props = {}) => { @@ -37,25 +29,25 @@ const renderWithProps = (props = {}) => { return renderWithChainProvider(); }; -describe("GroupInfo", () => { +describe('GroupInfo', () => { beforeAll(() => { - m.mockReturnValue({ balance: { amount: "1000000" } }); + m.mockReturnValue({ balance: { amount: '1000000' } }); }); afterEach(cleanup); - test("renders initial state correctly", () => { + test('renders initial state correctly', () => { renderWithProps(); - expect(screen.getByText("Info")).toBeInTheDocument(); - expect(screen.getByText("title1")).toBeInTheDocument(); - expect(screen.getByText("author1")).toBeInTheDocument(); - expect(screen.getByText("author2")).toBeInTheDocument(); - expect(screen.getByText("test_policy_...ddress")).toBeInTheDocument(); - expect(screen.getByText("5 / 10")).toBeInTheDocument(); + expect(screen.getByText('Info')).toBeInTheDocument(); + expect(screen.getByText('title1')).toBeInTheDocument(); + expect(screen.getByText('author1')).toBeInTheDocument(); + expect(screen.getByText('author2')).toBeInTheDocument(); + expect(screen.getByText('test_policy_...ddress')).toBeInTheDocument(); + expect(screen.getByText('5 / 10')).toBeInTheDocument(); }); test("renders 'No group Selected' when no group is provided", () => { renderWithProps({ group: null }); - expect(screen.getByText("No group Selected")).toBeInTheDocument(); + expect(screen.getByText('No group Selected')).toBeInTheDocument(); }); test("renders 'No authors available' when no authors are provided", () => { @@ -64,18 +56,18 @@ describe("GroupInfo", () => { group: { ...defaultProps.group, ipfsMetadata: { - authors: "", + authors: '', }, }, }; renderWithProps({ ...props }); - expect(screen.getByText("No authors available")).toBeInTheDocument(); + expect(screen.getByText('No authors available')).toBeInTheDocument(); }); test("renders 'No balance available' when no balance is provided", () => { m.mockReturnValue({ balance: { amount: undefined } }); renderWithProps(); - expect(screen.getByText("No balance available")).toBeInTheDocument(); + expect(screen.getByText('No balance available')).toBeInTheDocument(); }); // TODO: The following test fails because we allow the use of the `any` type @@ -113,7 +105,7 @@ describe("GroupInfo", () => { }, }; renderWithProps({ ...props }); - expect(screen.getByText("No threshold available")).toBeInTheDocument(); + expect(screen.getByText('No threshold available')).toBeInTheDocument(); }); test("renders 'No total weight available' when no total weight is provided", () => { @@ -121,32 +113,32 @@ describe("GroupInfo", () => { ...defaultProps, group: { ...defaultProps.group, - total_weight: "", + total_weight: '', }, }; renderWithProps({ ...props }); - expect(screen.getByText("No total weight available")).toBeInTheDocument(); + expect(screen.getByText('No total weight available')).toBeInTheDocument(); }); - test("triggers update modal on button click", () => { + test('triggers update modal on button click', () => { renderWithProps(); - const updateButton = screen.getByLabelText("update-btn"); + const updateButton = screen.getByLabelText('update-btn'); fireEvent.click(updateButton); const modal = document.getElementById( - `update_group_${defaultProps.group.id}`, + `update_group_${defaultProps.group.id}` ) as HTMLDialogElement; expect(modal).toBeInTheDocument(); - expect(screen.getByText("Update Group")).toBeInTheDocument(); + expect(screen.getByText('Update Group')).toBeInTheDocument(); }); - test("triggers group details modal on button click", () => { + test('triggers group details modal on button click', () => { renderWithProps(); - const moreInfoButton = screen.getByText("more info"); + const moreInfoButton = screen.getByText('more info'); fireEvent.click(moreInfoButton); const modal = document.getElementById( - `group_modal_${defaultProps.group.id}`, + `group_modal_${defaultProps.group.id}` ) as HTMLDialogElement; expect(modal).toBeInTheDocument(); - expect(screen.getByText("Group Details")).toBeInTheDocument(); + expect(screen.getByText('Group Details')).toBeInTheDocument(); }); }); diff --git a/components/groups/components/__tests__/groupProposals.test.tsx b/components/groups/components/__tests__/groupProposals.test.tsx index d4182f65..78cc1273 100644 --- a/components/groups/components/__tests__/groupProposals.test.tsx +++ b/components/groups/components/__tests__/groupProposals.test.tsx @@ -1,23 +1,15 @@ -import { - describe, - test, - afterEach, - expect, - jest, - mock, - afterAll, -} from "bun:test"; -import React from "react"; -import { screen, fireEvent, cleanup, waitFor } from "@testing-library/react"; -import ProposalsForPolicy from "@/components/groups/components/groupProposals"; -import matchers from "@testing-library/jest-dom/matchers"; -import { renderWithChainProvider } from "@/tests/render"; -import { mockProposals, mockGroup, mockGroup2 } from "@/tests/mock"; +import { describe, test, afterEach, expect, jest, mock, afterAll } from 'bun:test'; +import React from 'react'; +import { screen, fireEvent, cleanup, waitFor } from '@testing-library/react'; +import ProposalsForPolicy from '@/components/groups/components/groupProposals'; +import matchers from '@testing-library/jest-dom/matchers'; +import { renderWithChainProvider } from '@/tests/render'; +import { mockProposals, mockGroup, mockGroup2 } from '@/tests/mock'; expect.extend(matchers); // Mock next/router -mock.module("next/router", () => ({ +mock.module('next/router', () => ({ useRouter: jest.fn().mockReturnValue({ query: {}, push: jest.fn(), @@ -25,7 +17,7 @@ mock.module("next/router", () => ({ })); // Mock useQueries hooks -mock.module("@/hooks/useQueries", () => ({ +mock.module('@/hooks/useQueries', () => ({ useGroupsByMember: jest.fn().mockReturnValue({ groupByMemberData: { groups: [mockGroup, mockGroup2] }, isGroupByMemberLoading: false, @@ -33,7 +25,7 @@ mock.module("@/hooks/useQueries", () => ({ refetchGroupByMember: jest.fn(), }), useProposalsByPolicyAccount: jest.fn().mockReturnValue({ - proposals: mockProposals["test_policy_address"], + proposals: mockProposals['test_policy_address'], isProposalsLoading: false, isProposalsError: false, refetchProposals: jest.fn(), @@ -41,10 +33,10 @@ mock.module("@/hooks/useQueries", () => ({ useTallyCount: jest.fn().mockReturnValue({ tally: { tally: { - yes_count: "10", - no_count: "5", - abstain_count: "2", - no_with_veto_count: "1", + yes_count: '10', + no_count: '5', + abstain_count: '2', + no_with_veto_count: '1', }, }, isTallyLoading: false, @@ -58,17 +50,17 @@ mock.module("@/hooks/useQueries", () => ({ })); const mockProps = { - policyAddress: "test_policy_address", + policyAddress: 'test_policy_address', }; -describe("ProposalsForPolicy Component", () => { +describe('ProposalsForPolicy Component', () => { afterEach(() => { mock.restore(); cleanup(); }); - test("renders loading state correctly", () => { - mock.module("@/hooks/useQueries", () => ({ + test('renders loading state correctly', () => { + mock.module('@/hooks/useQueries', () => ({ useProposalsByPolicyAccount: jest.fn().mockReturnValue({ proposals: [], isProposalsLoading: true, @@ -77,11 +69,11 @@ describe("ProposalsForPolicy Component", () => { }), })); renderWithChainProvider(); - expect(screen.getByLabelText("loading")).toBeInTheDocument(); + expect(screen.getByLabelText('loading')).toBeInTheDocument(); }); - test("renders error state correctly", () => { - mock.module("@/hooks/useQueries", () => ({ + test('renders error state correctly', () => { + mock.module('@/hooks/useQueries', () => ({ useProposalsByPolicyAccount: jest.fn().mockReturnValue({ proposals: [], isProposalsLoading: false, @@ -90,11 +82,11 @@ describe("ProposalsForPolicy Component", () => { }), })); renderWithChainProvider(); - expect(screen.getByText("Error loading proposals")).toBeInTheDocument(); + expect(screen.getByText('Error loading proposals')).toBeInTheDocument(); }); - test("renders no proposals state correctly", () => { - mock.module("@/hooks/useQueries", () => ({ + test('renders no proposals state correctly', () => { + mock.module('@/hooks/useQueries', () => ({ useProposalsByPolicyAccount: jest.fn().mockReturnValue({ proposals: [], isProposalsLoading: false, @@ -103,9 +95,7 @@ describe("ProposalsForPolicy Component", () => { }), })); renderWithChainProvider(); - expect( - screen.getByText("No proposals found for this policy"), - ).toBeInTheDocument(); + expect(screen.getByText('No proposals found for this policy')).toBeInTheDocument(); }); // // TODO: We need to rework how this component works. diff --git a/components/groups/components/__tests__/myGroups.test.tsx b/components/groups/components/__tests__/myGroups.test.tsx index 284fa64d..8443f7ea 100644 --- a/components/groups/components/__tests__/myGroups.test.tsx +++ b/components/groups/components/__tests__/myGroups.test.tsx @@ -1,12 +1,12 @@ -import { describe, expect, test, jest, mock, afterEach } from "bun:test"; -import { screen, cleanup, waitFor, fireEvent } from "@testing-library/react"; -import { YourGroups } from "@/components/groups/components/myGroups"; -import { mockGroup, mockGroup2, mockProposals } from "@/tests/mock"; -import { renderWithChainProvider } from "@/tests/render"; +import { describe, expect, test, jest, mock, afterEach } from 'bun:test'; +import { screen, cleanup, waitFor, fireEvent } from '@testing-library/react'; +import { YourGroups } from '@/components/groups/components/myGroups'; +import { mockGroup, mockGroup2, mockProposals } from '@/tests/mock'; +import { renderWithChainProvider } from '@/tests/render'; // Mock useRouter const m = jest.fn(); -mock.module("next/router", () => ({ +mock.module('next/router', () => ({ useRouter: m.mockReturnValue({ query: {}, push: jest.fn(), @@ -28,42 +28,40 @@ function renderWithProps(props = {}) { return renderWithChainProvider(); } -describe("YourGroups Component", () => { +describe('YourGroups Component', () => { afterEach(cleanup); - test("renders empty group state correctly", () => { + test('renders empty group state correctly', () => { renderWithProps({ groups: { groups: [] } }); - expect(screen.getByText("My Groups")).toBeInTheDocument(); - expect(screen.getByPlaceholderText("Search...")).toBeInTheDocument(); - expect(screen.getByText("No groups found")).toBeInTheDocument(); + expect(screen.getByText('My Groups')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('Search...')).toBeInTheDocument(); + expect(screen.getByText('No groups found')).toBeInTheDocument(); }); - test("renders loading state correctly", () => { + test('renders loading state correctly', () => { renderWithProps(); - expect(screen.getByText("My Groups")).toBeInTheDocument(); - expect(screen.getByPlaceholderText("Search...")).toBeInTheDocument(); - expect(screen.getByText("title1")).toBeInTheDocument(); - expect(screen.getByText("title2")).toBeInTheDocument(); + expect(screen.getByText('My Groups')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('Search...')).toBeInTheDocument(); + expect(screen.getByText('title1')).toBeInTheDocument(); + expect(screen.getByText('title2')).toBeInTheDocument(); }); - test("search functionality works correctly", () => { + test('search functionality works correctly', () => { renderWithProps(); - const searchInput = screen.getByPlaceholderText("Search..."); - fireEvent.change(searchInput, { target: { value: "title1" } }); + const searchInput = screen.getByPlaceholderText('Search...'); + fireEvent.change(searchInput, { target: { value: 'title1' } }); - expect(screen.getByText("title1")).toBeInTheDocument(); - expect(screen.queryByText("title2")).not.toBeInTheDocument(); + expect(screen.getByText('title1')).toBeInTheDocument(); + expect(screen.queryByText('title2')).not.toBeInTheDocument(); }); - test("group selection works correctly", async () => { + test('group selection works correctly', async () => { mockOnSelectGroup.mockClear(); renderWithProps(); - const group1 = screen.getByText("title1"); + const group1 = screen.getByText('title1'); fireEvent.click(group1); - await waitFor(() => - expect(mockOnSelectGroup).toHaveBeenLastCalledWith("test_policy_address"), - ); + await waitFor(() => expect(mockOnSelectGroup).toHaveBeenLastCalledWith('test_policy_address')); }); }); diff --git a/components/groups/components/groupInfo.tsx b/components/groups/components/groupInfo.tsx index 83b7ca43..018ba547 100644 --- a/components/groups/components/groupInfo.tsx +++ b/components/groups/components/groupInfo.tsx @@ -1,9 +1,9 @@ -import { useBalance } from "@/hooks/useQueries"; -import { GroupDetailsModal, UpdateGroupModal } from "@/components"; -import { TruncatedAddressWithCopy } from "@/components/react/addressCopy"; -import { shiftDigits } from "@/utils"; -import { Key } from "react"; -import { PiArrowUpRightLight } from "react-icons/pi"; +import { useBalance } from '@/hooks/useQueries'; +import { GroupDetailsModal, UpdateGroupModal } from '@/components'; +import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; +import { shiftDigits } from '@/utils'; +import { Key } from 'react'; +import { PiArrowUpRightLight } from 'react-icons/pi'; export function GroupInfo({ group, @@ -22,32 +22,26 @@ export function GroupInfo({ const maybeAuthors = group?.ipfsMetadata?.authors; const maybePolicies = group?.policies?.[0]; - const threshold = - maybePolicies?.decision_policy?.threshold ?? "No threshold available"; + const threshold = maybePolicies?.decision_policy?.threshold ?? 'No threshold available'; const { balance } = useBalance(maybePolicies?.address); const renderAuthors = () => { if (maybeAuthors) { - if (maybeAuthors.startsWith("manifest")) { + if (maybeAuthors.startsWith('manifest')) { return ; - } else if (maybeAuthors.includes(",")) { + } else if (maybeAuthors.includes(',')) { return (
      - {maybeAuthors - .split(",") - .map((author: string, index: Key | null | undefined) => ( -
      - {author.trim().startsWith("manifest") ? ( - - ) : ( - {author.trim()} - )} -
      - ))} + {maybeAuthors.split(',').map((author: string, index: Key | null | undefined) => ( +
      + {author.trim().startsWith('manifest') ? ( + + ) : ( + {author.trim()} + )} +
      + ))}
      ); } else { @@ -67,7 +61,7 @@ export function GroupInfo({ @@ -183,7 +166,7 @@ export default function ProposalsForPolicy({ {isProposalsLoading ? (
      @@ -219,109 +202,91 @@ export default function ProposalsForPolicy({ - {filterProposals(proposals)?.map( - (proposal, index) => { - // Find the corresponding tally for this proposal - const proposalTally = tallies.find( - (t) => t.proposalId === proposal.id, - ); - const { isPassing = false } = proposalTally - ? isProposalPassing(proposalTally.tally) - : {}; - const endTime = new Date( - proposal?.voting_period_end, - ); - const now = new Date(); - const msPerMinute = 1000 * 60; - const msPerHour = msPerMinute * 60; - const msPerDay = msPerHour * 24; + {filterProposals(proposals)?.map((proposal, index) => { + // Find the corresponding tally for this proposal + const proposalTally = tallies.find(t => t.proposalId === proposal.id); + const { isPassing = false } = proposalTally + ? isProposalPassing(proposalTally.tally) + : {}; + const endTime = new Date(proposal?.voting_period_end); + const now = new Date(); + const msPerMinute = 1000 * 60; + const msPerHour = msPerMinute * 60; + const msPerDay = msPerHour * 24; - const diff = endTime.getTime() - now.getTime(); + const diff = endTime.getTime() - now.getTime(); - let timeLeft: string; + let timeLeft: string; - if (diff <= 0) { - timeLeft = "none"; - } else if (diff >= msPerDay) { - const days = Math.floor(diff / msPerDay); - timeLeft = `${days} day${ - days === 1 ? "" : "s" - }`; - } else if (diff >= msPerHour) { - const hours = Math.floor(diff / msPerHour); - timeLeft = `${hours} hour${ - hours === 1 ? "" : "s" - }`; - } else if (diff >= msPerMinute) { - const minutes = Math.floor(diff / msPerMinute); - timeLeft = `${minutes} minute${ - minutes === 1 ? "" : "s" - }`; - } else { - timeLeft = "less than a minute"; - } - return ( - handleRowClick(proposal)} - key={index} - style={{ maxHeight: "3rem" }} - className={`w-full + if (diff <= 0) { + timeLeft = 'none'; + } else if (diff >= msPerDay) { + const days = Math.floor(diff / msPerDay); + timeLeft = `${days} day${days === 1 ? '' : 's'}`; + } else if (diff >= msPerHour) { + const hours = Math.floor(diff / msPerHour); + timeLeft = `${hours} hour${hours === 1 ? '' : 's'}`; + } else if (diff >= msPerMinute) { + const minutes = Math.floor(diff / msPerMinute); + timeLeft = `${minutes} minute${minutes === 1 ? '' : 's'}`; + } else { + timeLeft = 'less than a minute'; + } + return ( + handleRowClick(proposal)} + key={index} + style={{ maxHeight: '3rem' }} + className={`w-full hover:bg-base-200 !important active:bg-base-100 focus:bg-base-300 focus:shadow-inner transition-all duration-200 cursor-pointer `} - > - - #{proposal.id.toString()} - - - {proposal.title.toLowerCase()} - - - {diff <= 0 && - proposal.executor_result === - ("PROPOSAL_EXECUTOR_RESULT_NOT_RUN" as unknown as ProposalExecutorResult) - ? "none" - : timeLeft} - - - {getHumanReadableType( - (proposal.messages[0] as any)["@type"], - )} - - - {isPassing && - diff > 0 && - proposal.executor_result === - ("PROPOSAL_EXECUTOR_RESULT_NOT_RUN" as unknown as ProposalExecutorResult) - ? "Passing" - : isPassing && - diff <= 0 && - proposal.executor_result === - ("PROPOSAL_EXECUTOR_RESULT_NOT_RUN" as unknown as ProposalExecutorResult) - ? "Passed" - : (diff > 0 && - proposal.executor_result === - ("PROPOSAL_EXECUTOR_RESULT_FAILURE" as unknown as ProposalExecutorResult)) || - (diff > 0 && - proposal.status === - ("PROPOSAL_STATUS_REJECTED" as unknown as ProposalStatus)) - ? "Failed" - : "Failing"} - - - - ); - }, - )} + > + #{proposal.id.toString()} + {proposal.title.toLowerCase()} + + {diff <= 0 && + proposal.executor_result === + ('PROPOSAL_EXECUTOR_RESULT_NOT_RUN' as unknown as ProposalExecutorResult) + ? 'none' + : timeLeft} + + + {getHumanReadableType((proposal.messages[0] as any)['@type'])} + + + {isPassing && + diff > 0 && + proposal.executor_result === + ('PROPOSAL_EXECUTOR_RESULT_NOT_RUN' as unknown as ProposalExecutorResult) + ? 'Passing' + : isPassing && + diff <= 0 && + proposal.executor_result === + ('PROPOSAL_EXECUTOR_RESULT_NOT_RUN' as unknown as ProposalExecutorResult) + ? 'Passed' + : (diff > 0 && + proposal.executor_result === + ('PROPOSAL_EXECUTOR_RESULT_FAILURE' as unknown as ProposalExecutorResult)) || + (diff > 0 && + proposal.status === + ('PROPOSAL_STATUS_REJECTED' as unknown as ProposalStatus)) + ? 'Failed' + : 'Failing'} + + + + ); + })}
      @@ -336,9 +301,7 @@ export default function ProposalsForPolicy({ ACTIVE - - {filterProposals(proposals).length} - + {filterProposals(proposals).length}
    @@ -350,10 +313,10 @@ export default function ProposalsForPolicy({ { filterProposals(proposals).filter( - (proposal) => + proposal => proposal.executor_result.toString() === - "PROPOSAL_EXECUTOR_RESULT_NOT_RUN" && - new Date(proposal.voting_period_end) < new Date(), + 'PROPOSAL_EXECUTOR_RESULT_NOT_RUN' && + new Date(proposal.voting_period_end) < new Date() ).length } @@ -368,18 +331,14 @@ export default function ProposalsForPolicy({ { } - +
    @@ -394,30 +353,21 @@ export default function ProposalsForPolicy({ const activeProposals = filterProposals(proposals); const now = new Date().getTime(); const futureActiveProposals = activeProposals.filter( - (proposal) => - new Date(proposal.voting_period_end).getTime() > - now, + proposal => new Date(proposal.voting_period_end).getTime() > now ); if (futureActiveProposals.length === 0) { - return "No active proposals ending soon"; + return 'No active proposals ending soon'; } - const closestEndingProposal = - futureActiveProposals.reduce( - (closest, proposal) => { - const proposalDate = new Date( - proposal.voting_period_end, - ).getTime(); - const closestDate = new Date( - closest.voting_period_end, - ).getTime(); + const closestEndingProposal = futureActiveProposals.reduce( + (closest, proposal) => { + const proposalDate = new Date(proposal.voting_period_end).getTime(); + const closestDate = new Date(closest.voting_period_end).getTime(); - return proposalDate - now < closestDate - now - ? proposal - : closest; - }, - ); + return proposalDate - now < closestDate - now ? proposal : closest; + } + ); return `#${closestEndingProposal.id.toString()}`; })()} @@ -446,14 +396,10 @@ function Modal({ members: MemberSDKType[]; proposal: ProposalSDKType; admin: string; - updateTally: ( - proposalId: bigint, - tally: QueryTallyResultResponseSDKType, - ) => void; + updateTally: (proposalId: bigint, tally: QueryTallyResultResponseSDKType) => void; refetchProposals: () => void; }) { - const { tally, isTallyLoading, isTallyError, refetchTally } = - useTallyCount(proposalId); + const { tally, isTallyLoading, isTallyError, refetchTally } = useTallyCount(proposalId); useEffect(() => { if (tally) { diff --git a/components/groups/components/index.tsx b/components/groups/components/index.tsx index ce96e89a..e7dda280 100644 --- a/components/groups/components/index.tsx +++ b/components/groups/components/index.tsx @@ -1,5 +1,5 @@ -export * from "./CountdownTimer"; -export * from "./groupInfo"; -export * from "./groupProposals"; -export * from "./myGroups"; -export * from "./StepIndicator"; +export * from './CountdownTimer'; +export * from './groupInfo'; +export * from './groupProposals'; +export * from './myGroups'; +export * from './StepIndicator'; diff --git a/components/groups/components/myGroups.tsx b/components/groups/components/myGroups.tsx index 526ce9c5..87039928 100644 --- a/components/groups/components/myGroups.tsx +++ b/components/groups/components/myGroups.tsx @@ -1,9 +1,9 @@ -import { ExtendedQueryGroupsByMemberResponseSDKType } from "@/hooks/useQueries"; -import ProfileAvatar from "@/utils/identicon"; -import { truncateString } from "@/utils"; -import { useEffect, useRef, useState } from "react"; -import { useRouter } from "next/router"; -import { ProposalSDKType } from "@chalabi/manifestjs/dist/codegen/cosmos/group/v1/types"; +import { ExtendedQueryGroupsByMemberResponseSDKType } from '@/hooks/useQueries'; +import ProfileAvatar from '@/utils/identicon'; +import { truncateString } from '@/utils'; +import { useEffect, useRef, useState } from 'react'; +import { useRouter } from 'next/router'; +import { ProposalSDKType } from '@chalabi/manifestjs/dist/codegen/cosmos/group/v1/types'; export function YourGroups({ groups, @@ -21,7 +21,7 @@ export function YourGroups({ proposals: any; // TODO: Define type }>) { const [selectedGroup, setSelectedGroup] = useState(null); - const [searchQuery, setSearchQuery] = useState(""); + const [searchQuery, setSearchQuery] = useState(''); const router = useRouter(); const groupRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}); @@ -47,7 +47,7 @@ export function YourGroups({ const scrollToGroup = (policyAddress: string) => { const groupElement = groupRefs.current[policyAddress]; if (groupElement) { - groupElement.scrollIntoView({ behavior: "smooth", block: "nearest" }); + groupElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } }; @@ -66,18 +66,16 @@ export function YourGroups({
    ); - const filteredGroups = groups.groups.filter((group) => - group.ipfsMetadata?.title - ?.toLowerCase() - .includes(searchQuery.toLowerCase()), + const filteredGroups = groups.groups.filter(group => + group.ipfsMetadata?.title?.toLowerCase().includes(searchQuery.toLowerCase()) ); const filterProposals = (proposals: ProposalSDKType[]) => { return proposals.filter( - (proposal) => - proposal.status.toString() !== "PROPOSAL_STATUS_ACCEPTED" && - proposal.status.toString() !== "PROPOSAL_STATUS_REJECTED" && - proposal.status.toString() !== "PROPOSAL_STATUS_WITHDRAWN", + proposal => + proposal.status.toString() !== 'PROPOSAL_STATUS_ACCEPTED' && + proposal.status.toString() !== 'PROPOSAL_STATUS_REJECTED' && + proposal.status.toString() !== 'PROPOSAL_STATUS_WITHDRAWN' ); }; @@ -90,7 +88,7 @@ export function YourGroups({ type="text" placeholder="Search..." value={searchQuery} - onChange={(e) => setSearchQuery(e.target.value)} + onChange={e => setSearchQuery(e.target.value)} className="input input-bordered input-xs w-1/3 max-w-xs" />
    @@ -103,46 +101,34 @@ export function YourGroups({ return (
    (groupRefs.current[policyAddress] = el)} + ref={el => (groupRefs.current[policyAddress] = el)} className={` relative flex flex-row justify-between rounded-md mb-4 mt-2 items-center px-4 py-2 hover:cursor-pointer transition-all duration-200 ${ selectedGroup === policyAddress - ? "bg-primary border-r-4 border-r-[#263c3add] border-b-[#263c3add] border-b-4 " - : "bg-base-300 border-r-4 border-r-base-200 border-b-base-200 border-b-4 active:scale-95 hover:bg-base-200" + ? 'bg-primary border-r-4 border-r-[#263c3add] border-b-[#263c3add] border-b-4 ' + : 'bg-base-300 border-r-4 border-r-base-200 border-b-base-200 border-b-4 active:scale-95 hover:bg-base-200' }`} onClick={() => handleGroupSelect(policyAddress)} > - {filterProposals(proposals[group?.policies[0]?.address ?? ""]) - .length > 0 && ( + {filterProposals(proposals[group?.policies[0]?.address ?? '']).length > 0 && (
    - { - filterProposals( - proposals[group?.policies[0]?.address ?? ""], - )?.length - } + {filterProposals(proposals[group?.policies[0]?.address ?? ''])?.length}
    )} - +
    - {truncateString( - group.ipfsMetadata?.title ?? "Untitled Group", - 24, - )} + {truncateString(group.ipfsMetadata?.title ?? 'Untitled Group', 24)}
    ); })} - {!groupByMemberDataLoading && - !groupByMemberDataError && - filteredGroups.length === 0 && ( -
    No groups found
    - )} + {!groupByMemberDataLoading && !groupByMemberDataError && filteredGroups.length === 0 && ( +
    No groups found
    + )}
    diff --git a/components/groups/forms/groups/ConfirmationForm.tsx b/components/groups/forms/groups/ConfirmationForm.tsx index 54f4c00c..b412f943 100644 --- a/components/groups/forms/groups/ConfirmationForm.tsx +++ b/components/groups/forms/groups/ConfirmationForm.tsx @@ -1,13 +1,13 @@ -import { useEffect, useState } from "react"; -import { TruncatedAddressWithCopy } from "@/components/react/addressCopy"; -import { FormData } from "@/helpers/formReducer"; -import { useFeeEstimation } from "@/hooks/useFeeEstimation"; -import { uploadJsonToIPFS } from "@/hooks/useIpfs"; -import { useTx } from "@/hooks/useTx"; -import { cosmos } from "@chalabi/manifestjs"; -import { ThresholdDecisionPolicy } from "@chalabi/manifestjs/dist/codegen/cosmos/group/v1/types"; -import { Duration } from "@chalabi/manifestjs/dist/codegen/google/protobuf/duration"; -import { useChain } from "@cosmos-kit/react"; +import { useEffect, useState } from 'react'; +import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; +import { FormData } from '@/helpers/formReducer'; +import { useFeeEstimation } from '@/hooks/useFeeEstimation'; +import { uploadJsonToIPFS } from '@/hooks/useIpfs'; +import { useTx } from '@/hooks/useTx'; +import { cosmos } from '@chalabi/manifestjs'; +import { ThresholdDecisionPolicy } from '@chalabi/manifestjs/dist/codegen/cosmos/group/v1/types'; +import { Duration } from '@chalabi/manifestjs/dist/codegen/google/protobuf/duration'; +import { useChain } from '@cosmos-kit/react'; export default function ConfirmationModal({ nextStep, @@ -18,7 +18,7 @@ export default function ConfirmationModal({ prevStep: () => void; formData: FormData; }>) { - const { address } = useChain("manifest"); + const { address } = useChain('manifest'); const { createGroupWithPolicy } = cosmos.group.v1.MessageComposer.withTypeUrl; const [isSigning, setIsSigning] = useState(false); const groupMetadata = { @@ -27,14 +27,14 @@ export default function ConfirmationModal({ summary: formData.summary, details: formData.description, proposalForumURL: formData.forumLink, - voteOptionContext: "", + voteOptionContext: '', }; // Convert the object to a JSON string const jsonString = JSON.stringify(groupMetadata); - const { tx } = useTx("manifest"); - const { estimateFee } = useFeeEstimation("manifest"); + const { tx } = useTx('manifest'); + const { estimateFee } = useFeeEstimation('manifest'); const uploadMetaDataToIPFS = async () => { const CID = await uploadJsonToIPFS(jsonString); @@ -54,12 +54,9 @@ export default function ConfirmationModal({ }, }; - const threshholdPolicyFromPartial = - ThresholdDecisionPolicy.fromPartial(thresholdMsg); + const threshholdPolicyFromPartial = ThresholdDecisionPolicy.fromPartial(thresholdMsg); - const threshholdPolicy = ThresholdDecisionPolicy.encode( - threshholdPolicyFromPartial, - ).finish(); + const threshholdPolicy = ThresholdDecisionPolicy.encode(threshholdPolicyFromPartial).finish(); const typeUrl = cosmos.group.v1.ThresholdDecisionPolicy.typeUrl; @@ -68,15 +65,15 @@ export default function ConfirmationModal({ try { const CID = await uploadMetaDataToIPFS(); const msg = createGroupWithPolicy({ - admin: address ?? "", - members: formData.members.map((member) => ({ + admin: address ?? '', + members: formData.members.map(member => ({ address: member.address, weight: member.weight, metadata: member.name, added_at: new Date(), })), groupMetadata: CID, - groupPolicyMetadata: "", + groupPolicyMetadata: '', groupPolicyAsAdmin: true, decisionPolicy: { threshold: formData.votingThreshold, @@ -85,7 +82,7 @@ export default function ConfirmationModal({ typeUrl: typeUrl, }, }); - const fee = await estimateFee(address ?? "", [msg]); + const fee = await estimateFee(address ?? '', [msg]); await tx([msg], { fee, onSuccess: () => { @@ -94,21 +91,21 @@ export default function ConfirmationModal({ }); } catch (error) { setIsSigning(false); - console.error("Error during transaction setup:", error); + console.error('Error during transaction setup:', error); } finally { setIsSigning(false); } }; const renderAuthors = () => { - if (formData.authors.startsWith("manifest")) { + if (formData.authors.startsWith('manifest')) { return ; - } else if (formData.authors.includes(",")) { + } else if (formData.authors.includes(',')) { return formData.authors - .split(",") + .split(',') .map((author, index) => (
    - {author.trim().startsWith("manifest") ? ( + {author.trim().startsWith('manifest') ? ( ) : ( {author.trim()} @@ -133,14 +130,10 @@ export default function ConfirmationModal({ {/* Group Details */}
    - +
    {formData.members.map((member, index) => (
    # {index + 1}
    @@ -236,7 +211,7 @@ export default function ConfirmationModal({ {isSigning ? ( ) : ( - "Sign Transaction" + 'Sign Transaction' )}
    diff --git a/components/groups/forms/groups/GroupDetailsForm.tsx b/components/groups/forms/groups/GroupDetailsForm.tsx index d497f380..fe4f37ff 100644 --- a/components/groups/forms/groups/GroupDetailsForm.tsx +++ b/components/groups/forms/groups/GroupDetailsForm.tsx @@ -1,6 +1,6 @@ -import { Action, FormData } from "@/helpers/formReducer"; -import Link from "next/link"; -import { PiAddressBook } from "react-icons/pi"; +import { Action, FormData } from '@/helpers/formReducer'; +import Link from 'next/link'; +import { PiAddressBook } from 'react-icons/pi'; export default function GroupDetails({ nextStep, @@ -14,7 +14,7 @@ export default function GroupDetails({ address: string; }>) { const updateField = (field: keyof FormData, value: any) => { - dispatch({ type: "UPDATE_FIELD", field, value }); + dispatch({ type: 'UPDATE_FIELD', field, value }); }; return (
    @@ -27,10 +27,7 @@ export default function GroupDetails({
    -
    -
    -
    -
    -
    @@ -118,16 +103,13 @@ export default function GroupDetails({ onClick={nextStep} className="w-full btn px-5 py-2.5 sm:py-3.5 btn-primary" disabled={ - !formData.title || - !formData.authors || - !formData.summary || - !formData.description + !formData.title || !formData.authors || !formData.summary || !formData.description } > Next: Group Policy
    - +
    {formData.members.map((member, index) => ( -
    +
    - {formData.authors.split(",").map((author, index) => ( + {formData.authors.split(',').map((author, index) => (
    - {author.trim().startsWith("manifest") ? ( + {author.trim().startsWith('manifest') ? ( ) : ( {author.trim()} @@ -39,15 +39,15 @@ export default function Success({ Your transaction was successfully signed and broadcasted.

    - You may now interact with your group by adding members, submitting or - voting on proposals, and changing group parameters. + You may now interact with your group by adding members, submitting or voting on proposals, + and changing group parameters.

    {/* TODO: Verify the render is correct. I changed the

    to a

    here because
    (in TruncatedAddressWithCopy) cannot be a descendant of

    */}

    - Remember to fund your group by sending tokens to the policy address{" "} + Remember to fund your group by sending tokens to the policy address{' '} @@ -80,26 +80,20 @@ export default function Success({

    {formData.forumLink}

    -

    - VOTING PERIOD -

    +

    VOTING PERIOD

    {formData.votingPeriod.seconds.toString()} seconds

    -

    - VOTING THRESHOLD -

    +

    VOTING THRESHOLD

    {formData.votingThreshold}

    - - + +
    diff --git a/components/groups/forms/groups/__tests__/ConfirmationForm.test.tsx b/components/groups/forms/groups/__tests__/ConfirmationForm.test.tsx index be160919..59aa832f 100644 --- a/components/groups/forms/groups/__tests__/ConfirmationForm.test.tsx +++ b/components/groups/forms/groups/__tests__/ConfirmationForm.test.tsx @@ -1,24 +1,24 @@ -import { describe, test, afterEach, expect, jest, mock } from "bun:test"; -import React from "react"; -import { screen, fireEvent, cleanup } from "@testing-library/react"; -import ConfirmationModal from "@/components/groups/forms/groups/ConfirmationForm"; -import matchers from "@testing-library/jest-dom/matchers"; -import { FormData } from "@/helpers/formReducer"; -import { renderWithChainProvider } from "@/tests/render"; +import { describe, test, afterEach, expect, jest, mock } from 'bun:test'; +import React from 'react'; +import { screen, fireEvent, cleanup } from '@testing-library/react'; +import ConfirmationModal from '@/components/groups/forms/groups/ConfirmationForm'; +import matchers from '@testing-library/jest-dom/matchers'; +import { FormData } from '@/helpers/formReducer'; +import { renderWithChainProvider } from '@/tests/render'; expect.extend(matchers); const mockFormData: FormData = { - title: "Test Group", - authors: "manifest1author", - summary: "This is a test group", - description: "Detailed description of the test group", - forumLink: "http://forumlink.com", - votingThreshold: "50%", + title: 'Test Group', + authors: 'manifest1author', + summary: 'This is a test group', + description: 'Detailed description of the test group', + forumLink: 'http://forumlink.com', + votingThreshold: '50%', votingPeriod: { seconds: BigInt(3600), nanos: 0 }, members: [ - { address: "manifest1member1", name: "Member 1", weight: "1" }, - { address: "manifest1member2", name: "Member 2", weight: "2" }, + { address: 'manifest1member1', name: 'Member 1', weight: '1' }, + { address: 'manifest1member2', name: 'Member 2', weight: '2' }, ], }; @@ -28,42 +28,42 @@ const mockProps = { formData: mockFormData, }; -describe("ConfirmationModal Component", () => { +describe('ConfirmationModal Component', () => { afterEach(cleanup); - test("renders component with correct details", () => { + test('renders component with correct details', () => { renderWithChainProvider(); - expect(screen.getByText("Confirmation")).toBeInTheDocument(); - expect(screen.getByText("GROUP DETAILS")).toBeInTheDocument(); - expect(screen.getByText("GROUP TITLE")).toBeInTheDocument(); - expect(screen.getByText("AUTHORS")).toBeInTheDocument(); - expect(screen.getByText("SUMMARY")).toBeInTheDocument(); - expect(screen.getByText("DESCRIPTION")).toBeInTheDocument(); - expect(screen.getByText("THRESHOLD")).toBeInTheDocument(); - expect(screen.getByText("VOTING PERIOD")).toBeInTheDocument(); - expect(screen.getByText("MEMBERS")).toBeInTheDocument(); + expect(screen.getByText('Confirmation')).toBeInTheDocument(); + expect(screen.getByText('GROUP DETAILS')).toBeInTheDocument(); + expect(screen.getByText('GROUP TITLE')).toBeInTheDocument(); + expect(screen.getByText('AUTHORS')).toBeInTheDocument(); + expect(screen.getByText('SUMMARY')).toBeInTheDocument(); + expect(screen.getByText('DESCRIPTION')).toBeInTheDocument(); + expect(screen.getByText('THRESHOLD')).toBeInTheDocument(); + expect(screen.getByText('VOTING PERIOD')).toBeInTheDocument(); + expect(screen.getByText('MEMBERS')).toBeInTheDocument(); }); test('calls prevStep when "Prev: Member Info" button is clicked', () => { renderWithChainProvider(); - const prevButton = screen.getByText("Prev: Member Info"); + const prevButton = screen.getByText('Prev: Member Info'); fireEvent.click(prevButton); expect(mockProps.prevStep).toHaveBeenCalled(); }); test('disables "Sign Transaction" button when isSigning is true', () => { renderWithChainProvider(); - const signButton = screen.getByText("Sign Transaction"); + const signButton = screen.getByText('Sign Transaction'); fireEvent.click(signButton); expect(signButton).toBeDisabled(); }); test('disables "Sign Transaction" button when address is not provided', () => { - mock.module("@/hooks/useChain", () => ({ - useChain: () => ({ address: "" }), + mock.module('@/hooks/useChain', () => ({ + useChain: () => ({ address: '' }), })); renderWithChainProvider(); - const signButton = screen.getByText("Sign Transaction"); + const signButton = screen.getByText('Sign Transaction'); fireEvent.click(signButton); expect(signButton).toBeDisabled(); }); diff --git a/components/groups/forms/groups/__tests__/GroupDetailsForm.test.tsx b/components/groups/forms/groups/__tests__/GroupDetailsForm.test.tsx index 2ebfdb6f..237b2e27 100644 --- a/components/groups/forms/groups/__tests__/GroupDetailsForm.test.tsx +++ b/components/groups/forms/groups/__tests__/GroupDetailsForm.test.tsx @@ -1,10 +1,10 @@ -import { afterEach, describe, expect, jest, test } from "bun:test"; -import React from "react"; -import { cleanup, fireEvent, screen } from "@testing-library/react"; -import GroupDetails from "@/components/groups/forms/groups/GroupDetailsForm"; -import matchers from "@testing-library/jest-dom/matchers"; -import { renderWithChainProvider } from "@/tests/render"; -import { mockGroupFormData } from "@/tests/mock"; +import { afterEach, describe, expect, jest, test } from 'bun:test'; +import React from 'react'; +import { cleanup, fireEvent, screen } from '@testing-library/react'; +import GroupDetails from '@/components/groups/forms/groups/GroupDetailsForm'; +import matchers from '@testing-library/jest-dom/matchers'; +import { renderWithChainProvider } from '@/tests/render'; +import { mockGroupFormData } from '@/tests/mock'; expect.extend(matchers); @@ -12,89 +12,85 @@ const mockProps = { nextStep: jest.fn(), formData: mockGroupFormData, dispatch: jest.fn(), - address: "cosmos1address", + address: 'cosmos1address', }; -describe("GroupDetails Component", () => { +describe('GroupDetails Component', () => { afterEach(cleanup); - test("renders component with correct details", () => { + test('renders component with correct details', () => { renderWithChainProvider(); - expect(screen.getByText("Group details")).toBeInTheDocument(); - expect(screen.getByText("Group Title")).toBeInTheDocument(); - expect(screen.getByText("Authors")).toBeInTheDocument(); - expect(screen.getByText("Summary")).toBeInTheDocument(); - expect(screen.getByText("Description")).toBeInTheDocument(); - expect(screen.getByText("Forum Link")).toBeInTheDocument(); + expect(screen.getByText('Group details')).toBeInTheDocument(); + expect(screen.getByText('Group Title')).toBeInTheDocument(); + expect(screen.getByText('Authors')).toBeInTheDocument(); + expect(screen.getByText('Summary')).toBeInTheDocument(); + expect(screen.getByText('Description')).toBeInTheDocument(); + expect(screen.getByText('Forum Link')).toBeInTheDocument(); }); - test("updates form fields correctly", () => { + test('updates form fields correctly', () => { renderWithChainProvider(); - const titleInput = screen.getByPlaceholderText("Title"); - fireEvent.change(titleInput, { target: { value: "New Group Title" } }); + const titleInput = screen.getByPlaceholderText('Title'); + fireEvent.change(titleInput, { target: { value: 'New Group Title' } }); expect(mockProps.dispatch).toHaveBeenCalledWith({ - type: "UPDATE_FIELD", - field: "title", - value: "New Group Title", + type: 'UPDATE_FIELD', + field: 'title', + value: 'New Group Title', }); - const authorsInput = screen.getByPlaceholderText( - "List of authors or address", - ); - fireEvent.change(authorsInput, { target: { value: "New Author" } }); + const authorsInput = screen.getByPlaceholderText('List of authors or address'); + fireEvent.change(authorsInput, { target: { value: 'New Author' } }); expect(mockProps.dispatch).toHaveBeenCalledWith({ - type: "UPDATE_FIELD", - field: "authors", - value: "New Author", + type: 'UPDATE_FIELD', + field: 'authors', + value: 'New Author', }); - const summaryInput = screen.getByPlaceholderText("Short Bio"); - fireEvent.change(summaryInput, { target: { value: "New Summary" } }); + const summaryInput = screen.getByPlaceholderText('Short Bio'); + fireEvent.change(summaryInput, { target: { value: 'New Summary' } }); expect(mockProps.dispatch).toHaveBeenCalledWith({ - type: "UPDATE_FIELD", - field: "summary", - value: "New Summary", + type: 'UPDATE_FIELD', + field: 'summary', + value: 'New Summary', }); - const descriptionInput = screen.getByPlaceholderText("Long Bio"); + const descriptionInput = screen.getByPlaceholderText('Long Bio'); fireEvent.change(descriptionInput, { - target: { value: "New Description" }, + target: { value: 'New Description' }, }); expect(mockProps.dispatch).toHaveBeenCalledWith({ - type: "UPDATE_FIELD", - field: "description", - value: "New Description", + type: 'UPDATE_FIELD', + field: 'description', + value: 'New Description', }); - const forumLinkInput = screen.getByPlaceholderText("Link to forum"); + const forumLinkInput = screen.getByPlaceholderText('Link to forum'); fireEvent.change(forumLinkInput, { - target: { value: "http://newforumlink.com" }, + target: { value: 'http://newforumlink.com' }, }); expect(mockProps.dispatch).toHaveBeenCalledWith({ - type: "UPDATE_FIELD", - field: "forumLink", - value: "http://newforumlink.com", + type: 'UPDATE_FIELD', + field: 'forumLink', + value: 'http://newforumlink.com', }); }); - test("next button is disabled when form is invalid", () => { - const invalidFormData = { ...mockGroupFormData, title: "" }; - renderWithChainProvider( - , - ); - const nextButton = screen.getByText("Next: Group Policy"); + test('next button is disabled when form is invalid', () => { + const invalidFormData = { ...mockGroupFormData, title: '' }; + renderWithChainProvider(); + const nextButton = screen.getByText('Next: Group Policy'); expect(nextButton).toBeDisabled(); }); - test("next button is enabled when form is valid", () => { + test('next button is enabled when form is valid', () => { renderWithChainProvider(); - const nextButton = screen.getByText("Next: Group Policy"); + const nextButton = screen.getByText('Next: Group Policy'); expect(nextButton).toBeEnabled(); }); - test("calls nextStep when next button is clicked", () => { + test('calls nextStep when next button is clicked', () => { renderWithChainProvider(); - const nextButton = screen.getByText("Next: Group Policy"); + const nextButton = screen.getByText('Next: Group Policy'); fireEvent.click(nextButton); expect(mockProps.nextStep).toHaveBeenCalled(); }); diff --git a/components/groups/forms/groups/__tests__/GroupPolicyForm.test.tsx b/components/groups/forms/groups/__tests__/GroupPolicyForm.test.tsx index 8bd9dded..13d081dc 100644 --- a/components/groups/forms/groups/__tests__/GroupPolicyForm.test.tsx +++ b/components/groups/forms/groups/__tests__/GroupPolicyForm.test.tsx @@ -1,10 +1,10 @@ -import { describe, test, afterEach, expect, jest } from "bun:test"; -import React from "react"; -import { screen, fireEvent, cleanup } from "@testing-library/react"; -import GroupPolicyForm from "@/components/groups/forms/groups/GroupPolicyForm"; -import matchers from "@testing-library/jest-dom/matchers"; -import { renderWithChainProvider } from "@/tests/render"; -import { mockGroupFormData } from "@/tests/mock"; +import { describe, test, afterEach, expect, jest } from 'bun:test'; +import React from 'react'; +import { screen, fireEvent, cleanup } from '@testing-library/react'; +import GroupPolicyForm from '@/components/groups/forms/groups/GroupPolicyForm'; +import matchers from '@testing-library/jest-dom/matchers'; +import { renderWithChainProvider } from '@/tests/render'; +import { mockGroupFormData } from '@/tests/mock'; expect.extend(matchers); @@ -15,56 +15,54 @@ const mockProps = { dispatch: jest.fn(), }; -describe("GroupPolicyForm Component", () => { +describe('GroupPolicyForm Component', () => { afterEach(cleanup); - test("renders component with correct details", () => { + test('renders component with correct details', () => { renderWithChainProvider(); - expect(screen.getByText("Group Policy")).toBeInTheDocument(); - expect(screen.getByText("Voting Period")).toBeInTheDocument(); - expect(screen.getByText("Voting Threshold")).toBeInTheDocument(); + expect(screen.getByText('Group Policy')).toBeInTheDocument(); + expect(screen.getByText('Voting Period')).toBeInTheDocument(); + expect(screen.getByText('Voting Threshold')).toBeInTheDocument(); }); - test("updates form fields correctly", () => { + test('updates form fields correctly', () => { renderWithChainProvider(); - const votingAmountInput = screen.getByPlaceholderText("Enter duration"); - fireEvent.change(votingAmountInput, { target: { value: "2" } }); + const votingAmountInput = screen.getByPlaceholderText('Enter duration'); + fireEvent.change(votingAmountInput, { target: { value: '2' } }); expect(votingAmountInput).toHaveValue(2); - const votingThresholdInput = screen.getByPlaceholderText("e.g. (1)"); - fireEvent.change(votingThresholdInput, { target: { value: "3" } }); + const votingThresholdInput = screen.getByPlaceholderText('e.g. (1)'); + fireEvent.change(votingThresholdInput, { target: { value: '3' } }); expect(mockProps.dispatch).toHaveBeenCalledWith({ - type: "UPDATE_FIELD", - field: "votingThreshold", - value: "3", + type: 'UPDATE_FIELD', + field: 'votingThreshold', + value: '3', }); }); - test("next button is disabled when form is invalid", () => { - const invalidFormData = { ...mockGroupFormData, votingThreshold: "" }; - renderWithChainProvider( - , - ); - const nextButton = screen.getByText("Next: Member Info"); + test('next button is disabled when form is invalid', () => { + const invalidFormData = { ...mockGroupFormData, votingThreshold: '' }; + renderWithChainProvider(); + const nextButton = screen.getByText('Next: Member Info'); expect(nextButton).toBeDisabled(); }); - test("next button is enabled when form is valid", () => { + test('next button is enabled when form is valid', () => { renderWithChainProvider(); - const nextButton = screen.getByText("Next: Member Info"); + const nextButton = screen.getByText('Next: Member Info'); expect(nextButton).toBeEnabled(); }); - test("calls nextStep when next button is clicked", () => { + test('calls nextStep when next button is clicked', () => { renderWithChainProvider(); - const nextButton = screen.getByText("Next: Member Info"); + const nextButton = screen.getByText('Next: Member Info'); fireEvent.click(nextButton); expect(mockProps.nextStep).toHaveBeenCalled(); }); - test("calls prevStep when prev button is clicked", () => { + test('calls prevStep when prev button is clicked', () => { renderWithChainProvider(); - const prevButton = screen.getByText("Prev: Group Details"); + const prevButton = screen.getByText('Prev: Group Details'); fireEvent.click(prevButton); expect(mockProps.prevStep).toHaveBeenCalled(); }); diff --git a/components/groups/forms/groups/__tests__/MemberInfoForm.test.tsx b/components/groups/forms/groups/__tests__/MemberInfoForm.test.tsx index d49346dc..c565e292 100644 --- a/components/groups/forms/groups/__tests__/MemberInfoForm.test.tsx +++ b/components/groups/forms/groups/__tests__/MemberInfoForm.test.tsx @@ -1,10 +1,10 @@ -import { describe, test, afterEach, expect, jest } from "bun:test"; -import React from "react"; -import { screen, fireEvent, cleanup } from "@testing-library/react"; -import MemberInfoForm from "@/components/groups/forms/groups/MemberInfoForm"; -import matchers from "@testing-library/jest-dom/matchers"; -import { renderWithChainProvider } from "@/tests/render"; -import { mockGroupFormData } from "@/tests/mock"; +import { describe, test, afterEach, expect, jest } from 'bun:test'; +import React from 'react'; +import { screen, fireEvent, cleanup } from '@testing-library/react'; +import MemberInfoForm from '@/components/groups/forms/groups/MemberInfoForm'; +import matchers from '@testing-library/jest-dom/matchers'; +import { renderWithChainProvider } from '@/tests/render'; +import { mockGroupFormData } from '@/tests/mock'; expect.extend(matchers); @@ -13,113 +13,111 @@ const mockProps = { prevStep: jest.fn(), formData: mockGroupFormData, dispatch: jest.fn(), - address: "cosmos1address", + address: 'cosmos1address', }; -describe("MemberInfoForm Component", () => { +describe('MemberInfoForm Component', () => { afterEach(cleanup); - test("renders component with correct details", () => { + test('renders component with correct details', () => { renderWithChainProvider(); - expect(screen.getByText("Member Info")).toBeInTheDocument(); + expect(screen.getByText('Member Info')).toBeInTheDocument(); - const addr0 = screen.getByLabelText("address-0"); + const addr0 = screen.getByLabelText('address-0'); expect(addr0).toBeInTheDocument(); - expect(addr0).toHaveValue("manifest1member1"); + expect(addr0).toHaveValue('manifest1member1'); - const addr1 = screen.getByLabelText("address-1"); + const addr1 = screen.getByLabelText('address-1'); expect(addr1).toBeInTheDocument(); - expect(addr1).toHaveValue("manifest1member2"); + expect(addr1).toHaveValue('manifest1member2'); - const name0 = screen.getByLabelText("name-0"); + const name0 = screen.getByLabelText('name-0'); expect(name0).toBeInTheDocument(); - expect(name0).toHaveValue("Member 1"); + expect(name0).toHaveValue('Member 1'); - const name1 = screen.getByLabelText("name-1"); + const name1 = screen.getByLabelText('name-1'); expect(name1).toBeInTheDocument(); - expect(name1).toHaveValue("Member 2"); + expect(name1).toHaveValue('Member 2'); - const weight0 = screen.getByLabelText("weight-0"); + const weight0 = screen.getByLabelText('weight-0'); expect(weight0).toBeInTheDocument(); - expect(weight0).toHaveValue("1"); + expect(weight0).toHaveValue('1'); - const weight1 = screen.getByLabelText("weight-1"); + const weight1 = screen.getByLabelText('weight-1'); expect(weight1).toBeInTheDocument(); - expect(weight1).toHaveValue("2"); + expect(weight1).toHaveValue('2'); }); - test("updates form fields correctly", () => { + test('updates form fields correctly', () => { renderWithChainProvider(); - const addressInput = screen.getByLabelText("address-0"); - fireEvent.change(addressInput, { target: { value: "newaddress" } }); + const addressInput = screen.getByLabelText('address-0'); + fireEvent.change(addressInput, { target: { value: 'newaddress' } }); expect(mockProps.dispatch).toHaveBeenCalledWith({ - type: "UPDATE_MEMBER", + type: 'UPDATE_MEMBER', index: 0, - field: "address", - value: "newaddress", + field: 'address', + value: 'newaddress', }); - const nameInput = screen.getByLabelText("name-0"); - fireEvent.change(nameInput, { target: { value: "New Name" } }); + const nameInput = screen.getByLabelText('name-0'); + fireEvent.change(nameInput, { target: { value: 'New Name' } }); expect(mockProps.dispatch).toHaveBeenCalledWith({ - type: "UPDATE_MEMBER", + type: 'UPDATE_MEMBER', index: 0, - field: "name", - value: "New Name", + field: 'name', + value: 'New Name', }); - const weightInput = screen.getByLabelText("weight-0"); - fireEvent.change(weightInput, { target: { value: "3" } }); + const weightInput = screen.getByLabelText('weight-0'); + fireEvent.change(weightInput, { target: { value: '3' } }); expect(mockProps.dispatch).toHaveBeenCalledWith({ - type: "UPDATE_MEMBER", + type: 'UPDATE_MEMBER', index: 0, - field: "weight", - value: "3", + field: 'weight', + value: '3', }); }); - test("next button is disabled when form is invalid", () => { + test('next button is disabled when form is invalid', () => { const invalidFormData = { ...mockGroupFormData, - members: [{ address: "", name: "", weight: "" }], + members: [{ address: '', name: '', weight: '' }], }; - renderWithChainProvider( - , - ); - const nextButton = screen.getByText("Next: Group Policy"); + renderWithChainProvider(); + const nextButton = screen.getByText('Next: Group Policy'); expect(nextButton).toBeDisabled(); }); - test("next button is enabled when form is valid", () => { + test('next button is enabled when form is valid', () => { renderWithChainProvider(); - const nextButton = screen.getByText("Next: Group Policy"); + const nextButton = screen.getByText('Next: Group Policy'); expect(nextButton).toBeEnabled(); }); - test("calls nextStep when next button is clicked", () => { + test('calls nextStep when next button is clicked', () => { renderWithChainProvider(); - const nextButton = screen.getByText("Next: Group Policy"); + const nextButton = screen.getByText('Next: Group Policy'); fireEvent.click(nextButton); expect(mockProps.nextStep).toHaveBeenCalled(); }); - test("calls prevStep when prev button is clicked", () => { + test('calls prevStep when prev button is clicked', () => { renderWithChainProvider(); - const prevButton = screen.getByText("Prev: Group Policy"); + const prevButton = screen.getByText('Prev: Group Policy'); fireEvent.click(prevButton); expect(mockProps.prevStep).toHaveBeenCalled(); }); - test("increases and decreases number of members correctly", () => { + test('increases and decreases number of members correctly', () => { renderWithChainProvider(); - const increaseButton = screen.getByText("+"); - const decreaseButton = screen.getByText("-"); - const memberCountInput = screen.getByLabelText("member-count"); + const increaseButton = screen.getByText('+'); + const decreaseButton = screen.getByText('-'); + const memberCountInput = screen.getByLabelText('member-count'); fireEvent.click(increaseButton); - expect(memberCountInput).toHaveValue("3"); + expect(memberCountInput).toHaveValue('3'); fireEvent.click(decreaseButton); - expect(memberCountInput).toHaveValue("2"); + expect(memberCountInput).toHaveValue('2'); }); }); diff --git a/components/groups/forms/groups/__tests__/Success.test.tsx b/components/groups/forms/groups/__tests__/Success.test.tsx index 77b7bb2d..5af959c7 100644 --- a/components/groups/forms/groups/__tests__/Success.test.tsx +++ b/components/groups/forms/groups/__tests__/Success.test.tsx @@ -1,9 +1,9 @@ -import { describe, test, afterEach, expect, jest } from "bun:test"; -import React from "react"; -import { render, screen, cleanup } from "@testing-library/react"; -import Success from "@/components/groups/forms/groups/Success"; -import matchers from "@testing-library/jest-dom/matchers"; -import { mockGroupFormData } from "@/tests/mock"; +import { describe, test, afterEach, expect, jest } from 'bun:test'; +import React from 'react'; +import { render, screen, cleanup } from '@testing-library/react'; +import Success from '@/components/groups/forms/groups/Success'; +import matchers from '@testing-library/jest-dom/matchers'; +import { mockGroupFormData } from '@/tests/mock'; expect.extend(matchers); @@ -12,27 +12,23 @@ const mockProps = { prevStep: jest.fn(), }; -describe("Success Component", () => { +describe('Success Component', () => { afterEach(cleanup); - test("renders component with correct details", () => { + test('renders component with correct details', () => { render(); - expect(screen.getByText("Success!")).toBeInTheDocument(); + expect(screen.getByText('Success!')).toBeInTheDocument(); expect( - screen.getByText( - "Your transaction was successfully signed and broadcasted.", - ), + screen.getByText('Your transaction was successfully signed and broadcasted.') ).toBeInTheDocument(); - expect(screen.getByText("Group Details")).toBeInTheDocument(); + expect(screen.getByText('Group Details')).toBeInTheDocument(); expect(screen.getByText(mockGroupFormData.title)).toBeInTheDocument(); - expect(screen.getByText("manifest1autho...author")).toBeInTheDocument(); + expect(screen.getByText('manifest1autho...author')).toBeInTheDocument(); expect(screen.getByText(mockGroupFormData.summary)).toBeInTheDocument(); expect(screen.getByText(mockGroupFormData.description)).toBeInTheDocument(); expect(screen.getByText(mockGroupFormData.forumLink)).toBeInTheDocument(); - expect(screen.getByText("3600 seconds")).toBeInTheDocument(); - expect( - screen.getByText(mockGroupFormData.votingThreshold), - ).toBeInTheDocument(); + expect(screen.getByText('3600 seconds')).toBeInTheDocument(); + expect(screen.getByText(mockGroupFormData.votingThreshold)).toBeInTheDocument(); }); // TODO: Test for `Back to Groups Page` button diff --git a/components/groups/forms/groups/index.tsx b/components/groups/forms/groups/index.tsx index 60558e29..05586dfa 100644 --- a/components/groups/forms/groups/index.tsx +++ b/components/groups/forms/groups/index.tsx @@ -1,4 +1,4 @@ -export * from "./ConfirmationForm"; -export * from "./GroupDetailsForm"; -export * from "./MemberInfoForm"; -export * from "./GroupPolicyForm"; +export * from './ConfirmationForm'; +export * from './GroupDetailsForm'; +export * from './MemberInfoForm'; +export * from './GroupPolicyForm'; diff --git a/components/groups/forms/index.tsx b/components/groups/forms/index.tsx index 128a4c6a..6a376e2a 100644 --- a/components/groups/forms/index.tsx +++ b/components/groups/forms/index.tsx @@ -1,2 +1,2 @@ -export * from "./groups"; -export * from "./proposals"; +export * from './groups'; +export * from './proposals'; diff --git a/components/groups/forms/proposals/ConfirmationForm.tsx b/components/groups/forms/proposals/ConfirmationForm.tsx index cac18ff2..457e14d7 100644 --- a/components/groups/forms/proposals/ConfirmationForm.tsx +++ b/components/groups/forms/proposals/ConfirmationForm.tsx @@ -1,24 +1,21 @@ -import React from "react"; -import { useFeeEstimation } from "@/hooks/useFeeEstimation"; -import { uploadJsonToIPFS } from "@/hooks/useIpfs"; -import { useTx } from "@/hooks/useTx"; -import { manifest, strangelove_ventures, cosmos } from "@chalabi/manifestjs"; -import { Any } from "@chalabi/manifestjs/dist/codegen/google/protobuf/any"; +import React from 'react'; +import { useFeeEstimation } from '@/hooks/useFeeEstimation'; +import { uploadJsonToIPFS } from '@/hooks/useIpfs'; +import { useTx } from '@/hooks/useTx'; +import { manifest, strangelove_ventures, cosmos } from '@chalabi/manifestjs'; +import { Any } from '@chalabi/manifestjs/dist/codegen/google/protobuf/any'; -import { TruncatedAddressWithCopy } from "@/components/react/addressCopy"; -import { ProposalFormData } from "@/helpers/formReducer"; -import { chainName } from "@/config"; -import { - MsgMultiSend, - MsgSend, -} from "@chalabi/manifestjs/dist/codegen/cosmos/bank/v1beta1/tx"; +import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; +import { ProposalFormData } from '@/helpers/formReducer'; +import { chainName } from '@/config'; +import { MsgMultiSend, MsgSend } from '@chalabi/manifestjs/dist/codegen/cosmos/bank/v1beta1/tx'; import { MsgRemovePending, MsgRemoveValidator, MsgSetPower, MsgUpdateStakingParams, MsgUpdateParams as MsgUpdatePoaParams, -} from "@chalabi/manifestjs/dist/codegen/strangelove_ventures/poa/v1/tx"; +} from '@chalabi/manifestjs/dist/codegen/strangelove_ventures/poa/v1/tx'; import { MsgCreateGroupWithPolicy, MsgExec, @@ -30,15 +27,15 @@ import { MsgUpdateGroupPolicyAdmin, MsgVote, MsgWithdrawProposal, -} from "@chalabi/manifestjs/dist/codegen/cosmos/group/v1/tx"; +} from '@chalabi/manifestjs/dist/codegen/cosmos/group/v1/tx'; import { MsgPayout, MsgUpdateParams as MsgUpdateManifestParams, -} from "@chalabi/manifestjs/dist/codegen/manifest/v1/tx"; +} from '@chalabi/manifestjs/dist/codegen/manifest/v1/tx'; import { MsgSoftwareUpgrade, MsgCancelUpgrade, -} from "@chalabi/manifestjs/dist/codegen/cosmos/upgrade/v1beta1/tx"; +} from '@chalabi/manifestjs/dist/codegen/cosmos/upgrade/v1beta1/tx'; export default function ConfirmationModal({ policyAddress, @@ -82,56 +79,42 @@ export default function ConfirmationModal({ [K in keyof MessageTypeMap]: (value: MessageTypeMap[K]) => any; } = { send: cosmos.bank.v1beta1.MessageComposer.withTypeUrl.send, - updatePoaParams: - strangelove_ventures.poa.v1.MessageComposer.withTypeUrl.updateParams, - removeValidator: - strangelove_ventures.poa.v1.MessageComposer.withTypeUrl.removeValidator, - removePending: - strangelove_ventures.poa.v1.MessageComposer.withTypeUrl.removePending, + updatePoaParams: strangelove_ventures.poa.v1.MessageComposer.withTypeUrl.updateParams, + removeValidator: strangelove_ventures.poa.v1.MessageComposer.withTypeUrl.removeValidator, + removePending: strangelove_ventures.poa.v1.MessageComposer.withTypeUrl.removePending, updateStakingParams: - strangelove_ventures.poa.v1.MessageComposer.withTypeUrl - .updateStakingParams, + strangelove_ventures.poa.v1.MessageComposer.withTypeUrl.updateStakingParams, setPower: strangelove_ventures.poa.v1.MessageComposer.withTypeUrl.setPower, updateManifestParams: manifest.v1.MessageComposer.withTypeUrl.updateParams, payoutStakeholders: manifest.v1.MessageComposer.withTypeUrl.payout, - updateGroupAdmin: - cosmos.group.v1.MessageComposer.withTypeUrl.updateGroupAdmin, - updateGroupMembers: - cosmos.group.v1.MessageComposer.withTypeUrl.updateGroupMembers, - updateGroupMetadata: - cosmos.group.v1.MessageComposer.withTypeUrl.updateGroupMetadata, - updateGroupPolicyAdmin: - cosmos.group.v1.MessageComposer.withTypeUrl.updateGroupPolicyAdmin, - createGroupWithPolicy: - cosmos.group.v1.MessageComposer.withTypeUrl.createGroupWithPolicy, + updateGroupAdmin: cosmos.group.v1.MessageComposer.withTypeUrl.updateGroupAdmin, + updateGroupMembers: cosmos.group.v1.MessageComposer.withTypeUrl.updateGroupMembers, + updateGroupMetadata: cosmos.group.v1.MessageComposer.withTypeUrl.updateGroupMetadata, + updateGroupPolicyAdmin: cosmos.group.v1.MessageComposer.withTypeUrl.updateGroupPolicyAdmin, + createGroupWithPolicy: cosmos.group.v1.MessageComposer.withTypeUrl.createGroupWithPolicy, submitProposal: cosmos.group.v1.MessageComposer.withTypeUrl.submitProposal, vote: cosmos.group.v1.MessageComposer.withTypeUrl.vote, - withdrawProposal: - cosmos.group.v1.MessageComposer.withTypeUrl.withdrawProposal, + withdrawProposal: cosmos.group.v1.MessageComposer.withTypeUrl.withdrawProposal, customMessage: cosmos.bank.v1beta1.MessageComposer.withTypeUrl.send, exec: cosmos.group.v1.MessageComposer.withTypeUrl.exec, leaveGroup: cosmos.group.v1.MessageComposer.withTypeUrl.leaveGroup, multiSend: cosmos.bank.v1beta1.MessageComposer.withTypeUrl.multiSend, - softwareUpgrade: - cosmos.upgrade.v1beta1.MessageComposer.withTypeUrl.softwareUpgrade, - cancelUpgrade: - cosmos.upgrade.v1beta1.MessageComposer.withTypeUrl.cancelUpgrade, + softwareUpgrade: cosmos.upgrade.v1beta1.MessageComposer.withTypeUrl.softwareUpgrade, + cancelUpgrade: cosmos.upgrade.v1beta1.MessageComposer.withTypeUrl.cancelUpgrade, }; const snakeToCamel = (str: string): string => - str.replace(/([-_][a-z])/gi, ($1) => - $1.toUpperCase().replace("-", "").replace("_", ""), - ); + str.replace(/([-_][a-z])/gi, $1 => $1.toUpperCase().replace('-', '').replace('_', '')); const convertKeysToCamelCase = (obj: any): any => { if (Array.isArray(obj)) { - return obj.map((v) => convertKeysToCamelCase(v)); - } else if (obj !== null && typeof obj === "object") { + return obj.map(v => convertKeysToCamelCase(v)); + } else if (obj !== null && typeof obj === 'object') { return Object.keys(obj).reduce( (acc, key) => { acc[snakeToCamel(key)] = convertKeysToCamelCase(obj[key]); return acc; }, - {} as Record, + {} as Record ); } return obj; @@ -142,15 +125,15 @@ export default function ConfirmationModal({ authors: formData.metadata.authors, summary: formData.metadata.summary, details: formData.metadata.details, - proposalForumURL: "", - voteOptionContext: "", + proposalForumURL: '', + voteOptionContext: '', }; const jsonString = JSON.stringify(proposalMetadata); const { tx, isSigning, setIsSigning } = useTx(chainName); - const { estimateFee } = useFeeEstimation("manifest"); + const { estimateFee } = useFeeEstimation('manifest'); const uploadMetaDataToIPFS = async () => { const CID = await uploadJsonToIPFS(jsonString); @@ -161,9 +144,8 @@ export default function ConfirmationModal({ setIsSigning(true); const CID = await uploadMetaDataToIPFS(); - const messages = formData.messages.map((message) => { - const composer = - messageTypeToComposer[message.type as keyof MessageTypeMap]; + const messages = formData.messages.map(message => { + const composer = messageTypeToComposer[message.type as keyof MessageTypeMap]; if (!composer) { throw new Error(`Unknown message type: ${message.type}`); } @@ -180,12 +162,11 @@ export default function ConfirmationModal({ }); const testMessage = { - typeUrl: "/cosmos.bank.v1beta1.MsgSend", + typeUrl: '/cosmos.bank.v1beta1.MsgSend', value: MsgSend.toProtoMsg({ - fromAddress: - "manifest1afk9zr2hn2jsac63h4hm60vl9z3e5u69gndzf7c99cqge3vzwjzsfmy9qj", - toAddress: "manifest1uwqjtgjhjctjc45ugy7ev5prprhehc7wclherd", - amount: [{ amount: "1", denom: "umfx" }], + fromAddress: 'manifest1afk9zr2hn2jsac63h4hm60vl9z3e5u69gndzf7c99cqge3vzwjzsfmy9qj', + toAddress: 'manifest1uwqjtgjhjctjc45ugy7ev5prprhehc7wclherd', + amount: [{ amount: '1', denom: 'umfx' }], }), }; @@ -205,14 +186,13 @@ export default function ConfirmationModal({ metadata: CID, messages: [ { - typeUrl: "/cosmos.bank.v1beta1.MsgSend", + typeUrl: '/cosmos.bank.v1beta1.MsgSend', value: MsgSend.encode( MsgSend.fromPartial({ - fromAddress: - "manifest1afk9zr2hn2jsac63h4hm60vl9z3e5u69gndzf7c99cqge3vzwjzsfmy9qj", - toAddress: "manifest1uwqjtgjhjctjc45ugy7ev5prprhehc7wclherd", - amount: [{ amount: "1", denom: "umfx" }], - }), + fromAddress: 'manifest1afk9zr2hn2jsac63h4hm60vl9z3e5u69gndzf7c99cqge3vzwjzsfmy9qj', + toAddress: 'manifest1uwqjtgjhjctjc45ugy7ev5prprhehc7wclherd', + amount: [{ amount: '1', denom: 'umfx' }], + }) ).finish(), }, ], @@ -223,8 +203,8 @@ export default function ConfirmationModal({ try { const fee = { - amount: [{ amount: "1000", denom: "umfx" }], - gas: "1000000", + amount: [{ amount: '1000', denom: 'umfx' }], + gas: '1000000', }; await tx([msg], { fee, @@ -233,7 +213,7 @@ export default function ConfirmationModal({ }, }); } catch (error) { - console.error("Transaction error:", error); + console.error('Transaction error:', error); } finally { setIsSigning(false); } @@ -253,10 +233,7 @@ export default function ConfirmationModal({
    {/* Proposal Details */}
    -
    @@ -292,14 +266,10 @@ export default function ConfirmationModal({ key={index} className="flex flex-col bg-base-100 p-2 mb-3 h-18 rounded-md relative" > -
    - # {index + 1} -
    +
    # {index + 1}
    @@ -309,9 +279,7 @@ export default function ConfirmationModal({
    - +
    @@ -333,20 +301,15 @@ export default function ConfirmationModal({ DETAILS
    -
    diff --git a/components/groups/forms/proposals/ProposalDetailsForm.tsx b/components/groups/forms/proposals/ProposalDetailsForm.tsx index c8133961..e0e65264 100644 --- a/components/groups/forms/proposals/ProposalDetailsForm.tsx +++ b/components/groups/forms/proposals/ProposalDetailsForm.tsx @@ -1,21 +1,19 @@ -import React from "react"; -import { Formik, Form } from "formik"; -import * as Yup from "yup"; -import { ProposalFormData, ProposalAction } from "@/helpers/formReducer"; -import Link from "next/link"; -import { PiAddressBook } from "react-icons/pi"; -import { TextInput, TextArea } from "@/components/react/inputs"; +import React from 'react'; +import { Formik, Form } from 'formik'; +import * as Yup from 'yup'; +import { ProposalFormData, ProposalAction } from '@/helpers/formReducer'; +import Link from 'next/link'; +import { PiAddressBook } from 'react-icons/pi'; +import { TextInput, TextArea } from '@/components/react/inputs'; const ProposalSchema = Yup.object().shape({ - title: Yup.string() - .required("Title is required") - .max(50, "Title must not exceed 50 characters"), + title: Yup.string().required('Title is required').max(50, 'Title must not exceed 50 characters'), proposers: Yup.string() - .required("Proposer is required") - .max(200, "Proposers must not exceed 200 characters"), + .required('Proposer is required') + .max(200, 'Proposers must not exceed 200 characters'), summary: Yup.string() - .required("Summary is required") - .min(10, "Summary must be at least 10 characters") - .max(500, "Summary must not exceed 500 characters"), + .required('Summary is required') + .min(10, 'Summary must be at least 10 characters') + .max(500, 'Summary must not exceed 500 characters'), }); export default function ProposalDetails({ @@ -30,7 +28,7 @@ export default function ProposalDetails({ address: string; }>) { const updateField = (field: keyof ProposalFormData, value: any) => { - dispatch({ type: "UPDATE_FIELD", field, value }); + dispatch({ type: 'UPDATE_FIELD', field, value }); }; console.log(formData); return ( @@ -56,8 +54,8 @@ export default function ProposalDetails({ placeholder="Title" value={formData.title} onChange={(e: React.ChangeEvent) => { - updateField("title", e.target.value); - setFieldValue("title", e.target.value); + updateField('title', e.target.value); + setFieldValue('title', e.target.value); }} />
    @@ -67,8 +65,8 @@ export default function ProposalDetails({ placeholder="List of authors" value={formData.proposers} onChange={(e: React.ChangeEvent) => { - updateField("proposers", e.target.value); - setFieldValue("proposers", e.target.value); + updateField('proposers', e.target.value); + setFieldValue('proposers', e.target.value); }} className="rounded-tr-none rounded-br-none" /> @@ -76,8 +74,8 @@ export default function ProposalDetails({ type="button" aria-label="address-btn" onClick={() => { - setFieldValue("proposers", address); - updateField("proposers", address); + setFieldValue('proposers', address); + updateField('proposers', address); }} className="btn btn-primary rounded-tr-lg rounded-br-lg rounded-bl-none rounded-tl-none" > @@ -90,8 +88,8 @@ export default function ProposalDetails({ placeholder="Short Bio" value={formData.summary} onChange={(e: React.ChangeEvent) => { - updateField("summary", e.target.value); - setFieldValue("summary", e.target.value); + updateField('summary', e.target.value); + setFieldValue('summary', e.target.value); }} />
    diff --git a/components/groups/forms/proposals/ProposalMessages.tsx b/components/groups/forms/proposals/ProposalMessages.tsx index 90b25f0a..7164ad11 100644 --- a/components/groups/forms/proposals/ProposalMessages.tsx +++ b/components/groups/forms/proposals/ProposalMessages.tsx @@ -1,18 +1,13 @@ -import React, { useEffect, useState } from "react"; -import { - ProposalFormData, - ProposalAction, - Message, - MessageFields, -} from "@/helpers/formReducer"; +import React, { useEffect, useState } from 'react'; +import { ProposalFormData, ProposalAction, Message, MessageFields } from '@/helpers/formReducer'; -import * as initialMessages from "./messages"; -import { FiArrowUp, FiMinusCircle, FiPlusCircle } from "react-icons/fi"; -import { TextInput } from "@/components/react/inputs"; +import * as initialMessages from './messages'; +import { FiArrowUp, FiMinusCircle, FiPlusCircle } from 'react-icons/fi'; +import { TextInput } from '@/components/react/inputs'; -import { Formik, Form, Field, FieldProps, FormikProps } from "formik"; +import { Formik, Form, Field, FieldProps, FormikProps } from 'formik'; -import * as Yup from "yup"; +import * as Yup from 'yup'; export default function ProposalMessages({ formData, @@ -27,15 +22,15 @@ export default function ProposalMessages({ }>) { const [isFormValid, setIsFormValid] = useState(false); const [visibleMessages, setVisibleMessages] = useState( - formData.messages.map(() => false), + formData.messages.map(() => false) ); const isMessageValid = (message: Message): boolean => { const checkFields = (obj: any): boolean => { for (const key in obj) { - if (typeof obj[key] === "object" && obj[key] !== null) { + if (typeof obj[key] === 'object' && obj[key] !== null) { if (!checkFields(obj[key])) return false; - } else if (obj[key] === "" || obj[key] === undefined) { + } else if (obj[key] === '' || obj[key] === undefined) { return false; } } @@ -56,7 +51,7 @@ export default function ProposalMessages({ const handleAddMessage = () => { dispatch({ - type: "ADD_MESSAGE", + type: 'ADD_MESSAGE', message: initialMessages.initialSendMessage, }); setVisibleMessages([...visibleMessages, false]); @@ -64,159 +59,153 @@ export default function ProposalMessages({ }; const handleRemoveMessage = (index: number) => { - dispatch({ type: "REMOVE_MESSAGE", index }); + dispatch({ type: 'REMOVE_MESSAGE', index }); setVisibleMessages(visibleMessages.filter((_, i) => i !== index)); checkFormValidity(); }; const toggleVisibility = (index: number) => { - setVisibleMessages( - visibleMessages.map((visible, i) => (i === index ? !visible : visible)), - ); + setVisibleMessages(visibleMessages.map((visible, i) => (i === index ? !visible : visible))); }; const isValidAddress = (address: string) => { - return address.startsWith("manifest"); + return address.startsWith('manifest'); }; - const handleChangeMessage = ( - index: number, - field: MessageFields, - value: any, - ) => { + const handleChangeMessage = (index: number, field: MessageFields, value: any) => { let updatedMessage = { ...formData.messages[index] }; - if (field === "type") { + if (field === 'type') { switch (value) { - case "send": + case 'send': updatedMessage = { ...initialMessages.initialSendMessage, type: value, }; break; - case "customMessage": + case 'customMessage': updatedMessage = { ...initialMessages.initialCustomMessage, type: value, }; break; - case "removeValidator": + case 'removeValidator': updatedMessage = { ...initialMessages.initialRemoveValidatorMessage, type: value, - sender: "", - validator_address: "", + sender: '', + validator_address: '', }; break; - case "removePending": + case 'removePending': updatedMessage = { ...initialMessages.initialRemovePendingMessage, type: value, }; break; - case "updatePoaParams": + case 'updatePoaParams': updatedMessage = { ...initialMessages.initialUpdatePoaParamsMessage, type: value, }; break; - case "updateStakingParams": + case 'updateStakingParams': updatedMessage = { ...initialMessages.initialUpdateStakingParamsMessage, type: value, }; break; - case "setPower": + case 'setPower': updatedMessage = { ...initialMessages.initialSetPowerMessage, type: value, }; break; - case "updateManifestParams": + case 'updateManifestParams': updatedMessage = { ...initialMessages.initialUpdateManifestParamsMessage, type: value, }; break; - case "payoutStakeholders": + case 'payoutStakeholders': updatedMessage = { ...initialMessages.initialPayoutStakeholdersMessage, type: value, }; break; - case "updateGroupAdmin": + case 'updateGroupAdmin': updatedMessage = { ...initialMessages.initialUpdateGroupAdminMessage, type: value, }; break; - case "updateGroupMembers": + case 'updateGroupMembers': updatedMessage = { ...initialMessages.initialUpdateGroupMembersMessage, type: value, }; break; - case "updateGroupMetadata": + case 'updateGroupMetadata': updatedMessage = { ...initialMessages.initialUpdateGroupMetadataMessage, type: value, }; break; - case "updateGroupPolicyAdmin": + case 'updateGroupPolicyAdmin': updatedMessage = { ...initialMessages.initialUpdateGroupPolicyAdminMessage, type: value, }; break; - case "createGroupWithPolicy": + case 'createGroupWithPolicy': updatedMessage = { ...initialMessages.initialCreateGroupWithPolicyMessage, type: value, }; break; - case "submitProposal": + case 'submitProposal': updatedMessage = { ...initialMessages.initialSubmitProposalMessage, type: value, }; break; - case "vote": + case 'vote': updatedMessage = { ...initialMessages.initialVoteMessage, type: value, }; break; - case "withdrawProposal": + case 'withdrawProposal': updatedMessage = { ...initialMessages.initialWithdrawProposalMessage, type: value, }; break; - case "exec": + case 'exec': updatedMessage = { ...initialMessages.initialExecMessage, type: value, }; break; - case "leaveGroup": + case 'leaveGroup': updatedMessage = { ...initialMessages.initialLeaveGroupMessage, type: value, }; break; - case "multiSend": + case 'multiSend': updatedMessage = { ...initialMessages.initialMultiSendMessage, type: value, }; break; - case "softwareUpgrade": + case 'softwareUpgrade': updatedMessage = { ...initialMessages.initialSoftwareUpgradeMessage, type: value, }; break; - case "cancelUpgrade": + case 'cancelUpgrade': updatedMessage = { ...initialMessages.initialCancelUpgradeMessage, type: value, @@ -228,7 +217,7 @@ export default function ProposalMessages({ } else { (updatedMessage as any)[field as string] = value; } - dispatch({ type: "UPDATE_MESSAGE", index, message: updatedMessage }); + dispatch({ type: 'UPDATE_MESSAGE', index, message: updatedMessage }); checkFormValidity(); }; @@ -237,68 +226,53 @@ export default function ProposalMessages({ const renderInputs = ( object: Record, handleChange: (field: string, value: any) => void, - path = "", + path = '' ) => { const generateValidationSchema = (obj: Record): any => { return Yup.object().shape( - Object.entries(obj).reduce( - (schema: Record, [key, value]) => { - if (key === "type") return schema; - - if ( - typeof value === "object" && - value !== null && - !Array.isArray(value) - ) { - schema[key] = generateValidationSchema(value); - } else { - schema[key] = Yup.string().required(`${key} is required`); - - if (key.includes("address")) { - schema[key] = schema[key].test( - "is-valid-address", - "Invalid address format, must start with manifest", - (val: string) => isValidAddress(val), - ); - } else if (key.includes("amount")) { - schema[key] = Yup.number() - .positive("Amount must be positive") - .required("Amount is required"); - } + Object.entries(obj).reduce((schema: Record, [key, value]) => { + if (key === 'type') return schema; + + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + schema[key] = generateValidationSchema(value); + } else { + schema[key] = Yup.string().required(`${key} is required`); + + if (key.includes('address')) { + schema[key] = schema[key].test( + 'is-valid-address', + 'Invalid address format, must start with manifest', + (val: string) => isValidAddress(val) + ); + } else if (key.includes('amount')) { + schema[key] = Yup.number() + .positive('Amount must be positive') + .required('Amount is required'); } - return schema; - }, - {}, - ), + } + return schema; + }, {}) ); }; const validationSchema = generateValidationSchema(object); - const renderField = ( - fieldPath: string, - fieldValue: any, - formikProps: FormikProps, - ) => { + const renderField = (fieldPath: string, fieldValue: any, formikProps: FormikProps) => { const { setFieldValue, errors, touched } = formikProps; - if ( - typeof fieldValue === "object" && - fieldValue !== null && - !Array.isArray(fieldValue) - ) { + if (typeof fieldValue === 'object' && fieldValue !== null && !Array.isArray(fieldValue)) { return (

    - {fieldPath.split(".").pop()?.replace(/_/g, " ")} + {fieldPath.split('.').pop()?.replace(/_/g, ' ')}

    {Object.entries(fieldValue).map(([key, value]) => - renderField(`${fieldPath}.${key}`, value, formikProps), + renderField(`${fieldPath}.${key}`, value, formikProps) )}
    ); - } else if (typeof fieldValue === "boolean") { + } else if (typeof fieldValue === 'boolean') { return ( {({ field }: FieldProps) => ( @@ -308,15 +282,13 @@ export default function ProposalMessages({ {...field} checked={field.value} className="checkbox checkbox-sm mr-2" - onChange={(e) => { + onChange={e => { field.onChange(e); handleChange(fieldPath, e.target.checked); setFieldValue(fieldPath, e.target.checked); }} /> - - {fieldPath.split(".").pop()?.replace(/_/g, " ")} - + {fieldPath.split('.').pop()?.replace(/_/g, ' ')} )} @@ -327,8 +299,8 @@ export default function ProposalMessages({ {({ field }: FieldProps) => (
    ) => { field.onChange(e); @@ -353,8 +325,7 @@ export default function ProposalMessages({ {(formikProps: FormikProps) => (
    {Object.entries(object).map( - ([key, value]) => - key !== "type" && renderField(key, value, formikProps), + ([key, value]) => key !== 'type' && renderField(key, value, formikProps) )}
    )} @@ -368,7 +339,7 @@ export default function ProposalMessages({ } const handleChange = (field: string, value: any) => { - const fieldPath = field.split("."); + const fieldPath = field.split('.'); let updatedMessage: any = { ...formData.messages[index] }; let current = updatedMessage; @@ -377,7 +348,7 @@ export default function ProposalMessages({ } current[fieldPath[fieldPath.length - 1]] = value; - dispatch({ type: "UPDATE_MESSAGE", index, message: updatedMessage }); + dispatch({ type: 'UPDATE_MESSAGE', index, message: updatedMessage }); }; return ( @@ -391,31 +362,31 @@ export default function ProposalMessages({ nextStep(); }; - const [searchTerm, setSearchTerm] = useState(""); + const [searchTerm, setSearchTerm] = useState(''); const filteredMessageTypes = [ - "send", - "customMessage", - "removeValidator", - "removePending", - "updatePoaParams", - "updateStakingParams", - "setPower", - "updateManifestParams", - "payoutStakeholders", - "updateGroupAdmin", - "updateGroupMembers", - "updateGroupMetadata", - "updateGroupPolicyAdmin", - "createGroupWithPolicy", - "submitProposal", - "vote", - "withdrawProposal", - "exec", - "leaveGroup", - "multiSend", - "softwareUpgrade", - "cancelUpgrade", - ].filter((type) => type.toLowerCase().includes(searchTerm.toLowerCase())); + 'send', + 'customMessage', + 'removeValidator', + 'removePending', + 'updatePoaParams', + 'updateStakingParams', + 'setPower', + 'updateManifestParams', + 'payoutStakeholders', + 'updateGroupAdmin', + 'updateGroupMembers', + 'updateGroupMetadata', + 'updateGroupPolicyAdmin', + 'createGroupWithPolicy', + 'submitProposal', + 'vote', + 'withdrawProposal', + 'exec', + 'leaveGroup', + 'multiSend', + 'softwareUpgrade', + 'cancelUpgrade', + ].filter(type => type.toLowerCase().includes(searchTerm.toLowerCase())); return (
    @@ -424,15 +395,13 @@ export default function ProposalMessages({
    -

    - Messages -

    +

    Messages

      - setSearchTerm(e.target.value) - } - style={{ boxShadow: "none" }} + onChange={e => setSearchTerm(e.target.value)} + style={{ boxShadow: 'none' }} />
    - {filteredMessageTypes.map((type) => ( + {filteredMessageTypes.map(type => (
  • ))} @@ -513,7 +473,7 @@ export default function ProposalMessages({ type="button" className="btn btn-secondary btn-xs" onClick={() => handleRemoveMessage(index)} - aria-label={"remove-message-btn"} + aria-label={'remove-message-btn'} > @@ -528,9 +488,7 @@ export default function ProposalMessages({ > @@ -539,9 +497,7 @@ export default function ProposalMessages({
    {visibleMessages[index] && ( -
    - {renderMessageFields(message, index)} -
    +
    {renderMessageFields(message, index)}
    )}
    ))} @@ -562,9 +518,7 @@ export default function ProposalMessages({ onClick={prevStep} className="text-center btn btn-neutral items-center w-1/2 py-2.5 sm:py-3.5 text-sm font-medium focus:outline-none rounded-lg border" > - - Prev: Proposal Details - + Prev: Proposal Details Prev: Info diff --git a/components/groups/forms/proposals/ProposalMetadataForm.tsx b/components/groups/forms/proposals/ProposalMetadataForm.tsx index 65b3409d..7db13afe 100644 --- a/components/groups/forms/proposals/ProposalMetadataForm.tsx +++ b/components/groups/forms/proposals/ProposalMetadataForm.tsx @@ -1,24 +1,22 @@ -import React from "react"; -import { Formik, Form } from "formik"; -import * as Yup from "yup"; -import { ProposalFormData, ProposalAction } from "@/helpers/formReducer"; -import { TextInput, TextArea } from "@/components/react/inputs"; +import React from 'react'; +import { Formik, Form } from 'formik'; +import * as Yup from 'yup'; +import { ProposalFormData, ProposalAction } from '@/helpers/formReducer'; +import { TextInput, TextArea } from '@/components/react/inputs'; const ProposalSchema = Yup.object().shape({ - title: Yup.string() - .required("Title is required") - .max(50, "Title must not exceed 50 characters"), + title: Yup.string().required('Title is required').max(50, 'Title must not exceed 50 characters'), authors: Yup.string() - .required("Authors are required") - .max(200, "Authors must not exceed 200 characters"), + .required('Authors are required') + .max(200, 'Authors must not exceed 200 characters'), summary: Yup.string() - .required("Summary is required") - .min(10, "Summary must be at least 10 characters") - .max(500, "Summary must not exceed 500 characters"), + .required('Summary is required') + .min(10, 'Summary must be at least 10 characters') + .max(500, 'Summary must not exceed 500 characters'), details: Yup.string() - .required("Details are required") - .min(10, "Details must be at least 10 characters") - .max(500, "Summary must not exceed 500 characters"), + .required('Details are required') + .min(10, 'Details must be at least 10 characters') + .max(500, 'Summary must not exceed 500 characters'), }); export default function ProposalMetadataForm({ @@ -32,13 +30,10 @@ export default function ProposalMetadataForm({ formData: ProposalFormData; dispatch: React.Dispatch; }>) { - const handleChange = ( - field: keyof ProposalFormData["metadata"], - value: any, - ) => { + const handleChange = (field: keyof ProposalFormData['metadata'], value: any) => { dispatch({ - type: "UPDATE_FIELD", - field: "metadata", + type: 'UPDATE_FIELD', + field: 'metadata', value: { ...formData.metadata, [field]: value, @@ -69,8 +64,8 @@ export default function ProposalMetadataForm({ placeholder="Type here" value={formData.metadata.title} onChange={(e: React.ChangeEvent) => { - handleChange("title", e.target.value); - setFieldValue("title", e.target.value); + handleChange('title', e.target.value); + setFieldValue('title', e.target.value); }} /> ) => { - handleChange("authors", e.target.value); - setFieldValue("authors", e.target.value); + handleChange('authors', e.target.value); + setFieldValue('authors', e.target.value); }} />
    @@ -572,14 +514,14 @@ export function UpdateGroupModal({ key={index} className={`flex relative flex-col gap-2 px-4 py-2 rounded-md border-4 ${ member.isAdmin && member.isPolicyAdmin - ? "border-r-primary border-b-primary border-l-secondary border-t-secondary" + ? 'border-r-primary border-b-primary border-l-secondary border-t-secondary' : member.isAdmin - ? "border-r-primary border-b-primary border-t-transparent border-l-transparent" + ? 'border-r-primary border-b-primary border-t-transparent border-l-transparent' : member.isPolicyAdmin - ? "border-l-secondary border-t-secondary border-r-base-100 border-b-base-100 " - : "border-r-transparent border-b-transparent border-t-transparent border-l-transparent" + ? 'border-l-secondary border-t-secondary border-r-base-100 border-b-base-100 ' + : 'border-r-transparent border-b-transparent border-t-transparent border-l-transparent' } transition-all duration-200 max-h-[12.4rem] ${ - !member.isActive ? "bg-base-100" : "bg-base-200" + !member.isActive ? 'bg-base-100' : 'bg-base-200' } `} >
    @@ -589,15 +531,11 @@ export function UpdateGroupModal({ onClick={() => handleMemberRemoval(index)} className={`btn btn-sm ${ member.isActive - ? "text-red-500 hover:bg-red-500" - : "text-primary hover:bg-primary " + ? 'text-red-500 hover:bg-red-500' + : 'text-primary hover:bg-primary ' } hover:text-white bg-base-300`} > - {member.isActive ? ( - - ) : ( - - )} + {member.isActive ? : }
    @@ -605,42 +543,26 @@ export function UpdateGroupModal({ type="text" disabled={member.isCoreMember && !member.isActive} value={member.member.metadata} - onChange={(e) => - handleChange(index, "metadata", e.target.value) - } + onChange={e => handleChange(index, 'metadata', e.target.value)} className="input input-sm input-bordered w-full disabled:border-base-100" - placeholder={ - member.isCoreMember ? member.member.metadata : "Name" - } + placeholder={member.isCoreMember ? member.member.metadata : 'Name'} /> - handleChange(index, "address", e.target.value) - } + onChange={e => handleChange(index, 'address', e.target.value)} className="input input-sm input-bordered w-full disabled:border-base-100" - placeholder={ - member.isCoreMember ? member.member.address : "Address" - } + placeholder={member.isCoreMember ? member.member.address : 'Address'} /> - handleChange(index, "weight", e.target.value) - } + value={member.isCoreMember && !member.isActive ? '0' : member.member.weight} + onChange={e => handleChange(index, 'weight', e.target.value)} className="input input-sm input-bordered w-full disabled:border-base-100" - placeholder={ - member.isCoreMember ? member.member.weight : "Weight" - } + placeholder={member.isCoreMember ? member.member.weight : 'Weight'} />
    @@ -652,7 +574,7 @@ export function UpdateGroupModal({
    diff --git a/components/groups/modals/voteDetailsModal.tsx b/components/groups/modals/voteDetailsModal.tsx index 9ac047cc..b50187d7 100644 --- a/components/groups/modals/voteDetailsModal.tsx +++ b/components/groups/modals/voteDetailsModal.tsx @@ -1,5 +1,5 @@ -import React, { useEffect, useState, useMemo } from "react"; -import dynamic from "next/dynamic"; +import React, { useEffect, useState, useMemo } from 'react'; +import dynamic from 'next/dynamic'; import { MemberSDKType, @@ -9,24 +9,24 @@ import { ProposalStatus, VoteOption, VoteSDKType, -} from "@chalabi/manifestjs/dist/codegen/cosmos/group/v1/types"; -import { QueryTallyResultResponseSDKType } from "@chalabi/manifestjs/dist/codegen/cosmos/group/v1/query"; -import { TruncatedAddressWithCopy } from "@/components/react/addressCopy"; -import VotingPopup from "./voteModal"; -import { ApexOptions } from "apexcharts"; -import { shiftDigits } from "@/utils"; -import { useChain } from "@cosmos-kit/react"; -import { chainName } from "@/config"; -import { useTx } from "@/hooks/useTx"; -import { cosmos } from "@chalabi/manifestjs"; -import { useTheme } from "@/contexts/theme"; -import CountdownTimer from "../components/CountdownTimer"; -import { useFeeEstimation } from "@/hooks"; -import ScrollableFade from "@/components/react/scrollableFade"; - -const Chart = dynamic(() => import("react-apexcharts"), { +} from '@chalabi/manifestjs/dist/codegen/cosmos/group/v1/types'; +import { QueryTallyResultResponseSDKType } from '@chalabi/manifestjs/dist/codegen/cosmos/group/v1/query'; +import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; +import VotingPopup from './voteModal'; +import { ApexOptions } from 'apexcharts'; +import { shiftDigits } from '@/utils'; +import { useChain } from '@cosmos-kit/react'; +import { chainName } from '@/config'; +import { useTx } from '@/hooks/useTx'; +import { cosmos } from '@chalabi/manifestjs'; +import { useTheme } from '@/contexts/theme'; +import CountdownTimer from '../components/CountdownTimer'; +import { useFeeEstimation } from '@/hooks'; +import ScrollableFade from '@/components/react/scrollableFade'; + +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false, -}); +}) as any; interface VoteMap { [key: string]: VoteOption; @@ -60,66 +60,66 @@ function VoteDetailsModal({ acc[voterKey] = vote?.option; return acc; }, {}), - [votes], + [votes] ); const { address } = useChain(chainName); const { theme } = useTheme(); - const textColor = theme === "dark" ? "#E0D1D4" : "#2e2e2e"; + const textColor = theme === 'dark' ? '#E0D1D4' : '#2e2e2e'; const normalizedMembers = useMemo( () => - members?.map((member) => ({ + members?.map(member => ({ ...member, })), - [members], + [members] ); const executorResultMapping: { [key: string]: string } = { - PROPOSAL_EXECUTOR_RESULT_NOT_RUN: "execute", - PROPOSAL_EXECUTOR_RESULT_SUCCESS: "success", - PROPOSAL_EXECUTOR_RESULT_FAILURE: "failed", + PROPOSAL_EXECUTOR_RESULT_NOT_RUN: 'execute', + PROPOSAL_EXECUTOR_RESULT_SUCCESS: 'success', + PROPOSAL_EXECUTOR_RESULT_FAILURE: 'failed', }; const votingStatusResultMapping: { [key: string]: string } = { - PROPOSAL_STATUS_CLOSED: "closed", - PROPOSAL_STATUS_SUBMITTED: "voting", - PROPOSAL_STATUS_ABORTED: "aborted", - PROPOSAL_STATUS_ACCEPTED: "accepted", - PROPOSAL_STATUS_REJECTED: "rejected", + PROPOSAL_STATUS_CLOSED: 'closed', + PROPOSAL_STATUS_SUBMITTED: 'voting', + PROPOSAL_STATUS_ABORTED: 'aborted', + PROPOSAL_STATUS_ACCEPTED: 'accepted', + PROPOSAL_STATUS_REJECTED: 'rejected', }; const voteMapping: { [key: string]: string } = { - VOTE_OPTION_YES: "yes", - VOTE_OPTION_NO: "no", - VOTE_OPTION_NO_WITH_VETO: "veto", - VOTE_OPTION_ABSTAIN: "abstain", + VOTE_OPTION_YES: 'yes', + VOTE_OPTION_NO: 'no', + VOTE_OPTION_NO_WITH_VETO: 'veto', + VOTE_OPTION_ABSTAIN: 'abstain', }; const getStatusLabel = (proposal: any) => { - if (proposal.executor_result === "PROPOSAL_EXECUTOR_RESULT_NOT_RUN") { - return votingStatusResultMapping[proposal.status] || "unknown status"; + if (proposal.executor_result === 'PROPOSAL_EXECUTOR_RESULT_NOT_RUN') { + return votingStatusResultMapping[proposal.status] || 'unknown status'; } - return executorResultMapping[proposal.executor_result] || "unknown status"; + return executorResultMapping[proposal.executor_result] || 'unknown status'; }; const [chartData, setChartData] = useState([0, 0, 0, 0]); useEffect(() => { - const yesCount = parseInt(tallies?.tally?.yes_count ?? "0"); - const noCount = parseInt(tallies?.tally?.no_count ?? "0"); - const vetoCount = parseInt(tallies?.tally?.no_with_veto_count ?? "0"); - const abstainCount = parseInt(tallies?.tally?.abstain_count ?? "0"); + const yesCount = parseInt(tallies?.tally?.yes_count ?? '0'); + const noCount = parseInt(tallies?.tally?.no_count ?? '0'); + const vetoCount = parseInt(tallies?.tally?.no_with_veto_count ?? '0'); + const abstainCount = parseInt(tallies?.tally?.abstain_count ?? '0'); setChartData([yesCount, noCount, vetoCount, abstainCount]); }, [tallies, votes]); const options: ApexOptions = { chart: { - type: "bar", + type: 'bar', height: 350, toolbar: { tools: { @@ -132,27 +132,25 @@ function VoteDetailsModal({ useSeriesColors: true, }, markers: { - width: 12, - height: 12, radius: 12, }, }, states: { normal: { - filter: { type: "none", value: 0 }, + filter: { type: 'none', value: 0 }, }, hover: { - filter: { type: "lighten", value: 0.2 }, + filter: { type: 'lighten', value: 0.2 }, }, active: { - filter: { type: "darken", value: 0.2 }, + filter: { type: 'darken', value: 0.2 }, allowMultipleDataPointsSelection: false, }, }, plotOptions: { bar: { horizontal: false, - columnWidth: "55%", + columnWidth: '55%', distributed: true, }, }, @@ -163,7 +161,7 @@ function VoteDetailsModal({ show: false, }, xaxis: { - categories: ["Yes", "No", "Veto", "Abstain"], + categories: ['Yes', 'No', 'Veto', 'Abstain'], labels: { style: { colors: textColor, @@ -188,7 +186,7 @@ function VoteDetailsModal({ data: chartData, }, ], - colors: ["#78f59599", "#fe6565b0", "#fcd4779a", "#a885f8a1"], + colors: ['#78f59599', '#fe6565b0', '#fcd4779a', '#a885f8a1'], tooltip: { enabled: false, }, @@ -201,17 +199,17 @@ function VoteDetailsModal({ const msgExec = exec({ proposalId: proposal?.id, - executor: address ?? "", + executor: address ?? '', }); const msgWithdraw = withdrawProposal({ proposalId: proposal?.id, - address: address ?? "", + address: address ?? '', }); const executeProposal = async () => { try { - const fee = await estimateFee(address ?? "", [msgExec]); + const fee = await estimateFee(address ?? '', [msgExec]); await tx([msgExec], { fee, onSuccess: () => { @@ -222,13 +220,13 @@ function VoteDetailsModal({ }, }); } catch (error) { - console.error("Failed to execute proposal: ", error); + console.error('Failed to execute proposal: ', error); } }; const executeWithdrawl = async () => { try { - const fee = await estimateFee(address ?? "", [msgWithdraw]); + const fee = await estimateFee(address ?? '', [msgWithdraw]); await tx([msgWithdraw], { fee, onSuccess: () => { @@ -238,24 +236,24 @@ function VoteDetailsModal({ }, }); } catch (error) { - console.error("Failed to execute proposal: ", error); + console.error('Failed to execute proposal: ', error); } }; const optionToVote = (option: string) => { switch (option) { - case "VOTE_OPTION_YES": - return "Yes"; - case "VOTE_OPTION_NO": - return "No"; - case "VOTE_OPTION_NO_WITH_VETO": - return "Veto"; - case "VOTE_OPTION_ABSTAIN": - return "Abstain"; + case 'VOTE_OPTION_YES': + return 'Yes'; + case 'VOTE_OPTION_NO': + return 'No'; + case 'VOTE_OPTION_NO_WITH_VETO': + return 'Veto'; + case 'VOTE_OPTION_ABSTAIN': + return 'Abstain'; case undefined: - return "N/A"; + return 'N/A'; default: - return "Unknown"; + return 'Unknown'; } }; @@ -275,9 +273,7 @@ function VoteDetailsModal({ if (timeDiff > 0) { const days = Math.floor(timeDiff / (1000 * 60 * 60 * 24)); - const hours = Math.floor( - (timeDiff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60), - ); + const hours = Math.floor((timeDiff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const minutes = Math.floor((timeDiff % (1000 * 60 * 60)) / (1000 * 60)); const seconds = Math.floor((timeDiff % (1000 * 60)) / 1000); return { days, hours, minutes, seconds }; @@ -300,7 +296,7 @@ function VoteDetailsModal({ const [isGridVisible, setIsGridVisible] = useState(false); const handleButtonClick = () => { - setIsGridVisible((prev) => !prev); + setIsGridVisible(prev => !prev); }; const proposalClosed = @@ -310,64 +306,45 @@ function VoteDetailsModal({ countdownValues.seconds === 0; - const userHasVoted = votes.some( - (vote) => vote.voter.toLowerCase().trim() === address, - ); + const userHasVoted = votes.some(vote => vote.voter.toLowerCase().trim() === address); const userVoteOption = userHasVoted - ? votes.find((vote) => vote.voter.toLowerCase().trim() === address)?.option + ? votes.find(vote => vote.voter.toLowerCase().trim() === address)?.option : null; const userVotedStatus = useMemo(() => userHasVoted, [votes]); const importantFields: { [key: string]: string[] } = { - "/cosmos.bank.v1beta1.MsgSend": ["from_address", "to_address", "amount"], - "/cosmos.group.v1.MsgCreateGroup": ["admin", "members", "metadata"], - "/cosmos.group.v1.MsgUpdateGroupMembers": [ - "admin", - "group_id", - "member_updates", - ], - "/cosmos.group.v1.MsgUpdateGroupAdmin": ["group_id", "admin", "new_admin"], - "/cosmos.group.v1.MsgUpdateGroupMetadata": [ - "admin", - "group_id", - "metadata", - ], - "/cosmos.group.v1.MsgCreateGroupPolicy": [ - "admin", - "group_id", - "metadata", - "decision_policy", + '/cosmos.bank.v1beta1.MsgSend': ['from_address', 'to_address', 'amount'], + '/cosmos.group.v1.MsgCreateGroup': ['admin', 'members', 'metadata'], + '/cosmos.group.v1.MsgUpdateGroupMembers': ['admin', 'group_id', 'member_updates'], + '/cosmos.group.v1.MsgUpdateGroupAdmin': ['group_id', 'admin', 'new_admin'], + '/cosmos.group.v1.MsgUpdateGroupMetadata': ['admin', 'group_id', 'metadata'], + '/cosmos.group.v1.MsgCreateGroupPolicy': ['admin', 'group_id', 'metadata', 'decision_policy'], + '/cosmos.group.v1.MsgCreateGroupWithPolicy': [ + 'admin', + 'members', + 'group_metadata', + 'group_policy_metadata', + 'decision_policy', ], - "/cosmos.group.v1.MsgCreateGroupWithPolicy": [ - "admin", - "members", - "group_metadata", - "group_policy_metadata", - "decision_policy", + '/cosmos.group.v1.MsgSubmitProposal': [ + 'group_policy_address', + 'proposers', + 'metadata', + 'messages', ], - "/cosmos.group.v1.MsgSubmitProposal": [ - "group_policy_address", - "proposers", - "metadata", - "messages", - ], - "/cosmos.group.v1.MsgVote": ["proposal_id", "voter", "option", "metadata"], - "/cosmos.group.v1.MsgExec": ["proposal_id", "executor"], - "/cosmos.group.v1.MsgLeaveGroup": ["address", "group_id"], + '/cosmos.group.v1.MsgVote': ['proposal_id', 'voter', 'option', 'metadata'], + '/cosmos.group.v1.MsgExec': ['proposal_id', 'executor'], + '/cosmos.group.v1.MsgLeaveGroup': ['address', 'group_id'], // Add more message types and their important fields here }; // Default fields to show if the message type is not in the mapping - const defaultFields = ["@type"]; - - const renderMessageField = ( - key: string, - value: any, - depth: number = 0, - ): JSX.Element => { - if (typeof value === "object" && value !== null) { + const defaultFields = ['@type']; + + const renderMessageField = (key: string, value: any, depth: number = 0): JSX.Element => { + if (typeof value === 'object' && value !== null) { if (Array.isArray(value)) { return (
    @@ -384,7 +361,7 @@ function VoteDetailsModal({

    {key}:

    {Object.entries(value).map(([subKey, subValue]) => - renderMessageField(subKey, subValue, depth + 1), + renderMessageField(subKey, subValue, depth + 1) )}
    ); @@ -393,7 +370,7 @@ function VoteDetailsModal({ return (

    {key}:

    - {typeof value === "string" && value.match(/^[a-zA-Z0-9]{40,}$/) ? ( + {typeof value === 'string' && value.match(/^[a-zA-Z0-9]{40,}$/) ? ( ) : (

    {String(value)}

    @@ -407,9 +384,7 @@ function VoteDetailsModal({
    - +
    @@ -425,21 +400,18 @@ function VoteDetailsModal({ your vote - {userVoteOption !== null - ? voteMapping[userVoteOption ?? ""] - : null} + {userVoteOption !== null ? voteMapping[userVoteOption ?? ''] : null}
    )} @@ -462,23 +434,15 @@ function VoteDetailsModal({

    MESSAGES

    {proposal.messages.map((message: any, index: number) => { - const messageType = message["@type"]; - const fieldsToShow = - importantFields[messageType] || defaultFields; + const messageType = message['@type']; + const fieldsToShow = importantFields[messageType] || defaultFields; return ( -
    +

    - {messageType.split(".").pop().replace("Msg", "")} + {messageType.split('.').pop().replace('Msg', '')}

    -
    - {fieldsToShow.map((field) => - renderMessageField(field, message[field]), - )} -
    +
    {fieldsToShow.map(field => renderMessageField(field, message[field]))}
    ); })} @@ -493,12 +457,7 @@ function VoteDetailsModal({

    TALLY

    - +

    MEMBERS

    @@ -523,15 +482,10 @@ function VoteDetailsModal({ return ( - + {member.member.weight} - - {optionToVote(memberVote?.toString()) || "N/A"} - + {optionToVote(memberVote?.toString()) || 'N/A'} ); })} @@ -543,29 +497,20 @@ function VoteDetailsModal({
    - {proposal.status === - ("PROPOSAL_STATUS_ACCEPTED" as unknown as ProposalStatus) && + {proposal.status === ('PROPOSAL_STATUS_ACCEPTED' as unknown as ProposalStatus) && proposal.executor_result === - ("PROPOSAL_EXECUTOR_RESULT_NOT_RUN" as unknown as ProposalExecutorResult) ? ( - ) : proposal.executor_result === - ("PROPOSAL_EXECUTOR_RESULT_NOT_RUN" as unknown as ProposalExecutorResult) && + ('PROPOSAL_EXECUTOR_RESULT_NOT_RUN' as unknown as ProposalExecutorResult) && proposalClosed && - proposal.status !== - ("PROPOSAL_STATUS_REJECTED" as unknown as ProposalStatus) ? ( - - ) : proposal.status !== - ("PROPOSAL_STATUS_CLOSED" as unknown as ProposalStatus) && + ) : proposal.status !== ('PROPOSAL_STATUS_CLOSED' as unknown as ProposalStatus) && !proposalClosed && userHasVoted === false ? ( <> @@ -576,7 +521,7 @@ function VoteDetailsModal({ > Vote - {proposal.proposers.includes(address ?? "") && ( + {proposal.proposers.includes(address ?? '') && (
    - +
    @@ -272,17 +255,14 @@ const EndpointSelector: React.FC = () => { placeholder="Enter API URL" className="input input-bordered" value={newAPIEndpoint} - onChange={(e) => setNewAPIEndpoint(e.target.value)} + onChange={e => setNewAPIEndpoint(e.target.value)} />
    -
    @@ -293,7 +273,6 @@ const EndpointSelector: React.FC = () => { ); }; -export const DynamicEndpointSelector = dynamic( - () => Promise.resolve(EndpointSelector), - { ssr: false }, -); +export const DynamicEndpointSelector = dynamic(() => Promise.resolve(EndpointSelector), { + ssr: false, +}); diff --git a/components/react/header.tsx b/components/react/header.tsx index 74ed4d9a..126d5781 100644 --- a/components/react/header.tsx +++ b/components/react/header.tsx @@ -1,4 +1,4 @@ -import { WalletSection } from "../wallet"; +import { WalletSection } from '../wallet'; export default function Header() { return ( diff --git a/components/react/index.ts b/components/react/index.ts index 2da1e8f1..e598da74 100644 --- a/components/react/index.ts +++ b/components/react/index.ts @@ -1,5 +1,5 @@ -export * from "./chain-card"; -export * from "./modal"; -export * from "./views"; -export * from "./settingsModal"; -export * from "./inputs"; \ No newline at end of file +export * from './chain-card'; +export * from './modal'; +export * from './views'; +export * from './settingsModal'; +export * from './inputs'; diff --git a/components/react/inputs/BaseInput.tsx b/components/react/inputs/BaseInput.tsx index 92efaa8c..3e61e19c 100644 --- a/components/react/inputs/BaseInput.tsx +++ b/components/react/inputs/BaseInput.tsx @@ -1,5 +1,5 @@ -import React from "react"; -import { useField } from "formik"; +import React from 'react'; +import { useField } from 'formik'; interface BaseInputProps { label: string; @@ -7,9 +7,10 @@ interface BaseInputProps { className?: string; } -export const BaseInput: React.FC< - BaseInputProps & React.InputHTMLAttributes -> = ({ label, ...props }) => { +export const BaseInput: React.FC> = ({ + label, + ...props +}) => { const [field, meta] = useField(props); const id = props.id || props.name; return ( diff --git a/components/react/inputs/NumberInput.tsx b/components/react/inputs/NumberInput.tsx index d20ee52a..2500962c 100644 --- a/components/react/inputs/NumberInput.tsx +++ b/components/react/inputs/NumberInput.tsx @@ -1,6 +1,6 @@ -import React from "react"; -import { BaseInput } from "./BaseInput"; +import React from 'react'; +import { BaseInput } from './BaseInput'; -export const NumberInput: React.FC> = ( - props, -) => ; +export const NumberInput: React.FC> = props => ( + +); diff --git a/components/react/inputs/TextArea.tsx b/components/react/inputs/TextArea.tsx index 5c1a3212..12c74d60 100644 --- a/components/react/inputs/TextArea.tsx +++ b/components/react/inputs/TextArea.tsx @@ -1,5 +1,5 @@ -import React from "react"; -import { useField } from "formik"; +import React from 'react'; +import { useField } from 'formik'; interface TextAreaProps { label: string; diff --git a/components/react/inputs/TextInput.tsx b/components/react/inputs/TextInput.tsx index e5b3c490..94f54e88 100644 --- a/components/react/inputs/TextInput.tsx +++ b/components/react/inputs/TextInput.tsx @@ -1,6 +1,6 @@ -import React from "react"; -import { BaseInput } from "./BaseInput"; +import React from 'react'; +import { BaseInput } from './BaseInput'; -export const TextInput: React.FC> = ( - props, -) => ; +export const TextInput: React.FC> = props => ( + +); diff --git a/components/react/inputs/__tests__/NumberInput.test.tsx b/components/react/inputs/__tests__/NumberInput.test.tsx index 5ffbaa18..cb831dca 100644 --- a/components/react/inputs/__tests__/NumberInput.test.tsx +++ b/components/react/inputs/__tests__/NumberInput.test.tsx @@ -1,59 +1,59 @@ -import { test, expect, afterEach, describe } from "bun:test"; -import React from "react"; -import matchers from "@testing-library/jest-dom/matchers"; -import { render, screen, cleanup, fireEvent } from "@testing-library/react"; -import { NumberInput } from "@/components/react/inputs"; -import { Formik, Form } from "formik"; +import { test, expect, afterEach, describe } from 'bun:test'; +import React from 'react'; +import matchers from '@testing-library/jest-dom/matchers'; +import { render, screen, cleanup, fireEvent } from '@testing-library/react'; +import { NumberInput } from '@/components/react/inputs'; +import { Formik, Form } from 'formik'; expect.extend(matchers); const TestForm = ({ children }: { children: React.ReactNode }) => ( - {}}> + {}}>
    {children}
    ); -describe("NumberInput", () => { +describe('NumberInput', () => { afterEach(() => { cleanup(); }); - test("renders correctly", () => { + test('renders correctly', () => { render( - , + ); - const input = screen.getByLabelText("Test Number"); + const input = screen.getByLabelText('Test Number'); expect(input).toBeInTheDocument(); - expect(input.tagName.toLowerCase()).toBe("input"); - expect(input).toHaveAttribute("type", "number"); + expect(input.tagName.toLowerCase()).toBe('input'); + expect(input).toHaveAttribute('type', 'number'); }); - test("updates value on change", () => { + test('updates value on change', () => { render( - , + ); - const input = screen.getByLabelText("Test Number"); - fireEvent.change(input, { target: { value: "42" } }); + const input = screen.getByLabelText('Test Number'); + fireEvent.change(input, { target: { value: '42' } }); expect(input).toHaveValue(42); }); - test("displays error message", () => { + test('displays error message', () => { render( {}} >
    -
    , +
    ); - expect(screen.getByText("Must be a number")).toBeInTheDocument(); + expect(screen.getByText('Must be a number')).toBeInTheDocument(); }); }); diff --git a/components/react/inputs/__tests__/TextArea.test.tsx b/components/react/inputs/__tests__/TextArea.test.tsx index f22ebd0e..f7dba8af 100644 --- a/components/react/inputs/__tests__/TextArea.test.tsx +++ b/components/react/inputs/__tests__/TextArea.test.tsx @@ -1,58 +1,58 @@ -import { test, expect, afterEach, describe } from "bun:test"; -import React from "react"; -import matchers from "@testing-library/jest-dom/matchers"; -import { render, screen, cleanup, fireEvent } from "@testing-library/react"; -import { TextArea } from "@/components/react/inputs"; -import { Formik, Form } from "formik"; +import { test, expect, afterEach, describe } from 'bun:test'; +import React from 'react'; +import matchers from '@testing-library/jest-dom/matchers'; +import { render, screen, cleanup, fireEvent } from '@testing-library/react'; +import { TextArea } from '@/components/react/inputs'; +import { Formik, Form } from 'formik'; expect.extend(matchers); const TestForm = ({ children }: { children: React.ReactNode }) => ( - {}}> + {}}>
    {children}
    ); -describe("TextArea", () => { +describe('TextArea', () => { afterEach(() => { cleanup(); }); - test("renders correctly", () => { + test('renders correctly', () => { render(