diff --git a/catalog/ui/jest.config.js b/catalog/ui/jest.config.js index 7c5217b6c..0bd0ab6e7 100644 --- a/catalog/ui/jest.config.js +++ b/catalog/ui/jest.config.js @@ -11,7 +11,6 @@ module.exports = { } }, verbose: true, - // Automatically clear mock calls and instances between every test clearMocks: true, diff --git a/catalog/ui/package.json b/catalog/ui/package.json index 5f099b9bc..6edd343af 100644 --- a/catalog/ui/package.json +++ b/catalog/ui/package.json @@ -88,6 +88,8 @@ "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^12.1.0", "@reduxjs/toolkit": "^1.6.1", + "@testing-library/jest-dom": "^5.14.1", + "@testing-library/react": "^12.1.0", "@types/react-redux": "^7.1.18", "asciidoctor": "^2.2.5", "classnames": "^2.3.1", diff --git a/catalog/ui/src/app/Catalog/Catalog.spec.tsx b/catalog/ui/src/app/Catalog/Catalog.spec.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/catalog/ui/src/app/Catalog/Catalog.tsx b/catalog/ui/src/app/Catalog/Catalog.tsx index b0916bd35..2b7ee935d 100644 --- a/catalog/ui/src/app/Catalog/Catalog.tsx +++ b/catalog/ui/src/app/Catalog/Catalog.tsx @@ -129,6 +129,16 @@ const Catalog: React.FunctionComponent = ({ } } + function creationTimestamp(catalogItem: { status: { creationTimestamp: any; }; metadata: { creationTimestamp: any; }; }) { + const ts = catalogItem.status && catalogItem.status.creationTimestamp ? catalogItem.status.creationTimestamp : catalogItem.metadata.creationTimestamp; + return (); + } + + function updateTimestamp(catalogItem: { status: { updateTimestamp: any; }; metadata: { creationTimestamp: any; }; }) { + const ts = catalogItem.status && catalogItem.status.updateTimestamp ? catalogItem.status.updateTimestamp : catalogItem.metadata.creationTimestamp; + return (); + } + function description(catalogItem, options={}): string { if (catalogItem.metadata?.annotations?.['babylon.gpte.redhat.com/description']) { options['format'] = catalogItem.metadata.annotations?.['babylon.gpte.redhat.com/descriptionFormat'] || 'asciidoc'; @@ -161,6 +171,7 @@ const Catalog: React.FunctionComponent = ({ catalogItem: selectedCatalogItem, catalogNamespace: catalogItemNamespace, }); + console.log("createServiceRequest", resourceClaim);; history.push(`/services/ns/${resourceClaim.metadata.namespace}/item/${resourceClaim.metadata.name}`); } } diff --git a/catalog/ui/src/app/Services/DeleteButton.spec.tsx b/catalog/ui/src/app/Services/DeleteButton.spec.tsx new file mode 100644 index 000000000..76eb00764 --- /dev/null +++ b/catalog/ui/src/app/Services/DeleteButton.spec.tsx @@ -0,0 +1,27 @@ +// jest.mock('../api'); +import "@testing-library/jest-dom"; + +import React from "react"; +import { render, waitFor, queryByAttribute, fireEvent, screen, cleanup } from "@testing-library/react"; +import { Provider } from 'react-redux'; +import user from "@testing-library/user-event" + +import {DeleteButton} from "./DeleteButton" +import { BrowserRouter as Router } from 'react-router-dom'; +// import { getApiSession, listClusterCustomObject } from "@app/api"; +import { store } from '@app/store'; + +const getById = queryByAttribute.bind(null, 'id'); + +// test.afterEach(cleanup) + +describe("DeleteButton", () => { + test("When DeleteButton layout renders, should display Delete Button", async () => { + const onClick = jest.fn(); + const { getByText, debug } = + render(); + + const testVar = getByText("Delete"); + await waitFor(() => expect(testVar).toBeInTheDocument()); + }); +}) \ No newline at end of file diff --git a/catalog/ui/src/app/Services/Item/DeleteModal/ServicesItemDeleteModal.spec.tsx b/catalog/ui/src/app/Services/Item/DeleteModal/ServicesItemDeleteModal.spec.tsx new file mode 100644 index 000000000..be0640c3e --- /dev/null +++ b/catalog/ui/src/app/Services/Item/DeleteModal/ServicesItemDeleteModal.spec.tsx @@ -0,0 +1,94 @@ +// jest.mock('../api'); +import "@testing-library/jest-dom"; + +import React from "react"; +import { render, waitFor, queryByAttribute, fireEvent, screen, cleanup } from "@testing-library/react"; +import { Provider } from 'react-redux'; +// import user from "@testing-library/user-event"; + +import {ServicesItemDeleteModal} from "./ServicesItemDeleteModal" +import { BrowserRouter as Router } from 'react-router-dom'; +// import { getApiSession, listClusterCustomObject } from "@app/api"; +import { store } from '@app/store'; + +const getById = queryByAttribute.bind(null, 'id'); + +// test.afterEach(cleanup) + +describe("ServicesItemDeleteModal", () => { + test("When ServicesItemDeleteModal layout renders, should display 'Confirm' option", async () => { + const closeModal = jest.fn(); + const handleDelete = jest.fn(); + const { getByText, debug } = + render( + ); + + const testVar = getByText("Confirm"); + await waitFor(() => expect(testVar).toBeInTheDocument()); + }); + + test("When ServicesItemDeleteModal layout renders, should display 'Cancle' option", async () => { + const closeModal = jest.fn(); + const handleDelete = jest.fn(); + const { getByText, debug } = + render( + ); + + const testVar = getByText("Cancel"); + await waitFor(() => expect(testVar).toBeInTheDocument()); + }); + + test("When ServicesItemDeleteModal layout renders, should display 'Delete ServiceName?' option", async () => { + const closeModal = jest.fn(); + const handleDelete = jest.fn(); + const catalogItemDisplayName = "Service"; + const { getByText, debug } = + render( + ); + + const testVar = getByText(`Delete ${catalogItemDisplayName}?`); + await waitFor(() => expect(testVar).toBeInTheDocument()); + }); + + test("When ServicesItemDeleteModal layout renders, should Confirm button click once", async () => { + const closeModal = jest.fn(); + const handleDelete = jest.fn(); + + const { getByText, debug } = render( + ); + const button = screen.getByText("Confirm"); + fireEvent.click(button); + await waitFor(() => expect(handleDelete).toBeCalledTimes(1)); + }); + + test("When ServicesItemDeleteModal layout renders, should Cancle button click once", async () => { + const closeModal = jest.fn(); + const handleDelete = jest.fn(); + + const { getByText, debug } = render( + ); + const button = screen.getByText("Cancel"); + fireEvent.click(button); + await waitFor(() => expect(closeModal).toBeCalledTimes(1)); + }); +}) \ No newline at end of file diff --git a/catalog/ui/src/app/Services/Item/OpenStackConsole.spec.tsx b/catalog/ui/src/app/Services/Item/OpenStackConsole.spec.tsx new file mode 100644 index 000000000..473578ba5 --- /dev/null +++ b/catalog/ui/src/app/Services/Item/OpenStackConsole.spec.tsx @@ -0,0 +1,46 @@ +// jest.mock('../api'); +import "@testing-library/jest-dom"; + +import React from "react"; +import { render, waitFor, queryByAttribute, fireEvent, screen, cleanup } from "@testing-library/react"; +import { Provider } from 'react-redux'; +import user from "@testing-library/user-event" + +import {OpenStackConsole} from "./OpenStackConsole" +import { BrowserRouter as Router } from 'react-router-dom'; +// import { getApiSession, listClusterCustomObject } from "@app/api"; +import { store } from '@app/store'; + +const getById = queryByAttribute.bind(null, 'id'); + +// test.afterEach(cleanup) + +describe("OpenStackConsole", () => { + test("When OpenStackConsole layout renders, should display 'start' option", async () => { + const { getByText, debug } = + render( + ); + const testVar = getByText("Start"); + await waitFor(() => expect(testVar).toBeInTheDocument()); + }); + + test("When OpenStackConsole layout renders, should display 'Reconnect' option", async () => { + const { getByText, debug } = + render( + ); + const testVar = getByText("Reconnect"); + await waitFor(() => expect(testVar).toBeInTheDocument()); + }); + + test("When OpenStackConsole layout renders, should display 'Reboot' option", async () => { + const { getByText, debug } = + render( + ); + const testVar = getByText("Reboot"); + await waitFor(() => expect(testVar).toBeInTheDocument()); + }); + +}) \ No newline at end of file diff --git a/catalog/ui/src/app/Services/Item/StartModal/ServicesItemStartModal.spec.tsx b/catalog/ui/src/app/Services/Item/StartModal/ServicesItemStartModal.spec.tsx new file mode 100644 index 000000000..5d99cb2bd --- /dev/null +++ b/catalog/ui/src/app/Services/Item/StartModal/ServicesItemStartModal.spec.tsx @@ -0,0 +1,113 @@ +// jest.mock('../api'); +import "@testing-library/jest-dom"; + +import React from "react"; +import { render, waitFor, queryByAttribute, fireEvent, screen, cleanup } from "@testing-library/react"; +import { Provider } from 'react-redux'; +import user from "@testing-library/user-event" + +import {ServicesItemStartModal} from "./ServicesItemStartModal" +import { BrowserRouter as Router } from 'react-router-dom'; +// import { getApiSession, listClusterCustomObject } from "@app/api"; +import { store } from '@app/store'; + +const getById = queryByAttribute.bind(null, 'id'); + +// test.afterEach(cleanup) + +describe("ServicesItemStartModal", () => { + test("When ServicesItemStartModal layout renders, should display 'Confirm' option", async () => { + const closeModal = jest.fn(); + const handleStart = jest.fn(); + const { getByText, debug } = + render( + ); + + const testVar = getByText("Confirm"); + await waitFor(() => expect(testVar).toBeInTheDocument()); + }); + + test("When ServicesItemStartModal layout renders, should display 'Cancle' option", async () => { + const closeModal = jest.fn(); + const handleStart = jest.fn(); + const { getByText, debug } = + render( + ); + + const testVar = getByText("Cancel"); + await waitFor(() => expect(testVar).toBeInTheDocument()); + }); + + test("When ServicesItemStartModal layout renders, should display 'Start Service?' option", async () => { + const closeModal = jest.fn(); + const handleStart = jest.fn(); + const catalogItemDisplayName = "Service"; + const { getByText, debug } = + render( + ); + + const testVar = getByText(`Start ${catalogItemDisplayName}?`); + await waitFor(() => expect(testVar).toBeInTheDocument()); + }); + + test("When ServicesItemStartModal layout renders, should Confirm button click once", async () => { + const closeModal = jest.fn(); + const handleStart = jest.fn(); + + const { getByText, debug } = render( + ); + const button = screen.getByText("Confirm"); + fireEvent.click(button); + await waitFor(() => expect(handleStart).toBeCalledTimes(1)); + }); + + test("When ServicesItemStartModal layout renders, should Cancle button click once", async () => { + const closeModal = jest.fn(); + const handleStart = jest.fn(); + + const { getByText, debug } = render( + ); + const button = screen.getByText("Cancel"); + fireEvent.click(button); + await waitFor(() => expect(closeModal).toBeCalledTimes(1)); + }); + + + // test.only("When ServicesItemStartModal layout renders, should display selected service will stop in ", async () => { + // const closeModal = jest.fn(); + // const handleStart = jest.fn(); + + // const { getByText, debug } = render( + // service will stop in + // {/* . */} + // + // ); + + // const testVar = getByText(`service will stop in`); + // await waitFor(() => expect(testVar).toBeInTheDocument()); + // }); +}) \ No newline at end of file diff --git a/catalog/ui/src/app/Services/Item/StopModal/ServicesItemStopModal.spec.tsx b/catalog/ui/src/app/Services/Item/StopModal/ServicesItemStopModal.spec.tsx new file mode 100644 index 000000000..7d19da69b --- /dev/null +++ b/catalog/ui/src/app/Services/Item/StopModal/ServicesItemStopModal.spec.tsx @@ -0,0 +1,96 @@ +// jest.mock('../api'); +import "@testing-library/jest-dom"; + +import React from "react"; +import { render, waitFor, queryByAttribute, fireEvent, screen, cleanup } from "@testing-library/react"; +import { Provider } from 'react-redux'; +import user from "@testing-library/user-event" + +import {ServicesItemStopModal} from "./ServicesItemStopModal" +import { BrowserRouter as Router } from 'react-router-dom'; +// import { getApiSession, listClusterCustomObject } from "@app/api"; +import { store } from '@app/store'; + +const getById = queryByAttribute.bind(null, 'id'); + +// test.afterEach(cleanup) + +describe("ServicesItemStopModal", () => { + test("When ServicesItemStopModal layout renders, should display 'Confirm' option", async () => { + const closeModal = jest.fn(); + const handleStop = jest.fn(); + const { getByText, debug } = + render( + ); + + const testVar = getByText("Confirm"); + await waitFor(() => expect(testVar).toBeInTheDocument()); + }); + + test("When ServicesItemStopModal layout renders, should display 'Cancle' option", async () => { + const closeModal = jest.fn(); + const handleStop = jest.fn(); + const { getByText, debug } = + render( + ); + + const testVar = getByText("Cancel"); + await waitFor(() => expect(testVar).toBeInTheDocument()); + }); + + test("When ServicesItemStopModal layout renders, should display 'stop Service?' option", async () => { + const closeModal = jest.fn(); + const handleStop = jest.fn(); + const catalogItemDisplayName = "Service"; + const { getByText, debug } = + render( + ); + + const testVar = getByText(`Stop ${catalogItemDisplayName}?`); + await waitFor(() => expect(testVar).toBeInTheDocument()); + console.log(debug); + }); + + test("When ServicesItemStopModal layout renders, should Confirm button click once", async () => { + const closeModal = jest.fn(); + const handleStop = jest.fn(); + + const { getByText, debug } = render( + ); + const button = screen.getByText("Confirm"); + fireEvent.click(button); + await waitFor(() => expect(handleStop).toBeCalledTimes(1)); + }); + + test("When ServicesItemStopModal layout renders, should Cancle button click once", async () => { + const closeModal = jest.fn(); + const handleStop = jest.fn(); + + const { getByText, debug } = render( + ); + const button = screen.getByText("Cancel"); + fireEvent.click(button); + await waitFor(() => expect(closeModal).toBeCalledTimes(1)); + }); + +}) \ No newline at end of file diff --git a/catalog/ui/src/app/Services/NamespaceSelector/ServicesNamespaceSelector.spec.tsx b/catalog/ui/src/app/Services/NamespaceSelector/ServicesNamespaceSelector.spec.tsx new file mode 100644 index 000000000..ba44b1547 --- /dev/null +++ b/catalog/ui/src/app/Services/NamespaceSelector/ServicesNamespaceSelector.spec.tsx @@ -0,0 +1,32 @@ +// jest.mock('../api'); +import "@testing-library/jest-dom"; + +import React from "react"; +import { render, waitFor, queryByAttribute, fireEvent, screen, cleanup } from "@testing-library/react"; +import { Provider } from 'react-redux'; +import user from "@testing-library/user-event" + +import { ServicesNamespaceSelector } from "./ServicesNamespaceSelector" +import { BrowserRouter as Router } from 'react-router-dom'; +// import { getApiSession, listClusterCustomObject } from "@app/api"; +import { store } from '@app/store'; + +const getById = queryByAttribute.bind(null, 'id'); + +// test.afterEach(cleanup) + +describe("ServicesNamespaceSelector", () => { + test("When ServicesNamespaceSelector layout renders, should display all projects ", async () => { + const ns = ""; + const { getByText, debug } = + render( { }} + /> + ); + + const testVar = getByText("Project: all projects"); + await waitFor(() => expect(testVar).toBeInTheDocument()); + }); +}) \ No newline at end of file diff --git a/catalog/ui/src/app/Services/ServiceActions.spec.tsx b/catalog/ui/src/app/Services/ServiceActions.spec.tsx new file mode 100644 index 000000000..5b8de10fc --- /dev/null +++ b/catalog/ui/src/app/Services/ServiceActions.spec.tsx @@ -0,0 +1,156 @@ +// jest.mock('../api'); +import "@testing-library/jest-dom"; + +import React from "react"; +import { render, waitFor, queryByAttribute, fireEvent, screen, cleanup } from "@testing-library/react"; +import { Provider } from 'react-redux'; +import user from "@testing-library/user-event" + +import {ServiceActions} from "./ServiceActions" +import { BrowserRouter as Router } from 'react-router-dom'; +// import { getApiSession, listClusterCustomObject } from "@app/api"; +import { store } from '@app/store'; + +const getById = queryByAttribute.bind(null, 'id'); + +// test.afterEach(cleanup) + +describe("ServiceActions", () => { + test("When ServiceActions layout renders, should display ServiceActions", async () => { + const openDeleteModal = jest.fn(); + const openScheduleActionModal = jest.fn(); + const openStartModal = jest.fn(); + const openStopModal = jest.fn(); + + const { getByText, debug } = + render( openDeleteModal("resourceClaim"), + lifespan: () => openScheduleActionModal("resourceClaim", 'retirement'), + runtime: () => openScheduleActionModal("resourceClaim", 'stop'), + start: () => openStartModal("resourceClaim", 'start'), + stop: () => openStopModal("resourceClaim", 'stop'), + }} + /> + ); + const testVar = getByText("Actions"); + await waitFor(() => expect(testVar).toBeInTheDocument()); + }); + + test("When ServiceActions layout renders, should display Options Delete", async () => { + const openDeleteModal = jest.fn(); + const openScheduleActionModal = jest.fn(); + const openStartModal = jest.fn(); + const openStopModal = jest.fn(); + + const { getByText, debug } = render( openDeleteModal("resourceClaim"), + lifespan: () => openScheduleActionModal("resourceClaim", 'retirement'), + runtime: () => openScheduleActionModal("resourceClaim", 'stop'), + start: () => openStartModal("resourceClaim", 'start'), + stop: () => openStopModal("resourceClaim", 'stop'), + }} + /> + ); + const button = screen.getByText("Actions"); + fireEvent.click(button); + await waitFor(() => expect(getByText("Delete")).toBeInTheDocument()); + }); + + test("When ServiceActions layout renders, should display Stop", async () => { + const openDeleteModal = jest.fn(); + const openScheduleActionModal = jest.fn(); + const openStartModal = jest.fn(); + const openStopModal = jest.fn(); + + const { getByText, debug } = render( openDeleteModal("resourceClaim"), + lifespan: () => openScheduleActionModal("resourceClaim", 'retirement'), + runtime: () => openScheduleActionModal("resourceClaim", 'stop'), + start: () => openStartModal("resourceClaim", 'start'), + stop: () => openStopModal("resourceClaim", 'stop'), + }} + /> + ); + const button = screen.getByText("Actions"); + fireEvent.click(button); + await waitFor(() => expect(getByText("Stop")).toBeInTheDocument()); + }); + + test("When ServiceActions layout renders, should display Start", async () => { + const openDeleteModal = jest.fn(); + const openScheduleActionModal = jest.fn(); + const openStartModal = jest.fn(); + const openStopModal = jest.fn(); + + const { getByText, debug } = render( openDeleteModal("resourceClaim"), + lifespan: () => openScheduleActionModal("resourceClaim", 'retirement'), + runtime: () => openScheduleActionModal("resourceClaim", 'stop'), + start: () => openStartModal("resourceClaim", 'start'), + stop: () => openStopModal("resourceClaim", 'stop'), + }} + /> + ); + const button = screen.getByText("Actions"); + fireEvent.click(button); + await waitFor(() => expect(getByText("Start")).toBeInTheDocument()); + }); + + test("When ServiceActions layout renders, should display Adjust Lifespan", async () => { + const openDeleteModal = jest.fn(); + const openScheduleActionModal = jest.fn(); + const openStartModal = jest.fn(); + const openStopModal = jest.fn(); + + const { getByText, debug } = render( openDeleteModal("resourceClaim"), + lifespan: () => openScheduleActionModal("resourceClaim", 'retirement'), + runtime: () => openScheduleActionModal("resourceClaim", 'stop'), + start: () => openStartModal("resourceClaim", 'start'), + stop: () => openStopModal("resourceClaim", 'stop'), + }} + /> + ); + const button = screen.getByText("Actions"); + fireEvent.click(button); + await waitFor(() => expect(getByText("Adjust Lifespan")).toBeInTheDocument()); + }); + + test("When ServiceActions layout renders, should display Adjust Runtime", async () => { + const openDeleteModal = jest.fn(); + const openScheduleActionModal = jest.fn(); + const openStartModal = jest.fn(); + const openStopModal = jest.fn(); + + const { getByText, debug } = render( openDeleteModal("resourceClaim"), + lifespan: () => openScheduleActionModal("resourceClaim", 'retirement'), + runtime: () => openScheduleActionModal("resourceClaim", 'stop'), + start: () => openStartModal("resourceClaim", 'start'), + stop: () => openStopModal("resourceClaim", 'stop'), + }} + /> + ); + const button = screen.getByText("Actions"); + fireEvent.click(button); + await waitFor(() => expect(getByText("Adjust Runtime")).toBeInTheDocument()); + }); +}) diff --git a/catalog/ui/src/app/Services/ServiceStatus.spec.tsx b/catalog/ui/src/app/Services/ServiceStatus.spec.tsx new file mode 100644 index 000000000..ecbb9a10d --- /dev/null +++ b/catalog/ui/src/app/Services/ServiceStatus.spec.tsx @@ -0,0 +1,32 @@ +// jest.mock('../api'); +import "@testing-library/jest-dom"; + +import React from "react"; +import { render, waitFor, queryByAttribute, fireEvent, screen, cleanup } from "@testing-library/react"; +import { Provider } from 'react-redux'; +import user from "@testing-library/user-event" + +import { ServiceStatus } from "./ServiceStatus" +import { BrowserRouter as Router } from 'react-router-dom'; +// import { getApiSession, listClusterCustomObject } from "@app/api"; +import { store } from '@app/store'; + +const getById = queryByAttribute.bind(null, 'id'); + +// test.afterEach(cleanup) + +describe("ServiceStatus", () => { + test("When ServiceStatus layout renders, should display ServiceStatus", async () => { + + const { getByText, debug } = + render( + ); + console.log(debug); + const testVar = getByText("Available"); + await waitFor(() => expect(testVar).toBeInTheDocument()); + }); +}) \ No newline at end of file diff --git a/catalog/ui/src/app/components/TermsOfService.tsx b/catalog/ui/src/app/components/TermsOfService.tsx index b8a59c7dd..399156340 100644 --- a/catalog/ui/src/app/components/TermsOfService.tsx +++ b/catalog/ui/src/app/components/TermsOfService.tsx @@ -12,7 +12,7 @@ import { export interface TermsOfServiceProps { agreed: boolean; onChange?: any; - text?: string; + text: string; } const TermsOfService: React.FunctionComponent = ({ diff --git a/catalog/ui/src/app/components/TimeInterval.tsx b/catalog/ui/src/app/components/TimeInterval.tsx index a2e252ac7..f1be63046 100644 --- a/catalog/ui/src/app/components/TimeInterval.tsx +++ b/catalog/ui/src/app/components/TimeInterval.tsx @@ -1,3 +1,4 @@ +import { number } from 'prop-types'; import * as React from 'react'; const parseDuration = require('parse-duration'); @@ -14,7 +15,7 @@ const TimeInterval: React.FunctionComponent = ({ to, }) => { const seconds = ( - to ? (("string" === typeof to ? Date.parse(to) : to) - Date.now()) / 1000 : + to ? (Number("string" === typeof to ? Date.parse(to) : to) - Date.now()) / 1000 : typeof(interval) === 'number' ? interval : parseDuration(interval) / 1000 ); diff --git a/catalog/ui/tsconfig.json b/catalog/ui/tsconfig.json index e7ce05b3c..bf5d6f41a 100644 --- a/catalog/ui/tsconfig.json +++ b/catalog/ui/tsconfig.json @@ -5,13 +5,14 @@ "outDir": "dist", "module": "esnext", "target": "es5", - "lib": ["es6", "dom"], + "lib": ["es2021", "dom"], "sourceMap": true, "jsx": "react", "moduleResolution": "node", "forceConsistentCasingInFileNames": true, "noImplicitReturns": true, "noImplicitThis": true, + "strictNullChecks": false, "noImplicitAny": false, "allowJs": true, "strictNullChecks": false, diff --git a/catalog/ui/webpack.common.js b/catalog/ui/webpack.common.js index 496c3525c..3ac36996f 100644 --- a/catalog/ui/webpack.common.js +++ b/catalog/ui/webpack.common.js @@ -19,7 +19,7 @@ module.exports = env => { { loader: 'ts-loader', options: { - transpileOnly: true, + transpileOnly: false, experimentalWatchApi: true, } }