UNPKG

frontitygit

Version:

A Frontity source package for the REST API of self-hosted and WordPress.com sites

465 lines (397 loc) 15 kB
import { createStore, observe, InitializedStore } from "@frontity/connect"; import clone from "clone-deep"; import wpSource from "../"; import WpSource, { Pattern, Handler } from "../../types"; import * as handlers from "../libraries/handlers"; import { getMatch } from "../libraries/get-match"; import { Data, CategoryData, ErrorData } from "@frontity/source/types"; import { isCategory, isError, isHome } from "@frontity/source"; // Create mock for handler generators jest.mock("../libraries/handlers"); const handlerMocks = handlers as jest.Mocked<typeof handlers>; handlerMocks.taxonomyHandler.mockReturnValue(jest.fn()); handlerMocks.postTypeHandler.mockReturnValue(jest.fn()); handlerMocks.postTypeArchiveHandler.mockReturnValue(jest.fn()); handlerMocks.postTypeWithQueryHandler.mockReturnValue(jest.fn()); let handler: jest.Mocked<Pattern<Handler>>; let store: InitializedStore<WpSource>; beforeEach(() => { // Reset mocks handlerMocks.taxonomyHandler.mockClear(); handlerMocks.postTypeHandler.mockClear(); handlerMocks.postTypeArchiveHandler.mockClear(); handlerMocks.postTypeWithQueryHandler.mockClear(); // Create a mock handler handler = { name: "always", priority: 0, pattern: "/(.*)", func: jest.fn(async ({ link, state }) => { await Promise.resolve(); Object.assign(state.source.data[link], { type: "example", id: 1, isPostType: true, isFetching: true, isReady: false, }); }), }; // Initialize the store store = createStore<WpSource>(clone(wpSource(), { clone: false })); store.state.source.url = "https://test.frontity.org/"; // Add mock handler to the store store.libraries.source.handlers.push(handler); }); /** * Helper that returns a link's data when the given prop has the given value. * * @param link - Link in the Frontity site. * @param props - Props and their values to check. * @returns Promise with the data object when is ready. */ const observeData = (link: string, props: Partial<Data>): Promise<Data> => new Promise((resolve) => { observe(() => { const data = store.state.source.get(link); // Exit if some condition fails. for (const prop in props) { if (!data[prop] === props[prop]) return; } // Resolve only when all conditions are true. resolve(data); }); }); describe("actions.source.fetch", () => { test("should work if data doesn't exist", async () => { await store.actions.source.fetch("/some/route/"); expect(handler.func).toHaveBeenCalledTimes(1); expect(store.state.source.data).toMatchSnapshot(); }); test("does nothing if data exists", async () => { store.state.source.data["/some/route/"] = { type: "example", id: 1, isPostType: true, isFetching: false, isReady: true, link: "/some/route/", route: "/some/route/", page: 1, query: {}, } as Data; await store.actions.source.fetch("/some/route/"); expect(handler.func).not.toHaveBeenCalled(); expect(store.state.source.data).toMatchSnapshot(); }); test("should switch isFetching and isReady even if data exists", async () => { store.state.source.data["/some/route/"] = { isFetching: false, isReady: false, link: "/some/route/", route: "/some/route/", page: 1, query: {}, }; const fetching = store.actions.source.fetch("/some/route/"); expect(store.state.source.get("/some/route").isFetching).toBe(true); expect(store.state.source.get("/some/route").isReady).toBe(false); await fetching; expect(store.state.source.get("/some/route").isFetching).toBe(false); expect(store.state.source.get("/some/route").isReady).toBe(true); }); test('should set isHome in "/"', async () => { const promisedData = observeData("/", { isReady: true }); store.actions.source.fetch("/"); expect(isHome(await promisedData)).toBe(true); }); test('should set isHome in "/page/x"', async () => { const promisedData = observeData("/page/123", { isReady: true }); store.actions.source.fetch("/page/123"); expect(isHome(await promisedData)).toBe(true); }); test('should set isHome in "/blog" when using a subdirectory', async () => { store.state.source.subdirectory = "/blog"; const promisedData = observeData("/blog", { isReady: true }); store.actions.source.fetch("/blog"); expect(isHome(await promisedData)).toBe(true); }); test('should set isHome in "/blog/page/x" when using a subdirectory', async () => { store.state.source.subdirectory = "/blog"; const promisedData = observeData("/blog/page/123", { isReady: true }); store.actions.source.fetch("/blog/page/123"); expect(isHome(await promisedData)).toBe(true); }); test('should set isHome in "/" when a redirection has matched', async () => { store.libraries.source.redirections = [ { name: "homepage", priority: 10, pattern: "/", func: () => "/front-page/", }, ]; const promisedData = observeData("/", { isReady: true }); store.actions.source.fetch("/"); expect(isHome(await promisedData)).toBe(true); }); test('should set isHome in "/page/x/" when a redirection has matched', async () => { store.libraries.source.redirections = [ { name: "homepage", priority: 10, pattern: "/", func: () => "/front-page/", }, ]; const promisedData = observeData("/page/123", { isReady: true }); store.actions.source.fetch("/page/123"); expect(isHome(await promisedData)).toBe(true); }); test("should run again when `force` is used", async () => { store.state.source.data["/some/route/"] = { errorStatusText: "Request Timeout", errorStatus: 408, isError: true, isFetching: false, isReady: true, link: "/some/route/", query: {}, } as ErrorData; await store.actions.source.fetch("/some/route/", { force: true }); expect(handler.func).toHaveBeenCalled(); expect(store.state.source.data).toMatchSnapshot(); }); test("Throw an error if fetch fails", async () => { handler.func = jest.fn(async (_) => { throw new Error("Handler error"); }); let error: Error; try { await store.actions.source.fetch("/some/route/"); throw new Error("This should not be reached"); } catch (e) { error = e; } expect(error.message).toBe("Handler error"); expect(store.state.source.data).toMatchSnapshot(); }); test("should allow to observe 'isReady' properly", async () => { expect(store.state.source.get("/").isReady).toBe(false); // `observeData` uses `observe`. const promisedData = observeData("/", { isReady: true }); store.actions.source.fetch("/"); await promisedData; }); test("should allow to observe 'isFetching' properly", async () => { expect(store.state.source.get("/").isFetching).toBe(false); store.actions.source.fetch("/"); expect(store.state.source.get("/").isFetching).toBe(true); // `observeData` uses `observe`. const promisedData = observeData("/", { isFetching: false }); await promisedData; }); test("Should throw a 404 error if no handler matched the link", async () => { await store.actions.source.fetch("@unknown/link"); expect(store.state.source.data).toMatchInlineSnapshot(` Object { "@unknown/link/": Object { "errorStatus": 404, "errorStatusText": "No handler has matched for the given link: \\"@unknown/link/\\"", "is404": true, "isError": true, "isFetching": false, "isReady": true, "link": "@unknown/link/", "page": 1, "query": Object {}, "route": "@unknown/link/", }, } `); }); }); describe("actions.source.init", () => { test("should add redirect for the specified homepage", async () => { store.state.source.homepage = "/about-us/"; await store.actions.source.init(); expect(store.libraries.source.redirections).toMatchSnapshot(); }); test("should add redirect for the specified posts page", async () => { store.state.source.postsPage = "/all-posts/"; await store.actions.source.init(); expect(store.libraries.source.redirections).toMatchSnapshot(); }); test("should add redirect for categories if 'categoryBase' is set", async () => { store.state.source.categoryBase = "wp-cat"; await store.actions.source.init(); expect(store.libraries.source.redirections).toMatchSnapshot(); // Test that the redirection works. const route = "/wp-cat/travel/"; const redirect = getMatch({ route }, store.libraries.source.redirections); expect(redirect).toBeTruthy(); expect(redirect.func(redirect.params)).toBe("/category/travel/"); }); test("should add redirect for tags if 'tagBase' is set", async () => { store.state.source.tagBase = "wp-tag"; await store.actions.source.init(); expect(store.libraries.source.redirections).toMatchSnapshot(); // Test that the redirection works. const route = "/wp-tag/paris/"; const redirect = getMatch({ route }, store.libraries.source.redirections); expect(redirect).toBeTruthy(); expect(redirect.func(redirect.params)).toBe("/tag/paris/"); }); test("should add redirect for tags if 'authorBase' is set", async () => { store.state.source.authorBase = "/blog/author"; await store.actions.source.init(); expect(store.libraries.source.redirections).toMatchSnapshot(); // Test that the redirection works. const route = "/blog/author/admin/"; const redirect = getMatch({ route }, store.libraries.source.redirections); expect(redirect).toBeTruthy(); expect(redirect.func(redirect.params)).toBe("/author/admin/"); }); test("should add redirect if 'subirectory' is present", async () => { store.state.source.homepage = "/about-us/"; store.state.source.postsPage = "/all-posts/"; store.state.source.categoryBase = "wp-cat"; store.state.source.tagBase = "wp-tag"; store.state.source.subdirectory = "blog"; await store.actions.source.init(); expect(store.libraries.source.redirections).toMatchSnapshot(); }); test("should add new handlers from postTypes array", async () => { store.state.source.postTypes.push( { type: "cpt1", endpoint: "cpts1", }, { type: "cpt2", endpoint: "cpts2", archive: "cpt2-archive", } ); await store.actions.source.init(); expect(store.libraries.source.handlers).toMatchSnapshot(); expect(handlerMocks.postTypeHandler.mock.calls).toMatchSnapshot(); expect(handlerMocks.postTypeArchiveHandler.mock.calls).toMatchSnapshot(); expect(handlerMocks.postTypeWithQueryHandler.mock.calls).toMatchSnapshot(); }); test("should add new handlers from taxonomies array", async () => { store.state.source.taxonomies.push( { taxonomy: "taxonomy1", endpoint: "taxonomies1", }, { taxonomy: "taxonomy2", endpoint: "taxonomies2", postTypeEndpoint: "cpt", }, { taxonomy: "taxonomy3", endpoint: "taxonomies3", postTypeEndpoint: "multiple-post-type", params: { type: ["posts", "cpts"], }, } ); await store.actions.source.init(); expect(store.libraries.source.handlers).toMatchSnapshot(); expect(handlerMocks.taxonomyHandler.mock.calls).toMatchSnapshot(); }); test("should populate link, route, page and query even if data exists", async () => { store.state.source.data["/some/route/page/2/?a=b"] = { isFetching: false, isReady: false, link: "/some/route/page/2/?a=b", route: "/some/route/", page: 2, query: { a: "b" }, }; await store.actions.source.fetch("/some/route/page/2/?a=b"); const { link, route, page, query } = store.state.source.get( "/some/route/page/2/?a=b" ); expect(link).toEqual("/some/route/page/2/?a=b"); expect(route).toEqual("/some/route/"); expect(page).toEqual(2); expect(query).toEqual({ a: "b" }); }); test("state.data['/some/route/'].isReady should stay true when fetching with { force: true }", async () => { // Get initial data into the store store.state.source.data["/some/route/"] = { isFetching: false, isReady: true, link: "/some/route/", route: "/some/route/", page: 1, query: {}, }; const fetchLink = store.actions.source.fetch("/some/route/", { force: true, }); // Normally this would be `false` if we hadn't already fetched the data expect(store.state.source.data["/some/route/"].isReady).toBe(true); await fetchLink; // It should stay `true` after having fetched, obviously expect(store.state.source.data["/some/route/"].isReady).toBe(true); }); test("state.data['/some/route/'].isCategory should be removed when fetching with { force: true }", async () => { // Get initial data into the store const initialData: CategoryData = { isArchive: true, isTerm: true, isCategory: true, taxonomy: "category", id: 7, items: [], isReady: true, isFetching: false, link: "/some/route/", query: {}, route: "/some/route/", page: 1, }; store.state.source.data["/some/route/"] = initialData; handler.func = jest.fn(async ({ link, state }) => { await Promise.resolve(); Object.assign(state.source.data[link], { isFetching: true, isReady: true, }); }); expect(isCategory(store.state.source.data["/some/route/"])).toBe(true); expect((store.state.source.data["/some/route/"] as any).items).toEqual([]); await store.actions.source.fetch("/some/route/", { force: true, }); const data = store.state.source.get("/some/route/"); // NOTE!!! This should fail in wp-source 2.0, because `isCategory` and `items` should be removed expect(data).toMatchSnapshot(); // NOTE!!! This should fail in wp-source 2.0, because `isCategory` and `items` should be removed expect(isCategory(data)).toBe(true); expect((data as any).items).toEqual([]); }); test("Errors for state.data['/some/route/'] should be removed when fetching with { force: true }", async () => { // Get initial data into the store store.state.source["/some/route/"] = { isError: true, errorStatusText: "Some error", errorStatus: 404, isReady: true, isFetching: false, }; await store.actions.source.fetch("/some/route/", { force: true, }); const data = store.state.source.get("/some/route/"); expect(data).toMatchSnapshot(); expect(isError(data)).toBe(false); expect((data as any).errorStatus).toBeUndefined(); expect((data as any).errorStatusText).toBeUndefined(); }); });