mosaic-headless 1.30.0 → 1.31.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/README.ja.md +4 -2
- package/README.ko.md +4 -2
- package/README.md +5 -3
- package/README.zh-TW.md +4 -2
- package/SKILL.md +83 -39
- package/assets/templates/platforms/claude-ai.json +1 -1
- package/assets/templates/platforms/claude-code.json +1 -1
- package/assets/templates/platforms/codex-cli.json +1 -1
- package/assets/templates/platforms/copilot.json +1 -1
- package/assets/templates/platforms/gemini-cli.json +1 -1
- package/data/browser-verification/moksa.csv +3986 -0
- package/data/browser-verification/my-account.csv +148 -0
- package/data/component-wall-verification.csv +1 -1
- package/data/condition-comparators.csv +2 -2
- package/data/content-loop-verification.csv +4 -1
- package/data/data-class-hierarchy.csv +1 -0
- package/data/dialog-form-verification.csv +2 -2
- package/data/dialog-verification.csv +1 -1
- package/data/form-surface-verification.csv +3 -0
- package/data/interaction-types.csv +1 -0
- package/data/intro-verification.csv +16 -16
- package/data/mobile-audit.csv +22 -19
- package/data/navigation-verification.csv +16 -13
- package/data/node-properties.csv +10 -5
- package/data/node-types.csv +1 -0
- package/data/node-verification.csv +3 -0
- package/data/placement-rules.csv +1 -0
- package/data/pluggables.csv +16 -4
- package/data/popover-verification.csv +38 -0
- package/data/style-states.csv +1 -0
- package/data/token-verification.csv +2 -2
- package/package.json +3 -1
- package/references/dialog-and-triggers.md +199 -8
- package/references/dynamic-content.md +19 -0
- package/references/failure-modes.md +36 -4
- package/references/styling.md +9 -0
- package/references/upgrading.md +46 -1
- package/scripts/check-release.mjs +41 -11
- package/sites/_dialogs.py +18 -8
- package/sites/_moksa.py +69 -39
- package/sites/_popovers.py +570 -0
- package/tools/audit_mobile.py +1 -1
- package/tools/extract_default_children.py +1 -1
- package/tools/extract_node_types.py +1 -1
- package/tools/sweep_node_types.py +55 -6
- package/tools/verify_browser.py +20 -8
- package/tools/verify_conversion.py +6 -2
- package/tools/verify_dialog.py +3 -2
- package/tools/verify_dialog_lab.py +11 -11
- package/tools/verify_intro.py +5 -3
- package/tools/verify_popovers.py +348 -0
- package/data/browser-verification.csv +0 -4133
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Drive every popover on the popover lab and record what actually happened.
|
|
3
|
+
|
|
4
|
+
python tools/verify_popovers.py --url https://site/popovers/ --csv data/popover-verification.csv
|
|
5
|
+
|
|
6
|
+
Mosaic 1.0.10's popover is a <div popover="manual"> the controller opens with
|
|
7
|
+
showPopover(), so none of what matters is in the markup: whether it opened, where
|
|
8
|
+
it landed relative to its anchor, whether it blocks the page, whether `closedby`
|
|
9
|
+
does what it says, whether `afterClose` chains to the next one, whether
|
|
10
|
+
`keepInView` flipped it and the flipped style state applied. Each of those is
|
|
11
|
+
measured here in Chromium.
|
|
12
|
+
|
|
13
|
+
The open state is read the way the runtime defines it - `:popover-open` - not from
|
|
14
|
+
an attribute: a popover never carries `open`.
|
|
15
|
+
|
|
16
|
+
Expected ids are the lab's (`sites/_popovers.py`); a missing one is recorded as
|
|
17
|
+
SKIPPED rather than failed, so the tool can run against a page that carries only
|
|
18
|
+
some of them.
|
|
19
|
+
"""
|
|
20
|
+
import argparse
|
|
21
|
+
import csv
|
|
22
|
+
import sys
|
|
23
|
+
import time
|
|
24
|
+
|
|
25
|
+
from playwright.sync_api import sync_playwright
|
|
26
|
+
|
|
27
|
+
ROWS = []
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def row(check, result, detail=""):
|
|
31
|
+
ROWS.append([check, result, detail])
|
|
32
|
+
print("%-30s %-7s %s" % (check, result, detail))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
IS_OPEN = "(id) => { const e = document.getElementById(id); return e ? e.matches(':popover-open') : null; }"
|
|
36
|
+
RECT = """(id) => { const e = document.getElementById(id); if (!e) return null;
|
|
37
|
+
const r = e.getBoundingClientRect();
|
|
38
|
+
return {x: r.x, y: r.y, w: r.width, h: r.height, r: r.right, b: r.bottom,
|
|
39
|
+
vw: document.documentElement.clientWidth, vh: innerHeight}; }"""
|
|
40
|
+
ATTRS = """(id) => { const e = document.getElementById(id); if (!e) return null;
|
|
41
|
+
const o = {}; for (const a of e.attributes) o[a.name] = a.value;
|
|
42
|
+
o.__tag = e.tagName.toLowerCase(); o.__display = getComputedStyle(e).display;
|
|
43
|
+
o.__bg = getComputedStyle(e).backgroundColor; return o; }"""
|
|
44
|
+
CLOSE_ALL = """() => document.querySelectorAll('.m-popover').forEach(p => {
|
|
45
|
+
if (p.matches(':popover-open')) {
|
|
46
|
+
const c = p.mosaicDialog;
|
|
47
|
+
c && c.requestClose ? c.requestClose() : p.hidePopover(); } })"""
|
|
48
|
+
OPEN_IDS = """() => [...document.querySelectorAll('.m-popover')]
|
|
49
|
+
.filter(p => p.matches(':popover-open')).map(p => p.id)"""
|
|
50
|
+
|
|
51
|
+
CELLS = [(v, h) for v in ("top", "center", "bottom") for h in ("left", "center", "right")]
|
|
52
|
+
EDGE = 16 # the lab insets screen popovers 12px from the viewport edge
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def main():
|
|
56
|
+
ap = argparse.ArgumentParser()
|
|
57
|
+
ap.add_argument("--url", required=True)
|
|
58
|
+
ap.add_argument("--csv")
|
|
59
|
+
a = ap.parse_args()
|
|
60
|
+
url = a.url + ("&" if "?" in a.url else "?") + "_v=%d" % int(time.time() * 1000)
|
|
61
|
+
|
|
62
|
+
with sync_playwright() as pw:
|
|
63
|
+
b = pw.chromium.launch()
|
|
64
|
+
ctx = b.new_context(viewport={"width": 1280, "height": 900})
|
|
65
|
+
p = ctx.new_page()
|
|
66
|
+
errors = []
|
|
67
|
+
p.on("pageerror", lambda e: errors.append(str(e)))
|
|
68
|
+
p.goto(url, wait_until="load")
|
|
69
|
+
p.wait_for_timeout(2000)
|
|
70
|
+
|
|
71
|
+
present = p.evaluate("() => [...document.querySelectorAll('.m-popover')].map(e => e.id)")
|
|
72
|
+
row("FOUND", "PASS" if present else "FAIL", "%d popovers on the page" % len(present))
|
|
73
|
+
if not present:
|
|
74
|
+
return finish(a, b)
|
|
75
|
+
|
|
76
|
+
# ---- the host
|
|
77
|
+
bad = []
|
|
78
|
+
for pid in present:
|
|
79
|
+
at = p.evaluate(ATTRS, pid)
|
|
80
|
+
if at["__tag"] != "div" or at.get("popover") != "manual" or "open" in at:
|
|
81
|
+
bad.append("%s <%s popover=%s open=%s>" % (pid, at["__tag"], at.get("popover"),
|
|
82
|
+
"open" in at))
|
|
83
|
+
row("HOST", "FAIL" if bad else "PASS",
|
|
84
|
+
"; ".join(bad) or "every one is <div popover=manual>, none carries `open`")
|
|
85
|
+
|
|
86
|
+
# ---- the notice opens itself on load, and does not block the page
|
|
87
|
+
if "pp-notice" in present:
|
|
88
|
+
opened = p.evaluate(IS_OPEN, "pp-notice")
|
|
89
|
+
row("NOTICE/pageLoad", "PASS" if opened else "FAIL",
|
|
90
|
+
"open after load" if opened else "not open after load")
|
|
91
|
+
at = p.evaluate(ATTRS, "pp-notice")
|
|
92
|
+
row("NOTICE/aria", "PASS" if at.get("role") == "status" and at.get("aria-live") == "polite"
|
|
93
|
+
and "aria-modal" not in at else "FAIL",
|
|
94
|
+
"role=%s aria-live=%s aria-modal=%s" % (at.get("role"), at.get("aria-live"),
|
|
95
|
+
at.get("aria-modal")))
|
|
96
|
+
if opened:
|
|
97
|
+
# what is under the middle of the viewport: the page, not a veil
|
|
98
|
+
top = p.evaluate("""() => { const e = document.elementFromPoint(innerWidth/2, innerHeight/2);
|
|
99
|
+
return e ? (e.closest('.m-popover') ? 'popover' : 'page') : null; }""")
|
|
100
|
+
y0 = p.evaluate("() => scrollY")
|
|
101
|
+
p.mouse.move(640, 450)
|
|
102
|
+
p.mouse.wheel(0, 600)
|
|
103
|
+
p.wait_for_timeout(500)
|
|
104
|
+
y1 = p.evaluate("() => scrollY")
|
|
105
|
+
row("NOTICE/non_blocking", "PASS" if top == "page" and y1 > y0 else "FAIL",
|
|
106
|
+
"centre hit-tests to the %s; wheel scrolled %d -> %d" % (top, y0, y1))
|
|
107
|
+
# a click elsewhere works while it is open, and does not close it
|
|
108
|
+
if "pp-menu" in present:
|
|
109
|
+
p.click("#pp-open-menu")
|
|
110
|
+
p.wait_for_timeout(400)
|
|
111
|
+
both = p.evaluate(OPEN_IDS)
|
|
112
|
+
row("NOTICE/page_usable", "PASS" if "pp-menu" in both and "pp-notice" in both
|
|
113
|
+
else "FAIL", "open while the notice shows: %s" % both)
|
|
114
|
+
p.keyboard.press("Escape")
|
|
115
|
+
p.wait_for_timeout(300)
|
|
116
|
+
still = p.evaluate(IS_OPEN, "pp-notice")
|
|
117
|
+
row("NOTICE/closedby_nothing", "PASS" if still else "FAIL",
|
|
118
|
+
"Esc left it open" if still else "Esc closed a closedby=nothing popover")
|
|
119
|
+
p.click("#pp-close-notice")
|
|
120
|
+
p.wait_for_timeout(400)
|
|
121
|
+
gone = not p.evaluate(IS_OPEN, "pp-notice")
|
|
122
|
+
row("NOTICE/close_button", "PASS" if gone else "FAIL",
|
|
123
|
+
"popoverClose closed it" if gone else "still open")
|
|
124
|
+
else:
|
|
125
|
+
row("NOTICE", "SKIPPED", "not on this page")
|
|
126
|
+
p.evaluate(CLOSE_ALL)
|
|
127
|
+
p.wait_for_timeout(300)
|
|
128
|
+
|
|
129
|
+
# ---- closed at rest: nothing else is showing, and closed means 0x0
|
|
130
|
+
shown = p.evaluate("""() => [...document.querySelectorAll('.m-popover')]
|
|
131
|
+
.filter(e => !e.matches(':popover-open'))
|
|
132
|
+
.filter(e => { const r = e.getBoundingClientRect(); return r.width || r.height; })
|
|
133
|
+
.map(e => e.id)""")
|
|
134
|
+
row("CLOSED_IS_HIDDEN", "FAIL" if shown else "PASS",
|
|
135
|
+
("still rendered: %s" % shown) if shown else "every closed popover measures 0x0")
|
|
136
|
+
|
|
137
|
+
# ---- the anchored menu: below its trigger, start-aligned, toggles, Esc
|
|
138
|
+
if "pp-menu" in present:
|
|
139
|
+
p.click("#pp-open-menu")
|
|
140
|
+
p.wait_for_timeout(400)
|
|
141
|
+
m, t = p.evaluate(RECT, "pp-menu"), p.evaluate(RECT, "pp-open-menu")
|
|
142
|
+
# block-end start, or block-start once keepInView flipped it for lack of
|
|
143
|
+
# room below - start-aligned with the trigger either way
|
|
144
|
+
flipped = "data-mosaic-popover-flipped" in p.evaluate(ATTRS, "pp-menu")
|
|
145
|
+
beside = (m["b"] <= t["y"] + 1) if flipped else (m["y"] >= t["b"] - 1)
|
|
146
|
+
ok = m and m["w"] and beside and abs(m["x"] - t["x"]) <= 2
|
|
147
|
+
row("MENU/placement", "PASS" if ok else "FAIL",
|
|
148
|
+
"%s: menu %d..%d high, trigger %d..%d, left edges %d / %d"
|
|
149
|
+
% ("flipped above" if flipped else "below", m["y"], m["b"], t["y"], t["b"],
|
|
150
|
+
m["x"], t["x"]) if m else "did not open")
|
|
151
|
+
p.click("#pp-open-menu")
|
|
152
|
+
p.wait_for_timeout(400)
|
|
153
|
+
row("MENU/toggle", "PASS" if not p.evaluate(IS_OPEN, "pp-menu") else "FAIL",
|
|
154
|
+
"the same button closed it")
|
|
155
|
+
p.click("#pp-open-menu")
|
|
156
|
+
p.wait_for_timeout(300)
|
|
157
|
+
p.keyboard.press("Escape")
|
|
158
|
+
p.wait_for_timeout(300)
|
|
159
|
+
row("MENU/esc", "PASS" if not p.evaluate(IS_OPEN, "pp-menu") else "FAIL",
|
|
160
|
+
"closedby=everything: Esc closed it")
|
|
161
|
+
p.evaluate(CLOSE_ALL)
|
|
162
|
+
|
|
163
|
+
# ---- the info card: inline-end, Esc does nothing, a click outside closes
|
|
164
|
+
if "pp-info" in present:
|
|
165
|
+
p.click("#pp-open-info")
|
|
166
|
+
p.wait_for_timeout(400)
|
|
167
|
+
m, t = p.evaluate(RECT, "pp-info"), p.evaluate(RECT, "pp-open-info")
|
|
168
|
+
flipped = "data-mosaic-popover-flipped" in p.evaluate(ATTRS, "pp-info")
|
|
169
|
+
# inline-end, or inline-start when keepInView flipped it - either way beside
|
|
170
|
+
# the trigger and vertically centred on it
|
|
171
|
+
beside = (m["r"] <= t["x"] + 1) if flipped else (m["x"] >= t["r"] - 1)
|
|
172
|
+
ok = m and beside and abs((m["y"] + m["h"] / 2) - (t["y"] + t["h"] / 2)) <= 3
|
|
173
|
+
row("INFO/placement", "PASS" if ok else "FAIL",
|
|
174
|
+
"%s: card %d..%d, trigger %d..%d; centres %d / %d"
|
|
175
|
+
% ("flipped to inline-start" if flipped else "inline-end", m["x"], m["r"],
|
|
176
|
+
t["x"], t["r"], m["y"] + m["h"] / 2, t["y"] + t["h"] / 2) if m else "did not open")
|
|
177
|
+
p.keyboard.press("Escape")
|
|
178
|
+
p.wait_for_timeout(300)
|
|
179
|
+
row("INFO/esc_ignored", "PASS" if p.evaluate(IS_OPEN, "pp-info") else "FAIL",
|
|
180
|
+
"closedby=clickOutside: Esc left it open")
|
|
181
|
+
p.mouse.click(20, 450)
|
|
182
|
+
p.wait_for_timeout(400)
|
|
183
|
+
row("INFO/click_outside", "PASS" if not p.evaluate(IS_OPEN, "pp-info") else "FAIL",
|
|
184
|
+
"a click outside closed it")
|
|
185
|
+
p.evaluate(CLOSE_ALL)
|
|
186
|
+
|
|
187
|
+
# ---- anchored to some other element than its trigger
|
|
188
|
+
if "pp-pointer" in present:
|
|
189
|
+
p.click("#pp-open-pointer")
|
|
190
|
+
p.wait_for_timeout(500)
|
|
191
|
+
m, card = p.evaluate(RECT, "pp-pointer"), p.evaluate(RECT, "pp-work-1")
|
|
192
|
+
ok = m and m["b"] <= card["y"] + 1 and abs((m["x"] + m["w"] / 2) - (card["x"] + card["w"] / 2)) <= 3
|
|
193
|
+
row("POINTER/anchor", "PASS" if ok else "FAIL",
|
|
194
|
+
"popover bottom %d vs card top %d; centres %d / %d"
|
|
195
|
+
% (m["b"], card["y"], m["x"] + m["w"] / 2, card["x"] + card["w"] / 2) if m else "did not open")
|
|
196
|
+
p.evaluate(CLOSE_ALL)
|
|
197
|
+
|
|
198
|
+
# ---- the screen grid: each cell where its name says
|
|
199
|
+
for v, h in CELLS:
|
|
200
|
+
pid = "pp-cell-%s-%s" % (v, h)
|
|
201
|
+
if pid not in present:
|
|
202
|
+
continue
|
|
203
|
+
p.click("#pp-open-cell-%s-%s" % (v, h))
|
|
204
|
+
p.wait_for_timeout(300)
|
|
205
|
+
r = p.evaluate(RECT, pid)
|
|
206
|
+
if not r or not r["w"]:
|
|
207
|
+
row("GRID/%s-%s" % (v, h), "FAIL", "did not open")
|
|
208
|
+
continue
|
|
209
|
+
# docked to its cell: within EDGE of the named edge, or centred on the axis
|
|
210
|
+
cx, cy = r["x"] + r["w"] / 2, r["y"] + r["h"] / 2
|
|
211
|
+
want_x = {"left": r["x"] <= EDGE, "right": r["r"] >= r["vw"] - EDGE,
|
|
212
|
+
"center": abs(cx - r["vw"] / 2) <= 2}[h]
|
|
213
|
+
want_y = {"top": r["y"] <= EDGE, "bottom": r["b"] >= r["vh"] - EDGE,
|
|
214
|
+
"center": abs(cy - r["vh"] / 2) <= 2}[v]
|
|
215
|
+
row("GRID/%s-%s" % (v, h), "PASS" if want_x and want_y else "FAIL",
|
|
216
|
+
"box %d,%d %dx%d in %dx%d" % (r["x"], r["y"], r["w"], r["h"], r["vw"], r["vh"]))
|
|
217
|
+
p.evaluate(CLOSE_ALL)
|
|
218
|
+
p.wait_for_timeout(200)
|
|
219
|
+
|
|
220
|
+
# ---- the tour: afterClose advances it, "end the tour" suspends it, Esc is ignored
|
|
221
|
+
if "pp-tour-brief" in present:
|
|
222
|
+
p.click("#pp-open-tour")
|
|
223
|
+
p.wait_for_timeout(500)
|
|
224
|
+
seen, under = [p.evaluate(OPEN_IDS)], []
|
|
225
|
+
for key in ("brief", "quote", "launch"):
|
|
226
|
+
m, box_ = p.evaluate(RECT, "pp-tour-" + key), p.evaluate(RECT, "pp-tour-box-" + key)
|
|
227
|
+
if m and m["w"]:
|
|
228
|
+
under.append(m["y"] >= box_["b"] - 1 or m["b"] <= box_["y"] + 1)
|
|
229
|
+
p.click("#pp-tour-next-" + key)
|
|
230
|
+
p.wait_for_timeout(800)
|
|
231
|
+
seen.append(p.evaluate(OPEN_IDS))
|
|
232
|
+
want = [["pp-tour-brief"], ["pp-tour-quote"], ["pp-tour-launch"], ["pp-tour-done"]]
|
|
233
|
+
row("TOUR/steps", "PASS" if seen == want else "FAIL",
|
|
234
|
+
" > ".join("+".join(s) or "none" for s in seen))
|
|
235
|
+
row("TOUR/anchored", "PASS" if under and all(under) else "FAIL",
|
|
236
|
+
"%d of 3 steps sit beside the box they explain" % sum(under))
|
|
237
|
+
p.evaluate(CLOSE_ALL)
|
|
238
|
+
p.wait_for_timeout(400)
|
|
239
|
+
# "end the tour" on a middle step suspends that step's afterClose before
|
|
240
|
+
# closing it, so nothing follows; Esc is not a way out (it would fire
|
|
241
|
+
# afterClose too), so a step ignores it
|
|
242
|
+
p.click("#pp-open-tour")
|
|
243
|
+
p.wait_for_timeout(500)
|
|
244
|
+
p.click("#pp-tour-next-brief")
|
|
245
|
+
p.wait_for_timeout(800)
|
|
246
|
+
p.keyboard.press("Escape")
|
|
247
|
+
p.wait_for_timeout(500)
|
|
248
|
+
esc = p.evaluate(OPEN_IDS)
|
|
249
|
+
row("TOUR/esc_ignored", "PASS" if esc == ["pp-tour-quote"] else "FAIL",
|
|
250
|
+
"open after Esc on step 2: %s" % ("+".join(esc) or "none"))
|
|
251
|
+
p.click("#pp-tour-quit-quote")
|
|
252
|
+
p.wait_for_timeout(800)
|
|
253
|
+
after = p.evaluate(OPEN_IDS)
|
|
254
|
+
row("TOUR/quit_ends", "PASS" if not after else "FAIL",
|
|
255
|
+
"open after 結束導覽 on step 2: %s" % ("+".join(after) or "none"))
|
|
256
|
+
# and the suspended step is resumed by the next start
|
|
257
|
+
p.click("#pp-open-tour")
|
|
258
|
+
p.wait_for_timeout(500)
|
|
259
|
+
p.click("#pp-tour-next-brief")
|
|
260
|
+
p.wait_for_timeout(800)
|
|
261
|
+
p.click("#pp-tour-next-quote")
|
|
262
|
+
p.wait_for_timeout(800)
|
|
263
|
+
again = p.evaluate(OPEN_IDS)
|
|
264
|
+
row("TOUR/restart_resumes", "PASS" if again == ["pp-tour-launch"] else "FAIL",
|
|
265
|
+
"restarted, two steps on: %s" % ("+".join(again) or "none"))
|
|
266
|
+
p.evaluate(CLOSE_ALL)
|
|
267
|
+
p.wait_for_timeout(1000)
|
|
268
|
+
p.evaluate(CLOSE_ALL)
|
|
269
|
+
|
|
270
|
+
# ---- keepInView: flips, and the flipped state styles it
|
|
271
|
+
if "pp-snap-on" in present:
|
|
272
|
+
p.click("#pp-open-snap-on")
|
|
273
|
+
p.wait_for_timeout(500)
|
|
274
|
+
at, m, t = (p.evaluate(ATTRS, "pp-snap-on"), p.evaluate(RECT, "pp-snap-on"),
|
|
275
|
+
p.evaluate(RECT, "pp-open-snap-on"))
|
|
276
|
+
flipped = "data-mosaic-popover-flipped" in at
|
|
277
|
+
row("SNAP/flips", "PASS" if flipped and m["r"] <= t["x"] + 1 else "FAIL",
|
|
278
|
+
"flipped=%s; popover right %d vs trigger left %d" % (flipped, m["r"], t["x"]))
|
|
279
|
+
row("SNAP/flipped_state", "PASS" if at["__bg"] in ("rgb(22, 24, 28)",) else "FAIL",
|
|
280
|
+
"background while flipped: %s" % at["__bg"])
|
|
281
|
+
p.evaluate(CLOSE_ALL)
|
|
282
|
+
p.wait_for_timeout(300)
|
|
283
|
+
p.click("#pp-open-snap-off")
|
|
284
|
+
p.wait_for_timeout(500)
|
|
285
|
+
at, m = p.evaluate(ATTRS, "pp-snap-off"), p.evaluate(RECT, "pp-snap-off")
|
|
286
|
+
t = p.evaluate(RECT, "pp-open-snap-off")
|
|
287
|
+
# Measured, and not what the name suggests: without keepInView the popover
|
|
288
|
+
# does not overflow. The browser shifts it back inside the viewport, onto
|
|
289
|
+
# its own anchor.
|
|
290
|
+
row("SNAP/off_shifted", "PASS" if "data-mosaic-popover-flipped" not in at
|
|
291
|
+
and m["r"] <= m["vw"] + 1 and m["x"] < t["r"] else "FAIL",
|
|
292
|
+
"keepInView=0, not flipped: popover %d..%d inside a %d viewport, over its trigger %d..%d"
|
|
293
|
+
% (m["x"], m["r"], m["vw"], t["x"], t["r"]))
|
|
294
|
+
p.evaluate(CLOSE_ALL)
|
|
295
|
+
|
|
296
|
+
# ---- the flex modal: display:flex on the host, still hidden when closed
|
|
297
|
+
if p.evaluate("() => !!document.getElementById('pp-flexmodal')"):
|
|
298
|
+
st = p.evaluate("""() => { const e = document.getElementById('pp-flexmodal');
|
|
299
|
+
const r = e.getBoundingClientRect(); return [getComputedStyle(e).display, r.width, r.height]; }""")
|
|
300
|
+
row("FLEXMODAL/closed", "PASS" if st[0] == "none" and not st[1] else "FAIL",
|
|
301
|
+
"authored display:flex, closed it computes display:%s %dx%d" % tuple(st))
|
|
302
|
+
p.click("#pp-open-flexmodal")
|
|
303
|
+
p.wait_for_timeout(500)
|
|
304
|
+
st = p.evaluate("""() => { const e = document.getElementById('pp-flexmodal');
|
|
305
|
+
const w = document.getElementById('pp-flexmodal-win').getBoundingClientRect();
|
|
306
|
+
return [getComputedStyle(e).display, e.matches(':popover-open'),
|
|
307
|
+
Math.round(w.x + w.width / 2), Math.round(w.y + w.height / 2),
|
|
308
|
+
innerWidth, innerHeight]; }""")
|
|
309
|
+
centred = abs(st[2] - st[4] / 2) <= 10 and abs(st[3] - st[5] / 2) <= 10
|
|
310
|
+
row("FLEXMODAL/open", "PASS" if st[0] == "flex" and st[1] and centred else "FAIL",
|
|
311
|
+
"display:%s top-layer=%s window centre %d,%d in %dx%d" % tuple(st))
|
|
312
|
+
p.keyboard.press("Escape")
|
|
313
|
+
p.wait_for_timeout(400)
|
|
314
|
+
else:
|
|
315
|
+
row("FLEXMODAL", "SKIPPED", "not on this page")
|
|
316
|
+
|
|
317
|
+
row("NO_JS_ERRORS", "FAIL" if errors else "PASS", "; ".join(errors[:3]) or "none")
|
|
318
|
+
|
|
319
|
+
# ---- a phone: the anchored menu still fits on screen
|
|
320
|
+
if "pp-menu" in present:
|
|
321
|
+
mp = b.new_page(viewport={"width": 390, "height": 844})
|
|
322
|
+
mp.goto(url, wait_until="load")
|
|
323
|
+
mp.wait_for_timeout(1500)
|
|
324
|
+
mp.evaluate(CLOSE_ALL)
|
|
325
|
+
mp.click("#pp-open-menu")
|
|
326
|
+
mp.wait_for_timeout(400)
|
|
327
|
+
r = mp.evaluate(RECT, "pp-menu")
|
|
328
|
+
row("PHONE/menu_fits", "PASS" if r and r["x"] >= 0 and r["r"] <= r["vw"] + 1 else "FAIL",
|
|
329
|
+
"menu %d..%d in a %d viewport" % (r["x"], r["r"], r["vw"]) if r else "did not open")
|
|
330
|
+
mp.close()
|
|
331
|
+
|
|
332
|
+
return finish(a, b)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def finish(a, b):
|
|
336
|
+
fails = sum(1 for r in ROWS if r[1] == "FAIL")
|
|
337
|
+
print("\n%d checks, %d FAIL" % (len(ROWS), fails))
|
|
338
|
+
if a.csv:
|
|
339
|
+
with open(a.csv, "w", newline="", encoding="utf-8") as fh:
|
|
340
|
+
w = csv.writer(fh)
|
|
341
|
+
w.writerow(["check", "result", "detail"])
|
|
342
|
+
w.writerows(ROWS)
|
|
343
|
+
print("wrote", a.csv)
|
|
344
|
+
sys.exit(1 if fails else 0)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
if __name__ == "__main__":
|
|
348
|
+
main()
|