Skip to content

Commit

Permalink
feat(content): add caching layer to content (#18)
Browse files Browse the repository at this point in the history
* feat(content): add caching layer to content

* test(routing.global): remove only

* test(routing.global): small fixes
  • Loading branch information
lksmsr authored and Dean Gite committed May 9, 2023
1 parent 33141cb commit 35a9c17
Show file tree
Hide file tree
Showing 10 changed files with 214 additions and 36 deletions.
21 changes: 19 additions & 2 deletions composables/content.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
import { Page } from "fsxa-api";

export function useContent() {
const content = useState<Page | null>();
return { content };
const currentPage = useState<Page | null>("currentPage");
const cachedPages = useState<{
[caasId: string]: Page;
}>("cachedPages", () => ({}));

function findCachedPageByCaaSId(caasDocumentId: string) {
return cachedPages.value[caasDocumentId];
}

function addToCache(key: string, data: Page) {
if (!cachedPages.value[key]) cachedPages.value[key] = data;
}

return {
currentPage,
cachedPages,
addToCache,
findCachedPageByCaaSId,
};
}
12 changes: 11 additions & 1 deletion composables/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,23 @@ export function useNavigationData() {
"activeNavigationItem"
);

function findNavItemByRoute(route: string) {
// find navigation item in navigation by seo route
if (!navigationData.value) return;
const navItemId = navigationData.value.seoRouteMap[route];
return navItemId ? navigationData.value.idMap[navItemId] : undefined;
}

return {
activeNavigationItem,
setActiveNavigationItem: (item: NavigationItem) => {
activeNavigationItem.value = item;
},

getNavigationStateFromRoute: async (route: string) => {
const item = await fetchNavigationItemFromRoute($fsxaApi, route);
const item =
findNavItemByRoute(route) ||
(await fetchNavigationItemFromRoute($fsxaApi, route));
const locale = getLocaleFromNavigationItem(item);

setLocale(locale);
Expand Down
26 changes: 8 additions & 18 deletions cypress/e2e/middleware/routing.global.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,50 +11,40 @@ describe(`routing.global.ts`, () => {
});

it("navigate to / with query+hash => redirect to index page and keep params", () => {
// go to / and check if we are redirect to any other page than /
cy.visit(`${baseURL}/?foo=bar#baz`);
cy.url().should("not.eq", `${baseURL}/`);
cy.url().should("contain", "?foo=bar");
cy.url().should("contain", "#baz");
});

it("navigate to normal page with query+hash => redirect to index page and keep params", () => {
// go to / and check if we are redirect to any other page than /
cy.visit(`${baseURL}/Unsere-Lösungen/?foo=bar#baz`);
cy.url().should("not.eq", `${baseURL}/`);
cy.url().should("contain", "?foo=bar");
cy.url().should("contain", "#baz");
});

it("navigate to normal page => render content", () => {
// go to / and check if we are redirect to any other page than /
cy.visit(`${baseURL}/Unsere-Lösungen`);

cy.get("body").should("contain", "Unsere Lösungen");
});

it("navigate to / => render content", () => {
// go to / and check if we are redirect to any other page than /
cy.visit(`${baseURL}/`);

cy.get("body").should("contain", "Startseite");
});

it("navigate to non-existing page => show error page", () => {
// go to / and check if we are redirect to any other page than /
cy.intercept({
method: "GET",
url: `${baseURL}/*`,
}).as("visit");
cy.visit(`${baseURL}/thisdoesnotexist`, { failOnStatusCode: false });

cy.wait("@visit").then(({ response }) => {
if (!response) throw new Error("response should exist");
expect(response.statusCode).to.eq(404);
});
// page should contain 404 somewhere
cy.get("body").should("contain", "404");

// should return 404 code
// cy.visit does not expose the status code, so we need to use cy.request
// See this issue: https://github.com/cypress-io/cypress/issues/22520
cy.request({
url: `${baseURL}/thisdoesnotexist`,
failOnStatusCode: false,
}).then((response) => {
expect(response.status).to.eq(404);
});
});
});
25 changes: 25 additions & 0 deletions cypress/e2e/pages/[...slug].cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const baseURL = Cypress.env("cyBaseURL");

describe(`slug page`, () => {
it("navigate to page => render content", () => {
cy.visit(`${baseURL}/Unsere-Lösungen`);
cy.get("body").should("contain", "Unsere Lösungen");
});

it("navigate back and forth => don't make new network calls for cached content", () => {
cy.intercept({
method: "GET",
url: "/api/elements",
}).as("fetchContent");
cy.visit(`${baseURL}`);
cy.get('[href="/Unsere-Lösungen/"]').click();
cy.get('[href="/Startseite/"]').click();
cy.get('[href="/Unsere-Lösungen/"]').click();
cy.wait(3000);

// newtwork calls happen once on the server
cy.get("@fetchContent").should("eq", null);
});
});

export {};
14 changes: 9 additions & 5 deletions pages/[...slug].vue
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
<template>
<div>
<component :is="pageLayoutComponent" v-if="page" :page="page" />
<component
:is="pageLayoutComponent"
v-if="currentPage"
:page="currentPage"
/>
<DevOnly>
<div class="fixed top-0 right-0 z-30">
<Dev v-if="page" :content="page" />
<Dev v-if="currentPage" :content="currentPage" />
</div>
</DevOnly>
</div>
</template>

<script setup lang="ts">
const { content: page } = useContent();
const { currentPage } = useContent();
const pageLayoutComponent = computed(() => {
switch (page.value?.layout) {
switch (currentPage.value?.layout) {
case "homepage":
return resolveComponent("PageLayoutHome");
case "standard":
Expand All @@ -29,6 +33,6 @@ definePageMeta({
// meta tags
useHead({
title: page.value?.data["pt_title"],
title: currentPage.value?.data["pt_title"],
});
</script>
22 changes: 17 additions & 5 deletions plugins/4.setupDataFetching.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,31 @@ export default defineNuxtPlugin(async () => {
{ watch: [localeConfig] }
);

const { content } = useContent();
const { currentPage, addToCache, findCachedPageByCaaSId } = useContent();
// fetch page content
await useAsyncData(
async () => {
// This state should not be possible.
// The middleware should have figured out both the locale and our current navigation item
if (!activeNavigationItem.value || !localeConfig.value.activeLocale)
throw new Error("No navigation item found");
content.value = await fetchContentFromNavigationItem(
$fsxaApi,
activeNavigationItem.value,
localeConfig.value.activeLocale

const cachedContent = findCachedPageByCaaSId(
activeNavigationItem.value.caasDocumentId
);
if (cachedContent) {
currentPage.value = cachedContent;
} else {
currentPage.value = await fetchContentFromNavigationItem(
$fsxaApi,
activeNavigationItem.value,
localeConfig.value.activeLocale
);
addToCache(
activeNavigationItem.value.caasDocumentId,
currentPage.value
);
}
},
// automatically refetch when the navigation item changes
{ watch: [activeNavigationItem, localeConfig] }
Expand Down
81 changes: 81 additions & 0 deletions tests/composables/content.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { it, expect, describe, beforeEach } from "vitest";
import { useContent } from "../../composables/content";
import { createPage } from "../testutils/createPage";
import { clearMockedState } from "../testutils/nuxtMocks";

describe("useContent", () => {
it("useContent => provide default current page, cached pages", () => {
const { currentPage, cachedPages } = useContent();
expect(currentPage.value).toBeUndefined();
expect(cachedPages.value).toEqual({});
});

describe("findCachedPageByCaaSId", () => {
beforeEach(() => {
clearMockedState();
});
it("item does not exist in cached pages => return undefined", () => {
const page1 = createPage();
const page2 = createPage();
const page3 = createPage();

const { cachedPages, findCachedPageByCaaSId } = useContent();

cachedPages.value[page1.refId] = page1;
cachedPages.value[page2.refId] = page2;
cachedPages.value[page3.refId] = page3;

expect(findCachedPageByCaaSId("unknown-id")).toBeUndefined();
});
it("item exists => return item", () => {
const page1 = createPage();
const page2 = createPage();
const page3 = createPage();

const { cachedPages, findCachedPageByCaaSId } = useContent();

cachedPages.value[page1.refId] = page1;
cachedPages.value[page2.refId] = page2;
cachedPages.value[page3.refId] = page3;

expect(findCachedPageByCaaSId(page2.refId)).toBe(page2);
});
});
describe("addToCache", () => {
beforeEach(() => {
clearMockedState();
});
it("item does not exist => add new item", () => {
const page1 = createPage();
const page2 = createPage();
const page3 = createPage();

const { cachedPages, addToCache } = useContent();

addToCache(page1.refId, page1);
addToCache(page2.refId, page2);
addToCache(page3.refId, page3);

expect(cachedPages.value[page1.refId]).toBe(page1);
expect(cachedPages.value[page2.refId]).toBe(page2);
expect(cachedPages.value[page3.refId]).toBe(page3);

expect(Object.keys(cachedPages.value).length).toBe(3);
});
it("item exists => don't add new item", () => {
const page1 = createPage();
const page2 = createPage();

const { cachedPages, addToCache } = useContent();

addToCache(page1.refId, page1);
addToCache(page2.refId, page2);
addToCache(page2.refId, page2);

expect(cachedPages.value[page1.refId]).toBe(page1);
expect(cachedPages.value[page2.refId]).toBe(page2);

expect(Object.keys(cachedPages.value).length).toBe(2);
});
});
});
6 changes: 3 additions & 3 deletions tests/pages/[...slug].spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,23 @@ describe("slug page", () => {

it("render with homepage layout prop => render homepage layout component", () => {
vi.spyOn(content, "useContent").mockReturnValue({
content: { value: createPage({ layout: "homepage" }) },
currentPage: { value: createPage({ layout: "homepage" }) },
});
const { getByTestId } = render(SlugPage, { global: renderConfig.global });
expect(getByTestId("homePageLayout")).toBeTruthy();
});

it("render with standard layout prop => render standard layout component", () => {
vi.spyOn(content, "useContent").mockReturnValue({
content: { value: createPage({ layout: "standard" }) },
currentPage: { value: createPage({ layout: "standard" }) },
});
const { getByTestId } = render(SlugPage, { global: renderConfig.global });
expect(getByTestId("standardPageLayout")).toBeTruthy();
});

it("render with unkown layout prop => render unknown component", () => {
vi.spyOn(content, "useContent").mockReturnValue({
content: { value: createPage({ layout: "unkown" }) },
currentPage: { value: createPage({ layout: "unkown" }) },
});
const { getByTestId } = render(SlugPage, { global: renderConfig.global });
expect(getByTestId("unknown")).toBeTruthy();
Expand Down
21 changes: 19 additions & 2 deletions tests/plugins/4.setupDataFetching.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import { useProjectProperties } from "../../composables/projectProperties";
import toplevelENnavData from "../fixtures/toplevelNavigation_en_GB.json";
import page from "../fixtures/page.json";
import projectPropertiesFixture from "../fixtures/projectProperties.json";
import { createNavigationItem } from "../testutils/createNavigationItem";
import { createPage } from "../testutils/createPage";

describe("setupDataFetching", () => {
it("setupDataFetching => navdata, project props, content get fetched", async () => {
const { setLocale } = useLocale();
const { navigationData, setActiveNavigationItem } = useNavigationData();
const { content } = useContent();
const { currentPage } = useContent();
const { projectProperties } = useProjectProperties();
setLocale("en_GB");

Expand All @@ -26,7 +28,22 @@ describe("setupDataFetching", () => {
await setupDataFetching();

expect(navigationData.value).toBe(toplevelENnavData);
expect(content.value).toBe(page);
expect(currentPage.value).toBe(page);
expect(projectProperties.value).toBe(projectPropertiesFixture);
});

it("content is cached => fetch content from cache", async () => {
const { setLocale } = useLocale();
const { setActiveNavigationItem } = useNavigationData();
const { currentPage, cachedPages } = useContent();
const navItem = createNavigationItem();
const cachedPage = createPage();
cachedPages.value[navItem.caasDocumentId] = cachedPage;
setLocale("en_GB");
setActiveNavigationItem(navItem);

await setupDataFetching();

expect(currentPage.value).toBe(cachedPage);
});
});
22 changes: 22 additions & 0 deletions tests/testutils/createNavigationItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NavigationItem } from "fsxa-api";
import { faker } from "@faker-js/faker";

export function createNavigationItem(
optionalNavigationItem?: Partial<NavigationItem>
): NavigationItem {
const NavigationItem: NavigationItem = {
caasDocumentId: faker.datatype.uuid(),
id: faker.datatype.uuid(),
contentReference: faker.internet.url(),
customData: {},
seoRoute: faker.random.word(),
seoRouteRegex: null,
label: faker.random.word(),
permissions: {
allowed: [],
denied: [],
},
parentIds: [],
};
return { ...NavigationItem, ...optionalNavigationItem };
}

0 comments on commit 35a9c17

Please sign in to comment.