@gjsify/adwaita-web 0.36.0 → 0.38.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 (151) hide show
  1. package/dist/adwaita-web.css +504 -34
  2. package/dist/adwaita-web.css.map +1 -1
  3. package/lib/types/accent.d.ts +28 -0
  4. package/lib/types/adw-accent.spec.d.ts +2 -0
  5. package/lib/types/adw-shortcut-label.spec.d.ts +2 -0
  6. package/lib/types/breakpoints.d.ts +10 -12
  7. package/lib/types/elements/adw-about-dialog.d.ts +19 -36
  8. package/lib/types/elements/adw-action-row.d.ts +9 -12
  9. package/lib/types/elements/adw-alert-dialog.d.ts +0 -12
  10. package/lib/types/elements/adw-avatar.d.ts +1 -0
  11. package/lib/types/elements/adw-bottom-sheet.d.ts +7 -12
  12. package/lib/types/elements/adw-button-content.d.ts +3 -4
  13. package/lib/types/elements/adw-button-row.d.ts +1 -1
  14. package/lib/types/elements/adw-carousel.d.ts +28 -49
  15. package/lib/types/elements/adw-checks.d.ts +2 -8
  16. package/lib/types/elements/adw-combo-row.d.ts +6 -6
  17. package/lib/types/elements/adw-data-grid.d.ts +5 -10
  18. package/lib/types/elements/adw-drop-down.d.ts +3 -11
  19. package/lib/types/elements/adw-entry-row.d.ts +20 -23
  20. package/lib/types/elements/adw-icon.d.ts +2 -4
  21. package/lib/types/elements/adw-inline-view-switcher.d.ts +18 -31
  22. package/lib/types/elements/adw-navigation-split-view.d.ts +7 -12
  23. package/lib/types/elements/adw-navigation-view.d.ts +2 -10
  24. package/lib/types/elements/adw-overlay-split-view.d.ts +28 -52
  25. package/lib/types/elements/adw-password-entry-row.d.ts +4 -5
  26. package/lib/types/elements/adw-popover.d.ts +2 -7
  27. package/lib/types/elements/adw-preferences-dialog.d.ts +21 -34
  28. package/lib/types/elements/adw-preferences-group.d.ts +12 -14
  29. package/lib/types/elements/adw-progress-bar.d.ts +3 -4
  30. package/lib/types/elements/adw-shortcut-label.d.ts +20 -0
  31. package/lib/types/elements/adw-sidebar.d.ts +5 -10
  32. package/lib/types/elements/adw-spinner.d.ts +3 -5
  33. package/lib/types/elements/adw-split-button.d.ts +6 -16
  34. package/lib/types/elements/adw-switch-row.d.ts +3 -3
  35. package/lib/types/elements/adw-tab-view.d.ts +16 -26
  36. package/lib/types/elements/adw-toast-overlay.d.ts +8 -13
  37. package/lib/types/elements/adw-view-stack.d.ts +7 -17
  38. package/lib/types/elements/adw-view-switcher-bar.d.ts +4 -8
  39. package/lib/types/elements/adw-view-switcher.d.ts +18 -38
  40. package/lib/types/elements/adw-wrap-box.d.ts +7 -17
  41. package/lib/types/elements/view-switcher-dom.d.ts +10 -13
  42. package/lib/types/index.d.ts +3 -0
  43. package/lib/types/source-view/hex.d.ts +7 -13
  44. package/lib/types/style-classes.spec.d.ts +11 -0
  45. package/lib/types/styles.generated.d.ts +1 -1
  46. package/package.json +18 -7
  47. package/scss/_about_dialog.scss +3 -2
  48. package/scss/_action_row.scss +33 -0
  49. package/scss/_avatar.scss +12 -0
  50. package/scss/_button.scss +44 -22
  51. package/scss/_card.scss +11 -8
  52. package/scss/_checks.scss +2 -4
  53. package/scss/_dialog.scss +3 -2
  54. package/scss/_headerbar.scss +23 -0
  55. package/scss/_popover.scss +8 -4
  56. package/scss/_preferences.scss +7 -7
  57. package/scss/_progress_bar.scss +9 -6
  58. package/scss/_reset.scss +1 -1
  59. package/scss/_shortcut_label.scss +80 -0
  60. package/scss/_sidebar.scss +57 -0
  61. package/scss/_status_page.scss +20 -0
  62. package/scss/_style_classes.scss +407 -0
  63. package/scss/_switch.scss +4 -3
  64. package/scss/_theme.scss +20 -2
  65. package/scss/_variables.scss +34 -1
  66. package/scss/adwaita-skin.scss +12 -0
  67. package/src/accent.ts +74 -0
  68. package/src/adw-about-dialog.spec.ts +34 -49
  69. package/src/adw-accent.spec.ts +105 -0
  70. package/src/adw-action-rows.spec.ts +25 -33
  71. package/src/adw-avatar.spec.ts +51 -20
  72. package/src/adw-banner.spec.ts +5 -8
  73. package/src/adw-button-content.spec.ts +9 -12
  74. package/src/adw-button-row.spec.ts +2 -4
  75. package/src/adw-carousel.spec.ts +40 -53
  76. package/src/adw-checks.spec.ts +36 -47
  77. package/src/adw-drop-down.spec.ts +25 -32
  78. package/src/adw-header-bar.spec.ts +5 -7
  79. package/src/adw-icon.spec.ts +23 -32
  80. package/src/adw-progress-bar.spec.ts +10 -12
  81. package/src/adw-row-state.spec.ts +9 -10
  82. package/src/adw-shortcut-label.spec.ts +112 -0
  83. package/src/adw-sidebar.spec.ts +17 -24
  84. package/src/adw-switch.spec.ts +11 -17
  85. package/src/adw-tab-view.spec.ts +7 -10
  86. package/src/adw-toast-overlay.spec.ts +7 -11
  87. package/src/adw-view-switcher-bar.spec.ts +10 -14
  88. package/src/adw-wrap-box.spec.ts +25 -36
  89. package/src/bottom-sheet.spec.ts +30 -36
  90. package/src/breakpoints.ts +15 -18
  91. package/src/chrome.spec.ts +13 -21
  92. package/src/elements/adw-about-dialog.ts +71 -114
  93. package/src/elements/adw-action-row.ts +22 -37
  94. package/src/elements/adw-alert-dialog.ts +25 -50
  95. package/src/elements/adw-avatar.ts +40 -15
  96. package/src/elements/adw-banner.ts +14 -18
  97. package/src/elements/adw-bottom-sheet.ts +31 -52
  98. package/src/elements/adw-button-content.ts +30 -37
  99. package/src/elements/adw-button-row.ts +14 -24
  100. package/src/elements/adw-carousel.ts +87 -148
  101. package/src/elements/adw-checks.ts +43 -56
  102. package/src/elements/adw-clamp.ts +8 -9
  103. package/src/elements/adw-combo-row.ts +17 -25
  104. package/src/elements/adw-data-grid.ts +26 -38
  105. package/src/elements/adw-dialog.ts +25 -56
  106. package/src/elements/adw-drop-down.ts +31 -51
  107. package/src/elements/adw-entry-row.ts +39 -46
  108. package/src/elements/adw-expander-row.ts +30 -53
  109. package/src/elements/adw-icon.ts +30 -53
  110. package/src/elements/adw-inline-view-switcher.ts +38 -67
  111. package/src/elements/adw-menu-button.ts +28 -58
  112. package/src/elements/adw-navigation-split-view.ts +35 -47
  113. package/src/elements/adw-navigation-view.ts +12 -28
  114. package/src/elements/adw-overlay-split-view.ts +100 -175
  115. package/src/elements/adw-password-entry-row.ts +18 -28
  116. package/src/elements/adw-popover.ts +28 -44
  117. package/src/elements/adw-preferences-dialog.ts +41 -66
  118. package/src/elements/adw-preferences-group.ts +33 -46
  119. package/src/elements/adw-progress-bar.ts +27 -47
  120. package/src/elements/adw-shortcut-label.ts +138 -0
  121. package/src/elements/adw-sidebar.ts +31 -39
  122. package/src/elements/adw-spin-row.ts +7 -14
  123. package/src/elements/adw-spinner.ts +31 -43
  124. package/src/elements/adw-split-button.ts +45 -75
  125. package/src/elements/adw-switch-row.ts +29 -46
  126. package/src/elements/adw-switch.ts +28 -47
  127. package/src/elements/adw-tab-view.ts +62 -103
  128. package/src/elements/adw-toast-overlay.ts +28 -35
  129. package/src/elements/adw-toggle-group.ts +9 -16
  130. package/src/elements/adw-view-stack.ts +32 -51
  131. package/src/elements/adw-view-switcher-bar.ts +26 -50
  132. package/src/elements/adw-view-switcher.ts +60 -110
  133. package/src/elements/adw-window-title.ts +4 -4
  134. package/src/elements/adw-wrap-box.ts +33 -59
  135. package/src/elements/view-switcher-dom.ts +15 -22
  136. package/src/entry-rows.spec.ts +18 -24
  137. package/src/index.ts +24 -15
  138. package/src/navigation-view.spec.ts +7 -10
  139. package/src/preferences-search.ts +11 -15
  140. package/src/preferences.spec.ts +13 -15
  141. package/src/source-view/adw-source-view.ts +6 -19
  142. package/src/source-view/hex.ts +7 -13
  143. package/src/split-button.spec.ts +32 -44
  144. package/src/split-views.spec.ts +83 -149
  145. package/src/style-classes.spec.ts +181 -0
  146. package/src/style-isolation.spec.ts +3 -6
  147. package/src/styles.generated.ts +1 -1
  148. package/src/tab-view.spec.ts +26 -35
  149. package/src/test.browser.mts +6 -0
  150. package/src/view-stack.spec.ts +23 -34
  151. package/src/view-switcher.spec.ts +56 -84
