@gjsify/adwaita-web 0.31.0 → 0.33.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.
Files changed (183) hide show
  1. package/dist/adwaita-web.css +835 -299
  2. package/dist/adwaita-web.css.map +1 -1
  3. package/lib/types/adw-about-dialog.spec.d.ts +1 -0
  4. package/lib/types/adw-action-rows.spec.d.ts +1 -0
  5. package/lib/types/adw-avatar.spec.d.ts +1 -0
  6. package/lib/types/adw-banner.spec.d.ts +1 -0
  7. package/lib/types/adw-button-content.spec.d.ts +1 -0
  8. package/lib/types/adw-carousel.spec.d.ts +1 -0
  9. package/lib/types/adw-checks.spec.d.ts +1 -0
  10. package/lib/types/adw-header-bar.spec.d.ts +1 -0
  11. package/lib/types/adw-icon.spec.d.ts +1 -0
  12. package/lib/types/adw-progress-bar.spec.d.ts +1 -0
  13. package/lib/types/adw-sidebar.spec.d.ts +1 -0
  14. package/lib/types/adw-switch.spec.d.ts +1 -0
  15. package/lib/types/adw-wrap-box.spec.d.ts +1 -0
  16. package/lib/types/bottom-sheet.spec.d.ts +1 -0
  17. package/lib/types/breakpoints.d.ts +22 -0
  18. package/lib/types/breakpoints.spec.d.ts +1 -0
  19. package/lib/types/chrome.spec.d.ts +1 -0
  20. package/lib/types/elements/adw-about-dialog.d.ts +47 -8
  21. package/lib/types/elements/adw-action-row.d.ts +35 -0
  22. package/lib/types/elements/adw-avatar.d.ts +11 -0
  23. package/lib/types/elements/adw-bottom-sheet.d.ts +33 -4
  24. package/lib/types/elements/adw-button-content.d.ts +12 -0
  25. package/lib/types/elements/adw-button-row.d.ts +10 -1
  26. package/lib/types/elements/adw-carousel.d.ts +111 -8
  27. package/lib/types/elements/adw-checks.d.ts +57 -0
  28. package/lib/types/elements/adw-clamp.d.ts +4 -1
  29. package/lib/types/elements/adw-combo-row.d.ts +10 -0
  30. package/lib/types/elements/adw-data-grid.d.ts +11 -31
  31. package/lib/types/elements/adw-drop-down.d.ts +17 -20
  32. package/lib/types/elements/adw-entry-row.d.ts +93 -3
  33. package/lib/types/elements/adw-expander-row.d.ts +7 -1
  34. package/lib/types/elements/adw-header-bar.d.ts +15 -0
  35. package/lib/types/elements/adw-icon.d.ts +25 -0
  36. package/lib/types/elements/adw-inline-view-switcher.d.ts +73 -11
  37. package/lib/types/elements/adw-menu-button.d.ts +6 -6
  38. package/lib/types/elements/adw-navigation-split-view.d.ts +48 -0
  39. package/lib/types/elements/adw-navigation-view.d.ts +63 -20
  40. package/lib/types/elements/adw-overlay-split-view.d.ts +94 -0
  41. package/lib/types/elements/adw-password-entry-row.d.ts +19 -10
  42. package/lib/types/elements/adw-popover.d.ts +58 -0
  43. package/lib/types/elements/adw-preferences-dialog.d.ts +57 -2
  44. package/lib/types/elements/adw-preferences-group.d.ts +31 -0
  45. package/lib/types/elements/adw-progress-bar.d.ts +38 -0
  46. package/lib/types/elements/adw-sidebar.d.ts +37 -5
  47. package/lib/types/elements/adw-spinner.d.ts +23 -0
  48. package/lib/types/elements/adw-split-button.d.ts +53 -7
  49. package/lib/types/elements/adw-switch-row.d.ts +11 -2
  50. package/lib/types/elements/adw-switch.d.ts +15 -0
  51. package/lib/types/elements/adw-tab-view.d.ts +129 -12
  52. package/lib/types/elements/adw-toolbar-view.d.ts +5 -0
  53. package/lib/types/elements/adw-view-stack.d.ts +39 -19
  54. package/lib/types/elements/adw-view-switcher-bar.d.ts +25 -7
  55. package/lib/types/elements/adw-view-switcher.d.ts +76 -13
  56. package/lib/types/elements/adw-window-title.d.ts +3 -1
  57. package/lib/types/elements/adw-wrap-box.d.ts +58 -3
  58. package/lib/types/elements/view-switcher-dom.d.ts +44 -0
  59. package/lib/types/entry-rows.spec.d.ts +1 -0
  60. package/lib/types/index.d.ts +7 -0
  61. package/lib/types/navigation-view.spec.d.ts +1 -0
  62. package/lib/types/preferences-search.d.ts +20 -0
  63. package/lib/types/preferences.spec.d.ts +1 -0
  64. package/lib/types/split-button.spec.d.ts +1 -0
  65. package/lib/types/split-views.spec.d.ts +1 -0
  66. package/lib/types/styles.generated.d.ts +1 -1
  67. package/lib/types/tab-view.spec.d.ts +1 -0
  68. package/lib/types/view-stack.spec.d.ts +1 -0
  69. package/lib/types/view-switcher.spec.d.ts +1 -0
  70. package/package.json +6 -6
  71. package/scss/_banner.scss +13 -0
  72. package/scss/_bottom_sheet.scss +5 -6
  73. package/scss/_button.scss +3 -26
  74. package/scss/_button_content.scss +28 -0
  75. package/scss/_checks.scss +228 -0
  76. package/scss/_clamp.scss +9 -3
  77. package/scss/_combo_row.scss +19 -0
  78. package/scss/_data_grid.scss +15 -7
  79. package/scss/_drop_down.scss +12 -55
  80. package/scss/_expander_row.scss +5 -50
  81. package/scss/_icon.scss +51 -0
  82. package/scss/_icons.generated.scss +6 -0
  83. package/scss/_inline_view_switcher.scss +34 -0
  84. package/scss/_labels.scss +135 -0
  85. package/scss/_menu_button.scss +10 -70
  86. package/scss/_navigation_split_view.scss +47 -8
  87. package/scss/_overlay_split_view.scss +117 -32
  88. package/scss/_popover.scss +186 -0
  89. package/scss/_preferences.scss +29 -0
  90. package/scss/_preferences_dialog.scss +17 -0
  91. package/scss/_progress_bar.scss +161 -0
  92. package/scss/_reset.scss +18 -7
  93. package/scss/_spinner.scss +37 -19
  94. package/scss/_split_button.scss +6 -50
  95. package/scss/_switch.scss +90 -0
  96. package/scss/_switch_row.scss +6 -52
  97. package/scss/_tab_view.scss +63 -0
  98. package/scss/_theme.scss +10 -0
  99. package/scss/_toolbar_view.scss +74 -0
  100. package/scss/_variables.scss +34 -0
  101. package/scss/_view_switcher.scss +40 -0
  102. package/scss/_view_switcher_bar.scss +37 -0
  103. package/scss/_wrap_box.scss +65 -6
  104. package/scss/adwaita-skin.scss +23 -0
  105. package/src/adw-about-dialog.spec.ts +350 -0
  106. package/src/adw-action-rows.spec.ts +342 -0
  107. package/src/adw-avatar.spec.ts +176 -0
  108. package/src/adw-banner.spec.ts +152 -0
  109. package/src/adw-button-content.spec.ts +207 -0
  110. package/src/adw-button-row.spec.ts +12 -6
  111. package/src/adw-carousel.spec.ts +389 -0
  112. package/src/adw-checks.spec.ts +368 -0
  113. package/src/adw-drop-down.spec.ts +123 -0
  114. package/src/adw-header-bar.spec.ts +83 -0
  115. package/src/adw-icon.spec.ts +167 -0
  116. package/src/adw-progress-bar.spec.ts +173 -0
  117. package/src/adw-row-state.spec.ts +49 -0
  118. package/src/adw-sidebar.spec.ts +332 -0
  119. package/src/adw-switch.spec.ts +242 -0
  120. package/src/adw-tab-view.spec.ts +12 -3
  121. package/src/adw-view-switcher-bar.spec.ts +61 -13
  122. package/src/adw-wrap-box.spec.ts +341 -0
  123. package/src/bottom-sheet.spec.ts +325 -0
  124. package/src/breakpoints.spec.ts +147 -0
  125. package/src/breakpoints.ts +67 -0
  126. package/src/chrome.spec.ts +400 -0
  127. package/src/elements/adw-about-dialog.ts +223 -163
  128. package/src/elements/adw-action-row.ts +131 -11
  129. package/src/elements/adw-avatar.ts +48 -54
  130. package/src/elements/adw-banner.ts +46 -10
  131. package/src/elements/adw-bottom-sheet.ts +145 -60
  132. package/src/elements/adw-button-content.ts +107 -33
  133. package/src/elements/adw-button-row.ts +61 -37
  134. package/src/elements/adw-button.ts +5 -7
  135. package/src/elements/adw-carousel.ts +467 -105
  136. package/src/elements/adw-checks.ts +299 -0
  137. package/src/elements/adw-clamp.ts +80 -10
  138. package/src/elements/adw-combo-row.ts +32 -7
  139. package/src/elements/adw-data-grid.ts +43 -91
  140. package/src/elements/adw-drop-down.ts +117 -128
  141. package/src/elements/adw-entry-row.ts +404 -36
  142. package/src/elements/adw-expander-row.ts +61 -29
  143. package/src/elements/adw-header-bar.ts +31 -6
  144. package/src/elements/adw-icon.ts +148 -0
  145. package/src/elements/adw-inline-view-switcher.ts +325 -131
  146. package/src/elements/adw-menu-button.ts +93 -70
  147. package/src/elements/adw-navigation-split-view.ts +220 -14
  148. package/src/elements/adw-navigation-view.ts +286 -148
  149. package/src/elements/adw-overlay-split-view.ts +536 -28
  150. package/src/elements/adw-password-entry-row.ts +111 -89
  151. package/src/elements/adw-popover.ts +283 -0
  152. package/src/elements/adw-preferences-dialog.ts +120 -21
  153. package/src/elements/adw-preferences-group.ts +128 -22
  154. package/src/elements/adw-progress-bar.ts +189 -0
  155. package/src/elements/adw-sidebar.ts +301 -142
  156. package/src/elements/adw-spin-row.ts +11 -5
  157. package/src/elements/adw-spinner.ts +168 -9
  158. package/src/elements/adw-split-button.ts +371 -92
  159. package/src/elements/adw-status-page.ts +9 -9
  160. package/src/elements/adw-switch-row.ts +100 -30
  161. package/src/elements/adw-switch.ts +129 -0
  162. package/src/elements/adw-tab-view.ts +700 -129
  163. package/src/elements/adw-toggle-group.ts +5 -7
  164. package/src/elements/adw-toolbar-view.ts +84 -21
  165. package/src/elements/adw-view-stack.ts +183 -85
  166. package/src/elements/adw-view-switcher-bar.ts +171 -81
  167. package/src/elements/adw-view-switcher.ts +328 -117
  168. package/src/elements/adw-window-title.ts +44 -11
  169. package/src/elements/adw-wrap-box.ts +329 -78
  170. package/src/elements/view-switcher-dom.ts +108 -0
  171. package/src/entry-rows.spec.ts +391 -0
  172. package/src/index.ts +23 -0
  173. package/src/navigation-view.spec.ts +435 -0
  174. package/src/preferences-search.ts +135 -0
  175. package/src/preferences.spec.ts +250 -0
  176. package/src/source-view/adw-source-view.ts +2 -5
  177. package/src/split-button.spec.ts +470 -0
  178. package/src/split-views.spec.ts +893 -0
  179. package/src/styles.generated.ts +1 -1
  180. package/src/tab-view.spec.ts +451 -0
  181. package/src/test.browser.mts +49 -0
  182. package/src/view-stack.spec.ts +272 -0
  183. package/src/view-switcher.spec.ts +665 -0
