Skip to content

Commit afaebf0

Browse files
committed
fix: Makes script-level var/function declarations reach the global scope
Removes the anonymous function wrapper around evaluated script code, so top-level var/function declarations attach to window (follows the spec). Adds a test covering the behavior and fixes existing tests that hardcoded the old wrapped source.
1 parent eac5a38 commit afaebf0

4 files changed

Lines changed: 75 additions & 49 deletions

File tree

packages/@happy-dom/server-renderer/test/ServerRendererBrowser.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,14 +419,14 @@ Timer #1
419419
headers: { key1: 'value' },
420420
outputFile: null,
421421
pageConsole: `Error: Error
422-
at https://example.com/gb/en/:1:59
422+
at https://example.com/gb/en/:1:26
423423
at Timeout._onTimeout (/window/BrowserWindow.ts:0:0)
424424
at listOnTimeout (node:internal/timers:0:0)
425425
at processTimers (node:internal/timers:0:0)
426426
`,
427427
pageErrors: [
428428
`Error: Error
429-
at https://example.com/gb/en/:1:59
429+
at https://example.com/gb/en/:1:26
430430
at Timeout._onTimeout (/window/BrowserWindow.ts:0:0)
431431
at listOnTimeout (node:internal/timers:0:0)
432432
at processTimers (node:internal/timers:0:0)`

packages/happy-dom/src/javascript/JavaScriptCompiler.ts

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export default class JavaScriptCompiler {
8181

8282
const regExp = new RegExp(STATEMENT_REGEXP);
8383
const count = this.count;
84-
let newCode = '(function anonymous($happy_dom) {';
84+
let newCode = '';
8585
let match: RegExpExecArray | null = null;
8686
let precedingToken: string;
8787
let textBetweenStatements: string;
@@ -143,29 +143,34 @@ export default class JavaScriptCompiler {
143143
newCode += '} catch (error) { $happy_dom.dispatchError(error); }';
144144
}
145145

146-
newCode += '})';
147-
148-
try {
149-
return {
150-
execute: this.window[PropertySymbol.evaluateScript](newCode, {
151-
filename: sourceURL
152-
})
153-
};
154-
} catch (error) {
155-
(<Error>error).message =
156-
`Failed to parse JavaScript in '${sourceURL}': ${(<Error>error).message}`;
157-
if (
158-
browserSettings.disableErrorCapturing ||
159-
browserSettings.errorCapture !== BrowserErrorCaptureEnum.tryAndCatch
160-
) {
161-
throw error;
162-
} else {
163-
this.window[PropertySymbol.dispatchError](<Error>error);
164-
return {
165-
execute: () => {}
166-
};
146+
return {
147+
execute: (happyDom): void => {
148+
// $happy_dom must be a global (not a function parameter), so top-level
149+
// var/function declarations in the script reach the real global scope
150+
// instead of being trapped in a wrapper function's scope.
151+
Object.defineProperty(this.window, '$happy_dom', {
152+
value: happyDom,
153+
enumerable: false,
154+
configurable: true
155+
});
156+
try {
157+
this.window[PropertySymbol.evaluateScript](newCode, {
158+
filename: sourceURL
159+
});
160+
} catch (error) {
161+
(<Error>error).message =
162+
`Failed to parse JavaScript in '${sourceURL}': ${(<Error>error).message}`;
163+
if (
164+
browserSettings.disableErrorCapturing ||
165+
browserSettings.errorCapture !== BrowserErrorCaptureEnum.tryAndCatch
166+
) {
167+
throw error;
168+
} else {
169+
this.window[PropertySymbol.dispatchError](<Error>error);
170+
}
171+
}
167172
}
168-
}
173+
};
169174
}
170175

171176
/**

packages/happy-dom/test/javascript/JavaScriptCompiler.test.ts

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { beforeEach, describe, it, expect } from 'vitest';
1+
import { beforeEach, describe, it, expect, vi } from 'vitest';
22
import JavaScriptCompiler from '../../src/javascript/JavaScriptCompiler.js';
33
import type BrowserWindow from '../../src/window/BrowserWindow.js';
44
import Window from '../../src/window/Window.js';
55
import BrowserErrorCaptureEnum from '../../src/browser/enums/BrowserErrorCaptureEnum.js';
6+
import * as PropertySymbol from '../../src/PropertySymbol.js';
67

78
describe('JavaScriptCompiler', () => {
89
let window: BrowserWindow;
@@ -22,7 +23,7 @@ describe('JavaScriptCompiler', () => {
2223
2324
class TestClass {
2425
constructor() {
25-
console.log('Hello \\\'World');
26+
console.log('Hello \\'World');
2627
}
2728
2829
async greet() {
@@ -34,13 +35,16 @@ describe('JavaScriptCompiler', () => {
3435
`;
3536
const compiler = new JavaScriptCompiler(window);
3637
const result = compiler.compile('http://localhost:8080/js/app/main.js', code);
38+
const evaluateScript = vi.spyOn(<any>window, PropertySymbol.evaluateScript);
3739

38-
expect(result.execute.toString()).toBe(`function anonymous($happy_dom) {
40+
result.execute({ dispatchError: () => {}, dynamicImport: vi.fn() });
41+
42+
expect(evaluateScript.mock.calls[0][0]).toBe(`
3943
const variable = 'hello';
4044
4145
class TestClass {
4246
constructor() {
43-
console.log('Hello \\\'World');
47+
console.log('Hello \\'World');
4448
}
4549
4650
async greet() {
@@ -49,12 +53,12 @@ describe('JavaScriptCompiler', () => {
4953
return someModule.getGreeting();
5054
}
5155
}
52-
}`);
56+
`);
5357
});
5458

5559
it('Handles import statement in strings.', () => {
5660
const code = `
57-
var r = new RegExp(/^([1-9][0-9]*)(["″”'′´]?)\s*([1-9][0-9]*\\/[1-9][0-9]*)["″”]?$/);
61+
var r = new RegExp(/^([1-9][0-9]*)(["″â€'′ô]?)\s*([1-9][0-9]*\\/[1-9][0-9]*)["″â€]?$/);
5862
const hexLookUp=Array.from({length:127},(n,e)=>/[^!"$&'()*+,\-.;=_\`a-z{}~]/u.test(String.fromCharCode(e)))
5963
class R{constructor(){this.lastTime=Date.now(),this.lastValue=0,this.__speed=0}set value(e){this.__speed=(e-this.lastValue)/(Date.now()-this.lastTime),this.lastValue=e,this.lastTime=Date.now()}}
6064
const n=["@import",\`url(\${JSON.stringify(t.href)}) import('@package/debugger')\`];const t="";
@@ -64,39 +68,43 @@ describe('JavaScriptCompiler', () => {
6468
`;
6569
const compiler = new JavaScriptCompiler(window);
6670
const result = compiler.compile('http://localhost:8080/js/app/main.js', code);
71+
const evaluateScript = vi
72+
.spyOn(<any>window, PropertySymbol.evaluateScript)
73+
.mockImplementation(() => {});
74+
75+
result.execute({ dispatchError: () => {}, dynamicImport: vi.fn() });
6776

68-
expect(result.execute.toString()).toBe(`function anonymous($happy_dom) {
69-
var r = new RegExp(/^([1-9][0-9]*)(["″”'′´]?)\s*([1-9][0-9]*\\/[1-9][0-9]*)["″”]?$/);
77+
expect(evaluateScript.mock.calls[0][0]).toBe(`
78+
var r = new RegExp(/^([1-9][0-9]*)(["″â€'′ô]?)\s*([1-9][0-9]*\\/[1-9][0-9]*)["″â€]?$/);
7079
const hexLookUp=Array.from({length:127},(n,e)=>/[^!"$&'()*+,-.;=_\`a-z{}~]/u.test(String.fromCharCode(e)))
7180
class R{constructor(){this.lastTime=Date.now(),this.lastValue=0,this.__speed=0}set value(e){this.__speed=(e-this.lastValue)/(Date.now()-this.lastTime),this.lastValue=e,this.lastTime=Date.now()}}
7281
const n=["@import",\`url(\${JSON.stringify(t.href)}) import('@package/debugger')\`];const t="";
7382
function log(){return console.log('To use the debugger you must import "@package/debugger"')}
7483
var i = "test";
7584
$happy_dom.dynamicImport("@package/debugger");
76-
}`);
85+
`);
7786
});
7887

7988
it('Adds try and catch statement if settings.errorCapture is set to "tryAndCatch".', () => {
8089
const window = new Window();
8190

82-
const code = `
83-
const variable = 'hello';
84-
console.log('Hello \\\'World');
85-
`;
91+
const code = `throw new Error('Hello World');`;
8692
const compiler = new JavaScriptCompiler(window);
8793
const result = compiler.compile('http://localhost:8080/js/app/main.js', code);
94+
const dispatchError = vi.fn();
8895

89-
expect(result.execute.toString()).toBe(`function anonymous($happy_dom) {try {
90-
const variable = 'hello';
91-
console.log('Hello \\'World');
92-
} catch (error) { $happy_dom.dispatchError(error); }}`);
96+
result.execute({ dispatchError, dynamicImport: vi.fn() });
97+
98+
expect(dispatchError).toHaveBeenCalledWith(new Error('Hello World'));
9399
});
94100

95101
it('Throws await in top level error', () => {
96102
const code = `const StringUtility = await import('http://localhost:8080/js/utilities/StringUtility.js');`;
97103
const compiler = new JavaScriptCompiler(window);
104+
const result = compiler.compile('http://localhost:8080/js/app/main.js', code);
105+
98106
expect(() => {
99-
compiler.compile('http://localhost:8080/js/app/main.js', code);
107+
result.execute({ dispatchError: () => {}, dynamicImport: vi.fn() });
100108
}).toThrow(
101109
`Failed to parse JavaScript in 'http://localhost:8080/js/app/main.js': await is only valid in async functions and the top level bodies of modules`
102110
);

packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,19 @@ describe('HTMLScriptElement', () => {
650650
expect((<any>window)['test']).toBe(undefined);
651651
});
652652

653+
it('Makes "var" and function declarations reach the global object, as according to spec.', () => {
654+
const element = document.createElement('script');
655+
element.text = `
656+
var test = 'test';
657+
function testFunction() {
658+
return 'testFunction';
659+
}
660+
`;
661+
document.body.appendChild(element);
662+
expect((<any>window)['test']).toBe('test');
663+
expect((<any>window)['testFunction']()).toBe('testFunction');
664+
});
665+
653666
it('Loads and evaluates an external script when "src" attribute has been set, but does not evaluate text content.', () => {
654667
const element = document.createElement('script');
655668

@@ -860,8 +873,8 @@ describe('HTMLScriptElement', () => {
860873
const consoleOutput = window.happyDOM?.virtualConsolePrinter.readAsString() || '';
861874
expect(
862875
consoleOutput.startsWith(`https://localhost:8080/base/path/to/script/:1
863-
(function anonymous($happy_dom) {try {globalThis.test = /;} catch (error) { $happy_dom.dispatchError(error); }})
864-
^
876+
try {globalThis.test = /;} catch (error) { $happy_dom.dispatchError(error); }
877+
^
865878
866879
SyntaxError: Invalid regular expression: missing /`)
867880
).toBe(true);
@@ -889,8 +902,8 @@ SyntaxError: Invalid regular expression: missing /`)
889902
const consoleOutput = window.happyDOM?.virtualConsolePrinter.readAsString() || '';
890903
expect(
891904
consoleOutput.startsWith(`https://localhost:8080/base/path/to/script/:1
892-
(function anonymous($happy_dom) {try {globalThis.test = /;} catch (error) { $happy_dom.dispatchError(error); }})
893-
^
905+
try {globalThis.test = /;} catch (error) { $happy_dom.dispatchError(error); }
906+
^
894907
895908
SyntaxError: Invalid regular expression: missing /`)
896909
).toBe(true);
@@ -913,8 +926,8 @@ SyntaxError: Invalid regular expression: missing /`)
913926
const consoleOutput = window.happyDOM?.virtualConsolePrinter.readAsString() || '';
914927
expect(
915928
consoleOutput.startsWith(`about:blank:1
916-
(function anonymous($happy_dom) {try {globalThis.test = /;} catch (error) { $happy_dom.dispatchError(error); }})
917-
^
929+
try {globalThis.test = /;} catch (error) { $happy_dom.dispatchError(error); }
930+
^
918931
919932
SyntaxError: Invalid regular expression: missing /`)
920933
).toBe(true);

0 commit comments

Comments
 (0)