@@ -1,13 +1,10 @@
1
1
  // DOM-level conformance tests for <adw-about-dialog>, driven by the vectors in
2
2
  // `@gjsify/adwaita-core/conformance` — the same table the core suite asserts.
3
3
  //
4
- // This element carried its own copy of `parse_person`, its own page-visibility
5
- // rules and its own set of invented fallbacks, and diverged from libadwaita on
6
- // thirteen counts. Two of them were not subtle: Support Questions and Report an
7
- // Issue were rendered on the DETAILS page, one navigation step deeper than GTK
8
- // puts them (adw-about-dialog.ui:146-186), and `website` was treated as Details
9
- // content, which is exactly the one input libadwaita leaves OUT of that
10
- // predicate (:1108). Nothing compared the two, so nothing failed.
4
+ // A local copy of `parse_person`, of the page-visibility rules or of the fallbacks
5
+ // diverges from libadwaita in thirteen ways, two of them structural: Support Questions
6
+ // and Report an Issue belong on the MAIN page, not one navigation step deeper on Details;
7
+ // and `website` is the one input the Details-visibility predicate leaves OUT.
11
8
  import { describe, expect, it } from '@gjsify/unit';
12
9
 
13
10
  import { ADW_ABOUT_DIALOG_LABELS, stripMnemonic } from '@gjsify/adwaita-core';
