From e5c93fcae2ce1fc128b11c64c78965748cbe4b81 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Fri, 24 Jan 2020 07:36:43 +0100 Subject: [PATCH] fix(overlay): only clear duplicate containers from different platform Currently we clear all overlay containers when we create a new one as a way to avoid duplicate content coming in from the server. Our current approach is a little too aggressive, because it can pick up containers from different apps. These changes add an extra attribute to the container so that we can determine which platform it's coming from. Fixes #16851. --- .../overlay/fullscreen-overlay-container.ts | 11 +++- src/cdk/overlay/overlay-container.spec.ts | 30 ++++++++++- src/cdk/overlay/overlay-container.ts | 53 ++++++++++++++++--- src/material/select/testing/shared.spec.ts | 4 -- tools/public_api_guard/cdk/overlay.d.ts | 7 ++- 5 files changed, 89 insertions(+), 16 deletions(-) diff --git a/src/cdk/overlay/fullscreen-overlay-container.ts b/src/cdk/overlay/fullscreen-overlay-container.ts index 0f0d8a541f22..55b33c8fd723 100644 --- a/src/cdk/overlay/fullscreen-overlay-container.ts +++ b/src/cdk/overlay/fullscreen-overlay-container.ts @@ -9,6 +9,7 @@ import {Injectable, Inject, OnDestroy} from '@angular/core'; import {OverlayContainer} from './overlay-container'; import {DOCUMENT} from '@angular/common'; +import {Platform} from '@angular/cdk/platform'; /** @@ -23,8 +24,14 @@ export class FullscreenOverlayContainer extends OverlayContainer implements OnDe private _fullScreenEventName: string | undefined; private _fullScreenListener: () => void; - constructor(@Inject(DOCUMENT) _document: any) { - super(_document); + constructor( + @Inject(DOCUMENT) _document: any, + /** + * @deprecated `platform` parameter to become required. + * @breaking-change 10.0.0 + */ + platform?: Platform) { + super(_document, platform); } ngOnDestroy() { diff --git a/src/cdk/overlay/overlay-container.spec.ts b/src/cdk/overlay/overlay-container.spec.ts index 226f71bebdac..eab81cbd01ca 100644 --- a/src/cdk/overlay/overlay-container.spec.ts +++ b/src/cdk/overlay/overlay-container.spec.ts @@ -53,9 +53,10 @@ describe('OverlayContainer', () => { .toBe(false, 'Expected the overlay container not to have class "commander-shepard"'); }); - it('should ensure that there is only one overlay container on the page', () => { + it('should remove overlay containers from the server when on the browser', () => { const extraContainer = document.createElement('div'); extraContainer.classList.add('cdk-overlay-container'); + extraContainer.setAttribute('platform', 'server'); document.body.appendChild(extraContainer); overlayContainer.getContainerElement(); @@ -65,6 +66,33 @@ describe('OverlayContainer', () => { extraContainer.parentNode.removeChild(extraContainer); } }); + + it('should remove overlay containers from other unit tests', () => { + const extraContainer = document.createElement('div'); + extraContainer.classList.add('cdk-overlay-container'); + extraContainer.setAttribute('platform', 'test'); + document.body.appendChild(extraContainer); + + overlayContainer.getContainerElement(); + expect(document.querySelectorAll('.cdk-overlay-container').length).toBe(1); + + if (extraContainer.parentNode) { + extraContainer.parentNode.removeChild(extraContainer); + } + }); + + it('should not remove extra containers that were created on the browser', () => { + const extraContainer = document.createElement('div'); + extraContainer.classList.add('cdk-overlay-container'); + document.body.appendChild(extraContainer); + + overlayContainer.getContainerElement(); + + expect(document.querySelectorAll('.cdk-overlay-container').length).toBe(2); + + extraContainer.parentNode!.removeChild(extraContainer); + }); + }); /** Test-bed component that contains a TempatePortal and an ElementRef. */ diff --git a/src/cdk/overlay/overlay-container.ts b/src/cdk/overlay/overlay-container.ts index 45afbc69fd74..2e078788f146 100644 --- a/src/cdk/overlay/overlay-container.ts +++ b/src/cdk/overlay/overlay-container.ts @@ -15,7 +15,14 @@ import { Optional, SkipSelf, } from '@angular/core'; +import {Platform} from '@angular/cdk/platform'; +/** + * Whether we're in a testing environment. + * TODO(crisbeto): remove this once we have an overlay testing module. + */ +const isTestEnvironment: boolean = typeof window !== 'undefined' && !!window && + !!((window as any).__karma__ || (window as any).jasmine); /** Container inside which all overlays will render. */ @Injectable({providedIn: 'root'}) @@ -23,13 +30,21 @@ export class OverlayContainer implements OnDestroy { protected _containerElement: HTMLElement; protected _document: Document; - constructor(@Inject(DOCUMENT) document: any) { + constructor( + @Inject(DOCUMENT) document: any, + /** + * @deprecated `platform` parameter to become required. + * @breaking-change 10.0.0 + */ + protected _platform?: Platform) { this._document = document; } ngOnDestroy() { - if (this._containerElement && this._containerElement.parentNode) { - this._containerElement.parentNode.removeChild(this._containerElement); + const container = this._containerElement; + + if (container && container.parentNode) { + container.parentNode.removeChild(container); } } @@ -52,16 +67,40 @@ export class OverlayContainer implements OnDestroy { * with the 'cdk-overlay-container' class on the document body. */ protected _createContainer(): void { + // @breaking-change 10.0.0 Remove null check for `_platform`. + const isBrowser = this._platform ? this._platform.isBrowser : typeof window !== 'undefined'; const containerClass = 'cdk-overlay-container'; - const previousContainers = this._document.getElementsByClassName(containerClass); - // Remove any old containers. This can happen when transitioning from the server to the client. - for (let i = 0; i < previousContainers.length; i++) { - previousContainers[i].parentNode!.removeChild(previousContainers[i]); + if (isBrowser || isTestEnvironment) { + const oppositePlatformContainers = + this._document.querySelectorAll(`.${containerClass}[platform="server"], ` + + `.${containerClass}[platform="test"]`); + + // Remove any old containers from the opposite platform. + // This can happen when transitioning from the server to the client. + for (let i = 0; i < oppositePlatformContainers.length; i++) { + oppositePlatformContainers[i].parentNode!.removeChild(oppositePlatformContainers[i]); + } } const container = this._document.createElement('div'); container.classList.add(containerClass); + + // A long time ago we kept adding new overlay containers whenever a new app was instantiated, + // but at some point we added logic which clears the duplicate ones in order to avoid leaks. + // The new logic was a little too aggressive since it was breaking some legitimate use cases. + // To mitigate the problem we made it so that only containers from a different platform are + // cleared, but the side-effect was that people started depending on the overly-aggressive + // logic to clean up their tests for them. Until we can introduce an overlay-specific testing + // module which does the cleanup, we try to detect that we're in a test environment and we + // always clear the container. See #17006. + // TODO(crisbeto): remove the test environment check once we have an overlay testing module. + if (isTestEnvironment) { + container.setAttribute('platform', 'test'); + } else if (!isBrowser) { + container.setAttribute('platform', 'server'); + } + this._document.body.appendChild(container); this._containerElement = container; } diff --git a/src/material/select/testing/shared.spec.ts b/src/material/select/testing/shared.spec.ts index f87405b2ea73..bf8e1573cddf 100644 --- a/src/material/select/testing/shared.spec.ts +++ b/src/material/select/testing/shared.spec.ts @@ -226,13 +226,11 @@ function getActiveElementId() { {{ state.name }} - {{ state.name }} - @@ -242,7 +240,6 @@ function getActiveElementId() { - {{ state.name }} @@ -283,4 +280,3 @@ class SelectHarnessTest { } ]; } - diff --git a/tools/public_api_guard/cdk/overlay.d.ts b/tools/public_api_guard/cdk/overlay.d.ts index 73ca21d4ba33..72e0b230e31c 100644 --- a/tools/public_api_guard/cdk/overlay.d.ts +++ b/tools/public_api_guard/cdk/overlay.d.ts @@ -152,7 +152,8 @@ export declare type FlexibleConnectedPositionStrategyOrigin = ElementRef | HTMLE }; export declare class FullscreenOverlayContainer extends OverlayContainer implements OnDestroy { - constructor(_document: any); + constructor(_document: any, + platform?: Platform); protected _createContainer(): void; getFullscreenElement(): Element; ngOnDestroy(): void; @@ -224,7 +225,9 @@ export interface OverlayConnectionPosition { export declare class OverlayContainer implements OnDestroy { protected _containerElement: HTMLElement; protected _document: Document; - constructor(document: any); + protected _platform?: Platform | undefined; + constructor(document: any, + _platform?: Platform | undefined); protected _createContainer(): void; getContainerElement(): HTMLElement; ngOnDestroy(): void;