Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(components): new component to wrap components so a display event is emitted when they appear in the viewport for the first time #1391

Merged
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ export const platformSearchResponse = {
}
],
tagging: {
display:
'https://api.staging.empathy.co/tagging/v1/track/empathy/display?q=jeans&lang=en&scope=desktop&totalHits=686&page=1&origin=url%3Aexternal&filtered=true&spellcheck=false',
query:
'https://api.staging.empathy.co/tagging/v1/track/empathy/query?q=jeans&lang=en&scope=desktop&totalHits=686&page=1&origin=url%3Aexternal&filtered=true&spellcheck=false'
},
Expand Down
12 changes: 8 additions & 4 deletions packages/x-adapter-platform/src/mappers/__tests__/url.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { extractUrlParameters, getDisplayClickTagging, getTaggingInfoFromUrl } from '../url.utils';
import {
extractUrlParameters,
getDisplayTaggingInfoFromUrl,
getTaggingInfoFromUrl
} from '../url.utils';

describe('url utils methods tests', () => {
describe('extractUrlParameters', () => {
Expand Down Expand Up @@ -45,14 +49,14 @@ describe('url utils methods tests', () => {

describe('getDisplayClickTagging', () => {
it('should not break when dealing with bad urls', () => {
expect(getDisplayClickTagging('null')).toStrictEqual({
expect(getDisplayTaggingInfoFromUrl('null')).toStrictEqual({
url: 'null',
params: { displayId: 'no_query', follow: false }
});
});

it('should retrieve the tagging info from the url, replacing q with displayId', () => {
const { url, params } = getDisplayClickTagging(
const { url, params } = getDisplayTaggingInfoFromUrl(
'https://api.empathy.co/?q=chips&env=mobile&lang=english&lang=spanish'
);
expect(url).toBe('https://api.empathy.co/');
Expand All @@ -65,7 +69,7 @@ describe('url utils methods tests', () => {
});

it('should set no_query tagging info when no q param exist as in topclicked response', () => {
const { url, params } = getDisplayClickTagging(
const { url, params } = getDisplayTaggingInfoFromUrl(
'https://api.empathy.co/?env=mobile&lang=english&lang=spanish'
);
expect(url).toBe('https://api.empathy.co/');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,20 @@ Object {
"url": "https://assets.empathy.co/",
},
],
"displayTagging": Object {
"params": Object {
"filtered": "true",
"follow": false,
"lang": "en",
"origin": "url:external",
"page": "1",
"q": "jeans",
"scope": "desktop",
"spellcheck": "false",
"totalHits": "686",
},
"url": "https://api.staging.empathy.co/tagging/v1/track/empathy/display",
},
"facets": Array [
Object {
"filters": Array [
Expand Down
12 changes: 6 additions & 6 deletions packages/x-adapter-platform/src/mappers/url.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ export function getTaggingInfoFromUrl(taggingUrl: string): TaggingRequest {
*
* @public
*/
export function getDisplayClickTagging(displayTaggingUrl: string): TaggingRequest {
const displayClickTagging = getTaggingInfoFromUrl(displayTaggingUrl);
const displayClickTaggingParams = displayClickTagging.params;
export function getDisplayTaggingInfoFromUrl(displayTaggingUrl: string): TaggingRequest {
const displayTagging = getTaggingInfoFromUrl(displayTaggingUrl);
const displayTaggingParams = displayTagging.params;

displayClickTaggingParams.displayId = displayClickTaggingParams.q ?? 'no_query';
delete displayClickTaggingParams.q;
displayTaggingParams.displayId = displayTaggingParams.q ?? 'no_query';
delete displayTaggingParams.q;

return displayClickTagging;
return displayTagging;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createMutableSchema } from '@empathyco/x-adapter';
import { Result } from '@empathyco/x-types';
import { getDisplayClickTagging, getTaggingInfoFromUrl } from '../../mappers/url.utils';
import { getDisplayTaggingInfoFromUrl, getTaggingInfoFromUrl } from '../../mappers/url.utils';
import { PlatformResult } from '../../types/models/result.model';

/**
Expand Down Expand Up @@ -36,7 +36,7 @@ export const resultSchema = createMutableSchema<PlatformResult, Result>({
add2cart: ({ add2cart }) => getTaggingInfoFromUrl(add2cart),
checkout: ({ checkout }) => getTaggingInfoFromUrl(checkout),
click: ({ click }) => getTaggingInfoFromUrl(click),
displayClick: ({ displayClick }) => getDisplayClickTagging(displayClick)
displayClick: ({ displayClick }) => getDisplayTaggingInfoFromUrl(displayClick)
}
}
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createMutableSchema } from '@empathyco/x-adapter';
import { SearchResponse } from '@empathyco/x-types';
import { getTaggingInfoFromUrl } from '../../mappers/url.utils';
import { getDisplayTaggingInfoFromUrl, getTaggingInfoFromUrl } from '../../mappers/url.utils';
import { PlatformSearchResponse } from '../../types/responses/search-response.model';
import { bannerSchema } from '../models/banner.schema';
import { facetSchema } from '../models/facet.schema';
Expand Down Expand Up @@ -41,5 +41,6 @@ export const searchResponseSchema = createMutableSchema<PlatformSearchResponse,
$path: 'catalog.partials',
$subSchema: partialResultsSchema
},
queryTagging: ({ catalog }) => getTaggingInfoFromUrl(catalog?.tagging?.query)
queryTagging: ({ catalog }) => getTaggingInfoFromUrl(catalog?.tagging?.query),
displayTagging: ({ catalog }) => getDisplayTaggingInfoFromUrl(catalog?.tagging?.display)
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface PlatformSearchResponse {
partials: PlatformPartialResult[];
tagging: {
query: string;
display: string;
};
};
direct: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export function getEmptySearchResponseStub(): SearchResponse {
params: {},
url: ''
},
displayTagging: {
params: {},
url: ''
},
redirections: [],
results: [],
spellcheck: '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export function getSearchResponseStub(): SearchResponse {
partialResults: [],
promoteds: getPromotedsStub(),
queryTagging: getTaggingResponseStub(),
displayTagging: getTaggingResponseStub(),
redirections: getRedirectionsStub(),
results: getResultsStub(),
spellcheck: '',
Expand Down
4 changes: 4 additions & 0 deletions packages/x-components/src/adapter/mocked-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,10 @@ export function createSearchResponse(partial?: Partial<SearchResponse>): SearchR
url: `${trackEndpoint}/query`,
params: { page: 1 }
},
displayTagging: {
url: `${trackEndpoint}/display`,
params: { page: 1 }
},
spellcheck: '',
...partial
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { mount, Wrapper } from '@vue/test-utils';
import Vue, { ref, nextTick, Ref } from 'vue';
import { TaggingRequest } from '@empathyco/x-types';
import { useEmitDisplayEvent } from '../../composables';
import DisplayEmitter from '../display-emitter.vue';
import { getDataTestSelector } from '../../__tests__/utils';

jest.mock('../../composables', () => ({
useEmitDisplayEvent: jest.fn()
}));

let emitDisplayEventElementSpy: Ref<Vue | null> = ref(null);
let emitDisplayEventPayloadSpy: TaggingRequest = { url: '', params: {} };
const unwatchDisplaySpy = jest.fn();
const refElementVisibility = ref(false);
(useEmitDisplayEvent as jest.Mock).mockImplementation(({ element, taggingRequest }) => {
// jest doesn't handle well evaluation of dynamic references with `toHaveBeenCalledWith`
// so we need a spy
emitDisplayEventElementSpy = element;
emitDisplayEventPayloadSpy = taggingRequest;

return {
isElementVisible: refElementVisibility,
unwatchDisplay: unwatchDisplaySpy
};
});

/**
* Renders the {@link DisplayEmitter} component, exposing a basic API for testing.
*
* @param options - The options to render the component with.
*
* @returns The API for testing the `DisplayEmitter` component.
*/
function renderDisplayEmitter(
{ payload }: RenderDisplayEmitterOptions = { payload: { url: '', params: {} } }
): RenderDisplayEmitterAPI {
const wrapper = mount(
{
components: {
DisplayEmitter
},
template: `
<DisplayEmitter :payload="payload">
<div data-test="child" />
</DisplayEmitter>`,
props: ['payload']
},
{
propsData: {
payload
}
}
);

return {
wrapper
};
}

describe('testing DisplayEmitter component', () => {
beforeEach(() => {
refElementVisibility.value = false;
});

it('renders everything passed to its default slot', () => {
const { wrapper } = renderDisplayEmitter();

expect(wrapper.find(getDataTestSelector('child')).exists()).toBe(true);
});

it('uses `useEmitDisplayEvent` underneath', () => {
renderDisplayEmitter();

expect(useEmitDisplayEvent).toHaveBeenCalled();
});

it('provides `useEmitDisplayEvent` with the element in the slot to watch', async () => {
renderDisplayEmitter();

await nextTick();

expect(emitDisplayEventElementSpy.value).not.toBe(null);
expect(emitDisplayEventElementSpy.value?.$el.getAttribute('data-test')).toBe('child');
});

// eslint-disable-next-line max-len
it('provides `useEmitDisplayEvent` with the payload to emit with the display event', () => {
const payload = { url: 'test-url', params: { test: 'param' } };
renderDisplayEmitter({
payload
});

expect(useEmitDisplayEvent).toHaveBeenCalled();
expect(emitDisplayEventPayloadSpy).toBe(payload);
});

it('removes the watcher on unmount', async () => {
const { wrapper } = renderDisplayEmitter();

wrapper.destroy();
await nextTick();
expect(unwatchDisplaySpy).toHaveBeenCalled();
});
});

interface RenderDisplayEmitterOptions {
/** The payload to provide. */
payload?: TaggingRequest;
}

interface RenderDisplayEmitterAPI {
/** The wrapper testing component instance. */
wrapper: Wrapper<Vue>;
}
86 changes: 86 additions & 0 deletions packages/x-components/src/components/display-emitter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<template>
<NoElement ref="root">
<slot />
</NoElement>
</template>

<script lang="ts">
import { defineComponent, onUnmounted, PropType, Ref, ref } from 'vue';
import { MaybeElement } from '@vueuse/core';
import { TaggingRequest } from '@empathyco/x-types';
import { useEmitDisplayEvent } from '../composables';
CachedaCodes marked this conversation as resolved.
Show resolved Hide resolved
import { WireMetadata } from '../wiring';
import { NoElement } from './no-element';

/**
* A component that emits a display event when it first appears in the viewport.
*/
export default defineComponent({
components: {
NoElement
},
props: {
/**
* The payload for the display event emit.
*
* @public
*/
payload: {
type: Object as PropType<TaggingRequest>,
required: true
},
/**
* Optional event metadata.
*
* @public
*/
eventMetadata: {
type: Object as PropType<Omit<WireMetadata, 'moduleName' | 'origin' | 'location'>>
}
},
setup(props) {
const root = ref(null);
const { unwatchDisplay } = useEmitDisplayEvent({
element: root as Ref<MaybeElement>,
taggingRequest: props.payload,
...(props.eventMetadata && { eventMetadata: props.eventMetadata })
});

onUnmounted(unwatchDisplay);

return {
root
};
}
});
</script>

<docs lang="mdx">
## Events

This component emits the following events:

- [`TrackableElementDisplayed`](https://github.com/empathyco/x/blob/main/packages/x-components/src/x-modules/tagging/events.types.ts)

## See it in action

In this example, the `DisplayEmitter` component will emit the `TrackableElementDisplayed` event when
the div inside first appears in the viewport.

```vue
<template>
<DisplayEmitter :payload="{ url: 'tagging/url', params: {} }">
<div>I'm displaying</div>
</DisplayEmitter>
</template>
<script>
import { DisplayEmitter } from '@empathyco/x-components';
export default {
name: 'DisplayEmitterDemo',
components: {
DisplayEmitter
}
};
</script>
```
</docs>
1 change: 1 addition & 0 deletions packages/x-components/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export { default as BaseKeyboardNavigation } from './base-keyboard-navigation.vu
export { default as BaseRating } from './base-rating.vue';
export { default as BaseSwitch } from './base-switch.vue';
export { default as BaseVariableColumnGrid } from './base-variable-column-grid.vue';
export { default as DisplayEmitter } from './display-emitter.vue';
export { default as GlobalXBus } from './global-x-bus.vue';
export { default as Highlight } from './highlight.vue';
export { default as ItemsList } from './items-list.vue';
Expand Down
Loading
Loading