Vitest
第一个测试
配置
bash
pnpm add \
@testing-library/dom \
@testing-library/jest-dom \
@testing-library/react \
@testing-library/user-event \
@vitest/coverage-v8 \
@vitest/ui \
jsdom \
vitest -Dts
// 配置合并
import { defineConfig, searchForWorkspaceRoot } from "vite";
import { defineConfig as defineTestConfig, mergeConfig } from "vitest/config";
// https://vitest.dev/guide/#configuring-vitest
import react from "@vitejs/plugin-react";
import { fileURLToPath } from "url";
// https://vite.dev/config/
const viteConfig = defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
server: {
fs: {
// for Rush.js
allow: [
searchForWorkspaceRoot(process.cwd()),
"../../common/temp/node_modules",
],
},
},
});
export default mergeConfig(
viteConfig,
defineTestConfig({
test: {
// 默认 node, 通过 jsdom 模拟浏览器环境
environment: "jsdom",
},
}),
);tsx
import { useState } from "react";
function App() {
const [count, setCount] = useState(0);
return (
<>
<h1>role: heading</h1>
<div className="counter">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
</div>
</>
);
}
export default App;tsx
import { expect, test } from "vitest";
import { render, screen } from "@testing-library/react";
import App from "@/App";
import "@testing-library/jest-dom/vitest";
test("expect h1 element to be in the document", () => {
render(<App />);
// h1: role=heading
const headingElement = screen.getByRole("heading");
const buttonElement = screen.getByRole("button");
// 必须副作用导入 "@testing-library/jest-dom/vitest" 以使用 toBeInTheDocument()
expect(headingElement).toBeInTheDocument();
expect(buttonElement).toBeInTheDocument();
});react act()
使用 act() 测试组件渲染
tsx
import { expect, test } from "vitest";
import App from "@/App";
import { act } from "react";
import { createRoot } from "react-dom/client";
test("react act", () => {
const container = document.createElement("div");
document.body.appendChild(container);
// 使用 act() 包裹组件渲染
act(() => {
createRoot(container).render(<App />);
});
const heading = container.querySelector("h1");
const button = container.querySelector("button");
act(() => {
// 使用 act() 包裹事件派发
button?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(heading?.textContent).toBe("role: heading");
expect(button?.textContent).toBe("count is 1");
});Pitfall 陷阱
事件派发只有在 container 被添加到文档时才有效, 即 document.body.appendChild(container);
测试渲染
tsx
function TogglePurple({
isPurple,
setIsPurple,
}: {
isPurple: boolean;
setIsPurple: (isPurple: boolean) => void;
}) {
return (
<label>
Purple
<input
type="checkbox"
checked={isPurple}
onChange={() => setIsPurple(!isPurple)}
/>
</label>
);
}
export default TogglePurple;tsx
import { expect, test } from "vitest";
import { render, screen } from "@testing-library/react";
import TogglePurple from "@/components/toggle-purple";
import "@testing-library/jest-dom/vitest";
// 避免重复渲染
let isPurple = false;
const setIsPurple = (newIsPurple: boolean) => {
isPurple = newIsPurple;
};
render(<TogglePurple isPurple={isPurple} setIsPurple={setIsPurple} />);
test("TogglePurple checkbox", () => {
const checkboxElement = screen.getByRole("checkbox");
expect(checkboxElement).toBeInTheDocument();
// not 否定断言
expect(checkboxElement).not.toBeChecked();
});
test("TogglePurple label", () => {
const labelElement = screen.getByText(/purple/i);
expect(labelElement).toBeInTheDocument();
});配置
ts
import { describe, test, it } from "vitest";- describe 测试分组
- it 等价于 test
globals 全局 API
如果 vitest 配置了 globals 全局 API, 则 @testing-library/react 会使用全局的 afterEach() 自动调用 cleanup(), 卸载通过 render() 挂载的 react 组件树
ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
// 默认 node, 通过 jsdom 模拟浏览器环境
environment: "jsdom",
// 不需要显式导入 describe, test, it 等
globals: true,
// 副作用导入 @testing-library/jest-dom/vitest
setupFiles: ["./tests/setup-jsdom.ts"],
},
});ts
// 保留样式
import "@/index.css";
// 副作用导入 @testing-library/jest-dom/vitest
import "@testing-library/jest-dom/vitest";json
{
"compilerOptions": {
"types": ["vite/client", "vitest/globals"]
},
"include": ["src", "tests"]
}优化测试
tsx
// tests/components/toggle-purple.test.tsx
import { render, screen } from "@testing-library/react";
import TogglePurple from "@/components/toggle-purple";
describe("TogglePurple", () => {
let isPurple = false;
const setIsPurple = (newIsPurple: boolean) => {
isPurple = newIsPurple;
};
beforeEach(() => {
render(<TogglePurple isPurple={isPurple} setIsPurple={setIsPurple} />);
});
it("should render the checkbox and the checkbox should not be checked by default", () => {
const checkboxElement = screen.getByRole("checkbox");
expect(checkboxElement).toBeInTheDocument();
expect(checkboxElement).not.toBeChecked();
// 自动调用 testing-library 的 cleanup 函数, 卸载组件并销毁容器
});
it("should render the label with purple text", () => {
screen.debug();
const labelElement = screen.getByText(/purple/i);
expect(labelElement).toBeInTheDocument();
});
});测试用户交互
tsx
import { render, screen } from "@testing-library/react";
import TogglePurple from "@/components/toggle-purple";
import userEvent from "@testing-library/user-event";
import { useState } from "react";
describe("TogglePurple", () => {
// 无效的 hook 调用, hook 只能在函数组件的内部调用
const [isPurple, setIsPurple] = useState(false);
beforeEach(() => {
render(<TogglePurple isPurple={isPurple} setIsPurple={setIsPurple} />);
});
describe("user interaction test", () => {
it("should be checked after user click", async () => {
const checkboxElement = screen.getByRole("checkbox");
const user = userEvent.setup();
await user.click(checkboxElement);
expect(checkboxElement).toBeChecked();
});
});
});tsx
import { render, screen } from "@testing-library/react";
import TogglePurple from "@/components/toggle-purple";
import userEvent from "@testing-library/user-event";
import { useState } from "react";
function TogglePurple4mock() {
const [isPurple, setIsPurple] = useState(false);
return <TogglePurple isPurple={isPurple} setIsPurple={setIsPurple} />;
}
describe("TogglePurple", () => {
beforeEach(() => {
render(<TogglePurple4mock />);
});
describe("user interaction test", () => {
it("should be checked after user click", async () => {
const checkboxElement = screen.getByRole("checkbox");
const user = userEvent.setup();
await user.click(checkboxElement);
expect(checkboxElement).toBeChecked();
});
});
});遍历测试
Vitest UI
bash
pnpm add @vitest/ui -D
pnpm exec vitest --uitsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useState } from "react";
import SelectColor from "@/components/select-color";
function SelectColor4mock() {
const [textColor, setTextColor] = useState("");
return <SelectColor textColor={textColor} setTextColor={setTextColor} />;
}
describe("SelectColor", () => {
beforeEach(() => {
render(<SelectColor4mock />);
});
describe("static render test", () => {
it("should render the select element", () => {
// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/select#technical_summary
const selectElement = screen.getByRole("combobox");
expect(selectElement).toBeInTheDocument();
});
it("should render the label element", () => {
// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/select#technical_summary
const labelElement = screen.getByText(/text color/i);
expect(labelElement).toBeInTheDocument();
});
});
describe("interaction test", () => {
it.each([
{ optionValue: "", label: "white" },
{ optionValue: "text-blue", label: "blue" },
{ optionValue: "text-green", label: "green" },
])(
"should display the $label after user click the $label option",
async ({ optionValue }) => {
const selectElement = screen.getByRole("combobox");
// screen.debug();
const user = userEvent.setup();
await user.click(selectElement);
// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/option#technical_summary
const optionElements = screen.getAllByRole("option");
expect(optionElements).toHaveLength(3);
// <option value="text-green">Green</option>
// name: Green, value: text-green
await user.selectOptions(selectElement, optionValue);
expect(selectElement).toHaveValue(optionValue);
},
);
});
});测试覆盖率
ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
// 默认 node, 通过 jsdom 模拟浏览器环境
environment: "jsdom",
// 不需要显式导入 describe, test, it 等
globals: true,
// 副作用导入 @testing-library/jest-dom/vitest
setupFiles: ["./tests/setup-jsdom.ts"],
// 测试覆盖率
coverage: {
enabled: true,
provider: "v8",
include: ["src/**/*.{ts,tsx}"],
},
},
});测试键盘输入
tsx
import { cleanup, render, screen } from "@testing-library/react";
import CircleProperty from "@/components/circle-property";
import userEvent from "@testing-library/user-event";
import { useState, type PropsWithChildren } from "react";
describe("CircleProperty", () => {
function CircleProperty4mock({ children = "demo" }: PropsWithChildren) {
const [property, setProperty] = useState(0);
return (
<CircleProperty property={property} setProperty={setProperty}>
{children}
</CircleProperty>
);
}
beforeEach(() => {
render(<CircleProperty4mock />);
});
describe("static render test", () => {
it("should render the input element", () => {
// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#technical_summary
const inputElement = screen.getByRole("spinbutton");
expect(inputElement).toBeInTheDocument();
});
it("should render the label element", () => {
// Unmount the `<CircleProperty4mock>` component by `beforeEach()`
cleanup();
const labelText = "Vitest is Awesome";
render(<CircleProperty4mock>{labelText}</CircleProperty4mock>);
screen.debug();
const labelElement = screen.getByText(labelText);
expect(labelElement).toBeInTheDocument();
});
});
describe("interaction test", () => {
it("should display the correct value after user type", async () => {
const inputElement = screen.getByRole("spinbutton");
const user = userEvent.setup();
const inputNumber = 30;
await user
// MUST `click()` before `keyboard()`
.click(inputElement)
.then(() => user.clear(inputElement))
.then(() => user.keyboard(inputNumber.toString()));
expect(inputElement).toHaveValue(inputNumber);
});
});
});Vitest 浏览器模式
Why
tsx
import { useState, type ComponentProps, type MouseEvent } from "react";
import "./button.css";
function Button(props: ComponentProps<"button">) {
const { onClick, children, ...rest } = props;
const [content, setContent] = useState("Click me");
const [classNames, setClassNames] = useState("btn");
const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
onClick?.(e);
setContent((content) => (content === "Click me" ? "Active" : "Click me"));
setClassNames((classNames) =>
classNames === "btn" ? "btn active" : "btn",
);
};
return (
<button {...rest} onClick={handleClick} className={classNames}>
{children ?? content}
</button>
);
}
export default Button;css
.active {
color: #fff;
background: #fb2c36;
}tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Button from "@/components/button";
it("should display red color after user click", async () => {
// Arrange
render(<Button />);
const buttonElement = screen.getByRole("button");
const user = userEvent.setup();
// Act
await user.click(buttonElement);
// Assert
expect(buttonElement).toHaveClass("active");
expect(buttonElement).toHaveStyle({
backgroundColor: "hex(#fb2c36)",
});
const actualBackgroundColor =
window.getComputedStyle(buttonElement).backgroundColor;
// Expected: "rgb(251, 44, 54)"
// Received: "rgba(0, 0, 0, 0)"
// 解决方法: 使用 vitest 浏览器模式
expect(actualBackgroundColor).toBe("rgb(251, 44, 54)");
});配置
bash
pnpm add \
@vitest/coverage-v8 \
@vitest/ui \
jsdom \
@vitest/browser-playwright \
vitest-browser-react \
vitest -Dts
/// <reference types="vitest/config" />
// https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import { playwright } from "@vitest/browser-playwright";
import { fileURLToPath } from "url";
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
test: {
globals: true,
// 浏览器模式
browser: {
enabled: true,
provider: playwright(),
// https://vitest.dev/config/browser/playwright
instances: [{ browser: "chromium" }],
},
},
});tsx
import Button from "@/components/button";
// import { render, screen } from "@testing-library/react";
import { render } from "vitest-browser-react";
// import userEvent from "@testing-library/user-event";
import { userEvent } from "vitest/browser";
it("should display red color after user click", async () => {
// Arrange
const renderResult = await render(<Button />);
const buttonLocator = renderResult.getByRole("button");
// Act
await userEvent.click(buttonLocator);
renderResult.debug();
// Assert
const backgroundColor = window.getComputedStyle(
buttonLocator.element(),
).backgroundColor;
expect(backgroundColor).toBe("rgb(251, 44, 54)");
});无障碍
tsx
<button
onClick={(e) => {
e.stopPropagation();
deleteNote(note.id);
}}
className="btn btn-square btn-error lg:btn-lg"
aria-label="delete-button"
>
{isDeleting ? (
<span className="loading loading-spinner" />
) : (
<TrashIcon size={24} weight="thin" />
)}
</button>tsx
import NoteItem from "@/components/note-item";
import ModalContextProvider from "@/context/modal-context-provider";
import type { Note } from "@/schema";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render } from "vitest-browser-react";
describe("NoteItem", () => {
const mockNote: Note = {
id: 1,
title: "Test Note",
content: "This is a test note.",
};
const queryClient = new QueryClient();
async function renderComponent() {
const renderResult = await render(
<QueryClientProvider client={queryClient}>
<ModalContextProvider>
<NoteItem note={mockNote} />
</ModalContextProvider>
</QueryClientProvider>,
);
return renderResult;
}
it("should render note item", async () => {
const renderResult = await renderComponent();
const { getByRole } = renderResult;
const deleteButtonLocator = getByRole("button", {
// 匹配 aria-label="delete-button"
name: "delete-button",
});
await expect.element(deleteButtonLocator).toBeInTheDocument();
});
});vi utility
vi.mock()vi.mocked()vi.waitFor()vi.importActual()- ...
tsx
import { getNotes } from "@/api/note";
import NoteList from "@/components/note-list";
import ModalContextProvider from "@/context/modal-context-provider";
import type { Note } from "@/schema";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render } from "vitest-browser-react";
// vi.mock 会被提升到文件的最顶层, 在所有 import 语句前执行
vi.mock("@/api/note", { spy: true });
describe("NoteList", () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// 请求失败时, 不重试
retry: false,
},
},
});
const mockedNotes: Note[] = Array.from({ length: 3 }).map((_, index) => ({
id: index + 1,
title: `Note ${index + 1}`,
content: `Content for note ${index + 1}`,
}));
const renderComponent = async () => {
const renderResult = await render(
<QueryClientProvider client={queryClient}>
<ModalContextProvider>
<NoteList />
</ModalContextProvider>
</QueryClientProvider>,
);
return renderResult;
};
describe("static render test", () => {
it("should render note list while request ok", async () => {
// Arrange
// mock 接口返回的数据, 先 mock 数据, 后渲染
vi.mocked(getNotes).mockResolvedValue(mockedNotes);
const renderResult = await renderComponent();
const { getByRole } = renderResult;
// 等待 (虚拟滚动) 列表渲染完成
await vi.waitFor(() => {
const mainLocator = getByRole("main");
expect(mainLocator).toBeInTheDocument();
});
// Act
const noteItems = getByRole("listitem").elements();
// Assert
expect(getNotes).toHaveBeenCalled(); // { spy: true }
expect(getNotes).toHaveBeenCalledTimes(1);
expect(getNotes).toHaveResolvedWith(mockedNotes);
expect(noteItems).toHaveLength(mockedNotes.length);
});
it("should render note list skeleton while request pending", async () => {
// Arrange
// mock `getNotes` 的实现
vi.mocked(getNotes).mockImplementation(() => {
return new Promise(() => {});
});
const { getByRole } = await renderComponent();
// Act
// 匹配 role="progressbar"
// div 标签没有语义, 没有 Aria role
// 手动指定 <div role="progressbar" />
const skeletonLocator = getByRole("progressbar");
// Assert
await expect.element(skeletonLocator).toBeInTheDocument();
});
it("should render note list error while request error", async () => {
const errorMessage = "Mocked error message";
vi.mocked(getNotes).mockRejectedValue(new Error(errorMessage));
const { getByRole } = await renderComponent();
// Act
// 匹配 role="alert"
// div 标签没有语义, 没有 Aria role
// 手动指定 <div role="alert" />
const errorAlertLocator = getByRole("alert");
// Assert
await expect.element(errorAlertLocator).toBeInTheDocument();
await expect.element(errorAlertLocator).toHaveTextContent(errorMessage);
});
});
});tsx
import EditModal from "@/components/edit-modal";
import ModalContext from "@/context/modal-context";
import type { Note } from "@/schema";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useAtomValue } from "jotai";
import { type PropsWithChildren, useState } from "react";
import { Toaster } from "sonner";
import { render } from "vitest-browser-react";
import { userEvent } from "vitest/browser";
import worker from "../mocks/worker";
const ModalContextProvider = ({ children }: NonNullable<PropsWithChildren>) => {
const [isOpen, setIsOpen] = useState(true);
return (
<ModalContext.Provider
value={{
isOpen,
openModal: () => setIsOpen(true),
closeModal: () => setIsOpen(false),
}}
>
{children}
</ModalContext.Provider>
);
};
const WrappedProvider = ({ children }: NonNullable<PropsWithChildren>) => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// 请求失败时, 不重试
retry: false,
},
},
});
return (
<QueryClientProvider client={queryClient}>
<ModalContextProvider>{children}</ModalContextProvider>
</QueryClientProvider>
);
};
describe("EditModal", () => {
beforeAll(() => worker.start());
afterEach(() => worker.resetHandlers());
afterAll(() => worker.stop());
const renderComponent = async () => {
return render(
<>
<EditModal />
<Toaster />
</>,
{ wrapper: WrappedProvider },
);
};
vi.mock("jotai", async () => {
const originalModule = await vi.importActual("jotai");
return { ...originalModule, useAtomValue: vi.fn() };
});
const mockedNote4add: Note = {
id: NaN,
title: "Mocked Title",
content: "Mocked Content",
};
describe("render test", () => {
it("should render the add note modal with default values", async () => {
vi.mocked(useAtomValue).mockReturnValue(mockedNote4add);
const { getByLabelText } = await renderComponent();
const titleLocator = getByLabelText(/title/i);
const contentLocator = getByLabelText(/content/i);
await expect.element(titleLocator).toHaveValue(mockedNote4add.title);
await expect.element(contentLocator).toHaveValue(mockedNote4add.content);
});
it("should render the toast after user click the add button", async () => {
vi.mocked(useAtomValue).mockReturnValue(mockedNote4add);
const { getByRole, getByText } = await renderComponent();
const addButtonLocator = getByRole("button", { name: /add/i });
await expect.element(addButtonLocator).toBeInTheDocument();
await userEvent.click(addButtonLocator);
const toastLocator = getByText(/created/i);
await expect.element(toastLocator).toBeInTheDocument();
});
});
});MSW
配置
ts
// 保留样式
import "@/index.css";
import { http, HttpResponse } from "msw";
// For node
import { setupServer } from "msw/node";
// For browser
import { setupWorker } from "msw/browser";
const API_URL = import.meta.env.VITE_API_URL;
const handlers = [
// Initial request handlers
http.get(API_URL, () => {
// 必须 return
return HttpResponse.json(
Array.from({ length: 2027 }).map((_, index) => ({
id: index + 1,
title: `Note ${index + 1}`,
content: `Content for note ${index + 1}`,
})),
); // Mocked data
}),
];
// For node
const server = setupServer(...handlers);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
// For browser
const worker = setupWorker(...handlers);
beforeAll(() => worker.start());
afterEach(() => worker.resetHandlers());
afterAll(() => worker.stop());ts
export default defineConfig({
globals: true,
// 浏览器模式
browser: {
enabled: true,
provider: playwright(),
// https://vitest.dev/config/browser/playwright
instances: [{ browser: "chromium" }],
},
setupFiles: ["./tests/vitest.setup.ts"],
});使用 MSW mock 接口
tsx
import NoteList from "@/components/note-list";
import ModalContextProvider from "@/context/modal-context-provider";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render } from "vitest-browser-react";
import worker from "../mocks/worker";
import { delay, http, HttpResponse } from "msw";
describe("NoteList by MSW", () => {
beforeAll(() => worker.start());
afterEach(() => worker.resetHandlers());
afterAll(() => worker.stop());
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// 请求失败时, 不重试
retry: false,
},
},
});
const renderComponent = async () => {
const renderResult = await render(
<QueryClientProvider client={queryClient}>
<ModalContextProvider>
<NoteList />
</ModalContextProvider>
</QueryClientProvider>,
);
return renderResult;
};
describe("static render test", () => {
it("should render note list while request ok", async () => {
// Arrange
const renderResult = await renderComponent();
const { getByRole } = renderResult;
// 等待 (虚拟滚动) 列表渲染完成
await vi.waitFor(() => {
const mainLocator = getByRole("main");
expect(mainLocator).toBeInTheDocument();
});
// Act
const noteItems = getByRole("listitem").elements();
// Assert
expect(noteItems.length).toBeGreaterThan(0);
});
it("should render note list skeleton while request pending", async () => {
worker.use(
// Runtime request handlers (override initial request handlers)
http.get(import.meta.env.VITE_API_URL, async () => {
await delay();
return HttpResponse.json([]);
}),
);
const { getByRole } = await renderComponent();
// Act
// 匹配 role="progressbar"
const skeletonLocator = getByRole("progressbar");
// Assert
await expect.element(skeletonLocator).toBeInTheDocument();
});
it("should render note list error while request error", async () => {
worker.use(
// Runtime request handlers (override initial request handlers)
http.get(import.meta.env.VITE_API_URL, async () => {
return HttpResponse.error();
}),
);
const { getByRole } = await renderComponent();
// Act
// 匹配 role="alert"
const errorAlertLocator = getByRole("alert");
// Assert
await expect.element(errorAlertLocator).toBeInTheDocument();
});
});
});使用 @mswjs/data mock CRUD
bash
# 使用 @mswjs/data mock CRUD
# 使用 faker-js mock 数据
pnpm add @mswjs/data @faker-js/faker -Dts
import { checkNote, noteSchema } from "@/schema";
import { Collection } from "@msw/data";
import { http, HttpResponse } from "msw";
import { faker } from "@faker-js/faker";
const API_URL = import.meta.env.VITE_API_URL;
// pnpm add @mswjs/data @faker-js/faker -D
const notes = new Collection({
schema: noteSchema, // zod schema
});
const createManyNotes = notes.createMany(10, (index) => ({
id: index + 1,
title: faker.book.title(),
content: faker.lorem.lines(2),
}));
export default [
// Initial request handlers
http.get(API_URL, async () => {
await createManyNotes;
// 必须 return
return HttpResponse.json(notes.all());
}),
http.delete<{ id: string /** 必须是 string, 不能是 number */ }>(
`${API_URL}/:id`,
async ({ params }) => {
const { id } = params;
const deletedNote = notes.delete((query) =>
query.where({ id: Number.parseInt(id) }),
);
return HttpResponse.json(deletedNote);
},
),
http.post(API_URL, async ({ request }) => {
const newPost = await request.clone().json();
const [newNote, ok] = checkNote(newPost);
if (!ok) {
return HttpResponse.json(
{ error: new Error("Bad request") },
{ status: 400 },
);
}
const createNote = await notes.create(newNote);
return HttpResponse.json(createNote);
}),
];