Skip to content

Commit

Permalink
feat(components): composable and component to fire callbacks when an …
Browse files Browse the repository at this point in the history
…element appears on viewport (#1391)
  • Loading branch information
CachedaCodes authored Jan 25, 2024
1 parent 9cd94f7 commit c463352
Show file tree
Hide file tree
Showing 10 changed files with 664 additions and 64 deletions.
115 changes: 115 additions & 0 deletions packages/x-components/src/components/__tests__/display-emitter.spec.ts
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

0 comments on commit c463352

Please sign in to comment.