Skip to content

Commit d736788

Browse files
author
Alex Lohr
committed
add renderDirective method
improve documentation
1 parent 36f9b19 commit d736788

6 files changed

Lines changed: 141 additions & 17 deletions

File tree

README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,59 @@ Solid.js' reactive changes are pretty instantaneous, so there is rarely need to
8080

8181
⚠️ Solid.js external reactive state does not require any DOM elements to run in, so our `renderHook` call has no `container`, `baseElement` or queries in its options or return value. Instead, it has an `owner` to be used with [`runWithOwner`](https://www.solidjs.com/docs/latest/api#runwithowner) if required. It also exposes a `cleanup` function, though this is already automatically called after the test is finished.
8282

83+
```ts
84+
function renderHook<Args extends any[], Result>(
85+
hook: (...args: Args) => Result,
86+
options: {
87+
initialProps?: Args,
88+
wrapper?: Component<{ children: JSX.Element }>
89+
}
90+
) => {
91+
result: Result;
92+
owner: Owner | null;
93+
cleanup: () => void;
94+
}
95+
```
96+
97+
This can be used to easily test a hook / primitive:
98+
99+
```ts
100+
const { result } = renderHook(createResult);
101+
expect(result).toBe(true);
102+
```
103+
104+
⚠️ Solid.js supports [custom directives](https://www.solidjs.com/docs/latest/api#use___), which is a convenient pattern to tie custom behavior to elements, so we also have a `renderDirective` call, which augments `renderHook` to take a directive as first argument, accept an `initialValue` for the argument and a `targetElement` (string, HTMLElement or function returning a HTMLElement) in the `options` and also returns `arg` and `setArg` to read and manipulate the argument of the directive.
105+
106+
```ts
107+
function renderDirective<
108+
Arg extends any,
109+
Elem extends HTMLElement
110+
>(
111+
directive: (ref: Elem, arg: Accessor<Arg>) => void,
112+
options?: {
113+
...renderOptions,
114+
initialValue: Arg,
115+
targetElement:
116+
| Lowercase<Elem['nodeName']>
117+
| Elem
118+
| (() => Elem)
119+
}
120+
): Result & { arg: Accessor<Arg>, setArg: Setter<Arg> };
121+
```
122+
123+
This allows for very effective and concise testing of directives:
124+
125+
```ts
126+
const { asFragment, setArg } = renderDirective(myDirective);
127+
expect(asFragment()).toBe(
128+
'<div data-directive="works"></div>'
129+
);
130+
setArg("perfect");
131+
expect(asFragment()).toBe(
132+
'<div data-directive="perfect"></div>'
133+
);
134+
```
135+
83136
## Issues
84137

85138
If you find any issues, please [check on the issues page](https://github.com/solidjs/solid-testing-library/issues) if they are already known. If not, opening an issue will be much appreciated, even more so if it contains a

package-lock.json

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@solidjs/testing-library",
3-
"version": "0.5.2",
3+
"version": "0.6.0",
44
"description": "Simple and complete Solid testing utilities that encourage good testing practices.",
55
"type": "module",
66
"main": "./dist/index.cjs",
@@ -64,7 +64,7 @@
6464
"@testing-library/user-event": "^14.4.3",
6565
"coveralls": "^3.1.1",
6666
"jsdom": "^21.0.0",
67-
"prettier": "^2.8.2",
67+
"prettier": "^2.8.3",
6868
"pretty-format": "^29.3.1",
6969
"solid-js": "^1.6.9",
7070
"tsup": "6.5.0",

src/__tests__/basic.tsx

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import "@testing-library/jest-dom/extend-expect";
2-
import { createSignal, createEffect, createContext, useContext, ParentComponent } from "solid-js";
3-
import { render, renderHook, screen } from "..";
2+
import {
3+
createSignal,
4+
createEffect,
5+
createContext,
6+
useContext,
7+
ParentComponent,
8+
Accessor
9+
} from "solid-js";
10+
import type { JSX } from "solid-js";
11+
import { render, renderDirective, renderHook, screen } from "..";
412
import userEvent from "@testing-library/user-event";
5-
import { m } from "vitest/dist/index-2f5b6168";
613

714
declare global {
815
var _$HY: Record<string, any>;
@@ -122,3 +129,37 @@ test("wrapper context is available in renderHook", () => {
122129
const { result } = renderHook(testHook, { wrapper: Wrapper });
123130
expect(result).toBe("context value");
124131
});
132+
133+
declare module "solid-js" {
134+
namespace JSX {
135+
interface Directives {
136+
noArgDirective: boolean;
137+
argDirective: string;
138+
}
139+
}
140+
}
141+
142+
type NoArgDirectiveArg = Accessor<JSX.Directives["noArgDirective"]>;
143+
144+
test("renderDirective works for directives without an argument", () => {
145+
const noArgDirective: (ref: HTMLElement, arg: NoArgDirectiveArg) => void = (ref: HTMLElement) => {
146+
ref.dataset.directive = "works";
147+
};
148+
const { asFragment } = renderDirective(noArgDirective);
149+
expect(asFragment()).toBe('<div data-directive="works"></div>');
150+
});
151+
152+
test("renderDirective works for directives with argument", () => {
153+
const argDirective = (ref: HTMLSpanElement, arg: Accessor<string>) => {
154+
createEffect(() => {
155+
ref.dataset.directive = arg();
156+
});
157+
};
158+
const { asFragment, setArg } = renderDirective(argDirective, {
159+
initialValue: "initial value",
160+
targetElement: "span"
161+
});
162+
expect(asFragment()).toBe('<span data-directive="initial value"></span>');
163+
setArg("updated value");
164+
expect(asFragment()).toBe('<span data-directive="updated value"></span>');
165+
});

src/index.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { getQueriesForElement, prettyDOM } from "@testing-library/dom";
2-
import { ComponentProps, createComponent, createRoot, getOwner, JSX } from "solid-js";
2+
import { Accessor, ComponentProps, createComponent, createRoot, createSignal, getOwner, JSX, onMount, Setter } from "solid-js";
33
import { hydrate as solidHydrate, render as solidRender } from "solid-js/web";
44

5-
import type { Ui, Result, Options, Ref, RenderHookResult, RenderHookOptions } from "./types";
5+
import type { Ui, Result, Options, Ref, RenderHookResult, RenderHookOptions, RenderDirectiveOptions, RenderDirectiveResult } from "./types";
66

77
/* istanbul ignore next */
88
if (!process.env.STL_SKIP_AUTO_CLEANUP) {
@@ -80,6 +80,26 @@ export function renderHook<A extends any[], R>(
8080
return { result, cleanup: dispose, owner };
8181
}
8282

83+
export function renderDirective<A extends any, U extends A, E extends HTMLElement>(
84+
directive: (ref: E, arg: Accessor<U>) => void,
85+
options?: RenderDirectiveOptions<U, E>
86+
): RenderDirectiveResult<U> {
87+
const [arg, setArg] = createSignal(options?.initialValue as U);
88+
return Object.assign(render(() => {
89+
const targetElement = options?.targetElement &&
90+
(options.targetElement instanceof HTMLElement
91+
? options.targetElement
92+
: typeof options.targetElement === 'string'
93+
? document.createElement(options.targetElement)
94+
: typeof options.targetElement === 'function'
95+
? options.targetElement()
96+
: undefined) ||
97+
document.createElement('div');
98+
onMount(() => directive(targetElement as E, arg as Accessor<U>));
99+
return targetElement;
100+
}, options), { arg, setArg });
101+
}
102+
83103
function cleanupAtContainer(ref: Ref) {
84104
const { container, dispose } = ref;
85105
dispose();

src/types.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Component, JSX, Owner } from "solid-js";
1+
import type { Accessor, Component, JSX, Owner, Setter } from "solid-js";
22
import { queries } from "@testing-library/dom";
33
import type { Queries, BoundFunctions, prettyFormat } from "@testing-library/dom";
44

@@ -41,3 +41,13 @@ export type RenderHookResult<R> = {
4141
owner: Owner | null;
4242
cleanup: () => void;
4343
};
44+
45+
export type RenderDirectiveOptions<A extends any, E extends HTMLElement = HTMLDivElement> = Options & {
46+
initialValue?: A;
47+
targetElement?: Lowercase<E['nodeName']> | E | (() => E);
48+
};
49+
50+
export type RenderDirectiveResult<A extends any> = Result & {
51+
arg: Accessor<A>,
52+
setArg: Setter<A>
53+
};

0 commit comments

Comments
 (0)