@harnessed-ts/angular 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 +27 -0
- package/LICENSE +21 -0
- package/README.md +137 -2
- package/dist/angular-query.d.ts +48 -0
- package/dist/angular-query.d.ts.map +1 -0
- package/dist/driver-id.d.ts +3 -0
- package/dist/driver-id.d.ts.map +1 -0
- package/dist/env.d.ts +56 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/index.cjs +291 -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 +283 -0
- package/dist/index.js.map +1 -0
- package/dist/navigation.d.ts +2 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/stable.d.ts +12 -0
- package/dist/stable.d.ts.map +1 -0
- package/package.json +75 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# @harnessed-ts/angular
|
|
2
|
+
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- ec06bdf: Vue, Solid, Svelte and Angular join React and Ember as conformance-tested UI frameworks, and node:test, Bun and Nightwatch join the runners.
|
|
8
|
+
|
|
9
|
+
New packages: `@harnessed-ts/nightwatch` drives Nightwatch 3 through the injected resolver, under Nightwatch's own runner and its Mocha runner, in end-to-end and component tests (`@nightwatch/react`, `@nightwatch/vue`), with cross-origin frames. `@harnessed-ts/angular` drives zoneless Angular 21+: every action settles at `fixture.whenStable()`, or at the `ApplicationRef`'s for an app bootstrapped without TestBed, and `goto()` drives a provided router.
|
|
10
|
+
|
|
11
|
+
`@harnessed-ts/core/register` loads TypeScript harnesses under `node --import`: esbuild strips the types, lowers standard decorators and `accessor`, and compiles JSX. `@harnessed-ts/core/bun` does the same as a `bun test` preload, so harnesses keep the standard decorator protocol whatever `experimentalDecorators` the tsconfig sets.
|
|
12
|
+
|
|
13
|
+
`@harnessed-ts/dom/matchers/bun` registers the matchers with `bun:test`. The dom driver runs on happy-dom too: it focuses the frames leading to an interaction's target, finds a frame's `<iframe>` where `frameElement` is unset, and reads a select's selection off `options`.
|
|
14
|
+
|
|
15
|
+
`@harnessed-ts/resolve/webdriver` holds the WebDriver half of injected resolution, shared by the WebdriverIO and Nightwatch drivers. `@harnessed-ts/gherkin/nightwatch` adapts the cucumber-js world to Nightwatch's Cucumber runner.
|
|
16
|
+
|
|
17
|
+
`@harnessed-ts/playwright`, and `@harnessed-ts/cypress` under `realEvents`, give the page a task after every action, so a read straight after it sees what a framework that renders on a timer (zoneless Angular) has rendered. The injectable resolver installs on the page's `window` rather than the script's global, which under Firefox's WebDriver is a sandbox in front of it.
|
|
18
|
+
|
|
19
|
+
`@harnessed-ts/conformance` exports `fixtureParity()`, the per-view parity check a port of the fixture to another framework runs, naming the first place its markup parts from the React original; it detects a simulated DOM from the DOM's own window.
|
|
20
|
+
|
|
21
|
+
`@harnessed-ts/eslint-plugin` recognises Nightwatch's, Angular's and Vue Test Utils' raw queries, and looks in `nightwatch/` by default. `npx @harnessed-ts/claude install` detects Angular, Nightwatch, node:test and Bun.
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- Updated dependencies [ec06bdf]
|
|
26
|
+
- @harnessed-ts/core@0.7.0
|
|
27
|
+
- @harnessed-ts/resolve@0.7.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,138 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @harnessed-ts/angular
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Angular driver for `@harnessed-ts/core`. Queries resolve through the shared
|
|
4
|
+
resolver, so they agree with every other driver; interactions go through
|
|
5
|
+
user-event, as in the Testing Library driver, and each one returns once the app
|
|
6
|
+
is stable.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { angular } from '@harnessed-ts/angular'
|
|
10
|
+
import { render } from '@testing-library/angular/zoneless'
|
|
11
|
+
|
|
12
|
+
// a component test, through TestBed or @testing-library/angular
|
|
13
|
+
const { fixture } = await render(LoginForm)
|
|
14
|
+
const form = new LoginFormHarness(angular({ fixture }))
|
|
15
|
+
await form.signInAs('ada@example.com', 'hunter2')
|
|
16
|
+
|
|
17
|
+
// with the router provided, a page's goto() drives it
|
|
18
|
+
const checkout = new CheckoutPage(angular({ fixture }))
|
|
19
|
+
await checkout.goto({ token })
|
|
20
|
+
|
|
21
|
+
// an app bootstrapped without TestBed
|
|
22
|
+
const app = await bootstrapApplication(App, appConfig)
|
|
23
|
+
const page = new CheckoutPage(angular({ app }))
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Zoneless, standalone components, Angular 21 and later. Runs under Vitest with
|
|
27
|
+
jsdom or in browser mode, and anywhere else TestBed does.
|
|
28
|
+
|
|
29
|
+
## The env
|
|
30
|
+
|
|
31
|
+
| Option | What it is |
|
|
32
|
+
| --------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
33
|
+
| `fixture` | A `ComponentFixture`. Every action ends at `fixture.whenStable()`. |
|
|
34
|
+
| `app` | The `ApplicationRef` from `bootstrapApplication()`, for an app with no fixture. Actions end at its `whenStable()`. |
|
|
35
|
+
| `root` | Where queries start, scoped and `{ global: true }` alike. Defaults to `document.body`. |
|
|
36
|
+
|
|
37
|
+
One of `fixture` or `app` is required. The driver does not look for an app on
|
|
38
|
+
its own: the `ng` global and `getAllAngularRootElements()` exist only in
|
|
39
|
+
development builds, and testabilities only where zone.js is loaded.
|
|
40
|
+
|
|
41
|
+
**What "stable" means.** Zoneless, an event handler that sets a signal schedules
|
|
42
|
+
change detection rather than running it. `whenStable()` waits for that render,
|
|
43
|
+
and for anything else the app registers as pending: a navigation, an
|
|
44
|
+
`HttpClient` request, work wrapped in `PendingTasks.run()`. It does not wait for
|
|
45
|
+
a bare `setTimeout`, which a zoneless app does not track.
|
|
46
|
+
|
|
47
|
+
## Navigation
|
|
48
|
+
|
|
49
|
+
`goto()` and the URL members go through the app's `Router`, when one is
|
|
50
|
+
provided (`provideRouter()` or `RouterModule.forRoot()`): `goto()` is
|
|
51
|
+
`router.navigateByUrl()` then `whenStable()`, and `currentUrl` is
|
|
52
|
+
`Location.path()`. That is not `window.location`: TestBed swaps in a mock
|
|
53
|
+
platform location, so a test's navigations never reach the address bar.
|
|
54
|
+
|
|
55
|
+
Without a router, a page still constructs and reads, but `goto()` and the URL
|
|
56
|
+
members refuse with `the "angular" driver cannot navigate`. With one, they
|
|
57
|
+
refuse until its first navigation rather than reading as `/` — TestBed performs
|
|
58
|
+
no initial navigation.
|
|
59
|
+
|
|
60
|
+
## Where Angular differs from the other drivers
|
|
61
|
+
|
|
62
|
+
- **`isVisible()`** is a layout check in a real browser, as under Playwright,
|
|
63
|
+
and the dom driver's computed-style check under jsdom, which lays nothing out.
|
|
64
|
+
- **`boundingBox()`** under jsdom is all zeros, as with the dom driver.
|
|
65
|
+
- **Queries start at `document.body`**, where TestBed attaches each fixture and
|
|
66
|
+
where CDK overlays render, so `{ global: true }` reaches them.
|
|
67
|
+
|
|
68
|
+
## Decorators: two kinds in one app
|
|
69
|
+
|
|
70
|
+
Angular's components compile with the legacy `experimentalDecorators` transform.
|
|
71
|
+
Harnesses use standard decorators and `accessor` fields. Keep them apart by file
|
|
72
|
+
name — `*.harness.ts` and `*.page.ts` — in both the build and the typecheck.
|
|
73
|
+
|
|
74
|
+
**Vite, through `@analogjs/vite-plugin-angular`.** `harnessedDecorators()` first,
|
|
75
|
+
claiming harness and page files only, and the Angular plugin told to leave them
|
|
76
|
+
alone — it emits each file from its own TypeScript program, so it would
|
|
77
|
+
otherwise replace the lowered harness with a legacy build of it:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import angular from '@analogjs/vite-plugin-angular'
|
|
81
|
+
import { harnessedDecorators } from '@harnessed-ts/core/vite'
|
|
82
|
+
|
|
83
|
+
const HARNESS_FILES = /\.(harness|page)\.ts$/
|
|
84
|
+
|
|
85
|
+
export default defineConfig({
|
|
86
|
+
plugins: [
|
|
87
|
+
harnessedDecorators({ include: HARNESS_FILES }),
|
|
88
|
+
angular({ transformFilter: (_code, id) => !HARNESS_FILES.test(id) }),
|
|
89
|
+
],
|
|
90
|
+
})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Two tsconfigs.** The app's keeps `experimentalDecorators` and excludes harness
|
|
94
|
+
and page files; a second one checks them, and the tests that use them, without
|
|
95
|
+
it. Angular's own decorators type-check under both, so a harness may import the
|
|
96
|
+
component it drives.
|
|
97
|
+
|
|
98
|
+
```jsonc
|
|
99
|
+
// tsconfig.app.json — checked with `ngc -p tsconfig.app.json --noEmit`
|
|
100
|
+
{
|
|
101
|
+
"compilerOptions": { "experimentalDecorators": true /* …the CLI's options */ },
|
|
102
|
+
"include": ["src/**/*.ts"],
|
|
103
|
+
"exclude": ["src/**/*.harness.ts", "src/**/*.page.ts"],
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// tsconfig.harness.json — checked with `tsc -p tsconfig.harness.json`
|
|
107
|
+
{
|
|
108
|
+
"extends": "@harnessed-ts/core/tsconfig.json",
|
|
109
|
+
"compilerOptions": { "noEmit": true, "strict": true, "moduleResolution": "bundler" },
|
|
110
|
+
"include": ["src/**/*.harness.ts", "src/**/*.page.ts", "src/**/*.spec.ts"],
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Under Vitest, load `@angular/compiler` in a setup file: Angular's packages ship
|
|
115
|
+
partially compiled, and Vitest loads them without the linker a build runs.
|
|
116
|
+
Without Vitest globals, also call `getTestBed().resetTestingModule()` in an
|
|
117
|
+
`afterEach`; TestBed registers that itself only where `afterEach` is a global.
|
|
118
|
+
|
|
119
|
+
## Compared with `@angular/cdk/testing`
|
|
120
|
+
|
|
121
|
+
The CDK's component harnesses are the closest thing Angular has, and they are a
|
|
122
|
+
good fit for what they are for: harnesses that ship with an Angular component
|
|
123
|
+
library, run through `TestbedHarnessEnvironment` or a WebDriver environment.
|
|
124
|
+
They are Angular-only by design — they find components by their Angular
|
|
125
|
+
selectors and stabilize through Angular.
|
|
126
|
+
|
|
127
|
+
harnessed harnesses query the DOM — roles, labels, test ids — so one harness
|
|
128
|
+
runs unchanged under Testing Library, Playwright, Cypress, WebdriverIO,
|
|
129
|
+
Puppeteer, TestCafe, Vitest browser mode and Storybook, against an Angular app
|
|
130
|
+
or any other. Every driver is held to one conformance catalog. Pick the CDK to
|
|
131
|
+
publish harnesses for an Angular component library's users; pick harnessed to
|
|
132
|
+
write a harness once and run it at every level of your own app's tests. The two
|
|
133
|
+
do not interoperate, and can sit side by side in one suite.
|
|
134
|
+
|
|
135
|
+
Full guide: the
|
|
136
|
+
[harnessed README](https://github.com/joegaudet/harnessed#using-with-angular).
|
|
137
|
+
|
|
138
|
+
MIT © Joe Gaudet, Jay Seo
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Query } from '@harnessed-ts/core';
|
|
2
|
+
import type { BoundingBox, Selector, WaitOptions, WaitState } from '@harnessed-ts/core';
|
|
3
|
+
import type { AngularEnv } from './env';
|
|
4
|
+
/**
|
|
5
|
+
* Angular driver. Resolution is the shared resolver, so it agrees with every
|
|
6
|
+
* other in-page driver. Interactions go through user-event, as in the dom
|
|
7
|
+
* driver, and each one returns only once the app is stable: zoneless, an event
|
|
8
|
+
* handler that changes a signal schedules change detection rather than running
|
|
9
|
+
* it, so the DOM a test reads next is the one `whenStable()` hands back.
|
|
10
|
+
*
|
|
11
|
+
* Each action is handed the resolved element, never a selector string, so
|
|
12
|
+
* strictness and scope stay this library's rather than `querySelector`'s.
|
|
13
|
+
*/
|
|
14
|
+
export declare class AngularQuery extends Query {
|
|
15
|
+
private readonly env;
|
|
16
|
+
constructor(env: AngularEnv, scope: readonly Selector[], selector: Selector);
|
|
17
|
+
protected clone(selector: Selector): Query;
|
|
18
|
+
/** Read per call: the body belongs to the current test, not the env. */
|
|
19
|
+
protected container(): HTMLElement;
|
|
20
|
+
protected element(options?: WaitOptions): Promise<HTMLElement>;
|
|
21
|
+
/** Ends an action where the app does: rendered, with nothing pending. */
|
|
22
|
+
protected settled(): Promise<void>;
|
|
23
|
+
/** Every match in one pass, each bound to its node. See `DomQuery.all`. */
|
|
24
|
+
all(): Promise<Query[]>;
|
|
25
|
+
click(options?: WaitOptions): Promise<void>;
|
|
26
|
+
fill(value: string, options?: WaitOptions): Promise<void>;
|
|
27
|
+
clear(options?: WaitOptions): Promise<void>;
|
|
28
|
+
check(options?: WaitOptions): Promise<void>;
|
|
29
|
+
uncheck(options?: WaitOptions): Promise<void>;
|
|
30
|
+
selectOption(value: string | string[], options?: WaitOptions): Promise<void>;
|
|
31
|
+
hover(options?: WaitOptions): Promise<void>;
|
|
32
|
+
focus(options?: WaitOptions): Promise<void>;
|
|
33
|
+
blur(options?: WaitOptions): Promise<void>;
|
|
34
|
+
press(key: string, options?: WaitOptions): Promise<void>;
|
|
35
|
+
upload(files: File | File[], options?: WaitOptions): Promise<void>;
|
|
36
|
+
text(options?: WaitOptions): Promise<string>;
|
|
37
|
+
inputValue(options?: WaitOptions): Promise<string>;
|
|
38
|
+
attribute(name: string, options?: WaitOptions): Promise<string | null>;
|
|
39
|
+
isVisible(options?: WaitOptions): Promise<boolean>;
|
|
40
|
+
isEnabled(options?: WaitOptions): Promise<boolean>;
|
|
41
|
+
isChecked(options?: WaitOptions): Promise<boolean>;
|
|
42
|
+
selectedOptions(options?: WaitOptions): Promise<string[]>;
|
|
43
|
+
/** Under jsdom, which computes no layout, every box is all zeros. */
|
|
44
|
+
boundingBox(options?: WaitOptions): Promise<BoundingBox>;
|
|
45
|
+
waitFor(state: WaitState, options?: WaitOptions): Promise<void>;
|
|
46
|
+
count(): Promise<number>;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=angular-query.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"angular-query.d.ts","sourceRoot":"","sources":["../src/angular-query.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"driver-id.d.ts","sourceRoot":"","sources":["../src/driver-id.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,eAAO,MAAM,cAAc,YAAY,CAAA"}
|
package/dist/env.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ApplicationRef, ComponentRef } from '@angular/core';
|
|
2
|
+
import type { EnvConfig } from '@harnessed-ts/core';
|
|
3
|
+
import { ANGULAR_DRIVER } from './driver-id';
|
|
4
|
+
import './angular-query';
|
|
5
|
+
import './navigation';
|
|
6
|
+
export { ANGULAR_DRIVER };
|
|
7
|
+
/**
|
|
8
|
+
* What the driver needs of a `ComponentFixture`: a structural type, so a
|
|
9
|
+
* fixture from TestBed, `@testing-library/angular` or `cypress/angular` all
|
|
10
|
+
* fit, and nothing here imports `@angular/core/testing` at runtime.
|
|
11
|
+
*/
|
|
12
|
+
export interface AngularFixture {
|
|
13
|
+
whenStable(): Promise<unknown>;
|
|
14
|
+
readonly componentRef: Pick<ComponentRef<unknown>, 'injector'>;
|
|
15
|
+
}
|
|
16
|
+
export interface AngularEnv extends EnvConfig {
|
|
17
|
+
readonly driver: typeof ANGULAR_DRIVER;
|
|
18
|
+
/** A test's fixture. Every action ends at its `whenStable()`. */
|
|
19
|
+
readonly fixture: AngularFixture | undefined;
|
|
20
|
+
/** A bootstrapped app. Without a fixture, every action ends at its `whenStable()`. */
|
|
21
|
+
readonly app: ApplicationRef | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Where queries start, scoped and `{ global: true }` alike. Defaults to
|
|
24
|
+
* `document.body`, read at query time: TestBed attaches each fixture there,
|
|
25
|
+
* and the CDK's overlays and other portals render there too.
|
|
26
|
+
*/
|
|
27
|
+
readonly root: Element | undefined;
|
|
28
|
+
}
|
|
29
|
+
export interface AngularEnvOptions {
|
|
30
|
+
/** A `ComponentFixture`: TestBed's `createComponent()`, or a render result's `fixture`. */
|
|
31
|
+
fixture?: AngularFixture;
|
|
32
|
+
/** The `ApplicationRef` `bootstrapApplication()` resolves to, for an app with no fixture. */
|
|
33
|
+
app?: ApplicationRef;
|
|
34
|
+
root?: Element;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Builds the env a harness is constructed with in an Angular test:
|
|
38
|
+
*
|
|
39
|
+
* ```ts
|
|
40
|
+
* const { fixture } = await render(LoginForm)
|
|
41
|
+
* const form = new LoginFormHarness(angular({ fixture }))
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* or, for an app bootstrapped without TestBed:
|
|
45
|
+
*
|
|
46
|
+
* ```ts
|
|
47
|
+
* const app = await bootstrapApplication(App, appConfig)
|
|
48
|
+
* const page = new CheckoutPage(angular({ app }))
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* One of the two is required. Nothing else finds an app reliably: the `ng`
|
|
52
|
+
* global and `getAllAngularRootElements()` exist only in development builds,
|
|
53
|
+
* and testabilities only where zone.js is loaded.
|
|
54
|
+
*/
|
|
55
|
+
export declare function angular({ fixture, app, root }?: AngularEnvOptions): AngularEnv;
|
|
56
|
+
//# 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,cAAc,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACjE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAG5C,OAAO,iBAAiB,CAAA;AACxB,OAAO,cAAc,CAAA;AAErB,OAAO,EAAE,cAAc,EAAE,CAAA;AAEzB;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC,CAAA;IAC9B,QAAQ,CAAC,YAAY,EAAE,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,UAAU,CAAC,CAAA;CAC/D;AAED,MAAM,WAAW,UAAW,SAAQ,SAAS;IAC3C,QAAQ,CAAC,MAAM,EAAE,OAAO,cAAc,CAAA;IACtC,iEAAiE;IACjE,QAAQ,CAAC,OAAO,EAAE,cAAc,GAAG,SAAS,CAAA;IAC5C,sFAAsF;IACtF,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,SAAS,CAAA;IACxC;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,SAAS,CAAA;CACnC;AAED,MAAM,WAAW,iBAAiB;IAChC,2FAA2F;IAC3F,OAAO,CAAC,EAAE,cAAc,CAAA;IACxB,6FAA6F;IAC7F,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,OAAO,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,GAAE,iBAAsB,GAAG,UAAU,CAQlF"}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var core = require('@harnessed-ts/core');
|
|
4
|
+
var resolve = require('@harnessed-ts/resolve');
|
|
5
|
+
var dom = require('@testing-library/dom');
|
|
6
|
+
var userEvent = require('@testing-library/user-event');
|
|
7
|
+
var common = require('@angular/common');
|
|
8
|
+
var router = require('@angular/router');
|
|
9
|
+
|
|
10
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
|
+
|
|
12
|
+
var userEvent__default = /*#__PURE__*/_interopDefault(userEvent);
|
|
13
|
+
|
|
14
|
+
// src/driver-id.ts
|
|
15
|
+
var ANGULAR_DRIVER = "angular";
|
|
16
|
+
|
|
17
|
+
// src/stable.ts
|
|
18
|
+
async function settle(env) {
|
|
19
|
+
await (env.fixture ?? env.app)?.whenStable();
|
|
20
|
+
}
|
|
21
|
+
function injectorOf(env) {
|
|
22
|
+
return env.fixture?.componentRef.injector ?? env.app?.injector;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/angular-query.ts
|
|
26
|
+
var users = /* @__PURE__ */ new WeakMap();
|
|
27
|
+
function userFor(element) {
|
|
28
|
+
const document2 = element.ownerDocument;
|
|
29
|
+
let user = users.get(document2);
|
|
30
|
+
if (user === void 0) {
|
|
31
|
+
user = userEvent__default.default.setup({ document: document2 });
|
|
32
|
+
users.set(document2, user);
|
|
33
|
+
}
|
|
34
|
+
return user;
|
|
35
|
+
}
|
|
36
|
+
function toKeyboardInput(key) {
|
|
37
|
+
return key.length === 1 ? key : `{${key}}`;
|
|
38
|
+
}
|
|
39
|
+
function computesLayout(document2) {
|
|
40
|
+
const agent = document2.defaultView?.navigator.userAgent ?? "";
|
|
41
|
+
return !/\bjsdom\b|HappyDOM/.test(agent);
|
|
42
|
+
}
|
|
43
|
+
function isVisibleByStyle(element, home) {
|
|
44
|
+
let current = element;
|
|
45
|
+
while (current !== null) {
|
|
46
|
+
const view = current.ownerDocument.defaultView ?? window;
|
|
47
|
+
const style = view.getComputedStyle(current);
|
|
48
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (current.hasAttribute("hidden")) return false;
|
|
52
|
+
current = current.parentElement ?? (current.ownerDocument === home ? null : view.frameElement);
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
async function waitUntil(condition, timeout) {
|
|
57
|
+
let refused;
|
|
58
|
+
await dom.waitFor(
|
|
59
|
+
async () => {
|
|
60
|
+
try {
|
|
61
|
+
await condition();
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (!(error instanceof resolve.FrameEntryError || error instanceof core.StrictModeViolation)) {
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
refused = error;
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{ timeout }
|
|
70
|
+
);
|
|
71
|
+
if (refused !== void 0) throw refused;
|
|
72
|
+
}
|
|
73
|
+
var AngularQuery = class _AngularQuery extends core.Query {
|
|
74
|
+
constructor(env, scope, selector) {
|
|
75
|
+
super(scope, selector);
|
|
76
|
+
this.env = env;
|
|
77
|
+
}
|
|
78
|
+
env;
|
|
79
|
+
clone(selector) {
|
|
80
|
+
return new _AngularQuery(this.env, this.scope, selector);
|
|
81
|
+
}
|
|
82
|
+
/** Read per call: the body belongs to the current test, not the env. */
|
|
83
|
+
container() {
|
|
84
|
+
return this.env.root ?? document.body;
|
|
85
|
+
}
|
|
86
|
+
element(options) {
|
|
87
|
+
return resolve.resolveOne(this.container(), this.scope, this.selector, options?.timeout);
|
|
88
|
+
}
|
|
89
|
+
/** Ends an action where the app does: rendered, with nothing pending. */
|
|
90
|
+
settled() {
|
|
91
|
+
return settle(this.env);
|
|
92
|
+
}
|
|
93
|
+
/** Every match in one pass, each bound to its node. See `DomQuery.all`. */
|
|
94
|
+
async all() {
|
|
95
|
+
return (await resolve.resolveAll(this.container(), this.scope, this.selector)).map(
|
|
96
|
+
(element, index) => new BoundAngularQuery(this.env, this.scope, core.nth(this.selector, index), element)
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
// --- interactions -------------------------------------------------------
|
|
100
|
+
async click(options) {
|
|
101
|
+
const element = await this.element(options);
|
|
102
|
+
await userFor(element).click(element);
|
|
103
|
+
await this.settled();
|
|
104
|
+
}
|
|
105
|
+
async fill(value, options) {
|
|
106
|
+
const element = await this.element(options);
|
|
107
|
+
const user = userFor(element);
|
|
108
|
+
await user.clear(element);
|
|
109
|
+
if (value !== "") await user.type(element, value);
|
|
110
|
+
await this.settled();
|
|
111
|
+
}
|
|
112
|
+
async clear(options) {
|
|
113
|
+
const element = await this.element(options);
|
|
114
|
+
await userFor(element).clear(element);
|
|
115
|
+
await this.settled();
|
|
116
|
+
}
|
|
117
|
+
async check(options) {
|
|
118
|
+
const element = await this.element(options);
|
|
119
|
+
if (!element.checked) await userFor(element).click(element);
|
|
120
|
+
await this.settled();
|
|
121
|
+
}
|
|
122
|
+
async uncheck(options) {
|
|
123
|
+
const element = await this.element(options);
|
|
124
|
+
if (element.checked) await userFor(element).click(element);
|
|
125
|
+
await this.settled();
|
|
126
|
+
}
|
|
127
|
+
async selectOption(value, options) {
|
|
128
|
+
const element = await this.element(options);
|
|
129
|
+
const user = userFor(element);
|
|
130
|
+
if (element.multiple) {
|
|
131
|
+
const selected = [...element.selectedOptions].map((option) => option.value);
|
|
132
|
+
if (selected.length > 0) await user.deselectOptions(element, selected);
|
|
133
|
+
}
|
|
134
|
+
await user.selectOptions(element, value);
|
|
135
|
+
await this.settled();
|
|
136
|
+
}
|
|
137
|
+
async hover(options) {
|
|
138
|
+
const element = await this.element(options);
|
|
139
|
+
await userFor(element).hover(element);
|
|
140
|
+
await this.settled();
|
|
141
|
+
}
|
|
142
|
+
async focus(options) {
|
|
143
|
+
(await this.element(options)).focus();
|
|
144
|
+
await this.settled();
|
|
145
|
+
}
|
|
146
|
+
async blur(options) {
|
|
147
|
+
(await this.element(options)).blur();
|
|
148
|
+
await this.settled();
|
|
149
|
+
}
|
|
150
|
+
async press(key, options) {
|
|
151
|
+
const element = await this.element(options);
|
|
152
|
+
element.focus();
|
|
153
|
+
await userFor(element).keyboard(toKeyboardInput(key));
|
|
154
|
+
await this.settled();
|
|
155
|
+
}
|
|
156
|
+
async upload(files, options) {
|
|
157
|
+
const list = Array.isArray(files) ? files : [files];
|
|
158
|
+
const input = resolve.fileInputOf(await this.element(options));
|
|
159
|
+
core.checkFileTarget(input, list.length, this.scope, this.selector);
|
|
160
|
+
await userFor(input).upload(input, list);
|
|
161
|
+
await this.settled();
|
|
162
|
+
}
|
|
163
|
+
// --- observations -------------------------------------------------------
|
|
164
|
+
async text(options) {
|
|
165
|
+
return ((await this.element(options)).textContent ?? "").trim();
|
|
166
|
+
}
|
|
167
|
+
async inputValue(options) {
|
|
168
|
+
const element = await this.element(options);
|
|
169
|
+
return element.value ?? "";
|
|
170
|
+
}
|
|
171
|
+
async attribute(name, options) {
|
|
172
|
+
return (await this.element(options)).getAttribute(name);
|
|
173
|
+
}
|
|
174
|
+
async isVisible(options) {
|
|
175
|
+
try {
|
|
176
|
+
const element = await this.element(options);
|
|
177
|
+
const home = this.container().ownerDocument;
|
|
178
|
+
return computesLayout(home) ? resolve.isVisibleInLayout(element, home) : isVisibleByStyle(element, home);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if (error instanceof resolve.FrameEntryError || error instanceof core.StrictModeViolation) throw error;
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async isEnabled(options) {
|
|
185
|
+
const element = await this.element(options);
|
|
186
|
+
const disabled = element.disabled === true || element.closest(":disabled") !== null;
|
|
187
|
+
return core.enabledFrom(disabled, element.getAttribute("aria-disabled"));
|
|
188
|
+
}
|
|
189
|
+
async isChecked(options) {
|
|
190
|
+
const element = await this.element(options);
|
|
191
|
+
return core.checkedFrom(
|
|
192
|
+
element.getAttribute("aria-checked"),
|
|
193
|
+
Boolean(element.checked)
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
async selectedOptions(options) {
|
|
197
|
+
const element = await this.element(options);
|
|
198
|
+
return [...element.selectedOptions ?? []].map((option) => option.value);
|
|
199
|
+
}
|
|
200
|
+
/** Under jsdom, which computes no layout, every box is all zeros. */
|
|
201
|
+
async boundingBox(options) {
|
|
202
|
+
return resolve.boundingBoxOf(await this.element(options), this.container().ownerDocument);
|
|
203
|
+
}
|
|
204
|
+
// --- waiting ------------------------------------------------------------
|
|
205
|
+
async waitFor(state, options) {
|
|
206
|
+
const timeout = core.timeoutFor(options?.timeout);
|
|
207
|
+
if (state === "visible") {
|
|
208
|
+
await waitUntil(async () => {
|
|
209
|
+
if (!await this.isVisible({ timeout })) throw new Error("not visible yet");
|
|
210
|
+
}, timeout);
|
|
211
|
+
} else {
|
|
212
|
+
await waitUntil(async () => {
|
|
213
|
+
if (await this.count() !== 0 && await this.isVisible({ timeout })) {
|
|
214
|
+
throw new Error("still visible");
|
|
215
|
+
}
|
|
216
|
+
}, timeout);
|
|
217
|
+
}
|
|
218
|
+
await this.settled();
|
|
219
|
+
}
|
|
220
|
+
async count() {
|
|
221
|
+
return resolve.countAll(this.container(), this.scope, this.selector);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
var BoundAngularQuery = class extends AngularQuery {
|
|
225
|
+
constructor(env, scope, selector, bound) {
|
|
226
|
+
super(env, scope, selector);
|
|
227
|
+
this.bound = bound;
|
|
228
|
+
}
|
|
229
|
+
bound;
|
|
230
|
+
async element(options) {
|
|
231
|
+
if (this.bound.isConnected) return this.bound;
|
|
232
|
+
return super.element(options);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
core.registerDriver(ANGULAR_DRIVER, (env, scope, selector) => {
|
|
236
|
+
return new AngularQuery(env, scope, selector);
|
|
237
|
+
});
|
|
238
|
+
function routing(env) {
|
|
239
|
+
const injector = injectorOf(env);
|
|
240
|
+
if (injector.get(router.ROUTES, null) === null) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
`harnessed: the "${ANGULAR_DRIVER}" driver cannot navigate without a Router, so a page cannot navigate with it. Provide one to the app or the TestBed (provideRouter()), or reach the page by rendering it.`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
return { router: injector.get(router.Router), location: injector.get(common.Location) };
|
|
246
|
+
}
|
|
247
|
+
function absolute(env) {
|
|
248
|
+
const { router, location } = routing(env);
|
|
249
|
+
if (!router.navigated) {
|
|
250
|
+
throw new Error(
|
|
251
|
+
"harnessed: the router has not navigated yet. Call a page\u2019s goto(), or router.navigateByUrl(), first."
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
return new URL(location.path(), window.location.origin).href;
|
|
255
|
+
}
|
|
256
|
+
core.registerNavigation(ANGULAR_DRIVER, {
|
|
257
|
+
async goto(env, url) {
|
|
258
|
+
const { router } = routing(env);
|
|
259
|
+
await router.navigateByUrl(url);
|
|
260
|
+
await settle(env);
|
|
261
|
+
},
|
|
262
|
+
currentUrl(env) {
|
|
263
|
+
return absolute(env);
|
|
264
|
+
},
|
|
265
|
+
async waitForUrl(env, matches, timeout) {
|
|
266
|
+
absolute(env);
|
|
267
|
+
await dom.waitFor(
|
|
268
|
+
() => {
|
|
269
|
+
if (!matches(new URL(absolute(env)))) throw new Error(`still at ${absolute(env)}`);
|
|
270
|
+
},
|
|
271
|
+
{ timeout }
|
|
272
|
+
);
|
|
273
|
+
await settle(env);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// src/env.ts
|
|
278
|
+
function angular({ fixture, app, root } = {}) {
|
|
279
|
+
if (fixture === void 0 && app === void 0) {
|
|
280
|
+
throw new TypeError(
|
|
281
|
+
"harnessed: angular() needs the app it drives \u2014 pass { fixture } from TestBed or a render result, or { app } with the ApplicationRef bootstrapApplication() resolved to."
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
return { driver: ANGULAR_DRIVER, fixture, app, root };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
exports.ANGULAR_DRIVER = ANGULAR_DRIVER;
|
|
288
|
+
exports.AngularQuery = AngularQuery;
|
|
289
|
+
exports.angular = angular;
|
|
290
|
+
//# sourceMappingURL=index.cjs.map
|
|
291
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/driver-id.ts","../src/stable.ts","../src/angular-query.ts","../src/navigation.ts","../src/env.ts"],"names":["document","userEvent","waitForCondition","FrameEntryError","StrictModeViolation","Query","resolveOne","resolveAll","withNth","fileInputOf","checkFileTarget","isVisibleInLayout","enabledFrom","checkedFrom","boundingBoxOf","timeoutFor","countAll","registerDriver","ROUTES","Router","Location","registerNavigation"],"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 ANGULAR_DRIVER = 'angular'\n","import type { Injector } from '@angular/core'\nimport type { AngularEnv } from './env'\n\n/**\n * The app's settle point: the fixture's `whenStable()` if there is one, else\n * the `ApplicationRef`'s. Zoneless, both wait for Angular's pending tasks — a\n * scheduled change detection, a navigation, an `HttpClient` request — and not\n * for a bare `setTimeout`, which a zoneless app does not track.\n */\nexport async function settle(env: AngularEnv): Promise<void> {\n await (env.fixture ?? env.app)?.whenStable()\n}\n\n/** Where the app's services live: the fixture's component injector, or the app's. */\nexport function injectorOf(env: AngularEnv): Injector {\n return (env.fixture?.componentRef.injector ?? env.app?.injector) as Injector\n}\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 { waitFor as waitForCondition } from '@testing-library/dom'\nimport userEvent from '@testing-library/user-event'\nimport type { UserEvent } from '@testing-library/user-event'\nimport { ANGULAR_DRIVER } from './driver-id'\nimport type { AngularEnv } from './env'\nimport { settle } from './stable'\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/**\n * Whether the engine lays pages out. jsdom and happy-dom do not: every box is\n * zero there, so a layout check would call everything invisible.\n */\nfunction computesLayout(document: Document): boolean {\n const agent = document.defaultView?.navigator.userAgent ?? ''\n return !/\\bjsdom\\b|HappyDOM/.test(agent)\n}\n\n/**\n * The dom driver's rule, for an engine with no layout: computed style up the\n * tree, through the frames the scope entered. `home` is the env's own document.\n */\nfunction isVisibleByStyle(element: HTMLElement, home: Document): boolean {\n let current: HTMLElement | null = element\n while (current !== null) {\n const view: Window = current.ownerDocument.defaultView ?? window\n const style = view.getComputedStyle(current)\n if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {\n return false\n }\n if (current.hasAttribute('hidden')) return false\n current =\n current.parentElement ??\n (current.ownerDocument === home ? null : (view.frameElement as HTMLElement | null))\n }\n return true\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.\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 * Angular driver. Resolution is the shared resolver, so it agrees with every\n * other in-page driver. Interactions go through user-event, as in the dom\n * driver, and each one returns only once the app is stable: zoneless, an event\n * handler that changes a signal schedules change detection rather than running\n * it, so the DOM a test reads next is the one `whenStable()` hands back.\n *\n * Each action 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 AngularQuery extends Query {\n constructor(\n private readonly env: AngularEnv,\n scope: readonly Selector[],\n selector: Selector,\n ) {\n super(scope, selector)\n }\n\n protected override clone(selector: Selector): Query {\n return new AngularQuery(this.env, this.scope, selector)\n }\n\n /** Read per call: the body belongs to the current test, not the env. */\n protected container(): HTMLElement {\n return (this.env.root ?? document.body) 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 /** Ends an action where the app does: rendered, with nothing pending. */\n protected settled(): Promise<void> {\n return settle(this.env)\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 BoundAngularQuery(this.env, this.scope, withNth(this.selector, index), element),\n )\n }\n\n // --- interactions -------------------------------------------------------\n\n override async click(options?: WaitOptions): Promise<void> {\n const element = await this.element(options)\n await userFor(element).click(element)\n await this.settled()\n }\n\n override async fill(value: string, options?: WaitOptions): Promise<void> {\n const element = await this.element(options)\n const user = userFor(element)\n await user.clear(element)\n // user-event throws on an empty string, and clearing is the whole intent\n // anyway — which is also what Playwright's fill('') does.\n if (value !== '') await user.type(element, value)\n await this.settled()\n }\n\n override async clear(options?: WaitOptions): Promise<void> {\n const element = await this.element(options)\n await userFor(element).clear(element)\n await this.settled()\n }\n\n override async check(options?: WaitOptions): Promise<void> {\n const element = await this.element(options)\n if (!(element as HTMLInputElement).checked) await userFor(element).click(element)\n await this.settled()\n }\n\n override async uncheck(options?: WaitOptions): Promise<void> {\n const element = await this.element(options)\n if ((element as HTMLInputElement).checked) await userFor(element).click(element)\n await this.settled()\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.\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 this.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 this.settled()\n }\n\n override async focus(options?: WaitOptions): Promise<void> {\n ;(await this.element(options)).focus()\n await this.settled()\n }\n\n override async blur(options?: WaitOptions): Promise<void> {\n ;(await this.element(options)).blur()\n await this.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 element.focus()\n await userFor(element).keyboard(toKeyboardInput(key))\n await this.settled()\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 await userFor(input).upload(input, list)\n await this.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 const element = await this.element(options)\n const home = this.container().ownerDocument\n // In a browser, a real layout check — Playwright's rule. Under jsdom,\n // which lays nothing out, the dom driver's computed-style walk.\n return computesLayout(home)\n ? isVisibleInLayout(element, home)\n : isVisibleByStyle(element, home)\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 /** Under jsdom, which computes no layout, every box is all zeros. */\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 // stable app, as every interaction does.\n await this.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 BoundAngularQuery extends AngularQuery {\n constructor(\n env: AngularEnv,\n scope: readonly Selector[],\n selector: Selector,\n private readonly bound: HTMLElement,\n ) {\n super(env, 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\nregisterDriver(ANGULAR_DRIVER, (env: EnvConfig, scope, selector) => {\n return new AngularQuery(env as AngularEnv, scope, selector)\n})\n","import { Location } from '@angular/common'\nimport { Router, ROUTES } from '@angular/router'\nimport { registerNavigation } from '@harnessed-ts/core'\nimport type { EnvConfig } from '@harnessed-ts/core'\nimport { waitFor as waitForCondition } from '@testing-library/dom'\nimport { ANGULAR_DRIVER } from './driver-id'\nimport type { AngularEnv } from './env'\nimport { injectorOf, settle } from './stable'\n\ninterface Routing {\n router: Router\n location: Location\n}\n\n/**\n * The app's router, or the standard refusal. A component rendered on its own\n * has no router to drive — the Angular shape of a jsdom test — and a page there\n * constructs and reads, but cannot navigate.\n */\nfunction routing(env: EnvConfig): Routing {\n const injector = injectorOf(env as AngularEnv)\n // Not `get(Router, null)`: the Router is provided in root, so it always\n // resolves — to one with no routes, which matches no URL. The ROUTES token is\n // what `provideRouter()` and `RouterModule.forRoot()` add.\n if (injector.get(ROUTES, null) === null) {\n throw new Error(\n `harnessed: the \"${ANGULAR_DRIVER}\" driver cannot navigate without a Router, so a page ` +\n 'cannot navigate with it. Provide one to the app or the TestBed (provideRouter()), or ' +\n 'reach the page by rendering it.',\n )\n }\n return { router: injector.get(Router), location: injector.get(Location) }\n}\n\n/**\n * The router's URL, made absolute. `Location.path()` is the truth, not\n * `window.location`: TestBed swaps in a mock platform location, so a test's\n * navigations never reach the address bar. Absolute because a page reads\n * `pathname` and `searchParams` off it with `new URL()`.\n *\n * There is no URL before the router's first navigation — TestBed performs\n * none of its own. Reading that as `/` would let `assertPathname('/')` pass\n * before anything navigated, so it refuses.\n */\nfunction absolute(env: EnvConfig): string {\n const { router, location } = routing(env)\n if (!router.navigated) {\n throw new Error(\n 'harnessed: the router has not navigated yet. Call a page’s goto(), or ' +\n 'router.navigateByUrl(), first.',\n )\n }\n return new URL(location.path(), window.location.origin).href\n}\n\n/** Navigation through the app's Router, wherever one is provided. */\nregisterNavigation(ANGULAR_DRIVER, {\n async goto(env, url) {\n const { router } = routing(env)\n // Resolves once the navigation ends; whenStable() then waits out the\n // change detection it scheduled, so the destination is on screen.\n await router.navigateByUrl(url)\n await settle(env as AngularEnv)\n },\n currentUrl(env) {\n return absolute(env)\n },\n async waitForUrl(env, matches, timeout) {\n // Outside the retry loop: no router is a refusal, not something to wait out.\n absolute(env)\n await waitForCondition(\n () => {\n if (!matches(new URL(absolute(env)))) throw new Error(`still at ${absolute(env)}`)\n },\n { timeout },\n )\n // The URL changes before the destination has rendered.\n await settle(env as AngularEnv)\n },\n})\n","import type { ApplicationRef, ComponentRef } from '@angular/core'\nimport type { EnvConfig } from '@harnessed-ts/core'\nimport { ANGULAR_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 './angular-query'\nimport './navigation'\n\nexport { ANGULAR_DRIVER }\n\n/**\n * What the driver needs of a `ComponentFixture`: a structural type, so a\n * fixture from TestBed, `@testing-library/angular` or `cypress/angular` all\n * fit, and nothing here imports `@angular/core/testing` at runtime.\n */\nexport interface AngularFixture {\n whenStable(): Promise<unknown>\n readonly componentRef: Pick<ComponentRef<unknown>, 'injector'>\n}\n\nexport interface AngularEnv extends EnvConfig {\n readonly driver: typeof ANGULAR_DRIVER\n /** A test's fixture. Every action ends at its `whenStable()`. */\n readonly fixture: AngularFixture | undefined\n /** A bootstrapped app. Without a fixture, every action ends at its `whenStable()`. */\n readonly app: ApplicationRef | undefined\n /**\n * Where queries start, scoped and `{ global: true }` alike. Defaults to\n * `document.body`, read at query time: TestBed attaches each fixture there,\n * and the CDK's overlays and other portals render there too.\n */\n readonly root: Element | undefined\n}\n\nexport interface AngularEnvOptions {\n /** A `ComponentFixture`: TestBed's `createComponent()`, or a render result's `fixture`. */\n fixture?: AngularFixture\n /** The `ApplicationRef` `bootstrapApplication()` resolves to, for an app with no fixture. */\n app?: ApplicationRef\n root?: Element\n}\n\n/**\n * Builds the env a harness is constructed with in an Angular test:\n *\n * ```ts\n * const { fixture } = await render(LoginForm)\n * const form = new LoginFormHarness(angular({ fixture }))\n * ```\n *\n * or, for an app bootstrapped without TestBed:\n *\n * ```ts\n * const app = await bootstrapApplication(App, appConfig)\n * const page = new CheckoutPage(angular({ app }))\n * ```\n *\n * One of the two is required. Nothing else finds an app reliably: the `ng`\n * global and `getAllAngularRootElements()` exist only in development builds,\n * and testabilities only where zone.js is loaded.\n */\nexport function angular({ fixture, app, root }: AngularEnvOptions = {}): AngularEnv {\n if (fixture === undefined && app === undefined) {\n throw new TypeError(\n 'harnessed: angular() needs the app it drives — pass { fixture } from TestBed or a ' +\n 'render result, or { app } with the ApplicationRef bootstrapApplication() resolved to.',\n )\n }\n return { driver: ANGULAR_DRIVER, fixture, app, root }\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,OAAO,EAAE,cAAc,EAAE,MAAM,OAAO,CAAA;AAC/C,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,OAAO,CAAA;AAC1E,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { registerDriver, registerNavigation, 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 { waitFor } from '@testing-library/dom';
|
|
4
|
+
import userEvent from '@testing-library/user-event';
|
|
5
|
+
import { Location } from '@angular/common';
|
|
6
|
+
import { ROUTES, Router } from '@angular/router';
|
|
7
|
+
|
|
8
|
+
// src/driver-id.ts
|
|
9
|
+
var ANGULAR_DRIVER = "angular";
|
|
10
|
+
|
|
11
|
+
// src/stable.ts
|
|
12
|
+
async function settle(env) {
|
|
13
|
+
await (env.fixture ?? env.app)?.whenStable();
|
|
14
|
+
}
|
|
15
|
+
function injectorOf(env) {
|
|
16
|
+
return env.fixture?.componentRef.injector ?? env.app?.injector;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/angular-query.ts
|
|
20
|
+
var users = /* @__PURE__ */ new WeakMap();
|
|
21
|
+
function userFor(element) {
|
|
22
|
+
const document2 = element.ownerDocument;
|
|
23
|
+
let user = users.get(document2);
|
|
24
|
+
if (user === void 0) {
|
|
25
|
+
user = userEvent.setup({ document: document2 });
|
|
26
|
+
users.set(document2, user);
|
|
27
|
+
}
|
|
28
|
+
return user;
|
|
29
|
+
}
|
|
30
|
+
function toKeyboardInput(key) {
|
|
31
|
+
return key.length === 1 ? key : `{${key}}`;
|
|
32
|
+
}
|
|
33
|
+
function computesLayout(document2) {
|
|
34
|
+
const agent = document2.defaultView?.navigator.userAgent ?? "";
|
|
35
|
+
return !/\bjsdom\b|HappyDOM/.test(agent);
|
|
36
|
+
}
|
|
37
|
+
function isVisibleByStyle(element, home) {
|
|
38
|
+
let current = element;
|
|
39
|
+
while (current !== null) {
|
|
40
|
+
const view = current.ownerDocument.defaultView ?? window;
|
|
41
|
+
const style = view.getComputedStyle(current);
|
|
42
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (current.hasAttribute("hidden")) return false;
|
|
46
|
+
current = current.parentElement ?? (current.ownerDocument === home ? null : view.frameElement);
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
async function waitUntil(condition, timeout) {
|
|
51
|
+
let refused;
|
|
52
|
+
await waitFor(
|
|
53
|
+
async () => {
|
|
54
|
+
try {
|
|
55
|
+
await condition();
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (!(error instanceof FrameEntryError || error instanceof StrictModeViolation)) {
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
refused = error;
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
{ timeout }
|
|
64
|
+
);
|
|
65
|
+
if (refused !== void 0) throw refused;
|
|
66
|
+
}
|
|
67
|
+
var AngularQuery = class _AngularQuery extends Query {
|
|
68
|
+
constructor(env, scope, selector) {
|
|
69
|
+
super(scope, selector);
|
|
70
|
+
this.env = env;
|
|
71
|
+
}
|
|
72
|
+
env;
|
|
73
|
+
clone(selector) {
|
|
74
|
+
return new _AngularQuery(this.env, this.scope, selector);
|
|
75
|
+
}
|
|
76
|
+
/** Read per call: the body belongs to the current test, not the env. */
|
|
77
|
+
container() {
|
|
78
|
+
return this.env.root ?? document.body;
|
|
79
|
+
}
|
|
80
|
+
element(options) {
|
|
81
|
+
return resolveOne(this.container(), this.scope, this.selector, options?.timeout);
|
|
82
|
+
}
|
|
83
|
+
/** Ends an action where the app does: rendered, with nothing pending. */
|
|
84
|
+
settled() {
|
|
85
|
+
return settle(this.env);
|
|
86
|
+
}
|
|
87
|
+
/** Every match in one pass, each bound to its node. See `DomQuery.all`. */
|
|
88
|
+
async all() {
|
|
89
|
+
return (await resolveAll(this.container(), this.scope, this.selector)).map(
|
|
90
|
+
(element, index) => new BoundAngularQuery(this.env, this.scope, nth(this.selector, index), element)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
// --- interactions -------------------------------------------------------
|
|
94
|
+
async click(options) {
|
|
95
|
+
const element = await this.element(options);
|
|
96
|
+
await userFor(element).click(element);
|
|
97
|
+
await this.settled();
|
|
98
|
+
}
|
|
99
|
+
async fill(value, options) {
|
|
100
|
+
const element = await this.element(options);
|
|
101
|
+
const user = userFor(element);
|
|
102
|
+
await user.clear(element);
|
|
103
|
+
if (value !== "") await user.type(element, value);
|
|
104
|
+
await this.settled();
|
|
105
|
+
}
|
|
106
|
+
async clear(options) {
|
|
107
|
+
const element = await this.element(options);
|
|
108
|
+
await userFor(element).clear(element);
|
|
109
|
+
await this.settled();
|
|
110
|
+
}
|
|
111
|
+
async check(options) {
|
|
112
|
+
const element = await this.element(options);
|
|
113
|
+
if (!element.checked) await userFor(element).click(element);
|
|
114
|
+
await this.settled();
|
|
115
|
+
}
|
|
116
|
+
async uncheck(options) {
|
|
117
|
+
const element = await this.element(options);
|
|
118
|
+
if (element.checked) await userFor(element).click(element);
|
|
119
|
+
await this.settled();
|
|
120
|
+
}
|
|
121
|
+
async selectOption(value, options) {
|
|
122
|
+
const element = await this.element(options);
|
|
123
|
+
const user = userFor(element);
|
|
124
|
+
if (element.multiple) {
|
|
125
|
+
const selected = [...element.selectedOptions].map((option) => option.value);
|
|
126
|
+
if (selected.length > 0) await user.deselectOptions(element, selected);
|
|
127
|
+
}
|
|
128
|
+
await user.selectOptions(element, value);
|
|
129
|
+
await this.settled();
|
|
130
|
+
}
|
|
131
|
+
async hover(options) {
|
|
132
|
+
const element = await this.element(options);
|
|
133
|
+
await userFor(element).hover(element);
|
|
134
|
+
await this.settled();
|
|
135
|
+
}
|
|
136
|
+
async focus(options) {
|
|
137
|
+
(await this.element(options)).focus();
|
|
138
|
+
await this.settled();
|
|
139
|
+
}
|
|
140
|
+
async blur(options) {
|
|
141
|
+
(await this.element(options)).blur();
|
|
142
|
+
await this.settled();
|
|
143
|
+
}
|
|
144
|
+
async press(key, options) {
|
|
145
|
+
const element = await this.element(options);
|
|
146
|
+
element.focus();
|
|
147
|
+
await userFor(element).keyboard(toKeyboardInput(key));
|
|
148
|
+
await this.settled();
|
|
149
|
+
}
|
|
150
|
+
async upload(files, options) {
|
|
151
|
+
const list = Array.isArray(files) ? files : [files];
|
|
152
|
+
const input = fileInputOf(await this.element(options));
|
|
153
|
+
checkFileTarget(input, list.length, this.scope, this.selector);
|
|
154
|
+
await userFor(input).upload(input, list);
|
|
155
|
+
await this.settled();
|
|
156
|
+
}
|
|
157
|
+
// --- observations -------------------------------------------------------
|
|
158
|
+
async text(options) {
|
|
159
|
+
return ((await this.element(options)).textContent ?? "").trim();
|
|
160
|
+
}
|
|
161
|
+
async inputValue(options) {
|
|
162
|
+
const element = await this.element(options);
|
|
163
|
+
return element.value ?? "";
|
|
164
|
+
}
|
|
165
|
+
async attribute(name, options) {
|
|
166
|
+
return (await this.element(options)).getAttribute(name);
|
|
167
|
+
}
|
|
168
|
+
async isVisible(options) {
|
|
169
|
+
try {
|
|
170
|
+
const element = await this.element(options);
|
|
171
|
+
const home = this.container().ownerDocument;
|
|
172
|
+
return computesLayout(home) ? isVisibleInLayout(element, home) : isVisibleByStyle(element, home);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (error instanceof FrameEntryError || error instanceof StrictModeViolation) throw error;
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async isEnabled(options) {
|
|
179
|
+
const element = await this.element(options);
|
|
180
|
+
const disabled = element.disabled === true || element.closest(":disabled") !== null;
|
|
181
|
+
return enabledFrom(disabled, element.getAttribute("aria-disabled"));
|
|
182
|
+
}
|
|
183
|
+
async isChecked(options) {
|
|
184
|
+
const element = await this.element(options);
|
|
185
|
+
return checkedFrom(
|
|
186
|
+
element.getAttribute("aria-checked"),
|
|
187
|
+
Boolean(element.checked)
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
async selectedOptions(options) {
|
|
191
|
+
const element = await this.element(options);
|
|
192
|
+
return [...element.selectedOptions ?? []].map((option) => option.value);
|
|
193
|
+
}
|
|
194
|
+
/** Under jsdom, which computes no layout, every box is all zeros. */
|
|
195
|
+
async boundingBox(options) {
|
|
196
|
+
return boundingBoxOf(await this.element(options), this.container().ownerDocument);
|
|
197
|
+
}
|
|
198
|
+
// --- waiting ------------------------------------------------------------
|
|
199
|
+
async waitFor(state, options) {
|
|
200
|
+
const timeout = timeoutFor(options?.timeout);
|
|
201
|
+
if (state === "visible") {
|
|
202
|
+
await waitUntil(async () => {
|
|
203
|
+
if (!await this.isVisible({ timeout })) throw new Error("not visible yet");
|
|
204
|
+
}, timeout);
|
|
205
|
+
} else {
|
|
206
|
+
await waitUntil(async () => {
|
|
207
|
+
if (await this.count() !== 0 && await this.isVisible({ timeout })) {
|
|
208
|
+
throw new Error("still visible");
|
|
209
|
+
}
|
|
210
|
+
}, timeout);
|
|
211
|
+
}
|
|
212
|
+
await this.settled();
|
|
213
|
+
}
|
|
214
|
+
async count() {
|
|
215
|
+
return countAll(this.container(), this.scope, this.selector);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
var BoundAngularQuery = class extends AngularQuery {
|
|
219
|
+
constructor(env, scope, selector, bound) {
|
|
220
|
+
super(env, scope, selector);
|
|
221
|
+
this.bound = bound;
|
|
222
|
+
}
|
|
223
|
+
bound;
|
|
224
|
+
async element(options) {
|
|
225
|
+
if (this.bound.isConnected) return this.bound;
|
|
226
|
+
return super.element(options);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
registerDriver(ANGULAR_DRIVER, (env, scope, selector) => {
|
|
230
|
+
return new AngularQuery(env, scope, selector);
|
|
231
|
+
});
|
|
232
|
+
function routing(env) {
|
|
233
|
+
const injector = injectorOf(env);
|
|
234
|
+
if (injector.get(ROUTES, null) === null) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`harnessed: the "${ANGULAR_DRIVER}" driver cannot navigate without a Router, so a page cannot navigate with it. Provide one to the app or the TestBed (provideRouter()), or reach the page by rendering it.`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
return { router: injector.get(Router), location: injector.get(Location) };
|
|
240
|
+
}
|
|
241
|
+
function absolute(env) {
|
|
242
|
+
const { router, location } = routing(env);
|
|
243
|
+
if (!router.navigated) {
|
|
244
|
+
throw new Error(
|
|
245
|
+
"harnessed: the router has not navigated yet. Call a page\u2019s goto(), or router.navigateByUrl(), first."
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
return new URL(location.path(), window.location.origin).href;
|
|
249
|
+
}
|
|
250
|
+
registerNavigation(ANGULAR_DRIVER, {
|
|
251
|
+
async goto(env, url) {
|
|
252
|
+
const { router } = routing(env);
|
|
253
|
+
await router.navigateByUrl(url);
|
|
254
|
+
await settle(env);
|
|
255
|
+
},
|
|
256
|
+
currentUrl(env) {
|
|
257
|
+
return absolute(env);
|
|
258
|
+
},
|
|
259
|
+
async waitForUrl(env, matches, timeout) {
|
|
260
|
+
absolute(env);
|
|
261
|
+
await waitFor(
|
|
262
|
+
() => {
|
|
263
|
+
if (!matches(new URL(absolute(env)))) throw new Error(`still at ${absolute(env)}`);
|
|
264
|
+
},
|
|
265
|
+
{ timeout }
|
|
266
|
+
);
|
|
267
|
+
await settle(env);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// src/env.ts
|
|
272
|
+
function angular({ fixture, app, root } = {}) {
|
|
273
|
+
if (fixture === void 0 && app === void 0) {
|
|
274
|
+
throw new TypeError(
|
|
275
|
+
"harnessed: angular() needs the app it drives \u2014 pass { fixture } from TestBed or a render result, or { app } with the ApplicationRef bootstrapApplication() resolved to."
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
return { driver: ANGULAR_DRIVER, fixture, app, root };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export { ANGULAR_DRIVER, AngularQuery, angular };
|
|
282
|
+
//# sourceMappingURL=index.js.map
|
|
283
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/driver-id.ts","../src/stable.ts","../src/angular-query.ts","../src/navigation.ts","../src/env.ts"],"names":["document","waitForCondition","withNth"],"mappings":";;;;;;;;AACO,IAAM,cAAA,GAAiB;;;ACQ9B,eAAsB,OAAO,GAAA,EAAgC;AAC3D,EAAA,MAAA,CAAO,GAAA,CAAI,OAAA,IAAW,GAAA,CAAI,GAAA,GAAM,UAAA,EAAW;AAC7C;AAGO,SAAS,WAAW,GAAA,EAA2B;AACpD,EAAA,OAAQ,GAAA,CAAI,OAAA,EAAS,YAAA,CAAa,QAAA,IAAY,IAAI,GAAA,EAAK,QAAA;AACzD;;;ACcA,IAAM,KAAA,uBAAY,OAAA,EAA6B;AAE/C,SAAS,QAAQ,OAAA,EAA6B;AAC5C,EAAA,MAAMA,YAAW,OAAA,CAAQ,aAAA;AACzB,EAAA,IAAI,IAAA,GAAO,KAAA,CAAM,GAAA,CAAIA,SAAQ,CAAA;AAC7B,EAAA,IAAI,SAAS,MAAA,EAAW;AACtB,IAAA,IAAA,GAAO,SAAA,CAAU,KAAA,CAAM,EAAE,QAAA,EAAAA,WAAU,CAAA;AACnC,IAAA,KAAA,CAAM,GAAA,CAAIA,WAAU,IAAI,CAAA;AAAA,EAC1B;AACA,EAAA,OAAO,IAAA;AACT;AAGA,SAAS,gBAAgB,GAAA,EAAqB;AAC5C,EAAA,OAAO,GAAA,CAAI,MAAA,KAAW,CAAA,GAAI,GAAA,GAAM,IAAI,GAAG,CAAA,CAAA,CAAA;AACzC;AAMA,SAAS,eAAeA,SAAAA,EAA6B;AACnD,EAAA,MAAM,KAAA,GAAQA,SAAAA,CAAS,WAAA,EAAa,SAAA,CAAU,SAAA,IAAa,EAAA;AAC3D,EAAA,OAAO,CAAC,oBAAA,CAAqB,IAAA,CAAK,KAAK,CAAA;AACzC;AAMA,SAAS,gBAAA,CAAiB,SAAsB,IAAA,EAAyB;AACvE,EAAA,IAAI,OAAA,GAA8B,OAAA;AAClC,EAAA,OAAO,YAAY,IAAA,EAAM;AACvB,IAAA,MAAM,IAAA,GAAe,OAAA,CAAQ,aAAA,CAAc,WAAA,IAAe,MAAA;AAC1D,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,gBAAA,CAAiB,OAAO,CAAA;AAC3C,IAAA,IAAI,KAAA,CAAM,YAAY,MAAA,IAAU,KAAA,CAAM,eAAe,QAAA,IAAY,KAAA,CAAM,YAAY,GAAA,EAAK;AACtF,MAAA,OAAO,KAAA;AAAA,IACT;AACA,IAAA,IAAI,OAAA,CAAQ,YAAA,CAAa,QAAQ,CAAA,EAAG,OAAO,KAAA;AAC3C,IAAA,OAAA,GACE,QAAQ,aAAA,KACP,OAAA,CAAQ,aAAA,KAAkB,IAAA,GAAO,OAAQ,IAAA,CAAK,YAAA,CAAA;AAAA,EACnD;AACA,EAAA,OAAO,IAAA;AACT;AAOA,eAAe,SAAA,CAAU,WAAgC,OAAA,EAAgC;AACvF,EAAA,IAAI,OAAA;AACJ,EAAA,MAAMC,OAAA;AAAA,IACJ,YAAY;AACV,MAAA,IAAI;AACF,QAAA,MAAM,SAAA,EAAU;AAAA,MAClB,SAAS,KAAA,EAAO;AACd,QAAA,IAAI,EAAE,KAAA,YAAiB,eAAA,IAAmB,KAAA,YAAiB,mBAAA,CAAA,EAAsB;AAC/E,UAAA,MAAM,KAAA;AAAA,QACR;AACA,QAAA,OAAA,GAAU,KAAA;AAAA,MACZ;AAAA,IACF,CAAA;AAAA,IACA,EAAE,OAAA;AAAQ,GACZ;AACA,EAAA,IAAI,OAAA,KAAY,QAAW,MAAM,OAAA;AACnC;AAYO,IAAM,YAAA,GAAN,MAAM,aAAA,SAAqB,KAAA,CAAM;AAAA,EACtC,WAAA,CACmB,GAAA,EACjB,KAAA,EACA,QAAA,EACA;AACA,IAAA,KAAA,CAAM,OAAO,QAAQ,CAAA;AAJJ,IAAA,IAAA,CAAA,GAAA,GAAA,GAAA;AAAA,EAKnB;AAAA,EALmB,GAAA;AAAA,EAOA,MAAM,QAAA,EAA2B;AAClD,IAAA,OAAO,IAAI,aAAA,CAAa,IAAA,CAAK,GAAA,EAAK,IAAA,CAAK,OAAO,QAAQ,CAAA;AAAA,EACxD;AAAA;AAAA,EAGU,SAAA,GAAyB;AACjC,IAAA,OAAQ,IAAA,CAAK,GAAA,CAAI,IAAA,IAAQ,QAAA,CAAS,IAAA;AAAA,EACpC;AAAA,EAEU,QAAQ,OAAA,EAA6C;AAC7D,IAAA,OAAO,UAAA,CAAW,KAAK,SAAA,EAAU,EAAG,KAAK,KAAA,EAAO,IAAA,CAAK,QAAA,EAAU,OAAA,EAAS,OAAO,CAAA;AAAA,EACjF;AAAA;AAAA,EAGU,OAAA,GAAyB;AACjC,IAAA,OAAO,MAAA,CAAO,KAAK,GAAG,CAAA;AAAA,EACxB;AAAA;AAAA,EAGA,MAAe,GAAA,GAAwB;AACrC,IAAA,OAAA,CAAQ,MAAM,WAAW,IAAA,CAAK,SAAA,IAAa,IAAA,CAAK,KAAA,EAAO,IAAA,CAAK,QAAQ,CAAA,EAAG,GAAA;AAAA,MACrE,CAAC,OAAA,EAAS,KAAA,KACR,IAAI,kBAAkB,IAAA,CAAK,GAAA,EAAK,IAAA,CAAK,KAAA,EAAOC,GAAA,CAAQ,IAAA,CAAK,QAAA,EAAU,KAAK,GAAG,OAAO;AAAA,KACtF;AAAA,EACF;AAAA;AAAA,EAIA,MAAe,MAAM,OAAA,EAAsC;AACzD,IAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAA;AAC1C,IAAA,MAAM,OAAA,CAAQ,OAAO,CAAA,CAAE,KAAA,CAAM,OAAO,CAAA;AACpC,IAAA,MAAM,KAAK,OAAA,EAAQ;AAAA,EACrB;AAAA,EAEA,MAAe,IAAA,CAAK,KAAA,EAAe,OAAA,EAAsC;AACvE,IAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAA;AAC1C,IAAA,MAAM,IAAA,GAAO,QAAQ,OAAO,CAAA;AAC5B,IAAA,MAAM,IAAA,CAAK,MAAM,OAAO,CAAA;AAGxB,IAAA,IAAI,UAAU,EAAA,EAAI,MAAM,IAAA,CAAK,IAAA,CAAK,SAAS,KAAK,CAAA;AAChD,IAAA,MAAM,KAAK,OAAA,EAAQ;AAAA,EACrB;AAAA,EAEA,MAAe,MAAM,OAAA,EAAsC;AACzD,IAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAA;AAC1C,IAAA,MAAM,OAAA,CAAQ,OAAO,CAAA,CAAE,KAAA,CAAM,OAAO,CAAA;AACpC,IAAA,MAAM,KAAK,OAAA,EAAQ;AAAA,EACrB;AAAA,EAEA,MAAe,MAAM,OAAA,EAAsC;AACzD,IAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAA;AAC1C,IAAA,IAAI,CAAE,QAA6B,OAAA,EAAS,MAAM,QAAQ,OAAO,CAAA,CAAE,MAAM,OAAO,CAAA;AAChF,IAAA,MAAM,KAAK,OAAA,EAAQ;AAAA,EACrB;AAAA,EAEA,MAAe,QAAQ,OAAA,EAAsC;AAC3D,IAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAA;AAC1C,IAAA,IAAK,QAA6B,OAAA,EAAS,MAAM,QAAQ,OAAO,CAAA,CAAE,MAAM,OAAO,CAAA;AAC/E,IAAA,MAAM,KAAK,OAAA,EAAQ;AAAA,EACrB;AAAA,EAEA,MAAe,YAAA,CAAa,KAAA,EAA0B,OAAA,EAAsC;AAC1F,IAAA,MAAM,OAAA,GAAW,MAAM,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAA;AAC3C,IAAA,MAAM,IAAA,GAAO,QAAQ,OAAO,CAAA;AAG5B,IAAA,IAAI,QAAQ,QAAA,EAAU;AACpB,MAAA,MAAM,QAAA,GAAW,CAAC,GAAG,OAAA,CAAQ,eAAe,CAAA,CAAE,GAAA,CAAI,CAAA,MAAA,KAAU,MAAA,CAAO,KAAK,CAAA;AACxE,MAAA,IAAI,SAAS,MAAA,GAAS,CAAA,QAAS,IAAA,CAAK,eAAA,CAAgB,SAAS,QAAQ,CAAA;AAAA,IACvE;AACA,IAAA,MAAM,IAAA,CAAK,aAAA,CAAc,OAAA,EAAS,KAAK,CAAA;AACvC,IAAA,MAAM,KAAK,OAAA,EAAQ;AAAA,EACrB;AAAA,EAEA,MAAe,MAAM,OAAA,EAAsC;AACzD,IAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAA;AAC1C,IAAA,MAAM,OAAA,CAAQ,OAAO,CAAA,CAAE,KAAA,CAAM,OAAO,CAAA;AACpC,IAAA,MAAM,KAAK,OAAA,EAAQ;AAAA,EACrB;AAAA,EAEA,MAAe,MAAM,OAAA,EAAsC;AACxD,IAAA,CAAC,MAAM,IAAA,CAAK,OAAA,CAAQ,OAAO,GAAG,KAAA,EAAM;AACrC,IAAA,MAAM,KAAK,OAAA,EAAQ;AAAA,EACrB;AAAA,EAEA,MAAe,KAAK,OAAA,EAAsC;AACvD,IAAA,CAAC,MAAM,IAAA,CAAK,OAAA,CAAQ,OAAO,GAAG,IAAA,EAAK;AACpC,IAAA,MAAM,KAAK,OAAA,EAAQ;AAAA,EACrB;AAAA,EAEA,MAAe,KAAA,CAAM,GAAA,EAAa,OAAA,EAAsC;AACtE,IAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAA;AAE1C,IAAA,OAAA,CAAQ,KAAA,EAAM;AACd,IAAA,MAAM,QAAQ,OAAO,CAAA,CAAE,QAAA,CAAS,eAAA,CAAgB,GAAG,CAAC,CAAA;AACpD,IAAA,MAAM,KAAK,OAAA,EAAQ;AAAA,EACrB;AAAA,EAEA,MAAe,MAAA,CAAO,KAAA,EAAsB,OAAA,EAAsC;AAChF,IAAA,MAAM,OAAO,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,GAAI,KAAA,GAAQ,CAAC,KAAK,CAAA;AAClD,IAAA,MAAM,QAAQ,WAAA,CAAY,MAAM,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAC,CAAA;AACrD,IAAA,eAAA,CAAgB,OAAO,IAAA,CAAK,MAAA,EAAQ,IAAA,CAAK,KAAA,EAAO,KAAK,QAAQ,CAAA;AAC7D,IAAA,MAAM,OAAA,CAAQ,KAAK,CAAA,CAAE,MAAA,CAAO,OAAO,IAAI,CAAA;AACvC,IAAA,MAAM,KAAK,OAAA,EAAQ;AAAA,EACrB;AAAA;AAAA,EAIA,MAAe,KAAK,OAAA,EAAwC;AAC1D,IAAA,OAAA,CAAA,CAAS,MAAM,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAA,EAAG,WAAA,IAAe,IAAI,IAAA,EAAK;AAAA,EAChE;AAAA,EAEA,MAAe,WAAW,OAAA,EAAwC;AAChE,IAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAA;AAC1C,IAAA,OAAQ,QAAuE,KAAA,IAAS,EAAA;AAAA,EAC1F;AAAA,EAEA,MAAe,SAAA,CAAU,IAAA,EAAc,OAAA,EAA+C;AACpF,IAAA,OAAA,CAAQ,MAAM,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAA,EAAG,aAAa,IAAI,CAAA;AAAA,EACxD;AAAA,EAEA,MAAe,UAAU,OAAA,EAAyC;AAChE,IAAA,IAAI;AACF,MAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAA;AAC1C,MAAA,MAAM,IAAA,GAAO,IAAA,CAAK,SAAA,EAAU,CAAE,aAAA;AAG9B,MAAA,OAAO,cAAA,CAAe,IAAI,CAAA,GACtB,iBAAA,CAAkB,SAAS,IAAI,CAAA,GAC/B,gBAAA,CAAiB,OAAA,EAAS,IAAI,CAAA;AAAA,IACpC,SAAS,KAAA,EAAO;AAGd,MAAA,IAAI,KAAA,YAAiB,eAAA,IAAmB,KAAA,YAAiB,mBAAA,EAAqB,MAAM,KAAA;AAEpF,MAAA,OAAO,KAAA;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAe,UAAU,OAAA,EAAyC;AAChE,IAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAA;AAE1C,IAAA,MAAM,WACH,OAAA,CAA6B,QAAA,KAAa,QAAQ,OAAA,CAAQ,OAAA,CAAQ,WAAW,CAAA,KAAM,IAAA;AACtF,IAAA,OAAO,WAAA,CAAY,QAAA,EAAU,OAAA,CAAQ,YAAA,CAAa,eAAe,CAAC,CAAA;AAAA,EACpE;AAAA,EAEA,MAAe,UAAU,OAAA,EAAyC;AAChE,IAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAA;AAC1C,IAAA,OAAO,WAAA;AAAA,MACL,OAAA,CAAQ,aAAa,cAAc,CAAA;AAAA,MACnC,OAAA,CAAS,QAA6B,OAAO;AAAA,KAC/C;AAAA,EACF;AAAA,EAEA,MAAe,gBAAgB,OAAA,EAA0C;AACvE,IAAA,MAAM,OAAA,GAAW,MAAM,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAA;AAC3C,IAAA,OAAO,CAAC,GAAI,OAAA,CAAQ,eAAA,IAAmB,EAAG,CAAA,CAAE,GAAA,CAAI,CAAA,MAAA,KAAU,MAAA,CAAO,KAAK,CAAA;AAAA,EACxE;AAAA;AAAA,EAGA,MAAe,YAAY,OAAA,EAA6C;AACtE,IAAA,OAAO,aAAA,CAAc,MAAM,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAA,EAAG,IAAA,CAAK,SAAA,EAAU,CAAE,aAAa,CAAA;AAAA,EAClF;AAAA;AAAA,EAIA,MAAe,OAAA,CAAQ,KAAA,EAAkB,OAAA,EAAsC;AAC7E,IAAA,MAAM,OAAA,GAAU,UAAA,CAAW,OAAA,EAAS,OAAO,CAAA;AAC3C,IAAA,IAAI,UAAU,SAAA,EAAW;AACvB,MAAA,MAAM,UAAU,YAAY;AAC1B,QAAA,IAAI,CAAE,MAAM,IAAA,CAAK,SAAA,CAAU,EAAE,OAAA,EAAS,CAAA,EAAI,MAAM,IAAI,KAAA,CAAM,iBAAiB,CAAA;AAAA,MAC7E,GAAG,OAAO,CAAA;AAAA,IACZ,CAAA,MAAO;AACL,MAAA,MAAM,UAAU,YAAY;AAC1B,QAAA,IAAK,MAAM,IAAA,CAAK,KAAA,EAAM,KAAO,CAAA,IAAM,MAAM,IAAA,CAAK,SAAA,CAAU,EAAE,OAAA,EAAS,CAAA,EAAI;AACrE,UAAA,MAAM,IAAI,MAAM,eAAe,CAAA;AAAA,QACjC;AAAA,MACF,GAAG,OAAO,CAAA;AAAA,IACZ;AAGA,IAAA,MAAM,KAAK,OAAA,EAAQ;AAAA,EACrB;AAAA,EAEA,MAAe,KAAA,GAAyB;AACtC,IAAA,OAAO,SAAS,IAAA,CAAK,SAAA,IAAa,IAAA,CAAK,KAAA,EAAO,KAAK,QAAQ,CAAA;AAAA,EAC7D;AACF;AAGA,IAAM,iBAAA,GAAN,cAAgC,YAAA,CAAa;AAAA,EAC3C,WAAA,CACE,GAAA,EACA,KAAA,EACA,QAAA,EACiB,KAAA,EACjB;AACA,IAAA,KAAA,CAAM,GAAA,EAAK,OAAO,QAAQ,CAAA;AAFT,IAAA,IAAA,CAAA,KAAA,GAAA,KAAA;AAAA,EAGnB;AAAA,EAHmB,KAAA;AAAA,EAKnB,MAAyB,QAAQ,OAAA,EAA6C;AAC5E,IAAA,IAAI,IAAA,CAAK,KAAA,CAAM,WAAA,EAAa,OAAO,IAAA,CAAK,KAAA;AAGxC,IAAA,OAAO,KAAA,CAAM,QAAQ,OAAO,CAAA;AAAA,EAC9B;AACF,CAAA;AAEA,cAAA,CAAe,cAAA,EAAgB,CAAC,GAAA,EAAgB,KAAA,EAAO,QAAA,KAAa;AAClE,EAAA,OAAO,IAAI,YAAA,CAAa,GAAA,EAAmB,KAAA,EAAO,QAAQ,CAAA;AAC5D,CAAC,CAAA;ACvTD,SAAS,QAAQ,GAAA,EAAyB;AACxC,EAAA,MAAM,QAAA,GAAW,WAAW,GAAiB,CAAA;AAI7C,EAAA,IAAI,QAAA,CAAS,GAAA,CAAI,MAAA,EAAQ,IAAI,MAAM,IAAA,EAAM;AACvC,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,mBAAmB,cAAc,CAAA,yKAAA;AAAA,KAGnC;AAAA,EACF;AACA,EAAA,OAAO,EAAE,MAAA,EAAQ,QAAA,CAAS,GAAA,CAAI,MAAM,GAAG,QAAA,EAAU,QAAA,CAAS,GAAA,CAAI,QAAQ,CAAA,EAAE;AAC1E;AAYA,SAAS,SAAS,GAAA,EAAwB;AACxC,EAAA,MAAM,EAAE,MAAA,EAAQ,QAAA,EAAS,GAAI,QAAQ,GAAG,CAAA;AACxC,EAAA,IAAI,CAAC,OAAO,SAAA,EAAW;AACrB,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KAEF;AAAA,EACF;AACA,EAAA,OAAO,IAAI,IAAI,QAAA,CAAS,IAAA,IAAQ,MAAA,CAAO,QAAA,CAAS,MAAM,CAAA,CAAE,IAAA;AAC1D;AAGA,kBAAA,CAAmB,cAAA,EAAgB;AAAA,EACjC,MAAM,IAAA,CAAK,GAAA,EAAK,GAAA,EAAK;AACnB,IAAA,MAAM,EAAE,MAAA,EAAO,GAAI,OAAA,CAAQ,GAAG,CAAA;AAG9B,IAAA,MAAM,MAAA,CAAO,cAAc,GAAG,CAAA;AAC9B,IAAA,MAAM,OAAO,GAAiB,CAAA;AAAA,EAChC,CAAA;AAAA,EACA,WAAW,GAAA,EAAK;AACd,IAAA,OAAO,SAAS,GAAG,CAAA;AAAA,EACrB,CAAA;AAAA,EACA,MAAM,UAAA,CAAW,GAAA,EAAK,OAAA,EAAS,OAAA,EAAS;AAEtC,IAAA,QAAA,CAAS,GAAG,CAAA;AACZ,IAAA,MAAMD,OAAAA;AAAA,MACJ,MAAM;AACJ,QAAA,IAAI,CAAC,OAAA,CAAQ,IAAI,GAAA,CAAI,QAAA,CAAS,GAAG,CAAC,CAAC,CAAA,EAAG,MAAM,IAAI,KAAA,CAAM,CAAA,SAAA,EAAY,QAAA,CAAS,GAAG,CAAC,CAAA,CAAE,CAAA;AAAA,MACnF,CAAA;AAAA,MACA,EAAE,OAAA;AAAQ,KACZ;AAEA,IAAA,MAAM,OAAO,GAAiB,CAAA;AAAA,EAChC;AACF,CAAC,CAAA;;;AClBM,SAAS,QAAQ,EAAE,OAAA,EAAS,KAAK,IAAA,EAAK,GAAuB,EAAC,EAAe;AAClF,EAAA,IAAI,OAAA,KAAY,MAAA,IAAa,GAAA,KAAQ,MAAA,EAAW;AAC9C,IAAA,MAAM,IAAI,SAAA;AAAA,MACR;AAAA,KAEF;AAAA,EACF;AACA,EAAA,OAAO,EAAE,MAAA,EAAQ,cAAA,EAAgB,OAAA,EAAS,KAAK,IAAA,EAAK;AACtD","file":"index.js","sourcesContent":["/** The registry key for this driver. Its own module so env and query share it without a cycle. */\nexport const ANGULAR_DRIVER = 'angular'\n","import type { Injector } from '@angular/core'\nimport type { AngularEnv } from './env'\n\n/**\n * The app's settle point: the fixture's `whenStable()` if there is one, else\n * the `ApplicationRef`'s. Zoneless, both wait for Angular's pending tasks — a\n * scheduled change detection, a navigation, an `HttpClient` request — and not\n * for a bare `setTimeout`, which a zoneless app does not track.\n */\nexport async function settle(env: AngularEnv): Promise<void> {\n await (env.fixture ?? env.app)?.whenStable()\n}\n\n/** Where the app's services live: the fixture's component injector, or the app's. */\nexport function injectorOf(env: AngularEnv): Injector {\n return (env.fixture?.componentRef.injector ?? env.app?.injector) as Injector\n}\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 { waitFor as waitForCondition } from '@testing-library/dom'\nimport userEvent from '@testing-library/user-event'\nimport type { UserEvent } from '@testing-library/user-event'\nimport { ANGULAR_DRIVER } from './driver-id'\nimport type { AngularEnv } from './env'\nimport { settle } from './stable'\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/**\n * Whether the engine lays pages out. jsdom and happy-dom do not: every box is\n * zero there, so a layout check would call everything invisible.\n */\nfunction computesLayout(document: Document): boolean {\n const agent = document.defaultView?.navigator.userAgent ?? ''\n return !/\\bjsdom\\b|HappyDOM/.test(agent)\n}\n\n/**\n * The dom driver's rule, for an engine with no layout: computed style up the\n * tree, through the frames the scope entered. `home` is the env's own document.\n */\nfunction isVisibleByStyle(element: HTMLElement, home: Document): boolean {\n let current: HTMLElement | null = element\n while (current !== null) {\n const view: Window = current.ownerDocument.defaultView ?? window\n const style = view.getComputedStyle(current)\n if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {\n return false\n }\n if (current.hasAttribute('hidden')) return false\n current =\n current.parentElement ??\n (current.ownerDocument === home ? null : (view.frameElement as HTMLElement | null))\n }\n return true\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.\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 * Angular driver. Resolution is the shared resolver, so it agrees with every\n * other in-page driver. Interactions go through user-event, as in the dom\n * driver, and each one returns only once the app is stable: zoneless, an event\n * handler that changes a signal schedules change detection rather than running\n * it, so the DOM a test reads next is the one `whenStable()` hands back.\n *\n * Each action 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 AngularQuery extends Query {\n constructor(\n private readonly env: AngularEnv,\n scope: readonly Selector[],\n selector: Selector,\n ) {\n super(scope, selector)\n }\n\n protected override clone(selector: Selector): Query {\n return new AngularQuery(this.env, this.scope, selector)\n }\n\n /** Read per call: the body belongs to the current test, not the env. */\n protected container(): HTMLElement {\n return (this.env.root ?? document.body) 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 /** Ends an action where the app does: rendered, with nothing pending. */\n protected settled(): Promise<void> {\n return settle(this.env)\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 BoundAngularQuery(this.env, this.scope, withNth(this.selector, index), element),\n )\n }\n\n // --- interactions -------------------------------------------------------\n\n override async click(options?: WaitOptions): Promise<void> {\n const element = await this.element(options)\n await userFor(element).click(element)\n await this.settled()\n }\n\n override async fill(value: string, options?: WaitOptions): Promise<void> {\n const element = await this.element(options)\n const user = userFor(element)\n await user.clear(element)\n // user-event throws on an empty string, and clearing is the whole intent\n // anyway — which is also what Playwright's fill('') does.\n if (value !== '') await user.type(element, value)\n await this.settled()\n }\n\n override async clear(options?: WaitOptions): Promise<void> {\n const element = await this.element(options)\n await userFor(element).clear(element)\n await this.settled()\n }\n\n override async check(options?: WaitOptions): Promise<void> {\n const element = await this.element(options)\n if (!(element as HTMLInputElement).checked) await userFor(element).click(element)\n await this.settled()\n }\n\n override async uncheck(options?: WaitOptions): Promise<void> {\n const element = await this.element(options)\n if ((element as HTMLInputElement).checked) await userFor(element).click(element)\n await this.settled()\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.\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 this.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 this.settled()\n }\n\n override async focus(options?: WaitOptions): Promise<void> {\n ;(await this.element(options)).focus()\n await this.settled()\n }\n\n override async blur(options?: WaitOptions): Promise<void> {\n ;(await this.element(options)).blur()\n await this.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 element.focus()\n await userFor(element).keyboard(toKeyboardInput(key))\n await this.settled()\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 await userFor(input).upload(input, list)\n await this.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 const element = await this.element(options)\n const home = this.container().ownerDocument\n // In a browser, a real layout check — Playwright's rule. Under jsdom,\n // which lays nothing out, the dom driver's computed-style walk.\n return computesLayout(home)\n ? isVisibleInLayout(element, home)\n : isVisibleByStyle(element, home)\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 /** Under jsdom, which computes no layout, every box is all zeros. */\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 // stable app, as every interaction does.\n await this.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 BoundAngularQuery extends AngularQuery {\n constructor(\n env: AngularEnv,\n scope: readonly Selector[],\n selector: Selector,\n private readonly bound: HTMLElement,\n ) {\n super(env, 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\nregisterDriver(ANGULAR_DRIVER, (env: EnvConfig, scope, selector) => {\n return new AngularQuery(env as AngularEnv, scope, selector)\n})\n","import { Location } from '@angular/common'\nimport { Router, ROUTES } from '@angular/router'\nimport { registerNavigation } from '@harnessed-ts/core'\nimport type { EnvConfig } from '@harnessed-ts/core'\nimport { waitFor as waitForCondition } from '@testing-library/dom'\nimport { ANGULAR_DRIVER } from './driver-id'\nimport type { AngularEnv } from './env'\nimport { injectorOf, settle } from './stable'\n\ninterface Routing {\n router: Router\n location: Location\n}\n\n/**\n * The app's router, or the standard refusal. A component rendered on its own\n * has no router to drive — the Angular shape of a jsdom test — and a page there\n * constructs and reads, but cannot navigate.\n */\nfunction routing(env: EnvConfig): Routing {\n const injector = injectorOf(env as AngularEnv)\n // Not `get(Router, null)`: the Router is provided in root, so it always\n // resolves — to one with no routes, which matches no URL. The ROUTES token is\n // what `provideRouter()` and `RouterModule.forRoot()` add.\n if (injector.get(ROUTES, null) === null) {\n throw new Error(\n `harnessed: the \"${ANGULAR_DRIVER}\" driver cannot navigate without a Router, so a page ` +\n 'cannot navigate with it. Provide one to the app or the TestBed (provideRouter()), or ' +\n 'reach the page by rendering it.',\n )\n }\n return { router: injector.get(Router), location: injector.get(Location) }\n}\n\n/**\n * The router's URL, made absolute. `Location.path()` is the truth, not\n * `window.location`: TestBed swaps in a mock platform location, so a test's\n * navigations never reach the address bar. Absolute because a page reads\n * `pathname` and `searchParams` off it with `new URL()`.\n *\n * There is no URL before the router's first navigation — TestBed performs\n * none of its own. Reading that as `/` would let `assertPathname('/')` pass\n * before anything navigated, so it refuses.\n */\nfunction absolute(env: EnvConfig): string {\n const { router, location } = routing(env)\n if (!router.navigated) {\n throw new Error(\n 'harnessed: the router has not navigated yet. Call a page’s goto(), or ' +\n 'router.navigateByUrl(), first.',\n )\n }\n return new URL(location.path(), window.location.origin).href\n}\n\n/** Navigation through the app's Router, wherever one is provided. */\nregisterNavigation(ANGULAR_DRIVER, {\n async goto(env, url) {\n const { router } = routing(env)\n // Resolves once the navigation ends; whenStable() then waits out the\n // change detection it scheduled, so the destination is on screen.\n await router.navigateByUrl(url)\n await settle(env as AngularEnv)\n },\n currentUrl(env) {\n return absolute(env)\n },\n async waitForUrl(env, matches, timeout) {\n // Outside the retry loop: no router is a refusal, not something to wait out.\n absolute(env)\n await waitForCondition(\n () => {\n if (!matches(new URL(absolute(env)))) throw new Error(`still at ${absolute(env)}`)\n },\n { timeout },\n )\n // The URL changes before the destination has rendered.\n await settle(env as AngularEnv)\n },\n})\n","import type { ApplicationRef, ComponentRef } from '@angular/core'\nimport type { EnvConfig } from '@harnessed-ts/core'\nimport { ANGULAR_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 './angular-query'\nimport './navigation'\n\nexport { ANGULAR_DRIVER }\n\n/**\n * What the driver needs of a `ComponentFixture`: a structural type, so a\n * fixture from TestBed, `@testing-library/angular` or `cypress/angular` all\n * fit, and nothing here imports `@angular/core/testing` at runtime.\n */\nexport interface AngularFixture {\n whenStable(): Promise<unknown>\n readonly componentRef: Pick<ComponentRef<unknown>, 'injector'>\n}\n\nexport interface AngularEnv extends EnvConfig {\n readonly driver: typeof ANGULAR_DRIVER\n /** A test's fixture. Every action ends at its `whenStable()`. */\n readonly fixture: AngularFixture | undefined\n /** A bootstrapped app. Without a fixture, every action ends at its `whenStable()`. */\n readonly app: ApplicationRef | undefined\n /**\n * Where queries start, scoped and `{ global: true }` alike. Defaults to\n * `document.body`, read at query time: TestBed attaches each fixture there,\n * and the CDK's overlays and other portals render there too.\n */\n readonly root: Element | undefined\n}\n\nexport interface AngularEnvOptions {\n /** A `ComponentFixture`: TestBed's `createComponent()`, or a render result's `fixture`. */\n fixture?: AngularFixture\n /** The `ApplicationRef` `bootstrapApplication()` resolves to, for an app with no fixture. */\n app?: ApplicationRef\n root?: Element\n}\n\n/**\n * Builds the env a harness is constructed with in an Angular test:\n *\n * ```ts\n * const { fixture } = await render(LoginForm)\n * const form = new LoginFormHarness(angular({ fixture }))\n * ```\n *\n * or, for an app bootstrapped without TestBed:\n *\n * ```ts\n * const app = await bootstrapApplication(App, appConfig)\n * const page = new CheckoutPage(angular({ app }))\n * ```\n *\n * One of the two is required. Nothing else finds an app reliably: the `ng`\n * global and `getAllAngularRootElements()` exist only in development builds,\n * and testabilities only where zone.js is loaded.\n */\nexport function angular({ fixture, app, root }: AngularEnvOptions = {}): AngularEnv {\n if (fixture === undefined && app === undefined) {\n throw new TypeError(\n 'harnessed: angular() needs the app it drives — pass { fixture } from TestBed or a ' +\n 'render result, or { app } with the ApplicationRef bootstrapApplication() resolved to.',\n )\n }\n return { driver: ANGULAR_DRIVER, fixture, app, root }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../src/navigation.ts"],"names":[],"mappings":""}
|
package/dist/stable.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Injector } from '@angular/core';
|
|
2
|
+
import type { AngularEnv } from './env';
|
|
3
|
+
/**
|
|
4
|
+
* The app's settle point: the fixture's `whenStable()` if there is one, else
|
|
5
|
+
* the `ApplicationRef`'s. Zoneless, both wait for Angular's pending tasks — a
|
|
6
|
+
* scheduled change detection, a navigation, an `HttpClient` request — and not
|
|
7
|
+
* for a bare `setTimeout`, which a zoneless app does not track.
|
|
8
|
+
*/
|
|
9
|
+
export declare function settle(env: AngularEnv): Promise<void>;
|
|
10
|
+
/** Where the app's services live: the fixture's component injector, or the app's. */
|
|
11
|
+
export declare function injectorOf(env: AngularEnv): Injector;
|
|
12
|
+
//# sourceMappingURL=stable.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stable.d.ts","sourceRoot":"","sources":["../src/stable.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAC7C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,OAAO,CAAA;AAEvC;;;;;GAKG;AACH,wBAAsB,MAAM,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAE3D;AAED,qFAAqF;AACrF,wBAAgB,UAAU,CAAC,GAAG,EAAE,UAAU,GAAG,QAAQ,CAEpD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,78 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@harnessed-ts/angular",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Angular driver for @harnessed-ts/core: interactions through user-event, each one ending once the app is stable. TestBed, Testing Library and bootstrapped apps, zoneless.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"testing",
|
|
7
|
+
"harness",
|
|
8
|
+
"angular",
|
|
9
|
+
"testbed",
|
|
10
|
+
"testing-library"
|
|
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/angular"
|
|
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
|
+
"@angular/common": ">=21",
|
|
57
|
+
"@angular/core": ">=21",
|
|
58
|
+
"@angular/router": ">=21",
|
|
59
|
+
"@testing-library/dom": ">=10",
|
|
60
|
+
"@testing-library/user-event": ">=14"
|
|
61
|
+
},
|
|
62
|
+
"publishConfig": {
|
|
63
|
+
"access": "public"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@angular/common": "^22.2.0",
|
|
67
|
+
"@angular/core": "^22.2.0",
|
|
68
|
+
"@angular/platform-browser": "^22.2.0",
|
|
69
|
+
"@angular/router": "^22.2.0",
|
|
70
|
+
"@testing-library/dom": "^10.4.2",
|
|
71
|
+
"@testing-library/user-event": "^14.6.7",
|
|
72
|
+
"rxjs": "^7.8.2"
|
|
73
|
+
},
|
|
74
|
+
"scripts": {
|
|
75
|
+
"build": "tsup",
|
|
76
|
+
"clean": "rm -rf dist *.tsbuildinfo"
|
|
77
|
+
}
|
|
6
78
|
}
|