diff --git a/plugins/techdocs-module-addons-contrib/src/TextSize/TextSize.test.tsx b/plugins/techdocs-module-addons-contrib/src/TextSize/TextSize.test.tsx
index 21478f0636..de66889312 100644
--- a/plugins/techdocs-module-addons-contrib/src/TextSize/TextSize.test.tsx
+++ b/plugins/techdocs-module-addons-contrib/src/TextSize/TextSize.test.tsx
@@ -16,10 +16,22 @@
import { TechDocsAddonTester } from '@backstage/plugin-techdocs-addons-test-utils';
import React from 'react';
-import { fireEvent, waitFor, act } from '@testing-library/react';
+import { act, fireEvent, waitFor } from '@testing-library/react';
import { TextSize } from '../plugin';
+import { useShadowRootElements } from '@backstage/plugin-techdocs-react';
+
+jest.mock('@backstage/plugin-techdocs-react', () => ({
+ ...jest.requireActual('@backstage/plugin-techdocs-react'),
+ useShadowRootElements: jest.fn(),
+}));
describe('TextSize', () => {
+ const useShadowRootElementsMock = useShadowRootElements as jest.Mock;
+
+ beforeEach(() => {
+ useShadowRootElementsMock.mockReturnValue([]);
+ });
+
it('renders without exploding', async () => {
const { getByText } = await TechDocsAddonTester.buildAddonsInTechDocs([
,
@@ -36,6 +48,9 @@ describe('TextSize', () => {
.withDom(
TEST_CONTENT)
.renderWithEffects();
+ const content = getByText('TEST_CONTENT');
+ useShadowRootElementsMock.mockReturnValue([content]);
+
fireEvent.click(getByTitle('Settings'));
await waitFor(() => {
@@ -60,7 +75,9 @@ describe('TextSize', () => {
let style = window.getComputedStyle(getByText('TEST_CONTENT'));
- expect(style.getPropertyValue('--md-typeset-font-size')).toBe('18.4px');
+ await waitFor(() => {
+ expect(style.getPropertyValue('--md-typeset-font-size')).toBe('18.4px');
+ });
fireEvent.keyDown(slider, {
key: 'ArrowLeft',
@@ -88,6 +105,9 @@ describe('TextSize', () => {
.withDom(TEST_CONTENT)
.renderWithEffects();
+ const content = getByText('TEST_CONTENT');
+ useShadowRootElementsMock.mockReturnValue([content]);
+
fireEvent.click(getByTitle('Settings'));
await waitFor(() => {
diff --git a/plugins/techdocs-react/src/hooks.test.ts b/plugins/techdocs-react/src/hooks.test.ts
index 0cd55fa10f..5982299000 100644
--- a/plugins/techdocs-react/src/hooks.test.ts
+++ b/plugins/techdocs-react/src/hooks.test.ts
@@ -19,7 +19,7 @@ import {
useShadowRootElements,
useShadowRootSelection,
} from './hooks';
-import { renderHook } from '@testing-library/react';
+import { act, renderHook } from '@testing-library/react';
import { fireEvent, waitFor } from '@testing-library/react';
const fireSelectionChangeEvent = (window: Window) => {
@@ -34,7 +34,7 @@ const getSelection = jest.fn();
const mockShadowRoot = () => {
const div = document.createElement('div');
const shadowRoot = div.attachShadow({ mode: 'open' });
- shadowRoot.innerHTML = 'Shadow DOM Mock
';
+ shadowRoot.innerHTML = 'Shadow DOM Mock
';
(shadowRoot as ShadowRoot & Pick).getSelection =
getSelection;
return shadowRoot;
@@ -85,6 +85,55 @@ describe('hooks', () => {
expect(result.current).toHaveLength(1);
});
+
+ it('should update elements if shadow root changes', async () => {
+ const { result, rerender } = renderHook(() =>
+ useShadowRootElements(['h1']),
+ );
+
+ act(() => {
+ shadowRoot.innerHTML = 'Updated Shadow DOM Mock
';
+ rerender();
+ });
+
+ await waitFor(() => {
+ expect(result.current[0].textContent).toBe('Updated Shadow DOM Mock');
+ });
+ });
+
+ describe('mutation observer', () => {
+ const observer: jest.Mocked = {
+ observe: jest.fn(),
+ disconnect: jest.fn(),
+ takeRecords: jest.fn(),
+ };
+
+ beforeEach(() => {
+ jest
+ .spyOn(window, 'MutationObserver')
+ .mockImplementation(() => observer);
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should observe shadow root changes', async () => {
+ renderHook(() => useShadowRootElements(['h1']));
+ expect(observer.observe).toHaveBeenCalledWith(shadowRoot, {
+ childList: true,
+ attributes: true,
+ characterData: true,
+ subtree: true,
+ });
+ });
+
+ it('should disconnect observer on unmount', async () => {
+ const { unmount } = renderHook(() => useShadowRootElements(['h1']));
+ unmount();
+ expect(observer.disconnect).toHaveBeenCalled();
+ });
+ });
});
describe('useShadowRootSelection', () => {
diff --git a/plugins/techdocs-react/src/hooks.ts b/plugins/techdocs-react/src/hooks.ts
index 2d922ab7f4..1203622cb2 100644
--- a/plugins/techdocs-react/src/hooks.ts
+++ b/plugins/techdocs-react/src/hooks.ts
@@ -39,13 +39,13 @@ export const useShadowRootElements = <
selectors: string[],
): TReturnedElement[] => {
const shadowRoot = useShadowRoot();
- const [render, rerender] = useState(false);
+ const [root, setRootNode] = useState(shadowRoot?.firstChild);
useEffect(() => {
let observer: MutationObserver;
if (shadowRoot) {
observer = new MutationObserver(() => {
- rerender(!render);
+ setRootNode(shadowRoot?.firstChild);
});
observer.observe(shadowRoot, {
attributes: true,
@@ -55,12 +55,12 @@ export const useShadowRootElements = <
});
}
return () => observer?.disconnect();
- }, [shadowRoot, render, rerender]);
+ }, [shadowRoot]);
- if (!shadowRoot) return [];
+ if (!root || !(root instanceof HTMLElement)) return [];
return selectors
- .map(selector => shadowRoot.querySelectorAll(selector))
+ .map(selector => root.querySelectorAll(selector))
.filter(nodeList => nodeList.length)
.map(nodeList => Array.from(nodeList))
.flat();
diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.test.tsx
index adef2f64d0..322ddf5592 100644
--- a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.test.tsx
+++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.test.tsx
@@ -20,6 +20,7 @@ import { CompoundEntityRef } from '@backstage/catalog-model';
import {
techdocsApiRef,
TechDocsReaderPageProvider,
+ useShadowRootElements,
} from '@backstage/plugin-techdocs-react';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
@@ -36,6 +37,7 @@ jest.mock('../useReaderState', () => ({
jest.mock('@backstage/plugin-techdocs-react', () => ({
...jest.requireActual('@backstage/plugin-techdocs-react'),
useShadowDomStylesLoading: jest.fn().mockReturnValue(false),
+ useShadowRootElements: jest.fn(),
}));
import { TechDocsReaderPageContent } from './TechDocsReaderPageContent';
@@ -88,6 +90,12 @@ const Wrapper = ({
);
describe('', () => {
+ const useShadowRootElementsMock = useShadowRootElements as jest.Mock;
+
+ beforeEach(() => {
+ useShadowRootElementsMock.mockReturnValue([]);
+ });
+
afterEach(() => {
jest.clearAllMocks();
});
@@ -154,6 +162,29 @@ describe('', () => {
});
});
+ it('should scroll to header if hash is not present in url', async () => {
+ jest.spyOn(document, 'querySelector');
+
+ getEntityMetadata.mockResolvedValue(mockEntityMetadata);
+ getTechDocsMetadata.mockResolvedValue(mockTechDocsMetadata);
+ useTechDocsReaderDom.mockReturnValue(document.createElement('html'));
+ useReaderState.mockReturnValue({ state: 'cached' });
+
+ const rendered = await renderInTestApp(
+
+
+ ,
+ );
+
+ await waitFor(() => {
+ expect(
+ rendered.getByTestId('techdocs-native-shadowroot'),
+ ).toBeInTheDocument();
+
+ expect(document.querySelector).toHaveBeenCalledWith('header');
+ });
+ });
+
it('should scroll to hash if hash is present in url', async () => {
jest.spyOn(document, 'querySelector');
@@ -165,6 +196,7 @@ describe('', () => {
const mockTechDocsPage = document.createElement('html');
mockTechDocsPage.appendChild(h2);
+ useShadowRootElementsMock.mockReturnValue([h2]);
getEntityMetadata.mockResolvedValue(mockEntityMetadata);
getTechDocsMetadata.mockResolvedValue(mockTechDocsMetadata);
useTechDocsReaderDom.mockReturnValue(mockTechDocsPage);
@@ -188,26 +220,4 @@ describe('', () => {
window.location.hash = '';
});
-
- it('should scroll to header if hash is not present in url', async () => {
- jest.spyOn(document, 'querySelector');
-
- getEntityMetadata.mockResolvedValue(mockEntityMetadata);
- getTechDocsMetadata.mockResolvedValue(mockTechDocsMetadata);
- useTechDocsReaderDom.mockReturnValue(document.createElement('html'));
- useReaderState.mockReturnValue({ state: 'cached' });
-
- const rendered = await renderInTestApp(
-
-
- ,
- );
-
- await waitFor(() => {
- expect(
- rendered.getByTestId('techdocs-native-shadowroot'),
- ).toBeInTheDocument();
- expect(document.querySelector).toHaveBeenCalledWith('header');
- });
- });
});