(`.${TEST_ITEM_CLASS}`)).map(el =>
+ Number(el.dataset.id),
+ );
+ assert.sameMembers(visibleIds, ids, "visible items");
}
});
diff --git a/packages/core/src/components/overlay/overlay.test.tsx b/packages/core/src/components/overlay/overlay.test.tsx
index daa29b81623..e643266be70 100644
--- a/packages/core/src/components/overlay/overlay.test.tsx
+++ b/packages/core/src/components/overlay/overlay.test.tsx
@@ -21,184 +21,164 @@
/* eslint-disable @typescript-eslint/no-deprecated */
-import { waitFor } from "@testing-library/dom";
-import { mount, ReactWrapper, shallow } from "enzyme";
-import { createRef } from "react";
+import { fireEvent, render, waitFor } from "@testing-library/react";
+import { cloneElement, createRef } from "react";
import { afterAll, afterEach, assert, beforeEach, describe, expect, it, vi } from "@blueprintjs/test-commons/vitest";
import { dispatchMouseEvent } from "@blueprintjs/test-commons/vitest-utils";
import { Classes, Utils } from "../../common";
import { sleep } from "../../common/test-utils";
-import { Portal, type PortalProps } from "../portal/portal";
import { Overlay } from "./overlay";
-import { type OverlayProps } from "./overlayProps";
-
-function findInPortal(overlay: ReactWrapper
, selector: string) {
- // React 16: createPortal preserves React tree so simple find works.
- const element = overlay.find(Portal).find(selector);
- if (element.exists()) {
- return element;
- }
-
- // React 15: unstable_renderSubtree does not preserve tree so we must create new wrapper.
- const portal = overlay.find(Portal).instance() as React.Component;
- const portalChildren = new ReactWrapper(<>{portal.props.children}>);
- if (portalChildren.is(selector)) {
- return portalChildren;
- }
- return portalChildren.find(selector);
-}
+type RenderResult = ReturnType;
const BACKDROP_SELECTOR = `.${Classes.OVERLAY_BACKDROP}`;
/*
IMPORTANT NOTE: It is critical that every wrapper be unmounted after the test, to avoid
polluting the DOM with leftover overlay elements. This was the cause of the Overlay test flakes of
late 2017/early 2018 and was resolved by ensuring that every wrapper is unmounted.
-
-The `wrapper` variable below and the `mountWrapper` method should be used for full enzyme mounts.
-For shallow mounts, be sure to call `shallowWrapper.unmount()` after the assertions.
*/
describe("", () => {
- let wrapper: ReactWrapper;
- let isMounted = false;
- const containerElement = document.createElement("div");
+ const containerElement: HTMLElement = document.createElement("div");
document.documentElement.appendChild(containerElement);
- /**
- * Mount the `content` into `containerElement` and assign to local `wrapper` variable.
- * Use this method in this suite instead of Enzyme's `mount` method.
- */
- function mountWrapper(content: React.JSX.Element) {
- wrapper = mount(content, { attachTo: containerElement });
- isMounted = true;
- return wrapper;
+ let result: RenderResult | undefined;
+
+ function renderOverlay(content: React.ReactElement) {
+ const ref = createRef();
+ const cloned = content.type === Overlay ? cloneElement(content, { ref } as any) : content;
+ const r = render(cloned, { container: containerElement });
+ result = r;
+ return r;
}
afterEach(() => {
- if (isMounted) {
- // clean up wrapper after each test, if it was used
- wrapper?.unmount();
- wrapper?.detach();
- isMounted = false;
- }
+ result?.unmount();
+ result = undefined;
+ // Clean up any portals leaked between tests
+ document.querySelectorAll(`.${Classes.PORTAL}`).forEach(el => el.remove());
+ document.body.classList.remove(Classes.OVERLAY_OPEN);
});
afterAll(() => {
document.documentElement.removeChild(containerElement);
});
+ function findInDocument(selector: string): HTMLElement | null {
+ return document.querySelector(selector);
+ }
+
+ function findAllInDocument(selector: string): HTMLElement[] {
+ return Array.from(document.querySelectorAll(selector));
+ }
+
it("renders its content correctly", () => {
- const overlay = shallow(
+ renderOverlay(
{createOverlayContents()}
,
);
- assert.lengthOf(overlay.find("strong"), 1);
- assert.lengthOf(overlay.find(BACKDROP_SELECTOR), 1);
- overlay.unmount();
+ assert.lengthOf(findAllInDocument("strong"), 1);
+ assert.lengthOf(findAllInDocument(BACKDROP_SELECTOR), 1);
});
it("renders contents to specified container correctly", () => {
const CLASS_TO_TEST = "bp-test-content";
- const container = document.createElement("div");
- document.body.appendChild(container);
- mountWrapper(
-
+ const portalContainer = document.createElement("div");
+ document.body.appendChild(portalContainer);
+ renderOverlay(
+
test
,
);
- assert.lengthOf(container.getElementsByClassName(CLASS_TO_TEST), 1);
- document.body.removeChild(container);
+ assert.lengthOf(portalContainer.getElementsByClassName(CLASS_TO_TEST), 1);
+ document.body.removeChild(portalContainer);
});
it("sets aria-live", () => {
- // Using an open Overlay because an initially closed Overlay will not render anything to the
- // DOM
- mountWrapper( );
+ renderOverlay( );
const overlayElement = document.querySelector(".aria-test");
assert.exists(overlayElement);
- // Element#ariaLive not supported in Firefox or IE
assert.equal(overlayElement?.getAttribute("aria-live"), "polite");
});
it("portalClassName appears on Portal", () => {
const CLASS_TO_TEST = "bp-test-content";
- mountWrapper(
+ renderOverlay(
test
,
);
- // search document for portal container element.
assert.isDefined(document.querySelector(`.${Classes.PORTAL}.${CLASS_TO_TEST}`));
});
it("renders Portal after first opened", () => {
- mountWrapper({createOverlayContents()} );
- assert.lengthOf(wrapper.find(Portal), 0, "unexpected Portal");
- wrapper.setProps({ isOpen: true });
- assert.lengthOf(wrapper.find(Portal), 1, "expected Portal");
+ const ref = createRef();
+ const { rerender } = render(
+
+ {createOverlayContents()}
+ ,
+ { container: containerElement },
+ );
+ result = { container: containerElement, rerender, unmount: () => undefined } as unknown as RenderResult;
+ assert.isNull(document.querySelector(`.${Classes.PORTAL}`), "unexpected Portal");
+ rerender(
+
+ {createOverlayContents()}
+ ,
+ );
+ assert.isNotNull(document.querySelector(`.${Classes.PORTAL}`), "expected Portal");
});
it("supports non-element children", () => {
- assert.doesNotThrow(() =>
- shallow(
+ assert.doesNotThrow(() => {
+ const local = render(
{null} {undefined}
,
- ).unmount(),
- );
+ );
+ local.unmount();
+ });
});
it("hasBackdrop=false does not render backdrop", () => {
- const overlay = shallow(
+ renderOverlay(
{createOverlayContents()}
,
);
- assert.lengthOf(overlay.find("strong"), 1);
- assert.lengthOf(overlay.find(BACKDROP_SELECTOR), 0);
- overlay.unmount();
- });
-
- it("renders portal attached to body when not inline after first opened", () => {
- mountWrapper({createOverlayContents()} );
- assert.lengthOf(wrapper.find(Portal), 0, "unexpected Portal");
- wrapper.setProps({ isOpen: true });
- assert.lengthOf(wrapper.find(Portal), 1, "expected Portal");
+ assert.lengthOf(findAllInDocument("strong"), 1);
+ assert.lengthOf(findAllInDocument(BACKDROP_SELECTOR), 0);
});
describe("onClose", () => {
it("invoked on backdrop mousedown when canOutsideClickClose=true", () => {
const onClose = vi.fn();
- const overlay = shallow(
+ renderOverlay(
{createOverlayContents()}
,
);
- overlay.find(BACKDROP_SELECTOR).simulate("mousedown");
+ fireEvent.mouseDown(findInDocument(BACKDROP_SELECTOR)!);
expect(onClose).toHaveBeenCalledOnce();
- overlay.unmount();
});
it("not invoked on backdrop mousedown when canOutsideClickClose=false", () => {
const onClose = vi.fn();
- const overlay = shallow(
+ renderOverlay(
{createOverlayContents()}
,
);
- overlay.find(BACKDROP_SELECTOR).simulate("mousedown");
+ fireEvent.mouseDown(findInDocument(BACKDROP_SELECTOR)!);
expect(onClose).not.toHaveBeenCalled();
- overlay.unmount();
});
it("invoked on document mousedown when hasBackdrop=false", () => {
const onClose = vi.fn();
- // mounting cuz we need document events + lifecycle
- mountWrapper(
+ renderOverlay(
{createOverlayContents()}
,
@@ -210,7 +190,7 @@ describe("", () => {
it("not invoked on document mousedown when hasBackdrop=false and canOutsideClickClose=false", () => {
const onClose = vi.fn();
- mountWrapper(
+ renderOverlay(
", () => {
it("not invoked on click of a nested overlay", () => {
const onClose = vi.fn();
- mountWrapper(
+ renderOverlay(
{createOverlayContents()}
@@ -238,44 +218,43 @@ describe("", () => {
,
);
- // this hackery is necessary for React 15 support, where Portals break trees.
- findInPortal(findInPortal(wrapper, "#outer-element"), "#inner-element").simulate("mousedown");
+ fireEvent.mouseDown(findInDocument("#inner-element")!);
expect(onClose).not.toHaveBeenCalled();
});
it("invoked on escape key", () => {
const onClose = vi.fn();
- mountWrapper(
+ renderOverlay(
{createOverlayContents()}
,
);
- wrapper.simulate("keydown", { key: "Escape" });
+ const overlayEl = findInDocument(`.${Classes.OVERLAY}`)!;
+ fireEvent.keyDown(overlayEl, { key: "Escape" });
expect(onClose).toHaveBeenCalledOnce();
});
it("not invoked on escape key when canEscapeKeyClose=false", () => {
const onClose = vi.fn();
- const overlay = shallow(
+ renderOverlay(
{createOverlayContents()}
,
);
- overlay.simulate("keydown", { key: "Escape" });
+ const overlayEl = findInDocument(`.${Classes.OVERLAY}`)!;
+ fireEvent.keyDown(overlayEl, { key: "Escape" });
expect(onClose).not.toHaveBeenCalled();
- overlay.unmount();
});
it("renders portal attached to body when not inline", () => {
- const overlay = shallow(
+ renderOverlay(
{createOverlayContents()}
,
);
- const portal = overlay.find(Portal);
- assert.isTrue(portal.exists(), "missing Portal");
- assert.lengthOf(portal.find("strong"), 1, "missing h1");
- overlay.unmount();
+ const portal = findInDocument(`.${Classes.PORTAL}`);
+ assert.isNotNull(portal, "missing Portal");
+ assert.lengthOf(portal!.querySelectorAll("strong"), 1, "missing h1");
});
});
@@ -283,7 +262,7 @@ describe("", () => {
const overlayClassName = "test-overlay";
it("brings focus to overlay if autoFocus=true", async () => {
- mountWrapper(
+ renderOverlay(
,
@@ -292,27 +271,27 @@ describe("", () => {
});
it("does not bring focus to overlay if autoFocus=false and enforceFocus=false", async () => {
- mountWrapper(
-
- something outside overlay for browser to focus on
-
-
-
-
,
+ renderOverlay(
+ (
+
+ something outside overlay for browser to focus on
+
+
+
+
+ ) as unknown as React.ReactElement,
);
await assertFocus("body");
});
- // React implements autoFocus itself so our `[autofocus]` logic never fires.
- // Still, worth testing we can control where the focus goes.
it("autoFocus element inside overlay gets the focus", async () => {
- mountWrapper(
+ renderOverlay(
,
@@ -323,13 +302,15 @@ describe("", () => {
it("returns focus to overlay if enforceFocus=true", async () => {
const buttonRef = createRef();
const inputRef = createRef();
- mountWrapper(
-
-
-
-
-
-
,
+ renderOverlay(
+ (
+
+
+
+
+
+
+ ) as unknown as React.ReactElement,
);
assert.strictEqual(document.activeElement, inputRef.current);
buttonRef.current?.focus();
@@ -337,7 +318,7 @@ describe("", () => {
});
it("returns focus to overlay after clicking the backdrop if enforceFocus=true", async () => {
- mountWrapper(
+ renderOverlay(
", () => {
{createOverlayContents()}
,
);
- wrapper.find(BACKDROP_SELECTOR).simulate("mousedown");
+ fireEvent.mouseDown(findInDocument(BACKDROP_SELECTOR)!);
await assertFocusIsInOverlay();
});
it("returns focus to overlay after clicking an outside element if enforceFocus=true", async () => {
- mountWrapper(
-
-
- {createOverlayContents()}
-
-
-
,
+ renderOverlay(
+ (
+
+
+ {createOverlayContents()}
+
+
+
+ ) as unknown as React.ReactElement,
);
- wrapper.find("#buttonId").simulate("click");
+ fireEvent.click(findInDocument("#buttonId")!);
await assertFocusIsInOverlay();
});
it("does not result in maximum call stack if two overlays open with enforceFocus=true", () => {
const anotherContainer = document.createElement("div");
document.documentElement.appendChild(anotherContainer);
- const temporaryWrapper = mount(
+ const temporary = render(
,
- { attachTo: anotherContainer },
+ { container: anotherContainer },
);
- mountWrapper(
-
+ const ref = createRef();
+ const { rerender } = render(
+
+
+ ,
+ { container: containerElement },
+ );
+ const bringFocusSpy = vi.spyOn(ref.current as Overlay, "bringFocusInsideOverlay");
+ rerender(
+
,
);
- // ES6 class property vs prototype, see: https://github.com/airbnb/enzyme/issues/365
- const bringFocusSpy = vi.spyOn(wrapper.instance() as Overlay, "bringFocusInsideOverlay");
- wrapper.setProps({ isOpen: true });
- // triggers the infinite recursion
- wrapper.find("#inputId").simulate("click");
+ fireEvent.click(findInDocument("#inputId")!);
expect(bringFocusSpy).toHaveBeenCalledOnce();
- // don't need spy.restore() since the wrapper will be destroyed after test anyways
- temporaryWrapper.unmount();
+ temporary.unmount();
document.documentElement.removeChild(anotherContainer);
+ // Manually unmount the secondary render — afterEach won't track it.
+ const cleanup = result;
+ cleanup?.unmount();
+ result = undefined;
});
it("does not return focus to overlay if enforceFocus=false", () => {
@@ -407,19 +397,21 @@ describe("", () => {
await waitFor(() => assert.strictEqual(buttonRef, document.activeElement));
};
- mountWrapper(
-
- (buttonRef = ref)} />
-
- ref && focusBtnAndAssert()} />
-
-
,
+ renderOverlay(
+ (
+
+ (buttonRef = ref)} />
+
+ ref && focusBtnAndAssert()} />
+
+
+ ) as unknown as React.ReactElement,
);
});
it("doesn't focus overlay if focus is already inside overlay", async () => {
let textarea: HTMLTextAreaElement | null;
- mountWrapper(
+ renderOverlay(
,
@@ -429,11 +421,13 @@ describe("", () => {
});
it("does not focus overlay when closed", async () => {
- mountWrapper(
-
- ref && ref.focus()} />
-
-
,
+ renderOverlay(
+ (
+
+ ref && ref.focus()} />
+
+
+ ) as unknown as React.ReactElement,
);
await assertFocus("button");
});
@@ -443,7 +437,7 @@ describe("", () => {
// event with window as the target to simulate clicking browser chrome.
// The underlying Blueprint behavior is still valid.
it.skip("does not crash while trying to return focus to overlay if user clicks outside the document", () => {
- mountWrapper(
+ renderOverlay(
", () => {
,
);
- // this is a fairly custom / nonstandard event dispatch, trying to simulate what happens in some browsers when a user clicks
- // on the browser toolbar (outside the document), but a focus event is still dispatched to document
- // see https://github.com/palantir/blueprint/issues/3928
const event = new FocusEvent("focus");
Object.defineProperty(event, "target", { value: window });
@@ -469,10 +460,7 @@ describe("", () => {
});
async function assertFocus(selector: string | (() => void)) {
- // the behavior being tested relies on requestAnimationFrame.
- // waitFor to reduce flakes.
await waitFor(() => {
- wrapper.update();
if (Utils.isFunction(selector)) {
selector();
} else {
@@ -497,41 +485,41 @@ describe("", () => {
});
it("disables document scrolling by default", async () => {
- wrapper = mountWrapper(renderBackdropOverlay());
+ renderOverlay(renderBackdropOverlay());
await assertBodyScrollingDisabled(true);
});
it("disables document scrolling if hasBackdrop=true and usePortal=true", async () => {
- wrapper = mountWrapper(renderBackdropOverlay(true, true));
+ renderOverlay(renderBackdropOverlay(true, true));
await assertBodyScrollingDisabled(true);
});
it("does not disable document scrolling if hasBackdrop=true and usePortal=false", async () => {
- wrapper = mountWrapper(renderBackdropOverlay(true, false));
+ renderOverlay(renderBackdropOverlay(true, false));
await assertBodyScrollingDisabled(false);
});
it("does not disable document scrolling if hasBackdrop=false and usePortal=true", async () => {
- wrapper = mountWrapper(renderBackdropOverlay(false, true));
+ renderOverlay(renderBackdropOverlay(false, true));
await assertBodyScrollingDisabled(false);
});
it("does not disable document scrolling if hasBackdrop=false and usePortal=false", async () => {
- wrapper = mountWrapper(renderBackdropOverlay(false, false));
+ renderOverlay(renderBackdropOverlay(false, false));
await assertBodyScrollingDisabled(false);
});
it("keeps scrolling disabled if hasBackdrop=true overlay exists following unmount", async () => {
- const backdropOverlay = mount(renderBackdropOverlay(true));
- wrapper = mountWrapper(renderBackdropOverlay(true));
+ const backdropOverlay = render(renderBackdropOverlay(true));
+ renderOverlay(renderBackdropOverlay(true));
backdropOverlay.unmount();
await assertBodyScrollingDisabled(true);
});
it("doesn't keep scrolling disabled if no hasBackdrop=true overlay exists following unmount", async () => {
- const backdropOverlay = mount(renderBackdropOverlay(true));
- wrapper = mountWrapper(renderBackdropOverlay(false));
+ const backdropOverlay = render(renderBackdropOverlay(true));
+ renderOverlay(renderBackdropOverlay(false));
backdropOverlay.unmount();
await assertBodyScrollingDisabled(false);
@@ -546,7 +534,6 @@ describe("", () => {
}
async function assertBodyScrollingDisabled(disabled: boolean) {
- // wait for the DOM to settle before checking body classes
await waitFor(() => {
const hasClass = document.body.classList.contains(Classes.OVERLAY_OPEN);
assert.equal(hasClass, disabled);
@@ -561,27 +548,38 @@ describe("", () => {
const onClosing = vi.fn();
const onOpened = vi.fn();
const onOpening = vi.fn();
- wrapper = mountWrapper(
+ const ref = createRef();
+ const { rerender } = render(
{createOverlayContents()}
,
+ { container: containerElement },
);
+ result = {} as RenderResult;
expect(onOpening).toHaveBeenCalledOnce();
expect(onOpened).not.toHaveBeenCalled();
await sleep(10);
- // on*ed called after transition completes
expect(onOpened).toHaveBeenCalledOnce();
- wrapper.setProps({ isOpen: false });
- // on*ing called immediately when prop changes
+ rerender(
+
+ {createOverlayContents()}
+ ,
+ );
expect(onClosing).toHaveBeenCalledOnce();
expect(onClosed).not.toHaveBeenCalled();
diff --git a/packages/core/src/components/overlay2/overlay2.test.tsx b/packages/core/src/components/overlay2/overlay2.test.tsx
index ccfccd6b776..4a5ea51093d 100644
--- a/packages/core/src/components/overlay2/overlay2.test.tsx
+++ b/packages/core/src/components/overlay2/overlay2.test.tsx
@@ -18,7 +18,7 @@ import { fireEvent, render, type RenderOptions, type RenderResult, screen, waitF
import userEvent from "@testing-library/user-event";
import { createRef, useState } from "react";
-import { describe, expect, it, vi } from "@blueprintjs/test-commons/vitest";
+import { afterAll, afterEach, describe, expect, it, vi } from "@blueprintjs/test-commons/vitest";
import { Classes } from "../../common";
import { OverlaysProvider } from "../../context/overlays/overlaysProvider";
@@ -43,6 +43,10 @@ function renderWithOverlaysProvider(ui: React.ReactElement, renderOptions: Rende
}
describe("", () => {
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(vi.fn());
+ afterEach(() => errorSpy.mockClear());
+ afterAll(() => errorSpy.mockRestore());
+
it("should render its contents", () => {
const { container } = renderWithOverlaysProvider(
diff --git a/packages/core/src/components/panel-stack/panelStack.test.tsx b/packages/core/src/components/panel-stack/panelStack.test.tsx
index 94211dcfec3..7a13148b113 100644
--- a/packages/core/src/components/panel-stack/panelStack.test.tsx
+++ b/packages/core/src/components/panel-stack/panelStack.test.tsx
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { mount, type ReactWrapper } from "enzyme";
+import { fireEvent, render } from "@testing-library/react";
import { useState } from "react";
import { afterEach, beforeEach, describe, expect, it, vi } from "@blueprintjs/test-commons/vitest";
@@ -25,6 +25,8 @@ import { NumericInput } from "../forms/numericInput";
import { PanelStack, type PanelStackProps } from "./panelStack";
import { type Panel, type PanelProps } from "./panelTypes";
+type RenderResult = ReturnType;
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
type TestPanelInfo = {};
type TestPanelType = Panel;
@@ -46,7 +48,7 @@ const TestPanel: React.FC> = props => {
describe("", () => {
let containerElement: HTMLElement;
- let panelStackWrapper: PanelStackWrapper;
+ let result: RenderResult | undefined;
const initialPanel: Panel = {
props: {},
@@ -65,121 +67,99 @@ describe("", () => {
});
afterEach(() => {
- panelStackWrapper?.unmount();
- panelStackWrapper?.detach();
+ result?.unmount();
+ result = undefined;
containerElement.remove();
});
- describe("uncontrolled mode", () => {
- it("renders a basic panel and allows opening and closing", () => {
- panelStackWrapper = renderPanelStack({ initialPanel });
- expect(panelStackWrapper).toBeDefined();
-
- const newPanelButton = panelStackWrapper.find("#new-panel-button");
- expect(newPanelButton).toBeDefined();
- newPanelButton.simulate("click");
+ function renderPanelStack(props: PanelStackProps) {
+ const r = render( , { container: containerElement });
+ result = r;
+ return r;
+ }
- const newPanelHeader = panelStackWrapper.findClass(Classes.HEADING);
- expect(newPanelHeader).toBeDefined();
- expect(newPanelHeader.at(0).text()).toBe("New Panel 1");
+ function findAll(selector: string): HTMLElement[] {
+ return Array.from(result!.container.querySelectorAll(selector));
+ }
- const backButton = panelStackWrapper.findClass(Classes.PANEL_STACK_HEADER_BACK);
- expect(backButton).toBeDefined();
- backButton.simulate("click");
+ function findFirst(selector: string): HTMLElement | null {
+ return result!.container.querySelector(selector);
+ }
- const oldPanelHeader = panelStackWrapper.findClass(Classes.HEADING);
- expect(oldPanelHeader).toBeDefined();
- expect(oldPanelHeader.at(1).text()).toBe("Test Title");
+ describe("uncontrolled mode", () => {
+ it("renders a basic panel and allows opening and closing", () => {
+ renderPanelStack({ initialPanel });
+ fireEvent.click(findFirst("#new-panel-button")!);
+ const headers = findAll(`.${Classes.HEADING}`);
+ expect(headers[0].textContent).toBe("New Panel 1");
+
+ fireEvent.click(findFirst(`.${Classes.PANEL_STACK_HEADER_BACK}`)!);
+ const headers2 = findAll(`.${Classes.HEADING}`);
+ // After back, the previous panel becomes active again. Its header reads "Test Title".
+ expect(headers2[headers2.length - 1].textContent).toBe("Test Title");
});
it("renders a panel stack without header and allows opening and closing", () => {
- panelStackWrapper = renderPanelStack({ initialPanel, showPanelHeader: false });
- expect(panelStackWrapper).toBeDefined();
-
- const newPanelButton = panelStackWrapper.find("#new-panel-button");
- expect(newPanelButton).toBeDefined();
- newPanelButton.simulate("click");
-
- const newPanelHeader = panelStackWrapper.findClass(Classes.HEADING);
- expect(newPanelHeader).toHaveLength(0);
-
- const backButton = panelStackWrapper.findClass(Classes.PANEL_STACK_HEADER_BACK);
- expect(backButton).toHaveLength(0);
-
- const closePanel = panelStackWrapper.find("#close-panel-button");
- expect(closePanel).toBeDefined();
- closePanel.last().simulate("click");
-
- const oldPanelHeader = panelStackWrapper.findClass(Classes.HEADING);
- expect(oldPanelHeader).toHaveLength(0);
+ renderPanelStack({ initialPanel, showPanelHeader: false });
+ fireEvent.click(findFirst("#new-panel-button")!);
+ expect(findAll(`.${Classes.HEADING}`)).toHaveLength(0);
+ expect(findAll(`.${Classes.PANEL_STACK_HEADER_BACK}`)).toHaveLength(0);
+
+ const closeButtons = findAll("#close-panel-button");
+ fireEvent.click(closeButtons[closeButtons.length - 1]);
+ expect(findAll(`.${Classes.HEADING}`)).toHaveLength(0);
});
it("does not call the callback handler onClose when there is only a single panel on the stack", () => {
const onClose = vi.fn();
- panelStackWrapper = renderPanelStack({ initialPanel, onClose });
-
- const closePanel = panelStackWrapper.find("#close-panel-button");
- expect(closePanel).toBeDefined();
-
- closePanel.simulate("click");
+ renderPanelStack({ initialPanel, onClose });
+ fireEvent.click(findFirst("#close-panel-button")!);
expect(onClose).not.toHaveBeenCalled();
});
it("calls the callback handlers onOpen and onClose", () => {
const onOpen = vi.fn();
const onClose = vi.fn();
- panelStackWrapper = renderPanelStack({ initialPanel, onClose, onOpen });
+ renderPanelStack({ initialPanel, onClose, onOpen });
- const newPanelButton = panelStackWrapper.find("#new-panel-button");
- expect(newPanelButton).toBeDefined();
- newPanelButton.simulate("click");
+ fireEvent.click(findFirst("#new-panel-button")!);
expect(onOpen).toHaveBeenCalledOnce();
expect(onClose).not.toHaveBeenCalled();
- const backButton = panelStackWrapper.findClass(Classes.PANEL_STACK_HEADER_BACK);
- expect(backButton).toBeDefined();
- backButton.simulate("click");
+ fireEvent.click(findFirst(`.${Classes.PANEL_STACK_HEADER_BACK}`)!);
expect(onClose).toHaveBeenCalledOnce();
expect(onOpen).toHaveBeenCalledOnce();
});
it("does not have the back button when only a single panel is on the stack", () => {
- panelStackWrapper = renderPanelStack({ initialPanel });
- const backButton = panelStackWrapper.findClass(Classes.PANEL_STACK_HEADER_BACK);
- expect(backButton).toHaveLength(0);
+ renderPanelStack({ initialPanel });
+ expect(findAll(`.${Classes.PANEL_STACK_HEADER_BACK}`)).toHaveLength(0);
});
it("assigns the class to TransitionGroup", () => {
const TEST_CLASS_NAME = "TEST_CLASS_NAME";
- panelStackWrapper = renderPanelStack({ className: TEST_CLASS_NAME, initialPanel });
- expect(panelStackWrapper.hasClass(TEST_CLASS_NAME)).toBe(true);
-
- const transitionGroupClassName = panelStackWrapper.findClass(TEST_CLASS_NAME).props().className;
- expect(transitionGroupClassName).toBeDefined();
- expect(transitionGroupClassName!.indexOf(Classes.PANEL_STACK)).toBe(0);
+ renderPanelStack({ className: TEST_CLASS_NAME, initialPanel });
+ const root = findFirst(`.${TEST_CLASS_NAME}`);
+ expect(root).not.toBeNull();
+ expect(root!.classList.contains(Classes.PANEL_STACK)).toBe(true);
});
it("can render a panel without a title", () => {
- panelStackWrapper = renderPanelStack({ initialPanel: emptyTitleInitialPanel });
- expect(panelStackWrapper).toBeDefined();
-
- const newPanelButton = panelStackWrapper.find("#new-panel-button");
- expect(newPanelButton).toBeDefined();
- newPanelButton.simulate("click");
+ renderPanelStack({ initialPanel: emptyTitleInitialPanel });
+ fireEvent.click(findFirst("#new-panel-button")!);
- const backButtonWithoutTitle = panelStackWrapper.findClass(Classes.PANEL_STACK_HEADER_BACK);
+ const backButtons = findAll(`.${Classes.PANEL_STACK_HEADER_BACK}`);
expect(
- backButtonWithoutTitle.prop("aria-label"),
+ backButtons[0].getAttribute("aria-label"),
"expected icon-only back button to have accessible label",
).toBe("Back");
- const newPanelButtonOnNotEmpty = panelStackWrapper.find("#new-panel-button").hostNodes().at(1);
- expect(newPanelButtonOnNotEmpty).toBeDefined();
- newPanelButtonOnNotEmpty.simulate("click");
+ const newPanelButtons = findAll("#new-panel-button");
+ fireEvent.click(newPanelButtons[1]);
- const backButtonWithTitle = panelStackWrapper.findClass(Classes.PANEL_STACK_HEADER_BACK).hostNodes().at(1);
+ const backButtonsAfter = findAll(`.${Classes.PANEL_STACK_HEADER_BACK}`);
expect(
- backButtonWithTitle.prop("aria-label"),
+ backButtonsAfter[backButtonsAfter.length - 1].getAttribute("aria-label"),
"expected icon-only back button to have accessible label",
).toBe("Back");
});
@@ -188,61 +168,49 @@ describe("", () => {
describe("controlled mode", () => {
it("can render a panel stack in controlled mode", () => {
const stack = [initialPanel];
- panelStackWrapper = renderPanelStack({ stack });
- expect(panelStackWrapper).toBeDefined();
+ renderPanelStack({ stack });
- const newPanelButton = panelStackWrapper.find("#new-panel-button");
- expect(newPanelButton).toBeDefined();
- newPanelButton.simulate("click");
+ fireEvent.click(findFirst("#new-panel-button")!);
// Expect the same panel as before since onOpen is not handled
- const newPanelHeader = panelStackWrapper.findClass(Classes.HEADING);
- expect(newPanelHeader).toBeDefined();
- expect(newPanelHeader.at(0).text()).toBe("Test Title");
+ const headers = findAll(`.${Classes.HEADING}`);
+ expect(headers[0].textContent).toBe("Test Title");
});
it("can open a panel in controlled mode", () => {
let stack = [initialPanel];
- panelStackWrapper = renderPanelStack({
+ const props: PanelStackProps = {
onOpen: panel => {
stack = [...stack, panel];
},
stack,
- });
- expect(panelStackWrapper).toBeDefined();
+ };
+ renderPanelStack(props);
- const newPanelButton = panelStackWrapper.find("#new-panel-button");
- expect(newPanelButton).toBeDefined();
- newPanelButton.simulate("click");
- panelStackWrapper.setProps({ stack });
+ fireEvent.click(findFirst("#new-panel-button")!);
+ // Re-render with new stack
+ result?.rerender( );
- const newPanelHeader = panelStackWrapper.findClass(Classes.HEADING);
- expect(newPanelHeader).toBeDefined();
- expect(newPanelHeader.at(0).text()).toBe("New Panel 1");
+ const headers = findAll(`.${Classes.HEADING}`);
+ expect(headers[0].textContent).toBe("New Panel 1");
});
it("can render a panel stack with multiple initial panels and close one", () => {
let stack: Array> = [initialPanel, { renderPanel: TestPanel, title: "New Panel 1" }];
- panelStackWrapper = renderPanelStack({
+ const props: PanelStackProps = {
onClose: () => {
stack = stack.slice(0, -1);
},
stack,
- });
- expect(panelStackWrapper).toBeDefined();
+ };
+ renderPanelStack(props);
- const panelHeader = panelStackWrapper.findClass(Classes.HEADING);
- expect(panelHeader).toBeDefined();
- expect(panelHeader.at(0).text()).toBe("New Panel 1");
+ expect(findAll(`.${Classes.HEADING}`)[0].textContent).toBe("New Panel 1");
- const backButton = panelStackWrapper.findClass(Classes.PANEL_STACK_HEADER_BACK);
- expect(backButton).toBeDefined();
- backButton.simulate("click");
- panelStackWrapper.setProps({ stack });
+ fireEvent.click(findFirst(`.${Classes.PANEL_STACK_HEADER_BACK}`)!);
+ result?.rerender( );
- const firstPanelHeader = panelStackWrapper.findClass(Classes.HEADING);
- expect(firstPanelHeader).toBeDefined();
- expect(firstPanelHeader.at(0).text()).toBe("Test Title");
+ expect(findAll(`.${Classes.HEADING}`)[0].textContent).toBe("Test Title");
});
it("renders only one panel by default", () => {
@@ -250,12 +218,11 @@ describe("", () => {
{ renderPanel: TestPanel, title: "Panel A" },
{ renderPanel: TestPanel, title: "Panel B" },
];
- panelStackWrapper = renderPanelStack({ stack });
+ renderPanelStack({ stack });
- const panelHeaders = panelStackWrapper.findClass(Classes.HEADING);
- expect(panelHeaders).toBeDefined();
- expect(panelHeaders).toHaveLength(1);
- expect(panelHeaders.at(0).text()).toBe(stack[1].title);
+ const headers = findAll(`.${Classes.HEADING}`);
+ expect(headers).toHaveLength(1);
+ expect(headers[0].textContent).toBe(stack[1].title);
});
describe("with renderActivePanelOnly={false}", () => {
@@ -264,18 +231,17 @@ describe("", () => {
{ renderPanel: TestPanel, title: "Panel A" },
{ renderPanel: TestPanel, title: "Panel B" },
];
- panelStackWrapper = renderPanelStack({ renderActivePanelOnly: false, stack });
+ renderPanelStack({ renderActivePanelOnly: false, stack });
- const panelHeaders = panelStackWrapper.findClass(Classes.HEADING);
- expect(panelHeaders).toBeDefined();
- expect(panelHeaders).toHaveLength(2);
- expect(panelHeaders.at(0).text()).toBe(stack[0].title);
- expect(panelHeaders.at(1).text()).toBe(stack[1].title);
+ const headers = findAll(`.${Classes.HEADING}`);
+ expect(headers).toHaveLength(2);
+ expect(headers[0].textContent).toBe(stack[0].title);
+ expect(headers[1].textContent).toBe(stack[1].title);
});
it("keeps panels mounted", () => {
let stack = [initialPanel];
- panelStackWrapper = renderPanelStack({
+ const props: PanelStackProps = {
onClose: () => {
stack = stack.slice(0, -1);
},
@@ -284,20 +250,18 @@ describe("", () => {
},
renderActivePanelOnly: false,
stack,
- });
+ };
+ renderPanelStack(props);
- const incrementButton = panelStackWrapper.find(`[aria-label="increment"]`);
- expect(incrementButton).toBeDefined();
- incrementButton.hostNodes().simulate("mousedown");
+ fireEvent.mouseDown(findFirst('[aria-label="increment"]')!);
expect(getFirstPanelCounterValue(), "clicking increment button should increase counter").toBe(1);
- const newPanelButton = panelStackWrapper.find("#new-panel-button");
- newPanelButton.hostNodes().simulate("click");
- panelStackWrapper.setProps({ stack });
+ fireEvent.click(findAll("#new-panel-button")[0]);
+ result?.rerender( );
+
+ fireEvent.click(findFirst('[aria-label="Back"]')!);
+ result?.rerender( );
- const backButton = panelStackWrapper.find(`[aria-label="Back"]`);
- backButton.hostNodes().simulate("click");
- panelStackWrapper.setProps({ stack });
expect(
getFirstPanelCounterValue(),
"first panel should retain its counter state when we return to it",
@@ -305,22 +269,9 @@ describe("", () => {
});
function getFirstPanelCounterValue() {
- const counterValue = panelStackWrapper.find(`[aria-label="counter value"]`);
- expect(counterValue).toBeDefined();
- return parseInt(counterValue.hostNodes().first().text().trim(), 10);
+ const counterValue = findFirst('[aria-label="counter value"]');
+ return parseInt(counterValue!.textContent!.trim(), 10);
}
});
});
-
- interface PanelStackWrapper> extends ReactWrapper, any> {
- findClass(className: string): ReactWrapper, any>;
- }
-
- function renderPanelStack(props: PanelStackProps): PanelStackWrapper {
- panelStackWrapper = mount( , {
- attachTo: containerElement,
- }) as PanelStackWrapper;
- panelStackWrapper.findClass = (className: string) => panelStackWrapper.find(`.${className}`).hostNodes();
- return panelStackWrapper;
- }
});
diff --git a/packages/core/src/components/portal/portal.test.tsx b/packages/core/src/components/portal/portal.test.tsx
index 5b98e4ed6ba..1a6d7afeec2 100644
--- a/packages/core/src/components/portal/portal.test.tsx
+++ b/packages/core/src/components/portal/portal.test.tsx
@@ -14,35 +14,43 @@
* limitations under the License.
*/
-import { mount, type ReactWrapper } from "enzyme";
+import { render } from "@testing-library/react";
+
+type RenderResult = ReturnType;
import { afterEach, assert, beforeEach, describe, it } from "@blueprintjs/test-commons/vitest";
import { Classes } from "../../common";
import { PortalProvider } from "../../context/portal/portalProvider";
-import { Portal, type PortalProps } from "./portal";
+import { Portal } from "./portal";
describe("", () => {
- let rootElement: HTMLElement | undefined;
- let portal: ReactWrapper;
+ let rootElement: HTMLElement;
+ let result: RenderResult | undefined;
beforeEach(() => {
rootElement = document.createElement("div");
document.body.appendChild(rootElement);
});
afterEach(() => {
- portal?.unmount();
- rootElement?.remove();
+ result?.unmount();
+ result = undefined;
+ rootElement.remove();
});
+ function renderInRoot(ui: React.ReactElement) {
+ const r = render(ui, { container: rootElement });
+ result = r;
+ return r;
+ }
+
it("attaches contents to document.body", () => {
const CLASS_TO_TEST = "bp-test-content";
- portal = mount(
+ renderInRoot(
test
,
- { attachTo: rootElement },
);
assert.lengthOf(document.getElementsByClassName(CLASS_TO_TEST), 1);
});
@@ -51,11 +59,10 @@ describe("", () => {
const CLASS_TO_TEST = "bp-test-content";
const container = document.createElement("div");
document.body.appendChild(container);
- portal = mount(
+ renderInRoot(
test
,
- { attachTo: rootElement },
);
assert.lengthOf(container.getElementsByClassName(CLASS_TO_TEST), 1);
document.body.removeChild(container);
@@ -63,11 +70,10 @@ describe("", () => {
it("propagates className to portal element", () => {
const CLASS_TO_TEST = "bp-test-klass";
- portal = mount(
+ renderInRoot(
test
,
- { attachTo: rootElement },
);
const portalChild = document.querySelector(`.${Classes.PORTAL}.${CLASS_TO_TEST}`);
@@ -75,26 +81,28 @@ describe("", () => {
});
it("updates className on portal element", () => {
- portal = mount(
+ const { rerender } = renderInRoot(
test
,
- { attachTo: rootElement },
);
- assert.exists(portal.find(".class-one"));
- portal.setProps({ className: "class-two" });
- assert.exists(portal.find(".class-two"));
+ assert.exists(document.querySelector(".class-one"));
+ rerender(
+
+ test
+ ,
+ );
+ assert.exists(document.querySelector(".class-two"));
});
it("respects portalClassName on context", () => {
const CLASS_TO_TEST = "bp-test-klass bp-other-class";
- portal = mount(
+ renderInRoot(
test
,
- { attachTo: rootElement },
);
const portalElement = document.querySelector(`.${CLASS_TO_TEST.replace(" ", ".")}`);
@@ -102,39 +110,43 @@ describe("", () => {
});
it("does not crash when removing multiple classes from className", () => {
- portal = mount(
+ const { rerender } = renderInRoot(
test
,
- { attachTo: rootElement },
);
- portal.setProps({ className: undefined });
+ rerender(
+
+ test
+ ,
+ );
// no assertion necessary - will crash on incorrect code
});
it("does not crash when an empty string is provided for className", () => {
- portal = mount(
+ const { rerender } = renderInRoot(
test
,
- { attachTo: rootElement },
);
- portal.setProps({ className: "class-one" });
+ rerender(
+
+ test
+ ,
+ );
// no assertion necessary - will crash on incorrect code
});
it("children mount before onChildrenMount invoked", () =>
new Promise(done => {
function handleChildrenMount() {
- // can't use `portal` in here as `mount()` has not finished, so we query DOM directly instead
assert.exists(document.querySelector("p"));
done();
}
- portal = mount(
+ renderInRoot(
test
,
- { attachTo: rootElement },
);
}));
});
diff --git a/packages/core/src/components/resize-sensor/resizeSensor.test.tsx b/packages/core/src/components/resize-sensor/resizeSensor.test.tsx
index c793381dfd3..131c4aebd3b 100644
--- a/packages/core/src/components/resize-sensor/resizeSensor.test.tsx
+++ b/packages/core/src/components/resize-sensor/resizeSensor.test.tsx
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { mount, type ReactWrapper } from "enzyme";
+import { render } from "@testing-library/react";
import { createRef } from "react";
import { afterAll, afterEach, describe, expect, it, type MockInstance, vi } from "@blueprintjs/test-commons/vitest";
@@ -23,16 +23,16 @@ import { sleep } from "../../common/test-utils";
import { ResizeSensor, type ResizeSensorProps } from "./resizeSensor";
+type RenderResult = ReturnType;
+
describe.skip("", () => {
- // this scope variable is assigned in mountResizeSensor() and used in resize()
- let wrapper: ReactWrapper | undefined;
+ let result: RenderResult | undefined;
const containerElement = document.createElement("div");
document.documentElement.appendChild(containerElement);
afterEach(() => {
- // clean up wrapper after each test, if it was used
- wrapper?.unmount();
- wrapper?.detach();
+ result?.unmount();
+ result = undefined;
});
afterAll(() => containerElement.remove());
@@ -67,11 +67,12 @@ describe.skip("", () => {
it("onResize can be changed", async () => {
const onResize1 = vi.fn();
- mountResizeSensor({ onResize: onResize1 });
+ const props1 = { onResize: onResize1 };
+ mountResizeSensor(props1);
await resize({ id: 1, width: 200 });
const onResize2 = vi.fn();
- wrapper!.setProps({ onResize: onResize2 });
+ result!.rerender( );
await resize({ height: 100, id: 2 });
await resize({ id: 3, width: 55 });
@@ -91,17 +92,17 @@ describe.skip("", () => {
expect(targetRef.current?.clientWidth, "user-provided targetRef.current.clientWidth").toBe(RESIZE_WIDTH);
});
+ let lastProps: ResizeTesterProps | undefined;
function mountResizeSensor(props: Omit) {
- return (wrapper = mount(
- ,
- // must be in the DOM for measurement
- { attachTo: containerElement },
- ));
+ lastProps = { id: 0, ...props };
+ const r = render( , { container: containerElement });
+ result = r;
+ return r;
}
async function resize(size: SizeProps) {
- wrapper!.setProps(size);
- wrapper!.update();
+ lastProps = { ...lastProps!, ...size };
+ result!.rerender( );
await sleep(30);
}
diff --git a/packages/core/src/components/slider/handle.test.tsx b/packages/core/src/components/slider/handle.test.tsx
index 60012d63f4a..5beecb27f89 100644
--- a/packages/core/src/components/slider/handle.test.tsx
+++ b/packages/core/src/components/slider/handle.test.tsx
@@ -14,11 +14,13 @@
* limitations under the License.
*/
-import { mount, type ReactWrapper } from "enzyme";
+import { fireEvent, render, type RenderResult } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "@blueprintjs/test-commons/vitest";
-import { Handle, type HandleState, type InternalHandleProps } from "./handle";
+import { Classes } from "../../common";
+
+import { Handle, type InternalHandleProps } from "./handle";
import { DRAG_SIZE, simulateMovement } from "./sliderTestUtils";
const HANDLE_PROPS: InternalHandleProps = {
@@ -37,7 +39,6 @@ describe("", () => {
let containerElement: HTMLElement;
beforeEach(() => {
- // need an element in the document for tickSize to be a real number
containerElement = document.createElement("div");
document.body.appendChild(containerElement);
});
@@ -46,30 +47,30 @@ describe("", () => {
it("disabled handle never invokes event handlers", () => {
const eventSpy = vi.fn();
- const handle = mountHandle(0, { disabled: true, onChange: eventSpy, onRelease: eventSpy });
+ const handle = renderHandle(0, { disabled: true, onChange: eventSpy, onRelease: eventSpy });
simulateMovement(handle, { dragTimes: 3 });
- handle.simulate("keydown", { key: "ArrowUp" });
+ fireEvent.keyDown(handle, { key: "ArrowUp" });
expect(eventSpy).not.toHaveBeenCalled();
});
describe("keyboard events", () => {
it("pressing arrow key down reduces value by stepSize", () => {
const onChange = vi.fn();
- mountHandle(3, { onChange, stepSize: 2 }).simulate("keydown", { key: "ArrowDown" });
+ fireEvent.keyDown(renderHandle(3, { onChange, stepSize: 2 }), { key: "ArrowDown" });
expect(onChange).toHaveBeenCalledWith(1);
});
it("pressing arrow key up increases value by stepSize", () => {
const onChange = vi.fn();
- mountHandle(3, { onChange, stepSize: 4 }).simulate("keydown", { key: "ArrowUp" });
+ fireEvent.keyDown(renderHandle(3, { onChange, stepSize: 4 }), { key: "ArrowUp" });
expect(onChange).toHaveBeenCalledWith(7);
});
it("releasing arrow key calls onRelease with value", () => {
const onRelease = vi.fn();
- mountHandle(3, { onRelease, stepSize: 4 })
- .simulate("keydown", { key: "ArrowUp" })
- .simulate("keyup", { key: "ArrowUp" });
+ const handle = renderHandle(3, { onRelease, stepSize: 4 });
+ fireEvent.keyDown(handle, { key: "ArrowUp" });
+ fireEvent.keyUp(handle, { key: "ArrowUp" });
expect(onRelease).toHaveBeenCalledWith(3);
});
});
@@ -80,7 +81,7 @@ describe("", () => {
const options = { touch, vertical, verticalHeight: 0 };
it("onChange is invoked each time movement changes value", () => {
const onChange = vi.fn();
- simulateMovement(mountHandle(0, { onChange, vertical }), {
+ simulateMovement(renderHandle(0, { onChange, vertical }), {
dragTimes: 3,
...options,
});
@@ -90,8 +91,7 @@ describe("", () => {
it("onChange is not invoked if new value === props.value", () => {
const onChange = vi.fn();
- // move around same value
- simulateMovement(mountHandle(0, { onChange, vertical }), {
+ simulateMovement(renderHandle(0, { onChange, vertical }), {
dragSize: 0.1,
dragTimes: 4,
...options,
@@ -101,7 +101,7 @@ describe("", () => {
it("onRelease is invoked once on mouseup", () => {
const onRelease = vi.fn();
- simulateMovement(mountHandle(0, { onRelease, vertical }), {
+ simulateMovement(renderHandle(0, { onRelease, vertical }), {
dragTimes: 3,
...options,
});
@@ -110,7 +110,7 @@ describe("", () => {
it("onRelease is invoked if new value === props.value", () => {
const onRelease = vi.fn();
- simulateMovement(mountHandle(0, { onRelease, vertical }), {
+ simulateMovement(renderHandle(0, { onRelease, vertical }), {
dragTimes: 0,
...options,
});
@@ -120,12 +120,15 @@ describe("", () => {
});
});
- function mountHandle(
- value: number,
- props: Partial = {},
- ): ReactWrapper {
- return mount( , {
- attachTo: containerElement,
- });
+ function renderHandle(value: number, props: Partial = {}): HTMLElement {
+ const result: RenderResult = render(
+ ,
+ { container: containerElement },
+ );
+ const handle = result.container.querySelector(`.${Classes.SLIDER_HANDLE}`);
+ if (handle == null) {
+ throw new Error("Handle element not found");
+ }
+ return handle;
}
});
diff --git a/packages/core/src/components/slider/multiSlider.test.tsx b/packages/core/src/components/slider/multiSlider.test.tsx
index d0b593aa9ab..ca43288f4be 100644
--- a/packages/core/src/components/slider/multiSlider.test.tsx
+++ b/packages/core/src/components/slider/multiSlider.test.tsx
@@ -14,14 +14,13 @@
* limitations under the License.
*/
-import { mount, type ReactWrapper } from "enzyme";
+import { fireEvent, render, type RenderResult } from "@testing-library/react";
import { expectPropValidationError } from "@blueprintjs/test-commons";
-import { afterEach, assert, beforeEach, describe, expect, it, vi } from "@blueprintjs/test-commons/vitest";
+import { afterEach, beforeEach, describe, expect, it, vi } from "@blueprintjs/test-commons/vitest";
import { Classes } from "../../common";
-import { Handle } from "./handle";
import { MultiSlider, MultiSliderHandle, type MultiSliderProps } from "./multiSlider";
import { mouseUpHorizontal, simulateMovement } from "./sliderTestUtils";
@@ -34,9 +33,7 @@ describe("", () => {
const onRelease = vi.fn();
beforeEach(() => {
- // need an element in the document for tickSize to be a real number
containerElement = document.createElement("div");
- // default min-max is 0-10 so there are 10 steps
containerElement.style.width = `${STEP_SIZE * 10}px`;
document.body.appendChild(containerElement);
@@ -50,28 +47,32 @@ describe("", () => {
describe("handles", () => {
it.skip("handle values are automatically sorted", () => {
- const slider = renderSlider({ onRelease, values: [5, 10, 0] });
- slider.find(Handle).first().simulate("mousedown", { clientX: 0 });
+ const { container } = renderSlider({ onRelease, values: [5, 10, 0] });
+ const firstHandle = container.querySelectorAll(`.${Classes.SLIDER_HANDLE}`)[0];
+ fireEvent.mouseDown(firstHandle, { clientX: 0 });
mouseUpHorizontal(0);
expect(onRelease).toHaveBeenCalledOnce();
expect(onRelease.mock.calls[0][0]).toEqual([0, 5, 10]);
});
it("propagates className to the handles", () => {
- const slider = mount(
+ const { container } = render(
,
- { attachTo: containerElement },
+ { container: containerElement },
);
- assert.lengthOf(slider.find("span.testClass"), 1);
+ expect(container.querySelectorAll("span.testClass")).toHaveLength(1);
});
it.skip("moving mouse on the first handle updates the first value", () => {
- const slider = renderSlider({ onChange });
- simulateMovement(slider, { dragSize: STEP_SIZE, dragTimes: 4, handleIndex: 0 });
- // called 3 times for the move to 1, 2, 3, and 4
+ const { container } = renderSlider({ onChange });
+ simulateMovement(container.firstElementChild as HTMLElement, {
+ dragSize: STEP_SIZE,
+ dragTimes: 4,
+ handleIndex: 0,
+ });
expect(onChange).toHaveBeenCalledTimes(4);
expect(onChange.mock.calls.map(arg => arg[0])).toEqual([
[1, 5, 10],
@@ -82,14 +83,13 @@ describe("", () => {
});
it.skip("moving mouse on the middle handle updates the middle value", () => {
- const slider = renderSlider({ onChange });
- simulateMovement(slider, {
+ const { container } = renderSlider({ onChange });
+ simulateMovement(container.firstElementChild as HTMLElement, {
dragSize: STEP_SIZE,
dragTimes: 4,
from: STEP_SIZE * 5,
handleIndex: 1,
});
- // called 3 times for the move to 6, 7, 8, and 9
expect(onChange).toHaveBeenCalledTimes(4);
expect(onChange.mock.calls.map(arg => arg[0])).toEqual([
[0, 6, 10],
@@ -100,14 +100,13 @@ describe("", () => {
});
it.skip("moving mouse on the last handle updates the last value", () => {
- const slider = renderSlider({ onChange });
- simulateMovement(slider, {
+ const { container } = renderSlider({ onChange });
+ simulateMovement(container.firstElementChild as HTMLElement, {
dragSize: -STEP_SIZE,
dragTimes: 4,
from: STEP_SIZE * 10,
handleIndex: 2,
});
- // called 3 times for the move to 9, 8, 7, and 6
expect(onChange).toHaveBeenCalledTimes(4);
expect(onChange.mock.calls.map(arg => arg[0])).toEqual([
[0, 5, 9],
@@ -118,54 +117,53 @@ describe("", () => {
});
it.skip("releasing mouse on a track value closer to the first handle moves the first handle", () => {
- const slider = renderSlider({ onChange });
- slider.simulate("mousedown", { clientX: STEP_SIZE });
+ const { container } = renderSlider({ onChange });
+ fireEvent.mouseDown(container.firstElementChild as HTMLElement, { clientX: STEP_SIZE });
expect(onChange).toHaveBeenCalledOnce();
expect(onChange.mock.calls[0][0]).toEqual([1, 5, 10]);
});
it.skip("releasing mouse on a track value slightly below the middle handle moves the middle handle", () => {
- const slider = renderSlider({ onChange });
- slider.simulate("mousedown", { clientX: STEP_SIZE * 4 });
+ const { container } = renderSlider({ onChange });
+ fireEvent.mouseDown(container.firstElementChild as HTMLElement, { clientX: STEP_SIZE * 4 });
expect(onChange).toHaveBeenCalledOnce();
expect(onChange.mock.calls[0][0]).toEqual([0, 4, 10]);
});
it.skip("releasing mouse on a track value slightly above the middle handle moves the middle handle", () => {
- const slider = renderSlider({ onChange });
- slider.simulate("mousedown", { clientX: STEP_SIZE * 6 });
+ const { container } = renderSlider({ onChange });
+ fireEvent.mouseDown(container.firstElementChild as HTMLElement, { clientX: STEP_SIZE * 6 });
expect(onChange).toHaveBeenCalledOnce();
expect(onChange.mock.calls[0][0]).toEqual([0, 6, 10]);
});
it.skip("releasing mouse on a track value closer to the last handle moves the last handle", () => {
- const slider = renderSlider({ onChange });
- slider.simulate("mousedown", { clientX: STEP_SIZE * 9 });
+ const { container } = renderSlider({ onChange });
+ fireEvent.mouseDown(container.firstElementChild as HTMLElement, { clientX: STEP_SIZE * 9 });
expect(onChange).toHaveBeenCalledOnce();
expect(onChange.mock.calls[0][0]).toEqual([0, 5, 9]);
});
it.skip("when values are equal, releasing mouse on a track still moves the nearest handle", () => {
- const slider = renderSlider({ onChange, values: [5, 5, 7] });
+ const { container } = renderSlider({ onChange, values: [5, 5, 7] });
+ const slider = container.firstElementChild as HTMLElement;
- slider.simulate("mousedown", { clientX: STEP_SIZE * 1 });
+ fireEvent.mouseDown(slider, { clientX: STEP_SIZE * 1 });
expect(onChange).toHaveBeenCalledOnce();
expect(onChange.mock.calls[0][0]).toEqual([1, 5, 7]);
onChange.mockClear();
- slider.simulate("mousedown", { clientX: STEP_SIZE * 9 });
+ fireEvent.mouseDown(slider, { clientX: STEP_SIZE * 9 });
expect(onChange).toHaveBeenCalledOnce();
expect(onChange.mock.calls[0][0]).toEqual([5, 5, 9]);
});
it("values outside of bounds are clamped", () => {
- const slider = renderSlider({ values: [-1, 5, 12] });
- slider.find(`.${Classes.SLIDER_PROGRESS}`).forEach(progress => {
- const { left, right } = progress.prop("style")!;
- // CSS properties are percentage strings, but parsing will ignore trailing "%".
- // percentages should be in 0-100% range.
- assert.isAtLeast(parseFloat(left!.toString()), 0);
- assert.isAtMost(parseFloat(right!.toString()), 100);
+ const { container } = renderSlider({ values: [-1, 5, 12] });
+ container.querySelectorAll(`.${Classes.SLIDER_PROGRESS}`).forEach(progress => {
+ const { left, right } = progress.style;
+ expect(parseFloat(left)).toBeGreaterThanOrEqual(0);
+ expect(parseFloat(right)).toBeLessThanOrEqual(100);
});
});
});
@@ -173,116 +171,122 @@ describe("", () => {
describe("labels", () => {
it("renders label with labelStepSize fallback of 1 when not provided", () => {
// [0 1 2 3 4 5]
- const wrapper = renderSlider({ max: 5, min: 0 });
- assertLabelCount(wrapper, 6);
+ const { container } = renderSlider({ max: 5, min: 0 });
+ expectLabelCount(container, 6);
});
it("renders label for value and for each labelStepSize", () => {
// [0 10 20 30 40 50]
- const wrapper = renderSlider({ labelStepSize: 10, max: 50, min: 0 });
- assertLabelCount(wrapper, 6);
+ const { container } = renderSlider({ labelStepSize: 10, max: 50, min: 0 });
+ expectLabelCount(container, 6);
});
it("renders labels provided in labelValues prop", () => {
const labelValues = [0, 30, 50, 60];
- const wrapper = renderSlider({ labelValues, max: 50, min: 0 });
- assertLabelCount(wrapper, 4);
+ const { container } = renderSlider({ labelValues, max: 50, min: 0 });
+ expectLabelCount(container, 4);
});
it("renders all labels even when floating point approx would cause the last one to be skipped", () => {
// [0 0.14 0.28 0.42 0.56 0.70]
- const wrapper = renderSlider({ labelStepSize: 0.14, max: 0.7, min: 0 });
- assertLabelCount(wrapper, 6);
+ const { container } = renderSlider({ labelStepSize: 0.14, max: 0.7, min: 0 });
+ expectLabelCount(container, 6);
});
it("renders result of labelRenderer() in each label", () => {
const labelRenderer = (val: number) => val + "#";
- const wrapper = renderSlider({ labelRenderer, labelStepSize: 10, max: 50, min: 0 });
- assert.strictEqual(wrapper.find(`.${Classes.SLIDER}-axis`).text(), "0#10#20#30#40#50#");
+ const { container } = renderSlider({ labelRenderer, labelStepSize: 10, max: 50, min: 0 });
+ expect(container.querySelector(`.${Classes.SLIDER}-axis`)?.textContent).toBe("0#10#20#30#40#50#");
});
it("renders result of labelRenderer() in each label with labelValues", () => {
const labelRenderer = (val: number) => val + "#";
- const wrapper = renderSlider({ labelRenderer, labelValues: [20, 40, 50], max: 50, min: 0 });
- assert.strictEqual(wrapper.find(`.${Classes.SLIDER}-axis`).text(), "20#40#50#");
+ const { container } = renderSlider({ labelRenderer, labelValues: [20, 40, 50], max: 50, min: 0 });
+ expect(container.querySelector(`.${Classes.SLIDER}-axis`)?.textContent).toBe("20#40#50#");
});
it("default labelRenderer() fixes decimal places to labelPrecision", () => {
- const wrapper = renderSlider({ labelPrecision: 1, values: [0.99 / 10, 1, 1] });
- const firstHandle = wrapper.find(Handle).first();
- assert.strictEqual(firstHandle.text(), "0.1");
+ const { container } = renderSlider({ labelPrecision: 1, values: [0.99 / 10, 1, 1] });
+ const firstHandle = container.querySelector(`.${Classes.SLIDER_HANDLE}`);
+ expect(firstHandle?.textContent).toBe("0.1");
});
it("infers precision of default labelRenderer from stepSize", () => {
- const wrapper = renderSlider({ stepSize: 0.01 });
- assert.strictEqual(wrapper.state("labelPrecision"), 2);
+ // stepSize 0.01 implies precision 2; verify via rendered label content.
+ const { container } = renderSlider({ stepSize: 0.01 });
+ const firstHandle = container.querySelector(`.${Classes.SLIDER_HANDLE}`);
+ expect(firstHandle?.textContent).toBe("0.00");
});
it("labelRenderer={false} removes all labels", () => {
- const wrapper = renderSlider({ labelRenderer: false });
- assertLabelCount(wrapper, 0);
+ const { container } = renderSlider({ labelRenderer: false });
+ expectLabelCount(container, 0);
});
- function assertLabelCount(wrapper: ReactWrapper, expected: number) {
- assert.lengthOf(wrapper.find(`.${Classes.SLIDER}-axis`).find(`.${Classes.SLIDER_LABEL}`), expected);
+ function expectLabelCount(container: HTMLElement, expected: number) {
+ expect(container.querySelectorAll(`.${Classes.SLIDER}-axis .${Classes.SLIDER_LABEL}`)).toHaveLength(
+ expected,
+ );
}
});
describe("track", () => {
- let slider: ReactWrapper;
+ let result: RenderResult;
beforeEach(() => {
- slider = mount(
+ result = render(
,
- { attachTo: containerElement },
+ { container: containerElement },
);
});
it("progress bars are rendered between all handles", () => {
// N values = N+1 track segments
- assert.lengthOf(slider.find(`.${Classes.SLIDER_PROGRESS}`), 4);
+ expect(result.container.querySelectorAll(`.${Classes.SLIDER_PROGRESS}`)).toHaveLength(4);
});
it("intentAfter beats intentBefore", () => {
- const intents = slider.find(`.${Classes.SLIDER_PROGRESS}`).map(segment => {
- const match = segment.prop("className")?.match(/-intent-(\w+)/) || [];
- return match[1];
- });
- // last segment has default intent
- assert.deepEqual(intents, ["primary", "danger", "danger", "warning"]);
+ const intents = Array.from(
+ result.container.querySelectorAll(`.${Classes.SLIDER_PROGRESS}`),
+ ).map(segment => segment.className.match(/-intent-(\w+)/)?.[1]);
+ expect(intents).toEqual(["primary", "danger", "danger", "warning"]);
});
it("showTrackFill=false ignores track intents", () => {
- slider.setProps({ showTrackFill: false });
- slider.find(`.${Classes.SLIDER_PROGRESS}`).map(segment => {
- // segments rendered but they nave no intent
- assert.isNull(segment.prop("className")?.match(/-intent-(\w+)/));
+ result.rerender(
+
+
+
+
+ ,
+ );
+ result.container.querySelectorAll(`.${Classes.SLIDER_PROGRESS}`).forEach(segment => {
+ expect(segment.className.match(/-intent-(\w+)/)).toBeNull();
});
});
it("track section positioning is correct", () => {
- slider = mount(
+ const { container } = render(
,
);
- const locations = slider.find(`.${Classes.SLIDER_PROGRESS}`).map(segment => {
- const match = segment.prop("style")!;
- return [match.left, match.right];
- });
- assert.deepEqual(locations, [
- ["0.00%", "100.00%"],
- ["0.00%", "80.00%"],
- ["20.00%", "0.00%"],
+ const locations = Array.from(container.querySelectorAll(`.${Classes.SLIDER_PROGRESS}`)).map(
+ segment => [parseFloat(segment.style.left), parseFloat(segment.style.right)],
+ );
+ expect(locations).toEqual([
+ [0, 100],
+ [0, 80],
+ [20, 0],
]);
});
it("trackStyleBefore and trackStyleAfter work as intended", () => {
- slider = mount(
+ const { container } = render(
", () => {
,
);
- const trackBackgrounds = slider
- .find(`.${Classes.SLIDER_PROGRESS}`)
- .map(segment => segment.prop("style")?.background);
+ const trackBackgrounds = Array.from(
+ container.querySelectorAll(`.${Classes.SLIDER_PROGRESS}`),
+ ).map(segment => segment.style.background);
- assert.equal(trackBackgrounds[0], "red");
- assert.equal(trackBackgrounds[1], "yellow");
- assert.equal(trackBackgrounds[2], "purple");
+ expect(trackBackgrounds[0]).toBe("red");
+ expect(trackBackgrounds[1]).toBe("yellow");
+ expect(trackBackgrounds[2]).toBe("purple");
});
});
@@ -341,15 +345,15 @@ describe("", () => {
});
});
- function renderSlider(joinedProps: MultiSliderProps & { values?: [number, number, number] } = {}) {
+ function renderSlider(joinedProps: MultiSliderProps & { values?: [number, number, number] } = {}): RenderResult {
const { values = [0, 5, 10], ...props } = joinedProps;
- return mount(
+ return render(
,
- { attachTo: containerElement },
+ { container: containerElement },
);
}
});
diff --git a/packages/core/src/components/slider/rangeSlider.test.tsx b/packages/core/src/components/slider/rangeSlider.test.tsx
index c7415793ca2..364e43f5e16 100644
--- a/packages/core/src/components/slider/rangeSlider.test.tsx
+++ b/packages/core/src/components/slider/rangeSlider.test.tsx
@@ -14,14 +14,13 @@
* limitations under the License.
*/
-import { mount } from "enzyme";
+import { fireEvent, render, type RenderResult } from "@testing-library/react";
import { expectPropValidationError } from "@blueprintjs/test-commons";
import { afterEach, beforeEach, describe, expect, it, vi } from "@blueprintjs/test-commons/vitest";
import { Classes } from "../../common";
-import { Handle } from "./handle";
import { RangeSlider } from "./rangeSlider";
const STEP_SIZE = 20;
@@ -30,9 +29,7 @@ describe("", () => {
let containerElement: HTMLElement;
beforeEach(() => {
- // need an element in the document for tickSize to be a real number
containerElement = document.createElement("div");
- // default min-max is 0-10 so there are 10 steps
containerElement.style.width = `${STEP_SIZE * 10}px`;
document.body.appendChild(containerElement);
});
@@ -40,16 +37,15 @@ describe("", () => {
afterEach(() => containerElement.remove());
it("renders two interactive s", () => {
- const handles = renderSlider( ).find(Handle);
- expect(handles).toHaveLength(2);
+ const { container } = renderSlider( );
+ expect(container.querySelectorAll(`.${Classes.SLIDER_HANDLE}`)).toHaveLength(2);
});
it.skip("renders primary track segment between two values", () => {
- const track = renderSlider( ).find(
- `.${Classes.SLIDER_PROGRESS}.${Classes.INTENT_PRIMARY}`,
- );
- expect(track).toHaveLength(1);
- expect(track.getDOMNode().getBoundingClientRect().width).toBe(STEP_SIZE * 3);
+ const { container } = renderSlider( );
+ const tracks = container.querySelectorAll(`.${Classes.SLIDER_PROGRESS}.${Classes.INTENT_PRIMARY}`);
+ expect(tracks).toHaveLength(1);
+ expect(tracks[0].getBoundingClientRect().width).toBe(STEP_SIZE * 3);
});
it("throws error if range value contains null", () => {
@@ -65,13 +61,14 @@ describe("", () => {
it("disabled slider does not respond to key presses", () => {
const changeSpy = vi.fn();
- const handles = renderSlider( ).find(Handle);
- handles.first().simulate("keydown", { key: "ArrowDown" });
- handles.last().simulate("keydown", { key: "ArrowDown" });
+ const { container } = renderSlider( );
+ const handles = container.querySelectorAll(`.${Classes.SLIDER_HANDLE}`);
+ fireEvent.keyDown(handles[0], { key: "ArrowDown" });
+ fireEvent.keyDown(handles[handles.length - 1], { key: "ArrowDown" });
expect(changeSpy).not.toHaveBeenCalled();
});
- function renderSlider(slider: React.JSX.Element) {
- return mount(slider, { attachTo: containerElement });
+ function renderSlider(slider: React.JSX.Element): RenderResult {
+ return render(slider, { container: containerElement });
}
});
diff --git a/packages/core/src/components/slider/slider.test.tsx b/packages/core/src/components/slider/slider.test.tsx
index 4c559449bfe..62a58a2e1c6 100644
--- a/packages/core/src/components/slider/slider.test.tsx
+++ b/packages/core/src/components/slider/slider.test.tsx
@@ -14,13 +14,12 @@
* limitations under the License.
*/
-import { mount } from "enzyme";
+import { fireEvent, render, type RenderResult } from "@testing-library/react";
-import { afterEach, assert, beforeEach, describe, expect, it, vi } from "@blueprintjs/test-commons/vitest";
+import { afterEach, beforeEach, describe, expect, it, vi } from "@blueprintjs/test-commons/vitest";
import { Classes } from "../../common";
-import { Handle } from "./handle";
import { Slider } from "./slider";
import { simulateMovement } from "./sliderTestUtils";
@@ -31,9 +30,7 @@ describe("", () => {
let containerElement: HTMLElement;
beforeEach(() => {
- // need an element in the document for tickSize to be a real number
containerElement = document.createElement("div");
- // default min-max is 0-10 so there are 10 steps
containerElement.style.width = `${STEP_SIZE * 10}px`;
document.body.appendChild(containerElement);
});
@@ -41,57 +38,54 @@ describe("", () => {
afterEach(() => containerElement.remove());
it("renders one interactive ", () => {
- const handles = renderSlider( ).find(Handle);
- assert.lengthOf(handles, 1);
+ const { container } = renderSlider( );
+ expect(container.querySelectorAll(`.${Classes.SLIDER_HANDLE}`)).toHaveLength(1);
});
it.skip("renders primary track segment between initialValue and value", () => {
- const tracks = renderSlider( ).find(
- `.${Classes.SLIDER_PROGRESS}.${Classes.INTENT_PRIMARY}`,
- );
- assert.lengthOf(tracks, 1);
- assert.equal(tracks.getDOMNode().getBoundingClientRect().width, STEP_SIZE * 3);
+ const { container } = renderSlider( );
+ const tracks = container.querySelectorAll(`.${Classes.SLIDER_PROGRESS}.${Classes.INTENT_PRIMARY}`);
+ expect(tracks).toHaveLength(1);
+ expect(tracks[0].getBoundingClientRect().width).toBe(STEP_SIZE * 3);
});
it.skip("renders primary track segment between initialValue and value when value is less than initial value", () => {
- const tracks = renderSlider( ).find(
- `.${Classes.SLIDER_PROGRESS}.${Classes.INTENT_PRIMARY}`,
- );
- assert.lengthOf(tracks, 1);
- assert.equal(tracks.getDOMNode().getBoundingClientRect().width, STEP_SIZE * 3);
+ const { container } = renderSlider( );
+ const tracks = container.querySelectorAll(`.${Classes.SLIDER_PROGRESS}.${Classes.INTENT_PRIMARY}`);
+ expect(tracks).toHaveLength(1);
+ expect(tracks[0].getBoundingClientRect().width).toBe(STEP_SIZE * 3);
});
it("renders no primary track segment when value equals initial value", () => {
- const tracks = renderSlider( ).find(
- `.${Classes.SLIDER_PROGRESS}.${Classes.INTENT_PRIMARY}`,
- );
- assert.lengthOf(tracks, 0);
+ const { container } = renderSlider( );
+ expect(container.querySelectorAll(`.${Classes.SLIDER_PROGRESS}.${Classes.INTENT_PRIMARY}`)).toHaveLength(0);
});
it("renders result of labelRenderer() in each label and differently in handle", () => {
const labelRenderer = (val: number, opts?: { isHandleTooltip: boolean }) =>
val + (opts?.isHandleTooltip ? "!" : "#");
- const wrapper = renderSlider(
+ const { container } = renderSlider(
,
);
- assert.strictEqual(wrapper.find(`.${Classes.SLIDER}-axis`).text(), "0#10#20#30#40#50#");
- assert.strictEqual(wrapper.find(`.${Classes.SLIDER_HANDLE}`).find(`.${Classes.SLIDER_LABEL}`).text(), "10!");
+ expect(container.querySelector(`.${Classes.SLIDER}-axis`)?.textContent).toBe("0#10#20#30#40#50#");
+ expect(container.querySelector(`.${Classes.SLIDER_HANDLE} .${Classes.SLIDER_LABEL}`)?.textContent).toBe("10!");
});
it.skip("moving mouse calls onChange with nearest value", () => {
const changeSpy = vi.fn();
- simulateMovement(renderSlider( ), {
+ const { container } = renderSlider( );
+ simulateMovement(container.firstElementChild as HTMLElement, {
dragSize: STEP_SIZE,
dragTimes: 4,
});
- // called 4 times, for the move to 1, 2, 3, and 4
expect(changeSpy).toHaveBeenCalledTimes(4);
expect(changeSpy.mock.calls).toEqual([[1], [2], [3], [4]]);
});
it.skip("releasing mouse calls onRelease with nearest value", () => {
const releaseSpy = vi.fn();
- simulateMovement(renderSlider( ), {
+ const { container } = renderSlider( );
+ simulateMovement(container.firstElementChild as HTMLElement, {
dragSize: STEP_SIZE,
dragTimes: 1,
});
@@ -101,16 +95,16 @@ describe("", () => {
it.skip("disabled slider never invokes event handlers", () => {
const eventSpy = vi.fn();
- const slider = renderSlider( );
- // handle drag and keys
+ const { container } = renderSlider( );
+ const slider = container.firstElementChild as HTMLElement;
simulateMovement(slider, { dragTimes: 3 });
- slider.simulate("keydown", { key: "ArrowUp" });
- // track click
- slider.find(TRACK_SELECTOR).simulate("mousedown", { target: containerElement.querySelector(TRACK_SELECTOR) });
+ fireEvent.keyDown(slider, { key: "ArrowUp" });
+ const track = slider.querySelector(TRACK_SELECTOR)!;
+ fireEvent.mouseDown(track);
expect(eventSpy).not.toHaveBeenCalled();
});
- function renderSlider(slider: React.JSX.Element) {
- return mount(slider, { attachTo: containerElement });
+ function renderSlider(slider: React.JSX.Element): RenderResult {
+ return render(slider, { container: containerElement });
}
});
diff --git a/packages/core/src/components/slider/sliderTestUtils.ts b/packages/core/src/components/slider/sliderTestUtils.ts
index 15038def70f..696e4967341 100644
--- a/packages/core/src/components/slider/sliderTestUtils.ts
+++ b/packages/core/src/components/slider/sliderTestUtils.ts
@@ -14,11 +14,11 @@
* limitations under the License.
*/
-import type { ReactWrapper } from "enzyme";
+import { fireEvent } from "@testing-library/react";
import { dispatchMouseEvent, dispatchTouchEvent } from "@blueprintjs/test-commons/vitest-utils";
-import { Handle, type InternalHandleProps } from "./handle";
+import { Classes } from "../../common";
interface MoveOptions {
/** Size in pixels of one drag event. Direction of drag is determined by `vertical` option. */
@@ -41,31 +41,30 @@ export const DRAG_SIZE = 20;
/**
* Simulates a full move of a slider handle: engage, move, release.
- * Supports touch and vertical events. Use options to configure exact movement.
+ * Pass either the slider container element (a handle is queried by `handleIndex`)
+ * or a specific handle element.
*/
-export function simulateMovement(wrapper: ReactWrapper, options: MoveOptions) {
+export function simulateMovement(target: HTMLElement, options: MoveOptions) {
const { from = 0, handleIndex = 0, touch = false } = options;
- const handle = wrapper.find(Handle).at(handleIndex);
+ const handle = target.classList.contains(Classes.SLIDER_HANDLE)
+ ? target
+ : (target.querySelectorAll(`.${Classes.SLIDER_HANDLE}`)[handleIndex] ?? target);
const eventData =
options.vertical !== undefined && options.verticalHeight !== undefined
? { clientY: options.verticalHeight - from }
: { clientX: from };
if (touch) {
- handle.simulate("touchstart", { changedTouches: [eventData] });
+ fireEvent.touchStart(handle, { changedTouches: [eventData] });
} else {
- handle.simulate("mousedown", eventData);
+ fireEvent.mouseDown(handle, eventData);
}
genericMove(options);
genericRelease(options);
- return wrapper;
}
/** Release the mouse at the given clientX pixel. Useful for ending a drag interaction. */
export const mouseUpHorizontal = (clientX: number) => genericRelease({ dragTimes: 0, from: clientX });
-// Private helpers
-// ===============
-
function genericMove(options: MoveOptions) {
const { dragSize = DRAG_SIZE, from = 0, dragTimes = 1, touch } = options;
const eventName = touch ? "touchmove" : "mousemove";
@@ -86,7 +85,6 @@ function dispatchEvent(options: MoveOptions, eventName: string, clientPixel: num
const { touch, vertical, verticalHeight = 0 } = options;
const dispatchFn = touch ? dispatchTouchEvent : dispatchMouseEvent;
if (vertical) {
- // vertical sliders go from bottom-up, so everything is backward
dispatchFn(document, eventName, undefined, verticalHeight - clientPixel);
} else {
dispatchFn(document, eventName, clientPixel, undefined);
diff --git a/packages/core/src/components/tabs/tabs.test.tsx b/packages/core/src/components/tabs/tabs.test.tsx
index 96e7cb235eb..7ee8e584f75 100644
--- a/packages/core/src/components/tabs/tabs.test.tsx
+++ b/packages/core/src/components/tabs/tabs.test.tsx
@@ -13,44 +13,72 @@
* limitations under the License.
*/
-import { waitFor } from "@testing-library/dom";
-import { mount, type ReactWrapper } from "enzyme";
-import { act } from "react";
+import { fireEvent, render, waitFor } from "@testing-library/react";
+import { cloneElement, createRef } from "react";
import { afterEach, assert, beforeEach, describe, expect, it, vi } from "@blueprintjs/test-commons/vitest";
import { Classes } from "../../common";
import { Tab } from "./tab";
-import { Tabs, type TabsProps, type TabsState } from "./tabs";
+import { Tabs } from "./tabs";
import { generateTabIds } from "./tabTitle";
+type RenderResult = ReturnType;
+
describe("", () => {
const ID = "tabsTests";
- // default tabs content is generated from these IDs in each test
const TAB_IDS = ["first", "second", "third"];
- // selectors using ARIA role
const TAB_SELECTOR = "[role='tab']";
const TAB_LIST_SELECTOR = "[role='tablist']";
const TAB_PANEL_SELECTOR = "[role='tabpanel']";
let containerElement: HTMLElement;
+ let result: RenderResult | undefined;
beforeEach(() => {
containerElement = document.createElement("div");
document.body.appendChild(containerElement);
});
- afterEach(() => containerElement.remove());
+ afterEach(() => {
+ result?.unmount();
+ result = undefined;
+ containerElement.remove();
+ });
+
+ function renderTabs(ui: React.ReactElement) {
+ const ref = createRef();
+ const cloned = ui.type === Tabs ? cloneElement(ui, { ref } as any) : ui;
+ const r = render(cloned, { container: containerElement });
+ result = r;
+ return { container: r.container, instance: ref.current!, rerender: r.rerender };
+ }
+
+ function findTabs(container: HTMLElement) {
+ return Array.from(container.querySelectorAll(TAB_SELECTOR));
+ }
+
+ function findTabPanels(container: HTMLElement) {
+ return Array.from(container.querySelectorAll(TAB_PANEL_SELECTOR));
+ }
+
+ function findTabById(container: HTMLElement, id: string): HTMLElement | null {
+ return container.querySelector(`${TAB_SELECTOR}[data-tab-id='${id}']`);
+ }
+
+ function getSelectedTabId(instance: Tabs): string | undefined {
+ return instance.state.selectedTabId as string | undefined;
+ }
it("gets by without children", () => {
- assert.doesNotThrow(() => mount( ));
+ assert.doesNotThrow(() => render( ));
});
it("supports non-existent children", () => {
assert.doesNotThrow(() =>
- mount(
+ render(
{null}
@@ -62,88 +90,83 @@ describe("", () => {
});
it("default selectedTabId is first non-null Tab id", () => {
- const wrapper = mount(
+ const { container, instance } = renderTabs(
{null}
- { }
+
{getTabsContents()}
,
);
- assert.lengthOf(wrapper.find(TAB_SELECTOR), 3);
- assert.strictEqual(wrapper.state("selectedTabId"), TAB_IDS[0]);
+ assert.lengthOf(findTabs(container), 3);
+ assert.strictEqual(getSelectedTabId(instance), TAB_IDS[0]);
});
it("renders one TabTitle and one TabPanel for each Tab, aria roles are correct", () => {
- const wrapper = mount({getTabsContents()} );
- assert.lengthOf(wrapper.find(TAB_SELECTOR), 3);
- assert.lengthOf(wrapper.find(TAB_LIST_SELECTOR), 1);
- assert.lengthOf(wrapper.find(TAB_PANEL_SELECTOR), 3);
+ const { container } = renderTabs({getTabsContents()} );
+ assert.lengthOf(findTabs(container), 3);
+ assert.lengthOf(container.querySelectorAll(TAB_LIST_SELECTOR), 1);
+ assert.lengthOf(findTabPanels(container), 3);
});
it("renders all Tab children, active is not aria-hidden", () => {
const activeIndex = 1;
- const wrapper = mount({getTabsContents()} );
- act(() => {
- wrapper.setState({ selectedTabId: TAB_IDS[activeIndex] });
- });
- const tabPanels = wrapper.find(TAB_PANEL_SELECTOR);
+ const { container, rerender } = renderTabs({getTabsContents()} );
+ rerender(
+
+ {getTabsContents()}
+ ,
+ );
+ const tabPanels = findTabPanels(container);
assert.lengthOf(tabPanels, 3);
for (let i = 0; i < TAB_IDS.length; i++) {
- // hidden unless it is active
- assert.equal(tabPanels.at(i).prop("aria-hidden"), i !== activeIndex);
+ assert.equal(tabPanels[i].getAttribute("aria-hidden"), String(i !== activeIndex));
}
});
it(`renders without ${Classes.LARGE} when by default`, () => {
- const wrapper = mount({getTabsContents()} );
- assert.lengthOf(wrapper.find(`${TAB_LIST_SELECTOR}.${Classes.LARGE}`), 0);
+ const { container } = renderTabs({getTabsContents()} );
+ assert.lengthOf(container.querySelectorAll(`${TAB_LIST_SELECTOR}.${Classes.LARGE}`), 0);
});
it(`renders using ${Classes.LARGE} when size="large"`, () => {
- const wrapper = mount(
+ const { container } = renderTabs(
{getTabsContents()}
,
);
- assert.lengthOf(wrapper.find(`${TAB_LIST_SELECTOR}.${Classes.LARGE}`), 1);
+ assert.lengthOf(container.querySelectorAll(`${TAB_LIST_SELECTOR}.${Classes.LARGE}`), 1);
});
it("attaches className to both tab and panel container if set", () => {
const tabClassName = "tabClassName";
- const wrapper = mount(
+ const { container } = renderTabs(
} />
- ,
} />
- ,
- } />,
+ } />
,
);
const NUM_TABS = 3;
- assert.lengthOf(wrapper.find(TAB_SELECTOR), NUM_TABS);
- assert.lengthOf(wrapper.find(TAB_PANEL_SELECTOR), NUM_TABS);
- assert.lengthOf(wrapper.find(`.${tabClassName}`).hostNodes(), NUM_TABS * 2);
+ assert.lengthOf(findTabs(container), NUM_TABS);
+ assert.lengthOf(findTabPanels(container), NUM_TABS);
+ assert.lengthOf(container.querySelectorAll(`.${tabClassName}`), NUM_TABS * 2);
});
it("attaches panelClassName to panel container if set", () => {
const panelClassName = "secondPanelClassName";
- const wrapper = mount(
+ const { container } = renderTabs(
- } />,
+ } />
} />
- ,
- } />,
+ } />
,
);
- const NUM_TABS = 3;
- assert.lengthOf(wrapper.find(TAB_SELECTOR), NUM_TABS);
- assert.lengthOf(wrapper.find(TAB_PANEL_SELECTOR), NUM_TABS);
- assert.lengthOf(wrapper.find(`.${panelClassName}`).hostNodes(), 1);
+ assert.lengthOf(container.querySelectorAll(`.${panelClassName}`), 1);
});
it("passes correct tabTitleId and tabPanelId to panel renderer", () => {
const expectedIds = generateTabIds(ID, "first");
- mount(
+ render(
", () => {
});
it("renderActiveTabPanelOnly only renders active tab panel", () => {
- const wrapper = mount(
+ const { container, rerender } = renderTabs(
{getTabsContents()}
,
);
for (const selectedTabId of TAB_IDS) {
- act(() => {
- wrapper.setState({ selectedTabId });
- });
- assert.lengthOf(wrapper.find("strong"), 1);
+ rerender(
+
+ {getTabsContents()}
+ ,
+ );
+ assert.lengthOf(container.querySelectorAll("strong"), 1);
}
});
it("sets aria-* attributes with matching IDs", () => {
- const wrapper = mount({getTabsContents()} );
- wrapper.find(TAB_SELECTOR).forEach(title => {
- // title "controls" tab element
- const titleControls = title.prop("aria-controls");
- const tab = wrapper.find(`#${titleControls}`);
- // tab element "labelled by" title element
- assert.isTrue(tab.is(TAB_PANEL_SELECTOR), "aria-controls isn't TAB_PANEL");
- assert.deepEqual(tab.prop("aria-labelledby"), title.prop("id"), "mismatched IDs");
+ const { container } = renderTabs({getTabsContents()} );
+ findTabs(container).forEach(title => {
+ const titleControls = title.getAttribute("aria-controls");
+ const tab = container.querySelector(`#${titleControls}`)!;
+ assert.isTrue(tab.matches(TAB_PANEL_SELECTOR), "aria-controls isn't TAB_PANEL");
+ assert.deepEqual(tab.getAttribute("aria-labelledby"), title.getAttribute("id"), "mismatched IDs");
});
});
@@ -187,81 +210,76 @@ describe("", () => {
const tabs = TAB_IDS.map(id => (
} title={id} data-arbitrary-attr="foo" />
));
- const wrapper = mount({tabs} );
- wrapper.find(TAB_SELECTOR).forEach(title => {
- assert.strictEqual(title.getDOMNode().getAttribute("data-arbitrary-attr"), "foo");
+ const { container } = renderTabs({tabs} );
+ findTabs(container).forEach(title => {
+ assert.strictEqual(title.getAttribute("data-arbitrary-attr"), "foo");
});
});
it("clicking selected tab still fires onChange", () => {
const tabId = TAB_IDS[0];
const changeSpy = vi.fn();
- const wrapper = mount(
+ const { container } = renderTabs(
{getTabsContents()}
,
- { attachTo: containerElement },
);
- findTabById(wrapper, tabId).simulate("click");
+ fireEvent.click(findTabById(container, tabId)!);
expect(changeSpy).toHaveBeenCalledWith(tabId, tabId, expect.anything());
});
it("clicking nested tab should not affect parent", () => {
const changeSpy = vi.fn();
- const wrapper = mount(
+ const { container, instance } = renderTabs(
{getTabsContents()}