Skip to content

Commit

Permalink
test: add initial plugin tests
Browse files Browse the repository at this point in the history
  • Loading branch information
dkonieczek committed Oct 11, 2024
1 parent 6add12a commit 007ea24
Show file tree
Hide file tree
Showing 8 changed files with 477 additions and 12 deletions.
19 changes: 12 additions & 7 deletions docs/TEMPLATES_CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,18 @@ It is possible to switch language and currency at run-time using methods on the


### Platform Middleware
The `platform` object allows you to configure platform-specific middleware. By default when platform is set to `shopify`, `bigcommerce`, `magento2` or `common`, the following middleware are enabled by default:

| Configuration Option | Description | Type | Default |
|----------------------|-------------|------|---------|
| `platform` | Platform-specific middleware configurations | Object | - |
| `platform[platform]` | Platform-specific configurations | Object | - |
| `platform[platform].backgroundFilters` | Background filter configurations | Object | - |
| `platform[platform].scrollToTop` | Configuration for scrolling to top after search | Object | - |
| `platform[platform].storeLogger` | Configuration for store logging | Object | - |
| `platform[platform].backgroundFilters` | Background filter configurations | Object | Enabled |
| `platform[platform].scrollToTop` | Configuration for scrolling to top after search | Object | Enabled |
| `platform[platform].storeLogger` | Configuration for store logging | Object | Enabled |

*Note*: When `shopify`, `bigcommerce` or `magento2` is defined as the platform, additional middleware is automatically applied in SnapTemplates to handle platform specific context variables from the Searchspring installation script block. The script context also supports defining generic `backgroundFilters` and does not require defining a platform to be defined.

The `platform` object allows you to configure platform-specific middleware. Currently, `shopify`, `bigcommerce`, `magento2` and `common` are supported and share the following common options:

#### backgroundFilters
Allows you to set up background filters. You can configure filters for tags, collections, or other fields.
Expand All @@ -107,7 +109,7 @@ Allows you to set up background filters. You can configure filters for tags, col
| `platform[platform].backgroundFilters.filters[]` | Background filter definitions | Array | - |
| `platform[platform].backgroundFilters.filters[].type` | Defines if filter should be 'value' or 'range' type | 'value' | 'range' | true |
| `platform[platform].backgroundFilters.filters[].field` | Defines filter field name | string | - |
| `platform[platform].backgroundFilters.filters[].value` | Defines filter value. If `type` is 'value', this must be a string, otherwise if `type` is 'range', this must be an object with `low` and `high` properties | string | { low: number, high: number } | - |
| `platform[platform].backgroundFilters.filters[].value` | Defines filter value. If `type` is 'value', this must be a string, otherwise if `type` is 'range', this must be an object with `low` and `high` properties | string \| { low: number, high: number } | - |

