@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,350 @@
1
+ // DOM-level conformance tests for <adw-about-dialog>, driven by the vectors in
2
+ // `@gjsify/adwaita-core/conformance` — the same table the core suite asserts.
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.
11
+ import { describe, expect, it } from '@gjsify/unit';
12
+
13
+ import { ADW_ABOUT_DIALOG_LABELS, stripMnemonic } from '@gjsify/adwaita-core';
14
+ import {
15
+ ABOUT_DIALOG_CREDITS_LEGAL_VECTORS,
16
+ ABOUT_DIALOG_DETAILS_VECTORS,
17
+ ABOUT_DIALOG_HEADER_VECTORS,
18
+ ABOUT_DIALOG_SUPPORT_VECTORS,
19
+ CREDITS_SECTIONS_VECTORS,
20
+ CREDIT_PERSON_VECTORS,
21
+ } from '@gjsify/adwaita-core/conformance';
22
+
23
+ /** The label a renderer without an accelerator layer paints for a row. */
24
+ const label = (key: keyof typeof ADW_ABOUT_DIALOG_LABELS) => stripMnemonic(ADW_ABOUT_DIALOG_LABELS[key]);
25
+
26
+ interface Mounted {
27
+ dialog: HTMLElement & { developers: string[]; designers: string[]; artists: string[]; documenters: string[] };
28
+ host: HTMLElement;
29
+ /** Every `window.open` the dialog performed, most recent last. */
30
+ opened: string[];
31
+ /** Restore the stubbed `window.open`. */
32
+ dispose: () => void;
33
+ }
34
+
35
+ /**
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.
39
+ */
40
+ function mount(attributes: Record<string, string> = {}): Mounted {
41
+ const host = document.createElement('div');
42
+ document.body.appendChild(host);
43
+ const dialog = document.createElement('adw-about-dialog') as Mounted['dialog'];
44
+ host.appendChild(dialog);
45
+ for (const [name, value] of Object.entries(attributes)) dialog.setAttribute(name, value);
46
+
47
+ const opened: string[] = [];
48
+ const original = window.open;
49
+ (window as unknown as { open: unknown }).open = (url?: string | URL) => {
50
+ opened.push(String(url ?? ''));
51
+ return null;
52
+ };
53
+
54
+ return {
55
+ dialog,
56
+ host,
57
+ opened,
58
+ dispose: () => {
59
+ (window as unknown as { open: unknown }).open = original;
60
+ host.remove();
61
+ },
62
+ };
63
+ }
64
+
65
+ /** The navigation page with `tag`, or `null` when the dialog did not build it. */
66
+ function page(dialog: HTMLElement, tag: string): HTMLElement | null {
67
+ return dialog.querySelector(`adw-navigation-page[tag="${tag}"]`);
68
+ }
69
+
70
+ /** The preferences group the template calls `id`, on whichever page it lives. */
71
+ function group(dialog: HTMLElement, id: string): HTMLElement | null {
72
+ return dialog.querySelector(`adw-preferences-group[data-group="${id}"]`);
73
+ }
74
+
75
+ /** Whether a row with `title` exists inside `scope`. */
76
+ function hasRow(scope: Element | null, title: string): boolean {
77
+ if (!scope) return false;
78
+ return scope.querySelector(`adw-action-row[title="${title}"]`) !== null;
79
+ }
80
+
81
+ /** The row titles inside `scope`, in document order. */
82
+ function rowTitles(scope: Element | null): string[] {
83
+ if (!scope) return [];
84
+ return [...scope.querySelectorAll('adw-action-row')].map((row) => row.getAttribute('title') ?? '');
85
+ }
86
+
87
+ /**
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.
92
+ */
93
+ function activateRow(dialog: HTMLElement, row: Element): string | null {
94
+ let uri: string | null = null;
95
+ const listener = (event: Event) => {
96
+ uri = (event as CustomEvent<{ uri: string }>).detail.uri;
97
+ };
98
+ dialog.addEventListener('activate-link', listener);
99
+ row.dispatchEvent(new CustomEvent('activated', { bubbles: true }));
100
+ dialog.removeEventListener('activate-link', listener);
101
+ return uri;
102
+ }
103
+
104
+ export const AdwAboutDialogTest = async () => {
105
+ await describe('adw-about-dialog credit parsing (parse_person vectors)', async () => {
106
+ for (const { input, name, uri, rule } of CREDIT_PERSON_VECTORS) {
107
+ await it(`${JSON.stringify(input)} — ${rule}`, () => {
108
+ const mounted = mount({ 'application-name': 'Builder' });
109
+ mounted.dialog.developers = [input];
110
+
111
+ const credits = page(mounted.dialog, 'credits');
112
+ const rows = credits ? [...credits.querySelectorAll('adw-action-row')] : [];
113
+ expect(rows).toHaveLength(1);
114
+
115
+ const row = rows[0]!;
116
+ expect(row.getAttribute('title')).toBe(name);
117
+ expect(activateRow(mounted.dialog, row)).toBe(uri);
118
+
119
+ mounted.dispose();
120
+ });
121
+ }
122
+
123
+ 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.
126
+ const mounted = mount();
127
+ mounted.dialog.developers = ['Ada <>'];
128
+ const row = page(mounted.dialog, 'credits')!.querySelector('adw-action-row')!;
129
+ expect(row.hasAttribute('activatable')).toBe(true);
130
+ expect(activateRow(mounted.dialog, row)).toBe('mailto:');
131
+ mounted.dispose();
132
+ });
133
+ });
134
+
135
+ await describe('adw-about-dialog credits page (update_credits vectors)', async () => {
136
+ for (const { input, sections, rule } of CREDITS_SECTIONS_VECTORS) {
137
+ // `add_credit_section` has no counterpart on this element yet, so the
138
+ // rows exercising it are asserted by the core suite alone.
139
+ if (input.creditSections) continue;
140
+ // Same for `translator-credits`: no property, so no "Translated by".
141
+ if (input.translatorCredits !== undefined) continue;
142
+
143
+ await it(rule, () => {
144
+ const mounted = mount();
145
+ mounted.dialog.developers = [...(input.developers ?? [])];
146
+ mounted.dialog.designers = [...(input.designers ?? [])];
147
+ mounted.dialog.artists = [...(input.artists ?? [])];
148
+ mounted.dialog.documenters = [...(input.documenters ?? [])];
149
+
150
+ const credits = page(mounted.dialog, 'credits');
151
+ const groups = credits ? [...credits.querySelectorAll('adw-preferences-group')] : [];
152
+ expect(groups.map((node) => node.getAttribute('title'))).toStrictEqual(
153
+ sections.map((section) => section.title),
154
+ );
155
+ for (const [index, section] of sections.entries()) {
156
+ expect(rowTitles(groups[index]!)).toStrictEqual(section.people.map((person) => person.name));
157
+ }
158
+
159
+ mounted.dispose();
160
+ });
161
+ }
162
+ });
163
+
164
+ await describe('adw-about-dialog main page (update_details vectors)', async () => {
165
+ // `add_link` has no counterpart on this element, so `has_custom_links`
166
+ // is always FALSE here; those rows are asserted by the core suite.
167
+ for (const vector of ABOUT_DIALOG_DETAILS_VECTORS.filter((row) => !row.customLinks && !row.releaseNotes)) {
168
+ const { website, comments, visible, rule } = vector;
169
+ await it(`website=${website} comments=${comments} — ${rule}`, () => {
170
+ const mounted = mount({
171
+ 'application-name': 'Builder',
172
+ ...(website ? { website: 'https://example.org' } : {}),
173
+ ...(comments ? { comments: 'A description.' } : {}),
174
+ });
175
+
176
+ const main = page(mounted.dialog, 'main');
177
+ const details = page(mounted.dialog, 'details');
178
+
179
+ expect({
180
+ detailsGroup: group(mounted.dialog, 'details') !== null,
181
+ detailsRow: hasRow(main, label('detailsRow')),
182
+ websiteRow: hasRow(main, label('websiteRow')),
183
+ commentsLabel: details?.querySelector('.adw-about-dialog-comments') != null,
184
+ linksGroup: group(mounted.dialog, 'links') !== null,
185
+ detailsWebsiteRow: hasRow(details, label('websiteRow')),
186
+ }).toStrictEqual({
187
+ detailsGroup: visible.detailsGroup,
188
+ detailsRow: visible.detailsRow,
189
+ websiteRow: visible.websiteRow,
190
+ commentsLabel: visible.commentsLabel,
191
+ linksGroup: visible.linksGroup,
192
+ detailsWebsiteRow: visible.detailsWebsiteRow,
193
+ });
194
+
195
+ mounted.dispose();
196
+ });
197
+ }
198
+
199
+ 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.
203
+ const mounted = mount({ 'application-name': 'Builder', website: 'https://example.org' });
204
+ expect(page(mounted.dialog, 'details')).toBe(null);
205
+ expect(hasRow(page(mounted.dialog, 'main'), label('websiteRow'))).toBe(true);
206
+ mounted.dispose();
207
+ });
208
+ });
209
+
210
+ 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.
213
+ for (const vector of ABOUT_DIALOG_SUPPORT_VECTORS.filter((row) => !row.debugInfo)) {
214
+ const { supportUrl, issueUrl, visible, rule } = vector;
215
+ await it(`support=${supportUrl} issue=${issueUrl} — ${rule}`, () => {
216
+ const mounted = mount({
217
+ 'application-name': 'Builder',
218
+ ...(supportUrl ? { 'support-url': 'https://example.org/help' } : {}),
219
+ ...(issueUrl ? { 'issue-url': 'https://example.org/issues' } : {}),
220
+ });
221
+
222
+ const main = page(mounted.dialog, 'main');
223
+ expect({
224
+ supportGroup: group(mounted.dialog, 'support') !== null,
225
+ supportRow: hasRow(main, label('supportRow')),
226
+ issueRow: hasRow(main, label('issueRow')),
227
+ }).toStrictEqual({
228
+ supportGroup: visible.supportGroup,
229
+ supportRow: visible.supportRow,
230
+ issueRow: visible.issueRow,
231
+ });
232
+
233
+ mounted.dispose();
234
+ });
235
+ }
236
+
237
+ 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).
240
+ const mounted = mount({
241
+ 'application-name': 'Builder',
242
+ 'support-url': 'https://example.org/help',
243
+ 'issue-url': 'https://example.org/issues',
244
+ });
245
+ expect(rowTitles(page(mounted.dialog, 'main'))).toContain(label('supportRow'));
246
+ expect(rowTitles(page(mounted.dialog, 'main'))).toContain(label('issueRow'));
247
+ // With neither comments nor custom links there is no Details page.
248
+ expect(page(mounted.dialog, 'details')).toBe(null);
249
+ mounted.dispose();
250
+ });
251
+ });
252
+
253
+ await describe('adw-about-dialog main page (credits/legal vectors)', async () => {
254
+ // No acknowledgements API on this element; the core suite covers it.
255
+ for (const vector of ABOUT_DIALOG_CREDITS_LEGAL_VECTORS.filter((row) => !row.hasAcknowledgements)) {
256
+ const { hasCredits, hasLegal, visible, rule } = vector;
257
+ await it(`credits=${hasCredits} legal=${hasLegal} — ${rule}`, () => {
258
+ const mounted = mount({
259
+ 'application-name': 'Builder',
260
+ ...(hasLegal ? { copyright: '© 2026 Ada' } : {}),
261
+ });
262
+ if (hasCredits) mounted.dialog.developers = ['Ada'];
263
+
264
+ const main = page(mounted.dialog, 'main');
265
+ expect({
266
+ creditsLegalGroup: group(mounted.dialog, 'credits-legal') !== null,
267
+ creditsRow: hasRow(main, label('creditsRow')),
268
+ legalRow: hasRow(main, label('legalRow')),
269
+ }).toStrictEqual({
270
+ creditsLegalGroup: visible.creditsLegalGroup,
271
+ creditsRow: visible.creditsRow,
272
+ legalRow: visible.legalRow,
273
+ });
274
+
275
+ mounted.dispose();
276
+ });
277
+ }
278
+ });
279
+
280
+ await describe('adw-about-dialog header fields (no invented fallbacks)', async () => {
281
+ for (const { property, value, visible, rule } of ABOUT_DIALOG_HEADER_VECTORS) {
282
+ if (property === 'application-icon' || property === 'application-name') {
283
+ await it(`${property}=${JSON.stringify(value)} — ${rule}`, () => {
284
+ const mounted = mount(value.length > 0 ? { [property]: value } : {});
285
+ const main = page(mounted.dialog, 'main')!;
286
+ const node =
287
+ property === 'application-icon'
288
+ ? main.querySelector('.adw-about-dialog-icon')
289
+ : main.querySelector('.adw-about-dialog-name');
290
+ expect(node !== null).toBe(visible);
291
+ mounted.dispose();
292
+ });
293
+ continue;
294
+ }
295
+
296
+ await it(`${property}=${JSON.stringify(value)} — ${rule}`, () => {
297
+ const mounted = mount(value.length > 0 ? { [property]: value } : {});
298
+ const main = page(mounted.dialog, 'main')!;
299
+ const selector =
300
+ property === 'developer-name' ? '.adw-about-dialog-developer' : '.adw-about-dialog-version';
301
+ expect(main.querySelector(selector) !== null).toBe(visible);
302
+ mounted.dispose();
303
+ });
304
+ }
305
+
306
+ 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).
310
+ const mounted = mount({ 'application-name': 'Builder' });
311
+ expect(page(mounted.dialog, 'main')!.getAttribute('title')).toBe('About');
312
+ mounted.dispose();
313
+ });
314
+ });
315
+
316
+ await describe('adw-about-dialog activate-link is cancellable', async () => {
317
+ await it('opens the URI when nothing handled the signal', () => {
318
+ const mounted = mount({ 'application-name': 'Builder', website: 'https://example.org' });
319
+ const row = page(mounted.dialog, 'main')!.querySelector(`adw-action-row[title="${label('websiteRow')}"]`)!;
320
+ row.dispatchEvent(new CustomEvent('activated', { bubbles: true }));
321
+ expect(mounted.opened).toStrictEqual(['https://example.org']);
322
+ mounted.dispose();
323
+ });
324
+
325
+ 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.
332
+ const mounted = mount({ 'application-name': 'Builder', website: 'https://example.org' });
333
+ mounted.dialog.addEventListener('activate-link', (event) => event.preventDefault());
334
+ const row = page(mounted.dialog, 'main')!.querySelector(`adw-action-row[title="${label('websiteRow')}"]`)!;
335
+ row.dispatchEvent(new CustomEvent('activated', { bubbles: true }));
336
+ expect(mounted.opened).toStrictEqual([]);
337
+ mounted.dispose();
338
+ });
339
+
340
+ await it('is cancelable on a credit row too', () => {
341
+ const mounted = mount();
342
+ mounted.dialog.developers = ['Ada <ada@lovelace.org>'];
343
+ mounted.dialog.addEventListener('activate-link', (event) => event.preventDefault());
344
+ const row = page(mounted.dialog, 'credits')!.querySelector('adw-action-row')!;
345
+ row.dispatchEvent(new CustomEvent('activated', { bubbles: true }));
346
+ expect(mounted.opened).toStrictEqual([]);
347
+ mounted.dispose();
348
+ });
349
+ });
350
+ };