Skip to content

Commit

Permalink
Merge pull request #1233 from searchspring/develop
Browse files Browse the repository at this point in the history
Release 0.63.0
  • Loading branch information
korgon authored Dec 19, 2024
2 parents 2c9561e + 77257f8 commit 8a2dbfc
Show file tree
Hide file tree
Showing 24 changed files with 11,202 additions and 4,118 deletions.
2 changes: 1 addition & 1 deletion docs/INTEGRATION_DEBUGGING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

This functionality is only currently possible with Searchspring managed Snap repositories (https://github.com/searchspring-implementations).

While browsing a page that contains a Snap integration, appending the `?branch=[branchname]` query parameter to the URL will stop the execution of the existing script, and load the build from the `[branchname]` branch `https://snapui.searchspring.io/[siteid]/[branchname]/bundle.js`
While browsing a page that contains a Snap integration, appending the `?searchspring-preview=[branchname]` query parameter to the URL will stop the execution of the existing script, and load the build from the `[branchname]` branch `https://snapui.searchspring.io/[siteid]/[branchname]/bundle.js`

You will see an interface overlay on the bottom right of the viewport indicating if successful and details of the build.

Expand Down
46 changes: 46 additions & 0 deletions packages/snap-client/src/Client/transforms/searchResponse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,52 @@ describe('search response transformer result', () => {
// @ts-ignore
expect(transformSearchResponse.results({}).results instanceof Array).toEqual(true);
});

it('search response search transforms badges', () => {
const resultWithBadgeFeature = {
...mockSingleResult,
badges: [
{
tag: 'qa-badge-2910',
value: 'QA Badge 2910',
},
{
tag: 'qa-off-29',
value: 'QA Off 29',
},
{
tag: 'gift-guide',
value: 'Gift Guide',
},
],
};
const resultWithRandomBadgeField = {
...mockSingleResult,
badges: ['1', '2', '3'],
};
const resultWithRandomBadgeField2 = {
...mockSingleResult,
badges: {
name: '1',
name2: '2',
name3: '3',
},
};

const result = transformSearchResponse.result(resultWithBadgeFeature);
expect(result.attributes?.badges).toBeUndefined();
expect(result.badges).toEqual(resultWithBadgeFeature.badges);

// @ts-ignore - typings are wrong intentionally here
const result2 = transformSearchResponse.result(resultWithRandomBadgeField);
expect(result2.attributes?.badges).toEqual(resultWithRandomBadgeField.badges);
expect(result2.badges).toEqual([]);

// @ts-ignore - typings are wrong intentionally here
const result3 = transformSearchResponse.result(resultWithRandomBadgeField2);
expect(result3.badges).toEqual([]);
expect(result3.attributes?.badges).toEqual('[object Object]');
});
});

describe('search response facet transformer', () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/snap-client/src/Client/transforms/searchResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ transformSearchResponse.result = (rawResult: rawResult): SearchResponseModelResu
const attributes = Object.keys(rawResult)
.filter((k) => CORE_FIELDS.indexOf(k) == -1)
// remove 'badges' from attributes - but only if it is an object
.filter((k) => !(k == 'badges' && typeof rawResult[k] == 'object' && !Array.isArray(rawResult[k])))
.filter((k) => !(k == 'badges' && Array.isArray(rawResult[k]) && typeof rawResult[k]?.[0] == 'object'))
.reduce((attributes, key) => {
return {
...attributes,
Expand Down Expand Up @@ -242,7 +242,7 @@ transformSearchResponse.result = (rawResult: rawResult): SearchResponseModelResu
core: coreFieldValues,
},
attributes,
badges: typeof rawResult.badges == 'object' && !Array.isArray(rawResult.badges) ? rawResult.badges : [],
badges: Array.isArray(rawResult.badges) && typeof rawResult.badges[0] == 'object' ? rawResult.badges : [],
children,
});
};
Expand Down
156 changes: 121 additions & 35 deletions packages/snap-controller/src/Search/SearchController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,32 +188,6 @@ describe('Search Controller', () => {
expect(controller.store.results.length).toBeGreaterThan(0);
});

it('tests infinite setting', async () => {
searchConfig = {
...searchConfig,
settings: {
infinite: {
backfill: 5,
},
},
};

const controller = new SearchController(searchConfig, {
client: new MockClient(globals, {}),
store: new SearchStore(searchConfig, services),
urlManager,
eventManager: new EventManager(),
profiler: new Profiler(),
logger: new Logger(),
tracker: new Tracker(globals),
});

expect(controller.config.settings!.infinite!.backfill).toBe(searchConfig.settings!.infinite!.backfill);

await controller.search();
expect(controller.store.results.length).toBeGreaterThan(0);
});

