diff --git a/.changeset/techdocs-spotty-tables-beam.md b/.changeset/techdocs-spotty-tables-beam.md
new file mode 100644
index 0000000000..397817b196
--- /dev/null
+++ b/.changeset/techdocs-spotty-tables-beam.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-techdocs': patch
+---
+
+Refactor the techdocs transformers to return `Promise`s and await all transformations.
diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx
index 282f0d3447..ce72da4bf6 100644
--- a/plugins/techdocs/src/reader/components/Reader.tsx
+++ b/plugins/techdocs/src/reader/components/Reader.tsx
@@ -109,35 +109,26 @@ export const Reader = ({ entityId, onReady }: Props) => {
// an update to "state" might lead to an updated UI so we include it as a trigger
}, [updateSidebarPosition, state]);
- useEffect(() => {
- if (!rawPage || !shadowDomRef.current) {
- // clear the shadow dom if no content is available
- if (shadowDomRef.current?.shadowRoot) {
- shadowDomRef.current.shadowRoot.innerHTML = '';
- }
- return;
- }
- if (onReady) {
- onReady();
- }
- // Pre-render
- const transformedElement = transformer(rawPage, [
- sanitizeDOM(),
- addBaseUrl({
- techdocsStorageApi,
- entityId: {
- kind,
- name,
- namespace,
- },
- path,
- }),
- rewriteDocLinks(),
- removeMkdocsHeader(),
- simplifyMkdocsFooter(),
- addGitFeedbackLink(scmIntegrationsApi),
- injectCss({
- css: `
+ // a function that performs transformations that are executed prior to adding it to the DOM
+ const preRender = useCallback(
+ (rawContent: string, contentPath: string) =>
+ transformer(rawContent, [
+ sanitizeDOM(),
+ addBaseUrl({
+ techdocsStorageApi,
+ entityId: {
+ kind,
+ name,
+ namespace,
+ },
+ path: contentPath,
+ }),
+ rewriteDocLinks(),
+ removeMkdocsHeader(),
+ simplifyMkdocsFooter(),
+ addGitFeedbackLink(scmIntegrationsApi),
+ injectCss({
+ css: `
body {
font-family: ${theme.typography.fontFamily};
--md-text-color: ${theme.palette.text.primary};
@@ -194,21 +185,21 @@ export const Reader = ({ entityId, onReady }: Props) => {
}
}
`,
- }),
- injectCss({
- // Disable CSS animations on link colors as they lead to issues in dark
- // mode. The dark mode color theme is applied later and theirfore there
- // is always an animation from light to dark mode when navigation
- // between pages.
- css: `
+ }),
+ injectCss({
+ // Disable CSS animations on link colors as they lead to issues in dark
+ // mode. The dark mode color theme is applied later and theirfore there
+ // is always an animation from light to dark mode when navigation
+ // between pages.
+ css: `
.md-nav__link, .md-typeset a, .md-typeset a::before, .md-typeset .headerlink {
transition: none;
}
`,
- }),
- injectCss({
- // Properly style code blocks.
- css: `
+ }),
+ injectCss({
+ // Properly style code blocks.
+ css: `
.md-typeset pre > code::-webkit-scrollbar-thumb {
background-color: hsla(0, 0%, 0%, 0.32);
}
@@ -216,17 +207,17 @@ export const Reader = ({ entityId, onReady }: Props) => {
background-color: hsla(0, 0%, 0%, 0.87);
}
`,
- }),
- injectCss({
- // Admonitions and others are using SVG masks to define icons. These
- // masks are defined as CSS variables.
- // As the MkDocs output is rendered in shadow DOM, the CSS variable
- // definitions on the root selector are not applied. Instead, the have
- // to be applied on :host.
- // As there is no way to transform the served main*.css yet (for
- // example in the backend), we have to copy from main*.css and modify
- // them.
- css: `
+ }),
+ injectCss({
+ // Admonitions and others are using SVG masks to define icons. These
+ // masks are defined as CSS variables.
+ // As the MkDocs output is rendered in shadow DOM, the CSS variable
+ // definitions on the root selector are not applied. Instead, the have
+ // to be applied on :host.
+ // As there is no way to transform the served main*.css yet (for
+ // example in the backend), we have to copy from main*.css and modify
+ // them.
+ css: `
:host {
--md-admonition-icon--note: url('data:image/svg+xml;charset=utf-8,');
--md-admonition-icon--abstract: url('data:image/svg+xml;charset=utf-8,');
@@ -252,97 +243,129 @@ export const Reader = ({ entityId, onReady }: Props) => {
--md-tasklist-icon--checked: url('data:image/svg+xml;charset=utf-8,');
}
`,
- }),
- ]);
+ }),
+ ]),
+ [
+ kind,
+ name,
+ namespace,
+ scmIntegrationsApi,
+ techdocsStorageApi,
+ theme.palette.background.default,
+ theme.palette.background.paper,
+ theme.palette.primary.main,
+ theme.palette.text.primary,
+ theme.typography.fontFamily,
+ ],
+ );
- if (!transformedElement) {
- return; // An unexpected error occurred
+ // a function that performs transformations that are executed after adding it to the DOM
+ const postRender = useCallback(
+ async (shadowRoot: ShadowRoot) =>
+ transformer(shadowRoot.children[0], [
+ dom => {
+ setTimeout(() => {
+ // Scoll to the desired anchor on initial navigation
+ if (window.location.hash) {
+ const hash = window.location.hash.slice(1);
+ shadowRoot?.getElementById(hash)?.scrollIntoView();
+ }
+ }, 200);
+ return dom;
+ },
+ addLinkClickListener({
+ baseUrl: window.location.origin,
+ onClick: (_: MouseEvent, url: string) => {
+ const parsedUrl = new URL(url);
+
+ if (parsedUrl.hash) {
+ navigate(`${parsedUrl.pathname}${parsedUrl.hash}`);
+
+ // Scroll to hash if it's on the current page
+ shadowRoot
+ ?.getElementById(parsedUrl.hash.slice(1))
+ ?.scrollIntoView();
+ } else {
+ navigate(parsedUrl.pathname);
+ }
+ },
+ }),
+ onCssReady({
+ docStorageUrl: await techdocsStorageApi.getApiOrigin(),
+ onLoading: (dom: Element) => {
+ (dom as HTMLElement).style.setProperty('opacity', '0');
+ },
+ onLoaded: (dom: Element) => {
+ (dom as HTMLElement).style.removeProperty('opacity');
+ // disable MkDocs drawer toggling ('for' attribute => checkbox mechanism)
+ (dom as HTMLElement)
+ .querySelector('.md-nav__title')
+ ?.removeAttribute('for');
+ const sideDivs: HTMLElement[] = Array.from(
+ shadowRoot!.querySelectorAll('.md-sidebar'),
+ );
+ setSidebars(sideDivs);
+ // set sidebar height so they don't initially render in wrong position
+ const docTopPosition = (dom as HTMLElement).getBoundingClientRect()
+ .top;
+ const mdTabs = dom.querySelector('.md-container > .md-tabs');
+ sideDivs!.forEach(sidebar => {
+ sidebar.style.top = mdTabs
+ ? `${docTopPosition + mdTabs.getBoundingClientRect().height}px`
+ : `${docTopPosition}px`;
+ });
+ },
+ }),
+ ]),
+ [navigate, techdocsStorageApi],
+ );
+
+ useEffect(() => {
+ if (!rawPage || !shadowDomRef.current) {
+ // clear the shadow dom if no content is available
+ if (shadowDomRef.current?.shadowRoot) {
+ shadowDomRef.current.shadowRoot.innerHTML = '';
+ }
+ return () => {};
+ }
+ if (onReady) {
+ onReady();
}
- const shadowDiv: HTMLElement = shadowDomRef.current!;
- const shadowRoot =
- shadowDiv.shadowRoot || shadowDiv.attachShadow({ mode: 'open' });
- Array.from(shadowRoot.children).forEach(child =>
- shadowRoot.removeChild(child),
- );
- shadowRoot.appendChild(transformedElement);
+ // if false, there is already a newer execution of this effect
+ let shouldReplaceContent = true;
- // Scroll to top after render
- window.scroll({ top: 0 });
+ // Pre-render
+ preRender(rawPage, path).then(async transformedElement => {
+ if (!transformedElement?.innerHTML) {
+ return; // An unexpected error occurred
+ }
- // Post-render
- transformer(shadowRoot.children[0], [
- dom => {
- setTimeout(() => {
- // Scoll to the desired anchor on initial navigation
- if (window.location.hash) {
- const hash = window.location.hash.slice(1);
- shadowRoot?.getElementById(hash)?.scrollIntoView();
- }
- }, 200);
- return dom;
- },
- addLinkClickListener({
- baseUrl: window.location.origin,
- onClick: (_: MouseEvent, url: string) => {
- const parsedUrl = new URL(url);
+ // don't manipulate the shadow dom if this isn't the latest effect execution
+ if (!shouldReplaceContent) {
+ return;
+ }
- if (parsedUrl.hash) {
- navigate(`${parsedUrl.pathname}${parsedUrl.hash}`);
+ const shadowDiv: HTMLElement = shadowDomRef.current!;
+ const shadowRoot =
+ shadowDiv.shadowRoot || shadowDiv.attachShadow({ mode: 'open' });
+ Array.from(shadowRoot.children).forEach(child =>
+ shadowRoot.removeChild(child),
+ );
+ shadowRoot.appendChild(transformedElement);
- // Scroll to hash if it's on the current page
- shadowRoot
- ?.getElementById(parsedUrl.hash.slice(1))
- ?.scrollIntoView();
- } else {
- navigate(parsedUrl.pathname);
- }
- },
- }),
- onCssReady({
- docStorageUrl: techdocsStorageApi.getApiOrigin(),
- onLoading: (dom: Element) => {
- (dom as HTMLElement).style.setProperty('opacity', '0');
- },
- onLoaded: (dom: Element) => {
- (dom as HTMLElement).style.removeProperty('opacity');
- // disable MkDocs drawer toggling ('for' attribute => checkbox mechanism)
- (dom as HTMLElement)
- .querySelector('.md-nav__title')
- ?.removeAttribute('for');
- const sideDivs: HTMLElement[] = Array.from(
- shadowRoot!.querySelectorAll('.md-sidebar'),
- );
- setSidebars(sideDivs);
- // set sidebar height so they don't initially render in wrong position
- const docTopPosition = (dom as HTMLElement).getBoundingClientRect()
- .top;
- const mdTabs = dom.querySelector('.md-container > .md-tabs');
- sideDivs!.forEach(sidebar => {
- sidebar.style.top = mdTabs
- ? `${docTopPosition + mdTabs.getBoundingClientRect().height}px`
- : `${docTopPosition}px`;
- });
- },
- }),
- ]);
- }, [
- path,
- kind,
- namespace,
- name,
- rawPage,
- navigate,
- onReady,
- shadowDomRef,
- techdocsStorageApi,
- theme.typography.fontFamily,
- theme.palette.text.primary,
- theme.palette.primary.main,
- theme.palette.background.paper,
- theme.palette.background.default,
- scmIntegrationsApi,
- ]);
+ // Scroll to top after render
+ window.scroll({ top: 0 });
+
+ // Post-render
+ await postRender(shadowRoot);
+ });
+
+ // cancel this execution
+ return () => {
+ shouldReplaceContent = false;
+ };
+ }, [onReady, path, postRender, preRender, rawPage]);
return (
<>
diff --git a/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts b/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts
index f04c9963f0..11e8375420 100644
--- a/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts
+++ b/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts
@@ -15,9 +15,9 @@
*/
import { waitFor } from '@testing-library/react';
-import { createTestShadowDom } from '../../test-utils';
-import { addBaseUrl } from '../transformers';
import { TechDocsStorageApi } from '../../api';
+import { createTestShadowDom } from '../../test-utils';
+import { addBaseUrl } from './addBaseUrl';
const DOC_STORAGE_URL = 'https://example-host.storage.googleapis.com';
const API_ORIGIN_URL = 'https://backstage.example.com/api/techdocs';
@@ -62,8 +62,8 @@ describe('addBaseUrl', () => {
global.fetch = originalFetch;
});
- it('contains relative paths', () => {
- createTestShadowDom(fixture, {
+ it('contains relative paths', async () => {
+ await createTestShadowDom(fixture, {
preTransformers: [
addBaseUrl({
techdocsStorageApi,
@@ -110,7 +110,7 @@ describe('addBaseUrl', () => {
text: jest.fn().mockResolvedValue(svgContent),
});
- const root = createTestShadowDom('
', {
+ const root = await createTestShadowDom('
', {
preTransformers: [
addBaseUrl({
techdocsStorageApi,
@@ -137,7 +137,7 @@ describe('addBaseUrl', () => {
text: jest.fn().mockResolvedValue(svgContent),
});
- const root = createTestShadowDom(
+ const root = await createTestShadowDom(
`
`,
{
preTransformers: [
@@ -162,16 +162,19 @@ describe('addBaseUrl', () => {
it('does not inline external svgs', async () => {
const expectedSrc = 'https://example.com/test.svg';
- const root = createTestShadowDom(`
`, {
- preTransformers: [
- addBaseUrl({
- techdocsStorageApi,
- entityId: mockEntityId,
- path: '',
- }),
- ],
- postTransformers: [],
- });
+ const root = await createTestShadowDom(
+ `
`,
+ {
+ preTransformers: [
+ addBaseUrl({
+ techdocsStorageApi,
+ entityId: mockEntityId,
+ path: '',
+ }),
+ ],
+ postTransformers: [],
+ },
+ );
await new Promise(done => {
process.nextTick(() => {
diff --git a/plugins/techdocs/src/reader/transformers/addBaseUrl.ts b/plugins/techdocs/src/reader/transformers/addBaseUrl.ts
index f21cb8c47a..8f08ec83a6 100644
--- a/plugins/techdocs/src/reader/transformers/addBaseUrl.ts
+++ b/plugins/techdocs/src/reader/transformers/addBaseUrl.ts
@@ -14,8 +14,8 @@
* limitations under the License.
*/
import { EntityName } from '@backstage/catalog-model';
-import type { Transformer } from './transformer';
import { TechDocsStorageApi } from '../../api';
+import type { Transformer } from './transformer';
type AddBaseUrlOptions = {
techdocsStorageApi: TechDocsStorageApi;
@@ -44,14 +44,15 @@ export const addBaseUrl = ({
entityId,
path,
}: AddBaseUrlOptions): Transformer => {
- return dom => {
- const updateDom = (
+ return async dom => {
+ const apiOrigin = await techdocsStorageApi.getApiOrigin();
+
+ const updateDom = async (
list: HTMLCollectionOf | NodeListOf,
attributeName: string,
- ): void => {
- Array.from(list)
- .filter(elem => !!elem.getAttribute(attributeName))
- .forEach(async (elem: T) => {
+ ) => {
+ for (const elem of list) {
+ if (elem.hasAttribute(attributeName)) {
const elemAttribute = elem.getAttribute(attributeName);
if (!elemAttribute) return;
@@ -61,7 +62,7 @@ export const addBaseUrl = ({
entityId,
path,
);
- const apiOrigin = await techdocsStorageApi.getApiOrigin();
+
if (isSvgNeedingInlining(attributeName, elemAttribute, apiOrigin)) {
try {
const svg = await fetch(newValue, { credentials: 'include' });
@@ -76,13 +77,16 @@ export const addBaseUrl = ({
} else {
elem.setAttribute(attributeName, newValue);
}
- });
+ }
+ }
};
- updateDom(dom.querySelectorAll('img'), 'src');
- updateDom(dom.querySelectorAll('script'), 'src');
- updateDom(dom.querySelectorAll('link'), 'href');
- updateDom(dom.querySelectorAll('a[download]'), 'href');
+ await Promise.all([
+ updateDom(dom.querySelectorAll('img'), 'src'),
+ updateDom(dom.querySelectorAll('script'), 'src'),
+ updateDom(dom.querySelectorAll('link'), 'href'),
+ updateDom(dom.querySelectorAll('a[download]'), 'href'),
+ ]);
return dom;
};
diff --git a/plugins/techdocs/src/reader/transformers/addGitFeedbackLink.test.ts b/plugins/techdocs/src/reader/transformers/addGitFeedbackLink.test.ts
index 2165afac98..0b8e431781 100644
--- a/plugins/techdocs/src/reader/transformers/addGitFeedbackLink.test.ts
+++ b/plugins/techdocs/src/reader/transformers/addGitFeedbackLink.test.ts
@@ -28,8 +28,8 @@ const integrations = ScmIntegrations.fromConfig(
);
describe('addGitFeedbackLink', () => {
- it('adds a feedback link when a Gitlab source edit link is available', () => {
- const shadowDom = createTestShadowDom(
+ it('adds a feedback link when a Gitlab source edit link is available', async () => {
+ const shadowDom = await createTestShadowDom(
`
@@ -53,8 +53,8 @@ describe('addGitFeedbackLink', () => {
);
});
- it('adds a feedback link when a Github source edit link is available', () => {
- const shadowDom = createTestShadowDom(
+ it('adds a feedback link when a Github source edit link is available', async () => {
+ const shadowDom = await createTestShadowDom(
`
@@ -78,8 +78,8 @@ describe('addGitFeedbackLink', () => {
);
});
- it('does not add a feedback link when no source edit link is available', () => {
- const shadowDom = createTestShadowDom(
+ it('does not add a feedback link when no source edit link is available', async () => {
+ const shadowDom = await createTestShadowDom(
`
@@ -97,8 +97,8 @@ describe('addGitFeedbackLink', () => {
expect(shadowDom.querySelector('#git-feedback-link')).toBeFalsy();
});
- it('does not add a feedback link when a Gitlab or Github source edit link is not available', () => {
- const shadowDom = createTestShadowDom(
+ it('does not add a feedback link when a Gitlab or Github source edit link is not available', async () => {
+ const shadowDom = await createTestShadowDom(
`
@@ -117,8 +117,8 @@ describe('addGitFeedbackLink', () => {
expect(shadowDom.querySelector('#git-feedback-link')).toBeFalsy();
});
- it('adds a feedback link when a Gitlab or Github source edit link is not available but hostname matches an integrations host', () => {
- const shadowDom = createTestShadowDom(
+ it('adds a feedback link when a Gitlab or Github source edit link is not available but hostname matches an integrations host', async () => {
+ const shadowDom = await createTestShadowDom(
`
diff --git a/plugins/techdocs/src/reader/transformers/addLinkClickListener.test.ts b/plugins/techdocs/src/reader/transformers/addLinkClickListener.test.ts
index 2d2d54a31c..fe3a6e557c 100644
--- a/plugins/techdocs/src/reader/transformers/addLinkClickListener.test.ts
+++ b/plugins/techdocs/src/reader/transformers/addLinkClickListener.test.ts
@@ -18,9 +18,9 @@ import { createTestShadowDom } from '../../test-utils';
import { addLinkClickListener } from './addLinkClickListener';
describe('addLinkClickListener', () => {
- it('calls onClick when a link has been clicked', () => {
+ it('calls onClick when a link has been clicked', async () => {
const fn = jest.fn();
- const shadowDom = createTestShadowDom(
+ const shadowDom = await createTestShadowDom(
`
@@ -45,9 +45,9 @@ describe('addLinkClickListener', () => {
expect(fn).toHaveBeenCalledTimes(1);
});
- it('does not call onClick when a link links to another baseUrl', () => {
+ it('does not call onClick when a link links to another baseUrl', async () => {
const fn = jest.fn();
- const shadowDom = createTestShadowDom(
+ const shadowDom = await createTestShadowDom(
`
diff --git a/plugins/techdocs/src/reader/transformers/index.test.ts b/plugins/techdocs/src/reader/transformers/index.test.ts
index 30607aedcd..16b42266df 100644
--- a/plugins/techdocs/src/reader/transformers/index.test.ts
+++ b/plugins/techdocs/src/reader/transformers/index.test.ts
@@ -17,14 +17,14 @@
import { Transformer, transform } from './transformer';
describe('transform', () => {
- it('calls the transformers', () => {
+ it('calls the transformers', async () => {
const fn = jest.fn();
const mockTransformer = (): Transformer => (dom: Element) => {
fn(dom);
return dom;
};
- transform('', [mockTransformer()]);
+ await transform('', [mockTransformer()]);
expect(fn).toHaveBeenCalledTimes(1);
expect(fn).toHaveBeenCalledWith(expect.any(Element));
diff --git a/plugins/techdocs/src/reader/transformers/injectCss.test.ts b/plugins/techdocs/src/reader/transformers/injectCss.test.ts
index 368d077b43..6d0eb8daa9 100644
--- a/plugins/techdocs/src/reader/transformers/injectCss.test.ts
+++ b/plugins/techdocs/src/reader/transformers/injectCss.test.ts
@@ -15,10 +15,10 @@
*/
import { createTestShadowDom } from '../../test-utils';
-import { injectCss } from '../transformers';
+import { injectCss } from './injectCss';
describe('injectCss', () => {
- it('should inject style with passed css in head', () => {
+ it('should inject style with passed css in head', async () => {
const html = `
@@ -27,7 +27,7 @@ describe('injectCss', () => {
`;
const injectedCss = '* {background-color: #fff}';
- const shadowDom = createTestShadowDom(html, {
+ const shadowDom = await createTestShadowDom(html, {
preTransformers: [injectCss({ css: injectedCss })],
postTransformers: [],
});
diff --git a/plugins/techdocs/src/reader/transformers/onCssReady.test.ts b/plugins/techdocs/src/reader/transformers/onCssReady.test.ts
index 3174e23f5c..b2a5aa9760 100644
--- a/plugins/techdocs/src/reader/transformers/onCssReady.test.ts
+++ b/plugins/techdocs/src/reader/transformers/onCssReady.test.ts
@@ -15,16 +15,15 @@
*/
import {
- createTestShadowDom,
- mockStylesheetEventListener,
- executeStylesheetEventListeners,
clearStylesheetEventListeners,
+ createTestShadowDom,
+ executeStylesheetEventListeners,
+ mockStylesheetEventListener,
} from '../../test-utils';
-import { onCssReady } from '../transformers';
+import { onCssReady } from './onCssReady';
-const docStorageUrl: Promise = Promise.resolve(
- 'https://techdocs-mock-sites.storage.googleapis.com',
-);
+const docStorageUrl: string =
+ 'https://techdocs-mock-sites.storage.googleapis.com';
const fixture = `
@@ -48,11 +47,11 @@ describe('onCssReady', () => {
clearStylesheetEventListeners();
});
- it('does not call onLoading and onLoaded without the onCssReady transformer', () => {
+ it('does not call onLoading and onLoaded without the onCssReady transformer', async () => {
const onLoading = jest.fn();
const onLoaded = jest.fn();
- createTestShadowDom(fixture, {
+ await createTestShadowDom(fixture, {
preTransformers: [],
postTransformers: [],
});
@@ -62,11 +61,11 @@ describe('onCssReady', () => {
expect(onLoaded).not.toHaveBeenCalled();
});
- it('calls the onLoading and onLoaded correctly', () => {
+ it('calls the onLoading and onLoaded correctly', async () => {
const onLoading = jest.fn();
const onLoaded = jest.fn();
- createTestShadowDom(fixture, {
+ await createTestShadowDom(fixture, {
preTransformers: [],
postTransformers: [
onCssReady({
diff --git a/plugins/techdocs/src/reader/transformers/onCssReady.ts b/plugins/techdocs/src/reader/transformers/onCssReady.ts
index 936c4306a7..e9406ba632 100644
--- a/plugins/techdocs/src/reader/transformers/onCssReady.ts
+++ b/plugins/techdocs/src/reader/transformers/onCssReady.ts
@@ -17,7 +17,7 @@
import type { Transformer } from './transformer';
type OnCssReadyOptions = {
- docStorageUrl: Promise;
+ docStorageUrl: string;
onLoading: (dom: Element) => void;
onLoaded: (dom: Element) => void;
};
@@ -30,9 +30,7 @@ export const onCssReady = ({
return dom => {
const cssPages = Array.from(
dom.querySelectorAll('head > link[rel="stylesheet"]'),
- ).filter(async elem =>
- elem.getAttribute('href')?.startsWith(await docStorageUrl),
- );
+ ).filter(elem => elem.getAttribute('href')?.startsWith(docStorageUrl));
let count = cssPages.length;
diff --git a/plugins/techdocs/src/reader/transformers/removeMkdocsHeader.test.ts b/plugins/techdocs/src/reader/transformers/removeMkdocsHeader.test.ts
index 70d6b2fa4a..60f0b9ca30 100644
--- a/plugins/techdocs/src/reader/transformers/removeMkdocsHeader.test.ts
+++ b/plugins/techdocs/src/reader/transformers/removeMkdocsHeader.test.ts
@@ -18,20 +18,26 @@ import { createTestShadowDom, FIXTURES } from '../../test-utils';
import { removeMkdocsHeader } from '../transformers';
describe('removeMkdocsHeader', () => {
- it('does not remove mkdocs header', () => {
- const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, {
- preTransformers: [],
- postTransformers: [],
- });
+ it('does not remove mkdocs header', async () => {
+ const shadowDom = await createTestShadowDom(
+ FIXTURES.FIXTURE_STANDARD_PAGE,
+ {
+ preTransformers: [],
+ postTransformers: [],
+ },
+ );
expect(shadowDom.querySelector('.md-header')).toBeTruthy();
});
- it('does remove mkdocs header', () => {
- const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, {
- preTransformers: [removeMkdocsHeader()],
- postTransformers: [],
- });
+ it('does remove mkdocs header', async () => {
+ const shadowDom = await createTestShadowDom(
+ FIXTURES.FIXTURE_STANDARD_PAGE,
+ {
+ preTransformers: [removeMkdocsHeader()],
+ postTransformers: [],
+ },
+ );
expect(shadowDom.querySelector('.md-header')).toBeFalsy();
});
diff --git a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts
index 62b80cd514..66fc5f29c1 100644
--- a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts
+++ b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts
@@ -19,8 +19,8 @@ import { rewriteDocLinks } from '../transformers';
import { normalizeUrl } from './rewriteDocLinks';
describe('rewriteDocLinks', () => {
- it('should not do anything', () => {
- const shadowDom = createTestShadowDom(`
+ it('should not do anything', async () => {
+ const shadowDom = await createTestShadowDom(`
Test
Test
Test
@@ -35,8 +35,8 @@ describe('rewriteDocLinks', () => {
]);
});
- it('should transform a href with localhost as baseUrl', () => {
- const shadowDom = createTestShadowDom(
+ it('should transform a href with localhost as baseUrl', async () => {
+ const shadowDom = await createTestShadowDom(
`
Test
Test
@@ -57,9 +57,9 @@ describe('rewriteDocLinks', () => {
]);
});
- it('should rewrite non-parseable URLs as text', () => {
+ it('should rewrite non-parseable URLs as text', async () => {
const expectedText = `www.my-internet.[top-level-domain]/pathname/[URLkey]`;
- const shadowDom = createTestShadowDom(
+ const shadowDom = await createTestShadowDom(
`${expectedText}`,
{
preTransformers: [rewriteDocLinks()],
diff --git a/plugins/techdocs/src/reader/transformers/sanitizeDOM/index.test.ts b/plugins/techdocs/src/reader/transformers/sanitizeDOM/index.test.ts
index 8456cf7c23..6296793e2a 100644
--- a/plugins/techdocs/src/reader/transformers/sanitizeDOM/index.test.ts
+++ b/plugins/techdocs/src/reader/transformers/sanitizeDOM/index.test.ts
@@ -16,7 +16,7 @@
import { createTestShadowDom, FIXTURES } from '../../../test-utils';
import { Transformer } from '../index';
-import { sanitizeDOM } from '../sanitizeDOM';
+import { sanitizeDOM } from './index';
const injectMaliciousLink = (): Transformer => dom => {
const link = document.createElement('a');
@@ -27,55 +27,64 @@ const injectMaliciousLink = (): Transformer => dom => {
};
describe('sanitizeDOM', () => {
- it('contains a script tag', () => {
- const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE);
+ it('contains a script tag', async () => {
+ const shadowDom = await createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE);
expect(shadowDom.querySelectorAll('script').length).toBeGreaterThan(0);
});
- it('does not contain a script tag', () => {
- const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, {
- preTransformers: [sanitizeDOM()],
- postTransformers: [],
- });
+ it('does not contain a script tag', async () => {
+ const shadowDom = await createTestShadowDom(
+ FIXTURES.FIXTURE_STANDARD_PAGE,
+ {
+ preTransformers: [sanitizeDOM()],
+ postTransformers: [],
+ },
+ );
expect(shadowDom.querySelectorAll('script').length).toBe(0);
});
- it('contains link with a onClick attribute', () => {
- const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, {
- preTransformers: [injectMaliciousLink()],
- postTransformers: [],
- });
+ it('contains link with a onClick attribute', async () => {
+ const shadowDom = await createTestShadowDom(
+ FIXTURES.FIXTURE_STANDARD_PAGE,
+ {
+ preTransformers: [injectMaliciousLink()],
+ postTransformers: [],
+ },
+ );
expect(
shadowDom.querySelector('#test-malicious-link')?.hasAttribute('onclick'),
).toBeTruthy();
});
- it('does not contain link with a onClick attribute', () => {
- const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, {
- preTransformers: [sanitizeDOM()],
- postTransformers: [],
- });
+ it('does not contain link with a onClick attribute', async () => {
+ const shadowDom = await createTestShadowDom(
+ FIXTURES.FIXTURE_STANDARD_PAGE,
+ {
+ preTransformers: [sanitizeDOM()],
+ postTransformers: [],
+ },
+ );
expect(
shadowDom.querySelector('#test-malicious-link')?.hasAttribute('onclick'),
).toBeFalsy();
});
- it('removes style tags', () => {
+ it('removes style tags', async () => {
const html = `
-
`;
- const shadowDom = createTestShadowDom(html, {
+ const shadowDom = await createTestShadowDom(html, {
preTransformers: [sanitizeDOM()],
postTransformers: [],
});
@@ -83,7 +92,7 @@ describe('sanitizeDOM', () => {
expect(shadowDom.querySelectorAll('style').length).toEqual(0);
});
- it('does not remove link tags', () => {
+ it('does not remove link tags', async () => {
const html = `
@@ -94,7 +103,7 @@ describe('sanitizeDOM', () => {
`;
- const shadowDom = createTestShadowDom(html, {
+ const shadowDom = await createTestShadowDom(html, {
preTransformers: [sanitizeDOM()],
postTransformers: [],
});
diff --git a/plugins/techdocs/src/reader/transformers/simplifyMkdocsFooter.test.ts b/plugins/techdocs/src/reader/transformers/simplifyMkdocsFooter.test.ts
index dbc3761e80..020befd522 100644
--- a/plugins/techdocs/src/reader/transformers/simplifyMkdocsFooter.test.ts
+++ b/plugins/techdocs/src/reader/transformers/simplifyMkdocsFooter.test.ts
@@ -18,20 +18,26 @@ import { createTestShadowDom, FIXTURES } from '../../test-utils';
import { simplifyMkdocsFooter } from './simplifyMkdocsFooter';
describe('simplifyMkdocsFooter', () => {
- it('does not remove mkdocs copyright', () => {
- const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, {
- preTransformers: [],
- postTransformers: [],
- });
+ it('does not remove mkdocs copyright', async () => {
+ const shadowDom = await createTestShadowDom(
+ FIXTURES.FIXTURE_STANDARD_PAGE,
+ {
+ preTransformers: [],
+ postTransformers: [],
+ },
+ );
expect(shadowDom.querySelector('.md-footer-copyright')).toBeTruthy();
});
- it('does remove mkdocs copyright', () => {
- const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, {
- preTransformers: [simplifyMkdocsFooter()],
- postTransformers: [],
- });
+ it('does remove mkdocs copyright', async () => {
+ const shadowDom = await createTestShadowDom(
+ FIXTURES.FIXTURE_STANDARD_PAGE,
+ {
+ preTransformers: [simplifyMkdocsFooter()],
+ postTransformers: [],
+ },
+ );
expect(shadowDom.querySelector('.md-footer-copyright')).toBeFalsy();
});
diff --git a/plugins/techdocs/src/reader/transformers/transformer.ts b/plugins/techdocs/src/reader/transformers/transformer.ts
index 7b440befbf..fc52f42b5c 100644
--- a/plugins/techdocs/src/reader/transformers/transformer.ts
+++ b/plugins/techdocs/src/reader/transformers/transformer.ts
@@ -14,12 +14,12 @@
* limitations under the License.
*/
-export type Transformer = (dom: Element) => Element;
+export type Transformer = (dom: Element) => Element | Promise;
-export const transform = (
+export const transform = async (
html: string | Element,
transformers: Transformer[],
-): Element => {
+): Promise => {
let dom: Element;
if (typeof html === 'string') {
@@ -30,9 +30,9 @@ export const transform = (
throw new Error('dom is not a recognized type');
}
- transformers.forEach(transformer => {
- dom = transformer(dom);
- });
+ for (const transformer of transformers) {
+ dom = await transformer(dom);
+ }
return dom;
};
diff --git a/plugins/techdocs/src/test-utils/shadowDom.ts b/plugins/techdocs/src/test-utils/shadowDom.ts
index 61055a87bd..f8289bf6d9 100644
--- a/plugins/techdocs/src/test-utils/shadowDom.ts
+++ b/plugins/techdocs/src/test-utils/shadowDom.ts
@@ -22,13 +22,13 @@ export type CreateTestShadowDomOptions = {
postTransformers: Transformer[];
};
-export const createTestShadowDom = (
+export const createTestShadowDom = async (
fixture: string,
opts: CreateTestShadowDomOptions = {
preTransformers: [],
postTransformers: [],
},
-): ShadowRoot => {
+): Promise => {
const divElement = document.createElement('div');
divElement.attachShadow({ mode: 'open' });
document.body.appendChild(divElement);
@@ -39,7 +39,7 @@ export const createTestShadowDom = (
'text/html',
).documentElement;
if (opts.preTransformers) {
- dom = transformer(dom, opts.preTransformers);
+ dom = await transformer(dom, opts.preTransformers);
}
// Mount the UI
@@ -47,7 +47,7 @@ export const createTestShadowDom = (
// Transformers after the UI is rendered
if (opts.postTransformers) {
- transformer(dom, opts.postTransformers);
+ await transformer(dom, opts.postTransformers);
}
return divElement.shadowRoot!;