mirror of
https://github.com/gethomepage/homepage.git
synced 2026-02-08 00:40:52 +08:00
test: add coverage for core pages + page assets
This commit is contained in:
37
src/pages/_app.test.jsx
Normal file
37
src/pages/_app.test.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
// Next's Head implementation relies on internal Next contexts; stub it for unit tests.
|
||||
vi.mock("next/head", () => ({
|
||||
default: ({ children }) => <>{children}</>,
|
||||
}));
|
||||
|
||||
vi.mock("utils/contexts/color", () => ({
|
||||
ColorProvider: ({ children }) => <>{children}</>,
|
||||
}));
|
||||
vi.mock("utils/contexts/theme", () => ({
|
||||
ThemeProvider: ({ children }) => <>{children}</>,
|
||||
}));
|
||||
vi.mock("utils/contexts/settings", () => ({
|
||||
SettingsProvider: ({ children }) => <>{children}</>,
|
||||
}));
|
||||
vi.mock("utils/contexts/tab", () => ({
|
||||
TabProvider: ({ children }) => <>{children}</>,
|
||||
}));
|
||||
|
||||
import App from "./_app.jsx";
|
||||
|
||||
describe("pages/_app", () => {
|
||||
it("renders the active page component with pageProps", () => {
|
||||
function Page({ message }) {
|
||||
return <div>msg:{message}</div>;
|
||||
}
|
||||
|
||||
render(<App Component={Page} pageProps={{ message: "hello" }} />);
|
||||
|
||||
expect(screen.getByText("msg:hello")).toBeInTheDocument();
|
||||
expect(document.querySelector('meta[name="viewport"]')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
24
src/pages/_document.test.jsx
Normal file
24
src/pages/_document.test.jsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("next/document", () => ({
|
||||
Html: ({ children }) => <div data-testid="html">{children}</div>,
|
||||
Head: ({ children }) => <div data-testid="head">{children}</div>,
|
||||
Main: () => <main data-testid="main" />,
|
||||
NextScript: () => <script data-testid="nextscript" />,
|
||||
}));
|
||||
|
||||
import Document from "./_document.jsx";
|
||||
|
||||
describe("pages/_document", () => {
|
||||
it("renders the PWA meta + custom css links", () => {
|
||||
const html = renderToStaticMarkup(<Document />);
|
||||
|
||||
expect(html).toContain('meta name="mobile-web-app-capable" content="yes"');
|
||||
expect(html).toContain('link rel="manifest" href="/site.webmanifest?v=4"');
|
||||
expect(html).toContain('link rel="preload" href="/api/config/custom.css" as="style"');
|
||||
expect(html).toContain('link rel="stylesheet" href="/api/config/custom.css"');
|
||||
expect(html).toContain('data-testid="main"');
|
||||
expect(html).toContain('data-testid="nextscript"');
|
||||
});
|
||||
});
|
||||
38
src/pages/browserconfig.xml.test.js
Normal file
38
src/pages/browserconfig.xml.test.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import themes from "utils/styles/themes";
|
||||
|
||||
const { getSettings } = vi.hoisted(() => ({
|
||||
getSettings: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("utils/config/config", () => ({
|
||||
getSettings,
|
||||
}));
|
||||
|
||||
import { getServerSideProps } from "./browserconfig.xml.jsx";
|
||||
|
||||
function createMockRes() {
|
||||
return {
|
||||
setHeader: vi.fn(),
|
||||
write: vi.fn(),
|
||||
end: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
describe("pages/browserconfig.xml", () => {
|
||||
it("writes a browserconfig xml response using the selected theme color", async () => {
|
||||
getSettings.mockReturnValueOnce({ color: "slate", theme: "dark" });
|
||||
const res = createMockRes();
|
||||
|
||||
await getServerSideProps({ res });
|
||||
|
||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "text/xml");
|
||||
expect(res.end).toHaveBeenCalled();
|
||||
|
||||
const xml = res.write.mock.calls[0][0];
|
||||
expect(xml).toContain('<?xml version="1.0" encoding="utf-8"?>');
|
||||
expect(xml).toContain('<square150x150logo src="/mstile-150x150.png?v=2"/>');
|
||||
expect(xml).toContain(`<TileColor>${themes.slate.dark}</TileColor>`);
|
||||
});
|
||||
});
|
||||
102
src/pages/index.test.js
Normal file
102
src/pages/index.test.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { state, getSettings, servicesResponse, bookmarksResponse, widgetsResponse, serverSideTranslations, logger } =
|
||||
vi.hoisted(() => {
|
||||
const state = {
|
||||
throwIn: null,
|
||||
};
|
||||
|
||||
const getSettings = vi.fn(() => ({
|
||||
providers: {},
|
||||
language: "en",
|
||||
title: "Homepage",
|
||||
}));
|
||||
|
||||
const servicesResponse = vi.fn(async () => {
|
||||
if (state.throwIn === "services") throw new Error("services failed");
|
||||
return [{ name: "svc" }];
|
||||
});
|
||||
const bookmarksResponse = vi.fn(async () => {
|
||||
if (state.throwIn === "bookmarks") throw new Error("bookmarks failed");
|
||||
return [{ name: "bm" }];
|
||||
});
|
||||
const widgetsResponse = vi.fn(async () => {
|
||||
if (state.throwIn === "widgets") throw new Error("widgets failed");
|
||||
return [{ type: "search" }];
|
||||
});
|
||||
|
||||
const serverSideTranslations = vi.fn(async (language) => ({ _translations: language }));
|
||||
const logger = { error: vi.fn() };
|
||||
|
||||
return { state, getSettings, servicesResponse, bookmarksResponse, widgetsResponse, serverSideTranslations, logger };
|
||||
});
|
||||
|
||||
vi.mock("next/dynamic", () => ({
|
||||
default: () => () => null,
|
||||
}));
|
||||
vi.mock("next/head", () => ({ default: ({ children }) => children }));
|
||||
vi.mock("next/script", () => ({ default: () => null }));
|
||||
vi.mock("next/router", () => ({ useRouter: () => ({ asPath: "/" }) }));
|
||||
|
||||
vi.mock("next-i18next/serverSideTranslations", () => ({
|
||||
serverSideTranslations,
|
||||
}));
|
||||
|
||||
vi.mock("utils/logger", () => ({
|
||||
default: () => logger,
|
||||
}));
|
||||
|
||||
vi.mock("utils/config/config", () => ({
|
||||
getSettings,
|
||||
}));
|
||||
|
||||
vi.mock("utils/config/api-response", () => ({
|
||||
servicesResponse,
|
||||
bookmarksResponse,
|
||||
widgetsResponse,
|
||||
}));
|
||||
|
||||
describe("pages/index getStaticProps", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
state.throwIn = null;
|
||||
});
|
||||
|
||||
it("returns initial settings and api fallbacks for swr", async () => {
|
||||
getSettings.mockReturnValueOnce({ providers: { x: 1 }, language: "en", title: "Homepage" });
|
||||
|
||||
const { getStaticProps } = await import("./index.jsx");
|
||||
const result = await getStaticProps();
|
||||
|
||||
expect(result.props.initialSettings).toEqual({ language: "en", title: "Homepage" });
|
||||
expect(result.props.fallback["/api/services"]).toEqual([{ name: "svc" }]);
|
||||
expect(result.props.fallback["/api/bookmarks"]).toEqual([{ name: "bm" }]);
|
||||
expect(result.props.fallback["/api/widgets"]).toEqual([{ type: "search" }]);
|
||||
expect(result.props.fallback["/api/hash"]).toBe(false);
|
||||
expect(serverSideTranslations).toHaveBeenCalledWith("en");
|
||||
});
|
||||
|
||||
it("normalizes legacy language codes before requesting translations", async () => {
|
||||
getSettings.mockReturnValueOnce({ providers: {}, language: "zh-CN" });
|
||||
|
||||
const { getStaticProps } = await import("./index.jsx");
|
||||
await getStaticProps();
|
||||
|
||||
expect(serverSideTranslations).toHaveBeenCalledWith("zh-Hans");
|
||||
});
|
||||
|
||||
it("falls back to empty settings and en translations on errors", async () => {
|
||||
getSettings.mockReturnValueOnce({ providers: {}, language: "de" });
|
||||
state.throwIn = "services";
|
||||
|
||||
const { getStaticProps } = await import("./index.jsx");
|
||||
const result = await getStaticProps();
|
||||
|
||||
expect(result.props.initialSettings).toEqual({});
|
||||
expect(result.props.fallback["/api/services"]).toEqual([]);
|
||||
expect(result.props.fallback["/api/bookmarks"]).toEqual([]);
|
||||
expect(result.props.fallback["/api/widgets"]).toEqual([]);
|
||||
expect(serverSideTranslations).toHaveBeenCalledWith("en");
|
||||
expect(logger.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
41
src/pages/robots.txt.test.js
Normal file
41
src/pages/robots.txt.test.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { getSettings } = vi.hoisted(() => ({
|
||||
getSettings: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("utils/config/config", () => ({
|
||||
getSettings,
|
||||
}));
|
||||
|
||||
import { getServerSideProps } from "./robots.txt.js";
|
||||
|
||||
function createMockRes() {
|
||||
return {
|
||||
setHeader: vi.fn(),
|
||||
write: vi.fn(),
|
||||
end: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
describe("pages/robots.txt", () => {
|
||||
it("allows indexing when disableIndexing is falsey", async () => {
|
||||
getSettings.mockReturnValueOnce({ disableIndexing: false });
|
||||
const res = createMockRes();
|
||||
|
||||
await getServerSideProps({ res });
|
||||
|
||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "text/plain");
|
||||
expect(res.write).toHaveBeenCalledWith("User-agent: *\nAllow: /");
|
||||
expect(res.end).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("disallows indexing when disableIndexing is truthy", async () => {
|
||||
getSettings.mockReturnValueOnce({ disableIndexing: true });
|
||||
const res = createMockRes();
|
||||
|
||||
await getServerSideProps({ res });
|
||||
|
||||
expect(res.write).toHaveBeenCalledWith("User-agent: *\nDisallow: /");
|
||||
});
|
||||
});
|
||||
55
src/pages/site.webmanifest.test.js
Normal file
55
src/pages/site.webmanifest.test.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import themes from "utils/styles/themes";
|
||||
|
||||
const { checkAndCopyConfig, getSettings } = vi.hoisted(() => ({
|
||||
checkAndCopyConfig: vi.fn(),
|
||||
getSettings: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("utils/config/config", () => ({
|
||||
default: checkAndCopyConfig,
|
||||
getSettings,
|
||||
}));
|
||||
|
||||
import { getServerSideProps } from "./site.webmanifest.jsx";
|
||||
|
||||
function createMockRes() {
|
||||
return {
|
||||
setHeader: vi.fn(),
|
||||
write: vi.fn(),
|
||||
end: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
describe("pages/site.webmanifest", () => {
|
||||
it("writes a manifest json response and triggers a settings config check", async () => {
|
||||
getSettings.mockReturnValueOnce({
|
||||
title: "My Homepage",
|
||||
startUrl: "/start",
|
||||
color: "slate",
|
||||
theme: "dark",
|
||||
pwa: {
|
||||
icons: [{ src: "/i.png", sizes: "1x1", type: "image/png" }],
|
||||
shortcuts: [{ name: "One", url: "/one" }],
|
||||
},
|
||||
});
|
||||
|
||||
const res = createMockRes();
|
||||
|
||||
await getServerSideProps({ res });
|
||||
|
||||
expect(checkAndCopyConfig).toHaveBeenCalledWith("settings.yaml");
|
||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "application/manifest+json");
|
||||
expect(res.end).toHaveBeenCalled();
|
||||
|
||||
const manifest = JSON.parse(res.write.mock.calls[0][0]);
|
||||
expect(manifest.name).toBe("My Homepage");
|
||||
expect(manifest.short_name).toBe("My Homepage");
|
||||
expect(manifest.start_url).toBe("/start");
|
||||
expect(manifest.icons).toEqual([{ src: "/i.png", sizes: "1x1", type: "image/png" }]);
|
||||
expect(manifest.shortcuts).toEqual([{ name: "One", url: "/one" }]);
|
||||
expect(manifest.theme_color).toBe(themes.slate.dark);
|
||||
expect(manifest.background_color).toBe(themes.slate.dark);
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,7 @@ export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
components: fileURLToPath(new URL("./src/components", import.meta.url)),
|
||||
styles: fileURLToPath(new URL("./src/styles", import.meta.url)),
|
||||
"test-utils": fileURLToPath(new URL("./src/test-utils", import.meta.url)),
|
||||
utils: fileURLToPath(new URL("./src/utils", import.meta.url)),
|
||||
widgets: fileURLToPath(new URL("./src/widgets", import.meta.url)),
|
||||
|
||||
@@ -10,6 +10,8 @@ afterEach(() => {
|
||||
|
||||
// implement a couple of common formatters mocked in next-i18next
|
||||
vi.mock("next-i18next", () => ({
|
||||
// Keep app/page components importable in unit tests.
|
||||
appWithTranslation: (Component) => Component,
|
||||
useTranslation: () => ({
|
||||
i18n: { language: "en" },
|
||||
t: (key, opts) => {
|
||||
|
||||
Reference in New Issue
Block a user