@@ -33,9 +30,9 @@ interface Mounted {
33
30
  }
34
31
 
35
32
  /**
36
- * Mount a dialog and set every attribute imperatively — several vectors hinge on
37
- * exact whitespace, which a value parsed out of markup is not a reliable
38
- * carrier for. `window.open` is stubbed because the link rows really do call it.
33
+ * Mount a dialog and set every attribute imperatively: several vectors hinge on exact
34
+ * whitespace, which markup is not a reliable carrier for. `window.open` is stubbed
35
+ * because the link rows really do call it.
39
36
  */
40
37
  function mount(attributes: Record<string, string> = {}): Mounted {
41
38
  const host = document.createElement('div');
@@ -85,10 +82,8 @@ function rowTitles(scope: Element | null): string[] {
85
82
  }
86
83
 
87
84
  /**
88
- * Activate a row and report the URI it asked for.
89
- *
90
- * The URI never reaches the DOM — it is captured in the row's activation
91
- * handler — so the only way to observe it is to drive the row and listen.
85
+ * Activate a row and report the URI it asked for. The URI never reaches the DOM — it is
86
+ * captured in the row's activation handler — so driving the row is the only way to see it.
92
87
  */
93
88
  function activateRow(dialog: HTMLElement, row: Element): string | null {
94
89
  let uri: string | null = null;
@@ -121,8 +116,8 @@ export const AdwAboutDialogTest = async () => {
121
116
  }
122
117
 
123
118
  await it('makes a row with an empty link a LINK row, not a plain one', () => {
124
- // `if (link)` is a pointer test in the C, so `"Ada <>"` still links,
125
- // to the URI `mailto:` — collapsing "" to null loses the row type.
119
+ // `if (link)` is a POINTER test in the C, so `"Ada <>"` still links, to the
120
+ // URI `mailto:` — collapsing "" to null loses the row type.
126
121
  const mounted = mount();
127
122
  mounted.dialog.developers = ['Ada <>'];
128
123
  const row = page(mounted.dialog, 'credits')!.querySelector('adw-action-row')!;
@@ -197,9 +192,9 @@ export const AdwAboutDialogTest = async () => {
197
192
  }
198
193
 
199
194
  await it('keeps a website-only dialog on the main page with no Details row', () => {
200
- // `show_details` is `has_comments || has_custom_links` (:1108) —
201
- // website is deliberately not in it, so a dialog whose only extra is
202
- // a website links straight from the main page.
195
+ // `show_details` is `has_comments || has_custom_links`; website is
196
+ // deliberately not in it, so a dialog whose only extra is a website links
197
+ // straight from the main page.
203
198
  const mounted = mount({ 'application-name': 'Builder', website: 'https://example.org' });
204
199
  expect(page(mounted.dialog, 'details')).toBe(null);
205
200
  expect(hasRow(page(mounted.dialog, 'main'), label('websiteRow'))).toBe(true);
@@ -208,8 +203,8 @@ export const AdwAboutDialogTest = async () => {
208
203
  });
209
204
 
210
205
  await describe('adw-about-dialog main page (update_support vectors)', async () => {
211
- // debug-info has no counterpart on this element (there is no
212
- // Troubleshooting page), so those rows are asserted by the core suite.
206
+ // debug-info has no counterpart here (no Troubleshooting page), so the core
207
+ // suite asserts those rows.
213
208
  for (const vector of ABOUT_DIALOG_SUPPORT_VECTORS.filter((row) => !row.debugInfo)) {
214
209
  const { supportUrl, issueUrl, visible, rule } = vector;
215
210
  await it(`support=${supportUrl} issue=${issueUrl} — ${rule}`, () => {
@@ -235,8 +230,7 @@ export const AdwAboutDialogTest = async () => {
235
230
  }
236
231
 
237
232
  await it('puts the support and issue rows on the MAIN page, not on Details', () => {
238
- // adw-about-dialog.ui:146-186 — `support_group` is a child of the
239
- // main page, and the class docs say so outright (:87-90).
233
+ // In `adw-about-dialog.ui`, `support_group` is a child of the MAIN page.
240
234
  const mounted = mount({
241
235
  'application-name': 'Builder',
242
236
  'support-url': 'https://example.org/help',
@@ -304,9 +298,8 @@ export const AdwAboutDialogTest = async () => {
304
298
  }
305
299
 
306
300
  await it('titles the dialog "About", not "About <app name>"', () => {
307
- // The main page's title is bound to AdwDialog:title, whose template
308
- // default is the bare word (.ui:6, :19); the application name lives
309
- // in the header revealer instead (.ui:29-31).
301
+ // The main page's title is bound to AdwDialog:title, whose template default
302
+ // is the bare word; the application name lives in the header revealer.
310
303
  const mounted = mount({ 'application-name': 'Builder' });
311
304
  expect(page(mounted.dialog, 'main')!.getAttribute('title')).toBe('About');
312
305
  mounted.dispose();
@@ -323,12 +316,11 @@ export const AdwAboutDialogTest = async () => {
323
316
  });
324
317
 
325
318
  await it('does NOT open when a handler cancels it', () => {
326
- // `activate_link_cb` returns the accumulated handler result
327
- // (:429-437, accumulator `g_signal_accumulator_true_handled`
328
- // :2098): a handler saying "handled" suppresses the default
329
- // navigation. `preventDefault()` is the web spelling of that, and it
330
- // was unavailable — the event was not cancelable and `window.open`
331
- // ran regardless.
319
+ // `activate_link_cb` returns the accumulated handler result (accumulator
320
+ // `g_signal_accumulator_true_handled`): a handler saying "handled"
321
+ // suppresses the default navigation, and `preventDefault()` is the web
322
+ // spelling of that — so the event must be cancelable, or `window.open` runs
323
+ // regardless.
332
324
  const mounted = mount({ 'application-name': 'Builder', website: 'https://example.org' });
333
325
  mounted.dialog.addEventListener('activate-link', (event) => event.preventDefault());
334
326
  const row = page(mounted.dialog, 'main')!.querySelector(`adw-action-row[title="${label('websiteRow')}"]`)!;
@@ -348,19 +340,14 @@ export const AdwAboutDialogTest = async () => {
348
340
  });
349
341
  });
350
342
 
351
- // AN OPENED DIALOG HAS TO OCCUPY SPACE.
343
+ // An opened dialog has to OCCUPY SPACE, and no other test here can see that: they
344
+ // all read the built DOM, and all passed while the sheet measured 360×0.
352
345
  //
353
- // Every other test in this file reads the built DOM, and all of them passed
354
- // while the dialog rendered INVISIBLE — its sheet measured 360×0. The suite
355
- // could not see that, because none of it looks at geometry.
356
- //
357
- // The sheet is `position: fixed` with a width and no height, which is what
358
- // `adw-about-dialog.ui:7` declares: `content-width` 360 and no
359
- // content-height, libadwaita taking the height from the content. The content
360
- // could not supply one — the navigation pages were absolutely positioned, so
361
- // they contributed nothing to the page stack, which contributed nothing to
362
- // the view, which left the sheet at zero; and the pages, being `inset: 0` of
363
- // a zero-height parent, were zero in turn.
346
+ // `adw-about-dialog.ui` declares `content-width` 360 and no content-height, so
347
+ // libadwaita takes the height from the content — which absolutely-positioned
348
+ // navigation pages cannot supply, since they contribute nothing to the page stack,
349
+ // which contributes nothing to the view, and the pages are then `inset: 0` of a
350
+ // zero-height parent.
364
351
  await describe('adw-about-dialog — an opened dialog is actually on screen', async () => {
365
352
  await it('gives its sheet a height derived from its content', () => {
366
353
  const mounted = mount({ 'application-name': 'Adwaita Storybook', version: '0.11.0' });
@@ -368,11 +355,9 @@ export const AdwAboutDialogTest = async () => {
368
355
 
369
356
  const sheet = mounted.dialog.querySelector('.adw-about-dialog-sheet') as HTMLElement;
370
357
  const rect = sheet.getBoundingClientRect();
371
- // HEIGHT only. The width was never the defect, and pinning it here
372
- // needs `calc(100vw - 48px)` restated in JS — where `100vw` and
373
- // `innerWidth` disagree by the scrollbar (360 against a measured
374
- // 342), so the assertion would report the harness rather than the
375
- // widget. A test should assert the rule the bug broke.
358
+ // HEIGHT only: pinning the width needs `calc(100vw - 48px)` restated in JS,
359
+ // where `100vw` and `innerWidth` disagree by the scrollbar, so the assertion
360
+ // would report the harness rather than the widget.
376
361
  expect(rect.height > 0).toBe(true);
377
362
  // And not merely the header: the sheet has to be taller than the one
378
363
  // child that had a size of its own all along, which is what made the
@@ -0,0 +1,105 @@
1
+ // Accent application against `ACCENT_COLOR_VECTORS` — the same table the core
2
+ // suite drives, here through the REAL computed style of a real element.
3
+ //
4
+ // This is what makes the table renderer-driven rather than a derivation asserted
5
+ // against itself: the browser resolves the custom properties and the assertions
6
+ // read them back with `getComputedStyle`, so a value that never reaches the
7
+ // cascade fails a test naming the accent.
8
+ import { describe, expect, it } from '@gjsify/unit';
9
+ import { ACCENT_COLOR_VECTORS } from '@gjsify/adwaita-core/conformance';
10
+
11
+ import '@gjsify/adwaita-web';
12
+ import {
13
+ ACCENT_BG_PROPERTY,
14
+ ACCENT_PROPERTY,
15
+ applyAdwaitaAccent,
16
+ clearAdwaitaAccent,
17
+ isAdwaitaDark,
18
+ } from './accent.js';
19
+
20
+ /** A custom property's resolved value, whitespace trimmed. */
21
+ const property = (element: HTMLElement, name: string): string =>
22
+ getComputedStyle(element).getPropertyValue(name).trim();
23
+
24
+ export const AdwAccentTest = async () => {
25
+ await describe('applyAdwaitaAccent (conformance vectors)', async () => {
26
+ for (const vector of ACCENT_COLOR_VECTORS) {
27
+ await it(`${vector.name} — the fill and the light standalone colour reach the cascade`, async () => {
28
+ const host = document.createElement('div');
29
+ host.className = 'theme-light';
30
+ document.body.append(host);
31
+
32
+ applyAdwaitaAccent(vector.name, { target: host });
33
+
34
+ expect(property(host, ACCENT_BG_PROPERTY)).toBe(vector.background);
35
+ expect(property(host, ACCENT_PROPERTY)).toBe(vector.standaloneLight);
36
+
37
+ host.remove();
38
+ });
39
+
40
+ await it(`${vector.name} — the DARK standalone colour differs and is the table's`, async () => {
41
+ // Asserted as a pair: a renderer that ignored `dark` would pass
42
+ // every light row above and still be wrong in half its uses.
43
+ const host = document.createElement('div');
44
+ host.className = 'theme-dark';
45
+ document.body.append(host);
46
+
47
+ applyAdwaitaAccent(vector.name, { target: host });
48
+
49
+ expect(property(host, ACCENT_PROPERTY)).toBe(vector.standaloneDark);
50
+ expect(vector.standaloneDark).not.toBe(vector.standaloneLight);
51
+
52
+ host.remove();
53
+ });
54
+ }
55
+ });
56
+
57
+ await describe('accent application', async () => {
58
+ await it('reads the resolved scheme from the element rather than assuming light', async () => {
59
+ const dark = document.createElement('div');
60
+ dark.className = 'theme-dark';
61
+ const light = document.createElement('div');
62
+ light.className = 'theme-light';
63
+ document.body.append(dark, light);
64
+
65
+ expect(isAdwaitaDark(dark)).toBe(true);
66
+ expect(isAdwaitaDark(light)).toBe(false);
67
+
68
+ // A nested element inherits the ancestor's scheme — `closest`, not a
69
+ // class check on the element itself, which is how the stylesheet's
70
+ // `.theme-dark` scope actually behaves.
71
+ const nested = document.createElement('span');
72
+ dark.append(nested);
73
+ expect(isAdwaitaDark(nested)).toBe(true);
74
+
75
+ dark.remove();
76
+ light.remove();
77
+ });
78
+
79
+ await it('an explicit dark option overrides what the element resolves to', async () => {
80
+ const host = document.createElement('div');
81
+ host.className = 'theme-light';
82
+ document.body.append(host);
83
+
84
+ applyAdwaitaAccent('blue', { target: host, dark: true });
85
+ expect(property(host, ACCENT_PROPERTY)).toBe(ACCENT_COLOR_VECTORS[0].standaloneDark);
86
+
87
+ host.remove();
88
+ });
89
+
90
+ await it('clearing restores the stylesheet value instead of leaving the override', async () => {
91
+ const host = document.createElement('div');
92
+ host.className = 'theme-light';
93
+ document.body.append(host);
94
+
95
+ applyAdwaitaAccent('red', { target: host });
96
+ expect(property(host, ACCENT_BG_PROPERTY)).toBe(ACCENT_COLOR_VECTORS[5].background);
97
+
98
+ clearAdwaitaAccent(host);
99
+ // Back to whatever the cascade says — which is NOT red.
100
+ expect(property(host, ACCENT_BG_PROPERTY)).not.toBe(ACCENT_COLOR_VECTORS[5].background);
101
+
102
+ host.remove();
103
+ });
104
+ });
105
+ };
@@ -3,20 +3,16 @@
3
3
  // vectors the NativeScript renderer asserts against
4
4
  // (`@gjsify/adwaita-core/conformance`).
5
5
  //
6
- // What this suite exists to catch, in the words of what it found:
7
- // - NEITHER renderer hid an empty TITLE. `string_is_not_empty` is bound to the
8
- // title label as much as to the subtitle (adw-action-row.ui:49-53 vs :71-75),
9
- // and the browser port hand-rolled the subtitle-only half of that block six
10
- // times over.
11
- // - a programmatic `switchRow.active = true` emitted nothing here, because the
12
- // DOM does not fire `change` for a scripted `.checked =` — while the
13
- // NativeScript port, whose `checkedChange` does fire, emitted. One C source,
14
- // two opposite answers, and nothing comparing them.
15
- // - clicking anywhere but the handle of a switch row did nothing, though
16
- // `adw_switch_row_init` makes the whole row the switch's activator.
17
- // - `<adw-button-row activatable="false">` was honoured, an opt-out libadwaita
18
- // does not have — and `<adw-action-row activatable="false">` meant the
19
- // OPPOSITE, because that element reads the attribute by presence.
6
+ // The rules it pins, each of which one renderer had wrong:
7
+ // - `string_is_not_empty` in `adw-action-row.ui` is bound to the TITLE label as much
8
+ // as to the subtitle, so an empty title hides too;
9
+ // - the DOM fires no `change` for a scripted `.checked =`, so a programmatic
10
+ // `switchRow.active = true` must be made to notify anyway — NativeScript's
11
+ // `checkedChange` does fire, and one C source must not give two answers;
12
+ // - `adw_switch_row_init` makes the WHOLE row the switch's activator;
13
+ // - libadwaita has no activatable opt-out for a button row, and
14
+ // `<adw-action-row activatable="false">` reads by PRESENCE, so it means the
15
+ // opposite of what it says.
20
16
  import { describe, expect, it } from '@gjsify/unit';
21
17
 
22
18
  import {
@@ -35,9 +31,8 @@ import type { AdwSwitchRow } from './elements/adw-switch-row.js';
35
31
  import type { AdwWindowTitle } from './elements/adw-window-title.js';
36
32
 
37
33
  /**
38
- * Mount an element imperatively — never through parsed HTML, because several
39
- * label vectors hinge on exact whitespace (`' '`, `'\t'`) that an attribute
40
- * value in markup is not a reliable carrier for.
34
+ * Mount an element imperatively, never through parsed HTML: several label vectors hinge
35
+ * on exact whitespace (`' '`, `'\t'`) that markup is not a reliable carrier for.
41
36
  */
42
37
  function mount<T extends HTMLElement>(tag: string): { el: T; host: HTMLElement } {
43
38
  const host = document.createElement('div');
@@ -144,13 +139,11 @@ export const AdwActionRowsTest = async () => {
144
139
  await it('reads `activatable` by PRESENCE, like every other boolean attribute here', () => {
145
140
  const { el: row, host } = mount<AdwActionRow>('adw-action-row');
146
141
  row.setAttribute('activatable', 'false');
147
- // The HTML boolean-attribute convention (`<input disabled="false">`
148
- // is disabled), which is how every other boolean attribute in this
149
- // package is read. NOT a libadwaita rule — libadwaita has a typed
150
- // gboolean and no attribute parsing at all — so it is pinned here
151
- // rather than in the cross-renderer vectors. Pinned so the next
152
- // reader does not "fix" it into the string comparison
153
- // <adw-button-row> used to carry, which gave one markup two
142
+ // The HTML boolean-attribute convention (`<input disabled="false">` IS
143
+ // disabled), how every other boolean attribute in this package is read. NOT a
144
+ // libadwaita rule — libadwaita has a typed gboolean and no attribute parsing —
145
+ // so it is pinned here rather than in the cross-renderer vectors, and pinned so
146
+ // nobody "fixes" it into a string comparison that would give one markup two
154
147
  // opposite meanings inside one package.
155
148
  expect(row.activatable).toBe(true);
156
149
  host.remove();
@@ -183,9 +176,9 @@ export const AdwActionRowsTest = async () => {
183
176
 
184
177
  widget.click();
185
178
 
186
- // One click, and no row activation: GtkListBox does not emit
187
- // `row-activated` for a click a child widget claimed. Re-clicking the
188
- // widget here would also bounce back into the row's own listener.
179
+ // One click, no row activation: GtkListBox does not emit `row-activated` for a
180
+ // click a child widget claimed, and re-clicking the widget here would bounce
181
+ // back into the row's own listener.
189
182
  expect(widgetClicks).toBe(1);
190
183
  expect(activations.length).toBe(0);
191
184
  host.remove();
@@ -257,16 +250,15 @@ export const AdwActionRowsTest = async () => {
257
250
  setAttr(row, 'start-icon-name', vector.startIconName);
258
251
  setAttr(row, 'end-icon-name', vector.endIconName);
259
252
 
260
- // The two `image.icon.{start,end}` nodes are <adw-icon> now, not
261
- // hand-rolled decorative spans.
253
+ // The two `image.icon.{start,end}` nodes are <adw-icon>, not hand-rolled
254
+ // decorative spans.
262
255
  const icons = Array.from(row.querySelectorAll('adw-icon')) as HTMLElement[];
263
256
  expect(icons.length).toBe(2);
264
257
  expect(!icons[0]!.hidden).toBe(vector.startIconVisible);
265
258
  expect(!icons[1]!.hidden).toBe(vector.endIconVisible);
266
- // The mask class is the NORMALIZED name: the generated classes
267
- // never carry `-symbolic`, and this row used to interpolate the
268
- // raw one — so `start-icon-name="list-add-symbolic"` asked for a
269
- // class that has never existed and drew an empty box.
259
+ // The mask class is the NORMALIZED name — the generated classes never
260
+ // carry `-symbolic`, so interpolating the raw one asks for a class that has
261
+ // never existed and draws an empty box.
270
262
  if (vector.startIconVisible) {
271
263
  expect(icons[0]!.classList.contains(`adw-icon--${normalizeIconName(vector.startIconName)}`)).toBe(
272
264
  true,
@@ -1,12 +1,10 @@
1
1
  // DOM-level conformance tests for <adw-avatar>, driven by the SAME vectors the
2
2
  // NativeScript renderer asserts against (`@gjsify/adwaita-core/conformance`).
3
3
  //
4
- // The two renderers used to carry independent copies of the initials + colour
5
- // derivation, and the copies had drifted apart AND away from libadwaita:
6
- // this element rendered "AD" for `text="Ada"` where GTK and NS render "A", and
7
- // both hashed UTF-16 code units where GLib hashes UTF-8 bytes, so every accented
8
- // name was painted the wrong colour. Nothing failed, because nothing compared
9
- // them. This suite is that comparison.
4
+ // Two rules independent copies of the initials + colour derivation drifted from: GTK
5
+ // takes ONE initial from a single-word `text`, and the colour hash runs over UTF-8 BYTES
6
+ // as GLib's does — hashing UTF-16 code units paints every accented name the wrong
7
+ // colour.
10
8
  import { describe, expect, it } from '@gjsify/unit';
11
9
 
12
10
  import { AVATAR_MODE_VECTORS } from '@gjsify/adwaita-core/conformance';
@@ -140,37 +138,70 @@ export const AdwAvatarTest = async () => {
140
138
  });
141
139
 
142
140
  await describe('<adw-avatar> mode (update_visibility, adw-avatar.c:117)', async () => {
143
- // The `hasCustomImage` rows are not reachable through this element: it
144
- // passes `hasCustomImage: false` unconditionally, because it has no
145
- // custom-image support at all — `Adw.Avatar:custom-image` is unported.
146
- // Named below rather than silently filtered.
141
+ // A 1x1 transparent GIF: a real, decodable image, so `custom-image` is
142
+ // exercised through the same path a photo would take — and no network.
143
+ const PIXEL = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
144
+
147
145
  for (const { hasCustomImage, showInitials, text, mode, rule } of AVATAR_MODE_VECTORS) {
148
- if (hasCustomImage) continue;
149
- await it(`initials=${showInitials} "${text}" → ${mode} — ${rule}`, () => {
146
+ await it(`image=${hasCustomImage} initials=${showInitials} "${text}" → ${mode} — ${rule}`, () => {
150
147
  const host = document.createElement('div');
151
148
  document.body.appendChild(host);
152
149
  const avatar = document.createElement('adw-avatar');
153
150
  if (showInitials) avatar.setAttribute('show-initials', '');
154
151
  if (text) avatar.setAttribute('text', text);
152
+ if (hasCustomImage) avatar.setAttribute('custom-image', PIXEL);
155
153
  host.appendChild(avatar);
156
154
 
157
- // Exactly ONE of the two reachable modes is drawn — the point of
158
- // the table is the precedence, and "both visible at once" is the
155
+ // Exactly ONE of the three modes is drawn — the point of the
156
+ // table is the precedence, and "two visible at once" is the
159
157
  // failure it guards against.
160
158
  const initials = (avatar.querySelector('.adw-avatar-text') as HTMLElement).hidden === false;
161
159
  const icon = (avatar.querySelector('.adw-avatar-icon') as HTMLElement).hidden === false;
160
+ const image = (avatar.querySelector('.adw-avatar-custom-image') as HTMLElement).hidden === false;
162
161
  expect(initials).toBe(mode === 'initials');
163
162
  expect(icon).toBe(mode === 'icon');
163
+ expect(image).toBe(mode === 'image');
164
164
  host.remove();
165
165
  });
166
166
  }
167
167
 
168
- await it('leaves out only the custom-image rows, which this element cannot reach', () => {
169
- const unreachable = AVATAR_MODE_VECTORS.filter((vector) => vector.hasCustomImage);
170
- expect(unreachable.length).toBeGreaterThan(0);
171
- // Every one of them is an `image` outcome, i.e. the whole mode is
172
- // missing rather than a corner of it.
173
- for (const vector of unreachable) expect(vector.mode).toBe('image');
168
+ await it('drops the custom image again when the attribute goes away', () => {
169
+ // The mode is re-derived on every attribute change, so removing the
170
+ // image has to fall back to whichever mode the remaining state picks
171
+ // — this is where a port that only ever ADDS the image node fails.
172
+ const host = document.createElement('div');
173
+ document.body.appendChild(host);
174
+ const avatar = document.createElement('adw-avatar');
175
+ avatar.setAttribute('show-initials', '');
176
+ avatar.setAttribute('text', 'Ada Lovelace');
177
+ avatar.setAttribute('custom-image', PIXEL);
178
+ host.appendChild(avatar);
179
+
180
+ expect((avatar.querySelector('.adw-avatar-custom-image') as HTMLElement).hidden).toBe(false);
181
+
182
+ avatar.removeAttribute('custom-image');
183
+ expect((avatar.querySelector('.adw-avatar-custom-image') as HTMLElement).hidden).toBe(true);
184
+ expect((avatar.querySelector('.adw-avatar-text') as HTMLElement).hidden).toBe(false);
185
+
186
+ host.remove();
187
+ });
188
+
189
+ await it('fills the circle with the image rather than letterboxing it', () => {
190
+ // `object-fit` is the whole reason the image looks like an avatar and
191
+ // not like a photo in a round frame — a class-name assertion cannot
192
+ // see it, and it is one word away from being wrong.
193
+ const host = document.createElement('div');
194
+ document.body.appendChild(host);
195
+ const avatar = document.createElement('adw-avatar');
196
+ avatar.setAttribute('custom-image', PIXEL);
197
+ avatar.setAttribute('size', '64');
198
+ host.appendChild(avatar);
199
+
200
+ const image = avatar.querySelector('.adw-avatar-custom-image') as HTMLElement;
201
+ expect(getComputedStyle(image).objectFit).toBe('cover');
202
+ expect(Math.round(image.getBoundingClientRect().width)).toBe(64);
203
+
204
+ host.remove();
174
205
  });
175
206
  });
176
207
  };
@@ -1,13 +1,10 @@
1
1
  // DOM-level conformance tests for <adw-banner>, driven by the SAME vectors the
2
2
  // NativeScript renderer asserts against (`@gjsify/adwaita-core/conformance`).
3
3
  //
4
- // The two renderers carried independent banner logic and had drifted apart AND
5
- // away from libadwaita: `revealed` was opt-in here (right) and initialised TRUE
6
- // on NativeScript with no `visibility` written at all (wrong), `use-markup` was
7
- // opt-in in both with a comment here asserting that WAS the C default (it is
8
- // TRUE, adw-banner.c:422-425), `button-style` was in neither, and the mnemonic
9
- // marker the template pins onto the button (adw-banner.ui:33) was painted as a
10
- // literal underscore. Nothing failed, because nothing compared them.
4
+ // The defaults independent banner logic drifted from, all of them in
5
+ // `ADW_BANNER_DEFAULTS`: `revealed` is FALSE, `use-markup` is TRUE (opting it in inverts
6
+ // the C), `button-style` exists, and the mnemonic marker the template pins onto the
7
+ // button is a marker, not a literal underscore to paint.
11
8
  import { describe, expect, it } from '@gjsify/unit';
12
9
 
13
10
  import { ADW_BANNER_DEFAULTS } from '@gjsify/adwaita-core';
@@ -64,7 +61,7 @@ export const AdwBannerTest = async () => {
64
61
  });
65
62
 
66
63
  await describe('adw-banner use-markup — a DELIBERATE departure from the C default', async () => {
67
- // The C default is unambiguously TRUE (adw-banner.c:422-425) and core
64
+ // The C default is unambiguously TRUE and core
68
65
  // carries it as the spec value. This renderer does NOT adopt it: Pango
69
66
  // markup is not HTML, and defaulting a `title` attribute to `innerHTML`
70
67
  // would make an injection sink out of a widget that is not one in GTK.
@@ -2,15 +2,12 @@
2
2
  // vectors the NativeScript renderer asserts against
3
3
  // (`@gjsify/adwaita-core/conformance`).
4
4
  //
5
- // THE STYLE CLASS IS WHY THIS SUITE EXISTS. `AdwButtonContent` puts
6
- // `image-text-button` on the button hosting it (adw-button-content.c:115) and
7
- // removes it on unroot (:126); the stylesheet gives that class
8
- // `padding-left/right: 9px` where a plain text button has 17px
9
- // (_buttons.scss:77-80 against :72-75). `grep -rn "image-text-button"` over both
10
- // renderer trees returned NOTHING, so every icon+label button in this package
11
- // was drawn with the wrong horizontal padding for its whole life. `use-underline`
12
- // was absent, and an empty `icon-name` hid the image where the C sets
13
- // `image-missing` (:355-356).
5
+ // The STYLE CLASS is why this suite exists: `AdwButtonContent` puts
6
+ // `image-text-button` on the button hosting it and removes it on unroot, and
7
+ // `_buttons.scss` gives that class 9px horizontal padding where a plain text button has
8
+ // 17px — so a renderer that never sets the class draws every icon+label button with the
9
+ // wrong padding. Also pinned: `use-underline`, and that an empty `icon-name` shows
10
+ // `image-missing` rather than hiding the image.
14
11
  import { describe, expect, it } from '@gjsify/unit';
15
12
 
16
13
  import { BUTTON_CONTENT_DEFAULT_VECTORS } from '@gjsify/adwaita-core/conformance';
@@ -93,8 +90,8 @@ export const AdwButtonContentTest = async () => {
93
90
  host.appendChild(split);
94
91
 
95
92
  // The class goes on the `splitbutton` node, which is what
96
- // `splitbutton.image-text-button > button` (:499-507) selects —
97
- // a different declaration block from the plain-button one.
93
+ // `splitbutton.image-text-button > button` selects — a different declaration
94
+ // block from the plain-button one.
98
95
  expect(split.classList.contains(BUTTON_CONTENT_STYLE_CLASS)).toBe(true);
99
96
  expect(action.classList.contains(BUTTON_CONTENT_STYLE_CLASS)).toBe(false);
100
97
  // …and the padding lands on the INNER button, which is what that
@@ -134,7 +131,7 @@ export const AdwButtonContentTest = async () => {
134
131
  maskName === '' ? 0 : 1,
135
132
  );
136
133
  // The image node is never hidden — `gtk_widget_set_visible` is
137
- // called on the LABEL only (:300, :398).
134
+ // called on the LABEL only.
138
135
  expect(icon.hidden).toBe(false);
139
136
  host.remove();
140
137
  });
@@ -36,17 +36,15 @@ export const AdwButtonRowTest = async () => {
36
36
 
37
37
  expect(errors.length).toBe(0);
38
38
  expect(row.textContent).toContain('Add account');
39
- // The start icon span carries the adw-icon class derived from the name.
40
39
  expect(row.querySelector('.adw-icon--list-add') !== null).toBe(true);
41
- // Activatable by default.
42
40
  expect(row.classList.contains('activatable')).toBe(true);
43
41
  host.remove();
44
42
  });
45
43
 
46
44
  // DELETED: `honours activatable="false"`. It was green, and what it
47
45
  // pinned was an INVENTION — `Adw.ButtonRow` is always activatable
48
- // (`<property name="activatable">True</property>`, adw-button-row.ui:5;
49
- // "AdwButtonRow is always activatable.", adw-button-row.c:31), and the
46
+ // (`<property name="activatable">True</property>`, adw-button-row.ui;
47
+ // "AdwButtonRow is always activatable."), and the
50
48
  // class exposes no property, no setter and no getter for it. The opt-out
51
49
  // also gave one markup two opposite meanings inside one package, because
52
50
  // <adw-action-row> reads `activatable` by PRESENCE — so