Skip to content

Commit

Permalink
feat: export our own async version of fireEvent (#3)
Browse files Browse the repository at this point in the history
* feat: export our own async version of fireEvent

The promise returned from fireEvent resolves once any pending updates
caused by the event have completed.

BREAKING CHANGE: fireEvent no longer returns a boolean and instead
returns a promise.
  • Loading branch information
mlrawlings authored and DylanPiercey committed Jul 30, 2019
1 parent f277590 commit 7ff8fbe
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 79 deletions.
87 changes: 52 additions & 35 deletions src/__tests__/render.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,18 @@ test("renders interactive content in the document", async () => {
const { getByText } = await render(Counter);
expect(getByText(/Value: 0/)).toBeInTheDocument();

fireEvent.click(getByText("Increment"));
await fireEvent.click(getByText("Increment"));

await wait(() => expect(getByText("Value: 1")));
expect(getByText("Value: 1")).toBeInTheDocument();
});

test("renders interactive content in the document with Marko 3", async () => {
const { getByText } = await render(LegacyCounter);
expect(getByText(/Value: 0/)).toBeInTheDocument();

fireEvent.click(getByText("Increment"));
await fireEvent.click(getByText("Increment"));

await wait(() => expect(getByText("Value: 1")));
expect(getByText("Value: 1")).toBeInTheDocument();
});

test("can be rerendered with new input", async () => {
Expand Down Expand Up @@ -63,54 +63,56 @@ test("records user events", async () => {
const { getByText, emitted } = await render(Clickable);
const button = getByText("Click");

fireEvent.click(button);
fireEvent.click(button);
await fireEvent.click(button);
await fireEvent.click(button);

expect(emitted("unknown-event")).toHaveProperty("length", 0);

expect(emitted("button-click")).toMatchInlineSnapshot(`
Array [
Array [
Object {
"count": 0,
},
],
Array [
Object {
"count": 1,
},
],
]
`);

expect(emitted().map(({ type }) => type)).toMatchInlineSnapshot(`
Array [
"button-click",
"button-click",
Array [
Object {
"count": 0,
},
],
Array [
Object {
"count": 1,
},
],
]
`);

fireEvent.click(button);
expect(emitted().map(({ type }) => type)).toMatchInlineSnapshot(`
Array [
"button-click",
"button-click",
]
`);

await fireEvent.click(button);

expect(emitted("button-click")).toMatchInlineSnapshot(`
Array [
Array [
Object {
"count": 2,
},
],
]
`);
expect(emitted().map(({ type }) => type)).toMatchInlineSnapshot(`
Array [
"button-click",
Array [
Object {
"count": 2,
},
],
]
`);
expect(emitted().map(({ type }) => type)).toMatchInlineSnapshot(`
Array [
"button-click",
]
`);
});

test("errors when trying to record internal events", async () => {
const { emitted } = await render(Clickable);
expect(() => emitted("mount" as string)).toThrow(/internal events/);
expect(() => emitted("mount" as string)).toThrowErrorMatchingInlineSnapshot(
`"The emitted helper cannot be used to listen to internal events."`
);
});

test("cleanup removes content from the document", async () => {
Expand All @@ -126,3 +128,18 @@ test("can render into a different container", async () => {
const { getByText } = await render(HelloWorld, null, { container });
expect(getByText("Hello World")).toHaveProperty("parentNode", container);
});

test("fireEvent waits for pending updates", async () => {
const { getByText } = await render(Counter);
expect(getByText(/Value: 0/)).toBeInTheDocument();

await fireEvent(
getByText("Increment"),
new MouseEvent("click", {
bubbles: true,
cancelable: true
})
);

expect(getByText("Value: 1")).toBeInTheDocument();
});
16 changes: 12 additions & 4 deletions src/__tests__/render.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,25 @@ test("renders static content from a Marko 3 component", async () => {

test("fails when rerendering", async () => {
const { rerender } = await render(HelloName, { name: "Michael" });
await expect(rerender({ name: "Dylan" })).rejects.toThrow(/cannot re-render/);
await expect(
rerender({ name: "Dylan" })
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Components cannot re-render on the server side"`
);
});

test("fails when checking emitted events", async () => {
const { emitted } = await render(Clickable);
expect(() => emitted("button-click")).toThrow(/should not emit events/);
expect(() => emitted("button-click")).toThrowErrorMatchingInlineSnapshot(
`"Components should not emit events on the server side"`
);
});

test("fails when emitting events", async () => {
const { getByText } = await render(Counter);
expect(() => fireEvent.click(getByText("Increment"))).toThrow(
/fireEvent currently supports/
await expect(
fireEvent.click(getByText("Increment"))
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Unable to find the \\"window\\" object for the given node. fireEvent currently supports firing events on DOM nodes, document, and window. Please file an issue with the code that's causing you to see this error: https://github.com/testing-library/dom-testing-library/issues/new"`
);
});
12 changes: 6 additions & 6 deletions src/index-browser.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { within, prettyDOM } from "@testing-library/dom";
import {
AsyncReturnValue,
RenderOptions,
Template,
EventRecord,
InternalEventNames,
INTERNAL_EVENTS
} from "./types";
} from "./shared";

const mountedComponents = new Set();

export * from "@testing-library/dom";
export { FireFunction, FireObject, fireEvent } from "./shared";

const mountedComponents = new Set();
export type RenderResult = AsyncReturnValue<typeof render>;

export async function render<T extends Template>(
template: T,
Expand Down Expand Up @@ -114,10 +118,6 @@ export function cleanup() {
mountedComponents.forEach(destroyComponent);
}

export type RenderResult = Parameters<
NonNullable<Parameters<ReturnType<typeof render>["then"]>[0]>
>[0];

function destroyComponent(component) {
const { instance, container, isDefaultContainer } = component;

Expand Down
14 changes: 7 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { JSDOM } from "jsdom";
import { within, prettyDOM } from "@testing-library/dom";
import {
AsyncReturnValue,
RenderOptions,
Template,
EventRecord,
InternalEventNames
} from "./types";
} from "./shared";

export * from "@testing-library/dom";
export { FireFunction, FireObject, fireEvent } from "./shared";

export type RenderResult = AsyncReturnValue<typeof render>;

export async function render<T extends Template>(
template: T,
Expand All @@ -31,11 +35,11 @@ export async function render<T extends Template>(
emitted<N extends string = "*">(
type?: N extends InternalEventNames ? never : N
): NonNullable<EventRecord[N]> {
throw new Error("Component's should not emit events on the server side");
throw new Error("Components should not emit events on the server side");
},
rerender(newInput?: typeof input): Promise<void> {
return Promise.reject(
new Error("Component's cannot re-render on the server side")
new Error("Components cannot re-render on the server side")
);
},
// eslint-disable-next-line no-console
Expand All @@ -56,7 +60,3 @@ export async function render<T extends Template>(

/* istanbul ignore next: There is no cleanup for SSR. */
export function cleanup() {}

export type RenderResult = Parameters<
NonNullable<Parameters<ReturnType<typeof render>["then"]>[0]>
>[0];
68 changes: 68 additions & 0 deletions src/shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {
fireEvent as originalFireEvent,
wait,
EventType,
FireFunction as originalFireFunction,
FireObject as originalFireObject
} from "@testing-library/dom";

export interface EventRecord {
"*"?: Array<{ type: string; args: unknown[] }>;
[x: string]: unknown[] | undefined;
}

export interface RenderOptions {
container?: Element | DocumentFragment;
}

export interface Template {
renderToString(
input: unknown,
cb: (err: Error | null, result: any) => void
): any;
render(input: unknown, cb: (err: Error | null, result: any) => void): any;
}

export const INTERNAL_EVENTS = [
"create",
"input",
"render",
"mount",
"update",
"destroy"
] as const;

export type InternalEventNames = typeof INTERNAL_EVENTS[number];

export type FireFunction = (
...params: Parameters<originalFireFunction>
) => Promise<void>;

export type FireObject = {
[K in EventType]: (
...params: Parameters<originalFireObject[keyof originalFireObject]>
) => Promise<void>;
};

export const fireEvent = (async (...params) => {
originalFireEvent(...params);
await wait();
}) as FireFunction & FireObject;

Object.keys(originalFireEvent).forEach((eventName: EventType) => {
const fire = originalFireEvent[eventName];
fireEvent[eventName] = async (...params) => {
fire(...params);

// TODO: this waits for a possible update using setTimeout(0) which should
// be sufficient, but ideally we would hook into the Marko lifecycle to
// determine when all pending updates are complete.
await wait();
};
});

export type AsyncReturnValue<
AsyncFunction extends (...args: any) => Promise<any>
> = Parameters<
NonNullable<Parameters<ReturnType<AsyncFunction>["then"]>[0]>
>[0];
27 changes: 0 additions & 27 deletions src/types.ts

This file was deleted.

0 comments on commit 7ff8fbe

Please sign in to comment.