【Chakra UI v3 × jest】テスト時のToastの型エラーの解消方法②（Type '{ style: { width: string; height: string; color: string; }; _hover: { cursor: string; }; }' is not assignable to type 'IntrinsicAttributes & ToastCloseTriggerProps & RefAttributes<HTMLButtonElement>'.）

# はじめに

お疲れ様です。

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

## 【 関連記事 】 今回発生したエラーの解消記事
1. [【Chakra UI v3 × jest】テスト時のDialogContentの型エラーの解消方法（Type '{ children: Element[]; }' has no properties in common with type 'IntrinsicAttributes & DialogContentProps & RefAttributes<HTMLDivElement>'.）](https://qiita.com/ritsu21ctws/items/ebf7c9c2e3f082c23ec1)
2. [【Chakra UI v3 × jest】テスト時のToastの型エラーの解消方法①（Type '{ children: (toast: any) => Element; toaster: any; insetInline: { mdDown: string; }; }' is not assignable to type 'IntrinsicAttributes & ToasterProps'.）](https://qiita.com/ritsu21ctws/items/e3ef69cf952c5e4803b2)
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<HTMLButtonElement>'.）](https://qiita.com/ritsu21ctws/items/e8efbcc831ea0d58f340)　**➡ この記事**
4. [【Chakra UI v3 × jest】テスト時のDialogの型エラーの解消方法①（Type '{ children: ReactNode; asChild: boolean; ref: ForwardedRef<HTMLDivElement>; }' is not assignable to type 'IntrinsicAttributes & DialogContentProps & RefAttributes<HTMLDivElement>'.）](https://qiita.com/ritsu21ctws/items/8029cb166116e3ffc31b)
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<HTMLButtonElement>'.）](https://qiita.com/ritsu21ctws/items/8940ca3a8eb0bdd79909)
6. [【Chakra UI v3 × jest】テスト時のDialogの型エラーの解消方法③（Property 'children' does not exist on type 'DialogCloseTriggerProps'.）](https://qiita.com/ritsu21ctws/items/5e217c754e16ead75d2b)

# 問題

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

前回の記事で1つ目のエラーが解消されましたので、今回は2つ目のエラーを解消していきます。

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

    src/components/ui/toaster.tsx:13:8 - error TS2322: Type '{ children: (toast: any) => Element; toaster: any; insetInline: { mdDown: string; }; }' is not assignable to type 'IntrinsicAttributes & ToasterProps'.
      Property 'children' does not exist on type 'IntrinsicAttributes & ToasterProps'.

    13       <ChakraToaster toaster={toaster} insetInline={{ mdDown: '4' }}>
              ~~~~~~~~~~~~~
    src/components/ui/toaster.tsx:22:58 - error TS2322: Type '{ style: { width: string; height: string; color: string; }; _hover: { cursor: string; }; }' is not assignable to type 'IntrinsicAttributes & ToastCloseTriggerProps & RefAttributes<HTMLButtonElement>'.
      Property 'style' does not exist on type 'IntrinsicAttributes & ToastCloseTriggerProps & RefAttributes<HTMLButtonElement>'.

    22             {toast.meta?.closable && <Toast.CloseTrigger style={{ width: '25px', height: '25px', color: 'white' }} _hover={{ cursor: 'pointer' }} />}
                                                                ~~~~~

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


なお、エラー発生部分は、以前トーストの閉じるボタンの調整を行った際に自分で付与した属性です。

https://qiita.com/ritsu21ctws/items/3ea38bb82f2d625140fc

## ソースコード

<details><summary>src/App.tsx</summary>

```react: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;
```

</details>

<details><summary>src/__tests__/App.spec.tsx</summary>

```react: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();
  });
});
```

</details>

# 解決方法

`src/components/ui/toaster.tsx`を修正します。

今回は、該当のコンポーネント（`Toast.CloseTrigger`）を`Box`に置き換える形で対応しました。
具体的な修正内容については以下の通りです。

```diff_tsx:src/components/ui/toaster.tsx
'use client';

- import { Toaster as ChakraToaster, Portal, Spinner, Stack, Toast, createToaster, type ToasterProps } from '@chakra-ui/react';
+ import { Toaster as ChakraToaster, Box, Portal, Spinner, Stack, Toast, createToaster, type ToasterProps } from '@chakra-ui/react'; // Boxをインポート

export const toaster = createToaster({
  placement: 'top',
  pauseOnPageIdle: true,
});

const toasterProps: ToasterProps = {
  toaster: toaster,
  insetInline: { mdDown: '4' },
  children: (toast) => (
    <Toast.Root width={{ md: 'sm' }}>
      {toast.type === 'loading' ? <Spinner size="sm" color="blue.solid" /> : <Toast.Indicator />}
      <Stack gap="1" flex="1" maxWidth="100%">
        {toast.title && <Toast.Title>{toast.title}</Toast.Title>}
        {toast.description && <Toast.Description>{toast.description}</Toast.Description>}
      </Stack>
      {toast.action && <Toast.ActionTrigger>{toast.action.label}</Toast.ActionTrigger>}
-     {toast.meta?.closable && <Toast.CloseTrigger style={{ width: '25px', height: '25px', color: 'white' }} _hover={{ cursor: 'pointer' }} />}
+     {toast.meta?.closable && <Box as={Toast.CloseTrigger} width="25px" height="25px" color="white" _hover={{ cursor: 'pointer' }} />} {/* Boxに置き換え */}
    </Toast.Root>
  ),
};

export const Toaster = () => {
  return (
    <Portal>
      <ChakraToaster {...toasterProps} />
    </Portal>
  );
};
```

# おわりに

上記の対応で`Toast`のエラーは解消されました。

なお、上記解消後にさらに別のエラーが発生しましたので、そちらについては別記事で解決していこうと思います。

**➡ 記事を書きました。**

https://qiita.com/ritsu21ctws/items/8029cb166116e3ffc31b

https://qiita.com/ritsu21ctws/items/8940ca3a8eb0bdd79909

https://qiita.com/ritsu21ctws/items/5e217c754e16ead75d2b

# 参考

https://www.chakra-ui.com/docs/components/toast

https://qiita.com/ritsu21ctws/items/3ea38bb82f2d625140fc