@gjsify/adwaita-web 0.64.13-next.283 → 0.64.14

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 (44) hide show
  1. package/lib/types/application.d.ts +3 -3
  2. package/lib/types/elements/adw-application-window.d.ts +4 -4
  3. package/lib/types/elements/adw-bin.d.ts +13 -0
  4. package/lib/types/elements/adw-split-button.d.ts +19 -28
  5. package/lib/types/elements/adw-tab-overview.d.ts +4 -4
  6. package/lib/types/elements/gtk-menu-button.d.ts +30 -42
  7. package/lib/types/elements/gtk-popover-bin.d.ts +18 -11
  8. package/lib/types/elements/gtk-popover-menu-bar.d.ts +15 -8
  9. package/lib/types/elements/gtk-popover-menu.d.ts +13 -15
  10. package/lib/types/elements/menu-slot.d.ts +49 -0
  11. package/lib/types/elements/popover-menu.d.ts +11 -7
  12. package/lib/types/index.d.ts +2 -1
  13. package/lib/types/namespace/gio.d.ts +1 -1
  14. package/lib/types/widget-element.d.ts +8 -2
  15. package/package.json +12 -13
  16. package/src/adw-application-window.spec.ts +5 -2
  17. package/src/adw-bin.spec.ts +20 -0
  18. package/src/application.ts +4 -4
  19. package/src/elements/adw-application-window.ts +17 -21
  20. package/src/elements/adw-bin.ts +20 -1
  21. package/src/elements/adw-split-button.ts +35 -59
  22. package/src/elements/adw-tab-overview.ts +18 -28
  23. package/src/elements/gtk-menu-button.ts +60 -82
  24. package/src/elements/gtk-popover-bin.ts +32 -38
  25. package/src/elements/gtk-popover-menu-bar.ts +41 -46
  26. package/src/elements/gtk-popover-menu.ts +35 -54
  27. package/src/elements/menu-slot.ts +139 -0
  28. package/src/elements/popover-menu.ts +26 -16
  29. package/src/gio-menu.spec.ts +173 -34
  30. package/src/gtk-image.spec.ts +3 -2
  31. package/src/gtk-menu-button.spec.ts +19 -16
  32. package/src/gtk-popover-bin.spec.ts +6 -5
  33. package/src/gtk-popover-menu-bar.spec.ts +11 -6
  34. package/src/gtk-popover-menu.spec.ts +22 -22
  35. package/src/index.ts +10 -2
  36. package/src/keyboard-operable.spec.ts +6 -5
  37. package/src/menu-actions.spec.ts +6 -1
  38. package/src/namespace/gio.ts +19 -2
  39. package/src/pango-markup.spec.ts +11 -0
  40. package/src/pango-markup.ts +5 -2
  41. package/src/shared-tree-builder.ts +6 -1
  42. package/src/split-button.spec.ts +25 -8
  43. package/src/widget-element.ts +28 -7
  44. package/src/widget-members.spec.ts +42 -3
package/src/index.ts CHANGED
@@ -114,8 +114,9 @@ export * as Adw from './namespace/adw.js';
114
114
  export * as Gtk from './namespace/gtk.js';
115
115
 
116
116
  // The THIRD namespace, and the one that defines no element: the GObject types an author
117
- // CONSTRUCTS while building a tree. `Gio.Menu` is what makes `menuModel: menu` the same
118
- // program here as on GJS — `./namespace/gio.ts` says what does not go in it.
117
+ // CONSTRUCTS while building a tree. `Gio.Menu` is a real `Gio.MenuModel` (ADR 0128), which
118
+ // is what makes `menuModel: menu` the same program here as on GJS — `./namespace/gio.ts`
119
+ // says what does not go in it.
119
120
  export * as Gio from './namespace/gio.js';
120
121
 
121
122
  // Gdk's clipboard (`Display`, `Clipboard`, `ContentProvider`): `./namespace/gdk.ts` says what is absent.
@@ -189,6 +190,12 @@ export type { AdwViewStackPageInfo } from './elements/adw-view-stack.js';
189
190
  // The portable menu model (ADR 0042) is `@gjsify/adwaita-core`'s, re-exported so a
190
191
  // consumer of THIS package reaches the menu types without a second dependency — one
191
192
  // definition, two doors, never two declarations.
