@harnessed-ts/ember 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 ADDED
@@ -0,0 +1,83 @@
1
+ # @harnessed-ts/ember
2
+
3
+ ## 0.7.0
4
+
5
+ ### Patch Changes
6
+
7
+ - 0ab9390: `upload()` no longer goes through user-event. user-event's setup patches the document for good, and an Ember test run shares one document, so after a test that uploaded a file, a number input in a later test read "42.50" back as "42.5". The driver now sets the files itself and fires `input` and `change`, keeping user-event's rules: a disabled input is skipped, files `accept` refuses are dropped, and an unchanged selection fires nothing. `press`, `hover` and `selectOption` still use user-event, so they still patch the document.
8
+ - Updated dependencies [ec06bdf]
9
+ - @harnessed-ts/core@0.7.0
10
+ - @harnessed-ts/resolve@0.7.0
11
+
12
+ ## 0.6.0
13
+
14
+ ### Minor Changes
15
+
16
+ - 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.
17
+
18
+ `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.
19
+
20
+ `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.
21
+
22
+ `Query` gains two abstract members, so a driver outside this repo implements both to keep compiling; the conformance catalog pins all three changes.
23
+
24
+ `@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.
25
+
26
+ `npx @harnessed-ts/claude install` detects ember-test-selectors attributes (in `.gts`, `.gjs` and `.hbs` files too) and writes `testIdAttribute: 'data-test-*'`.
27
+
28
+ `@harnessed-ts/storybook` writes and finds its story-ready marker under a `data-test-*` config, where `StoryPage` waits for it.
29
+
30
+ ### Patch Changes
31
+
32
+ - Updated dependencies [e324e94]
33
+ - @harnessed-ts/core@0.6.0
34
+ - @harnessed-ts/resolve@0.6.0
35
+
36
+ ## 0.5.0
37
+
38
+ ### Patch Changes
39
+
40
+ - @harnessed-ts/core@0.5.0
41
+ - @harnessed-ts/resolve@0.5.0
42
+
43
+ ## 0.4.0
44
+
45
+ ### Minor Changes
46
+
47
+ - 9666a3d: New `@harnessed-ts/ember` driver: queries resolve through `@harnessed-ts/resolve`, interactions go through `@ember/test-helpers` and settle after each one, and a page's `goto()` drives the router in application tests. It passes the whole conformance catalog under ember-qunit, on classic and Embroider + Vite builds, Ember 5.12 to latest.
48
+
49
+ `@harnessed-ts/conformance` exports `fixtureTree()` and `fixtureTrees` so a port of the fixture to another framework can prove it renders the same markup as the React original.
50
+
51
+ - 1823546: `isVisible()`, `waitFor('visible')` and `waitFor('hidden')` on a target matching
52
+ several nodes now reject with the shared strict-mode violation, at once, under
53
+ every driver. The dom, Ember, Vitest browser mode and Cypress drivers answered
54
+ `false` from `isVisible()`, waited out the timeout in `waitFor('visible')`, and —
55
+ worse — resolved `waitFor('hidden')` at once, passing a wait for three rendered
56
+ nodes to disappear; Playwright's waits raised its own wording instead of the
57
+ shared one. An `nth()`
58
+ past the last match answers `false` from `isVisible()` and counts as hidden, as it
59
+ does under Playwright — Puppeteer and TestCafe used to reject it as an index out
60
+ of range.
61
+
62
+ Core exports `StrictModeViolation`, the class `strictViolation()` now returns, so a
63
+ driver can tell ambiguity apart from absence. It is the class a caller catches
64
+ under every driver: core also exports `reviveStrictViolation()`, which the
65
+ Puppeteer, WebdriverIO and TestCafe drivers apply to what the page throws, so
66
+ a violation raised in the page arrives as core's class, worded exactly as an
67
+ in-process driver words it. `instanceof StrictModeViolation` checks a brand
68
+ rather than the prototype, so it holds across two copies of core in one page —
69
+ Cypress bundles the support file and each spec separately.
70
+
71
+ `WaitState` now documents the rule it is held to: a state joins only when every
72
+ driver honours it with the same meaning, so a runner's own states — Playwright's
73
+ `attached` and `detached` — stay out of the shared interface.
74
+
75
+ ### Patch Changes
76
+
77
+ - Updated dependencies [9666a3d]
78
+ - Updated dependencies [9666a3d]
79
+ - Updated dependencies [9666a3d]
80
+ - Updated dependencies [9666a3d]
81
+ - Updated dependencies [1823546]
82
+ - @harnessed-ts/core@0.4.0
83
+ - @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,28 @@
1
- # Temporary Holding Version
1
+ # @harnessed-ts/ember
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Ember driver for `@harnessed-ts/core`. Queries resolve through the shared
4
+ resolver, so they agree with every other driver; interactions go through
5
+ `@ember/test-helpers`, so each one resolves once the app has settled.
6
+
7
+ ```ts
8
+ import { ember } from '@harnessed-ts/ember'
9
+
10
+ // a rendering test
11
+ await render(<template><LoginForm /></template>)
12
+ const form = new LoginFormHarness(ember())
13
+ await form.signInAs('ada@example.com', 'hunter2')
14
+
15
+ // an application test: a page's goto() drives the router
16
+ const checkout = new CheckoutPage(ember())
17
+ await checkout.goto({ token })
18
+ ```
19
+
20
+ Works under ember-qunit, ember-exam, and Vitest browser mode (with
21
+ `ember-vitest`), on classic and Embroider + Vite builds, Ember 5.12 and later.
22
+ Harness files need `@harnessed-ts/core/babel` in the app's Babel config — Ember's
23
+ own decorators are legacy, harnesses are standard.
24
+
25
+ Full guide: the
26
+ [harnessed README](https://github.com/joegaudet/harnessed#using-with-ember).
27
+
28
+ MIT © Joe Gaudet, Jay Seo
@@ -0,0 +1,3 @@
1
+ /** The registry key for this driver. Its own module so env and query share it without a cycle. */
2
+ export declare const EMBER_DRIVER = "ember";
3
+ //# sourceMappingURL=driver-id.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"driver-id.d.ts","sourceRoot":"","sources":["../src/driver-id.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,eAAO,MAAM,YAAY,UAAU,CAAA"}
@@ -0,0 +1,55 @@
1
+ import { Query } from '@harnessed-ts/core';
2
+ import type { BoundingBox, Selector, WaitOptions, WaitState } from '@harnessed-ts/core';
3
+ /**
4
+ * Ember driver. Resolution is the shared resolver, so it agrees with every other
5
+ * in-page driver. Every interaction resolves only once the app has settled —
6
+ * runloop drained, rendering done, test waiters clear.
7
+ *
8
+ * Clicks and fills go through `@ember/test-helpers`, the way an Ember test
9
+ * dispatches them. Keys, hover, focus and selects go through user-event, as in
10
+ * the dom driver: test-helpers' versions dispatch events without a browser's
11
+ * default actions (a pressed key never reaches the value), refuse elements a
12
+ * browser would accept (a disabled control, a non-focusable dialog), and match
13
+ * options by value only.
14
+ *
15
+ * Each helper is handed the resolved element, never a selector string, so
16
+ * strictness and scope stay this library's rather than `querySelector`'s.
17
+ */
18
+ export declare class EmberQuery extends Query {
19
+ private readonly root;
20
+ constructor(root: Element | undefined, scope: readonly Selector[], selector: Selector);
21
+ protected clone(selector: Selector): Query;
22
+ /** Read per call: the root element belongs to the current test, not the env. */
23
+ protected container(): HTMLElement;
24
+ protected element(options?: WaitOptions): Promise<HTMLElement>;
25
+ /** Every match in one pass, each bound to its node. See `DomQuery.all`. */
26
+ all(): Promise<Query[]>;
27
+ click(options?: WaitOptions): Promise<void>;
28
+ fill(value: string, options?: WaitOptions): Promise<void>;
29
+ clear(options?: WaitOptions): Promise<void>;
30
+ check(options?: WaitOptions): Promise<void>;
31
+ uncheck(options?: WaitOptions): Promise<void>;
32
+ selectOption(value: string | string[], options?: WaitOptions): Promise<void>;
33
+ hover(options?: WaitOptions): Promise<void>;
34
+ focus(options?: WaitOptions): Promise<void>;
35
+ blur(options?: WaitOptions): Promise<void>;
36
+ press(key: string, options?: WaitOptions): Promise<void>;
37
+ /**
38
+ * What user-event's upload does — skip a disabled input, drop files `accept`
39
+ * refuses, fire nothing when the selection is unchanged — without user-event:
40
+ * its setup patches the document for good, and an Ember run shares one
41
+ * document, so a number input in a later test read "42.50" back as "42.5".
42
+ */
43
+ upload(files: File | File[], options?: WaitOptions): Promise<void>;
44
+ text(options?: WaitOptions): Promise<string>;
45
+ inputValue(options?: WaitOptions): Promise<string>;
46
+ attribute(name: string, options?: WaitOptions): Promise<string | null>;
47
+ isVisible(options?: WaitOptions): Promise<boolean>;
48
+ isEnabled(options?: WaitOptions): Promise<boolean>;
49
+ isChecked(options?: WaitOptions): Promise<boolean>;
50
+ selectedOptions(options?: WaitOptions): Promise<string[]>;
51
+ boundingBox(options?: WaitOptions): Promise<BoundingBox>;
52
+ waitFor(state: WaitState, options?: WaitOptions): Promise<void>;
53
+ count(): Promise<number>;
54
+ }
55
+ //# sourceMappingURL=ember-query.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ember-query.d.ts","sourceRoot":"","sources":["../src/ember-query.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAkB,MAAM,oBAAoB,CAAA;AAC1D,OAAO,KAAK,EAAE,WAAW,EAAa,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAsFlG;;;;;;;;;;;;;;GAcG;AACH,qBAAa,UAAW,SAAQ,KAAK;IAEjC,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,OAAO,GAAG,SAAS,EAC1C,KAAK,EAAE,SAAS,QAAQ,EAAE,EAC1B,QAAQ,EAAE,QAAQ;cAKD,KAAK,CAAC,QAAQ,EAAE,QAAQ,GAAG,KAAK;IAInD,gFAAgF;IAChF,SAAS,CAAC,SAAS,IAAI,WAAW;IAIlC,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IAI9D,2EAA2E;IAC5D,GAAG,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;IASvB,KAAK,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAI3C,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAKzD,KAAK,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAI3C,KAAK,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAK3C,OAAO,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAK7C,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAc5E,KAAK,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAM3C,KAAK,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAO3C,IAAI,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAK1C,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IASvE;;;;;OAKG;IACY,MAAM,CAAC,KAAK,EAAE,IAAI,GAAG,IAAI,EAAE,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBlE,IAAI,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC;IAI5C,UAAU,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC;IAKlD,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAItE,SAAS,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC;IAclD,SAAS,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC;IAQlD,SAAS,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC;IAQlD,eAAe,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAKzD,WAAW,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IAMxD,OAAO,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB/D,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;CAGxC"}
package/dist/env.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ import type { EnvConfig } from '@harnessed-ts/core';
2
+ import { EMBER_DRIVER } from './driver-id';
3
+ import './ember-query';
4
+ import './navigation';
5
+ export { EMBER_DRIVER };
6
+ export interface EmberEnv extends EnvConfig {
7
+ readonly driver: typeof EMBER_DRIVER;
8
+ /**
9
+ * Where queries start, scoped and `{ global: true }` alike. Defaults to the
10
+ * test's root element (`#ember-testing`), read at query time so one env serves
11
+ * a whole test. Not `document.body`: the test page also holds QUnit's own
12
+ * report, whose test names would make text and role queries ambiguous.
13
+ */
14
+ readonly root: Element | undefined;
15
+ }
16
+ export interface EmberEnvOptions {
17
+ root?: Element;
18
+ }
19
+ /**
20
+ * Builds the env a harness is constructed with in an Ember rendering or
21
+ * application test:
22
+ *
23
+ * ```ts
24
+ * await render(<template><LoginForm /></template>)
25
+ * const form = new LoginFormHarness(ember())
26
+ * ```
27
+ */
28
+ export declare function ember({ root }?: EmberEnvOptions): EmberEnv;
29
+ //# 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,YAAY,EAAE,MAAM,aAAa,CAAA;AAG1C,OAAO,eAAe,CAAA;AACtB,OAAO,cAAc,CAAA;AAErB,OAAO,EAAE,YAAY,EAAE,CAAA;AAEvB,MAAM,WAAW,QAAS,SAAQ,SAAS;IACzC,QAAQ,CAAC,MAAM,EAAE,OAAO,YAAY,CAAA;IACpC;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,SAAS,CAAA;CACnC;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED;;;;;;;;GAQG;AACH,wBAAgB,KAAK,CAAC,EAAE,IAAI,EAAE,GAAE,eAAoB,GAAG,QAAQ,CAE9D"}
package/dist/index.cjs ADDED
@@ -0,0 +1,255 @@
1
+ 'use strict';
2
+
3
+ var testHelpers = require('@ember/test-helpers');
4
+ var core = require('@harnessed-ts/core');
5
+ var resolve = require('@harnessed-ts/resolve');
6
+ var dom = require('@testing-library/dom');
7
+ var userEvent = require('@testing-library/user-event');
8
+
9
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
10
+
11
+ var userEvent__default = /*#__PURE__*/_interopDefault(userEvent);
12
+
13
+ // src/driver-id.ts
14
+ var EMBER_DRIVER = "ember";
15
+ var users = /* @__PURE__ */ new WeakMap();
16
+ function userFor(element) {
17
+ const document = element.ownerDocument;
18
+ let user = users.get(document);
19
+ if (user === void 0) {
20
+ user = userEvent__default.default.setup({ document });
21
+ users.set(document, user);
22
+ }
23
+ return user;
24
+ }
25
+ function toKeyboardInput(key) {
26
+ return key.length === 1 ? key : `{${key}}`;
27
+ }
28
+ function accepts(accept, file) {
29
+ if (accept === "") return true;
30
+ const normalize = (nameOrType) => nameOrType.toLowerCase().replace(/(\.|\/)jpg\b/g, "$1jpeg");
31
+ return normalize(accept).trim().split(/\s*,\s*/).some((token) => {
32
+ if (token.startsWith(".")) return normalize(file.name).endsWith(token);
33
+ if (["audio/*", "image/*", "video/*"].includes(token)) {
34
+ return normalize(file.type).startsWith(token.slice(0, -1));
35
+ }
36
+ return normalize(file.type) === token;
37
+ });
38
+ }
39
+ async function waitUntil(condition, timeout) {
40
+ let refused;
41
+ await dom.waitFor(
42
+ async () => {
43
+ try {
44
+ await condition();
45
+ } catch (error) {
46
+ if (!(error instanceof resolve.FrameEntryError || error instanceof core.StrictModeViolation)) {
47
+ throw error;
48
+ }
49
+ refused = error;
50
+ }
51
+ },
52
+ { timeout }
53
+ );
54
+ if (refused !== void 0) throw refused;
55
+ }
56
+ var EmberQuery = class _EmberQuery extends core.Query {
57
+ constructor(root, scope, selector) {
58
+ super(scope, selector);
59
+ this.root = root;
60
+ }
61
+ root;
62
+ clone(selector) {
63
+ return new _EmberQuery(this.root, this.scope, selector);
64
+ }
65
+ /** Read per call: the root element belongs to the current test, not the env. */
66
+ container() {
67
+ return this.root ?? testHelpers.getRootElement();
68
+ }
69
+ element(options) {
70
+ return resolve.resolveOne(this.container(), this.scope, this.selector, options?.timeout);
71
+ }
72
+ /** Every match in one pass, each bound to its node. See `DomQuery.all`. */
73
+ async all() {
74
+ return (await resolve.resolveAll(this.container(), this.scope, this.selector)).map(
75
+ (element, index) => new BoundEmberQuery(this.root, this.scope, core.nth(this.selector, index), element)
76
+ );
77
+ }
78
+ // --- interactions -------------------------------------------------------
79
+ async click(options) {
80
+ await testHelpers.click(await this.element(options));
81
+ }
82
+ async fill(value, options) {
83
+ await testHelpers.fillIn(await this.element(options), value);
84
+ }
85
+ async clear(options) {
86
+ await testHelpers.fillIn(await this.element(options), "");
87
+ }
88
+ async check(options) {
89
+ const element = await this.element(options);
90
+ if (!element.checked) await testHelpers.click(element);
91
+ }
92
+ async uncheck(options) {
93
+ const element = await this.element(options);
94
+ if (element.checked) await testHelpers.click(element);
95
+ }
96
+ async selectOption(value, options) {
97
+ const element = await this.element(options);
98
+ const user = userFor(element);
99
+ if (element.multiple) {
100
+ const selected = [...element.selectedOptions].map((option) => option.value);
101
+ if (selected.length > 0) await user.deselectOptions(element, selected);
102
+ }
103
+ await user.selectOptions(element, value);
104
+ await testHelpers.settled();
105
+ }
106
+ async hover(options) {
107
+ const element = await this.element(options);
108
+ await userFor(element).hover(element);
109
+ await testHelpers.settled();
110
+ }
111
+ async focus(options) {
112
+ (await this.element(options)).focus();
113
+ await testHelpers.settled();
114
+ }
115
+ async blur(options) {
116
+ (await this.element(options)).blur();
117
+ await testHelpers.settled();
118
+ }
119
+ async press(key, options) {
120
+ const element = await this.element(options);
121
+ element.focus();
122
+ await userFor(element).keyboard(toKeyboardInput(key));
123
+ await testHelpers.settled();
124
+ }
125
+ /**
126
+ * What user-event's upload does — skip a disabled input, drop files `accept`
127
+ * refuses, fire nothing when the selection is unchanged — without user-event:
128
+ * its setup patches the document for good, and an Ember run shares one
129
+ * document, so a number input in a later test read "42.50" back as "42.5".
130
+ */
131
+ async upload(files, options) {
132
+ const list = Array.isArray(files) ? files : [files];
133
+ const input = resolve.fileInputOf(await this.element(options));
134
+ core.checkFileTarget(input, list.length, this.scope, this.selector);
135
+ if (input.matches(":disabled")) return;
136
+ const chosen = list.filter((file) => accepts(input.accept, file));
137
+ const current = [...input.files ?? []];
138
+ if (chosen.length === current.length && chosen.every((file, i) => file === current[i])) return;
139
+ const transfer = new input.ownerDocument.defaultView.DataTransfer();
140
+ for (const file of chosen) transfer.items.add(file);
141
+ input.files = transfer.files;
142
+ input.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
143
+ input.dispatchEvent(new Event("change", { bubbles: true }));
144
+ await testHelpers.settled();
145
+ }
146
+ // --- observations -------------------------------------------------------
147
+ async text(options) {
148
+ return ((await this.element(options)).textContent ?? "").trim();
149
+ }
150
+ async inputValue(options) {
151
+ const element = await this.element(options);
152
+ return element.value ?? "";
153
+ }
154
+ async attribute(name, options) {
155
+ return (await this.element(options)).getAttribute(name);
156
+ }
157
+ async isVisible(options) {
158
+ try {
159
+ return resolve.isVisibleInLayout(await this.element(options), this.container().ownerDocument);
160
+ } catch (error) {
161
+ if (error instanceof resolve.FrameEntryError || error instanceof core.StrictModeViolation) throw error;
162
+ return false;
163
+ }
164
+ }
165
+ async isEnabled(options) {
166
+ const element = await this.element(options);
167
+ const disabled = element.disabled === true || element.closest(":disabled") !== null;
168
+ return core.enabledFrom(disabled, element.getAttribute("aria-disabled"));
169
+ }
170
+ async isChecked(options) {
171
+ const element = await this.element(options);
172
+ return core.checkedFrom(
173
+ element.getAttribute("aria-checked"),
174
+ Boolean(element.checked)
175
+ );
176
+ }
177
+ async selectedOptions(options) {
178
+ const element = await this.element(options);
179
+ return [...element.selectedOptions ?? []].map((option) => option.value);
180
+ }
181
+ async boundingBox(options) {
182
+ return resolve.boundingBoxOf(await this.element(options), this.container().ownerDocument);
183
+ }
184
+ // --- waiting ------------------------------------------------------------
185
+ async waitFor(state, options) {
186
+ const timeout = core.timeoutFor(options?.timeout);
187
+ if (state === "visible") {
188
+ await waitUntil(async () => {
189
+ if (!await this.isVisible({ timeout })) throw new Error("not visible yet");
190
+ }, timeout);
191
+ } else {
192
+ await waitUntil(async () => {
193
+ if (await this.count() !== 0 && await this.isVisible({ timeout })) {
194
+ throw new Error("still visible");
195
+ }
196
+ }, timeout);
197
+ }
198
+ await testHelpers.settled();
199
+ }
200
+ async count() {
201
+ return resolve.countAll(this.container(), this.scope, this.selector);
202
+ }
203
+ };
204
+ var BoundEmberQuery = class extends EmberQuery {
205
+ constructor(root, scope, selector, bound) {
206
+ super(root, scope, selector);
207
+ this.bound = bound;
208
+ }
209
+ bound;
210
+ async element(options) {
211
+ if (this.bound.isConnected) return this.bound;
212
+ return super.element(options);
213
+ }
214
+ };
215
+ core.registerDriver(EMBER_DRIVER, (env, scope, selector) => {
216
+ return new EmberQuery(env.root, scope, selector);
217
+ });
218
+ function absolute() {
219
+ const url = testHelpers.currentURL();
220
+ if (url == null) {
221
+ throw new Error(
222
+ "harnessed: the app has not visited a URL. A rendering test has no router; in an application test, call visit() or a page\u2019s goto() first."
223
+ );
224
+ }
225
+ return new URL(url, window.location.origin).href;
226
+ }
227
+ core.registerNavigation(EMBER_DRIVER, {
228
+ async goto(_env, url) {
229
+ await testHelpers.visit(url);
230
+ },
231
+ currentUrl() {
232
+ return absolute();
233
+ },
234
+ async waitForUrl(_env, matches, timeout) {
235
+ absolute();
236
+ await dom.waitFor(
237
+ () => {
238
+ if (!matches(new URL(absolute()))) throw new Error(`still at ${absolute()}`);
239
+ },
240
+ { timeout }
241
+ );
242
+ await testHelpers.settled();
243
+ }
244
+ });
245
+
246
+ // src/env.ts
247
+ function ember({ root } = {}) {
248
+ return { driver: EMBER_DRIVER, root };
249
+ }
250
+
251
+ exports.EMBER_DRIVER = EMBER_DRIVER;
252
+ exports.EmberQuery = EmberQuery;
253
+ exports.ember = ember;
254
+ //# sourceMappingURL=index.cjs.map
255
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/driver-id.ts","../src/ember-query.ts","../src/navigation.ts","../src/env.ts"],"names":["userEvent","waitForCondition","FrameEntryError","StrictModeViolation","Query","getRootElement","resolveOne","resolveAll","withNth","click","fillIn","settled","fileInputOf","checkFileTarget","isVisibleInLayout","enabledFrom","checkedFrom","boundingBoxOf","timeoutFor","countAll","registerDriver","currentURL","registerNavigation","visit"],"mappings":"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","file":"index.cjs","sourcesContent":["/** The registry key for this driver. Its own module so env and query share it without a cycle. */\nexport const EMBER_DRIVER = 'ember'\n","import { click, fillIn, getRootElement, settled } from '@ember/test-helpers'\nimport { 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 { waitFor as waitForCondition } from '@testing-library/dom'\nimport userEvent from '@testing-library/user-event'\nimport type { UserEvent } from '@testing-library/user-event'\nimport { EMBER_DRIVER } from './driver-id'\nimport type { EmberEnv } from './env'\n\n/**\n * One user-event instance per document, kept so pointer state carries between\n * calls: hovering B after A has to leave A first.\n */\nconst users = new WeakMap<Document, UserEvent>()\n\nfunction userFor(element: Element): UserEvent {\n const document = element.ownerDocument\n let user = users.get(document)\n if (user === undefined) {\n user = userEvent.setup({ document })\n users.set(document, user)\n }\n return user\n}\n\n/** Playwright key names such as `Enter` map onto user-event's `{Enter}` syntax. */\nfunction toKeyboardInput(key: string): string {\n return key.length === 1 ? key : `{${key}}`\n}\n\n/** user-event's `accept` rule: case-insensitive, jpg and jpeg alike, `image/*` wildcards. */\nfunction accepts(accept: string, file: File): boolean {\n if (accept === '') return true\n const normalize = (nameOrType: string) =>\n nameOrType.toLowerCase().replace(/(\\.|\\/)jpg\\b/g, '$1jpeg')\n return normalize(accept)\n .trim()\n .split(/\\s*,\\s*/)\n .some(token => {\n if (token.startsWith('.')) return normalize(file.name).endsWith(token)\n if (['audio/*', 'image/*', 'video/*'].includes(token)) {\n return normalize(file.type).startsWith(token.slice(0, -1))\n }\n return normalize(file.type) === token\n })\n}\n\n/**\n * Testing Library's `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. Not test-helpers' `waitUntil`: its callback is\n * synchronous, and every check here resolves through the async resolver.\n */\nasync function waitUntil(condition: () => Promise<void>, timeout: number): Promise<void> {\n let refused: FrameEntryError | StrictModeViolation | undefined\n await waitForCondition(\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 * Ember driver. Resolution is the shared resolver, so it agrees with every other\n * in-page driver. Every interaction resolves only once the app has settled —\n * runloop drained, rendering done, test waiters clear.\n *\n * Clicks and fills go through `@ember/test-helpers`, the way an Ember test\n * dispatches them. Keys, hover, focus and selects go through user-event, as in\n * the dom driver: test-helpers' versions dispatch events without a browser's\n * default actions (a pressed key never reaches the value), refuse elements a\n * browser would accept (a disabled control, a non-focusable dialog), and match\n * options by value only.\n *\n * Each helper is handed the resolved element, never a selector string, so\n * strictness and scope stay this library's rather than `querySelector`'s.\n */\nexport class EmberQuery extends Query {\n constructor(\n private readonly root: Element | undefined,\n scope: readonly Selector[],\n selector: Selector,\n ) {\n super(scope, selector)\n }\n\n protected override clone(selector: Selector): Query {\n return new EmberQuery(this.root, this.scope, selector)\n }\n\n /** Read per call: the root element belongs to the current test, not the env. */\n protected container(): HTMLElement {\n return (this.root ?? getRootElement()) as HTMLElement\n }\n\n protected element(options?: WaitOptions): Promise<HTMLElement> {\n return resolveOne(this.container(), this.scope, this.selector, options?.timeout)\n }\n\n /** Every match in one pass, each bound to its node. See `DomQuery.all`. */\n override async all(): Promise<Query[]> {\n return (await resolveAll(this.container(), this.scope, this.selector)).map(\n (element, index) =>\n new BoundEmberQuery(this.root, this.scope, withNth(this.selector, index), element),\n )\n }\n\n // --- interactions -------------------------------------------------------\n\n override async click(options?: WaitOptions): Promise<void> {\n await click(await this.element(options))\n }\n\n override async fill(value: string, options?: WaitOptions): Promise<void> {\n // fillIn replaces the value and fires input and change: Playwright's fill.\n await fillIn(await this.element(options), value)\n }\n\n override async clear(options?: WaitOptions): Promise<void> {\n await fillIn(await this.element(options), '')\n }\n\n override async check(options?: WaitOptions): Promise<void> {\n const element = await this.element(options)\n if (!(element as HTMLInputElement).checked) await click(element)\n }\n\n override async uncheck(options?: WaitOptions): Promise<void> {\n const element = await this.element(options)\n if ((element as HTMLInputElement).checked) await click(element)\n }\n\n override async selectOption(value: string | string[], options?: WaitOptions): Promise<void> {\n const element = (await this.element(options)) as HTMLSelectElement\n const user = userFor(element)\n // user-event adds to a multi-select's existing selection; the shared API\n // replaces it, so clear first — as the dom driver does. user-event matches\n // a value or a label, and rejects one that matches no option.\n if (element.multiple) {\n const selected = [...element.selectedOptions].map(option => option.value)\n if (selected.length > 0) await user.deselectOptions(element, selected)\n }\n await user.selectOptions(element, value)\n await settled()\n }\n\n override async hover(options?: WaitOptions): Promise<void> {\n const element = await this.element(options)\n await userFor(element).hover(element)\n await settled()\n }\n\n override async focus(options?: WaitOptions): Promise<void> {\n // The DOM method: a no-op on an element that cannot take focus, where\n // test-helpers' focus() throws.\n ;(await this.element(options)).focus()\n await settled()\n }\n\n override async blur(options?: WaitOptions): Promise<void> {\n ;(await this.element(options)).blur()\n await settled()\n }\n\n override async press(key: string, options?: WaitOptions): Promise<void> {\n const element = await this.element(options)\n // Focus, then press: the key goes to the focused element, as in a browser.\n // On an element that cannot take focus, that is whatever already had it.\n element.focus()\n await userFor(element).keyboard(toKeyboardInput(key))\n await settled()\n }\n\n /**\n * What user-event's upload does — skip a disabled input, drop files `accept`\n * refuses, fire nothing when the selection is unchanged — without user-event:\n * its setup patches the document for good, and an Ember run shares one\n * document, so a number input in a later test read \"42.50\" back as \"42.5\".\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 if (input.matches(':disabled')) return\n const chosen = list.filter(file => accepts(input.accept, file))\n const current = [...(input.files ?? [])]\n if (chosen.length === current.length && chosen.every((file, i) => file === current[i])) return\n // The input's own window: a DataTransfer from another realm is refused.\n const transfer = new input.ownerDocument.defaultView!.DataTransfer()\n for (const file of chosen) transfer.items.add(file)\n input.files = transfer.files\n input.dispatchEvent(new Event('input', { bubbles: true, composed: true }))\n input.dispatchEvent(new Event('change', { bubbles: true }))\n await settled()\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, so a real layout check — Playwright's rule, not jsdom's\n // 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.\n return false\n }\n }\n\n override async isEnabled(options?: WaitOptions): Promise<boolean> {\n const element = await this.element(options)\n // The property, and an ancestor `fieldset[disabled]`: see `DomQuery.isEnabled`.\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 } else {\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 // Whatever made it appear or go may have scheduled more work; hand back a\n // settled app, as every interaction does.\n await settled()\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 `BoundDomQuery`. */\nclass BoundEmberQuery extends EmberQuery {\n constructor(\n root: Element | undefined,\n scope: readonly Selector[],\n selector: Selector,\n private readonly bound: HTMLElement,\n ) {\n super(root, scope, selector)\n }\n\n protected override async element(options?: WaitOptions): Promise<HTMLElement> {\n if (this.bound.isConnected) return this.bound\n return super.element(options)\n }\n}\n\nregisterDriver(EMBER_DRIVER, (env: EnvConfig, scope, selector) => {\n return new EmberQuery((env as EmberEnv).root, scope, selector)\n})\n","import { currentURL, settled, visit } from '@ember/test-helpers'\nimport { registerNavigation } from '@harnessed-ts/core'\nimport { waitFor as waitForCondition } from '@testing-library/dom'\nimport { EMBER_DRIVER } from './driver-id'\n\n/**\n * The router's URL, made absolute. An application test drives the router, not\n * the address bar — its location is `none` — so `currentURL()` is the truth and\n * `window.location` is not. Absolute because a page reads `pathname` and\n * `searchParams` off it with `new URL()`.\n *\n * There is not always a router URL: a rendering test has no router, and an\n * application test has none until its first visit. Reading that as `/` would\n * let `assertPathname('/')` pass before anything navigated, so it refuses.\n */\nfunction absolute(): string {\n const url = currentURL()\n if (url == null) {\n throw new Error(\n 'harnessed: the app has not visited a URL. A rendering test has no router; ' +\n 'in an application test, call visit() or a page’s goto() first.',\n )\n }\n return new URL(url, window.location.origin).href\n}\n\n/** Navigation through the router, in application tests. */\nregisterNavigation(EMBER_DRIVER, {\n async goto(_env, url) {\n // visit() resolves once the transition, and everything it scheduled, settles.\n await visit(url)\n },\n currentUrl() {\n return absolute()\n },\n async waitForUrl(_env, matches, timeout) {\n // Outside the retry loop: no router is a refusal, not something to wait out.\n absolute()\n await waitForCondition(\n () => {\n if (!matches(new URL(absolute()))) throw new Error(`still at ${absolute()}`)\n },\n { timeout },\n )\n // The URL changes before the destination has rendered.\n await settled()\n },\n})\n","import type { EnvConfig } from '@harnessed-ts/core'\nimport { EMBER_DRIVER } from './driver-id'\n// Imported for their side effects: constructing an env is the moment the driver\n// has to be in the registry, and a bare re-export could be tree-shaken away.\nimport './ember-query'\nimport './navigation'\n\nexport { EMBER_DRIVER }\n\nexport interface EmberEnv extends EnvConfig {\n readonly driver: typeof EMBER_DRIVER\n /**\n * Where queries start, scoped and `{ global: true }` alike. Defaults to the\n * test's root element (`#ember-testing`), read at query time so one env serves\n * a whole test. Not `document.body`: the test page also holds QUnit's own\n * report, whose test names would make text and role queries ambiguous.\n */\n readonly root: Element | undefined\n}\n\nexport interface EmberEnvOptions {\n root?: Element\n}\n\n/**\n * Builds the env a harness is constructed with in an Ember rendering or\n * application test:\n *\n * ```ts\n * await render(<template><LoginForm /></template>)\n * const form = new LoginFormHarness(ember())\n * ```\n */\nexport function ember({ root }: EmberEnvOptions = {}): EmberEnv {\n return { driver: EMBER_DRIVER, root }\n}\n"]}
@@ -0,0 +1,4 @@
1
+ export { ember, EMBER_DRIVER } from './env';
2
+ export type { EmberEnv, EmberEnvOptions } from './env';
3
+ export { EmberQuery } from './ember-query';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAA;AAC3C,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,OAAO,CAAA;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,247 @@
1
+ import { settled, visit, getRootElement, click, fillIn, currentURL } from '@ember/test-helpers';
2
+ import { registerDriver, registerNavigation, Query, nth, checkFileTarget, StrictModeViolation, enabledFrom, checkedFrom, timeoutFor } from '@harnessed-ts/core';
3
+ import { resolveOne, resolveAll, fileInputOf, isVisibleInLayout, FrameEntryError, boundingBoxOf, countAll } from '@harnessed-ts/resolve';
4
+ import { waitFor } from '@testing-library/dom';
5
+ import userEvent from '@testing-library/user-event';
6
+
7
+ // src/driver-id.ts
8
+ var EMBER_DRIVER = "ember";
9
+ var users = /* @__PURE__ */ new WeakMap();
10
+ function userFor(element) {
11
+ const document = element.ownerDocument;
12
+ let user = users.get(document);
13
+ if (user === void 0) {
14
+ user = userEvent.setup({ document });
15
+ users.set(document, user);
16
+ }
17
+ return user;
18
+ }
19
+ function toKeyboardInput(key) {
20
+ return key.length === 1 ? key : `{${key}}`;
21
+ }
22
+ function accepts(accept, file) {
23
+ if (accept === "") return true;
24
+ const normalize = (nameOrType) => nameOrType.toLowerCase().replace(/(\.|\/)jpg\b/g, "$1jpeg");
25
+ return normalize(accept).trim().split(/\s*,\s*/).some((token) => {
26
+ if (token.startsWith(".")) return normalize(file.name).endsWith(token);
27
+ if (["audio/*", "image/*", "video/*"].includes(token)) {
28
+ return normalize(file.type).startsWith(token.slice(0, -1));
29
+ }
30
+ return normalize(file.type) === token;
31
+ });
32
+ }
33
+ async function waitUntil(condition, timeout) {
34
+ let refused;
35
+ await waitFor(
36
+ async () => {
37
+ try {
38
+ await condition();
39
+ } catch (error) {
40
+ if (!(error instanceof FrameEntryError || error instanceof StrictModeViolation)) {
41
+ throw error;
42
+ }
43
+ refused = error;
44
+ }
45
+ },
46
+ { timeout }
47
+ );
48
+ if (refused !== void 0) throw refused;
49
+ }
50
+ var EmberQuery = class _EmberQuery extends Query {
51
+ constructor(root, scope, selector) {
52
+ super(scope, selector);
53
+ this.root = root;
54
+ }
55
+ root;
56
+ clone(selector) {
57
+ return new _EmberQuery(this.root, this.scope, selector);
58
+ }
59
+ /** Read per call: the root element belongs to the current test, not the env. */
60
+ container() {
61
+ return this.root ?? getRootElement();
62
+ }
63
+ element(options) {
64
+ return resolveOne(this.container(), this.scope, this.selector, options?.timeout);
65
+ }
66
+ /** Every match in one pass, each bound to its node. See `DomQuery.all`. */
67
+ async all() {
68
+ return (await resolveAll(this.container(), this.scope, this.selector)).map(
69
+ (element, index) => new BoundEmberQuery(this.root, this.scope, nth(this.selector, index), element)
70
+ );
71
+ }
72
+ // --- interactions -------------------------------------------------------
73
+ async click(options) {
74
+ await click(await this.element(options));
75
+ }
76
+ async fill(value, options) {
77
+ await fillIn(await this.element(options), value);
78
+ }
79
+ async clear(options) {
80
+ await fillIn(await this.element(options), "");
81
+ }
82
+ async check(options) {
83
+ const element = await this.element(options);
84
+ if (!element.checked) await click(element);
85
+ }
86
+ async uncheck(options) {
87
+ const element = await this.element(options);
88
+ if (element.checked) await click(element);
89
+ }
90
+ async selectOption(value, options) {
91
+ const element = await this.element(options);
92
+ const user = userFor(element);
93
+ if (element.multiple) {
94
+ const selected = [...element.selectedOptions].map((option) => option.value);
95
+ if (selected.length > 0) await user.deselectOptions(element, selected);
96
+ }
97
+ await user.selectOptions(element, value);
98
+ await settled();
99
+ }
100
+ async hover(options) {
101
+ const element = await this.element(options);
102
+ await userFor(element).hover(element);
103
+ await settled();
104
+ }
105
+ async focus(options) {
106
+ (await this.element(options)).focus();
107
+ await settled();
108
+ }
109
+ async blur(options) {
110
+ (await this.element(options)).blur();
111
+ await settled();
112
+ }
113
+ async press(key, options) {
114
+ const element = await this.element(options);
115
+ element.focus();
116
+ await userFor(element).keyboard(toKeyboardInput(key));
117
+ await settled();
118
+ }
119
+ /**
120
+ * What user-event's upload does — skip a disabled input, drop files `accept`
121
+ * refuses, fire nothing when the selection is unchanged — without user-event:
122
+ * its setup patches the document for good, and an Ember run shares one
123
+ * document, so a number input in a later test read "42.50" back as "42.5".
124
+ */
125
+ async upload(files, options) {
126
+ const list = Array.isArray(files) ? files : [files];
127
+ const input = fileInputOf(await this.element(options));
128
+ checkFileTarget(input, list.length, this.scope, this.selector);
129
+ if (input.matches(":disabled")) return;
130
+ const chosen = list.filter((file) => accepts(input.accept, file));
131
+ const current = [...input.files ?? []];
132
+ if (chosen.length === current.length && chosen.every((file, i) => file === current[i])) return;
133
+ const transfer = new input.ownerDocument.defaultView.DataTransfer();
134
+ for (const file of chosen) transfer.items.add(file);
135
+ input.files = transfer.files;
136
+ input.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
137
+ input.dispatchEvent(new Event("change", { bubbles: true }));
138
+ await settled();
139
+ }
140
+ // --- observations -------------------------------------------------------
141
+ async text(options) {
142
+ return ((await this.element(options)).textContent ?? "").trim();
143
+ }
144
+ async inputValue(options) {
145
+ const element = await this.element(options);
146
+ return element.value ?? "";
147
+ }
148
+ async attribute(name, options) {
149
+ return (await this.element(options)).getAttribute(name);
150
+ }
151
+ async isVisible(options) {
152
+ try {
153
+ return isVisibleInLayout(await this.element(options), this.container().ownerDocument);
154
+ } catch (error) {
155
+ if (error instanceof FrameEntryError || error instanceof StrictModeViolation) throw error;
156
+ return false;
157
+ }
158
+ }
159
+ async isEnabled(options) {
160
+ const element = await this.element(options);
161
+ const disabled = element.disabled === true || element.closest(":disabled") !== null;
162
+ return enabledFrom(disabled, element.getAttribute("aria-disabled"));
163
+ }
164
+ async isChecked(options) {
165
+ const element = await this.element(options);
166
+ return checkedFrom(
167
+ element.getAttribute("aria-checked"),
168
+ Boolean(element.checked)
169
+ );
170
+ }
171
+ async selectedOptions(options) {
172
+ const element = await this.element(options);
173
+ return [...element.selectedOptions ?? []].map((option) => option.value);
174
+ }
175
+ async boundingBox(options) {
176
+ return boundingBoxOf(await this.element(options), this.container().ownerDocument);
177
+ }
178
+ // --- waiting ------------------------------------------------------------
179
+ async waitFor(state, options) {
180
+ const timeout = timeoutFor(options?.timeout);
181
+ if (state === "visible") {
182
+ await waitUntil(async () => {
183
+ if (!await this.isVisible({ timeout })) throw new Error("not visible yet");
184
+ }, timeout);
185
+ } else {
186
+ await waitUntil(async () => {
187
+ if (await this.count() !== 0 && await this.isVisible({ timeout })) {
188
+ throw new Error("still visible");
189
+ }
190
+ }, timeout);
191
+ }
192
+ await settled();
193
+ }
194
+ async count() {
195
+ return countAll(this.container(), this.scope, this.selector);
196
+ }
197
+ };
198
+ var BoundEmberQuery = class extends EmberQuery {
199
+ constructor(root, scope, selector, bound) {
200
+ super(root, scope, selector);
201
+ this.bound = bound;
202
+ }
203
+ bound;
204
+ async element(options) {
205
+ if (this.bound.isConnected) return this.bound;
206
+ return super.element(options);
207
+ }
208
+ };
209
+ registerDriver(EMBER_DRIVER, (env, scope, selector) => {
210
+ return new EmberQuery(env.root, scope, selector);
211
+ });
212
+ function absolute() {
213
+ const url = currentURL();
214
+ if (url == null) {
215
+ throw new Error(
216
+ "harnessed: the app has not visited a URL. A rendering test has no router; in an application test, call visit() or a page\u2019s goto() first."
217
+ );
218
+ }
219
+ return new URL(url, window.location.origin).href;
220
+ }
221
+ registerNavigation(EMBER_DRIVER, {
222
+ async goto(_env, url) {
223
+ await visit(url);
224
+ },
225
+ currentUrl() {
226
+ return absolute();
227
+ },
228
+ async waitForUrl(_env, matches, timeout) {
229
+ absolute();
230
+ await waitFor(
231
+ () => {
232
+ if (!matches(new URL(absolute()))) throw new Error(`still at ${absolute()}`);
233
+ },
234
+ { timeout }
235
+ );
236
+ await settled();
237
+ }
238
+ });
239
+
240
+ // src/env.ts
241
+ function ember({ root } = {}) {
242
+ return { driver: EMBER_DRIVER, root };
243
+ }
244
+
245
+ export { EMBER_DRIVER, EmberQuery, ember };
246
+ //# sourceMappingURL=index.js.map
247
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/driver-id.ts","../src/ember-query.ts","../src/navigation.ts","../src/env.ts"],"names":["waitForCondition","withNth","settled"],"mappings":"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","file":"index.js","sourcesContent":["/** The registry key for this driver. Its own module so env and query share it without a cycle. */\nexport const EMBER_DRIVER = 'ember'\n","import { click, fillIn, getRootElement, settled } from '@ember/test-helpers'\nimport { 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 { waitFor as waitForCondition } from '@testing-library/dom'\nimport userEvent from '@testing-library/user-event'\nimport type { UserEvent } from '@testing-library/user-event'\nimport { EMBER_DRIVER } from './driver-id'\nimport type { EmberEnv } from './env'\n\n/**\n * One user-event instance per document, kept so pointer state carries between\n * calls: hovering B after A has to leave A first.\n */\nconst users = new WeakMap<Document, UserEvent>()\n\nfunction userFor(element: Element): UserEvent {\n const document = element.ownerDocument\n let user = users.get(document)\n if (user === undefined) {\n user = userEvent.setup({ document })\n users.set(document, user)\n }\n return user\n}\n\n/** Playwright key names such as `Enter` map onto user-event's `{Enter}` syntax. */\nfunction toKeyboardInput(key: string): string {\n return key.length === 1 ? key : `{${key}}`\n}\n\n/** user-event's `accept` rule: case-insensitive, jpg and jpeg alike, `image/*` wildcards. */\nfunction accepts(accept: string, file: File): boolean {\n if (accept === '') return true\n const normalize = (nameOrType: string) =>\n nameOrType.toLowerCase().replace(/(\\.|\\/)jpg\\b/g, '$1jpeg')\n return normalize(accept)\n .trim()\n .split(/\\s*,\\s*/)\n .some(token => {\n if (token.startsWith('.')) return normalize(file.name).endsWith(token)\n if (['audio/*', 'image/*', 'video/*'].includes(token)) {\n return normalize(file.type).startsWith(token.slice(0, -1))\n }\n return normalize(file.type) === token\n })\n}\n\n/**\n * Testing Library's `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. Not test-helpers' `waitUntil`: its callback is\n * synchronous, and every check here resolves through the async resolver.\n */\nasync function waitUntil(condition: () => Promise<void>, timeout: number): Promise<void> {\n let refused: FrameEntryError | StrictModeViolation | undefined\n await waitForCondition(\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 * Ember driver. Resolution is the shared resolver, so it agrees with every other\n * in-page driver. Every interaction resolves only once the app has settled —\n * runloop drained, rendering done, test waiters clear.\n *\n * Clicks and fills go through `@ember/test-helpers`, the way an Ember test\n * dispatches them. Keys, hover, focus and selects go through user-event, as in\n * the dom driver: test-helpers' versions dispatch events without a browser's\n * default actions (a pressed key never reaches the value), refuse elements a\n * browser would accept (a disabled control, a non-focusable dialog), and match\n * options by value only.\n *\n * Each helper is handed the resolved element, never a selector string, so\n * strictness and scope stay this library's rather than `querySelector`'s.\n */\nexport class EmberQuery extends Query {\n constructor(\n private readonly root: Element | undefined,\n scope: readonly Selector[],\n selector: Selector,\n ) {\n super(scope, selector)\n }\n\n protected override clone(selector: Selector): Query {\n return new EmberQuery(this.root, this.scope, selector)\n }\n\n /** Read per call: the root element belongs to the current test, not the env. */\n protected container(): HTMLElement {\n return (this.root ?? getRootElement()) as HTMLElement\n }\n\n protected element(options?: WaitOptions): Promise<HTMLElement> {\n return resolveOne(this.container(), this.scope, this.selector, options?.timeout)\n }\n\n /** Every match in one pass, each bound to its node. See `DomQuery.all`. */\n override async all(): Promise<Query[]> {\n return (await resolveAll(this.container(), this.scope, this.selector)).map(\n (element, index) =>\n new BoundEmberQuery(this.root, this.scope, withNth(this.selector, index), element),\n )\n }\n\n // --- interactions -------------------------------------------------------\n\n override async click(options?: WaitOptions): Promise<void> {\n await click(await this.element(options))\n }\n\n override async fill(value: string, options?: WaitOptions): Promise<void> {\n // fillIn replaces the value and fires input and change: Playwright's fill.\n await fillIn(await this.element(options), value)\n }\n\n override async clear(options?: WaitOptions): Promise<void> {\n await fillIn(await this.element(options), '')\n }\n\n override async check(options?: WaitOptions): Promise<void> {\n const element = await this.element(options)\n if (!(element as HTMLInputElement).checked) await click(element)\n }\n\n override async uncheck(options?: WaitOptions): Promise<void> {\n const element = await this.element(options)\n if ((element as HTMLInputElement).checked) await click(element)\n }\n\n override async selectOption(value: string | string[], options?: WaitOptions): Promise<void> {\n const element = (await this.element(options)) as HTMLSelectElement\n const user = userFor(element)\n // user-event adds to a multi-select's existing selection; the shared API\n // replaces it, so clear first — as the dom driver does. user-event matches\n // a value or a label, and rejects one that matches no option.\n if (element.multiple) {\n const selected = [...element.selectedOptions].map(option => option.value)\n if (selected.length > 0) await user.deselectOptions(element, selected)\n }\n await user.selectOptions(element, value)\n await settled()\n }\n\n override async hover(options?: WaitOptions): Promise<void> {\n const element = await this.element(options)\n await userFor(element).hover(element)\n await settled()\n }\n\n override async focus(options?: WaitOptions): Promise<void> {\n // The DOM method: a no-op on an element that cannot take focus, where\n // test-helpers' focus() throws.\n ;(await this.element(options)).focus()\n await settled()\n }\n\n override async blur(options?: WaitOptions): Promise<void> {\n ;(await this.element(options)).blur()\n await settled()\n }\n\n override async press(key: string, options?: WaitOptions): Promise<void> {\n const element = await this.element(options)\n // Focus, then press: the key goes to the focused element, as in a browser.\n // On an element that cannot take focus, that is whatever already had it.\n element.focus()\n await userFor(element).keyboard(toKeyboardInput(key))\n await settled()\n }\n\n /**\n * What user-event's upload does — skip a disabled input, drop files `accept`\n * refuses, fire nothing when the selection is unchanged — without user-event:\n * its setup patches the document for good, and an Ember run shares one\n * document, so a number input in a later test read \"42.50\" back as \"42.5\".\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 if (input.matches(':disabled')) return\n const chosen = list.filter(file => accepts(input.accept, file))\n const current = [...(input.files ?? [])]\n if (chosen.length === current.length && chosen.every((file, i) => file === current[i])) return\n // The input's own window: a DataTransfer from another realm is refused.\n const transfer = new input.ownerDocument.defaultView!.DataTransfer()\n for (const file of chosen) transfer.items.add(file)\n input.files = transfer.files\n input.dispatchEvent(new Event('input', { bubbles: true, composed: true }))\n input.dispatchEvent(new Event('change', { bubbles: true }))\n await settled()\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, so a real layout check — Playwright's rule, not jsdom's\n // 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.\n return false\n }\n }\n\n override async isEnabled(options?: WaitOptions): Promise<boolean> {\n const element = await this.element(options)\n // The property, and an ancestor `fieldset[disabled]`: see `DomQuery.isEnabled`.\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 } else {\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 // Whatever made it appear or go may have scheduled more work; hand back a\n // settled app, as every interaction does.\n await settled()\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 `BoundDomQuery`. */\nclass BoundEmberQuery extends EmberQuery {\n constructor(\n root: Element | undefined,\n scope: readonly Selector[],\n selector: Selector,\n private readonly bound: HTMLElement,\n ) {\n super(root, scope, selector)\n }\n\n protected override async element(options?: WaitOptions): Promise<HTMLElement> {\n if (this.bound.isConnected) return this.bound\n return super.element(options)\n }\n}\n\nregisterDriver(EMBER_DRIVER, (env: EnvConfig, scope, selector) => {\n return new EmberQuery((env as EmberEnv).root, scope, selector)\n})\n","import { currentURL, settled, visit } from '@ember/test-helpers'\nimport { registerNavigation } from '@harnessed-ts/core'\nimport { waitFor as waitForCondition } from '@testing-library/dom'\nimport { EMBER_DRIVER } from './driver-id'\n\n/**\n * The router's URL, made absolute. An application test drives the router, not\n * the address bar — its location is `none` — so `currentURL()` is the truth and\n * `window.location` is not. Absolute because a page reads `pathname` and\n * `searchParams` off it with `new URL()`.\n *\n * There is not always a router URL: a rendering test has no router, and an\n * application test has none until its first visit. Reading that as `/` would\n * let `assertPathname('/')` pass before anything navigated, so it refuses.\n */\nfunction absolute(): string {\n const url = currentURL()\n if (url == null) {\n throw new Error(\n 'harnessed: the app has not visited a URL. A rendering test has no router; ' +\n 'in an application test, call visit() or a page’s goto() first.',\n )\n }\n return new URL(url, window.location.origin).href\n}\n\n/** Navigation through the router, in application tests. */\nregisterNavigation(EMBER_DRIVER, {\n async goto(_env, url) {\n // visit() resolves once the transition, and everything it scheduled, settles.\n await visit(url)\n },\n currentUrl() {\n return absolute()\n },\n async waitForUrl(_env, matches, timeout) {\n // Outside the retry loop: no router is a refusal, not something to wait out.\n absolute()\n await waitForCondition(\n () => {\n if (!matches(new URL(absolute()))) throw new Error(`still at ${absolute()}`)\n },\n { timeout },\n )\n // The URL changes before the destination has rendered.\n await settled()\n },\n})\n","import type { EnvConfig } from '@harnessed-ts/core'\nimport { EMBER_DRIVER } from './driver-id'\n// Imported for their side effects: constructing an env is the moment the driver\n// has to be in the registry, and a bare re-export could be tree-shaken away.\nimport './ember-query'\nimport './navigation'\n\nexport { EMBER_DRIVER }\n\nexport interface EmberEnv extends EnvConfig {\n readonly driver: typeof EMBER_DRIVER\n /**\n * Where queries start, scoped and `{ global: true }` alike. Defaults to the\n * test's root element (`#ember-testing`), read at query time so one env serves\n * a whole test. Not `document.body`: the test page also holds QUnit's own\n * report, whose test names would make text and role queries ambiguous.\n */\n readonly root: Element | undefined\n}\n\nexport interface EmberEnvOptions {\n root?: Element\n}\n\n/**\n * Builds the env a harness is constructed with in an Ember rendering or\n * application test:\n *\n * ```ts\n * await render(<template><LoginForm /></template>)\n * const form = new LoginFormHarness(ember())\n * ```\n */\nexport function ember({ root }: EmberEnvOptions = {}): EmberEnv {\n return { driver: EMBER_DRIVER, root }\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=navigation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../src/navigation.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,72 @@
1
1
  {
2
2
  "name": "@harnessed-ts/ember",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.7.0",
4
+ "description": "Ember driver for @harnessed-ts/core: interactions through @ember/test-helpers, settled after every one. Rendering and application tests.",
5
+ "keywords": [
6
+ "testing",
7
+ "harness",
8
+ "ember",
9
+ "ember-qunit",
10
+ "test-helpers"
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/ember"
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
+ "./package.json": "./package.json"
41
+ },
42
+ "files": [
43
+ "dist",
44
+ "CHANGELOG.md",
45
+ "README.md",
46
+ "LICENSE"
47
+ ],
48
+ "engines": {
49
+ "node": ">=22.12.0"
50
+ },
51
+ "dependencies": {
52
+ "@harnessed-ts/core": "^0.7.0",
53
+ "@harnessed-ts/resolve": "^0.7.0"
54
+ },
55
+ "peerDependencies": {
56
+ "@ember/test-helpers": ">=5",
57
+ "@testing-library/dom": ">=10",
58
+ "@testing-library/user-event": ">=14"
59
+ },
60
+ "publishConfig": {
61
+ "access": "public"
62
+ },
63
+ "devDependencies": {
64
+ "@ember/test-helpers": "^5.4.3",
65
+ "@testing-library/dom": "^10.4.2",
66
+ "@testing-library/user-event": "^14.6.7"
67
+ },
68
+ "scripts": {
69
+ "build": "tsup",
70
+ "clean": "rm -rf dist *.tsbuildinfo"
71
+ }
6
72
  }