@tito10047/stimulus-test-utils 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/dist/index.d.ts +175 -0
- package/dist/index.js +684 -0
- package/dist/index.js.map +1 -0
- package/dist/register.d.ts +2 -0
- package/dist/register.js +32 -0
- package/dist/register.js.map +1 -0
- package/package.json +76 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tito10047
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# @tito10047/stimulus-test-utils
|
|
2
|
+
|
|
3
|
+
[](https://github.com/tito10047/stimulus-test-utils/actions/workflows/test.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@tito10047/stimulus-test-utils)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
|
|
7
|
+
Zero‑config, Testing‑Library‑flavoured test harness for [Stimulus](https://stimulus.hotwired.dev) controllers.
|
|
8
|
+
|
|
9
|
+
Write your tests in plain JavaScript or TypeScript, mount a controller with a single call, simulate user interactions, and assert against the DOM / controller state — without ever touching happy‑dom/JSDOM, the Stimulus `Application`, or `MutationObserver` timing by hand.
|
|
10
|
+
|
|
11
|
+
## What it's for
|
|
12
|
+
|
|
13
|
+
Testing Stimulus controllers usually means:
|
|
14
|
+
|
|
15
|
+
- setting `document.body.innerHTML`,
|
|
16
|
+
- creating and starting an `Application`,
|
|
17
|
+
- registering the controller,
|
|
18
|
+
- waiting for `connect()` via `MutationObserver` or `await nextTick()`,
|
|
19
|
+
- cleaning up after every test.
|
|
20
|
+
|
|
21
|
+
This library hides all of that behind a single `render()` call and exposes a familiar [Testing Library](https://testing-library.com/)-style API (`getByRole`, `findByText`, `user.click`, …).
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install -D @tito10047/stimulus-test-utils @hotwired/stimulus vitest happy-dom
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`@hotwired/stimulus` is a **peer dependency** — you bring the version your app uses.
|
|
30
|
+
|
|
31
|
+
`vitest.config.ts`:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { defineConfig } from 'vitest/config'
|
|
35
|
+
|
|
36
|
+
export default defineConfig({
|
|
37
|
+
test: {
|
|
38
|
+
environment: 'happy-dom',
|
|
39
|
+
setupFiles: ['@tito10047/stimulus-test-utils/register'],
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The `/register` module wires up `afterEach(cleanup)` automatically. If you prefer to clean up manually, omit `setupFiles` and call `cleanup()` yourself.
|
|
45
|
+
|
|
46
|
+
## Quick example
|
|
47
|
+
|
|
48
|
+
Controller:
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
// hello_controller.js
|
|
52
|
+
import { Controller } from '@hotwired/stimulus'
|
|
53
|
+
|
|
54
|
+
export default class extends Controller {
|
|
55
|
+
static targets = ['name', 'output']
|
|
56
|
+
static values = { greeting: { type: String, default: 'Hello' } }
|
|
57
|
+
|
|
58
|
+
greet() {
|
|
59
|
+
this.outputTarget.textContent = `${this.greetingValue}, ${this.nameTarget.value}!`
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Test:
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
import {
|
|
68
|
+
render,
|
|
69
|
+
stimulusController,
|
|
70
|
+
stimulusTarget,
|
|
71
|
+
stimulusAction,
|
|
72
|
+
} from '@tito10047/stimulus-test-utils'
|
|
73
|
+
import { expect, test } from 'vitest'
|
|
74
|
+
import HelloController from './hello_controller.js'
|
|
75
|
+
|
|
76
|
+
test('greets by name', async () => {
|
|
77
|
+
const { element, controller, user, getByRole } = await render(HelloController, {
|
|
78
|
+
html: `
|
|
79
|
+
<div ${stimulusController('hello', { greeting: 'Hi' })}>
|
|
80
|
+
<input ${stimulusTarget('hello', 'name')} />
|
|
81
|
+
<button ${stimulusAction('hello', 'greet', 'click')}>Greet</button>
|
|
82
|
+
<span ${stimulusTarget('hello', 'output')}></span>
|
|
83
|
+
</div>
|
|
84
|
+
`,
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
await user.type(element.querySelector('input'), 'Ada')
|
|
88
|
+
await user.click(getByRole('button', { name: 'Greet' }))
|
|
89
|
+
|
|
90
|
+
expect(controller.outputTarget.textContent).toBe('Hi, Ada!')
|
|
91
|
+
})
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
No `Application.start()`, no `document.body.innerHTML = …`, no manual `await nextTick()`.
|
|
95
|
+
|
|
96
|
+
## What you get
|
|
97
|
+
|
|
98
|
+
- **`render(ControllerClass, options)`** — mounts the fixture, starts an `Application`, registers the controller and waits for `connect()`.
|
|
99
|
+
- **Query helpers** — `getByRole`, `getByText`, `getByTestId`, `findBy*`, `queryBy*`, `getAllBy*` scoped to the mounted root.
|
|
100
|
+
- **`user`** — user‑event simulations: `click`, `type`, `keyboard`, `hover`, …
|
|
101
|
+
- **`waitFor` / `nextTick`** — async assertions for reactive DOM changes.
|
|
102
|
+
- **Attribute helpers** — `stimulusController`, `stimulusTarget`, `stimulusAction`, `combine` produce safe, typo‑free `data-*` attributes.
|
|
103
|
+
- **`cleanup()`** — automatically stops the `Application` and removes the fixture (via the `/register` setup, or called manually).
|
|
104
|
+
|
|
105
|
+
A complete API overview is available in [`public_api.md`](./public_api.md) and on the documentation site (see [Documentation](#documentation) below).
|
|
106
|
+
|
|
107
|
+
## Supported versions
|
|
108
|
+
|
|
109
|
+
- Node.js `18.x`, `20.x`, `22.x`
|
|
110
|
+
- `@hotwired/stimulus` `^3.2`
|
|
111
|
+
- Vitest `^2`
|
|
112
|
+
|
|
113
|
+
## Contributing
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
npm ci
|
|
117
|
+
npm test # vitest in watch mode
|
|
118
|
+
npm run typecheck # tsc --noEmit
|
|
119
|
+
npm run build # tsup -> dist/
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Pull requests are welcome. Before opening a PR, please run `npm run typecheck` and `npx vitest run`.
|
|
123
|
+
|
|
124
|
+
## Documentation
|
|
125
|
+
|
|
126
|
+
The full documentation lives in [`docs/`](./docs) and is published at **https://tito10047.github.io/stimulus-test-utils/**.
|
|
127
|
+
|
|
128
|
+
It is built with [VitePress](https://vitepress.dev/) (prose + navigation) and [TypeDoc](https://typedoc.org/) with [`typedoc-plugin-markdown`](https://github.com/tgreyuk/typedoc-plugin-markdown) (auto‑generated API reference from TSDoc comments in `src/`).
|
|
129
|
+
|
|
130
|
+
### Working on the docs locally
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
npm ci
|
|
134
|
+
npm run docs:dev # start dev server on http://localhost:5173
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
This generates the API reference into `docs/api/generated/` (gitignored) and runs the VitePress dev server with hot‑reload. Edit any `.md` file under `docs/` and see the change instantly.
|
|
138
|
+
|
|
139
|
+
### Building a static site
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
npm run docs:build # runs docs:api, then vitepress build
|
|
143
|
+
npm run docs:preview # serve the production build locally
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The static site is emitted to `docs/.vitepress/dist/`.
|
|
147
|
+
|
|
148
|
+
### Deploying to GitHub Pages
|
|
149
|
+
|
|
150
|
+
Deployment is fully automated via [`.github/workflows/docs.yml`](./.github/workflows/docs.yml):
|
|
151
|
+
|
|
152
|
+
- **Trigger:** every push to `main`, or a manual run from the *Actions* tab (`workflow_dispatch`).
|
|
153
|
+
- **Build:** `npm ci` → `npm run docs:build` → upload `docs/.vitepress/dist` as a Pages artifact.
|
|
154
|
+
- **Deploy:** `actions/deploy-pages@v4` publishes it.
|
|
155
|
+
|
|
156
|
+
One-time repository setup (only needed once):
|
|
157
|
+
|
|
158
|
+
1. Go to **Settings → Pages**.
|
|
159
|
+
2. Set **Source** to **GitHub Actions**.
|
|
160
|
+
3. Push to `main` (or trigger the workflow manually). The first run will populate the URL shown above.
|
|
161
|
+
|
|
162
|
+
> If you fork the repository, update the `base` option in [`docs/.vitepress/config.ts`](./docs/.vitepress/config.ts) to match your repository name (for example `/my-fork/`), and tweak the GitHub link in the same file.
|
|
163
|
+
|
|
164
|
+
## License
|
|
165
|
+
|
|
166
|
+
[MIT](./LICENSE) © tito10047
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { Controller, Application } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
type ControllerConstructor = new (...args: any[]) => Controller;
|
|
4
|
+
interface RenderOptions {
|
|
5
|
+
html: string | HTMLElement;
|
|
6
|
+
identifier?: string;
|
|
7
|
+
controllers?: Record<string, ControllerConstructor>;
|
|
8
|
+
application?: Application;
|
|
9
|
+
container?: HTMLElement;
|
|
10
|
+
}
|
|
11
|
+
interface WaitForOptions {
|
|
12
|
+
timeout?: number;
|
|
13
|
+
interval?: number;
|
|
14
|
+
}
|
|
15
|
+
interface UserEvent {
|
|
16
|
+
click(el: Element): Promise<void>;
|
|
17
|
+
dblClick(el: Element): Promise<void>;
|
|
18
|
+
hover(el: Element): Promise<void>;
|
|
19
|
+
type(el: Element, text: string): Promise<void>;
|
|
20
|
+
clear(el: Element): Promise<void>;
|
|
21
|
+
keyboard(keys: string): Promise<void>;
|
|
22
|
+
tab(opts?: {
|
|
23
|
+
shift?: boolean;
|
|
24
|
+
}): Promise<void>;
|
|
25
|
+
selectOption(select: HTMLSelectElement, value: string | string[]): Promise<void>;
|
|
26
|
+
submit(form: HTMLFormElement): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
interface QueryHelpers {
|
|
29
|
+
getByTestId(id: string): HTMLElement;
|
|
30
|
+
queryByTestId(id: string): HTMLElement | null;
|
|
31
|
+
findByTestId(id: string, opts?: WaitForOptions): Promise<HTMLElement>;
|
|
32
|
+
getAllByTestId(id: string): HTMLElement[];
|
|
33
|
+
getByRole(role: string, opts?: {
|
|
34
|
+
name?: string | RegExp;
|
|
35
|
+
}): HTMLElement;
|
|
36
|
+
queryByRole(role: string, opts?: {
|
|
37
|
+
name?: string | RegExp;
|
|
38
|
+
}): HTMLElement | null;
|
|
39
|
+
findByRole(role: string, opts?: {
|
|
40
|
+
name?: string | RegExp;
|
|
41
|
+
} & WaitForOptions): Promise<HTMLElement>;
|
|
42
|
+
getAllByRole(role: string, opts?: {
|
|
43
|
+
name?: string | RegExp;
|
|
44
|
+
}): HTMLElement[];
|
|
45
|
+
getByText(text: string | RegExp): HTMLElement;
|
|
46
|
+
queryByText(text: string | RegExp): HTMLElement | null;
|
|
47
|
+
findByText(text: string | RegExp, opts?: WaitForOptions): Promise<HTMLElement>;
|
|
48
|
+
getAllByText(text: string | RegExp): HTMLElement[];
|
|
49
|
+
getByLabelText(text: string | RegExp): HTMLElement;
|
|
50
|
+
queryByLabelText(text: string | RegExp): HTMLElement | null;
|
|
51
|
+
findByLabelText(text: string | RegExp, opts?: WaitForOptions): Promise<HTMLElement>;
|
|
52
|
+
}
|
|
53
|
+
interface RenderResult<C extends Controller = Controller> extends QueryHelpers {
|
|
54
|
+
controller: C;
|
|
55
|
+
element: HTMLElement;
|
|
56
|
+
application: Application;
|
|
57
|
+
user: UserEvent;
|
|
58
|
+
waitFor: <T>(cb: () => T | Promise<T>, opts?: WaitForOptions) => Promise<T>;
|
|
59
|
+
rerender(next: {
|
|
60
|
+
html: string | HTMLElement;
|
|
61
|
+
}): Promise<void>;
|
|
62
|
+
unmount(): void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Infer a Stimulus identifier from a controller class name.
|
|
67
|
+
* HelloController → "hello"
|
|
68
|
+
* HelloWorldController → "helloworld"
|
|
69
|
+
* APIController → "api"
|
|
70
|
+
* Anonymous / minified → throws (caller must pass options.identifier)
|
|
71
|
+
*
|
|
72
|
+
* The rule mirrors Symfony UX / Asset Mapper behaviour: the class name is
|
|
73
|
+
* lowercased as a whole (CamelCase is NOT split with dashes). If you want a
|
|
74
|
+
* hyphenated identifier, pass `options.identifier` explicitly.
|
|
75
|
+
*/
|
|
76
|
+
declare function inferIdentifier(ctor: ControllerConstructor): string;
|
|
77
|
+
declare function render<C extends Controller>(ControllerClass: new (...args: any[]) => C, options: RenderOptions): Promise<RenderResult<C>>;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Stimulus identifier utilities.
|
|
81
|
+
*
|
|
82
|
+
* Stimulus convention for controllers living in sub-folders:
|
|
83
|
+
* ./assets/controllers/MyApp/MyController_controller.js → "myapp--mycontroller"
|
|
84
|
+
* ./assets/controllers/Users/List_controller.js → "users--list"
|
|
85
|
+
* ./assets/controllers/hello_controller.js → "hello"
|
|
86
|
+
*
|
|
87
|
+
* Rules:
|
|
88
|
+
* - Directory separators "/" become "--".
|
|
89
|
+
* - Each path segment is lowercased (CamelCase is NOT split into kebab-case
|
|
90
|
+
* — "MyApp" becomes "myapp", not "my-app"). This mirrors the Symfony UX
|
|
91
|
+
* / Asset Mapper / @hotwired/stimulus-webpack-helpers behaviour.
|
|
92
|
+
* - Any trailing "_controller" / "-controller" / "Controller" suffix is
|
|
93
|
+
* stripped from the last segment.
|
|
94
|
+
* - The leading "./", "assets/controllers/", "controllers/" prefix is
|
|
95
|
+
* stripped so you can paste the full path as-is.
|
|
96
|
+
* - File extension (".js" / ".ts" / ".mjs" / ".tsx") is stripped.
|
|
97
|
+
*/
|
|
98
|
+
declare function identifierFromPath(filePath: string): string;
|
|
99
|
+
/**
|
|
100
|
+
* Normalize whatever identifier-ish string the user handed us into the
|
|
101
|
+
* canonical Stimulus identifier:
|
|
102
|
+
*
|
|
103
|
+
* "hello" → "hello" (plain, untouched)
|
|
104
|
+
* "myapp--mycontroller" → "myapp--mycontroller" (already canonical)
|
|
105
|
+
* "MyApp/MyController" → "myapp--mycontroller"
|
|
106
|
+
* "MyApp/MyController_controller.js" → "myapp--mycontroller"
|
|
107
|
+
* "users/list_controller" → "users--list"
|
|
108
|
+
*
|
|
109
|
+
* Rules:
|
|
110
|
+
* - If the string contains "/" or an uppercase letter or ends with a file
|
|
111
|
+
* extension, we route it through `identifierFromPath`.
|
|
112
|
+
* - A trailing `_controller` on a plain (no‑slash) string is also stripped
|
|
113
|
+
* so `stimulusController("hello_controller")` DWIMs into `hello`.
|
|
114
|
+
* - Otherwise we return it as-is (so already-valid identifiers like
|
|
115
|
+
* "hello", "myapp--mycontroller", "data-picker" round‑trip unchanged).
|
|
116
|
+
*/
|
|
117
|
+
declare function normalizeIdentifier(raw: string): string;
|
|
118
|
+
|
|
119
|
+
declare function cleanup(): void;
|
|
120
|
+
|
|
121
|
+
declare function createUserEvent(): UserEvent;
|
|
122
|
+
declare function fireEvent(target: EventTarget, eventOrName: Event | string, init?: EventInit): Promise<void>;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Flush microtasks + one macrotask tick so Stimulus' MutationObserver
|
|
126
|
+
* observers and scheduled callbacks settle before the test continues.
|
|
127
|
+
*/
|
|
128
|
+
declare function nextTick(): Promise<void>;
|
|
129
|
+
declare function waitFor<T>(callback: () => T | Promise<T>, options?: WaitForOptions): Promise<T>;
|
|
130
|
+
|
|
131
|
+
/** Convert camelCase / snake_case / PascalCase to kebab-case. */
|
|
132
|
+
declare function toKebabCase(input: string): string;
|
|
133
|
+
/**
|
|
134
|
+
* HTML-escape a value for safe placement inside a double-quoted attribute.
|
|
135
|
+
* We intentionally only escape `&` and `"` — `<` / `>` / `'` are valid
|
|
136
|
+
* inside double-quoted attribute values per the HTML spec, and escaping
|
|
137
|
+
* them would mangle Stimulus' own syntax (e.g. `click->hello#greet`).
|
|
138
|
+
*/
|
|
139
|
+
declare function escapeAttr(value: string): string;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* HTML attribute helpers producing AttrSpec objects that serialize
|
|
143
|
+
* transparently inside template literals via Symbol.toPrimitive.
|
|
144
|
+
*
|
|
145
|
+
* Design notes:
|
|
146
|
+
* - Every helper returns an AttrSpec — a structured description of the
|
|
147
|
+
* attributes it contributes. `combine()` merges specs by reading the
|
|
148
|
+
* structured data, not by parsing strings.
|
|
149
|
+
* - `toString()` is the single point of serialization (HTML‑escaping,
|
|
150
|
+
* kebab‑case conversion, JSON encoding of complex values).
|
|
151
|
+
*/
|
|
152
|
+
|
|
153
|
+
interface AttrSpec {
|
|
154
|
+
toString(): string;
|
|
155
|
+
toJSON(): string;
|
|
156
|
+
[Symbol.toPrimitive](hint: string): string;
|
|
157
|
+
}
|
|
158
|
+
declare function stimulusController(identifier: string, values?: Record<string, unknown>, classes?: Record<string, string>, outlets?: Record<string, string>): AttrSpec;
|
|
159
|
+
declare function stimulusTarget(identifier: string, ...targetNames: string[]): AttrSpec;
|
|
160
|
+
interface StimulusActionOptions {
|
|
161
|
+
prevent?: boolean;
|
|
162
|
+
stop?: boolean;
|
|
163
|
+
once?: boolean;
|
|
164
|
+
passive?: boolean;
|
|
165
|
+
capture?: boolean;
|
|
166
|
+
self?: boolean;
|
|
167
|
+
}
|
|
168
|
+
declare function stimulusAction(identifier: string, method: string, event?: string, options?: StimulusActionOptions): AttrSpec;
|
|
169
|
+
/**
|
|
170
|
+
* Merge multiple AttrSpecs onto a single element.
|
|
171
|
+
* Throws on duplicate controller identifier.
|
|
172
|
+
*/
|
|
173
|
+
declare function combine(...specs: AttrSpec[]): AttrSpec;
|
|
174
|
+
|
|
175
|
+
export { type AttrSpec, type ControllerConstructor, type QueryHelpers, type RenderOptions, type RenderResult, type StimulusActionOptions, type UserEvent, type WaitForOptions, cleanup, combine, createUserEvent, escapeAttr, fireEvent, identifierFromPath, inferIdentifier, nextTick, normalizeIdentifier, render, stimulusAction, stimulusController, stimulusTarget, toKebabCase, waitFor };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
import { Application } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
// src/render.ts
|
|
4
|
+
|
|
5
|
+
// src/wait-for.ts
|
|
6
|
+
async function nextTick() {
|
|
7
|
+
await Promise.resolve();
|
|
8
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
9
|
+
await Promise.resolve();
|
|
10
|
+
}
|
|
11
|
+
var DEFAULT_TIMEOUT = 1e3;
|
|
12
|
+
var DEFAULT_INTERVAL = 20;
|
|
13
|
+
async function waitFor(callback, options = {}) {
|
|
14
|
+
const timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
15
|
+
const interval = options.interval ?? DEFAULT_INTERVAL;
|
|
16
|
+
const deadline = Date.now() + timeout;
|
|
17
|
+
let lastError;
|
|
18
|
+
while (true) {
|
|
19
|
+
try {
|
|
20
|
+
const result = await callback();
|
|
21
|
+
return result;
|
|
22
|
+
} catch (err) {
|
|
23
|
+
lastError = err;
|
|
24
|
+
if (Date.now() >= deadline) break;
|
|
25
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
throw lastError instanceof Error ? lastError : new Error(`waitFor: timed out after ${timeout}ms`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/user-event.ts
|
|
32
|
+
function dispatch(target, event) {
|
|
33
|
+
target.dispatchEvent(event);
|
|
34
|
+
}
|
|
35
|
+
function isDisabled(el) {
|
|
36
|
+
return el.disabled === true;
|
|
37
|
+
}
|
|
38
|
+
function focusIfPossible(el) {
|
|
39
|
+
const focusable = el;
|
|
40
|
+
if (typeof focusable.focus === "function") {
|
|
41
|
+
focusable.focus();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function clickImpl(el) {
|
|
45
|
+
if (isDisabled(el)) return;
|
|
46
|
+
focusIfPossible(el);
|
|
47
|
+
dispatch(el, new MouseEvent("mousedown", { bubbles: true, cancelable: true }));
|
|
48
|
+
dispatch(el, new MouseEvent("mouseup", { bubbles: true, cancelable: true }));
|
|
49
|
+
dispatch(el, new MouseEvent("click", { bubbles: true, cancelable: true }));
|
|
50
|
+
if (el instanceof HTMLButtonElement && el.type === "submit" && el.form) {
|
|
51
|
+
await submitImpl(el.form);
|
|
52
|
+
}
|
|
53
|
+
await nextTick();
|
|
54
|
+
}
|
|
55
|
+
async function dblClickImpl(el) {
|
|
56
|
+
if (isDisabled(el)) return;
|
|
57
|
+
await clickImpl(el);
|
|
58
|
+
dispatch(el, new MouseEvent("dblclick", { bubbles: true, cancelable: true }));
|
|
59
|
+
await nextTick();
|
|
60
|
+
}
|
|
61
|
+
async function hoverImpl(el) {
|
|
62
|
+
dispatch(el, new MouseEvent("mouseover", { bubbles: true, cancelable: true }));
|
|
63
|
+
dispatch(el, new MouseEvent("mouseenter", { bubbles: false, cancelable: true }));
|
|
64
|
+
dispatch(el, new MouseEvent("mousemove", { bubbles: true, cancelable: true }));
|
|
65
|
+
await nextTick();
|
|
66
|
+
}
|
|
67
|
+
function getValueElement(el) {
|
|
68
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) return el;
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
async function typeImpl(el, text) {
|
|
72
|
+
const input = getValueElement(el);
|
|
73
|
+
if (!input) throw new TypeError("user.type(): target must be <input> or <textarea>");
|
|
74
|
+
focusIfPossible(input);
|
|
75
|
+
for (const ch of [...text]) {
|
|
76
|
+
dispatch(input, new KeyboardEvent("keydown", { key: ch, bubbles: true, cancelable: true }));
|
|
77
|
+
input.value = input.value + ch;
|
|
78
|
+
dispatch(input, new InputEvent("input", { data: ch, bubbles: true, cancelable: true }));
|
|
79
|
+
dispatch(input, new KeyboardEvent("keyup", { key: ch, bubbles: true, cancelable: true }));
|
|
80
|
+
}
|
|
81
|
+
dispatch(input, new Event("change", { bubbles: true }));
|
|
82
|
+
await nextTick();
|
|
83
|
+
}
|
|
84
|
+
async function clearImpl(el) {
|
|
85
|
+
const input = getValueElement(el);
|
|
86
|
+
if (!input) throw new TypeError("user.clear(): target must be <input> or <textarea>");
|
|
87
|
+
focusIfPossible(input);
|
|
88
|
+
input.value = "";
|
|
89
|
+
dispatch(input, new InputEvent("input", { bubbles: true, cancelable: true }));
|
|
90
|
+
dispatch(input, new Event("change", { bubbles: true }));
|
|
91
|
+
await nextTick();
|
|
92
|
+
}
|
|
93
|
+
function parseKeyboard(input) {
|
|
94
|
+
const tokens = [];
|
|
95
|
+
let i = 0;
|
|
96
|
+
while (i < input.length) {
|
|
97
|
+
const ch = input[i];
|
|
98
|
+
if (ch === "{") {
|
|
99
|
+
const end = input.indexOf("}", i);
|
|
100
|
+
if (end === -1) throw new SyntaxError(`user.keyboard: unclosed "{" in ${JSON.stringify(input)}`);
|
|
101
|
+
let body = input.slice(i + 1, end);
|
|
102
|
+
let type = "key";
|
|
103
|
+
if (body.endsWith(">")) {
|
|
104
|
+
body = body.slice(0, -1);
|
|
105
|
+
type = "down";
|
|
106
|
+
} else if (body.startsWith("/")) {
|
|
107
|
+
body = body.slice(1);
|
|
108
|
+
type = "up";
|
|
109
|
+
}
|
|
110
|
+
tokens.push({ type, key: body });
|
|
111
|
+
i = end + 1;
|
|
112
|
+
} else {
|
|
113
|
+
tokens.push({ type: "key", key: ch });
|
|
114
|
+
i++;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return tokens;
|
|
118
|
+
}
|
|
119
|
+
async function keyboardImpl(keys) {
|
|
120
|
+
const tokens = parseKeyboard(keys);
|
|
121
|
+
const target = document.activeElement ?? document.body;
|
|
122
|
+
const modifiers = /* @__PURE__ */ new Set();
|
|
123
|
+
for (const tok of tokens) {
|
|
124
|
+
const key = tok.key;
|
|
125
|
+
const init = {
|
|
126
|
+
key,
|
|
127
|
+
bubbles: true,
|
|
128
|
+
cancelable: true,
|
|
129
|
+
shiftKey: modifiers.has("Shift"),
|
|
130
|
+
ctrlKey: modifiers.has("Control"),
|
|
131
|
+
altKey: modifiers.has("Alt"),
|
|
132
|
+
metaKey: modifiers.has("Meta")
|
|
133
|
+
};
|
|
134
|
+
if (tok.type === "down") {
|
|
135
|
+
modifiers.add(key);
|
|
136
|
+
dispatch(target, new KeyboardEvent("keydown", init));
|
|
137
|
+
} else if (tok.type === "up") {
|
|
138
|
+
modifiers.delete(key);
|
|
139
|
+
dispatch(target, new KeyboardEvent("keyup", init));
|
|
140
|
+
} else {
|
|
141
|
+
dispatch(target, new KeyboardEvent("keydown", init));
|
|
142
|
+
dispatch(target, new KeyboardEvent("keyup", init));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
await nextTick();
|
|
146
|
+
}
|
|
147
|
+
async function tabImpl(opts = {}) {
|
|
148
|
+
const target = document.activeElement ?? document.body;
|
|
149
|
+
const init = {
|
|
150
|
+
key: "Tab",
|
|
151
|
+
bubbles: true,
|
|
152
|
+
cancelable: true,
|
|
153
|
+
shiftKey: !!opts.shift
|
|
154
|
+
};
|
|
155
|
+
dispatch(target, new KeyboardEvent("keydown", init));
|
|
156
|
+
dispatch(target, new KeyboardEvent("keyup", init));
|
|
157
|
+
await nextTick();
|
|
158
|
+
}
|
|
159
|
+
async function selectOptionImpl(select, value) {
|
|
160
|
+
const values = Array.isArray(value) ? value : [value];
|
|
161
|
+
if (!select.multiple && values.length > 1) {
|
|
162
|
+
throw new TypeError("user.selectOption(): cannot select multiple values on a single-select <select>");
|
|
163
|
+
}
|
|
164
|
+
let matched = 0;
|
|
165
|
+
for (const option of Array.from(select.options)) {
|
|
166
|
+
const should = values.includes(option.value);
|
|
167
|
+
option.selected = should;
|
|
168
|
+
if (should) matched++;
|
|
169
|
+
}
|
|
170
|
+
if (matched === 0) {
|
|
171
|
+
throw new Error(`user.selectOption(): no <option> matched value(s) ${JSON.stringify(value)}`);
|
|
172
|
+
}
|
|
173
|
+
dispatch(select, new Event("input", { bubbles: true }));
|
|
174
|
+
dispatch(select, new Event("change", { bubbles: true }));
|
|
175
|
+
await nextTick();
|
|
176
|
+
}
|
|
177
|
+
async function submitImpl(form) {
|
|
178
|
+
dispatch(form, new SubmitEvent("submit", { bubbles: true, cancelable: true }));
|
|
179
|
+
await nextTick();
|
|
180
|
+
}
|
|
181
|
+
function createUserEvent() {
|
|
182
|
+
return {
|
|
183
|
+
click: clickImpl,
|
|
184
|
+
dblClick: dblClickImpl,
|
|
185
|
+
hover: hoverImpl,
|
|
186
|
+
type: typeImpl,
|
|
187
|
+
clear: clearImpl,
|
|
188
|
+
keyboard: keyboardImpl,
|
|
189
|
+
tab: tabImpl,
|
|
190
|
+
selectOption: selectOptionImpl,
|
|
191
|
+
submit: submitImpl
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
async function fireEvent(target, eventOrName, init) {
|
|
195
|
+
const event = typeof eventOrName === "string" ? new CustomEvent(eventOrName, { bubbles: true, cancelable: true, ...init }) : eventOrName;
|
|
196
|
+
target.dispatchEvent(event);
|
|
197
|
+
await nextTick();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/queries.ts
|
|
201
|
+
function textMatches(actual, expected) {
|
|
202
|
+
const normalized = actual.replace(/\s+/g, " ").trim();
|
|
203
|
+
if (typeof expected === "string") return normalized === expected;
|
|
204
|
+
return expected.test(normalized);
|
|
205
|
+
}
|
|
206
|
+
function allByTestId(root, id) {
|
|
207
|
+
return Array.from(root.querySelectorAll(`[data-testid="${CSS.escape(id)}"]`));
|
|
208
|
+
}
|
|
209
|
+
var IMPLICIT_ROLES = {
|
|
210
|
+
BUTTON: "button",
|
|
211
|
+
A: "link",
|
|
212
|
+
// only when [href] — handled below
|
|
213
|
+
INPUT: "textbox",
|
|
214
|
+
// overridden by type
|
|
215
|
+
TEXTAREA: "textbox",
|
|
216
|
+
SELECT: "combobox",
|
|
217
|
+
FORM: "form",
|
|
218
|
+
NAV: "navigation",
|
|
219
|
+
MAIN: "main",
|
|
220
|
+
HEADER: "banner",
|
|
221
|
+
FOOTER: "contentinfo",
|
|
222
|
+
H1: "heading",
|
|
223
|
+
H2: "heading",
|
|
224
|
+
H3: "heading",
|
|
225
|
+
H4: "heading",
|
|
226
|
+
H5: "heading",
|
|
227
|
+
H6: "heading",
|
|
228
|
+
UL: "list",
|
|
229
|
+
OL: "list",
|
|
230
|
+
LI: "listitem",
|
|
231
|
+
TABLE: "table",
|
|
232
|
+
IMG: "img",
|
|
233
|
+
DIALOG: "dialog"
|
|
234
|
+
};
|
|
235
|
+
var INPUT_TYPE_ROLES = {
|
|
236
|
+
button: "button",
|
|
237
|
+
submit: "button",
|
|
238
|
+
reset: "button",
|
|
239
|
+
checkbox: "checkbox",
|
|
240
|
+
radio: "radio",
|
|
241
|
+
range: "slider",
|
|
242
|
+
search: "searchbox",
|
|
243
|
+
email: "textbox",
|
|
244
|
+
tel: "textbox",
|
|
245
|
+
url: "textbox",
|
|
246
|
+
text: "textbox",
|
|
247
|
+
number: "spinbutton",
|
|
248
|
+
password: "textbox"
|
|
249
|
+
};
|
|
250
|
+
function implicitRole(el) {
|
|
251
|
+
const explicit = el.getAttribute("role");
|
|
252
|
+
if (explicit) return explicit;
|
|
253
|
+
const tag = el.tagName;
|
|
254
|
+
if (tag === "A") {
|
|
255
|
+
return el.hasAttribute("href") ? "link" : null;
|
|
256
|
+
}
|
|
257
|
+
if (tag === "INPUT") {
|
|
258
|
+
const type = el.type || "text";
|
|
259
|
+
return INPUT_TYPE_ROLES[type] ?? "textbox";
|
|
260
|
+
}
|
|
261
|
+
return IMPLICIT_ROLES[tag] ?? null;
|
|
262
|
+
}
|
|
263
|
+
function accessibleName(el) {
|
|
264
|
+
const aria = el.getAttribute("aria-label");
|
|
265
|
+
if (aria) return aria.trim();
|
|
266
|
+
const labelledBy = el.getAttribute("aria-labelledby");
|
|
267
|
+
if (labelledBy) {
|
|
268
|
+
const refs = labelledBy.split(/\s+/).map((id) => document.getElementById(id)).filter((n) => !!n);
|
|
269
|
+
if (refs.length > 0) return refs.map((r) => (r.textContent || "").trim()).join(" ").trim();
|
|
270
|
+
}
|
|
271
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
|
|
272
|
+
const id = el.id;
|
|
273
|
+
if (id) {
|
|
274
|
+
const lbl = el.ownerDocument.querySelector(`label[for="${CSS.escape(id)}"]`);
|
|
275
|
+
if (lbl) return (lbl.textContent || "").trim();
|
|
276
|
+
}
|
|
277
|
+
const wrapping = el.closest("label");
|
|
278
|
+
if (wrapping) return (wrapping.textContent || "").trim();
|
|
279
|
+
if (el instanceof HTMLInputElement && el.type === "submit") return el.value || "";
|
|
280
|
+
}
|
|
281
|
+
return (el.textContent || "").replace(/\s+/g, " ").trim();
|
|
282
|
+
}
|
|
283
|
+
function allByRole(root, role, opts = {}) {
|
|
284
|
+
const candidates = Array.from(root.querySelectorAll("*"));
|
|
285
|
+
return candidates.filter((el) => {
|
|
286
|
+
if (implicitRole(el) !== role) return false;
|
|
287
|
+
if (opts.name !== void 0) {
|
|
288
|
+
const name = accessibleName(el);
|
|
289
|
+
return textMatches(name, opts.name);
|
|
290
|
+
}
|
|
291
|
+
return true;
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
function allByText(root, text) {
|
|
295
|
+
const result = [];
|
|
296
|
+
const els = root.querySelectorAll("*");
|
|
297
|
+
for (const el of els) {
|
|
298
|
+
const ownText = Array.from(el.childNodes).filter((n) => n.nodeType === 3).map((n) => n.textContent || "").join("");
|
|
299
|
+
if (textMatches(ownText, text)) result.push(el);
|
|
300
|
+
}
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
function allByLabelText(root, text) {
|
|
304
|
+
const labels = Array.from(root.querySelectorAll("label"));
|
|
305
|
+
const result = [];
|
|
306
|
+
for (const lbl of labels) {
|
|
307
|
+
if (!textMatches(lbl.textContent || "", text)) continue;
|
|
308
|
+
const forId = lbl.getAttribute("for");
|
|
309
|
+
if (forId) {
|
|
310
|
+
const target = root.querySelector(`#${CSS.escape(forId)}`);
|
|
311
|
+
if (target) result.push(target);
|
|
312
|
+
} else {
|
|
313
|
+
const nested = lbl.querySelector("input, textarea, select");
|
|
314
|
+
if (nested) result.push(nested);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
function singleOrThrow(arr, label) {
|
|
320
|
+
if (arr.length === 0) throw new Error(`${label}: no element found`);
|
|
321
|
+
if (arr.length > 1) throw new Error(`${label}: expected one element, found ${arr.length}`);
|
|
322
|
+
return arr[0];
|
|
323
|
+
}
|
|
324
|
+
function createQueries(root) {
|
|
325
|
+
return {
|
|
326
|
+
getByTestId: (id) => singleOrThrow(allByTestId(root, id), `getByTestId("${id}")`),
|
|
327
|
+
queryByTestId: (id) => allByTestId(root, id)[0] ?? null,
|
|
328
|
+
getAllByTestId: (id) => {
|
|
329
|
+
const all = allByTestId(root, id);
|
|
330
|
+
if (all.length === 0) throw new Error(`getAllByTestId("${id}"): no element found`);
|
|
331
|
+
return all;
|
|
332
|
+
},
|
|
333
|
+
findByTestId: (id, opts) => waitFor(() => singleOrThrow(allByTestId(root, id), `findByTestId("${id}")`), opts),
|
|
334
|
+
getByRole: (role, opts) => singleOrThrow(allByRole(root, role, opts), `getByRole("${role}")`),
|
|
335
|
+
queryByRole: (role, opts) => allByRole(root, role, opts)[0] ?? null,
|
|
336
|
+
getAllByRole: (role, opts) => {
|
|
337
|
+
const all = allByRole(root, role, opts);
|
|
338
|
+
if (all.length === 0) throw new Error(`getAllByRole("${role}"): no element found`);
|
|
339
|
+
return all;
|
|
340
|
+
},
|
|
341
|
+
findByRole: (role, opts) => waitFor(() => singleOrThrow(allByRole(root, role, opts), `findByRole("${role}")`), opts),
|
|
342
|
+
getByText: (t) => singleOrThrow(allByText(root, t), `getByText(${String(t)})`),
|
|
343
|
+
queryByText: (t) => allByText(root, t)[0] ?? null,
|
|
344
|
+
getAllByText: (t) => {
|
|
345
|
+
const all = allByText(root, t);
|
|
346
|
+
if (all.length === 0) throw new Error(`getAllByText(${String(t)}): no element found`);
|
|
347
|
+
return all;
|
|
348
|
+
},
|
|
349
|
+
findByText: (t, opts) => waitFor(() => singleOrThrow(allByText(root, t), `findByText(${String(t)})`), opts),
|
|
350
|
+
getByLabelText: (t) => singleOrThrow(allByLabelText(root, t), `getByLabelText(${String(t)})`),
|
|
351
|
+
queryByLabelText: (t) => allByLabelText(root, t)[0] ?? null,
|
|
352
|
+
findByLabelText: (t, opts) => waitFor(() => singleOrThrow(allByLabelText(root, t), `findByLabelText(${String(t)})`), opts)
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/cleanup.ts
|
|
357
|
+
var registry = /* @__PURE__ */ new Set();
|
|
358
|
+
function registerFixture(fx) {
|
|
359
|
+
registry.add(fx);
|
|
360
|
+
}
|
|
361
|
+
function destroyFixture(fx) {
|
|
362
|
+
if (fx.destroyed) return;
|
|
363
|
+
fx.destroyed = true;
|
|
364
|
+
for (const node of fx.nodes) {
|
|
365
|
+
if (node.parentNode) node.parentNode.removeChild(node);
|
|
366
|
+
}
|
|
367
|
+
if (fx.ownsApplication) {
|
|
368
|
+
try {
|
|
369
|
+
fx.application.stop();
|
|
370
|
+
} catch {
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
registry.delete(fx);
|
|
374
|
+
}
|
|
375
|
+
function cleanup() {
|
|
376
|
+
for (const fx of Array.from(registry)) {
|
|
377
|
+
destroyFixture(fx);
|
|
378
|
+
}
|
|
379
|
+
registry.clear();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// src/identifier.ts
|
|
383
|
+
var PATH_PREFIX = /^\.?\/?(?:assets\/)?(?:controllers\/)?/i;
|
|
384
|
+
var FILE_EXT = /\.[jt]sx?$/i;
|
|
385
|
+
var CONTROLLER_SUFFIX = /[_-][Cc]ontroller$/;
|
|
386
|
+
function identifierFromPath(filePath) {
|
|
387
|
+
if (!filePath) throw new TypeError("identifierFromPath(): path must be a non-empty string");
|
|
388
|
+
const withoutPrefix = filePath.replace(PATH_PREFIX, "");
|
|
389
|
+
const withoutExt = withoutPrefix.replace(FILE_EXT, "");
|
|
390
|
+
const segments = withoutExt.split("/").filter(Boolean);
|
|
391
|
+
if (segments.length === 0) {
|
|
392
|
+
throw new Error(`identifierFromPath(): cannot derive identifier from "${filePath}"`);
|
|
393
|
+
}
|
|
394
|
+
const last = segments[segments.length - 1];
|
|
395
|
+
const stripped = last.replace(CONTROLLER_SUFFIX, "");
|
|
396
|
+
if (!stripped) {
|
|
397
|
+
throw new Error(`identifierFromPath(): last segment of "${filePath}" is empty after stripping "_controller"`);
|
|
398
|
+
}
|
|
399
|
+
segments[segments.length - 1] = stripped;
|
|
400
|
+
const lowered = segments.map((seg) => seg.toLowerCase());
|
|
401
|
+
if (lowered.some((s) => !s)) {
|
|
402
|
+
throw new Error(`identifierFromPath(): empty segment in "${filePath}"`);
|
|
403
|
+
}
|
|
404
|
+
return lowered.join("--");
|
|
405
|
+
}
|
|
406
|
+
function normalizeIdentifier(raw) {
|
|
407
|
+
if (!raw || typeof raw !== "string") {
|
|
408
|
+
throw new TypeError("Stimulus identifier must be a non-empty string");
|
|
409
|
+
}
|
|
410
|
+
const looksLikePath = raw.includes("/") || /[A-Z]/.test(raw) || /\.[jt]sx?$/i.test(raw) || /_controller$/i.test(raw);
|
|
411
|
+
if (!looksLikePath) return raw;
|
|
412
|
+
return identifierFromPath(raw);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/render.ts
|
|
416
|
+
function inferIdentifier(ctor) {
|
|
417
|
+
const name = ctor.name;
|
|
418
|
+
if (!name || name.length < 2) {
|
|
419
|
+
throw new Error(
|
|
420
|
+
"render(): could not infer Stimulus identifier from an anonymous or single-character class. Pass options.identifier explicitly."
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
const stripped = name.endsWith("Controller") ? name.slice(0, -"Controller".length) : name;
|
|
424
|
+
if (!stripped) {
|
|
425
|
+
throw new Error(
|
|
426
|
+
`render(): class name "${name}" produces an empty identifier. Pass options.identifier explicitly.`
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
return stripped.toLowerCase();
|
|
430
|
+
}
|
|
431
|
+
function parseHtml(html) {
|
|
432
|
+
const template = document.createElement("template");
|
|
433
|
+
template.innerHTML = html.trim();
|
|
434
|
+
return Array.from(template.content.children);
|
|
435
|
+
}
|
|
436
|
+
function findControllerRoot(container, identifier) {
|
|
437
|
+
return container.querySelector(`[data-controller~="${CSS.escape(identifier)}"]`);
|
|
438
|
+
}
|
|
439
|
+
async function waitForController(application, element, identifier, timeout = 1e3) {
|
|
440
|
+
return waitFor(
|
|
441
|
+
() => {
|
|
442
|
+
const instance = application.getControllerForElementAndIdentifier(element, identifier);
|
|
443
|
+
if (!instance) throw new Error(`render(): controller "${identifier}" did not connect within ${timeout}ms`);
|
|
444
|
+
return instance;
|
|
445
|
+
},
|
|
446
|
+
{ timeout, interval: 10 }
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
async function render(ControllerClass, options) {
|
|
450
|
+
if (!ControllerClass) throw new TypeError("render(): ControllerClass is required");
|
|
451
|
+
if (!options || options.html === void 0 || options.html === null) {
|
|
452
|
+
throw new TypeError("render(): options.html is required");
|
|
453
|
+
}
|
|
454
|
+
const identifier = options.identifier ? normalizeIdentifier(options.identifier) : inferIdentifier(ControllerClass);
|
|
455
|
+
const container = options.container ?? document.body;
|
|
456
|
+
const insertedNodes = [];
|
|
457
|
+
if (typeof options.html === "string") {
|
|
458
|
+
for (const node of parseHtml(options.html)) {
|
|
459
|
+
container.appendChild(node);
|
|
460
|
+
insertedNodes.push(node);
|
|
461
|
+
}
|
|
462
|
+
} else {
|
|
463
|
+
container.appendChild(options.html);
|
|
464
|
+
insertedNodes.push(options.html);
|
|
465
|
+
}
|
|
466
|
+
const ownsApplication = !options.application;
|
|
467
|
+
const application = options.application ?? Application.start();
|
|
468
|
+
application.register(identifier, ControllerClass);
|
|
469
|
+
if (options.controllers) {
|
|
470
|
+
for (const [id, ctor] of Object.entries(options.controllers)) {
|
|
471
|
+
application.register(id, ctor);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
await nextTick();
|
|
475
|
+
const element = insertedNodes.map((n) => n instanceof HTMLElement && n.matches(`[data-controller~="${CSS.escape(identifier)}"]`) ? n : null).find((n) => !!n) ?? findControllerRoot(container, identifier);
|
|
476
|
+
if (!element) {
|
|
477
|
+
throw new Error(
|
|
478
|
+
`render(): no element with data-controller~="${identifier}" found in the mounted fixture. Check your HTML or pass options.identifier.`
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
const controller = await waitForController(application, element, identifier);
|
|
482
|
+
const fixture = {
|
|
483
|
+
application,
|
|
484
|
+
nodes: insertedNodes,
|
|
485
|
+
ownsApplication,
|
|
486
|
+
destroyed: false
|
|
487
|
+
};
|
|
488
|
+
registerFixture(fixture);
|
|
489
|
+
const user = createUserEvent();
|
|
490
|
+
const queries = createQueries(element);
|
|
491
|
+
const result = {
|
|
492
|
+
controller,
|
|
493
|
+
element,
|
|
494
|
+
application,
|
|
495
|
+
user,
|
|
496
|
+
waitFor,
|
|
497
|
+
rerender: async (next) => {
|
|
498
|
+
for (const n of insertedNodes) {
|
|
499
|
+
if (n.parentNode) n.parentNode.removeChild(n);
|
|
500
|
+
}
|
|
501
|
+
insertedNodes.length = 0;
|
|
502
|
+
const newNodes = typeof next.html === "string" ? parseHtml(next.html) : [next.html];
|
|
503
|
+
for (const n of newNodes) {
|
|
504
|
+
container.appendChild(n);
|
|
505
|
+
insertedNodes.push(n);
|
|
506
|
+
}
|
|
507
|
+
fixture.nodes = insertedNodes;
|
|
508
|
+
await nextTick();
|
|
509
|
+
const newElement = findControllerRoot(container, identifier);
|
|
510
|
+
if (!newElement) {
|
|
511
|
+
throw new Error(`rerender(): new fixture has no [data-controller~="${identifier}"]`);
|
|
512
|
+
}
|
|
513
|
+
const newCtrl = await waitForController(application, newElement, identifier);
|
|
514
|
+
result.controller = newCtrl;
|
|
515
|
+
result.element = newElement;
|
|
516
|
+
},
|
|
517
|
+
unmount: () => destroyFixture(fixture),
|
|
518
|
+
...queries
|
|
519
|
+
};
|
|
520
|
+
return result;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// src/utils.ts
|
|
524
|
+
function toKebabCase(input) {
|
|
525
|
+
return input.replace(/_/g, "-").replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").toLowerCase();
|
|
526
|
+
}
|
|
527
|
+
function escapeAttr(value) {
|
|
528
|
+
return value.replace(/&/g, "&").replace(/"/g, """);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// src/attributes.ts
|
|
532
|
+
var SPEC_DATA = /* @__PURE__ */ Symbol("stimulus-test-utils:attrData");
|
|
533
|
+
function createSpec(data) {
|
|
534
|
+
const spec = {
|
|
535
|
+
[SPEC_DATA]: data,
|
|
536
|
+
toString() {
|
|
537
|
+
return serialize(data);
|
|
538
|
+
},
|
|
539
|
+
toJSON() {
|
|
540
|
+
return serialize(data);
|
|
541
|
+
},
|
|
542
|
+
[Symbol.toPrimitive](_hint) {
|
|
543
|
+
return serialize(data);
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
return spec;
|
|
547
|
+
}
|
|
548
|
+
function getData(spec) {
|
|
549
|
+
return spec[SPEC_DATA];
|
|
550
|
+
}
|
|
551
|
+
function emptyData() {
|
|
552
|
+
return {
|
|
553
|
+
controllers: [],
|
|
554
|
+
actions: [],
|
|
555
|
+
values: /* @__PURE__ */ new Map(),
|
|
556
|
+
classes: /* @__PURE__ */ new Map(),
|
|
557
|
+
outlets: /* @__PURE__ */ new Map(),
|
|
558
|
+
targets: /* @__PURE__ */ new Map()
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
function serializeValue(raw) {
|
|
562
|
+
if (raw === null) return "null";
|
|
563
|
+
switch (typeof raw) {
|
|
564
|
+
case "string":
|
|
565
|
+
return raw;
|
|
566
|
+
case "number":
|
|
567
|
+
case "boolean":
|
|
568
|
+
return String(raw);
|
|
569
|
+
default:
|
|
570
|
+
return JSON.stringify(raw);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
function serialize(data) {
|
|
574
|
+
const parts = [];
|
|
575
|
+
if (data.controllers.length > 0) {
|
|
576
|
+
parts.push(`data-controller="${escapeAttr(data.controllers.join(" "))}"`);
|
|
577
|
+
}
|
|
578
|
+
for (const [identifier, names] of data.targets) {
|
|
579
|
+
parts.push(`data-${identifier}-target="${escapeAttr(names.join(" "))}"`);
|
|
580
|
+
}
|
|
581
|
+
for (const [key, raw] of data.values) {
|
|
582
|
+
parts.push(`data-${key}-value="${escapeAttr(serializeValue(raw))}"`);
|
|
583
|
+
}
|
|
584
|
+
for (const [key, raw] of data.classes) {
|
|
585
|
+
parts.push(`data-${key}-class="${escapeAttr(raw)}"`);
|
|
586
|
+
}
|
|
587
|
+
for (const [key, raw] of data.outlets) {
|
|
588
|
+
parts.push(`data-${key}-outlet="${escapeAttr(raw)}"`);
|
|
589
|
+
}
|
|
590
|
+
if (data.actions.length > 0) {
|
|
591
|
+
parts.push(`data-action="${escapeAttr(data.actions.join(" "))}"`);
|
|
592
|
+
}
|
|
593
|
+
return parts.join(" ");
|
|
594
|
+
}
|
|
595
|
+
function stimulusController(identifier, values, classes, outlets) {
|
|
596
|
+
if (!identifier || typeof identifier !== "string") {
|
|
597
|
+
throw new TypeError(`stimulusController(): identifier must be a non-empty string, got ${String(identifier)}`);
|
|
598
|
+
}
|
|
599
|
+
identifier = normalizeIdentifier(identifier);
|
|
600
|
+
const data = emptyData();
|
|
601
|
+
data.controllers.push(identifier);
|
|
602
|
+
if (values) {
|
|
603
|
+
for (const [k, v] of Object.entries(values)) {
|
|
604
|
+
data.values.set(`${identifier}-${toKebabCase(k)}`, v);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (classes) {
|
|
608
|
+
for (const [k, v] of Object.entries(classes)) {
|
|
609
|
+
data.classes.set(`${identifier}-${toKebabCase(k)}`, v);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (outlets) {
|
|
613
|
+
for (const [k, v] of Object.entries(outlets)) {
|
|
614
|
+
data.outlets.set(`${identifier}-${toKebabCase(k)}`, v);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return createSpec(data);
|
|
618
|
+
}
|
|
619
|
+
function stimulusTarget(identifier, ...targetNames) {
|
|
620
|
+
if (!identifier) throw new TypeError("stimulusTarget(): identifier is required");
|
|
621
|
+
if (targetNames.length === 0) throw new TypeError("stimulusTarget(): at least one target name is required");
|
|
622
|
+
identifier = normalizeIdentifier(identifier);
|
|
623
|
+
const data = emptyData();
|
|
624
|
+
const unique = [];
|
|
625
|
+
for (const n of targetNames) {
|
|
626
|
+
if (!unique.includes(n)) unique.push(n);
|
|
627
|
+
}
|
|
628
|
+
data.targets.set(identifier, unique);
|
|
629
|
+
return createSpec(data);
|
|
630
|
+
}
|
|
631
|
+
var ACTION_OPTION_ORDER = [
|
|
632
|
+
"prevent",
|
|
633
|
+
"stop",
|
|
634
|
+
"once",
|
|
635
|
+
"passive",
|
|
636
|
+
"capture",
|
|
637
|
+
"self"
|
|
638
|
+
];
|
|
639
|
+
function stimulusAction(identifier, method, event, options) {
|
|
640
|
+
if (!identifier) throw new TypeError("stimulusAction(): identifier is required");
|
|
641
|
+
if (!method) throw new TypeError("stimulusAction(): method is required");
|
|
642
|
+
identifier = normalizeIdentifier(identifier);
|
|
643
|
+
const base = event ? `${event}->${identifier}#${method}` : `${identifier}#${method}`;
|
|
644
|
+
let descriptor = base;
|
|
645
|
+
if (options) {
|
|
646
|
+
for (const key of ACTION_OPTION_ORDER) {
|
|
647
|
+
if (options[key]) descriptor += `:${key}`;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
const data = emptyData();
|
|
651
|
+
data.actions.push(descriptor);
|
|
652
|
+
return createSpec(data);
|
|
653
|
+
}
|
|
654
|
+
function combine(...specs) {
|
|
655
|
+
const merged = emptyData();
|
|
656
|
+
for (const spec of specs) {
|
|
657
|
+
const d = getData(spec);
|
|
658
|
+
if (!d) {
|
|
659
|
+
throw new TypeError("combine(): all arguments must be AttrSpec values returned from stimulus* helpers");
|
|
660
|
+
}
|
|
661
|
+
for (const id of d.controllers) {
|
|
662
|
+
if (merged.controllers.includes(id)) {
|
|
663
|
+
throw new Error(
|
|
664
|
+
`combine(): duplicate Stimulus controller identifier "${id}". Declare each controller once and pass all its values/classes/outlets in a single stimulusController() call.`
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
merged.controllers.push(id);
|
|
668
|
+
}
|
|
669
|
+
for (const [id, names] of d.targets) {
|
|
670
|
+
const existing = merged.targets.get(id) ?? [];
|
|
671
|
+
for (const n of names) if (!existing.includes(n)) existing.push(n);
|
|
672
|
+
merged.targets.set(id, existing);
|
|
673
|
+
}
|
|
674
|
+
for (const [k, v] of d.values) merged.values.set(k, v);
|
|
675
|
+
for (const [k, v] of d.classes) merged.classes.set(k, v);
|
|
676
|
+
for (const [k, v] of d.outlets) merged.outlets.set(k, v);
|
|
677
|
+
for (const a of d.actions) merged.actions.push(a);
|
|
678
|
+
}
|
|
679
|
+
return createSpec(merged);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
export { cleanup, combine, createUserEvent, escapeAttr, fireEvent, identifierFromPath, inferIdentifier, nextTick, normalizeIdentifier, render, stimulusAction, stimulusController, stimulusTarget, toKebabCase, waitFor };
|
|
683
|
+
//# sourceMappingURL=index.js.map
|
|
684
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/wait-for.ts","../src/user-event.ts","../src/queries.ts","../src/cleanup.ts","../src/identifier.ts","../src/render.ts","../src/utils.ts","../src/attributes.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { WaitForOptions } from './types.js'\n\n/**\n * Flush microtasks + one macrotask tick so Stimulus' MutationObserver\n * observers and scheduled callbacks settle before the test continues.\n */\nexport async function nextTick(): Promise<void> {\n // Flush microtasks.\n await Promise.resolve()\n // Let MutationObserver / setTimeout(0) fire.\n await new Promise<void>((resolve) => setTimeout(resolve, 0))\n // Flush any microtasks queued by those callbacks.\n await Promise.resolve()\n}\n\nconst DEFAULT_TIMEOUT = 1000\nconst DEFAULT_INTERVAL = 20\n\nexport async function waitFor<T>(\n callback: () => T | Promise<T>,\n options: WaitForOptions = {},\n): Promise<T> {\n const timeout = options.timeout ?? DEFAULT_TIMEOUT\n const interval = options.interval ?? DEFAULT_INTERVAL\n const deadline = Date.now() + timeout\n\n let lastError: unknown\n // Initial attempt right away.\n while (true) {\n try {\n const result = await callback()\n return result\n } catch (err) {\n lastError = err\n if (Date.now() >= deadline) break\n await new Promise<void>((resolve) => setTimeout(resolve, interval))\n }\n }\n throw lastError instanceof Error\n ? lastError\n : new Error(`waitFor: timed out after ${timeout}ms`)\n}\n","import { nextTick } from './wait-for.js'\nimport type { UserEvent } from './types.js'\n\n/**\n * Minimal user-event layer. Focused on the event surface Stimulus actions\n * actually listen for: click/dblclick/mouseover-hover, input/change, keydown/up,\n * submit, focus/blur. Every method awaits a tick so Stimulus' MutationObserver\n * and action dispatch settle before returning.\n */\n\nfunction dispatch(target: EventTarget, event: Event): void {\n target.dispatchEvent(event)\n}\n\nfunction isDisabled(el: Element): boolean {\n return (el as HTMLInputElement).disabled === true\n}\n\nfunction focusIfPossible(el: Element): void {\n const focusable = el as HTMLElement\n if (typeof focusable.focus === 'function') {\n focusable.focus()\n }\n}\n\nasync function clickImpl(el: Element): Promise<void> {\n if (isDisabled(el)) return\n focusIfPossible(el)\n dispatch(el, new MouseEvent('mousedown', { bubbles: true, cancelable: true }))\n dispatch(el, new MouseEvent('mouseup', { bubbles: true, cancelable: true }))\n dispatch(el, new MouseEvent('click', { bubbles: true, cancelable: true }))\n // Forms: clicking a submit button submits.\n if (el instanceof HTMLButtonElement && el.type === 'submit' && el.form) {\n await submitImpl(el.form)\n }\n await nextTick()\n}\n\nasync function dblClickImpl(el: Element): Promise<void> {\n if (isDisabled(el)) return\n await clickImpl(el)\n dispatch(el, new MouseEvent('dblclick', { bubbles: true, cancelable: true }))\n await nextTick()\n}\n\nasync function hoverImpl(el: Element): Promise<void> {\n dispatch(el, new MouseEvent('mouseover', { bubbles: true, cancelable: true }))\n dispatch(el, new MouseEvent('mouseenter', { bubbles: false, cancelable: true }))\n dispatch(el, new MouseEvent('mousemove', { bubbles: true, cancelable: true }))\n await nextTick()\n}\n\nfunction getValueElement(el: Element): HTMLInputElement | HTMLTextAreaElement | null {\n if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) return el\n return null\n}\n\nasync function typeImpl(el: Element, text: string): Promise<void> {\n const input = getValueElement(el)\n if (!input) throw new TypeError('user.type(): target must be <input> or <textarea>')\n focusIfPossible(input)\n for (const ch of [...text]) {\n dispatch(input, new KeyboardEvent('keydown', { key: ch, bubbles: true, cancelable: true }))\n input.value = input.value + ch\n dispatch(input, new InputEvent('input', { data: ch, bubbles: true, cancelable: true }))\n dispatch(input, new KeyboardEvent('keyup', { key: ch, bubbles: true, cancelable: true }))\n }\n dispatch(input, new Event('change', { bubbles: true }))\n await nextTick()\n}\n\nasync function clearImpl(el: Element): Promise<void> {\n const input = getValueElement(el)\n if (!input) throw new TypeError('user.clear(): target must be <input> or <textarea>')\n focusIfPossible(input)\n input.value = ''\n dispatch(input, new InputEvent('input', { bubbles: true, cancelable: true }))\n dispatch(input, new Event('change', { bubbles: true }))\n await nextTick()\n}\n\n/** Parse \"{Enter}\", \"{Shift>}A{/Shift}\", \"abc{Backspace}\" into a token list. */\ninterface KeyToken {\n type: 'key' | 'down' | 'up'\n key: string\n}\n\nfunction parseKeyboard(input: string): KeyToken[] {\n const tokens: KeyToken[] = []\n let i = 0\n while (i < input.length) {\n const ch = input[i]!\n if (ch === '{') {\n const end = input.indexOf('}', i)\n if (end === -1) throw new SyntaxError(`user.keyboard: unclosed \"{\" in ${JSON.stringify(input)}`)\n let body = input.slice(i + 1, end)\n let type: KeyToken['type'] = 'key'\n if (body.endsWith('>')) {\n body = body.slice(0, -1)\n type = 'down'\n } else if (body.startsWith('/')) {\n body = body.slice(1)\n type = 'up'\n }\n tokens.push({ type, key: body })\n i = end + 1\n } else {\n tokens.push({ type: 'key', key: ch })\n i++\n }\n }\n return tokens\n}\n\nasync function keyboardImpl(keys: string): Promise<void> {\n const tokens = parseKeyboard(keys)\n const target = (document.activeElement as HTMLElement | null) ?? document.body\n const modifiers = new Set<string>()\n\n for (const tok of tokens) {\n const key = tok.key\n const init: KeyboardEventInit = {\n key,\n bubbles: true,\n cancelable: true,\n shiftKey: modifiers.has('Shift'),\n ctrlKey: modifiers.has('Control'),\n altKey: modifiers.has('Alt'),\n metaKey: modifiers.has('Meta'),\n }\n if (tok.type === 'down') {\n modifiers.add(key)\n dispatch(target, new KeyboardEvent('keydown', init))\n } else if (tok.type === 'up') {\n modifiers.delete(key)\n dispatch(target, new KeyboardEvent('keyup', init))\n } else {\n dispatch(target, new KeyboardEvent('keydown', init))\n // Printable single-char keys that aren't a named key should also\n // produce an input on editable fields. Keep minimal: just up.\n dispatch(target, new KeyboardEvent('keyup', init))\n }\n }\n await nextTick()\n}\n\nasync function tabImpl(opts: { shift?: boolean } = {}): Promise<void> {\n const target = (document.activeElement as HTMLElement | null) ?? document.body\n const init: KeyboardEventInit = {\n key: 'Tab',\n bubbles: true,\n cancelable: true,\n shiftKey: !!opts.shift,\n }\n dispatch(target, new KeyboardEvent('keydown', init))\n dispatch(target, new KeyboardEvent('keyup', init))\n await nextTick()\n}\n\nasync function selectOptionImpl(\n select: HTMLSelectElement,\n value: string | string[],\n): Promise<void> {\n const values = Array.isArray(value) ? value : [value]\n if (!select.multiple && values.length > 1) {\n throw new TypeError('user.selectOption(): cannot select multiple values on a single-select <select>')\n }\n let matched = 0\n for (const option of Array.from(select.options)) {\n const should = values.includes(option.value)\n option.selected = should\n if (should) matched++\n }\n if (matched === 0) {\n throw new Error(`user.selectOption(): no <option> matched value(s) ${JSON.stringify(value)}`)\n }\n dispatch(select, new Event('input', { bubbles: true }))\n dispatch(select, new Event('change', { bubbles: true }))\n await nextTick()\n}\n\nasync function submitImpl(form: HTMLFormElement): Promise<void> {\n dispatch(form, new SubmitEvent('submit', { bubbles: true, cancelable: true }))\n await nextTick()\n}\n\nexport function createUserEvent(): UserEvent {\n return {\n click: clickImpl,\n dblClick: dblClickImpl,\n hover: hoverImpl,\n type: typeImpl,\n clear: clearImpl,\n keyboard: keyboardImpl,\n tab: tabImpl,\n selectOption: selectOptionImpl,\n submit: submitImpl,\n }\n}\n\nexport async function fireEvent(\n target: EventTarget,\n eventOrName: Event | string,\n init?: EventInit,\n): Promise<void> {\n const event =\n typeof eventOrName === 'string'\n ? new CustomEvent(eventOrName, { bubbles: true, cancelable: true, ...init })\n : eventOrName\n target.dispatchEvent(event)\n await nextTick()\n}\n","import { waitFor } from './wait-for.js'\nimport type { QueryHelpers, WaitForOptions } from './types.js'\n\n/**\n * Scoped, Testing-Library-flavoured query helpers. The container is the\n * fixture root returned by `render()`; every helper searches inside it\n * (including itself for text/role match on root).\n */\n\nfunction textMatches(actual: string, expected: string | RegExp): boolean {\n const normalized = actual.replace(/\\s+/g, ' ').trim()\n if (typeof expected === 'string') return normalized === expected\n return expected.test(normalized)\n}\n\n/* ------------------------------- TestId ------------------------------- */\n\nfunction allByTestId(root: ParentNode, id: string): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(`[data-testid=\"${CSS.escape(id)}\"]`))\n}\n\n/* -------------------------------- Role -------------------------------- */\n\nconst IMPLICIT_ROLES: Record<string, string> = {\n BUTTON: 'button',\n A: 'link', // only when [href] — handled below\n INPUT: 'textbox', // overridden by type\n TEXTAREA: 'textbox',\n SELECT: 'combobox',\n FORM: 'form',\n NAV: 'navigation',\n MAIN: 'main',\n HEADER: 'banner',\n FOOTER: 'contentinfo',\n H1: 'heading',\n H2: 'heading',\n H3: 'heading',\n H4: 'heading',\n H5: 'heading',\n H6: 'heading',\n UL: 'list',\n OL: 'list',\n LI: 'listitem',\n TABLE: 'table',\n IMG: 'img',\n DIALOG: 'dialog',\n}\n\nconst INPUT_TYPE_ROLES: Record<string, string> = {\n button: 'button',\n submit: 'button',\n reset: 'button',\n checkbox: 'checkbox',\n radio: 'radio',\n range: 'slider',\n search: 'searchbox',\n email: 'textbox',\n tel: 'textbox',\n url: 'textbox',\n text: 'textbox',\n number: 'spinbutton',\n password: 'textbox',\n}\n\nfunction implicitRole(el: Element): string | null {\n const explicit = el.getAttribute('role')\n if (explicit) return explicit\n const tag = el.tagName\n if (tag === 'A') {\n return (el as HTMLAnchorElement).hasAttribute('href') ? 'link' : null\n }\n if (tag === 'INPUT') {\n const type = (el as HTMLInputElement).type || 'text'\n return INPUT_TYPE_ROLES[type] ?? 'textbox'\n }\n return IMPLICIT_ROLES[tag] ?? null\n}\n\nfunction accessibleName(el: Element): string {\n const aria = el.getAttribute('aria-label')\n if (aria) return aria.trim()\n const labelledBy = el.getAttribute('aria-labelledby')\n if (labelledBy) {\n const refs = labelledBy\n .split(/\\s+/)\n .map((id) => document.getElementById(id))\n .filter((n): n is HTMLElement => !!n)\n if (refs.length > 0) return refs.map((r) => (r.textContent || '').trim()).join(' ').trim()\n }\n if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {\n const id = el.id\n if (id) {\n const lbl = el.ownerDocument.querySelector<HTMLLabelElement>(`label[for=\"${CSS.escape(id)}\"]`)\n if (lbl) return (lbl.textContent || '').trim()\n }\n const wrapping = el.closest('label')\n if (wrapping) return (wrapping.textContent || '').trim()\n if (el instanceof HTMLInputElement && el.type === 'submit') return el.value || ''\n }\n return (el.textContent || '').replace(/\\s+/g, ' ').trim()\n}\n\nfunction allByRole(\n root: ParentNode,\n role: string,\n opts: { name?: string | RegExp } = {},\n): HTMLElement[] {\n const candidates = Array.from(root.querySelectorAll<HTMLElement>('*'))\n return candidates.filter((el) => {\n if (implicitRole(el) !== role) return false\n if (opts.name !== undefined) {\n const name = accessibleName(el)\n return textMatches(name, opts.name)\n }\n return true\n })\n}\n\n/* -------------------------------- Text -------------------------------- */\n\nfunction allByText(root: ParentNode, text: string | RegExp): HTMLElement[] {\n const result: HTMLElement[] = []\n const els = root.querySelectorAll<HTMLElement>('*')\n for (const el of els) {\n // Only leaf-ish text nodes — skip if a descendant already matches.\n const ownText = Array.from(el.childNodes)\n .filter((n) => n.nodeType === 3)\n .map((n) => n.textContent || '')\n .join('')\n if (textMatches(ownText, text)) result.push(el)\n }\n return result\n}\n\n/* ------------------------------ LabelText ----------------------------- */\n\nfunction allByLabelText(root: ParentNode, text: string | RegExp): HTMLElement[] {\n const labels = Array.from(root.querySelectorAll<HTMLLabelElement>('label'))\n const result: HTMLElement[] = []\n for (const lbl of labels) {\n if (!textMatches(lbl.textContent || '', text)) continue\n const forId = lbl.getAttribute('for')\n if (forId) {\n const target = root.querySelector<HTMLElement>(`#${CSS.escape(forId)}`)\n if (target) result.push(target)\n } else {\n const nested = lbl.querySelector<HTMLElement>('input, textarea, select')\n if (nested) result.push(nested)\n }\n }\n return result\n}\n\n/* ------------------------------- Factory ------------------------------ */\n\nfunction singleOrThrow<T>(arr: T[], label: string): T {\n if (arr.length === 0) throw new Error(`${label}: no element found`)\n if (arr.length > 1) throw new Error(`${label}: expected one element, found ${arr.length}`)\n return arr[0]!\n}\n\nexport function createQueries(root: HTMLElement): QueryHelpers {\n return {\n getByTestId: (id) => singleOrThrow(allByTestId(root, id), `getByTestId(\"${id}\")`),\n queryByTestId: (id) => allByTestId(root, id)[0] ?? null,\n getAllByTestId: (id) => {\n const all = allByTestId(root, id)\n if (all.length === 0) throw new Error(`getAllByTestId(\"${id}\"): no element found`)\n return all\n },\n findByTestId: (id, opts?: WaitForOptions) =>\n waitFor(() => singleOrThrow(allByTestId(root, id), `findByTestId(\"${id}\")`), opts),\n\n getByRole: (role, opts) =>\n singleOrThrow(allByRole(root, role, opts), `getByRole(\"${role}\")`),\n queryByRole: (role, opts) => allByRole(root, role, opts)[0] ?? null,\n getAllByRole: (role, opts) => {\n const all = allByRole(root, role, opts)\n if (all.length === 0) throw new Error(`getAllByRole(\"${role}\"): no element found`)\n return all\n },\n findByRole: (role, opts) =>\n waitFor(() => singleOrThrow(allByRole(root, role, opts), `findByRole(\"${role}\")`), opts),\n\n getByText: (t) => singleOrThrow(allByText(root, t), `getByText(${String(t)})`),\n queryByText: (t) => allByText(root, t)[0] ?? null,\n getAllByText: (t) => {\n const all = allByText(root, t)\n if (all.length === 0) throw new Error(`getAllByText(${String(t)}): no element found`)\n return all\n },\n findByText: (t, opts) => waitFor(() => singleOrThrow(allByText(root, t), `findByText(${String(t)})`), opts),\n\n getByLabelText: (t) => singleOrThrow(allByLabelText(root, t), `getByLabelText(${String(t)})`),\n queryByLabelText: (t) => allByLabelText(root, t)[0] ?? null,\n findByLabelText: (t, opts) =>\n waitFor(() => singleOrThrow(allByLabelText(root, t), `findByLabelText(${String(t)})`), opts),\n }\n}\n","import type { Application } from '@hotwired/stimulus'\n\n/**\n * Module-level registry of everything `render()` has created so that\n * `cleanup()` (or an individual `unmount()`) can tear it all down between\n * tests, leaving the DOM and Stimulus registry pristine.\n */\n\nexport interface MountedFixture {\n application: Application\n /** DOM nodes that `render()` inserted into the container. */\n nodes: Element[]\n /** Was the Stimulus Application created by us (vs. BYO)? */\n ownsApplication: boolean\n /** Mark destroyed so double-unmount is a no-op. */\n destroyed: boolean\n}\n\nconst registry = new Set<MountedFixture>()\n\nexport function registerFixture(fx: MountedFixture): void {\n registry.add(fx)\n}\n\nexport function destroyFixture(fx: MountedFixture): void {\n if (fx.destroyed) return\n fx.destroyed = true\n for (const node of fx.nodes) {\n if (node.parentNode) node.parentNode.removeChild(node)\n }\n if (fx.ownsApplication) {\n try {\n fx.application.stop()\n } catch {\n // best-effort: ignore teardown errors so cleanup keeps going\n }\n }\n registry.delete(fx)\n}\n\nexport function cleanup(): void {\n for (const fx of Array.from(registry)) {\n destroyFixture(fx)\n }\n registry.clear()\n}\n\n/** Test helper — internal only. */\nexport function _registrySize(): number {\n return registry.size\n}\n","/**\n * Stimulus identifier utilities.\n *\n * Stimulus convention for controllers living in sub-folders:\n * ./assets/controllers/MyApp/MyController_controller.js → \"myapp--mycontroller\"\n * ./assets/controllers/Users/List_controller.js → \"users--list\"\n * ./assets/controllers/hello_controller.js → \"hello\"\n *\n * Rules:\n * - Directory separators \"/\" become \"--\".\n * - Each path segment is lowercased (CamelCase is NOT split into kebab-case\n * — \"MyApp\" becomes \"myapp\", not \"my-app\"). This mirrors the Symfony UX\n * / Asset Mapper / @hotwired/stimulus-webpack-helpers behaviour.\n * - Any trailing \"_controller\" / \"-controller\" / \"Controller\" suffix is\n * stripped from the last segment.\n * - The leading \"./\", \"assets/controllers/\", \"controllers/\" prefix is\n * stripped so you can paste the full path as-is.\n * - File extension (\".js\" / \".ts\" / \".mjs\" / \".tsx\") is stripped.\n */\n\nconst PATH_PREFIX = /^\\.?\\/?(?:assets\\/)?(?:controllers\\/)?/i\nconst FILE_EXT = /\\.[jt]sx?$/i\nconst CONTROLLER_SUFFIX = /[_-][Cc]ontroller$/\n\nexport function identifierFromPath(filePath: string): string {\n if (!filePath) throw new TypeError('identifierFromPath(): path must be a non-empty string')\n\n const withoutPrefix = filePath.replace(PATH_PREFIX, '')\n const withoutExt = withoutPrefix.replace(FILE_EXT, '')\n const segments = withoutExt.split('/').filter(Boolean)\n if (segments.length === 0) {\n throw new Error(`identifierFromPath(): cannot derive identifier from \"${filePath}\"`)\n }\n\n // Strip \"_controller\" / \"Controller\" / \"-controller\" from the last segment only.\n const last = segments[segments.length - 1]!\n const stripped = last.replace(CONTROLLER_SUFFIX, '')\n if (!stripped) {\n throw new Error(`identifierFromPath(): last segment of \"${filePath}\" is empty after stripping \"_controller\"`)\n }\n segments[segments.length - 1] = stripped\n\n const lowered = segments.map((seg) => seg.toLowerCase())\n if (lowered.some((s) => !s)) {\n throw new Error(`identifierFromPath(): empty segment in \"${filePath}\"`)\n }\n\n return lowered.join('--')\n}\n\n/**\n * Normalize whatever identifier-ish string the user handed us into the\n * canonical Stimulus identifier:\n *\n * \"hello\" → \"hello\" (plain, untouched)\n * \"myapp--mycontroller\" → \"myapp--mycontroller\" (already canonical)\n * \"MyApp/MyController\" → \"myapp--mycontroller\"\n * \"MyApp/MyController_controller.js\" → \"myapp--mycontroller\"\n * \"users/list_controller\" → \"users--list\"\n *\n * Rules:\n * - If the string contains \"/\" or an uppercase letter or ends with a file\n * extension, we route it through `identifierFromPath`.\n * - A trailing `_controller` on a plain (no‑slash) string is also stripped\n * so `stimulusController(\"hello_controller\")` DWIMs into `hello`.\n * - Otherwise we return it as-is (so already-valid identifiers like\n * \"hello\", \"myapp--mycontroller\", \"data-picker\" round‑trip unchanged).\n */\nexport function normalizeIdentifier(raw: string): string {\n if (!raw || typeof raw !== 'string') {\n throw new TypeError('Stimulus identifier must be a non-empty string')\n }\n const looksLikePath =\n raw.includes('/') ||\n /[A-Z]/.test(raw) ||\n /\\.[jt]sx?$/i.test(raw) ||\n /_controller$/i.test(raw)\n if (!looksLikePath) return raw\n return identifierFromPath(raw)\n}\n","import { Application, Controller } from '@hotwired/stimulus'\nimport type { ControllerConstructor, RenderOptions, RenderResult } from './types.js'\nimport { createUserEvent } from './user-event.js'\nimport { createQueries } from './queries.js'\nimport { nextTick, waitFor } from './wait-for.js'\nimport { destroyFixture, registerFixture, type MountedFixture } from './cleanup.js'\nimport { normalizeIdentifier } from './identifier.js'\n\n/**\n * Infer a Stimulus identifier from a controller class name.\n * HelloController → \"hello\"\n * HelloWorldController → \"helloworld\"\n * APIController → \"api\"\n * Anonymous / minified → throws (caller must pass options.identifier)\n *\n * The rule mirrors Symfony UX / Asset Mapper behaviour: the class name is\n * lowercased as a whole (CamelCase is NOT split with dashes). If you want a\n * hyphenated identifier, pass `options.identifier` explicitly.\n */\nexport function inferIdentifier(ctor: ControllerConstructor): string {\n const name = ctor.name\n if (!name || name.length < 2) {\n throw new Error(\n 'render(): could not infer Stimulus identifier from an anonymous or single-character class. ' +\n 'Pass options.identifier explicitly.',\n )\n }\n const stripped = name.endsWith('Controller') ? name.slice(0, -'Controller'.length) : name\n if (!stripped) {\n throw new Error(\n `render(): class name \"${name}\" produces an empty identifier. ` +\n 'Pass options.identifier explicitly.',\n )\n }\n return stripped.toLowerCase()\n}\n\nfunction parseHtml(html: string): Element[] {\n const template = document.createElement('template')\n template.innerHTML = html.trim()\n return Array.from(template.content.children)\n}\n\nfunction findControllerRoot(container: ParentNode, identifier: string): HTMLElement | null {\n // CSS.escape on the identifier for safety.\n return container.querySelector<HTMLElement>(`[data-controller~=\"${CSS.escape(identifier)}\"]`)\n}\n\nasync function waitForController<C extends Controller>(\n application: Application,\n element: HTMLElement,\n identifier: string,\n timeout = 1000,\n): Promise<C> {\n return waitFor(\n () => {\n const instance = application.getControllerForElementAndIdentifier(element, identifier)\n if (!instance) throw new Error(`render(): controller \"${identifier}\" did not connect within ${timeout}ms`)\n return instance as unknown as C\n },\n { timeout, interval: 10 },\n )\n}\n\nexport async function render<C extends Controller>(\n ControllerClass: new (...args: any[]) => C,\n options: RenderOptions,\n): Promise<RenderResult<C>> {\n if (!ControllerClass) throw new TypeError('render(): ControllerClass is required')\n if (!options || options.html === undefined || options.html === null) {\n throw new TypeError('render(): options.html is required')\n }\n\n const identifier = options.identifier\n ? normalizeIdentifier(options.identifier)\n : inferIdentifier(ControllerClass as unknown as ControllerConstructor)\n const container = options.container ?? document.body\n\n // Insert fixture.\n const insertedNodes: Element[] = []\n if (typeof options.html === 'string') {\n for (const node of parseHtml(options.html)) {\n container.appendChild(node)\n insertedNodes.push(node)\n }\n } else {\n container.appendChild(options.html)\n insertedNodes.push(options.html)\n }\n\n // Boot or reuse Application.\n const ownsApplication = !options.application\n const application = options.application ?? Application.start()\n\n application.register(identifier, ControllerClass as unknown as ControllerConstructor)\n if (options.controllers) {\n for (const [id, ctor] of Object.entries(options.controllers)) {\n application.register(id, ctor)\n }\n }\n\n // Wait for MutationObserver to pick up the fixture + connect() to fire.\n await nextTick()\n\n const element =\n insertedNodes\n .map((n) => (n instanceof HTMLElement && n.matches(`[data-controller~=\"${CSS.escape(identifier)}\"]`) ? n : null))\n .find((n): n is HTMLElement => !!n) ??\n findControllerRoot(container, identifier)\n if (!element) {\n throw new Error(\n `render(): no element with data-controller~=\"${identifier}\" found in the mounted fixture. ` +\n 'Check your HTML or pass options.identifier.',\n )\n }\n\n const controller = await waitForController<C>(application, element, identifier)\n\n const fixture: MountedFixture = {\n application,\n nodes: insertedNodes,\n ownsApplication,\n destroyed: false,\n }\n registerFixture(fixture)\n\n const user = createUserEvent()\n const queries = createQueries(element)\n\n const result: RenderResult<C> = {\n controller,\n element,\n application,\n user,\n waitFor,\n rerender: async (next) => {\n // Remove current inserted nodes, insert new.\n for (const n of insertedNodes) {\n if (n.parentNode) n.parentNode.removeChild(n)\n }\n insertedNodes.length = 0\n const newNodes =\n typeof next.html === 'string' ? parseHtml(next.html) : [next.html]\n for (const n of newNodes) {\n container.appendChild(n)\n insertedNodes.push(n)\n }\n fixture.nodes = insertedNodes\n await nextTick()\n const newElement = findControllerRoot(container, identifier)\n if (!newElement) {\n throw new Error(`rerender(): new fixture has no [data-controller~=\"${identifier}\"]`)\n }\n const newCtrl = await waitForController<C>(application, newElement, identifier)\n // Mutate in-place so references stay stable.\n ;(result as { controller: C }).controller = newCtrl\n ;(result as { element: HTMLElement }).element = newElement\n },\n unmount: () => destroyFixture(fixture),\n ...queries,\n }\n\n return result\n}\n","/** Convert camelCase / snake_case / PascalCase to kebab-case. */\nexport function toKebabCase(input: string): string {\n return input\n .replace(/_/g, '-')\n .replace(/([a-z0-9])([A-Z])/g, '$1-$2')\n .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')\n .toLowerCase()\n}\n\n/**\n * HTML-escape a value for safe placement inside a double-quoted attribute.\n * We intentionally only escape `&` and `\"` — `<` / `>` / `'` are valid\n * inside double-quoted attribute values per the HTML spec, and escaping\n * them would mangle Stimulus' own syntax (e.g. `click->hello#greet`).\n */\nexport function escapeAttr(value: string): string {\n return value.replace(/&/g, '&').replace(/\"/g, '"')\n}\n","/**\n * HTML attribute helpers producing AttrSpec objects that serialize\n * transparently inside template literals via Symbol.toPrimitive.\n *\n * Design notes:\n * - Every helper returns an AttrSpec — a structured description of the\n * attributes it contributes. `combine()` merges specs by reading the\n * structured data, not by parsing strings.\n * - `toString()` is the single point of serialization (HTML‑escaping,\n * kebab‑case conversion, JSON encoding of complex values).\n */\n\nimport { normalizeIdentifier } from './identifier.js'\nimport { toKebabCase, escapeAttr } from './utils.js'\nexport { toKebabCase, escapeAttr } from './utils.js'\n\nexport interface AttrSpec {\n toString(): string\n toJSON(): string\n [Symbol.toPrimitive](hint: string): string\n}\n\n/** Internal structured representation. Not exported. */\ninterface AttrData {\n /** Ordered unique controller identifiers (data-controller tokens). */\n controllers: string[]\n /** Raw `data-action` descriptors in declaration order. */\n actions: string[]\n /** Values keyed by \"<identifier>-<kebab-key>\". */\n values: Map<string, unknown>\n /** Classes keyed by \"<identifier>-<kebab-key>\". */\n classes: Map<string, string>\n /** Outlets keyed by \"<identifier>-<kebab-key>\". */\n outlets: Map<string, string>\n /** Targets keyed by identifier → ordered unique target names. */\n targets: Map<string, string[]>\n}\n\nconst SPEC_DATA = Symbol('stimulus-test-utils:attrData')\n\nfunction createSpec(data: AttrData): AttrSpec {\n const spec = {\n [SPEC_DATA]: data,\n toString(): string {\n return serialize(data)\n },\n toJSON(): string {\n return serialize(data)\n },\n [Symbol.toPrimitive](_hint: string): string {\n return serialize(data)\n },\n }\n return spec as unknown as AttrSpec\n}\n\nfunction getData(spec: AttrSpec): AttrData | undefined {\n return (spec as unknown as { [SPEC_DATA]?: AttrData })[SPEC_DATA]\n}\n\nfunction emptyData(): AttrData {\n return {\n controllers: [],\n actions: [],\n values: new Map(),\n classes: new Map(),\n outlets: new Map(),\n targets: new Map(),\n }\n}\n\n\nfunction serializeValue(raw: unknown): string {\n if (raw === null) return 'null'\n switch (typeof raw) {\n case 'string':\n return raw\n case 'number':\n case 'boolean':\n return String(raw)\n default:\n return JSON.stringify(raw)\n }\n}\n\nfunction serialize(data: AttrData): string {\n const parts: string[] = []\n\n if (data.controllers.length > 0) {\n parts.push(`data-controller=\"${escapeAttr(data.controllers.join(' '))}\"`)\n }\n\n for (const [identifier, names] of data.targets) {\n parts.push(`data-${identifier}-target=\"${escapeAttr(names.join(' '))}\"`)\n }\n\n for (const [key, raw] of data.values) {\n parts.push(`data-${key}-value=\"${escapeAttr(serializeValue(raw))}\"`)\n }\n for (const [key, raw] of data.classes) {\n parts.push(`data-${key}-class=\"${escapeAttr(raw)}\"`)\n }\n for (const [key, raw] of data.outlets) {\n parts.push(`data-${key}-outlet=\"${escapeAttr(raw)}\"`)\n }\n\n if (data.actions.length > 0) {\n parts.push(`data-action=\"${escapeAttr(data.actions.join(' '))}\"`)\n }\n\n return parts.join(' ')\n}\n\nexport function stimulusController(\n identifier: string,\n values?: Record<string, unknown>,\n classes?: Record<string, string>,\n outlets?: Record<string, string>,\n): AttrSpec {\n if (!identifier || typeof identifier !== 'string') {\n throw new TypeError(`stimulusController(): identifier must be a non-empty string, got ${String(identifier)}`)\n }\n identifier = normalizeIdentifier(identifier)\n const data = emptyData()\n data.controllers.push(identifier)\n if (values) {\n for (const [k, v] of Object.entries(values)) {\n data.values.set(`${identifier}-${toKebabCase(k)}`, v)\n }\n }\n if (classes) {\n for (const [k, v] of Object.entries(classes)) {\n data.classes.set(`${identifier}-${toKebabCase(k)}`, v)\n }\n }\n if (outlets) {\n for (const [k, v] of Object.entries(outlets)) {\n data.outlets.set(`${identifier}-${toKebabCase(k)}`, v)\n }\n }\n return createSpec(data)\n}\n\nexport function stimulusTarget(identifier: string, ...targetNames: string[]): AttrSpec {\n if (!identifier) throw new TypeError('stimulusTarget(): identifier is required')\n if (targetNames.length === 0) throw new TypeError('stimulusTarget(): at least one target name is required')\n identifier = normalizeIdentifier(identifier)\n const data = emptyData()\n const unique: string[] = []\n for (const n of targetNames) {\n if (!unique.includes(n)) unique.push(n)\n }\n data.targets.set(identifier, unique)\n return createSpec(data)\n}\n\nexport interface StimulusActionOptions {\n prevent?: boolean\n stop?: boolean\n once?: boolean\n passive?: boolean\n capture?: boolean\n self?: boolean\n}\n\nconst ACTION_OPTION_ORDER: (keyof StimulusActionOptions)[] = [\n 'prevent',\n 'stop',\n 'once',\n 'passive',\n 'capture',\n 'self',\n]\n\nexport function stimulusAction(\n identifier: string,\n method: string,\n event?: string,\n options?: StimulusActionOptions,\n): AttrSpec {\n if (!identifier) throw new TypeError('stimulusAction(): identifier is required')\n if (!method) throw new TypeError('stimulusAction(): method is required')\n identifier = normalizeIdentifier(identifier)\n\n const base = event ? `${event}->${identifier}#${method}` : `${identifier}#${method}`\n let descriptor = base\n if (options) {\n for (const key of ACTION_OPTION_ORDER) {\n if (options[key]) descriptor += `:${key}`\n }\n }\n const data = emptyData()\n data.actions.push(descriptor)\n return createSpec(data)\n}\n\n/**\n * Merge multiple AttrSpecs onto a single element.\n * Throws on duplicate controller identifier.\n */\nexport function combine(...specs: AttrSpec[]): AttrSpec {\n const merged = emptyData()\n\n for (const spec of specs) {\n const d = getData(spec)\n if (!d) {\n throw new TypeError('combine(): all arguments must be AttrSpec values returned from stimulus* helpers')\n }\n\n for (const id of d.controllers) {\n if (merged.controllers.includes(id)) {\n throw new Error(\n `combine(): duplicate Stimulus controller identifier \"${id}\". ` +\n `Declare each controller once and pass all its values/classes/outlets in a single stimulusController() call.`,\n )\n }\n merged.controllers.push(id)\n }\n\n for (const [id, names] of d.targets) {\n const existing = merged.targets.get(id) ?? []\n for (const n of names) if (!existing.includes(n)) existing.push(n)\n merged.targets.set(id, existing)\n }\n\n for (const [k, v] of d.values) merged.values.set(k, v)\n for (const [k, v] of d.classes) merged.classes.set(k, v)\n for (const [k, v] of d.outlets) merged.outlets.set(k, v)\n for (const a of d.actions) merged.actions.push(a)\n }\n\n return createSpec(merged)\n}\n"]}
|
package/dist/register.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// src/cleanup.ts
|
|
2
|
+
var registry = /* @__PURE__ */ new Set();
|
|
3
|
+
function destroyFixture(fx) {
|
|
4
|
+
if (fx.destroyed) return;
|
|
5
|
+
fx.destroyed = true;
|
|
6
|
+
for (const node of fx.nodes) {
|
|
7
|
+
if (node.parentNode) node.parentNode.removeChild(node);
|
|
8
|
+
}
|
|
9
|
+
if (fx.ownsApplication) {
|
|
10
|
+
try {
|
|
11
|
+
fx.application.stop();
|
|
12
|
+
} catch {
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
registry.delete(fx);
|
|
16
|
+
}
|
|
17
|
+
function cleanup() {
|
|
18
|
+
for (const fx of Array.from(registry)) {
|
|
19
|
+
destroyFixture(fx);
|
|
20
|
+
}
|
|
21
|
+
registry.clear();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/register.ts
|
|
25
|
+
var g = globalThis;
|
|
26
|
+
if (typeof g.afterEach === "function") {
|
|
27
|
+
g.afterEach(() => {
|
|
28
|
+
cleanup();
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=register.js.map
|
|
32
|
+
//# sourceMappingURL=register.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cleanup.ts","../src/register.ts"],"names":[],"mappings":";AAkBA,IAAM,QAAA,uBAAe,GAAA,EAAoB;AAMlC,SAAS,eAAe,EAAA,EAA0B;AACvD,EAAA,IAAI,GAAG,SAAA,EAAW;AAClB,EAAA,EAAA,CAAG,SAAA,GAAY,IAAA;AACf,EAAA,KAAA,MAAW,IAAA,IAAQ,GAAG,KAAA,EAAO;AAC3B,IAAA,IAAI,IAAA,CAAK,UAAA,EAAY,IAAA,CAAK,UAAA,CAAW,YAAY,IAAI,CAAA;AAAA,EACvD;AACA,EAAA,IAAI,GAAG,eAAA,EAAiB;AACtB,IAAA,IAAI;AACF,MAAA,EAAA,CAAG,YAAY,IAAA,EAAK;AAAA,IACtB,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACF;AACA,EAAA,QAAA,CAAS,OAAO,EAAE,CAAA;AACpB;AAEO,SAAS,OAAA,GAAgB;AAC9B,EAAA,KAAA,MAAW,EAAA,IAAM,KAAA,CAAM,IAAA,CAAK,QAAQ,CAAA,EAAG;AACrC,IAAA,cAAA,CAAe,EAAE,CAAA;AAAA,EACnB;AACA,EAAA,QAAA,CAAS,KAAA,EAAM;AACjB;;;AClCA,IAAM,CAAA,GAAI,UAAA;AACV,IAAI,OAAO,CAAA,CAAE,SAAA,KAAc,UAAA,EAAY;AACrC,EAAA,CAAA,CAAE,UAAU,MAAM;AAChB,IAAA,OAAA,EAAQ;AAAA,EACV,CAAC,CAAA;AACH","file":"register.js","sourcesContent":["import type { Application } from '@hotwired/stimulus'\n\n/**\n * Module-level registry of everything `render()` has created so that\n * `cleanup()` (or an individual `unmount()`) can tear it all down between\n * tests, leaving the DOM and Stimulus registry pristine.\n */\n\nexport interface MountedFixture {\n application: Application\n /** DOM nodes that `render()` inserted into the container. */\n nodes: Element[]\n /** Was the Stimulus Application created by us (vs. BYO)? */\n ownsApplication: boolean\n /** Mark destroyed so double-unmount is a no-op. */\n destroyed: boolean\n}\n\nconst registry = new Set<MountedFixture>()\n\nexport function registerFixture(fx: MountedFixture): void {\n registry.add(fx)\n}\n\nexport function destroyFixture(fx: MountedFixture): void {\n if (fx.destroyed) return\n fx.destroyed = true\n for (const node of fx.nodes) {\n if (node.parentNode) node.parentNode.removeChild(node)\n }\n if (fx.ownsApplication) {\n try {\n fx.application.stop()\n } catch {\n // best-effort: ignore teardown errors so cleanup keeps going\n }\n }\n registry.delete(fx)\n}\n\nexport function cleanup(): void {\n for (const fx of Array.from(registry)) {\n destroyFixture(fx)\n }\n registry.clear()\n}\n\n/** Test helper — internal only. */\nexport function _registrySize(): number {\n return registry.size\n}\n","/**\n * Side-effect entry: auto-wires `afterEach(cleanup)` in Vitest.\n * Enable via `setupFiles: ['@tito10047/stimulus-test-utils/register']`.\n *\n * If no test runner global `afterEach` is found, this file is a no-op — users\n * can still call `cleanup()` manually from their own setup.\n */\nimport { cleanup } from './cleanup.js'\n\ntype HookFn = (cb: () => void | Promise<void>) => void\n\nconst g = globalThis as unknown as { afterEach?: HookFn }\nif (typeof g.afterEach === 'function') {\n g.afterEach(() => {\n cleanup()\n })\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tito10047/stimulus-test-utils",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Zero-config, Testing-Library-flavoured test harness for Stimulus controllers.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/tito10047/stimulus-test-utils.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/tito10047/stimulus-test-utils/issues"
|
|
14
|
+
},
|
|
15
|
+
"author": {
|
|
16
|
+
"name": "Jozef Môstka",
|
|
17
|
+
"email": "jozef@mostka.sk",
|
|
18
|
+
"url": "https://mostka.sk/"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://tito10047.github.io/stimulus-test-utils/",
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"main": "./dist/index.js",
|
|
30
|
+
"module": "./dist/index.js",
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"import": "./dist/index.js"
|
|
36
|
+
},
|
|
37
|
+
"./register": {
|
|
38
|
+
"types": "./dist/register.d.ts",
|
|
39
|
+
"import": "./dist/register.js"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsup",
|
|
44
|
+
"test": "vitest",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"docs:api": "typedoc --plugin typedoc-plugin-markdown --out docs/api/generated --readme none --hideBreadcrumbs --hidePageHeader src/index.ts",
|
|
47
|
+
"docs:dev": "npm run docs:api && vitepress dev docs",
|
|
48
|
+
"docs:build": "npm run docs:api && vitepress build docs",
|
|
49
|
+
"docs:preview": "vitepress preview docs",
|
|
50
|
+
"clean": "rm -rf dist",
|
|
51
|
+
"prepublishOnly": "npm run clean && npm run typecheck && npm test -- --run && npm run build"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@hotwired/stimulus": "^3.2"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@hotwired/stimulus": "^3.2.2",
|
|
58
|
+
"@vitest/coverage-v8": "^2.1.9",
|
|
59
|
+
"happy-dom": "^15.0.0",
|
|
60
|
+
"tsup": "^8.0.0",
|
|
61
|
+
"typedoc": "^0.28.0",
|
|
62
|
+
"typedoc-plugin-markdown": "^4.2.0",
|
|
63
|
+
"typescript": "^5.4.0",
|
|
64
|
+
"vitepress": "^1.3.0",
|
|
65
|
+
"vitest": "^2.0.0"
|
|
66
|
+
},
|
|
67
|
+
"keywords": [
|
|
68
|
+
"stimulus",
|
|
69
|
+
"hotwired",
|
|
70
|
+
"testing",
|
|
71
|
+
"test-utils",
|
|
72
|
+
"testing-library",
|
|
73
|
+
"vitest",
|
|
74
|
+
"happy-dom"
|
|
75
|
+
]
|
|
76
|
+
}
|