2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Chakra UI v3 × jest】テスト時のDialogContentの型エラーの解消方法(Type '{ children: Element[]; }' has no properties in common with type 'IntrinsicAttributes & DialogContentProps & RefAttributes<HTMLDivElement>'.)

Last updated at Posted at 2024-12-25

はじめに

お疲れ様です、りつです。

現在実装中のReactアプリで、テスト実行時にChakra UI関連のエラーが複数発生しました。

今回はそのうちの1つ目、Chakra UI v3のDialogContentコンポーネントで発生したエラーの解決方法です。

【 関連記事 】 今回発生したエラーの解消記事

  1. 【Chakra UI v3 × jest】テスト時のDialogContentの型エラーの解消方法(Type '{ children: Element[]; }' has no properties in common with type 'IntrinsicAttributes & DialogContentProps & RefAttributes'.) ➡ この記事
  2. 【Chakra UI v3 × jest】テスト時のToastの型エラーの解消方法①(Type '{ children: (toast: any) => Element; toaster: any; insetInline: { mdDown: string; }; }' is not assignable to type 'IntrinsicAttributes & ToasterProps'.)
  3. 【Chakra UI v3 × jest】テスト時のToastの型エラーの解消方法②(Type '{ style: { width: string; height: string; color: string; }; _hover: { cursor: string; }; }' is not assignable to type 'IntrinsicAttributes & ToastCloseTriggerProps & RefAttributes'.)
  4. 【Chakra UI v3 × jest】テスト時のDialogの型エラーの解消方法①(Type '{ children: ReactNode; asChild: boolean; ref: ForwardedRef; }' is not assignable to type 'IntrinsicAttributes & DialogContentProps & RefAttributes'.)
  5. 【Chakra UI v3 × jest】テスト時のDialogの型エラーの解消方法②(Type '{ children: Element; asChild: true; position: string; top: string; insetEnd: string; }' is not assignable to type 'IntrinsicAttributes & DialogCloseTriggerProps & RefAttributes'.)
  6. 【Chakra UI v3 × jest】テスト時のDialogの型エラーの解消方法③(Property 'children' does not exist on type 'DialogCloseTriggerProps'.)

問題

npm run test実行時に以下のエラーが発生しました。

エラー内容
 FAIL  src/__tests__/App.spec.tsx
  ● Test suite failed to run

    src/App.tsx:176:10 - error TS2559: Type '{ children: Element[]; }' has no properties in common with type 'IntrinsicAttributes & DialogContentProps & RefAttributes<HTMLDivElement>'.

    176         <DialogContent>
                 ~~~~~~~~~~~~~
    src/App.tsx:231:10 - error TS2559: Type '{ children: Element[]; }' has no properties in common with type 'IntrinsicAttributes & DialogContentProps & RefAttributes<HTMLDivElement>'.

    231         <DialogContent>
                 ~~~~~~~~~~~~~

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        4.609 s
Ran all test suites.

なお、DialogContentはChakra UIでモーダルを表示させる際に必要なコンポーネントの一つです。

ソースコード

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の内容を修正します。
以下のように、DialogContentPropsインターフェースにchildrenの型定義を追加します。

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; // children を追加
}

// 省略

おわりに

上記対応により、DialogContentコンポーネントのエラーが発生しなくなりました。

しかし、続けて別のエラーが発生したので、そちらは別の記事で解決方法をご紹介したいと思います。

➡ 記事を書きました。

参考

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?