193
+ //
194
+ // `buildMenu` is a VALUE and not only a type, because since ADR 0128 a `menuModel`
195
+ // property takes a real `Gio.MenuModel` and an authored array is refused: without the
196
+ // builder an app depending on this package alone could not write a menu in code at all.
197
+ // The `menu-model` ATTRIBUTE still takes the JSON string, so markup needs none of this.
198
+ export { buildMenu, menuTree, watchMenuModel } from '@gjsify/adwaita-core';
192
199
  export type {
193
200
  AdwMenuActions,
194
201
  AdwMenuEntryInput,
@@ -199,6 +206,7 @@ export type {
199
206
  AdwMenuPath,
200
207
  AdwMenuSection,
201
208
  AdwMenuSubmenu,
209
+ AdwMenuTree,
202
210
  } from '@gjsify/adwaita-core';
203
211
  export type { GtkDropDownOption } from './elements/gtk-drop-down.js';
204
212
  export type { AdwDialogPresentationMode } from './elements/adw-dialog.js';
@@ -19,6 +19,7 @@
19
19
  // A real Tab press escaping an untrapped dialog is measured one layer up, by
20
20
  // `tests/browser/specs/adwaita-keyboard.spec.ts`.
21
21
  import { describe, expect, it } from '@gjsify/unit';
22
+ import { buildMenu } from '@gjsify/adwaita-core';
22
23
 
23
24
  import type { AdwAlertDialog } from './elements/adw-alert-dialog.js';
24
25
  import { AdwToggle, AdwToggleGroup } from './elements/adw-toggle-group.js';
@@ -737,11 +738,11 @@ export const AdwKeyboardOperableTest = async () => {
737
738
  };
738
739
  document.body.appendChild(el);
739
740
  el.actions = { 'app.export': { enabled: false }, 'app.new': {} };
740
- el.menuModel = [
741
+ el.menuModel = buildMenu([
741
742
  { label: 'New', action: 'app.new' },
742
743
  { section: [{ label: 'Export', action: 'app.export' }, { label: 'Print' }] },
743
744
  { label: 'More', submenu: [{ label: 'Rename' }, { label: 'Duplicate' }] },
744
- ];
745
+ ]);
745
746
  (el.querySelector('.adw-menu-button-button') as HTMLElement).click();
746
747
  return {
747
748
  el,
@@ -828,14 +829,14 @@ export const AdwKeyboardOperableTest = async () => {
828
829
  };
829
830
  document.body.appendChild(el);
830
831
  el.actions = { 'app.off': { enabled: false } };
831
- el.menuModel = [
832
+ el.menuModel = buildMenu([
832
833
  { label: 'Dim', action: 'app.off' },
833
834
  { label: 'Live' },
834
835
  {
835
836
  label: 'More',
836
837
  submenu: [{ label: 'SubDim', action: 'app.off' }, { label: 'SubLive' }],
837
838
  },
838
- ];
839
+ ]);
839
840
  (el.querySelector('.adw-menu-button-button') as HTMLElement).click();
840
841
  const rows = () => [...el.querySelectorAll<HTMLButtonElement>('.adw-popover-item')];
841
842
  const label = () =>
@@ -867,7 +868,7 @@ export const AdwKeyboardOperableTest = async () => {
867
868
  };
868
869
  document.body.appendChild(el);
869
870
  el.actions = { 'app.off': { enabled: false } };
870
- el.menuModel = [{ label: 'More', submenu: [{ label: 'Only', action: 'app.off' }] }];
871
+ el.menuModel = buildMenu([{ label: 'More', submenu: [{ label: 'Only', action: 'app.off' }] }]);
871
872
  (el.querySelector('.adw-menu-button-button') as HTMLElement).click();
872
873
  const opener = el.querySelector('.adw-popover-item') as HTMLButtonElement;
873
874
  opener.focus();
@@ -7,6 +7,7 @@ import {
7
7
  registryMenuActionScene,
8
8
  type SharedTreeNode,
9
9
  } from '@gjsify/adwaita-core/conformance';
10
+ import { menuTree } from '@gjsify/adwaita-core';
10
11
  import { describe, expect, it } from '@gjsify/unit';
11
12
 
12
13
  import { parentOfElement } from './application.js';
@@ -39,7 +40,11 @@ export const MenuActionsTest = async () => {
39
40
  return;
40
41
  }
41
42
  const button = buildSharedTree(node) as unknown as GtkMenuButton;
42
- expect(button.menuModel.map((entry) => entry.kind).join(',')).toBe(result.kinds.join(','));
43
+ expect(
44
+ menuTree(button.menuModel!)
45
+ .map((entry) => entry.kind)
46
+ .join(','),
47
+ ).toBe(result.kinds.join(','));
43
48
  });
