Skip to content

Commit

Permalink
test: add useProgrammatic tests
Browse files Browse the repository at this point in the history
  • Loading branch information
mlmoravek committed Sep 24, 2024
1 parent 119ab35 commit 08867e5
Show file tree
Hide file tree
Showing 11 changed files with 457 additions and 68 deletions.
11 changes: 6 additions & 5 deletions packages/oruga/src/components/loading/useLoadingProgrammatic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { type ComponentInternalInstance } from "vue";
import {
InstanceRegistry,
useProgrammatic,
type ProgrammaticComponentOptions,
type PublicProgrammaticComponentOptions,
type ProgrammaticExpose,
} from "../programmatic";

Expand All @@ -12,7 +12,7 @@ import type { ComponentProps } from "vue-component-type-helpers";

declare module "../../index" {
interface OrugaProgrammatic {
loading: typeof LoadingProgrammatic;
loading: typeof useLoadingProgrammatic;
}
}

Expand All @@ -22,11 +22,12 @@ const instances = new InstanceRegistry<ComponentInternalInstance>();
/** all properties of the loading component */
export type LoadingProps = ComponentProps<typeof Loading>;

/** useLoadingProgrammatic composable options */
type LoadingProgrammaticOptions = Readonly<Omit<LoadingProps, "label">> & {
label?: string | Array<unknown>;
} & ProgrammaticComponentOptions<typeof Loading>;
} & PublicProgrammaticComponentOptions;

const LoadingProgrammatic = {
const useLoadingProgrammatic = {
/**
* create a new programmatic modal component
* @param options loading label string or loading component props object
Expand Down Expand Up @@ -76,4 +77,4 @@ const LoadingProgrammatic = {
},
};

export default LoadingProgrammatic;
export default useLoadingProgrammatic;
4 changes: 2 additions & 2 deletions packages/oruga/src/components/modal/useModalProgrammatic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { type ComponentInternalInstance } from "vue";
import {
InstanceRegistry,
useProgrammatic,
type ProgrammaticComponentOptions,
type PublicProgrammaticComponentOptions,
type ProgrammaticExpose,
} from "../programmatic";

Expand All @@ -25,7 +25,7 @@ export type ModalProps = ComponentProps<typeof Modal>;
/** useModalProgrammatic composable options */
type ModalProgrammaticOptions = Readonly<Omit<ModalProps, "content">> & {
content?: string | Array<unknown>;
} & ProgrammaticComponentOptions<typeof Modal>;
} & PublicProgrammaticComponentOptions;

const useModalProgrammatic = {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ComponentInternalInstance } from "vue";
import {
InstanceRegistry,
useProgrammatic,
type ProgrammaticComponentOptions,
type PublicProgrammaticComponentOptions,
type ProgrammaticExpose,
} from "../programmatic";
import { getOption } from "@/utils/config";
Expand All @@ -25,12 +25,13 @@ const instances = new InstanceRegistry<ComponentInternalInstance>();
export type NotifcationProps = ComponentProps<typeof Notification>;
export type NotifcationNoticeProps = ComponentProps<typeof NotificationNotice>;

/** useNotificationProgrammatic composable options */
type NotifcationProgrammaticOptions = Readonly<
Omit<NotifcationNoticeProps, "container">
> &
Readonly<Omit<NotifcationProps, "message">> & {
message?: string | Array<unknown>;
} & ProgrammaticComponentOptions<typeof NotificationNotice>;
} & PublicProgrammaticComponentOptions;

