Skip to content

Commit 7a3e150

Browse files
authoredFeb 25, 2025
fix(runtime): mock querySelectorAll (#6175)
* fix(runtime): mock querySelectorAll * add test * prettier * remove doc from runtime * resolve buil error * fix unit test
1 parent 8d596b0 commit 7a3e150

16 files changed

+99
-58
lines changed
 

‎src/client/client-patch-browser.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { BUILD, NAMESPACE } from '@app-data';
2-
import { consoleDevInfo, doc, H, promiseResolve } from '@platform';
2+
import { consoleDevInfo, H, promiseResolve, win } from '@platform';
33

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

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

1717
const scriptElm = BUILD.scriptDataOpts
18-
? Array.from(doc.querySelectorAll('script')).find(
18+
? win.document &&
19+
Array.from(win.document.querySelectorAll('script')).find(
1920
(s) =>
2021
new RegExp(`\/${NAMESPACE}(\\.esm)?\\.js($|\\?|#)`).test(s.src) ||
2122
s.getAttribute('data-stencil-namespace') === NAMESPACE,

‎src/client/client-window.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import { BUILD } from '@app-data';
22

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

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

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

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

@@ -33,7 +35,7 @@ export const supportsShadow = BUILD.shadowDom;
3335
export const supportsListenerOptions = /*@__PURE__*/ (() => {
3436
let supportsListenerOptions = false;
3537
try {
36-
doc.addEventListener(
38+
win.document?.addEventListener(
3739
'e',
3840
null,
3941
Object.defineProperty({}, 'passive', {

‎src/compiler/docs/style-docs.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -68,22 +68,22 @@ function parseCssComment(styleDocs: d.StyleDoc[], comment: string, mode: string
6868
const docs = comment.split(CSS_PROP_ANNOTATION);
6969

7070
docs.forEach((d) => {
71-
const doc = d.trim();
71+
const cssDocument = d.trim();
7272

73-
if (!doc.startsWith(`--`)) {
73+
if (!cssDocument.startsWith(`--`)) {
7474
return;
7575
}
7676

77-
const splt = doc.split(`:`);
78-
const cssDoc: d.StyleDoc = {
77+
const splt = cssDocument.split(`:`);
78+
const styleDoc: d.StyleDoc = {
7979
name: splt[0].trim(),
8080
docs: (splt.shift() && splt.join(`:`)).trim(),
8181
annotation: 'prop',
8282
mode,
8383
};
8484

85-
if (!styleDocs.some((c) => c.name === cssDoc.name && c.annotation === 'prop')) {
86-
styleDocs.push(cssDoc);
85+
if (!styleDocs.some((c) => c.name === styleDoc.name && c.annotation === 'prop')) {
86+
styleDocs.push(styleDoc);
8787
}
8888
});
8989
}

‎src/hydrate/platform/hydrate-app.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { globalScripts } from '@app-globals';
2-
import { addHostEventListeners, doc, getHostRef, loadModule, plt, registerHost } from '@platform';
2+
import { addHostEventListeners, getHostRef, loadModule, plt, registerHost } from '@platform';
33
import { connectedCallback, insertVdomAnnotations } from '@runtime';
44
import { CMP_FLAGS } from '@utils';
55

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

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

174174
globalScripts();
175175

‎src/hydrate/platform/index.ts

-2
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,6 @@ export const registerComponents = (Cstrs: d.ComponentNativeConstructor[]) => {
5454

5555
export const win = window;
5656

57-
export const doc = win.document;
58-
5957
export const readTask = (cb: Function) => {
6058
nextTick(() => {
6159
try {

‎src/runtime/bootstrap-lazy.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { BUILD } from '@app-data';
2-
import { doc, getHostRef, plt, registerHost, supportsShadow, win } from '@platform';
2+
import { getHostRef, plt, registerHost, supportsShadow, win } from '@platform';
33
import { addHostEventListeners } from '@runtime';
44
import { CMP_FLAGS, queryNonceMetaTagContent } from '@utils';
55

@@ -27,19 +27,24 @@ export const bootstrapLazy = (lazyBundles: d.LazyBundlesRuntimeData, options: d.
2727
}
2828
installDevTools();
2929

30+
if (!win.document) {
31+
console.warn('Stencil: No document found. Skipping bootstrapping lazy components.');
32+
return;
33+
}
34+
3035
const endBootstrap = createTime('bootstrapLazy');
3136
const cmpTags: string[] = [];
3237
const exclude = options.exclude || [];
3338
const customElements = win.customElements;
34-
const head = doc.head;
39+
const head = win.document.head;
3540
const metaCharset = /*@__PURE__*/ head.querySelector('meta[charset]');
36-
const dataStyles = /*@__PURE__*/ doc.createElement('style');
41+
const dataStyles = /*@__PURE__*/ win.document.createElement('style');
3742
const deferredConnectedCallbacks: { connectedCallback: () => void }[] = [];
3843
let appLoadFallback: any;
3944
let isBootstrapping = true;
4045

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

261266
// Apply CSP nonce to the style tag if it exists
262-
const nonce = plt.$nonce$ ?? queryNonceMetaTagContent(doc);
267+
const nonce = plt.$nonce$ ?? queryNonceMetaTagContent(win.document);
263268
if (nonce != null) {
264269
dataStyles.setAttribute('nonce', nonce);
265270
}

‎src/runtime/client-hydrate.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { BUILD } from '@app-data';
2-
import { doc, plt } from '@platform';
2+
import { plt, win } from '@platform';
33
import { CMP_FLAGS } from '@utils';
44

55
import type * as d from '../declarations';
@@ -64,10 +64,10 @@ export const initializeClientHydrate = (
6464
}
6565
}
6666

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

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

581-
if (BUILD.shadowDom && shadowRootNodes) {
581+
if (BUILD.shadowDom && shadowRootNodes && win.document) {
582582
/* SHADOW */
583583

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

587587
if (childVNode.$name$) {
588588
// Add the slot name attribute

‎src/runtime/connected-callback.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { BUILD } from '@app-data';
2-
import { addHostEventListeners, doc, getHostRef, nextTick, plt, supportsShadow } from '@platform';
2+
import { addHostEventListeners, getHostRef, nextTick, plt, supportsShadow, win } from '@platform';
33
import { CMP_FLAGS, HOST_FLAGS, MEMBER_FLAGS } from '@utils';
44

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

126126
const setContentReference = (elm: d.HostElement) => {
127+
if (!win.document) {
128+
return;
129+
}
130+
127131
// only required when we're NOT using native shadow dom (slot)
128132
// or this browser doesn't support native shadow dom
129133
// and this host element was NOT created with SSR
130134
// let's pick out the inner content for slot projection
131135
// create a node to represent where the original
132136
// content was first placed, which is useful later on
133-
const contentRefElm = (elm['s-cr'] = doc.createComment(
137+
const contentRefElm = (elm['s-cr'] = win.document.createComment(
134138
BUILD.isDebug ? `content-ref (host=${elm.localName})` : '',
135139
) as any);
136140
contentRefElm['s-cn'] = true;

‎src/runtime/host-listener.ts

+16-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { BUILD } from '@app-data';
2-
import { consoleError, doc, plt, supportsListenerOptions, win } from '@platform';
2+
import { consoleError, plt, supportsListenerOptions, win } from '@platform';
33
import { HOST_FLAGS, LISTENER_FLAGS } from '@utils';
44

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

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

61-
const getHostListenerTarget = (elm: Element, flags: number): EventTarget => {
62-
if (BUILD.hostListenerTargetDocument && flags & LISTENER_FLAGS.TargetDocument) return doc;
63-
if (BUILD.hostListenerTargetWindow && flags & LISTENER_FLAGS.TargetWindow) return win;
64-
if (BUILD.hostListenerTargetBody && flags & LISTENER_FLAGS.TargetBody) return doc.body;
65-
if (BUILD.hostListenerTargetParent && flags & LISTENER_FLAGS.TargetParent && elm.parentElement)
61+
const getHostListenerTarget = (doc: Document, elm: Element, flags: number): EventTarget => {
62+
if (BUILD.hostListenerTargetDocument && flags & LISTENER_FLAGS.TargetDocument) {
63+
return doc;
64+
}
65+
if (BUILD.hostListenerTargetWindow && flags & LISTENER_FLAGS.TargetWindow) {
66+
return win;
67+
}
68+
if (BUILD.hostListenerTargetBody && flags & LISTENER_FLAGS.TargetBody) {
69+
return doc.body;
70+
}
71+
if (BUILD.hostListenerTargetParent && flags & LISTENER_FLAGS.TargetParent && elm.parentElement) {
6672
return elm.parentElement;
73+
}
74+
6775
return elm;
6876
};
6977

‎src/runtime/styles.ts

+11-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { BUILD } from '@app-data';
2-
import { doc, plt, styles, supportsConstructableStylesheets, supportsShadow } from '@platform';
2+
import { plt, styles, supportsConstructableStylesheets, supportsShadow, win } from '@platform';
33
import { CMP_FLAGS, queryNonceMetaTagContent } from '@utils';
44

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

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

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

8182
// Apply CSP nonce to the style tag if it exists
82-
const nonce = plt.$nonce$ ?? queryNonceMetaTagContent(doc);
83+
const nonce = plt.$nonce$ ?? queryNonceMetaTagContent(win.document);
8384
if (nonce != null) {
8485
styleElm.setAttribute('nonce', nonce);
8586
}
@@ -247,7 +248,11 @@ export const convertScopedToShadow = (css: string) => css.replace(/\/\*!@([^\/]+
247248
* and add them to a constructable stylesheet.
248249
*/
249250
export const hydrateScopedToShadow = () => {
250-
const styles = doc.querySelectorAll(`[${HYDRATED_STYLE_ID}]`);
251+
if (!win.document) {
252+
return;
253+
}
254+
255+
const styles = win.document.querySelectorAll(`[${HYDRATED_STYLE_ID}]`);
251256
let i = 0;
252257
for (; i < styles.length; i++) {
253258
registerStyle(styles[i].getAttribute(HYDRATED_STYLE_ID), convertScopedToShadow(styles[i].innerHTML), true);

‎src/runtime/test/bootstrap-lazy.spec.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { doc } from '@platform';
1+
import { win } from '@platform';
22

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

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

1010
bootstrapLazy([]);
1111

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

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

3030
const lazyBundles: LazyBundlesRuntimeData = [
3131
['my-component', [[0, 'my-component', { first: [1], middle: [1], last: [1] }]]],

0 commit comments

Comments
 (0)
Failed to load comments.