@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cdevhub/ngx-tw",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Angular component library for Tailwind CSS v4 — accessible, signal-based, built on Angular CDK.",
5
5
  "keywords": [
6
6
  "angular",
@@ -193,6 +193,10 @@
193
193
  "types": "./types/cdevhub-ngx-tw-popover.d.ts",
194
194
  "default": "./fesm2022/cdevhub-ngx-tw-popover.mjs"
195
195
  },
196
+ "./popover/testing": {
197
+ "types": "./types/cdevhub-ngx-tw-popover-testing.d.ts",
198
+ "default": "./fesm2022/cdevhub-ngx-tw-popover-testing.mjs"
199
+ },
196
200
  "./progress-bar": {
197
201
  "types": "./types/cdevhub-ngx-tw-progress-bar.d.ts",
198
202
  "default": "./fesm2022/cdevhub-ngx-tw-progress-bar.mjs"
@@ -305,6 +309,10 @@
305
309
  "types": "./types/cdevhub-ngx-tw-tooltip.d.ts",
306
310
  "default": "./fesm2022/cdevhub-ngx-tw-tooltip.mjs"
307
311
  },
312
+ "./tooltip/testing": {
313
+ "types": "./types/cdevhub-ngx-tw-tooltip-testing.d.ts",
314
+ "default": "./fesm2022/cdevhub-ngx-tw-tooltip-testing.mjs"
315
+ },
308
316
  "./transfer": {
309
317
  "types": "./types/cdevhub-ngx-tw-transfer.d.ts",
310
318
  "default": "./fesm2022/cdevhub-ngx-tw-transfer.mjs"
@@ -0,0 +1,138 @@
1
+ import { ComponentHarness, HarnessLoader, BaseHarnessFilters, HarnessPredicate } from '@angular/cdk/testing';
2
+
3
+ /** Filters accepted by `PopoverHarness.with`. */
4
+ interface PopoverHarnessFilters extends BaseHarnessFilters {
5
+ /** Match by the text rendered in the trigger. */
6
+ triggerText?: string | RegExp;
7
+ }
8
+ /**
9
+ * Harness for a `[twPopover]` trigger and the panel it opens.
10
+ *
11
+ * ## Nothing here awaits application stabilization, and that is load-bearing
12
+ *
13
+ * `TestbedHarnessEnvironment` routes every `TestElement` operation through
14
+ * `forceStabilize()` — `fixture.detectChanges()` then
15
+ * `await fixture.whenStable()` — and that await resolves only when Angular's
16
+ * `PendingTasks` set is empty. Under full-suite contention it was observed
17
+ * **not to resolve at all**, and everything built on it hung for the whole test
18
+ * budget instead of failing. This harness was withdrawn twice for that.
19
+ *
20
+ * Every method body therefore runs inside CDK's `manualChangeDetection()`,
21
+ * which sets the flag `forceStabilize()` early-returns on, and so does
22
+ * acquisition, via {@link load} / {@link loadAll}. The spec beside this file
23
+ * adds the third piece: it never awaits `fixture.whenStable()` either, not even
24
+ * in `beforeEach`. All three were needed — each of the two CI failures during
25
+ * this restoration was traced to one of them, and the second landed on
26
+ * `tooltip` rather than here, which is how it became clear the fault belongs to
27
+ * whichever harness spec lands in the unlucky worker slot rather than to any
28
+ * one component. `grep -c whenStable` over this file and its spec returns zero,
29
+ * which is the whole claim and is checkable in one command rather than by
30
+ * counting green runs. The spec pins the rest with tests that hold a real
31
+ * `PendingTasks` entry open across acquisition and every method.
32
+ *
33
+ * Why the application stops stabilizing is **not** known; this removes the
34
+ * dependency rather than curing it.
35
+ *
36
+ * The cost is that change detection is not forced on your behalf. Instead every
37
+ * method spends one macrotask on the scheduler (see {@link afterSchedulerTick}),
38
+ * which covers everything already scheduled — including the panel's first
39
+ * render. What it does not cover is state behind the component's own timers:
40
+ * {@link close} dispatches Escape and returns, and the panel detaches only after
41
+ * the 120 ms leave window in `popover.ts`. Wait for that by polling the DOM —
42
+ * `document.querySelector` needs no stabilization and so can neither hang nor
43
+ * burn a fixed interval — and only then read through the harness.
44
+ *
45
+ * ## Loading it
46
+ *
47
+ * The host is the trigger, which lives in the fixture, so the ordinary
48
+ * `TestbedHarnessEnvironment.loader(fixture)` is correct. The panel renders into
49
+ * the CDK overlay container outside the fixture, and this harness resolves it
50
+ * internally via `documentRootLocatorFactory()` — a consumer never needs
51
+ * `documentRootLoader`.
52
+ *
53
+ * The host selector is the directive's static `data-tw-popover-trigger` marker.
54
+ * `[twPopover]` cannot be used: it takes a required `TemplateRef` or component
55
+ * type, so it is always property-bound and Angular renders no attribute for a
56
+ * bound input. The marker also makes the match exact, so no disambiguation
57
+ * against `aria-haspopup="dialog"` — which the two date-picker triggers also
58
+ * carry — is needed.
59
+ *
60
+ * ## The panel is detached, not disposed
61
+ *
62
+ * Unlike `tw-select`, closing a popover **detaches** the portal and keeps the
63
+ * `OverlayRef` for reuse; it is only rebuilt when `twPopoverBackdrop` or
64
+ * `twPopoverScrollStrategy` changes. The panel element is therefore absent while
65
+ * closed and present again after a reopen, on the same overlay.
66
+ */
67
+ declare class PopoverHarness extends ComponentHarness {
68
+ static hostSelector: string;
69
+ /**
70
+ * Acquires one harness without waiting for the application to stabilize —
71
+ * the counterpart to the guarantee the methods below make.
72
+ *
73
+ * `loader.getHarness(...)` is CDK's own acquisition path and it stabilizes:
74
+ * `getAllRawElements` calls `forceStabilize()`, and `HarnessPredicate`
75
+ * filtering routes through `parallel()`, which asks *every* active fixture in
76
+ * the worker to settle. Both await `fixture.whenStable()`, which is the one
77
+ * thing this harness exists to avoid — and the failure that withdrew it was
78
+ * observed there, at acquisition, before any method had run.
79
+ *
80
+ * So acquisition is wrapped too, and `manualChangeDetection()` nests: the
81
+ * inner `parallel()` sees the flag already set and skips the stabilization
82
+ * entirely. **Render the fixture first** (`fixture.detectChanges()`), because
83
+ * nothing here will do it for you; an unrendered fixture fails loudly with
84
+ * CDK's "failed to find element" rather than returning something wrong.
85
+ *
86
+ * Plain `loader.getHarness(PopoverHarness)` still works and is still supported.
87
+ * This is the path to use when a suite must not be able to hang.
88
+ */
89
+ static load(loader: HarnessLoader, options?: PopoverHarnessFilters): Promise<PopoverHarness>;
90
+ /** {@link load} for every matching trigger rather than the first. */
91
+ static loadAll(loader: HarnessLoader, options?: PopoverHarnessFilters): Promise<PopoverHarness[]>;
92
+ /** Predicate for `locatorFor` / `locatorForAll`. */
93
+ static with(options?: PopoverHarnessFilters): HarnessPredicate<PopoverHarness>;
94
+ /** The text currently rendered in the trigger, trimmed. */
95
+ getTriggerText(): Promise<string>;
96
+ /** Whether the popover is open, read from the trigger's `aria-expanded`. */
97
+ isOpen(): Promise<boolean>;
98
+ /**
99
+ * Opens the popover by clicking the trigger. No-op when already open.
100
+ *
101
+ * This is the gesture for the default `twPopoverTriggerOn="click"`. A
102
+ * `'focus'`- or `'manual'`-triggered popover is opened through the directive's
103
+ * own `open()` (reachable via `exportAs: 'twPopover'`), not through a click.
104
+ */
105
+ open(): Promise<void>;
106
+ /**
107
+ * Closes the popover by sending Escape to the trigger — the one dismissal
108
+ * that works for click, focus and manual triggers alike. No-op when already
109
+ * closed, and deliberately inert when `twPopoverCloseOnEscape` is `false`.
110
+ *
111
+ * Returns as soon as the key is dispatched. The panel detaches only after the
112
+ * 120 ms leave window; poll the DOM for its removal before asserting.
113
+ */
114
+ close(): Promise<void>;
115
+ /**
116
+ * Text rendered inside the panel, trimmed, or `null` when the popover is
117
+ * closed and the panel is detached.
118
+ */
119
+ getText(): Promise<string | null>;
120
+ /**
121
+ * Whether the panel renders its directional arrow (`twPopoverArrow`). `false`
122
+ * while the popover is closed, because the panel does not exist then.
123
+ */
124
+ hasArrow(): Promise<boolean>;
125
+ /**
126
+ * The panel element, or `null` when the popover is closed. Callers are already
127
+ * inside `manualChangeDetection`.
128
+ */
129
+ private getPanel;
130
+ /**
131
+ * The id of this trigger's own panel, or `null` when closed. Scoping by
132
+ * `aria-controls` keeps sibling popovers apart.
133
+ */
134
+ private getPanelId;
135
+ }
136
+
137
+ export { PopoverHarness };
138
+ export type { PopoverHarnessFilters };
@@ -0,0 +1,131 @@
1
+ import { ComponentHarness, HarnessLoader, BaseHarnessFilters, HarnessPredicate } from '@angular/cdk/testing';
2
+
3
+ /** Filters accepted by `TooltipHarness.with`. */
4
+ interface TooltipHarnessFilters extends BaseHarnessFilters {
5
+ /** Match by the text rendered in the trigger. */
6
+ triggerText?: string | RegExp;
7
+ }
8
+ /**
9
+ * Harness for a `[twTooltip]` trigger and the panel it shows.
10
+ *
11
+ * Deliberately narrow: a tooltip's whole observable surface is *whether it is
12
+ * showing and what it says*. Position, delays, color, size and arrow are
13
+ * configuration, not state, and a harness method for any of them would freeze an
14
+ * API that may still move — so none is offered.
15
+ *
16
+ * ## Nothing here awaits application stabilization, and that is load-bearing
17
+ *
18
+ * `TestbedHarnessEnvironment` routes every `TestElement` operation through
19
+ * `forceStabilize()` — `fixture.detectChanges()` then
20
+ * `await fixture.whenStable()` — and that await resolves only when Angular's
21
+ * `PendingTasks` set is empty. Under full-suite contention it was observed
22
+ * **not to resolve at all**, and everything built on it hung for the whole test
23
+ * budget instead of failing. This harness was withdrawn once for that, on five
24
+ * green local runs followed by one red CI run.
25
+ *
26
+ * Every method body therefore runs inside CDK's `manualChangeDetection()`,
27
+ * which sets the flag `forceStabilize()` early-returns on, and so does
28
+ * acquisition, via {@link load} / {@link loadAll}. The spec beside this file
29
+ * adds the third piece: it never awaits `fixture.whenStable()` either, not even
30
+ * in `beforeEach`. All three were needed — each of the two CI failures during
31
+ * this restoration was traced to one of them, and the second landed on
32
+ * `popover` rather than here, which is how it became clear the fault belongs to
33
+ * whichever harness spec lands in the unlucky worker slot rather than to any
34
+ * one component. `grep -c whenStable` over this file and its spec returns zero,
35
+ * which is the whole claim and is checkable in one command rather than by
36
+ * counting green runs. The spec pins the rest with tests that hold a real
37
+ * `PendingTasks` entry open across acquisition and every method.
38
+ *
39
+ * Why the application stops stabilizing is **not** known; this removes the
40
+ * dependency rather than curing it.
41
+ *
42
+ * The cost is that change detection is not forced on your behalf. Instead every
43
+ * method spends one macrotask on the scheduler (see {@link afterSchedulerTick}),
44
+ * which covers everything already scheduled — including the panel's first
45
+ * render, which is why {@link getTooltipText} does not come back empty on a
46
+ * tooltip that has only just attached. What it does not cover is state behind
47
+ * the component's own timers: {@link show} and {@link hide} dispatch the
48
+ * interaction and return, and the panel appears or detaches only once
49
+ * `twTooltipShowDelay` (200 ms by default) or `twTooltipHideDelay` (150 ms)
50
+ * elapses. Set both to `0` in a fixture, poll the DOM for the panel —
51
+ * `document.querySelector` needs no stabilization and so can neither hang nor
52
+ * burn a fixed interval — and only then read through the harness.
53
+ *
54
+ * ## Loading it
55
+ *
56
+ * The host is the trigger, which lives in the fixture, so the ordinary
57
+ * `TestbedHarnessEnvironment.loader(fixture)` is correct. The panel renders into
58
+ * the CDK overlay container outside the fixture, and this harness resolves it
59
+ * internally via `documentRootLocatorFactory()` — a consumer never needs
60
+ * `documentRootLoader`.
61
+ *
62
+ * The host selector is the directive's static `data-tw-tooltip-trigger` marker,
63
+ * which matches both spellings of the input: `twTooltip="literal"` and the bound
64
+ * `[twTooltip]="expr()"`, for which Angular renders no attribute at all. A
65
+ * harness matching the directive's own selector would silently miss every bound
66
+ * trigger.
67
+ *
68
+ * Unlike `MenuHarness` and `PopoverHarness`, a tooltip trigger carries no
69
+ * `aria-controls` linking it to its panel (`aria-describedby` points at CDK
70
+ * `AriaDescriber`'s shared hidden message element for string content), so the
71
+ * panel is resolved as "the tooltip showing in the document". That is exact for
72
+ * the hover/focus model, where only one tooltip is visible at a time, but a test
73
+ * that forces two open at once cannot tell them apart.
74
+ */
75
+ declare class TooltipHarness extends ComponentHarness {
76
+ static hostSelector: string;
77
+ /** Resolves the tooltip panel, which lives outside this harness's host. */
78
+ private readonly panel;
79
+ /**
80
+ * Acquires one harness without waiting for the application to stabilize —
81
+ * the counterpart to the guarantee the methods below make.
82
+ *
83
+ * `loader.getHarness(...)` is CDK's own acquisition path and it stabilizes:
84
+ * `getAllRawElements` calls `forceStabilize()`, and `HarnessPredicate`
85
+ * filtering routes through `parallel()`, which asks *every* active fixture in
86
+ * the worker to settle. Both await `fixture.whenStable()`, which is the one
87
+ * thing this harness exists to avoid — and the failure that withdrew it was
88
+ * observed there, at acquisition, before any method had run.
89
+ *
90
+ * So acquisition is wrapped too, and `manualChangeDetection()` nests: the
91
+ * inner `parallel()` sees the flag already set and skips the stabilization
92
+ * entirely. **Render the fixture first** (`fixture.detectChanges()`), because
93
+ * nothing here will do it for you; an unrendered fixture fails loudly with
94
+ * CDK's "failed to find element" rather than returning something wrong.
95
+ *
96
+ * Plain `loader.getHarness(TooltipHarness)` still works and is still supported.
97
+ * This is the path to use when a suite must not be able to hang.
98
+ */
99
+ static load(loader: HarnessLoader, options?: TooltipHarnessFilters): Promise<TooltipHarness>;
100
+ /** {@link load} for every matching trigger rather than the first. */
101
+ static loadAll(loader: HarnessLoader, options?: TooltipHarnessFilters): Promise<TooltipHarness[]>;
102
+ /** Predicate for `locatorFor` / `locatorForAll`. */
103
+ static with(options?: TooltipHarnessFilters): HarnessPredicate<TooltipHarness>;
104
+ /** The text currently rendered in the trigger, trimmed. */
105
+ getTriggerText(): Promise<string>;
106
+ /** Whether a tooltip panel is currently showing. */
107
+ isOpen(): Promise<boolean>;
108
+ /**
109
+ * The tooltip's message, trimmed, or `null` when nothing is showing. Works
110
+ * for string and `TemplateRef` content alike.
111
+ */
112
+ getTooltipText(): Promise<string | null>;
113
+ /** Hovers the trigger. The panel appears once `twTooltipShowDelay` elapses. */
114
+ show(): Promise<void>;
115
+ /** Moves the pointer off the trigger. The panel detaches once `twTooltipHideDelay` elapses. */
116
+ hide(): Promise<void>;
117
+ /**
118
+ * Focuses the trigger — the keyboard equivalent of {@link show}, and the path
119
+ * WCAG 2.1 SC 1.4.13 requires to work.
120
+ *
121
+ * Moves real DOM focus *and* dispatches `focusin`, because a programmatic
122
+ * `focus()` does not reliably raise `focusin` in every test DOM. The directive
123
+ * treats a repeated show as a no-op, so the belt-and-braces pair is safe.
124
+ */
125
+ focusTrigger(): Promise<void>;
126
+ /** Blurs the trigger — the keyboard equivalent of {@link hide}. */
127
+ blurTrigger(): Promise<void>;
128
+ }
129
+
130
+ export { TooltipHarness };
131
+ export type { TooltipHarnessFilters };