Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 75 additions & 16 deletions packages/core/src/runtime/init.timelineHidesClip.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,46 @@
import { afterEach, describe, expect, it } from "vitest";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import gsap from "gsap";
import { initSandboxRuntimeModular } from "./init";
import type { RuntimeTimelineLike } from "./types";

// Importing gsap installs it on window; a test opts in by setting window.gsap itself.
delete window.gsap;

// jsdom keeps a declaration's !important when a value is assigned over it; browsers drop it.
const styleProto = Object.getPrototypeOf(
document.createElement("div").style,
) as CSSStyleDeclaration;
const visibilityAccessor = Object.getOwnPropertyDescriptor(styleProto, "visibility")!;
const setProperty = styleProto.setProperty;
beforeAll(() => {
Object.defineProperty(styleProto, "visibility", {
...visibilityAccessor,
set(this: CSSStyleDeclaration, value: string) {
this.removeProperty("visibility");
visibilityAccessor.set!.call(this, value);
},
});
styleProto.setProperty = function (this: CSSStyleDeclaration, name, value, priority) {
if (!priority) this.removeProperty(name);
setProperty.call(this, name, value, priority ?? "");
};
});
afterAll(() => {
Object.defineProperty(styleProto, "visibility", visibilityAccessor);
styleProto.setProperty = setProperty;
});

function mountClipHiddenByTimeline() {
document.body.innerHTML =
`<div id="root" data-composition-id="main" data-root="true" data-start="0" data-duration="5" data-width="640" data-height="360">` +
`<div id="clip" class="clip" data-start="0" data-duration="5"></div>` +
`<div id="late" class="clip" data-start="1" data-duration="4"></div>` +
`<div id="short" class="clip" data-start="0" data-duration="4"></div>` +
`</div>`;
const tl = gsap.timeline({ paused: true });
tl.set("#clip", { visibility: "hidden" }, 2.67);
tl.set("#late", { visibility: "hidden" }, 2.67);
tl.set("#short", { visibility: "hidden" }, 2.67);
window.__timelines = { main: tl as unknown as RuntimeTimelineLike };
const parent = window.parent;
Object.defineProperty(window, "parent", { configurable: true, value: {} });
Expand All @@ -23,7 +49,11 @@ function mountClipHiddenByTimeline() {
} finally {
Object.defineProperty(window, "parent", { configurable: true, value: parent });
}
return { clip: document.getElementById("clip")!, late: document.getElementById("late")! };
return {
clip: document.getElementById("clip")!,
late: document.getElementById("late")!,
short: document.getElementById("short")!,
};
}

