Skip to content

Commit a734d92

Browse files
kingpanther13claude
andcommitted
fix(test): skip Astro frontmatter in script extraction; drain microtasks before clock advance
CI surfaced two harness bugs the local smoke-tests didn't catch: 1. `extract_script_body` and the discovery walker greedily matched the first `<script>` substring in the source, which in setup.astro is actually a frontmatter comment: `// below in the <script> block keyed off the entry's id.` That made the "script body" start mid-frontmatter and the extracted text wasn't valid JS — esbuild and JSDOM both rejected it with "Unexpected identifier 'keyed'". Fix: strip the `--- ... ---` Astro frontmatter block before searching for `<script>` tags. Plain .py and .html sources have no frontmatter and pass through unchanged. 2. `clock.advance(settleMs)` returned immediately when no timers were yet scheduled, but the script under test often awaits a chain of stubbed-fetch promises BEFORE hitting its first `setTimeout`. With only one microtask drain between eval and advance, those promises hadn't resolved yet, so no timers existed, advance was a no-op, and the script stayed suspended — `restartAddon`'s POST to /api/settings/restart never fired and the `alert(msg)` in the 4xx branch never ran. Fix: drain microtasks aggressively at the start of advance() so pending promises get to schedule their timers, and drain again when the timer queue temporarily empties (a promise resolution may queue new timers). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 755e448 commit a734d92

2 files changed

Lines changed: 50 additions & 6 deletions

File tree

tests/js/harness.mjs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,17 @@ class FakeClock {
130130
// to the real event loop with setImmediate.
131131
async advance(untilMs) {
132132
const target = this.now + untilMs;
133+
134+
// Drain microtasks aggressively up front. The script under test may
135+
// be awaiting a chain of stubbed-fetch promises before it hits its
136+
// first setTimeout — if we checked for ready timers immediately,
137+
// we'd see none and return without advancing, leaving the script
138+
// suspended forever. Letting promises resolve first gives them a
139+
// chance to schedule timers we can then fire.
140+
for (let i = 0; i < 50; i++) {
141+
await new Promise((r) => setImmediate(r));
142+
}
143+
133144
// Outer loop in case a fired task schedules more tasks before target.
134145
// Cap iterations to surface runaway recursion as a test failure rather
135146
// than hanging the suite.
@@ -142,7 +153,19 @@ class FakeClock {
142153
nextId = id;
143154
}
144155
}
145-
if (nextId == null) break;
156+
if (nextId == null) {
157+
// No ready timers. Drain microtasks one more time in case a
158+
// recently-resolved promise just queued one, then re-check.
159+
for (let i = 0; i < 10; i++) {
160+
await new Promise((r) => setImmediate(r));
161+
}
162+
let stillNone = true;
163+
for (const [, t] of this.tasks) {
164+
if (t.time <= target) { stillNone = false; break; }
165+
}
166+
if (stillNone) break;
167+
continue;
168+
}
146169
const task = this.tasks.get(nextId);
147170
this.now = task.time;
148171
if (task.interval != null) {

tests/src/unit/_js_harness.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -192,24 +192,45 @@ def astro_vars_prelude(vars_: dict[str, Any]) -> str:
192192
return "\n".join(parts)
193193

194194

195+
def _strip_astro_frontmatter(source: str) -> str:
196+
"""Drop the leading ``--- ... ---`` block if present.
197+
198+
Astro frontmatter is TypeScript that runs at build-time, not JS that
199+
runs in the browser, so any ``<script>`` substring inside a comment
200+
or string there is not a real script tag. Stripping it before
201+
searching prevents
202+
``// below in the <script> block keyed off the entry's id``
203+
(line 17 of setup.astro) from being mis-matched as the script.
204+
"""
205+
if not source.startswith("---\n"):
206+
return source
207+
close = source.find("\n---\n", 4)
208+
if close == -1:
209+
return source
210+
return source[close + len("\n---\n") :]
211+
212+
195213
def extract_script_body(source: str, *, marker: str = "<script>") -> str:
196214
"""Return the first non-external inline ``<script>`` body in ``source``.
197215
198216
Handles both the bare ``<script>`` form used by ``_SETTINGS_HTML`` and
199217
attributed forms like Astro's ``<script define:vars={...}>``. The body
200218
is everything between the opening tag's ``>`` and the next
201219
``</script>``. External scripts (``<script src=...></script>``) are
202-
skipped — they have no inline body to extract.
220+
skipped — they have no inline body to extract. Astro frontmatter is
221+
skipped before searching so ``<script>`` mentions in frontmatter
222+
comments don't match.
203223
"""
204-
for match in re.finditer(r"<script\b([^>]*)>", source):
224+
search_source = _strip_astro_frontmatter(source)
225+
for match in re.finditer(r"<script\b([^>]*)>", search_source):
205226
attrs = match.group(1)
206227
if re.search(r"\bsrc\s*=", attrs):
207228
continue
208229
start = match.end()
209-
end = source.find("</script>", start)
230+
end = search_source.find("</script>", start)
210231
if end == -1:
211232
raise ValueError("unterminated <script> in source")
212-
return source[start:end]
233+
return search_source[start:end]
213234
raise ValueError(f"no inline <script> tag in source (marker hint: {marker!r})")
214235

215236

@@ -303,7 +324,7 @@ def _render_settings() -> str:
303324
site_dir = repo_root / "site" / "src"
304325
if site_dir.is_dir():
305326
for path in sorted(site_dir.rglob("*.astro")):
306-
text = path.read_text(encoding="utf-8")
327+
text = _strip_astro_frontmatter(path.read_text(encoding="utf-8"))
307328
for match in re.finditer(r"<script\b([^>]*)>", text):
308329
attrs = match.group(1)
309330
if re.search(r"\bsrc\s*=", attrs):

0 commit comments

Comments
 (0)