44
49
  }
45
50
  });
@@ -1,4 +1,4 @@
1
- // The GIO half of `@gjsify/adwaita-web`'s vocabulary — `Gio.Menu`, `Gio.MenuItem`.
1
+ // The GIO half of `@gjsify/adwaita-web`'s vocabulary — `Gio.MenuModel` and its subclasses.
2
2
  // `export * as Gio from './namespace/gio.js'` in `src/index.ts` makes it the namespace,
3
3
  // the same module shape `./adw.ts` and `./gtk.ts` take and for the same reason (one list
4
4
  // carrying the value AND the type meaning — see the header of `./adw.ts`).
@@ -28,7 +28,24 @@ import { createAppInfo } from '@gjsify/adwaita-core';
28
28
 
29
29
  import { webPlatformHost } from '../platform-host.js';
30
30
 
31
- export { GioMenu as Menu, GioMenuItem as MenuItem } from '@gjsify/adwaita-core';
31
+ // ADR 0128: `Gio.MenuModel` is an abstract CLASS, so the whole menu hierarchy is here and
32
+ // `menu instanceof Gio.MenuModel` answers the same on both runtimes. The two iterators come
33
+ // with it because `iterate_item_attributes` / `iterate_item_links` return them by name.
34
+ export {
35
+ GioMenu as Menu,
36
+ GioMenuItem as MenuItem,
37
+ GioMenuModel as MenuModel,
38
+ GioMenuAttributeIter as MenuAttributeIter,
39
+ GioMenuLinkIter as MenuLinkIter,
40
+ MENU_ATTRIBUTE_ACTION,
41
+ MENU_ATTRIBUTE_ACTION_NAMESPACE,
42
+ MENU_ATTRIBUTE_ICON,
43
+ MENU_ATTRIBUTE_LABEL,
44
+ MENU_ATTRIBUTE_TARGET,
45
+ MENU_EXPORTER_MAX_SECTION_SIZE,
46
+ MENU_LINK_SECTION,
47
+ MENU_LINK_SUBMENU,
48
+ } from '@gjsify/adwaita-core';
32
49
  // ADR 0122 § 2: the interface a `Gtk.StringList` implements and an `Implements: [Gio.ListModel]`
33
50
  // names. It is a token, not a class — the whole of the subset's interface support.
34
51
  export { ListModel } from '@gjsify/adwaita-core';
@@ -73,6 +73,17 @@ export const PangoMarkupTest = async () => {
73
73
  expect(target.querySelector('i > u')?.textContent).toBe('b');
74
74
  });
75
75
 
