fix: correct popup fallback positioning offsets (#981)

This commit is contained in:
Sam Potts
2026-03-17 13:14:35 +11:00
committed by GitHub
parent 561d03eb5a
commit 82ede77322
12 changed files with 498 additions and 20 deletions
@@ -1,4 +1,4 @@
import { supportsAnchorPositioning } from '@videojs/utils/dom';
import { resolveCSSLength, supportsAnchorPositioning } from '@videojs/utils/dom';
import type { PopoverAlign, PopoverSide } from '../../../core/ui/popover/popover-core';
import { type PopoverCSSVarKey, PopoverCSSVars } from '../../../core/ui/popover/popover-css-vars';
@@ -258,7 +258,32 @@ export function getManualPositionStyle(
export function resolveOffsets(el: Element, cssVars: PositioningCSSVars = PopoverCSSVars): ManualOffsets {
const computed = getComputedStyle(el);
return {
sideOffset: Number.parseFloat(computed.getPropertyValue(cssVars.sideOffset)) || 0,
alignOffset: Number.parseFloat(computed.getPropertyValue(cssVars.alignOffset)) || 0,
sideOffset: resolveCSSLength(el, computed.getPropertyValue(cssVars.sideOffset)),
alignOffset: resolveCSSLength(el, computed.getPropertyValue(cssVars.alignOffset)),
};
}
/**
* Measure the popup's layout box for positioning.
*
* `getBoundingClientRect()` includes active transforms, which causes the
* fallback position to drift while opening/closing animations scale the popup.
* Using `offsetWidth`/`offsetHeight` preserves the untransformed size.
*/
export function getPopupPositionRect(el: HTMLElement): DOMRect {
const rect = el.getBoundingClientRect();
const width = el.offsetWidth || rect.width;
const height = el.offsetHeight || rect.height;
const adjustedRect = {
...rect,
width,
height,
right: rect.left + width,
bottom: rect.top + height,
};
return {
...adjustedRect,
toJSON: () => adjustedRect,
};
}
@@ -87,6 +87,16 @@ export function createPopover(options: PopoverOptions): PopoverApi {
return globalThis.matchMedia?.('(hover: hover)')?.matches ?? false;
}
function canOpenOnFocus(): boolean {
if (!canHover()) return false;
return globalThis.matchMedia?.('(pointer: fine)')?.matches ?? false;
}
function canToggleOnClick(): boolean {
if (!options.openOnHover?.()) return true;
return canHover();
}
// --- Open/close ---
/**
@@ -168,6 +178,8 @@ export function createPopover(options: PopoverOptions): PopoverApi {
const triggerProps: PopoverTriggerProps = {
onClick(event) {
if (!canToggleOnClick()) return;
// During a close animation (open=true, status=ending), treat
// the click as a re-open rather than a second close attempt.
if (state.current.active && state.current.status !== 'ending') {
@@ -203,6 +215,7 @@ export function createPopover(options: PopoverOptions): PopoverApi {
onFocusIn(_event) {
if (options.openOnHover?.()) {
if (!canOpenOnFocus()) return;
applyOpen('focus');
}
},
@@ -5,7 +5,9 @@ import {
getAnchorPositionStyle,
getManualPositionStyle,
getPopoverCSSVars,
getPopupPositionRect,
type ManualOffsets,
resolveOffsets,
} from '../popover-positioning';
// Mock supportsAnchorPositioning for deterministic tests.
@@ -182,6 +184,67 @@ describe('getAnchorPositionStyle', () => {
});
});
describe('resolveOffsets', () => {
it('resolves non-pixel CSS lengths to pixels', () => {
const el = document.createElement('div');
const getComputedStyleSpy = vi.spyOn(globalThis, 'getComputedStyle').mockImplementation(
(target: Element) =>
({
fontSize: target === document.documentElement ? '16px' : '14px',
getPropertyValue(name: string) {
if (name === PopoverCSSVars.sideOffset) return '0.5rem';
if (name === PopoverCSSVars.alignOffset) return '1em';
return '';
},
}) as CSSStyleDeclaration
);
expect(resolveOffsets(el)).toEqual({ sideOffset: 8, alignOffset: 14 });
getComputedStyleSpy.mockRestore();
});
});
describe('getPopupPositionRect', () => {
it('uses untransformed layout size when transforms change the client rect', () => {
const el = document.createElement('div');
Object.defineProperty(el, 'offsetWidth', { configurable: true, value: 200 });
Object.defineProperty(el, 'offsetHeight', { configurable: true, value: 80 });
vi.spyOn(el, 'getBoundingClientRect').mockImplementation(() => makeDOMRect(20, 40, 100, 40));
const rect = getPopupPositionRect(el);
expect(rect.left).toBe(20);
expect(rect.top).toBe(40);
expect(rect.width).toBe(200);
expect(rect.height).toBe(80);
expect(rect.right).toBe(220);
expect(rect.bottom).toBe(120);
});
it('serializes adjusted rect values from toJSON', () => {
const el = document.createElement('div');
Object.defineProperty(el, 'offsetWidth', { configurable: true, value: 200 });
Object.defineProperty(el, 'offsetHeight', { configurable: true, value: 80 });
vi.spyOn(el, 'getBoundingClientRect').mockImplementation(() => makeDOMRect(20, 40, 100, 40));
const rect = getPopupPositionRect(el);
expect(rect.toJSON()).toEqual(
expect.objectContaining({
left: 20,
top: 40,
width: 200,
height: 80,
right: 220,
bottom: 120,
})
);
});
});
// Tests the CSS anchor positioning path via getAnchorPositionStyle with
// a fresh module import where supportsAnchorPositioning returns true.
describe('getAnchorPositionStyle (CSS Anchor Positioning)', () => {
@@ -126,6 +126,75 @@ describe('createPopover', () => {
expect(popover.input.current.status).not.toBe('ending');
expect(onOpenChange).toHaveBeenCalledWith(true, expect.objectContaining({ reason: 'click' }));
});
it('does not open on click on touch devices when openOnHover is enabled', () => {
const matchMedia = vi.fn((query: string) => ({
matches: query === '(hover: hover)' ? false : false,
}));
vi.stubGlobal('matchMedia', matchMedia);
const { popover, onOpenChange } = createTestPopover({
openOnHover: () => true,
});
popover.triggerProps.onClick({ preventDefault: vi.fn() } as unknown as UIEvent);
expect(onOpenChange).not.toHaveBeenCalled();
expect(popover.input.current.active).toBe(false);
vi.unstubAllGlobals();
});
it('does not open via focus on touch devices when openOnHover is enabled', () => {
const matchMedia = vi.fn((query: string) => ({
matches: query === '(hover: hover)' ? false : false,
}));
vi.stubGlobal('matchMedia', matchMedia);
const { popover, onOpenChange } = createTestPopover({
openOnHover: () => true,
});
popover.triggerProps.onFocusIn({ relatedTarget: null, preventDefault: vi.fn() });
expect(onOpenChange).not.toHaveBeenCalled();
vi.unstubAllGlobals();
});
it('does not open via focus when pointer is not fine', () => {
const matchMedia = vi.fn((query: string) => ({
matches: query === '(hover: hover)',
}));
vi.stubGlobal('matchMedia', matchMedia);
const { popover, onOpenChange } = createTestPopover({
openOnHover: () => true,
});
popover.triggerProps.onFocusIn({ relatedTarget: null, preventDefault: vi.fn() });
expect(onOpenChange).not.toHaveBeenCalled();
vi.unstubAllGlobals();
});
it('opens via focus when hover and fine pointer are supported', () => {
const matchMedia = vi.fn((query: string) => ({
matches: query === '(hover: hover)' || query === '(pointer: fine)',
}));
vi.stubGlobal('matchMedia', matchMedia);
const { popover, onOpenChange } = createTestPopover({
openOnHover: () => true,
});
popover.triggerProps.onFocusIn({ relatedTarget: null, preventDefault: vi.fn() });
expect(onOpenChange).toHaveBeenCalledWith(true, { reason: 'focus' });
vi.unstubAllGlobals();
});
});
describe('element setters', () => {
+9 -6
View File
@@ -27,12 +27,15 @@ const createConfig = (mode: BuildMode): UserConfig => ({
define: {
__DEV__: mode === 'dev' ? 'true' : 'false',
},
dts: {
build: true,
// Unified tsconfig covering both core and dom sources.
// Needs DOM libs to preserve MediaApiMixin return types.
tsconfig: 'tsconfig.dts.json',
},
dts:
mode === 'dev'
? {
build: true,
// Unified tsconfig covering both core and dom sources.
// Needs DOM libs to preserve MediaApiMixin return types.
tsconfig: 'tsconfig.dts.json',
}
: false,
});
export default defineConfig(buildModes.map((mode) => createConfig(mode)));
@@ -6,6 +6,7 @@ import {
createTransition,
getAnchorNameStyle,
getAnchorPositionStyle,
getPopupPositionRect,
type PopoverApi,
type PopoverChangeDetails,
resolveOffsets,
@@ -51,6 +52,10 @@ export class PopoverElement extends MediaElement {
#disconnect: AbortController | null = null;
#triggerAbort: AbortController | null = null;
#currentTrigger: HTMLElement | null = null;
#positionAbort: AbortController | null = null;
#positionFrame = 0;
#resizeObserver: ResizeObserver | null = null;
#positionTrigger: HTMLElement | null = null;
override connectedCallback(): void {
super.connectedCallback();
@@ -99,11 +104,13 @@ export class PopoverElement extends MediaElement {
override disconnectedCallback(): void {
super.disconnectedCallback();
this.#cleanupPositioning();
this.#disconnect?.abort();
this.#disconnect = null;
}
override destroyCallback(): void {
this.#cleanupPositioning();
this.#cleanupTrigger();
this.#popover?.destroy();
super.destroyCallback();
@@ -158,7 +165,10 @@ export class PopoverElement extends MediaElement {
}
// Skip positioning when closed — no rects to measure.
if (!state.open) return;
if (!state.open) {
this.#cleanupPositioning();
return;
}
// Apply positioning styles to self.
const posOpts = { side: state.side, align: state.align };
@@ -169,11 +179,13 @@ export class PopoverElement extends MediaElement {
} else {
// JS fallback: measure rects and resolve CSS var offsets.
const triggerRect = this.#currentTrigger?.getBoundingClientRect();
const selfRect = this.getBoundingClientRect();
const selfRect = getPopupPositionRect(this);
const boundaryRect = document.documentElement.getBoundingClientRect();
const offsets = resolveOffsets(this);
applyStyles(this, getAnchorPositionStyle(this.id, posOpts, triggerRect, selfRect, boundaryRect, offsets));
}
this.#syncPositioning();
}
// --- Trigger discovery ---
@@ -187,6 +199,7 @@ export class PopoverElement extends MediaElement {
#syncTrigger(triggerEl: HTMLElement | null): void {
if (triggerEl === this.#currentTrigger) return;
this.#cleanupPositioning();
this.#cleanupTrigger();
this.#currentTrigger = triggerEl;
this.#popover?.setTriggerElement(triggerEl);
@@ -212,4 +225,49 @@ export class PopoverElement extends MediaElement {
this.#triggerAbort = null;
this.#currentTrigger = null;
}
#syncPositioning(): void {
if (supportsAnchorPositioning()) return;
const triggerEl = this.#currentTrigger;
if (!triggerEl) return;
if (this.#positionAbort && this.#positionTrigger === triggerEl) return;
this.#cleanupPositioning();
this.#positionAbort = new AbortController();
this.#positionTrigger = triggerEl;
const { signal } = this.#positionAbort;
const reposition = () => {
cancelAnimationFrame(this.#positionFrame);
this.#positionFrame = requestAnimationFrame(() => {
if (signal.aborted) return;
this.requestUpdate();
});
};
window.addEventListener('scroll', reposition, { capture: true, passive: true, signal });
window.addEventListener('resize', reposition, { signal });
if (typeof ResizeObserver === 'function') {
this.#resizeObserver = new ResizeObserver(() => {
reposition();
});
this.#resizeObserver.observe(triggerEl);
this.#resizeObserver.observe(this);
}
reposition();
}
#cleanupPositioning(): void {
this.#positionAbort?.abort();
this.#positionAbort = null;
this.#positionTrigger = null;
cancelAnimationFrame(this.#positionFrame);
this.#positionFrame = 0;
this.#resizeObserver?.disconnect();
this.#resizeObserver = null;
}
}
@@ -6,6 +6,7 @@ import {
createTransition,
getAnchorNameStyle,
getAnchorPositionStyle,
getPopupPositionRect,
resolveOffsets,
type TooltipApi,
type TooltipChangeDetails,
@@ -50,6 +51,10 @@ export class TooltipElement extends MediaElement {
#disconnect: AbortController | null = null;
#triggerAbort: AbortController | null = null;
#currentTrigger: HTMLElement | null = null;
#positionAbort: AbortController | null = null;
#positionFrame = 0;
#resizeObserver: ResizeObserver | null = null;
#positionTrigger: HTMLElement | null = null;
override connectedCallback(): void {
super.connectedCallback();
@@ -95,6 +100,7 @@ export class TooltipElement extends MediaElement {
override disconnectedCallback(): void {
super.disconnectedCallback();
this.#cleanupPositioning();
this.#cleanupTrigger();
this.#tooltip?.destroy();
this.#tooltip = null;
@@ -151,7 +157,10 @@ export class TooltipElement extends MediaElement {
}
// Skip positioning when closed — no rects to measure.
if (!state.open) return;
if (!state.open) {
this.#cleanupPositioning();
return;
}
// Apply positioning styles to self.
const posOpts = { side: state.side, align: state.align };
@@ -165,7 +174,7 @@ export class TooltipElement extends MediaElement {
} else {
// JS fallback: measure rects and resolve CSS var offsets.
const triggerRect = this.#currentTrigger?.getBoundingClientRect();
const selfRect = this.getBoundingClientRect();
const selfRect = getPopupPositionRect(this);
const boundaryRect = document.documentElement.getBoundingClientRect();
const offsets = resolveOffsets(this, TooltipCSSVars);
applyStyles(
@@ -173,6 +182,8 @@ export class TooltipElement extends MediaElement {
getAnchorPositionStyle(this.id, posOpts, triggerRect, selfRect, boundaryRect, offsets, TooltipCSSVars)
);
}
this.#syncPositioning();
}
// --- Trigger discovery ---
@@ -186,6 +197,7 @@ export class TooltipElement extends MediaElement {
#syncTrigger(triggerEl: HTMLElement | null): void {
if (triggerEl === this.#currentTrigger) return;
this.#cleanupPositioning();
this.#cleanupTrigger();
this.#currentTrigger = triggerEl;
this.#tooltip?.setTriggerElement(triggerEl);
@@ -209,4 +221,49 @@ export class TooltipElement extends MediaElement {
this.#triggerAbort = null;
this.#currentTrigger = null;
}
#syncPositioning(): void {
if (supportsAnchorPositioning()) return;
const triggerEl = this.#currentTrigger;
if (!triggerEl) return;
if (this.#positionAbort && this.#positionTrigger === triggerEl) return;
this.#cleanupPositioning();
this.#positionAbort = new AbortController();
this.#positionTrigger = triggerEl;
const { signal } = this.#positionAbort;
const reposition = () => {
cancelAnimationFrame(this.#positionFrame);
this.#positionFrame = requestAnimationFrame(() => {
if (signal.aborted) return;
this.requestUpdate();
});
};
window.addEventListener('scroll', reposition, { capture: true, passive: true, signal });
window.addEventListener('resize', reposition, { signal });
if (typeof ResizeObserver === 'function') {
this.#resizeObserver = new ResizeObserver(() => {
reposition();
});
this.#resizeObserver.observe(triggerEl);
this.#resizeObserver.observe(this);
}
reposition();
}
#cleanupPositioning(): void {
this.#positionAbort?.abort();
this.#positionAbort = null;
this.#positionTrigger = null;
cancelAnimationFrame(this.#positionFrame);
this.#positionFrame = 0;
this.#resizeObserver?.disconnect();
this.#resizeObserver = null;
}
}
@@ -1,7 +1,7 @@
'use client';
import type { PopoverState } from '@videojs/core';
import { getAnchorPositionStyle, resolveOffsets } from '@videojs/core/dom';
import { getAnchorPositionStyle, getPopupPositionRect, resolveOffsets } from '@videojs/core/dom';
import { supportsAnchorPositioning } from '@videojs/utils/dom';
import type { CSSProperties } from 'react';
import { forwardRef, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
@@ -64,7 +64,7 @@ export const PopoverPopup = forwardRef<HTMLDivElement, PopoverPopupProps>(functi
if (!triggerEl || !popupEl) return;
const triggerRect = triggerEl.getBoundingClientRect();
const popupRect = popupEl.getBoundingClientRect();
const popupRect = getPopupPositionRect(popupEl);
const boundaryRect = document.documentElement.getBoundingClientRect();
const offsets = resolveOffsets(popupEl);
@@ -74,19 +74,40 @@ export const PopoverPopup = forwardRef<HTMLDivElement, PopoverPopupProps>(functi
}
measure();
const triggerEl = popover.triggerElement;
const popupEl = internalRef.current;
// Recompute on scroll/resize so the popover tracks its trigger.
let rafId = 0;
function reposition(): void {
cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(measure);
}
// Re-measure after the popover has entered the top layer and whenever
// its own size or the trigger size changes.
reposition();
const resizeObserver =
typeof ResizeObserver === 'function'
? new ResizeObserver(() => {
reposition();
})
: null;
if (triggerEl && resizeObserver) {
resizeObserver.observe(triggerEl);
}
if (popupEl && resizeObserver) {
resizeObserver.observe(popupEl);
}
window.addEventListener('scroll', reposition, { capture: true, passive: true });
window.addEventListener('resize', reposition);
return () => {
cancelAnimationFrame(rafId);
resizeObserver?.disconnect();
window.removeEventListener('scroll', reposition, true);
window.removeEventListener('resize', reposition);
};
@@ -2,7 +2,7 @@
import type { TooltipState } from '@videojs/core';
import { TooltipCSSVars } from '@videojs/core';
import { getAnchorPositionStyle, resolveOffsets } from '@videojs/core/dom';
import { getAnchorPositionStyle, getPopupPositionRect, resolveOffsets } from '@videojs/core/dom';
import { supportsAnchorPositioning } from '@videojs/utils/dom';
import type { CSSProperties } from 'react';
import { forwardRef, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
@@ -73,7 +73,7 @@ export const TooltipPopup = forwardRef<HTMLDivElement, TooltipPopupProps>(functi
if (!triggerEl || !popupEl) return;
const triggerRect = triggerEl.getBoundingClientRect();
const popupRect = popupEl.getBoundingClientRect();
const popupRect = getPopupPositionRect(popupEl);
const boundaryRect = document.documentElement.getBoundingClientRect();
const offsets = resolveOffsets(popupEl, TooltipCSSVars);
@@ -91,19 +91,40 @@ export const TooltipPopup = forwardRef<HTMLDivElement, TooltipPopupProps>(functi
}
measure();
const triggerEl = tooltip.triggerElement;
const popupEl = internalRef.current;
// Recompute on scroll/resize so the tooltip tracks its trigger.
let rafId = 0;
function reposition(): void {
cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(measure);
}
// Re-measure after the tooltip has entered the top layer and whenever
// its own size or the trigger size changes.
reposition();
const resizeObserver =
typeof ResizeObserver === 'function'
? new ResizeObserver(() => {
reposition();
})
: null;
if (triggerEl && resizeObserver) {
resizeObserver.observe(triggerEl);
}
if (popupEl && resizeObserver) {
resizeObserver.observe(popupEl);
}
window.addEventListener('scroll', reposition, { capture: true, passive: true });
window.addEventListener('resize', reposition);
return () => {
cancelAnimationFrame(rafId);
resizeObserver?.disconnect();
window.removeEventListener('scroll', reposition, true);
window.removeEventListener('resize', reposition);
};
+1 -1
View File
@@ -8,7 +8,7 @@ export { tryHidePopover, tryShowPopover } from './popover';
export { isHTMLAudioElement, isHTMLMediaElement, isHTMLVideoElement } from './predicates';
export { type RafThrottled, rafThrottle } from './raf-throttle';
export { getSlottedElement, querySlot } from './slotted';
export { applyStyles } from './style';
export { applyStyles, resolveCSSLength } from './style';
export { supportsAnchorPositioning, supportsAnimationFrame, supportsIdleCallback } from './supports';
export { findTrackElement, getTextTrackList } from './text-track';
export { serializeTimeRanges } from './time-ranges';
+47
View File
@@ -9,3 +9,50 @@ export function applyStyles(element: HTMLElement, styles: Record<string, string
}
}
}
export function resolveCSSLength(el: Element, value: string): number {
const trimmed = value.trim();
if (!trimmed) return 0;
const parsed = Number.parseFloat(trimmed);
if (Number.isNaN(parsed)) return 0;
if (/^-?\d*\.?\d+$/.test(trimmed) || trimmed.endsWith('px')) return parsed;
const doc = el.ownerDocument;
const root = doc?.documentElement;
if (trimmed.endsWith('rem')) {
const rootFontSize = root ? Number.parseFloat(getComputedStyle(root).fontSize) || 16 : 16;
return parsed * rootFontSize;
}
if (trimmed.endsWith('em')) {
const fontSize = el instanceof HTMLElement ? Number.parseFloat(getComputedStyle(el).fontSize) || 16 : 16;
return parsed * fontSize;
}
if (!doc) return parsed;
const measurementEl = doc.createElement('div');
measurementEl.style.position = 'absolute';
measurementEl.style.visibility = 'hidden';
measurementEl.style.pointerEvents = 'none';
measurementEl.style.inlineSize = trimmed;
measurementEl.style.blockSize = '0';
measurementEl.style.padding = '0';
measurementEl.style.border = '0';
measurementEl.style.inset = '0';
const parent = doc.body ?? doc.documentElement;
if (!parent) return parsed;
parent.appendChild(measurementEl);
const pixels = measurementEl.getBoundingClientRect().width;
measurementEl.remove();
return Number.isFinite(pixels) ? pixels : parsed;
}
+101
View File
@@ -0,0 +1,101 @@
import { describe, expect, it, vi } from 'vitest';
import { resolveCSSLength } from '../style';
describe('resolveCSSLength', () => {
it('Returns px values directly', () => {
const el = document.createElement('div');
expect(resolveCSSLength(el, '8px')).toBe(8);
});
it('Resolves rem values using the root font size', () => {
const el = document.createElement('div');
const getComputedStyleSpy = vi.spyOn(globalThis, 'getComputedStyle').mockImplementation(
(target: Element) =>
({
fontSize: target === document.documentElement ? '16px' : '14px',
}) as CSSStyleDeclaration
);
expect(resolveCSSLength(el, '0.5rem')).toBe(8);
getComputedStyleSpy.mockRestore();
});
it('Resolves em values using the element font size', () => {
const el = document.createElement('div');
const getComputedStyleSpy = vi.spyOn(globalThis, 'getComputedStyle').mockImplementation(
() =>
({
fontSize: '14px',
}) as CSSStyleDeclaration
);
expect(resolveCSSLength(el, '1em')).toBe(14);
getComputedStyleSpy.mockRestore();
});
it('Falls back to measurement for other CSS lengths', () => {
const el = document.createElement('div');
const createElement = document.createElement.bind(document);
const appendChildSpy = vi.spyOn(document.body, 'appendChild');
const createElementSpy = vi.spyOn(document, 'createElement').mockImplementation((tagName: string) => {
const node = createElement(tagName);
if (tagName === 'div') {
vi.spyOn(node, 'getBoundingClientRect').mockImplementation(() => {
const width = node.style.inlineSize === '10vw' ? 24 : 0;
return {
x: 0,
y: 0,
width,
height: 0,
top: 0,
left: 0,
right: width,
bottom: 0,
toJSON() {},
};
});
}
return node;
});
expect(resolveCSSLength(el, '10vw')).toBe(24);
expect(appendChildSpy).toHaveBeenCalled();
createElementSpy.mockRestore();
appendChildSpy.mockRestore();
});
it('Preserves measured zero pixel values', () => {
const el = document.createElement('div');
const createElement = document.createElement.bind(document);
const createElementSpy = vi.spyOn(document, 'createElement').mockImplementation((tagName: string) => {
const node = createElement(tagName);
if (tagName === 'div') {
vi.spyOn(node, 'getBoundingClientRect').mockImplementation(() => ({
x: 0,
y: 0,
width: 0,
height: 0,
top: 0,
left: 0,
right: 0,
bottom: 0,
toJSON() {},
}));
}
return node;
});
expect(resolveCSSLength(el, 'calc(1px - 1px)')).toBe(0);
createElementSpy.mockRestore();
});
});