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
Original file line number Diff line number Diff line change
Expand Up @@ -549,16 +549,16 @@ describe("calcite-input-number", () => {

it("should emit an event on an interval when ArrowUp/ArrowDown keys are down and stop on key up", async () => {
await page.setContent(html`<calcite-input-number value="0"></calcite-input-number>`);
const calciteInputNumberInput = await page.spyOnEvent("calciteInputNumberInput");
const inputEventSpy = await page.spyOnEvent("calciteInputNumberInput");
const input = await page.find("calcite-input-number");
expect(calciteInputNumberInput).toHaveReceivedEventTimes(0);
expect(inputEventSpy).toHaveReceivedEventTimes(0);
await input.callMethod("setFocus");
await page.waitForChanges();

const eventSpy = await page.spyOnEvent("keydown");
await page.keyboard.down("ArrowUp");
await page.waitForTimeout(delayFor2UpdatesInMs);
await page.waitForEvent("calciteInputNumberInput");
await inputEventSpy.next();

expect(eventSpy).toHaveReceivedEventTimes(1);
expect(eventSpy.lastEvent.defaultPrevented).toBe(true);
Expand All @@ -569,7 +569,7 @@ describe("calcite-input-number", () => {
expect(eventSpy).toHaveReceivedEventTimes(2);
expect(eventSpy.lastEvent.defaultPrevented).toBe(true);

const totalNudgesUp = calciteInputNumberInput.length;
const totalNudgesUp = inputEventSpy.length;
expect(await input.getProperty("value")).toBe(`${totalNudgesUp}`);

await page.keyboard.down("ArrowDown");
Expand All @@ -585,7 +585,7 @@ describe("calcite-input-number", () => {
expect(eventSpy).toHaveReceivedEventTimes(4);
expect(eventSpy.lastEvent.defaultPrevented).toBe(true);

const totalNudgesDown = calciteInputNumberInput.length - totalNudgesUp;
const totalNudgesDown = inputEventSpy.length - totalNudgesUp;
const finalNudgedValue = totalNudgesUp - totalNudgesDown;
expect(await input.getProperty("value")).toBe(`${finalNudgedValue}`);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,21 +145,21 @@ describe("calcite-input-time-picker", () => {

await assertDisplayedTime(page, "04:35 AM");

const openEvent = page.waitForEvent("calciteInputTimePickerOpen");
const openEventSpy = await page.spyOnEvent("calciteInputTimePickerOpen");
inputTimePicker.setProperty("open", true);
await page.waitForChanges();
await openEvent;
await openEventSpy.next();

const hourUpEl = await page.find(`calcite-input-time-picker >>> .${TimePickerCSS.buttonHourUp}`);
const minuteUpEl = await page.find(`calcite-input-time-picker >>> .${TimePickerCSS.buttonMinuteUp}`);

await hourUpEl.click();
await minuteUpEl.click();

const closeEvent = page.waitForEvent("calciteInputTimePickerClose");
const closeEventSpy = await page.spyOnEvent("calciteInputTimePickerClose");
await page.keyboard.press("Escape");
await page.waitForChanges();
await closeEvent;
await closeEventSpy.next();

expect(await inputTimePicker.getProperty("value")).toBe("05:36");
await assertDisplayedTime(page, "05:36 AM");
Expand Down
10 changes: 5 additions & 5 deletions packages/calcite-components/src/components/input/input.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -537,27 +537,27 @@ describe("calcite-input", () => {

it("on input type number, should emit an event on an interval when ArrowUp/ArrowDown keys are down and stop on key up", async () => {
await page.setContent(html`<calcite-input type="number" value="0"></calcite-input>`);
const calciteInputInput = await page.spyOnEvent("calciteInputInput");
const inputEventSpy = await page.spyOnEvent("calciteInputInput");
const input = await page.find("calcite-input");
expect(calciteInputInput).toHaveReceivedEventTimes(0);
expect(inputEventSpy).toHaveReceivedEventTimes(0);
await input.callMethod("setFocus");
await page.waitForChanges();

await page.keyboard.down("ArrowUp");
await page.waitForTimeout(delayFor2UpdatesInMs);
await page.waitForEvent("calciteInputInput");
await inputEventSpy.next();
await page.keyboard.up("ArrowUp");
await page.waitForChanges();

const totalNudgesUp = calciteInputInput.length;
const totalNudgesUp = inputEventSpy.length;
expect(await input.getProperty("value")).toBe(`${totalNudgesUp}`);

await page.keyboard.down("ArrowDown");
await page.waitForTimeout(delayFor2UpdatesInMs);
await page.keyboard.up("ArrowDown");
await page.waitForChanges();

const totalNudgesDown = calciteInputInput.length - totalNudgesUp;
const totalNudgesDown = inputEventSpy.length - totalNudgesUp;
const finalNudgedValue = totalNudgesUp - totalNudgesDown;
expect(await input.getProperty("value")).toBe(`${finalNudgedValue}`);
});
Expand Down
34 changes: 4 additions & 30 deletions packages/calcite-components/src/utils/dom.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @ts-strict-ignore
import { beforeAll, beforeEach, describe, expect, it, Mock, vi } from "vitest";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { ModeName } from "../components/interfaces";
import { html } from "../../support/formatting";
import { waitForAnimationFrame } from "../tests/utils/timing";
Expand Down Expand Up @@ -440,13 +440,9 @@ describe("dom", () => {
*/
describe("transition/animation helpers", () => {
let element: HTMLDivElement;
let onStartCallback: Mock;
let onEndCallback: Mock;

beforeEach(() => {
element = window.document.createElement("div");
onStartCallback = vi.fn();
onEndCallback = vi.fn();
});

const helpers = [whenTransitionDone, whenAnimationDone] as const;
Expand All @@ -469,20 +465,14 @@ describe("dom", () => {
];
element.getAnimations = () => animationsPerCall.shift();

const promise = helper(element, testTransitionOrAnimationName, onStartCallback, onEndCallback);
const promise = helper(element, testTransitionOrAnimationName);
expect(await promiseState(promise)).toHaveProperty("status", "pending");
expect(onStartCallback).toHaveBeenCalled();
expect(onEndCallback).not.toHaveBeenCalled();

controlledPromise.resolve();

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

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

it(`should return a promise that resolves after the ${type} (running frame after call time)`, async () => {
Expand All @@ -498,45 +488,29 @@ describe("dom", () => {
];
element.getAnimations = () => animationsPerCall.shift();

const promise = helper(element, testTransitionOrAnimationName, onStartCallback, onEndCallback);
const promise = helper(element, testTransitionOrAnimationName);

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

await waitForAnimationFrame();

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

controlledPromise.resolve();

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

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

it(`should return a promise that resolves after 0s ${type} or has not started when expected (fallback cases)`, async () => {
const animationsPerCall = [[], []];
element.getAnimations = () => animationsPerCall.shift();

const promise = helper(element, testTransitionOrAnimationName, onStartCallback, onEndCallback);
const promise = helper(element, testTransitionOrAnimationName);
expect(await promiseState(promise)).toHaveProperty("status", "pending");

await waitForAnimationFrame();
expect(onStartCallback).not.toHaveBeenCalled();
expect(onEndCallback).not.toHaveBeenCalled();

await waitForAnimationFrame();
expect(onStartCallback).toHaveBeenCalled();

await waitForAnimationFrame();
expect(onEndCallback).toHaveBeenCalled();

expect(await promiseState(promise)).toHaveProperty("status", "fulfilled");
});
Expand Down
41 changes: 5 additions & 36 deletions packages/calcite-components/src/utils/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -586,47 +586,24 @@ export function isBefore(a: HTMLElement, b: HTMLElement): boolean {
*
* @param targetEl The element to watch for the animation to complete.
* @param animationName The name of the animation to watch for completion.
* @param onStart A callback to run when the animation starts.
* @param onEnd A callback to run when the animation ends or is canceled.
*/
export async function whenAnimationDone(
targetEl: HTMLElement,
animationName: string,
onStart?: () => void,
onEnd?: () => void,
): Promise<void> {
return whenTransitionOrAnimationDone(targetEl, animationName, "animation", onStart, onEnd);
export async function whenAnimationDone(targetEl: HTMLElement, animationName: string): Promise<void> {
return whenTransitionOrAnimationDone(targetEl, animationName, "animation");
}

/**
* This util helps determine when a transition has completed.
*
* @param targetEl The element to watch for the transition to complete.
* @param transitionProp The name of the transition to watch for completion.
* @param onStart A callback to run when the transition starts.
* @param onEnd A callback to run when the transition ends or is canceled.
*/
export async function whenTransitionDone(
targetEl: HTMLElement,
transitionProp: string,
onStart?: () => void,
onEnd?: () => void,
): Promise<void> {
return whenTransitionOrAnimationDone(targetEl, transitionProp, "transition", onStart, onEnd);
export async function whenTransitionDone(targetEl: HTMLElement, transitionProp: string): Promise<void> {
return whenTransitionOrAnimationDone(targetEl, transitionProp, "transition");
}

type TransitionOrAnimation = "transition" | "animation";
type TransitionOrAnimationInstance = CSSTransition | Animation;

async function triggerFallbackStartEnd(start: () => void, end: () => void): Promise<void> {
// offset callbacks by a frame to simulate event counterparts
await nextFrame();
start?.();

await nextFrame();
end?.();
}

function findAnimation(
targetEl: HTMLElement,
type: TransitionOrAnimation,
Expand All @@ -644,15 +621,11 @@ function findAnimation(
* @param targetEl The element to watch for the transition or animation to complete.
* @param transitionPropOrAnimationName The transition or animation property to watch for completion.
* @param type The type of property to watch for completion. Defaults to "transition".
* @param onStart A callback to run when the transition or animation starts.
* @param onEnd A callback to run when the transition or animation ends or is canceled.
*/
export async function whenTransitionOrAnimationDone(
targetEl: HTMLElement,
transitionPropOrAnimationName: string,
type: TransitionOrAnimation,
onStart?: () => void,
onEnd?: () => void,
): Promise<void> {
let anim = findAnimation(targetEl, type, transitionPropOrAnimationName);

Expand All @@ -663,17 +636,13 @@ export async function whenTransitionOrAnimationDone(
}

if (!anim) {
return triggerFallbackStartEnd(onStart, onEnd);
return;
}

onStart?.();

try {
await anim.finished;
} catch {
// swallow error if canceled
} finally {
onEnd?.();
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { describe, expect, it, vi } from "vitest";
import { JsxNode, LitElement } from "@arcgis/lumina";
import { mount } from "@arcgis/lumina-compiler/testing";
import { waitForAnimationFrame } from "../tests/utils/timing";
import { createControlledPromise } from "../tests/utils/promises";
import { onToggleOpenCloseComponent } from "./openCloseComponent";

describe("openCloseComponent", () => {
describe("toggleOpenCloseComponent", () => {
it("emits beforeOpen/beforeClose events when the transition starts and open/close events when the transition is done", async () => {
const emittedEvents: string[] = [];

class Test extends LitElement {
open = false;

transitionEl!: HTMLDivElement;
openProp = "open";
transitionProp = "opacity" as const;

onBeforeOpen(): void {
emittedEvents.push("beforeOpen");
}

onOpen(): void {
emittedEvents.push("open");
}

onBeforeClose(): void {
emittedEvents.push("beforeClose");
}

onClose(): void {
emittedEvents.push("close");
}

override render(): JsxNode {
return (
<div
ref={(el) => {
if (!el) {
return;
}
this.transitionEl = el;
}}
/>
);
}
}

const { component } = await mount(Test);

expect(emittedEvents).toEqual([]);

const openingControlledPromise = createControlledPromise<void>();

const getAnimationsSpy = vi.spyOn(component.transitionEl, "getAnimations");

getAnimationsSpy.mockImplementation(() => [
{
transitionProperty: "opacity",
finished: openingControlledPromise.promise,
} as unknown as CSSTransition,
]);

component.open = true;
onToggleOpenCloseComponent(component);
await waitForAnimationFrame();
expect(emittedEvents).toEqual(["beforeOpen"]);

openingControlledPromise.resolve();
await waitForAnimationFrame();
expect(emittedEvents).toEqual(["beforeOpen", "open"]);

const closingControlledPromise = createControlledPromise<void>();
getAnimationsSpy.mockImplementation(() => [
{
transitionProperty: "opacity",
finished: closingControlledPromise.promise,
} as unknown as CSSTransition,
]);

component.open = false;
onToggleOpenCloseComponent(component);
await waitForAnimationFrame();

expect(emittedEvents).toEqual(["beforeOpen", "open", "beforeClose"]);

closingControlledPromise.resolve();
await waitForAnimationFrame();

expect(emittedEvents).toEqual(["beforeOpen", "open", "beforeClose", "close"]);
});
});
});
Loading
Loading