describe("a timeline set that hides a timed clip", () => {
Expand All @@ -37,28 +67,57 @@ describe("a timeline set that hides a timed clip", () => {
};

for (const [name, seek] of Object.entries(paths)) {
const visibilityAt = (times: number[]) => {
const { clip, late } = mountClipHiddenByTimeline();
// Each clip's visibility after each seek: clip (starts at 0), late (starts at 1), short (ends at 4).
const seenAt = (times: number[]) => {
const { clip, late, short } = mountClipHiddenByTimeline();
return times.map((t) => {
seek(t);
return `${clip.style.visibility}/${late.style.visibility}`;
return [clip, late, short].map((el) => el.style.visibility[0]).join("");
});
};

it(`hides the clip from its position on the ${name} path`, () => {
expect(visibilityAt([0.5, 1.5, 2.6, 2.7, 4, 2.6, 1.5])).toEqual([
"visible/hidden",
"visible/visible",
"visible/visible",
"hidden/hidden",
"hidden/hidden",
"visible/visible",
"visible/visible",
it(`hides each clip from its position on the ${name} path`, () => {
expect(seenAt([0.5, 1.5, 2.6, 2.7, 4, 2.6, 1.5])).toEqual([
"vhv",
"vvv",
"vvv",
"hhh",
"hhh",
"vvv",
"vvv",
]);
});

it(`hides the clip on a first seek past its position on the ${name} path`, () => {
expect(visibilityAt([3]).map((seen) => seen.split("/")[0])).toEqual(["hidden"]);
it(`hides each clip on a first seek past its position on the ${name} path`, () => {
expect(seenAt([3])).toEqual(["hhh"]);
});

it(`hides a clip after a seek from before its window straight past its position on the ${name} path`, () => {
expect(seenAt([0.5, 3, 1.5])).toEqual(["vhv", "hhh", "vvv"]);
});

it(`hides a clip when its window is left and re-entered past its position on the ${name} path`, () => {
expect(seenAt([2, 4.5, 3])).toEqual(["vvv", "hhh", "hhh"]);
});
}

for (const [name, seek] of Object.entries(paths)) {
it(`hides a clip a tween left at inherit once its window has ended on the ${name} path`, () => {
document.body.innerHTML =
`<div data-composition-id="main" data-root="true" data-start="0" data-duration="5" data-width="640" data-height="360">` +
`<div id="c" class="clip" data-start="0" data-duration="2"></div></div>`;
const tl = gsap.timeline({ paused: true });
tl.to("#c", { autoAlpha: 0.5, duration: 0.5 }, 1);
window.__timelines = { main: tl as unknown as RuntimeTimelineLike };
const parent = window.parent;
Object.defineProperty(window, "parent", { configurable: true, value: {} });
try {
initSandboxRuntimeModular();
} finally {
Object.defineProperty(window, "parent", { configurable: true, value: parent });
}
seek(3);
expect(document.getElementById("c")!.style.visibility).toBe("hidden");
});
}

Expand Down
17 changes: 11 additions & 6 deletions packages/core/src/runtime/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3088,6 +3088,7 @@ export function initSandboxRuntimeModular(): void {
// plain inline hidden on a clip in its window is the script's, except on a clip authored that way.
const clipsSeen = new WeakSet<Element>();
const authoredInlineHidden = new WeakSet<Element>();
const scriptVisibility = new WeakMap<Element, string>();
const hiddenByScript = (el: StylableElement): boolean =>
el.style.visibility === "hidden" &&
el.style.getPropertyPriority("visibility") !== "important" &&
Expand Down Expand Up @@ -3231,12 +3232,16 @@ export function initSandboxRuntimeModular(): void {
clipsSeen.add(rawNode);
if (rawNode.style.visibility === "hidden") authoredInlineHidden.add(rawNode);
}
if (!isRuntimeStamped && !(isVisibleNow && hiddenByScript(rawNode)))
rawNode.style.setProperty(
"visibility",
isVisibleNow ? "visible" : "hidden",
isVisibleNow ? "" : "important",
);
if (!isRuntimeStamped && !(isVisibleNow && hiddenByScript(rawNode))) {
const style = rawNode.style;
const passHid = style.getPropertyPriority("visibility") === "important";
if (!isVisibleNow && !passHid && !authoredInlineHidden.has(rawNode))
scriptVisibility.set(rawNode, style.getPropertyValue("visibility"));
const shown = isVisibleNow
? (passHid && scriptVisibility.get(rawNode)) || "visible"
: "hidden";
style.setProperty("visibility", shown, isVisibleNow ? "" : "important");
}
rawNode.toggleAttribute(CLIP_HIDDEN_ATTR, !isRuntimeStamped && !isVisibleNow);
if (!isRuntimeStamped && !isVisibleNow && !clipsStyledVisible.has(rawNode))
clipsNeverStyledVisible.add(rawNode);
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/runtime/timedClipHide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ export function showHiddenClips(only: (el: HTMLElement) => boolean = () => true)
export function showClips(hidden: Array<Element & ElementCSSInlineStyle>): () => void {
const putMarksBack = hidden.map((el) => revealClip(el));
return () => {
for (const el of hidden) el.style.setProperty("visibility", "hidden", "important");
for (const el of hidden) {
if (el.style.getPropertyValue("visibility") !== "hidden")
el.style.setProperty("visibility", "hidden", "important");
}
for (const putMarkBack of putMarksBack) putMarkBack();
};
}
Expand Down
Loading