diff --git a/src/components/MainWalletPage.test.tsx b/src/components/MainWalletPage.test.tsx index e4661cee..5d456415 100644 --- a/src/components/MainWalletPage.test.tsx +++ b/src/components/MainWalletPage.test.tsx @@ -53,7 +53,7 @@ vi.mock('./wallet/WalletJarsDetailsOverlay', () => ({ ), })) -vi.mock('./ui/jam/Balance', () => ({ +vi.mock('@/components/ui/jam/Balance', () => ({ Balance: ({ valueString, onClick }: { valueString: string; onClick?: () => void }) => ( {valueString} ), @@ -74,6 +74,7 @@ const makeJar = (jarIndex: number, name: string): Jar => calculatedTotalBalanceInSats: 100, calculatedAvailableBalanceInSats: 100, calculatedFrozenOrLockedBalanceInSats: 0, + calculatedAvailableFrozenBalanceInSats: 0, }, }) as unknown as Jar diff --git a/src/components/earn/CreateFidelityBondDialog/CreateFidelityBondDialogSteps.test.tsx b/src/components/earn/CreateFidelityBondDialog/CreateFidelityBondDialogSteps.test.tsx index 4b5b62a0..fd0afdd3 100644 --- a/src/components/earn/CreateFidelityBondDialog/CreateFidelityBondDialogSteps.test.tsx +++ b/src/components/earn/CreateFidelityBondDialog/CreateFidelityBondDialogSteps.test.tsx @@ -53,16 +53,16 @@ vi.mock('@/context/JamWalletInfoContext', () => ({ vi.mock('@/components/ui/jam/SelectableJar', () => ({ SelectableJar: ({ name, - onClick, + onSelect, disabled, isSelected, }: { name?: string - onClick?: () => void + onSelect?: () => void disabled?: boolean isSelected?: boolean }) => ( - ), diff --git a/src/components/earn/FidelityBondCard.test.tsx b/src/components/earn/FidelityBondCard.test.tsx index 60b17a50..b7cffbfc 100644 --- a/src/components/earn/FidelityBondCard.test.tsx +++ b/src/components/earn/FidelityBondCard.test.tsx @@ -34,7 +34,7 @@ vi.mock('../ui/jam/Address', () => ({ Address: ({ value }: { value?: string }) => {value}, })) -vi.mock('../ui/jam/Balance', () => ({ +vi.mock('@/components/ui/jam/Balance', () => ({ Balance: ({ valueString }: { valueString?: string }) => {valueString}, })) diff --git a/src/components/earn/FidelityBondCard.tsx b/src/components/earn/FidelityBondCard.tsx index 7cb649bf..7b6e10f0 100644 --- a/src/components/earn/FidelityBondCard.tsx +++ b/src/components/earn/FidelityBondCard.tsx @@ -2,12 +2,12 @@ import { useMemo, type PropsWithChildren } from 'react' import { ClockIcon, CoinsIcon, CopyIcon } from 'lucide-react' import { Trans, useTranslation } from 'react-i18next' import { Card, CardContent, CardAction, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' +import { Balance } from '@/components/ui/jam/Balance' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import type { FidelityBondUtxo } from '@/hooks/useQueryUtxos' import * as fb from '@/lib/fidelityBondUtils' import { time } from '@/lib/utils' import { Address } from '../ui/jam/Address' -import { Balance } from '../ui/jam/Balance' interface FidelityBondCardProps { value: FidelityBondUtxo diff --git a/src/components/earn/MoveToJarDialog.test.tsx b/src/components/earn/MoveToJarDialog.test.tsx index 9215d192..98210f2a 100644 --- a/src/components/earn/MoveToJarDialog.test.tsx +++ b/src/components/earn/MoveToJarDialog.test.tsx @@ -72,16 +72,16 @@ vi.mock('@/context/JamWalletInfoContext', () => ({ vi.mock('@/components/ui/jam/SelectableJar', () => ({ SelectableJar: ({ name, - onClick, + onSelect, disabled, isSelected, }: { name?: string - onClick?: () => void + onSelect?: () => void disabled?: boolean isSelected?: boolean }) => ( - ), diff --git a/src/components/earn/OfferCard.test.tsx b/src/components/earn/OfferCard.test.tsx index 62821467..2f8e48f2 100644 --- a/src/components/earn/OfferCard.test.tsx +++ b/src/components/earn/OfferCard.test.tsx @@ -18,7 +18,7 @@ vi.mock('@/lib/utils', () => ({ isRelativeOffer: (type: string) => type === 'sw0relo', })) -vi.mock('../ui/jam/Balance', () => ({ +vi.mock('@/components/ui/jam/Balance', () => ({ Balance: ({ valueString }: { valueString?: string }) => {valueString}, })) diff --git a/src/components/earn/fidelity-bond/FidelityBondJarSelector.tsx b/src/components/earn/fidelity-bond/FidelityBondJarSelector.tsx index 68f6a4e4..db4db5f7 100644 --- a/src/components/earn/fidelity-bond/FidelityBondJarSelector.tsx +++ b/src/components/earn/fidelity-bond/FidelityBondJarSelector.tsx @@ -24,7 +24,7 @@ export function FidelityBondJarSelector({ selectedJarIndex, onSelect, isJarDisab frozenOrLockedBalance={jar.balanceSummary.calculatedFrozenOrLockedBalanceInSats} totalWalletBalance={walletBalanceSummary.calculatedTotalBalanceInSats} isSelected={selectedJarIndex === jar.jarIndex} - onClick={() => onSelect(jar.jarIndex)} + onSelect={() => onSelect(jar.jarIndex)} disabled={isJarDisabled(jar)} /> ))} diff --git a/src/components/layout/AppNavbar.tsx b/src/components/layout/AppNavbar.tsx index c4a883ff..4255c19a 100644 --- a/src/components/layout/AppNavbar.tsx +++ b/src/components/layout/AppNavbar.tsx @@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next' import { Link, useNavigate, type NavigateFunction } from 'react-router-dom' import { DevBadge } from '@/components/dev/DevBadge' import { Button } from '@/components/ui/button' +import { Balance } from '@/components/ui/jam/Balance' import { ThemeToggleButton } from '@/components/ui/jam/ThemeToggleButton' import { Skeleton } from '@/components/ui/skeleton' import type { SidebarContextProps } from '@/components/ui/use-sidebar' @@ -15,7 +16,6 @@ import type { RescanInfo } from '@/context/JamSessionInfoContext' import { cn, shortenStringMiddle } from '@/lib/utils' import type { AmountSats } from '@/types/global' import { WithActivityIndicator } from '../ui/jam/ActivityIndicator' -import { Balance } from '../ui/jam/Balance' import { Spinner } from '../ui/spinner' import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip' diff --git a/src/components/orderbook/OrderbookContent.tsx b/src/components/orderbook/OrderbookContent.tsx index e6c9893b..ae616ade 100644 --- a/src/components/orderbook/OrderbookContent.tsx +++ b/src/components/orderbook/OrderbookContent.tsx @@ -18,6 +18,7 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { Input } from '@/components/ui/input' +import { Balance } from '@/components/ui/jam/Balance' import { Label } from '@/components/ui/label' import { Switch } from '@/components/ui/switch' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' @@ -36,7 +37,6 @@ import { } from '@/lib/utils' import { useDeveloperMode } from '@/store/jamSettingsStore' import { jmSessionStore } from '@/store/jmSessionStore' -import { Balance } from '../ui/jam/Balance' import { Spinner } from '../ui/spinner' import { OrderbookChart } from './OrderbookChart' import { OrderbookTable, type OrderTableEntry } from './OrderbookTable' diff --git a/src/components/receive/ReceiveForm.test.tsx b/src/components/receive/ReceiveForm.test.tsx index 5eba191d..9e04c219 100644 --- a/src/components/receive/ReceiveForm.test.tsx +++ b/src/components/receive/ReceiveForm.test.tsx @@ -19,8 +19,8 @@ vi.mock('@/context/JamWalletInfoContext', () => ({ })) vi.mock('@/components/ui/jam/SelectableJar', () => ({ - SelectableJar: ({ name, isSelected, onClick }: { name: string; isSelected: boolean; onClick: () => void }) => ( - ), diff --git a/src/components/receive/ReceiveForm.tsx b/src/components/receive/ReceiveForm.tsx index f3be268f..2ed14ade 100644 --- a/src/components/receive/ReceiveForm.tsx +++ b/src/components/receive/ReceiveForm.tsx @@ -91,7 +91,7 @@ export const ReceiveForm = ({ className, defaultValues, onSubmit, jars, disabled > {t('receive.label_source_jar')} -
+
{jars.map((jar, index) => ( { + onSelect={() => { setValue('source.fromJar', jar.jarIndex, { shouldValidate: true }) }} disabled={disabled || isSubmitting} diff --git a/src/components/send/JarSelectorDialog.test.tsx b/src/components/send/JarSelectorDialog.test.tsx index ca199a44..6d86673a 100644 --- a/src/components/send/JarSelectorDialog.test.tsx +++ b/src/components/send/JarSelectorDialog.test.tsx @@ -11,16 +11,16 @@ vi.mock('react-i18next', () => ({ vi.mock('../ui/jam/SelectableJar', () => ({ SelectableJar: ({ name, - onClick, + onSelect, disabled, isSelected, }: { name?: string - onClick?: () => void + onSelect?: () => void disabled?: boolean isSelected?: boolean }) => ( - ), diff --git a/src/components/send/JarSelectorDialog.tsx b/src/components/send/JarSelectorDialog.tsx index 27b15d64..fd8e8206 100644 --- a/src/components/send/JarSelectorDialog.tsx +++ b/src/components/send/JarSelectorDialog.tsx @@ -90,7 +90,7 @@ export default function JarSelectorDialog({ frozenOrLockedBalance={jar.balanceSummary.calculatedFrozenOrLockedBalanceInSats} totalWalletBalance={walletBalanceSummary.calculatedTotalBalanceInSats} isSelected={selectedJarIndex === jar.jarIndex} - onClick={() => + onSelect={() => setValue('jarIndex', jar.jarIndex, { shouldDirty: true, shouldTouch: true, diff --git a/src/components/send/PaymentConfirmDialog.test.tsx b/src/components/send/PaymentConfirmDialog.test.tsx index f3b2962f..cf6c04ce 100644 --- a/src/components/send/PaymentConfirmDialog.test.tsx +++ b/src/components/send/PaymentConfirmDialog.test.tsx @@ -27,7 +27,7 @@ vi.mock('../ui/jam/Address', () => ({ Address: ({ value }: { value: string }) => address:{value}, })) -vi.mock('../ui/jam/Balance', () => ({ +vi.mock('@/components/ui/jam/Balance', () => ({ Balance: ({ valueString }: { valueString: string }) => balance:{valueString}, })) @@ -50,6 +50,7 @@ const sourceJar: Jar = { calculatedAvailableBalanceInSats: 50_000, calculatedTotalBalanceInSats: 50_000, calculatedConfirmedAvailableBalanceInSats: 50_000, + calculatedAvailableFrozenBalanceInSats: 0, calculatedFrozenOrLockedBalanceInSats: 0, }, color: '#e2b86a', diff --git a/src/components/send/PaymentConfirmDialog.tsx b/src/components/send/PaymentConfirmDialog.tsx index 5e455a8d..462beaba 100644 --- a/src/components/send/PaymentConfirmDialog.tsx +++ b/src/components/send/PaymentConfirmDialog.tsx @@ -3,6 +3,7 @@ import { InfoIcon } from 'lucide-react' import { Trans, useTranslation } from 'react-i18next' import { Badge } from '@/components/ui/badge' import { jarBadgeVariant } from '@/components/ui/badge-variants' +import { Balance } from '@/components/ui/jam/Balance' import type { Jar } from '@/context/JamWalletInfoContext' import type { Utxo } from '@/hooks/useQueryUtxos' import type { JamFeeConfigValues } from '@/lib/feeConfig' @@ -13,7 +14,6 @@ import { Button } from '../ui/button' import { Card, CardContent, CardHeader } from '../ui/card' import { Dialog, DialogTitle, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '../ui/dialog' import { Address } from '../ui/jam/Address' -import { Balance } from '../ui/jam/Balance' import { Spinner } from '../ui/spinner' import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip' import { estimateMaxCollaboratorFee, useMiningFeeText, type EstimateMaxCollaboratorFeeResult } from './feeEstimate' diff --git a/src/components/send/SendForm.schema.ts b/src/components/send/SendForm.schema.ts index 9b353799..73589821 100644 --- a/src/components/send/SendForm.schema.ts +++ b/src/components/send/SendForm.schema.ts @@ -64,11 +64,28 @@ export const createSendFormSchema = ( .object({ source: yup .object({ - fromJar: sourceJarField( - t('send.feedback_invalid_source_jar'), - (jarIndex) => - (jars.find((it) => it.jarIndex === jarIndex)?.balanceSummary.calculatedAvailableBalanceInSats || 0) > 0, - ), + fromJar: sourceJarField(t('send.feedback_invalid_source_jar'), (jarIndex) => + jars.some((it) => it.jarIndex === jarIndex), + ) + .test( + 'valid-source-jar-must-unfreeze-utxos-test', + t('send.feedback_invalid_source_jar_must_unfreeze_utxos'), + (jarIndex) => { + const jar = jars.find((it) => it.jarIndex === jarIndex) + if (!jar) return true + if (jar.balanceSummary.calculatedAvailableBalanceInSats > 0) return true + return jar.balanceSummary.calculatedAvailableFrozenBalanceInSats <= 0 + }, + ) + .test( + 'valid-source-jar-has-funds-test', + t('send.feedback_invalid_source_jar_no_available_utxos'), + (jarIndex) => { + const jar = jars.find((it) => it.jarIndex === jarIndex) + if (!jar) return true + return jar.balanceSummary.calculatedAvailableBalanceInSats > 0 + }, + ), }) .required(), destination: yup diff --git a/src/components/send/SendForm.test.tsx b/src/components/send/SendForm.test.tsx index 78597dd9..10f4a0a3 100644 --- a/src/components/send/SendForm.test.tsx +++ b/src/components/send/SendForm.test.tsx @@ -55,13 +55,13 @@ vi.mock('../ui/jam/Address', () => ({ Address: () =>
, })) -vi.mock('../ui/jam/Balance', () => ({ +vi.mock('@/components/ui/jam/Balance', () => ({ Balance: ({ valueString }: { valueString?: string }) =>
{valueString}
, })) vi.mock('../ui/jam/SelectableJar', () => ({ - SelectableJar: ({ name, onClick, disabled }: { name?: string; onClick?: () => void; disabled?: boolean }) => ( - ), diff --git a/src/components/send/SendForm.tsx b/src/components/send/SendForm.tsx index 78dce756..bad1016f 100644 --- a/src/components/send/SendForm.tsx +++ b/src/components/send/SendForm.tsx @@ -5,11 +5,11 @@ import { getAddressInfo, Network } from 'bitcoin-address-validation' import type { AddressInfo } from 'bitcoin-address-validation' import { AlertTriangleIcon, BrushCleaningIcon, MilkIcon, ScanQrCodeIcon, XIcon } from 'lucide-react' import { FormProvider, useForm, useWatch } from 'react-hook-form' -import type { Resolver, SubmitHandler } from 'react-hook-form' +import type { FieldErrors, Resolver, SubmitHandler } from 'react-hook-form' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import QrScannerDialog from '@/components/ui/QrScannerDialog' -import { isDevMode } from '@/constants/debugFeatures' +import { Balance } from '@/components/ui/jam/Balance' import { MAX_NUM_COLLABORATORS } from '@/constants/jam' import { JM_MINIMUM_MAKERS_DEFAULT, JM_TAKER_UTXO_AGE } from '@/constants/jm' import { useDetectNetwork, type AddressSummary, type Jar } from '@/context/JamWalletInfoContext' @@ -31,7 +31,6 @@ import { Field, FieldDescription, FieldError, FieldLabel } from '../ui/field' import { Input } from '../ui/input' import { inputVariants } from '../ui/input-variants' import { Address } from '../ui/jam/Address' -import { Balance } from '../ui/jam/Balance' import { SatSymbol } from '../ui/jam/CurrencySymbol' import { SelectableJar } from '../ui/jam/SelectableJar' import { Slider } from '../ui/slider' @@ -94,12 +93,6 @@ const AddressFromJarSelectorDialog = ({ ) } -// set the default to one collaborator in dev mode -const DEV_INITIAL_NUM_COLLABORATORS_INPUT = 1 - -// TODO: this value should be dynamic via jm backend settings -const MIN_NUM_COLLABORATORS = isDevMode() ? DEV_INITIAL_NUM_COLLABORATORS_INPUT : JM_MINIMUM_MAKERS_DEFAULT - const FieldPrefixSatSymbol = ( onSourceJarChange?: (jarIndex: JarIndex | undefined) => void - sourceJarLabelButton?: React.ReactElement + onSourceJarClicked?: (jarIndex: JarIndex | undefined) => void + sourceJarLabelButton?: (errors?: FieldErrors<{ fromJar: number }>) => React.ReactElement minNumberOfCollaborators?: number feeConfigValues: JamFeeConfigValues walletFileName: WalletFileName @@ -129,11 +123,12 @@ export function SendForm({ className, onSubmit, onSourceJarChange, + onSourceJarClicked, sourceJarLabelButton, disabled, feeConfigValues, walletFileName, - minNumberOfCollaborators = MIN_NUM_COLLABORATORS, + minNumberOfCollaborators = JM_MINIMUM_MAKERS_DEFAULT, jars, walletBalanceSummary, addressSummary, @@ -204,6 +199,15 @@ export function SendForm({ onSourceJarChange?.(sourceJarIndex) }, [onSourceJarChange, sourceJarIndex]) + useEffect(() => { + if (formState.dirtyFields.source?.fromJar) { + console.debug( + 'Validating `source.fromJar` as wallet balance summary changed (e.g. frozen/unfrozen utxos, new transaction, etc.)', + ) + void trigger('source.fromJar') + } + }, [walletBalanceSummary, trigger, formState.dirtyFields.source?.fromJar]) + const destinationJar = useMemo(() => { if (destinationJarIndex === undefined) return return jars.find((it) => it.jarIndex === destinationJarIndex) @@ -300,41 +304,58 @@ export function SendForm({ {...sendFormMethods} >
void doOnSubmit(event)} className={cn('flex flex-col gap-4', className)} noValidate> -
- +
+
{t('send.label_source_jar')} - {sourceJarLabelButton && <>{sourceJarLabelButton}} + {sourceJarLabelButton?.(errors.source)}
-
- {jars.map((jar, index) => ( - { - setValue('source.fromJar', jar.jarIndex, { shouldValidate: true }) +
+ {jars.map((jar, index) => { + const noFunds = + jar.balanceSummary.calculatedAvailableBalanceInSats <= 0 && + jar.balanceSummary.calculatedAvailableFrozenBalanceInSats <= 0 - if (isSweep === true) { - setValue('amount.isSweep', false, { shouldValidate: true }) - setValue('amount.sweepAmount', undefined, { shouldValidate: true }) - setValue('amount.amount', undefined, { shouldValidate: true }) - } - if (destinationJarIndex === jar.jarIndex) { - setValue('destination.address', '', { shouldValidate: true }) - setValue('destination.fromJar', undefined, { shouldValidate: true }) - } else if (destinationAddress !== undefined) { - void trigger('destination.address') - } - }} - disabled={disabled || jar.balanceSummary.calculatedAvailableBalanceInSats <= 0} - /> - ))} + const isSelected = sourceJarIndex === jar.jarIndex + + return ( + { + if (isSelected) { + onSourceJarClicked?.(sourceJarIndex) + } + }} + onSelect={() => { + setValue('source.fromJar', jar.jarIndex, { + shouldValidate: true, + shouldDirty: true, + shouldTouch: true, + }) + + if (isSweep === true) { + setValue('amount.isSweep', false, { shouldValidate: true }) + setValue('amount.sweepAmount', undefined, { shouldValidate: true }) + setValue('amount.amount', undefined, { shouldValidate: true }) + } + if (destinationJarIndex === jar.jarIndex) { + setValue('destination.address', '', { shouldValidate: true }) + setValue('destination.fromJar', undefined, { shouldValidate: true }) + } else if (destinationAddress !== undefined) { + void trigger('destination.address') + } + }} + disabled={disabled || noFunds} + /> + ) + })}
{errors.source?.fromJar?.message ? {errors.source?.fromJar.message} : null} diff --git a/src/components/send/SendPage.test.tsx b/src/components/send/SendPage.test.tsx index 5aa3bd87..d7ec0b85 100644 --- a/src/components/send/SendPage.test.tsx +++ b/src/components/send/SendPage.test.tsx @@ -209,11 +209,11 @@ vi.mock('./SendForm', () => ({ disabled?: boolean onSourceJarChange?: (jarIndex: number | undefined) => void onSubmit: (values: SendFormValues) => void - sourceJarLabelButton?: React.ReactElement + sourceJarLabelButton?: () => React.ReactElement }) => (
send-form:{String(disabled)} -
{sourceJarLabelButton}
+
{sourceJarLabelButton?.()}
- } + onSourceJarClicked={onOpenUtxoSelector} + sourceJarLabelButton={(errors) => { + const errorSolvedByUtxoSelection = errors?.fromJar?.type === 'valid-source-jar-must-unfreeze-utxos-test' + const animate = utxoSelectionDialog.dialogProps.open === false && errorSolvedByUtxoSelection + return ( + + ) + }} /> diff --git a/src/components/sweep/SweepPage.test.tsx b/src/components/sweep/SweepPage.test.tsx index b290d6ea..c46ef3e3 100644 --- a/src/components/sweep/SweepPage.test.tsx +++ b/src/components/sweep/SweepPage.test.tsx @@ -303,6 +303,7 @@ const makeWalletInfo = (overrides: Partial = {}): WalletInfo => { calculatedAvailableBalanceInSats: 100_000, calculatedTotalBalanceInSats: 100_000, calculatedConfirmedAvailableBalanceInSats: 100_000, + calculatedAvailableFrozenBalanceInSats: 0, calculatedFrozenOrLockedBalanceInSats: 0, }, color: '#e2b86a', @@ -315,6 +316,7 @@ const makeWalletInfo = (overrides: Partial = {}): WalletInfo => { calculatedAvailableBalanceInSats: 0, calculatedTotalBalanceInSats: 0, calculatedConfirmedAvailableBalanceInSats: 0, + calculatedAvailableFrozenBalanceInSats: 0, calculatedFrozenOrLockedBalanceInSats: 0, }, color: '#ccc', @@ -341,6 +343,7 @@ const makeWalletInfo = (overrides: Partial = {}): WalletInfo => { calculatedAvailableBalanceInSats: 100_000, calculatedTotalBalanceInSats: 100_000, calculatedConfirmedAvailableBalanceInSats: 100_000, + calculatedAvailableFrozenBalanceInSats: 0, calculatedFrozenOrLockedBalanceInSats: 0, }, walletName: 'wallet.jmdat', diff --git a/src/components/ui/jam/Balance.test.tsx b/src/components/ui/jam/Balance.test.tsx index 25489f7e..b5c8ce40 100644 --- a/src/components/ui/jam/Balance.test.tsx +++ b/src/components/ui/jam/Balance.test.tsx @@ -3,8 +3,8 @@ import '@testing-library/jest-dom/vitest' import { render as reactRender, screen, type RenderOptions } from '@testing-library/react' import user from '@testing-library/user-event' import { describe, it, expect, vi } from 'vitest' +import { Balance } from '@/components/ui/jam/Balance' import { JamDisplayContextProvider } from '@/context/JamDisplayContextProvider' -import { Balance } from './Balance' const render = (ui: React.ReactNode, options?: Omit) => { const providers = ({ children }: { children: React.ReactNode }) => { diff --git a/src/components/ui/jam/Jar.test.tsx b/src/components/ui/jam/Jar.test.tsx new file mode 100644 index 00000000..d9c82b42 --- /dev/null +++ b/src/components/ui/jam/Jar.test.tsx @@ -0,0 +1,70 @@ +import { render, screen } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' +import { type Jar as JarType } from '@/context/JamWalletInfoContext' +import type { AmountSats } from '@/types/global' +import { Jar } from './Jar' + +vi.mock('@/components/ui/jam/Balance', () => ({ + Balance: ({ valueString, onClick }: { valueString: string; onClick?: () => void }) => ( + balance:{valueString} + ), +})) + +const DUMMY_JAR: JarType = { + balanceSummary: { + calculatedTotalBalanceInSats: 5, + calculatedAvailableBalanceInSats: 4, + calculatedConfirmedAvailableBalanceInSats: 3, + calculatedAvailableFrozenBalanceInSats: 2, + calculatedFrozenOrLockedBalanceInSats: 1, + }, + color: '#666', + jarIndex: 0, + name: 'Test jar', + utxos: [], +} + +describe('Jar', () => { + it('renders correctly', () => { + render( + , + ) + + expect(screen.getByText(DUMMY_JAR.name)).toBeInTheDocument() + expect(screen.getByText('balance:' + DUMMY_JAR.balanceSummary.calculatedAvailableBalanceInSats)).toBeInTheDocument() + expect( + screen.getByText('balance:' + DUMMY_JAR.balanceSummary.calculatedFrozenOrLockedBalanceInSats), + ).toBeInTheDocument() + expect( + screen.getByText('balance:' + DUMMY_JAR.balanceSummary.calculatedFrozenOrLockedBalanceInSats).parentNode, + ).not.toHaveClass('invisible') + }) + it('does not render zero frozen balance', () => { + const frozenBalance: AmountSats = 0 + + render( + , + ) + + expect(screen.getByText(DUMMY_JAR.name)).toBeInTheDocument() + expect(screen.getByText('balance:' + DUMMY_JAR.balanceSummary.calculatedAvailableBalanceInSats)).toBeInTheDocument() + expect(screen.getByText('balance:' + frozenBalance)).toBeInTheDocument() + expect(screen.getByText('balance:' + frozenBalance).parentNode).toHaveClass('invisible') + }) +}) diff --git a/src/components/ui/jam/Jar.tsx b/src/components/ui/jam/Jar.tsx index 451ea510..8cb6250f 100644 --- a/src/components/ui/jam/Jar.tsx +++ b/src/components/ui/jam/Jar.tsx @@ -1,7 +1,7 @@ +import { Balance } from '@/components/ui/jam/Balance' import type { JarColor } from '@/context/JamWalletInfoContext' import { cn } from '@/lib/utils' import type { AmountSats } from '@/types/global' -import { Balance } from './Balance' import { JarIcon } from './JarIcon' interface JarProps { @@ -53,7 +53,7 @@ export function Jar({
diff --git a/src/components/ui/jam/SelectableJar.test.tsx b/src/components/ui/jam/SelectableJar.test.tsx new file mode 100644 index 00000000..cb7c7a6e --- /dev/null +++ b/src/components/ui/jam/SelectableJar.test.tsx @@ -0,0 +1,131 @@ +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { describe, expect, it, vi } from 'vitest' +import { beforeEach } from 'vitest' +import type { Jar } from '@/context/JamWalletInfoContext' +import { SelectableJar } from './SelectableJar' + +const mocks = vi.hoisted(() => ({ + onClick: vi.fn(), + onSelect: vi.fn(), +})) + +vi.mock('@/components/ui/jam/Balance', () => ({ + Balance: ({ valueString, onClick }: { valueString: string; onClick?: () => void }) => ( + balance:{valueString} + ), +})) + +const DUMMY_JAR: Jar = { + balanceSummary: { + calculatedTotalBalanceInSats: 5, + calculatedAvailableBalanceInSats: 4, + calculatedConfirmedAvailableBalanceInSats: 3, + calculatedAvailableFrozenBalanceInSats: 2, + calculatedFrozenOrLockedBalanceInSats: 1, + }, + color: '#666', + jarIndex: 0, + name: 'Test jar', + utxos: [], +} + +describe('SelectableJar', () => { + beforeEach(() => { + mocks.onClick.mockReset() + mocks.onSelect.mockReset() + }) + + it('renders correctly', () => { + render( + , + ) + + expect(screen.getByText(DUMMY_JAR.name)).toBeInTheDocument() + expect(screen.getByText('balance:' + DUMMY_JAR.balanceSummary.calculatedAvailableBalanceInSats)).toBeInTheDocument() + expect( + screen.getByText('balance:' + DUMMY_JAR.balanceSummary.calculatedFrozenOrLockedBalanceInSats), + ).toBeInTheDocument() + }) + + it('can be selected', async () => { + const user = userEvent.setup() + + render( + , + ) + + await user.click(screen.getByRole('button')) + + expect(mocks.onClick).toHaveBeenCalled() + expect(mocks.onSelect).toHaveBeenCalled() + }) + + it('can not be selected if disabled', async () => { + const user = userEvent.setup() + + render( + , + ) + + await user.click(screen.getByRole('button')) + + expect(mocks.onClick).not.toHaveBeenCalled() + expect(mocks.onSelect).not.toHaveBeenCalled() + }) + + it('does not call onSelect if already selected', async () => { + const user = userEvent.setup() + + render( + , + ) + + await user.click(screen.getByRole('button')) + + expect(mocks.onClick).toHaveBeenCalled() + expect(mocks.onSelect).not.toHaveBeenCalled() + }) +}) diff --git a/src/components/ui/jam/SelectableJar.tsx b/src/components/ui/jam/SelectableJar.tsx index 60e76f9b..a526b7cf 100644 --- a/src/components/ui/jam/SelectableJar.tsx +++ b/src/components/ui/jam/SelectableJar.tsx @@ -4,7 +4,8 @@ import { Jar } from './Jar' interface SelectableJarProps extends ComponentProps { isSelected: NonNullable['checked']> - onClick: NonNullable['onChange']> + onClick?: NonNullable['onClick']> + onSelect: NonNullable['onChange']> } export const SelectableJar = ({ @@ -16,20 +17,19 @@ export const SelectableJar = ({ totalWalletBalance, isSelected, disabled = false, + onSelect, onClick, }: SelectableJarProps) => { const radioRef = useRef(null) return (
@@ -235,6 +241,7 @@ export const Interactive: Story = { frozenOrLockedBalance: 100_000, totalWalletBalance: 500_000, isSelected: false, + onSelect: () => alert('Selected'), onClick: () => alert('Interactive jar clicked'), }, parameters: {