```jsx
platform: {
Expand Down Expand Up @@ -141,10 +143,12 @@ platform: {
#### scrollToTop
Configures the behavior of scrolling to the top of the page upon the 'afterStore' event

*Note*: Only applicable to SearchController (search feature target for search and category pages)

| Configuration Option | Description | Type | Default |
|----------------------|-------------|------|---------|
| `platform[platform].scrollToTop` | Scroll to top middleware configuration | Object | - |
| `platform[platform].scrollToTop.enabled` | Enables middleware | boolean | false |
| `platform[platform].scrollToTop.enabled` | Enables middleware | boolean | true |
| `platform[platform].scrollToTop.selector` | Query selector to scroll to | string | - |
| `platform[platform].scrollToTop.options` | [`window.scroll` options configuration](https://developer.mozilla.org/en-US/docs/Web/API/Window/scroll#options) | Object | `{ top: 0, left: 0, behavior: 'smooth' }` |

Expand All @@ -153,6 +157,7 @@ platform: {
common: {
scrollToTop: {
enabled: true,
selector: '#searchspring-layout',
options: {
top: 0,
left: 0,
Expand Down Expand Up @@ -192,7 +197,7 @@ In addition when platform is `shopify`, the following middleware is available:


#### mutateResults
Enables updating the URL with search results. Product urls will be prefixed with their category route.
Enables updating the URL with search results. Product urls will be prefixed with their category route. This also requires platform specific context variable `collection` to be defined.

```jsx
platform: {
Expand Down
158 changes: 158 additions & 0 deletions packages/snap-platforms/shopify/src/pluginMutateResults.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { pluginMutateResults } from './pluginMutateResults';
import { MockClient } from '@searchspring/snap-shared';
import { SearchStore } from '@searchspring/snap-store-mobx';
import { UrlManager, QueryStringTranslator, reactLinker } from '@searchspring/snap-url-manager';
import { EventManager } from '@searchspring/snap-event-manager';
import { Profiler } from '@searchspring/snap-profiler';
import { Logger } from '@searchspring/snap-logger';
import { Tracker } from '@searchspring/snap-tracker';
import { SearchController } from '@searchspring/snap-controller';

const ORIGIN = 'http://localhost';
const ROOT = 'root/';

const urlManager = new UrlManager(new QueryStringTranslator(), reactLinker);
const services = {
urlManager: urlManager,
};
let searchConfig = {
id: 'search',
};

const globals = { siteId: '8uyt2m' };
const searchConfigDefault = {
id: 'search',
globals: {
filters: [],
},
settings: {},
};
let controller: any;
let errMock: any;

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

describe('Shopify pluginMutateResults', () => {
describe('requires shopify to exist on the dom', () => {
beforeAll(async () => {
searchConfig = { ...searchConfigDefault };
controller = new SearchController(searchConfig, controllerServices);
errMock = jest.spyOn(console, 'log').mockImplementation(() => {});
});

beforeEach(() => {
delete window.Shopify;
});

afterEach(() => {
errMock.mockRestore();
});

it('requires shopify to exist on the dom', () => {
const config = {
url: {
enabled: true,
},
};

pluginMutateResults(controller, config);

expect(errMock).toHaveBeenCalledWith(expect.stringContaining('Error: window.Shopify not found'), expect.any(String), expect.any(String));
});
});

describe('has shopify in the dom', () => {
beforeAll(async () => {
errMock = jest.spyOn(console, 'log').mockImplementation(() => {});
});

beforeEach(() => {
searchConfig = { ...searchConfigDefault };
controller = new SearchController(searchConfig, controllerServices);

global.window = Object.create(window);

Object.defineProperty(window, 'location', {
value: {
origin: ORIGIN,
href: ORIGIN,
},
writable: true,
});

// @ts-ignore
global.Shopify = {
routes: {
root: ROOT,
},
};
});

afterEach(() => {
errMock.mockRestore();
});

it('requires config.enabled', async () => {
const config = {
url: {
enabled: false,
},
};

pluginMutateResults(controller, config);
await controller.search();

expect(errMock).not.toHaveBeenCalled();
expect(controller.store.results[0].mappings.core?.url).toEqual(`/product/C-LTO-R6-L5316`);
});

it('requires context.collection to be defined', async () => {
const config = {
url: {
enabled: true,
},
};

pluginMutateResults(controller, config);
await controller.search();

expect(errMock).not.toHaveBeenCalled();
expect(controller.store.results[0].mappings.core?.url).toEqual(`/product/C-LTO-R6-L5316`);
});

it('has context.collection defined', async () => {
// plugin requires collection context
const collectionContext = {
handle: 'collection-handle',
name: 'Collection Name',
};
controller.context.collection = collectionContext;

const config = {
url: {
enabled: true,
},
};

pluginMutateResults(controller, config);

await controller.search();

// plugin requires handle attribute
expect(controller.store.results[0].attributes.handle).toBeDefined();
expect(errMock).not.toHaveBeenCalled();

expect(controller.store.results[0].mappings.core?.url).toEqual(
`${ROOT}collections/${collectionContext.handle}/products/${controller.store.results[0].attributes.handle}`
);
});
});
});
4 changes: 2 additions & 2 deletions packages/snap-platforms/shopify/src/pluginMutateResults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const pluginMutateResults = async (cntrlr: AbstractController, config: Pl
};

const updateUrlFn = (handle: string): string | undefined => {
if (cntrlr.type == 'search') {
if (cntrlr.type == 'search' && handle) {
const hasRoute =
typeof window.Shopify == 'object' && typeof window.Shopify.routes == 'object' && typeof window.Shopify.routes.root == 'string'
? true
Expand All @@ -47,7 +47,7 @@ export const pluginMutateResults = async (cntrlr: AbstractController, config: Pl
results.forEach((result: Product | Banner) => {
if (result.type != 'banner') {
const updatedUrl = updateUrlFn(result.attributes.handle as string);
if (updatedUrl) {
if (updatedUrl && updatedUrl !== result.mappings.core?.url) {
result.mappings.core!.url = updatedUrl;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import 'whatwg-fetch';

import { pluginScrollToTop, type ScrollBehavior } from './pluginScrollToTop';
import { MockClient } from '@searchspring/snap-shared';
import { SearchStore } from '@searchspring/snap-store-mobx';
import { UrlManager, QueryStringTranslator, reactLinker } from '@searchspring/snap-url-manager';
import { EventManager } from '@searchspring/snap-event-manager';
import { Profiler } from '@searchspring/snap-profiler';
import { Logger } from '@searchspring/snap-logger';
import { Tracker } from '@searchspring/snap-tracker';
import { SearchController } from '@searchspring/snap-controller';

const urlManager = new UrlManager(new QueryStringTranslator(), reactLinker);
const services = {
urlManager: urlManager,
};
let searchConfig = {
id: 'search',
};

const globals = { siteId: '8uyt2m' };
const searchConfigDefault = {
id: 'search',
globals: {
filters: [],
},
settings: {},
};
let controller: any;
let scrollMock: any;

const wait = (time = 1) => {
return new Promise((resolve) => {
setTimeout(resolve, time);
});
};

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

describe('pluginScrollToTop', () => {
beforeAll(async () => {
scrollMock = jest.spyOn(global.window, 'scroll').mockImplementation(() => {});
});

beforeEach(() => {
searchConfig = { ...searchConfigDefault };
controller = new SearchController(searchConfig, controllerServices);
});

afterEach(() => {
scrollMock.mockClear();
});

it('requires config.enabled', async () => {
const config = {
enabled: false,
};

pluginScrollToTop(controller, config);
await controller.search();

await wait(10);

expect(scrollMock).not.toHaveBeenCalled();
});

it('can scroll with defaults', async () => {
const config = {
enabled: true,
};

expect(controller.type).toEqual('search');

pluginScrollToTop(controller, config);
await controller.search();

await wait(10);

expect(scrollMock).toHaveBeenCalled();
expect(scrollMock).toHaveBeenCalledWith({ top: 0, left: 0, behavior: 'smooth' });
});

it('can scroll with options', async () => {
const config = {
enabled: true,
options: {
top: 100,
left: 100,
behavior: 'instant' as ScrollBehavior,
},
};

pluginScrollToTop(controller, config);
await controller.search();

await wait(10);

expect(scrollMock).toHaveBeenCalled();
expect(scrollMock).toHaveBeenCalledWith(config.options);
});

it('can scroll to selector', async () => {
const config = {
enabled: true,
selector: '#test-selector',
options: {
top: 100,
left: 100,
behavior: 'instant' as ScrollBehavior,
},
};

global.document.body.innerHTML = '<div style="position: relative;"><div id="test-selector" style="position: absolute; top: 100px;"></div></div>';
const element = document.querySelector('#test-selector');
jest.spyOn(element as any, 'getBoundingClientRect').mockImplementation(() => {
// return new DOMRect(0, 0, 100, 500) //100px wide, 500px tall
return {
top: 100,
};
});
expect(element?.getBoundingClientRect().top).toEqual(100);

// @ts-ignore - override the element's getBoundingClientRect method
element.getBoundingClientRect = () => {
return {
top: 100,
};
};

pluginScrollToTop(controller, config);
await controller.search();

await wait(10);

expect(scrollMock).toHaveBeenCalled();
expect(scrollMock).toHaveBeenCalledWith({ top: 200, left: 100, behavior: 'instant' });
});
});
Loading

0 comments on commit 007ea24

Please sign in to comment.