Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: fix regression causing open/close events from emitting in proper order #9560

Merged
merged 7 commits into from
Jun 12, 2024
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export interface AnimationEventDispatcher {
(element: HTMLElement, type: "animationstart" | "animationend", animationName: string): void;
}

/**
* Must be called in a `beforeEach` block to create a animation event dispatcher.
*/
export function createAnimationEventDispatcher(): AnimationEventDispatcher {
// we define AnimationEvent since JSDOM doesn't support it yet -
class AnimationEvent extends window.Event {
elapsedTime: number;

animationName: string;

constructor(type: string, eventInitDict: EventInit & Partial<{ elapsedTime: number; animationName: string }>) {
super(type, eventInitDict);
this.elapsedTime = eventInitDict.elapsedTime;
this.animationName = eventInitDict.animationName;
}
}

return (element: HTMLElement, type: "animationstart" | "animationend", animationName: string): void => {
element.dispatchEvent(new AnimationEvent(type, { animationName }));
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Mocks `getComputedStyle` to return the provided values for the provided element.
* This is needed due to JSDOM issue with getComputedStyle - https://github.com/jsdom/jsdom/issues/3090
*
* @param element
* @param fakeComputedStyle
*/
export function mockGetComputedStyleFor(element: Element, fakeComputedStyle: Partial<CSSStyleDeclaration>): void {
jest.spyOn(window, "getComputedStyle").mockImplementation((el: Element): CSSStyleDeclaration => {
if (el === element) {
return fakeComputedStyle as CSSStyleDeclaration;
}
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export interface TransitionEventDispatcher {
(element: HTMLElement, type: "transitionstart" | "transitionend", propertyName: string): void;
}

/**
* Must be called in a `beforeEach` block to create a transition event dispatcher.
*/
export function createTransitionEventDispatcher(): TransitionEventDispatcher {
// we define TransitionEvent since JSDOM doesn't support it yet - https://github.com/jsdom/jsdom/issues/1781
class TransitionEvent extends window.Event {
elapsedTime: number;

propertyName: string;

constructor(type: string, eventInitDict: EventInit & Partial<{ elapsedTime: number; propertyName: string }>) {
super(type, eventInitDict);
this.elapsedTime = eventInitDict.elapsedTime;
this.propertyName = eventInitDict.propertyName;
}
}

return (element: HTMLElement, type: "transitionstart" | "transitionend", propertyName: string): void => {
element.dispatchEvent(new TransitionEvent(type, { propertyName }));
};
}
223 changes: 147 additions & 76 deletions packages/calcite-components/src/utils/dom.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { JSDOM } from "jsdom";
import { ModeName } from "../../src/components/interfaces";
import { ModeName } from "../components/interfaces";
import { html } from "../../support/formatting";
import { createTransitionEventDispatcher, TransitionEventDispatcher } from "../tests/spec-helpers/transitionEvents";
import { AnimationEventDispatcher, createAnimationEventDispatcher } from "../tests/spec-helpers/animationEvents";
import { mockGetComputedStyleFor } from "../tests/spec-helpers/computedStyle";
import { guidPattern } from "./guid.spec";
import {
ensureId,
focusElementInGroup,
Expand All @@ -23,7 +26,6 @@ import {
whenAnimationDone,
whenTransitionDone,
} from "./dom";
import { guidPattern } from "./guid.spec";

describe("dom", () => {
describe("getElementProp()", () => {
Expand Down Expand Up @@ -623,127 +625,196 @@ describe("dom", () => {
}

describe("whenTransitionDone", () => {
let dispatchTransitionEvent: (
element: HTMLElement,
type: "transitionstart" | "transitionend",
propertyName: string,
) => void;
const testProp = "opacity";
const testDuration = "0.5s";

let element: HTMLDivElement;
let dispatchTransitionEvent: TransitionEventDispatcher;
let onStartCallback: jest.Mock<any, any, any>;
let onEndCallback: jest.Mock<any, any, any>;

beforeEach(() => {
// we clobber Stencil's custom Mock document implementation
const { window: win } = new JSDOM();
dispatchTransitionEvent = createTransitionEventDispatcher();
element = window.document.createElement("div");
onStartCallback = jest.fn();
onEndCallback = jest.fn();
});

it("should return a promise that resolves after the transition", async () => {
const testTransition = `${testProp} ${testDuration} ease 0s`;

element.style.transition = testTransition;
window.document.body.append(element);
mockGetComputedStyleFor(element, {
transition: testTransition,
transitionDuration: testDuration,
transitionProperty: testProp,
});

// eslint-disable-next-line no-global-assign -- overriding to make window references use JSDOM (which is a subset, hence the type cast)
window = win as any as Window & typeof globalThis;
const promise = whenTransitionDone(element, testProp, onStartCallback, onEndCallback);
element.style.opacity = "0";

// we define TransitionEvent since JSDOM doesn't support it yet - https://github.com/jsdom/jsdom/issues/1781
class TransitionEvent extends window.Event {
elapsedTime: number;
expect(await promiseState(promise)).toHaveProperty("status", "pending");
expect(onStartCallback).not.toHaveBeenCalled();
expect(onEndCallback).not.toHaveBeenCalled();

propertyName: string;
dispatchTransitionEvent(element, "transitionstart", testProp);

constructor(type: string, eventInitDict: EventInit & Partial<{ elapsedTime: number; propertyName: string }>) {
super(type, eventInitDict);
this.elapsedTime = eventInitDict.elapsedTime;
this.propertyName = eventInitDict.propertyName;
}
}
expect(await promiseState(promise)).toHaveProperty("status", "pending");
expect(onStartCallback).toHaveBeenCalled();
expect(onEndCallback).not.toHaveBeenCalled();

dispatchTransitionEvent(element, "transitionend", testProp);

dispatchTransitionEvent = (
element: HTMLElement,
type: "transitionstart" | "transitionend",
propertyName: string,
): void => {
element.dispatchEvent(new TransitionEvent(type, { propertyName }));
};
expect(await promiseState(promise)).toHaveProperty("status", "pending");
expect(onStartCallback).toHaveBeenCalled();
expect(onEndCallback).toHaveBeenCalled();

expect(await promiseState(promise)).toHaveProperty("status", "fulfilled");
expect(onStartCallback).toHaveBeenCalled();
expect(onEndCallback).toHaveBeenCalled();
});

it("should return a promise that resolves after the transition", async () => {
const element = window.document.createElement("div");
const testProp = "opacity";
const testDuration = "0.5s";
it("should return a promise that resolves after 0s transition", async () => {
const testDuration = "0s"; // shadows the outer testDuration
const testTransition = `${testProp} ${testDuration} ease 0s`;

element.style.transition = testTransition;

// need to mock due to JSDOM issue with getComputedStyle - https://github.com/jsdom/jsdom/issues/3090
window.getComputedStyle = jest.fn().mockReturnValue({
window.document.body.append(element);
mockGetComputedStyleFor(element, {
transition: testTransition,
transitionDuration: testDuration,
transitionProperty: testProp,
});
window.document.body.append(element);

const promise = whenTransitionDone(element, "opacity");
const promise = whenTransitionDone(element, testProp, onStartCallback, onEndCallback);
element.style.opacity = "0";
expect(await promiseState(promise)).toHaveProperty("status", "pending");
expect(onStartCallback).toHaveBeenCalled();
expect(onEndCallback).toHaveBeenCalled();

dispatchTransitionEvent(element, "transitionstart", "opacity");
expect(await promiseState(promise)).toHaveProperty("status", "pending");
expect(await promiseState(promise)).toHaveProperty("status", "fulfilled");
});

it("should return a promise that resolves when called and transition has not started when expected", async () => {
const testTransition = `${testProp} ${testDuration} ease 0s`;

dispatchTransitionEvent(element, "transitionend", "opacity");
element.style.transition = testTransition;
window.document.body.append(element);
mockGetComputedStyleFor(element, {
transition: testTransition,
transitionDuration: testDuration,
transitionProperty: testProp,
});

const promise = whenTransitionDone(element, testProp, onStartCallback, onEndCallback);
element.style.opacity = "0";
expect(await promiseState(promise)).toHaveProperty("status", "pending");
expect(onStartCallback).not.toHaveBeenCalled();
expect(onEndCallback).not.toHaveBeenCalled();

await new Promise((resolve) => setTimeout(resolve, 500));

expect(await promiseState(promise)).toHaveProperty("status", "fulfilled");
expect(onStartCallback).toHaveBeenCalled();
expect(onEndCallback).toHaveBeenCalled();
});
});

describe("whenAnimationDone", () => {
let dispatchAnimationEvent: (
element: HTMLElement,
type: "animationstart" | "animationend",
animationName: string,
) => void;
const testAnimationName = "fade";
const testDuration = "0.5s";

let element: HTMLDivElement;
let dispatchAnimationEvent: AnimationEventDispatcher;
let onStartCallback: jest.Mock<any, any, any>;
let onEndCallback: jest.Mock<any, any, any>;

beforeEach(() => {
// we clobber Stencil's custom Mock document implementation
const { window: win } = new JSDOM();
dispatchAnimationEvent = createAnimationEventDispatcher();
element = window.document.createElement("div");
onStartCallback = jest.fn();
onEndCallback = jest.fn();
});

it("should return a promise that resolves after the animation", async () => {
const testAnimation = `${testAnimationName} ${testDuration} ease 0s`;

// eslint-disable-next-line no-global-assign -- overriding to make window references use JSDOM (which is a subset, hence the type cast)
window = win as any as Window & typeof globalThis;
element.style.animation = testAnimation;
window.document.body.append(element);
mockGetComputedStyleFor(element, {
animation: testAnimation,
animationDuration: testDuration,
animationName: testAnimationName,
});

// we define AnimationEvent since JSDOM doesn't support it yet -
const promise = whenAnimationDone(element, testAnimationName, onStartCallback, onEndCallback);
element.style.animationName = "none";

class AnimationEvent extends window.Event {
elapsedTime: number;
expect(await promiseState(promise)).toHaveProperty("status", "pending");
expect(onStartCallback).not.toHaveBeenCalled();
expect(onEndCallback).not.toHaveBeenCalled();

animationName: string;
dispatchAnimationEvent(element, "animationstart", testAnimationName);

constructor(type: string, eventInitDict: EventInit & Partial<{ elapsedTime: number; animationName: string }>) {
super(type, eventInitDict);
this.elapsedTime = eventInitDict.elapsedTime;
this.animationName = eventInitDict.animationName;
}
}
expect(await promiseState(promise)).toHaveProperty("status", "pending");
expect(onStartCallback).toHaveBeenCalled();
expect(onEndCallback).not.toHaveBeenCalled();

dispatchAnimationEvent = (
element: HTMLElement,
type: "animationstart" | "animationend",
animationName: string,
): void => {
element.dispatchEvent(new AnimationEvent(type, { animationName }));
};
dispatchAnimationEvent(element, "animationend", testAnimationName);

expect(await promiseState(promise)).toHaveProperty("status", "pending");
expect(onStartCallback).toHaveBeenCalled();
expect(onEndCallback).toHaveBeenCalled();

expect(await promiseState(promise)).toHaveProperty("status", "fulfilled");
expect(onStartCallback).toHaveBeenCalled();
expect(onEndCallback).toHaveBeenCalled();
});

it("should return a promise that resolves after the animation", async () => {
const element = window.document.createElement("div");
const testAnimationName = "fade";
const testDuration = "0.5s";
it("should return a promise that resolves after 0s animation", async () => {
const testDuration = "0s"; // shadows the outer testDuration
const testAnimation = `${testAnimationName} ${testDuration} ease 0s`;

element.style.animation = `${testAnimationName} ${testDuration} ease 0s`;
element.style.animation = testAnimation;
window.document.body.append(element);
mockGetComputedStyleFor(element, {
animation: testAnimation,
animationDuration: testDuration,
animationName: testAnimationName,
});

const promise = whenAnimationDone(element, testAnimationName);
const promise = whenAnimationDone(element, testAnimationName, onStartCallback, onEndCallback);
element.style.animationName = "none";
expect(await promiseState(promise)).toHaveProperty("status", "pending");
expect(onStartCallback).toHaveBeenCalled();
expect(onEndCallback).toHaveBeenCalled();

dispatchAnimationEvent(element, "animationstart", testAnimationName);
expect(await promiseState(promise)).toHaveProperty("status", "pending");
expect(await promiseState(promise)).toHaveProperty("status", "fulfilled");
});

dispatchAnimationEvent(element, "animationend", testAnimationName);
it("should return a promise that resolves when called and animation has not started when expected", async () => {
const testAnimation = `${testAnimationName} ${testDuration} ease 0s`;

element.style.animation = testAnimation;
window.document.body.append(element);
mockGetComputedStyleFor(element, {
animation: testAnimation,
animationDuration: testDuration,
animationName: testAnimationName,
});

const promise = whenAnimationDone(element, testAnimationName, onStartCallback, onEndCallback);
element.style.animationName = "none";
expect(await promiseState(promise)).toHaveProperty("status", "pending");
expect(onStartCallback).not.toHaveBeenCalled();
expect(onEndCallback).not.toHaveBeenCalled();

await new Promise((resolve) => setTimeout(resolve, 500));

expect(await promiseState(promise)).toHaveProperty("status", "fulfilled");
expect(onStartCallback).toHaveBeenCalled();
expect(onEndCallback).toHaveBeenCalled();
});
});
});
Loading
Loading