@gjsify/adwaita-web 0.22.0 → 0.23.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.
@@ -0,0 +1 @@
1
+ export declare const AdwRowStateTest: () => Promise<void>;
@@ -0,0 +1 @@
1
+ export declare const AdwToastOverlayTest: () => Promise<void>;
@@ -1,5 +1,6 @@
1
+ import type { AdwResponseAppearance } from '@gjsify/adwaita-core';
1
2
  /** Response button appearance — mirrors Adw.ResponseAppearance. */
2
- export type AdwResponseAppearance = 'default' | 'suggested' | 'destructive';
3
+ export type { AdwResponseAppearance } from '@gjsify/adwaita-core';
3
4
  /**
4
5
  * A single response declared in markup. Child of <adw-alert-dialog>; its `id`
5
6
  * attribute is the response ID, its text content the label. Optional
@@ -17,10 +18,10 @@ export declare class AdwAlertDialog extends HTMLElement {
17
18
  private _bodyEl;
18
19
  private _childEl;
19
20
  private _responseAreaEl;
20
- private _responses;
21
- private _byId;
22
- private _defaultResponse;
23
- private _closeResponse;
21
+ /** The headless response registry + default/close semantics + resolution (ADR 0004). */
22
+ private _model;
23
+ /** The rendered button per registered response id (the DOM half of the model). */
24
+ private readonly _buttons;
24
25
  private _blockCloseResponse;
25
26
  static get observedAttributes(): string[];
26
27
  /** The heading of the dialog. */
