1
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】テスト時のDialogの型エラーの解消方法③(Property 'children' does not exist on type 'DialogCloseTriggerProps'.)

Last updated at Posted at 2024-12-28

はじめに

お疲れ様です。

前回に引き続き、Chakra UI起因のテスト実行時のエラー解決方法についてです。

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

  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実行時に以下のエラーが発生しました。

今回は3つ目のエラーの解消方法です。

エラー内容
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, type DialogCloseTriggerProps } 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;
  ref?: React.ForwardedRef<HTMLDivElement>;
}

+ // DialogCloseTriggerPropsを継承した新しいインターフェースを定義し、childrenを追加
+ interface NewDialogCloseTriggerProps extends DialogCloseTriggerProps {
+   children?: React.ReactNode;
+ }

export const DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps>(function DialogContent(props, ref) {
  const { children, portalled = true, portalRef, backdrop = true, ...rest } = props;
  const chakraDialogContentProps: DialogContentProps = {
    ref: ref,
    asChild: false,
    children: children,
  };

  return (
    <Portal disabled={!portalled} container={portalRef}>
      {backdrop && <ChakraDialog.Backdrop />}
      <ChakraDialog.Positioner>
        <ChakraDialog.Content {...rest} {...chakraDialogContentProps} />
      </ChakraDialog.Positioner>
    </Portal>
  );
});

- export const DialogCloseTrigger = React.forwardRef<HTMLButtonElement, ChakraDialog.CloseTriggerProps>(function DialogCloseTrigger(props, ref) {
+ export const DialogCloseTrigger = React.forwardRef<HTMLButtonElement, NewDialogCloseTriggerProps>(function DialogCloseTrigger(props, ref) { // NewDialogCloseTriggerPropsに置き換え
  const chakraDialogCloseTriggerProps: DialogCloseTriggerProps = {
    asChild: true,
    position: 'absolute',
    top: '2',
    insetEnd: '2',
    children: (
      <CloseButton size="sm" ref={ref}>
        {props.children}
      </CloseButton>
    ),
  };

  return <ChakraDialog.CloseTrigger {...chakraDialogCloseTriggerProps} {...props} />;
});

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;

おわりに

これまでの対応によって、自分の環境では全てのエラーが解消され、テストが通るようになりました。
特にDialogToastによるエラーが多かったですね。

もし他によい解決方法をご存知の方がいらっしゃれば、ご教示いただけると幸いです。

参考

1
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
1
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?