Hone logo
Hone
Problems

Vue Mount Wrapper

This challenge requires you to create a utility function that wraps Vue's mount function. This wrapper should provide additional convenience and control over component mounting, particularly for testing or complex component compositions. A well-designed mount wrapper can significantly simplify the process of setting up and interacting with Vue components.

Problem Description

You need to implement a TypeScript function, let's call it createMountWrapper, that takes a Vue component and a configuration object as arguments and returns a wrapper object. This wrapper object should expose methods to interact with the mounted component, similar to what you might find in testing libraries, but with custom extensions.

Key Requirements:

  • Component Mounting: The wrapper must correctly mount the provided Vue component using Vue's mount function.
  • Props Handling: Allow passing initial props to the mounted component.
  • Slots Handling: Support the rendering of named and default slots.
  • Emitted Events: Provide a mechanism to access and assert on events emitted by the component.
  • Instance Access: Offer a way to access the Vue component instance.
  • DOM Element Access: Provide access to the root DOM element of the mounted component.
  • Unmount Functionality: Include a method to properly unmount the component and clean up resources.
  • Custom Options: Allow passing additional options to the underlying Vue mount function (e.g., global plugins, stubs).

Expected Behavior:

The createMountWrapper function should return an object with the following methods and properties:

  • find(selector): Finds a DOM element within the mounted component using a CSS selector. Returns a wrapper around the found element or null if not found.
  • findAll(selector): Finds all DOM elements matching the selector. Returns an array of element wrappers.
  • emitted(event): Returns an array of arguments for a specific emitted event. If the event was not emitted, returns an empty array.
  • emittedBy(componentName): Returns a list of all events emitted by a specific child component.
  • getComponent(selector): Finds a child component instance by its selector.
  • getProps(): Returns the current props of the mounted component.
  • setProps(props): Updates the props of the mounted component and triggers a re-render.
  • destroy(): Unmounts the component and cleans up.
  • vm: The Vue component instance.
  • element: The root DOM element of the mounted component.

Edge Cases:

  • Components that don't emit any events.
  • Components with no slots.
  • Nested components and their emitted events.
  • Dynamic prop updates.

Examples

Example 1: Basic Mounting and Prop Access

import { defineComponent, ref } from 'vue';
import { createMountWrapper } from './mountWrapper'; // Assuming your wrapper is in mountWrapper.ts

const MyComponent = defineComponent({
  props: {
    message: String,
  },
  template: '<div>{{ message }}</div>',
});

// Usage:
const wrapper = await createMountWrapper(MyComponent, {
  props: { message: 'Hello, Vue!' },
});

console.log(wrapper.element.textContent); // Expected: "Hello, Vue!"
console.log(wrapper.getProps().message); // Expected: "Hello, Vue!"

await wrapper.destroy();

Output:

Hello, Vue!
Hello, Vue!

Explanation:

The createMountWrapper function successfully mounts MyComponent with the provided message prop. The element property gives us access to the DOM, and getProps retrieves the component's current props.

Example 2: Slot Rendering and Event Emission

import { defineComponent, ref } from 'vue';
import { createMountWrapper } from './mountWrapper';

const ChildComponent = defineComponent({
  emits: ['clicked'],
  template: '<button @click="$emit(\'clicked\', \'some-data\')">Click Me</button>',
});

const ParentComponent = defineComponent({
  components: { ChildComponent },
  template: '<child-component>Default Slot Content</child-component>',
});

// Usage:
const wrapper = await createMountWrapper(ParentComponent, {
  slots: {
    default: 'Default Slot Content',
  },
});

const childWrapper = wrapper.getComponent('child-component');
const clickEvent = childWrapper.emitted('clicked');

expect(clickEvent).toEqual([['some-data']]);

await wrapper.destroy();

Output:

(Conceptual - actual output would be in test assertions)

The clicked event emitted by ChildComponent with the argument 'some-data' is captured. The slots option correctly renders the default slot content.

Example 3: Prop Updates and Nested Event Emission

import { defineComponent, ref } from 'vue';
import { createMountWrapper } from './mountWrapper';

const Grandchild = defineComponent({
  props: {
    count: Number,
  },
  emits: ['incremented'],
  template: '<button @click="$emit(\'incremented\', count + 1)">Increment</button>',
});

const Child = defineComponent({
  components: { Grandchild },
  template: '<grandchild :count="initialCount" />',
  props: {
    initialCount: Number,
  },
});

const Parent = defineComponent({
  components: { Child },
  template: '<child :initialCount="startValue" />',
  props: {
    startValue: Number,
  },
});

// Usage:
const wrapper = await createMountWrapper(Parent, {
  props: { startValue: 0 },
});

const childWrapper = wrapper.getComponent('child');
const grandchildWrapper = childWrapper.getComponent('grandchild');

// Emit an event from grandchild
await grandchildWrapper.find('button').trigger('click');

// Check if the grandchild emitted the event
const incrementEvent = grandchildWrapper.emitted('incremented');
expect(incrementEvent).toEqual([[1]]);

// Update a prop on the parent and observe its effect on the grandchild
await wrapper.setProps({ startValue: 5 });

// Re-access grandchild and check its props (should reflect the update)
const updatedGrandchildWrapper = childWrapper.getComponent('grandchild');
expect(updatedGrandchildWrapper.getProps().count).toBe(5);

await wrapper.destroy();

Output:

(Conceptual - actual output would be in test assertions)

The incremented event from Grandchild is captured. When startValue on Parent is updated via setProps, the Child component re-renders, and consequently, the Grandchild receives the updated count prop.

Constraints

  • The createMountWrapper function must be implemented in TypeScript.
  • It should leverage Vue's mount function from 'vue'.
  • The returned wrapper object's methods should be robust and handle missing elements or events gracefully.
  • Consider the lifecycle of the mounted component; ensure destroy() is called to prevent memory leaks.
  • The implementation should aim for a balance between functionality and performance, especially for large or complex component trees.

Notes

  • You will need to create a TypeScript class or an object with factory functions for your MountWrapper.
  • Think about how to handle asynchronous operations within your wrapper, especially for methods that might trigger DOM updates or interactions.
  • For finding elements and components, consider using Vue Test Utils' selectors (e.g., CSS selectors, component names).
  • The emittedBy method might require inspecting the emitted events and tracing them back to their source components, which can be a more advanced part of the challenge.
  • Consider using an options object for createMountWrapper to pass global configurations like plugins, mocks, and stubs to the Vue mount function.
Loading editor...
typescript