はじめに
お疲れ様です。
前回に引き続き、Chakra UI起因のテスト実行時のエラー解決方法についてです。
【 関連記事 】 今回発生したエラーの解消記事
- 【Chakra UI v3 × jest】テスト時のDialogContentの型エラーの解消方法(Type '{ children: Element[]; }' has no properties in common with type 'IntrinsicAttributes & DialogContentProps & RefAttributes'.)
- 【Chakra UI v3 × jest】テスト時のToastの型エラーの解消方法①(Type '{ children: (toast: any) => Element; toaster: any; insetInline: { mdDown: string; }; }' is not assignable to type 'IntrinsicAttributes & ToasterProps'.)
- 【Chakra UI v3 × jest】テスト時のToastの型エラーの解消方法②(Type '{ style: { width: string; height: string; color: string; }; _hover: { cursor: string; }; }' is not assignable to type 'IntrinsicAttributes & ToastCloseTriggerProps & RefAttributes'.)
- 【Chakra UI v3 × jest】テスト時のDialogの型エラーの解消方法①(Type '{ children: ReactNode; asChild: boolean; ref: ForwardedRef; }' is not assignable to type 'IntrinsicAttributes & DialogContentProps & RefAttributes'.) ➡ この記事
- 【Chakra UI v3 × jest】テスト時のDialogの型エラーの解消方法②(Type '{ children: Element; asChild: true; position: string; top: string; insetEnd: string; }' is not assignable to type 'IntrinsicAttributes & DialogCloseTriggerProps & RefAttributes'.)
- 【Chakra UI v3 × jest】テスト時のDialogの型エラーの解消方法③(Property 'children' does not exist on type 'DialogCloseTriggerProps'.)
問題
npm run test
実行時に以下のエラーが発生しました。
今回は、このうち1つ目のエラーを解消しました。
エラー内容
FAIL src/__tests__/App.spec.tsx
● Test suite failed to run
src/components/ui/dialog.tsx:19:10 - error TS2322: Type '{ children: ReactNode; asChild: boolean; ref: ForwardedRef<HTMLDivElement>; }' is not assignable to type 'IntrinsicAttributes & DialogContentProps & RefAttributes<HTMLDivElement>'.
Property 'children' does not exist on type 'IntrinsicAttributes & DialogContentProps & RefAttributes<HTMLDivElement>'.
19 <ChakraDialog.Content ref={ref} {...rest} asChild={false}>
~~~~~~~~~~~~~~~~~~~~
src/components/ui/dialog.tsx:29:6 - error TS2322: Type '{ children: Element; asChild: true; position: string; top: string; insetEnd: string; }' is not assignable to type 'IntrinsicAttributes & DialogCloseTriggerProps & RefAttributes<HTMLButtonElement>'.
Property 'children' does not exist on type 'IntrinsicAttributes & DialogCloseTriggerProps & RefAttributes<HTMLButtonElement>'.
29 <ChakraDialog.CloseTrigger position="absolute" top="2" insetEnd="2" {...props} asChild>
~~~~~~~~~~~~~~~~~~~~~~~~~
src/components/ui/dialog.tsx:31:16 - error TS2339: Property 'children' does not exist on type 'DialogCloseTriggerProps'.
31 {props.children}
~~~~~~~~
Test Suites: 1 failed, 1 total
Tests: 0 total
Snapshots: 0 total
Time: 3.808 s
Ran all test suites.
ソースコード
src/App.tsx
src/App.tsx
import { useEffect, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { MdEdit, MdDeleteOutline } from 'react-icons/md';
import { FiPlusCircle } from 'react-icons/fi';
import { Box, Center, Container, Flex, Heading, IconButton, Input, Spinner, Stack, Table } from '@chakra-ui/react';
import { Button } from '@/components/ui/button';
import { Field } from '@/components/ui/field';
import { NumberInputField, NumberInputRoot } from '@/components/ui/number-input';
import { Toaster } from '@/components/ui/toaster';
import { Record } from '@/domain/record';
import { useMessage } from '@/hooks/useMessage';
import { fetchAllRecords, insertRecord, deleteRecord } from '@/utils/supabaseFunctions';
import {
DialogActionTrigger,
DialogBody,
DialogCloseTrigger,
DialogContent,
DialogFooter,
DialogHeader,
DialogRoot,
DialogTitle,
} from '@/components/ui/dialog';
function App() {
const [records, setRecords] = useState<Record[]>([]);
const [deleteTargetId, setDeleteTargetId] = useState<string | undefined>(undefined);
const [isLoading, setIsLoading] = useState(false);
const [isCreating, setIsCreating] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [open, setOpen] = useState(false);
const [openConfirm, setOpenConfirm] = useState(false);
const { showMessage } = useMessage();
const {
control,
handleSubmit,
reset,
formState: { errors },
} = useForm<Record>({
defaultValues: {
title: '',
time: '0',
},
});
useEffect(() => {
getAllRecords();
}, []);
const onClickOpenModal = () => {
setOpen(true);
};
const getAllRecords = () => {
setIsLoading(true);
fetchAllRecords()
.then((data) => {
setRecords(data);
})
.catch(() => {
showMessage({ title: '一覧の取得に失敗しました', type: 'error' });
})
.finally(() => {
setIsLoading(false);
});
};
const onSubmit = handleSubmit((data: Record) => {
setIsCreating(true);
insertRecord(data)
.then(() => {
showMessage({ title: '学習記録の登録が完了しました', type: 'success' });
})
.catch(() => {
showMessage({ title: '学習記録の登録に失敗しました', type: 'error' });
})
.finally(() => {
setIsCreating(false);
reset(); // 登録フォームの初期化
setOpen(false);
getAllRecords();
});
});
const onClickDeleteConfirm = (id: string) => {
setDeleteTargetId(id);
setOpenConfirm(true);
};
const onClickDelete = () => {
setIsDeleting(true);
if (typeof deleteTargetId === 'undefined') {
showMessage({ title: '削除対象のデータが見つかりません', type: 'error' });
setIsDeleting(false);
setOpenConfirm(false);
setDeleteTargetId(undefined);
return;
}
deleteRecord(deleteTargetId)
.then(() => {
showMessage({ title: '学習記録の削除が完了しました', type: 'success' });
})
.catch(() => {
showMessage({ title: '学習記録の削除に失敗しました', type: 'error' });
})
.finally(() => {
setIsDeleting(false);
setOpenConfirm(false);
setDeleteTargetId(undefined);
getAllRecords();
});
};
return (
<>
<Toaster />
<Box bg="teal.500" py="4" data-testid="title" color="gray.100">
<Container maxW="6xl">
<Flex justify="space-between" align="center">
<Heading as="h1" textAlign="left">
学習記録アプリ
</Heading>
<IconButton
aria-label="Search database"
variant="ghost"
size="lg"
color="white"
_hover={{ bg: 'teal.500', color: 'gray.200' }}
onClick={onClickOpenModal}
>
<FiPlusCircle />
</IconButton>
</Flex>
</Container>
</Box>
{isLoading ? (
<Center h="100vh">
<Spinner />
</Center>
) : (
<Container maxW="6xl">
<Table.Root size="md" variant="line" my={10} interactive>
<Table.Header>
<Table.Row>
<Table.ColumnHeader>タイトル</Table.ColumnHeader>
<Table.ColumnHeader>時間</Table.ColumnHeader>
<Table.ColumnHeader textAlign="end"></Table.ColumnHeader>
</Table.Row>
</Table.Header>
<Table.Body>
{records.map((record) => (
<Table.Row key={record.id}>
<Table.Cell>{record.title}</Table.Cell>
<Table.Cell>{record.time}</Table.Cell>
<Table.Cell textAlign="end">
<Button colorPalette="blue" variant="outline" mr="4">
<MdEdit />
編集
</Button>
<Button colorPalette="red" variant="outline" onClick={() => onClickDeleteConfirm(record.id)}>
<MdDeleteOutline />
削除
</Button>
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
</Container>
)}
<DialogRoot lazyMount open={open} onOpenChange={(e) => setOpen(e.open)} motionPreset="slide-in-bottom" trapFocus={false}>
<DialogContent>
<DialogCloseTrigger />
<DialogHeader>
<DialogTitle>学習記録登録</DialogTitle>
</DialogHeader>
<form onSubmit={onSubmit}>
<DialogBody>
<Stack gap={4}>
<Field label="学習内容" invalid={!!errors.title} errorText={errors.title?.message}>
<Controller
name="title"
control={control}
rules={{
required: '内容の入力は必須です',
}}
render={({ field }) => <Input {...field} />}
/>
</Field>
<Field label="学習時間" invalid={!!errors.time} errorText={errors.time?.message}>
<Controller
name="time"
control={control}
rules={{
required: '時間の入力は必須です',
min: { value: 0, message: '時間は0以上である必要があります' },
}}
render={({ field }) => (
<NumberInputRoot width="100%" name={field.name} value={field.value} onValueChange={({ value }) => field.onChange(value)}>
<NumberInputField />
</NumberInputRoot>
)}
/>
</Field>
</Stack>
</DialogBody>
<DialogFooter mb="2">
<DialogActionTrigger asChild>
<Button variant="outline">キャンセル</Button>
</DialogActionTrigger>
<Button colorPalette="teal" loading={isCreating} type="submit">
登録
</Button>
</DialogFooter>
</form>
</DialogContent>
</DialogRoot>
<DialogRoot
role="alertdialog"
lazyMount
open={openConfirm}
onOpenChange={(e) => setOpenConfirm(e.open)}
motionPreset="slide-in-bottom"
trapFocus={false}
>
<DialogContent>
<DialogHeader>
<DialogTitle>削除の確認</DialogTitle>
</DialogHeader>
<DialogBody>
<p>削除したデータは戻せません。削除してもよろしいですか?</p>
</DialogBody>
<DialogFooter mb="2">
<DialogActionTrigger asChild>
<Button variant="outline">キャンセル</Button>
</DialogActionTrigger>
<Button colorPalette="red" loading={isDeleting} onClick={onClickDelete}>
削除
</Button>
</DialogFooter>
<DialogCloseTrigger />
</DialogContent>
</DialogRoot>
</>
);
}
export default App;
src/__tests__/App.spec.tsx
src/__tests__/App.spec.tsx
import App from '../App';
import { render, screen } from '@testing-library/react';
import { ChakraProvider, defaultSystem } from '@chakra-ui/react';
describe('App', () => {
test('タイトルがあること', async () => {
render(
<ChakraProvider value={defaultSystem}>
<App />
</ChakraProvider>
);
const title = screen.getByTestId('title');
expect(title).toBeInTheDocument();
});
});
解決方法
src/components/ui/dialog.tsx
を修正します。
src/components/ui/dialog.tsx
import { Dialog as ChakraDialog, Portal } from '@chakra-ui/react';
import { CloseButton } from './close-button';
import * as React from 'react';
interface DialogContentProps extends ChakraDialog.ContentProps {
portalled?: boolean;
portalRef?: React.RefObject<HTMLElement>;
backdrop?: boolean;
children?: React.ReactNode;
+ asChild?: boolean; // asChild を追加
+ ref?: React.ForwardedRef<HTMLDivElement>; // ref を追加
}
export const DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps>(function DialogContent(props, ref) {
const { children, portalled = true, portalRef, backdrop = true, ...rest } = props;
+ // chakraDialogContentPropsを型「DialogContentProps」として定義
+ const chakraDialogContentProps: DialogContentProps = {
+ ref: ref,
+ asChild: false,
+ children: children,
+ };
return (
<Portal disabled={!portalled} container={portalRef}>
{backdrop && <ChakraDialog.Backdrop />}
<ChakraDialog.Positioner>
- <ChakraDialog.Content ref={ref} {...rest} asChild={false}>
- {children}
- </ChakraDialog.Content>
+ <ChakraDialog.Content {...rest} {...chakraDialogContentProps} /> {/* chakraDialogContentPropsに置き換え */}
</ChakraDialog.Positioner>
</Portal>
);
});
export const DialogCloseTrigger = React.forwardRef<HTMLButtonElement, ChakraDialog.CloseTriggerProps>(function DialogCloseTrigger(props, ref) {
return (
<ChakraDialog.CloseTrigger position="absolute" top="2" insetEnd="2" {...props} asChild>
<CloseButton size="sm" ref={ref}>
{props.children}
</CloseButton>
</ChakraDialog.CloseTrigger>
);
});
export const DialogRoot = ChakraDialog.Root;
export const DialogFooter = ChakraDialog.Footer;
export const DialogHeader = ChakraDialog.Header;
export const DialogBody = ChakraDialog.Body;
export const DialogBackdrop = ChakraDialog.Backdrop;
export const DialogTitle = ChakraDialog.Title;
export const DialogDescription = ChakraDialog.Description;
export const DialogTrigger = ChakraDialog.Trigger;
export const DialogActionTrigger = ChakraDialog.ActionTrigger;
おわりに
上記の対応でエラーは解消されました。
残りのエラーについては別記事で解決方法を記載します。
➡ 記事を書きました。
参考