@askdialog/dialog-sdk 2.9.1 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +44 -10
  2. package/dist/Dialog.d.ts +21 -6
  3. package/dist/Dialog.d.ts.map +1 -1
  4. package/dist/Dialog.js +72 -12
  5. package/dist/__tests__/dialogCurrentProduct.spec.d.ts +2 -0
  6. package/dist/__tests__/dialogCurrentProduct.spec.d.ts.map +1 -0
  7. package/dist/__tests__/dialogCurrentProduct.spec.js +134 -0
  8. package/dist/__tests__/dialogSearchAnalytics.spec.js +1 -0
  9. package/dist/__tests__/publicTypes.spec.js +2 -1
  10. package/dist/__tests__/searchController.spec.js +126 -35
  11. package/dist/__tests__/searchControllerAttribution.spec.js +30 -26
  12. package/dist/__tests__/searchImpressions.spec.js +3 -20
  13. package/dist/__tests__/searchService.spec.js +65 -60
  14. package/dist/config/config.development.js +1 -1
  15. package/dist/config/config.production.js +1 -1
  16. package/dist/config/index.js +1 -1
  17. package/dist/searchController.d.ts +1 -1
  18. package/dist/searchController.d.ts.map +1 -1
  19. package/dist/searchController.js +34 -24
  20. package/dist/services/search.d.ts +3 -2
  21. package/dist/services/search.d.ts.map +1 -1
  22. package/dist/services/search.js +4 -9
  23. package/dist/types/constructor.d.ts +15 -0
  24. package/dist/types/constructor.d.ts.map +1 -1
  25. package/dist/types/search.d.ts +24 -22
  26. package/dist/types/search.d.ts.map +1 -1
  27. package/dist/types/search.js +7 -4
  28. package/dist/types/searchAnalytics.d.ts +2 -0
  29. package/dist/types/searchAnalytics.d.ts.map +1 -1
  30. package/dist/types/searchController.d.ts +13 -5
  31. package/dist/types/searchController.d.ts.map +1 -1
  32. package/dist/utils/searchControllerAnalytics.d.ts +4 -4
  33. package/dist/utils/searchControllerAnalytics.d.ts.map +1 -1
  34. package/dist/utils/searchControllerAnalytics.js +14 -16
  35. package/dist/utils/searchImpressions.d.ts.map +1 -1
  36. package/dist/utils/searchImpressions.js +1 -6
  37. package/dist/utils/searchSections.d.ts +5 -0
  38. package/dist/utils/searchSections.d.ts.map +1 -0
  39. package/dist/utils/searchSections.js +21 -0
  40. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- /* eslint max-lines: ["error", 400] */
1
+ /* eslint max-lines: ["error", 460] */
2
2
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import { createSearchController } from "../searchController";
4
4
  import { SearchStatus } from "../types/searchController";
@@ -13,18 +13,20 @@ vi.mock("../utils/searchImpressions", () => ({
13
13
  }));
14
14
  const DEBOUNCE_MS = 250;
15
15
  const response = (overrides = {}) => ({
16
- queryId: "qid-1",
17
- hits: [
18
- { id: "h1", score: 1, product: { id: "p1" } },
19
- { id: "h2", score: 0.9, product: { id: "p2" } },
16
+ results: [
17
+ {
18
+ index: "products_fr",
19
+ hits: [{ objectID: "p1" }, { objectID: "p2" }],
20
+ nbHits: 2,
21
+ page: 0,
22
+ nbPages: 3,
23
+ hitsPerPage: 12,
24
+ processingTimeMS: 5,
25
+ query: "shoes",
26
+ queryID: "qid-1",
27
+ ...overrides,
28
+ },
20
29
  ],
21
- nbHits: 2,
22
- page: 0,
23
- nbPages: 3,
24
- hitsPerPage: 12,
25
- processingTimeMs: 5,
26
- query: "shoes",
27
- ...overrides,
28
30
  });
29
31
  const deferred = () => {
30
32
  let resolve;
@@ -52,6 +54,7 @@ const createController = () => {
52
54
  trackViewSearchResults,
53
55
  trackSelectSearchResult,
54
56
  },
57
+ locale: "fr",
55
58
  });
56
59
  controller.subscribe((state) => states.push(state));
57
60
  return controller;
