/* * Copyright 2015 Palantir Technologies, Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @fileoverview This component is DEPRECATED, and the code is frozen. * All changes & bugfixes should be made to Overlay2 instead. */ /* eslint-disable @typescript-eslint/no-deprecated */ import { waitFor } from "@testing-library/dom"; import { mount, ReactWrapper, shallow } from "enzyme"; import { 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); } 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"); 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; } afterEach(() => { if (isMounted) { // clean up wrapper after each test, if it was used wrapper?.unmount(); wrapper?.detach(); isMounted = false; } }); afterAll(() => { document.documentElement.removeChild(containerElement); }); it("renders its content correctly", () => { const overlay = shallow( {createOverlayContents()} , ); assert.lengthOf(overlay.find("strong"), 1); assert.lengthOf(overlay.find(BACKDROP_SELECTOR), 1); overlay.unmount(); }); it("renders contents to specified container correctly", () => { const CLASS_TO_TEST = "bp-test-content"; const container = document.createElement("div"); document.body.appendChild(container); mountWrapper(

test

, ); assert.lengthOf(container.getElementsByClassName(CLASS_TO_TEST), 1); document.body.removeChild(container); }); it("sets aria-live", () => { // Using an open Overlay because an initially closed Overlay will not render anything to the // DOM mountWrapper(); 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(

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"); }); it("supports non-element children", () => { assert.doesNotThrow(() => shallow( {null} {undefined} , ).unmount(), ); }); it("hasBackdrop=false does not render backdrop", () => { const overlay = shallow( {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"); }); describe("onClose", () => { it("invoked on backdrop mousedown when canOutsideClickClose=true", () => { const onClose = vi.fn(); const overlay = shallow( {createOverlayContents()} , ); overlay.find(BACKDROP_SELECTOR).simulate("mousedown"); expect(onClose).toHaveBeenCalledOnce(); overlay.unmount(); }); it("not invoked on backdrop mousedown when canOutsideClickClose=false", () => { const onClose = vi.fn(); const overlay = shallow( {createOverlayContents()} , ); overlay.find(BACKDROP_SELECTOR).simulate("mousedown"); 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( {createOverlayContents()} , ); dispatchMouseEvent(document.documentElement, "mousedown"); expect(onClose).toHaveBeenCalledOnce(); }); it("not invoked on document mousedown when hasBackdrop=false and canOutsideClickClose=false", () => { const onClose = vi.fn(); mountWrapper( {createOverlayContents()} , ); dispatchMouseEvent(document.documentElement, "mousedown"); expect(onClose).not.toHaveBeenCalled(); }); it("not invoked on click of a nested overlay", () => { const onClose = vi.fn(); mountWrapper(
{createOverlayContents()}
{createOverlayContents()}
, ); // this hackery is necessary for React 15 support, where Portals break trees. findInPortal(findInPortal(wrapper, "#outer-element"), "#inner-element").simulate("mousedown"); expect(onClose).not.toHaveBeenCalled(); }); it("invoked on escape key", () => { const onClose = vi.fn(); mountWrapper( {createOverlayContents()} , ); wrapper.simulate("keydown", { key: "Escape" }); expect(onClose).toHaveBeenCalledOnce(); }); it("not invoked on escape key when canEscapeKeyClose=false", () => { const onClose = vi.fn(); const overlay = shallow( {createOverlayContents()} , ); overlay.simulate("keydown", { key: "Escape" }); expect(onClose).not.toHaveBeenCalled(); overlay.unmount(); }); it("renders portal attached to body when not inline", () => { const overlay = shallow( {createOverlayContents()} , ); const portal = overlay.find(Portal); assert.isTrue(portal.exists(), "missing Portal"); assert.lengthOf(portal.find("strong"), 1, "missing h1"); overlay.unmount(); }); }); describe("Focus management", () => { const overlayClassName = "test-overlay"; it("brings focus to overlay if autoFocus=true", async () => { mountWrapper( , ); await assertFocusIsInOverlay(); }); it("does not bring focus to overlay if autoFocus=false and enforceFocus=false", async () => { mountWrapper(
, ); 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( , ); await assertFocus("input"); }); it("returns focus to overlay if enforceFocus=true", async () => { const buttonRef = createRef(); const inputRef = createRef(); mountWrapper(
, ); assert.strictEqual(document.activeElement, inputRef.current); buttonRef.current?.focus(); await assertFocusIsInOverlay(); }); it("returns focus to overlay after clicking the backdrop if enforceFocus=true", async () => { mountWrapper( {createOverlayContents()} , ); wrapper.find(BACKDROP_SELECTOR).simulate("mousedown"); await assertFocusIsInOverlay(); }); it("returns focus to overlay after clicking an outside element if enforceFocus=true", async () => { mountWrapper(
{createOverlayContents()}
, ); wrapper.find("#buttonId").simulate("click"); 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( , { attachTo: anotherContainer }, ); mountWrapper( , ); // 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"); expect(bringFocusSpy).toHaveBeenCalledOnce(); // don't need spy.restore() since the wrapper will be destroyed after test anyways temporaryWrapper.unmount(); document.documentElement.removeChild(anotherContainer); }); it("does not return focus to overlay if enforceFocus=false", () => { let buttonRef: HTMLElement | null; const focusBtnAndAssert = async () => { buttonRef?.focus(); await waitFor(() => assert.strictEqual(buttonRef, document.activeElement)); }; mountWrapper(
, ); }); it("doesn't focus overlay if focus is already inside overlay", async () => { let textarea: HTMLTextAreaElement | null; mountWrapper(