mosaic-headless 1.29.0 → 1.29.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.
@@ -172,8 +172,14 @@ class Surface:
172
172
  any(x in needs for x in (ancestors + ((parent_type,)
173
173
  if parent_type else ()))))
174
174
 
175
+ # ...and the same question the `allow` branch asks below: is this type
176
+ # part of the required internal structure of something we are inside?
177
+ in_ancestor_structure = any(t in self.default_children.get(x, [])
178
+ for x in ancestors)
179
+
175
180
  outcome = self.outcome.get(t)
176
181
  if (not declared_child and not nested_as_family and not nested_ok
182
+ and not in_ancestor_structure
177
183
  and (outcome == "BROKE_PAGE" or (outcome or "").startswith("COMMIT_5"))):
178
184
  problems.append("%s is measured %s under a plain container, and %s is not its declared parent"
179
185
  % (t, outcome, parent_type or "<root>"))
@@ -205,6 +211,17 @@ class Surface:
205
211
  # disagreement, take the rule from the grandparent, which is both
206
212
  # types' actual authority.
207
213
  # ancestors[-1] IS the parent; the grandparent is one further up
214
+ # A type that is part of the required internal structure of
215
+ # something we are INSIDE is legal wherever that structure puts
216
+ # it. `multi-steps-forms` declares `canBeParentFor` ->
217
+ # `form-multi-steps`, which is its own parent; the real child,
218
+ # `multi-steps-form-step`, is in `form-multi-steps`'s healed
219
+ # default children. Same disagreement as the loop containers.
220
+ if t not in allowed:
221
+ for anc in ancestors:
222
+ if t in self.default_children.get(anc, []):
223
+ allowed = allowed + [t]
224
+ break
208
225
  if t not in allowed and "-loop-" in parent_type and len(ancestors) > 1:
209
226
  gp = self.rules.get(ancestors[-2], {})
210
227
  allowed = allowed + (gp.get("allowed_children") or "").split("|")
@@ -53,6 +53,14 @@ def main():
53
53
  help="a loop-pagination-numbers whose children are page links")
54
54
  ap.add_argument("--paged-item", default="ct-arch-title",
55
55
  help="the repeated attrID whose text should change per page")
56
+ ap.add_argument("--navbar", default="nb",
57
+ help="a navbar; its toggle is <id>-toggle and its nav <id>-nav")
58
+ ap.add_argument("--media", default="md-img",
59
+ help="an image node; icons are looked for beside it")
60
+ ap.add_argument("--shortcode", default="wp-sc",
61
+ help="a wpShortcode node")
62
+ ap.add_argument("--steps", default="ms-panel-%d:3",
63
+ help="multi-step panel attrID pattern and how many")
56
64
  ap.add_argument("--instances", default="cs-card",
57
65
  help="a component's root attrID; instances are prefixed")
58
66
  a = ap.parse_args()
@@ -167,6 +175,111 @@ def main():
167
175
  check("FORM/required_blocks", bool(invalid),
168
176
  "empty form reports invalid: %s" % invalid[:4])
169
177
 