@@ -84,33 +87,31 @@ describe("createSearchController", () => {
84
87
  vi.advanceTimersByTime(DEBOUNCE_MS);
85
88
  await settle();
86
89
  expect(search).toHaveBeenCalledTimes(1);
87
- expect(search.mock.calls[0][0]).toMatchObject({ query: "shoes", page: 0 });
90
+ expect(search.mock.calls[0][0].requests[0]).toMatchObject({
91
+ query: "shoes",
92
+ page: 0,
93
+ });
88
94
  expect(controller.getState().status).toBe(SearchStatus.SUCCESS);
89
95
  });
90
- it("stamps the configured market on every request, and omits it otherwise", async () => {
91
- const marketController = createSearchController({
96
+ it("sends one products entry named after the bare-language locale", async () => {
97
+ const controller = createSearchController({
92
98
  search,
93
99
  analytics: {
94
100
  surface: "search_page",
95
101
  trackViewSearchResults,
96
102
  trackSelectSearchResult,
97
103
  },
98
- locale: "fr",
99
- countryCode: "CA",
104
+ locale: "fr-FR",
100
105
  });
101
106
  search.mockResolvedValue(response({ query: "shoes" }));
102
- marketController.submit("shoes");
107
+ controller.submit("shoes");
103
108
  await settle();
104
- expect(search.mock.calls[0][0]).toMatchObject({
105
- locale: "fr",
106
- countryCode: "CA",
109
+ expect(search.mock.calls[0][0]).toEqual({
110
+ requests: [
111
+ { indexName: "products_fr", query: "shoes", page: 0, hitsPerPage: 12 },
112
+ ],
107
113
  });
108
- marketController.dispose();
109
- const bareController = createController();
110
- bareController.submit("shoes");
111
- await settle();
112
- expect(search.mock.calls[1][0]).not.toHaveProperty("locale");
113
- expect(search.mock.calls[1][0]).not.toHaveProperty("countryCode");
114
+ controller.dispose();
114
115
  });
115
116
  it("submits immediately without waiting for the debounce", async () => {
116
117
  const controller = createController();
@@ -119,7 +120,9 @@ describe("createSearchController", () => {
119
120
  controller.submit("shoes");
120
121
  await settle();
121
122
  expect(search).toHaveBeenCalledTimes(1);
122
- expect(search.mock.calls[0][0]).toMatchObject({ query: "shoes" });
123
+ expect(search.mock.calls[0][0].requests[0]).toMatchObject({
124
+ query: "shoes",
125
+ });
123
126
  expect(controller.getState().status).toBe(SearchStatus.SUCCESS);
124
127
  });
125
128
  it("aborts the previous in-flight request when a new one starts", async () => {
@@ -133,7 +136,7 @@ describe("createSearchController", () => {
133
136
  const firstSignal = search.mock.calls[0][1]?.signal;
134
137
  controller.submit("boots");
135
138
  expect(firstSignal?.aborted).toBe(true);
136
- second.resolve(response({ query: "boots", queryId: "qid-2" }));
139
+ second.resolve(response({ query: "boots", queryID: "qid-2" }));
137
140
  await settle();
138
141
  expect(controller.getState().response?.query).toBe("boots");
139
142
  });
@@ -146,10 +149,10 @@ describe("createSearchController", () => {
146
149
  .mockReturnValueOnce(fresh.promise);
147
150
  controller.submit("shoes");
148
151
  controller.submit("boots");
149
- fresh.resolve(response({ query: "boots", queryId: "qid-2" }));
152
+ fresh.resolve(response({ query: "boots", queryID: "qid-2" }));
150
153
  await settle();
151
154
  // The stale transport ignored the abort and answers after the fresh one.
152
- stale.resolve(response({ query: "shoes", queryId: "qid-1" }));
155
+ stale.resolve(response({ query: "shoes", queryID: "qid-1" }));
153
156
  await settle();
154
157
  expect(controller.getState().status).toBe(SearchStatus.SUCCESS);
155
158
  expect(controller.getState().response?.query).toBe("boots");
@@ -159,7 +162,7 @@ describe("createSearchController", () => {
159
162
  const first = deferred();
160
163
  search
161
164
  .mockReturnValueOnce(first.promise)
162
- .mockResolvedValueOnce(response({ query: "boots", queryId: "qid-2" }));
165
+ .mockResolvedValueOnce(response({ query: "boots", queryID: "qid-2" }));
163
166
  controller.submit("shoes");
164
167
  controller.submit("boots");
165
168
  first.reject(new DOMException("The operation was aborted.", "AbortError"));
@@ -186,7 +189,10 @@ describe("createSearchController", () => {
186
189
  controller.retry();
187
190
  await settle();
188
191
  expect(search).toHaveBeenCalledTimes(3);
189
- expect(search.mock.calls[2][0]).toMatchObject({ query: "shoes", page: 2 });
192
+ expect(search.mock.calls[2][0].requests[0]).toMatchObject({
193
+ query: "shoes",
194
+ page: 2,
195
+ });
190
196
  expect(controller.getState().status).toBe(SearchStatus.SUCCESS);
191
197
  });
192
198
  it("ignores retry outside the error state", async () => {
@@ -225,7 +231,10 @@ describe("createSearchController", () => {
225
231
  await settle();
226
232
  controller.submit("boots");
227
233
  await settle();
228
- expect(search.mock.calls[2][0]).toMatchObject({ query: "boots", page: 0 });
234
+ expect(search.mock.calls[2][0].requests[0]).toMatchObject({
235
+ query: "boots",
236
+ page: 0,
237
+ });
229
238
  });
230
239
  it("flushes a pending debounced query instead of paginating stale results", async () => {
231
240
  const controller = createController();
@@ -236,7 +245,10 @@ describe("createSearchController", () => {
236
245
  controller.setPage(1);
237
246
  await settle();
238
247
  expect(search).toHaveBeenCalledTimes(2);
239
- expect(search.mock.calls[1][0]).toMatchObject({ query: "boots", page: 0 });
248
+ expect(search.mock.calls[1][0].requests[0]).toMatchObject({
249
+ query: "boots",
250
+ page: 0,
251
+ });
240
252
  });
241
253
  it("ignores pagination without a committed query", () => {
242
254
  const controller = createController();
@@ -276,4 +288,83 @@ describe("createSearchController", () => {
276
288
  expect(search).toHaveBeenCalledTimes(1);
277
289
  expect(controller.getState().status).toBe(SearchStatus.LOADING);
278
290
  });
291
+ it("requests the sections on their first page with the products and exposes their entries", async () => {
292
+ const controller = createSearchController({
293
+ search,
294
+ analytics: {
295
+ surface: "search_page",
296
+ trackViewSearchResults,
297
+ trackSelectSearchResult,
298
+ },
299
+ locale: "fr",
300
+ sections: [{ index: "collections", hitsPerPage: 5 }],
301
+ });
302
+ const collections = {
303
+ ...response().results[0],
304
+ index: "collections_fr",
305
+ hits: [{ objectID: "c1" }],
306
+ nbHits: 1,
307
+ };
308
+ search.mockResolvedValueOnce({
309
+ results: [...response().results, collections],
310
+ });
311
+ controller.submit("shoes");
312
+ await settle();
313
+ expect(search.mock.calls[0][0].requests).toEqual([
314
+ { indexName: "products_fr", query: "shoes", page: 0, hitsPerPage: 12 },
315
+ { indexName: "collections_fr", query: "shoes", page: 0, hitsPerPage: 5 },
316
+ ]);
317
+ expect(controller.getState().response?.index).toBe("products_fr");
318
+ expect(controller.getState().sections).toEqual({ collections });
319
+ });
320
+ it("resolves a sections function per request and leaves a missing entry out", async () => {
321
+ let enabled = false;
322
+ const controller = createSearchController({
323
+ search,
324
+ analytics: {
325
+ surface: "search_page",
326
+ trackViewSearchResults,
327
+ trackSelectSearchResult,
328
+ },
329
+ locale: "fr",
330
+ sections: () => (enabled ? [{ index: "collections" }] : []),
331
+ });
332
+ search.mockResolvedValue(response());
333
+ controller.submit("shoes");
334
+ await settle();
335
+ enabled = true;
336
+ controller.submit("boots");
337
+ await settle();
338
+ expect(search.mock.calls[0][0].requests).toHaveLength(1);
339
+ expect(search.mock.calls[1][0].requests).toHaveLength(2);
340
+ expect(search.mock.calls[1][0].requests[1]).toMatchObject({
341
+ indexName: "collections_fr",
342
+ hitsPerPage: 12,
343
+ });
344
+ expect(controller.getState().sections).toEqual({});
345
+ });
346
+ it("enters the error state when the sections resolver throws", async () => {
347
+ const failure = new Error("entitlement lookup failed");
348
+ const controller = createSearchController({
349
+ search,
350
+ analytics: {
351
+ surface: "search_page",
352
+ trackViewSearchResults,
353
+ trackSelectSearchResult,
354
+ },
355
+ locale: "fr",
356
+ sections: () => {
357
+ throw failure;
358
+ },
359
+ });
360
+ controller.submit("shoes");
361
+ await settle();
362
+ expect(search).not.toHaveBeenCalled();
363
+ expect(controller.getState()).toMatchObject({
364
+ status: SearchStatus.ERROR,
365
+ error: failure,
366
+ response: undefined,
367
+ sections: undefined,
368
+ });
369
+ });
279
370
  });
@@ -12,22 +12,23 @@ vi.mock("../utils/searchImpressions", () => ({
12
12
  createSearchImpressionTracker: () => tracker,
13
13
  }));
14
14
  const response = (overrides = {}) => ({
15
- queryId: "qid-1",
16
- hits: [
15
+ results: [
17
16
  {
18
- id: "h1",
19
- score: 1,
20
- product: { id: "p1", url: "https://shop.example/p1" },
17
+ index: "products_fr",
18
+ hits: [
19
+ { objectID: "p1", url: "https://shop.example/p1" },
20
+ { objectID: "p2" },
21
+ ],
22
+ nbHits: 30,
23
+ page: 0,
24
+ nbPages: 3,
25
+ hitsPerPage: 12,
26
+ processingTimeMS: 5,
27
+ query: "shoes",
28
+ queryID: "qid-1",
29
+ ...overrides,
21
30
  },
22
- { id: "h2", score: 0.9, product: { id: "p2" } },
23
31
  ],
24
- nbHits: 30,
25
- page: 0,
26
- nbPages: 3,
27
- hitsPerPage: 12,
28
- processingTimeMs: 5,
29
- query: "shoes",
30
- ...overrides,
31
32
  });
32
33
  const search = vi.fn();
33
34
  const trackViewSearchResults = vi.fn();
@@ -41,6 +42,7 @@ const createController = () => createSearchController({
41
42
  trackSelectSearchResult,
42
43
  },
43
44
  navigate,
45
+ locale: "fr",
44
46
  });
45
47
  beforeEach(() => {
46
48
  vi.useFakeTimers();
@@ -54,27 +56,26 @@ const settle = async () => {
54
56
  await Promise.resolve();
55
57
  };
56
58
  describe("search controller attribution", () => {
57
- it("omits queryId for a new query and resends it while the query is unchanged", async () => {
59
+ it("never resends a query id: each request is its own query", async () => {
58
60
  const controller = createController();
59
61
  search.mockResolvedValue(response());
60
62
  controller.submit("shoes");
61
63
  await settle();
62
64
  controller.setPage(1);
63
65
  await settle();
64
- expect(search.mock.calls[0][0].queryId).toBeUndefined();
65
- expect(search.mock.calls[1][0]).toMatchObject({
66
- query: "shoes",
67
- page: 1,
68
- queryId: "qid-1",
69
- });
66
+ for (const [request] of search.mock.calls) {
67
+ expect(request.requests[0]).not.toHaveProperty("queryId");
68
+ expect(request.requests[0]).not.toHaveProperty("queryID");
69
+ }
70
70
  });
71
- it("declares the rendered response's envelope to the impression tracker", async () => {
71
+ it("declares the rendered result's envelope to the impression tracker", async () => {
72
72
  const controller = createController();
73
73
  search.mockResolvedValue(response({ page: 1, query: "shoes" }));
74
74
  controller.submit("shoes");
75
75
  await settle();
76
76
  expect(tracker.setContext).toHaveBeenCalledWith({
77
77
  query_id: "qid-1",
78
+ index: "products_fr",
78
79
  surface: "search_page",
79
80
  search_type: "lexical",
80
81
  page: 2,
@@ -82,12 +83,12 @@ describe("search controller attribution", () => {
82
83
  query_length: 5,
83
84
  });
84
85
  });
85
- it("keeps query_id stable across pagination and rotates it on a new query", async () => {
86
+ it("follows the response's queryID: every page is its own query", async () => {
86
87
  const controller = createController();
87
88
  search
88
89
  .mockResolvedValueOnce(response())
89
- .mockResolvedValueOnce(response({ page: 1, queryId: "qid-1" }))
90
- .mockResolvedValueOnce(response({ query: "boots", queryId: "qid-2" }));
90
+ .mockResolvedValueOnce(response({ page: 1, queryID: "qid-2" }))
91
+ .mockResolvedValueOnce(response({ query: "boots", queryID: "qid-3" }));
91
92
  controller.submit("shoes");
92
93
  await settle();
93
94
  controller.setPage(1);
@@ -95,7 +96,7 @@ describe("search controller attribution", () => {
95
96
  controller.submit("boots");
96
97
  await settle();
97
98
  const queryIds = tracker.setContext.mock.calls.map(([envelope]) => envelope.query_id);
98
- expect(queryIds).toEqual(["qid-1", "qid-1", "qid-2"]);
99
+ expect(queryIds).toEqual(["qid-1", "qid-2", "qid-3"]);
99
100
  });
100
101
  it("emits the zero-items view event for a rendered no-results state", async () => {
101
102
  const controller = createController();
@@ -104,6 +105,7 @@ describe("search controller attribution", () => {
104
105
  await settle();
105
106
  expect(trackViewSearchResults).toHaveBeenCalledWith({
106
107
  query_id: "qid-1",
108
+ index: "products_fr",
107
109
  surface: "search_page",
108
110
  search_type: "lexical",
109
111
  page: 1,
@@ -149,6 +151,7 @@ describe("search controller attribution", () => {
149
151
  });
150
152
  expect(trackSelectSearchResult).toHaveBeenCalledWith({
151
153
  query_id: "qid-1",
154
+ index: "products_fr",
152
155
  surface: "search_page",
153
156
  search_type: "lexical",
154
157
  page: 1,
@@ -156,7 +159,7 @@ describe("search controller attribution", () => {
156
159
  query_length: 5,
157
160
  items: [{ product_id: "p1", position: 1 }],
158
161
  });
159
- expect(navigate).toHaveBeenCalledWith("https://shop.example/p1", landed.hits[0]);
162
+ expect(navigate).toHaveBeenCalledWith("https://shop.example/p1", landed.results[0].hits[0]);
160
163
  expect(order).toEqual(["impression", "select", "navigate"]);
161
164
  });
162
165
  it("still records attribution when the hit has no URL to navigate to", async () => {
@@ -203,6 +206,7 @@ describe("search controller attribution", () => {
203
206
  trackViewSearchResults,
204
207
  trackSelectSearchResult,
205
208
  },
209
+ locale: "fr",
206
210
  });
207
211
  search.mockResolvedValue(response());
208
212
  controller.submit("shoes");
@@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import { createSearchImpressionTracker } from "../utils/searchImpressions";
4
4
  const envelope = {
5
5
  query_id: "query-1",
6
+ index: "products_fr",
6
7
  surface: "search_page",
7
8
  search_type: "lexical",
8
9
  page: 1,
@@ -133,7 +134,7 @@ describe("createSearchImpressionTracker", () => {
133
134
  tracker.forceImpression({ product_id: "product-1", position: 1 });
134
135
  expect(emit).toHaveBeenCalledTimes(1);
135
136
  });
136
- it("keeps deduplication across pagination of the same query", () => {
137
+ it("restarts deduplication on every context change: each response mints a fresh query_id", () => {
137
138
  const emit = vi.fn();
138
139
  const tracker = createSearchImpressionTracker({ emit });
139
140
  tracker.setContext(envelope);
@@ -141,29 +142,11 @@ describe("createSearchImpressionTracker", () => {
141
142
  tracker.observe(card, { product_id: "product-1", position: 1 });
142
143
  intersect(card, 0.6);
143
144
  vi.advanceTimersByTime(2000);
144
- tracker.setContext({ ...envelope, page: 2 });
145
+ tracker.setContext({ ...envelope, query_id: "query-2", page: 2 });
145
146
  const cardOnPage2 = element();
146
147
  tracker.observe(cardOnPage2, { product_id: "product-1", position: 1 });
147
148
  intersect(cardOnPage2, 0.6);
148
149
  vi.advanceTimersByTime(2000);
149
- expect(emit).toHaveBeenCalledTimes(1);
150
- });
151
- it("drops a previous query's dedup keys when the query changes", () => {
152
- const emit = vi.fn();
153
- const tracker = createSearchImpressionTracker({ emit });
154
- tracker.setContext(envelope);
155
- const card = element();
156
- tracker.observe(card, { product_id: "product-1", position: 1 });
157
- intersect(card, 0.6);
158
- vi.advanceTimersByTime(2000);
159
- tracker.setContext({ ...envelope, query_id: "query-2" });
160
- // Same query_id served again (e.g. a cached response): a fresh render is
161
- // a new exposure, like the bfcache restore.
162
- tracker.setContext(envelope);
163
- const cardAgain = element();
164
- tracker.observe(cardAgain, { product_id: "product-1", position: 1 });
165
- intersect(cardAgain, 0.6);
166
- vi.advanceTimersByTime(2000);
167
150
  expect(emit).toHaveBeenCalledTimes(2);
168
151
  });
169
152
  it("flushes qualified impressions on pagehide", () => {
@@ -1,16 +1,31 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
2
  import { DialogSearchError } from "../DialogSearchError";
3
- import { searchProducts } from "../services/search";
3
+ import { searchIndexName, searchLexical } from "../services/search";
4
4
  const API_KEY = "pk_test_abcdef";
5
+ const request = {
6
+ requests: [
7
+ {
8
+ indexName: "products_fr",
9
+ query: "running shoes",
10
+ page: 2,
11
+ hitsPerPage: 50,
12
+ },
13
+ ],
14
+ };
5
15
  const emptyResponse = {
6
- queryId: "0198c3f2-0000-7000-8000-000000000000",
7
- hits: [],
8
- nbHits: 0,
9
- page: 0,
10
- nbPages: 0,
11
- hitsPerPage: 20,
12
- processingTimeMs: 3,
13
- query: "running shoes",
16
+ results: [
17
+ {
18
+ index: "products_fr",
19
+ hits: [],
20
+ nbHits: 0,
21
+ page: 0,
22
+ nbPages: 0,
23
+ hitsPerPage: 20,
24
+ processingTimeMS: 3,
25
+ query: "running shoes",
26
+ queryID: "0198c3f2-0000-7000-8000-000000000000",
27
+ },
28
+ ],
14
29
  };
15
30
  const jsonResponse = (body, status = 200) => new Response(JSON.stringify(body), {
16
31
  status,
@@ -24,86 +39,76 @@ afterEach(() => {
24
39
  vi.unstubAllGlobals();
25
40
  vi.clearAllMocks();
26
41
  });
27
- describe("searchProducts", () => {
28
- it("POSTs the exact request to /public/search with only the api key header", async () => {
42
+ describe("searchIndexName", () => {
43
+ it.each([
44
+ ["fr-FR", "products_fr"],
45
+ ["pt-BR", "products_pt"],
46
+ ["fr", "products_fr"],
47
+ ["not a locale", "products_not a locale"],
48
+ ])("reduces the locale %s to a bare-language index name", (locale, name) => {
49
+ expect(searchIndexName("products", locale)).toBe(name);
50
+ });
51
+ it("names every logical index", () => {
52
+ expect(searchIndexName("collections", "en-US")).toBe("collections_en");
53
+ });
54
+ });
55
+ describe("searchLexical", () => {
56
+ it("POSTs the exact request to /public/search/lexical with only the api key header", async () => {
29
57
  fetchMock.mockResolvedValue(jsonResponse(emptyResponse));
30
- await searchProducts(API_KEY, {
31
- query: "running shoes",
32
- page: 2,
33
- hitsPerPage: 50,
34
- queryId: "0198c3f2-0000-7000-8000-000000000000",
35
- });
58
+ await searchLexical(API_KEY, request);
36
59
  expect(fetchMock).toHaveBeenCalledTimes(1);
37
60
  const [url, init] = fetchMock.mock.calls[0];
38
- expect(url).toMatch(/\/public\/search$/);
61
+ expect(url).toMatch(/\/public\/search\/lexical$/);
39
62
  expect(init.method).toBe("POST");
40
63
  expect(init.headers).toEqual({
41
64
  "Content-Type": "application/json",
42
65
  "x-dialog-api-key": API_KEY,
43
66
  });
44
- expect(JSON.parse(init.body)).toEqual({
45
- query: "running shoes",
46
- page: 2,
47
- hitsPerPage: 50,
48
- queryId: "0198c3f2-0000-7000-8000-000000000000",
49
- });
67
+ expect(JSON.parse(init.body)).toEqual(request);
50
68
  });
51
- it.each([
52
- ["fr-FR", "fr"],
53
- ["pt-BR", "pt"],
54
- ["fr", "fr"],
55
- ["not a locale", "not a locale"],
56
- ])("sends the locale %s as its bare ISO 639-1 language %s", async (locale, expected) => {
69
+ it("keeps optional per-entry fields the caller did not provide off the wire", async () => {
57
70
  fetchMock.mockResolvedValue(jsonResponse(emptyResponse));
58
- await searchProducts(API_KEY, { query: "running shoes", locale });
59
- const [, init] = fetchMock.mock.calls[0];
60
- expect(JSON.parse(init.body)).toEqual({
61
- query: "running shoes",
62
- locale: expected,
71
+ await searchLexical(API_KEY, {
72
+ requests: [{ indexName: "products_fr", query: "running shoes" }],
63
73
  });
64
- });
65
- it("omits pagination fields and queryId the caller did not provide", async () => {
66
- fetchMock.mockResolvedValue(jsonResponse(emptyResponse));
67
- await searchProducts(API_KEY, { query: "running shoes" });
68
74
  const [, init] = fetchMock.mock.calls[0];
69
75
  expect(JSON.parse(init.body)).toEqual({
70
- query: "running shoes",
76
+ requests: [{ indexName: "products_fr", query: "running shoes" }],
71
77
  });
72
78
  });
73
79
  it("resolves the typed response, including empty results", async () => {
74
80
  fetchMock.mockResolvedValue(jsonResponse(emptyResponse));
75
- await expect(searchProducts(API_KEY, { query: "running shoes" })).resolves.toEqual(emptyResponse);
81
+ await expect(searchLexical(API_KEY, request)).resolves.toEqual(emptyResponse);
76
82
  });
77
83
  it.each([
78
- [401, "INVALID_WIDGET_API_KEY", "Invalid widget API key"],
79
- [
80
- 422,
81
- "VALIDATION_ERROR",
82
- "query must contain at least 2 visible characters",
83
- ],
84
- [429, "THROTTLED", "Too many requests"],
85
- [503, "STOREFRONT_SEARCH_UNAVAILABLE", "Storefront search is unavailable"],
86
- ])("rejects a %i answer with a DialogSearchError carrying status, code and message", async (status, code, message) => {
87
- fetchMock.mockResolvedValue(jsonResponse({ statusCode: status, error: code, message }, status));
88
- const error = await searchProducts(API_KEY, {
89
- query: "running shoes",
90
- }).catch((caught) => caught);
84
+ [404, "Index products_xx does not exist"],
85
+ [400, "Unknown parameter: foo"],
86
+ ])("rejects the Algolia-shaped %i error body with a DialogSearchError", async (status, message) => {
87
+ fetchMock.mockResolvedValue(jsonResponse({ message, status }, status));
88
+ const error = await searchLexical(API_KEY, request).catch((caught) => caught);
91
89
  expect(error).toBeInstanceOf(DialogSearchError);
92
90
  expect(error).toMatchObject({
93
91
  name: "DialogSearchError",
94
92
  status,
95
- code,
93
+ code: undefined,
96
94
  message,
97
95
  });
98
96
  });
97
+ it.each([
98
+ [401, "INVALID_WIDGET_API_KEY", "Invalid widget API key"],
99
+ [429, "THROTTLED", "Too many requests"],
100
+ ])("still reads the code of a Nest-shaped %i guard error", async (status, code, message) => {
101
+ fetchMock.mockResolvedValue(jsonResponse({ statusCode: status, error: code, message }, status));
102
+ const error = await searchLexical(API_KEY, request).catch((caught) => caught);
103
+ expect(error).toBeInstanceOf(DialogSearchError);
104
+ expect(error).toMatchObject({ status, code, message });
105
+ });
99
106
  it("falls back to statusText when the error body is not JSON", async () => {
100
107
  fetchMock.mockResolvedValue(new Response("<html>Bad Gateway</html>", {
101
108
  status: 502,
102
109
  statusText: "Bad Gateway",
103
110
  }));
104
- const error = await searchProducts(API_KEY, {
105
- query: "running shoes",
106
- }).catch((caught) => caught);
111
+ const error = await searchLexical(API_KEY, request).catch((caught) => caught);
107
112
  expect(error).toBeInstanceOf(DialogSearchError);
108
113
  expect(error).toMatchObject({
109
114
  status: 502,
@@ -114,13 +119,13 @@ describe("searchProducts", () => {
114
119
  it("propagates network failures untouched", async () => {
115
120
  const networkError = new TypeError("Failed to fetch");
116
121
  fetchMock.mockRejectedValue(networkError);
117
- await expect(searchProducts(API_KEY, { query: "running shoes" })).rejects.toBe(networkError);
122
+ await expect(searchLexical(API_KEY, request)).rejects.toBe(networkError);
118
123
  });
119
124
  it("forwards the AbortSignal and preserves the native AbortError", async () => {
120
125
  const abortError = new DOMException("The operation was aborted.", "AbortError");
121
126
  fetchMock.mockRejectedValue(abortError);
122
127
  const controller = new AbortController();
123
- await expect(searchProducts(API_KEY, { query: "running shoes" }, { signal: controller.signal })).rejects.toBe(abortError);
128
+ await expect(searchLexical(API_KEY, request, { signal: controller.signal })).rejects.toBe(abortError);
124
129
  const [, init] = fetchMock.mock.calls[0];
125
130
  expect(init.signal).toBe(controller.signal);
126
131
  });
@@ -1,6 +1,6 @@
1
1
  export const config = {
2
2
  baseApiUrl: "https://hr5buzenb1.execute-api.eu-west-1.amazonaws.com",
3
- // Nest monolith (staging) — serves POST /public/search.
3
+ // Nest monolith (staging) — serves POST /public/search/lexical.
4
4
  monolithApiUrl: "https://fvcphlqyle.execute-api.eu-west-1.amazonaws.com",
5
5
  assistantUrl: "https://d2bycosa71tnxv.cloudfront.net/assets/index.js",
6
6
  };
@@ -1,6 +1,6 @@
1
1
  export const config = {
2
2
  baseApiUrl: "https://rtbzcxkmwj.execute-api.eu-west-1.amazonaws.com",
3
- // Nest monolith (production) — serves POST /public/search. Custom domain,
3
+ // Nest monolith (production) — serves POST /public/search/lexical. Custom domain,
4
4
  // same as the dashboard's VITE_MONOLITH_API_URL: the raw execute-api
5
5
  // gateway URL answers 500 on every route and must not be used.
6
6
  monolithApiUrl: "https://api.askdialog.ai",
@@ -1,6 +1,6 @@
1
1
  export const config = {
2
2
  baseApiUrl: "https://rtbzcxkmwj.execute-api.eu-west-1.amazonaws.com",
3
- // Nest monolith (production) — serves POST /public/search. Custom domain,
3
+ // Nest monolith (production) — serves POST /public/search/lexical. Custom domain,
4
4
  // same as the dashboard's VITE_MONOLITH_API_URL: the raw execute-api
5
5
  // gateway URL answers 500 on every route and must not be used.
6
6
  monolithApiUrl: "https://api.askdialog.ai",
@@ -1,3 +1,3 @@
1
1
  import { SearchController, SearchControllerOptions } from "./types/searchController";
2
- export declare function createSearchController({ search, analytics, navigate, debounceMs, hitsPerPage, locale, countryCode, }: SearchControllerOptions): SearchController;
2
+ export declare function createSearchController({ search, analytics, navigate, debounceMs, hitsPerPage, locale, sections, }: SearchControllerOptions): SearchController;
3
3
  //# sourceMappingURL=searchController.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"searchController.d.ts","sourceRoot":"","sources":["../src/searchController.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EAGxB,MAAM,0BAA0B,CAAC;AAgBlC,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,SAAS,EACT,QAAQ,EACR,UAAgC,EAChC,WAAmC,EACnC,MAAM,EACN,WAAW,GACZ,EAAE,uBAAuB,GAAG,gBAAgB,CAsL5C"}
1
+ {"version":3,"file":"searchController.d.ts","sourceRoot":"","sources":["../src/searchController.ts"],"names":[],"mappings":"AAOA,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EAIxB,MAAM,0BAA0B,CAAC;AAqBlC,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,SAAS,EACT,QAAQ,EACR,UAAgC,EAChC,WAAmC,EACnC,MAAM,EACN,QAAa,GACd,EAAE,uBAAuB,GAAG,gBAAgB,CA6L5C"}