@@ -0,0 +1,167 @@
1
+ // DOM-level tests for <adw-icon>, driven by the SAME `normalizeIconName` vectors
2
+ // the core suite asserts against (`@gjsify/adwaita-core/conformance`), plus the
3
+ // REGRESSION PROOF for the drift the element was extracted to close.
4
+ //
5
+ // The drift: `<span class="adw-icon adw-icon--<name>" aria-hidden="true">` was
6
+ // built by hand at twenty-three sites across seventeen files, nine of them
7
+ // re-deriving `name.replace(/-symbolic$/, '')` on the spot. Exactly ONE site —
8
+ // `<adw-split-button>` — also checked the result was a single CSS token. So
9
+ // `icon-name="a b"` interpolated into `class="adw-icon adw-icon--a b"` and shipped
10
+ // a stray `b` class through `<adw-menu-button>`, for its own icon AND for every
11
+ // JSON menu entry's, while the one element that guarded kept passing its spec.
12
+ // The vectors below now run against the ELEMENT, so a renderer that starts
13
+ // building the class by hand again fails naming the input.
14
+ import { describe, expect, it } from '@gjsify/unit';
15
+
16
+ import { VIEW_STACK_ICON_NAME_VECTORS } from '@gjsify/adwaita-core/conformance';
17
+
18
+ import type { AdwIcon } from './elements/adw-icon.js';
19
+ import type { AdwMenuButton } from './elements/adw-menu-button.js';
20
+
21
+ function mount<T extends HTMLElement>(tag: string): { el: T; host: HTMLElement } {
22
+ const host = document.createElement('div');
23
+ document.body.appendChild(host);
24
+ const el = document.createElement(tag) as T;
25
+ host.appendChild(el);
26
+ return { el, host };
27
+ }
28
+
29
+ /** Every class on the node that is not the base `.adw-icon`. */
30
+ function extraClasses(el: HTMLElement): string[] {
31
+ return [...el.classList].filter((c) => c !== 'adw-icon').sort();
32
+ }
33
+
34
+ export const AdwIconTest = async () => {
35
+ await describe('<adw-icon> mask class (normalizeIconName vectors)', async () => {
36
+ for (const { icon, normalized, rule } of VIEW_STACK_ICON_NAME_VECTORS) {
37
+ await it(`${JSON.stringify(icon)} → ${JSON.stringify(normalized)} — ${rule}`, () => {
38
+ const { el, host } = mount<AdwIcon>('adw-icon');
39
+ el.iconName = icon ?? null;
40
+
41
+ expect(el.resolvedIconName).toBe(normalized);
42
+ // The base class is unconditional — a nameless icon is still an
43
+ // icon box, which is what `<adw-status-page>` hides rather than
44
+ // omits.
45
+ expect(el.classList.contains('adw-icon')).toBe(true);
46
+ // …and NOTHING else is on the node. This is the assertion the
47
+ // hand-rolled copies could not make: `icon-name="a b"` used to
48
+ // leave a stray `b` here.
49
+ expect(extraClasses(el)).toStrictEqual(normalized === '' ? [] : [`adw-icon--${normalized}`]);
50
+ host.remove();
51
+ });
52
+ }
53
+ });
54
+
55
+ await describe('<adw-icon> node contract', async () => {
56
+ await it('is decorative — the accessible name belongs to the host control', () => {
57
+ const { el, host } = mount<AdwIcon>('adw-icon');
58
+ expect(el.getAttribute('aria-hidden')).toBe('true');
59
+ host.remove();
60
+ });
61
+
62
+ await it('swaps ONLY the mask class, keeping the caller position classes', () => {
63
+ const { el, host } = mount<AdwIcon>('adw-icon');
64
+ el.classList.add('adw-drop-down-arrow', 'start');
65
+ el.iconName = 'go-down';
66
+ expect(extraClasses(el)).toStrictEqual(['adw-drop-down-arrow', 'adw-icon--go-down', 'start']);
67
+
68
+ el.iconName = 'go-next';
69
+ // Exactly one mask class at a time, and the caller's are untouched —
70
+ // a `className =` assignment would have taken them with it.
71
+ expect(extraClasses(el)).toStrictEqual(['adw-drop-down-arrow', 'adw-icon--go-next', 'start']);
72
+
73
+ el.iconName = null;
74
+ expect(extraClasses(el)).toStrictEqual(['adw-drop-down-arrow', 'start']);
75
+ host.remove();
76
+ });
77
+
78
+ await it('sizes the box AND the mask together', () => {
79
+ const { el, host } = mount<AdwIcon>('adw-icon');
80
+ el.iconName = 'go-next';
81
+ // Unset: the stylesheet's 16px.
82
+ expect(el.size).toBe(null);
83
+ expect(getComputedStyle(el).width).toBe('16px');
84
+
85
+ el.size = 32;
86
+ const styled = getComputedStyle(el);
87
+ expect(styled.width).toBe('32px');
88
+ expect(styled.height).toBe('32px');
89
+ // Sizing only the box would leave a 16px glyph floating in it. Only
90
+ // the axis we set is asserted: `mask-size: 32px` means "32px wide,
91
+ // height from the aspect ratio", and the CSSOM re-serializes the
92
+ // omitted half (Firefox reports `32px auto`, in the inline style too).
93
+ expect(styled.maskSize.split(' ')[0]).toBe('32px');
94
+ expect(styled.webkitMaskSize.split(' ')[0]).toBe('32px');
95
+
96
+ el.size = null;
97
+ expect(getComputedStyle(el).width).toBe('16px');
98
+ host.remove();
99
+ });
100
+
101
+ await it('paints in the INHERITED colour, not the boundary reset colour', () => {
102
+ // `.adw-icon` masks with `currentColor`, and <adw-icon> is a custom
103
+ // element — so the ADR-0010 `$adw-components` reset re-roots `color`
104
+ // on it. Without `color: inherit` in `_icon.scss` every icon would
105
+ // repaint in `--window-fg-color` regardless of the button it sits in.
106
+ const { el: box, host } = mount<HTMLElement>('div');
107
+ box.style.color = 'rgb(255, 0, 0)';
108
+ const icon = document.createElement('adw-icon') as AdwIcon;
109
+ icon.iconName = 'go-next';
110
+ box.appendChild(icon);
111
+ expect(getComputedStyle(icon).backgroundColor).toBe('rgb(255, 0, 0)');
112
+ host.remove();
113
+ });
114
+ });
115
+
116
+ await describe('the hand-rolled copies are gone', async () => {
117
+ await it('<adw-menu-button> no longer injects a stray class from its icon-name', () => {
118
+ const { el, host } = mount<AdwMenuButton>('adw-menu-button');
119
+ el.setAttribute('icon-name', 'a b');
120
+ const icon = el.querySelector('.adw-icon') as HTMLElement;
121
+ // `adw-icon--a b` would have added a second, unrelated `b` class.
122
+ expect(extraClasses(icon)).toStrictEqual(['adw-menu-button-icon']);
123
+ host.remove();
124
+ });
125
+
126
+ await it('<adw-menu-button> menu entries guard their icons too', () => {
127
+ const { el, host } = mount<AdwMenuButton>('adw-menu-button');
128
+ el.menuItems = [
129
+ { label: 'Bad', icon: 'a b' },
130
+ { label: 'Good', icon: 'view-refresh-symbolic' },
131
+ { label: 'None' },
132
+ ];
133
+ const items = [...el.querySelectorAll('.adw-menu-button-item')];
134
+ expect(items.length).toBe(3);
135
+ // An unusable name draws nothing rather than injecting markup, and
136
+ // an entry with no icon still gets no icon node at all.
137
+ expect(items[0].querySelector('.adw-icon')).toBe(null);
138
+ expect(items[2].querySelector('.adw-icon')).toBe(null);
139
+ const good = items[1].querySelector('.adw-icon') as HTMLElement;
140
+ expect(extraClasses(good)).toStrictEqual(['adw-icon--view-refresh', 'adw-menu-button-item-icon']);
141
+ host.remove();
142
+ });
143
+
144
+ await it('every icon in the package is an <adw-icon>', () => {
145
+ // The point of the extraction, asserted structurally: build a page of
146
+ // the widgets that draw icons and check no bare span carries the
147
+ // base class. A new hand-rolled `<span class="adw-icon …">` fails
148
+ // here even if it happens to guard its own name correctly.
149
+ const { el: page, host } = mount<HTMLElement>('div');
150
+ page.innerHTML = `
151
+ <adw-button icon="go-next" label="Next"></adw-button>
152
+ <adw-button-row title="Row" start-icon-name="list-add" end-icon-name="go-next"></adw-button-row>
153
+ <adw-status-page icon="folder" title="Empty"></adw-status-page>
154
+ <adw-split-button label="Save" menu='[{"label":"Save As"}]'></adw-split-button>
155
+ <adw-drop-down options='["a","b"]'></adw-drop-down>
156
+ <adw-expander-row title="More" show-enable-switch></adw-expander-row>
157
+ <adw-avatar text="Ada" icon="avatar-default"></adw-avatar>
158
+ <adw-toggle-group><adw-toggle icon-name="view-list"></adw-toggle></adw-toggle-group>
159
+ `;
160
+ const bareSpans = [...host.querySelectorAll('span.adw-icon')];
161
+ expect(bareSpans.length).toBe(0);
162
+ // …and the icons really are there, so the check is not vacuous.
163
+ expect(host.querySelectorAll('adw-icon.adw-icon').length > 5).toBe(true);
164
+ host.remove();
165
+ });
166
+ });
167
+ };
@@ -0,0 +1,173 @@
1
+ // DOM-level tests for <adw-progress-bar>.
2
+ //
3
+ // Three things here are worth more than the attribute round-trips: the CLAMP is
4
+ // GLib's (`glibClamp`, shared from `@gjsify/adwaita-core`, not a local
5
+ // `Math.min/max`), the ZERO fraction has to make the indicator VANISH rather
6
+ // than draw a rounded stub (`> trough.empty > progress { all: unset }`,
7
+ // _progress-bar.scss:85), and the text node has to actually PICK UP `.dimmed` +
8
+ // `.numeric` from `_labels.scss` — that composition is the reason those classes
9
+ // exist, and a `@extend` that silently reached nothing would look identical in
10
+ // the source.
11
+
12
+ import { describe, expect, it } from '@gjsify/unit';
13
+
14
+ import type { AdwProgressBar } from './elements/adw-progress-bar.js';
15
+
16
+ function mount(): { el: AdwProgressBar; host: HTMLElement } {
17
+ const host = document.createElement('div');
18
+ document.body.appendChild(host);
19
+ const el = document.createElement('adw-progress-bar') as AdwProgressBar;
20
+ host.appendChild(el);
21
+ return { el, host };
22
+ }
23
+
24
+ /** The parts, by the markup contract the stylesheet selects on. */
25
+ function parts(bar: AdwProgressBar): { trough: HTMLElement; progress: HTMLElement; text: HTMLElement } {
26
+ return {
27
+ trough: bar.querySelector('.adw-progress-bar-trough') as HTMLElement,
28
+ progress: bar.querySelector('.adw-progress-bar-progress') as HTMLElement,
29
+ text: bar.querySelector('.adw-progress-bar-text') as HTMLElement,
30
+ };
31
+ }
32
+
33
+ export const AdwProgressBarTest = async () => {
34
+ await describe('<adw-progress-bar> fraction', async () => {
35
+ await it('starts empty, with the trough flagged so the indicator disappears', () => {
36
+ const { el, host } = mount();
37
+ const { trough, progress } = parts(el);
38
+ expect(el.fraction).toBe(0);
39
+ expect(trough).toBeTruthy();
40
+ expect(progress).toBeTruthy();
41
+ expect(trough.classList.contains('empty')).toBe(true);
42
+ expect(el.getAttribute('role')).toBe('progressbar');
43
+ expect(el.getAttribute('aria-valuenow')).toBe('0');
44
+ host.remove();
45
+ });
46
+
47
+ await it('fills to the fraction and drops the empty flag', () => {
48
+ const { el, host } = mount();
49
+ el.fraction = 0.25;
50
+ const { trough, progress } = parts(el);
51
+ expect(trough.classList.contains('empty')).toBe(false);
52
+ expect(progress.style.width).toBe('25%');
53
+ expect(el.getAttribute('aria-valuenow')).toBe('0.25');
54
+ host.remove();
55
+ });
56
+
57
+ await it('clamps out-of-range and unusable fractions', () => {
58
+ const { el, host } = mount();
59
+ el.fraction = 1.5;
60
+ expect(el.fraction).toBe(1);
61
+ expect(parts(el).progress.style.width).toBe('100%');
62
+ el.fraction = -3;
63
+ expect(el.fraction).toBe(0);
64
+ // A garbage attribute is GtkProgressBar's default 0, never NaN —
65
+ // NaN would reach both the width and `aria-valuenow`.
66
+ el.setAttribute('fraction', 'later');
67
+ expect(el.fraction).toBe(0);
68
+ expect(el.getAttribute('aria-valuenow')).toBe('0');
69
+ host.remove();
70
+ });
71
+ });
72
+
73
+ await describe('<adw-progress-bar> indeterminate', async () => {
74
+ await it('pulse() enters the pulsing state and drops aria-valuenow', () => {
75
+ const { el, host } = mount();
76
+ el.fraction = 0.4;
77
+ expect(el.getAttribute('aria-valuenow')).toBe('0.4');
78
+
79
+ el.pulse();
80
+
81
+ expect(el.pulsing).toBe(true);
82
+ expect(el.hasAttribute('pulsing')).toBe(true);
83
+ // ARIA spells "indeterminate" as an ABSENT valuenow; a stale 0.4
84
+ // would be announced as real progress.
85
+ expect(el.hasAttribute('aria-valuenow')).toBe(false);
86
+ // A pulsing bar is never `.empty` — the bouncing block is what says
87
+ // "working", so it must not be unset away.
88
+ expect(parts(el).trough.classList.contains('empty')).toBe(false);
89
+ host.remove();
90
+ });
91
+
92
+ await it('leaving the pulsing state restores the determinate value', () => {
93
+ const { el, host } = mount();
94
+ el.fraction = 0;
95
+ el.pulse();
96
+ el.pulsing = false;
97
+ expect(el.getAttribute('aria-valuenow')).toBe('0');
98
+ expect(parts(el).trough.classList.contains('empty')).toBe(true);
99
+ host.remove();
100
+ });
101
+ });
102
+
103
+ await describe('<adw-progress-bar> text', async () => {
104
+ await it('hides the text node until show-text, then shows the percentage', () => {
105
+ const { el, host } = mount();
106
+ expect(parts(el).text.hidden).toBe(true);
107
+
108
+ el.fraction = 0.42;
109
+ el.showText = true;
110
+
111
+ expect(parts(el).text.hidden).toBe(false);
112
+ expect(parts(el).text.textContent).toBe('42%');
113
+ host.remove();
114
+ });
115
+
116
+ await it('an explicit text wins over the percentage', () => {
117
+ const { el, host } = mount();
118
+ el.showText = true;
119
+ el.fraction = 0.5;
120
+ el.text = 'Copying 3 of 6';
121
+ expect(el.text).toBe('Copying 3 of 6');
122
+ expect(parts(el).text.textContent).toBe('Copying 3 of 6');
123
+ host.remove();
124
+ });
125
+
126
+ await it('the text node composes .dimmed and .numeric from _labels.scss', () => {
127
+ // `> text { @extend .dimmed; @extend .numeric; }`
128
+ // (_progress-bar.scss:17-21) — this asserts the extends actually
129
+ // REACHED the utility classes, which a source read cannot tell.
130
+ const { el, host } = mount();
131
+ el.showText = true;
132
+ const style = getComputedStyle(parts(el).text);
133
+ expect(style.getPropertyValue('font-variant-numeric')).toBe('tabular-nums');
134
+ expect(Number.parseFloat(style.opacity)).toBeLessThan(1);
135
+ host.remove();
136
+ });
137
+ });
138
+
139
+ await describe('<adw-progress-bar> variants', async () => {
140
+ await it('is an 8px trough by default and a 2px hairline with osd', () => {
141
+ // The 8px is libadwaita's (_progress-bar.scss:6) and the 2px is its
142
+ // `.osd` variant (:59-83) — NOT the vendored web port's 6px, which
143
+ // has no OSD variant at all.
144
+ //
145
+ // TWO bars rather than one flipped in place, on purpose: the trough
146
+ // TRANSITIONS `background`, so a computed read taken right after an
147
+ // attribute flip returns the INTERPOLATED colour — i.e. still the
148
+ // old one — and the assertion would be timing-dependent. Setting the
149
+ // state before the element's first style resolution leaves no
150
+ // transition to be part-way through.
151
+ const host = document.createElement('div');
152
+ document.body.appendChild(host);
153
+ const plain = document.createElement('adw-progress-bar') as AdwProgressBar;
154
+ const osd = document.createElement('adw-progress-bar') as AdwProgressBar;
155
+ osd.setAttribute('osd', '');
156
+ host.append(plain, osd);
157
+
158
+ expect(getComputedStyle(parts(plain).trough).height).toBe('8px');
159
+ const osdTrough = getComputedStyle(parts(osd).trough);
160
+ expect(osdTrough.height).toBe('2px');
161
+ expect(osdTrough.backgroundColor).toBe('rgba(0, 0, 0, 0)');
162
+ host.remove();
163
+ });
164
+
165
+ await it('inverted fills from the far end', () => {
166
+ const { el, host } = mount();
167
+ expect(getComputedStyle(parts(el).trough).justifyContent).toBe('normal');
168
+ el.inverted = true;
169
+ expect(getComputedStyle(parts(el).trough).justifyContent).toBe('flex-end');
170
+ host.remove();
171
+ });
172
+ });
173
+ };
@@ -9,6 +9,8 @@
9
9
  // display silently.
