@cdevhub/ngx-tw 0.7.0 → 0.8.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/fesm2022/cdevhub-ngx-tw-popover-testing.mjs +213 -0
- package/fesm2022/cdevhub-ngx-tw-popover-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-tooltip-testing.mjs +195 -0
- package/fesm2022/cdevhub-ngx-tw-tooltip-testing.mjs.map +1 -0
- package/index.json +1 -1
- package/package.json +9 -1
- package/types/cdevhub-ngx-tw-popover-testing.d.ts +138 -0
- package/types/cdevhub-ngx-tw-tooltip-testing.d.ts +131 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { ComponentHarness, manualChangeDetection, HarnessPredicate, TestKey } from '@angular/cdk/testing';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lets the zoneless change-detection scheduler run its pending tick.
|
|
5
|
+
*
|
|
6
|
+
* `ApplicationRef` schedules a tick with `setTimeout(cb)` raced against
|
|
7
|
+
* `requestAnimationFrame`. A timer registered *after* the notify that dirtied a
|
|
8
|
+
* signal therefore fires *after* that tick, so one macrotask is enough to
|
|
9
|
+
* observe everything already scheduled. It is a fixed, bounded yield, not a
|
|
10
|
+
* stabilization await, so it cannot hang.
|
|
11
|
+
*
|
|
12
|
+
* Every method spends one: an action yields after dispatching, so the effects
|
|
13
|
+
* the directive applied synchronously are rendered; a read yields before
|
|
14
|
+
* looking, so it sees any tick that was already pending. The composition is what
|
|
15
|
+
* matters — `open()` returns while the panel's first render is still pending,
|
|
16
|
+
* and the following read's own yield is what picks the rendered content up.
|
|
17
|
+
*/
|
|
18
|
+
function afterSchedulerTick() {
|
|
19
|
+
return new Promise((resolve) => setTimeout(resolve));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Harness for a `[twPopover]` trigger and the panel it opens.
|
|
23
|
+
*
|
|
24
|
+
* ## Nothing here awaits application stabilization, and that is load-bearing
|
|
25
|
+
*
|
|
26
|
+
* `TestbedHarnessEnvironment` routes every `TestElement` operation through
|
|
27
|
+
* `forceStabilize()` — `fixture.detectChanges()` then
|
|
28
|
+
* `await fixture.whenStable()` — and that await resolves only when Angular's
|
|
29
|
+
* `PendingTasks` set is empty. Under full-suite contention it was observed
|
|
30
|
+
* **not to resolve at all**, and everything built on it hung for the whole test
|
|
31
|
+
* budget instead of failing. This harness was withdrawn twice for that.
|
|
32
|
+
*
|
|
33
|
+
* Every method body therefore runs inside CDK's `manualChangeDetection()`,
|
|
34
|
+
* which sets the flag `forceStabilize()` early-returns on, and so does
|
|
35
|
+
* acquisition, via {@link load} / {@link loadAll}. The spec beside this file
|
|
36
|
+
* adds the third piece: it never awaits `fixture.whenStable()` either, not even
|
|
37
|
+
* in `beforeEach`. All three were needed — each of the two CI failures during
|
|
38
|
+
* this restoration was traced to one of them, and the second landed on
|
|
39
|
+
* `tooltip` rather than here, which is how it became clear the fault belongs to
|
|
40
|
+
* whichever harness spec lands in the unlucky worker slot rather than to any
|
|
41
|
+
* one component. `grep -c whenStable` over this file and its spec returns zero,
|
|
42
|
+
* which is the whole claim and is checkable in one command rather than by
|
|
43
|
+
* counting green runs. The spec pins the rest with tests that hold a real
|
|
44
|
+
* `PendingTasks` entry open across acquisition and every method.
|
|
45
|
+
*
|
|
46
|
+
* Why the application stops stabilizing is **not** known; this removes the
|
|
47
|
+
* dependency rather than curing it.
|
|
48
|
+
*
|
|
49
|
+
* The cost is that change detection is not forced on your behalf. Instead every
|
|
50
|
+
* method spends one macrotask on the scheduler (see {@link afterSchedulerTick}),
|
|
51
|
+
* which covers everything already scheduled — including the panel's first
|
|
52
|
+
* render. What it does not cover is state behind the component's own timers:
|
|
53
|
+
* {@link close} dispatches Escape and returns, and the panel detaches only after
|
|
54
|
+
* the 120 ms leave window in `popover.ts`. Wait for that by polling the DOM —
|
|
55
|
+
* `document.querySelector` needs no stabilization and so can neither hang nor
|
|
56
|
+
* burn a fixed interval — and only then read through the harness.
|
|
57
|
+
*
|
|
58
|
+
* ## Loading it
|
|
59
|
+
*
|
|
60
|
+
* The host is the trigger, which lives in the fixture, so the ordinary
|
|
61
|
+
* `TestbedHarnessEnvironment.loader(fixture)` is correct. The panel renders into
|
|
62
|
+
* the CDK overlay container outside the fixture, and this harness resolves it
|
|
63
|
+
* internally via `documentRootLocatorFactory()` — a consumer never needs
|
|
64
|
+
* `documentRootLoader`.
|
|
65
|
+
*
|
|
66
|
+
* The host selector is the directive's static `data-tw-popover-trigger` marker.
|
|
67
|
+
* `[twPopover]` cannot be used: it takes a required `TemplateRef` or component
|
|
68
|
+
* type, so it is always property-bound and Angular renders no attribute for a
|
|
69
|
+
* bound input. The marker also makes the match exact, so no disambiguation
|
|
70
|
+
* against `aria-haspopup="dialog"` — which the two date-picker triggers also
|
|
71
|
+
* carry — is needed.
|
|
72
|
+
*
|
|
73
|
+
* ## The panel is detached, not disposed
|
|
74
|
+
*
|
|
75
|
+
* Unlike `tw-select`, closing a popover **detaches** the portal and keeps the
|
|
76
|
+
* `OverlayRef` for reuse; it is only rebuilt when `twPopoverBackdrop` or
|
|
77
|
+
* `twPopoverScrollStrategy` changes. The panel element is therefore absent while
|
|
78
|
+
* closed and present again after a reopen, on the same overlay.
|
|
79
|
+
*/
|
|
80
|
+
class PopoverHarness extends ComponentHarness {
|
|
81
|
+
static hostSelector = '[data-tw-popover-trigger]';
|
|
82
|
+
/**
|
|
83
|
+
* Acquires one harness without waiting for the application to stabilize —
|
|
84
|
+
* the counterpart to the guarantee the methods below make.
|
|
85
|
+
*
|
|
86
|
+
* `loader.getHarness(...)` is CDK's own acquisition path and it stabilizes:
|
|
87
|
+
* `getAllRawElements` calls `forceStabilize()`, and `HarnessPredicate`
|
|
88
|
+
* filtering routes through `parallel()`, which asks *every* active fixture in
|
|
89
|
+
* the worker to settle. Both await `fixture.whenStable()`, which is the one
|
|
90
|
+
* thing this harness exists to avoid — and the failure that withdrew it was
|
|
91
|
+
* observed there, at acquisition, before any method had run.
|
|
92
|
+
*
|
|
93
|
+
* So acquisition is wrapped too, and `manualChangeDetection()` nests: the
|
|
94
|
+
* inner `parallel()` sees the flag already set and skips the stabilization
|
|
95
|
+
* entirely. **Render the fixture first** (`fixture.detectChanges()`), because
|
|
96
|
+
* nothing here will do it for you; an unrendered fixture fails loudly with
|
|
97
|
+
* CDK's "failed to find element" rather than returning something wrong.
|
|
98
|
+
*
|
|
99
|
+
* Plain `loader.getHarness(PopoverHarness)` still works and is still supported.
|
|
100
|
+
* This is the path to use when a suite must not be able to hang.
|
|
101
|
+
*/
|
|
102
|
+
static load(loader, options = {}) {
|
|
103
|
+
return manualChangeDetection(() => loader.getHarness(PopoverHarness.with(options)));
|
|
104
|
+
}
|
|
105
|
+
/** {@link load} for every matching trigger rather than the first. */
|
|
106
|
+
static loadAll(loader, options = {}) {
|
|
107
|
+
return manualChangeDetection(() => loader.getAllHarnesses(PopoverHarness.with(options)));
|
|
108
|
+
}
|
|
109
|
+
/** Predicate for `locatorFor` / `locatorForAll`. */
|
|
110
|
+
static with(options = {}) {
|
|
111
|
+
return new HarnessPredicate(PopoverHarness, options).addOption('triggerText', options.triggerText, async (h, text) => HarnessPredicate.stringMatches(await h.getTriggerText(), text));
|
|
112
|
+
}
|
|
113
|
+
/** The text currently rendered in the trigger, trimmed. */
|
|
114
|
+
async getTriggerText() {
|
|
115
|
+
return manualChangeDetection(async () => {
|
|
116
|
+
await afterSchedulerTick();
|
|
117
|
+
return (await (await this.host()).text()).trim();
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/** Whether the popover is open, read from the trigger's `aria-expanded`. */
|
|
121
|
+
async isOpen() {
|
|
122
|
+
return manualChangeDetection(async () => {
|
|
123
|
+
await afterSchedulerTick();
|
|
124
|
+
return (await (await this.host()).getAttribute('aria-expanded')) === 'true';
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Opens the popover by clicking the trigger. No-op when already open.
|
|
129
|
+
*
|
|
130
|
+
* This is the gesture for the default `twPopoverTriggerOn="click"`. A
|
|
131
|
+
* `'focus'`- or `'manual'`-triggered popover is opened through the directive's
|
|
132
|
+
* own `open()` (reachable via `exportAs: 'twPopover'`), not through a click.
|
|
133
|
+
*/
|
|
134
|
+
async open() {
|
|
135
|
+
await manualChangeDetection(async () => {
|
|
136
|
+
await afterSchedulerTick();
|
|
137
|
+
const host = await this.host();
|
|
138
|
+
if ((await host.getAttribute('aria-expanded')) === 'true')
|
|
139
|
+
return;
|
|
140
|
+
await host.click();
|
|
141
|
+
await afterSchedulerTick();
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Closes the popover by sending Escape to the trigger — the one dismissal
|
|
146
|
+
* that works for click, focus and manual triggers alike. No-op when already
|
|
147
|
+
* closed, and deliberately inert when `twPopoverCloseOnEscape` is `false`.
|
|
148
|
+
*
|
|
149
|
+
* Returns as soon as the key is dispatched. The panel detaches only after the
|
|
150
|
+
* 120 ms leave window; poll the DOM for its removal before asserting.
|
|
151
|
+
*/
|
|
152
|
+
async close() {
|
|
153
|
+
await manualChangeDetection(async () => {
|
|
154
|
+
await afterSchedulerTick();
|
|
155
|
+
const host = await this.host();
|
|
156
|
+
if ((await host.getAttribute('aria-expanded')) !== 'true')
|
|
157
|
+
return;
|
|
158
|
+
await host.sendKeys(TestKey.ESCAPE);
|
|
159
|
+
await afterSchedulerTick();
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Text rendered inside the panel, trimmed, or `null` when the popover is
|
|
164
|
+
* closed and the panel is detached.
|
|
165
|
+
*/
|
|
166
|
+
async getText() {
|
|
167
|
+
return manualChangeDetection(async () => {
|
|
168
|
+
await afterSchedulerTick();
|
|
169
|
+
const panel = await this.getPanel();
|
|
170
|
+
return panel ? (await panel.text()).trim() : null;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Whether the panel renders its directional arrow (`twPopoverArrow`). `false`
|
|
175
|
+
* while the popover is closed, because the panel does not exist then.
|
|
176
|
+
*/
|
|
177
|
+
async hasArrow() {
|
|
178
|
+
return manualChangeDetection(async () => {
|
|
179
|
+
await afterSchedulerTick();
|
|
180
|
+
const id = await this.getPanelId();
|
|
181
|
+
if (!id)
|
|
182
|
+
return false;
|
|
183
|
+
// The arrow has no dedicated attribute hook: it is the panel wrapper's only
|
|
184
|
+
// `aria-hidden` grandchild span, with the content nested one level deeper.
|
|
185
|
+
const arrow = await this.documentRootLocatorFactory().locatorForOptional(`#${id} > div > span[aria-hidden="true"]`)();
|
|
186
|
+
return arrow !== null;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* The panel element, or `null` when the popover is closed. Callers are already
|
|
191
|
+
* inside `manualChangeDetection`.
|
|
192
|
+
*/
|
|
193
|
+
async getPanel() {
|
|
194
|
+
const id = await this.getPanelId();
|
|
195
|
+
if (!id)
|
|
196
|
+
return null;
|
|
197
|
+
return this.documentRootLocatorFactory().locatorForOptional(`#${id}`)();
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* The id of this trigger's own panel, or `null` when closed. Scoping by
|
|
201
|
+
* `aria-controls` keeps sibling popovers apart.
|
|
202
|
+
*/
|
|
203
|
+
async getPanelId() {
|
|
204
|
+
return (await this.host()).getAttribute('aria-controls');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Generated bundle index. Do not edit.
|
|
210
|
+
*/
|
|
211
|
+
|
|
212
|
+
export { PopoverHarness };
|
|
213
|
+
//# sourceMappingURL=cdevhub-ngx-tw-popover-testing.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cdevhub-ngx-tw-popover-testing.mjs","sources":["../../../projects/ngx-tw/popover/testing/popover-harness.ts","../../../projects/ngx-tw/popover/testing/cdevhub-ngx-tw-popover-testing.ts"],"sourcesContent":["import {\n ComponentHarness,\n HarnessPredicate,\n TestKey,\n manualChangeDetection,\n} from '@angular/cdk/testing';\nimport type { BaseHarnessFilters, HarnessLoader, TestElement } from '@angular/cdk/testing';\n\n/** Filters accepted by `PopoverHarness.with`. */\nexport interface PopoverHarnessFilters extends BaseHarnessFilters {\n /** Match by the text rendered in the trigger. */\n triggerText?: string | RegExp;\n}\n\n/**\n * Lets the zoneless change-detection scheduler run its pending tick.\n *\n * `ApplicationRef` schedules a tick with `setTimeout(cb)` raced against\n * `requestAnimationFrame`. A timer registered *after* the notify that dirtied a\n * signal therefore fires *after* that tick, so one macrotask is enough to\n * observe everything already scheduled. It is a fixed, bounded yield, not a\n * stabilization await, so it cannot hang.\n *\n * Every method spends one: an action yields after dispatching, so the effects\n * the directive applied synchronously are rendered; a read yields before\n * looking, so it sees any tick that was already pending. The composition is what\n * matters — `open()` returns while the panel's first render is still pending,\n * and the following read's own yield is what picks the rendered content up.\n */\nfunction afterSchedulerTick(): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve));\n}\n\n/**\n * Harness for a `[twPopover]` trigger and the panel it opens.\n *\n * ## Nothing here awaits application stabilization, and that is load-bearing\n *\n * `TestbedHarnessEnvironment` routes every `TestElement` operation through\n * `forceStabilize()` — `fixture.detectChanges()` then\n * `await fixture.whenStable()` — and that await resolves only when Angular's\n * `PendingTasks` set is empty. Under full-suite contention it was observed\n * **not to resolve at all**, and everything built on it hung for the whole test\n * budget instead of failing. This harness was withdrawn twice for that.\n *\n * Every method body therefore runs inside CDK's `manualChangeDetection()`,\n * which sets the flag `forceStabilize()` early-returns on, and so does\n * acquisition, via {@link load} / {@link loadAll}. The spec beside this file\n * adds the third piece: it never awaits `fixture.whenStable()` either, not even\n * in `beforeEach`. All three were needed — each of the two CI failures during\n * this restoration was traced to one of them, and the second landed on\n * `tooltip` rather than here, which is how it became clear the fault belongs to\n * whichever harness spec lands in the unlucky worker slot rather than to any\n * one component. `grep -c whenStable` over this file and its spec returns zero,\n * which is the whole claim and is checkable in one command rather than by\n * counting green runs. The spec pins the rest with tests that hold a real\n * `PendingTasks` entry open across acquisition and every method.\n *\n * Why the application stops stabilizing is **not** known; this removes the\n * dependency rather than curing it.\n *\n * The cost is that change detection is not forced on your behalf. Instead every\n * method spends one macrotask on the scheduler (see {@link afterSchedulerTick}),\n * which covers everything already scheduled — including the panel's first\n * render. What it does not cover is state behind the component's own timers:\n * {@link close} dispatches Escape and returns, and the panel detaches only after\n * the 120 ms leave window in `popover.ts`. Wait for that by polling the DOM —\n * `document.querySelector` needs no stabilization and so can neither hang nor\n * burn a fixed interval — and only then read through the harness.\n *\n * ## Loading it\n *\n * The host is the trigger, which lives in the fixture, so the ordinary\n * `TestbedHarnessEnvironment.loader(fixture)` is correct. The panel renders into\n * the CDK overlay container outside the fixture, and this harness resolves it\n * internally via `documentRootLocatorFactory()` — a consumer never needs\n * `documentRootLoader`.\n *\n * The host selector is the directive's static `data-tw-popover-trigger` marker.\n * `[twPopover]` cannot be used: it takes a required `TemplateRef` or component\n * type, so it is always property-bound and Angular renders no attribute for a\n * bound input. The marker also makes the match exact, so no disambiguation\n * against `aria-haspopup=\"dialog\"` — which the two date-picker triggers also\n * carry — is needed.\n *\n * ## The panel is detached, not disposed\n *\n * Unlike `tw-select`, closing a popover **detaches** the portal and keeps the\n * `OverlayRef` for reuse; it is only rebuilt when `twPopoverBackdrop` or\n * `twPopoverScrollStrategy` changes. The panel element is therefore absent while\n * closed and present again after a reopen, on the same overlay.\n */\nexport class PopoverHarness extends ComponentHarness {\n static hostSelector = '[data-tw-popover-trigger]';\n\n /**\n * Acquires one harness without waiting for the application to stabilize —\n * the counterpart to the guarantee the methods below make.\n *\n * `loader.getHarness(...)` is CDK's own acquisition path and it stabilizes:\n * `getAllRawElements` calls `forceStabilize()`, and `HarnessPredicate`\n * filtering routes through `parallel()`, which asks *every* active fixture in\n * the worker to settle. Both await `fixture.whenStable()`, which is the one\n * thing this harness exists to avoid — and the failure that withdrew it was\n * observed there, at acquisition, before any method had run.\n *\n * So acquisition is wrapped too, and `manualChangeDetection()` nests: the\n * inner `parallel()` sees the flag already set and skips the stabilization\n * entirely. **Render the fixture first** (`fixture.detectChanges()`), because\n * nothing here will do it for you; an unrendered fixture fails loudly with\n * CDK's \"failed to find element\" rather than returning something wrong.\n *\n * Plain `loader.getHarness(PopoverHarness)` still works and is still supported.\n * This is the path to use when a suite must not be able to hang.\n */\n static load(loader: HarnessLoader, options: PopoverHarnessFilters = {}): Promise<PopoverHarness> {\n return manualChangeDetection(() => loader.getHarness(PopoverHarness.with(options)));\n }\n\n /** {@link load} for every matching trigger rather than the first. */\n static loadAll(\n loader: HarnessLoader,\n options: PopoverHarnessFilters = {},\n ): Promise<PopoverHarness[]> {\n return manualChangeDetection(() => loader.getAllHarnesses(PopoverHarness.with(options)));\n }\n\n /** Predicate for `locatorFor` / `locatorForAll`. */\n static with(options: PopoverHarnessFilters = {}): HarnessPredicate<PopoverHarness> {\n return new HarnessPredicate(PopoverHarness, options).addOption(\n 'triggerText',\n options.triggerText,\n async (h, text) => HarnessPredicate.stringMatches(await h.getTriggerText(), text),\n );\n }\n\n /** The text currently rendered in the trigger, trimmed. */\n async getTriggerText(): Promise<string> {\n return manualChangeDetection(async () => {\n await afterSchedulerTick();\n return (await (await this.host()).text()).trim();\n });\n }\n\n /** Whether the popover is open, read from the trigger's `aria-expanded`. */\n async isOpen(): Promise<boolean> {\n return manualChangeDetection(async () => {\n await afterSchedulerTick();\n return (await (await this.host()).getAttribute('aria-expanded')) === 'true';\n });\n }\n\n /**\n * Opens the popover by clicking the trigger. No-op when already open.\n *\n * This is the gesture for the default `twPopoverTriggerOn=\"click\"`. A\n * `'focus'`- or `'manual'`-triggered popover is opened through the directive's\n * own `open()` (reachable via `exportAs: 'twPopover'`), not through a click.\n */\n async open(): Promise<void> {\n await manualChangeDetection(async () => {\n await afterSchedulerTick();\n const host = await this.host();\n if ((await host.getAttribute('aria-expanded')) === 'true') return;\n await host.click();\n await afterSchedulerTick();\n });\n }\n\n /**\n * Closes the popover by sending Escape to the trigger — the one dismissal\n * that works for click, focus and manual triggers alike. No-op when already\n * closed, and deliberately inert when `twPopoverCloseOnEscape` is `false`.\n *\n * Returns as soon as the key is dispatched. The panel detaches only after the\n * 120 ms leave window; poll the DOM for its removal before asserting.\n */\n async close(): Promise<void> {\n await manualChangeDetection(async () => {\n await afterSchedulerTick();\n const host = await this.host();\n if ((await host.getAttribute('aria-expanded')) !== 'true') return;\n await host.sendKeys(TestKey.ESCAPE);\n await afterSchedulerTick();\n });\n }\n\n /**\n * Text rendered inside the panel, trimmed, or `null` when the popover is\n * closed and the panel is detached.\n */\n async getText(): Promise<string | null> {\n return manualChangeDetection(async () => {\n await afterSchedulerTick();\n const panel = await this.getPanel();\n return panel ? (await panel.text()).trim() : null;\n });\n }\n\n /**\n * Whether the panel renders its directional arrow (`twPopoverArrow`). `false`\n * while the popover is closed, because the panel does not exist then.\n */\n async hasArrow(): Promise<boolean> {\n return manualChangeDetection(async () => {\n await afterSchedulerTick();\n const id = await this.getPanelId();\n if (!id) return false;\n // The arrow has no dedicated attribute hook: it is the panel wrapper's only\n // `aria-hidden` grandchild span, with the content nested one level deeper.\n const arrow = await this.documentRootLocatorFactory().locatorForOptional(\n `#${id} > div > span[aria-hidden=\"true\"]`,\n )();\n return arrow !== null;\n });\n }\n\n /**\n * The panel element, or `null` when the popover is closed. Callers are already\n * inside `manualChangeDetection`.\n */\n private async getPanel(): Promise<TestElement | null> {\n const id = await this.getPanelId();\n if (!id) return null;\n return this.documentRootLocatorFactory().locatorForOptional(`#${id}`)();\n }\n\n /**\n * The id of this trigger's own panel, or `null` when closed. Scoping by\n * `aria-controls` keeps sibling popovers apart.\n */\n private async getPanelId(): Promise<string | null> {\n return (await this.host()).getAttribute('aria-controls');\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;AAcA;;;;;;;;;;;;;;AAcG;AACH,SAAS,kBAAkB,GAAA;AACzB,IAAA,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,KAAK,UAAU,CAAC,OAAO,CAAC,CAAC;AACtD;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0DG;AACG,MAAO,cAAe,SAAQ,gBAAgB,CAAA;AAClD,IAAA,OAAO,YAAY,GAAG,2BAA2B;AAEjD;;;;;;;;;;;;;;;;;;;AAmBG;AACH,IAAA,OAAO,IAAI,CAAC,MAAqB,EAAE,UAAiC,EAAE,EAAA;AACpE,QAAA,OAAO,qBAAqB,CAAC,MAAM,MAAM,CAAC,UAAU,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;IACrF;;AAGA,IAAA,OAAO,OAAO,CACZ,MAAqB,EACrB,UAAiC,EAAE,EAAA;AAEnC,QAAA,OAAO,qBAAqB,CAAC,MAAM,MAAM,CAAC,eAAe,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;IAC1F;;AAGA,IAAA,OAAO,IAAI,CAAC,OAAA,GAAiC,EAAE,EAAA;AAC7C,QAAA,OAAO,IAAI,gBAAgB,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,SAAS,CAC5D,aAAa,EACb,OAAO,CAAC,WAAW,EACnB,OAAO,CAAC,EAAE,IAAI,KAAK,gBAAgB,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,cAAc,EAAE,EAAE,IAAI,CAAC,CAClF;IACH;;AAGA,IAAA,MAAM,cAAc,GAAA;AAClB,QAAA,OAAO,qBAAqB,CAAC,YAAW;YACtC,MAAM,kBAAkB,EAAE;AAC1B,YAAA,OAAO,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE;AAClD,QAAA,CAAC,CAAC;IACJ;;AAGA,IAAA,MAAM,MAAM,GAAA;AACV,QAAA,OAAO,qBAAqB,CAAC,YAAW;YACtC,MAAM,kBAAkB,EAAE;AAC1B,YAAA,OAAO,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,EAAE,YAAY,CAAC,eAAe,CAAC,MAAM,MAAM;AAC7E,QAAA,CAAC,CAAC;IACJ;AAEA;;;;;;AAMG;AACH,IAAA,MAAM,IAAI,GAAA;AACR,QAAA,MAAM,qBAAqB,CAAC,YAAW;YACrC,MAAM,kBAAkB,EAAE;AAC1B,YAAA,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE;YAC9B,IAAI,CAAC,MAAM,IAAI,CAAC,YAAY,CAAC,eAAe,CAAC,MAAM,MAAM;gBAAE;AAC3D,YAAA,MAAM,IAAI,CAAC,KAAK,EAAE;YAClB,MAAM,kBAAkB,EAAE;AAC5B,QAAA,CAAC,CAAC;IACJ;AAEA;;;;;;;AAOG;AACH,IAAA,MAAM,KAAK,GAAA;AACT,QAAA,MAAM,qBAAqB,CAAC,YAAW;YACrC,MAAM,kBAAkB,EAAE;AAC1B,YAAA,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE;YAC9B,IAAI,CAAC,MAAM,IAAI,CAAC,YAAY,CAAC,eAAe,CAAC,MAAM,MAAM;gBAAE;YAC3D,MAAM,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC;YACnC,MAAM,kBAAkB,EAAE;AAC5B,QAAA,CAAC,CAAC;IACJ;AAEA;;;AAGG;AACH,IAAA,MAAM,OAAO,GAAA;AACX,QAAA,OAAO,qBAAqB,CAAC,YAAW;YACtC,MAAM,kBAAkB,EAAE;AAC1B,YAAA,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,QAAQ,EAAE;AACnC,YAAA,OAAO,KAAK,GAAG,CAAC,MAAM,KAAK,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,GAAG,IAAI;AACnD,QAAA,CAAC,CAAC;IACJ;AAEA;;;AAGG;AACH,IAAA,MAAM,QAAQ,GAAA;AACZ,QAAA,OAAO,qBAAqB,CAAC,YAAW;YACtC,MAAM,kBAAkB,EAAE;AAC1B,YAAA,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,UAAU,EAAE;AAClC,YAAA,IAAI,CAAC,EAAE;AAAE,gBAAA,OAAO,KAAK;;;AAGrB,YAAA,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,0BAA0B,EAAE,CAAC,kBAAkB,CACtE,CAAA,CAAA,EAAI,EAAE,CAAA,iCAAA,CAAmC,CAC1C,EAAE;YACH,OAAO,KAAK,KAAK,IAAI;AACvB,QAAA,CAAC,CAAC;IACJ;AAEA;;;AAGG;AACK,IAAA,MAAM,QAAQ,GAAA;AACpB,QAAA,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,UAAU,EAAE;AAClC,QAAA,IAAI,CAAC,EAAE;AAAE,YAAA,OAAO,IAAI;AACpB,QAAA,OAAO,IAAI,CAAC,0BAA0B,EAAE,CAAC,kBAAkB,CAAC,CAAA,CAAA,EAAI,EAAE,CAAA,CAAE,CAAC,EAAE;IACzE;AAEA;;;AAGG;AACK,IAAA,MAAM,UAAU,GAAA;AACtB,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,EAAE,YAAY,CAAC,eAAe,CAAC;IAC1D;;;ACzOF;;AAEG;;;;"}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { ComponentHarness, manualChangeDetection, HarnessPredicate } from '@angular/cdk/testing';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lets the zoneless change-detection scheduler run its pending tick.
|
|
5
|
+
*
|
|
6
|
+
* `ApplicationRef` schedules a tick with `setTimeout(cb)` raced against
|
|
7
|
+
* `requestAnimationFrame`. A timer registered *after* the notify that dirtied a
|
|
8
|
+
* signal therefore fires *after* that tick, so one macrotask is enough to
|
|
9
|
+
* observe everything already scheduled. It is a fixed, bounded yield, not a
|
|
10
|
+
* stabilization await, so it cannot hang.
|
|
11
|
+
*
|
|
12
|
+
* Every method spends one: an action yields after dispatching, so the effects
|
|
13
|
+
* the directive applied synchronously are rendered; a read yields before
|
|
14
|
+
* looking, so it sees any tick that was already pending. The composition is what
|
|
15
|
+
* matters — `show()` returns while the panel is still behind
|
|
16
|
+
* `twTooltipShowDelay`, and the following read's own yield is what picks the
|
|
17
|
+
* rendered content up.
|
|
18
|
+
*/
|
|
19
|
+
function afterSchedulerTick() {
|
|
20
|
+
return new Promise((resolve) => setTimeout(resolve));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Harness for a `[twTooltip]` trigger and the panel it shows.
|
|
24
|
+
*
|
|
25
|
+
* Deliberately narrow: a tooltip's whole observable surface is *whether it is
|
|
26
|
+
* showing and what it says*. Position, delays, color, size and arrow are
|
|
27
|
+
* configuration, not state, and a harness method for any of them would freeze an
|
|
28
|
+
* API that may still move — so none is offered.
|
|
29
|
+
*
|
|
30
|
+
* ## Nothing here awaits application stabilization, and that is load-bearing
|
|
31
|
+
*
|
|
32
|
+
* `TestbedHarnessEnvironment` routes every `TestElement` operation through
|
|
33
|
+
* `forceStabilize()` — `fixture.detectChanges()` then
|
|
34
|
+
* `await fixture.whenStable()` — and that await resolves only when Angular's
|
|
35
|
+
* `PendingTasks` set is empty. Under full-suite contention it was observed
|
|
36
|
+
* **not to resolve at all**, and everything built on it hung for the whole test
|
|
37
|
+
* budget instead of failing. This harness was withdrawn once for that, on five
|
|
38
|
+
* green local runs followed by one red CI run.
|
|
39
|
+
*
|
|
40
|
+
* Every method body therefore runs inside CDK's `manualChangeDetection()`,
|
|
41
|
+
* which sets the flag `forceStabilize()` early-returns on, and so does
|
|
42
|
+
* acquisition, via {@link load} / {@link loadAll}. The spec beside this file
|
|
43
|
+
* adds the third piece: it never awaits `fixture.whenStable()` either, not even
|
|
44
|
+
* in `beforeEach`. All three were needed — each of the two CI failures during
|
|
45
|
+
* this restoration was traced to one of them, and the second landed on
|
|
46
|
+
* `popover` rather than here, which is how it became clear the fault belongs to
|
|
47
|
+
* whichever harness spec lands in the unlucky worker slot rather than to any
|
|
48
|
+
* one component. `grep -c whenStable` over this file and its spec returns zero,
|
|
49
|
+
* which is the whole claim and is checkable in one command rather than by
|
|
50
|
+
* counting green runs. The spec pins the rest with tests that hold a real
|
|
51
|
+
* `PendingTasks` entry open across acquisition and every method.
|
|
52
|
+
*
|
|
53
|
+
* Why the application stops stabilizing is **not** known; this removes the
|
|
54
|
+
* dependency rather than curing it.
|
|
55
|
+
*
|
|
56
|
+
* The cost is that change detection is not forced on your behalf. Instead every
|
|
57
|
+
* method spends one macrotask on the scheduler (see {@link afterSchedulerTick}),
|
|
58
|
+
* which covers everything already scheduled — including the panel's first
|
|
59
|
+
* render, which is why {@link getTooltipText} does not come back empty on a
|
|
60
|
+
* tooltip that has only just attached. What it does not cover is state behind
|
|
61
|
+
* the component's own timers: {@link show} and {@link hide} dispatch the
|
|
62
|
+
* interaction and return, and the panel appears or detaches only once
|
|
63
|
+
* `twTooltipShowDelay` (200 ms by default) or `twTooltipHideDelay` (150 ms)
|
|
64
|
+
* elapses. Set both to `0` in a fixture, poll the DOM for the panel —
|
|
65
|
+
* `document.querySelector` needs no stabilization and so can neither hang nor
|
|
66
|
+
* burn a fixed interval — and only then read through the harness.
|
|
67
|
+
*
|
|
68
|
+
* ## Loading it
|
|
69
|
+
*
|
|
70
|
+
* The host is the trigger, which lives in the fixture, so the ordinary
|
|
71
|
+
* `TestbedHarnessEnvironment.loader(fixture)` is correct. The panel renders into
|
|
72
|
+
* the CDK overlay container outside the fixture, and this harness resolves it
|
|
73
|
+
* internally via `documentRootLocatorFactory()` — a consumer never needs
|
|
74
|
+
* `documentRootLoader`.
|
|
75
|
+
*
|
|
76
|
+
* The host selector is the directive's static `data-tw-tooltip-trigger` marker,
|
|
77
|
+
* which matches both spellings of the input: `twTooltip="literal"` and the bound
|
|
78
|
+
* `[twTooltip]="expr()"`, for which Angular renders no attribute at all. A
|
|
79
|
+
* harness matching the directive's own selector would silently miss every bound
|
|
80
|
+
* trigger.
|
|
81
|
+
*
|
|
82
|
+
* Unlike `MenuHarness` and `PopoverHarness`, a tooltip trigger carries no
|
|
83
|
+
* `aria-controls` linking it to its panel (`aria-describedby` points at CDK
|
|
84
|
+
* `AriaDescriber`'s shared hidden message element for string content), so the
|
|
85
|
+
* panel is resolved as "the tooltip showing in the document". That is exact for
|
|
86
|
+
* the hover/focus model, where only one tooltip is visible at a time, but a test
|
|
87
|
+
* that forces two open at once cannot tell them apart.
|
|
88
|
+
*/
|
|
89
|
+
class TooltipHarness extends ComponentHarness {
|
|
90
|
+
static hostSelector = '[data-tw-tooltip-trigger]';
|
|
91
|
+
/** Resolves the tooltip panel, which lives outside this harness's host. */
|
|
92
|
+
panel = this.documentRootLocatorFactory().locatorForOptional('tw-tooltip-overlay');
|
|
93
|
+
/**
|
|
94
|
+
* Acquires one harness without waiting for the application to stabilize —
|
|
95
|
+
* the counterpart to the guarantee the methods below make.
|
|
96
|
+
*
|
|
97
|
+
* `loader.getHarness(...)` is CDK's own acquisition path and it stabilizes:
|
|
98
|
+
* `getAllRawElements` calls `forceStabilize()`, and `HarnessPredicate`
|
|
99
|
+
* filtering routes through `parallel()`, which asks *every* active fixture in
|
|
100
|
+
* the worker to settle. Both await `fixture.whenStable()`, which is the one
|
|
101
|
+
* thing this harness exists to avoid — and the failure that withdrew it was
|
|
102
|
+
* observed there, at acquisition, before any method had run.
|
|
103
|
+
*
|
|
104
|
+
* So acquisition is wrapped too, and `manualChangeDetection()` nests: the
|
|
105
|
+
* inner `parallel()` sees the flag already set and skips the stabilization
|
|
106
|
+
* entirely. **Render the fixture first** (`fixture.detectChanges()`), because
|
|
107
|
+
* nothing here will do it for you; an unrendered fixture fails loudly with
|
|
108
|
+
* CDK's "failed to find element" rather than returning something wrong.
|
|
109
|
+
*
|
|
110
|
+
* Plain `loader.getHarness(TooltipHarness)` still works and is still supported.
|
|
111
|
+
* This is the path to use when a suite must not be able to hang.
|
|
112
|
+
*/
|
|
113
|
+
static load(loader, options = {}) {
|
|
114
|
+
return manualChangeDetection(() => loader.getHarness(TooltipHarness.with(options)));
|
|
115
|
+
}
|
|
116
|
+
/** {@link load} for every matching trigger rather than the first. */
|
|
117
|
+
static loadAll(loader, options = {}) {
|
|
118
|
+
return manualChangeDetection(() => loader.getAllHarnesses(TooltipHarness.with(options)));
|
|
119
|
+
}
|
|
120
|
+
/** Predicate for `locatorFor` / `locatorForAll`. */
|
|
121
|
+
static with(options = {}) {
|
|
122
|
+
return new HarnessPredicate(TooltipHarness, options).addOption('triggerText', options.triggerText, async (h, text) => HarnessPredicate.stringMatches(await h.getTriggerText(), text));
|
|
123
|
+
}
|
|
124
|
+
/** The text currently rendered in the trigger, trimmed. */
|
|
125
|
+
async getTriggerText() {
|
|
126
|
+
return manualChangeDetection(async () => {
|
|
127
|
+
await afterSchedulerTick();
|
|
128
|
+
return (await (await this.host()).text()).trim();
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/** Whether a tooltip panel is currently showing. */
|
|
132
|
+
async isOpen() {
|
|
133
|
+
return manualChangeDetection(async () => {
|
|
134
|
+
await afterSchedulerTick();
|
|
135
|
+
return (await this.panel()) !== null;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The tooltip's message, trimmed, or `null` when nothing is showing. Works
|
|
140
|
+
* for string and `TemplateRef` content alike.
|
|
141
|
+
*/
|
|
142
|
+
async getTooltipText() {
|
|
143
|
+
return manualChangeDetection(async () => {
|
|
144
|
+
await afterSchedulerTick();
|
|
145
|
+
const panel = await this.panel();
|
|
146
|
+
return panel ? (await panel.text()).trim() : null;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/** Hovers the trigger. The panel appears once `twTooltipShowDelay` elapses. */
|
|
150
|
+
async show() {
|
|
151
|
+
await manualChangeDetection(async () => {
|
|
152
|
+
await (await this.host()).hover();
|
|
153
|
+
await afterSchedulerTick();
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/** Moves the pointer off the trigger. The panel detaches once `twTooltipHideDelay` elapses. */
|
|
157
|
+
async hide() {
|
|
158
|
+
await manualChangeDetection(async () => {
|
|
159
|
+
await (await this.host()).mouseAway();
|
|
160
|
+
await afterSchedulerTick();
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Focuses the trigger — the keyboard equivalent of {@link show}, and the path
|
|
165
|
+
* WCAG 2.1 SC 1.4.13 requires to work.
|
|
166
|
+
*
|
|
167
|
+
* Moves real DOM focus *and* dispatches `focusin`, because a programmatic
|
|
168
|
+
* `focus()` does not reliably raise `focusin` in every test DOM. The directive
|
|
169
|
+
* treats a repeated show as a no-op, so the belt-and-braces pair is safe.
|
|
170
|
+
*/
|
|
171
|
+
async focusTrigger() {
|
|
172
|
+
await manualChangeDetection(async () => {
|
|
173
|
+
const host = await this.host();
|
|
174
|
+
await host.focus();
|
|
175
|
+
await host.dispatchEvent('focusin');
|
|
176
|
+
await afterSchedulerTick();
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/** Blurs the trigger — the keyboard equivalent of {@link hide}. */
|
|
180
|
+
async blurTrigger() {
|
|
181
|
+
await manualChangeDetection(async () => {
|
|
182
|
+
const host = await this.host();
|
|
183
|
+
await host.blur();
|
|
184
|
+
await host.dispatchEvent('focusout');
|
|
185
|
+
await afterSchedulerTick();
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Generated bundle index. Do not edit.
|
|
192
|
+
*/
|
|
193
|
+
|
|
194
|
+
export { TooltipHarness };
|
|
195
|
+
//# sourceMappingURL=cdevhub-ngx-tw-tooltip-testing.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cdevhub-ngx-tw-tooltip-testing.mjs","sources":["../../../projects/ngx-tw/tooltip/testing/tooltip-harness.ts","../../../projects/ngx-tw/tooltip/testing/cdevhub-ngx-tw-tooltip-testing.ts"],"sourcesContent":["import { ComponentHarness, HarnessPredicate, manualChangeDetection } from '@angular/cdk/testing';\nimport type { BaseHarnessFilters, HarnessLoader, TestElement } from '@angular/cdk/testing';\n\n/** Filters accepted by `TooltipHarness.with`. */\nexport interface TooltipHarnessFilters extends BaseHarnessFilters {\n /** Match by the text rendered in the trigger. */\n triggerText?: string | RegExp;\n}\n\n/**\n * Lets the zoneless change-detection scheduler run its pending tick.\n *\n * `ApplicationRef` schedules a tick with `setTimeout(cb)` raced against\n * `requestAnimationFrame`. A timer registered *after* the notify that dirtied a\n * signal therefore fires *after* that tick, so one macrotask is enough to\n * observe everything already scheduled. It is a fixed, bounded yield, not a\n * stabilization await, so it cannot hang.\n *\n * Every method spends one: an action yields after dispatching, so the effects\n * the directive applied synchronously are rendered; a read yields before\n * looking, so it sees any tick that was already pending. The composition is what\n * matters — `show()` returns while the panel is still behind\n * `twTooltipShowDelay`, and the following read's own yield is what picks the\n * rendered content up.\n */\nfunction afterSchedulerTick(): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve));\n}\n\n/**\n * Harness for a `[twTooltip]` trigger and the panel it shows.\n *\n * Deliberately narrow: a tooltip's whole observable surface is *whether it is\n * showing and what it says*. Position, delays, color, size and arrow are\n * configuration, not state, and a harness method for any of them would freeze an\n * API that may still move — so none is offered.\n *\n * ## Nothing here awaits application stabilization, and that is load-bearing\n *\n * `TestbedHarnessEnvironment` routes every `TestElement` operation through\n * `forceStabilize()` — `fixture.detectChanges()` then\n * `await fixture.whenStable()` — and that await resolves only when Angular's\n * `PendingTasks` set is empty. Under full-suite contention it was observed\n * **not to resolve at all**, and everything built on it hung for the whole test\n * budget instead of failing. This harness was withdrawn once for that, on five\n * green local runs followed by one red CI run.\n *\n * Every method body therefore runs inside CDK's `manualChangeDetection()`,\n * which sets the flag `forceStabilize()` early-returns on, and so does\n * acquisition, via {@link load} / {@link loadAll}. The spec beside this file\n * adds the third piece: it never awaits `fixture.whenStable()` either, not even\n * in `beforeEach`. All three were needed — each of the two CI failures during\n * this restoration was traced to one of them, and the second landed on\n * `popover` rather than here, which is how it became clear the fault belongs to\n * whichever harness spec lands in the unlucky worker slot rather than to any\n * one component. `grep -c whenStable` over this file and its spec returns zero,\n * which is the whole claim and is checkable in one command rather than by\n * counting green runs. The spec pins the rest with tests that hold a real\n * `PendingTasks` entry open across acquisition and every method.\n *\n * Why the application stops stabilizing is **not** known; this removes the\n * dependency rather than curing it.\n *\n * The cost is that change detection is not forced on your behalf. Instead every\n * method spends one macrotask on the scheduler (see {@link afterSchedulerTick}),\n * which covers everything already scheduled — including the panel's first\n * render, which is why {@link getTooltipText} does not come back empty on a\n * tooltip that has only just attached. What it does not cover is state behind\n * the component's own timers: {@link show} and {@link hide} dispatch the\n * interaction and return, and the panel appears or detaches only once\n * `twTooltipShowDelay` (200 ms by default) or `twTooltipHideDelay` (150 ms)\n * elapses. Set both to `0` in a fixture, poll the DOM for the panel —\n * `document.querySelector` needs no stabilization and so can neither hang nor\n * burn a fixed interval — and only then read through the harness.\n *\n * ## Loading it\n *\n * The host is the trigger, which lives in the fixture, so the ordinary\n * `TestbedHarnessEnvironment.loader(fixture)` is correct. The panel renders into\n * the CDK overlay container outside the fixture, and this harness resolves it\n * internally via `documentRootLocatorFactory()` — a consumer never needs\n * `documentRootLoader`.\n *\n * The host selector is the directive's static `data-tw-tooltip-trigger` marker,\n * which matches both spellings of the input: `twTooltip=\"literal\"` and the bound\n * `[twTooltip]=\"expr()\"`, for which Angular renders no attribute at all. A\n * harness matching the directive's own selector would silently miss every bound\n * trigger.\n *\n * Unlike `MenuHarness` and `PopoverHarness`, a tooltip trigger carries no\n * `aria-controls` linking it to its panel (`aria-describedby` points at CDK\n * `AriaDescriber`'s shared hidden message element for string content), so the\n * panel is resolved as \"the tooltip showing in the document\". That is exact for\n * the hover/focus model, where only one tooltip is visible at a time, but a test\n * that forces two open at once cannot tell them apart.\n */\nexport class TooltipHarness extends ComponentHarness {\n static hostSelector = '[data-tw-tooltip-trigger]';\n\n /** Resolves the tooltip panel, which lives outside this harness's host. */\n private readonly panel =\n this.documentRootLocatorFactory().locatorForOptional('tw-tooltip-overlay');\n\n /**\n * Acquires one harness without waiting for the application to stabilize —\n * the counterpart to the guarantee the methods below make.\n *\n * `loader.getHarness(...)` is CDK's own acquisition path and it stabilizes:\n * `getAllRawElements` calls `forceStabilize()`, and `HarnessPredicate`\n * filtering routes through `parallel()`, which asks *every* active fixture in\n * the worker to settle. Both await `fixture.whenStable()`, which is the one\n * thing this harness exists to avoid — and the failure that withdrew it was\n * observed there, at acquisition, before any method had run.\n *\n * So acquisition is wrapped too, and `manualChangeDetection()` nests: the\n * inner `parallel()` sees the flag already set and skips the stabilization\n * entirely. **Render the fixture first** (`fixture.detectChanges()`), because\n * nothing here will do it for you; an unrendered fixture fails loudly with\n * CDK's \"failed to find element\" rather than returning something wrong.\n *\n * Plain `loader.getHarness(TooltipHarness)` still works and is still supported.\n * This is the path to use when a suite must not be able to hang.\n */\n static load(loader: HarnessLoader, options: TooltipHarnessFilters = {}): Promise<TooltipHarness> {\n return manualChangeDetection(() => loader.getHarness(TooltipHarness.with(options)));\n }\n\n /** {@link load} for every matching trigger rather than the first. */\n static loadAll(\n loader: HarnessLoader,\n options: TooltipHarnessFilters = {},\n ): Promise<TooltipHarness[]> {\n return manualChangeDetection(() => loader.getAllHarnesses(TooltipHarness.with(options)));\n }\n\n /** Predicate for `locatorFor` / `locatorForAll`. */\n static with(options: TooltipHarnessFilters = {}): HarnessPredicate<TooltipHarness> {\n return new HarnessPredicate(TooltipHarness, options).addOption(\n 'triggerText',\n options.triggerText,\n async (h, text) => HarnessPredicate.stringMatches(await h.getTriggerText(), text),\n );\n }\n\n /** The text currently rendered in the trigger, trimmed. */\n async getTriggerText(): Promise<string> {\n return manualChangeDetection(async () => {\n await afterSchedulerTick();\n return (await (await this.host()).text()).trim();\n });\n }\n\n /** Whether a tooltip panel is currently showing. */\n async isOpen(): Promise<boolean> {\n return manualChangeDetection(async () => {\n await afterSchedulerTick();\n return (await this.panel()) !== null;\n });\n }\n\n /**\n * The tooltip's message, trimmed, or `null` when nothing is showing. Works\n * for string and `TemplateRef` content alike.\n */\n async getTooltipText(): Promise<string | null> {\n return manualChangeDetection(async () => {\n await afterSchedulerTick();\n const panel: TestElement | null = await this.panel();\n return panel ? (await panel.text()).trim() : null;\n });\n }\n\n /** Hovers the trigger. The panel appears once `twTooltipShowDelay` elapses. */\n async show(): Promise<void> {\n await manualChangeDetection(async () => {\n await (await this.host()).hover();\n await afterSchedulerTick();\n });\n }\n\n /** Moves the pointer off the trigger. The panel detaches once `twTooltipHideDelay` elapses. */\n async hide(): Promise<void> {\n await manualChangeDetection(async () => {\n await (await this.host()).mouseAway();\n await afterSchedulerTick();\n });\n }\n\n /**\n * Focuses the trigger — the keyboard equivalent of {@link show}, and the path\n * WCAG 2.1 SC 1.4.13 requires to work.\n *\n * Moves real DOM focus *and* dispatches `focusin`, because a programmatic\n * `focus()` does not reliably raise `focusin` in every test DOM. The directive\n * treats a repeated show as a no-op, so the belt-and-braces pair is safe.\n */\n async focusTrigger(): Promise<void> {\n await manualChangeDetection(async () => {\n const host = await this.host();\n await host.focus();\n await host.dispatchEvent('focusin');\n await afterSchedulerTick();\n });\n }\n\n /** Blurs the trigger — the keyboard equivalent of {@link hide}. */\n async blurTrigger(): Promise<void> {\n await manualChangeDetection(async () => {\n const host = await this.host();\n await host.blur();\n await host.dispatchEvent('focusout');\n await afterSchedulerTick();\n });\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|