const useNotificationProgrammatic = {
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export default class InstanceRegistry<T> {
import type { ComponentInternalInstance } from "vue";

export default class InstanceRegistry<T = ComponentInternalInstance> {
entries: Array<T>;

constructor() {
Expand Down
106 changes: 62 additions & 44 deletions packages/oruga/src/components/programmatic/ProgrammaticComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,51 +8,63 @@ import {
type ComponentInternalInstance,
type VNode,
} from "vue";
import type {
// ComponentExposed,
ComponentProps,
} from "vue-component-type-helpers";

import type InstanceRegistry from "@/components/programmatic/InstanceRegistry";
import type { ComponentProps } from "vue-component-type-helpers";

import { isClient } from "@/utils/ssr";

// type ComponentPropsType<C> = C extends ComponentInternalInstance
// ? C["props"]
// : C extends DefineComponent
// ? C["$props"]
// : Record<string, unknown>;
export type ProgrammaticComponentProps<C extends string | Component = unknown> =
{
/**
* Component to be injected.
* Terminate the component by emitting a 'close' event — emits('close')
*/
component: C;
/**
* Props to be binded to the injected component.
* Both attributes and properties can be used in props.
* Vue automatically picks the right way to assign it.
* `class` and `style` have the same object / array value support like in templates.
* Event listeners should be passed as onXxx.
* @see https://vuejs.org/api/render-function.html#h
*/
props?: ComponentProps<C>;
/** Programmatic component registry instance */
instances?: InstanceRegistry<ComponentInternalInstance>;
};

export type ProgrammaticComponentProps<C extends string | Component> = {
/**
* Component to be injected.
* Terminate the component by emitting a 'close' event — emits('close')
*/
component: C;
export type ProgrammaticComponentEmits = {
/**
* Props to be binded to the injected component.
* Both attributes and properties can be used in props.
* Vue automatically picks the right way to assign it.
* `class` and `style` have the same object / array value support like in templates.
* Event listeners should be passed as onXxx.
* @see https://vuejs.org/api/render-function.html#h
* On component close event.
* This get called when the component emits `close` or the exposed `close` function get called.
*/
props?: ComponentProps<C>;
/** Callback function to call on close event */
onClose?: (...args: unknown[]) => void;
/**
* This is used internally for programmatic usage
* @ignore
*/
instances: InstanceRegistry<ComponentInternalInstance>;
/**
* This is used internally for programmatic usage
* @ignore
*/
destroy: () => void;
close?: (...args: unknown[]) => void;
/** On component destroy event which get called when the component should be destroyed. */
destroy?: () => void;
};

export const ProgrammaticComponent = defineComponent(
// there is a bug with functional defineComponent and extracting the exposed type
// export type ProgrammaticComponentExpose = ComponentExposed<
// typeof ProgrammaticComponent
// >;

export type ProgrammaticComponentExpose = {
/** call close event function */
close: (...args: unknown[]) => void;
/** promise which get called on close event */
promise: Promise<unknown>;
};

export const ProgrammaticComponent = defineComponent<
ProgrammaticComponentProps,
ProgrammaticComponentEmits
>(
<C extends string | Component>(
props: ProgrammaticComponentProps<C>,
{ expose, slots },
{ expose, emit, slots },
) => {
// getting a hold of the internal instance in setup()
const vm = getCurrentInstance();
Expand All @@ -62,28 +74,28 @@ export const ProgrammaticComponent = defineComponent(
const promise = new Promise<unknown>((p1) => (resolve = p1));

// add component instance to instance register
onMounted(() => props.instances.add(vm));
onMounted(() => props.instances?.add(vm));

// remove component instance from instance register
onUnmounted(() => props.instances.remove(vm));
onUnmounted(() => props.instances?.remove(vm));

function close(...args: unknown[]): void {
// call `onClose` handler if given
if (typeof props.onClose === "function") props.onClose(...args);
// emit `onClose` event
emit("close", ...args);

// call promise resolve
resolve(...args);

// call `destory` after animation is finished
// emit `destory` event after animation is finished
setTimeout(() => {
if (isClient)
window.requestAnimationFrame(() => props.destroy());
else props.destroy();
window.requestAnimationFrame(() => emit("destroy"));
else emit("destroy");
});
}

/** expose public functionalities for programmatic usage */
expose({ close, promise });
expose({ close, promise } satisfies ProgrammaticComponentExpose);

// return render function which renders given component
return (): VNode =>
Expand All @@ -93,6 +105,12 @@ export const ProgrammaticComponent = defineComponent(
slots["default"],
);
},
// manual runtime props declaration is currently still needed.
{ props: ["component", "props", "onClose", "destroy", "instances"] },
{
// manual runtime props declaration is currently still needed.
props: ["component", "props", "instances"],
// manual runtime emits declaration
emits: ["close", "destroy"],
// manual runtime slot declaration
slots: ["default"],
},
);
2 changes: 1 addition & 1 deletion packages/oruga/src/components/programmatic/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import InstanceRegistry from "./InstanceRegistry";
export type {
ProgrammaticOptions,
ProgrammaticExpose,
ProgrammaticComponentOptions,
PublicProgrammaticComponentOptions,
} from "./useProgrammatic";
export { InstanceRegistry };

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { createVNode, markRaw } from "vue";
import { describe, test, expect, afterEach, vi } from "vitest";
import { enableAutoUnmount, flushPromises, mount } from "@vue/test-utils";

import { OModal } from "@/components/modal";

import InstanceRegistry from "../InstanceRegistry";
import {
ProgrammaticComponent,
type ProgrammaticComponentExpose,
} from "../ProgrammaticComponent";

describe("ProgrammaticComponent tests", () => {
enableAutoUnmount(afterEach);

test("test render simple div correctly", () => {
const wrapper = mount(ProgrammaticComponent, {
props: {
component: "div",
props: { "data-oruga": "programmatic" },
},
});

expect(!!wrapper.vm).toBeTruthy();
expect(wrapper.exists()).toBeTruthy();
expect(wrapper.html()).toMatchSnapshot();
const element = wrapper.find('[data-oruga="programmatic"]');
expect(element.exists()).toBeTruthy();
});

test("test render slot correctly", () => {
const wrapper = mount(ProgrammaticComponent, {
props: {
component: createVNode({
template: `<div><slot /></div>`,
}),
props: { "data-oruga": "programmatic" },
},
slots: {
default: `<p data-oruga="inner-slot">HELP</p>`,
},
});

const element = wrapper.find('[data-oruga="programmatic"]');
expect(element.exists()).toBeTruthy();
const inner = wrapper.find('[data-oruga="inner-slot"]');
expect(inner.exists()).toBeTruthy();
});

test("test render complex component with props correctly", () => {
const content = "This is my content";
const wrapper = mount(ProgrammaticComponent, {
props: {
component: markRaw(OModal),
props: { content },
},
});

const model = wrapper.find('[data-oruga="modal"]');
expect(model.exists()).toBeTruthy();
expect(model.text()).toBe(content);
});

test("test close is called correctly", async () => {
vi.useFakeTimers();

const onClose = vi.fn();
const onDestroy = vi.fn();
const wrapper = mount(ProgrammaticComponent, {
props: {
component: createVNode({
template: `<button @click="$emit('close', 'abc')"></button>`,
}),
props: { "data-oruga": "programmatic" },
onClose,
onDestroy,
},
});

const button = wrapper.find("button");
await button.trigger("click");

vi.runAllTimers();

const closeEmits = wrapper.emitted("close");
expect(closeEmits).toHaveLength(1);
expect(closeEmits[0][0]).toBe("abc");
const destroyEmits = wrapper.emitted("destroy");
expect(destroyEmits).toHaveLength(1);

expect(onClose).toHaveBeenCalledOnce();
expect(onDestroy).toHaveBeenCalledOnce();

vi.useRealTimers();
});

test("test promise is called correctly", async () => {
const wrapper = mount(ProgrammaticComponent, {
props: {
component: "div",
props: { "data-oruga": "programmatic" },
},
});

const component = wrapper.vm as unknown as ProgrammaticComponentExpose;
expect(component.promise).not.toBeUndefined();

// check promise get called
const handler = vi.fn();
component.promise.then(() => handler());
expect(handler).not.toHaveBeenCalled();

component.close(); // call close programmaticaly

const closeEmits = wrapper.emitted("close");
expect(closeEmits).toHaveLength(1);

await flushPromises(); // await promise finished
expect(handler).toHaveBeenCalledOnce();
});

test("test instance registry is called correctly", async () => {
const instanceRegistry = new InstanceRegistry();

expect(instanceRegistry.entries).toHaveLength(0);

const wrapper = mount(ProgrammaticComponent, {
props: {
component: "div",
instances: instanceRegistry,
},
});

expect(instanceRegistry.entries).toHaveLength(1);

wrapper.unmount();

expect(instanceRegistry.entries).toHaveLength(0);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`ProgrammaticComponent tests > test render simple div correctly 1`] = `"<div data-oruga="programmatic"></div>"`;
Loading

0 comments on commit 08867e5

Please sign in to comment.