mosaic-headless 1.27.0 → 1.28.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.
@@ -195,6 +195,19 @@ class Surface:
195
195
  problems.append("%s is a leaf and accepts no children (tried %s)" % (parent_type, t))
196
196
  elif rule.get("rule") == "allow":
197
197
  allowed = rule["allowed_children"].split("|") + self.default_children.get(parent_type, [])
198
+ # A loop container stands in for its parent's children: a
199
+ # `slider-loop-slides` sits inside `slider-slides` and repeats a
200
+ # `slider-slide`, so what it accepts is what `slider-slides`
201
+ # accepts. The plugin's own canBeParentFor says `slider-slides`
202
+ # here, which would nest the track inside its own loop - and the
203
+ # factory's default children say `slider-slide`, which is the
204
+ # structure the editor builds. Rather than hard-code the
205
+ # disagreement, take the rule from the grandparent, which is both
206
+ # types' actual authority.
207
+ # ancestors[-1] IS the parent; the grandparent is one further up
208
+ if t not in allowed and "-loop-" in parent_type and len(ancestors) > 1:
209
+ gp = self.rules.get(ancestors[-2], {})
210
+ allowed = allowed + (gp.get("allowed_children") or "").split("|")
198
211
  if t not in allowed:
199
212
  problems.append("%s accepts only %s, not %s" % (parent_type, "/".join(allowed), t))
200
213
  # customStyles is the pre-1.0.8 name of customDeclarations; expand_shorthands maps it
@@ -46,6 +46,13 @@ def main():
46
46
  ap.add_argument("--faq", default="cp-faq-q-%d/cp-faq-a-%d:4")
47
47
  ap.add_argument("--form", default="cp-form")
48
48
  ap.add_argument("--map", default="cp-map")
49
+ ap.add_argument("--loop-slider", default="ct-slider",
50
+ help="a slider whose slides come from a loop")
51
+ ap.add_argument("--loop-tabs", default="ct-tabs-menu")
52
+ ap.add_argument("--pagination", default="ct-arch-nums",
53
+ help="a loop-pagination-numbers whose children are page links")
54
+ ap.add_argument("--paged-item", default="ct-arch-title",
55
+ help="the repeated attrID whose text should change per page")
49
56
  a = ap.parse_args()
50
57
 
51
58
  rows, fails = [], []
@@ -158,6 +165,59 @@ def main():
158
165
  check("FORM/required_blocks", bool(invalid),
159
166
  "empty form reports invalid: %s" % invalid[:4])
160
167
 
168
+ # ---- components whose children come from a loop
169
+ #
170
+ # One authored template, repeated per row. The assertion is COUNT: a
171
+ # template that failed to loop renders exactly once and looks fine.
172
+ if not present(a.loop_slider):
173
+ skip("LOOP/slider", "not on this page")
174
+ else:
175
+ n = p.evaluate("""(id) => document.querySelectorAll(
176
+ '#' + id + ' mosaic-slider-slide').length""", a.loop_slider)
177
+ check("LOOP/slider", n > 1,
178
+ "%d slides from one authored slider-slide" % n)
179
+ if not present(a.loop_tabs):
180
+ skip("LOOP/tabs", "not on this page")
181
+ else:
182
+ n = p.evaluate("""(id) => document.querySelectorAll(
183
+ '#' + id + ' .m-tab').length""", a.loop_tabs)
184
+ labels = p.evaluate("""(id) => [...document.querySelectorAll(
185
+ '#' + id + ' .m-tab')].map(e => e.textContent.trim())""", a.loop_tabs)
186
+ check("LOOP/tabs", n > 1, "%d tabs from one authored tabs-tab" % n)
187
+ # @substr counts BYTES; a cut through a multi-byte character leaves
188
+ # U+FFFD, which is invisible in a count and obvious to a reader
189
+ check("LOOP/tab_labels_intact",
190
+ all("�" not in x for x in labels),
191
+ "no replacement characters - @substr counts bytes, so a length "
192
+ "that is not a multiple of the encoding cuts a glyph in half")
193
+
194
+ # ---- pagination actually pages
195
+ if not present(a.pagination):
196
+ skip("LOOP/pagination", "not on this page")
197
+ else:
198
+ def titles():
199
+ return p.evaluate("""(id) => [...document.querySelectorAll(
200
+ '[id=' + id + ']')].map(e => e.textContent.trim())""",
201
+ a.paged_item)
202
+ links = p.evaluate("""(id) => [...document.getElementById(id).children]
203
+ .map(e => e.getAttribute('href'))""", a.pagination)
204
+ check("LOOP/pagination_links", len(links) > 1 and all(links),
205
+ "%d page links: %s" % (len(links),
206
+ [l.split("?")[-1] for l in links if l][:3]))
207
+ # `p` is one of WordPress's own query vars - ?p=2 is a 301 to post 2 -
208
+ # so a paginationKey of "p" takes the visitor off the page entirely
209
+ bad_key = [l for l in links if l and ("?p=" in l or "&p=" in l)]
210
+ check("LOOP/pagination_key_is_safe", not bad_key,
211
+ "paginationKey does not collide with a WordPress query var"
212
+ if not bad_key else "uses ?p=, which WordPress reads as a post ID")
213
+ before = titles()
214
+ p.click("#%s > *:nth-child(2)" % a.pagination)
215
+ p.wait_for_timeout(2000)
216
+ after = titles()
217
+ check("LOOP/pagination_changes_rows",
218
+ bool(after) and set(after) != set(before),
219
+ "page 1 %s -> page 2 %s" % (len(before), len(after)))
220
+
161
221
  # ---- map
162
222
  if not present(a.map):
163
223
  skip("MAP", "not on this page")