From 9306eefc9a7faf22f7f9421739d5e0a1cf3c91b1 Mon Sep 17 00:00:00 2001 From: JC Franco Date: Thu, 5 Sep 2024 13:35:36 -0700 Subject: [PATCH 1/4] fix(input-time-zone): fix duplicate open/close event emitting --- .../input-time-zone/input-time-zone.e2e.ts | 9 ++++ .../input-time-zone/input-time-zone.tsx | 8 +++- .../src/tests/commonTests/openClose.ts | 46 +++++++++---------- 3 files changed, 38 insertions(+), 25 deletions(-) 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..86ed1dbb7a3 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,14 @@ describe("calcite-input-time-zone", () => { t9n(simpleTestProvider); }); + describe("openClose", () => { + openClose(html``); + + describe.skip("initially open", () => { + openClose(html``, { initialToggleValue: true }); + }); + }); + describe("mode", () => { describe("offset (default)", () => { describe("selects user's matching time zone offset on initialization", () => { 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/tests/commonTests/openClose.ts b/packages/calcite-components/src/tests/commonTests/openClose.ts index fa5386ae8ea..498ae278bae 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 { getTag, getTagAndPage } from "./utils"; +import { ComponentTag, TagOrHTML } from "./interfaces"; expect.extend(toHaveNoViolations); @@ -87,20 +87,19 @@ export function openClose(componentTagOrHTML: TagOrHTML, options?: OpenCloseOpti const customizedOptions = { ...defaultOptions, ...options }; type EventOrderWindow = GlobalTestProps<{ events: string[] }>; - const eventSequence = setUpEventSequence(componentTagOrHTML); + const tag = getTag(componentTagOrHTML); + const eventSequence = setUpEventSequence(tag); - function setUpEventSequence(componentTagOrHTML: TagOrHTML): string[] { - const tag = getTag(componentTagOrHTML); - - const camelCaseTag = tag.replace(/-([a-z])/g, (lettersAfterHyphen) => lettersAfterHyphen[1].toUpperCase()); + function setUpEventSequence(componentTag: ComponentTag): string[] { + const camelCaseTag = componentTag.replace(/-([a-z])/g, (lettersAfterHyphen) => lettersAfterHyphen[1].toUpperCase()); const eventSuffixes = [`BeforeOpen`, `Open`, `BeforeClose`, `Close`]; return eventSuffixes.map((suffix) => `${camelCaseTag}${suffix}`); } - async function setUpPage(componentTagOrHTML: TagOrHTML, page: E2EPage): Promise { + async function setUpPage(componentTag: ComponentTag, page: E2EPage): Promise { await page.evaluate( - (eventSequence: string[], initialToggleValue: boolean, openPropName: string, componentTagOrHTML: string) => { + (eventSequence: string[], initialToggleValue: boolean, openPropName: string, componentTag: string) => { const receivedEvents: string[] = []; (window as EventOrderWindow).events = receivedEvents; @@ -113,7 +112,7 @@ export function openClose(componentTagOrHTML: TagOrHTML, options?: OpenCloseOpti return; } - const component = document.createElement(componentTagOrHTML); + const component = document.createElement(componentTag); component[openPropName] = true; document.body.append(component); @@ -121,7 +120,7 @@ export function openClose(componentTagOrHTML: TagOrHTML, options?: OpenCloseOpti eventSequence, customizedOptions.initialToggleValue, customizedOptions.openPropName, - componentTagOrHTML, + componentTag, ); } @@ -138,12 +137,11 @@ export function openClose(componentTagOrHTML: TagOrHTML, options?: OpenCloseOpti } async function testOpenCloseEvents( - componentTagOrHTML: TagOrHTML, + componentTag: ComponentTag, page: E2EPage, animationsEnabled = true, ): Promise { - const tag = getTag(componentTagOrHTML); - const element = await page.find(tag); + const element = await page.find(componentTag); const timestamps: Record = { beforeOpen: undefined, @@ -214,8 +212,8 @@ export function openClose(componentTagOrHTML: TagOrHTML, options?: OpenCloseOpti await page.addStyleTag({ content: `:root { --calcite-duration-factor: 2; }`, }); - await setUpPage(componentTagOrHTML, page); - await testOpenCloseEvents(componentTagOrHTML, page, !customizedOptions.willUseFallback); + await setUpPage(tag, page); + await testOpenCloseEvents(tag, page, !customizedOptions.willUseFallback); }); it("emits on initialization with animations disabled", async () => { @@ -223,26 +221,26 @@ export function openClose(componentTagOrHTML: TagOrHTML, options?: OpenCloseOpti await page.addStyleTag({ content: `:root { --calcite-duration-factor: 0; }`, }); - await setUpPage(componentTagOrHTML, page); - await testOpenCloseEvents(componentTagOrHTML, page, false); + await setUpPage(tag, page); + await testOpenCloseEvents(tag, page, false); }); } else { it(`emits with animations enabled`, async () => { - const page = await simplePageSetup(componentTagOrHTML); + const { page, tag } = await getTagAndPage(componentTagOrHTML); await page.addStyleTag({ content: `:root { --calcite-duration-factor: 2; }`, }); - await setUpPage(componentTagOrHTML, page); - await testOpenCloseEvents(componentTagOrHTML, page, !customizedOptions.willUseFallback); + await setUpPage(tag, page); + await testOpenCloseEvents(tag, page, !customizedOptions.willUseFallback); }); it(`emits with animations disabled`, async () => { - const page = await simplePageSetup(componentTagOrHTML); + const { page, tag } = await getTagAndPage(componentTagOrHTML); await page.addStyleTag({ content: `:root { --calcite-duration-factor: 0; }`, }); - await setUpPage(componentTagOrHTML, page); - await testOpenCloseEvents(componentTagOrHTML, page, false); + await setUpPage(tag, page); + await testOpenCloseEvents(tag, page, false); }); } } From 9cb6a1c72b1141618c9dd7e8c6dfa08f163505c7 Mon Sep 17 00:00:00 2001 From: JC Franco Date: Thu, 5 Sep 2024 17:21:30 -0700 Subject: [PATCH 2/4] update openClose to support test provider as argument --- .../input-time-zone/input-time-zone.e2e.ts | 16 ++++++----- .../src/tests/commonTests/interfaces.ts | 4 ++- .../src/tests/commonTests/openClose.ts | 28 +++++++++---------- .../src/tests/commonTests/utils.ts | 7 +++-- 4 files changed, 31 insertions(+), 24 deletions(-) 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 86ed1dbb7a3..05036d3acc0 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 @@ -40,10 +40,12 @@ describe("calcite-input-time-zone", () => { { name: "Pacific/Galapagos", offset: -360, label: "GMT-6" }, ]; - async function simpleTestProvider(): Promise { - const page = await newE2EPage(); + async function simpleTestProvider(programmaticE2EPage: E2EPage): Promise { + const page = programmaticE2EPage || (await newE2EPage()); await page.emulateTimezone(testTimeZoneItems[0].name); - await page.setContent(overrideSupportedTimeZones(html``)); + await page.setContent( + overrideSupportedTimeZones(programmaticE2EPage ? "" : html``), + ); return { page, @@ -130,10 +132,10 @@ describe("calcite-input-time-zone", () => { }); describe("openClose", () => { - openClose(html``); + openClose(simpleTestProvider); describe.skip("initially open", () => { - openClose(html``, { initialToggleValue: true }); + openClose(simpleTestProvider, { initialToggleValue: true }); }); }); @@ -163,7 +165,7 @@ describe("calcite-input-time-zone", () => { const page = await newE2EPage(); await page.emulateTimezone(testTimeZoneItems[0].name); await page.setContent( - await overrideSupportedTimeZones( + overrideSupportedTimeZones( html``, ), ); @@ -181,7 +183,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/tests/commonTests/interfaces.ts b/packages/calcite-components/src/tests/commonTests/interfaces.ts index 05ae276147e..93fedf086f7 100644 --- a/packages/calcite-components/src/tests/commonTests/interfaces.ts +++ b/packages/calcite-components/src/tests/commonTests/interfaces.ts @@ -25,7 +25,9 @@ export type TagOrHTMLWithBeforeContent = { }; export type ComponentTestContent = TagOrHTML | TagAndPage; -export type ComponentTestSetupProvider = (() => ComponentTestContent) | (() => Promise); +export type ComponentTestSetupProvider = + | ((programmaticE2EPage?: E2EPage) => ComponentTestContent) + | ((programmaticE2EPage?: E2EPage) => Promise); export type ComponentTestSetup = ComponentTestContent | ComponentTestSetupProvider; /** diff --git a/packages/calcite-components/src/tests/commonTests/openClose.ts b/packages/calcite-components/src/tests/commonTests/openClose.ts index 498ae278bae..d7735d52818 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, getTagAndPage } from "./utils"; -import { ComponentTag, TagOrHTML } from "./interfaces"; +import { getTagAndPage } from "./utils"; +import { ComponentTag, ComponentTestSetup } from "./interfaces"; expect.extend(toHaveNoViolations); @@ -74,11 +74,11 @@ interface OpenCloseOptions { * }); * }); * - * @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(componentTagOrHTML: TagOrHTML, options?: OpenCloseOptions): void { +export function openClose(componentTestSetup: ComponentTestSetup, options?: OpenCloseOptions): void { const defaultOptions: OpenCloseOptions = { initialToggleValue: false, openPropName: "open", @@ -87,10 +87,8 @@ export function openClose(componentTagOrHTML: TagOrHTML, options?: OpenCloseOpti const customizedOptions = { ...defaultOptions, ...options }; type EventOrderWindow = GlobalTestProps<{ events: string[] }>; - const tag = getTag(componentTagOrHTML); - const eventSequence = setUpEventSequence(tag); - function setUpEventSequence(componentTag: ComponentTag): string[] { + function getEventSequence(componentTag: ComponentTag): string[] { const camelCaseTag = componentTag.replace(/-([a-z])/g, (lettersAfterHyphen) => lettersAfterHyphen[1].toUpperCase()); const eventSuffixes = [`BeforeOpen`, `Open`, `BeforeClose`, `Close`]; @@ -117,7 +115,7 @@ export function openClose(componentTagOrHTML: TagOrHTML, options?: OpenCloseOpti document.body.append(component); }, - eventSequence, + getEventSequence(componentTag), customizedOptions.initialToggleValue, customizedOptions.openPropName, componentTag, @@ -141,6 +139,8 @@ export function openClose(componentTagOrHTML: TagOrHTML, options?: OpenCloseOpti page: E2EPage, animationsEnabled = true, ): Promise { + await setUpPage(componentTag, page); + const element = await page.find(componentTag); const timestamps: Record = { @@ -150,6 +150,8 @@ export function openClose(componentTagOrHTML: TagOrHTML, options?: OpenCloseOpti close: undefined, }; + const eventSequence = getEventSequence(componentTag); + const [beforeOpenEvent, openEvent, beforeCloseEvent, closeEvent] = eventSequence.map((event) => { return page.waitForEvent(event).then((spy) => { timestamps[toOpenCloseName(event)] = Date.now(); @@ -212,7 +214,7 @@ export function openClose(componentTagOrHTML: TagOrHTML, options?: OpenCloseOpti await page.addStyleTag({ content: `:root { --calcite-duration-factor: 2; }`, }); - await setUpPage(tag, page); + const { tag } = await getTagAndPage(componentTestSetup, page); await testOpenCloseEvents(tag, page, !customizedOptions.willUseFallback); }); @@ -221,25 +223,23 @@ export function openClose(componentTagOrHTML: TagOrHTML, options?: OpenCloseOpti await page.addStyleTag({ content: `:root { --calcite-duration-factor: 0; }`, }); - await setUpPage(tag, page); + const { tag } = await getTagAndPage(componentTestSetup, page); await testOpenCloseEvents(tag, page, false); }); } else { it(`emits with animations enabled`, async () => { - const { page, tag } = await getTagAndPage(componentTagOrHTML); + const { page, tag } = await getTagAndPage(componentTestSetup); await page.addStyleTag({ content: `:root { --calcite-duration-factor: 2; }`, }); - await setUpPage(tag, page); await testOpenCloseEvents(tag, page, !customizedOptions.willUseFallback); }); it(`emits with animations disabled`, async () => { - const { page, tag } = await getTagAndPage(componentTagOrHTML); + const { page, tag } = await getTagAndPage(componentTestSetup); await page.addStyleTag({ content: `:root { --calcite-duration-factor: 0; }`, }); - await setUpPage(tag, page); await testOpenCloseEvents(tag, page, false); }); } diff --git a/packages/calcite-components/src/tests/commonTests/utils.ts b/packages/calcite-components/src/tests/commonTests/utils.ts index 679418eac61..5e0a3d198dd 100644 --- a/packages/calcite-components/src/tests/commonTests/utils.ts +++ b/packages/calcite-components/src/tests/commonTests/utils.ts @@ -42,9 +42,12 @@ export async function simplePageSetup(componentTagOrHTML: TagOrHTML): Promise { +export async function getTagAndPage( + componentTestSetup: ComponentTestSetup, + programmaticE2EPage?: E2EPage, +): Promise { if (typeof componentTestSetup === "function") { - componentTestSetup = await componentTestSetup(); + componentTestSetup = await componentTestSetup(programmaticE2EPage); } if (typeof componentTestSetup === "string") { From f30d853f154091dd1fa5fba194cfa2b8251a2e53 Mon Sep 17 00:00:00 2001 From: JC Franco Date: Fri, 6 Sep 2024 15:10:01 -0700 Subject: [PATCH 3/4] roll back programmatic page argument for test setup providers --- .../calcite-components/src/tests/commonTests/interfaces.ts | 4 +--- packages/calcite-components/src/tests/commonTests/utils.ts | 7 ++----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/calcite-components/src/tests/commonTests/interfaces.ts b/packages/calcite-components/src/tests/commonTests/interfaces.ts index 93fedf086f7..05ae276147e 100644 --- a/packages/calcite-components/src/tests/commonTests/interfaces.ts +++ b/packages/calcite-components/src/tests/commonTests/interfaces.ts @@ -25,9 +25,7 @@ export type TagOrHTMLWithBeforeContent = { }; export type ComponentTestContent = TagOrHTML | TagAndPage; -export type ComponentTestSetupProvider = - | ((programmaticE2EPage?: E2EPage) => ComponentTestContent) - | ((programmaticE2EPage?: E2EPage) => Promise); +export type ComponentTestSetupProvider = (() => ComponentTestContent) | (() => Promise); export type ComponentTestSetup = ComponentTestContent | ComponentTestSetupProvider; /** diff --git a/packages/calcite-components/src/tests/commonTests/utils.ts b/packages/calcite-components/src/tests/commonTests/utils.ts index 5e0a3d198dd..679418eac61 100644 --- a/packages/calcite-components/src/tests/commonTests/utils.ts +++ b/packages/calcite-components/src/tests/commonTests/utils.ts @@ -42,12 +42,9 @@ export async function simplePageSetup(componentTagOrHTML: TagOrHTML): Promise { +export async function getTagAndPage(componentTestSetup: ComponentTestSetup): Promise { if (typeof componentTestSetup === "function") { - componentTestSetup = await componentTestSetup(programmaticE2EPage); + componentTestSetup = await componentTestSetup(); } if (typeof componentTestSetup === "string") { From 273a0bc652a561102711daddd830f0ecfdfc37b9 Mon Sep 17 00:00:00 2001 From: JC Franco Date: Fri, 6 Sep 2024 14:57:30 -0700 Subject: [PATCH 4/4] add separate function in helper to simplify usage --- .../src/components/dialog/dialog.e2e.ts | 7 +- .../input-time-picker.e2e.ts | 5 +- .../input-time-zone/input-time-zone.e2e.ts | 25 +- .../src/components/modal/modal.e2e.ts | 7 +- .../src/components/sheet/sheet.e2e.ts | 7 +- .../src/tests/commonTests/interfaces.ts | 4 +- .../src/tests/commonTests/openClose.ts | 375 ++++++++++-------- .../src/tests/commonTests/utils.ts | 14 + 8 files changed, 254 insertions(+), 190 deletions(-) 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 51c96aa6570..b733385c9ae 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("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 05036d3acc0..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 @@ -40,12 +40,10 @@ describe("calcite-input-time-zone", () => { { name: "Pacific/Galapagos", offset: -360, label: "GMT-6" }, ]; - async function simpleTestProvider(programmaticE2EPage: E2EPage): Promise { - const page = programmaticE2EPage || (await newE2EPage()); + async function simpleTestProvider(): Promise { + const page = await newE2EPage(); await page.emulateTimezone(testTimeZoneItems[0].name); - await page.setContent( - overrideSupportedTimeZones(programmaticE2EPage ? "" : html``), - ); + await page.setContent(overrideSupportedTimeZones(html``)); return { page, @@ -134,8 +132,21 @@ describe("calcite-input-time-zone", () => { describe("openClose", () => { openClose(simpleTestProvider); - describe.skip("initially open", () => { - openClose(simpleTestProvider, { initialToggleValue: true }); + 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(); + }, + }); }); }); 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 d7735d52818..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 { getTagAndPage } from "./utils"; -import { ComponentTag, ComponentTestSetup } 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,199 +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 {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 defaultOptions: OpenCloseOptions = { - initialToggleValue: false, - openPropName: "open", - willUseFallback: false, + 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; }`, + }); + + 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 tag = componentTag; + const beforeContent = getBeforeContent(effectiveOptions); - function getEventSequence(componentTag: ComponentTag): string[] { - const camelCaseTag = componentTag.replace(/-([a-z])/g, (lettersAfterHyphen) => lettersAfterHyphen[1].toUpperCase()); - const eventSuffixes = [`BeforeOpen`, `Open`, `BeforeClose`, `Close`]; + 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, + }); + }); - return eventSuffixes.map((suffix) => `${camelCaseTag}${suffix}`); - } + 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, + }); + }); +}; - async function setUpPage(componentTag: ComponentTag, page: E2EPage): Promise { - await page.evaluate( - (eventSequence: string[], initialToggleValue: boolean, openPropName: string, componentTag: string) => { - const receivedEvents: string[] = []; +interface TestOpenCloseEventsParams { + /** + * The component tag to test. + */ + tag: ComponentTag; + + /** + * The E2E page instance. + */ + page: E2EPage; + + /** + * The property name used to control the open state of the component. + */ + openPropName: string; + + /** + * Whether the component should start in the open state. + */ + startOpen?: boolean; + + /** + * Functions to simulate user input (mouse or keyboard) to open or close the component. + */ + beforeToggle?: BeforeToggle; + + /** + * Whether animations are enabled. + */ + animationsEnabled: boolean; +} - (window as EventOrderWindow).events = receivedEvents; +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); - eventSequence.forEach((eventType) => { - document.addEventListener(eventType, (event) => receivedEvents.push(event.type)); - }); + const [beforeOpenEvent, openEvent, beforeCloseEvent, closeEvent] = eventSequence.map((event) => { + return page.waitForEvent(event).then((spy) => { + timestamps[toOpenCloseName(event)] = Date.now(); + return spy; + }); + }); - if (!initialToggleValue) { - return; - } + 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]); + } - const component = document.createElement(componentTag); + if (startOpen) { + await page.evaluate( + (openPropName: string, componentTagOrHTML: string) => { + const component = document.createElement(componentTagOrHTML); component[openPropName] = true; document.body.append(component); }, - getEventSequence(componentTag), - customizedOptions.initialToggleValue, - customizedOptions.openPropName, - componentTag, + 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( - componentTag: ComponentTag, - page: E2EPage, - animationsEnabled = true, - ): Promise { - await setUpPage(componentTag, page); - - const element = await page.find(componentTag); - - const timestamps: Record = { - beforeOpen: undefined, - open: undefined, - beforeClose: undefined, - close: undefined, - }; - - const eventSequence = getEventSequence(componentTag); - - 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; }`, - }); - const { tag } = await getTagAndPage(componentTestSetup, page); - await testOpenCloseEvents(tag, 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; }`, - }); - const { tag } = await getTagAndPage(componentTestSetup, page); - await testOpenCloseEvents(tag, page, false); - }); - } else { - it(`emits with animations enabled`, async () => { - const { page, tag } = await getTagAndPage(componentTestSetup); - await page.addStyleTag({ - content: `:root { --calcite-duration-factor: 2; }`, - }); - await testOpenCloseEvents(tag, page, !customizedOptions.willUseFallback); - }); +type OpenCloseName = "beforeOpen" | "open" | "beforeClose" | "close"; - it(`emits with animations disabled`, async () => { - const { page, tag } = await getTagAndPage(componentTestSetup); - await page.addStyleTag({ - content: `:root { --calcite-duration-factor: 0; }`, - }); - await testOpenCloseEvents(tag, 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;