@gjsify/adwaita-web 0.51.1 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adwaita-web.css +255 -27
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/blueprint-layout.spec.d.ts +1 -0
- package/lib/types/blueprint-markup.spec.d.ts +4 -0
- package/lib/types/blueprint-tree.spec.d.ts +1 -0
- package/lib/types/elements/adw-bottom-sheet.d.ts +31 -13
- package/lib/types/elements/adw-carousel.d.ts +5 -0
- package/lib/types/elements/adw-header-bar.d.ts +15 -0
- package/lib/types/elements/adw-inline-view-switcher.d.ts +1 -0
- package/lib/types/elements/adw-navigation-view.d.ts +1 -0
- package/lib/types/elements/adw-sidebar.d.ts +10 -0
- package/lib/types/elements/adw-tab-view.d.ts +1 -0
- package/lib/types/elements/adw-toggle-group.d.ts +11 -0
- package/lib/types/elements/gtk-action-bar.d.ts +19 -0
- package/lib/types/elements/gtk-box.d.ts +17 -0
- package/lib/types/elements/gtk-button.d.ts +2 -0
- package/lib/types/elements/gtk-label.d.ts +122 -0
- package/lib/types/gio-menu.spec.d.ts +1 -0
- package/lib/types/gtk-box.spec.d.ts +1 -0
- package/lib/types/gtk-label.spec.d.ts +1 -0
- package/lib/types/gtk-value-doors.spec.d.ts +1 -0
- package/lib/types/index.d.ts +4 -1
- package/lib/types/namespace/gio.d.ts +1 -0
- package/lib/types/namespace/gtk.d.ts +5 -0
- package/lib/types/shared-tree-builder.d.ts +64 -0
- package/lib/types/source-view/asm6502.d.ts +1 -20
- package/lib/types/source-view/index.d.ts +3 -2
- package/lib/types/source-view/theme.d.ts +1 -1
- package/lib/types/string-list-slot.d.ts +4 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/lib/types/tags.spec.d.ts +1 -0
- package/lib/types/value-lists.spec.d.ts +1 -0
- package/package.json +8 -6
- package/scss/_action_bar.scss +51 -0
- package/scss/_banner.scss +8 -1
- package/scss/_bottom_sheet.scss +57 -0
- package/scss/_box.scss +23 -0
- package/scss/_button.scss +29 -7
- package/scss/_button_content.scss +6 -1
- package/scss/_button_row.scss +5 -2
- package/scss/_carousel.scss +6 -0
- package/scss/_labels.scss +126 -12
- package/scss/_navigation_split_view.scss +18 -0
- package/scss/_overlay_split_view.scss +5 -0
- package/scss/_reset.scss +29 -4
- package/scss/_split_button.scss +5 -2
- package/scss/_status_page.scss +31 -17
- package/scss/_theme.scss +11 -3
- package/scss/_toolbar_view.scss +10 -0
- package/scss/_variables.scss +1 -0
- package/scss/_widget.scss +55 -0
- package/scss/_window_title.scss +9 -0
- package/scss/adwaita-skin.scss +8 -2
- package/src/adw-banner.spec.ts +16 -0
- package/src/adw-button-row.spec.ts +8 -0
- package/src/adw-header-bar.spec.ts +17 -0
- package/src/blueprint-layout.spec.ts +223 -0
- package/src/blueprint-markup.spec.ts +155 -0
- package/src/blueprint-tree.spec.ts +174 -0
- package/src/bottom-sheet.spec.ts +166 -1
- package/src/chrome.spec.ts +5 -2
- package/src/elements/adw-bottom-sheet.ts +166 -59
- package/src/elements/adw-carousel.ts +10 -5
- package/src/elements/adw-clamp.ts +12 -0
- package/src/elements/adw-combo-row.ts +13 -4
- package/src/elements/adw-header-bar.ts +46 -3
- package/src/elements/adw-inline-view-switcher.ts +11 -0
- package/src/elements/adw-navigation-view.ts +10 -0
- package/src/elements/adw-sidebar.ts +22 -0
- package/src/elements/adw-spin-row.ts +44 -5
- package/src/elements/adw-status-page.ts +12 -6
- package/src/elements/adw-tab-view.ts +20 -3
- package/src/elements/adw-toggle-group.ts +45 -4
- package/src/elements/adw-toolbar-view.ts +9 -0
- package/src/elements/gtk-action-bar.ts +90 -0
- package/src/elements/gtk-box.ts +87 -0
- package/src/elements/gtk-button.ts +60 -13
- package/src/elements/gtk-drop-down.ts +10 -1
- package/src/elements/gtk-label.ts +415 -0
- package/src/gio-menu.spec.ts +125 -0
- package/src/globals.d.ts +7 -0
- package/src/gtk-box.spec.ts +168 -0
- package/src/gtk-button.spec.ts +123 -1
- package/src/gtk-label.spec.ts +595 -0
- package/src/gtk-value-doors.spec.ts +104 -0
- package/src/index.ts +14 -1
- package/src/keyboard-operable.spec.ts +2 -1
- package/src/namespace/gio.ts +19 -0
- package/src/namespace/gtk.ts +14 -0
- package/src/shared-tree-builder.ts +268 -0
- package/src/shared-trees.spec.ts +296 -32
- package/src/source-view/adw-source-view.ts +3 -3
- package/src/source-view/asm6502.ts +11 -191
- package/src/source-view/index.ts +17 -11
- package/src/source-view/theme.ts +16 -16
- package/src/split-button.spec.ts +7 -0
- package/src/string-list-slot.ts +21 -0
- package/src/styles.generated.ts +1 -1
- package/src/tags.spec.ts +51 -0
- package/src/test.browser.mts +18 -0
- package/src/value-lists.spec.ts +93 -0
- package/lib/types/source-view/hex.d.ts +0 -21
- package/src/source-view/hex.ts +0 -37
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// The custom-element markup of the `index.html` file the gallery's Web Components tab shows for
|
|
2
|
+
// a one-Blueprint block, PARSED back by the browser and held to the tree `buildSharedTree`
|
|
3
|
+
// makes from the same `.blp`.
|
|
4
|
+
//
|
|
5
|
+
// The website renders that markup with `@gjsify/adwaita-core/markup` from the block's
|
|
6
|
+
// `?shared-tree` projection, on every build. Markup a reader copies has to build the same
|
|
7
|
+
// widget: an attribute spelled against `attributeOf`, a boolean written as `"true"` where the
|
|
8
|
+
// element reads presence, or a slot dropped would each parse fine and mount something else. So every `.blp` under
|
|
9
|
+
// `website/src/blueprints/` is imported below — `scripts/check-website-blueprint-markup.mjs`
|
|
10
|
+
// fails on one that is not.
|
|
11
|
+
|
|
12
|
+
import { describe, expect, it } from '@gjsify/unit';
|
|
13
|
+
|
|
14
|
+
import type { SharedTreeNode } from '@gjsify/adwaita-core/conformance';
|
|
15
|
+
import { sharedTreeHtml } from '@gjsify/adwaita-core/markup';
|
|
16
|
+
import { hostTagOf } from '@gjsify/adwaita-core/tags';
|
|
17
|
+
|
|
18
|
+
import adwaitaActionRowTree from '../../../../website/src/blueprints/adwaita/action-row.blp?shared-tree';
|
|
19
|
+
import adwaitaAvatarTree from '../../../../website/src/blueprints/adwaita/avatar.blp?shared-tree';
|
|
20
|
+
import adwaitaBannerTree from '../../../../website/src/blueprints/adwaita/banner.blp?shared-tree';
|
|
21
|
+
import adwaitaBottomSheetTree from '../../../../website/src/blueprints/adwaita/bottom-sheet.blp?shared-tree';
|
|
22
|
+
import adwaitaButtonContentTree from '../../../../website/src/blueprints/adwaita/button-content.blp?shared-tree';
|
|
23
|
+
import adwaitaButtonRowTree from '../../../../website/src/blueprints/adwaita/button-row.blp?shared-tree';
|
|
24
|
+
import adwaitaCarouselTree from '../../../../website/src/blueprints/adwaita/carousel.blp?shared-tree';
|
|
25
|
+
import clampTree from '../../../../website/src/blueprints/adwaita/clamp.blp?shared-tree';
|
|
26
|
+
import adwaitaComboRowTree from '../../../../website/src/blueprints/adwaita/combo-row.blp?shared-tree';
|
|
27
|
+
import adwaitaEntryRowTree from '../../../../website/src/blueprints/adwaita/entry-row.blp?shared-tree';
|
|
28
|
+
import adwaitaExpanderRowTree from '../../../../website/src/blueprints/adwaita/expander-row.blp?shared-tree';
|
|
29
|
+
import adwaitaHeaderBarTree from '../../../../website/src/blueprints/adwaita/header-bar.blp?shared-tree';
|
|
30
|
+
import adwaitaNavigationSplitViewTree from '../../../../website/src/blueprints/adwaita/navigation-split-view.blp?shared-tree';
|
|
31
|
+
import adwaitaNavigationViewTree from '../../../../website/src/blueprints/adwaita/navigation-view.blp?shared-tree';
|
|
32
|
+
import adwaitaOverlaySplitViewTree from '../../../../website/src/blueprints/adwaita/overlay-split-view.blp?shared-tree';
|
|
33
|
+
import adwaitaPasswordEntryRowTree from '../../../../website/src/blueprints/adwaita/password-entry-row.blp?shared-tree';
|
|
34
|
+
import adwaitaPreferencesGroupTree from '../../../../website/src/blueprints/adwaita/preferences-group.blp?shared-tree';
|
|
35
|
+
import adwaitaShortcutLabelTree from '../../../../website/src/blueprints/adwaita/shortcut-label.blp?shared-tree';
|
|
36
|
+
import adwaitaSidebarTree from '../../../../website/src/blueprints/adwaita/sidebar.blp?shared-tree';
|
|
37
|
+
import adwaitaSpinRowTree from '../../../../website/src/blueprints/adwaita/spin-row.blp?shared-tree';
|
|
38
|
+
import adwaitaSplitButtonTree from '../../../../website/src/blueprints/adwaita/split-button.blp?shared-tree';
|
|
39
|
+
import adwaitaStatusPageTree from '../../../../website/src/blueprints/adwaita/status-page.blp?shared-tree';
|
|
40
|
+
import adwaitaSwitchRowTree from '../../../../website/src/blueprints/adwaita/switch-row.blp?shared-tree';
|
|
41
|
+
import adwaitaToggleGroupTree from '../../../../website/src/blueprints/adwaita/toggle-group.blp?shared-tree';
|
|
42
|
+
import adwaitaToolbarViewTree from '../../../../website/src/blueprints/adwaita/toolbar-view.blp?shared-tree';
|
|
43
|
+
import adwaitaViewSwitcherBarTree from '../../../../website/src/blueprints/adwaita/view-switcher-bar.blp?shared-tree';
|
|
44
|
+
import adwaitaWindowTitleTree from '../../../../website/src/blueprints/adwaita/window-title.blp?shared-tree';
|
|
45
|
+
import adwaitaWrapBoxTree from '../../../../website/src/blueprints/adwaita/wrap-box.blp?shared-tree';
|
|
46
|
+
import gtkButtonTree from '../../../../website/src/blueprints/gtk/button.blp?shared-tree';
|
|
47
|
+
import gtkMenuButtonTree from '../../../../website/src/blueprints/gtk/menu-button.blp?shared-tree';
|
|
48
|
+
|
|
49
|
+
import { buildSharedTree } from './shared-tree-builder.js';
|
|
50
|
+
|
|
51
|
+
/** Every one-Blueprint `.blp` of the gallery, by its path under `website/src/blueprints/`. */
|
|
52
|
+
export const GALLERY_BLUEPRINTS: Readonly<Record<string, SharedTreeNode>> = {
|
|
53
|
+
'adwaita/action-row.blp': adwaitaActionRowTree,
|
|
54
|
+
'adwaita/avatar.blp': adwaitaAvatarTree,
|
|
55
|
+
'adwaita/banner.blp': adwaitaBannerTree,
|
|
56
|
+
'adwaita/bottom-sheet.blp': adwaitaBottomSheetTree,
|
|
57
|
+
'adwaita/button-content.blp': adwaitaButtonContentTree,
|
|
58
|
+
'adwaita/button-row.blp': adwaitaButtonRowTree,
|
|
59
|
+
'adwaita/carousel.blp': adwaitaCarouselTree,
|
|
60
|
+
'adwaita/clamp.blp': clampTree,
|
|
61
|
+
'adwaita/combo-row.blp': adwaitaComboRowTree,
|
|
62
|
+
'adwaita/entry-row.blp': adwaitaEntryRowTree,
|
|
63
|
+
'adwaita/expander-row.blp': adwaitaExpanderRowTree,
|
|
64
|
+
'adwaita/header-bar.blp': adwaitaHeaderBarTree,
|
|
65
|
+
'adwaita/navigation-split-view.blp': adwaitaNavigationSplitViewTree,
|
|
66
|
+
'adwaita/navigation-view.blp': adwaitaNavigationViewTree,
|
|
67
|
+
'adwaita/overlay-split-view.blp': adwaitaOverlaySplitViewTree,
|
|
68
|
+
'adwaita/password-entry-row.blp': adwaitaPasswordEntryRowTree,
|
|
69
|
+
'adwaita/preferences-group.blp': adwaitaPreferencesGroupTree,
|
|
70
|
+
'adwaita/shortcut-label.blp': adwaitaShortcutLabelTree,
|
|
71
|
+
'adwaita/sidebar.blp': adwaitaSidebarTree,
|
|
72
|
+
'adwaita/spin-row.blp': adwaitaSpinRowTree,
|
|
73
|
+
'adwaita/split-button.blp': adwaitaSplitButtonTree,
|
|
74
|
+
'adwaita/status-page.blp': adwaitaStatusPageTree,
|
|
75
|
+
'adwaita/switch-row.blp': adwaitaSwitchRowTree,
|
|
76
|
+
'adwaita/toggle-group.blp': adwaitaToggleGroupTree,
|
|
77
|
+
'adwaita/toolbar-view.blp': adwaitaToolbarViewTree,
|
|
78
|
+
'adwaita/view-switcher-bar.blp': adwaitaViewSwitcherBarTree,
|
|
79
|
+
'adwaita/window-title.blp': adwaitaWindowTitleTree,
|
|
80
|
+
'adwaita/wrap-box.blp': adwaitaWrapBoxTree,
|
|
81
|
+
'gtk/button.blp': gtkButtonTree,
|
|
82
|
+
'gtk/menu-button.blp': gtkMenuButtonTree,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* An attribute's value as the comparison reads it. `style` is read as the declarations it
|
|
87
|
+
* sets, one longhand each and sorted: which of the builder's margin and an element's own
|
|
88
|
+
* `flex-direction` was written first, and whether the serialiser folds two margins into a
|
|
89
|
+
* `margin-inline`, is text neither side chose and no style resolves differently for.
|
|
90
|
+
*/
|
|
91
|
+
const attributeText = (el: Element, attribute: Attr): string => {
|
|
92
|
+
if (attribute.name !== 'style') return attribute.value;
|
|
93
|
+
const style = (el as HTMLElement).style;
|
|
94
|
+
return [...style]
|
|
95
|
+
.map((property) => `${property}: ${style.getPropertyValue(property)}`)
|
|
96
|
+
.sort()
|
|
97
|
+
.join('; ');
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* An element tree as text a comparison can hold: the tag, the attributes SORTED (their order is
|
|
102
|
+
* the one thing markup and a builder may legitimately disagree on, and no element reads it), and
|
|
103
|
+
* the children, with the indentation between elements dropped since the builder never makes any.
|
|
104
|
+
*/
|
|
105
|
+
const canonical = (el: Element, depth = 0): string => {
|
|
106
|
+
const attributes = [...el.attributes].map((a) => `${a.name}=${JSON.stringify(attributeText(el, a))}`).sort();
|
|
107
|
+
const pad = ' '.repeat(depth);
|
|
108
|
+
const lines = [`${pad}<${el.localName}${attributes.map((a) => ` ${a}`).join('')}>`];
|
|
109
|
+
for (const node of el.childNodes) {
|
|
110
|
+
if (node.nodeType === Node.ELEMENT_NODE) lines.push(canonical(node as Element, depth + 1));
|
|
111
|
+
else if (node.nodeType === Node.TEXT_NODE && node.textContent?.trim() !== '') {
|
|
112
|
+
lines.push(`${pad} ${JSON.stringify(node.textContent)}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return lines.join('\n');
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const withIds = (node: SharedTreeNode, into: SharedTreeNode[] = []): SharedTreeNode[] => {
|
|
119
|
+
if (node.id !== undefined) into.push(node);
|
|
120
|
+
for (const child of node.children ?? []) withIds(child, into);
|
|
121
|
+
return into;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const AdwBlueprintMarkupTest = async () => {
|
|
125
|
+
await describe('adwaita-web: the gallery’s markup, parsed', async () => {
|
|
126
|
+
for (const [file, tree] of Object.entries(GALLERY_BLUEPRINTS)) {
|
|
127
|
+
// Both sides DETACHED and upgraded: a detached build runs each element's constructor
|
|
128
|
+
// and attribute callbacks (a label renders its text on `label=`), and so does parsing
|
|
129
|
+
// in this document. Neither is connected, so what differs is only what was authored.
|
|
130
|
+
await it(`${file}: parses to the element tree buildSharedTree makes`, async () => {
|
|
131
|
+
const parsed = document.createRange().createContextualFragment(sharedTreeHtml(tree));
|
|
132
|
+
expect(canonical(parsed.firstElementChild!)).toBe(canonical(buildSharedTree(tree)));
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
await it(`${file}: mounted, every id reaches an upgraded element of its tag`, async () => {
|
|
136
|
+
const host = document.createElement('div');
|
|
137
|
+
host.innerHTML = sharedTreeHtml(tree);
|
|
138
|
+
document.body.append(host);
|
|
139
|
+
try {
|
|
140
|
+
const nodes = withIds(tree);
|
|
141
|
+
expect(nodes.length > 0).toBe(true);
|
|
142
|
+
for (const node of nodes) {
|
|
143
|
+
const found = host.querySelector(`#${node.id}`);
|
|
144
|
+
const tag = hostTagOf(node.tag);
|
|
145
|
+
expect(found?.localName).toBe(tag);
|
|
146
|
+
const defined = customElements.get(tag);
|
|
147
|
+
expect(defined !== undefined && found instanceof defined).toBe(true);
|
|
148
|
+
}
|
|
149
|
+
} finally {
|
|
150
|
+
host.remove();
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// ONE `.blp`, RENDERED BY THIS RENDERER — the acceptance case for ADR 0053 clause 1's second
|
|
2
|
+
// exit, taken all the way through a build.
|
|
3
|
+
//
|
|
4
|
+
// `./shared-trees.spec.ts` drives ADR 0051's corpus, which is authored as `SharedTreeNode`
|
|
5
|
+
// literals in `scripts/adwaita-gallery-shared-trees.mjs`. This file drives the OTHER reader of
|
|
6
|
+
// the same shape: a Blueprint file, parsed and projected by `@gjsify/blueprint` and handed over
|
|
7
|
+
// by `@gjsify/vite-plugin-blueprint`'s `?shared-tree` exit at build time. So what is under test
|
|
8
|
+
// is not the builder — that is the sibling's job — it is the JOIN: that a `.blp` a GTK showcase
|
|
9
|
+
// actually ships arrives on this target as a tree this renderer mounts, with the fields ADRs
|
|
10
|
+
// 0066, 0067 and 0068 put on the node still on it — `template`/`object-id`, the translatable
|
|
11
|
+
// marking, and the style classes. Naming two of the three read as a complete list once 0068
|
|
12
|
+
// landed, which is how a reader learns a field is not carried when it is.
|
|
13
|
+
//
|
|
14
|
+
// WHY A SHIPPED FILE AND NOT A FIXTURE. A fixture written for this test would be written to
|
|
15
|
+
// pass it. `showcases/gtk/effect-adw-services/src/window.blp` is a real interface — an
|
|
16
|
+
// `Adw.ToolbarView` with a header bar, a clamp, three preference groups and named rows — and one
|
|
17
|
+
// of the shipped `.blp` whose projection declares NO loss, so nothing about it is refused before
|
|
18
|
+
// it can be rendered. The reach across the tree is the shape the NativeScript driver already uses
|
|
19
|
+
// for the corpus module (`packages/nativescript-bridge/adwaita/src/shared-trees.spec.ts`), for
|
|
20
|
+
// the same reason: the thing under test lives outside the package that renders it.
|
|
21
|
+
//
|
|
22
|
+
// WHAT THIS MEASURED THAT NOTHING HAD, AND WHAT CLOSED IT: SLOT PLACEMENT WAS NOT CARRIED, AND
|
|
23
|
+
// IT COST CAPTIONS. `SharedTreeNode.slot` was on the node shape and read by none of the three
|
|
24
|
+
// tree builders. That went unnoticed because ADR 0051's seven-block corpus authors zero slots —
|
|
25
|
+
// a `.blp` is the first source that authors any, and this one authors four. The two cases below
|
|
26
|
+
// were `it.failing` markers for exactly this gap — the header bar authored `[top]` landing in the
|
|
27
|
+
// toolbar view's CONTENT instead, and the `title-widget:` window title then discarded by
|
|
28
|
+
// `adw-header-bar`'s own build, which derives a title element of its own when its centre is
|
|
29
|
+
// empty — until the builders were given a slot reader (`shared-tree-builder.ts`,
|
|
30
|
+
// `adw-header-bar.ts`, `adw-toolbar-view.ts`); both are plain `it()` now that placement holds.
|
|
31
|
+
//
|
|
32
|
+
// Giving `slot` a reader is a SHARED-VOCABULARY decision and not wiring: `content:`, `[top]` and
|
|
33
|
+
// `title-widget:` are GtkBuilder's names, this renderer's are the unnamed slot, `top` and
|
|
34
|
+
// `center`, and a table mapping one to the other inside one renderer is the per-surface
|
|
35
|
+
// translator ADR 0051 § Alternatives rejected turned down on a measurement. Tracked in
|
|
36
|
+
// `status/open-todos.md`.
|
|
37
|
+
|
|
38
|
+
import { describe, expect, it } from '@gjsify/unit';
|
|
39
|
+
|
|
40
|
+
import { hostTagOf } from '@gjsify/adwaita-core/tags';
|
|
41
|
+
|
|
42
|
+
import { mountSharedTree } from './shared-tree-builder.js';
|
|
43
|
+
|
|
44
|
+
// The build seam itself. `?shared-tree` is the projection; the same file without the query is
|
|
45
|
+
// the GtkBuilder XML the GTK showcase compiles, and neither import moves the other.
|
|
46
|
+
import windowTree from '../../../../showcases/gtk/effect-adw-services/src/window.blp?shared-tree';
|
|
47
|
+
|
|
48
|
+
type Node = typeof windowTree;
|
|
49
|
+
|
|
50
|
+
/** Every tag the authored tree names, in document order, so nothing is transcribed. */
|
|
51
|
+
const authoredTags = (node: Node, into: string[] = []): string[] => {
|
|
52
|
+
into.push(node.tag);
|
|
53
|
+
for (const child of node.children ?? []) authoredTags(child, into);
|
|
54
|
+
return into;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** Every prop value the source marked `_()`, read off the tree rather than off the `.blp`. */
|
|
58
|
+
const markedCaptions = (node: Node, into: string[] = []): string[] => {
|
|
59
|
+
for (const prop of Object.keys(node.translatable ?? {})) into.push(String(node.props?.[prop]));
|
|
60
|
+
for (const child of node.children ?? []) markedCaptions(child, into);
|
|
61
|
+
return into;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const AdwBlueprintTreeTest = async () => {
|
|
65
|
+
await describe('adwaita-web: a shipped .blp, mounted', async () => {
|
|
66
|
+
await it('arrives as a projected node with its composite class and markings', async () => {
|
|
67
|
+
// The three fields ADRs 0066 and 0067 added are what make a `.blp` worth reading at
|
|
68
|
+
// all: without them the root of every shipped file projected to its PARENT type and
|
|
69
|
+
// said nothing about the class the file is about.
|
|
70
|
+
expect(windowTree.tag).toBe('AdwApplicationWindow');
|
|
71
|
+
expect(windowTree.template).toBe('EffectServicesWindow');
|
|
72
|
+
expect(windowTree.props?.['default-width']).toBe(620);
|
|
73
|
+
expect(windowTree.translatable?.title !== undefined).toBe(true);
|
|
74
|
+
// Seven of the fourteen nodes are placed by slot, which is what makes this file the
|
|
75
|
+
// probe the corpus could not be.
|
|
76
|
+
expect(authoredTags(windowTree).length).toBe(14);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await it('mounts the content subtree exactly as authored', async () => {
|
|
80
|
+
// FROM THE SCROLLED WINDOW DOWN, and the bound is the measurement rather than
|
|
81
|
+
// convenience: everything the `.blp` places at `content:` on the toolbar view is
|
|
82
|
+
// reached through the UNNAMED slot, which is the one placement this renderer and
|
|
83
|
+
// GtkBuilder spell the same way. Asserting the whole tree here would PASS and prove
|
|
84
|
+
// nothing — `adw-header-bar` derives a title element of its own, so the filtered
|
|
85
|
+
// sequence has one `adw-window-title` either way and the authored one can vanish
|
|
86
|
+
// with the count intact. That node is held by the caption case below instead.
|
|
87
|
+
const { root, unmount } = mountSharedTree(windowTree);
|
|
88
|
+
try {
|
|
89
|
+
const authored = new Set(authoredTags(windowTree).map(hostTagOf));
|
|
90
|
+
const realised = [...root.querySelectorAll('*')]
|
|
91
|
+
.map((el) => el.localName)
|
|
92
|
+
.filter((name) => authored.has(name));
|
|
93
|
+
const from = realised.indexOf('gtk-scrolled-window');
|
|
94
|
+
expect(from >= 0).toBe(true);
|
|
95
|
+
expect(realised.slice(from).join(' ')).toBe(
|
|
96
|
+
'gtk-scrolled-window adw-clamp gtk-box adw-preferences-group adw-entry-row ' +
|
|
97
|
+
'adw-preferences-group adw-action-row adw-preferences-group adw-action-row adw-action-row',
|
|
98
|
+
);
|
|
99
|
+
} finally {
|
|
100
|
+
unmount();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await it('upgrades the custom elements it mounted, and carries the authored props', async () => {
|
|
105
|
+
const { root, unmount } = mountSharedTree(windowTree);
|
|
106
|
+
try {
|
|
107
|
+
// UPGRADED, not merely created: an element this package defines is an instance
|
|
108
|
+
// of its own class once connected. An unknown tag would be a plain HTMLElement,
|
|
109
|
+
// and that is what tells the two apart.
|
|
110
|
+
const defined = customElements.get('adw-preferences-group');
|
|
111
|
+
expect(defined !== undefined).toBe(true);
|
|
112
|
+
expect(root.querySelector('adw-preferences-group') instanceof defined!).toBe(true);
|
|
113
|
+
|
|
114
|
+
// An authored property, through `attributeOf`'s case rule and nothing else.
|
|
115
|
+
expect(root.getAttribute('default-width')).toBe('620');
|
|
116
|
+
expect(root.querySelector('adw-clamp')?.getAttribute('maximum-size')).toBe('560');
|
|
117
|
+
|
|
118
|
+
// And a caption that reached the SCREEN, not just an attribute: the element read
|
|
119
|
+
// it on connect and wrote it into its own label.
|
|
120
|
+
expect(root.querySelector('adw-preferences-group .adw-preferences-group-title')?.textContent).toBe(
|
|
121
|
+
'Read a directory',
|
|
122
|
+
);
|
|
123
|
+
expect(root.querySelector('adw-action-row .adw-row-subtitle')?.textContent).toBe('Type a path above.');
|
|
124
|
+
} finally {
|
|
125
|
+
unmount();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
await it('places a [top] child in the bar the .blp named', async () => {
|
|
130
|
+
const { root, unmount } = mountSharedTree(windowTree);
|
|
131
|
+
try {
|
|
132
|
+
const headerBar = root.querySelector('adw-header-bar');
|
|
133
|
+
expect(headerBar?.parentElement?.className).toBe('adw-toolbar-view-top');
|
|
134
|
+
} finally {
|
|
135
|
+
unmount();
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// The TypeScript beside a `.blp` addresses a widget by its Blueprint id, as GTK's
|
|
140
|
+
// `InternalChildren` does. An id the builder dropped would leave that code nothing to find.
|
|
141
|
+
await it('reaches a widget by the id the .blp gave it', async () => {
|
|
142
|
+
const { root, unmount } = mountSharedTree(windowTree);
|
|
143
|
+
try {
|
|
144
|
+
expect(root.querySelector('#pathRow')?.localName).toBe('adw-entry-row');
|
|
145
|
+
} finally {
|
|
146
|
+
unmount();
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// This `.blp` authors no `styles [ ]`, so the tree is written out here.
|
|
151
|
+
await it('carries the style classes a tree authors', async () => {
|
|
152
|
+
const { root, unmount } = mountSharedTree({ tag: 'AdwClamp', styleClasses: ['card', 'dim-label'] });
|
|
153
|
+
try {
|
|
154
|
+
expect([...root.classList].filter((name) => name === 'card' || name === 'dim-label')).toStrictEqual([
|
|
155
|
+
'card',
|
|
156
|
+
'dim-label',
|
|
157
|
+
]);
|
|
158
|
+
} finally {
|
|
159
|
+
unmount();
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await it('renders every caption the .blp marks for translation', async () => {
|
|
164
|
+
const { root, unmount } = mountSharedTree(windowTree);
|
|
165
|
+
try {
|
|
166
|
+
const text = root.textContent ?? '';
|
|
167
|
+
const missing = markedCaptions(windowTree).filter((caption) => !text.includes(caption));
|
|
168
|
+
expect(missing.join(' | ')).toBe('');
|
|
169
|
+
} finally {
|
|
170
|
+
unmount();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
};
|
package/src/bottom-sheet.spec.ts
CHANGED
|
@@ -15,11 +15,14 @@ import { describe, expect, it } from '@gjsify/unit';
|
|
|
15
15
|
|
|
16
16
|
import type { AdwBottomSheet } from './elements/adw-bottom-sheet.js';
|
|
17
17
|
import {
|
|
18
|
+
BOTTOM_SHEET_BOTTOM_BAR_VECTORS,
|
|
18
19
|
BOTTOM_SHEET_CLOSE_VECTORS,
|
|
20
|
+
BOTTOM_SHEET_OPEN_VECTORS,
|
|
19
21
|
BOTTOM_SHEET_PRESENTATION_VECTORS,
|
|
22
|
+
runBottomSheetBottomBarSteps,
|
|
20
23
|
runBottomSheetSteps,
|
|
21
24
|
} from '@gjsify/adwaita-core/conformance';
|
|
22
|
-
import type { BottomSheetCloseOutcome } from '@gjsify/adwaita-core';
|
|
25
|
+
import type { BottomSheetChrome, BottomSheetCloseOutcome } from '@gjsify/adwaita-core';
|
|
23
26
|
|
|
24
27
|
/** A mounted sheet with one child in each slot, ready to be driven. */
|
|
25
28
|
function mountSheet(markup = ''): AdwBottomSheet {
|
|
@@ -44,6 +47,34 @@ function insideSheet(sheet: AdwBottomSheet): HTMLElement {
|
|
|
44
47
|
return sheet.querySelector('.adw-bottom-sheet-sheet-body > button') as HTMLElement;
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
function bottomBar(sheet: AdwBottomSheet): HTMLButtonElement {
|
|
51
|
+
return sheet.querySelector('.adw-bottom-sheet-bottom-bar') as HTMLButtonElement;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A bar child, the shape an app declares: one label inside the bin. */
|
|
55
|
+
function barLabel(text = 'Help'): HTMLElement {
|
|
56
|
+
const label = document.createElement('span');
|
|
57
|
+
label.textContent = text;
|
|
58
|
+
return label;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The chrome, read back off the REAL DOM rather than off the state that produced it.
|
|
63
|
+
*
|
|
64
|
+
* `surfaceVisible` has no element of its own: the sheet bin's on-screen-ness is a CSS
|
|
65
|
+
* transform here (`gtk_widget_set_child_visible` has no DOM counterpart that keeps the
|
|
66
|
+
* slide), and the two classes that drive that transform are what it is readable from.
|
|
67
|
+
*/
|
|
68
|
+
function chromeOf(sheet: AdwBottomSheet): BottomSheetChrome {
|
|
69
|
+
const bar = bottomBar(sheet);
|
|
70
|
+
return {
|
|
71
|
+
layer: bar.hidden ? 'sheet' : 'bottom-bar',
|
|
72
|
+
surfaceVisible: sheet.classList.contains('open') || sheet.classList.contains('showing-bottom-bar'),
|
|
73
|
+
bottomBarInert: bar.classList.contains('inert'),
|
|
74
|
+
dimmed: dimming(sheet).classList.contains('visible'),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
47
78
|
/** Count the dismissal signals a sheet raises while `run` executes. */
|
|
48
79
|
function recordSignals(sheet: AdwBottomSheet, run: () => void): { attempts: number; delegated: number } {
|
|
49
80
|
let attempts = 0;
|
|
@@ -115,6 +146,140 @@ export const AdwBottomSheetTest = async () => {
|
|
|
115
146
|
}
|
|
116
147
|
});
|
|
117
148
|
|
|
149
|
+
await describe('adw-bottom-sheet open gate (libadwaita conformance vectors)', async () => {
|
|
150
|
+
for (const vector of BOTTOM_SHEET_OPEN_VECTORS) {
|
|
151
|
+
const { source, open, canOpen, hasBottomBar, revealBottomBar, outcome, rule } = vector;
|
|
152
|
+
const gates = `canOpen=${canOpen} · bar=${hasBottomBar} · revealed=${revealBottomBar}`;
|
|
153
|
+
const label = `${source} · open=${open} · ${gates}`;
|
|
154
|
+
await it(`${label} → ${outcome} — ${rule}`, () => {
|
|
155
|
+
const sheet = mountSheet();
|
|
156
|
+
sheet.setBottomBar(hasBottomBar ? barLabel() : null);
|
|
157
|
+
sheet.canOpen = canOpen;
|
|
158
|
+
sheet.revealBottomBar = revealBottomBar;
|
|
159
|
+
sheet.open = open;
|
|
160
|
+
|
|
161
|
+
expect(sheet.requestOpen(source)).toBe(outcome);
|
|
162
|
+
// Returning the right word and opening anyway is still wrong.
|
|
163
|
+
expect(sheet.open).toBe(outcome === 'open' ? true : open);
|
|
164
|
+
sheet.remove();
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
await describe('adw-bottom-sheet bottom bar (libadwaita conformance vectors)', async () => {
|
|
170
|
+
for (const vector of BOTTOM_SHEET_BOTTOM_BAR_VECTORS) {
|
|
171
|
+
await it(vector.rule, () => {
|
|
172
|
+
const sheet = mountSheet();
|
|
173
|
+
const notifications: boolean[] = [];
|
|
174
|
+
sheet.addEventListener('notify::open', (event) => {
|
|
175
|
+
notifications.push((event as CustomEvent).detail.open);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const outcomes = runBottomSheetBottomBarSteps(
|
|
179
|
+
{
|
|
180
|
+
setBottomBar: (present) => {
|
|
181
|
+
sheet.setBottomBar(present ? barLabel() : null);
|
|
182
|
+
},
|
|
183
|
+
setCanOpen: (canOpen) => {
|
|
184
|
+
sheet.canOpen = canOpen;
|
|
185
|
+
},
|
|
186
|
+
setRevealBottomBar: (reveal) => {
|
|
187
|
+
sheet.revealBottomBar = reveal;
|
|
188
|
+
},
|
|
189
|
+
setModal: (modal) => {
|
|
190
|
+
sheet.modal = modal;
|
|
191
|
+
},
|
|
192
|
+
setOpen: (open) => {
|
|
193
|
+
sheet.open = open;
|
|
194
|
+
},
|
|
195
|
+
requestOpen: (source) => sheet.requestOpen(source),
|
|
196
|
+
},
|
|
197
|
+
vector.steps,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
expect(outcomes).toStrictEqual([...vector.outcomes]);
|
|
201
|
+
expect(notifications).toStrictEqual([...vector.notifications]);
|
|
202
|
+
expect(sheet.open).toBe(vector.open);
|
|
203
|
+
expect(chromeOf(sheet)).toStrictEqual({ ...vector.chrome });
|
|
204
|
+
sheet.remove();
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
await describe('adw-bottom-sheet bottom-bar affordance', async () => {
|
|
210
|
+
await it('a real click on the bar opens the sheet', () => {
|
|
211
|
+
// The vectors above call `requestOpen` directly, which proves the GATE and
|
|
212
|
+
// not the wiring: an element that never listens for the click passes every
|
|
213
|
+
// one of them. This is the event path an app actually gets.
|
|
214
|
+
const sheet = mountSheet();
|
|
215
|
+
sheet.setBottomBar(barLabel());
|
|
216
|
+
expect(sheet.open).toBe(false);
|
|
217
|
+
bottomBar(sheet).dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
218
|
+
expect(sheet.open).toBe(true);
|
|
219
|
+
sheet.remove();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
await it('declares the bar in markup, and the slot wrapper leaves nothing behind', () => {
|
|
223
|
+
const sheet = mountSheet(
|
|
224
|
+
'<adw-bottom-sheet-content><button id="under">Under</button></adw-bottom-sheet-content>' +
|
|
225
|
+
'<adw-bottom-sheet-bottom-bar><span id="bar">Help</span></adw-bottom-sheet-bottom-bar>' +
|
|
226
|
+
'<adw-bottom-sheet-sheet><button id="inside">Inside</button></adw-bottom-sheet-sheet>',
|
|
227
|
+
);
|
|
228
|
+
expect(sheet.querySelector('adw-bottom-sheet-bottom-bar')).toBe(null);
|
|
229
|
+
expect((sheet.bottomBar as HTMLElement).id).toBe('bar');
|
|
230
|
+
expect(chromeOf(sheet).layer).toBe('bottom-bar');
|
|
231
|
+
bottomBar(sheet).dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
232
|
+
expect(sheet.open).toBe(true);
|
|
233
|
+
sheet.remove();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
await it('keeps the inert bar enabled and focusable', () => {
|
|
237
|
+
// `can-open` off adds a style class and nothing else
|
|
238
|
+
// (adw-bottom-sheet.c:2033-2036). `disabled` would remove the focus stop and
|
|
239
|
+
// silence the click at the DOM instead of at the gate, which is a different
|
|
240
|
+
// widget: the bar is still the thing a screen reader announces.
|
|
241
|
+
const sheet = mountSheet();
|
|
242
|
+
sheet.setBottomBar(barLabel());
|
|
243
|
+
sheet.canOpen = false;
|
|
244
|
+
const bar = bottomBar(sheet);
|
|
245
|
+
expect(bar.disabled).toBe(false);
|
|
246
|
+
expect(bar.classList.contains('inert')).toBe(true);
|
|
247
|
+
bar.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
248
|
+
expect(sheet.open).toBe(false);
|
|
249
|
+
sheet.remove();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
await it('shows the bar again after the sheet is dismissed', () => {
|
|
253
|
+
// The morph has to run BOTH ways. A port that switched the bin to the sheet
|
|
254
|
+
// page on open and never switched back leaves a sheet that can be opened
|
|
255
|
+
// exactly once, and every state assertion above still passes.
|
|
256
|
+
const sheet = mountSheet();
|
|
257
|
+
sheet.setBottomBar(barLabel());
|
|
258
|
+
bottomBar(sheet).dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
259
|
+
expect(chromeOf(sheet).layer).toBe('sheet');
|
|
260
|
+
dimming(sheet).dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
261
|
+
expect(sheet.open).toBe(false);
|
|
262
|
+
expect(chromeOf(sheet).layer).toBe('bottom-bar');
|
|
263
|
+
bottomBar(sheet).dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
264
|
+
expect(sheet.open).toBe(true);
|
|
265
|
+
sheet.remove();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
await it('a sheet with no bar has no way in but its owner', () => {
|
|
269
|
+
// REGRESSION PIN: this element shipped without a bottom bar at all, so an app
|
|
270
|
+
// whose only affordance is the bar had to reach past the widget and write
|
|
271
|
+
// `open` itself. Nothing on screen opens this one.
|
|
272
|
+
const sheet = mountSheet();
|
|
273
|
+
expect(bottomBar(sheet).hidden).toBe(true);
|
|
274
|
+
expect(sheet.requestOpen('bottom-bar')).toBe('ignored');
|
|
275
|
+
expect(sheet.requestOpen('swipe')).toBe('ignored');
|
|
276
|
+
expect(sheet.open).toBe(false);
|
|
277
|
+
sheet.open = true;
|
|
278
|
+
expect(sheet.open).toBe(true);
|
|
279
|
+
sheet.remove();
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
118
283
|
await describe('adw-bottom-sheet open attribute semantics', async () => {
|
|
119
284
|
await it('open="false" renders CLOSED', () => {
|
|
120
285
|
// `hasAttribute('open')` for the getter while every other boolean uses
|
package/src/chrome.spec.ts
CHANGED
|
@@ -503,12 +503,15 @@ export const AdwChromeTest = async () => {
|
|
|
503
503
|
});
|
|
504
504
|
|
|
505
505
|
await it('centres a status page in the pane rather than against its top bar', async () => {
|
|
506
|
-
|
|
506
|
+
// 400px, not the preview's 340: at upstream's metrics this page needs 298px, and
|
|
507
|
+
// in the 293px a 340px view leaves under its bar GTK would scroll it rather than
|
|
508
|
+
// centre it.
|
|
509
|
+
const host = mountPreview(STATUS_PANE_PREVIEW, 400);
|
|
507
510
|
await settle();
|
|
508
511
|
const area = host.querySelector('.adw-toolbar-view-content') as HTMLElement;
|
|
509
512
|
const status = host.querySelector('adw-status-page') as HTMLElement;
|
|
510
513
|
|
|
511
|
-
expect(heightOf(area) > 0 && heightOf(area) <
|
|
514
|
+
expect(heightOf(area) > 0 && heightOf(area) < 400).toBe(true);
|
|
512
515
|
expect(heightOf(status)).toBe(heightOf(area));
|
|
513
516
|
|
|
514
517
|
// What the reader actually sees, and the discriminator at the same time:
|