diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index 42be2732d0..1975d350e1 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -27,6 +27,7 @@ import transformer, { addLinkClickListener, removeMkdocsHeader, modifyCss, + onCssReady, } from '../transformers'; import { docStorageURL } from '../../config'; import URLFormatter from '../urlFormatter'; @@ -133,16 +134,25 @@ export const Reader = () => { shadowRoot?.querySelector(parsedUrl.hash)?.scrollIntoView(); }, }), + onCssReady({ + docStorageURL, + onLoading: (dom: Element) => { + (dom as HTMLElement).style.setProperty('opacity', '0'); + }, + onLoaded: (dom: Element) => { + (dom as HTMLElement).style.removeProperty('opacity'); + }, + }), ]); }, [componentId, path, shadowRoot, state]); // eslint-disable-line react-hooks/exhaustive-deps - if (state.value instanceof Error) return ; + if (state.value instanceof Error) { + return ; + } return ( - <> - -
- - + +
+ ); }; diff --git a/plugins/techdocs/src/reader/transformers/index.ts b/plugins/techdocs/src/reader/transformers/index.ts index f35d6aa478..4e12dcae48 100644 --- a/plugins/techdocs/src/reader/transformers/index.ts +++ b/plugins/techdocs/src/reader/transformers/index.ts @@ -19,6 +19,7 @@ export * from './rewriteDocLinks'; export * from './addLinkClickListener'; export * from './removeMkdocsHeader'; export * from './modifyCss'; +export * from './onCssReady'; export type Transformer = (dom: Element) => Element; diff --git a/plugins/techdocs/src/reader/transformers/onCssReady.test.ts b/plugins/techdocs/src/reader/transformers/onCssReady.test.ts new file mode 100644 index 0000000000..10edbbae5e --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/onCssReady.test.ts @@ -0,0 +1,87 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + FIXTURES, + createTestShadowDom, + mockStylesheetEventListener, + executeStylesheetEventListeners, + clearStylesheetEventListeners, +} from '../../test-utils'; +import { addBaseUrl, onCssReady } from '../transformers'; + +const docStorageURL: string = + 'https://techdocs-mock-sites.storage.googleapis.com'; + +jest.useFakeTimers(); + +describe('onCssReady', () => { + beforeEach(() => { + mockStylesheetEventListener(100); + }); + + afterEach(() => { + clearStylesheetEventListeners(); + }); + + it('does not call onLoading and onLoaded without the addBaseUrl transformer', () => { + const onLoading = jest.fn(); + const onLoaded = jest.fn(); + + createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, { + transformers: [ + onCssReady({ + docStorageURL, + onLoading, + onLoaded, + }), + ], + }); + + expect(onLoading).not.toHaveBeenCalled(); + executeStylesheetEventListeners(); + expect(onLoaded).not.toHaveBeenCalled(); + }); + + it('calls the onLoading and onLoaded correctly', () => { + const onLoading = jest.fn(); + const onLoaded = jest.fn(); + + createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, { + transformers: [ + addBaseUrl({ + docStorageURL, + componentId: 'mkdocs', + path: '', + }), + onCssReady({ + docStorageURL, + onLoading, + onLoaded, + }), + ], + }); + + expect(onLoading).toHaveBeenCalledTimes(1); + expect(onLoading).toHaveBeenCalledWith(expect.any(Element)); + expect(onLoaded).not.toHaveBeenCalled(); + + executeStylesheetEventListeners(); + + expect(onLoaded).toHaveBeenCalledTimes(1); + expect(onLoaded).toHaveBeenCalledWith(expect.any(Element)); + }); +}); diff --git a/plugins/techdocs/src/reader/transformers/onCssReady.ts b/plugins/techdocs/src/reader/transformers/onCssReady.ts new file mode 100644 index 0000000000..2d355574b6 --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/onCssReady.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Transformer } from './index'; + +type OnCssReadyOptions = { + docStorageURL: string; + onLoading: (dom: Element) => void; + onLoaded: (dom: Element) => void; +}; + +export const onCssReady = ({ + docStorageURL, + onLoading, + onLoaded, +}: OnCssReadyOptions): Transformer => { + return dom => { + const cssPages = Array.from( + dom.querySelectorAll('head > link[rel="stylesheet"]'), + ).filter(elem => elem.getAttribute('href')?.startsWith(docStorageURL)); + + let count = cssPages.length; + + if (count > 0) { + onLoading(dom); + } + + cssPages.forEach(cssPage => + cssPage.addEventListener('load', () => { + count -= 1; + + if (count === 0) { + onLoaded(dom); + } + }), + ); + + return dom; + }; +}; diff --git a/plugins/techdocs/src/test-utils/index.ts b/plugins/techdocs/src/test-utils/index.ts index f9ced5f930..d782f225d5 100644 --- a/plugins/techdocs/src/test-utils/index.ts +++ b/plugins/techdocs/src/test-utils/index.ts @@ -15,49 +15,10 @@ */ import FIXTURE_STANDARD_PAGE from './fixtures/mkdocs-index'; -import transformer from '../reader/transformers'; -import type { Transformer } from '../reader/transformers'; export const FIXTURES = { FIXTURE_STANDARD_PAGE, }; -export type CreateTestShadowDomOptions = { - transformers: Transformer[]; -}; - -export const createTestShadowDom = ( - fixture: string, - opts: CreateTestShadowDomOptions = { transformers: [] }, -): ShadowRoot => { - const divElement = document.createElement('div'); - divElement.attachShadow({ mode: 'open' }); - document.body.appendChild(divElement); - - const domParser = new DOMParser().parseFromString(fixture, 'text/html'); - divElement.shadowRoot?.appendChild(domParser.documentElement); - - if (opts.transformers) { - transformer(divElement.shadowRoot!.children[0], opts.transformers); - } - - return divElement.shadowRoot!; -}; - -export const getSample = ( - shadowDom: ShadowRoot, - elementName: string, - elementAttribute: string, - sampleSize = 2, -) => { - const rootElement = shadowDom.children[0]; - - return Array.from(rootElement.getElementsByTagName(elementName)) - .filter(elem => { - return elem.hasAttribute(elementAttribute); - }) - .slice(0, sampleSize) - .map(elem => { - return elem.getAttribute(elementAttribute); - }); -}; +export * from './stylesheets'; +export * from './shadowDom'; diff --git a/plugins/techdocs/src/test-utils/shadowDom.ts b/plugins/techdocs/src/test-utils/shadowDom.ts new file mode 100644 index 0000000000..ded6b6a860 --- /dev/null +++ b/plugins/techdocs/src/test-utils/shadowDom.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import transformer from '../reader/transformers'; +import type { Transformer } from '../reader/transformers'; + +export type CreateTestShadowDomOptions = { + transformers: Transformer[]; +}; + +export const createTestShadowDom = ( + fixture: string, + opts: CreateTestShadowDomOptions = { transformers: [] }, +): ShadowRoot => { + const divElement = document.createElement('div'); + divElement.attachShadow({ mode: 'open' }); + document.body.appendChild(divElement); + + const domParser = new DOMParser().parseFromString(fixture, 'text/html'); + divElement.shadowRoot?.appendChild(domParser.documentElement); + + if (opts.transformers) { + transformer(divElement.shadowRoot!.children[0], opts.transformers); + } + + return divElement.shadowRoot!; +}; + +export const getSample = ( + shadowDom: ShadowRoot, + elementName: string, + elementAttribute: string, + sampleSize = 2, +) => { + const rootElement = shadowDom.children[0]; + + return Array.from(rootElement.getElementsByTagName(elementName)) + .filter(elem => { + return elem.hasAttribute(elementAttribute); + }) + .slice(0, sampleSize) + .map(elem => { + return elem.getAttribute(elementAttribute); + }); +}; diff --git a/plugins/techdocs/src/test-utils/stylesheets.ts b/plugins/techdocs/src/test-utils/stylesheets.ts new file mode 100644 index 0000000000..0532c71272 --- /dev/null +++ b/plugins/techdocs/src/test-utils/stylesheets.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const mockStylesheetEventListener = (timeToCallbackMs: number): void => { + HTMLLinkElement.prototype.addEventListener = ( + _eventName: string, + eventCallback: any, + ) => { + setTimeout(() => { + eventCallback(); + }, timeToCallbackMs); + }; +}; + +export const executeStylesheetEventListeners = (): void => { + jest.runOnlyPendingTimers(); +}; + +export const clearStylesheetEventListeners = (): void => { + HTMLLinkElement.prototype.addEventListener = + Element.prototype.addEventListener; +};