From fcbbc48c6256fae32c18b7a1222bb5f08b2e6246 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20Westk=C3=A4mper?= Date: Mon, 22 Jul 2024 11:22:20 +0300 Subject: [PATCH 01/10] Simplify useRenderCampaigns --- src/components/Nosto404.tsx | 10 ++---- src/components/NostoCategory.tsx | 8 ++--- src/components/NostoCheckout.tsx | 10 ++---- src/components/NostoHome.tsx | 9 ++--- src/components/NostoOrder.tsx | 9 +++-- src/components/NostoOther.tsx | 10 ++---- src/components/NostoProduct.tsx | 8 ++--- src/components/NostoProvider.tsx | 61 +------------------------------- src/components/NostoSearch.tsx | 8 ++--- src/context.ts | 24 ++++--------- src/hooks/index.ts | 3 +- src/hooks/useRenderCampaigns.tsx | 55 ++++++++++++++++++++++++++++ 12 files changed, 90 insertions(+), 125 deletions(-) create mode 100644 src/hooks/useRenderCampaigns.tsx diff --git a/src/components/Nosto404.tsx b/src/components/Nosto404.tsx index 2927dd9..821040c 100644 --- a/src/components/Nosto404.tsx +++ b/src/components/Nosto404.tsx @@ -1,4 +1,4 @@ -import { useNostoContext, useNostoApi } from "../hooks" +import { useNostoApi, useRenderCampaigns } from "../hooks" /** * You can personalise your cart and checkout pages by using the `Nosto404` component. @@ -21,9 +21,7 @@ import { useNostoContext, useNostoApi } from "../hooks" * @group Components */ export default function Nosto404(props: { placements?: string[] }) { - const { recommendationComponent, useRenderCampaigns } = useNostoContext() - - const { renderCampaigns, pageTypeUpdated } = useRenderCampaigns("404") + const { renderCampaigns } = useRenderCampaigns() useNostoApi( async (api) => { @@ -32,8 +30,6 @@ export default function Nosto404(props: { placements?: string[] }) { .setPlacements(props.placements || api.placements.getPlacements()) .load() renderCampaigns(data, api) - }, - [recommendationComponent, pageTypeUpdated] - ) + }) return null } diff --git a/src/components/NostoCategory.tsx b/src/components/NostoCategory.tsx index 59a1951..0cf36eb 100644 --- a/src/components/NostoCategory.tsx +++ b/src/components/NostoCategory.tsx @@ -1,4 +1,4 @@ -import { useNostoContext, useNostoApi } from "../hooks" +import { useNostoApi, useRenderCampaigns } from "../hooks" /** * You can personalise your category and collection pages by using the NostoCategory component. @@ -24,9 +24,7 @@ import { useNostoContext, useNostoApi } from "../hooks" */ export default function NostoCategory(props: { category: string; placements?: string[] }) { const { category, placements } = props - const { recommendationComponent, useRenderCampaigns } = useNostoContext() - - const { renderCampaigns, pageTypeUpdated } = useRenderCampaigns("home") + const { renderCampaigns } = useRenderCampaigns() useNostoApi( async (api) => { @@ -36,7 +34,7 @@ export default function NostoCategory(props: { category: string; placements?: st .load() renderCampaigns(data, api) }, - [category, recommendationComponent, pageTypeUpdated] + [category] ) return null } diff --git a/src/components/NostoCheckout.tsx b/src/components/NostoCheckout.tsx index 82117f8..0ed95ad 100644 --- a/src/components/NostoCheckout.tsx +++ b/src/components/NostoCheckout.tsx @@ -1,4 +1,4 @@ -import { useNostoContext, useNostoApi } from "../hooks" +import { useNostoApi, useRenderCampaigns } from "../hooks" /** * You can personalise your cart and checkout pages by using the NostoCheckout component. @@ -20,9 +20,7 @@ import { useNostoContext, useNostoApi } from "../hooks" * @group Components */ export default function NostoCheckout(props: { placements?: string[] }) { - const { recommendationComponent, useRenderCampaigns } = useNostoContext() - - const { renderCampaigns, pageTypeUpdated } = useRenderCampaigns("checkout") + const { renderCampaigns } = useRenderCampaigns() useNostoApi( async (api) => { @@ -31,8 +29,6 @@ export default function NostoCheckout(props: { placements?: string[] }) { .setPlacements(props.placements || api.placements.getPlacements()) .load() renderCampaigns(data, api) - }, - [recommendationComponent, pageTypeUpdated] - ) + }) return null } diff --git a/src/components/NostoHome.tsx b/src/components/NostoHome.tsx index 0b76d65..92860f2 100644 --- a/src/components/NostoHome.tsx +++ b/src/components/NostoHome.tsx @@ -1,4 +1,4 @@ -import { useNostoContext, useNostoApi } from "../hooks" +import { useRenderCampaigns, useNostoApi } from "../hooks" /** * The `NostoHome` component must be used to personalise the home page. The component does not require any props. @@ -24,9 +24,7 @@ import { useNostoContext, useNostoApi } from "../hooks" * @group Components */ export default function NostoHome(props: { placements?: string[] }) { - const { recommendationComponent, useRenderCampaigns } = useNostoContext() - - const { renderCampaigns, pageTypeUpdated } = useRenderCampaigns("home") + const { renderCampaigns } = useRenderCampaigns() useNostoApi( async (api) => { @@ -35,8 +33,7 @@ export default function NostoHome(props: { placements?: string[] }) { .setPlacements(props.placements || api.placements.getPlacements()) .load() renderCampaigns(data, api) - }, - [recommendationComponent, pageTypeUpdated] + } ) return null } diff --git a/src/components/NostoOrder.tsx b/src/components/NostoOrder.tsx index b4103f7..1381f62 100644 --- a/src/components/NostoOrder.tsx +++ b/src/components/NostoOrder.tsx @@ -1,5 +1,5 @@ import { Order } from "../types" -import { useNostoContext, useNostoApi } from "../hooks" +import { useRenderCampaigns, useNostoApi } from "../hooks" import { snakeize } from "../utils/snakeize" /** @@ -26,9 +26,7 @@ export default function NostoOrder(props: { placements?: string[] }) { const { order, placements } = props - const { recommendationComponent, useRenderCampaigns } = useNostoContext() - - const { renderCampaigns, pageTypeUpdated } = useRenderCampaigns("order") + const { renderCampaigns } = useRenderCampaigns() useNostoApi( async (api) => { @@ -38,7 +36,8 @@ export default function NostoOrder(props: { .load() renderCampaigns(data, api) }, - [recommendationComponent, pageTypeUpdated] + [order], + { deep: true } ) return null } diff --git a/src/components/NostoOther.tsx b/src/components/NostoOther.tsx index efd9c9f..a50abfc 100644 --- a/src/components/NostoOther.tsx +++ b/src/components/NostoOther.tsx @@ -1,4 +1,4 @@ -import { useNostoContext, useNostoApi } from "../hooks" +import { useRenderCampaigns, useNostoApi } from "../hooks" /** * You can personalise your miscellaneous pages by using the NostoOther component. @@ -20,9 +20,7 @@ import { useNostoContext, useNostoApi } from "../hooks" * @group Components */ export default function NostoOther(props: { placements?: string[] }) { - const { recommendationComponent, useRenderCampaigns } = useNostoContext() - - const { renderCampaigns, pageTypeUpdated } = useRenderCampaigns("other") + const { renderCampaigns } = useRenderCampaigns() useNostoApi( async (api) => { @@ -31,8 +29,6 @@ export default function NostoOther(props: { placements?: string[] }) { .setPlacements(props.placements || api.placements.getPlacements()) .load() renderCampaigns(data, api) - }, - [recommendationComponent, pageTypeUpdated] - ) + }) return null } diff --git a/src/components/NostoProduct.tsx b/src/components/NostoProduct.tsx index b9f7575..e6c2721 100644 --- a/src/components/NostoProduct.tsx +++ b/src/components/NostoProduct.tsx @@ -1,4 +1,4 @@ -import { useNostoContext, useNostoApi } from "../hooks" +import { useRenderCampaigns, useNostoApi } from "../hooks" import { Product } from "../types" /** @@ -31,9 +31,7 @@ export default function NostoProduct(props: { placements?: string[] }) { const { product, tagging, placements } = props - const { recommendationComponent, useRenderCampaigns } = useNostoContext() - - const { renderCampaigns, pageTypeUpdated } = useRenderCampaigns("product") + const { renderCampaigns } = useRenderCampaigns() useNostoApi( async (api) => { @@ -43,7 +41,7 @@ export default function NostoProduct(props: { .load() renderCampaigns(data, api) }, - [product, recommendationComponent, pageTypeUpdated] + [product] ) return null } diff --git a/src/components/NostoProvider.tsx b/src/components/NostoProvider.tsx index fbf1166..19a96c6 100644 --- a/src/components/NostoProvider.tsx +++ b/src/components/NostoProvider.tsx @@ -78,63 +78,6 @@ export default function NostoProvider(props: NostoProviderProps) { // Set responseMode for loading campaigns: const responseMode = isValidElement(recommendationComponent) ? "JSON_ORIGINAL" : "HTML" - // RecommendationComponent for client-side rendering: - function RecommendationComponentWrapper(props: { nostoRecommendation: Recommendation }) { - return React.cloneElement(recommendationComponent!, { - // eslint-disable-next-line react/prop-types - nostoRecommendation: props.nostoRecommendation, - }) - } - - // custom hook for rendering campaigns (CSR/SSR): - const [pageType, setPageType] = useState("") - - function useRenderCampaigns(type: string = "") { - const placementRefs = useRef>({}) - useEffect(() => { - if (pageType !== type) { - setPageType(type) - } - }, []) - - const pageTypeUpdated = type === pageType - - function renderCampaigns( - data: { - recommendations: Record - campaigns: { - recommendations: Record - } - }, - api: NostoClient - ) { - if (responseMode == "HTML") { - // inject content campaigns as usual: - api.placements.injectCampaigns(data.recommendations) - } else { - // render recommendation component into placements: - const recommendations = data.campaigns.recommendations - for (const key in recommendations) { - const recommendation = recommendations[key] - const placementSelector = "#" + key - const placement = () => document.querySelector(placementSelector) - - if (placement()) { - if (!placementRefs.current[key]) placementRefs.current[key] = createRoot(placement()!) - const root = placementRefs.current[key]! - root.render( - - ) - } - } - } - } - - return { renderCampaigns, pageTypeUpdated } - } - useEffect(() => { if (!window.nostojs) { window.nostojs = (cb: (api: NostoClient) => void) => { @@ -211,9 +154,7 @@ export default function NostoProvider(props: NostoProviderProps) { clientScriptLoaded, currentVariation, responseMode, - recommendationComponent, - useRenderCampaigns, - pageType, + recommendationComponent }} > {children} diff --git a/src/components/NostoSearch.tsx b/src/components/NostoSearch.tsx index 2913ff6..ab7056d 100644 --- a/src/components/NostoSearch.tsx +++ b/src/components/NostoSearch.tsx @@ -1,4 +1,4 @@ -import { useNostoContext, useNostoApi } from "../hooks" +import { useRenderCampaigns, useNostoApi } from "../hooks" /** * You can personalise your search pages by using the NostoSearch component. @@ -25,9 +25,7 @@ import { useNostoContext, useNostoApi } from "../hooks" */ export default function NostoSearch(props: { query: string; placements?: string[] }) { const { query, placements } = props - const { recommendationComponent, useRenderCampaigns } = useNostoContext() - - const { renderCampaigns, pageTypeUpdated } = useRenderCampaigns("search") + const { renderCampaigns } = useRenderCampaigns() useNostoApi( async (api) => { @@ -37,7 +35,7 @@ export default function NostoSearch(props: { query: string; placements?: string[ .load() renderCampaigns(data, api) }, - [query, recommendationComponent, pageTypeUpdated] + [query] ) return null } diff --git a/src/context.ts b/src/context.ts index 526ec9e..356a562 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,8 +1,12 @@ import { createContext } from "react" -import { NostoClient, Recommendation, RenderMode } from "./types" +import { Recommendation, RenderMode } from "./types" type AnyFunction = (...args: unknown[]) => unknown +export type RecommendationComponent = React.ReactElement<{ + nostoRecommendation: Recommendation +}> + /** * @group Types */ @@ -12,14 +16,7 @@ export interface NostoContextType { currentVariation?: string renderFunction?: AnyFunction responseMode: RenderMode - recommendationComponent?: React.ReactElement<{ - nostoRecommendation: Recommendation - }> - useRenderCampaigns(type: string): { - renderCampaigns(data: unknown, api: NostoClient): void - pageTypeUpdated: boolean - } - pageType: string + recommendationComponent?: RecommendationComponent } /** @@ -28,14 +25,7 @@ export interface NostoContextType { export const NostoContext = createContext({ account: "", currentVariation: "", - pageType: "", responseMode: "HTML", - clientScriptLoaded: false, - useRenderCampaigns: () => { - return { - renderCampaigns: () => {}, - pageTypeUpdated: false, - } - }, + clientScriptLoaded: false }) diff --git a/src/hooks/index.ts b/src/hooks/index.ts index a511d77..28585f6 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,3 +1,4 @@ export { useDeepCompareEffect } from "./useDeepCompareEffect" export { useNostoApi } from "./useNostoApi" -export { useNostoContext } from "./useNostoContext" \ No newline at end of file +export { useNostoContext } from "./useNostoContext" +export { useRenderCampaigns } from "./useRenderCampaigns" \ No newline at end of file diff --git a/src/hooks/useRenderCampaigns.tsx b/src/hooks/useRenderCampaigns.tsx new file mode 100644 index 0000000..8cb93fa --- /dev/null +++ b/src/hooks/useRenderCampaigns.tsx @@ -0,0 +1,55 @@ +import { useRef } from "react" +import { createRoot, Root } from "react-dom/client" +import { ActionResponse, NostoClient, Recommendation } from "../types" +import { useNostoContext } from "./useNostoContext" +import React from "react" +import { RecommendationComponent } from "../context" + +// RecommendationComponent for client-side rendering: +function RecommendationComponentWrapper(props: { + recommendationComponent: RecommendationComponent, + nostoRecommendation: Recommendation }) { + + return React.cloneElement(props.recommendationComponent, { + // eslint-disable-next-line react/prop-types + nostoRecommendation: props.nostoRecommendation, + }) +} + +function injectCampaigns(data: ActionResponse, api: NostoClient) { + api.placements.injectCampaigns(data.recommendations) +} + +export function useRenderCampaigns() { + const { responseMode, recommendationComponent } = useNostoContext() + const placementRefs = useRef>({}) + + if (responseMode == "HTML") { + return { renderCampaigns: injectCampaigns } + } + + function renderCampaigns(data: ActionResponse) { + // render recommendation component into placements: + const recommendations = data.campaigns?.recommendations ?? {} + for (const key in recommendations) { + const recommendation = recommendations[key] as Recommendation + const placementSelector = "#" + key + const placementElement = document.querySelector(placementSelector) + + if (placementElement) { + if (!placementRefs.current[key]) { + placementRefs.current[key] = createRoot(placementElement) + } + const root = placementRefs.current[key]! + root.render( + + ) + } + } + } + + return { renderCampaigns } +} \ No newline at end of file From 0b68c7a019e51c9443dfec5e34a699214a5af4bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20Westk=C3=A4mper?= Date: Mon, 22 Jul 2024 11:35:06 +0300 Subject: [PATCH 02/10] Add test for module exports --- spec/module.spec.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 spec/module.spec.ts diff --git a/spec/module.spec.ts b/spec/module.spec.ts new file mode 100644 index 0000000..da4bf4f --- /dev/null +++ b/spec/module.spec.ts @@ -0,0 +1,19 @@ +import * as imports from "../src/index" + +test("module structure is stable", () => { + expect(Object.keys(imports)).toEqual([ + "NostoContext", + "useNostoContext", + "Nosto404", + "NostoOther", + "NostoCheckout", + "NostoProduct", + "NostoCategory", + "NostoSearch", + "NostoOrder", + "NostoHome", + "NostoPlacement", + "NostoProvider", + "NostoSession" + ]) +}) \ No newline at end of file From 05a47dc65ba1abf2539b4d9e933a19d1708492d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20Westk=C3=A4mper?= Date: Mon, 22 Jul 2024 11:42:55 +0300 Subject: [PATCH 03/10] Simplify renderCampaigns signature --- src/components/Nosto404.tsx | 2 +- src/components/NostoCategory.tsx | 2 +- src/components/NostoCheckout.tsx | 2 +- src/components/NostoHome.tsx | 2 +- src/components/NostoOrder.tsx | 2 +- src/components/NostoOther.tsx | 2 +- src/components/NostoProduct.tsx | 2 +- src/components/NostoProvider.tsx | 11 ++++------- src/components/NostoSearch.tsx | 2 +- src/hooks/useRenderCampaigns.tsx | 8 +++++--- 10 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/components/Nosto404.tsx b/src/components/Nosto404.tsx index 821040c..3ddddf8 100644 --- a/src/components/Nosto404.tsx +++ b/src/components/Nosto404.tsx @@ -29,7 +29,7 @@ export default function Nosto404(props: { placements?: string[] }) { .viewNotFound() .setPlacements(props.placements || api.placements.getPlacements()) .load() - renderCampaigns(data, api) + renderCampaigns(data) }) return null } diff --git a/src/components/NostoCategory.tsx b/src/components/NostoCategory.tsx index 0cf36eb..748542b 100644 --- a/src/components/NostoCategory.tsx +++ b/src/components/NostoCategory.tsx @@ -32,7 +32,7 @@ export default function NostoCategory(props: { category: string; placements?: st .viewCategory(category) .setPlacements(placements || api.placements.getPlacements()) .load() - renderCampaigns(data, api) + renderCampaigns(data) }, [category] ) diff --git a/src/components/NostoCheckout.tsx b/src/components/NostoCheckout.tsx index 0ed95ad..605b6a2 100644 --- a/src/components/NostoCheckout.tsx +++ b/src/components/NostoCheckout.tsx @@ -28,7 +28,7 @@ export default function NostoCheckout(props: { placements?: string[] }) { .viewCart() .setPlacements(props.placements || api.placements.getPlacements()) .load() - renderCampaigns(data, api) + renderCampaigns(data) }) return null } diff --git a/src/components/NostoHome.tsx b/src/components/NostoHome.tsx index 92860f2..7bd3391 100644 --- a/src/components/NostoHome.tsx +++ b/src/components/NostoHome.tsx @@ -32,7 +32,7 @@ export default function NostoHome(props: { placements?: string[] }) { .viewFrontPage() .setPlacements(props.placements || api.placements.getPlacements()) .load() - renderCampaigns(data, api) + renderCampaigns(data) } ) return null diff --git a/src/components/NostoOrder.tsx b/src/components/NostoOrder.tsx index 1381f62..3ea1b1a 100644 --- a/src/components/NostoOrder.tsx +++ b/src/components/NostoOrder.tsx @@ -34,7 +34,7 @@ export default function NostoOrder(props: { .addOrder(snakeize(order)) .setPlacements(placements || api.placements.getPlacements()) .load() - renderCampaigns(data, api) + renderCampaigns(data) }, [order], { deep: true } diff --git a/src/components/NostoOther.tsx b/src/components/NostoOther.tsx index a50abfc..846b0b0 100644 --- a/src/components/NostoOther.tsx +++ b/src/components/NostoOther.tsx @@ -28,7 +28,7 @@ export default function NostoOther(props: { placements?: string[] }) { .viewOther() .setPlacements(props.placements || api.placements.getPlacements()) .load() - renderCampaigns(data, api) + renderCampaigns(data) }) return null } diff --git a/src/components/NostoProduct.tsx b/src/components/NostoProduct.tsx index e6c2721..a131b11 100644 --- a/src/components/NostoProduct.tsx +++ b/src/components/NostoProduct.tsx @@ -39,7 +39,7 @@ export default function NostoProduct(props: { .viewProduct(tagging ?? product) .setPlacements(placements || api.placements.getPlacements()) .load() - renderCampaigns(data, api) + renderCampaigns(data) }, [product] ) diff --git a/src/components/NostoProvider.tsx b/src/components/NostoProvider.tsx index 19a96c6..36f2b94 100644 --- a/src/components/NostoProvider.tsx +++ b/src/components/NostoProvider.tsx @@ -1,7 +1,6 @@ -import React, { useEffect, isValidElement, useState, useRef } from "react" -import { NostoContext } from "../context" -import { createRoot, Root } from "react-dom/client" -import { NostoClient, Recommendation } from "../types" +import React, { useEffect, isValidElement } from "react" +import { NostoContext, RecommendationComponent } from "../context" +import { NostoClient } from "../types" /** * @group Components @@ -27,9 +26,7 @@ export interface NostoProviderProps { /** * Recommendation component which holds nostoRecommendation object */ - recommendationComponent?: React.ReactElement<{ - nostoRecommendation: Recommendation - }> + recommendationComponent?: RecommendationComponent /** * Enables Shopify markets with language and market id */ diff --git a/src/components/NostoSearch.tsx b/src/components/NostoSearch.tsx index ab7056d..3093ff7 100644 --- a/src/components/NostoSearch.tsx +++ b/src/components/NostoSearch.tsx @@ -33,7 +33,7 @@ export default function NostoSearch(props: { query: string; placements?: string[ .viewSearch(query) .setPlacements(placements || api.placements.getPlacements()) .load() - renderCampaigns(data, api) + renderCampaigns(data) }, [query] ) diff --git a/src/hooks/useRenderCampaigns.tsx b/src/hooks/useRenderCampaigns.tsx index 8cb93fa..ba8dd06 100644 --- a/src/hooks/useRenderCampaigns.tsx +++ b/src/hooks/useRenderCampaigns.tsx @@ -1,6 +1,6 @@ import { useRef } from "react" import { createRoot, Root } from "react-dom/client" -import { ActionResponse, NostoClient, Recommendation } from "../types" +import { ActionResponse, Recommendation } from "../types" import { useNostoContext } from "./useNostoContext" import React from "react" import { RecommendationComponent } from "../context" @@ -16,8 +16,10 @@ function RecommendationComponentWrapper(props: { }) } -function injectCampaigns(data: ActionResponse, api: NostoClient) { - api.placements.injectCampaigns(data.recommendations) +function injectCampaigns(data: ActionResponse) { + window.nostojs(api => { + api.placements.injectCampaigns(data.recommendations) + }) } export function useRenderCampaigns() { From 35c087226ed09bcb74db2d3dbaa937b0487ecad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20Westk=C3=A4mper?= Date: Mon, 22 Jul 2024 11:54:23 +0300 Subject: [PATCH 04/10] Improve signature for children --- spec/category.spec.tsx | 8 +++----- spec/checkout.spec.tsx | 10 ++++------ spec/fohofo.spec.tsx | 8 +++----- spec/home.spec.tsx | 10 ++++------ spec/other.spec.tsx | 12 +++++------- spec/product.spec.tsx | 10 ++++------ spec/search.spec.tsx | 8 +++----- src/components/NostoProvider.tsx | 5 ++++- 8 files changed, 30 insertions(+), 41 deletions(-) diff --git a/spec/category.spec.tsx b/spec/category.spec.tsx index ada852f..ea96af7 100644 --- a/spec/category.spec.tsx +++ b/spec/category.spec.tsx @@ -11,11 +11,9 @@ test("Category page render", async () => { account="shopify-11368366139" recommendationComponent={} > - <> - - - - + + + ) diff --git a/spec/checkout.spec.tsx b/spec/checkout.spec.tsx index c235ef8..10f6f77 100644 --- a/spec/checkout.spec.tsx +++ b/spec/checkout.spec.tsx @@ -11,12 +11,10 @@ test("Checkout page render", async () => { account="shopify-11368366139" recommendationComponent={} > - <> - - - - - + + + + ) diff --git a/spec/fohofo.spec.tsx b/spec/fohofo.spec.tsx index 62ae5fc..49080cc 100644 --- a/spec/fohofo.spec.tsx +++ b/spec/fohofo.spec.tsx @@ -11,11 +11,9 @@ test("404 page render", async () => { account="shopify-11368366139" recommendationComponent={} > - <> - - - - + + + ) diff --git a/spec/home.spec.tsx b/spec/home.spec.tsx index 1e4632e..861d6b8 100644 --- a/spec/home.spec.tsx +++ b/spec/home.spec.tsx @@ -11,12 +11,10 @@ test("Home page render", async () => { account="shopify-11368366139" recommendationComponent={} > - <> - - - - - + + + + ) diff --git a/spec/other.spec.tsx b/spec/other.spec.tsx index 0bce06d..b289e49 100644 --- a/spec/other.spec.tsx +++ b/spec/other.spec.tsx @@ -11,13 +11,11 @@ test("Other page render", async () => { account="shopify-11368366139" recommendationComponent={} > - <> - - - - - - + + + + + ) diff --git a/spec/product.spec.tsx b/spec/product.spec.tsx index e0a245b..1620d81 100644 --- a/spec/product.spec.tsx +++ b/spec/product.spec.tsx @@ -11,12 +11,10 @@ test("Product page render", async () => { account="shopify-11368366139" recommendationComponent={} > - <> - - - - - + + + + ) diff --git a/spec/search.spec.tsx b/spec/search.spec.tsx index 2c4d33c..0911d3a 100644 --- a/spec/search.spec.tsx +++ b/spec/search.spec.tsx @@ -11,11 +11,9 @@ test("Search page render", async () => { account="shopify-11368366139" recommendationComponent={} > - <> - - - - + + + ) diff --git a/src/components/NostoProvider.tsx b/src/components/NostoProvider.tsx index 36f2b94..75e175c 100644 --- a/src/components/NostoProvider.tsx +++ b/src/components/NostoProvider.tsx @@ -18,7 +18,10 @@ export interface NostoProviderProps { * Indicates an url of a server */ host?: string - children: React.ReactElement + /** + * children + */ + children: React.ReactElement | React.ReactElement[] /** * Indicates if merchant uses multiple currencies */ From e1cae5910eb01d4aae92e67050d19b77861359c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20Westk=C3=A4mper?= Date: Mon, 22 Jul 2024 11:59:24 +0300 Subject: [PATCH 05/10] Add nostojs check --- src/hooks/useRenderCampaigns.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/hooks/useRenderCampaigns.tsx b/src/hooks/useRenderCampaigns.tsx index ba8dd06..2808a6c 100644 --- a/src/hooks/useRenderCampaigns.tsx +++ b/src/hooks/useRenderCampaigns.tsx @@ -17,6 +17,9 @@ function RecommendationComponentWrapper(props: { } function injectCampaigns(data: ActionResponse) { + if (!window.nostojs) { + throw new Error("Nosto has not yet been initialized") + } window.nostojs(api => { api.placements.injectCampaigns(data.recommendations) }) From e2b9664482238967489649644ff796753ce8afea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20Westk=C3=A4mper?= Date: Mon, 22 Jul 2024 14:46:52 +0300 Subject: [PATCH 06/10] Add test for navigation --- package-lock.json | 46 ++++++++++++++++ package.json | 2 + spec/events.ts | 46 ++++++++++++++++ spec/navigation.spec.tsx | 92 ++++++++++++++++++++++++++++++++ src/components/NostoProvider.tsx | 2 +- src/types.ts | 1 + 6 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 spec/events.ts create mode 100644 spec/navigation.spec.tsx diff --git a/package-lock.json b/package-lock.json index fd77488..b1d5734 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,8 @@ "prettier": "^2.0.5", "react": "*", "react-dom": "*", + "react-router": "^6.25.1", + "react-router-dom": "^6.25.1", "rimraf": "^3.0.2", "ts-jest": "^29.1.0", "typedoc": "^0.24.1", @@ -1857,6 +1859,16 @@ "node": ">= 8" } }, + "node_modules/@remix-run/router": { + "version": "1.18.0", + "resolved": "https://npm.nos.to/@remix-run%2frouter/-/router-1.18.0.tgz", + "integrity": "sha512-L3jkqmqoSVBVKHfpGZmLrex0lxR5SucGA0sUfFzGctehw+S/ggL9L/0NnC5mw6P8HUWpFZ3nQw3cRApjjWx9Sw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/pluginutils": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", @@ -7904,6 +7916,40 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.25.1", + "resolved": "https://npm.nos.to/react-router/-/react-router-6.25.1.tgz", + "integrity": "sha512-u8ELFr5Z6g02nUtpPAggP73Jigj1mRePSwhS/2nkTrlPU5yEkH1vYzWNyvSnSzeeE2DNqWdH+P8OhIh9wuXhTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.18.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.25.1", + "resolved": "https://npm.nos.to/react-router-dom/-/react-router-dom-6.25.1.tgz", + "integrity": "sha512-0tUDpbFvk35iv+N89dWNrJp+afLgd+y4VtorJZuOCXK0kkCWjEvb3vTJM++SYvMEpbVwXKf3FjeVveVEb6JpDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.18.0", + "react-router": "6.25.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", diff --git a/package.json b/package.json index 93b27bf..9830855 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,8 @@ "prettier": "^2.0.5", "react": "*", "react-dom": "*", + "react-router": "^6.25.1", + "react-router-dom": "^6.25.1", "rimraf": "^3.0.2", "ts-jest": "^29.1.0", "typedoc": "^0.24.1", diff --git a/spec/events.ts b/spec/events.ts new file mode 100644 index 0000000..1acf47b --- /dev/null +++ b/spec/events.ts @@ -0,0 +1,46 @@ +function createEvent(event: T) { + return { + cart_popup: false, + elements: [], + events: [], + response_mode: "JSON_ORIGINAL", + url: "http://localhost/", + ...event + } +} + +export function frontEvent() { + return createEvent({ + elements: [ + "frontpage-nosto-1", + "frontpage-nosto-3", + "frontpage-nosto-4", + ], + page_type: "front" + }) +} + +export function categoryEvent(category: string) { + return createEvent({ + elements: [ + "categorypage-nosto-1", + "categorypage-nosto-2", + ], + page_type: "category", + categories: [category] + }) +} + +export function productEvent(product: string) { + return createEvent({ + elements: [ + "productpage-nosto-1", + "productpage-nosto-2", + "productpage-nosto-3", + ], + events: [ + ["vp", product], + ], + page_type: "product" + }) +} diff --git a/spec/navigation.spec.tsx b/spec/navigation.spec.tsx new file mode 100644 index 0000000..0f3b55b --- /dev/null +++ b/spec/navigation.spec.tsx @@ -0,0 +1,92 @@ +import React from "react" +import { NostoCategory, NostoHome, NostoPlacement, NostoProduct, NostoProvider } from "../src" +import RecommendationComponent from "./renderer" +import { Link, MemoryRouter, Route, Routes, useParams } from "react-router-dom" +import { fireEvent, render, screen, waitFor } from "@testing-library/react" +import { WAIT_FOR_TIMEOUT } from "./utils" +import { categoryEvent, frontEvent, productEvent } from "./events" + +function HomePage() { + return <> + + + + + Hoodies + +} + +function CategoryPage() { + const { category } = useParams() + return <> + + + + Product 123 + Product 234 + Home + +} + +function ProductPage() { + const { product } = useParams() + return <> + + + + + Product 234 + Hoodies + Home + +} + +function Main() { + return }> + + + } /> + } /> + } /> + + + +} + +test("navigation events", async () => { + render(
) + + await waitFor(() => { + expect(screen.getAllByTestId("recommendation")).toHaveLength(3) + }, { timeout: WAIT_FOR_TIMEOUT }) + + const requests: unknown[] = [] + window.nostojs(api => api.listen("prerequest", req => requests.push(req))) + + // home -> category + fireEvent.click(screen.getByText("Hoodies")) + expect(requests).toEqual([ frontEvent(), categoryEvent("hoodies")]) + requests.length = 0 + + // category -> product + fireEvent.click(screen.getByText("Product 123")) + expect(requests).toEqual([ productEvent("123") ]) + requests.length = 0 + + // product -> product + fireEvent.click(screen.getByText("Product 234")) + expect(requests).toEqual([ productEvent("234") ]) + requests.length = 0 + + // product -> category + fireEvent.click(screen.getByText("Hoodies")) + expect(requests).toEqual([ categoryEvent("hoodies") ]) + requests.length = 0 + + // category -> home + fireEvent.click(screen.getByText("Home")) + expect(requests).toEqual([ frontEvent() ]) + requests.length = 0 +}) \ No newline at end of file diff --git a/src/components/NostoProvider.tsx b/src/components/NostoProvider.tsx index 75e175c..c7dc762 100644 --- a/src/components/NostoProvider.tsx +++ b/src/components/NostoProvider.tsx @@ -67,7 +67,7 @@ export default function NostoProvider(props: NostoProviderProps) { host, children, recommendationComponent, - shopifyMarkets, + shopifyMarkets } = props const [clientScriptLoadedState, setClientScriptLoadedState] = React.useState(false) const clientScriptLoaded = React.useMemo(() => clientScriptLoadedState, [clientScriptLoadedState]) diff --git a/src/types.ts b/src/types.ts index 0abb9e1..83300cc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -17,6 +17,7 @@ declare global { export interface NostoClient { setAutoLoad(autoload: boolean): void defaultSession(): Session + listen(event: string, callback: (data: unknown) => void): void placements: { getPlacements(): string[] injectCampaigns(recommendations: Record): void From 6c223cdc6ba81c4ab783700fc6ccce791a38fb68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20Westk=C3=A4mper?= Date: Mon, 22 Jul 2024 14:49:38 +0300 Subject: [PATCH 07/10] Use BrowserRouter for better test data --- spec/events.ts | 6 ++++-- spec/navigation.spec.tsx | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/spec/events.ts b/spec/events.ts index 1acf47b..c3f39ae 100644 --- a/spec/events.ts +++ b/spec/events.ts @@ -27,7 +27,8 @@ export function categoryEvent(category: string) { "categorypage-nosto-2", ], page_type: "category", - categories: [category] + categories: [category], + url: `http://localhost/collections/${category}` }) } @@ -41,6 +42,7 @@ export function productEvent(product: string) { events: [ ["vp", product], ], - page_type: "product" + page_type: "product", + url: `http://localhost/products/${product}` }) } diff --git a/spec/navigation.spec.tsx b/spec/navigation.spec.tsx index 0f3b55b..d62301f 100644 --- a/spec/navigation.spec.tsx +++ b/spec/navigation.spec.tsx @@ -1,7 +1,7 @@ import React from "react" import { NostoCategory, NostoHome, NostoPlacement, NostoProduct, NostoProvider } from "../src" import RecommendationComponent from "./renderer" -import { Link, MemoryRouter, Route, Routes, useParams } from "react-router-dom" +import { Link, BrowserRouter, Route, Routes, useParams } from "react-router-dom" import { fireEvent, render, screen, waitFor } from "@testing-library/react" import { WAIT_FOR_TIMEOUT } from "./utils" import { categoryEvent, frontEvent, productEvent } from "./events" @@ -45,13 +45,13 @@ function Main() { return }> - + } /> } /> } /> - + } From d25192190f2c565e50e2a13286f098d9f992d4b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20Westk=C3=A4mper?= Date: Mon, 22 Jul 2024 14:59:17 +0300 Subject: [PATCH 08/10] Cleanup tests --- spec/navigation.spec.tsx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/navigation.spec.tsx b/spec/navigation.spec.tsx index d62301f..ae8087a 100644 --- a/spec/navigation.spec.tsx +++ b/spec/navigation.spec.tsx @@ -64,29 +64,29 @@ test("navigation events", async () => { const requests: unknown[] = [] window.nostojs(api => api.listen("prerequest", req => requests.push(req))) + + function verifyEvents(given: unknown[]) { + expect(requests).toEqual(given) + requests.length = 0 + } // home -> category fireEvent.click(screen.getByText("Hoodies")) - expect(requests).toEqual([ frontEvent(), categoryEvent("hoodies")]) - requests.length = 0 + verifyEvents([ frontEvent(), categoryEvent("hoodies")]) // category -> product fireEvent.click(screen.getByText("Product 123")) - expect(requests).toEqual([ productEvent("123") ]) - requests.length = 0 + verifyEvents([ productEvent("123") ]) // product -> product fireEvent.click(screen.getByText("Product 234")) - expect(requests).toEqual([ productEvent("234") ]) - requests.length = 0 + verifyEvents([ productEvent("234") ]) // product -> category fireEvent.click(screen.getByText("Hoodies")) - expect(requests).toEqual([ categoryEvent("hoodies") ]) - requests.length = 0 + verifyEvents([ categoryEvent("hoodies") ]) // category -> home fireEvent.click(screen.getByText("Home")) - expect(requests).toEqual([ frontEvent() ]) - requests.length = 0 + verifyEvents([ frontEvent() ]) }) \ No newline at end of file From 39f96b24a09e7aff62b0b263bb6e2c13b7d745fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20Westk=C3=A4mper?= Date: Mon, 22 Jul 2024 15:10:43 +0300 Subject: [PATCH 09/10] Fix dependencies --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b1d5734..2e60466 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1861,7 +1861,7 @@ }, "node_modules/@remix-run/router": { "version": "1.18.0", - "resolved": "https://npm.nos.to/@remix-run%2frouter/-/router-1.18.0.tgz", + "resolved": "https://registry.npmjs.org/@remix-run%2frouter/-/router-1.18.0.tgz", "integrity": "sha512-L3jkqmqoSVBVKHfpGZmLrex0lxR5SucGA0sUfFzGctehw+S/ggL9L/0NnC5mw6P8HUWpFZ3nQw3cRApjjWx9Sw==", "dev": true, "license": "MIT", @@ -7918,7 +7918,7 @@ }, "node_modules/react-router": { "version": "6.25.1", - "resolved": "https://npm.nos.to/react-router/-/react-router-6.25.1.tgz", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.25.1.tgz", "integrity": "sha512-u8ELFr5Z6g02nUtpPAggP73Jigj1mRePSwhS/2nkTrlPU5yEkH1vYzWNyvSnSzeeE2DNqWdH+P8OhIh9wuXhTw==", "dev": true, "license": "MIT", @@ -7934,7 +7934,7 @@ }, "node_modules/react-router-dom": { "version": "6.25.1", - "resolved": "https://npm.nos.to/react-router-dom/-/react-router-dom-6.25.1.tgz", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.25.1.tgz", "integrity": "sha512-0tUDpbFvk35iv+N89dWNrJp+afLgd+y4VtorJZuOCXK0kkCWjEvb3vTJM++SYvMEpbVwXKf3FjeVveVEb6JpDQ==", "dev": true, "license": "MIT", From 630e404a210ff841d55609e05259f13bd4a86996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20Westk=C3=A4mper?= Date: Mon, 22 Jul 2024 15:15:13 +0300 Subject: [PATCH 10/10] Improve types --- spec/events.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/spec/events.ts b/spec/events.ts index c3f39ae..3622b75 100644 --- a/spec/events.ts +++ b/spec/events.ts @@ -1,4 +1,14 @@ -function createEvent(event: T) { +interface Event { + cart_popup?: boolean + elements?: string[] + events?: [string, string][] + response_mode?: string + url?: string + categories?: string[] + page_type?: string +} + +function createEvent(event: Event): Event { return { cart_popup: false, elements: [],