buninu-browser 0.5.1 → 0.7.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.md +13 -6
- package/index.js +10 -6
- package/lib/audio/pcm.js +11 -0
- package/lib/audio/pulse-client.js +7 -2
- package/lib/cdp-server.js +94 -46
- package/lib/happy-dom/document-base.js +29 -1
- package/lib/happy-dom/mutation-observer.js +157 -0
- package/lib/happy-dom/parser.js +2 -0
- package/lib/headless-shell.js +49 -9
- package/lib/layout/repaint.js +89 -0
- package/lib/media/mp4-keyframes.js +144 -0
- package/lib/media/video-keyframe.js +29 -1
- package/lib/media/webm-snapshots.js +282 -0
- package/lib/network/cookie-jar.js +18 -2
- package/lib/paint/display-list.js +4 -3
- package/lib/paint/frame.js +12 -5
- package/lib/render-tree/index.js +3 -0
- package/lib/renderer/animation-frames.js +76 -0
- package/lib/renderer/bindings.js +2 -0
- package/lib/renderer/compositor-channel.js +95 -10
- package/lib/renderer/compositor.js +54 -18
- package/lib/renderer/document-cookies.js +87 -0
- package/lib/renderer/font-loading.js +60 -16
- package/lib/renderer/host.js +48 -19
- package/lib/renderer/idle-callbacks.js +109 -0
- package/lib/renderer/main.js +5 -1
- package/lib/renderer/media-source.js +4 -0
- package/lib/renderer/page-realm.js +159 -49
- package/lib/renderer/page-renderer.js +567 -197
- package/lib/renderer/resource-elements.js +88 -0
- package/lib/renderer/script-rewrite.js +18 -1
- package/lib/renderer/seccomp.js +6 -4
- package/lib/renderer/timers.js +71 -0
- package/lib/renderer/video-channel.js +94 -0
- package/lib/renderer/video-mask.js +115 -0
- package/lib/renderer/video-presentation.js +304 -39
- package/lib/renderer/video-worker.js +46 -0
- package/lib/renderer/workers.js +7 -17
- package/lib/style/computed-style.js +257 -61
- package/package.json +1 -1
- package/test/animation-frames.test.js +67 -0
- package/test/canvas-style-snapshot.test.js +18 -0
- package/test/cdp-stream.test.js +96 -3
- package/test/cdp-viewers.test.js +43 -0
- package/test/compositor-channel-ownership.test.js +194 -0
- package/test/compositor.test.js +27 -1
- package/test/computed-style-binding.test.js +49 -0
- package/test/computed-style-connectivity.test.js +13 -0
- package/test/computed-style.test.js +86 -0
- package/test/cookie-sync.test.js +82 -0
- package/test/document-base-cache.test.js +103 -0
- package/test/document-cookies.test.js +117 -0
- package/test/duplicate-style-keys.test.js +11 -0
- package/test/fixtures/animation-frames.js +56 -0
- package/test/fixtures/canvas-style-snapshot.js +81 -0
- package/test/fixtures/computed-style-connectivity.js +101 -0
- package/test/fixtures/computed-style-lazy.js +135 -0
- package/test/fixtures/duplicate-style-keys.js +97 -0
- package/test/fixtures/fake-renderer.js +12 -0
- package/test/fixtures/font-definition-invalidation.js +97 -0
- package/test/fixtures/font-metrics-invalidation.js +87 -0
- package/test/fixtures/full-style-identity.js +36 -0
- package/test/fixtures/hidden-style-engine.js +260 -0
- package/test/fixtures/hidden-style-reentry.js +232 -0
- package/test/fixtures/hidden-style-renderer.js +203 -0
- package/test/fixtures/layout-repaint.js +69 -0
- package/test/fixtures/media-presentation-continuation.js +141 -0
- package/test/fixtures/media-presentation-refill.js +113 -0
- package/test/fixtures/media-probe.js +148 -0
- package/test/fixtures/media-refill-cancel.js +79 -0
- package/test/fixtures/media-repaint.js +48 -0
- package/test/fixtures/media-resource-reselection.js +94 -0
- package/test/fixtures/media-source-attachment.js +136 -0
- package/test/fixtures/media-source-no-decoder.js +17 -0
- package/test/fixtures/media-track-refill.js +132 -0
- package/test/fixtures/media-webm-seek.js +51 -0
- package/test/fixtures/mutation-observer.js +201 -0
- package/test/fixtures/observer-invalidation.js +105 -0
- package/test/fixtures/prepaint-scheduling.js +73 -0
- package/test/fixtures/resource-discovery.js +32 -0
- package/test/fixtures/spare-renderer-scheduling.js +124 -0
- package/test/fixtures/style-propagation.js +26 -0
- package/test/fixtures/svg-node-reuse.js +83 -0
- package/test/fixtures/video-black-replay.js +140 -0
- package/test/fixtures/video-controls-invalidation.js +71 -0
- package/test/fixtures/viewport-bounds.js +148 -0
- package/test/font-loading.test.js +77 -0
- package/test/font-metrics-invalidation.test.js +60 -0
- package/test/full-style-identity.test.js +191 -0
- package/test/hidden-style-lazy.test.js +31 -0
- package/test/hidden-style-reentry.test.js +20 -0
- package/test/idle-callback.test.js +89 -0
- package/test/idle-scheduler.test.js +148 -0
- package/test/index.test.js +1 -0
- package/test/inline-style-cascade.test.js +63 -0
- package/test/layout-repaint-renderer.test.js +9 -0
- package/test/layout-repaint.test.js +222 -0
- package/test/media-presentation-continuation.test.js +14 -0
- package/test/media-presentation-refill.test.js +8 -0
- package/test/media-probe.test.js +16 -0
- package/test/media-repaint.test.js +8 -0
- package/test/media-source-attachment.test.js +34 -0
- package/test/media-track-refill.test.js +26 -0
- package/test/media-webm-seek.test.js +11 -0
- package/test/mp4-keyframes.test.js +36 -0
- package/test/mutation-observer.test.js +91 -0
- package/test/observer-invalidation.test.js +12 -0
- package/test/page-scripts.test.js +35 -0
- package/test/page-timers.test.js +67 -0
- package/test/paint-frame.test.js +18 -1
- package/test/pcm.test.js +63 -0
- package/test/prepaint-scheduling.test.js +13 -0
- package/test/pulse-client.test.js +165 -0
- package/test/raw-capture.test.js +34 -5
- package/test/renderer-host.test.js +51 -0
- package/test/resource-discovery.test.js +12 -0
- package/test/resource-elements.test.js +234 -0
- package/test/root-self-style.test.js +104 -0
- package/test/script-rewrite.test.js +35 -0
- package/test/seccomp.test.js +45 -0
- package/test/spare-renderer-scheduling.test.js +8 -0
- package/test/style-propagation.test.js +225 -0
- package/test/style-sheets.test.js +19 -0
- package/test/svg.test.js +45 -0
- package/test/timers.test.js +58 -0
- package/test/video-black-replay.test.js +34 -0
- package/test/video-capture.test.js +170 -0
- package/test/video-channel.test.js +253 -0
- package/test/video-continuation.test.js +57 -0
- package/test/video-controls-invalidation.test.js +12 -0
- package/test/video-keyframe.test.js +104 -4
- package/test/video-mask.test.js +94 -0
- package/test/video-presentation.test.js +239 -1
- package/test/video-timing-presentation.test.js +115 -0
- package/test/viewport-bounds.test.js +13 -0
- package/test/webm-snapshots.test.js +372 -0
|
@@ -14,6 +14,7 @@ const BLOCK_TAGS = new Set([
|
|
|
14
14
|
"HEADER", "HR", "HTML", "LI", "MAIN", "NAV", "OL", "P", "PRE", "SECTION", "UL",
|
|
15
15
|
]);
|
|
16
16
|
const NONE_TAGS = new Set(["HEAD", "LINK", "META", "NOFRAMES", "SCRIPT", "STYLE", "TEMPLATE", "TITLE"]);
|
|
17
|
+
const HIDDEN_EAGER_TAGS = new Set(["HTML", "BODY", "BASE", "BASEFONT", "BGSOUND", "NOSCRIPT", "BR", "IMG", "CANVAS", "VIDEO", "AUDIO", "IFRAME", "OBJECT", "EMBED", "INPUT", "TEXTAREA", "SELECT"]);
|
|
17
18
|
const INHERITED = new Set([
|
|
18
19
|
"color", "color-scheme", "font-family", "font-size", "font-weight", "font-style", "font-stretch", "line-height", "list-style-type", "text-align", "white-space",
|
|
19
20
|
"visibility", "overflow-wrap", "word-break", "border-spacing", "border-collapse", "pointer-events", "direction",
|
|
@@ -106,6 +107,14 @@ const PROPERTY_FIELDS = Object.freeze({
|
|
|
106
107
|
top: ["inset"], right: ["inset"], bottom: ["inset"], left: ["inset"],
|
|
107
108
|
});
|
|
108
109
|
const BORDER_FIELDS = Object.freeze(["borderSpecifiedWidths", "borderStyles", "borderColors", "borderWidths"]);
|
|
110
|
+
// Inputs read from a parent even without an explicit `inherit` declaration.
|
|
111
|
+
// Display affects flex/grid item floats; text decorations propagate separately
|
|
112
|
+
// from the ordinary inherited properties. Root font changes also affect rem.
|
|
113
|
+
const CHILD_STYLE_INPUTS = [...new Set([
|
|
114
|
+
...[...INHERITED].flatMap(name => PROPERTY_FIELDS[name]
|
|
115
|
+
?? [INHERITED_FIELDS.get(name) ?? name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())]),
|
|
116
|
+
"display", "textDecorations",
|
|
117
|
+
])];
|
|
109
118
|
|
|
110
119
|
const INITIAL = Object.freeze({
|
|
111
120
|
display: "inline",
|
|
@@ -323,9 +332,16 @@ let environment = DEFAULT_ENVIRONMENT;
|
|
|
323
332
|
|
|
324
333
|
export class StyleEngine {
|
|
325
334
|
#measureText;
|
|
335
|
+
#deferHidden;
|
|
336
|
+
#hiddenRoots = new WeakSet();
|
|
337
|
+
#hiddenAncestors = new WeakMap();
|
|
338
|
+
#ensured = null;
|
|
339
|
+
#queryContext = null;
|
|
340
|
+
#usesFontMetrics = false;
|
|
326
341
|
#styles = new WeakMap();
|
|
327
342
|
#customProperties = new WeakMap();
|
|
328
343
|
#pseudoStyles = new WeakMap();
|
|
344
|
+
#parentDependencies = new WeakMap();
|
|
329
345
|
#document = null;
|
|
330
346
|
#sheetChanges = null;
|
|
331
347
|
/** Elements computed by the last compute(), and whether it was incremental. */
|
|
@@ -338,10 +354,20 @@ export class StyleEngine {
|
|
|
338
354
|
#parsedSheets = new Map();
|
|
339
355
|
#parsedSheetsKey = "";
|
|
340
356
|
|
|
341
|
-
|
|
342
|
-
|
|
357
|
+
// Renderers may defer invisible descendants; standalone callers remain eager.
|
|
358
|
+
constructor({ measureText = null, deferHidden = false } = {}) {
|
|
359
|
+
this.#deferHidden = deferHidden;
|
|
360
|
+
this.#measureText = measureText && ((text, style) => {
|
|
361
|
+
this.#usesFontMetrics = true;
|
|
362
|
+
return measureText(text, style);
|
|
363
|
+
});
|
|
343
364
|
}
|
|
344
365
|
|
|
366
|
+
// Font-relative computed lengths (currently ch) depend on loaded typefaces.
|
|
367
|
+
// Keep this conservative across incremental passes, including var()/pseudo
|
|
368
|
+
// values and a root font-size which can change descendants' rem lengths.
|
|
369
|
+
get usesFontMetrics() { return this.#usesFontMetrics; }
|
|
370
|
+
|
|
345
371
|
/**
|
|
346
372
|
* Computes the document's styles. With `dirty` (elements whose subtree's
|
|
347
373
|
* styles may have changed), only those subtrees are restyled and every
|
|
@@ -350,6 +376,7 @@ export class StyleEngine {
|
|
|
350
376
|
* `restyled` afterwards counts the elements this call computed.
|
|
351
377
|
*/
|
|
352
378
|
compute(document, styleSheets = [], viewport = {}, dirty = null, selfOnly = null) {
|
|
379
|
+
if (this.#document !== document) this.#usesFontMetrics = false;
|
|
353
380
|
const previous = environment;
|
|
354
381
|
environment = {
|
|
355
382
|
viewportWidth: viewport.width ?? DEFAULT_ENVIRONMENT.viewportWidth,
|
|
@@ -359,9 +386,16 @@ export class StyleEngine {
|
|
|
359
386
|
measureText: this.#measureText,
|
|
360
387
|
};
|
|
361
388
|
this.restyled = 0;
|
|
389
|
+
let queryRules = null, failed = false;
|
|
390
|
+
if (this.#deferHidden) {
|
|
391
|
+
this.#hiddenAncestors = new WeakMap();
|
|
392
|
+
this.#ensured = { styles: new WeakMap(), customProperties: new WeakMap(),
|
|
393
|
+
pseudoStyles: new WeakMap(), pending: new WeakSet() };
|
|
394
|
+
this.#queryContext = null;
|
|
395
|
+
}
|
|
362
396
|
try {
|
|
363
397
|
const before = this.#ruleCache.index;
|
|
364
|
-
const rules = this.#rulesFor(styleSheets);
|
|
398
|
+
const rules = queryRules = this.#rulesFor(styleSheets);
|
|
365
399
|
const root = document.documentElement;
|
|
366
400
|
if (dirty !== null && rules !== before) {
|
|
367
401
|
const affected = this.#elementsForSheetChanges(document);
|
|
@@ -371,11 +405,20 @@ export class StyleEngine {
|
|
|
371
405
|
&& Boolean(root) && this.#styles.has(root) && !dirty.has(root);
|
|
372
406
|
this.incremental = incremental;
|
|
373
407
|
if (!incremental) {
|
|
408
|
+
// Recompute every element into fresh maps so detached nodes lose their
|
|
409
|
+
// styles. On the same document, the prior pass can supply equal immutable
|
|
410
|
+
// identities for render/layout reuse. Keep this fallback local to the
|
|
411
|
+
// traversal, including when a callback starts another compute().
|
|
412
|
+
const previousMaps = this.#document === document ? {
|
|
413
|
+
styles: this.#styles, customProperties: this.#customProperties, pseudoStyles: this.#pseudoStyles,
|
|
414
|
+
} : null;
|
|
374
415
|
this.#styles = new WeakMap();
|
|
375
416
|
this.#customProperties = new WeakMap();
|
|
376
417
|
this.#pseudoStyles = new WeakMap();
|
|
418
|
+
this.#parentDependencies = new WeakMap();
|
|
419
|
+
if (this.#deferHidden) this.#hiddenRoots = new WeakSet();
|
|
377
420
|
this.#document = document;
|
|
378
|
-
if (root) this.#visit(root, null, null, rules);
|
|
421
|
+
if (root) this.#visit(root, null, null, rules, undefined, previousMaps);
|
|
379
422
|
return this;
|
|
380
423
|
}
|
|
381
424
|
// rem units resolve against the root's font size, which is unchanged.
|
|
@@ -388,13 +431,24 @@ export class StyleEngine {
|
|
|
388
431
|
while (start !== root && !this.#styles.has(start.parentElement)) start = start.parentElement;
|
|
389
432
|
starts.add(start);
|
|
390
433
|
}
|
|
391
|
-
if (starts.has(root)
|
|
434
|
+
if (starts.has(root)) {
|
|
392
435
|
this.#visit(root, null, null, rules);
|
|
393
436
|
return this;
|
|
394
437
|
}
|
|
438
|
+
if (selfOnly?.has(root)) {
|
|
439
|
+
// A root-only selector/attribute change may leave its computed values
|
|
440
|
+
// unchanged. Check it like other self-only elements before cascading
|
|
441
|
+
// through the document; other dirty subtrees still need their pass.
|
|
442
|
+
// The root's own font-size rem units use the initial font size, not
|
|
443
|
+
// the previous generation's root size. #styleOne then sets the basis
|
|
444
|
+
// used by descendants, including a necessary full subtree restyle.
|
|
445
|
+
environment = { ...environment, rootFontSize: DEFAULT_ENVIRONMENT.rootFontSize };
|
|
446
|
+
if (this.#restyleSelf(root, rules)) return this;
|
|
447
|
+
}
|
|
395
448
|
const topmost = topmostConnected(starts, root);
|
|
396
449
|
for (const start of topmost) {
|
|
397
450
|
const parent = start.parentElement;
|
|
451
|
+
if (this.#deferHidden && this.#hiddenBoundary(parent)) continue;
|
|
398
452
|
const ancestors = this.#ancestorsOf(start);
|
|
399
453
|
this.#visit(start, this.#styles.get(parent), this.#customProperties.get(parent) ?? EMPTY_MAP, rules, ancestors);
|
|
400
454
|
}
|
|
@@ -407,7 +461,14 @@ export class StyleEngine {
|
|
|
407
461
|
for (let node = element; node && !inside; node = node.parentElement) inside = covered.has(node);
|
|
408
462
|
if (!inside) this.#restyleSelf(element, rules);
|
|
409
463
|
}
|
|
464
|
+
} catch (error) {
|
|
465
|
+
failed = true;
|
|
466
|
+
throw error;
|
|
410
467
|
} finally {
|
|
468
|
+
if (this.#deferHidden && !failed && this.#document === document) {
|
|
469
|
+
this.#queryContext = { rules: queryRules, environment: { ...environment,
|
|
470
|
+
rootFontSize: this.#styles.get(document.documentElement)?.fontSize ?? DEFAULT_ENVIRONMENT.rootFontSize } };
|
|
471
|
+
}
|
|
411
472
|
environment = previous;
|
|
412
473
|
}
|
|
413
474
|
return this;
|
|
@@ -502,29 +563,106 @@ export class StyleEngine {
|
|
|
502
563
|
}
|
|
503
564
|
|
|
504
565
|
get(element) {
|
|
505
|
-
return this.#styles.get(element) ?? INITIAL;
|
|
566
|
+
return (this.#ensureHidden(element)?.styles ?? this.#styles).get(element) ?? INITIAL;
|
|
506
567
|
}
|
|
507
568
|
|
|
508
569
|
/** An element's custom properties (--*), name to value text, inherited ones included. */
|
|
509
570
|
customPropertiesOf(element) {
|
|
510
|
-
return this.#customProperties.get(element) ?? EMPTY_MAP;
|
|
571
|
+
return (this.#ensureHidden(element)?.customProperties ?? this.#customProperties).get(element) ?? EMPTY_MAP;
|
|
511
572
|
}
|
|
512
573
|
|
|
513
574
|
/** The computed style of an element's ::before/::after box, or null when it generates none. */
|
|
514
575
|
getPseudo(element, pseudo) {
|
|
515
|
-
return this.#pseudoStyles.get(element)?.[pseudo] ?? null;
|
|
576
|
+
return (this.#ensureHidden(element)?.pseudoStyles ?? this.#pseudoStyles).get(element)?.[pseudo] ?? null;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Only descendants are deferred: the boundary itself must remain styled so
|
|
580
|
+
// its display/inherited/custom values can be observed or reveal its children.
|
|
581
|
+
#canDefer(element, style) {
|
|
582
|
+
return style.display === "none" && element.namespaceURI === "http://www.w3.org/1999/xhtml"
|
|
583
|
+
&& element !== this.#document.documentElement && element !== this.#document.body
|
|
584
|
+
&& !NONE_TAGS.has(element.tagName)
|
|
585
|
+
&& !HIDDEN_EAGER_TAGS.has(element.tagName);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Inclusive outermost hidden boundary. Nested old boundaries can have stale
|
|
589
|
+
// base styles after their ancestor hides, so queries begin at the outermost.
|
|
590
|
+
// Cache ancestor paths per compute pass;
|
|
591
|
+
// changing a boundary clears them before any later traversal/query can use it.
|
|
592
|
+
#hiddenBoundary(element) {
|
|
593
|
+
const path = [];
|
|
594
|
+
let node = element;
|
|
595
|
+
while (node && !this.#hiddenAncestors.has(node)) { path.push(node); node = node.parentElement; }
|
|
596
|
+
let boundary = node ? this.#hiddenAncestors.get(node) : null;
|
|
597
|
+
for (let index = path.length - 1; index >= 0; index--) {
|
|
598
|
+
const current = path[index];
|
|
599
|
+
if (!boundary && this.#hiddenRoots.has(current)) boundary = current;
|
|
600
|
+
this.#hiddenAncestors.set(current, boundary);
|
|
601
|
+
}
|
|
602
|
+
return boundary;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
#ensureHidden(element) {
|
|
606
|
+
if (!this.#deferHidden || !this.#queryContext || !element?.isConnected
|
|
607
|
+
|| element.ownerDocument !== this.#document) return null;
|
|
608
|
+
const boundary = this.#hiddenBoundary(element.parentElement);
|
|
609
|
+
if (!boundary) return null;
|
|
610
|
+
const target = this.#ensured, context = this.#queryContext;
|
|
611
|
+
// Font measurement may synchronously read styles. Do not restart an
|
|
612
|
+
// unfinished chain. During pseudo computation the principal style is
|
|
613
|
+
// already available, while pseudo reads still see the prior generation.
|
|
614
|
+
if (target.pending.has(element)) return target.styles.has(element)
|
|
615
|
+
? { styles: target.styles, customProperties: target.customProperties, pseudoStyles: this.#pseudoStyles }
|
|
616
|
+
: null;
|
|
617
|
+
if (target.styles.has(element)) return target;
|
|
618
|
+
const chain = [];
|
|
619
|
+
let parent = element;
|
|
620
|
+
while (parent && parent !== boundary && !target.styles.has(parent)) {
|
|
621
|
+
if (target.pending.has(parent)) return null;
|
|
622
|
+
chain.push(parent); parent = parent.parentElement;
|
|
623
|
+
}
|
|
624
|
+
if (!parent || target.pending.has(parent)) return null;
|
|
625
|
+
const parentMaps = parent === boundary ? { styles: this.#styles, customProperties: this.#customProperties } : target;
|
|
626
|
+
let style = parentMaps.styles.get(parent), custom = parentMaps.customProperties.get(parent) ?? EMPTY_MAP;
|
|
627
|
+
for (const node of chain) target.pending.add(node);
|
|
628
|
+
const previous = environment;
|
|
629
|
+
environment = context.environment;
|
|
630
|
+
try {
|
|
631
|
+
const ancestors = this.#ancestorsOf(chain.at(-1));
|
|
632
|
+
const previousMaps = { styles: this.#styles, customProperties: this.#customProperties, pseudoStyles: this.#pseudoStyles };
|
|
633
|
+
for (let index = chain.length - 1; index >= 0; index--) {
|
|
634
|
+
const node = chain[index], keys = elementKeys(node);
|
|
635
|
+
try {
|
|
636
|
+
const result = this.#styleOne(node, style, custom, context.rules, keys, ancestors, previousMaps, target);
|
|
637
|
+
style = result.style; custom = result.customProperties;
|
|
638
|
+
} catch (error) {
|
|
639
|
+
// A pseudo can fail after principal values were stored. Retain only
|
|
640
|
+
// complete ancestors; the failed node must be recomputed on retry.
|
|
641
|
+
target.styles.delete(node); target.customProperties.delete(node); target.pseudoStyles.delete(node);
|
|
642
|
+
throw error;
|
|
643
|
+
} finally { target.pending.delete(node); }
|
|
644
|
+
ancestors.push(keys);
|
|
645
|
+
}
|
|
646
|
+
} finally {
|
|
647
|
+
for (const node of chain) target.pending.delete(node);
|
|
648
|
+
environment = previous;
|
|
649
|
+
}
|
|
650
|
+
return target;
|
|
516
651
|
}
|
|
517
652
|
|
|
518
653
|
/** Computes and stores one element's style and ::before/::after styles. */
|
|
519
|
-
#styleOne(element, parentStyle, parentCustomProperties, rules, keys, ancestors) {
|
|
520
|
-
const
|
|
521
|
-
const
|
|
522
|
-
const
|
|
654
|
+
#styleOne(element, parentStyle, parentCustomProperties, rules, keys, ancestors, previousMaps = null, target = null) {
|
|
655
|
+
const styles = target?.styles ?? this.#styles, customMap = target?.customProperties ?? this.#customProperties;
|
|
656
|
+
const pseudoMap = target?.pseudoStyles ?? this.#pseudoStyles;
|
|
657
|
+
const previousStyle = styles.get(element) ?? previousMaps?.styles.get(element);
|
|
658
|
+
const previousCustom = customMap.get(element) ?? previousMaps?.customProperties.get(element);
|
|
659
|
+
const previousPseudo = pseudoMap.get(element) ?? previousMaps?.pseudoStyles.get(element);
|
|
523
660
|
// Inline style takes part in the cascade before custom properties are
|
|
524
661
|
// resolved: style="--x: ..." must win over stylesheet values of --x.
|
|
525
662
|
const declarations = withInlineStyle(element, cascadeDeclarations(element, rules, null, keys, ancestors));
|
|
526
663
|
let customProperties = withCustomProperties(parentCustomProperties ?? EMPTY_MAP, declarations);
|
|
527
|
-
|
|
664
|
+
const dependencies = { inheritsParent: false };
|
|
665
|
+
let style = computeElementStyle(element, parentStyle, declarations, customProperties, dependencies, true);
|
|
528
666
|
// A descendant may be visited because an ancestor's selector state
|
|
529
667
|
// changed even though its final computed values did not. Preserve the
|
|
530
668
|
// immutable identities so retained render and layout subtrees survive.
|
|
@@ -532,36 +670,49 @@ export class StyleEngine {
|
|
|
532
670
|
if (sameCustom(previousCustom, customProperties)) customProperties = previousCustom;
|
|
533
671
|
// rem units resolve against the root element's computed font size.
|
|
534
672
|
if (!parentStyle) environment = { ...environment, rootFontSize: style.fontSize };
|
|
535
|
-
|
|
536
|
-
|
|
673
|
+
styles.set(element, style);
|
|
674
|
+
customMap.set(element, customProperties);
|
|
537
675
|
let pseudoStyles = {};
|
|
538
676
|
for (const pseudo of ["before", "after"]) {
|
|
539
677
|
const pseudoDeclarations = cascadeDeclarations(element, rules, pseudo, keys, ancestors);
|
|
540
678
|
if (!pseudoDeclarations.has("content")) continue;
|
|
541
679
|
const pseudoCustom = withCustomProperties(customProperties, pseudoDeclarations);
|
|
542
|
-
const pseudoStyle = computeElementStyle(pseudoElement(pseudo), style, pseudoDeclarations, pseudoCustom);
|
|
680
|
+
const pseudoStyle = computeElementStyle(pseudoElement(pseudo), style, pseudoDeclarations, pseudoCustom, dependencies);
|
|
543
681
|
// `content: none | normal` generates no box (CSS Pseudo 4 §2.3).
|
|
544
682
|
if (pseudoStyle.content != null && pseudoStyle.display !== "none") pseudoStyles[pseudo] = pseudoStyle;
|
|
545
683
|
}
|
|
546
684
|
if (sameValue(previousPseudo ?? null, (pseudoStyles.before || pseudoStyles.after) ? pseudoStyles : null)) {
|
|
547
685
|
pseudoStyles = previousPseudo ?? {};
|
|
548
686
|
}
|
|
549
|
-
if (pseudoStyles.before || pseudoStyles.after)
|
|
550
|
-
else
|
|
551
|
-
|
|
687
|
+
if (pseudoStyles.before || pseudoStyles.after) pseudoMap.set(element, pseudoStyles);
|
|
688
|
+
else pseudoMap.delete(element);
|
|
689
|
+
// Dependencies can change without changing computed values, for example
|
|
690
|
+
// width:100px -> width:inherit while the parent is already 100px wide.
|
|
691
|
+
if (!target) {
|
|
692
|
+
this.#parentDependencies.set(element, dependencies.inheritsParent);
|
|
693
|
+
if (this.#deferHidden) {
|
|
694
|
+
const hidden = this.#canDefer(element, style);
|
|
695
|
+
if (hidden !== this.#hiddenRoots.has(element)) {
|
|
696
|
+
if (hidden) this.#hiddenRoots.add(element); else this.#hiddenRoots.delete(element);
|
|
697
|
+
this.#hiddenAncestors = new WeakMap();
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
this.restyled++;
|
|
701
|
+
}
|
|
552
702
|
return { style, customProperties };
|
|
553
703
|
}
|
|
554
704
|
|
|
555
|
-
#visit(element, parentStyle, parentCustomProperties, rules, ancestors = new AncestorFilter()) {
|
|
705
|
+
#visit(element, parentStyle, parentCustomProperties, rules, ancestors = new AncestorFilter(), previousMaps = null) {
|
|
556
706
|
const keys = elementKeys(element);
|
|
557
|
-
const { style, customProperties } = this.#styleOne(element, parentStyle, parentCustomProperties, rules, keys, ancestors);
|
|
558
|
-
this.#visitChildren(element, keys, style, customProperties, rules, ancestors);
|
|
707
|
+
const { style, customProperties } = this.#styleOne(element, parentStyle, parentCustomProperties, rules, keys, ancestors, previousMaps);
|
|
708
|
+
this.#visitChildren(element, keys, style, customProperties, rules, ancestors, previousMaps);
|
|
559
709
|
}
|
|
560
710
|
|
|
561
|
-
#visitChildren(element, keys, style, customProperties, rules, ancestors) {
|
|
711
|
+
#visitChildren(element, keys, style, customProperties, rules, ancestors, previousMaps = null) {
|
|
712
|
+
if (this.#deferHidden && this.#hiddenRoots.has(element)) return;
|
|
562
713
|
// The element is an ancestor of everything below it.
|
|
563
714
|
const own = ancestors.push(keys);
|
|
564
|
-
for (const child of element.children) this.#visit(child, style, customProperties, rules, ancestors);
|
|
715
|
+
for (const child of element.children) this.#visit(child, style, customProperties, rules, ancestors, previousMaps);
|
|
565
716
|
ancestors.pop(own);
|
|
566
717
|
}
|
|
567
718
|
|
|
@@ -573,32 +724,59 @@ export class StyleEngine {
|
|
|
573
724
|
}
|
|
574
725
|
|
|
575
726
|
/**
|
|
576
|
-
* Restyles one element
|
|
577
|
-
*
|
|
727
|
+
* Restyles one element and propagates changed parent inputs. Returns true
|
|
728
|
+
* only when every descendant was visited, so a root-only update cannot
|
|
729
|
+
* swallow independently dirty subtrees after a partial propagation.
|
|
578
730
|
*/
|
|
579
|
-
#restyleSelf(element, rules) {
|
|
731
|
+
#restyleSelf(element, rules, ancestors = this.#ancestorsOf(element)) {
|
|
580
732
|
const parent = element.parentElement;
|
|
733
|
+
if (this.#deferHidden && this.#hiddenBoundary(parent)) return false;
|
|
581
734
|
const before = { style: this.#styles.get(element), pseudo: this.#pseudoStyles.get(element), custom: this.#customProperties.get(element) };
|
|
582
735
|
const keys = elementKeys(element);
|
|
583
|
-
const ancestors = this.#ancestorsOf(element);
|
|
584
736
|
const { style, customProperties } = this.#styleOne(element, this.#styles.get(parent), this.#customProperties.get(parent) ?? EMPTY_MAP, rules, keys, ancestors);
|
|
737
|
+
if (this.#deferHidden && this.#hiddenRoots.has(element)) return true;
|
|
585
738
|
const pseudo = this.#pseudoStyles.get(element);
|
|
586
739
|
if (sameValue(before.style, style) && sameValue(before.pseudo ?? null, pseudo ?? null) && sameCustom(before.custom, customProperties)) {
|
|
587
740
|
// Unchanged: keep the previous objects so later comparisons stay cheap.
|
|
588
741
|
this.#styles.set(element, before.style);
|
|
589
|
-
return;
|
|
742
|
+
return false;
|
|
590
743
|
}
|
|
591
|
-
|
|
744
|
+
if (!sameChildStyleInputs(before.style, style) || !sameCustom(before.custom, customProperties)) {
|
|
745
|
+
this.#visitChildren(element, keys, style, customProperties, rules, ancestors);
|
|
746
|
+
return true;
|
|
747
|
+
}
|
|
748
|
+
// The parent's ordinary inherited inputs did not change. Only immediate
|
|
749
|
+
// children with explicit inheritance can observe its other properties;
|
|
750
|
+
// an unchanged child also shields its descendants from those changes.
|
|
751
|
+
const own = ancestors.push(keys);
|
|
752
|
+
try {
|
|
753
|
+
for (const child of element.children) {
|
|
754
|
+
if (!this.#styles.has(child) || !this.#parentDependencies.has(child)) {
|
|
755
|
+
this.#visit(child, style, customProperties, rules, ancestors);
|
|
756
|
+
} else if (before.style !== style && this.#parentDependencies.get(child)) {
|
|
757
|
+
this.#restyleSelf(child, rules, ancestors);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
} finally { ancestors.pop(own); }
|
|
761
|
+
return false;
|
|
592
762
|
}
|
|
593
763
|
}
|
|
594
764
|
|
|
765
|
+
function sameChildStyleInputs(before, after) {
|
|
766
|
+
return Boolean(before && after) && CHILD_STYLE_INPUTS.every(field => sameValue(before[field], after[field]));
|
|
767
|
+
}
|
|
768
|
+
|
|
595
769
|
/** Structural equality of computed style values (frozen objects and arrays). */
|
|
596
770
|
function sameValue(left, right) {
|
|
597
771
|
if (left === right) return true;
|
|
598
772
|
if (!left || !right || typeof left !== "object" || typeof right !== "object") return false;
|
|
599
773
|
const keys = Object.keys(left);
|
|
600
774
|
if (keys.length !== Object.keys(right).length) return false;
|
|
601
|
-
|
|
775
|
+
for (let index = 0; index < keys.length; index++) {
|
|
776
|
+
const key = keys[index], a = left[key], b = right[key];
|
|
777
|
+
if (a !== b && !sameValue(a, b)) return false;
|
|
778
|
+
}
|
|
779
|
+
return true;
|
|
602
780
|
}
|
|
603
781
|
|
|
604
782
|
function sameCustom(left, right) {
|
|
@@ -684,7 +862,7 @@ function withCustomProperties(inherited, declarations) {
|
|
|
684
862
|
return result;
|
|
685
863
|
}
|
|
686
864
|
|
|
687
|
-
export function computeElementStyle(element, parentStyle = null, authorDeclarations = null, customProperties = new Map()) {
|
|
865
|
+
export function computeElementStyle(element, parentStyle = null, authorDeclarations = null, customProperties = new Map(), dependencies = null, inlineCascaded = false) {
|
|
688
866
|
const values = { ...INITIAL };
|
|
689
867
|
if (parentStyle) {
|
|
690
868
|
for (const property of INHERITED) {
|
|
@@ -742,14 +920,18 @@ export function computeElementStyle(element, parentStyle = null, authorDeclarati
|
|
|
742
920
|
declarations.set(declaration.name, { ...declaration, specificity: -1, order: -1, layer: -Infinity });
|
|
743
921
|
}
|
|
744
922
|
}
|
|
745
|
-
|
|
923
|
+
// StyleEngine already merged inline declarations before resolving custom
|
|
924
|
+
// properties. Direct callers still need the ordinary inline cascade here.
|
|
925
|
+
for (const declaration of inlineCascaded ? [] : parseDeclarations(element.getAttribute("style") ?? "").values()) {
|
|
746
926
|
const current = declarations.get(declaration.name);
|
|
747
927
|
if (!current?.important || declaration.important) {
|
|
748
928
|
declarations.set(declaration.name, { ...declaration, specificity: 1_000_000, order: Infinity });
|
|
749
929
|
}
|
|
750
930
|
}
|
|
751
931
|
const reset = declarations.get("all");
|
|
752
|
-
|
|
932
|
+
const resetValue = reset && resolveVariables(reset.value, customProperties);
|
|
933
|
+
if (reset && CSS_WIDE_KEYWORDS.has(resetValue)) {
|
|
934
|
+
if (dependencies && resetValue === "inherit") dependencies.inheritsParent = true;
|
|
753
935
|
// Expand before resolving font prerequisites. Competing declarations
|
|
754
936
|
// keep their normal importance, specificity and source-order priority.
|
|
755
937
|
for (const name of ALL_PROPERTIES) {
|
|
@@ -763,17 +945,18 @@ export function computeElementStyle(element, parentStyle = null, authorDeclarati
|
|
|
763
945
|
const font = declarations.get("font");
|
|
764
946
|
if (font && (!fontSize || compareDeclarations(font, fontSize) > 0)) {
|
|
765
947
|
const fontValue = resolveVariables(font.value, customProperties);
|
|
948
|
+
if (dependencies && fontValue === "inherit") dependencies.inheritsParent = true;
|
|
766
949
|
const parsed = parseFontShorthand(fontValue);
|
|
767
950
|
if (CSS_WIDE_KEYWORDS.has(fontValue)) setProperty(values, "font-size", fontValue, parentStyle);
|
|
768
951
|
else if (parsed) setProperty(values, "font-size", parsed.size, parentStyle);
|
|
769
952
|
} else if (fontSize) {
|
|
770
|
-
applyDeclaration(values, fontSize, customProperties, parentStyle);
|
|
953
|
+
applyDeclaration(values, fontSize, customProperties, parentStyle, dependencies);
|
|
771
954
|
}
|
|
772
955
|
// light-dark() depends on the used scheme regardless of declaration order.
|
|
773
956
|
const colorScheme = declarations.get("color-scheme");
|
|
774
|
-
if (colorScheme) applyDeclaration(values, colorScheme, customProperties, parentStyle);
|
|
957
|
+
if (colorScheme) applyDeclaration(values, colorScheme, customProperties, parentStyle, dependencies);
|
|
775
958
|
const directionDeclaration = declarations.get("direction");
|
|
776
|
-
if (directionDeclaration) applyDeclaration(values, directionDeclaration, customProperties, parentStyle);
|
|
959
|
+
if (directionDeclaration) applyDeclaration(values, directionDeclaration, customProperties, parentStyle, dependencies);
|
|
777
960
|
// Apply in cascade order so a later shorthand overrides earlier longhands and vice versa.
|
|
778
961
|
const ordered = [...declarations.values()].sort(compareDeclarations);
|
|
779
962
|
// Font-relative lengths such as ch use this element's final font, even
|
|
@@ -781,6 +964,7 @@ export function computeElementStyle(element, parentStyle = null, authorDeclarati
|
|
|
781
964
|
for (const declaration of ordered) {
|
|
782
965
|
if (declaration.name === "font") {
|
|
783
966
|
const fontValue = resolveVariables(declaration.value, customProperties);
|
|
967
|
+
if (dependencies && fontValue === "inherit") dependencies.inheritsParent = true;
|
|
784
968
|
const parsed = parseFontShorthand(fontValue);
|
|
785
969
|
if (CSS_WIDE_KEYWORDS.has(fontValue)) {
|
|
786
970
|
setProperty(values, "font-family", fontValue, parentStyle);
|
|
@@ -795,13 +979,13 @@ export function computeElementStyle(element, parentStyle = null, authorDeclarati
|
|
|
795
979
|
setProperty(values, "font-stretch", parsed.stretch ?? "normal", parentStyle);
|
|
796
980
|
}
|
|
797
981
|
} else if (["font-family", "font-weight", "font-style", "font-stretch", "font-width"].includes(declaration.name)) {
|
|
798
|
-
applyDeclaration(values, declaration, customProperties, parentStyle);
|
|
982
|
+
applyDeclaration(values, declaration, customProperties, parentStyle, dependencies);
|
|
799
983
|
}
|
|
800
984
|
}
|
|
801
985
|
for (const declaration of ordered) {
|
|
802
986
|
if (declaration.name.startsWith("--") || ["font-size", "font-family", "font-weight", "font-style", "font-stretch", "font-width", "color-scheme", "direction"].includes(declaration.name)) continue;
|
|
803
987
|
// The shorthand's size was settled above; its other parts apply in order.
|
|
804
|
-
applyDeclaration(values, declaration.name === "font" ? { ...declaration, name: "font-parts" } : declaration, customProperties, parentStyle);
|
|
988
|
+
applyDeclaration(values, declaration.name === "font" ? { ...declaration, name: "font-parts" } : declaration, customProperties, parentStyle, dependencies);
|
|
805
989
|
}
|
|
806
990
|
// Floats (approximation): laid out as atomic inlines in the line, left floats
|
|
807
991
|
// in order and right floats at the line's end. Absolutely positioned boxes
|
|
@@ -817,8 +1001,10 @@ export function computeElementStyle(element, parentStyle = null, authorDeclarati
|
|
|
817
1001
|
values.borderWidths = Object.freeze(values.borderSpecifiedWidths.map((width, index) =>
|
|
818
1002
|
["none", "hidden"].includes(values.borderStyles[index]) ? 0 : width));
|
|
819
1003
|
values.borderColors = Object.freeze(values.borderColors.map((color) => color ?? values.color));
|
|
820
|
-
|
|
821
|
-
|
|
1004
|
+
// Initial/inherited arrays are already immutable. Setters use copy-on-write;
|
|
1005
|
+
// retain those arrays, but still isolate any mutable caller-owned values.
|
|
1006
|
+
values.borderStyles = Object.isFrozen(values.borderStyles) ? values.borderStyles : Object.freeze([...values.borderStyles]);
|
|
1007
|
+
values.borderSpecifiedWidths = Object.isFrozen(values.borderSpecifiedWidths) ? values.borderSpecifiedWidths : Object.freeze([...values.borderSpecifiedWidths]);
|
|
822
1008
|
// A unitless line-height inherits as a factor of each element's own font size.
|
|
823
1009
|
if (values.lineHeightFactor != null) values.lineHeight = values.lineHeightFactor * values.fontSize;
|
|
824
1010
|
if (values.color === "rgba(0, 0, 0, 0)" && values.backgroundClip === "text" && values.backgroundImage) {
|
|
@@ -838,7 +1024,7 @@ export function computeElementStyle(element, parentStyle = null, authorDeclarati
|
|
|
838
1024
|
values.textDecorations = decorations;
|
|
839
1025
|
if (control === "textarea" || control === "text") applyControlSize(element, control, values);
|
|
840
1026
|
values.margin = Object.freeze(values.margin);
|
|
841
|
-
values.inset = Object.freeze([...values.inset]);
|
|
1027
|
+
values.inset = Object.isFrozen(values.inset) ? values.inset : Object.freeze([...values.inset]);
|
|
842
1028
|
values.padding = Object.freeze(values.padding);
|
|
843
1029
|
return Object.freeze(values);
|
|
844
1030
|
}
|
|
@@ -1298,9 +1484,11 @@ const reportedDeclarationErrors = new Set();
|
|
|
1298
1484
|
* Applies one cascaded declaration. A declaration the engine cannot handle is
|
|
1299
1485
|
* dropped, as an invalid declaration would be, instead of failing the page.
|
|
1300
1486
|
*/
|
|
1301
|
-
function applyDeclaration(style, declaration, customProperties, parentStyle) {
|
|
1487
|
+
function applyDeclaration(style, declaration, customProperties, parentStyle, dependencies = null) {
|
|
1302
1488
|
try {
|
|
1303
|
-
|
|
1489
|
+
const value = resolveVariables(declaration.value, customProperties);
|
|
1490
|
+
if (dependencies && value === "inherit") dependencies.inheritsParent = true;
|
|
1491
|
+
setProperty(style, declaration.name, value, parentStyle);
|
|
1304
1492
|
} catch (error) {
|
|
1305
1493
|
const key = `${declaration.name}: ${declaration.value}`;
|
|
1306
1494
|
if (reportedDeclarationErrors.size < 100 && !reportedDeclarationErrors.has(key)) {
|
|
@@ -2130,7 +2318,11 @@ export function matchSelectors(root, selectors, compiledSelectors = new Map()) {
|
|
|
2130
2318
|
function elementKeys(element) {
|
|
2131
2319
|
const id = element.getAttribute?.("id") || null;
|
|
2132
2320
|
const className = element.getAttribute?.("class");
|
|
2133
|
-
|
|
2321
|
+
// Candidate stamps deduplicate rules/selectors, and ancestor counts balance
|
|
2322
|
+
// repeated keys. Deduplicate long lists to bound repeated bucket walks while
|
|
2323
|
+
// avoiding a Set allocation for the usual short class attribute.
|
|
2324
|
+
let classes = className ? className.split(/\s+/).filter(Boolean) : [];
|
|
2325
|
+
if (classes.length > 16) classes = [...new Set(classes)];
|
|
2134
2326
|
const attrs = [];
|
|
2135
2327
|
for (const attribute of element.attributes ?? []) attrs.push(String(attribute.name).toLowerCase());
|
|
2136
2328
|
const tag = element.localName ?? element.tagName;
|
|
@@ -2509,23 +2701,27 @@ function copyProperty(style, name, source) {
|
|
|
2509
2701
|
}
|
|
2510
2702
|
}
|
|
2511
2703
|
|
|
2704
|
+
// Most inheritance asks for one value (often line-height). Do not serialize
|
|
2705
|
+
// unrelated font families and numeric properties for every element.
|
|
2512
2706
|
function cssValue(style, name) {
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
"
|
|
2516
|
-
"font-
|
|
2517
|
-
"font-
|
|
2518
|
-
"font-
|
|
2519
|
-
"font-
|
|
2520
|
-
"font-
|
|
2521
|
-
"
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
"
|
|
2525
|
-
"
|
|
2526
|
-
"
|
|
2527
|
-
"
|
|
2528
|
-
|
|
2707
|
+
let value;
|
|
2708
|
+
switch (name) {
|
|
2709
|
+
case "background-color": value = style.backgroundColor; break;
|
|
2710
|
+
case "font-family": value = style.fontFamily.map((family) => JSON.stringify(family)).join(", "); break;
|
|
2711
|
+
case "font-size": value = `${style.fontSize}px`; break;
|
|
2712
|
+
case "font-weight": value = String(style.fontWeight); break;
|
|
2713
|
+
case "font-style": value = style.fontStyle; break;
|
|
2714
|
+
case "font-stretch":
|
|
2715
|
+
case "font-width": value = `${style.fontStretch}%`; break;
|
|
2716
|
+
case "line-height": value = style.lineHeightFactor != null ? String(style.lineHeightFactor)
|
|
2717
|
+
: style.lineHeight == null ? "normal" : `${style.lineHeight}px`; break;
|
|
2718
|
+
case "list-style-type": value = style.listStyleType; break;
|
|
2719
|
+
case "pointer-events": value = style.pointerEvents; break;
|
|
2720
|
+
case "user-select": value = style.userSelect; break;
|
|
2721
|
+
case "text-align": value = style.textAlign; break;
|
|
2722
|
+
case "white-space": value = style.whiteSpace; break;
|
|
2723
|
+
}
|
|
2724
|
+
return value ?? style[name];
|
|
2529
2725
|
}
|
|
2530
2726
|
|
|
2531
2727
|
function pixelLength(value) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { createAnimationFrames, supportsFrameCheckpoint } from "../lib/renderer/animation-frames.js";
|
|
3
|
+
|
|
4
|
+
// Bun.version hides the suffix of unsafe 1.4.3 canaries. Require the later
|
|
5
|
+
// 1.4.4 branch and retain timers for unknown prerelease channels.
|
|
6
|
+
test("animation batching requires a microtask-only Bun checkpoint", () => {
|
|
7
|
+
for (const version of ["1.4.2", "1.4.3", "1.4.3-canary.1", "1.4.4-dev.1", "1.4.4+local", "v1.4.4", "unknown", null]) {
|
|
8
|
+
expect(supportsFrameCheckpoint(version)).toBe(false);
|
|
9
|
+
}
|
|
10
|
+
for (const version of ["1.4.4", "1.4.4-canary", "1.4.4-canary.1", "1.5.0"]) expect(supportsFrameCheckpoint(version)).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const frameTest = supportsFrameCheckpoint() ? test : test.skip;
|
|
14
|
+
frameTest("frame snapshots preserve timestamps, checkpoints, cancellation and disposal", () => {
|
|
15
|
+
let now = 0, nextWake = 0;
|
|
16
|
+
const pending = new Map(), events = [], timestamps = [], errors = [];
|
|
17
|
+
const timers = {
|
|
18
|
+
schedule(callback, delay, repeat, timerTask) {
|
|
19
|
+
expect([repeat, timerTask]).toEqual([false, false]);
|
|
20
|
+
pending.set(++nextWake, { callback, delay });
|
|
21
|
+
return nextWake;
|
|
22
|
+
},
|
|
23
|
+
cancel(id) { pending.delete(id); },
|
|
24
|
+
dispose() { pending.clear(); },
|
|
25
|
+
};
|
|
26
|
+
const fire = () => {
|
|
27
|
+
const [id, entry] = pending.entries().next().value;
|
|
28
|
+
pending.delete(id); now += entry.delay; entry.callback();
|
|
29
|
+
};
|
|
30
|
+
const frames = createAnimationFrames({ now: () => now, frameTimers: timers,
|
|
31
|
+
invoke(callback, time) { try { callback(time); } catch (error) { errors.push(error.message); } },
|
|
32
|
+
});
|
|
33
|
+
let cancelled;
|
|
34
|
+
frames.request(time => {
|
|
35
|
+
timestamps.push(time); events.push("first"); now += 40;
|
|
36
|
+
Promise.resolve().then(() => events.push("microtask-one")).then(() => {
|
|
37
|
+
events.push("microtask-two"); frames.cancel(cancelled);
|
|
38
|
+
frames.request(time => {
|
|
39
|
+
events.push("nested"); timestamps.push(time);
|
|
40
|
+
Promise.resolve().then(() => frames.dispose());
|
|
41
|
+
});
|
|
42
|
+
frames.request(() => events.push("disposed"));
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
cancelled = frames.request(() => events.push("cancelled"));
|
|
46
|
+
frames.request(time => { timestamps.push(time); throw new Error("expected"); });
|
|
47
|
+
frames.request(time => { events.push("last"); timestamps.push(time); });
|
|
48
|
+
expect(pending.size).toBe(1); fire();
|
|
49
|
+
expect(events).toEqual(["first", "microtask-one", "microtask-two", "last"]);
|
|
50
|
+
expect(timestamps).toEqual([16, 16, 16]);
|
|
51
|
+
expect(errors).toEqual(["expected"]);
|
|
52
|
+
expect(pending.size).toBe(1); fire();
|
|
53
|
+
expect(events.at(-1)).toBe("nested");
|
|
54
|
+
expect(timestamps.at(-1)).toBe(56);
|
|
55
|
+
expect(pending.size).toBe(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("actual SES frame callbacks match the browser callback order and lifecycle", async () => {
|
|
59
|
+
const child = Bun.spawn([process.execPath, new URL("./fixtures/animation-frames.js", import.meta.url).pathname], {
|
|
60
|
+
stdout: "pipe", stderr: "pipe",
|
|
61
|
+
});
|
|
62
|
+
const [out, error, code] = await Promise.all([
|
|
63
|
+
new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited,
|
|
64
|
+
]);
|
|
65
|
+
expect({ code, error }).toEqual({ code: 0, error: "" });
|
|
66
|
+
expect(out).toContain("animation frame ordering and lifecycle passed");
|
|
67
|
+
}, 15000);
|