Skip to content

Commit

Permalink
fix(runtime): mock querySelectorAll (#6175)
Browse files Browse the repository at this point in the history
* fix(runtime): mock querySelectorAll

* add test

* prettier

* remove doc from runtime

* resolve buil error

* fix unit test
  • Loading branch information
christian-bromann authored Feb 25, 2025
1 parent 8d596b0 commit 7a3e150
Show file tree
Hide file tree
Showing 16 changed files with 99 additions and 58 deletions.
5 changes: 3 additions & 2 deletions src/client/client-patch-browser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BUILD, NAMESPACE } from '@app-data';
import { consoleDevInfo, doc, H, promiseResolve } from '@platform';
import { consoleDevInfo, H, promiseResolve, win } from '@platform';

import type * as d from '../declarations';

Expand All @@ -15,7 +15,8 @@ export const patchBrowser = (): Promise<d.CustomElementsDefineOptions> => {
}

const scriptElm = BUILD.scriptDataOpts
? Array.from(doc.querySelectorAll('script')).find(
? win.document &&
Array.from(win.document.querySelectorAll('script')).find(
(s) =>
new RegExp(`\/${NAMESPACE}(\\.esm)?\\.js($|\\?|#)`).test(s.src) ||
s.getAttribute('data-stencil-namespace') === NAMESPACE,
Expand Down
8 changes: 5 additions & 3 deletions src/client/client-window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { BUILD } from '@app-data';

import type * as d from '../declarations';

export const win = typeof window !== 'undefined' ? window : ({} as Window);
interface StencilWindow extends Omit<Window, 'document'> {
document?: Document;
}

export const doc = win.document || ({ head: {} } as Document);
export const win = (typeof window !== 'undefined' ? window : ({} as StencilWindow)) as StencilWindow;

export const H = ((win as any).HTMLElement || (class {} as any)) as HTMLElement;

Expand Down Expand Up @@ -33,7 +35,7 @@ export const supportsShadow = BUILD.shadowDom;
export const supportsListenerOptions = /*@__PURE__*/ (() => {
let supportsListenerOptions = false;
try {
doc.addEventListener(
win.document?.addEventListener(
'e',
null,
Object.defineProperty({}, 'passive', {
Expand Down
12 changes: 6 additions & 6 deletions src/compiler/docs/style-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,22 +68,22 @@ function parseCssComment(styleDocs: d.StyleDoc[], comment: string, mode: string
const docs = comment.split(CSS_PROP_ANNOTATION);

docs.forEach((d) => {
const doc = d.trim();
const cssDocument = d.trim();

if (!doc.startsWith(`--`)) {
if (!cssDocument.startsWith(`--`)) {
return;
}

const splt = doc.split(`:`);
const cssDoc: d.StyleDoc = {
const splt = cssDocument.split(`:`);
const styleDoc: d.StyleDoc = {
name: splt[0].trim(),
docs: (splt.shift() && splt.join(`:`)).trim(),
annotation: 'prop',
mode,
};

if (!styleDocs.some((c) => c.name === cssDoc.name && c.annotation === 'prop')) {
styleDocs.push(cssDoc);
if (!styleDocs.some((c) => c.name === styleDoc.name && c.annotation === 'prop')) {
styleDocs.push(styleDoc);
}
});
}
Expand Down
4 changes: 2 additions & 2 deletions src/hydrate/platform/hydrate-app.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { globalScripts } from '@app-globals';
import { addHostEventListeners, doc, getHostRef, loadModule, plt, registerHost } from '@platform';
import { addHostEventListeners, getHostRef, loadModule, plt, registerHost } from '@platform';
import { connectedCallback, insertVdomAnnotations } from '@runtime';
import { CMP_FLAGS } from '@utils';

Expand Down Expand Up @@ -169,7 +169,7 @@ export function hydrateApp(
// ensure we use NodeJS's native setTimeout, not the mocked hydrate app scoped one
tmrId = globalThis.setTimeout(timeoutExceeded, opts.timeout);

plt.$resourcesUrl$ = new URL(opts.resourcesUrl || './', doc.baseURI).href;
plt.$resourcesUrl$ = new URL(opts.resourcesUrl || './', win.document.baseURI).href;

globalScripts();

Expand Down
2 changes: 0 additions & 2 deletions src/hydrate/platform/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,6 @@ export const registerComponents = (Cstrs: d.ComponentNativeConstructor[]) => {

export const win = window;

export const doc = win.document;

export const readTask = (cb: Function) => {
nextTick(() => {
try {
Expand Down
15 changes: 10 additions & 5 deletions src/runtime/bootstrap-lazy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BUILD } from '@app-data';
import { doc, getHostRef, plt, registerHost, supportsShadow, win } from '@platform';
import { getHostRef, plt, registerHost, supportsShadow, win } from '@platform';
import { addHostEventListeners } from '@runtime';
import { CMP_FLAGS, queryNonceMetaTagContent } from '@utils';

Expand Down Expand Up @@ -27,19 +27,24 @@ export const bootstrapLazy = (lazyBundles: d.LazyBundlesRuntimeData, options: d.
}
installDevTools();

if (!win.document) {
console.warn('Stencil: No document found. Skipping bootstrapping lazy components.');
return;
}

const endBootstrap = createTime('bootstrapLazy');
const cmpTags: string[] = [];
const exclude = options.exclude || [];
const customElements = win.customElements;
const head = doc.head;
const head = win.document.head;
const metaCharset = /*@__PURE__*/ head.querySelector('meta[charset]');
const dataStyles = /*@__PURE__*/ doc.createElement('style');
const dataStyles = /*@__PURE__*/ win.document.createElement('style');
const deferredConnectedCallbacks: { connectedCallback: () => void }[] = [];
let appLoadFallback: any;
let isBootstrapping = true;

Object.assign(plt, options);
plt.$resourcesUrl$ = new URL(options.resourcesUrl || './', doc.baseURI).href;
plt.$resourcesUrl$ = new URL(options.resourcesUrl || './', win.document.baseURI).href;
if (BUILD.asyncQueue) {
if (options.syncQueue) {
plt.$flags$ |= PLATFORM_FLAGS.queueSync;
Expand Down Expand Up @@ -259,7 +264,7 @@ export const bootstrapLazy = (lazyBundles: d.LazyBundlesRuntimeData, options: d.
dataStyles.setAttribute('data-styles', '');

// Apply CSP nonce to the style tag if it exists
const nonce = plt.$nonce$ ?? queryNonceMetaTagContent(doc);
const nonce = plt.$nonce$ ?? queryNonceMetaTagContent(win.document);
if (nonce != null) {
dataStyles.setAttribute('nonce', nonce);
}
Expand Down
10 changes: 5 additions & 5 deletions src/runtime/client-hydrate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BUILD } from '@app-data';
import { doc, plt } from '@platform';
import { plt, win } from '@platform';
import { CMP_FLAGS } from '@utils';

import type * as d from '../declarations';
Expand Down Expand Up @@ -64,10 +64,10 @@ export const initializeClientHydrate = (
}
}

if (!plt.$orgLocNodes$ || !plt.$orgLocNodes$.size) {
if (win.document && (!plt.$orgLocNodes$ || !plt.$orgLocNodes$.size)) {
// This is the first pass over of this whole document;
// does a scrape to construct a 'bare-bones' tree of what elements we have and where content has been moved from
initializeDocumentHydrate(doc.body, (plt.$orgLocNodes$ = new Map()));
initializeDocumentHydrate(win.document.body, (plt.$orgLocNodes$ = new Map()));
}

hostElm[HYDRATE_ID] = hostId;
Expand Down Expand Up @@ -578,11 +578,11 @@ function addSlot(
// Important because where it is now in the constructed SSR markup might be different to where to *should* be
const parentNodeId = parentVNode?.$elm$ ? parentVNode.$elm$['s-id'] || parentVNode.$elm$.getAttribute('s-id') : '';

if (BUILD.shadowDom && shadowRootNodes) {
if (BUILD.shadowDom && shadowRootNodes && win.document) {
/* SHADOW */

// Browser supports shadowRoot and this is a shadow dom component; create an actual slot element
const slot = (childVNode.$elm$ = doc.createElement(childVNode.$tag$ as string) as d.RenderNode);
const slot = (childVNode.$elm$ = win.document.createElement(childVNode.$tag$ as string) as d.RenderNode);

if (childVNode.$name$) {
// Add the slot name attribute
Expand Down
8 changes: 6 additions & 2 deletions src/runtime/connected-callback.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BUILD } from '@app-data';
import { addHostEventListeners, doc, getHostRef, nextTick, plt, supportsShadow } from '@platform';
import { addHostEventListeners, getHostRef, nextTick, plt, supportsShadow, win } from '@platform';
import { CMP_FLAGS, HOST_FLAGS, MEMBER_FLAGS } from '@utils';

import type * as d from '../declarations';
Expand Down Expand Up @@ -124,13 +124,17 @@ export const connectedCallback = (elm: d.HostElement) => {
};

const setContentReference = (elm: d.HostElement) => {
if (!win.document) {
return;
}

// only required when we're NOT using native shadow dom (slot)
// or this browser doesn't support native shadow dom
// and this host element was NOT created with SSR
// let's pick out the inner content for slot projection
// create a node to represent where the original
// content was first placed, which is useful later on
const contentRefElm = (elm['s-cr'] = doc.createComment(
const contentRefElm = (elm['s-cr'] = win.document.createComment(
BUILD.isDebug ? `content-ref (host=${elm.localName})` : '',
) as any);
contentRefElm['s-cn'] = true;
Expand Down
24 changes: 16 additions & 8 deletions src/runtime/host-listener.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BUILD } from '@app-data';
import { consoleError, doc, plt, supportsListenerOptions, win } from '@platform';
import { consoleError, plt, supportsListenerOptions, win } from '@platform';
import { HOST_FLAGS, LISTENER_FLAGS } from '@utils';

import type * as d from '../declarations';
Expand All @@ -10,7 +10,7 @@ export const addHostEventListeners = (
listeners?: d.ComponentRuntimeHostListener[],
attachParentListeners?: boolean,
) => {
if (BUILD.hostListener && listeners) {
if (BUILD.hostListener && listeners && win.document) {
// this is called immediately within the element's constructor
// initialize our event listeners on the host element
// we do this now so that we can listen to events that may
Expand All @@ -32,7 +32,7 @@ export const addHostEventListeners = (
}

listeners.map(([flags, name, method]) => {
const target = BUILD.hostListenerTarget ? getHostListenerTarget(elm, flags) : elm;
const target = BUILD.hostListenerTarget ? getHostListenerTarget(win.document, elm, flags) : elm;
const handler = hostListenerProxy(hostRef, method);
const opts = hostListenerOpts(flags);
plt.ael(target, name, handler, opts);
Expand All @@ -58,12 +58,20 @@ const hostListenerProxy = (hostRef: d.HostRef, methodName: string) => (ev: Event
}
};

const getHostListenerTarget = (elm: Element, flags: number): EventTarget => {
if (BUILD.hostListenerTargetDocument && flags & LISTENER_FLAGS.TargetDocument) return doc;
if (BUILD.hostListenerTargetWindow && flags & LISTENER_FLAGS.TargetWindow) return win;
if (BUILD.hostListenerTargetBody && flags & LISTENER_FLAGS.TargetBody) return doc.body;
if (BUILD.hostListenerTargetParent && flags & LISTENER_FLAGS.TargetParent && elm.parentElement)
const getHostListenerTarget = (doc: Document, elm: Element, flags: number): EventTarget => {
if (BUILD.hostListenerTargetDocument && flags & LISTENER_FLAGS.TargetDocument) {
return doc;
}
if (BUILD.hostListenerTargetWindow && flags & LISTENER_FLAGS.TargetWindow) {
return win;
}
if (BUILD.hostListenerTargetBody && flags & LISTENER_FLAGS.TargetBody) {
return doc.body;
}
if (BUILD.hostListenerTargetParent && flags & LISTENER_FLAGS.TargetParent && elm.parentElement) {
return elm.parentElement;
}

return elm;
};

Expand Down
17 changes: 11 additions & 6 deletions src/runtime/styles.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BUILD } from '@app-data';
import { doc, plt, styles, supportsConstructableStylesheets, supportsShadow } from '@platform';
import { plt, styles, supportsConstructableStylesheets, supportsShadow, win } from '@platform';
import { CMP_FLAGS, queryNonceMetaTagContent } from '@utils';

import type * as d from '../declarations';
Expand Down Expand Up @@ -51,12 +51,12 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet
const scopeId = getScopeId(cmpMeta, mode);
const style = styles.get(scopeId);

if (!BUILD.attachStyles) {
if (!BUILD.attachStyles || !win.document) {
return scopeId;
}
// if an element is NOT connected then getRootNode() will return the wrong root node
// so the fallback is to always use the document for the root node in those cases
styleContainerNode = styleContainerNode.nodeType === NODE_TYPE.DocumentFragment ? styleContainerNode : doc;
styleContainerNode = styleContainerNode.nodeType === NODE_TYPE.DocumentFragment ? styleContainerNode : win.document;

if (style) {
if (typeof style === 'string') {
Expand All @@ -75,11 +75,12 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet
// This is only happening on native shadow-dom, do not needs CSS var shim
styleElm.innerHTML = style;
} else {
styleElm = document.querySelector(`[${HYDRATED_STYLE_ID}="${scopeId}"]`) || doc.createElement('style');
styleElm =
document.querySelector(`[${HYDRATED_STYLE_ID}="${scopeId}"]`) || win.document.createElement('style');
styleElm.innerHTML = style;

// Apply CSP nonce to the style tag if it exists
const nonce = plt.$nonce$ ?? queryNonceMetaTagContent(doc);
const nonce = plt.$nonce$ ?? queryNonceMetaTagContent(win.document);
if (nonce != null) {
styleElm.setAttribute('nonce', nonce);
}
Expand Down Expand Up @@ -247,7 +248,11 @@ export const convertScopedToShadow = (css: string) => css.replace(/\/\*!@([^\/]+
* and add them to a constructable stylesheet.
*/
export const hydrateScopedToShadow = () => {
const styles = doc.querySelectorAll(`[${HYDRATED_STYLE_ID}]`);
if (!win.document) {
return;
}

const styles = win.document.querySelectorAll(`[${HYDRATED_STYLE_ID}]`);
let i = 0;
for (; i < styles.length; i++) {
registerStyle(styles[i].getAttribute(HYDRATED_STYLE_ID), convertScopedToShadow(styles[i].innerHTML), true);
Expand Down
6 changes: 3 additions & 3 deletions src/runtime/test/bootstrap-lazy.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { doc } from '@platform';
import { win } from '@platform';

import { LazyBundlesRuntimeData } from '../../internal';
import { bootstrapLazy } from '../bootstrap-lazy';

describe('bootstrap lazy', () => {
it('should not inject invalid CSS when no lazy bundles are provided', () => {
const spy = jest.spyOn(doc.head, 'insertBefore');
const spy = jest.spyOn(win.document.head, 'insertBefore');

bootstrapLazy([]);

Expand All @@ -25,7 +25,7 @@ describe('bootstrap lazy', () => {
});

it('should not inject invalid CSS when components are already in custom element registry', () => {
const spy = jest.spyOn(doc.head, 'insertBefore');
const spy = jest.spyOn(win.document.head, 'insertBefore');

const lazyBundles: LazyBundlesRuntimeData = [
['my-component', [[0, 'my-component', { first: [1], middle: [1], last: [1] }]]],
Expand Down
Loading

0 comments on commit 7a3e150

Please sign in to comment.