const events = ['init', 'beforeSearch', 'afterSearch', 'afterStore'];
events.forEach((event) => {
it(`tests ${event} middleware err handled`, async function () {
Expand Down Expand Up @@ -358,14 +332,6 @@ describe('Search Controller', () => {
});

it('can invoke controller track.product.click', async () => {
searchConfig = {
...searchConfig,
settings: {
infinite: {
backfill: 5,
},
},
};
const controller = new SearchController(searchConfig, {
client: new MockClient(globals, {}),
store: new SearchStore(searchConfig, services),
Expand Down Expand Up @@ -400,6 +366,125 @@ describe('Search Controller', () => {
storagefn.mockClear();
});

it('tests that a repeated search with infinite does not duplicate results', async () => {
searchConfig = {
...searchConfig,
settings: {
infinite: {},
},
};

const controller = new SearchController(searchConfig, {
client: new MockClient(globals, {}),
store: new SearchStore(searchConfig, services),
urlManager,
eventManager: new EventManager(),
profiler: new Profiler(),
logger: new Logger(),
tracker: new Tracker(globals),
});

expect(controller.config.settings!.infinite).toBeDefined();

// set page 1 data
(controller.client as MockClient).mockData.updateConfig({ search: 'infinite.page1', siteId: '8uyt2m' });

await controller.search();
expect(controller.store.results.length).toBe(30);

await controller.search();
expect(controller.store.results.length).toBe(30);

await controller.search();
expect(controller.store.results.length).toBe(30);
});

it('tests infinite setting', async () => {
searchConfig = {
...searchConfig,
settings: {
infinite: {},
},
};

const controller = new SearchController(searchConfig, {
client: new MockClient(globals, {}),
store: new SearchStore(searchConfig, services),
urlManager,
eventManager: new EventManager(),
profiler: new Profiler(),
logger: new Logger(),
tracker: new Tracker(globals),
});

expect(controller.config.settings!.infinite).toBeDefined();

// set page 1 data
(controller.client as MockClient).mockData.updateConfig({ search: 'infinite.page1', siteId: '8uyt2m' });

await controller.search();
expect(controller.store.results.length).toBe(30);

// set page 2 data
(controller.client as MockClient).mockData.updateConfig({ search: 'infinite.page2', siteId: '8uyt2m' });
controller.urlManager = controller.urlManager.set('page', 2);

await controller.search();
expect(controller.store.results.length).toBe(60);

// set page 3 data
(controller.client as MockClient).mockData.updateConfig({ search: 'infinite.page3', siteId: '8uyt2m' });
controller.urlManager = controller.urlManager.set('page', 3);

await controller.search();
expect(controller.store.results.length).toBe(90);
});

it('when using infinite setting and applying a filter the pagination resets', async () => {
searchConfig = {
...searchConfig,
settings: {
infinite: {},
},
};

const controller = new SearchController(searchConfig, {
client: new MockClient(globals, {}),
store: new SearchStore(searchConfig, services),
urlManager,
eventManager: new EventManager(),
profiler: new Profiler(),
logger: new Logger(),
tracker: new Tracker(globals),
});

expect(controller.config.settings!.infinite).toBeDefined();

// set page 1 data
(controller.client as MockClient).mockData.updateConfig({ search: 'infinite.page1', siteId: '8uyt2m' });

await controller.search();
expect(controller.store.results.length).toBe(30);

// set page 2 data
(controller.client as MockClient).mockData.updateConfig({ search: 'infinite.page2', siteId: '8uyt2m' });

await controller.search();
expect(controller.store.results.length).toBe(60);

// simulate a filter being applied
controller.urlManager = controller.urlManager.merge('filter.color_family', 'Blue');
expect(controller.params.filters?.length).toBe(1);

// set filtered data
(controller.client as MockClient).mockData.updateConfig({ search: 'infinite.filtered', siteId: '8uyt2m' });

await controller.search();

// ensure that we do not keep concatenating results
expect(controller.store.results.length).toBe(30);
});

it('backfills results', async () => {
searchConfig = {
...searchConfig,
Expand All @@ -419,7 +504,7 @@ describe('Search Controller', () => {
tracker: new Tracker(globals),
});

(controller.client as MockClient).mockData.updateConfig({ search: 'infinitePage3', siteId: '8uyt2m' });
(controller.client as MockClient).mockData.updateConfig({ search: 'infinite.page1', siteId: '8uyt2m' });

const searchfn = jest.spyOn(controller.client, 'search');

Expand All @@ -432,6 +517,7 @@ describe('Search Controller', () => {
const { pageSize } = controller.store.pagination;
expect(searchfn).toHaveBeenCalledTimes(page);

// asserting that search API has been called the same number of times as the current page parameter
expect(searchfn).toHaveBeenNthCalledWith(1, expect.objectContaining({ pagination: {} }));
expect(searchfn).toHaveBeenNthCalledWith(2, expect.objectContaining({ pagination: { page: 2 }, search: { redirectResponse: 'full' } }));
expect(searchfn).toHaveBeenNthCalledWith(3, expect.objectContaining({ pagination: { page: 3 }, search: { redirectResponse: 'full' } }));
Expand Down
16 changes: 3 additions & 13 deletions packages/snap-controller/src/Search/SearchController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ export class SearchController extends AbstractController {
declare store: SearchStore;
declare config: SearchControllerConfig;
storage: StorageStore;
private previousResults: Array<SearchResponseModelResult> = [];

constructor(
config: SearchControllerConfig,
Expand Down Expand Up @@ -96,8 +95,7 @@ export class SearchController extends AbstractController {
config?.settings?.redirects?.singleResult &&
search?.response?.search?.query &&
search?.response?.pagination?.totalResults === 1 &&
!nonBackgroundFilters?.length &&
!(search.controller as SearchController).previousResults.length
!nonBackgroundFilters?.length
) {
window.location.replace(search?.response.results[0].mappings.core.url);
return false;
Expand Down Expand Up @@ -363,7 +361,7 @@ export class SearchController extends AbstractController {
}

// infinite backfill is enabled AND we have not yet fetched any results
if (this.config.settings?.infinite.backfill && !this.previousResults.length) {
if (this.config.settings?.infinite.backfill && !this.store.loaded) {
// create requests for all missing pages (using Arrray(page).fill() to populate an array to map)
const backfillRequests = Array(params.pagination.page)
.fill('backfill')
Expand Down Expand Up @@ -408,12 +406,9 @@ export class SearchController extends AbstractController {
} else {
// infinite with no backfills.
[meta, response] = await this.client.search(params);

// append new results to previous results
response.results = [...this.previousResults, ...(response.results || [])];
}
} else {
// standard request (not using infinite scroll)
// normal request for next page
[meta, response] = await this.client.search(params);
}

Expand Down Expand Up @@ -447,11 +442,6 @@ export class SearchController extends AbstractController {
afterSearchProfile.stop();
this.log.profile(afterSearchProfile);

// store previous results for infinite usage
if (this.config.settings?.infinite) {
this.previousResults = JSON.parse(JSON.stringify(response.results));
}

// update the store
this.store.update(response);

Expand Down
2 changes: 1 addition & 1 deletion packages/snap-preact-components/src/utilities/snapify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type CreateConfig = {
};

// configure MobX
configureMobx({ useProxies: 'never', isolateGlobalState: true, enforceActions: 'never' });
configureMobx({ useProxies: 'always', isolateGlobalState: true, enforceActions: 'never' });

const controllers: {
[id: string]: SearchController | AutocompleteController | RecommendationController;
Expand Down
24 changes: 22 additions & 2 deletions packages/snap-preact-demo/src/components/Results/Results.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { h, Component } from 'preact';
import { useRef } from 'preact/hooks';
import { observer } from 'mobx-react';

import { Pagination, Results as ResultsComponent, withStore, withController } from '@searchspring/snap-preact-components';
import { Pagination, Results as ResultsComponent, withStore, withController, useIntersection } from '@searchspring/snap-preact-components';

import { Profile } from '../Profile/Profile';
import { Toolbar } from '../Toolbar/Toolbar';
Expand All @@ -28,10 +29,23 @@ const resultsBreakpoints = {
@observer
export class Results extends Component<ResultsProps> {
render() {
const loading = this.props.store.loading;
const results = this.props.store.results;
const pagination = this.props.store.pagination;
const controller = this.props.controller;

const infiniteEnabled = Boolean(controller.config.settings.infinite);
const infiniteRef = useRef(null);
if (infiniteEnabled) {
const atBottom = useIntersection(infiniteRef, '50px');

if (atBottom && pagination.next && !loading && pagination.totalResults > 0) {
setTimeout(() => {
pagination.next.url.go({ history: 'replace' });
});
}
}

return (
<div class="ss-results">
<Toolbar />
Expand All @@ -42,11 +56,17 @@ export class Results extends Component<ResultsProps> {
<div id="ss_results">
<ResultsComponent breakpoints={resultsBreakpoints} controller={controller} results={results} />
</div>
{infiniteEnabled && <div style={{ display: loading ? 'none' : 'block' }} ref={infiniteRef}></div>}
</Profile>

<div class="clear"></div>

<div class="ss-toolbar ss-toolbar-bottom">{pagination.totalPages > 1 && <Pagination pagination={pagination} />}</div>
<div class="ss-toolbar ss-toolbar-bottom">{!infiniteEnabled && pagination.totalPages > 1 && <Pagination pagination={pagination} />}</div>
{infiniteEnabled && (
<div class="ss-page-circle">
<span class="ss-page-circle-number">{pagination.current.number}</span>
</div>
)}

<div class="clear"></div>
</div>
Expand Down
Loading

0 comments on commit 8a2dbfc

Please sign in to comment.