sts-sponsors team mailing list archive
-
sts-sponsors team
-
Mailing list archive
-
Message #06724
[Merge] ~petermakowski/maas-site-manager:add-test-coverage-ci into maas-site-manager:main
Peter Makowski has proposed merging ~petermakowski/maas-site-manager:add-test-coverage-ci into maas-site-manager:main.
Commit message:
test: enforce minumum test coverage
- set Document title for each page
Requested reviews:
MAAS Lander (maas-lander): unittests
MAAS Committers (maas-committers)
For more details, see:
https://code.launchpad.net/~petermakowski/maas-site-manager/+git/site-manager/+merge/440316
enforce minimum 80% test coverage
--
Your team MAAS Committers is requested to review the proposed merge of ~petermakowski/maas-site-manager:add-test-coverage-ci into maas-site-manager:main.
diff --git a/Makefile b/Makefile
index 8976954..e73f7f8 100644
--- a/Makefile
+++ b/Makefile
@@ -78,7 +78,7 @@ ci-frontend-lint:
.PHONY: ci-frontend-lint
ci-frontend-test:
- env -C frontend VITEST_JUNIT_SUITE_NAME='maas-site-manager frontend tests' yarnpkg run test --silent --reporter=junit --reporter=default --outputFile.junit=../junit-frontend.xml run
+ env -C frontend VITEST_JUNIT_SUITE_NAME='maas-site-manager frontend tests' yarnpkg run coverage --silent --reporter=junit --reporter=default --outputFile.junit=../junit-frontend.xml run
.PHONY: ci-test
ci-e2e-test:
diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx
new file mode 100644
index 0000000..7c50b95
--- /dev/null
+++ b/frontend/src/App.test.tsx
@@ -0,0 +1,14 @@
+/* eslint-disable testing-library/no-container */
+import App from "./App";
+
+import { waitFor, render, within } from "@/test-utils";
+
+it("renders vanilla layout components correctly", async () => {
+ const { container } = render(<App />);
+ await waitFor(() => expect(container.querySelector(".l-application")).toBeInTheDocument());
+ const application = container.querySelector(".l-application") as HTMLElement;
+ expect(application.querySelector(".l-navigation-bar")).toBeInTheDocument();
+ expect(application.querySelector(".l-main")).toBeInTheDocument();
+ expect(application.querySelector(".l-navigation-bar")).toBeInTheDocument();
+ expect(within(container).getByRole("heading", { name: /MAAS Site Manager/i })).toBeInTheDocument();
+});
diff --git a/frontend/src/components/DocumentTitle/DocumentTitle.tsx b/frontend/src/components/DocumentTitle/DocumentTitle.tsx
new file mode 100644
index 0000000..f36302b
--- /dev/null
+++ b/frontend/src/components/DocumentTitle/DocumentTitle.tsx
@@ -0,0 +1,12 @@
+import { useEffect, Children } from "react";
+
+const DocumentTitle = ({ children }: { children: string | string[] }) => {
+ useEffect(() => {
+ const title = Children.toArray(children).join("");
+ document.title = title;
+ }, [children]);
+
+ return null;
+};
+
+export default DocumentTitle;
diff --git a/frontend/src/components/DocumentTitle/index.ts b/frontend/src/components/DocumentTitle/index.ts
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/frontend/src/components/DocumentTitle/index.ts
diff --git a/frontend/src/components/MainLayout/MainLayout.tsx b/frontend/src/components/MainLayout/MainLayout.tsx
index f3b4f34..b2ce99a 100644
--- a/frontend/src/components/MainLayout/MainLayout.tsx
+++ b/frontend/src/components/MainLayout/MainLayout.tsx
@@ -5,10 +5,13 @@ import { Col, Row, usePrevious } from "@canonical/react-components";
import classNames from "classnames";
import { Outlet, useLocation } from "react-router-dom";
+import DocumentTitle from "@/components/DocumentTitle/DocumentTitle";
import Navigation from "@/components/Navigation";
import RemoveRegions from "@/components/RemoveRegions";
import { useAppContext } from "@/context";
import TokensCreate from "@/pages/tokens/create";
+import type { RoutePath } from "@/routes";
+import { routesConfig } from "@/routes";
export const sidebarLabels: Record<"removeRegions" | "createToken", string> = {
removeRegions: "Remove regions",
@@ -29,6 +32,11 @@ const Aside = ({ children, isOpen, ...props }: PropsWithChildren<{ isOpen: boole
</aside>
);
+const getPageTitle = (pathname: RoutePath) => {
+ const title = Object.values(routesConfig).find(({ path }) => path === pathname)?.title;
+ return title ? `${title} | MAAS Site Manager` : "MAAS Site Manager";
+};
+
const MainLayout: React.FC = () => {
const { sidebar, setSidebar } = useAppContext();
const { pathname } = useLocation();
@@ -42,24 +50,27 @@ const MainLayout: React.FC = () => {
}, [pathname, previousPathname, setSidebar]);
return (
- <div className="l-application">
- <Navigation />
- <main className="l-main is-maas-site-manager">
- <div className="row">
- <div className="col-12">
- <h1 className="u-hide">MAAS Site Manager</h1>
- <Outlet />
+ <>
+ <DocumentTitle>{getPageTitle(pathname as RoutePath)}</DocumentTitle>
+ <div className="l-application">
+ <Navigation />
+ <main className="l-main is-maas-site-manager">
+ <div className="row">
+ <div className="col-12">
+ <h1 className="u-hide">MAAS Site Manager</h1>
+ <Outlet />
+ </div>
</div>
- </div>
- </main>
- <Aside aria-label={sidebar ? sidebarLabels[sidebar] : undefined} isOpen={!!sidebar}>
- {!!sidebar && sidebar === "createToken" ? (
- <TokensCreate />
- ) : !!sidebar && sidebar === "removeRegions" ? (
- <RemoveRegions />
- ) : null}
- </Aside>
- </div>
+ </main>
+ <Aside aria-label={sidebar ? sidebarLabels[sidebar] : undefined} isOpen={!!sidebar}>
+ {!!sidebar && sidebar === "createToken" ? (
+ <TokensCreate />
+ ) : !!sidebar && sidebar === "removeRegions" ? (
+ <RemoveRegions />
+ ) : null}
+ </Aside>
+ </div>
+ </>
);
};
diff --git a/frontend/src/components/base/TablePagination/TablePagination.test.tsx b/frontend/src/components/base/TablePagination/TablePagination.test.tsx
index e1c4a3d..c7c3344 100644
--- a/frontend/src/components/base/TablePagination/TablePagination.test.tsx
+++ b/frontend/src/components/base/TablePagination/TablePagination.test.tsx
@@ -1,6 +1,6 @@
import { vi } from "vitest";
-import TablePagination from "./TablePagination";
+import TablePagination from "./index";
import { render, screen, userEvent } from "@/test-utils";
diff --git a/frontend/src/main.test.tsx b/frontend/src/main.test.tsx
new file mode 100644
index 0000000..66973bf
--- /dev/null
+++ b/frontend/src/main.test.tsx
@@ -0,0 +1,14 @@
+/* eslint-disable testing-library/no-node-access */
+import { waitFor } from "@/test-utils";
+
+beforeAll(() => {
+ const rootElement = document.createElement("div");
+ rootElement.setAttribute("id", "root");
+ document.body.appendChild(rootElement);
+ import("./main");
+});
+
+it("renders the app in the root element", async () => {
+ const container = document.getElementById("root") as HTMLElement;
+ await waitFor(() => expect(container.querySelector(".l-application")).toBeInTheDocument());
+});
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index 4d3cd70..785c02c 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -5,6 +5,7 @@ import * as ReactDOM from "react-dom/client";
import App from "./App";
import { isDev } from "./constants";
+/* c8 ignore next 4 */
if (isDev) {
const { worker } = await import("./mocks/browser");
await worker.start();
diff --git a/frontend/src/mocks/resolvers.ts b/frontend/src/mocks/resolvers.ts
index 6410af8..f35c0ad 100644
--- a/frontend/src/mocks/resolvers.ts
+++ b/frontend/src/mocks/resolvers.ts
@@ -3,8 +3,8 @@ import type { RestRequest, restContext, ResponseResolver } from "msw";
import { siteFactory, tokenFactory, enrollmentRequestFactory } from "./factories";
+import type { GetSitesQueryParams, PostTokensData } from "@/api/handlers";
import urls from "@/api/urls";
-import type { GetSitesQueryParams, PostTokensData } from "api/handlers";
export const sitesList = siteFactory.buildList(155);
export const tokensList = tokenFactory.buildList(100);
@@ -96,3 +96,4 @@ export const postTokens = rest.post(urls.tokens, createMockTokensResolver());
export const getTokens = rest.get(urls.tokens, createMockGetTokensResolver());
export const getEnrollmentRequests = rest.get(urls.enrollmentRequests, createMockGetEnrollmentRequestsResolver());
export const patchEnrollmentRequests = rest.patch(urls.enrollmentRequests, createMockPostEnrollmentRequestsResolver());
+export const allResolvers = [getSites, postTokens, getTokens, getEnrollmentRequests, patchEnrollmentRequests];
diff --git a/frontend/src/routes.test.tsx b/frontend/src/routes.test.tsx
index 1f00735..d8dd90b 100644
--- a/frontend/src/routes.test.tsx
+++ b/frontend/src/routes.test.tsx
@@ -1,26 +1,23 @@
import { createMemoryRouter, RouterProvider } from "react-router-dom";
-import urls from "./api/urls";
-import { siteFactory } from "./mocks/factories";
-import { createMockSitesResolver } from "./mocks/resolvers";
-import { createMockGetServer } from "./mocks/server";
-import routes from "./routes";
-import { render, waitFor } from "./test-utils";
+import { allResolvers } from "./mocks/resolvers";
+import routes, { routesConfig } from "./routes";
-const sites = siteFactory.buildList(1);
-const mockServer = createMockGetServer(urls.sites, createMockSitesResolver(sites));
+import { render, waitFor, setupServer } from "@/test-utils";
-beforeAll(() => {
- mockServer.listen();
-});
-afterEach(() => {
- mockServer.resetHandlers();
-});
-afterAll(() => {
- mockServer.close();
-});
+const mockServer = setupServer(...allResolvers);
describe("router", () => {
+ beforeAll(() => {
+ mockServer.listen();
+ });
+ afterEach(() => {
+ mockServer.resetHandlers();
+ });
+ afterAll(() => {
+ mockServer.close();
+ });
+
it("redirects to the default route", async () => {
const router = createMemoryRouter(routes);
render(<RouterProvider router={router} />);
@@ -28,4 +25,12 @@ describe("router", () => {
expect(router.state.location.pathname).toEqual("/");
await waitFor(() => expect(router.state.location.pathname).toEqual("/sites"));
});
+
+ Object.values(routesConfig).forEach(({ title, path }) => {
+ it(`displays correct document title for ${title} page`, async () => {
+ const router = createMemoryRouter(routes, { initialEntries: [path], initialIndex: 0 });
+ render(<RouterProvider router={router} />);
+ expect(document.title).toBe(`${title} | MAAS Site Manager`);
+ });
+ });
});
diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx
index b9ebdc8..cabc2b2 100644
--- a/frontend/src/routes.tsx
+++ b/frontend/src/routes.tsx
@@ -5,6 +5,24 @@ import Requests from "@/pages/requests";
import SitesList from "@/pages/sites";
import Tokens from "@/pages/tokens/tokens";
+export const routesConfig = {
+ sites: {
+ path: "/sites",
+ title: "Regions",
+ },
+ requests: {
+ path: "/requests",
+ title: "Requests",
+ },
+ tokens: {
+ path: "/tokens",
+ title: "Tokens",
+ },
+} as const;
+
+type RoutesConfig = typeof routesConfig;
+export type RoutePath = RoutesConfig[keyof RoutesConfig]["path"];
+
export const routes = createRoutesFromElements(
<Route element={<MainLayout />} path="/">
{/*
diff --git a/frontend/src/test-utils.tsx b/frontend/src/test-utils.tsx
index f949896..e34ff1c 100644
--- a/frontend/src/test-utils.tsx
+++ b/frontend/src/test-utils.tsx
@@ -52,3 +52,4 @@ export { renderHook } from "@testing-library/react-hooks";
export { default as userEvent } from "@testing-library/user-event";
export { renderWithMemoryRouter };
export { Providers };
+export { setupServer } from "msw/node";
diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts
index 1d31aae..eed1bf5 100644
--- a/frontend/vitest.config.ts
+++ b/frontend/vitest.config.ts
@@ -1,4 +1,4 @@
-import { configDefaults, defineConfig } from "vitest/config";
+import { configDefaults, coverageConfigDefaults, defineConfig } from "vitest/config";
import * as path from "path";
export default defineConfig({
@@ -10,6 +10,18 @@ export default defineConfig({
environment: "jsdom",
setupFiles: ["./setupTests.ts"],
exclude: [...configDefaults.exclude, "**/tests/**"],
+ coverage: {
+ // exclude index files as they're only used to export other files
+ // exclude pages as they're covered by playwright tests
+ exclude: [...coverageConfigDefaults.exclude, "**/index.ts", "src/mocks/**/*", "src/pages/**/*", "**/types.ts"],
+ include: ["src/**/*.{ts,tsx}"],
+ reporter: ["text", "json", "html"],
+ all: true,
+ lines: 80,
+ functions: 80,
+ branches: 80,
+ statements: 80,
+ },
clearMocks: true,
},
});