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 all 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
@@ -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/use-on-display';
import DisplayEmitter from '../display-emitter.vue';
import { getDataTestSelector } from '../../__tests__/utils';

jest.mock('../../composables/use-on-display', () => ({
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>;
}
85 changes: 85 additions & 0 deletions packages/x-components/src/components/display-emitter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<template>
<NoElement ref="root">
<slot />
</NoElement>
</template>

<script lang="ts">
import { defineComponent, onUnmounted, PropType, Ref, ref } from 'vue';
import { TaggingRequest } from '@empathyco/x-types';
import { useEmitDisplayEvent } from '../composables/use-on-display';
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<HTMLElement | null>,
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