Skip to content

Commit

Permalink
feat(display-emitter): support Vue3 to the component (#1555)
Browse files Browse the repository at this point in the history
  • Loading branch information
joseacabaneros authored Jul 23, 2024
1 parent 54574db commit 21bcd52
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 110 deletions.
1 change: 1 addition & 0 deletions packages/_vue3-migration-test/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ export { default as TestSearch } from './test-search.vue';
export { default as TestTagging } from './tagging/test-tagging.vue';
export { default as TestRenderlessExtraParam } from './extra-params/test-renderless-extra-param.vue';
export { default as TestIcons } from './icons/test-icons.vue';
export { default as TestDisplayEmitter } from './test-display-emitter.vue';
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<template>
<ul class="list">
<DisplayEmitter
v-for="item in items"
:key="item.id"
:payload="{
url: 'tagging/url',
params: { displayId: item.id, totalHits: item.index }
}"
:eventMetadata="semanticFeature"
>
<li>{{ item.id }}</li>
</DisplayEmitter>
</ul>
</template>

<script setup lang="ts">
import DisplayEmitter from '../../../x-components/src/components/display-emitter.vue';
import { useXBus } from '../../../x-components/src/composables/use-x-bus';
import { DisplayWireMetadata } from '../../../x-components/src/wiring/wiring.types';
const xBus = useXBus();
const items = Array.from({ length: 50 }, (_, index) => ({ id: `item-${index}`, index }));
const semanticFeature: Partial<DisplayWireMetadata> = {
feature: 'semantics',
displayOriginalQuery: 'mercedes',
location: 'low_results'
};
/* eslint-disable no-console */
xBus
.on('TrackableElementDisplayed', true)
.subscribe(args => console.log('TrackableElementDisplayed event ->', args));
/* eslint-enable no-console */
</script>

<style>
.list {
height: 50px;
overflow-y: auto;
}
</style>
3 changes: 2 additions & 1 deletion packages/_vue3-migration-test/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ const adapter = {
}
})
);
}
},
tagging: () => new Promise(resolve => resolve())
} as unknown as XComponentsAdapter;

const store = createStore({});
Expand Down
8 changes: 7 additions & 1 deletion packages/_vue3-migration-test/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ import {
TestTagging,
TestRenderlessExtraParam,
TestAnimationFactory,
TestIcons
TestIcons,
TestDisplayEmitter
} from './';

const routes = [
Expand Down Expand Up @@ -302,6 +303,11 @@ const routes = [
path: '/icons',
name: 'Icons',
component: TestIcons
},
{
path: '/display-emitter',
name: 'DisplayEmitter',
component: TestDisplayEmitter
}
];

Expand Down
111 changes: 37 additions & 74 deletions packages/x-components/src/components/__tests__/display-emitter.spec.ts
Original file line number Diff line number Diff line change
@@ -1,115 +1,78 @@
import { mount, Wrapper } from '@vue/test-utils';
import Vue, { ref, nextTick, Ref } from 'vue';
import { TaggingRequest } from '@empathyco/x-types';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
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
}
}
);
(useEmitDisplayEvent as jest.Mock).mockReturnValue({ unwatchDisplay: unwatchDisplaySpy });

function render({
payload = { url: 'tagging/url', params: { test: 'param' } },
eventMetadata = { test: 'param' }
} = {}) {
const wrapper = mount({
components: { DisplayEmitter },
template: `
<DisplayEmitter :payload="payload" :eventMetadata="eventMetadata">
<div data-test="child" />
</DisplayEmitter>`,
data: () => ({ payload, eventMetadata })
});

return {
wrapper
wrapper: wrapper.findComponent(DisplayEmitter),
element: wrapper.find(getDataTestSelector('child')).element,
payload,
eventMetadata
};
}

describe('testing DisplayEmitter component', () => {
beforeEach(() => {
refElementVisibility.value = false;
(useEmitDisplayEvent as jest.Mock).mockClear();
unwatchDisplaySpy.mockClear();
});

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

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

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

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

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

await nextTick();

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

// 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
});
it('provides `useEmitDisplayEvent` with the payload and metadata to emit with the display event', async () => {
const { payload, eventMetadata } = render();

expect(useEmitDisplayEvent).toHaveBeenCalled();
expect(emitDisplayEventPayloadSpy).toBe(payload);
await nextTick();

expect(useEmitDisplayEvent).toHaveBeenCalledWith(
expect.objectContaining({ taggingRequest: payload, eventMetadata })
);
});

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

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>;
}
69 changes: 35 additions & 34 deletions packages/x-components/src/components/display-emitter.vue
Original file line number Diff line number Diff line change
@@ -1,55 +1,56 @@
<template>
<NoElement ref="root">
<slot />
</NoElement>
</template>

<script lang="ts">
import { defineComponent, onUnmounted, PropType, Ref, ref } from 'vue';
import {
defineComponent,
getCurrentInstance,
h,
onMounted,
onUnmounted,
PropType,
WatchStopHandle
} 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.
*/
/** A component that emits a display event when it first appears in the viewport. */
export default defineComponent({
components: {
NoElement
},
name: 'DisplayEmitter',
props: {
/**
* The payload for the display event emit.
*
* @public
*/
/** The payload for the display event emit. */
payload: {
type: Object as PropType<TaggingRequest>,
required: true
},
/**
* Optional event metadata.
*
* @public
*/
/** Optional event metadata. */
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 })
setup(props, { slots }) {
let unwatchDisplay: WatchStopHandle | undefined;
onMounted(() => {
const element = getCurrentInstance()?.proxy.$el as HTMLElement | undefined;
if (element) {
unwatchDisplay = useEmitDisplayEvent({
element,
taggingRequest: props.payload,
...(props.eventMetadata && { eventMetadata: props.eventMetadata })
}).unwatchDisplay;
}
});
onUnmounted(unwatchDisplay);
onUnmounted(() => {
unwatchDisplay?.();
});
return {
root
};
/*
* Obtains the vNodes array of the default slot and renders only the first one.
* It avoids to render a `Fragment` with the vNodes in Vue3 and the same behaviour in Vue2
* because Vue2 only allows a single root node. Then, `getCurrentInstance()?.proxy?.$el` to
* retrieve the HTML element in both versions.
*/
return () => slots.default?.()[0] ?? h();
}
});
</script>
Expand Down

0 comments on commit 21bcd52

Please sign in to comment.