You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(jspsych): track <head> stylesheet changes during replay capture
The session-start snapshot only sees stylesheets that exist when start()
is called. Plugins that inject <style> blocks into <head> mid-experiment
(e.g. a survey widget mounting its theme) would still render unstyled in
replay because their rules were never captured.
Add a MutationObserver scoped to document.head that emits stylesheet.add,
stylesheet.remove, and stylesheet.update events with timestamps into a
new top-level stylesheet_events log. Snapshot ids assigned at start are
reused so add/remove/update can reference earlier entries. Pending
records are drained on stop() so changes near the boundary aren't lost.
|`rng_calls`| Chronological log of every `Math.random` output. Includes calls outside trial boundaries (parameter eval, ITI, `on_finish`). |
@@ -123,33 +125,62 @@ Every node in `initial_dom` is assigned a monotonically-increasing integer `id`.
123
125
- Canvas/WebGL pixel content. Only the element and its dimensions are recorded.
124
126
- Audio/video media data. Only playback events (`media.play`, `media.pause`, etc.) and the source URL are recorded.
125
127
- Shadow DOM. jsPsych does not use it in core; recordings will not capture mutations inside shadow roots.
126
-
- Stylesheet additions, removals, or rule mutations that occur after `start()`. The session-level [stylesheets](#stylesheets) snapshot is taken once at session start; it is not updated when later trials inject `<style>` tags or modify rules at runtime.
128
+
- CSSOM rule-level edits (`sheet.insertRule`, `deleteRule`, etc.) made on a captured stylesheet after `start()` without touching the owning element's children. These bypass `MutationObserver` and so are not reflected in [`stylesheet_events`](#stylesheet-events). Edits via `<style>.textContent = …` or `appendChild`/`removeChild` on the inner text node are tracked.
129
+
- Stylesheets attached to the document outside of `<head>` (e.g. a `<style>` placed in `<body>` outside the display element) after `start()`. The initial snapshot picks them up; subsequent changes are not tracked.
`stylesheets` is the snapshot of every entry in `document.styleSheets` at session start. It exists so a replayer can re-apply the same CSS to the reconstructed DOM — `initial_dom` carries class hooks like `.jspsych-display-element`, and without the matching rules the replay would render unstyled.
137
140
138
141
| Field | Description |
139
142
| ----- | ----------- |
143
+
|`id`| Session-unique integer that subsequent [`stylesheet_events`](#stylesheet-events) reference when this sheet is later removed or its rule text changes. |
140
144
|`kind`|`"inline"` for `<style>` tags; `"link"` for `<link rel="stylesheet">`. |
141
145
|`css`| Resolved rule text (joined `cssRules.cssText`). `null` for `<link>` sheets when `cssRules` access throws (cross-origin sheets without CORS headers). |
142
146
|`href`| The link's resolved URL. Replayers can refetch the source from this URL when `css` is `null`. |
143
147
|`media`| The sheet's `media` attribute, or `null` if unset. |
144
148
145
-
**Replay guidance.** For each entry, inject a `<style>` element with the captured `css` text into the replayer's document head. When `css` is `null` for a `link` entry, fetch the stylesheet from `href` (or substitute a known-good copy of the same asset) and inject the result. Apply the `media` attribute on the injected element so media-conditional rules behave correctly.
149
+
**Replay guidance.** For each entry, inject a `<style>` element with the captured `css` text into the replayer's document head. When `css` is `null` for a `link` entry, fetch the stylesheet from `href` (or substitute a known-good copy of the same asset) and inject the result. Apply the `media` attribute on the injected element so media-conditional rules behave correctly. Track the injected element by `id` so [`stylesheet_events`](#stylesheet-events) can be applied later.
146
150
147
151
**Limitations.**
148
152
149
-
-Snapshot is taken once at `start()`. Later mutations to the document's stylesheets — additions, removals, or `CSSOM` rule edits — are not tracked.
153
+
-Captured at `start()`. Later mutations to `<head>`stylesheets are tracked separately in [`stylesheet_events`](#stylesheet-events); changes elsewhere (e.g. a `<style>` placed in `<body>` outside the display element) are not.
150
154
-`@import` rules within a captured stylesheet are recorded as text. The imported sheet itself is not inlined; if the replayer's environment cannot resolve the import URL, those rules will not apply.
151
155
- Pseudo-classes (`:hover`, `:focus`) and media queries are recorded as part of the rule text but only take effect during replay if the replayer reproduces the corresponding state or viewport.
A chronological log of `<head>` stylesheet changes after `start()`. Plugins commonly inject `<style>` blocks into `<head>` mid-session (e.g. when a survey widget mounts); without this log a replayer would only have the start-of-session snapshot and would render those trials unstyled.
167
+
168
+
| Type | Field | Meaning |
169
+
| ---- | ----- | ------- |
170
+
|`stylesheet.add`|`sheet`| Full snapshot of the newly attached element, including a fresh `id`. |
171
+
|`stylesheet.remove`|`id`| Id of an entry from `stylesheets` or a prior `stylesheet.add`. The replayer should remove the corresponding injected element. |
172
+
|`stylesheet.update`|`id`| Id of a tracked `<style>` element. |
173
+
||`css`| New resolved rule text. The replayer should overwrite the injected element's text content. |
174
+
175
+
**Scope.** The observer is rooted at `document.head`. Stylesheet changes inside the trial display element are already represented by the per-trial DOM mutation stream (`dom.add` / `dom.remove` / `dom.text`); they do not appear here. Changes to stylesheets elsewhere in `<body>` (outside the display element) after `start()` are not tracked.
176
+
177
+
**Update coalescing.** Multiple child mutations to a single `<style>` element delivered in the same observer batch produce one `stylesheet.update` event carrying the final rule text. Updates and adds across separate batches are recorded individually.
178
+
179
+
**Replay procedure.** Apply each entry in order:
180
+
1.`stylesheet.add` — inject a `<style>` (or `<link>`) matching `sheet.kind`, mirroring the start-of-session snapshot logic.
181
+
2.`stylesheet.remove` — find the previously-injected element by `id` and detach it.
182
+
3.`stylesheet.update` — overwrite the previously-injected element's text content with the new `css`.
183
+
153
184
## Event types
154
185
155
186
`events` and `viewport_changes` are arrays of one of the following discriminated unions.
0 commit comments