@gjsify/adwaita-web 0.31.0 → 0.32.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/dist/adwaita-web.css +786 -272
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/adw-about-dialog.spec.d.ts +1 -0
- package/lib/types/adw-action-rows.spec.d.ts +1 -0
- package/lib/types/adw-avatar.spec.d.ts +1 -0
- package/lib/types/adw-banner.spec.d.ts +1 -0
- package/lib/types/adw-button-content.spec.d.ts +1 -0
- package/lib/types/adw-carousel.spec.d.ts +1 -0
- package/lib/types/adw-checks.spec.d.ts +1 -0
- package/lib/types/adw-header-bar.spec.d.ts +1 -0
- package/lib/types/adw-icon.spec.d.ts +1 -0
- package/lib/types/adw-progress-bar.spec.d.ts +1 -0
- package/lib/types/adw-sidebar.spec.d.ts +1 -0
- package/lib/types/adw-switch.spec.d.ts +1 -0
- package/lib/types/adw-wrap-box.spec.d.ts +1 -0
- package/lib/types/bottom-sheet.spec.d.ts +1 -0
- package/lib/types/breakpoints.d.ts +22 -0
- package/lib/types/breakpoints.spec.d.ts +1 -0
- package/lib/types/chrome.spec.d.ts +1 -0
- package/lib/types/elements/adw-about-dialog.d.ts +47 -8
- package/lib/types/elements/adw-action-row.d.ts +35 -0
- package/lib/types/elements/adw-avatar.d.ts +11 -0
- package/lib/types/elements/adw-bottom-sheet.d.ts +33 -4
- package/lib/types/elements/adw-button-content.d.ts +12 -0
- package/lib/types/elements/adw-button-row.d.ts +10 -1
- package/lib/types/elements/adw-carousel.d.ts +111 -8
- package/lib/types/elements/adw-checks.d.ts +57 -0
- package/lib/types/elements/adw-clamp.d.ts +4 -1
- package/lib/types/elements/adw-data-grid.d.ts +11 -31
- package/lib/types/elements/adw-drop-down.d.ts +17 -20
- package/lib/types/elements/adw-entry-row.d.ts +93 -3
- package/lib/types/elements/adw-expander-row.d.ts +7 -1
- package/lib/types/elements/adw-header-bar.d.ts +15 -0
- package/lib/types/elements/adw-icon.d.ts +25 -0
- package/lib/types/elements/adw-inline-view-switcher.d.ts +37 -11
- package/lib/types/elements/adw-menu-button.d.ts +6 -6
- package/lib/types/elements/adw-navigation-split-view.d.ts +6 -0
- package/lib/types/elements/adw-navigation-view.d.ts +63 -20
- package/lib/types/elements/adw-overlay-split-view.d.ts +38 -0
- package/lib/types/elements/adw-password-entry-row.d.ts +19 -10
- package/lib/types/elements/adw-popover.d.ts +58 -0
- package/lib/types/elements/adw-preferences-dialog.d.ts +57 -2
- package/lib/types/elements/adw-preferences-group.d.ts +31 -0
- package/lib/types/elements/adw-progress-bar.d.ts +38 -0
- package/lib/types/elements/adw-sidebar.d.ts +37 -5
- package/lib/types/elements/adw-split-button.d.ts +53 -7
- package/lib/types/elements/adw-switch-row.d.ts +11 -2
- package/lib/types/elements/adw-switch.d.ts +15 -0
- package/lib/types/elements/adw-tab-view.d.ts +129 -12
- package/lib/types/elements/adw-toolbar-view.d.ts +5 -0
- package/lib/types/elements/adw-view-stack.d.ts +27 -19
- package/lib/types/elements/adw-view-switcher-bar.d.ts +23 -7
- package/lib/types/elements/adw-view-switcher.d.ts +22 -11
- package/lib/types/elements/adw-window-title.d.ts +3 -1
- package/lib/types/elements/view-switcher-dom.d.ts +44 -0
- package/lib/types/entry-rows.spec.d.ts +1 -0
- package/lib/types/index.d.ts +7 -0
- package/lib/types/navigation-view.spec.d.ts +1 -0
- package/lib/types/preferences-search.d.ts +20 -0
- package/lib/types/preferences.spec.d.ts +1 -0
- package/lib/types/split-button.spec.d.ts +1 -0
- package/lib/types/split-views.spec.d.ts +1 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/lib/types/tab-view.spec.d.ts +1 -0
- package/lib/types/view-stack.spec.d.ts +1 -0
- package/lib/types/view-switcher.spec.d.ts +1 -0
- package/package.json +6 -6
- package/scss/_banner.scss +13 -0
- package/scss/_bottom_sheet.scss +5 -6
- package/scss/_button.scss +3 -26
- package/scss/_button_content.scss +28 -0
- package/scss/_checks.scss +228 -0
- package/scss/_clamp.scss +9 -3
- package/scss/_data_grid.scss +15 -7
- package/scss/_drop_down.scss +12 -55
- package/scss/_expander_row.scss +5 -50
- package/scss/_icon.scss +51 -0
- package/scss/_icons.generated.scss +6 -0
- package/scss/_inline_view_switcher.scss +34 -0
- package/scss/_labels.scss +135 -0
- package/scss/_menu_button.scss +10 -70
- package/scss/_popover.scss +186 -0
- package/scss/_preferences.scss +29 -0
- package/scss/_preferences_dialog.scss +17 -0
- package/scss/_progress_bar.scss +161 -0
- package/scss/_reset.scss +18 -7
- package/scss/_spinner.scss +7 -3
- package/scss/_split_button.scss +6 -50
- package/scss/_switch.scss +90 -0
- package/scss/_switch_row.scss +6 -52
- package/scss/_tab_view.scss +64 -0
- package/scss/_theme.scss +10 -0
- package/scss/_toolbar_view.scss +74 -0
- package/scss/_variables.scss +34 -0
- package/scss/_view_switcher.scss +40 -0
- package/scss/_view_switcher_bar.scss +37 -0
- package/scss/_wrap_box.scss +56 -6
- package/scss/adwaita-skin.scss +23 -0
- package/src/adw-about-dialog.spec.ts +350 -0
- package/src/adw-action-rows.spec.ts +342 -0
- package/src/adw-avatar.spec.ts +139 -0
- package/src/adw-banner.spec.ts +152 -0
- package/src/adw-button-content.spec.ts +183 -0
- package/src/adw-button-row.spec.ts +12 -6
- package/src/adw-carousel.spec.ts +389 -0
- package/src/adw-checks.spec.ts +368 -0
- package/src/adw-drop-down.spec.ts +123 -0
- package/src/adw-header-bar.spec.ts +83 -0
- package/src/adw-icon.spec.ts +167 -0
- package/src/adw-progress-bar.spec.ts +173 -0
- package/src/adw-sidebar.spec.ts +332 -0
- package/src/adw-switch.spec.ts +242 -0
- package/src/adw-tab-view.spec.ts +12 -3
- package/src/adw-view-switcher-bar.spec.ts +61 -13
- package/src/adw-wrap-box.spec.ts +194 -0
- package/src/bottom-sheet.spec.ts +325 -0
- package/src/breakpoints.spec.ts +147 -0
- package/src/breakpoints.ts +67 -0
- package/src/chrome.spec.ts +301 -0
- package/src/elements/adw-about-dialog.ts +223 -163
- package/src/elements/adw-action-row.ts +120 -10
- package/src/elements/adw-avatar.ts +48 -54
- package/src/elements/adw-banner.ts +46 -10
- package/src/elements/adw-bottom-sheet.ts +145 -60
- package/src/elements/adw-button-content.ts +107 -33
- package/src/elements/adw-button-row.ts +61 -37
- package/src/elements/adw-button.ts +5 -7
- package/src/elements/adw-carousel.ts +467 -105
- package/src/elements/adw-checks.ts +299 -0
- package/src/elements/adw-clamp.ts +80 -10
- package/src/elements/adw-combo-row.ts +15 -7
- package/src/elements/adw-data-grid.ts +43 -91
- package/src/elements/adw-drop-down.ts +117 -128
- package/src/elements/adw-entry-row.ts +404 -36
- package/src/elements/adw-expander-row.ts +61 -29
- package/src/elements/adw-header-bar.ts +31 -6
- package/src/elements/adw-icon.ts +148 -0
- package/src/elements/adw-inline-view-switcher.ts +240 -129
- package/src/elements/adw-menu-button.ts +93 -70
- package/src/elements/adw-navigation-split-view.ts +48 -7
- package/src/elements/adw-navigation-view.ts +286 -148
- package/src/elements/adw-overlay-split-view.ts +164 -14
- package/src/elements/adw-password-entry-row.ts +111 -89
- package/src/elements/adw-popover.ts +283 -0
- package/src/elements/adw-preferences-dialog.ts +120 -21
- package/src/elements/adw-preferences-group.ts +128 -22
- package/src/elements/adw-progress-bar.ts +189 -0
- package/src/elements/adw-sidebar.ts +301 -142
- package/src/elements/adw-spin-row.ts +11 -5
- package/src/elements/adw-spinner.ts +23 -8
- package/src/elements/adw-split-button.ts +371 -92
- package/src/elements/adw-status-page.ts +9 -9
- package/src/elements/adw-switch-row.ts +100 -30
- package/src/elements/adw-switch.ts +129 -0
- package/src/elements/adw-tab-view.ts +682 -129
- package/src/elements/adw-toggle-group.ts +5 -7
- package/src/elements/adw-toolbar-view.ts +84 -21
- package/src/elements/adw-view-stack.ts +162 -85
- package/src/elements/adw-view-switcher-bar.ts +163 -81
- package/src/elements/adw-view-switcher.ts +231 -112
- package/src/elements/adw-window-title.ts +44 -11
- package/src/elements/adw-wrap-box.ts +131 -35
- package/src/elements/view-switcher-dom.ts +108 -0
- package/src/entry-rows.spec.ts +391 -0
- package/src/index.ts +23 -0
- package/src/navigation-view.spec.ts +435 -0
- package/src/preferences-search.ts +135 -0
- package/src/preferences.spec.ts +250 -0
- package/src/source-view/adw-source-view.ts +2 -5
- package/src/split-button.spec.ts +470 -0
- package/src/split-views.spec.ts +309 -0
- package/src/styles.generated.ts +1 -1
- package/src/tab-view.spec.ts +451 -0
- package/src/test.browser.mts +49 -0
- package/src/view-stack.spec.ts +272 -0
- package/src/view-switcher.spec.ts +446 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
// DOM-level tests for <adw-checkbox> and <adw-radio>, plus the browser side of
|
|
2
|
+
// the `RADIO_GROUP_VECTORS` conformance table.
|
|
3
|
+
//
|
|
4
|
+
// THE ONE CLAIM WORTH THE MOST HERE: the browser's native radio exclusivity is
|
|
5
|
+
// NOT enough. `<input type="radio" name="g">` unchecks its sibling INPUT and
|
|
6
|
+
// stops there — the sibling `<adw-radio>` HOST keeps its `checked` attribute,
|
|
7
|
+
// which is the element's published state AND the selector `_checks.scss` paints
|
|
8
|
+
// from, so a group left to the browser draws two selected radios. Every vector
|
|
9
|
+
// below reads back the HOST attribute of every member for exactly that reason.
|
|
10
|
+
//
|
|
11
|
+
// The vectors run under a per-scenario group-name prefix: `RadioGroupState` is
|
|
12
|
+
// document-global by design (as a bare `<input type=radio name=x>` outside a
|
|
13
|
+
// form is), so two scenarios sharing the literal name `colour` would share a
|
|
14
|
+
// selection. The prefix is the isolation, not a change to what is being tested.
|
|
15
|
+
|
|
16
|
+
import { describe, expect, it } from '@gjsify/unit';
|
|
17
|
+
import { RADIO_GROUP_VECTORS } from '@gjsify/adwaita-core/conformance';
|
|
18
|
+
|
|
19
|
+
import type { AdwCheckbox, AdwRadio } from './elements/adw-checks.js';
|
|
20
|
+
|
|
21
|
+
function host(): HTMLElement {
|
|
22
|
+
const el = document.createElement('div');
|
|
23
|
+
document.body.appendChild(el);
|
|
24
|
+
return el;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function mountCheckbox(): { el: AdwCheckbox; parent: HTMLElement } {
|
|
28
|
+
const parent = host();
|
|
29
|
+
const el = document.createElement('adw-checkbox') as AdwCheckbox;
|
|
30
|
+
parent.appendChild(el);
|
|
31
|
+
return { el, parent };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The parts, by the markup contract the stylesheet selects on. */
|
|
35
|
+
function parts(check: HTMLElement): { input: HTMLInputElement; indicator: HTMLElement; label: HTMLElement } {
|
|
36
|
+
return {
|
|
37
|
+
input: check.querySelector('input.adw-check-input') as HTMLInputElement,
|
|
38
|
+
indicator: check.querySelector('.adw-check-indicator') as HTMLElement,
|
|
39
|
+
label: check.querySelector('.adw-check-label') as HTMLElement,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Collect the `detail` of every `event` bubbling to `el`. */
|
|
44
|
+
function record(el: HTMLElement, event: string): unknown[] {
|
|
45
|
+
const details: unknown[] = [];
|
|
46
|
+
el.addEventListener(event, (e) => details.push((e as CustomEvent).detail));
|
|
47
|
+
return details;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The glyph the indicator's mask is currently painting. */
|
|
51
|
+
function glyph(check: HTMLElement): string {
|
|
52
|
+
return getComputedStyle(parts(check).indicator, '::after').getPropertyValue('mask-image');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const AdwChecksTest = async () => {
|
|
56
|
+
await describe('<adw-checkbox> state', async () => {
|
|
57
|
+
await it('builds the input + indicator + label markup', () => {
|
|
58
|
+
const { el, parent } = mountCheckbox();
|
|
59
|
+
const { input, indicator, label } = parts(el);
|
|
60
|
+
expect(input).toBeTruthy();
|
|
61
|
+
expect(input.type).toBe('checkbox');
|
|
62
|
+
expect(indicator).toBeTruthy();
|
|
63
|
+
// The focus ring is `input:focus-visible ~ .adw-check-indicator`, so
|
|
64
|
+
// the input MUST precede the indicator.
|
|
65
|
+
expect(input.nextElementSibling).toBe(indicator);
|
|
66
|
+
expect(el.checked).toBe(false);
|
|
67
|
+
expect(input.getAttribute('aria-checked')).toBe('false');
|
|
68
|
+
// An empty label takes no space.
|
|
69
|
+
expect(label.hidden).toBe(true);
|
|
70
|
+
parent.remove();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await it('adopts authored child nodes as the label, and the attribute wins', () => {
|
|
74
|
+
const parent = host();
|
|
75
|
+
const authored = document.createElement('adw-checkbox') as AdwCheckbox;
|
|
76
|
+
authored.textContent = 'Enable';
|
|
77
|
+
parent.appendChild(authored);
|
|
78
|
+
expect(parts(authored).label.textContent).toBe('Enable');
|
|
79
|
+
expect(parts(authored).label.hidden).toBe(false);
|
|
80
|
+
|
|
81
|
+
const labelled = document.createElement('adw-checkbox') as AdwCheckbox;
|
|
82
|
+
labelled.setAttribute('label', 'From the attribute');
|
|
83
|
+
labelled.textContent = 'ignored';
|
|
84
|
+
parent.appendChild(labelled);
|
|
85
|
+
expect(parts(labelled).label.textContent).toBe('From the attribute');
|
|
86
|
+
parent.remove();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await it('a programmatic set notifies, and re-setting the same value does not', () => {
|
|
90
|
+
const { el, parent } = mountCheckbox();
|
|
91
|
+
const events = record(el, 'notify::checked');
|
|
92
|
+
el.checked = true;
|
|
93
|
+
expect(events).toStrictEqual([{ checked: true }]);
|
|
94
|
+
expect(parts(el).input.checked).toBe(true);
|
|
95
|
+
el.checked = true;
|
|
96
|
+
expect(events.length).toBe(1);
|
|
97
|
+
el.checked = false;
|
|
98
|
+
expect(events.length).toBe(2);
|
|
99
|
+
parent.remove();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
await it('a click toggles exactly once', () => {
|
|
103
|
+
const { el, parent } = mountCheckbox();
|
|
104
|
+
const events = record(el, 'notify::checked');
|
|
105
|
+
el.click();
|
|
106
|
+
expect(el.checked).toBe(true);
|
|
107
|
+
expect(el.hasAttribute('checked')).toBe(true);
|
|
108
|
+
expect(events.length).toBe(1);
|
|
109
|
+
parent.remove();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await it('a click on the hidden input toggles exactly once', () => {
|
|
113
|
+
// The input is 0x0 so a pointer never reaches it, but keyboard
|
|
114
|
+
// activation and `input.click()` do — and the host listener must not
|
|
115
|
+
// toggle a second time on the way out.
|
|
116
|
+
const { el, parent } = mountCheckbox();
|
|
117
|
+
const events = record(el, 'notify::checked');
|
|
118
|
+
parts(el).input.click();
|
|
119
|
+
expect(el.checked).toBe(true);
|
|
120
|
+
expect(events.length).toBe(1);
|
|
121
|
+
parent.remove();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await it('a disabled checkbox does not toggle on a click', () => {
|
|
125
|
+
const { el, parent } = mountCheckbox();
|
|
126
|
+
el.disabled = true;
|
|
127
|
+
const events = record(el, 'notify::checked');
|
|
128
|
+
el.click();
|
|
129
|
+
expect(el.checked).toBe(false);
|
|
130
|
+
expect(events.length).toBe(0);
|
|
131
|
+
expect(parts(el).input.disabled).toBe(true);
|
|
132
|
+
// …but it is not frozen: a programmatic set still applies, matching
|
|
133
|
+
// GObject property semantics (sensitivity gates INPUT, not value).
|
|
134
|
+
el.checked = true;
|
|
135
|
+
expect(events).toStrictEqual([{ checked: true }]);
|
|
136
|
+
parent.remove();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await describe('<adw-checkbox> indeterminate', async () => {
|
|
141
|
+
await it('announces as mixed and outranks checked', () => {
|
|
142
|
+
const { el, parent } = mountCheckbox();
|
|
143
|
+
el.checked = true;
|
|
144
|
+
el.indeterminate = true;
|
|
145
|
+
const { input } = parts(el);
|
|
146
|
+
// HTML has a native mixed CHECKBOX, and an explicit aria-checked so
|
|
147
|
+
// the radio (which has no native mixed) reads the same way.
|
|
148
|
+
expect(input.indeterminate).toBe(true);
|
|
149
|
+
expect(input.getAttribute('aria-checked')).toBe('mixed');
|
|
150
|
+
parent.remove();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
await it('draws the dash even when also checked', () => {
|
|
154
|
+
// `check, radio { &:indeterminate { … dash.svg } }`
|
|
155
|
+
// (_checks.scss:79-81) is re-declared AFTER both `:checked` rules at
|
|
156
|
+
// equal specificity, so the dash wins. Comparing the three states
|
|
157
|
+
// against each other proves the cascade, not just that a mask exists.
|
|
158
|
+
const parent = host();
|
|
159
|
+
const make = (attrs: string[]) => {
|
|
160
|
+
const el = document.createElement('adw-checkbox') as AdwCheckbox;
|
|
161
|
+
for (const attr of attrs) el.setAttribute(attr, '');
|
|
162
|
+
parent.appendChild(el);
|
|
163
|
+
return el;
|
|
164
|
+
};
|
|
165
|
+
const checked = make(['checked']);
|
|
166
|
+
const mixed = make(['indeterminate']);
|
|
167
|
+
const both = make(['checked', 'indeterminate']);
|
|
168
|
+
|
|
169
|
+
expect(glyph(both)).toBe(glyph(mixed));
|
|
170
|
+
expect(glyph(both)).not.toBe(glyph(checked));
|
|
171
|
+
parent.remove();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
await it('activating an indeterminate checkbox clears it and checks', () => {
|
|
175
|
+
// HTML's pre-click activation steps, not a reading of
|
|
176
|
+
// GtkCheckButton:inconsistent — GTK's is not verifiable in this tree.
|
|
177
|
+
const { el, parent } = mountCheckbox();
|
|
178
|
+
el.indeterminate = true;
|
|
179
|
+
el.click();
|
|
180
|
+
expect(el.indeterminate).toBe(false);
|
|
181
|
+
expect(el.checked).toBe(true);
|
|
182
|
+
parent.remove();
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
await describe('check and radio share one stylesheet', async () => {
|
|
187
|
+
await it('both draw the 14px + 3px indicator, differing only in the radius', () => {
|
|
188
|
+
const parent = host();
|
|
189
|
+
const check = document.createElement('adw-checkbox') as AdwCheckbox;
|
|
190
|
+
const radio = document.createElement('adw-radio') as AdwRadio;
|
|
191
|
+
parent.append(check, radio);
|
|
192
|
+
|
|
193
|
+
for (const el of [check, radio]) {
|
|
194
|
+
const style = getComputedStyle(parts(el).indicator);
|
|
195
|
+
// GTK sizes the CONTENT box at 14px and grows the drawn control
|
|
196
|
+
// with `padding: 3px` (_checks.scss:22-26) — content-box, not
|
|
197
|
+
// the ADR-0010 descendant border-box pin.
|
|
198
|
+
expect(style.width).toBe('14px');
|
|
199
|
+
expect(style.height).toBe('14px');
|
|
200
|
+
expect(style.paddingTop).toBe('3px');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// `check { border-radius: 6px }` (:67-68) vs
|
|
204
|
+
// `radio { border-radius: 100% }` (:73-74).
|
|
205
|
+
expect(getComputedStyle(parts(check).indicator).borderTopLeftRadius).toBe('6px');
|
|
206
|
+
expect(getComputedStyle(parts(radio).indicator).borderTopLeftRadius).toContain('%');
|
|
207
|
+
parent.remove();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
await it('a checked radio FILLS, with the SAME accent background as a checked check', () => {
|
|
211
|
+
// refs/adwaita-web's `_radio.scss:22-33` colours only the border and
|
|
212
|
+
// the dot, leaving the indicator on the view background; libadwaita
|
|
213
|
+
// fills the whole control from the block it SHARES with the checkbox
|
|
214
|
+
// (:52-60). libadwaita wins — so the two fills must be identical.
|
|
215
|
+
//
|
|
216
|
+
// THREE elements rather than one flipped in place: the indicator
|
|
217
|
+
// TRANSITIONS `background`, so a computed read taken right after
|
|
218
|
+
// `checked = true` returns the INTERPOLATED colour — still the old
|
|
219
|
+
// one. Declaring the state before insertion leaves no transition to
|
|
220
|
+
// be part-way through. The names are distinct so seeding one group
|
|
221
|
+
// cannot reach the reference radio.
|
|
222
|
+
const parent = host();
|
|
223
|
+
const plain = document.createElement('adw-radio') as AdwRadio;
|
|
224
|
+
plain.setAttribute('name', 'fill-plain');
|
|
225
|
+
const radio = document.createElement('adw-radio') as AdwRadio;
|
|
226
|
+
radio.setAttribute('name', 'fill-checked');
|
|
227
|
+
radio.setAttribute('value', 'x');
|
|
228
|
+
radio.setAttribute('checked', '');
|
|
229
|
+
const check = document.createElement('adw-checkbox') as AdwCheckbox;
|
|
230
|
+
check.setAttribute('checked', '');
|
|
231
|
+
parent.append(plain, radio, check);
|
|
232
|
+
|
|
233
|
+
const before = getComputedStyle(parts(plain).indicator).backgroundColor;
|
|
234
|
+
const after = getComputedStyle(parts(radio).indicator).backgroundColor;
|
|
235
|
+
|
|
236
|
+
expect(after).not.toBe(before);
|
|
237
|
+
expect(after).toBe(getComputedStyle(parts(check).indicator).backgroundColor);
|
|
238
|
+
parent.remove();
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
await describe('<adw-radio> group exclusivity (conformance vectors)', async () => {
|
|
243
|
+
for (const [index, vector] of RADIO_GROUP_VECTORS.entries()) {
|
|
244
|
+
await it(`${vector.name} — ${vector.rule}`, () => {
|
|
245
|
+
const parent = host();
|
|
246
|
+
// Per-scenario prefix: the registry is document-global, so two
|
|
247
|
+
// scenarios sharing a literal group name would share a selection.
|
|
248
|
+
const scope = (name: string) => `v${index}-${name}`;
|
|
249
|
+
|
|
250
|
+
const members = vector.members.map(([name, value]) => {
|
|
251
|
+
const el = document.createElement('adw-radio') as AdwRadio;
|
|
252
|
+
el.setAttribute('name', scope(name));
|
|
253
|
+
el.setAttribute('value', value);
|
|
254
|
+
parent.appendChild(el);
|
|
255
|
+
return el;
|
|
256
|
+
});
|
|
257
|
+
const events = record(parent, 'notify::checked');
|
|
258
|
+
|
|
259
|
+
for (const step of vector.steps) {
|
|
260
|
+
const target = members.find((el) => el.name === scope(step.name) && el.value === step.value);
|
|
261
|
+
expect(target).toBeTruthy();
|
|
262
|
+
(target as AdwRadio).click();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const expected = new Map(vector.selected.map(([name, value]) => [scope(name), value]));
|
|
266
|
+
for (const [i, el] of members.entries()) {
|
|
267
|
+
const [name, value] = vector.members[i];
|
|
268
|
+
const shouldHold = expected.get(scope(name)) === value;
|
|
269
|
+
// The HOST attribute, not the input: this is precisely what
|
|
270
|
+
// the browser's own exclusivity leaves stale.
|
|
271
|
+
expect(el.hasAttribute('checked')).toBe(shouldHold);
|
|
272
|
+
expect(parts(el).input.checked).toBe(shouldHold);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Every emitted change repaints the winner, and the loser too
|
|
276
|
+
// when there was one — that second repaint is the whole reason
|
|
277
|
+
// the group state exists.
|
|
278
|
+
const expectedEvents = vector.emitted.reduce(
|
|
279
|
+
(total, change) => total + (change.deselected === null ? 1 : 2),
|
|
280
|
+
0,
|
|
281
|
+
);
|
|
282
|
+
expect(events.length).toBe(expectedEvents);
|
|
283
|
+
parent.remove();
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
await describe('<adw-radio> group behaviour the browser alone gets wrong', async () => {
|
|
289
|
+
await it('picking a second member clears the first HOST attribute', () => {
|
|
290
|
+
const parent = host();
|
|
291
|
+
const first = document.createElement('adw-radio') as AdwRadio;
|
|
292
|
+
first.setAttribute('name', 'stale-check');
|
|
293
|
+
first.setAttribute('value', 'a');
|
|
294
|
+
first.setAttribute('checked', '');
|
|
295
|
+
const second = document.createElement('adw-radio') as AdwRadio;
|
|
296
|
+
second.setAttribute('name', 'stale-check');
|
|
297
|
+
second.setAttribute('value', 'b');
|
|
298
|
+
parent.append(first, second);
|
|
299
|
+
|
|
300
|
+
second.click();
|
|
301
|
+
|
|
302
|
+
expect(second.checked).toBe(true);
|
|
303
|
+
expect(first.checked).toBe(false);
|
|
304
|
+
expect(first.hasAttribute('checked')).toBe(false);
|
|
305
|
+
expect(parts(first).input.checked).toBe(false);
|
|
306
|
+
parent.remove();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
await it('groups are scoped by name', () => {
|
|
310
|
+
const parent = host();
|
|
311
|
+
const make = (name: string, value: string) => {
|
|
312
|
+
const el = document.createElement('adw-radio') as AdwRadio;
|
|
313
|
+
el.setAttribute('name', name);
|
|
314
|
+
el.setAttribute('value', value);
|
|
315
|
+
parent.appendChild(el);
|
|
316
|
+
return el;
|
|
317
|
+
};
|
|
318
|
+
const left = make('scoped-a', 'x');
|
|
319
|
+
const right = make('scoped-b', 'x');
|
|
320
|
+
|
|
321
|
+
left.click();
|
|
322
|
+
right.click();
|
|
323
|
+
|
|
324
|
+
expect(left.checked).toBe(true);
|
|
325
|
+
expect(right.checked).toBe(true);
|
|
326
|
+
parent.remove();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
await it('a NAMELESS radio is not in a group', () => {
|
|
330
|
+
// HTML forms a radio button group only from a non-empty `name`.
|
|
331
|
+
// Without that rule every nameless radio in the document would land
|
|
332
|
+
// in one `''` group and fight over it — which is exactly what a
|
|
333
|
+
// registry keyed by name does if nothing guards the empty key.
|
|
334
|
+
const parent = host();
|
|
335
|
+
const first = document.createElement('adw-radio') as AdwRadio;
|
|
336
|
+
const second = document.createElement('adw-radio') as AdwRadio;
|
|
337
|
+
parent.append(first, second);
|
|
338
|
+
|
|
339
|
+
first.click();
|
|
340
|
+
second.click();
|
|
341
|
+
|
|
342
|
+
expect(first.checked).toBe(true);
|
|
343
|
+
expect(second.checked).toBe(true);
|
|
344
|
+
parent.remove();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
await it('a value falls back to the label text so a bare group still works', () => {
|
|
348
|
+
const parent = host();
|
|
349
|
+
const make = (text: string) => {
|
|
350
|
+
const el = document.createElement('adw-radio') as AdwRadio;
|
|
351
|
+
el.setAttribute('name', 'bare');
|
|
352
|
+
el.setAttribute('label', text);
|
|
353
|
+
parent.appendChild(el);
|
|
354
|
+
return el;
|
|
355
|
+
};
|
|
356
|
+
const red = make('Red');
|
|
357
|
+
const green = make('Green');
|
|
358
|
+
|
|
359
|
+
expect(red.value).toBe('Red');
|
|
360
|
+
red.click();
|
|
361
|
+
green.click();
|
|
362
|
+
|
|
363
|
+
expect(red.checked).toBe(false);
|
|
364
|
+
expect(green.checked).toBe(true);
|
|
365
|
+
parent.remove();
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
};
|
|
@@ -2,8 +2,21 @@
|
|
|
2
2
|
// @gjsify/adwaita-web browser test axis. Asserts option parsing, selected↔value
|
|
3
3
|
// sync, notify-on-change semantics, popover open/close, item selection, search
|
|
4
4
|
// filtering and the disabled guard.
|
|
5
|
+
//
|
|
6
|
+
// The SELECTION half is driven by the SAME table the NativeScript renderer
|
|
7
|
+
// asserts against (`COMBO_SELECTION_VECTORS`, `@gjsify/adwaita-core/conformance`)
|
|
8
|
+
// — replayed against the real element through the API a consumer would use, and
|
|
9
|
+
// read back as what a user would see: the button label, `selected`/`selectedValue`
|
|
10
|
+
// and the `change` stream. `<adw-combo-row>` composes the same `ComboState` but
|
|
11
|
+
// cannot drive the table, because two of its four step ops have no DOM spelling:
|
|
12
|
+
// its options come from the `items` attribute at connect time only (`items` is not
|
|
13
|
+
// even observed), and it publishes no select-by-value setter. So this element is
|
|
14
|
+
// the browser's driver for both.
|
|
5
15
|
import { describe, it, expect } from '@gjsify/unit';
|
|
6
16
|
|
|
17
|
+
import { COMBO_SELECTION_VECTORS } from '@gjsify/adwaita-core/conformance';
|
|
18
|
+
import type { ComboSelectionStep, ComboSelectionVector } from '@gjsify/adwaita-core/conformance';
|
|
19
|
+
|
|
7
20
|
import type { AdwDropDown } from './elements/adw-drop-down.js';
|
|
8
21
|
|
|
9
22
|
function makeDropDown(): AdwDropDown {
|
|
@@ -12,7 +25,117 @@ function makeDropDown(): AdwDropDown {
|
|
|
12
25
|
return el;
|
|
13
26
|
}
|
|
14
27
|
|
|
28
|
+
/** The text the closed button shows — the rendered `selectedLabel`. */
|
|
29
|
+
function labelText(dd: AdwDropDown): string {
|
|
30
|
+
return dd.querySelector('.adw-drop-down-label')?.textContent ?? '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Open the chooser the way a user does. A no-op over an empty model, as the element guards. */
|
|
34
|
+
function openChooser(dd: AdwDropDown): void {
|
|
35
|
+
(dd.querySelector('.adw-drop-down-button') as HTMLButtonElement).click();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Replay one vector step against the real element — property sets for the
|
|
40
|
+
* programmatic ops, a popover gesture for the interactive one.
|
|
41
|
+
*/
|
|
42
|
+
function applyStep(dd: AdwDropDown, step: ComboSelectionStep): void {
|
|
43
|
+
switch (step.op) {
|
|
44
|
+
case 'setOptions':
|
|
45
|
+
dd.options = step.options;
|
|
46
|
+
return;
|
|
47
|
+
case 'setSelectedIndex':
|
|
48
|
+
dd.selected = step.index;
|
|
49
|
+
return;
|
|
50
|
+
case 'setSelectedValue':
|
|
51
|
+
dd.selectedValue = step.value;
|
|
52
|
+
return;
|
|
53
|
+
case 'select':
|
|
54
|
+
openChooser(dd);
|
|
55
|
+
if (step.index < 0) {
|
|
56
|
+
// The dismissed chooser: the popover closes with nothing picked,
|
|
57
|
+
// which here is an outside pointerdown rather than a cancelled
|
|
58
|
+
// native sheet.
|
|
59
|
+
document.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
(dd.querySelectorAll('.adw-drop-down-item')[step.index] as HTMLButtonElement | undefined)?.click();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The ONE row this element answers differently, and does so on purpose: its
|
|
69
|
+
* published `selected` contract REJECTS an out-of-range set (`_selectIndex` gates
|
|
70
|
+
* on `ComboState.hasIndex`), where the core state accepts it and reports an empty
|
|
71
|
+
* value — the split `ComboState.hasIndex`'s doc comment records and
|
|
72
|
+
* `<adw-combo-row>` takes the other side of. The row is still driven below,
|
|
73
|
+
* against the element's answer rather than the state's, so neither policy can
|
|
74
|
+
* change silently.
|
|
75
|
+
*/
|
|
76
|
+
const DROP_DOWN_REJECTS = 'an index past the end';
|
|
77
|
+
|
|
78
|
+
/** The named vector — a hard failure if the table renamed it out from under the carve-out. */
|
|
79
|
+
function vectorNamed(name: string): ComboSelectionVector {
|
|
80
|
+
const vector = COMBO_SELECTION_VECTORS.find((v) => v.name === name);
|
|
81
|
+
if (!vector) throw new Error(`COMBO_SELECTION_VECTORS has no row "${name}" — this spec's carve-out is stale`);
|
|
82
|
+
return vector;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Mount a drop-down and record every `change` detail it dispatches. */
|
|
86
|
+
function mountRecording(): { dd: AdwDropDown; changes: unknown[] } {
|
|
87
|
+
const dd = makeDropDown();
|
|
88
|
+
const changes: unknown[] = [];
|
|
89
|
+
dd.addEventListener('change', (event) => changes.push((event as CustomEvent).detail));
|
|
90
|
+
return { dd, changes };
|
|
91
|
+
}
|
|
92
|
+
|
|
15
93
|
export const AdwDropDownTest = async () => {
|
|
94
|
+
await describe('adw-drop-down selection (libadwaita conformance vectors)', async () => {
|
|
95
|
+
for (const vector of COMBO_SELECTION_VECTORS) {
|
|
96
|
+
if (vector.name === DROP_DOWN_REJECTS) continue;
|
|
97
|
+
|
|
98
|
+
await it(`${vector.name} — ${vector.rule}`, async () => {
|
|
99
|
+
const { dd, changes } = mountRecording();
|
|
100
|
+
for (const step of vector.steps) applyStep(dd, step);
|
|
101
|
+
|
|
102
|
+
expect(dd.selected).toBe(vector.selected);
|
|
103
|
+
expect(dd.selectedValue).toBe(vector.value);
|
|
104
|
+
expect(labelText(dd)).toBe(vector.label);
|
|
105
|
+
// `change` is this element's DOM spelling of the `interactive`
|
|
106
|
+
// flag: it fires for a user pick and never for a programmatic
|
|
107
|
+
// set, so the interactive frames of the emission stream ARE the
|
|
108
|
+
// events a listener sees.
|
|
109
|
+
expect(changes).toStrictEqual(
|
|
110
|
+
vector.emitted
|
|
111
|
+
.filter((change) => change.interactive)
|
|
112
|
+
.map((change) => ({ index: change.selected, value: change.value, label: change.label })),
|
|
113
|
+
);
|
|
114
|
+
dd.remove();
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
await it(`${DROP_DOWN_REJECTS} — rejected here: the element's published contract, not the state's`, async () => {
|
|
119
|
+
const vector = vectorNamed(DROP_DOWN_REJECTS);
|
|
120
|
+
const { dd, changes } = mountRecording();
|
|
121
|
+
|
|
122
|
+
// Everything up to the out-of-range set…
|
|
123
|
+
for (const step of vector.steps.slice(0, -1)) applyStep(dd, step);
|
|
124
|
+
const before = { selected: dd.selected, value: dd.selectedValue, label: labelText(dd) };
|
|
125
|
+
// …and then the set itself, which the core state would accept and
|
|
126
|
+
// report as nothing-to-draw (`value`/`label` both empty below).
|
|
127
|
+
expect(vector.value).toBe('');
|
|
128
|
+
expect(vector.label).toBe('');
|
|
129
|
+
applyStep(dd, vector.steps[vector.steps.length - 1]);
|
|
130
|
+
|
|
131
|
+
// The element refused it: the selection did not move, and a refusal
|
|
132
|
+
// is not a change, so nothing was dispatched either.
|
|
133
|
+
expect({ selected: dd.selected, value: dd.selectedValue, label: labelText(dd) }).toStrictEqual(before);
|
|
134
|
+
expect(changes).toStrictEqual([]);
|
|
135
|
+
dd.remove();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
16
139
|
await describe('adw-drop-down options', async () => {
|
|
17
140
|
await it('accepts a string[] (value === label)', async () => {
|
|
18
141
|
const dd = makeDropDown();
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// DOM-level tests for <adw-header-bar>'s title.
|
|
2
|
+
//
|
|
3
|
+
// `title` was read exactly once, in `connectedCallback`, and the element declared
|
|
4
|
+
// no `observedAttributes` at all — so every later write was a silent no-op. A
|
|
5
|
+
// header bar whose title tracks the open document kept whatever it was created
|
|
6
|
+
// with, and nothing in the build was in a position to notice: the attribute was
|
|
7
|
+
// still THERE, and the element still rendered A title.
|
|
8
|
+
//
|
|
9
|
+
// `Adw.HeaderBar` builds its centre from `title`/`subtitle` through a derived
|
|
10
|
+
// `AdwWindowTitle` bound to the properties, unless a `title-widget` replaces it
|
|
11
|
+
// — which is the either/or the `slot="center"` case below asserts.
|
|
12
|
+
|
|
13
|
+
import { describe, expect, it } from '@gjsify/unit';
|
|
14
|
+
|
|
15
|
+
/** The rendered centre text of a mounted header bar. */
|
|
16
|
+
function centerText(bar: HTMLElement): string {
|
|
17
|
+
return bar.querySelector('.adw-header-bar-center')?.textContent ?? '';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Mount a header bar with the given attributes and slotted children. */
|
|
21
|
+
function mountHeaderBar(attrs: Record<string, string>, slotted: readonly HTMLElement[] = []): HTMLElement {
|
|
22
|
+
const bar = document.createElement('adw-header-bar');
|
|
23
|
+
for (const [name, value] of Object.entries(attrs)) bar.setAttribute(name, value);
|
|
24
|
+
for (const child of slotted) bar.appendChild(child);
|
|
25
|
+
document.body.appendChild(bar);
|
|
26
|
+
return bar;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A `slot="center"` title-widget, the equivalent of `Adw.HeaderBar:title-widget`. */
|
|
30
|
+
function centerWidget(text: string): HTMLElement {
|
|
31
|
+
const widget = document.createElement('div');
|
|
32
|
+
widget.setAttribute('slot', 'center');
|
|
33
|
+
widget.id = 'url-entry';
|
|
34
|
+
widget.textContent = text;
|
|
35
|
+
return widget;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Remove every header bar a test mounted. */
|
|
39
|
+
function unmountAll(): void {
|
|
40
|
+
for (const bar of Array.from(document.querySelectorAll('adw-header-bar'))) bar.remove();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const AdwHeaderBarTest = async () => {
|
|
44
|
+
await describe('<adw-header-bar> title', async () => {
|
|
45
|
+
await it('renders the title it was created with', () => {
|
|
46
|
+
const bar = mountHeaderBar({ title: 'Documents' });
|
|
47
|
+
expect(centerText(bar)).toBe('Documents');
|
|
48
|
+
unmountAll();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
await it('follows a LATER write, instead of freezing at connect time', () => {
|
|
52
|
+
const bar = mountHeaderBar({ title: 'Documents' });
|
|
53
|
+
bar.setAttribute('title', 'Pictures');
|
|
54
|
+
expect(centerText(bar)).toBe('Pictures');
|
|
55
|
+
unmountAll();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await it('clears when the attribute is removed', () => {
|
|
59
|
+
const bar = mountHeaderBar({ title: 'Documents' });
|
|
60
|
+
bar.removeAttribute('title');
|
|
61
|
+
expect(centerText(bar)).toBe('');
|
|
62
|
+
unmountAll();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await it('a title set before the element is connected still lands', () => {
|
|
66
|
+
const bar = document.createElement('adw-header-bar');
|
|
67
|
+
bar.setAttribute('title', 'Early');
|
|
68
|
+
document.body.appendChild(bar);
|
|
69
|
+
expect(centerText(bar)).toBe('Early');
|
|
70
|
+
unmountAll();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await it('a slot="center" widget wins, and a title write does not overwrite it', () => {
|
|
74
|
+
const bar = mountHeaderBar({ title: 'Ignored' }, [centerWidget('example.org')]);
|
|
75
|
+
expect(bar.querySelector('#url-entry')).toBeTruthy();
|
|
76
|
+
expect(centerText(bar)).toBe('example.org');
|
|
77
|
+
|
|
78
|
+
bar.setAttribute('title', 'Still ignored');
|
|
79
|
+
expect(centerText(bar)).toBe('example.org');
|
|
80
|
+
unmountAll();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
};
|