import '@testing-library/jest-dom'; import { act, render } from '@testing-library/react'; import { screen } from 'shadow-dom-testing-library'; import { expectStylesInDom, MockComponent, registerComponent } from '../__mocks__'; import { IWebComponent } from '../common'; describe('[web-components] register', () => { const baseComponentName = 'mock-app'; const componentName = `${baseComponentName}-40b38a38`; let props: Record | undefined; let dataAttrs: Record | undefined; let onReady: (() => void) | undefined; let onDispose: (() => void) | undefined; let onloadRefs: Function[]; class Page { static get componentShadowRoot() { return document.querySelector(componentName)!.shadowRoot!; } static get portalElement() { return document.querySelector('body')!.lastElementChild!; } static get portalShadowRoot() { return Page.portalElement.shadowRoot!; } } beforeAll(() => { registerComponent(MockComponent, componentName, { sharedDependenciesNames: ['@servicetitan/design-system'], }); /* * mock 's onload so that we can call it directly from the test, * because it won't call itself in the test environment */ Object.defineProperty(HTMLLinkElement.prototype, 'onload', { get() { return this._onload; }, set(onload: Function) { onloadRefs.push(onload); this._onload = onload; }, }); }); beforeEach(() => { props = undefined; dataAttrs = undefined; onReady = undefined; onDispose = undefined; onloadRefs = []; }); const subject = () => { jest.spyOn(console, 'error').mockImplementation(jest.fn()); // suppress "attempt to unmount root while rendering" error const handleRef = (webComponent: IWebComponent | null) => { webComponent?.provide({ onReady, onDispose }); }; const data = props && { 'data-mfe-data': JSON.stringify(props) }; const Component: any = componentName; const renderResult = render(); act(() => { for (const onload of onloadRefs) { onload(); } }); return renderResult; }; test('exposes metadata', () => { const metadata = { shadowRoot: 'ShadowRoot', portalShadowRoot: 'ShadowRoot', }; subject(); Object.entries(metadata).forEach(([name, value]) => expect( screen.getByShadowText(`Metadata: ${name}, Value: ${JSON.stringify(value)}`) ).toBeInTheDocument() ); }); describe('when component has style urls', () => { const styleUrls = ['foo.css', 'bar.css']; beforeEach(() => { dataAttrs = { 'style-urls': JSON.stringify(styleUrls) }; }); test('loads the styles into the component shadow DOM', () => { subject(); expectStylesInDom(styleUrls, () => Page.componentShadowRoot); }); test('loads the styles into the portal shadow DOM', () => { subject(); expectStylesInDom(styleUrls, () => Page.portalShadowRoot); }); describe('when a design system link is present in the DOM', () => { let designSystemLink: HTMLLinkElement; beforeEach(() => { designSystemLink = Object.assign(document.createElement('link'), { href: 'http://localhost/design-system.foobar.bundle.css', rel: 'stylesheet', }); document.body.appendChild(designSystemLink); }); afterEach(() => { designSystemLink.remove(); }); test('loads the styles and design system styles into the component shadow DOM', () => { subject(); expectStylesInDom( [...styleUrls, designSystemLink.href], () => Page.componentShadowRoot ); }); test('loads the styles and design system styles into the portal shadow DOM', () => { subject(); expectStylesInDom( [...styleUrls, designSystemLink.href], () => Page.portalShadowRoot ); }); }); }); describe('when component has custom data-attributes', () => { beforeEach(() => { props = { string: 'foo', array: JSON.stringify(['foo', 'bar']), object: JSON.stringify({ foo: 'bar' }), objectNotStringified: { foo: 'bar' }, }; }); it('passes data attributes as props', () => { subject(); Object.entries(props!).forEach(([name, value]) => expect( screen.getByShadowText(`Prop: ${name}, Value: ${JSON.stringify(value)}`) ).toBeInTheDocument() ); }); test('useMFEDataContext exposes data attributes', () => { subject(); Object.entries(props!).forEach(([name, value]) => expect( screen.getByShadowText(`Data: ${name}, Value: ${JSON.stringify(value)}`) ).toBeInTheDocument() ); }); }); test('logs an error when WEB_COMPONENT_NAME is not set', () => { const consoleSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn()); jest.isolateModules(() => { registerComponent(MockComponent, undefined); }); expect(consoleSpy).toHaveBeenCalledWith('"WEB_COMPONENT_NAME" is not defined!'); }); test('configures the shadow root in PopupPropsContext', () => { subject(); expect(screen.getByShadowText(/TablePopupPropsContext appendTo:/)).toHaveTextContent( /\[object ShadowRoot\]/ ); }); test('assigns "data-mfe-portal-name" to portal element', () => { subject(); expect(Page.portalElement).toHaveAttribute('data-mfe-portal-name', baseComponentName); }); test('does not throw when document body is removed', () => { const body = document.body; const parentNode = body.parentNode!; try { subject(); expect(() => parentNode.removeChild(body)).not.toThrow(); } finally { parentNode.appendChild(body); } }); describe('with onReady callback', () => { beforeEach(() => (onReady = jest.fn())); test('calls onReady', () => { subject(); expect(onReady).toHaveBeenCalled(); }); }); describe('with onDispose callback', () => { beforeEach(() => (onDispose = jest.fn())); test('calls onDispose when element is removed from DOM', () => { const { container } = subject(); container.parentNode!.removeChild(container); expect(onDispose).toHaveBeenCalled(); }); }); });