10
10
  import { describe, expect, it } from '@gjsify/unit';
11
11
 
12
+ import { COMBO_CHOOSER_VECTORS } from '@gjsify/adwaita-core/conformance';
13
+
12
14
  import type { AdwComboRow } from './elements/adw-combo-row.js';
13
15
  import type { AdwExpanderRow } from './elements/adw-expander-row.js';
14
16
  import type { AdwSpinRow } from './elements/adw-spin-row.js';
@@ -72,6 +74,53 @@ export const AdwRowStateTest = async () => {
72
74
  expect((row.querySelector('.adw-row-value') as HTMLSpanElement).textContent).toBe('');
73
75
  host.remove();
74
76
  });
77
+
78
+ for (const { count, presentsChooser, rule } of COMBO_CHOOSER_VECTORS) {
79
+ await it(`${count} option(s) → ${presentsChooser ? 'a chooser' : 'not a chooser'}: ${rule}`, async () => {
80
+ const items = JSON.stringify(Array.from({ length: count }, (_, i) => `Item ${i}`));
81
+ const { el: row, host } = parse<AdwComboRow>(
82
+ `<adw-combo-row title="Pick" items='${items}'></adw-combo-row>`,
83
+ 'adw-combo-row',
84
+ );
85
+ const value = row.querySelector('.adw-row-value') as HTMLSpanElement;
86
+ const select = row.querySelector('select') as HTMLSelectElement;
87
+
88
+ // The ARROW: `.adw-row-value::after` is masked in, and the
89
+ // `[data-presents-chooser='false']` block removes it. Asserted
90
+ // as "not none" rather than against the authored `inline-block`,
91
+ // because `.adw-row-value` is a flex container and blockifies
92
+ // its children's display.
93
+ expect(getComputedStyle(value, '::after').display === 'none').toBe(!presentsChooser);
94
+ // ...and the ROW being activatable, which on this port is the
95
+ // overlaid <select> taking clicks at all. Hiding the arrow
96
+ // without this leaves a row that still opens a one-entry popup.
97
+ expect(select.disabled).toBe(!presentsChooser);
98
+
99
+ host.remove();
100
+ });
101
+ }
102
+
103
+ await it('gains its arrow back when the model grows past one item', async () => {
104
+ const { el: row, host } = parse<AdwComboRow>(
105
+ `<adw-combo-row title="Pick" items='["Only"]'></adw-combo-row>`,
106
+ 'adw-combo-row',
107
+ );
108
+ const select = row.querySelector('select') as HTMLSelectElement;
109
+ expect(select.disabled).toBe(true);
110
+
111
+ // `items` is read at connect only, so the model is replaced through
112
+ // the state the element composes — the same path a consumer has.
113
+ (
114
+ row as unknown as { _state: { setOptions(o: { label: string; value: string }[]): void } }
115
+ )._state.setOptions([
116
+ { label: 'One', value: 'a' },
117
+ { label: 'Two', value: 'b' },
118
+ ]);
119
+ expect(select.disabled).toBe(false);
120
+ expect(row.dataset.presentsChooser).toBe('true');
121
+
122
+ host.remove();
123
+ });
75
124
  });
76
125
 
77
126
  await describe('adw-spin-row (SpinState)', async () => {