@@ -3,7 +3,8 @@ export declare class AdwComboRow extends HTMLElement {
3
3
  private _valueEl;
4
4
  private _titleEl;
5
5
  private _subtitleEl;
6
- private _items;
6
+ /** The headless options list + selectedIndex↔selectedValue state machine (ADR 0004). */
7
+ private readonly _state;
7
8
  private _initialized;
8
9
  static get observedAttributes(): string[];
9
10
  get selected(): number;
@@ -8,7 +8,10 @@ export declare class AdwExpanderRow extends HTMLElement {
8
8
  private _contentEl;
9
9
  private _prefixEl;
10
10
  private _suffixEl;
11
+ /** The headless expanded/collapsed disclosure state machine (ADR 0004). */
12
+ private readonly _state;
11
13
  private _initialized;
14
+ constructor();
12
15
  static get observedAttributes(): string[];
13
16
  get expanded(): boolean;
14
17
  set expanded(value: boolean);
@@ -21,6 +24,6 @@ export declare class AdwExpanderRow extends HTMLElement {
21
24
  /** The header prefix section — append icons/widgets here imperatively. */
22
25
  get prefixSection(): HTMLDivElement;
23
26
  connectedCallback(): void;
24
- attributeChangedCallback(): void;
27
+ attributeChangedCallback(name: string): void;
25
28
  private _render;
26
29
  }
@@ -2,10 +2,8 @@ export declare class AdwSpinRow extends HTMLElement {
2
2
  private _input;
3
3
  private _titleEl;
4
4
  private _subtitleEl;
5
- private _min;
6
- private _max;
7
- private _step;
8
- private _value;
5
+ /** The headless value/min/max/step adjustment with clamping (ADR 0004). */
6
+ private readonly _state;
9
7
  private _initialized;
10
8
  static get observedAttributes(): string[];
11
9
  get value(): number;
@@ -13,8 +11,12 @@ export declare class AdwSpinRow extends HTMLElement {
13
11
  connectedCallback(): void;
14
12
  attributeChangedCallback(name: string, _old: string | null, val: string | null): void;
15
13
  private _renderText;
14
+ /** A stepper press — the interactive path, so it emits `notify::value`. */
16
15
  private _adjust;
17
16
  private _emitChange;
17
+ /** Mirror the core value into the input display + the reflected attribute. */
18
+ private _syncValue;
19
+ private _roundToStep;
18
20
  private _countDecimals;
19
21
  private _formatValue;
20
22
  }
@@ -1,3 +1,6 @@
1
+ import { AdwToast } from '@gjsify/adwaita-core';
2
+ import type { ToastScheduler } from '@gjsify/adwaita-core';
3
+ export type { ToastScheduler, ToastTimerHandle } from '@gjsify/adwaita-core';
1
4
  /** Options for {@link AdwToastOverlay.addToast}, mirroring Adw.Toast's properties. */
2
5
  export interface AdwToastOptions {
3
6
  /** Seconds before the toast auto-dismisses; `0` keeps it until dismissed. Mirrors Adw.Toast:timeout (default 5). */
@@ -10,14 +13,52 @@ export interface AdwToastOptions {
10
13
  export declare class AdwToastOverlay extends HTMLElement {
11
14
  private _toasts;
12
15
  private _initialized;
16
+ /** The headless one-at-a-time + FIFO + auto-dismiss state machine (ADR 0004). */
17
+ private readonly _queue;
18
+ /** The timing source the queue's auto-dismiss runs on — swappable, see {@link scheduler}. */
19
+ private _scheduler;
20
+ /** The strip currently mounted for {@link AdwToastQueue.current}, if any. */
21
+ private _currentEl;
22
+ /** Per-toast action callbacks (the core toast descriptor carries only the label). */
23
+ private readonly _actions;
24
+ constructor();
13
25
  connectedCallback(): void;
26
+ /**
27
+ * The timing source driving the queue's auto-dismiss and the exit transition.
28
+ * Defaults to the browser's `setTimeout`/`clearTimeout`; swap it for a
29
+ * deterministic fake to drive toast lifetimes without real timers (the
30
+ * {@link ToastScheduler} seam `@gjsify/adwaita-core` defines).
31
+ */
32
+ get scheduler(): ToastScheduler;
33
+ set scheduler(scheduler: ToastScheduler);
34
+ /** The toast currently on screen, or `null` when nothing is shown. */
35
+ get currentToast(): AdwToast | null;
36
+ /** Number of toasts queued behind the visible one. */
37
+ get pendingToasts(): number;
14
38
  /**
15
39
  * Show a toast notification — the web equivalent of Adw.ToastOverlay.add_toast()
16
40
  * with a freshly-built Adw.Toast.
17
41
  *
42
+ * Like Adw.ToastOverlay, only ONE toast is visible at a time: this one shows
43
+ * immediately when the slot is free, otherwise it waits its turn behind the
44
+ * toasts already queued.
45
+ *
18
46
  * @param title The text to display.
19
- * @param options Either a timeout in seconds (legacy shorthand) or an
47
+ * @param options Either a timeout in milliseconds (legacy shorthand) or an
20
48
  * {@link AdwToastOptions} bag (title text already supplied here).
21
49
  */
22
50
  addToast(title: string, options?: number | AdwToastOptions): void;
51
+ /** Dismiss the visible toast early; the next queued toast takes its place. */
52
+ dismiss(): void;
53
+ /** Dismiss the visible toast and discard everything still queued behind it. */
54
+ clearToasts(): void;
55
+ /** Build + attach the strip for `toast` — the DOM half of the queue's `onShow`. */
56
+ private _mountToast;
57
+ /**
58
+ * Detach the mounted strip — the DOM half of the queue's `onHide`.
59
+ *
60
+ * @param immediate Skip the exit transition because the next queued toast is
61
+ * about to take this slot (keeps exactly one strip mounted).
62
+ */
63
+ private _unmountToast;
23
64
  }
@@ -5,7 +5,8 @@ export declare class AdwToggle extends HTMLElement {
5
5
  export declare class AdwToggleGroup extends HTMLElement {
6
6
  private _innerEl;
7
7
  private _buttons;
8
- private _active;
8
+ /** The headless segment list + guarded selection state machine (ADR 0004). */
9
+ private readonly _state;
9
10
  private _initialized;
10
11
  static get observedAttributes(): string[];
11
12
  /** Zero-based index of the active toggle. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/adwaita-web",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Adwaita/Libadwaita web components for browser targets",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -41,13 +41,14 @@
41
41
  "@codemirror/language": "^6.12.4",
42
42
  "@codemirror/state": "^6.7.0",
43
43
  "@codemirror/view": "^6.43.4",
44
- "@gjsify/adwaita-fonts": "^0.22.0",
45
- "@gjsify/adwaita-icons": "^0.22.0",
44
+ "@gjsify/adwaita-core": "^0.23.0",
45
+ "@gjsify/adwaita-fonts": "^0.23.0",
46
+ "@gjsify/adwaita-icons": "^0.23.0",
46
47
  "@lezer/highlight": "^1.2.3"
47
48
  },
48
49
  "devDependencies": {
49
- "@gjsify/cli": "^0.22.0",
50
- "@gjsify/unit": "^0.22.0",
50
+ "@gjsify/cli": "^0.23.0",
51
+ "@gjsify/unit": "^0.23.0",
51
52
  "sass": "^1.101.0",
52
53
  "typescript": "^6.0.3"
53
54
  },
package/scss/_toast.scss CHANGED
@@ -20,8 +20,11 @@ adw-toast-overlay {
20
20
  height: 100%;
21
21
  }
22
22
 
23
- // Toasts are layered over the bottom edge of the overlay, stacked newest-last
24
- // and centered, the way AdwToastOverlay floats them above its child.
23
+ // The toast slot: layered over the bottom edge of the overlay and centered,
24
+ // the way AdwToastOverlay floats a toast above its child. Like
25
+ // Adw.ToastOverlay only ONE toast is mounted at a time (the queue lives in
26
+ // `@gjsify/adwaita-core`); the flex column keeps it centered and lets an
27
+ // outgoing `.hiding` strip finish its exit transition in place.
25
28
  .adw-toast-overlay-toasts {
26
29
  position: absolute;
27
30
  bottom: var(--spacing-m);
@@ -0,0 +1,217 @@
1
+ // DOM-level behaviour tests for the rows whose state machines now come from
2
+ // `@gjsify/adwaita-core` (ADR 0004): <adw-combo-row> (ComboState),
3
+ // <adw-spin-row> (SpinState), <adw-toggle-group> (ToggleGroupState) and
4
+ // <adw-expander-row> (ExpanderState).
5
+ //
6
+ // They pin the contract the composition must preserve — most importantly the
7
+ // programmatic-vs-interactive split this port has always had: a user interaction
8
+ // emits the `notify::*` event, a programmatic property/attribute set updates the
9
+ // display silently.
10
+ import { describe, expect, it } from '@gjsify/unit';
11
+
12
+ import type { AdwComboRow } from './elements/adw-combo-row.js';
13
+ import type { AdwExpanderRow } from './elements/adw-expander-row.js';
14
+ import type { AdwSpinRow } from './elements/adw-spin-row.js';
15
+ import type { AdwToggleGroup } from './elements/adw-toggle-group.js';
16
+
17
+ /** Parse markup so attributes are present when connectedCallback runs. */
18
+ function parse<T extends HTMLElement>(html: string, selector: string): { el: T; host: HTMLElement } {
19
+ const host = document.createElement('div');
20
+ document.body.appendChild(host);
21
+ host.innerHTML = html;
22
+ return { el: host.querySelector(selector) as T, host };
23
+ }
24
+
25
+ /** Collect the `detail` of every `event` dispatched on `el` while `run()` executes. */
26
+ function record(el: HTMLElement, event: string): { details: unknown[]; stop: () => void } {
27
+ const details: unknown[] = [];
28
+ const listener = (e: Event) => details.push((e as CustomEvent).detail);
29
+ el.addEventListener(event, listener);
30
+ return { details, stop: () => el.removeEventListener(event, listener) };
31
+ }
32
+
33
+ export const AdwRowStateTest = async () => {
34
+ await describe('adw-combo-row (ComboState)', async () => {
35
+ await it('renders the selected label and notifies only on a user pick', async () => {
36
+ const { el: row, host } = parse<AdwComboRow>(
37
+ `<adw-combo-row title="Colour" items='["Red","Green","Blue"]' selected="1"></adw-combo-row>`,
38
+ 'adw-combo-row',
39
+ );
40
+ const select = row.querySelector('select') as HTMLSelectElement;
41
+ const value = row.querySelector('.adw-row-value') as HTMLSpanElement;
42
+ const events = record(row, 'notify::selected');
43
+
44
+ expect(row.selected).toBe(1);
45
+ expect(value.textContent).toBe('Green');
46
+ expect(select.selectedIndex).toBe(1);
47
+
48
+ // A user pick through the native <select>.
49
+ select.selectedIndex = 2;
50
+ select.dispatchEvent(new Event('change'));
51
+ expect(row.selected).toBe(2);
52
+ expect(value.textContent).toBe('Blue');
53
+ expect(row.getAttribute('selected')).toBe('2');
54
+ expect(events.details.length).toBe(1);
55
+ expect((events.details[0] as { selected: number }).selected).toBe(2);
56
+
57
+ // A programmatic set refreshes the display but stays silent.
58
+ row.selected = 0;
59
+ expect(value.textContent).toBe('Red');
60
+ expect(select.selectedIndex).toBe(0);
61
+ expect(events.details.length).toBe(1);
62
+
63
+ events.stop();
64
+ host.remove();
65
+ });
66
+
67
+ await it('renders an empty value for an out-of-range selection', async () => {
68
+ const { el: row, host } = parse<AdwComboRow>(
69
+ `<adw-combo-row title="Empty" items='[]' selected="3"></adw-combo-row>`,
70
+ 'adw-combo-row',
71
+ );
72
+ expect((row.querySelector('.adw-row-value') as HTMLSpanElement).textContent).toBe('');
73
+ host.remove();
74
+ });
75
+ });
76
+
77
+ await describe('adw-spin-row (SpinState)', async () => {
78
+ await it('steps at step precision and notifies only on a stepper press', async () => {
79
+ const { el: row, host } = parse<AdwSpinRow>(
80
+ '<adw-spin-row title="Amount" min="0" max="10" step="0.1" value="0"></adw-spin-row>',
81
+ 'adw-spin-row',
82
+ );
83
+ const input = row.querySelector('input') as HTMLInputElement;
84
+ const inc = row.querySelector('.adw-spin-inc') as HTMLButtonElement;
85
+ const events = record(row, 'notify::value');
86
+
87
+ expect(input.value).toBe('0.0');
88
+
89
+ inc.click();
90
+ inc.click();
91
+ inc.click();
92
+ // Without the step-precision rounding this drifts to 0.30000000000000004.
93
+ expect(row.value).toBe(0.3);
94
+ expect(input.value).toBe('0.3');
95
+ expect(events.details.length).toBe(3);
96
+
97
+ // A programmatic set clamps to max and stays silent.
98
+ row.value = 99;
99
+ expect(row.value).toBe(10);
100
+ expect(input.value).toBe('10.0');
101
+ expect(events.details.length).toBe(3);
102
+
103
+ events.stop();
104
+ host.remove();
105
+ });
106
+
107
+ await it('clamps to the lower bound and re-clamps when a bound moves', async () => {
108
+ const { el: row, host } = parse<AdwSpinRow>(
109
+ '<adw-spin-row title="Amount" min="2" max="8" step="1" value="5"></adw-spin-row>',
110
+ 'adw-spin-row',
111
+ );
112
+ const input = row.querySelector('input') as HTMLInputElement;
113
+
114
+ row.value = -4;
115
+ expect(row.value).toBe(2);
116
+
117
+ // Moving the upper bound below the value pulls the value into range.
118
+ row.value = 8;
119
+ row.setAttribute('max', '6');
120
+ expect(row.value).toBe(6);
121
+ expect(input.value).toBe('6');
122
+
123
+ host.remove();
124
+ });
125
+ });
126
+
127
+ await describe('adw-toggle-group (ToggleGroupState)', async () => {
128
+ await it('tracks the active segment and notifies only on a click', async () => {
129
+ const { el: group, host } = parse<AdwToggleGroup>(
130
+ `<adw-toggle-group active="1">
131
+ <adw-toggle label="One"></adw-toggle>
132
+ <adw-toggle label="Two"></adw-toggle>
133
+ <adw-toggle label="Three"></adw-toggle>
134
+ </adw-toggle-group>`,
135
+ 'adw-toggle-group',
136
+ );
137
+ const buttons = Array.from(group.querySelectorAll('button.adw-toggle')) as HTMLButtonElement[];
138
+ const events = record(group, 'notify::active');
139
+
140
+ expect(buttons.length).toBe(3);
141
+ expect(group.active).toBe(1);
142
+ expect(buttons[1].classList.contains('active')).toBe(true);
143
+ expect(buttons[1].getAttribute('aria-pressed')).toBe('true');
144
+
145
+ buttons[2].click();
146
+ expect(group.active).toBe(2);
147
+ expect(buttons[2].classList.contains('active')).toBe(true);
148
+ expect(buttons[1].classList.contains('active')).toBe(false);
149
+ expect(group.getAttribute('active')).toBe('2');
150
+ expect(events.details.length).toBe(1);
151
+
152
+ // Re-clicking the active segment is a no-op.
153
+ buttons[2].click();
154
+ expect(events.details.length).toBe(1);
155
+
156
+ // A programmatic set moves the pill without notifying.
157
+ group.active = 0;
158
+ expect(group.active).toBe(0);
159
+ expect(buttons[0].classList.contains('active')).toBe(true);
160
+ expect(events.details.length).toBe(1);
161
+
162
+ events.stop();
163
+ host.remove();
164
+ });
165
+
166
+ await it('clamps an out-of-range active attribute to the last segment', async () => {
167
+ const { el: group, host } = parse<AdwToggleGroup>(
168
+ `<adw-toggle-group active="99">
169
+ <adw-toggle label="One"></adw-toggle>
170
+ <adw-toggle label="Two"></adw-toggle>
171
+ </adw-toggle-group>`,
172
+ 'adw-toggle-group',
173
+ );
174
+ expect(group.active).toBe(1);
175
+ host.remove();
176
+ });
177
+ });
178
+
179
+ await describe('adw-expander-row (ExpanderState)', async () => {
180
+ await it('discloses on a header click and notifies only then', async () => {
181
+ const { el: row, host } = parse<AdwExpanderRow>(
182
+ '<adw-expander-row title="More"><adw-action-row title="Nested"></adw-action-row></adw-expander-row>',
183
+ 'adw-expander-row',
184
+ );
185
+ const header = row.querySelector('.adw-expander-row-header') as HTMLDivElement;
186
+ const events = record(row, 'notify::expanded');
187
+
188
+ expect(row.expanded).toBe(false);
189
+ expect(row.classList.contains('expanded')).toBe(false);
190
+
191
+ header.click();
192
+ expect(row.expanded).toBe(true);
193
+ expect(row.hasAttribute('expanded')).toBe(true);
194
+ expect(row.classList.contains('expanded')).toBe(true);
195
+ expect(events.details.length).toBe(1);
196
+
197
+ // A programmatic collapse reflects to the attribute but stays silent.
198
+ row.expanded = false;
199
+ expect(row.hasAttribute('expanded')).toBe(false);
200
+ expect(row.classList.contains('expanded')).toBe(false);
201
+ expect(events.details.length).toBe(1);
202
+
203
+ events.stop();
204
+ host.remove();
205
+ });
206
+
207
+ await it('adopts a declarative expanded attribute', async () => {
208
+ const { el: row, host } = parse<AdwExpanderRow>(
209
+ '<adw-expander-row title="Open" expanded></adw-expander-row>',
210
+ 'adw-expander-row',
211
+ );
212
+ expect(row.expanded).toBe(true);
213
+ expect(row.classList.contains('expanded')).toBe(true);
214
+ host.remove();
215
+ });
216
+ });
217
+ };
@@ -0,0 +1,172 @@
1
+ // DOM-level behaviour tests for <adw-toast-overlay>. Runs in a real browser via
2
+ // the @gjsify/adwaita-web browser test axis.
3
+ //
4
+ // The regression these guard: the overlay used to append EVERY toast as its own
5
+ // strip the moment `addToast()` was called, so three calls stacked three visible
6
+ // toasts and each ran its own timer. `Adw.ToastOverlay` shows exactly ONE toast
7
+ // at a time and queues the rest FIFO, advancing only when the visible one is
8
+ // dismissed (manually or by its timeout) — the policy `AdwToastQueue` in
9
+ // `@gjsify/adwaita-core` owns and this element now composes.
10
+ //
11
+ // Timing is driven through core's injected `ToastScheduler` seam (a virtual
12
+ // clock), so the auto-dismiss lifecycle is asserted deterministically with no
13
+ // real timers.
14
+ import { describe, expect, it } from '@gjsify/unit';
15
+
16
+ import type { AdwToastOverlay, ToastScheduler, ToastTimerHandle } from './elements/adw-toast-overlay.js';
17
+
18
+ /** A deterministic {@link ToastScheduler}: timers fire only when time is advanced. */
19
+ class VirtualClock implements ToastScheduler {
20
+ private _now = 0;
21
+ private _seq = 0;
22
+ private readonly _timers = new Map<number, { at: number; run: () => void }>();
23
+
24
+ schedule(callback: () => void, ms: number): ToastTimerHandle {
25
+ const handle = ++this._seq;
26
+ this._timers.set(handle, { at: this._now + ms, run: callback });
27
+ return handle;
28
+ }
29
+
30
+ cancel(handle: ToastTimerHandle): void {
31
+ this._timers.delete(handle as number);
32
+ }
33
+
34
+ /** Move the clock forward, draining every timer that comes due. */
35
+ advance(ms: number): void {
36
+ this._now += ms;
37
+ // A fired timer may schedule another one that is already due (the exit
38
+ // fallback of a toast dismissed mid-advance), so drain until settled.
39
+ for (let guard = 0; guard < 100; guard++) {
40
+ const due = [...this._timers].filter(([, timer]) => timer.at <= this._now);
41
+ if (due.length === 0) return;
42
+ for (const [handle, timer] of due) {
43
+ this._timers.delete(handle);
44
+ timer.run();
45
+ }
46
+ }
47
+ throw new Error('VirtualClock.advance did not settle — a timer keeps rescheduling itself');
48
+ }
49
+ }
50
+
51
+ /** Mount an overlay driven by a virtual clock instead of the browser's timers. */
52
+ function mountOverlay(): { overlay: AdwToastOverlay; clock: VirtualClock; host: HTMLElement } {
53
+ const host = document.createElement('div');
54
+ document.body.appendChild(host);
55
+ host.innerHTML = '<adw-toast-overlay><p>content</p></adw-toast-overlay>';
56
+ const overlay = host.querySelector('adw-toast-overlay') as AdwToastOverlay;
57
+ const clock = new VirtualClock();
58
+ overlay.scheduler = clock;
59
+ return { overlay, clock, host };
60
+ }
61
+
62
+ /** The titles of every toast strip currently in the overlay's DOM. */
63
+ function toastTitles(overlay: AdwToastOverlay): string[] {
64
+ return Array.from(overlay.querySelectorAll('.adw-toast .adw-toast-title')).map((el) => el.textContent ?? '');
65
+ }
66
+
67
+ export const AdwToastOverlayTest = async () => {
68
+ await describe('adw-toast-overlay', async () => {
69
+ await it('shows exactly one toast at a time, in FIFO order', async () => {
70
+ const { overlay, clock, host } = mountOverlay();
71
+
72
+ overlay.addToast('First', { timeout: 3 });
73
+ overlay.addToast('Second', { timeout: 3 });
74
+ overlay.addToast('Third', { timeout: 3 });
75
+
76
+ // BEFORE the fix this was ['First', 'Second', 'Third'] — all three
77
+ // strips mounted at once.
78
+ expect(toastTitles(overlay)).toStrictEqual(['First']);
79
+ expect(overlay.pendingToasts).toBe(2);
80
+ expect(overlay.currentToast?.title).toBe('First');
81
+
82
+ // The next toast appears only once the previous one timed out.
83
+ clock.advance(3000);
84
+ expect(toastTitles(overlay)).toStrictEqual(['Second']);
85
+ expect(overlay.pendingToasts).toBe(1);
86
+
87
+ clock.advance(3000);
88
+ expect(toastTitles(overlay)).toStrictEqual(['Third']);
89
+ expect(overlay.pendingToasts).toBe(0);
90
+
91
+ // The last one fades out; the exit fallback detaches the strip.
92
+ clock.advance(3000);
93
+ expect(overlay.currentToast).toBeNull();
94
+ clock.advance(300);
95
+ expect(toastTitles(overlay)).toStrictEqual([]);
96
+
97
+ host.remove();
98
+ });
99
+
100
+ await it('holds the queue while a sticky toast (timeout 0) is shown', async () => {
101
+ const { overlay, clock, host } = mountOverlay();
102
+
103
+ overlay.addToast('Sticky', { timeout: 0 });
104
+ overlay.addToast('Next', { timeout: 3 });
105
+
106
+ // No timer was scheduled for the sticky toast, so time does not advance it.
107
+ clock.advance(60_000);
108
+ expect(toastTitles(overlay)).toStrictEqual(['Sticky']);
109
+ expect(overlay.pendingToasts).toBe(1);
110
+
111
+ // Dismissing it hands the slot to the queued toast.
112
+ overlay.dismiss();
113
+ expect(toastTitles(overlay)).toStrictEqual(['Next']);
114
+ expect(overlay.pendingToasts).toBe(0);
115
+
116
+ host.remove();
117
+ });
118
+
119
+ await it('advances the queue when the close button is pressed', async () => {
120
+ const { overlay, host } = mountOverlay();
121
+
122
+ overlay.addToast('First', { timeout: 3 });
123
+ overlay.addToast('Second', { timeout: 3 });
124
+
125
+ const close = overlay.querySelector('.adw-toast-close-button') as HTMLButtonElement;
126
+ close.click();
127
+
128
+ expect(toastTitles(overlay)).toStrictEqual(['Second']);
129
+ expect(overlay.pendingToasts).toBe(0);
130
+
131
+ host.remove();
132
+ });
133
+
134
+ await it('runs the action callback and advances the queue', async () => {
135
+ const { overlay, host } = mountOverlay();
136
+
137
+ let undone = 0;
138
+ overlay.addToast('File moved to Trash', {
139
+ timeout: 3,
140
+ buttonLabel: 'Undo',
141
+ onAction: () => {
142
+ undone += 1;
143
+ },
144
+ });
145
+ overlay.addToast('Second', { timeout: 3 });
146
+
147
+ const action = overlay.querySelector('.adw-toast-action-button') as HTMLButtonElement;
148
+ expect(action.textContent).toBe('Undo');
149
+ action.click();
150
+
151
+ expect(undone).toBe(1);
152
+ expect(toastTitles(overlay)).toStrictEqual(['Second']);
153
+
154
+ host.remove();
155
+ });
156
+
157
+ await it('drops the queued toasts on clearToasts()', async () => {
158
+ const { overlay, clock, host } = mountOverlay();
159
+
160
+ overlay.addToast('First', { timeout: 3 });
161
+ overlay.addToast('Second', { timeout: 3 });
162
+ overlay.clearToasts();
163
+
164
+ expect(overlay.currentToast).toBeNull();
165
+ expect(overlay.pendingToasts).toBe(0);
166
+ clock.advance(300);
167
+ expect(toastTitles(overlay)).toStrictEqual([]);
168
+
169
+ host.remove();
170
+ });
171
+ });
172
+ };