From daac019080176d115d39e82091dc1e4397c89c38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rokas=20Bar=C5=A1auskas?= Date: Wed, 6 Dec 2023 11:44:01 +0200 Subject: [PATCH] Add analytics tests --- spec/liquid.spec.ts | 219 +++++++++++++++++++++++++++++++++++++++--- spec/mustache.spec.ts | 52 ++++++++-- 2 files changed, 248 insertions(+), 23 deletions(-) diff --git a/spec/liquid.spec.ts b/spec/liquid.spec.ts index e18b435..ebb15aa 100644 --- a/spec/liquid.spec.ts +++ b/spec/liquid.spec.ts @@ -18,6 +18,12 @@ interface WindowWithNostoJS extends Window { [ callback: (api: { search: jest.Mock> + recordSearchSubmit: jest.Mock< + ReturnType + > + recordSearchClick: jest.Mock< + ReturnType + > }) => unknown, ] > @@ -79,15 +85,18 @@ const template = ` {% for hit in response.products.hits %} + href="javascript:void(0);" + data-ns-hit="{{ hit | json | escape }}" + data-testid="product" + > + {{ hit.name }} + class="ns-autocomplete-product-image" + src="{{ hit.imageUrl }}" + alt="{{ hit.name }}" + width="60" + height="40" + />
{% if hit.brand %}
@@ -155,9 +164,19 @@ const handleAutocomplete = async ( }) } +const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) + const w = window as unknown as WindowWithNostoJS -beforeAll(() => { +let searchSpy: jest.Mock> +let recordSearchSubmitSpy: jest.Mock< + ReturnType +> +let recordSearchClickSpy: jest.Mock< + ReturnType +> + +beforeEach(() => { document.body.innerHTML = `
@@ -170,24 +189,44 @@ beforeAll(() => { liquidScript.src = "https://cdn.jsdelivr.net/npm/liquidjs@10.9.3/dist/liquid.browser.min.js" document.body.appendChild(liquidScript) -}) -beforeEach(() => { - const searchSpy = jest.fn( + searchSpy = jest.fn( () => Promise.resolve(searchResponse) as unknown as ReturnType< NostoClient["search"] > ) + recordSearchSubmitSpy = jest.fn( + () => + Promise.resolve() as unknown as ReturnType< + NostoClient["recordSearchSubmit"] + > + ) + + recordSearchClickSpy = jest.fn( + () => + Promise.resolve() as unknown as ReturnType< + NostoClient["recordSearchClick"] + > + ) + w.nostojs = jest.fn( ( callback: (api: { search: jest.Mock> + recordSearchSubmit: jest.Mock< + ReturnType + > + recordSearchClick: jest.Mock< + ReturnType + > }) => unknown ) => callback({ search: searchSpy, + recordSearchSubmit: recordSearchSubmitSpy, + recordSearchClick: recordSearchClickSpy, }) ) }) @@ -197,9 +236,6 @@ afterEach(() => { const dropdown = screen.getByTestId("dropdown") const newElement = dropdown.cloneNode(true) dropdown?.parentNode?.replaceChild(newElement, dropdown) -}) - -afterAll(() => { document.body.innerHTML = "" }) @@ -409,6 +445,159 @@ describe("fromLiquidTemplate", () => { ) }) }) + + describe("analytics", () => { + it("should record search submit", async () => { + const user = userEvent.setup() + + await waitFor(() => + handleAutocomplete(fromLiquidTemplate(template)) + ) + + await user.type(screen.getByTestId("input"), "black") + await user.click(screen.getByTestId("search-button")) + + await waitFor(() => + expect(recordSearchSubmitSpy).toHaveBeenCalledWith("black") + ) + }) + + it("should record search submit with keyboard", async () => { + const user = userEvent.setup() + + await waitFor(() => + handleAutocomplete(fromLiquidTemplate(template)) + ) + + await user.type(screen.getByTestId("input"), "black") + await user.keyboard("{enter}") + + await waitFor(() => + expect(recordSearchSubmitSpy).toHaveBeenCalledWith("black") + ) + }) + + it("should record search click on keyword click", async () => { + const user = userEvent.setup() + + await waitFor(() => + handleAutocomplete(fromLiquidTemplate(template)) + ) + + await user.clear(screen.getByTestId("input")) + await user.type(screen.getByTestId("input"), "black") + + await waitFor(async () => { + await user.click(screen.getAllByTestId("keyword")?.[0]) + expect(recordSearchClickSpy).toHaveBeenCalledWith( + "autocomplete", + { + _highlight: { + keyword: "black", + }, + keyword: "black", + } + ) + }) + }) + + it("should record search click on product click", async () => { + const user = userEvent.setup() + + const oldLocation = w.location + // @ts-expect-error: mock location + delete w.location + w.location = { ...oldLocation, assign: jest.fn(() => ({})) } + + await waitFor(() => + handleAutocomplete(fromLiquidTemplate(template)) + ) + + await user.type(screen.getByTestId("input"), "black") + await wait(500) + await user.click(screen.getAllByTestId("product")?.[0]) + + await waitFor(() => { + expect(recordSearchClickSpy).toHaveBeenCalledWith( + "autocomplete", + { + brand: "Brand", + imageUrl: + "https://cdn.shopify.com/s/files/1/0097/5821/2174/products/Perry-Ellis-mens-Slim-Fit-Washable-Suit-Pant-Suit-Pants-Black-black.jpg?v=1675970770", + listPrice: 150, + name: "Slim Fit Washable Suit Pant", + price: 40, + productId: "6853920686158", + url: "https://www.perryellis.com/products/slim-fit-washable-suit-pant-black-4isb4316rt-010", + } + ) + }) + }) + + it("should record search click on keyword submitted with keyboard", async () => { + const user = userEvent.setup() + + await waitFor(() => + handleAutocomplete(fromLiquidTemplate(template)) + ) + + await user.type(screen.getByTestId("input"), "black") + await wait(500) + await user.keyboard("{arrowdown}") + await user.keyboard("{enter}") + + await waitFor(() => + expect(recordSearchClickSpy).toHaveBeenCalledWith( + "autocomplete", + { + _highlight: { + keyword: "black", + }, + keyword: "black", + } + ) + ) + }) + + it("should record search click on product submitted with keyboard", async () => { + const user = userEvent.setup() + + const oldLocation = w.location + // @ts-expect-error: mock location + delete w.location + w.location = { ...oldLocation, assign: jest.fn(() => ({})) } + + await waitFor(() => + handleAutocomplete(fromLiquidTemplate(template)) + ) + + await user.type(screen.getByTestId("input"), "black") + await wait(500) + await user.keyboard("{arrowdown}") + await user.keyboard("{arrowdown}") + await user.keyboard("{arrowdown}") + await user.keyboard("{arrowdown}") + await user.keyboard("{arrowdown}") + await user.keyboard("{arrowdown}") + await user.keyboard("{enter}") + + await waitFor(() => { + expect(recordSearchClickSpy).toHaveBeenCalledWith( + "autocomplete", + { + brand: "Brand", + imageUrl: + "https://cdn.shopify.com/s/files/1/0097/5821/2174/products/Perry-Ellis-mens-Slim-Fit-Washable-Suit-Pant-Suit-Pants-Black-black.jpg?v=1675970770", + listPrice: 150, + name: "Slim Fit Washable Suit Pant", + price: 40, + productId: "6853920686158", + url: "https://www.perryellis.com/products/slim-fit-washable-suit-pant-black-4isb4316rt-010", + } + ) + }) + }) + }) }) describe("fromRemoteLiquidTemplate", () => { diff --git a/spec/mustache.spec.ts b/spec/mustache.spec.ts index 06b1bd7..07c9923 100644 --- a/spec/mustache.spec.ts +++ b/spec/mustache.spec.ts @@ -7,10 +7,10 @@ import { fromMustacheTemplate, fromRemoteMustacheTemplate, } from "../src/mustache" -import { NostoClient } from '../src/api/client' -import { AutocompleteConfig } from '../src/config' -import { DefaultState } from '../src/utils/state' -import { autocomplete } from '../src/autocomplete' +import { NostoClient } from "../src/api/client" +import { AutocompleteConfig } from "../src/config" +import { DefaultState } from "../src/utils/state" +import { autocomplete } from "../src/autocomplete" interface WindowWithNostoJS extends Window { nostojs: jest.Mock< @@ -18,6 +18,12 @@ interface WindowWithNostoJS extends Window { [ callback: (api: { search: jest.Mock> + recordSearchSubmit: jest.Mock< + ReturnType + > + recordSearchClick: jest.Mock< + ReturnType + > }) => unknown, ] > @@ -146,9 +152,19 @@ const handleAutocomplete = ( }) } +const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) + const w = window as unknown as WindowWithNostoJS -beforeAll(() => { +let searchSpy: jest.Mock> +let recordSearchSubmitSpy: jest.Mock< + ReturnType +> +let recordSearchClickSpy: jest.Mock< + ReturnType +> + +beforeEach(() => { document.body.innerHTML = ` @@ -160,24 +176,44 @@ beforeAll(() => { const mustacheScript = document.createElement("script") mustacheScript.src = "https://unpkg.com/mustache@4.2.0/mustache.min.js" document.body.appendChild(mustacheScript) -}) -beforeEach(() => { - const searchSpy = jest.fn( + searchSpy = jest.fn( () => Promise.resolve(searchResponse) as unknown as ReturnType< NostoClient["search"] > ) + recordSearchSubmitSpy = jest.fn( + () => + Promise.resolve() as unknown as ReturnType< + NostoClient["recordSearchSubmit"] + > + ) + + recordSearchClickSpy = jest.fn( + () => + Promise.resolve() as unknown as ReturnType< + NostoClient["recordSearchClick"] + > + ) + w.nostojs = jest.fn( ( callback: (api: { search: jest.Mock> + recordSearchSubmit: jest.Mock< + ReturnType + > + recordSearchClick: jest.Mock< + ReturnType + > }) => unknown ) => callback({ search: searchSpy, + recordSearchSubmit: recordSearchSubmitSpy, + recordSearchClick: recordSearchClickSpy, }) ) })