zz-meridian 0.19.0 → 0.19.2
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/package.json +1 -1
- package/payload/CHANGELOG.md +12 -0
- package/payload/package.json +1 -1
- package/payload/scripts/interactions.ts +39 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zz-meridian",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.2",
|
|
4
4
|
"description": "Bring ZZ Meridian, a dashboard design system, into a Next.js project, or start a new dashboard on it. Copies the files in; nothing depends on this package at runtime.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/payload/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
Every release of ZZ Meridian, newest first. Versions follow semver: a removed or renamed token, prop or card is major; a new card, token or variant is minor; a corrected value is a patch. Each entry says what breaks and what to do instead.
|
|
4
4
|
|
|
5
|
+
## [0.19.2] · 2026-10-19
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **The presses suite finds a control again by what it is** ([#65](https://github.com/zhixuan312/zz-meridian/issues/65)). It tagged each control by its place in the walk and, after opening the route again, pressed whatever stood at that place: a later load that drew the controls in another order, or fewer of them for a while (a table still loading on a busy machine, rows another suite's write changed), pressed the wrong control or reported "was listed but could not be found again" for one that was on the page a moment later. A control is now found by its accessible name, and which of the controls named alike it is, and one not drawn yet is waited for up to the hang bound before it is called gone.
|
|
10
|
+
|
|
11
|
+
## [0.19.1] · 2026-10-19
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **The presses suite no longer crashes when a press replaces the document late** ([#64](https://github.com/zhixuan312/zz-meridian/issues/64)). A control whose answer reloads the page once a request is accepted, a team switch that has the server draw the new team's chrome, could replace the document after `scripts/interactions.ts` had looked for an answer; its next read of the page then found no helper and stopped the whole suite with `ReferenceError: deepQuery is not defined`. A document replaced after the look is a press that moved the page: the suite opens the route again and presses the next control, and the press is not reported as doing nothing.
|
|
16
|
+
|
|
5
17
|
## [0.19.0] · 2026-10-19
|
|
6
18
|
|
|
7
19
|
### Breaking
|
package/payload/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zz-meridian-template",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.2",
|
|
4
4
|
"private": true,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "ZZ Meridian: a dashboard design system and starter. Six layers, from DTCG tokens through four layers of React components to product workspaces, and a Design Atlas, in a light and a dark theme.",
|
|
@@ -43,24 +43,32 @@ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
|
43
43
|
const HANG_BOUND_MS = 5000;
|
|
44
44
|
|
|
45
45
|
/** Tag every visible, enabled, not-yet-chosen control with its index; return their names (with the hosts they sit in) and the page's links. */
|
|
46
|
-
|
|
46
|
+
/**
|
|
47
|
+
* The page's controls and how each is named: its accessible name and the roots it sits in. A control is found again by
|
|
48
|
+
* that name (and which of the controls with it), never by its place in the walk: a later load can draw the controls in
|
|
49
|
+
* another order or fewer of them for a while, a table still loading or rows a concurrent write changed (#65).
|
|
50
|
+
*/
|
|
51
|
+
const CONTROLS = `
|
|
47
52
|
const deepClosest = (el, sel) => { for (let n = el; n; n = deepParent(n)) if (n.matches(sel)) return n; return null; };
|
|
48
53
|
const vis = (el) => { const r = el.getBoundingClientRect(); const s = getComputedStyle(el); return r.width > 0 && r.height > 0 && s.visibility !== 'hidden' && !deepClosest(el, '[aria-hidden="true"],[inert],.sr-only'); };
|
|
49
|
-
const chosen = (el) => ['aria-pressed', 'aria-checked', 'aria-selected'].some((a) => el.getAttribute(a) === 'true') || el.getAttribute('aria-current') === 'page' || ['on', 'active', 'checked'].includes(el.getAttribute('data-state'));
|
|
50
54
|
const hosts = (el) => { const out = []; for (let r = el.getRootNode(); r instanceof ShadowRoot; r = r.host.getRootNode()) out.unshift(r.host.localName); return out; };
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
const
|
|
55
|
+
const nameOf = (b) => { const h = hosts(b); return (b.getAttribute('aria-label') || deepText(b) || b.title || '').trim().replace(/\\s+/g, ' ').slice(0, 60) + (h.length ? ' (in ' + h.join(' > ') + ')' : ''); };
|
|
56
|
+
// A control already chosen is not pressed: choosing it again changes nothing, which is not a dead control.
|
|
57
|
+
const chosen = (el) => ['aria-pressed', 'aria-checked', 'aria-selected'].some((a) => el.getAttribute(a) === 'true') || el.getAttribute('aria-current') === 'page' || ['on', 'active', 'checked'].includes(el.getAttribute('data-state'));
|
|
58
|
+
const pressable = (every) => every.filter((b) => b.matches('button, [role="button"], [role="tab"], [role="switch"], [role="checkbox"], [role="radio"], [role="menuitem"]'))
|
|
54
59
|
.filter((b) => !b.disabled && b.getAttribute('aria-disabled') !== 'true' && vis(b) && !chosen(b));
|
|
60
|
+
`;
|
|
61
|
+
const LIST = `(() => {${CONTROLS}
|
|
62
|
+
const every = deepAll(document.body);
|
|
55
63
|
return {
|
|
56
|
-
controls:
|
|
64
|
+
controls: pressable(every).map(nameOf),
|
|
57
65
|
links: every.filter((a) => a.matches('a[href]') && vis(a)).map((a) => a.getAttribute('href')),
|
|
58
66
|
};
|
|
59
67
|
})()`;
|
|
60
68
|
|
|
61
69
|
/** Scroll the control into view, check nothing covers it, and start counting what changes. */
|
|
62
|
-
const ARM = (
|
|
63
|
-
const el = deepAll(document.body).
|
|
70
|
+
const ARM = (name: string, nth: number) => `(() => {${CONTROLS}
|
|
71
|
+
const el = pressable(deepAll(document.body)).filter((b) => nameOf(b) === ${JSON.stringify(name)})[${nth}]; if (!el) return null;
|
|
64
72
|
el.scrollIntoView({ block: 'center', inline: 'nearest' });
|
|
65
73
|
const r = el.getBoundingClientRect(), x = r.left + r.width / 2, y = r.top + r.height / 2;
|
|
66
74
|
const hit = document.elementFromPoint(x, y);
|
|
@@ -76,7 +84,7 @@ const ARM = (i: number) => `(() => {
|
|
|
76
84
|
window.__mo = new MutationObserver((ms) => { for (const m of ms) {
|
|
77
85
|
if (tipOnly(m.target)) continue;
|
|
78
86
|
if (m.type === 'childList' && [...m.addedNodes, ...m.removedNodes].every(tipOnly)) continue;
|
|
79
|
-
if (m.type === 'attributes' && m.target === el && ['data-state', 'class', 'style', '
|
|
87
|
+
if (m.type === 'attributes' && m.target === el && ['data-state', 'class', 'style', 'aria-describedby'].includes(m.attributeName)) continue;
|
|
80
88
|
window.__changes++;
|
|
81
89
|
} });
|
|
82
90
|
setTimeout(() => roots().forEach((r) => window.__mo.observe(r, { subtree: true, childList: true, attributes: true, characterData: true })), 40);
|
|
@@ -125,9 +133,19 @@ for (const width of WIDTHS) {
|
|
|
125
133
|
if (status >= 400) failures.push(`${route}: link ${href}${path === href ? '' : ` (${path})`} answers ${status}`);
|
|
126
134
|
}
|
|
127
135
|
for (let i = 0; i < controls.length; i++) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
136
|
+
// Which of the controls named alike this is, in the order the first load listed them.
|
|
137
|
+
const nth = controls.slice(0, i).filter((c) => c === controls[i]).length;
|
|
138
|
+
type Armed = { x: number; y: number; covered?: string } | null;
|
|
139
|
+
// A document replaced since the last read has no helper: that is a page to open again, never a crash (#64).
|
|
140
|
+
const arm = () => page.eval<Armed>(ARM(controls[i], nth)).catch((): Armed => null);
|
|
141
|
+
let armed = await arm();
|
|
142
|
+
// Something left open or a toast in the way: start the page again and try once more. A control not drawn yet on the
|
|
143
|
+
// new load (a list still loading) is waited for up to the hang bound before it is called gone.
|
|
144
|
+
if (!armed || armed.covered) {
|
|
145
|
+
await fresh();
|
|
146
|
+
armed = await arm();
|
|
147
|
+
for (let waited = 0; !armed && waited < HANG_BOUND_MS; waited += 200) { await sleep(200); armed = await arm(); }
|
|
148
|
+
}
|
|
131
149
|
// Tagged by this suite a moment ago and gone after a fresh load: say so, never skip it.
|
|
132
150
|
if (!armed) { failures.push(`${route} @${width}: "${controls[i]}" was listed but could not be found again`); continue; }
|
|
133
151
|
if (armed.covered) { failures.push(`${route} @${width}: "${controls[i]}" is covered by ${armed.covered}`); continue; }
|
|
@@ -150,14 +168,15 @@ for (const width of WIDTHS) {
|
|
|
150
168
|
if (!chosen) r = await look().catch(() => ({ changes: 0, moved: true }));
|
|
151
169
|
}
|
|
152
170
|
if (chosen) continue;
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
171
|
+
if (r.moved) { await fresh(); continue; }
|
|
172
|
+
await page.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Escape', code: 'Escape', windowsVirtualKeyCode: 27 });
|
|
173
|
+
await page.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Escape', code: 'Escape', windowsVirtualKeyCode: 27 });
|
|
174
|
+
await sleep(200);
|
|
175
|
+
// A document replaced after the look closed (a team switch that reloads once its request is accepted) has lost the
|
|
176
|
+
// helper and the counters: the press moved the page, late, and the route is opened again (#64).
|
|
177
|
+
const after = await page.eval<{ replaced: boolean; open: boolean }>(`typeof deepQuery !== 'function' ? { replaced: true, open: false } : { replaced: location.href !== window.__href, open: !!deepQuery('[role="dialog"],[role="alertdialog"],[role="menu"],[role="listbox"]', document.body) }`).catch(() => ({ replaced: true, open: false }));
|
|
178
|
+
if (!r.changes && !after.replaced) failures.push(`${route} @${width}: "${controls[i]}" does nothing when ${touch ? 'tapped' : 'pressed'}`);
|
|
179
|
+
if (after.replaced || after.open) await fresh();
|
|
161
180
|
}
|
|
162
181
|
if (page.errors.length) failures.push(`${route} @${width}: ${page.errors[0]}`);
|
|
163
182
|
}
|