diff --git a/packages/calcite-components/src/components/dialog/dialog.e2e.ts b/packages/calcite-components/src/components/dialog/dialog.e2e.ts
index f4e02be30b8..483cc123221 100644
--- a/packages/calcite-components/src/components/dialog/dialog.e2e.ts
+++ b/packages/calcite-components/src/components/dialog/dialog.e2e.ts
@@ -53,12 +53,7 @@ describe("calcite-dialog", () => {
describe("openClose", () => {
openClose("calcite-dialog");
-
- describe("initially open", () => {
- openClose("calcite-dialog", {
- initialToggleValue: true,
- });
- });
+ openClose.initial("calcite-dialog");
});
describe("slots", () => {
diff --git a/packages/calcite-components/src/components/input-time-picker/input-time-picker.e2e.ts b/packages/calcite-components/src/components/input-time-picker/input-time-picker.e2e.ts
index e435103cd53..6232e958b6c 100644
--- a/packages/calcite-components/src/components/input-time-picker/input-time-picker.e2e.ts
+++ b/packages/calcite-components/src/components/input-time-picker/input-time-picker.e2e.ts
@@ -101,10 +101,7 @@ describe("calcite-input-time-picker", () => {
describe("openClose", () => {
openClose("calcite-input-time-picker");
-
- describe.skip("initially open", () => {
- openClose("calcite-input-time-picker", { initialToggleValue: true });
- });
+ openClose.initial("calcite-input-time-picker");
});
it("when set to readOnly, element still focusable but won't display the controls or allow for changing the value", async () => {
diff --git a/packages/calcite-components/src/components/input-time-zone/input-time-zone.e2e.ts b/packages/calcite-components/src/components/input-time-zone/input-time-zone.e2e.ts
index da4b93d456e..1571d349924 100644
--- a/packages/calcite-components/src/components/input-time-zone/input-time-zone.e2e.ts
+++ b/packages/calcite-components/src/components/input-time-zone/input-time-zone.e2e.ts
@@ -8,6 +8,7 @@ import {
formAssociated,
hidden,
labelable,
+ openClose,
reflects,
renders,
t9n,
@@ -128,6 +129,27 @@ describe("calcite-input-time-zone", () => {
t9n(simpleTestProvider);
});
+ describe("openClose", () => {
+ openClose(simpleTestProvider);
+
+ describe("initially open", () => {
+ openClose.initial("calcite-input-time-zone", {
+ beforeContent: async (page) => {
+ await page.emulateTimezone(testTimeZoneItems[0].name);
+
+ // we add the override script this way because `setContent` was already used before this hook, and calling it again will result in an error.
+ await page.evaluate(
+ (supportedTimeZoneOverrideHtml) =>
+ document.body.insertAdjacentHTML("beforeend", supportedTimeZoneOverrideHtml),
+ overrideSupportedTimeZones(""),
+ );
+
+ await page.waitForChanges();
+ },
+ });
+ });
+ });
+
describe("mode", () => {
describe("offset (default)", () => {
describe("selects user's matching time zone offset on initialization", () => {
@@ -154,7 +176,7 @@ describe("calcite-input-time-zone", () => {
const page = await newE2EPage();
await page.emulateTimezone(testTimeZoneItems[0].name);
await page.setContent(
- await overrideSupportedTimeZones(
+ overrideSupportedTimeZones(
html``,
),
);
@@ -172,7 +194,7 @@ describe("calcite-input-time-zone", () => {
const page = await newE2EPage();
await page.emulateTimezone(testTimeZoneItems[0].name);
await page.setContent(
- await overrideSupportedTimeZones(html``),
+ overrideSupportedTimeZones(html``),
);
const input = await page.find("calcite-input-time-zone");
diff --git a/packages/calcite-components/src/components/input-time-zone/input-time-zone.tsx b/packages/calcite-components/src/components/input-time-zone/input-time-zone.tsx
index 6e0dd6459fa..d27f07ef92f 100644
--- a/packages/calcite-components/src/components/input-time-zone/input-time-zone.tsx
+++ b/packages/calcite-components/src/components/input-time-zone/input-time-zone.tsx
@@ -191,6 +191,12 @@ export class InputTimeZone
/** When `true`, displays and positions the component. */
@Prop({ mutable: true, reflect: true }) open = false;
+ @Watch("open")
+ openChanged(): void {
+ // we set the property instead of the attribute to ensure open/close events are emitted properly
+ this.comboboxEl.open = this.open;
+ }
+
/**
* Determines the type of positioning to use for the overlaid content.
*
@@ -487,6 +493,7 @@ export class InputTimeZone
componentDidLoad(): void {
setComponentLoaded(this);
this.overrideSelectedLabelForRegion(this.open);
+ this.openChanged();
}
componentDidRender(): void {
@@ -508,7 +515,6 @@ export class InputTimeZone
onCalciteComboboxChange={this.onComboboxChange}
onCalciteComboboxClose={this.onComboboxClose}
onCalciteComboboxOpen={this.onComboboxOpen}
- open={this.open}
overlayPositioning={this.overlayPositioning}
placeholder={
this.mode === "name"
diff --git a/packages/calcite-components/src/components/modal/modal.e2e.ts b/packages/calcite-components/src/components/modal/modal.e2e.ts
index d1914311e15..dbb4c945c25 100644
--- a/packages/calcite-components/src/components/modal/modal.e2e.ts
+++ b/packages/calcite-components/src/components/modal/modal.e2e.ts
@@ -15,12 +15,7 @@ describe("calcite-modal", () => {
describe("openClose", () => {
openClose("calcite-modal");
-
- describe("initially open", () => {
- openClose("calcite-modal", {
- initialToggleValue: true,
- });
- });
+ openClose.initial("calcite-modal");
});
describe("slots", () => {
diff --git a/packages/calcite-components/src/components/sheet/sheet.e2e.ts b/packages/calcite-components/src/components/sheet/sheet.e2e.ts
index 94b50a9d8df..2c9408b547f 100644
--- a/packages/calcite-components/src/components/sheet/sheet.e2e.ts
+++ b/packages/calcite-components/src/components/sheet/sheet.e2e.ts
@@ -81,12 +81,7 @@ describe("calcite-sheet properties", () => {
describe("openClose", () => {
openClose("calcite-sheet");
-
- describe("initially open", () => {
- openClose("calcite-sheet", {
- initialToggleValue: true,
- });
- });
+ openClose.initial("calcite-sheet");
});
it("sets custom width correctly", async () => {
diff --git a/packages/calcite-components/src/tests/commonTests/interfaces.ts b/packages/calcite-components/src/tests/commonTests/interfaces.ts
index 05ae276147e..a5665015bf8 100644
--- a/packages/calcite-components/src/tests/commonTests/interfaces.ts
+++ b/packages/calcite-components/src/tests/commonTests/interfaces.ts
@@ -11,9 +11,9 @@ export type TagAndPage = {
page: E2EPage;
};
-export type TagOrHTMLWithBeforeContent = {
- tagOrHTML: TagOrHTML;
+export type TagOrHTMLWithBeforeContent = WithBeforeContent<{ tagOrHTML: TagOrHTML }>;
+export type WithBeforeContent = TestContent & {
/**
* Allows for custom setup of the page.
*
diff --git a/packages/calcite-components/src/tests/commonTests/openClose.ts b/packages/calcite-components/src/tests/commonTests/openClose.ts
index fa5386ae8ea..71ceded7fad 100644
--- a/packages/calcite-components/src/tests/commonTests/openClose.ts
+++ b/packages/calcite-components/src/tests/commonTests/openClose.ts
@@ -1,8 +1,8 @@
import { E2EPage } from "@stencil/core/testing";
import { toHaveNoViolations } from "jest-axe";
import { GlobalTestProps, newProgrammaticE2EPage } from "../utils";
-import { getTag, simplePageSetup } from "./utils";
-import { TagOrHTML } from "./interfaces";
+import { getBeforeContent, getTagAndPage, noopBeforeContent } from "./utils";
+import { ComponentTag, ComponentTestSetup, WithBeforeContent } from "./interfaces";
expect.extend(toHaveNoViolations);
@@ -24,11 +24,6 @@ interface OpenCloseOptions {
*/
openPropName?: string;
- /**
- * Indicates the initial value of the toggle property.
- */
- initialToggleValue?: boolean;
-
/**
* Optional argument with functions to simulate user input (mouse or keyboard), to open or close the component.
*/
@@ -40,6 +35,11 @@ interface OpenCloseOptions {
willUseFallback?: boolean;
}
+const defaultOptions: OpenCloseOptions = {
+ openPropName: "open",
+ willUseFallback: false,
+};
+
/**
* Helper to test openClose component setup.
*
@@ -48,201 +48,256 @@ interface OpenCloseOptions {
* @example
*
* describe("openClose", () => {
- * openClose("calcite-combobox", {
- * beforeToggle: {
- * open: async (page) => {
- * await page.keyboard.press("Tab");
- * await page.waitForChanges();
- * },
- * close: async (page) => {
- * await page.keyboard.press("Tab");
- * await page.waitForChanges();
- * },
- * }
- * });
- *
- * describe("initially open", () => {
- * openClose("calcite-combobox", {
- * initialToggleValue: true,
- * beforeToggle: {
- * close: async (page) => {
- * await page.keyboard.press("Tab");
- * await page.waitForChanges();
- * },
- * }
- * }
+ * openClose("calcite-combobox");
+ * openClose.initial("calcite-combobox", {
+ * beforeContent: async (page: E2EPage) => {
+ * // configure page before component is created and appended
+ * }
* });
* });
*
- * @param componentTagOrHTML - The component tag or HTML markup to test against.
+ *
+ * @param {ComponentTestSetup} componentTestSetup - A component tag, html, or the tag and e2e page for setting up a test.
* @param {object} [options] - Additional options to assert.
*/
+export function openClose(componentTestSetup: ComponentTestSetup, options?: OpenCloseOptions): void {
+ const effectiveOptions = { ...defaultOptions, ...options };
+
+ it(`emits with animations enabled`, async () => {
+ const { page, tag } = await getTagAndPage(componentTestSetup);
+ await page.addStyleTag({
+ content: `:root { --calcite-duration-factor: 2; }`,
+ });
-export function openClose(componentTagOrHTML: TagOrHTML, options?: OpenCloseOptions): void {
- const defaultOptions: OpenCloseOptions = {
- initialToggleValue: false,
- openPropName: "open",
- willUseFallback: false,
+ await setUpEventListeners(tag, page);
+ await testOpenCloseEvents({
+ tag,
+ page,
+ openPropName: effectiveOptions.openPropName,
+ beforeToggle: effectiveOptions.beforeToggle,
+ animationsEnabled: !effectiveOptions.willUseFallback,
+ });
+ });
+
+ it(`emits with animations disabled`, async () => {
+ const { page, tag } = await getTagAndPage(componentTestSetup);
+ await page.addStyleTag({
+ content: `:root { --calcite-duration-factor: 0; }`,
+ });
+ await setUpEventListeners(tag, page);
+ await testOpenCloseEvents({
+ animationsEnabled: false,
+ beforeToggle: effectiveOptions.beforeToggle,
+ openPropName: effectiveOptions.openPropName,
+ page,
+ tag,
+ });
+ });
+}
+
+/**
+ * Helper to test openClose component setup on initialization.
+ *
+ * @param componentTag - The component tag to test.
+ * @param options - Additional options to assert.
+ */
+openClose.initial = function openCloseInitial(
+ componentTag: ComponentTag,
+ options?: WithBeforeContent,
+): void {
+ const effectiveOptions = {
+ ...defaultOptions,
+ beforeContent: noopBeforeContent,
+ ...options,
};
- const customizedOptions = { ...defaultOptions, ...options };
- type EventOrderWindow = GlobalTestProps<{ events: string[] }>;
- const eventSequence = setUpEventSequence(componentTagOrHTML);
+ const tag = componentTag;
+ const beforeContent = getBeforeContent(effectiveOptions);
- function setUpEventSequence(componentTagOrHTML: TagOrHTML): string[] {
- const tag = getTag(componentTagOrHTML);
+ it("emits on initialization with animations enabled", async () => {
+ const page = await newProgrammaticE2EPage();
+ await page.addStyleTag({
+ content: `:root { --calcite-duration-factor: 2; }`,
+ });
+ await beforeContent(page);
+ await setUpEventListeners(tag, page);
+ await testOpenCloseEvents({
+ animationsEnabled: true,
+ beforeToggle: effectiveOptions.beforeToggle,
+ openPropName: effectiveOptions.openPropName,
+ page,
+ startOpen: true,
+ tag,
+ });
+ });
- const camelCaseTag = tag.replace(/-([a-z])/g, (lettersAfterHyphen) => lettersAfterHyphen[1].toUpperCase());
- const eventSuffixes = [`BeforeOpen`, `Open`, `BeforeClose`, `Close`];
+ it("emits on initialization with animations disabled", async () => {
+ const page = await newProgrammaticE2EPage();
+ await page.addStyleTag({
+ content: `:root { --calcite-duration-factor: 0; }`,
+ });
+ await beforeContent(page);
+ await setUpEventListeners(tag, page);
+ await testOpenCloseEvents({
+ animationsEnabled: false,
+ beforeToggle: effectiveOptions.beforeToggle,
+ openPropName: effectiveOptions.openPropName,
+ page,
+ startOpen: true,
+ tag,
+ });
+ });
+};
- return eventSuffixes.map((suffix) => `${camelCaseTag}${suffix}`);
- }
+interface TestOpenCloseEventsParams {
+ /**
+ * The component tag to test.
+ */
+ tag: ComponentTag;
- async function setUpPage(componentTagOrHTML: TagOrHTML, page: E2EPage): Promise {
- await page.evaluate(
- (eventSequence: string[], initialToggleValue: boolean, openPropName: string, componentTagOrHTML: string) => {
- const receivedEvents: string[] = [];
+ /**
+ * The E2E page instance.
+ */
+ page: E2EPage;
- (window as EventOrderWindow).events = receivedEvents;
+ /**
+ * The property name used to control the open state of the component.
+ */
+ openPropName: string;
- eventSequence.forEach((eventType) => {
- document.addEventListener(eventType, (event) => receivedEvents.push(event.type));
- });
+ /**
+ * Whether the component should start in the open state.
+ */
+ startOpen?: boolean;
- if (!initialToggleValue) {
- return;
- }
+ /**
+ * Functions to simulate user input (mouse or keyboard) to open or close the component.
+ */
+ beforeToggle?: BeforeToggle;
+
+ /**
+ * Whether animations are enabled.
+ */
+ animationsEnabled: boolean;
+}
+
+async function testOpenCloseEvents({
+ animationsEnabled,
+ beforeToggle,
+ openPropName,
+ page,
+ startOpen = false,
+ tag,
+}: TestOpenCloseEventsParams): Promise {
+ const timestamps: Record = {
+ beforeOpen: undefined,
+ open: undefined,
+ beforeClose: undefined,
+ close: undefined,
+ };
+ const eventSequence = getEventSequence(tag);
+
+ const [beforeOpenEvent, openEvent, beforeCloseEvent, closeEvent] = eventSequence.map((event) => {
+ return page.waitForEvent(event).then((spy) => {
+ timestamps[toOpenCloseName(event)] = Date.now();
+ return spy;
+ });
+ });
+ const [beforeOpenSpy, openSpy, beforeCloseSpy, closeSpy] = await Promise.all(
+ eventSequence.map(async (event) => await page.spyOnEvent(event)),
+ );
+
+ function assertEventSequence(expectedTimesPerEvent: [number, number, number, number]): void {
+ expect(beforeOpenSpy).toHaveReceivedEventTimes(expectedTimesPerEvent[0]);
+ expect(openSpy).toHaveReceivedEventTimes(expectedTimesPerEvent[1]);
+ expect(beforeCloseSpy).toHaveReceivedEventTimes(expectedTimesPerEvent[2]);
+ expect(closeSpy).toHaveReceivedEventTimes(expectedTimesPerEvent[3]);
+ }
+
+ if (startOpen) {
+ await page.evaluate(
+ (openPropName: string, componentTagOrHTML: string) => {
const component = document.createElement(componentTagOrHTML);
component[openPropName] = true;
document.body.append(component);
},
- eventSequence,
- customizedOptions.initialToggleValue,
- customizedOptions.openPropName,
- componentTagOrHTML,
+ openPropName,
+ tag,
);
}
- type OpenCloseName = "beforeOpen" | "open" | "beforeClose" | "close";
+ const element = await page.find(tag);
+ await page.waitForChanges();
- function toOpenCloseName(eventName: string): OpenCloseName {
- return eventName.includes("BeforeOpen")
- ? "beforeOpen"
- : eventName.includes("Open")
- ? "open"
- : eventName.includes("BeforeClose")
- ? "beforeClose"
- : "close";
+ if (!startOpen) {
+ if (beforeToggle) {
+ await beforeToggle.open(page);
+ } else {
+ element.setProperty(openPropName, true);
+ }
}
- async function testOpenCloseEvents(
- componentTagOrHTML: TagOrHTML,
- page: E2EPage,
- animationsEnabled = true,
- ): Promise {
- const tag = getTag(componentTagOrHTML);
- const element = await page.find(tag);
-
- const timestamps: Record = {
- beforeOpen: undefined,
- open: undefined,
- beforeClose: undefined,
- close: undefined,
- };
-
- const [beforeOpenEvent, openEvent, beforeCloseEvent, closeEvent] = eventSequence.map((event) => {
- return page.waitForEvent(event).then((spy) => {
- timestamps[toOpenCloseName(event)] = Date.now();
- return spy;
- });
- });
+ await page.waitForChanges();
+ await beforeOpenEvent;
+ await openEvent;
- const [beforeOpenSpy, openSpy, beforeCloseSpy, closeSpy] = await Promise.all(
- eventSequence.map(async (event) => await element.spyOnEvent(event)),
- );
+ assertEventSequence([1, 1, 0, 0]);
- function assertEventSequence(expectedTimesPerEvent: [number, number, number, number]): void {
- expect(beforeOpenSpy).toHaveReceivedEventTimes(expectedTimesPerEvent[0]);
- expect(openSpy).toHaveReceivedEventTimes(expectedTimesPerEvent[1]);
- expect(beforeCloseSpy).toHaveReceivedEventTimes(expectedTimesPerEvent[2]);
- expect(closeSpy).toHaveReceivedEventTimes(expectedTimesPerEvent[3]);
- }
+ if (startOpen || !beforeToggle) {
+ element.setProperty(openPropName, false);
+ } else {
+ await beforeToggle.close(page);
+ }
- await page.waitForChanges();
+ await page.waitForChanges();
+ await beforeCloseEvent;
+ await closeEvent;
- if (customizedOptions.beforeToggle) {
- await customizedOptions.beforeToggle.open(page);
- } else {
- element.setProperty(customizedOptions.openPropName, true);
- }
+ assertEventSequence([1, 1, 1, 1]);
- await page.waitForChanges();
- await beforeOpenEvent;
- await openEvent;
+ expect(await page.evaluate(() => (window as EventOrderWindow).events)).toEqual(eventSequence);
- assertEventSequence([1, 1, 0, 0]);
+ const delayDeltaThreshold = 100; // smallest internal animation timing used
+ const delayBetweenBeforeOpenAndOpen = timestamps.open - timestamps.beforeOpen;
+ const delayBetweenBeforeCloseAndClose = timestamps.close - timestamps.beforeClose;
- if (customizedOptions.beforeToggle) {
- await customizedOptions.beforeToggle.close(page);
- } else {
- element.setProperty(customizedOptions.openPropName, false);
- }
+ const matcherName = animationsEnabled ? "toBeGreaterThan" : "toBeLessThanOrEqual";
- await page.waitForChanges();
- await beforeCloseEvent;
- await closeEvent;
+ expect(delayBetweenBeforeOpenAndOpen)[matcherName](delayDeltaThreshold);
+ expect(delayBetweenBeforeCloseAndClose)[matcherName](delayDeltaThreshold);
+}
- assertEventSequence([1, 1, 1, 1]);
+type EventOrderWindow = GlobalTestProps<{ events: string[] }>;
- expect(await page.evaluate(() => (window as EventOrderWindow).events)).toEqual(eventSequence);
+function getEventSequence(componentTag: ComponentTag): string[] {
+ const camelCaseTag = componentTag.replace(/-([a-z])/g, (lettersAfterHyphen) => lettersAfterHyphen[1].toUpperCase());
+ const eventSuffixes = [`BeforeOpen`, `Open`, `BeforeClose`, `Close`];
- const delayDeltaThreshold = 100; // smallest internal animation timing used
- const delayBetweenBeforeOpenAndOpen = timestamps.open - timestamps.beforeOpen;
- const delayBetweenBeforeCloseAndClose = timestamps.close - timestamps.beforeClose;
+ return eventSuffixes.map((suffix) => `${camelCaseTag}${suffix}`);
+}
- const matcherName = animationsEnabled ? "toBeGreaterThan" : ("toBeLessThanOrEqual" as const);
+async function setUpEventListeners(componentTag: ComponentTag, page: E2EPage): Promise {
+ await page.evaluate((eventSequence: string[]) => {
+ const receivedEvents: string[] = [];
- expect(delayBetweenBeforeOpenAndOpen)[matcherName](delayDeltaThreshold);
- expect(delayBetweenBeforeCloseAndClose)[matcherName](delayDeltaThreshold);
- }
+ (window as EventOrderWindow).events = receivedEvents;
- if (customizedOptions.initialToggleValue === true) {
- it("emits on initialization with animations enabled", async () => {
- const page = await newProgrammaticE2EPage();
- await page.addStyleTag({
- content: `:root { --calcite-duration-factor: 2; }`,
- });
- await setUpPage(componentTagOrHTML, page);
- await testOpenCloseEvents(componentTagOrHTML, page, !customizedOptions.willUseFallback);
+ eventSequence.forEach((eventType) => {
+ document.addEventListener(eventType, (event) => receivedEvents.push(event.type));
});
+ }, getEventSequence(componentTag));
+}
- it("emits on initialization with animations disabled", async () => {
- const page = await newProgrammaticE2EPage();
- await page.addStyleTag({
- content: `:root { --calcite-duration-factor: 0; }`,
- });
- await setUpPage(componentTagOrHTML, page);
- await testOpenCloseEvents(componentTagOrHTML, page, false);
- });
- } else {
- it(`emits with animations enabled`, async () => {
- const page = await simplePageSetup(componentTagOrHTML);
- await page.addStyleTag({
- content: `:root { --calcite-duration-factor: 2; }`,
- });
- await setUpPage(componentTagOrHTML, page);
- await testOpenCloseEvents(componentTagOrHTML, page, !customizedOptions.willUseFallback);
- });
+type OpenCloseName = "beforeOpen" | "open" | "beforeClose" | "close";
- it(`emits with animations disabled`, async () => {
- const page = await simplePageSetup(componentTagOrHTML);
- await page.addStyleTag({
- content: `:root { --calcite-duration-factor: 0; }`,
- });
- await setUpPage(componentTagOrHTML, page);
- await testOpenCloseEvents(componentTagOrHTML, page, false);
- });
- }
+function toOpenCloseName(eventName: string): OpenCloseName {
+ return eventName.includes("BeforeOpen")
+ ? "beforeOpen"
+ : eventName.includes("Open")
+ ? "open"
+ : eventName.includes("BeforeClose")
+ ? "beforeClose"
+ : "close";
}
diff --git a/packages/calcite-components/src/tests/commonTests/utils.ts b/packages/calcite-components/src/tests/commonTests/utils.ts
index 679418eac61..bc75161907f 100644
--- a/packages/calcite-components/src/tests/commonTests/utils.ts
+++ b/packages/calcite-components/src/tests/commonTests/utils.ts
@@ -8,6 +8,8 @@ import type {
TagAndPage,
TagOrHTMLWithBeforeContent,
BeforeContent,
+ WithBeforeContent,
+ ComponentTestContent,
} from "./interfaces";
expect.extend(toHaveNoViolations);
@@ -57,6 +59,18 @@ export async function getTagAndPage(componentTestSetup: ComponentTestSetup): Pro
return componentTestSetup;
}
+export async function noopBeforeContent(): Promise {
+ /* noop */
+}
+
+export function getBeforeContent(
+ componentTestSetup: WithBeforeContent,
+): BeforeContent {
+ return typeof componentTestSetup === "string"
+ ? noopBeforeContent
+ : componentTestSetup?.beforeContent || noopBeforeContent;
+}
+
export function getTagOrHTMLWithBeforeContent(componentTestSetup: TagOrHTML | TagOrHTMLWithBeforeContent): {
tagOrHTML: TagOrHTML;
beforeContent?: BeforeContent;