76
+ await it("sub and sup carry pango's 1 / 1.2 font_scale fallback", () => {
77
+ const { target } = render('<sub>d</sub><sup>e</sup>');
78
+ // Compared as a computed ratio: browsers serialise the `calc()` differently
79
+ // (Firefox gives `calc(0.833333em)`).
80
+ const base = Number.parseFloat(getComputedStyle(target).fontSize);
81
+ for (const el of target.children) {
82
+ const ratio = Number.parseFloat(getComputedStyle(el).fontSize) / base;
83
+ expect(Math.abs(ratio - 1 / 1.2)).toBeLessThan(0.01);
84
+ }
85
+ });
86
+
76
87
  await it("<markup> draws its children — it is pango's implicit root, not a box", async () => {
77
88
  const { target } = render('<markup><b>x</b></markup>');
78
89
  expect(target.querySelector('b')?.textContent).toBe('x');
@@ -59,8 +59,11 @@ export const TAG_STYLES: Readonly<Record<string, { element: string; style?: Read
59
59
  tt: { element: 'span', style: { 'font-family': 'monospace' } },
60
60
  big: { element: 'span', style: { 'font-size': 'larger' } },
61
61
  small: { element: 'span', style: { 'font-size': 'smaller' } },
62
- sub: { element: 'sub' },
63
- sup: { element: 'sup' },
62
+ // pango's `font_scale` for `<sub>`/`<sup>` falls back to `1 / 1.2` when the font's OpenType
63
+ // metrics carry no script size (pango/itemize.c, `PANGO_FONT_SCALE_SUBSCRIPT`/`SUPERSCRIPT`);
64
+ // spelled out so it does not depend on each UA's `font-size: smaller` ratio.
65
+ sub: { element: 'sub', style: { 'font-size': 'calc(1em / 1.2)' } },
66
+ sup: { element: 'sup', style: { 'font-size': 'calc(1em / 1.2)' } },
64
67
  markup: { element: 'span', style: { display: 'contents' } },
65
68
  span: { element: 'span' },
66
69
  };
@@ -35,6 +35,7 @@
35
35
  // this small, most of which (the `SharedTreeNode` type) is erased at build anyway.
36
36
 
37
37
  import {
38
+ buildMenu,
38
39
  createBreakpointDriver,
39
40
  menuAssignments,
40
41
  parseBreakpointCondition,
@@ -512,8 +513,12 @@ function writeLook(el: HTMLElement, node: SharedTreeNode, menus?: SharedTreeNode
512
513
  for (const [prop, value] of Object.entries(node.props ?? {})) {
513
514
  if (prop !== 'menu-model') writeProp(el, prop, value);
514
515
  }
516
+ // `buildMenu`, because since ADR 0128 `menuModel` is a real `Gio.MenuModel` and a tree
517
+ // written at it is refused the way GJS refuses one. `menuAssignments` stays a tree walker
518
+ // — it resolves a `menu-model: id` against the file's `menus` — so the model is built
519
+ // here, at the one place the resolved tree becomes a widget's property.
515
520
  for (const [prop, model] of menuAssignments(node, menus)) {
516
- (el as unknown as Record<string, unknown>)[propertyOf(prop)] = model;
521
+ (el as unknown as Record<string, unknown>)[propertyOf(prop)] = buildMenu(model);
517
522
  }
518
523
  // `styleClasses` is `GtkWidget:css-classes`, and this renderer's door for it is the
519
524
  // `class` attribute — what `.title-1`, `.dimmed` and `.card` select on. Unread, a
@@ -17,7 +17,7 @@ import {
17
17
  SPLIT_BUTTON_STYLE_CLASS_VECTORS,
18
18
  SPLIT_BUTTON_TOOLTIP_VECTORS,
19
19
  } from '@gjsify/adwaita-core/conformance';
20
- import { ADW_MENU_SURFACE_WEB, flattenMenu, menuRefusals } from '@gjsify/adwaita-core';
20
+ import { ADW_MENU_SURFACE_WEB, buildMenu, flattenMenu, menuRefusals, menuTree } from '@gjsify/adwaita-core';
21
21
  import type { SplitButtonProperty } from '@gjsify/adwaita-core';
22
22
  import type { AdwSplitButton } from './elements/adw-split-button.js';
23
23
  import { fallbackMask, maskOf } from './icon-masks.spec.js';
@@ -238,24 +238,24 @@ export const AdwSplitButtonTest = async () => {
238
238
  });
239
239
 
240
240
  await describe('adw-split-button menu', async () => {
241
- for (const { input, model, rule } of MENU_NORMALIZE_VECTORS) {
241
+ for (const { input, tree, rule } of MENU_NORMALIZE_VECTORS) {
242
242
  await it(`normalises an authored menu — ${rule}`, () => {
243
243
  const { el, host } = mount({ label: 'Save' });
244
244
  // What THIS surface refuses is core's answer, not a list restated here:
245
245
  // a `custom` item names an application widget the browser cannot host,
246
246
  // and the setter is where that is said out loud.
247
- if (menuRefusals(model, ADW_MENU_SURFACE_WEB).length > 0) {
247
+ if (menuRefusals(tree, ADW_MENU_SURFACE_WEB).length > 0) {
248
248
  expect(() => {
249
- el.menuModel = input;
249
+ el.menuModel = buildMenu(input);
250
250
  }).toThrow('adwaita-web');
251
251
  host.remove();
252
252
  return;
253
253
  }
254
- el.menuModel = input;
255
- expect([...el.menuModel]).toStrictEqual([...model]);
254
+ el.menuModel = buildMenu(input);
255
+ expect(menuTree(el.menuModel!)).toStrictEqual([...tree]);
256
256
  // Every node the model draws is a row: a section is inlined, a submenu
257
257
  // is one row of its own.
258
- expect(el.querySelectorAll('.adw-split-button-menu-item').length).toBe(flattenMenu(model).length);
258
+ expect(el.querySelectorAll('.adw-split-button-menu-item').length).toBe(flattenMenu(tree).length);
259
259
  host.remove();
260
260
  });
261
261
  }
@@ -312,11 +312,28 @@ export const AdwSplitButtonTest = async () => {
312
312
  const { el, host } = mount({ 'menu-model': '[{"label":"Print"}]' });
313
313
  dropdownHalf(el).click();
314
314
  expect(el.active).toBe(true);
315
- el.setAttribute('menu-model', '[]');
315
+ // REMOVING the attribute is how a menu is lost since ADR 0128 — `null` is the
316
+ // only spelling of "no menu", and the row below is the other half of that.
317
+ el.removeAttribute('menu-model');
316
318
  expect(el.active).toBe(false);
317
319
  expect(el.dropdownEnabled).toBe(false);
318
320
  host.remove();
319
321
  });
322
+
323
+ await it('an EMPTY menu is still a menu, so the dropdown stays live', () => {
324
+ // Measured on `Adw.SplitButton`: an empty non-null `Gio.Menu` at `menu-model`
325
+ // still builds a `GtkPopoverMenu` and `set_menu_model(null)` is what returns
326
+ // the popover to null (`MENU_MODEL_DOOR_VECTORS`). `[]` therefore edits the
327
+ // menu this element OWNS rather than taking it away, so the open popup follows
328
+ // the edit instead of being closed by it.
329
+ const { el, host } = mount({ 'menu-model': '[{"label":"Print"}]' });
330
+ dropdownHalf(el).click();
331
+ el.setAttribute('menu-model', '[]');
332
+ expect(el.dropdownEnabled).toBe(true);
333
+ expect(el.active).toBe(true);
334
+ expect(el.querySelectorAll('.adw-split-button-menu-item').length).toBe(0);
335
+ host.remove();
336
+ });
320
337
  });
321
338
 
322
339
  await describe('adw-split-button signals + state classes', async () => {
@@ -16,10 +16,25 @@
16
16
  // Reference: GTK gtk/gtkwidget.c (gtk_widget_set_visible, gtk_widget_set_tooltip_text)
17
17
  // Copyright (c) GNOME contributors (GTK). LGPLv2.1+.
18
18
 
19
+ import { isWidgetNode } from '@gjsify/adwaita-core';
20
+
19
21
  import { WIDGET_VISIBLE_GETTERS, installElementApi, markLazyElementApi, notifyElement } from './gobject-elements.js';
20
22
  import { dispatchedSignalsOf, type DispatchedSignals } from './signals.js';
21
23
  import { isMapped } from './widget-mapping.js';
22
24
 
25
+ /** Element links, walked until one lands on a widget: `isWidgetNode` says which those are. */
26
+ type ElementLink =
27
+ | 'firstElementChild'
28
+ | 'lastElementChild'
29
+ | 'nextElementSibling'
30
+ | 'previousElementSibling'
31
+ | 'parentElement';
32
+
33
+ function widgetChild(from: Element, first: ElementLink, next: ElementLink): Element | null {
34
+ for (let at = from[first]; at !== null; at = at[next]) if (isWidgetNode(at)) return at;
35
+ return null;
36
+ }
37
+
23
38
  export class WidgetElement extends HTMLElement {
24
39
  /** `Gtk.Widget::map` / `::unmap`, which every subclass inherits (`dispatchedSignalsOf`). */
25
40
  static readonly signals: DispatchedSignals = { map: 'map', unmap: 'unmap' };
@@ -41,25 +56,31 @@ export class WidgetElement extends HTMLElement {
41
56
  set_visible(visible: boolean): void {
42
57
  this.visible = visible;
43
58
  }
44
- /** `gtk_widget_get_first_child`: the first child element, `null` for a leaf. */
59
+ /**
60
+ * `gtk_widget_get_first_child`: the first child WIDGET, `null` for a leaf.
61
+ *
62
+ * The five walkers skip the elements a port builds for its own insides — `isWidgetNode`
63
+ * in `@gjsify/adwaita-core` carries the rule and the incident behind it, and the
64
+ * registered-class copy in `widgetTreeMembers()` answers from the same function.
65
+ */
45
66
  get_first_child(): Element | null {
46
- return this.firstElementChild;
67
+ return widgetChild(this, 'firstElementChild', 'nextElementSibling');
47
68
  }
48
69
  /** `gtk_widget_get_last_child`. */
49
70
  get_last_child(): Element | null {
50
- return this.lastElementChild;
71
+ return widgetChild(this, 'lastElementChild', 'previousElementSibling');
51
72
  }
52
73
  /** `gtk_widget_get_next_sibling`. */
53
74
  get_next_sibling(): Element | null {
54
- return this.nextElementSibling;
75
+ return widgetChild(this, 'nextElementSibling', 'nextElementSibling');
55
76
  }
56
77
  /** `gtk_widget_get_prev_sibling`. */
57
78
  get_prev_sibling(): Element | null {
58
- return this.previousElementSibling;
79
+ return widgetChild(this, 'previousElementSibling', 'previousElementSibling');
59
80
  }
60
- /** `gtk_widget_get_parent`. */
81
+ /** `gtk_widget_get_parent`: the nearest ancestor that is a widget, not a port's own wrapper. */
61
82
  get_parent(): Element | null {
62
- return this.parentElement;
83
+ return widgetChild(this, 'parentElement', 'parentElement');
63
84
  }
64
85
  /** `gtk_widget_get_width`: the layout box in CSS px; 0 until the widget is rendered. */
65
86
  get_width(): number {
@@ -60,10 +60,10 @@ export const WidgetMembersTest = async () => {
60
60
  expect(() => widget.set_direction(7)).toThrow();
61
61
  });
62
62
 
63
- await it('walks the element tree with the GTK link names', () => {
63
+ await it('walks the widget tree with the GTK link names', () => {
64
64
  const parent = make('WidgetMembersParent');
65
- const a = document.createElement('div');
66
- const b = document.createElement('div');
65
+ const a = new Gtk.Label() as unknown as WidgetLike;
66
+ const b = new Gtk.Label() as unknown as WidgetLike;
67
67
  parent.append(a, b);
68
68
  expect(parent.get_first_child()).toBe(a);
69
69
  expect(parent.get_last_child()).toBe(b);
@@ -73,6 +73,45 @@ export const WidgetMembersTest = async () => {
73
73
  expect(parent.get_parent()).toBe(null);
74
74
  });
75
75
 
76
+ await it("steps over the plain elements a port builds for a widget's insides", () => {
77
+ // `gtk_widget_get_first_child` hands back a GtkWidget, always. In a DOM port the
78
+ // same `children` list also holds what the port made for itself — the `<span>` a
79
+ // label's markup renderer writes, the `<div>` a row lays its slots out in — and a
80
+ // caller walking the tree would get one of those and find no `get_first_child` on
81
+ // it at all. The custom-element hyphen separates the two kinds (`isWidgetNode`).
82
+ const parent = make('WidgetMembersInsides');
83
+ const a = new Gtk.Label() as unknown as WidgetLike;
84
+ const b = new Gtk.Label() as unknown as WidgetLike;
85
+ parent.append(
86
+ document.createElement('span'),
87
+ a,
88
+ document.createElement('div'),
89
+ b,
90
+ document.createElement('span'),
91
+ );
92
+ expect(parent.get_first_child()).toBe(a);
93
+ expect(parent.get_last_child()).toBe(b);
94
+ expect(a.get_next_sibling()).toBe(b);
95
+ expect(b.get_prev_sibling()).toBe(a);
96
+ expect(b.get_next_sibling()).toBe(null);
97
+ });
98
+
99
+ await it('a widget holding only its own markup is a LEAF', () => {
100
+ const label = make('WidgetMembersLeaf');
101
+ label.append(document.createElement('span'), document.createElement('span'));
102
+ expect(label.get_first_child()).toBe(null);
103
+ expect(label.get_last_child()).toBe(null);
104
+ });
105
+
106
+ await it('get_parent reports the nearest ancestor that is a widget', () => {
107
+ const box = new Gtk.Box() as unknown as WidgetLike;
108
+ const wrapper = document.createElement('div');
109
+ const label = new Gtk.Label() as unknown as WidgetLike;
110
+ box.append(wrapper);
111
+ wrapper.append(label);
112
+ expect(label.get_parent()).toBe(box);
113
+ });
114
+
76
115
  await it('walks the element tree of a built-in widget with the GTK link names', () => {
77
116
  const box = new Gtk.Box() as unknown as WidgetLike;
78
117
  const a = new Gtk.Label() as unknown as WidgetLike;