178
+ # ---- navbar: the toggle only exists below its breakpoint
179
+ if not present(a.navbar):
180
+ skip("NAVBAR", "not on this page")
181
+ else:
182
+ wide = p.evaluate("""(id) => {
183
+ const t = document.getElementById(id + '-toggle');
184
+ const n = document.getElementById(id + '-nav');
185
+ const vis = e => { const r = e.getBoundingClientRect(),
186
+ s = getComputedStyle(e);
187
+ return r.height > 2 && s.display !== 'none'
188
+ && s.visibility !== 'hidden'; };
189
+ return {toggle: vis(t), nav: vis(n),
190
+ links: n.querySelectorAll('a').length};
191
+ }""", a.navbar)
192
+ check("NAVBAR/wide", wide["nav"] and not wide["toggle"],
193
+ "at this width the links are shown and the toggle is not "
194
+ "(nav=%s toggle=%s, %d links)"
195
+ % (wide["nav"], wide["toggle"], wide["links"]))
196
+ p.set_viewport_size({"width": 900, "height": 900})
197
+ p.wait_for_timeout(900)
198
+ narrow = p.evaluate("""(id) => {
199
+ const t = document.getElementById(id + '-toggle');
200
+ const n = document.getElementById(id + '-nav');
201
+ const vis = e => { const r = e.getBoundingClientRect(),
202
+ s = getComputedStyle(e);
203
+ return r.height > 2 && s.display !== 'none'; };
204
+ return {toggle: vis(t), nav: vis(n),
205
+ mode: String(document.getElementById(id).className)
206
+ .includes('dropdown-mode')};
207
+ }""", a.navbar)
208
+ check("NAVBAR/narrow", narrow["toggle"] and not narrow["nav"]
209
+ and narrow["mode"],
210
+ "below buttonBreakpoint it collapses behind the toggle "
211
+ "(dropdown-mode=%s)" % narrow["mode"])
212
+ p.click("#%s-toggle" % a.navbar)
213
+ p.wait_for_timeout(800)
214
+ opened = p.evaluate("""(id) => {
215
+ const n = document.getElementById(id + '-nav');
216
+ const r = n.getBoundingClientRect();
217
+ return r.height > 2 && getComputedStyle(n).display !== 'none'; }""",
218
+ a.navbar)
219
+ check("NAVBAR/toggle_opens", opened, "clicking it reveals the links")
220
+ p.set_viewport_size({"width": 1280, "height": 900})
221
+ p.wait_for_timeout(700)
222
+
223
+ # ---- media: an attachment-protocol image and raw-SVG icons
224
+ if not present(a.media):
225
+ skip("MEDIA", "not on this page")
226
+ else:
227
+ img = p.evaluate("""(id) => {
228
+ const e = document.querySelector('#' + id + ' img, img#' + id);
229
+ if (!e) return null;
230
+ return {w: Math.round(e.getBoundingClientRect().width),
231
+ natural: e.naturalWidth, src: (e.currentSrc || e.src || '')};
232
+ }""", a.media)
233
+ # naturalWidth > 0 is the only proof the file actually loaded; a broken
234
+ # src renders an <img> of the right size and no picture
235
+ check("MEDIA/image_loaded",
236
+ bool(img) and img["natural"] > 0 and img["w"] > 0,
237
+ "%dpx wide, natural %spx" % (img["w"], img["natural"]) if img
238
+ else "no <img> rendered")
239
+ icons = p.evaluate("""() => [...document.querySelectorAll('svg')]
240
+ .filter(s => s.closest('[id^=md-i]')).length""")
241
+ check("MEDIA/icons_inline", icons > 0,
242
+ "%d inline <svg> - `icon` takes RAW SVG in its `svg` property, "
243
+ "not a URL or a library name" % icons)
244
+
245
+ # ---- WordPress interop
246
+ if not present(a.shortcode):
247
+ skip("WORDPRESS", "not on this page")
248
+ else:
249
+ n = p.evaluate("""(id) => document.querySelectorAll(
250
+ '#' + id + ' .product, #' + id + ' li').length""", a.shortcode)
251
+ check("WORDPRESS/shortcode_ran", n > 0,
252
+ "%d items from the shortcode - this is WooCommerce's own "
253
+ "output, not a reimplementation" % n)
254
+
255
+ # ---- a multi-step form shows ONE step at a time
256
+ step_pat, n_steps = a.steps.rsplit(":", 1)
257
+ n_steps = int(n_steps)
258
+ if not present(step_pat % 0):
259
+ skip("MULTISTEP", "not on this page")
260
+ else:
261
+ def steps_shown():
262
+ return [i for i in range(n_steps)
263
+ if p.evaluate(VISIBLE, step_pat % i)]
264
+ at_rest = steps_shown()
265
+ # Mosaic ships NO visibility CSS for a form step - the plugin moves
266
+ # `m-form-step--active` and leaves the hiding to the theme's variant.
267
+ # Without that rule every panel is on screen at once and the form
268
+ # looks like one long page with three sets of buttons.
269
+ check("MULTISTEP/one_at_rest", at_rest == [0],
270
+ "visible steps: %s" % at_rest
271
+ + ("" if at_rest == [0] else
272
+ " - nothing hides the inactive ones; style .m-form-step"))
273
+ if at_rest == [0]:
274
+ p.click("#ms-next-0")
275
+ p.wait_for_timeout(900)
276
+ check("MULTISTEP/next", steps_shown() == [1],
277
+ "NEXT -> %s" % steps_shown())
278
+ p.click("#ms-back-1")
279
+ p.wait_for_timeout(900)
280
+ check("MULTISTEP/back", steps_shown() == [0],
281
+ "BACK -> %s" % steps_shown())
282
+
170
283
  # ---- component instances
171
284
  #
172
285
  # An instance's ids are PREFIXED with the instance's own attrID