@harnessed-ts/vitest-browser 0.0.0-stage → 0.7.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/CHANGELOG.md +80 -0
- package/LICENSE +21 -0
- package/README.md +23 -2
- package/dist/driver-id.d.ts +3 -0
- package/dist/driver-id.d.ts.map +1 -0
- package/dist/env.d.ts +18 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/index.cjs +214 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +210 -0
- package/dist/index.js.map +1 -0
- package/dist/matchers.cjs +9 -0
- package/dist/matchers.cjs.map +1 -0
- package/dist/matchers.d.ts +9 -0
- package/dist/matchers.d.ts.map +1 -0
- package/dist/matchers.js +7 -0
- package/dist/matchers.js.map +1 -0
- package/dist/vitest-browser-query.d.ts +54 -0
- package/dist/vitest-browser-query.d.ts.map +1 -0
- package/package.json +73 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# @harnessed-ts/vitest-browser
|
|
2
|
+
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [ec06bdf]
|
|
8
|
+
- @harnessed-ts/core@0.7.0
|
|
9
|
+
- @harnessed-ts/resolve@0.7.0
|
|
10
|
+
|
|
11
|
+
## 0.6.0
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- e324e94: ember-test-selectors style test ids: `configure({ testIdAttribute: 'data-test-*' })` makes `testId('x')` match any element that has the attribute `data-test-x`, whatever its value, with the same scoping, strictness, `{ global: true }`, frame and absence rules as before under every driver. A plain attribute name keeps matching by value. A presence id is held to letters, digits, `-`, `_`, `.` and `:`, and anything else (or a `RegExp`) is refused rather than spliced into a selector.
|
|
16
|
+
|
|
17
|
+
`Query.boundingBox()` resolves to `{ x, y, width, height }`: the target's border box in CSS pixels, relative to the top-level viewport, offset by every frame crossed. Strict, and on an absent target it rejects after the timeout, as `text()` does. A node that draws no box reads as all zeros, and so does every node under the dom driver: jsdom computes no layout.
|
|
18
|
+
|
|
19
|
+
`Query.upload(files)` sets the files of an `<input type="file">`, or of the input a `<label>` is for, and fires `input` and `change`, replacing any earlier selection. It refuses any other element, and several files for an input that is not `multiple`. Puppeteer, WebdriverIO and TestCafe take only paths, so there each `File` is written to a temporary directory first: its name and bytes arrive, and the browser infers its type from the extension.
|
|
20
|
+
|
|
21
|
+
`Query` gains two abstract members, so a driver outside this repo implements both to keep compiling; the conformance catalog pins all three changes.
|
|
22
|
+
|
|
23
|
+
`@harnessed-ts/eslint-plugin` supports ESLint 8.57 and legacy `.eslintrc`: the peer range is now `>=8.57`, `legacy-recommended` and `legacy-strict` name the rules under `@harnessed-ts/`, and the CJS build exposes `rules` and `configs` at the top level, where eslintrc reads them.
|
|
24
|
+
|
|
25
|
+
`npx @harnessed-ts/claude install` detects ember-test-selectors attributes (in `.gts`, `.gjs` and `.hbs` files too) and writes `testIdAttribute: 'data-test-*'`.
|
|
26
|
+
|
|
27
|
+
`@harnessed-ts/storybook` writes and finds its story-ready marker under a `data-test-*` config, where `StoryPage` waits for it.
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- Updated dependencies [e324e94]
|
|
32
|
+
- @harnessed-ts/core@0.6.0
|
|
33
|
+
- @harnessed-ts/resolve@0.6.0
|
|
34
|
+
|
|
35
|
+
## 0.5.0
|
|
36
|
+
|
|
37
|
+
### Patch Changes
|
|
38
|
+
|
|
39
|
+
- @harnessed-ts/core@0.5.0
|
|
40
|
+
- @harnessed-ts/resolve@0.5.0
|
|
41
|
+
|
|
42
|
+
## 0.4.0
|
|
43
|
+
|
|
44
|
+
### Minor Changes
|
|
45
|
+
|
|
46
|
+
- 1823546: `isVisible()`, `waitFor('visible')` and `waitFor('hidden')` on a target matching
|
|
47
|
+
several nodes now reject with the shared strict-mode violation, at once, under
|
|
48
|
+
every driver. The dom, Ember, Vitest browser mode and Cypress drivers answered
|
|
49
|
+
`false` from `isVisible()`, waited out the timeout in `waitFor('visible')`, and —
|
|
50
|
+
worse — resolved `waitFor('hidden')` at once, passing a wait for three rendered
|
|
51
|
+
nodes to disappear; Playwright's waits raised its own wording instead of the
|
|
52
|
+
shared one. An `nth()`
|
|
53
|
+
past the last match answers `false` from `isVisible()` and counts as hidden, as it
|
|
54
|
+
does under Playwright — Puppeteer and TestCafe used to reject it as an index out
|
|
55
|
+
of range.
|
|
56
|
+
|
|
57
|
+
Core exports `StrictModeViolation`, the class `strictViolation()` now returns, so a
|
|
58
|
+
driver can tell ambiguity apart from absence. It is the class a caller catches
|
|
59
|
+
under every driver: core also exports `reviveStrictViolation()`, which the
|
|
60
|
+
Puppeteer, WebdriverIO and TestCafe drivers apply to what the page throws, so
|
|
61
|
+
a violation raised in the page arrives as core's class, worded exactly as an
|
|
62
|
+
in-process driver words it. `instanceof StrictModeViolation` checks a brand
|
|
63
|
+
rather than the prototype, so it holds across two copies of core in one page —
|
|
64
|
+
Cypress bundles the support file and each spec separately.
|
|
65
|
+
|
|
66
|
+
`WaitState` now documents the rule it is held to: a state joins only when every
|
|
67
|
+
driver honours it with the same meaning, so a runner's own states — Playwright's
|
|
68
|
+
`attached` and `detached` — stay out of the shared interface.
|
|
69
|
+
|
|
70
|
+
- 9666a3d: New package `@harnessed-ts/vitest-browser`: a driver for Vitest browser mode. Harnesses resolve in the page through the shared resolver and act through the provider's `userEvent`, so clicks and typing are real browser input — including inside same-origin frames under the Playwright provider. `@harnessed-ts/vitest-browser/matchers` registers the shared matchers with Vitest's `expect`.
|
|
71
|
+
|
|
72
|
+
### Patch Changes
|
|
73
|
+
|
|
74
|
+
- Updated dependencies [9666a3d]
|
|
75
|
+
- Updated dependencies [9666a3d]
|
|
76
|
+
- Updated dependencies [9666a3d]
|
|
77
|
+
- Updated dependencies [9666a3d]
|
|
78
|
+
- Updated dependencies [1823546]
|
|
79
|
+
- @harnessed-ts/core@0.4.0
|
|
80
|
+
- @harnessed-ts/resolve@0.4.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joe Gaudet, Jay Seo
|
|
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
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @harnessed-ts/vitest-browser
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Vitest browser mode driver for `@harnessed-ts/core`. The test runs in the page, so
|
|
4
|
+
harnesses resolve there with the shared resolver, and every action goes through
|
|
5
|
+
the provider's `userEvent` — real browser input, not synthesised DOM events.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { vitestBrowser } from '@harnessed-ts/vitest-browser'
|
|
9
|
+
import '@harnessed-ts/vitest-browser/matchers'
|
|
10
|
+
import { render } from 'vitest-browser-react'
|
|
11
|
+
|
|
12
|
+
await render(<LoginForm />)
|
|
13
|
+
const form = new LoginFormHarness(vitestBrowser())
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Queries start at `document.body` by default, which is where portals land. Pass
|
|
17
|
+
`container` to scope a harness to one tree. Elements inside same-origin frames are
|
|
18
|
+
driven through the Playwright provider; component testing has no navigation, so a
|
|
19
|
+
page's `goto()` is refused.
|
|
20
|
+
|
|
21
|
+
Full guide, the cross-driver guarantees, and the API: the
|
|
22
|
+
[harnessed README](https://github.com/joegaudet/harnessed#readme).
|
|
23
|
+
|
|
24
|
+
MIT © Joe Gaudet, Jay Seo
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"driver-id.d.ts","sourceRoot":"","sources":["../src/driver-id.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,eAAO,MAAM,qBAAqB,mBAAmB,CAAA"}
|
package/dist/env.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { EnvConfig } from '@harnessed-ts/core';
|
|
2
|
+
import { VITEST_BROWSER_DRIVER } from './driver-id';
|
|
3
|
+
import './vitest-browser-query';
|
|
4
|
+
export { VITEST_BROWSER_DRIVER };
|
|
5
|
+
export interface VitestBrowserEnv extends EnvConfig {
|
|
6
|
+
readonly driver: typeof VITEST_BROWSER_DRIVER;
|
|
7
|
+
/**
|
|
8
|
+
* Where queries start. Defaults to `document.body`, which is where a portal
|
|
9
|
+
* lands — scope it to a render's `container` only when the tree has none.
|
|
10
|
+
*/
|
|
11
|
+
readonly container: HTMLElement;
|
|
12
|
+
}
|
|
13
|
+
export interface VitestBrowserEnvOptions {
|
|
14
|
+
container?: HTMLElement;
|
|
15
|
+
}
|
|
16
|
+
/** Builds the env a harness is constructed with under Vitest browser mode. */
|
|
17
|
+
export declare function vitestBrowser({ container }?: VitestBrowserEnvOptions): VitestBrowserEnv;
|
|
18
|
+
//# sourceMappingURL=env.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACnD,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAGnD,OAAO,wBAAwB,CAAA;AAE/B,OAAO,EAAE,qBAAqB,EAAE,CAAA;AAEhC,MAAM,WAAW,gBAAiB,SAAQ,SAAS;IACjD,QAAQ,CAAC,MAAM,EAAE,OAAO,qBAAqB,CAAA;IAC7C;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAA;CAChC;AAED,MAAM,WAAW,uBAAuB;IACtC,SAAS,CAAC,EAAE,WAAW,CAAA;CACxB;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,EAAE,SAAS,EAAE,GAAE,uBAA4B,GAAG,gBAAgB,CAK3F"}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var core = require('@harnessed-ts/core');
|
|
4
|
+
var resolve = require('@harnessed-ts/resolve');
|
|
5
|
+
var vitest = require('vitest');
|
|
6
|
+
var browser = require('vitest/browser');
|
|
7
|
+
|
|
8
|
+
// src/driver-id.ts
|
|
9
|
+
var VITEST_BROWSER_DRIVER = "vitest-browser";
|
|
10
|
+
browser.locators.extend({
|
|
11
|
+
harnessedSelector(selector) {
|
|
12
|
+
return selector;
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
function target(element) {
|
|
16
|
+
return element.ownerDocument === document ? element : locatorFor(element);
|
|
17
|
+
}
|
|
18
|
+
function locatorFor(element) {
|
|
19
|
+
if (element.ownerDocument === document) return browser.page.elementLocator(element);
|
|
20
|
+
const frame = element.ownerDocument.defaultView?.frameElement;
|
|
21
|
+
if (frame == null) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`harnessed: <${element.localName}> belongs to a document the vitest-browser driver cannot reach.`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return browser.page.frameLocator(locatorFor(frame)).harnessedSelector(browser.page.elementLocator(element).selector);
|
|
27
|
+
}
|
|
28
|
+
function actionOptions(options) {
|
|
29
|
+
return { timeout: core.timeoutFor(options?.timeout) };
|
|
30
|
+
}
|
|
31
|
+
function toKeyboardInput(key) {
|
|
32
|
+
return key.length === 1 ? key : `{${key}}`;
|
|
33
|
+
}
|
|
34
|
+
async function waitUntil(condition, timeout) {
|
|
35
|
+
let refused;
|
|
36
|
+
await vitest.vi.waitFor(
|
|
37
|
+
async () => {
|
|
38
|
+
try {
|
|
39
|
+
await condition();
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (!(error instanceof resolve.FrameEntryError || error instanceof core.StrictModeViolation)) {
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
refused = error;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{ timeout }
|
|
48
|
+
);
|
|
49
|
+
if (refused !== void 0) throw refused;
|
|
50
|
+
}
|
|
51
|
+
var VitestBrowserQuery = class _VitestBrowserQuery extends core.Query {
|
|
52
|
+
constructor(container, scope, selector) {
|
|
53
|
+
super(scope, selector);
|
|
54
|
+
this.container = container;
|
|
55
|
+
}
|
|
56
|
+
container;
|
|
57
|
+
clone(selector) {
|
|
58
|
+
return new _VitestBrowserQuery(this.container, this.scope, selector);
|
|
59
|
+
}
|
|
60
|
+
element(options) {
|
|
61
|
+
return resolve.resolveOne(this.container, this.scope, this.selector, options?.timeout);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Every match, resolved in one pass: the scope chain is walked once and the
|
|
65
|
+
* siblings scanned once, where the inherited default re-does both per element.
|
|
66
|
+
*
|
|
67
|
+
* Each result is bound to its already-resolved node. A callback that mutates
|
|
68
|
+
* the page can detach those nodes, so a bound query falls back to ordinary
|
|
69
|
+
* index resolution the moment its node leaves the document.
|
|
70
|
+
*/
|
|
71
|
+
async all() {
|
|
72
|
+
return (await resolve.resolveAll(this.container, this.scope, this.selector)).map(
|
|
73
|
+
(element, index) => new BoundVitestBrowserQuery(
|
|
74
|
+
this.container,
|
|
75
|
+
this.scope,
|
|
76
|
+
core.nth(this.selector, index),
|
|
77
|
+
element
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
// --- interactions -------------------------------------------------------
|
|
82
|
+
async click(options) {
|
|
83
|
+
await browser.userEvent.click(target(await this.element(options)), actionOptions(options));
|
|
84
|
+
}
|
|
85
|
+
async fill(value, options) {
|
|
86
|
+
await browser.userEvent.fill(target(await this.element(options)), value, actionOptions(options));
|
|
87
|
+
}
|
|
88
|
+
async clear(options) {
|
|
89
|
+
await browser.userEvent.fill(target(await this.element(options)), "", actionOptions(options));
|
|
90
|
+
}
|
|
91
|
+
async check(options) {
|
|
92
|
+
const element = await this.element(options);
|
|
93
|
+
if (!element.checked) {
|
|
94
|
+
await browser.userEvent.click(target(element), actionOptions(options));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async uncheck(options) {
|
|
98
|
+
const element = await this.element(options);
|
|
99
|
+
if (element.checked) {
|
|
100
|
+
await browser.userEvent.click(target(element), actionOptions(options));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async selectOption(value, options) {
|
|
104
|
+
await browser.userEvent.selectOptions(
|
|
105
|
+
target(await this.element(options)),
|
|
106
|
+
value,
|
|
107
|
+
actionOptions(options)
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
async hover(options) {
|
|
111
|
+
await browser.userEvent.hover(target(await this.element(options)), actionOptions(options));
|
|
112
|
+
}
|
|
113
|
+
async focus(options) {
|
|
114
|
+
(await this.element(options)).focus();
|
|
115
|
+
}
|
|
116
|
+
async blur(options) {
|
|
117
|
+
(await this.element(options)).blur();
|
|
118
|
+
}
|
|
119
|
+
async press(key, options) {
|
|
120
|
+
const element = await this.element(options);
|
|
121
|
+
element.focus();
|
|
122
|
+
await browser.userEvent.keyboard(toKeyboardInput(key));
|
|
123
|
+
}
|
|
124
|
+
async upload(files, options) {
|
|
125
|
+
const list = Array.isArray(files) ? files : [files];
|
|
126
|
+
const input = resolve.fileInputOf(await this.element(options));
|
|
127
|
+
core.checkFileTarget(input, list.length, this.scope, this.selector);
|
|
128
|
+
await browser.userEvent.upload(target(input), list, actionOptions(options));
|
|
129
|
+
}
|
|
130
|
+
// --- observations -------------------------------------------------------
|
|
131
|
+
async text(options) {
|
|
132
|
+
return ((await this.element(options)).textContent ?? "").trim();
|
|
133
|
+
}
|
|
134
|
+
async inputValue(options) {
|
|
135
|
+
const element = await this.element(options);
|
|
136
|
+
return element.value ?? "";
|
|
137
|
+
}
|
|
138
|
+
async attribute(name, options) {
|
|
139
|
+
return (await this.element(options)).getAttribute(name);
|
|
140
|
+
}
|
|
141
|
+
async isVisible(options) {
|
|
142
|
+
try {
|
|
143
|
+
return resolve.isVisibleInLayout(await this.element(options), this.container.ownerDocument);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (error instanceof resolve.FrameEntryError || error instanceof core.StrictModeViolation) throw error;
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async isEnabled(options) {
|
|
150
|
+
const element = await this.element(options);
|
|
151
|
+
const disabled = element.disabled === true || element.closest(":disabled") !== null;
|
|
152
|
+
return core.enabledFrom(disabled, element.getAttribute("aria-disabled"));
|
|
153
|
+
}
|
|
154
|
+
async isChecked(options) {
|
|
155
|
+
const element = await this.element(options);
|
|
156
|
+
return core.checkedFrom(
|
|
157
|
+
element.getAttribute("aria-checked"),
|
|
158
|
+
Boolean(element.checked)
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
async selectedOptions(options) {
|
|
162
|
+
const element = await this.element(options);
|
|
163
|
+
return [...element.selectedOptions ?? []].map((option) => option.value);
|
|
164
|
+
}
|
|
165
|
+
async boundingBox(options) {
|
|
166
|
+
return resolve.boundingBoxOf(await this.element(options), this.container.ownerDocument);
|
|
167
|
+
}
|
|
168
|
+
// --- waiting ------------------------------------------------------------
|
|
169
|
+
async waitFor(state, options) {
|
|
170
|
+
const timeout = core.timeoutFor(options?.timeout);
|
|
171
|
+
if (state === "visible") {
|
|
172
|
+
await waitUntil(async () => {
|
|
173
|
+
if (!await this.isVisible({ timeout })) throw new Error("not visible yet");
|
|
174
|
+
}, timeout);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
await waitUntil(async () => {
|
|
178
|
+
if (await this.count() !== 0 && await this.isVisible({ timeout })) {
|
|
179
|
+
throw new Error("still visible");
|
|
180
|
+
}
|
|
181
|
+
}, timeout);
|
|
182
|
+
}
|
|
183
|
+
async count() {
|
|
184
|
+
return resolve.countAll(this.container, this.scope, this.selector);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
var BoundVitestBrowserQuery = class extends VitestBrowserQuery {
|
|
188
|
+
constructor(container, scope, selector, bound) {
|
|
189
|
+
super(container, scope, selector);
|
|
190
|
+
this.bound = bound;
|
|
191
|
+
}
|
|
192
|
+
bound;
|
|
193
|
+
async element(options) {
|
|
194
|
+
if (this.bound.isConnected) return this.bound;
|
|
195
|
+
return super.element(options);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
core.registerDriver(VITEST_BROWSER_DRIVER, (env, scope, selector) => {
|
|
199
|
+
return new VitestBrowserQuery(env.container, scope, selector);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// src/env.ts
|
|
203
|
+
function vitestBrowser({ container } = {}) {
|
|
204
|
+
return {
|
|
205
|
+
driver: VITEST_BROWSER_DRIVER,
|
|
206
|
+
container: container ?? document.body
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
exports.VITEST_BROWSER_DRIVER = VITEST_BROWSER_DRIVER;
|
|
211
|
+
exports.VitestBrowserQuery = VitestBrowserQuery;
|
|
212
|
+
exports.vitestBrowser = vitestBrowser;
|
|
213
|
+
//# sourceMappingURL=index.cjs.map
|
|
214
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/driver-id.ts","../src/vitest-browser-query.ts","../src/env.ts"],"names":["locators","page","timeoutFor","vi","FrameEntryError","StrictModeViolation","Query","resolveOne","resolveAll","withNth","userEvent","fileInputOf","checkFileTarget","isVisibleInLayout","enabledFrom","checkedFrom","boundingBoxOf","countAll","registerDriver"],"mappings":"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","file":"index.cjs","sourcesContent":["/** Its own module so `env.ts` and `vitest-browser-query.ts` can share it without a cycle. */\nexport const VITEST_BROWSER_DRIVER = 'vitest-browser'\n","import { Query, registerDriver } from '@harnessed-ts/core'\nimport type { BoundingBox, EnvConfig, Selector, WaitOptions, WaitState } from '@harnessed-ts/core'\nimport {\n checkedFrom,\n checkFileTarget,\n enabledFrom,\n nth as withNth,\n StrictModeViolation,\n timeoutFor,\n} from '@harnessed-ts/core'\nimport {\n boundingBoxOf,\n countAll,\n fileInputOf,\n FrameEntryError,\n isVisibleInLayout,\n resolveAll,\n resolveOne,\n} from '@harnessed-ts/resolve'\nimport { vi } from 'vitest'\nimport { locators, page, userEvent } from 'vitest/browser'\nimport type { Locator } from 'vitest/browser'\nimport { VITEST_BROWSER_DRIVER } from './driver-id'\nimport type { VitestBrowserEnv } from './env'\n\ndeclare module 'vitest/browser' {\n interface LocatorSelectors {\n /**\n * Chains a raw provider selector onto a locator. Registered by\n * `@harnessed-ts/vitest-browser` to reach into frames; not for tests.\n */\n harnessedSelector(selector: string): Locator\n }\n}\n\n// The only public way to chain an arbitrary selector after `frameLocator()`,\n// which otherwise offers just the getBy* family.\nlocators.extend({\n harnessedSelector(selector: string) {\n return selector\n },\n})\n\n/**\n * What a provider action is handed for an element the shared resolver found.\n *\n * An element in the test's own document goes as itself. One inside an `<iframe>`\n * cannot: Vitest recognises an element with `instanceof Element`, which fails\n * across realms, and it then calls the node's own method instead — a synthetic\n * `click()`, and no `fill()` at all. So a framed element becomes a locator that\n * enters each frame on the way down, which the provider drives for real.\n */\nfunction target<E extends Element>(element: E): E | Locator {\n return element.ownerDocument === document ? element : locatorFor(element)\n}\n\nfunction locatorFor(element: Element): Locator {\n if (element.ownerDocument === document) return page.elementLocator(element)\n const frame = element.ownerDocument.defaultView?.frameElement\n if (frame == null) {\n throw new Error(\n `harnessed: <${element.localName}> belongs to a document the vitest-browser driver cannot reach.`,\n )\n }\n // The element's own locator is generated against its own document, which is\n // exactly the document the frame locator has entered.\n return page\n .frameLocator(locatorFor(frame))\n .harnessedSelector(page.elementLocator(element).selector)\n}\n\n/**\n * The provider options an action is handed. Every provider action waits for its\n * element to be actionable, and the Playwright provider's own `actionTimeout`\n * defaults to none — so without this, an action that can never happen (a click\n * on a disabled button) hangs until the test itself times out.\n *\n * Not an object literal at the call site: the option interfaces are empty until\n * a provider augments them, and Playwright's is the one that declares `timeout`.\n */\nfunction actionOptions(options?: WaitOptions): { timeout: number } {\n return { timeout: timeoutFor(options?.timeout) }\n}\n\n/** Playwright key names such as `Enter` map onto the user-event `{Enter}` syntax. */\nfunction toKeyboardInput(key: string): string {\n return key.length === 1 ? key : `{${key}}`\n}\n\n/**\n * Vitest's own `waitFor` retries every throw, but a frame that cannot be\n * entered never becomes enterable, and several matches never become one — so\n * either ends the wait at once.\n */\nasync function waitUntil(condition: () => Promise<void>, timeout: number): Promise<void> {\n let refused: FrameEntryError | StrictModeViolation | undefined\n await vi.waitFor(\n async () => {\n try {\n await condition()\n } catch (error) {\n if (!(error instanceof FrameEntryError || error instanceof StrictModeViolation)) {\n throw error\n }\n refused = error\n }\n },\n { timeout },\n )\n if (refused !== undefined) throw refused\n}\n\n/**\n * Vitest browser mode driver. The test runs in the page, so resolution is the\n * shared in-process resolver; only the interactions are the provider's, which\n * makes them real browser input rather than synthesised DOM events.\n */\nexport class VitestBrowserQuery extends Query {\n constructor(\n private readonly container: HTMLElement,\n scope: readonly Selector[],\n selector: Selector,\n ) {\n super(scope, selector)\n }\n\n protected override clone(selector: Selector): Query {\n return new VitestBrowserQuery(this.container, this.scope, selector)\n }\n\n protected element(options?: WaitOptions): Promise<HTMLElement> {\n return resolveOne(this.container, this.scope, this.selector, options?.timeout)\n }\n\n /**\n * Every match, resolved in one pass: the scope chain is walked once and the\n * siblings scanned once, where the inherited default re-does both per element.\n *\n * Each result is bound to its already-resolved node. A callback that mutates\n * the page can detach those nodes, so a bound query falls back to ordinary\n * index resolution the moment its node leaves the document.\n */\n override async all(): Promise<Query[]> {\n return (await resolveAll(this.container, this.scope, this.selector)).map(\n (element, index) =>\n new BoundVitestBrowserQuery(\n this.container,\n this.scope,\n withNth(this.selector, index),\n element,\n ),\n )\n }\n\n // --- interactions -------------------------------------------------------\n\n override async click(options?: WaitOptions): Promise<void> {\n await userEvent.click(target(await this.element(options)), actionOptions(options))\n }\n\n override async fill(value: string, options?: WaitOptions): Promise<void> {\n // The provider's fill is Playwright's: it replaces the value, and '' clears.\n await userEvent.fill(target(await this.element(options)), value, actionOptions(options))\n }\n\n override async clear(options?: WaitOptions): Promise<void> {\n // The provider's clear() drops its options, so it could not be given a\n // timeout; Playwright's clear is fill('') in any case.\n await userEvent.fill(target(await this.element(options)), '', actionOptions(options))\n }\n\n override async check(options?: WaitOptions): Promise<void> {\n const element = await this.element(options)\n if (!(element as HTMLInputElement).checked) {\n await userEvent.click(target(element), actionOptions(options))\n }\n }\n\n override async uncheck(options?: WaitOptions): Promise<void> {\n const element = await this.element(options)\n if ((element as HTMLInputElement).checked) {\n await userEvent.click(target(element), actionOptions(options))\n }\n }\n\n override async selectOption(value: string | string[], options?: WaitOptions): Promise<void> {\n // Playwright's selectOption replaces a multi-select's selection, which is the\n // semantic the shared API promises, so there is nothing to clear first.\n await userEvent.selectOptions(\n target(await this.element(options)),\n value,\n actionOptions(options),\n )\n }\n\n override async hover(options?: WaitOptions): Promise<void> {\n await userEvent.hover(target(await this.element(options)), actionOptions(options))\n }\n\n override async focus(options?: WaitOptions): Promise<void> {\n ;(await this.element(options)).focus()\n }\n\n override async blur(options?: WaitOptions): Promise<void> {\n ;(await this.element(options)).blur()\n }\n\n override async press(key: string, options?: WaitOptions): Promise<void> {\n const element = await this.element(options)\n element.focus()\n await userEvent.keyboard(toKeyboardInput(key))\n }\n\n override async upload(files: File | File[], options?: WaitOptions): Promise<void> {\n const list = Array.isArray(files) ? files : [files]\n const input = fileInputOf(await this.element(options))\n checkFileTarget(input, list.length, this.scope, this.selector)\n // The provider's upload: Playwright's setInputFiles, or WebdriverIO's, with the bytes.\n await userEvent.upload(target(input), list, actionOptions(options))\n }\n\n // --- observations -------------------------------------------------------\n\n override async text(options?: WaitOptions): Promise<string> {\n return ((await this.element(options)).textContent ?? '').trim()\n }\n\n override async inputValue(options?: WaitOptions): Promise<string> {\n const element = await this.element(options)\n return (element as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement).value ?? ''\n }\n\n override async attribute(name: string, options?: WaitOptions): Promise<string | null> {\n return (await this.element(options)).getAttribute(name)\n }\n\n override async isVisible(options?: WaitOptions): Promise<boolean> {\n try {\n // A real browser lays the page out, so this is Playwright's layout check\n // rather than the dom driver's computed-style walk.\n return isVisibleInLayout(await this.element(options), this.container.ownerDocument)\n } catch (error) {\n // Several matches is ambiguity, not invisibility: answering false would\n // claim that nodes on screen are not.\n if (error instanceof FrameEntryError || error instanceof StrictModeViolation) throw error\n // Not on screen at all, or an index past the last match. Prefer isAbsent()\n // to ask this — it answers without first waiting out the retry timeout.\n return false\n }\n }\n\n override async isEnabled(options?: WaitOptions): Promise<boolean> {\n const element = await this.element(options)\n // `disabled` is inherited from an ancestor fieldset, and the DOM property\n // already accounts for that where the attribute does not.\n const disabled =\n (element as HTMLInputElement).disabled === true || element.closest(':disabled') !== null\n return enabledFrom(disabled, element.getAttribute('aria-disabled'))\n }\n\n override async isChecked(options?: WaitOptions): Promise<boolean> {\n const element = await this.element(options)\n return checkedFrom(\n element.getAttribute('aria-checked'),\n Boolean((element as HTMLInputElement).checked),\n )\n }\n\n override async selectedOptions(options?: WaitOptions): Promise<string[]> {\n const element = (await this.element(options)) as HTMLSelectElement\n return [...(element.selectedOptions ?? [])].map(option => option.value)\n }\n\n override async boundingBox(options?: WaitOptions): Promise<BoundingBox> {\n return boundingBoxOf(await this.element(options), this.container.ownerDocument)\n }\n\n // --- waiting ------------------------------------------------------------\n\n override async waitFor(state: WaitState, options?: WaitOptions): Promise<void> {\n const timeout = timeoutFor(options?.timeout)\n if (state === 'visible') {\n await waitUntil(async () => {\n if (!(await this.isVisible({ timeout }))) throw new Error('not visible yet')\n }, timeout)\n return\n }\n await waitUntil(async () => {\n if ((await this.count()) !== 0 && (await this.isVisible({ timeout }))) {\n throw new Error('still visible')\n }\n }, timeout)\n }\n\n override async count(): Promise<number> {\n return countAll(this.container, this.scope, this.selector)\n }\n}\n\n/** A query pinned to a node `all()` already found. See `VitestBrowserQuery.all`. */\nclass BoundVitestBrowserQuery extends VitestBrowserQuery {\n constructor(\n container: HTMLElement,\n scope: readonly Selector[],\n selector: Selector,\n private readonly bound: HTMLElement,\n ) {\n super(container, scope, selector)\n }\n\n protected override async element(options?: WaitOptions): Promise<HTMLElement> {\n if (this.bound.isConnected) return this.bound\n // A re-render replaced the node; the selector still carries this query's\n // index, so ordinary resolution finds the replacement.\n return super.element(options)\n }\n}\n\n/**\n * Registering on import is what lets `@harnessed-ts/core` stay driver-free: core\n * holds a registry keyed by driver id and never imports a driver itself.\n */\nregisterDriver(VITEST_BROWSER_DRIVER, (env: EnvConfig, scope, selector) => {\n return new VitestBrowserQuery((env as VitestBrowserEnv).container, scope, selector)\n})\n","import type { EnvConfig } from '@harnessed-ts/core'\nimport { VITEST_BROWSER_DRIVER } from './driver-id'\n// Imported for its side effect: constructing an env is the moment the driver has\n// to be in the registry, and a bare re-export could be tree-shaken away.\nimport './vitest-browser-query'\n\nexport { VITEST_BROWSER_DRIVER }\n\nexport interface VitestBrowserEnv extends EnvConfig {\n readonly driver: typeof VITEST_BROWSER_DRIVER\n /**\n * Where queries start. Defaults to `document.body`, which is where a portal\n * lands — scope it to a render's `container` only when the tree has none.\n */\n readonly container: HTMLElement\n}\n\nexport interface VitestBrowserEnvOptions {\n container?: HTMLElement\n}\n\n/** Builds the env a harness is constructed with under Vitest browser mode. */\nexport function vitestBrowser({ container }: VitestBrowserEnvOptions = {}): VitestBrowserEnv {\n return {\n driver: VITEST_BROWSER_DRIVER,\n container: container ?? document.body,\n }\n}\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,OAAO,CAAA;AAC5D,YAAY,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,OAAO,CAAA;AACtE,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { registerDriver, Query, nth, checkFileTarget, StrictModeViolation, enabledFrom, checkedFrom, timeoutFor } from '@harnessed-ts/core';
|
|
2
|
+
import { resolveOne, resolveAll, fileInputOf, isVisibleInLayout, FrameEntryError, boundingBoxOf, countAll } from '@harnessed-ts/resolve';
|
|
3
|
+
import { vi } from 'vitest';
|
|
4
|
+
import { locators, userEvent, page } from 'vitest/browser';
|
|
5
|
+
|
|
6
|
+
// src/driver-id.ts
|
|
7
|
+
var VITEST_BROWSER_DRIVER = "vitest-browser";
|
|
8
|
+
locators.extend({
|
|
9
|
+
harnessedSelector(selector) {
|
|
10
|
+
return selector;
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
function target(element) {
|
|
14
|
+
return element.ownerDocument === document ? element : locatorFor(element);
|
|
15
|
+
}
|
|
16
|
+
function locatorFor(element) {
|
|
17
|
+
if (element.ownerDocument === document) return page.elementLocator(element);
|
|
18
|
+
const frame = element.ownerDocument.defaultView?.frameElement;
|
|
19
|
+
if (frame == null) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`harnessed: <${element.localName}> belongs to a document the vitest-browser driver cannot reach.`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
return page.frameLocator(locatorFor(frame)).harnessedSelector(page.elementLocator(element).selector);
|
|
25
|
+
}
|
|
26
|
+
function actionOptions(options) {
|
|
27
|
+
return { timeout: timeoutFor(options?.timeout) };
|
|
28
|
+
}
|
|
29
|
+
function toKeyboardInput(key) {
|
|
30
|
+
return key.length === 1 ? key : `{${key}}`;
|
|
31
|
+
}
|
|
32
|
+
async function waitUntil(condition, timeout) {
|
|
33
|
+
let refused;
|
|
34
|
+
await vi.waitFor(
|
|
35
|
+
async () => {
|
|
36
|
+
try {
|
|
37
|
+
await condition();
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (!(error instanceof FrameEntryError || error instanceof StrictModeViolation)) {
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
refused = error;
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{ timeout }
|
|
46
|
+
);
|
|
47
|
+
if (refused !== void 0) throw refused;
|
|
48
|
+
}
|
|
49
|
+
var VitestBrowserQuery = class _VitestBrowserQuery extends Query {
|
|
50
|
+
constructor(container, scope, selector) {
|
|
51
|
+
super(scope, selector);
|
|
52
|
+
this.container = container;
|
|
53
|
+
}
|
|
54
|
+
container;
|
|
55
|
+
clone(selector) {
|
|
56
|
+
return new _VitestBrowserQuery(this.container, this.scope, selector);
|
|
57
|
+
}
|
|
58
|
+
element(options) {
|
|
59
|
+
return resolveOne(this.container, this.scope, this.selector, options?.timeout);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Every match, resolved in one pass: the scope chain is walked once and the
|
|
63
|
+
* siblings scanned once, where the inherited default re-does both per element.
|
|
64
|
+
*
|
|
65
|
+
* Each result is bound to its already-resolved node. A callback that mutates
|
|
66
|
+
* the page can detach those nodes, so a bound query falls back to ordinary
|
|
67
|
+
* index resolution the moment its node leaves the document.
|
|
68
|
+
*/
|
|
69
|
+
async all() {
|
|
70
|
+
return (await resolveAll(this.container, this.scope, this.selector)).map(
|
|
71
|
+
(element, index) => new BoundVitestBrowserQuery(
|
|
72
|
+
this.container,
|
|
73
|
+
this.scope,
|
|
74
|
+
nth(this.selector, index),
|
|
75
|
+
element
|
|
76
|
+
)
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
// --- interactions -------------------------------------------------------
|
|
80
|
+
async click(options) {
|
|
81
|
+
await userEvent.click(target(await this.element(options)), actionOptions(options));
|
|
82
|
+
}
|
|
83
|
+
async fill(value, options) {
|
|
84
|
+
await userEvent.fill(target(await this.element(options)), value, actionOptions(options));
|
|
85
|
+
}
|
|
86
|
+
async clear(options) {
|
|
87
|
+
await userEvent.fill(target(await this.element(options)), "", actionOptions(options));
|
|
88
|
+
}
|
|
89
|
+
async check(options) {
|
|
90
|
+
const element = await this.element(options);
|
|
91
|
+
if (!element.checked) {
|
|
92
|
+
await userEvent.click(target(element), actionOptions(options));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async uncheck(options) {
|
|
96
|
+
const element = await this.element(options);
|
|
97
|
+
if (element.checked) {
|
|
98
|
+
await userEvent.click(target(element), actionOptions(options));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async selectOption(value, options) {
|
|
102
|
+
await userEvent.selectOptions(
|
|
103
|
+
target(await this.element(options)),
|
|
104
|
+
value,
|
|
105
|
+
actionOptions(options)
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
async hover(options) {
|
|
109
|
+
await userEvent.hover(target(await this.element(options)), actionOptions(options));
|
|
110
|
+
}
|
|
111
|
+
async focus(options) {
|
|
112
|
+
(await this.element(options)).focus();
|
|
113
|
+
}
|
|
114
|
+
async blur(options) {
|
|
115
|
+
(await this.element(options)).blur();
|
|
116
|
+
}
|
|
117
|
+
async press(key, options) {
|
|
118
|
+
const element = await this.element(options);
|
|
119
|
+
element.focus();
|
|
120
|
+
await userEvent.keyboard(toKeyboardInput(key));
|
|
121
|
+
}
|
|
122
|
+
async upload(files, options) {
|
|
123
|
+
const list = Array.isArray(files) ? files : [files];
|
|
124
|
+
const input = fileInputOf(await this.element(options));
|
|
125
|
+
checkFileTarget(input, list.length, this.scope, this.selector);
|
|
126
|
+
await userEvent.upload(target(input), list, actionOptions(options));
|
|
127
|
+
}
|
|
128
|
+
// --- observations -------------------------------------------------------
|
|
129
|
+
async text(options) {
|
|
130
|
+
return ((await this.element(options)).textContent ?? "").trim();
|
|
131
|
+
}
|
|
132
|
+
async inputValue(options) {
|
|
133
|
+
const element = await this.element(options);
|
|
134
|
+
return element.value ?? "";
|
|
135
|
+
}
|
|
136
|
+
async attribute(name, options) {
|
|
137
|
+
return (await this.element(options)).getAttribute(name);
|
|
138
|
+
}
|
|
139
|
+
async isVisible(options) {
|
|
140
|
+
try {
|
|
141
|
+
return isVisibleInLayout(await this.element(options), this.container.ownerDocument);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (error instanceof FrameEntryError || error instanceof StrictModeViolation) throw error;
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async isEnabled(options) {
|
|
148
|
+
const element = await this.element(options);
|
|
149
|
+
const disabled = element.disabled === true || element.closest(":disabled") !== null;
|
|
150
|
+
return enabledFrom(disabled, element.getAttribute("aria-disabled"));
|
|
151
|
+
}
|
|
152
|
+
async isChecked(options) {
|
|
153
|
+
const element = await this.element(options);
|
|
154
|
+
return checkedFrom(
|
|
155
|
+
element.getAttribute("aria-checked"),
|
|
156
|
+
Boolean(element.checked)
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
async selectedOptions(options) {
|
|
160
|
+
const element = await this.element(options);
|
|
161
|
+
return [...element.selectedOptions ?? []].map((option) => option.value);
|
|
162
|
+
}
|
|
163
|
+
async boundingBox(options) {
|
|
164
|
+
return boundingBoxOf(await this.element(options), this.container.ownerDocument);
|
|
165
|
+
}
|
|
166
|
+
// --- waiting ------------------------------------------------------------
|
|
167
|
+
async waitFor(state, options) {
|
|
168
|
+
const timeout = timeoutFor(options?.timeout);
|
|
169
|
+
if (state === "visible") {
|
|
170
|
+
await waitUntil(async () => {
|
|
171
|
+
if (!await this.isVisible({ timeout })) throw new Error("not visible yet");
|
|
172
|
+
}, timeout);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
await waitUntil(async () => {
|
|
176
|
+
if (await this.count() !== 0 && await this.isVisible({ timeout })) {
|
|
177
|
+
throw new Error("still visible");
|
|
178
|
+
}
|
|
179
|
+
}, timeout);
|
|
180
|
+
}
|
|
181
|
+
async count() {
|
|
182
|
+
return countAll(this.container, this.scope, this.selector);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
var BoundVitestBrowserQuery = class extends VitestBrowserQuery {
|
|
186
|
+
constructor(container, scope, selector, bound) {
|
|
187
|
+
super(container, scope, selector);
|
|
188
|
+
this.bound = bound;
|
|
189
|
+
}
|
|
190
|
+
bound;
|
|
191
|
+
async element(options) {
|
|
192
|
+
if (this.bound.isConnected) return this.bound;
|
|
193
|
+
return super.element(options);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
registerDriver(VITEST_BROWSER_DRIVER, (env, scope, selector) => {
|
|
197
|
+
return new VitestBrowserQuery(env.container, scope, selector);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// src/env.ts
|
|
201
|
+
function vitestBrowser({ container } = {}) {
|
|
202
|
+
return {
|
|
203
|
+
driver: VITEST_BROWSER_DRIVER,
|
|
204
|
+
container: container ?? document.body
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export { VITEST_BROWSER_DRIVER, VitestBrowserQuery, vitestBrowser };
|
|
209
|
+
//# sourceMappingURL=index.js.map
|
|
210
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/driver-id.ts","../src/vitest-browser-query.ts","../src/env.ts"],"names":["withNth"],"mappings":"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","file":"index.js","sourcesContent":["/** Its own module so `env.ts` and `vitest-browser-query.ts` can share it without a cycle. */\nexport const VITEST_BROWSER_DRIVER = 'vitest-browser'\n","import { Query, registerDriver } from '@harnessed-ts/core'\nimport type { BoundingBox, EnvConfig, Selector, WaitOptions, WaitState } from '@harnessed-ts/core'\nimport {\n checkedFrom,\n checkFileTarget,\n enabledFrom,\n nth as withNth,\n StrictModeViolation,\n timeoutFor,\n} from '@harnessed-ts/core'\nimport {\n boundingBoxOf,\n countAll,\n fileInputOf,\n FrameEntryError,\n isVisibleInLayout,\n resolveAll,\n resolveOne,\n} from '@harnessed-ts/resolve'\nimport { vi } from 'vitest'\nimport { locators, page, userEvent } from 'vitest/browser'\nimport type { Locator } from 'vitest/browser'\nimport { VITEST_BROWSER_DRIVER } from './driver-id'\nimport type { VitestBrowserEnv } from './env'\n\ndeclare module 'vitest/browser' {\n interface LocatorSelectors {\n /**\n * Chains a raw provider selector onto a locator. Registered by\n * `@harnessed-ts/vitest-browser` to reach into frames; not for tests.\n */\n harnessedSelector(selector: string): Locator\n }\n}\n\n// The only public way to chain an arbitrary selector after `frameLocator()`,\n// which otherwise offers just the getBy* family.\nlocators.extend({\n harnessedSelector(selector: string) {\n return selector\n },\n})\n\n/**\n * What a provider action is handed for an element the shared resolver found.\n *\n * An element in the test's own document goes as itself. One inside an `<iframe>`\n * cannot: Vitest recognises an element with `instanceof Element`, which fails\n * across realms, and it then calls the node's own method instead — a synthetic\n * `click()`, and no `fill()` at all. So a framed element becomes a locator that\n * enters each frame on the way down, which the provider drives for real.\n */\nfunction target<E extends Element>(element: E): E | Locator {\n return element.ownerDocument === document ? element : locatorFor(element)\n}\n\nfunction locatorFor(element: Element): Locator {\n if (element.ownerDocument === document) return page.elementLocator(element)\n const frame = element.ownerDocument.defaultView?.frameElement\n if (frame == null) {\n throw new Error(\n `harnessed: <${element.localName}> belongs to a document the vitest-browser driver cannot reach.`,\n )\n }\n // The element's own locator is generated against its own document, which is\n // exactly the document the frame locator has entered.\n return page\n .frameLocator(locatorFor(frame))\n .harnessedSelector(page.elementLocator(element).selector)\n}\n\n/**\n * The provider options an action is handed. Every provider action waits for its\n * element to be actionable, and the Playwright provider's own `actionTimeout`\n * defaults to none — so without this, an action that can never happen (a click\n * on a disabled button) hangs until the test itself times out.\n *\n * Not an object literal at the call site: the option interfaces are empty until\n * a provider augments them, and Playwright's is the one that declares `timeout`.\n */\nfunction actionOptions(options?: WaitOptions): { timeout: number } {\n return { timeout: timeoutFor(options?.timeout) }\n}\n\n/** Playwright key names such as `Enter` map onto the user-event `{Enter}` syntax. */\nfunction toKeyboardInput(key: string): string {\n return key.length === 1 ? key : `{${key}}`\n}\n\n/**\n * Vitest's own `waitFor` retries every throw, but a frame that cannot be\n * entered never becomes enterable, and several matches never become one — so\n * either ends the wait at once.\n */\nasync function waitUntil(condition: () => Promise<void>, timeout: number): Promise<void> {\n let refused: FrameEntryError | StrictModeViolation | undefined\n await vi.waitFor(\n async () => {\n try {\n await condition()\n } catch (error) {\n if (!(error instanceof FrameEntryError || error instanceof StrictModeViolation)) {\n throw error\n }\n refused = error\n }\n },\n { timeout },\n )\n if (refused !== undefined) throw refused\n}\n\n/**\n * Vitest browser mode driver. The test runs in the page, so resolution is the\n * shared in-process resolver; only the interactions are the provider's, which\n * makes them real browser input rather than synthesised DOM events.\n */\nexport class VitestBrowserQuery extends Query {\n constructor(\n private readonly container: HTMLElement,\n scope: readonly Selector[],\n selector: Selector,\n ) {\n super(scope, selector)\n }\n\n protected override clone(selector: Selector): Query {\n return new VitestBrowserQuery(this.container, this.scope, selector)\n }\n\n protected element(options?: WaitOptions): Promise<HTMLElement> {\n return resolveOne(this.container, this.scope, this.selector, options?.timeout)\n }\n\n /**\n * Every match, resolved in one pass: the scope chain is walked once and the\n * siblings scanned once, where the inherited default re-does both per element.\n *\n * Each result is bound to its already-resolved node. A callback that mutates\n * the page can detach those nodes, so a bound query falls back to ordinary\n * index resolution the moment its node leaves the document.\n */\n override async all(): Promise<Query[]> {\n return (await resolveAll(this.container, this.scope, this.selector)).map(\n (element, index) =>\n new BoundVitestBrowserQuery(\n this.container,\n this.scope,\n withNth(this.selector, index),\n element,\n ),\n )\n }\n\n // --- interactions -------------------------------------------------------\n\n override async click(options?: WaitOptions): Promise<void> {\n await userEvent.click(target(await this.element(options)), actionOptions(options))\n }\n\n override async fill(value: string, options?: WaitOptions): Promise<void> {\n // The provider's fill is Playwright's: it replaces the value, and '' clears.\n await userEvent.fill(target(await this.element(options)), value, actionOptions(options))\n }\n\n override async clear(options?: WaitOptions): Promise<void> {\n // The provider's clear() drops its options, so it could not be given a\n // timeout; Playwright's clear is fill('') in any case.\n await userEvent.fill(target(await this.element(options)), '', actionOptions(options))\n }\n\n override async check(options?: WaitOptions): Promise<void> {\n const element = await this.element(options)\n if (!(element as HTMLInputElement).checked) {\n await userEvent.click(target(element), actionOptions(options))\n }\n }\n\n override async uncheck(options?: WaitOptions): Promise<void> {\n const element = await this.element(options)\n if ((element as HTMLInputElement).checked) {\n await userEvent.click(target(element), actionOptions(options))\n }\n }\n\n override async selectOption(value: string | string[], options?: WaitOptions): Promise<void> {\n // Playwright's selectOption replaces a multi-select's selection, which is the\n // semantic the shared API promises, so there is nothing to clear first.\n await userEvent.selectOptions(\n target(await this.element(options)),\n value,\n actionOptions(options),\n )\n }\n\n override async hover(options?: WaitOptions): Promise<void> {\n await userEvent.hover(target(await this.element(options)), actionOptions(options))\n }\n\n override async focus(options?: WaitOptions): Promise<void> {\n ;(await this.element(options)).focus()\n }\n\n override async blur(options?: WaitOptions): Promise<void> {\n ;(await this.element(options)).blur()\n }\n\n override async press(key: string, options?: WaitOptions): Promise<void> {\n const element = await this.element(options)\n element.focus()\n await userEvent.keyboard(toKeyboardInput(key))\n }\n\n override async upload(files: File | File[], options?: WaitOptions): Promise<void> {\n const list = Array.isArray(files) ? files : [files]\n const input = fileInputOf(await this.element(options))\n checkFileTarget(input, list.length, this.scope, this.selector)\n // The provider's upload: Playwright's setInputFiles, or WebdriverIO's, with the bytes.\n await userEvent.upload(target(input), list, actionOptions(options))\n }\n\n // --- observations -------------------------------------------------------\n\n override async text(options?: WaitOptions): Promise<string> {\n return ((await this.element(options)).textContent ?? '').trim()\n }\n\n override async inputValue(options?: WaitOptions): Promise<string> {\n const element = await this.element(options)\n return (element as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement).value ?? ''\n }\n\n override async attribute(name: string, options?: WaitOptions): Promise<string | null> {\n return (await this.element(options)).getAttribute(name)\n }\n\n override async isVisible(options?: WaitOptions): Promise<boolean> {\n try {\n // A real browser lays the page out, so this is Playwright's layout check\n // rather than the dom driver's computed-style walk.\n return isVisibleInLayout(await this.element(options), this.container.ownerDocument)\n } catch (error) {\n // Several matches is ambiguity, not invisibility: answering false would\n // claim that nodes on screen are not.\n if (error instanceof FrameEntryError || error instanceof StrictModeViolation) throw error\n // Not on screen at all, or an index past the last match. Prefer isAbsent()\n // to ask this — it answers without first waiting out the retry timeout.\n return false\n }\n }\n\n override async isEnabled(options?: WaitOptions): Promise<boolean> {\n const element = await this.element(options)\n // `disabled` is inherited from an ancestor fieldset, and the DOM property\n // already accounts for that where the attribute does not.\n const disabled =\n (element as HTMLInputElement).disabled === true || element.closest(':disabled') !== null\n return enabledFrom(disabled, element.getAttribute('aria-disabled'))\n }\n\n override async isChecked(options?: WaitOptions): Promise<boolean> {\n const element = await this.element(options)\n return checkedFrom(\n element.getAttribute('aria-checked'),\n Boolean((element as HTMLInputElement).checked),\n )\n }\n\n override async selectedOptions(options?: WaitOptions): Promise<string[]> {\n const element = (await this.element(options)) as HTMLSelectElement\n return [...(element.selectedOptions ?? [])].map(option => option.value)\n }\n\n override async boundingBox(options?: WaitOptions): Promise<BoundingBox> {\n return boundingBoxOf(await this.element(options), this.container.ownerDocument)\n }\n\n // --- waiting ------------------------------------------------------------\n\n override async waitFor(state: WaitState, options?: WaitOptions): Promise<void> {\n const timeout = timeoutFor(options?.timeout)\n if (state === 'visible') {\n await waitUntil(async () => {\n if (!(await this.isVisible({ timeout }))) throw new Error('not visible yet')\n }, timeout)\n return\n }\n await waitUntil(async () => {\n if ((await this.count()) !== 0 && (await this.isVisible({ timeout }))) {\n throw new Error('still visible')\n }\n }, timeout)\n }\n\n override async count(): Promise<number> {\n return countAll(this.container, this.scope, this.selector)\n }\n}\n\n/** A query pinned to a node `all()` already found. See `VitestBrowserQuery.all`. */\nclass BoundVitestBrowserQuery extends VitestBrowserQuery {\n constructor(\n container: HTMLElement,\n scope: readonly Selector[],\n selector: Selector,\n private readonly bound: HTMLElement,\n ) {\n super(container, scope, selector)\n }\n\n protected override async element(options?: WaitOptions): Promise<HTMLElement> {\n if (this.bound.isConnected) return this.bound\n // A re-render replaced the node; the selector still carries this query's\n // index, so ordinary resolution finds the replacement.\n return super.element(options)\n }\n}\n\n/**\n * Registering on import is what lets `@harnessed-ts/core` stay driver-free: core\n * holds a registry keyed by driver id and never imports a driver itself.\n */\nregisterDriver(VITEST_BROWSER_DRIVER, (env: EnvConfig, scope, selector) => {\n return new VitestBrowserQuery((env as VitestBrowserEnv).container, scope, selector)\n})\n","import type { EnvConfig } from '@harnessed-ts/core'\nimport { VITEST_BROWSER_DRIVER } from './driver-id'\n// Imported for its side effect: constructing an env is the moment the driver has\n// to be in the registry, and a bare re-export could be tree-shaken away.\nimport './vitest-browser-query'\n\nexport { VITEST_BROWSER_DRIVER }\n\nexport interface VitestBrowserEnv extends EnvConfig {\n readonly driver: typeof VITEST_BROWSER_DRIVER\n /**\n * Where queries start. Defaults to `document.body`, which is where a portal\n * lands — scope it to a render's `container` only when the tree has none.\n */\n readonly container: HTMLElement\n}\n\nexport interface VitestBrowserEnvOptions {\n container?: HTMLElement\n}\n\n/** Builds the env a harness is constructed with under Vitest browser mode. */\nexport function vitestBrowser({ container }: VitestBrowserEnvOptions = {}): VitestBrowserEnv {\n return {\n driver: VITEST_BROWSER_DRIVER,\n container: container ?? document.body,\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/matchers.ts"],"names":["expect","harnessMatchers"],"mappings":";;;;;;AAOAA,aAAA,CAAO,OAAOC,oBAAe,CAAA","file":"matchers.cjs","sourcesContent":["import { harnessMatchers } from '@harnessed-ts/core'\nimport type { Assertable } from '@harnessed-ts/core'\nimport { expect } from 'vitest'\n\n// `import '@harnessed-ts/vitest-browser/matchers'` from a setup file registers all\n// three. Browser mode runs the same `expect` as Node, so this is the dom\n// driver's registration unchanged.\nexpect.extend(harnessMatchers)\n\ndeclare module 'vitest' {\n // The parameter list must match vitest's own declaration exactly. `T` is the\n // subject, and gating on it means calling these on something that is not a\n // target or a harness is a type error rather than a runtime surprise.\n interface Matchers<R extends void | Promise<void> = void | Promise<void>, T = unknown> {\n toBeSelected(): T extends Assertable ? R : never\n toBeAbsent(): T extends Assertable ? R : never\n toReadAs(expected: string | RegExp): T extends Assertable ? R : never\n }\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Assertable } from '@harnessed-ts/core';
|
|
2
|
+
declare module 'vitest' {
|
|
3
|
+
interface Matchers<R extends void | Promise<void> = void | Promise<void>, T = unknown> {
|
|
4
|
+
toBeSelected(): T extends Assertable ? R : never;
|
|
5
|
+
toBeAbsent(): T extends Assertable ? R : never;
|
|
6
|
+
toReadAs(expected: string | RegExp): T extends Assertable ? R : never;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=matchers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"matchers.d.ts","sourceRoot":"","sources":["../src/matchers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAQpD,OAAO,QAAQ,QAAQ,CAAC;IAItB,UAAU,QAAQ,CAAC,CAAC,SAAS,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,OAAO;QACnF,YAAY,IAAI,CAAC,SAAS,UAAU,GAAG,CAAC,GAAG,KAAK,CAAA;QAChD,UAAU,IAAI,CAAC,SAAS,UAAU,GAAG,CAAC,GAAG,KAAK,CAAA;QAC9C,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,CAAC,SAAS,UAAU,GAAG,CAAC,GAAG,KAAK,CAAA;KACtE;CACF"}
|
package/dist/matchers.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/matchers.ts"],"names":[],"mappings":";;;;AAOA,MAAA,CAAO,OAAO,eAAe,CAAA","file":"matchers.js","sourcesContent":["import { harnessMatchers } from '@harnessed-ts/core'\nimport type { Assertable } from '@harnessed-ts/core'\nimport { expect } from 'vitest'\n\n// `import '@harnessed-ts/vitest-browser/matchers'` from a setup file registers all\n// three. Browser mode runs the same `expect` as Node, so this is the dom\n// driver's registration unchanged.\nexpect.extend(harnessMatchers)\n\ndeclare module 'vitest' {\n // The parameter list must match vitest's own declaration exactly. `T` is the\n // subject, and gating on it means calling these on something that is not a\n // target or a harness is a type error rather than a runtime surprise.\n interface Matchers<R extends void | Promise<void> = void | Promise<void>, T = unknown> {\n toBeSelected(): T extends Assertable ? R : never\n toBeAbsent(): T extends Assertable ? R : never\n toReadAs(expected: string | RegExp): T extends Assertable ? R : never\n }\n}\n"]}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Query } from '@harnessed-ts/core';
|
|
2
|
+
import type { BoundingBox, Selector, WaitOptions, WaitState } from '@harnessed-ts/core';
|
|
3
|
+
import type { Locator } from 'vitest/browser';
|
|
4
|
+
declare module 'vitest/browser' {
|
|
5
|
+
interface LocatorSelectors {
|
|
6
|
+
/**
|
|
7
|
+
* Chains a raw provider selector onto a locator. Registered by
|
|
8
|
+
* `@harnessed-ts/vitest-browser` to reach into frames; not for tests.
|
|
9
|
+
*/
|
|
10
|
+
harnessedSelector(selector: string): Locator;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Vitest browser mode driver. The test runs in the page, so resolution is the
|
|
15
|
+
* shared in-process resolver; only the interactions are the provider's, which
|
|
16
|
+
* makes them real browser input rather than synthesised DOM events.
|
|
17
|
+
*/
|
|
18
|
+
export declare class VitestBrowserQuery extends Query {
|
|
19
|
+
private readonly container;
|
|
20
|
+
constructor(container: HTMLElement, scope: readonly Selector[], selector: Selector);
|
|
21
|
+
protected clone(selector: Selector): Query;
|
|
22
|
+
protected element(options?: WaitOptions): Promise<HTMLElement>;
|
|
23
|
+
/**
|
|
24
|
+
* Every match, resolved in one pass: the scope chain is walked once and the
|
|
25
|
+
* siblings scanned once, where the inherited default re-does both per element.
|
|
26
|
+
*
|
|
27
|
+
* Each result is bound to its already-resolved node. A callback that mutates
|
|
28
|
+
* the page can detach those nodes, so a bound query falls back to ordinary
|
|
29
|
+
* index resolution the moment its node leaves the document.
|
|
30
|
+
*/
|
|
31
|
+
all(): Promise<Query[]>;
|
|
32
|
+
click(options?: WaitOptions): Promise<void>;
|
|
33
|
+
fill(value: string, options?: WaitOptions): Promise<void>;
|
|
34
|
+
clear(options?: WaitOptions): Promise<void>;
|
|
35
|
+
check(options?: WaitOptions): Promise<void>;
|
|
36
|
+
uncheck(options?: WaitOptions): Promise<void>;
|
|
37
|
+
selectOption(value: string | string[], options?: WaitOptions): Promise<void>;
|
|
38
|
+
hover(options?: WaitOptions): Promise<void>;
|
|
39
|
+
focus(options?: WaitOptions): Promise<void>;
|
|
40
|
+
blur(options?: WaitOptions): Promise<void>;
|
|
41
|
+
press(key: string, options?: WaitOptions): Promise<void>;
|
|
42
|
+
upload(files: File | File[], options?: WaitOptions): Promise<void>;
|
|
43
|
+
text(options?: WaitOptions): Promise<string>;
|
|
44
|
+
inputValue(options?: WaitOptions): Promise<string>;
|
|
45
|
+
attribute(name: string, options?: WaitOptions): Promise<string | null>;
|
|
46
|
+
isVisible(options?: WaitOptions): Promise<boolean>;
|
|
47
|
+
isEnabled(options?: WaitOptions): Promise<boolean>;
|
|
48
|
+
isChecked(options?: WaitOptions): Promise<boolean>;
|
|
49
|
+
selectedOptions(options?: WaitOptions): Promise<string[]>;
|
|
50
|
+
boundingBox(options?: WaitOptions): Promise<BoundingBox>;
|
|
51
|
+
waitFor(state: WaitState, options?: WaitOptions): Promise<void>;
|
|
52
|
+
count(): Promise<number>;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=vitest-browser-query.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vitest-browser-query.d.ts","sourceRoot":"","sources":["../src/vitest-browser-query.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,76 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@harnessed-ts/vitest-browser",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Vitest browser mode driver for @harnessed-ts/core: in-page resolution, provider-native user events.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"testing",
|
|
7
|
+
"harness",
|
|
8
|
+
"vitest",
|
|
9
|
+
"browser-mode",
|
|
10
|
+
"component-testing"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"contributors": [
|
|
14
|
+
{
|
|
15
|
+
"name": "Joe Gaudet",
|
|
16
|
+
"url": "https://github.com/joegaudet"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"name": "Jay Seo",
|
|
20
|
+
"url": "https://github.com/jayseo5953"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/joegaudet/harnessed.git",
|
|
26
|
+
"directory": "packages/vitest-browser"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/joegaudet/harnessed#readme",
|
|
29
|
+
"bugs": "https://github.com/joegaudet/harnessed/issues",
|
|
30
|
+
"type": "module",
|
|
31
|
+
"main": "./dist/index.cjs",
|
|
32
|
+
"module": "./dist/index.js",
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"import": "./dist/index.js",
|
|
38
|
+
"require": "./dist/index.cjs"
|
|
39
|
+
},
|
|
40
|
+
"./matchers": {
|
|
41
|
+
"types": "./dist/matchers.d.ts",
|
|
42
|
+
"import": "./dist/matchers.js",
|
|
43
|
+
"require": "./dist/matchers.cjs"
|
|
44
|
+
},
|
|
45
|
+
"./package.json": "./package.json"
|
|
46
|
+
},
|
|
47
|
+
"files": [
|
|
48
|
+
"dist",
|
|
49
|
+
"CHANGELOG.md",
|
|
50
|
+
"README.md",
|
|
51
|
+
"LICENSE"
|
|
52
|
+
],
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=22.12.0"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@harnessed-ts/core": "^0.7.0",
|
|
58
|
+
"@harnessed-ts/resolve": "^0.7.0"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"@testing-library/dom": ">=10",
|
|
62
|
+
"vitest": ">=4"
|
|
63
|
+
},
|
|
64
|
+
"publishConfig": {
|
|
65
|
+
"access": "public"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@testing-library/dom": "^10.4.2",
|
|
69
|
+
"@vitest/browser": "5.0.3",
|
|
70
|
+
"vitest": "^5.0.3"
|
|
71
|
+
},
|
|
72
|
+
"scripts": {
|
|
73
|
+
"build": "tsup",
|
|
74
|
+
"clean": "rm -rf dist *.tsbuildinfo"
|
|
75
|
+
}
|
|
6
76
|
}
|