diff --git a/.eslintrc.cjs b/.eslintrc.cjs
new file mode 100644
index 000000000..ad492446b
--- /dev/null
+++ b/.eslintrc.cjs
@@ -0,0 +1,186 @@
+const WARN = "warn";
+const ERROR = "error";
+const OFF = "off";
+
+const COMMON_CONFIG = {
+ plugins: ["jsdoc", "filenames", "import", "prettier"],
+ extends: ["prettier", "plugin:prettier/recommended"],
+ rules: {
+ "prettier/prettier": WARN,
+ "no-underscore-dangle": OFF,
+ "no-debugger": WARN,
+ "space-infix-ops": ERROR,
+ "no-console": WARN,
+ "wrap-iife": OFF,
+ "no-self-assign": ERROR,
+ "no-self-compare": ERROR,
+ "no-loop-func": OFF,
+ "array-callback-return": ERROR,
+ curly: ERROR,
+ "no-fallthrough": OFF,
+ "dot-notation": OFF,
+ "prefer-const": WARN,
+ "no-empty-function": OFF,
+ "no-with": ERROR,
+ "one-var": [ERROR, "never"],
+ camelcase: [WARN, { properties: "always", ignoreImports: true }],
+ "spaced-comment": [WARN, "always"],
+ "capitalized-comments": [WARN, "always", { ignorePattern: "prettier" }],
+ "no-useless-rename": WARN,
+ "jsdoc/check-alignment": WARN,
+ "jsdoc/check-examples": OFF,
+ "jsdoc/check-indentation": WARN,
+ "jsdoc/check-syntax": WARN,
+ "jsdoc/check-tag-names": WARN,
+ "jsdoc/check-types": WARN,
+ "jsdoc/implements-on-classes": WARN,
+ "jsdoc/match-description": OFF,
+ "jsdoc/newline-after-description": WARN,
+ "jsdoc/no-types": OFF,
+ "jsdoc/no-undefined-types": OFF,
+ "jsdoc/require-description": OFF,
+ "jsdoc/require-description-complete-sentence": OFF,
+ "jsdoc/require-example": OFF,
+ "jsdoc/require-hyphen-before-param-description": [WARN, "never"],
+ "jsdoc/require-param": WARN,
+ "jsdoc/require-param-name": WARN,
+ "jsdoc/require-returns-check": WARN,
+ "jsdoc/require-returns-description": WARN,
+ "jsdoc/valid-types": WARN,
+ "filenames/match-exported": WARN,
+ "no-useless-constructor": WARN,
+ "jsdoc/require-jsdoc": [
+ WARN,
+ {
+ require: {
+ ArrowFunctionExpression: false,
+ FunctionDeclaration: false,
+ FunctionExpression: false,
+ ClassDeclaration: true,
+ MethodDefinition: true,
+ },
+ },
+ ],
+ "import/no-extraneous-dependencies": WARN,
+ },
+};
+
+const TS_PARSER_FIELDS = {
+ parser: "@typescript-eslint/parser",
+ parserOptions: {
+ ecmaFeatures: {
+ jsx: true,
+ },
+ ecmaVersion: 2020,
+ sourceType: "module",
+ },
+};
+
+module.exports = {
+ env: {
+ es6: true,
+ browser: true,
+ node: true,
+ },
+ overrides: [
+ {
+ files: ["*.js", "*.jsx", "*.mjs"],
+ ...TS_PARSER_FIELDS,
+ plugins: COMMON_CONFIG.plugins,
+ extends: COMMON_CONFIG.extends,
+ rules: {
+ ...COMMON_CONFIG.rules,
+ "no-undef": ERROR,
+ "jsdoc/check-param-names": WARN,
+ "jsdoc/require-param-type": WARN,
+ "jsdoc/require-returns": WARN,
+ "jsdoc/require-param-description": WARN,
+ "jsdoc/require-returns-type": WARN,
+ "consistent-return": WARN,
+ },
+ },
+ {
+ files: ["*.json"],
+ plugins: ["json"],
+ extends: ["eslint:recommended", "plugin:json/recommended"],
+ },
+ {
+ files: ["*.ts", "*.tsx"],
+ plugins: [...COMMON_CONFIG.plugins, "@typescript-eslint"],
+ ...TS_PARSER_FIELDS,
+ extends: [
+ ...COMMON_CONFIG.extends,
+ "plugin:@typescript-eslint/recommended",
+ ],
+ rules: {
+ ...COMMON_CONFIG.rules,
+ "@typescript-eslint/explicit-member-accessibility": [
+ ERROR,
+ { overrides: { constructors: "no-public" } },
+ ],
+ "@typescript-eslint/no-unused-vars": OFF, // TSC is already doing this
+ "@typescript-eslint/ban-types": OFF, // TSC is already doing this
+ "no-undef": OFF, // TSC is already doing this
+ "@typescript-eslint/no-var-requires": OFF,
+ "@typescript-eslint/explicit-module-boundary-types": OFF, // TSC is already doing this
+ "@typescript-eslint/consistent-type-assertions": [
+ WARN,
+ { assertionStyle: "angle-bracket" },
+ ],
+ "@typescript-eslint/no-explicit-any": OFF,
+ "@typescript-eslint/no-empty-function": OFF,
+ "@typescript-eslint/no-use-before-define": OFF,
+ "@typescript-eslint/no-this-alias": OFF,
+ "@typescript-eslint/explicit-function-return-type": [
+ ERROR,
+ { allowExpressions: true },
+ ],
+ "@typescript-eslint/member-ordering": [
+ WARN,
+ {
+ default: [
+ "signature",
+ "field",
+ "constructor",
+ ["get", "set"],
+ "method",
+ ],
+ },
+ ],
+ "@typescript-eslint/ban-ts-comment": OFF,
+ "jsdoc/no-types": WARN,
+ "import/named": OFF,
+ "import/no-named-as-default": WARN,
+ "import/no-extraneous-dependencies": WARN,
+ "import/no-absolute-path": WARN,
+ "@typescript-eslint/naming-convention": [
+ WARN,
+ {
+ selector: "interface",
+ format: ["PascalCase"],
+ prefix: ["I"],
+ },
+ {
+ selector: "enum",
+ format: ["PascalCase"],
+ suffix: ["Enum"],
+ },
+ {
+ selector: "typeParameter",
+ format: ["PascalCase"],
+ },
+ {
+ selector: "memberLike",
+ modifiers: ["private"],
+ leadingUnderscore: "allow",
+ format: ["camelCase"],
+ },
+ {
+ selector: "class",
+ format: ["PascalCase"],
+ },
+ ],
+ },
+ },
+ ],
+};
diff --git a/.huskyrc.json b/.huskyrc.json
index 4375e705c..6aff645db 100644
--- a/.huskyrc.json
+++ b/.huskyrc.json
@@ -1,6 +1,6 @@
{
"hooks": {
- "pre-commit": "npm run lint:fix",
+ "pre-commit": "npm run lint:changed",
"commit-msg": "node ./bin/husky-commit-message.js --filepath=$HUSKY_GIT_PARAMS%HUSKY_GIT_PARAMS%"
}
}
diff --git a/.lintignore b/.lintignore
new file mode 100755
index 000000000..debffe068
--- /dev/null
+++ b/.lintignore
@@ -0,0 +1,5 @@
+**/node_modules
+**/tmp
+**/lib
+**/cjs
+**/tmp
\ No newline at end of file
diff --git a/bin/generate-release-notes.js b/bin/generate-release-notes.js
index 6bd2b03ce..40e84336e 100644
--- a/bin/generate-release-notes.js
+++ b/bin/generate-release-notes.js
@@ -1,59 +1,77 @@
#!/bin/env node
-'use strict';
+"use strict";
-const GitUtility = require('../build/GitUtility');
-const FS = require('fs');
-const Path = require('path');
+const GitUtility = require("../build/GitUtility");
+const FS = require("fs");
+const Path = require("path");
-process.on('unhandledRejection', error => {
+/* eslint-disable no-console*/
+
+process.on("unhandledRejection", (error) => {
console.error(error);
process.exit(1);
});
async function main() {
- const nextVersion = process.argv.find(text => text.startsWith('--next-version=')).split('=').reverse()[0];
- const latestVersion = process.argv.find(text => text.startsWith('--latest-version=')).split('=').reverse()[0];
+ const nextVersion = process.argv
+ .find((text) => text.startsWith("--next-version="))
+ .split("=")
+ .reverse()[0];
+ const latestVersion = process.argv
+ .find((text) => text.startsWith("--latest-version="))
+ .split("=")
+ .reverse()[0];
- const commitMessages = await GitUtility.getCommitMessages('v' + latestVersion, 'v' + nextVersion);
+ const commitMessages = await GitUtility.getCommitMessages(
+ "v" + latestVersion,
+ "v" + nextVersion
+ );
const commits = { trivial: [], patch: [], minor: [], major: [] };
const duplicates = { trivial: [], patch: [], minor: [], major: [] };
-
+
for (const commitMessage of commitMessages) {
const parsed = GitUtility.parseCommitMessage(commitMessage);
-
- if (parsed.commit && !parsed.errors.length && !duplicates[parsed.commit.versionType].includes(parsed.commit.description)) {
+
+ if (
+ parsed.commit &&
+ !parsed.errors.length &&
+ !duplicates[parsed.commit.versionType].includes(parsed.commit.description)
+ ) {
commits[parsed.commit.versionType].push(parsed.commit);
duplicates[parsed.commit.versionType].push(parsed.commit.description);
}
}
-
- let output = [];
-
- if(commits.major.length > 0) {
- let notes = '### :bomb: Breaking Changes\n\n'
- for(const commit of commits.major) {
+
+ const output = [];
+
+ if (commits.major.length > 0) {
+ let notes = "### :bomb: Breaking Changes\n\n";
+ for (const commit of commits.major) {
notes += ` - ${commit.description} (${commit.taskId})\n`;
}
output.push(notes);
}
-
- if(commits.minor.length > 0) {
- let notes = '### :art: Features\n\n'
- for(const commit of commits.minor) {
+
+ if (commits.minor.length > 0) {
+ let notes = "### :art: Features\n\n";
+ for (const commit of commits.minor) {
notes += ` - ${commit.description} (${commit.taskId})\n`;
}
output.push(notes);
}
-
- if(commits.patch.length > 0) {
- let notes = '### :construction_worker_man: Patch fixes\n\n';
- for(const commit of commits.patch) {
+
+ if (commits.patch.length > 0) {
+ let notes = "### :construction_worker_man: Patch fixes\n\n";
+ for (const commit of commits.patch) {
notes += ` - ${commit.description} (${commit.taskId})\n`;
}
output.push(notes);
}
-
- await FS.promises.writeFile(Path.resolve('RELEASE_NOTES.md'), output.join('\n\n'));
+
+ await FS.promises.writeFile(
+ Path.resolve("RELEASE_NOTES.md"),
+ output.join("\n\n")
+ );
}
-main();
\ No newline at end of file
+main();
diff --git a/bin/husky-commit-message.js b/bin/husky-commit-message.js
index 79f688a61..b7aa9b4be 100644
--- a/bin/husky-commit-message.js
+++ b/bin/husky-commit-message.js
@@ -1,41 +1,47 @@
#!/bin/env node
-'use strict';
+"use strict";
-const Fs = require('fs');
-const Path = require('path');
-const Chalk = require('chalk');
-const GitUtility = require('../build/GitUtility');
+const Fs = require("fs");
+const Path = require("path");
+const Chalk = require("chalk");
+const GitUtility = require("../build/GitUtility");
-process.on('unhandledRejection', error => {
+/* eslint-disable no-console*/
+
+process.on("unhandledRejection", (error) => {
console.error(error);
process.exit(1);
});
async function main() {
- const argument = process.argv.find(text => text.startsWith('--filepath='));
- const filepath = argument ? argument
- .split(/=/)[1]
- .replace('%HUSKY_GIT_PARAMS%', '')
- .replace('$HUSKY_GIT_PARAMS', '') : null;
+ const argument = process.argv.find((text) => text.startsWith("--filepath="));
+ const filepath = argument
+ ? argument
+ .split(/=/)[1]
+ .replace("%HUSKY_GIT_PARAMS%", "")
+ .replace("$HUSKY_GIT_PARAMS", "")
+ : null;
if (!filepath) {
- throw new Error('Failed to validate commit message. The argument "--filepath=" was not provided.');
+ throw new Error(
+ 'Failed to validate commit message. The argument "--filepath=" was not provided.'
+ );
}
const commitBuffer = await Fs.promises.readFile(Path.resolve(filepath));
const commitMessage = GitUtility.parseCommitMessage(commitBuffer.toString());
if (commitMessage.errors.length > 0) {
- console.error(Chalk.red('\nCommit message validation failed:'));
+ console.error(Chalk.red("\nCommit message validation failed:"));
for (const error of commitMessage.errors) {
console.error(Chalk.red(` ✖ ${error}`));
}
- console.log('');
+ console.log("");
process.exit(1);
}
}
-main();
\ No newline at end of file
+main();
diff --git a/bin/print-latest-version.js b/bin/print-latest-version.js
index 776802a7b..2abdc24c4 100644
--- a/bin/print-latest-version.js
+++ b/bin/print-latest-version.js
@@ -1,9 +1,11 @@
#!/bin/env node
-'use strict';
+"use strict";
-const GitUtility = require('../build/GitUtility');
+const GitUtility = require("../build/GitUtility");
-process.on('unhandledRejection', error => {
+/* eslint-disable no-console*/
+
+process.on("unhandledRejection", (error) => {
console.error(error);
process.exit(1);
});
@@ -13,4 +15,4 @@ async function main() {
process.stdout.write(latestVersion);
}
-main();
\ No newline at end of file
+main();
diff --git a/bin/print-next-version.js b/bin/print-next-version.js
index c2e5d2ea7..46e1896a2 100644
--- a/bin/print-next-version.js
+++ b/bin/print-next-version.js
@@ -1,9 +1,11 @@
#!/bin/env node
-'use strict';
+"use strict";
-const GitUtility = require('../build/GitUtility');
+const GitUtility = require("../build/GitUtility");
-process.on('unhandledRejection', error => {
+/* eslint-disable no-console*/
+
+process.on("unhandledRejection", (error) => {
console.error(error);
process.exit(1);
});
@@ -13,4 +15,4 @@ async function main() {
process.stdout.write(nextVersion);
}
-main();
\ No newline at end of file
+main();
diff --git a/bin/set-version.js b/bin/set-version.js
index da982b623..17f68a0cd 100644
--- a/bin/set-version.js
+++ b/bin/set-version.js
@@ -4,6 +4,8 @@
const FS = require("fs");
const Path = require("path");
+/* eslint-disable no-console*/
+
process.on("unhandledRejection", (error) => {
console.error(error);
process.exit(1);
diff --git a/bin/validate-commit-messages.js b/bin/validate-commit-messages.js
index 3a68880c8..a1ea9b518 100644
--- a/bin/validate-commit-messages.js
+++ b/bin/validate-commit-messages.js
@@ -1,36 +1,41 @@
#!/bin/env node
-'use strict';
+"use strict";
-const GitUtility = require('../build/GitUtility');
-const Chalk = require('chalk');
+const GitUtility = require("../build/GitUtility");
+const Chalk = require("chalk");
-process.on('unhandledRejection', error => {
+/* eslint-disable no-console*/
+
+process.on("unhandledRejection", (error) => {
console.error(error);
process.exit(1);
});
async function main() {
- const commitMessages = await GitUtility.getCommitMessages('origin/master', 'HEAD');
+ const commitMessages = await GitUtility.getCommitMessages(
+ "origin/master",
+ "HEAD"
+ );
let hasErrors = false;
for (const commitMessage of commitMessages) {
const parsed = GitUtility.parseCommitMessage(commitMessage);
if (!parsed.errors.length) {
- console.log(Chalk.green('✓ ' + commitMessage));
+ console.log(Chalk.green("✓ " + commitMessage));
} else {
- console.log(Chalk.red('✖' + commitMessage));
- for(const error of parsed.errors) {
+ console.log(Chalk.red("✖" + commitMessage));
+ for (const error of parsed.errors) {
console.log(Chalk.red(` - ${error}`));
}
hasErrors = true;
}
}
- if(hasErrors) {
- console.error('Commit validation failed.');
+ if (hasErrors) {
+ console.error("Commit validation failed.");
process.exit(1);
}
}
-main();
\ No newline at end of file
+main();
diff --git a/build/GitUtility.js b/build/GitUtility.js
index 1db975a46..06beb1d50 100644
--- a/build/GitUtility.js
+++ b/build/GitUtility.js
@@ -1,71 +1,84 @@
-const ChildProcess = require('child_process');
+const ChildProcess = require("child_process");
+const SemVer = require("semver");
const COMMIT_MESSAGE_REGEXP = /(#[0-9]+)@([^:]+): (.+)/ms;
-const VERSION_TYPES = ['trivial', 'patch', 'minor', 'major'];
+const VERSION_TYPES = ["trivial", "patch", "minor", "major"];
/**
* Git Utility.
*/
class GitUtility {
-
/**
* Returns commit messages.
- *
+ *
* @param {string} from From branch.
* @param {string} to To branch.
* @returns {string[]} Commit messages.
*/
static async getCommitMessages(from, to) {
const commits = await this.__getDiffCommits(from, to);
- return await Promise.all(commits.map(commit => {
- return new Promise((resolve, reject) => {
- ChildProcess.exec(`git show -s --format=%B ${commit}`, (error, stdout) => {
- if(error) {
- reject(error);
- } else {
- resolve(stdout.replace(/[\n\r]/gm, '').trim());
- }
+ return await Promise.all(
+ commits.map((commit) => {
+ return new Promise((resolve, reject) => {
+ ChildProcess.exec(
+ `git show -s --format=%B ${commit}`,
+ (error, stdout) => {
+ if (error) {
+ reject(error);
+ } else {
+ resolve(stdout.replace(/[\n\r]/gm, "").trim());
+ }
+ }
+ );
});
- });
- }));
+ })
+ );
}
/**
* Parses a commit message.
- *
+ *
* @param {string} commitMessage Commit message.
* @returns {{ errors: string[]; commit: { taskId: string; versionType: string; description: string; } }} Parsed commit information.
*/
static parseCommitMessage(commitMessage) {
- if(commitMessage.startsWith('Merge')) {
- return {
- errors: [],
- commit: null
- };
- }
+ if (commitMessage.startsWith("Merge")) {
+ return {
+ errors: [],
+ commit: null,
+ };
+ }
const match = commitMessage.match(COMMIT_MESSAGE_REGEXP);
const errors = [];
- if(!match) {
+ if (!match) {
return {
- errors: ['Invalid format. Expected format to be: "#{taskId}@{trivial|patch|minor|major}: {description}."'],
- commit: null
+ errors: [
+ 'Invalid format. Expected format to be: "#{taskId}@{trivial|patch|minor|major}: {description}."',
+ ],
+ commit: null,
};
}
const description = match[3].trim();
- if(!VERSION_TYPES.includes(match[2])) {
- errors.push(`Invalid version type. Valid version types: ${VERSION_TYPES.join(', ')}`);
+ if (!VERSION_TYPES.includes(match[2])) {
+ errors.push(
+ `Invalid version type. Valid version types: ${VERSION_TYPES.join(", ")}`
+ );
}
- if(description[0] !== description[0].toUpperCase()) {
- errors.push(`Invalid description. Expected description to start with a capital letter`);
+ if (description[0] !== description[0].toUpperCase()) {
+ errors.push(
+ `Invalid description. Expected description to start with a capital letter`
+ );
}
- if(!description.endsWith('.')) {
- errors.push(`Invalid description. Expected description to end with a period (".")`);
+ if (!description.endsWith(".")) {
+ errors.push(
+ `Invalid description. Expected description to end with a period (".")`
+ );
}
return {
@@ -73,36 +86,36 @@ class GitUtility {
commit: {
taskId: match[1],
versionType: match[2],
- description
- }
+ description,
+ },
};
}
/**
* Returns the next version.
- *
+ *
* @returns {string} Next version (e.g. "v1.2.3").
*/
static async getNextVersion() {
const latest = await this.getLatestVersion();
- const versionType = await this.__getVersionType('v' + latest, 'HEAD');
- return require('semver').inc(latest, versionType);
+ const versionType = await this.__getVersionType("v" + latest, "HEAD");
+ return SemVer.inc(latest, versionType);
}
/**
* Returns the latest version.
- *
+ *
* @returns {string} Latest version (e.g. "v1.2.3").
*/
static getLatestVersion() {
return new Promise((resolve, reject) => {
ChildProcess.exec(`git tag -l --sort=v:refname`, (error, stdout) => {
- if(error) {
+ if (error) {
reject(error);
} else {
- const gitTags = stdout.trim().split('\n');
- gitTags.sort(require('semver').compare);
- resolve(gitTags[gitTags.length - 1].replace('v', ''));
+ const gitTags = stdout.trim().split("\n");
+ gitTags.sort(SemVer.compare);
+ resolve(gitTags[gitTags.length - 1].replace("v", ""));
}
});
});
@@ -116,21 +129,21 @@ class GitUtility {
static async __getVersionType(from, to) {
const commitMessages = await this.getCommitMessages(from, to);
let isMinor = false;
-
+
for (const commitMessage of commitMessages) {
const parsed = this.parseCommitMessage(commitMessage);
-
+
if (parsed.commit && parsed.commit.versionType) {
- if(parsed.commit.versionType === 'major') {
- return 'major';
+ if (parsed.commit.versionType === "major") {
+ return "major";
}
- if(parsed.commit.versionType === 'minor') {
+ if (parsed.commit.versionType === "minor") {
isMinor = true;
}
}
}
- return isMinor ? 'minor' : 'patch';
+ return isMinor ? "minor" : "patch";
}
/**
@@ -142,16 +155,21 @@ class GitUtility {
*/
static __getDiffCommits(from, to) {
return new Promise((resolve, reject) => {
- ChildProcess.exec(`git log ${from}..${to} --pretty=format:"%h"`, (error, stdout) => {
- if(error) {
- reject(error);
- } else {
- resolve(stdout
- .trim()
- .split('\n')
- .filter(str => str !== ''));
+ ChildProcess.exec(
+ `git log ${from}..${to} --pretty=format:"%h"`,
+ (error, stdout) => {
+ if (error) {
+ reject(error);
+ } else {
+ resolve(
+ stdout
+ .trim()
+ .split("\n")
+ .filter((str) => str !== "")
+ );
+ }
}
- });
+ );
});
}
}
diff --git a/package-lock.json b/package-lock.json
index bb5256f88..abad5394c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -24,7 +24,7 @@
"husky": "2.3.0",
"prettier": "^2.6.0",
"semver": "^7.3.5",
- "turbo": "^1.7.3",
+ "turbo": "^1.11.3",
"typescript": "^5.0.4",
"vitest": "^0.32.4"
},
@@ -5313,30 +5313,6 @@
"semver": "bin/semver.js"
}
},
- "node_modules/eslint-plugin-jest": {
- "version": "26.9.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-26.9.0.tgz",
- "integrity": "sha512-TWJxWGp1J628gxh2KhaH1H1paEdgE2J61BBF1I59c6xWeL5+D1BzMxGDN/nXAfX+aSkR5u80K+XhskK6Gwq9ng==",
- "dev": true,
- "dependencies": {
- "@typescript-eslint/utils": "^5.10.0"
- },
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "peerDependencies": {
- "@typescript-eslint/eslint-plugin": "^5.0.0",
- "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
- },
- "peerDependenciesMeta": {
- "@typescript-eslint/eslint-plugin": {
- "optional": true
- },
- "jest": {
- "optional": true
- }
- }
- },
"node_modules/eslint-plugin-jsdoc": {
"version": "38.1.6",
"resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-38.1.6.tgz",
@@ -5393,15 +5369,6 @@
}
}
},
- "node_modules/eslint-plugin-turbo": {
- "version": "0.0.7",
- "resolved": "https://registry.npmjs.org/eslint-plugin-turbo/-/eslint-plugin-turbo-0.0.7.tgz",
- "integrity": "sha512-iajOH8eD4jha3duztGVBD1BEmvNrQBaA/y3HFHf91vMDRYRwH7BpHSDFtxydDpk5ghlhRxG299SFxz7D6z4MBQ==",
- "dev": true,
- "peerDependencies": {
- "eslint": ">6.6.0"
- }
- },
"node_modules/eslint-scope": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
@@ -11034,26 +11001,26 @@
}
},
"node_modules/turbo": {
- "version": "1.10.14",
- "resolved": "https://registry.npmjs.org/turbo/-/turbo-1.10.14.tgz",
- "integrity": "sha512-hr9wDNYcsee+vLkCDIm8qTtwhJ6+UAMJc3nIY6+PNgUTtXcQgHxCq8BGoL7gbABvNWv76CNbK5qL4Lp9G3ZYRA==",
+ "version": "1.11.3",
+ "resolved": "https://registry.npmjs.org/turbo/-/turbo-1.11.3.tgz",
+ "integrity": "sha512-RCJOUFcFMQNIGKSjC9YmA5yVP1qtDiBA0Lv9VIgrXraI5Da1liVvl3VJPsoDNIR9eFMyA/aagx1iyj6UWem5hA==",
"dev": true,
"bin": {
"turbo": "bin/turbo"
},
"optionalDependencies": {
- "turbo-darwin-64": "1.10.14",
- "turbo-darwin-arm64": "1.10.14",
- "turbo-linux-64": "1.10.14",
- "turbo-linux-arm64": "1.10.14",
- "turbo-windows-64": "1.10.14",
- "turbo-windows-arm64": "1.10.14"
+ "turbo-darwin-64": "1.11.3",
+ "turbo-darwin-arm64": "1.11.3",
+ "turbo-linux-64": "1.11.3",
+ "turbo-linux-arm64": "1.11.3",
+ "turbo-windows-64": "1.11.3",
+ "turbo-windows-arm64": "1.11.3"
}
},
"node_modules/turbo-darwin-64": {
- "version": "1.10.14",
- "resolved": "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-1.10.14.tgz",
- "integrity": "sha512-I8RtFk1b9UILAExPdG/XRgGQz95nmXPE7OiGb6ytjtNIR5/UZBS/xVX/7HYpCdmfriKdVwBKhalCoV4oDvAGEg==",
+ "version": "1.11.3",
+ "resolved": "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-1.11.3.tgz",
+ "integrity": "sha512-IsOOg2bVbIt3o/X8Ew9fbQp5t1hTHN3fGNQYrPQwMR2W1kIAC6RfbVD4A9OeibPGyEPUpwOH79hZ9ydFH5kifw==",
"cpu": [
"x64"
],
@@ -11064,9 +11031,9 @@
]
},
"node_modules/turbo-darwin-arm64": {
- "version": "1.10.14",
- "resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-1.10.14.tgz",
- "integrity": "sha512-KAdUWryJi/XX7OD0alOuOa0aJ5TLyd4DNIYkHPHYcM6/d7YAovYvxRNwmx9iv6Vx6IkzTnLeTiUB8zy69QkG9Q==",
+ "version": "1.11.3",
+ "resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-1.11.3.tgz",
+ "integrity": "sha512-FsJL7k0SaPbJzI/KCnrf/fi3PgCDCjTliMc/kEFkuWVA6Httc3Q4lxyLIIinz69q6JTx8wzh6yznUMzJRI3+dg==",
"cpu": [
"arm64"
],
@@ -11077,9 +11044,9 @@
]
},
"node_modules/turbo-linux-64": {
- "version": "1.10.14",
- "resolved": "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-1.10.14.tgz",
- "integrity": "sha512-BOBzoREC2u4Vgpap/WDxM6wETVqVMRcM8OZw4hWzqCj2bqbQ6L0wxs1LCLWVrghQf93JBQtIGAdFFLyCSBXjWQ==",
+ "version": "1.11.3",
+ "resolved": "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-1.11.3.tgz",
+ "integrity": "sha512-SvW7pvTVRGsqtSkII5w+wriZXvxqkluw5FO/MNAdFw0qmoov+PZ237+37/NgArqE3zVn1GX9P6nUx9VO+xcQAg==",
"cpu": [
"x64"
],
@@ -11090,9 +11057,9 @@
]
},
"node_modules/turbo-linux-arm64": {
- "version": "1.10.14",
- "resolved": "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-1.10.14.tgz",
- "integrity": "sha512-D8T6XxoTdN5D4V5qE2VZG+/lbZX/89BkAEHzXcsSUTRjrwfMepT3d2z8aT6hxv4yu8EDdooZq/2Bn/vjMI32xw==",
+ "version": "1.11.3",
+ "resolved": "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-1.11.3.tgz",
+ "integrity": "sha512-YhUfBi1deB3m+3M55X458J6B7RsIS7UtM3P1z13cUIhF+pOt65BgnaSnkHLwETidmhRh8Dl3GelaQGrB3RdCDw==",
"cpu": [
"arm64"
],
@@ -11103,9 +11070,9 @@
]
},
"node_modules/turbo-windows-64": {
- "version": "1.10.14",
- "resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-1.10.14.tgz",
- "integrity": "sha512-zKNS3c1w4i6432N0cexZ20r/aIhV62g69opUn82FLVs/zk3Ie0GVkSB6h0rqIvMalCp7enIR87LkPSDGz9K4UA==",
+ "version": "1.11.3",
+ "resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-1.11.3.tgz",
+ "integrity": "sha512-s+vEnuM2TiZuAUUUpmBHDr6vnNbJgj+5JYfnYmVklYs16kXh+EppafYQOAkcRIMAh7GjV3pLq5/uGqc7seZeHA==",
"cpu": [
"x64"
],
@@ -11116,9 +11083,9 @@
]
},
"node_modules/turbo-windows-arm64": {
- "version": "1.10.14",
- "resolved": "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-1.10.14.tgz",
- "integrity": "sha512-rkBwrTPTxNSOUF7of8eVvvM+BkfkhA2OvpHM94if8tVsU+khrjglilp8MTVPHlyS9byfemPAmFN90oRIPB05BA==",
+ "version": "1.11.3",
+ "resolved": "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-1.11.3.tgz",
+ "integrity": "sha512-ZR5z5Zpc7cASwfdRAV5yNScCZBsgGSbcwiA/u3farCacbPiXsfoWUkz28iyrx21/TRW0bi6dbsB2v17swa8bjw==",
"cpu": [
"arm64"
],
@@ -11946,14 +11913,6 @@
"@types/node": "^16.11.7",
"@typescript-eslint/eslint-plugin": "^5.16.0",
"@typescript-eslint/parser": "^5.16.0",
- "eslint": "^8.11.0",
- "eslint-config-prettier": "^8.5.0",
- "eslint-plugin-filenames": "^1.3.2",
- "eslint-plugin-import": "^2.27.5",
- "eslint-plugin-jsdoc": "^38.0.6",
- "eslint-plugin-json": "^3.1.0",
- "eslint-plugin-prettier": "^4.0.0",
- "eslint-plugin-turbo": "^0.0.7",
"prettier": "^2.6.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@@ -11979,14 +11938,6 @@
"@typescript-eslint/parser": "^5.16.0",
"@vitest/ui": "^0.33.0",
"@webref/css": "6.6.2",
- "eslint": "^8.11.0",
- "eslint-config-prettier": "^8.5.0",
- "eslint-plugin-filenames": "^1.3.2",
- "eslint-plugin-import": "^2.27.5",
- "eslint-plugin-jsdoc": "^38.0.6",
- "eslint-plugin-json": "^3.1.0",
- "eslint-plugin-prettier": "^4.0.0",
- "eslint-plugin-turbo": "^0.0.7",
"prettier": "^2.6.0",
"typescript": "^5.0.4",
"vitest": "^0.32.4"
@@ -12003,18 +11954,23 @@
"@types/node": "^16.11.7",
"@typescript-eslint/eslint-plugin": "^5.16.0",
"@typescript-eslint/parser": "^5.16.0",
- "eslint": "^8.11.0",
- "eslint-config-prettier": "^8.5.0",
- "eslint-plugin-filenames": "^1.3.2",
- "eslint-plugin-import": "^2.27.5",
- "eslint-plugin-jsdoc": "^38.0.6",
- "eslint-plugin-json": "^3.1.0",
- "eslint-plugin-prettier": "^4.0.0",
- "eslint-plugin-turbo": "^0.0.7",
+ "chalk": "^5.3.0",
"express": "^4.18.2",
"prettier": "^2.6.0"
}
},
+ "packages/integration-test/node_modules/chalk": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
+ "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
+ "dev": true,
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
"packages/jest-environment": {
"name": "@happy-dom/jest-environment",
"version": "0.0.0",
@@ -12044,15 +12000,6 @@
"@typescript-eslint/eslint-plugin": "^5.16.0",
"@typescript-eslint/parser": "^5.16.0",
"cpy": "^8.1.2",
- "eslint": "^8.11.0",
- "eslint-config-prettier": "^8.5.0",
- "eslint-plugin-filenames": "^1.3.2",
- "eslint-plugin-import": "^2.27.5",
- "eslint-plugin-jest": "^26.1.2",
- "eslint-plugin-jsdoc": "^38.0.6",
- "eslint-plugin-json": "^3.1.0",
- "eslint-plugin-prettier": "^4.0.0",
- "eslint-plugin-turbo": "^0.0.7",
"express": "^4.18.2",
"glob": "^7.2.0",
"jest": "^29.4.0",
@@ -12074,25 +12021,7 @@
"name": "@happy-dom/uncaught-exception-observer",
"version": "0.0.0",
"license": "MIT",
- "devDependencies": {
- "@types/node": "^16.11.7",
- "@typescript-eslint/eslint-plugin": "^5.16.0",
- "@typescript-eslint/parser": "^5.16.0",
- "eslint": "^8.11.0",
- "eslint-config-prettier": "^8.5.0",
- "eslint-plugin-filenames": "^1.3.2",
- "eslint-plugin-import": "^2.27.5",
- "eslint-plugin-jsdoc": "^38.0.6",
- "eslint-plugin-json": "^3.1.0",
- "eslint-plugin-prettier": "^4.0.0",
- "eslint-plugin-turbo": "^0.0.7",
- "happy-dom": "^0.0.0",
- "prettier": "^2.6.0",
- "typescript": "^5.0.4"
- },
- "peerDependencies": {
- "happy-dom": ">= 2.25.2"
- }
+ "devDependencies": {}
}
}
}
diff --git a/package.json b/package.json
index 7da917e64..251439d3e 100644
--- a/package.json
+++ b/package.json
@@ -8,8 +8,9 @@
"compile": "turbo run compile --cache-dir=.turbo",
"watch": "turbo run watch --parallel",
"clean": "git clean -Xdfq",
- "lint": "turbo run lint --cache-dir=.turbo",
- "lint:fix": "turbo run lint:fix",
+ "lint": "eslint --ignore-path .lintignore --max-warnings 0 .",
+ "lint:fix": "eslint --ignore-path .lintignore --max-warnings 0 --fix .",
+ "lint:changed": "eslint --ignore-path .lintignore --max-warnings 0 --fix $(git diff --diff-filter=d --name-only HEAD | grep -E '^[a-zA-Z0-9_].*\\.(cjs|mjs|js|jsx|ts|tsx|json)$' | xargs)",
"test": "turbo run test --cache-dir=.turbo",
"test:watch": "turbo run test:watch --parallel"
},
@@ -30,7 +31,7 @@
"vitest": "^0.32.4",
"prettier": "^2.6.0",
"semver": "^7.3.5",
- "turbo": "^1.7.3",
+ "turbo": "^1.11.3",
"typescript": "^5.0.4"
},
"engines": {
diff --git a/packages/global-registrator/.eslintrc.cjs b/packages/global-registrator/.eslintrc.cjs
deleted file mode 100644
index f4088a566..000000000
--- a/packages/global-registrator/.eslintrc.cjs
+++ /dev/null
@@ -1 +0,0 @@
-module.exports = require('happy-dom/.eslintrc.cjs');
diff --git a/packages/global-registrator/package.json b/packages/global-registrator/package.json
index aaa1d0240..5c2cb86d4 100644
--- a/packages/global-registrator/package.json
+++ b/packages/global-registrator/package.json
@@ -68,8 +68,6 @@
"compile": "tsc && tsc --moduleResolution Node --module CommonJS --outDir cjs && npm run change-cjs-file-extension",
"change-cjs-file-extension": "node ../happy-dom/bin/change-file-extension.cjs --dir=./cjs --fromExt=.js --toExt=.cjs",
"watch": "npm run compile && tsc -w --preserveWatchOutput",
- "lint": "eslint --ignore-path .gitignore --max-warnings 0 .",
- "lint:fix": "eslint --ignore-path .gitignore --max-warnings 0 --fix .",
"test": "tsc --project ./test && node ../happy-dom/bin/change-file-extension.cjs --dir=./tmp --fromExt=.js --toExt=.cjs && node ./tmp/react/React.test.cjs",
"test:debug": "tsc --project ./test && node ../happy-dom/bin/change-file-extension.cjs --dir=./tmp --fromExt=.js --toExt=.cjs && node --inspect-brk ./tmp/react/React.test.cjs"
},
@@ -80,14 +78,6 @@
"@typescript-eslint/eslint-plugin": "^5.16.0",
"@typescript-eslint/parser": "^5.16.0",
"@types/node": "^16.11.7",
- "eslint": "^8.11.0",
- "eslint-config-prettier": "^8.5.0",
- "eslint-plugin-prettier": "^4.0.0",
- "eslint-plugin-filenames": "^1.3.2",
- "eslint-plugin-import": "^2.27.5",
- "eslint-plugin-jsdoc": "^38.0.6",
- "eslint-plugin-json": "^3.1.0",
- "eslint-plugin-turbo": "^0.0.7",
"prettier": "^2.6.0",
"typescript": "^5.0.4",
"react": "^18.2.0",
diff --git a/packages/global-registrator/src/GlobalRegistrator.ts b/packages/global-registrator/src/GlobalRegistrator.ts
index 0a0c2329d..774165e63 100644
--- a/packages/global-registrator/src/GlobalRegistrator.ts
+++ b/packages/global-registrator/src/GlobalRegistrator.ts
@@ -25,11 +25,10 @@ export default class GlobalRegistrator {
if (global[key] !== window[key] && !IGNORE_LIST.includes(key)) {
this.registered[key] =
global[key] !== window[key] && global[key] !== undefined ? global[key] : null;
-
- // Only bind functions that aren't used as classes, since bound functions can't be extended.
- const bind = typeof window[key] === 'function' && !isClassLikeName(key);
-
- global[key] = bind ? window[key].bind(global) : window[key];
+ global[key] =
+ typeof window[key] === 'function' && !window[key].toString().startsWith('class ')
+ ? window[key].bind(global)
+ : window[key];
}
}
@@ -60,7 +59,3 @@ export default class GlobalRegistrator {
this.registered = null;
}
}
-
-function isClassLikeName(name: string): boolean {
- return name[0] === name[0].toUpperCase();
-}
diff --git a/packages/happy-dom/.eslintrc.cjs b/packages/happy-dom/.eslintrc.cjs
deleted file mode 100644
index 728d1a2cd..000000000
--- a/packages/happy-dom/.eslintrc.cjs
+++ /dev/null
@@ -1,174 +0,0 @@
-const WARN = 'warn';
-const ERROR = 'error';
-const OFF = 'off';
-
-const COMMON_CONFIG = {
- plugins: ['turbo', 'jsdoc', 'filenames', 'import', 'prettier'],
- extends: ['plugin:@typescript-eslint/recommended', 'prettier', 'plugin:prettier/recommended'],
- rules: {
- 'prettier/prettier': WARN,
- 'no-underscore-dangle': OFF,
- 'no-debugger': WARN,
- 'space-infix-ops': ERROR,
- 'no-console': WARN,
- 'wrap-iife': OFF,
- 'no-self-assign': ERROR,
- 'no-self-compare': ERROR,
- 'no-loop-func': OFF,
- 'array-callback-return': ERROR,
- curly: ERROR,
- 'no-fallthrough': OFF,
- 'dot-notation': OFF,
- 'prefer-const': WARN,
- 'no-empty-function': OFF,
- 'no-with': ERROR,
- 'one-var': [ERROR, 'never'],
- camelcase: [WARN, { properties: 'always', ignoreImports: true }],
- 'spaced-comment': [WARN, 'always'],
- 'capitalized-comments': [WARN, 'always', { ignorePattern: 'prettier' }],
- 'no-useless-rename': WARN,
- 'jsdoc/check-alignment': WARN,
- 'jsdoc/check-examples': OFF,
- 'jsdoc/check-indentation': WARN,
- 'jsdoc/check-syntax': WARN,
- 'jsdoc/check-tag-names': WARN,
- 'jsdoc/check-types': WARN,
- 'jsdoc/implements-on-classes': WARN,
- 'jsdoc/match-description': OFF,
- 'jsdoc/newline-after-description': WARN,
- 'jsdoc/no-types': OFF,
- 'jsdoc/no-undefined-types': OFF,
- 'jsdoc/require-description': OFF,
- 'jsdoc/require-description-complete-sentence': OFF,
- 'jsdoc/require-example': OFF,
- 'jsdoc/require-hyphen-before-param-description': [WARN, 'never'],
- 'jsdoc/require-param': WARN,
- 'jsdoc/require-param-name': WARN,
- 'jsdoc/require-returns-check': WARN,
- 'jsdoc/require-returns-description': WARN,
- 'jsdoc/valid-types': WARN,
- 'filenames/match-exported': WARN,
- 'no-useless-constructor': WARN,
- 'jsdoc/require-jsdoc': [
- WARN,
- {
- require: {
- ArrowFunctionExpression: false,
- FunctionDeclaration: false,
- FunctionExpression: false,
- ClassDeclaration: true,
- MethodDefinition: true
- }
- }
- ],
- 'import/no-extraneous-dependencies': WARN
- }
-};
-
-const TS_PARSER_FIELDS = {
- parser: '@typescript-eslint/parser',
- parserOptions: {
- ecmaFeatures: {
- jsx: true
- },
- ecmaVersion: 2020,
- sourceType: 'module'
- }
-};
-
-module.exports = {
- env: {
- es6: true,
- browser: true,
- node: true
- },
- overrides: [
- {
- files: ['*.js', '*.jsx', '*.mjs'],
- ...TS_PARSER_FIELDS,
- plugins: COMMON_CONFIG.plugins,
- extends: COMMON_CONFIG.extends,
- rules: {
- ...COMMON_CONFIG.rules,
- 'no-undef': ERROR,
- 'jsdoc/check-param-names': WARN,
- 'jsdoc/require-param-type': WARN,
- 'jsdoc/require-returns': WARN,
- 'jsdoc/require-param-description': WARN,
- 'jsdoc/require-returns-type': WARN,
- 'consistent-return': WARN
- }
- },
- {
- files: ['*.json'],
- plugins: ['json'],
- extends: ['eslint:recommended', 'plugin:json/recommended']
- },
- {
- files: ['*.ts', '*.tsx'],
- plugins: [...COMMON_CONFIG.plugins, '@typescript-eslint'],
- ...TS_PARSER_FIELDS,
- extends: [...COMMON_CONFIG.extends, 'plugin:@typescript-eslint/recommended'],
- rules: {
- ...COMMON_CONFIG.rules,
- '@typescript-eslint/explicit-member-accessibility': [
- ERROR,
- { overrides: { constructors: 'no-public' } }
- ],
- '@typescript-eslint/no-unused-vars': OFF, // TSC is already doing this
- '@typescript-eslint/ban-types': OFF, // TSC is already doing this
- 'no-undef': OFF, // TSC is already doing this
- '@typescript-eslint/no-var-requires': OFF,
- '@typescript-eslint/explicit-module-boundary-types': OFF, // TSC is already doing this
- '@typescript-eslint/consistent-type-assertions': [
- WARN,
- { assertionStyle: 'angle-bracket' }
- ],
- '@typescript-eslint/no-explicit-any': ERROR,
- '@typescript-eslint/no-empty-function': OFF,
- '@typescript-eslint/no-use-before-define': OFF,
- '@typescript-eslint/no-this-alias': OFF,
- '@typescript-eslint/explicit-function-return-type': [ERROR, { allowExpressions: true }],
- '@typescript-eslint/member-ordering': [
- WARN,
- {
- default: ['signature', 'field', 'constructor', ['get', 'set'], 'method']
- }
- ],
- '@typescript-eslint/ban-ts-comment': OFF,
- 'jsdoc/no-types': WARN,
- 'import/named': OFF,
- 'import/no-named-as-default': WARN,
- 'import/no-extraneous-dependencies': WARN,
- 'import/no-absolute-path': WARN,
- '@typescript-eslint/naming-convention': [
- WARN,
- {
- selector: 'interface',
- format: ['PascalCase'],
- prefix: ['I']
- },
- {
- selector: 'enum',
- format: ['PascalCase'],
- suffix: ['Enum']
- },
- {
- selector: 'typeParameter',
- format: ['PascalCase']
- },
- {
- selector: 'memberLike',
- modifiers: ['private'],
- leadingUnderscore: 'allow',
- format: ['camelCase']
- },
- {
- selector: 'class',
- format: ['PascalCase']
- }
- ]
- }
- }
- ]
-};
diff --git a/packages/happy-dom/README.md b/packages/happy-dom/README.md
index f4d7ed260..741ed234b 100644
--- a/packages/happy-dom/README.md
+++ b/packages/happy-dom/README.md
@@ -51,14 +51,12 @@ npm install happy-dom
A simple example of how you can use Happy DOM.
+## Window
+
```javascript
import { Window } from 'happy-dom';
-const window = new Window({
- url: 'https://localhost:8080',
- width: 1024,
- height: 768
-});
+const window = new Window({ url: 'https://localhost:8080' });
const document = window.document;
document.body.innerHTML = '
';
@@ -72,6 +70,30 @@ container.appendChild(button);
console.log(document.body.innerHTML);
```
+## Browser
+
+```javascript
+import { Browser, BrowserErrorCaptureEnum } from 'happy-dom';
+
+const browser = new Browser({ settings: { errorCapture: BrowserErrorCaptureEnum.processLevel } });
+const page = browser.newPage();
+
+// Navigates page
+await page.goto('https://github.com/capricorn86');
+
+// Clicks on link
+page.mainFrame.document.querySelector('a[href*="capricorn86/happy-dom"]').click();
+
+// Waits for all operations on the page to complete (fetch, timers etc.)
+await page.waitUntilComplete();
+
+// Outputs "GitHub - capricorn86/happy-dom: Happy DOM..."
+console.log(page.mainFrame.document.title);
+
+// Closes the browser
+await browser.close();
+```
+
# Documentation
Read more about how to use Happy DOM in our [Wiki](https://github.com/capricorn86/happy-dom/wiki).
diff --git a/packages/happy-dom/package.json b/packages/happy-dom/package.json
index f1d3eef84..1d43d30fe 100644
--- a/packages/happy-dom/package.json
+++ b/packages/happy-dom/package.json
@@ -65,12 +65,10 @@
"access": "public"
},
"scripts": {
- "compile": "tsc && tsc --moduleResolution Node --module CommonJS --outDir cjs && npm run change-cjs-file-extension && npm run build-version-file",
+ "compile": "tsc && rm -rf ./cjs && tsc --moduleResolution Node --module CommonJS --outDir cjs && npm run change-cjs-file-extension && npm run build-version-file",
"change-cjs-file-extension": "node ./bin/change-file-extension.cjs --dir=./cjs --fromExt=.js --toExt=.cjs",
"build-version-file": "node ./bin/build-version-file.cjs",
"watch": "tsc -w --preserveWatchOutput",
- "lint": "eslint --ignore-path .gitignore --max-warnings 0 .",
- "lint:fix": "eslint --ignore-path .gitignore --max-warnings 0 --fix .",
"test": "vitest run --singleThread",
"test:ui": "vitest --ui",
"test:watch": "vitest --singleThread",
@@ -92,14 +90,6 @@
"@typescript-eslint/parser": "^5.16.0",
"@vitest/ui": "^0.33.0",
"@webref/css": "6.6.2",
- "eslint": "^8.11.0",
- "eslint-config-prettier": "^8.5.0",
- "eslint-plugin-filenames": "^1.3.2",
- "eslint-plugin-import": "^2.27.5",
- "eslint-plugin-jsdoc": "^38.0.6",
- "eslint-plugin-json": "^3.1.0",
- "eslint-plugin-prettier": "^4.0.0",
- "eslint-plugin-turbo": "^0.0.7",
"prettier": "^2.6.0",
"typescript": "^5.0.4",
"vitest": "^0.32.4"
diff --git a/packages/happy-dom/src/PropertySymbol.ts b/packages/happy-dom/src/PropertySymbol.ts
new file mode 100644
index 000000000..116b41550
--- /dev/null
+++ b/packages/happy-dom/src/PropertySymbol.ts
@@ -0,0 +1,150 @@
+export const abort = Symbol('abort');
+export const activeElement = Symbol('activeElement');
+export const appendFormControlItem = Symbol('appendFormControlItem');
+export const appendNamedItem = Symbol('appendNamedItem');
+export const asyncTaskManager = Symbol('asyncTaskManager');
+export const bodyBuffer = Symbol('bodyBuffer');
+export const buffer = Symbol('buffer');
+export const cacheID = Symbol('cacheID');
+export const cachedResponse = Symbol('cachedResponse');
+export const callbacks = Symbol('callbacks');
+export const captureEventListenerCount = Symbol('captureEventListenerCount');
+export const checked = Symbol('checked');
+export const childNodes = Symbol('childNodes');
+export const children = Symbol('children');
+export const classList = Symbol('classList');
+export const computedStyle = Symbol('computedStyle');
+export const connectToNode = Symbol('connectToNode');
+export const contentLength = Symbol('contentLength');
+export const contentType = Symbol('contentType');
+export const cssText = Symbol('cssText');
+export const currentScript = Symbol('currentScript');
+export const currentTarget = Symbol('currentTarget');
+export const data = Symbol('data');
+export const defaultView = Symbol('defaultView');
+export const destroy = Symbol('destroy');
+export const dirtyness = Symbol('dirtyness');
+export const end = Symbol('end');
+export const entries = Symbol('entries');
+export const evaluateCSS = Symbol('evaluateCSS');
+export const evaluateScript = Symbol('evaluateScript');
+export const exceptionObserver = Symbol('exceptionObserver');
+export const formNode = Symbol('formNode');
+export const getAttributeName = Symbol('getAttributeName');
+export const happyDOMSettingsID = Symbol('happyDOMSettingsID');
+export const height = Symbol('height');
+export const immediatePropagationStopped = Symbol('immediatePropagationStopped');
+export const isFirstWrite = Symbol('isFirstWrite');
+export const isFirstWriteAfterOpen = Symbol('isFirstWriteAfterOpen');
+export const isInPassiveEventListener = Symbol('isInPassiveEventListener');
+export const isValidPropertyName = Symbol('isValidPropertyName');
+export const isValue = Symbol('isValue');
+export const listenerOptions = Symbol('listenerOptions');
+export const listeners = Symbol('listeners');
+export const namedItems = Symbol('namedItems');
+export const nextActiveElement = Symbol('nextActiveElement');
+export const observe = Symbol('observe');
+export const observedAttributes = Symbol('observedAttributes');
+export const observers = Symbol('observers');
+export const ownerDocument = Symbol('ownerDocument');
+export const ownerElement = Symbol('ownerElement');
+export const propagationStopped = Symbol('propagationStopped');
+export const readyStateManager = Symbol('readyStateManager');
+export const referrer = Symbol('referrer');
+export const registry = Symbol('registry');
+export const relList = Symbol('relList');
+export const removeFormControlItem = Symbol('removeFormControlItem');
+export const removeNamedItem = Symbol('removeNamedItem');
+export const removeNamedItemIndex = Symbol('removeNamedItemIndex');
+export const removeNamedItemWithoutConsequences = Symbol('removeNamedItemWithoutConsequences');
+export const resetSelection = Symbol('resetSelection');
+export const rootNode = Symbol('rootNode');
+export const selectNode = Symbol('selectNode');
+export const selectedness = Symbol('selectedness');
+export const selection = Symbol('selection');
+export const setNamedItemWithoutConsequences = Symbol('setNamedItemWithoutConsequences');
+export const setupVMContext = Symbol('setupVMContext');
+export const shadowRoot = Symbol('shadowRoot');
+export const start = Symbol('start');
+export const style = Symbol('style');
+export const target = Symbol('target');
+export const textAreaNode = Symbol('textAreaNode');
+export const unobserve = Symbol('unobserve');
+export const updateIndices = Symbol('updateIndices');
+export const updateOptionItems = Symbol('updateOptionItems');
+export const url = Symbol('url');
+export const value = Symbol('value');
+export const width = Symbol('width');
+export const window = Symbol('window');
+export const windowResizeListener = Symbol('windowResizeListener');
+export const mutationObservers = Symbol('mutationObservers');
+export const openerFrame = Symbol('openerFrame');
+export const openerWindow = Symbol('openerFrame');
+export const popup = Symbol('popup');
+export const isConnected = Symbol('isConnected');
+export const parentNode = Symbol('parentNode');
+export const nodeType = Symbol('nodeType');
+export const tagName = Symbol('tagName');
+export const prefix = Symbol('prefix');
+export const scrollHeight = Symbol('scrollHeight');
+export const scrollWidth = Symbol('scrollWidth');
+export const scrollTop = Symbol('scrollTop');
+export const scrollLeft = Symbol('scrollLeft');
+export const attributes = Symbol('attributes');
+export const namespaceURI = Symbol('namespaceURI');
+export const accessKey = Symbol('accessKey');
+export const accessKeyLabel = Symbol('accessKeyLabel');
+export const contentEditable = Symbol('contentEditable');
+export const isContentEditable = Symbol('isContentEditable');
+export const offsetHeight = Symbol('offsetHeight');
+export const offsetWidth = Symbol('offsetWidth');
+export const offsetLeft = Symbol('offsetLeft');
+export const offsetTop = Symbol('offsetTop');
+export const clientHeight = Symbol('clientHeight');
+export const clientWidth = Symbol('clientWidth');
+export const clientLeft = Symbol('clientLeft');
+export const clientTop = Symbol('clientTop');
+export const name = Symbol('name');
+export const specified = Symbol('specified');
+export const adoptedStyleSheets = Symbol('adoptedStyleSheets');
+export const implementation = Symbol('implementation');
+export const readyState = Symbol('readyState');
+export const ownerWindow = Symbol('ownerWindow');
+export const publicId = Symbol('publicId');
+export const systemId = Symbol('systemId');
+export const validationMessage = Symbol('validationMessage');
+export const validity = Symbol('validity');
+export const returnValue = Symbol('returnValue');
+export const elements = Symbol('elements');
+export const length = Symbol('length');
+export const complete = Symbol('complete');
+export const naturalHeight = Symbol('naturalHeight');
+export const naturalWidth = Symbol('naturalWidth');
+export const loading = Symbol('loading');
+export const x = Symbol('x');
+export const y = Symbol('y');
+export const defaultChecked = Symbol('defaultChecked');
+export const files = Symbol('files');
+export const sheet = Symbol('sheet');
+export const volume = Symbol('volume');
+export const paused = Symbol('paused');
+export const currentTime = Symbol('currentTime');
+export const playbackRate = Symbol('playbackRate');
+export const defaultPlaybackRate = Symbol('defaultPlaybackRate');
+export const muted = Symbol('muted');
+export const defaultMuted = Symbol('defaultMuted');
+export const preservesPitch = Symbol('preservesPitch');
+export const buffered = Symbol('buffered');
+export const duration = Symbol('duration');
+export const error = Symbol('error');
+export const ended = Symbol('ended');
+export const networkState = Symbol('networkState');
+export const textTracks = Symbol('textTracks');
+export const videoTracks = Symbol('videoTracks');
+export const seeking = Symbol('seeking');
+export const seekable = Symbol('seekable');
+export const played = Symbol('played');
+export const options = Symbol('options');
+export const content = Symbol('content');
+export const mode = Symbol('mode');
+export const host = Symbol('host');
diff --git a/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts b/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts
index ae906b455..2b066c1e5 100644
--- a/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts
+++ b/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts
@@ -1,58 +1,46 @@
+// We need to set this as a global constant, so that using fake timers in Jest and Vitest won't override this on the global object.
+const TIMER = {
+ setImmediate: setImmediate,
+ clearImmediate: clearImmediate,
+ clearTimeout: clearTimeout
+};
+
/**
* Handles async tasks.
*/
export default class AsyncTaskManager {
private static taskID = 0;
- private runningTasks: { [k: string]: () => void } = {};
+ private runningTasks: { [k: string]: (destroy: boolean) => void } = {};
private runningTaskCount = 0;
private runningTimers: NodeJS.Timeout[] = [];
private runningImmediates: NodeJS.Immediate[] = [];
- private whenCompleteImmediate: NodeJS.Immediate | null = null;
- private whenCompleteResolvers: Array<() => void> = [];
+ private waitUntilCompleteTimer: NodeJS.Immediate | null = null;
+ private waitUntilCompleteResolvers: Array<() => void> = [];
/**
* Returns a promise that is resolved when async tasks are complete.
*
* @returns Promise.
*/
- public whenComplete(): Promise {
+ public waitUntilComplete(): Promise {
return new Promise((resolve) => {
- this.whenCompleteResolvers.push(resolve);
+ this.waitUntilCompleteResolvers.push(resolve);
this.endTask(this.startTask());
});
}
/**
- * Cancels all tasks.
+ * Aborts all tasks.
*/
- public cancelAll(): void {
- const runningTimers = this.runningTimers;
- const runningImmediates = this.runningImmediates;
- const runningTasks = this.runningTasks;
-
- this.runningTasks = {};
- this.runningTaskCount = 0;
- this.runningImmediates = [];
- this.runningTimers = [];
-
- if (this.whenCompleteImmediate) {
- global.clearImmediate(this.whenCompleteImmediate);
- this.whenCompleteImmediate = null;
- }
-
- for (const immediate of runningImmediates) {
- global.clearImmediate(immediate);
- }
-
- for (const timer of runningTimers) {
- global.clearTimeout(timer);
- }
-
- for (const key of Object.keys(runningTasks)) {
- runningTasks[key]();
- }
+ public abort(): Promise {
+ return this.abortAll(false);
+ }
- this.resolveWhenComplete();
+ /**
+ * Destroys the manager.
+ */
+ public destroy(): Promise {
+ return this.abortAll(true);
}
/**
@@ -125,12 +113,12 @@ export default class AsyncTaskManager {
if (this.runningTasks[taskID]) {
delete this.runningTasks[taskID];
this.runningTaskCount--;
- if (this.whenCompleteImmediate) {
- global.clearImmediate(this.whenCompleteImmediate);
+ if (this.waitUntilCompleteTimer) {
+ TIMER.clearImmediate(this.waitUntilCompleteTimer);
}
if (!this.runningTaskCount && !this.runningTimers.length && !this.runningImmediates.length) {
- this.whenCompleteImmediate = global.setImmediate(() => {
- this.whenCompleteImmediate = null;
+ this.waitUntilCompleteTimer = TIMER.setImmediate(() => {
+ this.waitUntilCompleteTimer = null;
if (
!this.runningTaskCount &&
!this.runningTimers.length &&
@@ -166,10 +154,46 @@ export default class AsyncTaskManager {
* Resolves when complete.
*/
private resolveWhenComplete(): void {
- const resolvers = this.whenCompleteResolvers;
- this.whenCompleteResolvers = [];
+ const resolvers = this.waitUntilCompleteResolvers;
+ this.waitUntilCompleteResolvers = [];
for (const resolver of resolvers) {
resolver();
}
}
+
+ /**
+ * Aborts all tasks.
+ *
+ * @param destroy Destroy.
+ */
+ private abortAll(destroy: boolean): Promise {
+ const runningTimers = this.runningTimers;
+ const runningImmediates = this.runningImmediates;
+ const runningTasks = this.runningTasks;
+
+ this.runningTasks = {};
+ this.runningTaskCount = 0;
+ this.runningImmediates = [];
+ this.runningTimers = [];
+
+ if (this.waitUntilCompleteTimer) {
+ TIMER.clearImmediate(this.waitUntilCompleteTimer);
+ this.waitUntilCompleteTimer = null;
+ }
+
+ for (const immediate of runningImmediates) {
+ TIMER.clearImmediate(immediate);
+ }
+
+ for (const timer of runningTimers) {
+ TIMER.clearTimeout(timer);
+ }
+
+ for (const key of Object.keys(runningTasks)) {
+ runningTasks[key](destroy);
+ }
+
+ // We need to wait for microtasks to complete before resolving.
+ return this.waitUntilComplete();
+ }
}
diff --git a/packages/happy-dom/src/browser/Browser.ts b/packages/happy-dom/src/browser/Browser.ts
new file mode 100644
index 000000000..0df1f0407
--- /dev/null
+++ b/packages/happy-dom/src/browser/Browser.ts
@@ -0,0 +1,104 @@
+import IBrowserSettings from './types/IBrowserSettings.js';
+import BrowserContext from './BrowserContext.js';
+import IOptionalBrowserSettings from './types/IOptionalBrowserSettings.js';
+import BrowserSettingsFactory from './BrowserSettingsFactory.js';
+import BrowserPage from './BrowserPage.js';
+import IBrowser from './types/IBrowser.js';
+
+/**
+ * Browser.
+ *
+ * Much of the interface for the browser has been taken from Puppeteer and Playwright, so that the API is familiar.
+ */
+export default class Browser implements IBrowser {
+ public readonly contexts: BrowserContext[];
+ public readonly settings: IBrowserSettings;
+ public readonly console: Console | null;
+
+ /**
+ * Constructor.
+ *
+ * @param [options] Options.
+ * @param [options.settings] Browser settings.
+ * @param [options.console] Console.
+ */
+ constructor(options?: { settings?: IOptionalBrowserSettings; console?: Console }) {
+ this.console = options?.console || null;
+ this.settings = BrowserSettingsFactory.getSettings(options?.settings);
+ this.contexts = [new BrowserContext(this)];
+ }
+
+ /**
+ * Returns the default context.
+ *
+ * @returns Default context.
+ */
+ public get defaultContext(): BrowserContext {
+ if (this.contexts.length === 0) {
+ throw new Error('No default context. The browser has been closed.');
+ }
+ return this.contexts[0];
+ }
+
+ /**
+ * Aborts all ongoing operations and destroys the browser.
+ */
+ public async close(): Promise {
+ await Promise.all(this.contexts.slice().map((context) => context.close()));
+ (this.contexts) = [];
+ }
+
+ /**
+ * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete.
+ *
+ * @returns Promise.
+ */
+ public async waitUntilComplete(): Promise {
+ if (this.contexts.length === 0) {
+ throw new Error('No default context. The browser has been closed.');
+ }
+ await Promise.all(this.contexts.map((page) => page.waitUntilComplete()));
+ }
+
+ /**
+ * Aborts all ongoing operations.
+ */
+ public abort(): Promise {
+ // Using Promise instead of async/await to prevent microtask
+ return new Promise((resolve, reject) => {
+ if (!this.contexts.length) {
+ resolve();
+ return;
+ }
+ Promise.all(this.contexts.slice().map((context) => context.abort()))
+ .then(() => resolve())
+ .catch((error) => reject(error));
+ });
+ }
+
+ /**
+ * Creates a new incognito context.
+ *
+ * @returns Context.
+ */
+ public newIncognitoContext(): BrowserContext {
+ if (this.contexts.length === 0) {
+ throw new Error('No default context. The browser has been closed.');
+ }
+ const context = new BrowserContext(this);
+ this.contexts.push(context);
+ return context;
+ }
+
+ /**
+ * Creates a new page.
+ *
+ * @returns Page.
+ */
+ public newPage(): BrowserPage {
+ if (this.contexts.length === 0) {
+ throw new Error('No default context. The browser has been closed.');
+ }
+ return this.contexts[0].newPage();
+ }
+}
diff --git a/packages/happy-dom/src/browser/BrowserContext.ts b/packages/happy-dom/src/browser/BrowserContext.ts
new file mode 100644
index 000000000..f0123eb1e
--- /dev/null
+++ b/packages/happy-dom/src/browser/BrowserContext.ts
@@ -0,0 +1,89 @@
+import CookieContainer from '../cookie/CookieContainer.js';
+import ICookieContainer from '../cookie/types/ICookieContainer.js';
+import ResponseCache from '../fetch/cache/response/ResponseCache.js';
+import IResponseCache from '../fetch/cache/response/IResponseCache.js';
+import Browser from './Browser.js';
+import BrowserPage from './BrowserPage.js';
+import IBrowserContext from './types/IBrowserContext.js';
+import IPreflightResponseCache from '../fetch/cache/preflight/IPreflightResponseCache.js';
+import PreflightResponseCache from '../fetch/cache/preflight/PreflightResponseCache.js';
+
+/**
+ * Browser context.
+ */
+export default class BrowserContext implements IBrowserContext {
+ public readonly pages: BrowserPage[] = [];
+ public readonly browser: Browser;
+ public readonly cookieContainer: ICookieContainer = new CookieContainer();
+ public readonly responseCache: IResponseCache = new ResponseCache();
+ public readonly preflightResponseCache: IPreflightResponseCache = new PreflightResponseCache();
+
+ /**
+ * Constructor.
+ *
+ * @param browser
+ */
+ constructor(browser: Browser) {
+ this.browser = browser;
+ }
+
+ /**
+ * Aborts all ongoing operations and destroys the context.
+ */
+ public async close(): Promise {
+ if (!this.browser) {
+ return;
+ }
+ await Promise.all(this.pages.slice().map((page) => page.close()));
+ const browser = this.browser;
+ const index = browser.contexts.indexOf(this);
+ if (index !== -1) {
+ browser.contexts.splice(index, 1);
+ }
+ (this.pages) = [];
+ (this.browser) = null;
+ (this.cookieContainer) = null;
+ this.responseCache.clear();
+ this.preflightResponseCache.clear();
+ (this.responseCache) = null;
+ (this.preflightResponseCache) = null;
+ if (browser.contexts.length === 0) {
+ browser.close();
+ }
+ }
+
+ /**
+ * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete.
+ *
+ * @returns Promise.
+ */
+ public async waitUntilComplete(): Promise {
+ await Promise.all(this.pages.map((page) => page.waitUntilComplete()));
+ }
+
+ /**
+ * Aborts all ongoing operations.
+ */
+ public abort(): Promise {
+ return new Promise((resolve, reject) => {
+ if (!this.pages.length) {
+ resolve();
+ return;
+ }
+ Promise.all(this.pages.slice().map((page) => page.abort()))
+ .then(() => resolve())
+ .catch((error) => reject(error));
+ });
+ }
+
+ /**
+ * Creates a new page.
+ *
+ * @returns Page.
+ */
+ public newPage(): BrowserPage {
+ const page = new BrowserPage(this);
+ this.pages.push(page);
+ return page;
+ }
+}
diff --git a/packages/happy-dom/src/browser/BrowserFrame.ts b/packages/happy-dom/src/browser/BrowserFrame.ts
new file mode 100644
index 000000000..fccaff3c8
--- /dev/null
+++ b/packages/happy-dom/src/browser/BrowserFrame.ts
@@ -0,0 +1,168 @@
+import BrowserPage from './BrowserPage.js';
+import * as PropertySymbol from '../PropertySymbol.js';
+import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js';
+import IBrowserFrame from './types/IBrowserFrame.js';
+import BrowserWindow from '../window/BrowserWindow.js';
+import IBrowserWindow from '../window/IBrowserWindow.js';
+import ICrossOriginBrowserWindow from '../window/ICrossOriginBrowserWindow.js';
+import Location from '../location/Location.js';
+import IResponse from '../fetch/types/IResponse.js';
+import IGoToOptions from './types/IGoToOptions.js';
+import { Script } from 'vm';
+import BrowserFrameURL from './utilities/BrowserFrameURL.js';
+import BrowserFrameScriptEvaluator from './utilities/BrowserFrameScriptEvaluator.js';
+import BrowserFrameNavigator from './utilities/BrowserFrameNavigator.js';
+import IReloadOptions from './types/IReloadOptions.js';
+import BrowserFrameExceptionObserver from './utilities/BrowserFrameExceptionObserver.js';
+import BrowserErrorCaptureEnum from './enums/BrowserErrorCaptureEnum.js';
+import IDocument from '../nodes/document/IDocument.js';
+
+/**
+ * Browser frame.
+ */
+export default class BrowserFrame implements IBrowserFrame {
+ public readonly childFrames: BrowserFrame[] = [];
+ public readonly parentFrame: BrowserFrame | null = null;
+ public readonly page: BrowserPage;
+ public readonly window: BrowserWindow;
+ public [PropertySymbol.asyncTaskManager] = new AsyncTaskManager();
+ public [PropertySymbol.exceptionObserver]: BrowserFrameExceptionObserver | null = null;
+ public [PropertySymbol.listeners]: { navigation: Array<() => void> } = { navigation: [] };
+ public [PropertySymbol.openerFrame]: IBrowserFrame | null = null;
+ public [PropertySymbol.openerWindow]: IBrowserWindow | ICrossOriginBrowserWindow | null = null;
+ public [PropertySymbol.popup] = false;
+
+ /**
+ * Constructor.
+ *
+ * @param page Page.
+ */
+ constructor(page: BrowserPage) {
+ this.page = page;
+ this.window = new BrowserWindow(this);
+
+ // Attach process level error capturing.
+ if (page.context.browser.settings.errorCapture === BrowserErrorCaptureEnum.processLevel) {
+ this[PropertySymbol.exceptionObserver] = new BrowserFrameExceptionObserver();
+ this[PropertySymbol.exceptionObserver].observe(this);
+ }
+ }
+
+ /**
+ * Returns the content.
+ *
+ * @returns Content.
+ */
+ public get content(): string {
+ return this.window.document.documentElement.outerHTML;
+ }
+
+ /**
+ * Sets the content.
+ *
+ * @param content Content.
+ */
+ public set content(content) {
+ this.window.document[PropertySymbol.isFirstWrite] = true;
+ this.window.document[PropertySymbol.isFirstWriteAfterOpen] = false;
+ this.window.document.open();
+ this.window.document.write(content);
+ }
+
+ /**
+ * Returns the URL.
+ *
+ * @returns URL.
+ */
+ public get url(): string {
+ return this.window.location.href;
+ }
+
+ /**
+ * Sets the content.
+ *
+ * @param url URL.
+ */
+ public set url(url) {
+ (this.window.location) = new Location(
+ this,
+ BrowserFrameURL.getRelativeURL(this, url).href
+ );
+ }
+
+ /**
+ * Returns document.
+ *
+ * @returns Document.
+ */
+ public get document(): IDocument {
+ return this.window?.document ?? null;
+ }
+
+ /**
+ * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete.
+ */
+ public async waitUntilComplete(): Promise {
+ await Promise.all([
+ this[PropertySymbol.asyncTaskManager].waitUntilComplete(),
+ ...this.childFrames.map((frame) => frame.waitUntilComplete())
+ ]);
+ }
+
+ /**
+ * Returns a promise that is resolved when the frame has navigated and the response HTML has been written to the document.
+ */
+ public waitForNavigation(): Promise {
+ return new Promise((resolve) => this[PropertySymbol.listeners].navigation.push(resolve));
+ }
+
+ /**
+ * Aborts all ongoing operations.
+ */
+ public abort(): Promise {
+ if (!this.childFrames.length) {
+ return this[PropertySymbol.asyncTaskManager].abort();
+ }
+ return new Promise((resolve, reject) => {
+ // Using Promise instead of async/await to prevent microtask
+ Promise.all(
+ this.childFrames
+ .map((frame) => frame.abort())
+ .concat([this[PropertySymbol.asyncTaskManager].abort()])
+ )
+ .then(() => resolve())
+ .catch(reject);
+ });
+ }
+
+ /**
+ * Evaluates code or a VM Script in the page's context.
+ *
+ * @param script Script.
+ * @returns Result.
+ */
+ public evaluate(script: string | Script): any {
+ return BrowserFrameScriptEvaluator.evaluate(this, script);
+ }
+
+ /**
+ * Go to a page.
+ *
+ * @param url URL.
+ * @param [options] Options.
+ * @returns Response.
+ */
+ public goto(url: string, options?: IGoToOptions): Promise {
+ return BrowserFrameNavigator.goto(BrowserWindow, this, url, options);
+ }
+
+ /**
+ * Reloads the current frame.
+ *
+ * @param [options] Options.
+ * @returns Response.
+ */
+ public reload(options: IReloadOptions): Promise {
+ return BrowserFrameNavigator.goto(BrowserWindow, this, this.url, options);
+ }
+}
diff --git a/packages/happy-dom/src/browser/BrowserPage.ts b/packages/happy-dom/src/browser/BrowserPage.ts
new file mode 100644
index 000000000..77c665105
--- /dev/null
+++ b/packages/happy-dom/src/browser/BrowserPage.ts
@@ -0,0 +1,153 @@
+import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js';
+import BrowserFrame from './BrowserFrame.js';
+import BrowserContext from './BrowserContext.js';
+import VirtualConsole from '../console/VirtualConsole.js';
+import IBrowserPage from './types/IBrowserPage.js';
+import BrowserPageUtility from './utilities/BrowserPageUtility.js';
+import { Script } from 'vm';
+import IGoToOptions from './types/IGoToOptions.js';
+import IResponse from '../fetch/types/IResponse.js';
+import IReloadOptions from './types/IReloadOptions.js';
+import IBrowserPageViewport from './types/IBrowserPageViewport.js';
+import IOptionalBrowserPageViewport from './types/IOptionalBrowserPageViewport.js';
+import DefaultBrowserPageViewport from './DefaultBrowserPageViewport.js';
+import Event from '../event/Event.js';
+
+/**
+ * Browser page.
+ */
+export default class BrowserPage implements IBrowserPage {
+ public readonly virtualConsolePrinter = new VirtualConsolePrinter();
+ public readonly mainFrame: BrowserFrame;
+ public readonly context: BrowserContext;
+ public readonly console: Console;
+ public readonly viewport: IBrowserPageViewport = Object.assign({}, DefaultBrowserPageViewport);
+
+ /**
+ * Constructor.
+ *
+ * @param context Browser context.
+ */
+ constructor(context: BrowserContext) {
+ this.context = context;
+ this.console = context.browser.console ?? new VirtualConsole(this.virtualConsolePrinter);
+ this.mainFrame = new BrowserFrame(this);
+ }
+
+ /**
+ * Returns frames.
+ */
+ public get frames(): BrowserFrame[] {
+ return BrowserPageUtility.getFrames(this);
+ }
+
+ /**
+ * Returns the viewport.
+ */
+ public get content(): string {
+ return this.mainFrame.content;
+ }
+
+ /**
+ * Sets the content.
+ *
+ * @param content Content.
+ */
+ public set content(content) {
+ this.mainFrame.content = content;
+ }
+
+ /**
+ * Returns the URL.
+ *
+ * @returns URL.
+ */
+ public get url(): string {
+ return this.mainFrame.url;
+ }
+
+ /**
+ * Sets the content.
+ *
+ * @param url URL.
+ */
+ public set url(url) {
+ this.mainFrame.url = url;
+ }
+
+ /**
+ * Aborts all ongoing operations and destroys the page.
+ */
+ public close(): Promise {
+ return BrowserPageUtility.closePage(this);
+ }
+
+ /**
+ * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete.
+ */
+ public waitUntilComplete(): Promise {
+ return this.mainFrame.waitUntilComplete();
+ }
+
+ /**
+ * Returns a promise that is resolved when the page has navigated and the response HTML has been written to the document.
+ */
+ public waitForNavigation(): Promise {
+ return this.mainFrame.waitForNavigation();
+ }
+
+ /**
+ * Aborts all ongoing operations.
+ */
+ public abort(): Promise {
+ return this.mainFrame.abort();
+ }
+
+ /**
+ * Evaluates code or a VM Script in the page's context.
+ *
+ * @param script Script.
+ * @returns Result.
+ */
+ public evaluate(script: string | Script): any {
+ return this.mainFrame.evaluate(script);
+ }
+
+ /**
+ * Sets the viewport.
+ *
+ * @param viewport Viewport.
+ */
+ public setViewport(viewport: IOptionalBrowserPageViewport): void {
+ const previousViewport = Object.assign({}, this.viewport);
+ Object.assign(this.viewport, viewport);
+ if (
+ previousViewport.width !== this.viewport.width ||
+ previousViewport.height !== this.viewport.height ||
+ previousViewport.devicePixelRatio !== this.viewport.devicePixelRatio
+ ) {
+ this.mainFrame.window.dispatchEvent(new Event('resize'));
+ }
+ }
+
+ /**
+ * Go to a page.
+ *
+ * @param url URL.
+ * @param [options] Options.
+ * @returns Response.
+ */
+ public goto(url: string, options?: IGoToOptions): Promise {
+ return this.mainFrame.goto(url, options);
+ }
+
+ /**
+ * Reloads the current page.
+ *
+ * @param [options] Options.
+ * @returns Response.
+ */
+ public reload(options: IReloadOptions): Promise {
+ return this.mainFrame.reload(options);
+ }
+}
diff --git a/packages/happy-dom/src/browser/BrowserSettingsFactory.ts b/packages/happy-dom/src/browser/BrowserSettingsFactory.ts
new file mode 100644
index 000000000..7ed799ab2
--- /dev/null
+++ b/packages/happy-dom/src/browser/BrowserSettingsFactory.ts
@@ -0,0 +1,34 @@
+import IBrowserSettings from './types/IBrowserSettings.js';
+import IOptionalBrowserSettings from './types/IOptionalBrowserSettings.js';
+import DefaultBrowserSettings from './DefaultBrowserSettings.js';
+
+/**
+ * Browser settings utility.
+ */
+export default class BrowserSettingsFactory {
+ /**
+ * Returns browser settings.
+ *
+ * @param [settings] Browser settings.
+ * @param [freezeObject] "true" to freeze the object.
+ * @returns Settings.
+ */
+ public static getSettings(settings?: IOptionalBrowserSettings): IBrowserSettings {
+ return {
+ ...DefaultBrowserSettings,
+ ...settings,
+ navigation: {
+ ...DefaultBrowserSettings.navigation,
+ ...settings?.navigation
+ },
+ navigator: {
+ ...DefaultBrowserSettings.navigator,
+ ...settings?.navigator
+ },
+ device: {
+ ...DefaultBrowserSettings.device,
+ ...settings?.device
+ }
+ };
+ }
+}
diff --git a/packages/happy-dom/src/browser/DefaultBrowserPageViewport.ts b/packages/happy-dom/src/browser/DefaultBrowserPageViewport.ts
new file mode 100644
index 000000000..616bc4b03
--- /dev/null
+++ b/packages/happy-dom/src/browser/DefaultBrowserPageViewport.ts
@@ -0,0 +1,7 @@
+import IBrowserPageViewport from './types/IBrowserPageViewport.js';
+
+export default {
+ width: 1024,
+ height: 768,
+ devicePixelRatio: 1
+};
diff --git a/packages/happy-dom/src/browser/DefaultBrowserSettings.ts b/packages/happy-dom/src/browser/DefaultBrowserSettings.ts
new file mode 100644
index 000000000..808d3f347
--- /dev/null
+++ b/packages/happy-dom/src/browser/DefaultBrowserSettings.ts
@@ -0,0 +1,32 @@
+import PackageVersion from '../version.js';
+import BrowserErrorCaptureEnum from './enums/BrowserErrorCaptureEnum.js';
+import BrowserNavigationCrossOriginPolicyEnum from './enums/BrowserNavigationCrossOriginPolicyEnum.js';
+import IBrowserSettings from './types/IBrowserSettings.js';
+
+export default {
+ disableJavaScriptEvaluation: false,
+ disableJavaScriptFileLoading: false,
+ disableCSSFileLoading: false,
+ disableIframePageLoading: false,
+ disableComputedStyleRendering: false,
+ disableErrorCapturing: false,
+ errorCapture: BrowserErrorCaptureEnum.tryAndCatch,
+ enableFileSystemHttpRequests: false,
+ navigation: {
+ disableMainFrameNavigation: false,
+ disableChildFrameNavigation: false,
+ disableChildPageNavigation: false,
+ disableFallbackToSetURL: false,
+ crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.anyOrigin
+ },
+ navigator: {
+ userAgent: `Mozilla/5.0 (X11; ${
+ process.platform.charAt(0).toUpperCase() + process.platform.slice(1) + ' ' + process.arch
+ }) AppleWebKit/537.36 (KHTML, like Gecko) HappyDOM/${PackageVersion.version}`,
+ maxTouchPoints: 0
+ },
+ device: {
+ prefersColorScheme: 'light',
+ mediaType: 'screen'
+ }
+};
diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts
new file mode 100644
index 000000000..4959d35fb
--- /dev/null
+++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts
@@ -0,0 +1,111 @@
+import IBrowserSettings from '../types/IBrowserSettings.js';
+import DetachedBrowserContext from './DetachedBrowserContext.js';
+import IOptionalBrowserSettings from '../types/IOptionalBrowserSettings.js';
+import BrowserSettingsFactory from '../BrowserSettingsFactory.js';
+import DetachedBrowserPage from './DetachedBrowserPage.js';
+import IBrowser from '../types/IBrowser.js';
+import IBrowserFrame from '../types/IBrowserFrame.js';
+import IBrowserWindow from '../../window/IBrowserWindow.js';
+
+/**
+ * Detached browser used when constructing a Window instance without a browser.
+ *
+ * Much of the interface for the browser has been taken from Puppeteer and Playwright, so that the API is familiar.
+ */
+export default class DetachedBrowser implements IBrowser {
+ public readonly contexts: DetachedBrowserContext[];
+ public readonly settings: IBrowserSettings;
+ public readonly console: Console | null;
+ public readonly windowClass: new (
+ browserFrame: IBrowserFrame,
+ options?: { url?: string; width?: number; height?: number }
+ ) => IBrowserWindow | null;
+
+ /**
+ * Constructor.
+ *
+ * @param windowClass Window class.
+ * @param [options] Options.
+ * @param [options.settings] Browser settings.
+ * @param [options.console] Console.
+ */
+ constructor(
+ windowClass: new (
+ browserFrame: IBrowserFrame,
+ options?: { url?: string; width?: number; height?: number }
+ ) => IBrowserWindow,
+ options?: { settings?: IOptionalBrowserSettings; console?: Console }
+ ) {
+ this.windowClass = windowClass;
+ this.console = options?.console || null;
+ this.settings = BrowserSettingsFactory.getSettings(options?.settings);
+ this.contexts = [];
+ this.contexts.push(new DetachedBrowserContext(this));
+ }
+
+ /**
+ * Returns the default context.
+ *
+ * @returns Default context.
+ */
+ public get defaultContext(): DetachedBrowserContext {
+ if (this.contexts.length === 0) {
+ throw new Error('No default context. The browser has been closed.');
+ }
+ return this.contexts[0];
+ }
+
+ /**
+ * Aborts all ongoing operations and destroys the browser.
+ */
+ public async close(): Promise {
+ await Promise.all(this.contexts.slice().map((context) => context.close()));
+ (this.contexts) = [];
+ (this.console) = null;
+ ( IBrowserWindow | null>this.windowClass) = null;
+ }
+
+ /**
+ * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete.
+ *
+ * @returns Promise.
+ */
+ public async waitUntilComplete(): Promise {
+ await Promise.all(this.contexts.map((page) => page.waitUntilComplete()));
+ }
+
+ /**
+ * Aborts all ongoing operations.
+ */
+ public abort(): Promise {
+ // Using Promise instead of async/await to prevent microtask
+ return new Promise((resolve, reject) => {
+ if (!this.contexts.length) {
+ resolve();
+ return;
+ }
+ Promise.all(this.contexts.slice().map((context) => context.abort()))
+ .then(() => resolve())
+ .catch((error) => reject(error));
+ });
+ }
+
+ /**
+ * Creates a new incognito context.
+ */
+ public newIncognitoContext(): DetachedBrowserContext {
+ throw new Error('Not possible to create a new context on a detached browser.');
+ }
+
+ /**
+ * Creates a new page.
+ *
+ * @returns Page.
+ */
+ public newPage(): DetachedBrowserPage {
+ if (this.contexts.length === 0) {
+ throw new Error('No default context. The browser has been closed.');
+ }
+ return this.contexts[0].newPage();
+ }
+}
diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts
new file mode 100644
index 000000000..c1e73419a
--- /dev/null
+++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts
@@ -0,0 +1,92 @@
+import DetachedBrowser from './DetachedBrowser.js';
+import DetachedBrowserPage from './DetachedBrowserPage.js';
+import IBrowserContext from '../types/IBrowserContext.js';
+import ICookieContainer from '../../cookie/types/ICookieContainer.js';
+import CookieContainer from '../../cookie/CookieContainer.js';
+import ResponseCache from '../../fetch/cache/response/ResponseCache.js';
+import IResponseCache from '../../fetch/cache/response/IResponseCache.js';
+import IPreflightResponseCache from '../../fetch/cache/preflight/IPreflightResponseCache.js';
+import PreflightResponseCache from '../../fetch/cache/preflight/PreflightResponseCache.js';
+
+/**
+ * Detached browser context used when constructing a Window instance without a browser.
+ */
+export default class DetachedBrowserContext implements IBrowserContext {
+ public readonly pages: DetachedBrowserPage[];
+ public readonly browser: DetachedBrowser;
+ public readonly cookieContainer: ICookieContainer = new CookieContainer();
+ public readonly responseCache: IResponseCache = new ResponseCache();
+ public readonly preflightResponseCache: IPreflightResponseCache = new PreflightResponseCache();
+
+ /**
+ * Constructor.
+ *
+ * @param browser Browser.
+ */
+ constructor(browser: DetachedBrowser) {
+ this.browser = browser;
+ this.pages = [];
+ this.pages.push(new DetachedBrowserPage(this));
+ }
+
+ /**
+ * Aborts all ongoing operations and destroys the context.
+ */
+ public async close(): Promise {
+ if (!this.browser) {
+ return;
+ }
+ await Promise.all(this.pages.slice().map((page) => page.close()));
+ const browser = this.browser;
+ const index = browser.contexts.indexOf(this);
+ if (index !== -1) {
+ browser.contexts.splice(index, 1);
+ }
+ (this.pages) = [];
+ (this.browser) = null;
+ (this.cookieContainer) = null;
+ this.responseCache.clear();
+ this.preflightResponseCache.clear();
+ (this.responseCache) = null;
+ (this.preflightResponseCache) = null;
+ if (browser.contexts.length === 0) {
+ browser.close();
+ }
+ }
+
+ /**
+ * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete.
+ *
+ * @returns Promise.
+ */
+ public async waitUntilComplete(): Promise {
+ await Promise.all(this.pages.map((page) => page.waitUntilComplete()));
+ }
+
+ /**
+ * Aborts all ongoing operations.
+ */
+ public abort(): Promise {
+ return new Promise((resolve, reject) => {
+ if (!this.pages.length) {
+ resolve();
+ return;
+ }
+ Promise.all(this.pages.slice().map((page) => page.abort()))
+ .then(() => resolve())
+ .catch((error) => reject(error));
+ });
+ }
+
+ /**
+ * Creates a new page.
+ *
+ * @param [opener] Opener.
+ * @returns Page.
+ */
+ public newPage(): DetachedBrowserPage {
+ const page = new DetachedBrowserPage(this);
+ this.pages.push(page);
+ return page;
+ }
+}
diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts
new file mode 100644
index 000000000..f602d9136
--- /dev/null
+++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts
@@ -0,0 +1,188 @@
+import DetachedBrowserPage from './DetachedBrowserPage.js';
+import * as PropertySymbol from '../../PropertySymbol.js';
+import AsyncTaskManager from '../../async-task-manager/AsyncTaskManager.js';
+import IBrowserFrame from '../types/IBrowserFrame.js';
+import Location from '../../location/Location.js';
+import IResponse from '../../fetch/types/IResponse.js';
+import IGoToOptions from '../types/IGoToOptions.js';
+import { Script } from 'vm';
+import BrowserFrameURL from '../utilities/BrowserFrameURL.js';
+import BrowserFrameScriptEvaluator from '../utilities/BrowserFrameScriptEvaluator.js';
+import BrowserFrameNavigator from '../utilities/BrowserFrameNavigator.js';
+import IBrowserWindow from '../../window/IBrowserWindow.js';
+import IReloadOptions from '../types/IReloadOptions.js';
+import BrowserErrorCaptureEnum from '../enums/BrowserErrorCaptureEnum.js';
+import BrowserFrameExceptionObserver from '../utilities/BrowserFrameExceptionObserver.js';
+import IDocument from '../../nodes/document/IDocument.js';
+import ICrossOriginBrowserWindow from '../../window/ICrossOriginBrowserWindow.js';
+
+/**
+ * Browser frame used when constructing a Window instance without a browser.
+ */
+export default class DetachedBrowserFrame implements IBrowserFrame {
+ public readonly childFrames: DetachedBrowserFrame[] = [];
+ public readonly parentFrame: DetachedBrowserFrame | null = null;
+ public readonly page: DetachedBrowserPage;
+ // Needs to be injected from the outside when the browser frame is constructed.
+ public window: IBrowserWindow;
+ public [PropertySymbol.asyncTaskManager] = new AsyncTaskManager();
+ public [PropertySymbol.exceptionObserver]: BrowserFrameExceptionObserver | null = null;
+ public [PropertySymbol.listeners]: { navigation: Array<() => void> } = { navigation: [] };
+ public [PropertySymbol.openerFrame]: IBrowserFrame | null = null;
+ public [PropertySymbol.openerWindow]: IBrowserWindow | ICrossOriginBrowserWindow | null = null;
+ public [PropertySymbol.popup] = false;
+
+ /**
+ * Constructor.
+ *
+ * @param page Page.
+ * @param [window] Window.
+ */
+ constructor(page: DetachedBrowserPage) {
+ this.page = page;
+ if (page.context.browser.contexts[0]?.pages[0]?.mainFrame) {
+ this.window = new this.page.context.browser.windowClass(this);
+ }
+
+ // Attach process level error capturing.
+ if (page.context.browser.settings.errorCapture === BrowserErrorCaptureEnum.processLevel) {
+ this[PropertySymbol.exceptionObserver] = new BrowserFrameExceptionObserver();
+ this[PropertySymbol.exceptionObserver].observe(this);
+ }
+ }
+
+ /**
+ * Returns the content.
+ *
+ * @returns Content.
+ */
+ public get content(): string {
+ if (!this.window) {
+ throw new Error('The frame has been destroyed, the "window" property is not set.');
+ }
+ return this.window.document.documentElement.outerHTML;
+ }
+
+ /**
+ * Sets the content.
+ *
+ * @param content Content.
+ */
+ public set content(content) {
+ if (!this.window) {
+ throw new Error('The frame has been destroyed, the "window" property is not set.');
+ }
+ this.window.document[PropertySymbol.isFirstWrite] = true;
+ this.window.document[PropertySymbol.isFirstWriteAfterOpen] = false;
+ this.window.document.open();
+ this.window.document.write(content);
+ }
+
+ /**
+ * Returns the URL.
+ *
+ * @returns URL.
+ */
+ public get url(): string {
+ if (!this.window) {
+ throw new Error('The frame has been destroyed, the "window" property is not set.');
+ }
+ return this.window.location.href;
+ }
+
+ /**
+ * Sets the content.
+ *
+ * @param url URL.
+ */
+ public set url(url) {
+ if (!this.window) {
+ throw new Error('The frame has been destroyed, the "window" property is not set.');
+ }
+ (this.window.location) = new Location(
+ this,
+ BrowserFrameURL.getRelativeURL(this, url).href
+ );
+ }
+
+ /**
+ * Returns document.
+ *
+ * @returns Document.
+ */
+ public get document(): IDocument {
+ return this.window?.document ?? null;
+ }
+
+ /**
+ * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete.
+ */
+ public async waitUntilComplete(): Promise {
+ await Promise.all([
+ this[PropertySymbol.asyncTaskManager].waitUntilComplete(),
+ ...this.childFrames.map((frame) => frame.waitUntilComplete())
+ ]);
+ }
+
+ /**
+ * Returns a promise that is resolved when the frame has navigated and the response HTML has been written to the document.
+ */
+ public waitForNavigation(): Promise {
+ return new Promise((resolve) => this[PropertySymbol.listeners].navigation.push(resolve));
+ }
+
+ /**
+ * Aborts all ongoing operations.
+ */
+ public abort(): Promise {
+ if (!this.childFrames.length) {
+ return this[PropertySymbol.asyncTaskManager].abort();
+ }
+ return new Promise((resolve, reject) => {
+ // Using Promise instead of async/await to prevent microtask
+ Promise.all(
+ this.childFrames
+ .map((frame) => frame.abort())
+ .concat([this[PropertySymbol.asyncTaskManager].abort()])
+ )
+ .then(() => resolve())
+ .catch(reject);
+ });
+ }
+
+ /**
+ * Evaluates code or a VM Script in the page's context.
+ *
+ * @param script Script.
+ * @returns Result.
+ */
+ public evaluate(script: string | Script): any {
+ return BrowserFrameScriptEvaluator.evaluate(this, script);
+ }
+
+ /**
+ * Go to a page.
+ *
+ * @param url URL.
+ * @param [options] Options.
+ * @returns Response.
+ */
+ public goto(url: string, options?: IGoToOptions): Promise {
+ return BrowserFrameNavigator.goto(this.page.context.browser.windowClass, this, url, options);
+ }
+
+ /**
+ * Reloads the current frame.
+ *
+ * @param [options] Options.
+ * @returns Response.
+ */
+ public reload(options: IReloadOptions): Promise {
+ return BrowserFrameNavigator.goto(
+ this.page.context.browser.windowClass,
+ this,
+ this.url,
+ options
+ );
+ }
+}
diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts
new file mode 100644
index 000000000..943d27463
--- /dev/null
+++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts
@@ -0,0 +1,165 @@
+import VirtualConsolePrinter from '../../console/VirtualConsolePrinter.js';
+import DetachedBrowserFrame from './DetachedBrowserFrame.js';
+import DetachedBrowserContext from './DetachedBrowserContext.js';
+import VirtualConsole from '../../console/VirtualConsole.js';
+import IBrowserPage from '../types/IBrowserPage.js';
+import { Script } from 'vm';
+import IGoToOptions from '../types/IGoToOptions.js';
+import IResponse from '../../fetch/types/IResponse.js';
+import BrowserPageUtility from '../utilities/BrowserPageUtility.js';
+import IReloadOptions from '../types/IReloadOptions.js';
+import DefaultBrowserPageViewport from '../DefaultBrowserPageViewport.js';
+import IOptionalBrowserPageViewport from '../types/IOptionalBrowserPageViewport.js';
+import IBrowserPageViewport from '../types/IBrowserPageViewport.js';
+import Event from '../../event/Event.js';
+
+/**
+ * Detached browser page used when constructing a Window instance without a browser.
+ */
+export default class DetachedBrowserPage implements IBrowserPage {
+ public readonly virtualConsolePrinter = new VirtualConsolePrinter();
+ public readonly mainFrame: DetachedBrowserFrame;
+ public readonly context: DetachedBrowserContext;
+ public readonly console: Console;
+ public readonly viewport: IBrowserPageViewport = Object.assign({}, DefaultBrowserPageViewport);
+
+ /**
+ * Constructor.
+ *
+ * @param context Browser context.
+ */
+ constructor(context: DetachedBrowserContext) {
+ this.context = context;
+ this.console = context.browser.console ?? new VirtualConsole(this.virtualConsolePrinter);
+ this.mainFrame = new DetachedBrowserFrame(this);
+ }
+
+ /**
+ * Returns frames.
+ */
+ public get frames(): DetachedBrowserFrame[] {
+ return BrowserPageUtility.getFrames(this);
+ }
+
+ /**
+ * Returns the viewport.
+ */
+ public get content(): string {
+ return this.mainFrame.content;
+ }
+
+ /**
+ * Sets the content.
+ *
+ * @param content Content.
+ */
+ public set content(content) {
+ this.mainFrame.content = content;
+ }
+
+ /**
+ * Returns the URL.
+ *
+ * @returns URL.
+ */
+ public get url(): string {
+ return this.mainFrame.url;
+ }
+
+ /**
+ * Sets the content.
+ *
+ * @param url URL.
+ */
+ public set url(url) {
+ this.mainFrame.url = url;
+ }
+
+ /**
+ * Aborts all ongoing operations and destroys the page.
+ */
+ public close(): Promise {
+ // Using Promise instead of async/await to prevent microtask
+ return new Promise((resolve, reject) => {
+ const context = this.context;
+ BrowserPageUtility.closePage(this)
+ .then(() => {
+ // As we are in a detached page, a context or browser should not exist without a page as there are no references to them.
+ if (context.pages[0] === this) {
+ context.close();
+ }
+ resolve();
+ })
+ .catch((error) => reject(error));
+ });
+ }
+
+ /**
+ * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete.
+ */
+ public waitUntilComplete(): Promise {
+ return this.mainFrame.waitUntilComplete();
+ }
+
+ /**
+ * Returns a promise that is resolved when the page has navigated and the response HTML has been written to the document.
+ */
+ public waitForNavigation(): Promise {
+ return this.mainFrame.waitForNavigation();
+ }
+
+ /**
+ * Aborts all ongoing operations.
+ */
+ public abort(): Promise {
+ return this.mainFrame.abort();
+ }
+
+ /**
+ * Evaluates code or a VM Script in the page's context.
+ *
+ * @param script Script.
+ * @returns Result.
+ */
+ public evaluate(script: string | Script): any {
+ return this.mainFrame.evaluate(script);
+ }
+
+ /**
+ * Sets the viewport.
+ *
+ * @param viewport Viewport.
+ */
+ public setViewport(viewport: IOptionalBrowserPageViewport): void {
+ const previousViewport = Object.assign({}, this.viewport);
+ Object.assign(this.viewport, viewport);
+ if (
+ previousViewport.width !== this.viewport.width ||
+ previousViewport.height !== this.viewport.height ||
+ previousViewport.devicePixelRatio !== this.viewport.devicePixelRatio
+ ) {
+ this.mainFrame.window.dispatchEvent(new Event('resize'));
+ }
+ }
+
+ /**
+ * Go to a page.
+ *
+ * @param url URL.
+ * @param [options] Options.
+ * @returns Response.
+ */
+ public goto(url: string, options?: IGoToOptions): Promise {
+ return this.mainFrame.goto(url, options);
+ }
+
+ /**
+ * Reloads the current page.
+ *
+ * @param [options] Options.
+ * @returns Response.
+ */
+ public reload(options: IReloadOptions): Promise {
+ return this.mainFrame.reload(options);
+ }
+}
diff --git a/packages/happy-dom/src/browser/enums/BrowserErrorCaptureEnum.ts b/packages/happy-dom/src/browser/enums/BrowserErrorCaptureEnum.ts
new file mode 100644
index 000000000..f7c004f2a
--- /dev/null
+++ b/packages/happy-dom/src/browser/enums/BrowserErrorCaptureEnum.ts
@@ -0,0 +1,10 @@
+enum BrowserErrorCaptureEnum {
+ /** Happy DOM use try and catch when evaluating code, but will not be able to catch all errors and Promise rejections. This will decrease performance as using try and catch makes the execution significally slower. This is the default setting. */
+ tryAndCatch = 'tryAndCatch',
+ /** Happy DOM will add an event listener to the Node.js process to catch all errors and Promise rejections. This will not work in Jest and Vitest as it conflicts with their error listeners. */
+ processLevel = 'processLevel',
+ /** Error capturing is disabled. Errors and Promise rejections will be thrown. */
+ disabled = 'disabled'
+}
+
+export default BrowserErrorCaptureEnum;
diff --git a/packages/happy-dom/src/browser/enums/BrowserNavigationCrossOriginPolicyEnum.ts b/packages/happy-dom/src/browser/enums/BrowserNavigationCrossOriginPolicyEnum.ts
new file mode 100644
index 000000000..10bd41767
--- /dev/null
+++ b/packages/happy-dom/src/browser/enums/BrowserNavigationCrossOriginPolicyEnum.ts
@@ -0,0 +1,10 @@
+enum BrowserNavigationCrossOriginPolicyEnum {
+ /** The browser can navigate to any origin. */
+ anyOrigin = 'anyOrigin',
+ /** The browser can only navigate to the same origin as the current page or its parent. */
+ sameOrigin = 'sameOrigin',
+ /** The browser can never navigate from a secure protocol (https) to an unsecure protocol (http), but it can always navigate to a secure (https). */
+ strictOrigin = 'strictOrigin'
+}
+
+export default BrowserNavigationCrossOriginPolicyEnum;
diff --git a/packages/happy-dom/src/browser/types/IBrowser.ts b/packages/happy-dom/src/browser/types/IBrowser.ts
new file mode 100644
index 000000000..34fd479c4
--- /dev/null
+++ b/packages/happy-dom/src/browser/types/IBrowser.ts
@@ -0,0 +1,46 @@
+import IBrowserContext from './IBrowserContext.js';
+import IBrowserPage from './IBrowserPage.js';
+import IBrowserSettings from './IBrowserSettings.js';
+
+/**
+ * Browser.
+ *
+ * Much of the interface for the browser has been taken from Puppeteer and Playwright, so that the API is familiar.
+ */
+export default interface IBrowser {
+ readonly defaultContext: IBrowserContext;
+ readonly contexts: IBrowserContext[];
+ readonly settings: IBrowserSettings;
+ readonly console: Console | null;
+
+ /**
+ * Aborts all ongoing operations and destroys the browser.
+ */
+ close(): void;
+
+ /**
+ * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete.
+ *
+ * @returns Promise.
+ */
+ waitUntilComplete(): Promise;
+
+ /**
+ * Aborts all ongoing operations.
+ */
+ abort(): void;
+
+ /**
+ * Creates a new incognito context.
+ *
+ * @returns Context.
+ */
+ newIncognitoContext(): IBrowserContext;
+
+ /**
+ * Creates a new page.
+ *
+ * @returns Page.
+ */
+ newPage(): IBrowserPage;
+}
diff --git a/packages/happy-dom/src/browser/types/IBrowserContext.ts b/packages/happy-dom/src/browser/types/IBrowserContext.ts
new file mode 100644
index 000000000..c4b4b01bc
--- /dev/null
+++ b/packages/happy-dom/src/browser/types/IBrowserContext.ts
@@ -0,0 +1,40 @@
+import ICookieContainer from '../../cookie/types/ICookieContainer.js';
+import IResponseCache from '../../fetch/cache/response/IResponseCache.js';
+import IBrowser from './IBrowser.js';
+import IBrowserPage from './IBrowserPage.js';
+import IPreflightResponseCache from '../../fetch/cache/preflight/IPreflightResponseCache.js';
+
+/**
+ * Browser context.
+ */
+export default interface IBrowserContext {
+ readonly pages: IBrowserPage[];
+ readonly browser: IBrowser;
+ readonly cookieContainer: ICookieContainer;
+ readonly responseCache: IResponseCache;
+ readonly preflightResponseCache: IPreflightResponseCache;
+
+ /**
+ * Aborts all ongoing operations and destroys the context.
+ */
+ close(): Promise;
+
+ /**
+ * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete.
+ *
+ * @returns Promise.
+ */
+ waitUntilComplete(): Promise;
+
+ /**
+ * Aborts all ongoing operations.
+ */
+ abort(): Promise;
+
+ /**
+ * Creates a new page.
+ *
+ * @returns Page.
+ */
+ newPage(): IBrowserPage;
+}
diff --git a/packages/happy-dom/src/browser/types/IBrowserFrame.ts b/packages/happy-dom/src/browser/types/IBrowserFrame.ts
new file mode 100644
index 000000000..f3dc8d5a1
--- /dev/null
+++ b/packages/happy-dom/src/browser/types/IBrowserFrame.ts
@@ -0,0 +1,68 @@
+import AsyncTaskManager from '../../async-task-manager/AsyncTaskManager.js';
+import * as PropertySymbol from '../../PropertySymbol.js';
+import IBrowserWindow from '../../window/IBrowserWindow.js';
+import IDocument from '../../nodes/document/IDocument.js';
+import IBrowserPage from './IBrowserPage.js';
+import IResponse from '../../fetch/types/IResponse.js';
+import IGoToOptions from './IGoToOptions.js';
+import { Script } from 'vm';
+import IReloadOptions from './IReloadOptions.js';
+import BrowserFrameExceptionObserver from '../utilities/BrowserFrameExceptionObserver.js';
+import ICrossOriginBrowserWindow from '../../window/ICrossOriginBrowserWindow.js';
+
+/**
+ * Browser frame.
+ */
+export default interface IBrowserFrame {
+ readonly childFrames: IBrowserFrame[];
+ readonly parentFrame: IBrowserFrame | null;
+ readonly page: IBrowserPage;
+ readonly window: IBrowserWindow;
+ readonly document: IDocument;
+ content: string;
+ url: string;
+ [PropertySymbol.asyncTaskManager]: AsyncTaskManager;
+ [PropertySymbol.exceptionObserver]: BrowserFrameExceptionObserver | null;
+ [PropertySymbol.listeners]: { navigation: Array<() => void> };
+ [PropertySymbol.openerFrame]: IBrowserFrame | null;
+ [PropertySymbol.openerWindow]: IBrowserWindow | ICrossOriginBrowserWindow | null;
+ [PropertySymbol.popup]: boolean;
+
+ /**
+ * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete.
+ */
+ waitUntilComplete(): Promise;
+
+ /**
+ * Returns a promise that is resolved when the frame has navigated and the response HTML has been written to the document.
+ */
+ waitForNavigation(): Promise;
+
+ /**
+ * Aborts all ongoing operations.
+ */
+ abort(): Promise;
+
+ /**
+ * Evaluates code or a VM Script in the page's context.
+ *
+ * @param script Script.
+ * @returns Result.
+ */
+ evaluate(script: string | Script): any;
+
+ /**
+ * Go to a page.
+ *
+ * @param url URL.
+ * @param [options] Options.
+ */
+ goto(url: string, options?: IGoToOptions): Promise;
+
+ /**
+ * Reloads the current frame.
+ *
+ * @param [options] Options.
+ */
+ reload(options: IReloadOptions): Promise;
+}
diff --git a/packages/happy-dom/src/browser/types/IBrowserPage.ts b/packages/happy-dom/src/browser/types/IBrowserPage.ts
new file mode 100644
index 000000000..3e16ddee9
--- /dev/null
+++ b/packages/happy-dom/src/browser/types/IBrowserPage.ts
@@ -0,0 +1,71 @@
+import IBrowserPageViewport from '../types/IBrowserPageViewport.js';
+import VirtualConsolePrinter from '../../console/VirtualConsolePrinter.js';
+import IBrowserFrame from './IBrowserFrame.js';
+import IBrowserContext from './IBrowserContext.js';
+import { Script } from 'vm';
+import IGoToOptions from './IGoToOptions.js';
+import IResponse from '../../fetch/types/IResponse.js';
+import IReloadOptions from './IReloadOptions.js';
+import IOptionalBrowserPageViewport from './IOptionalBrowserPageViewport.js';
+
+/**
+ * Browser page.
+ */
+export default interface IBrowserPage {
+ readonly virtualConsolePrinter: VirtualConsolePrinter;
+ readonly mainFrame: IBrowserFrame;
+ readonly context: IBrowserContext;
+ readonly console: Console;
+ readonly frames: IBrowserFrame[];
+ readonly viewport: IBrowserPageViewport;
+ content: string;
+ url: string;
+
+ /**
+ * Aborts all ongoing operations and destroys the page.
+ */
+ close(): Promise;
+
+ /**
+ * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete.
+ */
+ waitUntilComplete(): Promise;
+
+ /**
+ * Returns a promise that is resolved when the page has navigated and the response HTML has been written to the document.
+ */
+ waitForNavigation(): Promise;
+
+ /**
+ * Aborts all ongoing operations.
+ */
+ abort(): Promise;
+
+ /**
+ * Evaluates code or a VM Script in the page's context.
+ */
+ evaluate(script: string | Script): any;
+
+ /**
+ * Sets the viewport.
+ *
+ * @param viewport Viewport.
+ */
+ setViewport(viewport: IOptionalBrowserPageViewport): void;
+
+ /**
+ * Go to a page.
+ *
+ * @param url URL.
+ * @param [options] Options.
+ */
+ goto(url: string, options?: IGoToOptions): Promise;
+
+ /**
+ * Reloads the current page.
+ *
+ * @param [options] Options.
+ * @returns Response.
+ */
+ reload(options: IReloadOptions): Promise;
+}
diff --git a/packages/happy-dom/src/browser/types/IBrowserPageViewport.ts b/packages/happy-dom/src/browser/types/IBrowserPageViewport.ts
new file mode 100644
index 000000000..9a952f9e1
--- /dev/null
+++ b/packages/happy-dom/src/browser/types/IBrowserPageViewport.ts
@@ -0,0 +1,5 @@
+export default interface IBrowserPageViewport {
+ width: number;
+ height: number;
+ devicePixelRatio: number;
+}
diff --git a/packages/happy-dom/src/browser/types/IBrowserSettings.ts b/packages/happy-dom/src/browser/types/IBrowserSettings.ts
new file mode 100644
index 000000000..c53cddb3a
--- /dev/null
+++ b/packages/happy-dom/src/browser/types/IBrowserSettings.ts
@@ -0,0 +1,77 @@
+import BrowserErrorCaptureEnum from '../enums/BrowserErrorCaptureEnum.js';
+import BrowserNavigationCrossOriginPolicyEnum from '../enums/BrowserNavigationCrossOriginPolicyEnum.js';
+
+/**
+ * Browser settings.
+ */
+export default interface IBrowserSettings {
+ /** Disables JavaScript evaluation. */
+ disableJavaScriptEvaluation: boolean;
+
+ /** Disables JavaScript file loading. */
+ disableJavaScriptFileLoading: boolean;
+
+ /** Disables CSS file loading. */
+ disableCSSFileLoading: boolean;
+
+ /** Disables computed style rendering. */
+ disableComputedStyleRendering: boolean;
+
+ /**
+ * Disables error capturing.
+ *
+ * @deprecated Use errorCapture instead.
+ */
+ disableErrorCapturing: boolean;
+
+ /**
+ * Error capturing policy.
+ */
+ errorCapture: BrowserErrorCaptureEnum;
+
+ /**
+ * @deprecated Not something that browsers support anymore as it is not secure.
+ */
+ enableFileSystemHttpRequests: boolean;
+
+ /**
+ * @deprecated Use navigation.disableChildFrameNavigation instead.
+ */
+ disableIframePageLoading: boolean;
+
+ /**
+ * Settings for the browser's navigation (when following links or opening windows).
+ */
+ navigation: {
+ /** Disables navigation to other pages in the main frame or a page. */
+ disableMainFrameNavigation: boolean;
+
+ /** Disables navigation to other pages in child frames (such as iframes). */
+ disableChildFrameNavigation: boolean;
+
+ /** Disables navigation to other pages in child pages (such as popup windows). */
+ disableChildPageNavigation: boolean;
+
+ /** Disables the fallback to setting the URL when navigating to a page is disabled or when inside a detached browser frame. */
+ disableFallbackToSetURL: boolean;
+
+ /** Sets the policy for cross-origin navigation. */
+ crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum;
+ };
+
+ /**
+ * Settings for the browser's navigator.
+ */
+ navigator: {
+ userAgent: string;
+ maxTouchPoints: number;
+ };
+
+ /**
+ * Settings for the browser's device.
+ */
+ device: {
+ prefersColorScheme: string;
+ mediaType: string;
+ };
+}
diff --git a/packages/happy-dom/src/browser/types/IGoToOptions.ts b/packages/happy-dom/src/browser/types/IGoToOptions.ts
new file mode 100644
index 000000000..7b051546c
--- /dev/null
+++ b/packages/happy-dom/src/browser/types/IGoToOptions.ts
@@ -0,0 +1,17 @@
+import IRequestReferrerPolicy from '../../fetch/types/IRequestReferrerPolicy.js';
+import IReloadOptions from './IReloadOptions.js';
+
+/**
+ * Go to options.
+ */
+export default interface IGoToOptions extends IReloadOptions {
+ /**
+ * Referrer.
+ */
+ referrer?: string;
+
+ /**
+ * Referrer policy.
+ */
+ referrerPolicy?: IRequestReferrerPolicy;
+}
diff --git a/packages/happy-dom/src/browser/types/IOptionalBrowserPageViewport.ts b/packages/happy-dom/src/browser/types/IOptionalBrowserPageViewport.ts
new file mode 100644
index 000000000..d80c11958
--- /dev/null
+++ b/packages/happy-dom/src/browser/types/IOptionalBrowserPageViewport.ts
@@ -0,0 +1,5 @@
+export default interface IOptionalBrowserPageViewport {
+ width?: number;
+ height?: number;
+ devicePixelRatio?: number;
+}
diff --git a/packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts b/packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts
new file mode 100644
index 000000000..fed9ef5b4
--- /dev/null
+++ b/packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts
@@ -0,0 +1,74 @@
+import BrowserErrorCaptureEnum from '../enums/BrowserErrorCaptureEnum.js';
+import BrowserNavigationCrossOriginPolicyEnum from '../enums/BrowserNavigationCrossOriginPolicyEnum.js';
+
+export default interface IOptionalBrowserSettings {
+ /** Disables JavaScript evaluation. */
+ disableJavaScriptEvaluation?: boolean;
+
+ /** Disables JavaScript file loading. */
+ disableJavaScriptFileLoading?: boolean;
+
+ /** Disables CSS file loading. */
+ disableCSSFileLoading?: boolean;
+
+ /** Disables computed style rendering. */
+ disableComputedStyleRendering?: boolean;
+
+ /**
+ * Disables error capturing.
+ *
+ * @deprecated Use errorCapture instead.
+ */
+ disableErrorCapturing?: boolean;
+
+ /**
+ * Error capturing policy.
+ */
+ errorCapture?: BrowserErrorCaptureEnum;
+
+ /**
+ * @deprecated Not something that browsers support anymore as it is not secure.
+ */
+ enableFileSystemHttpRequests?: boolean;
+
+ /**
+ * @deprecated Use navigation.disableChildFrameNavigation instead.
+ */
+ disableIframePageLoading?: boolean;
+
+ /**
+ * Settings for the browser's navigation (when following links or opening windows).
+ */
+ navigation?: {
+ /** Disables navigation to other pages in the main frame or a page. */
+ disableMainFrameNavigation?: boolean;
+
+ /** Disables navigation to other pages in child frames (such as iframes). */
+ disableChildFrameNavigation?: boolean;
+
+ /** Disables navigation to other pages in child pages (such as popup windows). */
+ disableChildPageNavigation?: boolean;
+
+ /** Disables the fallback to setting the URL when navigating to a page is disabled or when inside a detached browser frame. */
+ disableFallbackToSetURL?: boolean;
+
+ /** Sets the policy for cross-origin navigation. */
+ crossOriginPolicy?: BrowserNavigationCrossOriginPolicyEnum;
+ };
+
+ /**
+ * Settings for the browser's navigator.
+ */
+ navigator?: {
+ userAgent?: string;
+ maxTouchPoints?: number;
+ };
+
+ /**
+ * Settings for the browser's device.
+ */
+ device?: {
+ prefersColorScheme?: string;
+ mediaType?: string;
+ };
+}
diff --git a/packages/happy-dom/src/browser/types/IReloadOptions.ts b/packages/happy-dom/src/browser/types/IReloadOptions.ts
new file mode 100644
index 000000000..2ddbcf5b5
--- /dev/null
+++ b/packages/happy-dom/src/browser/types/IReloadOptions.ts
@@ -0,0 +1,14 @@
+/**
+ * Reload options.
+ */
+export default interface IReloadOptions {
+ /**
+ * Set to true to bypass the cache.
+ */
+ hard?: boolean;
+
+ /**
+ * Timeout in ms. Default is 30000ms.
+ */
+ timeout?: number;
+}
diff --git a/packages/uncaught-exception-observer/src/UncaughtExceptionObserver.ts b/packages/happy-dom/src/browser/utilities/BrowserFrameExceptionObserver.ts
similarity index 59%
rename from packages/uncaught-exception-observer/src/UncaughtExceptionObserver.ts
rename to packages/happy-dom/src/browser/utilities/BrowserFrameExceptionObserver.ts
index 2e4933054..1df264583 100644
--- a/packages/uncaught-exception-observer/src/UncaughtExceptionObserver.ts
+++ b/packages/happy-dom/src/browser/utilities/BrowserFrameExceptionObserver.ts
@@ -1,11 +1,11 @@
-import IWindow from 'happy-dom/lib/window/IWindow.js';
+import IBrowserFrame from '../types/IBrowserFrame.js';
/**
* Listens for uncaught exceptions coming from Happy DOM on the running Node process and dispatches error events on the Window instance.
*/
-export default class UncaughtExceptionObserver {
+export default class BrowserFrameExceptionObserver {
private static listenerCount = 0;
- private window: IWindow | null = null;
+ private browserFrame: IBrowserFrame | null = null;
private uncaughtExceptionListener: (
error: Error,
origin: 'uncaughtException' | 'unhandledRejection'
@@ -15,16 +15,16 @@ export default class UncaughtExceptionObserver {
/**
* Observes the Node process for uncaught exceptions.
*
- * @param window
+ * @param browserFrame Browser frame.
*/
- public observe(window: IWindow): void {
- if (this.window) {
+ public observe(browserFrame: IBrowserFrame): void {
+ if (this.browserFrame) {
throw new Error('Already observing.');
}
- this.window = window;
+ this.browserFrame = browserFrame;
- (this.constructor).listenerCount++;
+ (this.constructor).listenerCount++;
this.uncaughtExceptionListener = (
error: unknown,
@@ -35,16 +35,16 @@ export default class UncaughtExceptionObserver {
}
if (
- (error instanceof this.window.Error || error instanceof this.window.DOMException) &&
- error.stack?.includes('/happy-dom/')
+ error instanceof this.browserFrame.window.Error ||
+ error instanceof this.browserFrame.window.DOMException
) {
- this.window.console.error(error);
- this.window.dispatchEvent(
- new this.window.ErrorEvent('error', { error, message: error.message })
+ this.browserFrame.window.console.error(error);
+ this.browserFrame.window.dispatchEvent(
+ new this.browserFrame.window.ErrorEvent('error', { error, message: error.message })
);
} else if (
process.listenerCount('uncaughtException') ===
- (this.constructor).listenerCount
+ (this.constructor).listenerCount
) {
// eslint-disable-next-line no-console
console.error(error);
@@ -57,16 +57,16 @@ export default class UncaughtExceptionObserver {
// Therefore we want to use the "unhandledRejection" event as well.
this.uncaughtRejectionListener = (error: unknown) => {
if (
- (error instanceof this.window.Error || error instanceof this.window.DOMException) &&
- error.stack?.includes('/happy-dom/')
+ error instanceof this.browserFrame.window.Error ||
+ error instanceof this.browserFrame.window.DOMException
) {
- this.window.console.error(error);
- this.window.dispatchEvent(
- new this.window.ErrorEvent('error', { error, message: error.message })
+ this.browserFrame.window.console.error(error);
+ this.browserFrame.window.dispatchEvent(
+ new this.browserFrame.window.ErrorEvent('error', { error, message: error.message })
);
} else if (
process.listenerCount('unhandledRejection') ===
- (this.constructor).listenerCount
+ (this.constructor).listenerCount
) {
// eslint-disable-next-line no-console
console.error(error);
@@ -83,17 +83,17 @@ export default class UncaughtExceptionObserver {
* Disconnects observer.
*/
public disconnect(): void {
- if (!this.window) {
+ if (!this.browserFrame) {
return;
}
- (this.constructor).listenerCount--;
+ (this.constructor).listenerCount--;
process.off('uncaughtException', this.uncaughtExceptionListener);
process.off('unhandledRejection', this.uncaughtRejectionListener);
this.uncaughtExceptionListener = null;
this.uncaughtRejectionListener = null;
- this.window = null;
+ this.browserFrame = null;
}
}
diff --git a/packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts b/packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts
new file mode 100644
index 000000000..f506ae6bc
--- /dev/null
+++ b/packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts
@@ -0,0 +1,71 @@
+import IBrowserFrame from '../types/IBrowserFrame.js';
+import * as PropertySymbol from '../../PropertySymbol.js';
+import IBrowserWindow from '../../window/IBrowserWindow.js';
+import IBrowserPage from '../types/IBrowserPage.js';
+/**
+ * Browser frame factory.
+ */
+export default class BrowserFrameFactory {
+ /**
+ * Creates a new frame.
+ *
+ * @param parentFrame Parent frame.
+ * @returns Frame.
+ */
+ public static newChildFrame(parentFrame: IBrowserFrame): IBrowserFrame {
+ const frame = new ( IBrowserFrame>parentFrame.constructor)(
+ parentFrame.page
+ );
+ (frame.parentFrame) = parentFrame;
+ parentFrame.childFrames.push(frame);
+ return frame;
+ }
+
+ /**
+ * Aborts all ongoing operations and destroys the frame.
+ *
+ * @param frame Frame.
+ */
+ public static destroyFrame(frame: IBrowserFrame): Promise {
+ // Using Promise instead of async/await to prevent microtask
+ return new Promise((resolve, reject) => {
+ if (!frame.window) {
+ resolve();
+ return;
+ }
+
+ if (frame.parentFrame) {
+ const index = frame.parentFrame.childFrames.indexOf(frame);
+ if (index !== -1) {
+ frame.parentFrame.childFrames.splice(index, 1);
+ }
+ }
+
+ // We need to destroy the Window instance before triggering any async tasks as Window.close() is not async.
+ frame.window[PropertySymbol.destroy]();
+ (frame.page) = null;
+ (frame.window) = null;
+ frame[PropertySymbol.openerFrame] = null;
+ frame[PropertySymbol.openerWindow] = null;
+
+ if (!frame.childFrames.length) {
+ return frame[PropertySymbol.asyncTaskManager]
+ .destroy()
+ .then(() => {
+ frame[PropertySymbol.exceptionObserver]?.disconnect();
+ resolve();
+ })
+ .catch((error) => reject(error));
+ }
+
+ Promise.all(frame.childFrames.slice().map((childFrame) => this.destroyFrame(childFrame)))
+ .then(() => {
+ return frame[PropertySymbol.asyncTaskManager].destroy().then(() => {
+ frame[PropertySymbol.exceptionObserver]?.disconnect();
+ resolve();
+ });
+ })
+ .catch((error) => reject(error));
+ });
+ }
+}
diff --git a/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts b/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts
new file mode 100644
index 000000000..fc95b2fba
--- /dev/null
+++ b/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts
@@ -0,0 +1,179 @@
+import IBrowserFrame from '../types/IBrowserFrame.js';
+import * as PropertySymbol from '../../PropertySymbol.js';
+import IGoToOptions from '../types/IGoToOptions.js';
+import IResponse from '../../fetch/types/IResponse.js';
+import DocumentReadyStateManager from '../../nodes/document/DocumentReadyStateManager.js';
+import IBrowserWindow from '../../window/IBrowserWindow.js';
+import WindowErrorUtility from '../../window/WindowErrorUtility.js';
+import Location from '../../location/Location.js';
+import AbortController from '../../fetch/AbortController.js';
+import BrowserFrameFactory from './BrowserFrameFactory.js';
+import BrowserFrameURL from './BrowserFrameURL.js';
+import BrowserFrameValidator from './BrowserFrameValidator.js';
+import AsyncTaskManager from '../../async-task-manager/AsyncTaskManager.js';
+import BrowserErrorCaptureEnum from '../enums/BrowserErrorCaptureEnum.js';
+
+/**
+ * Browser frame navigation utility.
+ */
+export default class BrowserFrameNavigator {
+ /**
+ * Go to a page.
+ *
+ * @throws Error if the request can't be resolved (because of SSL error or similar). It will not throw if the response is not ok.
+ * @param windowClass Window class.
+ * @param frame Frame.
+ * @param url URL.
+ * @param [options] Options.
+ * @returns Response.
+ */
+ public static async goto(
+ windowClass: new (
+ browserFrame: IBrowserFrame,
+ options?: { url?: string; width?: number; height?: number }
+ ) => IBrowserWindow,
+ frame: IBrowserFrame,
+ url: string,
+ options?: IGoToOptions
+ ): Promise {
+ const targetURL = BrowserFrameURL.getRelativeURL(frame, url);
+
+ if (!frame.window) {
+ throw new Error('The frame has been destroyed, the "window" property is not set.');
+ }
+
+ if (targetURL.protocol === 'javascript:') {
+ if (frame && !frame.page.context.browser.settings.disableJavaScriptEvaluation) {
+ const readyStateManager = (<
+ { [PropertySymbol.readyStateManager]: DocumentReadyStateManager }
+ >(frame.window))[PropertySymbol.readyStateManager];
+
+ readyStateManager.startTask();
+
+ // The browser will wait for the next tick before executing the script.
+ await new Promise((resolve) => frame.page.mainFrame.window.setTimeout(resolve));
+
+ const code =
+ '//# sourceURL=' + frame.url + '\n' + targetURL.href.replace('javascript:', '');
+
+ if (
+ frame.page.context.browser.settings.disableErrorCapturing ||
+ frame.page.context.browser.settings.errorCapture !== BrowserErrorCaptureEnum.tryAndCatch
+ ) {
+ frame.window.eval(code);
+ } else {
+ WindowErrorUtility.captureError(frame.window, () => frame.window.eval(code));
+ }
+
+ readyStateManager.endTask();
+ }
+
+ return null;
+ }
+
+ if (!BrowserFrameValidator.validateCrossOriginPolicy(frame, targetURL)) {
+ return null;
+ }
+
+ if (!BrowserFrameValidator.validateFrameNavigation(frame)) {
+ if (!frame.page.context.browser.settings.navigation.disableFallbackToSetURL) {
+ (frame.window.location) = new Location(frame, targetURL.href);
+ }
+
+ return null;
+ }
+
+ const width = frame.window.innerWidth;
+ const height = frame.window.innerHeight;
+ const devicePixelRatio = frame.window.devicePixelRatio;
+
+ for (const childFrame of frame.childFrames) {
+ BrowserFrameFactory.destroyFrame(childFrame);
+ }
+
+ (frame.childFrames) = [];
+ frame.window[PropertySymbol.destroy]();
+ frame[PropertySymbol.asyncTaskManager].destroy();
+ frame[PropertySymbol.asyncTaskManager] = new AsyncTaskManager();
+
+ (frame.window) = new windowClass(frame, { url: targetURL.href, width, height });
+ (frame.window.devicePixelRatio) = devicePixelRatio;
+
+ if (options?.referrer) {
+ frame.window.document[PropertySymbol.referrer] = options.referrer;
+ }
+
+ if (targetURL.protocol === 'about:') {
+ return null;
+ }
+
+ const readyStateManager = (<{ [PropertySymbol.readyStateManager]: DocumentReadyStateManager }>(
+ (frame.window)
+ ))[PropertySymbol.readyStateManager];
+
+ readyStateManager.startTask();
+
+ const abortController = new AbortController();
+ let response: IResponse;
+ let responseText: string;
+
+ const timeout = frame.window.setTimeout(
+ () => abortController.abort('Request timed out.'),
+ options?.timeout ?? 30000
+ );
+ const finalize = (): void => {
+ frame.window.clearTimeout(timeout);
+ readyStateManager.endTask();
+ const listeners = frame[PropertySymbol.listeners].navigation;
+ frame[PropertySymbol.listeners].navigation = [];
+ for (const listener of listeners) {
+ listener();
+ }
+ };
+
+ try {
+ response = await frame.window.fetch(targetURL.href, {
+ referrer: options?.referrer,
+ referrerPolicy: options?.referrerPolicy,
+ signal: abortController.signal,
+ headers: options?.hard ? { 'Cache-Control': 'no-cache' } : undefined
+ });
+
+ // Handles the "X-Frame-Options" header for child frames.
+ if (frame.parentFrame) {
+ const originURL = frame.parentFrame.window.location;
+ const xFrameOptions = response.headers.get('X-Frame-Options')?.toLowerCase();
+ const isSameOrigin = originURL.origin === targetURL.origin || targetURL.origin === 'null';
+
+ if (xFrameOptions === 'deny' || (xFrameOptions === 'sameorigin' && !isSameOrigin)) {
+ throw new Error(
+ `Refused to display '${url}' in a frame because it set 'X-Frame-Options' to '${xFrameOptions}'.`
+ );
+ }
+ }
+
+ responseText = await response.text();
+ } catch (error) {
+ finalize();
+ throw error;
+ }
+
+ if (!response.ok) {
+ frame.page.console.error(`GET ${targetURL.href} ${response.status} (${response.statusText})`);
+ }
+
+ // Fixes issue where evaluating the response can throw an error.
+ // By using requestAnimationFrame() the error will not reject the promise.
+ // The error will be caught by process error level listener or a try and catch in the requestAnimationFrame().
+ frame.window.requestAnimationFrame(() => (frame.content = responseText));
+
+ await new Promise((resolve) =>
+ frame.window.requestAnimationFrame(() => {
+ finalize();
+ resolve(null);
+ })
+ );
+
+ return response;
+ }
+}
diff --git a/packages/happy-dom/src/browser/utilities/BrowserFrameScriptEvaluator.ts b/packages/happy-dom/src/browser/utilities/BrowserFrameScriptEvaluator.ts
new file mode 100644
index 000000000..560da2ce6
--- /dev/null
+++ b/packages/happy-dom/src/browser/utilities/BrowserFrameScriptEvaluator.ts
@@ -0,0 +1,22 @@
+import IBrowserFrame from '../types/IBrowserFrame.js';
+import { Script } from 'vm';
+
+/**
+ * Browser frame script evaluator.
+ */
+export default class BrowserFrameScriptEvaluator {
+ /**
+ * Evaluates code or a VM Script in the frame's context.
+ *
+ * @param frame Frame.
+ * @param script Script.
+ * @returns Result.
+ */
+ public static evaluate(frame: IBrowserFrame, script: string | Script): any {
+ if (!frame.window) {
+ throw new Error('The frame has been destroyed, the "window" property is not set.');
+ }
+ script = typeof script === 'string' ? new Script(script) : script;
+ return script.runInContext(frame.window);
+ }
+}
diff --git a/packages/happy-dom/src/browser/utilities/BrowserFrameURL.ts b/packages/happy-dom/src/browser/utilities/BrowserFrameURL.ts
new file mode 100644
index 000000000..e4edf48d6
--- /dev/null
+++ b/packages/happy-dom/src/browser/utilities/BrowserFrameURL.ts
@@ -0,0 +1,40 @@
+import IBrowserFrame from '../types/IBrowserFrame.js';
+import { URL } from 'url';
+import DOMException from '../../exception/DOMException.js';
+import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js';
+
+/**
+ * Browser frame URL utility.
+ */
+export default class BrowserFrameURL {
+ /**
+ * Returns relative URL.
+ *
+ * @param frame Frame.
+ * @param url URL.
+ * @returns Relative URL.
+ */
+ public static getRelativeURL(frame: IBrowserFrame, url: string): URL {
+ url = url || 'about:blank';
+
+ if (url.startsWith('about:') || url.startsWith('javascript:')) {
+ return new URL(url);
+ }
+
+ try {
+ return new URL(url, frame.window.location);
+ } catch (e) {
+ if (frame.window.location.hostname) {
+ throw new DOMException(
+ `Failed to construct URL from string "${url}".`,
+ DOMExceptionNameEnum.uriMismatchError
+ );
+ } else {
+ throw new DOMException(
+ `Failed to construct URL from string "${url}" relative to URL "${frame.window.location.href}".`,
+ DOMExceptionNameEnum.uriMismatchError
+ );
+ }
+ }
+ }
+}
diff --git a/packages/happy-dom/src/browser/utilities/BrowserFrameValidator.ts b/packages/happy-dom/src/browser/utilities/BrowserFrameValidator.ts
new file mode 100644
index 000000000..28ba9b15c
--- /dev/null
+++ b/packages/happy-dom/src/browser/utilities/BrowserFrameValidator.ts
@@ -0,0 +1,83 @@
+import IBrowserFrame from '../types/IBrowserFrame.js';
+import { URL } from 'url';
+import BrowserNavigationCrossOriginPolicyEnum from '../enums/BrowserNavigationCrossOriginPolicyEnum.js';
+import DetachedBrowserFrame from '../detached-browser/DetachedBrowserFrame.js';
+import * as PropertySymbol from '../../PropertySymbol.js';
+
+/**
+ * Browser frame validator.
+ */
+export default class BrowserFrameValidator {
+ /**
+ * Returns true if the frame navigation complies with the cross origin policy.
+ *
+ * @param frame Frame.
+ * @param toURL URL.
+ * @returns True if the frame navigation complies with the cross origin policy.
+ */
+ public static validateCrossOriginPolicy(frame: IBrowserFrame, toURL: URL): boolean {
+ const settings = frame.page.context.browser.settings;
+ let fromURL = frame.page.mainFrame.window.location;
+
+ if (frame[PropertySymbol.openerFrame]) {
+ fromURL = frame[PropertySymbol.openerFrame].window.location;
+ } else if (frame.parentFrame) {
+ fromURL = frame.parentFrame.window.location;
+ }
+
+ if (
+ settings.navigation.crossOriginPolicy === BrowserNavigationCrossOriginPolicyEnum.sameOrigin &&
+ fromURL.protocol !== 'about:' &&
+ toURL.protocol !== 'about:' &&
+ toURL.protocol !== 'javascript:' &&
+ fromURL.origin !== toURL.origin
+ ) {
+ return false;
+ }
+
+ if (
+ settings.navigation.crossOriginPolicy ===
+ BrowserNavigationCrossOriginPolicyEnum.strictOrigin &&
+ fromURL.protocol === 'https:' &&
+ toURL.protocol === 'http:'
+ ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns true if navigation is allowed for the frame.
+ *
+ * @param frame Frame.
+ * @returns True if navigation is allowed for the frame.
+ */
+ public static validateFrameNavigation(frame: IBrowserFrame): boolean {
+ const settings = frame.page.context.browser.settings;
+
+ // When using the Window instance directly and not via the Browser API we should not navigate the browser frame.
+ if (
+ frame instanceof DetachedBrowserFrame &&
+ frame.page.context === frame.page.context.browser.defaultContext &&
+ frame.page.context.pages[0] === frame.page &&
+ frame.page.mainFrame === frame
+ ) {
+ return false;
+ }
+
+ if (settings.navigation.disableMainFrameNavigation && frame.page.mainFrame === frame) {
+ return false;
+ }
+
+ if (settings.navigation.disableChildFrameNavigation && frame.page.mainFrame !== frame) {
+ return false;
+ }
+
+ if (settings.navigation.disableChildPageNavigation && !!frame[PropertySymbol.openerFrame]) {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/packages/happy-dom/src/browser/utilities/BrowserPageUtility.ts b/packages/happy-dom/src/browser/utilities/BrowserPageUtility.ts
new file mode 100644
index 000000000..d4e2f41ca
--- /dev/null
+++ b/packages/happy-dom/src/browser/utilities/BrowserPageUtility.ts
@@ -0,0 +1,63 @@
+import IBrowserFrame from '../types/IBrowserFrame.js';
+import IBrowserPage from '../types/IBrowserPage.js';
+import IVirtualConsolePrinter from '../../console/types/IVirtualConsolePrinter.js';
+import IBrowserContext from '../types/IBrowserContext.js';
+import BrowserFrameFactory from './BrowserFrameFactory.js';
+
+/**
+ * Browser page utility.
+ */
+export default class BrowserPageUtility {
+ /**
+ * Returns frames for a page.
+ *
+ * @param page Page.
+ * @returns Frames.
+ */
+ public static getFrames(page: IBrowserPage): IBrowserFrame[] {
+ return this.findFrames(page.mainFrame);
+ }
+
+ /**
+ * Aborts all ongoing operations and destroys the page.
+ *
+ * @param page Page.
+ */
+ public static closePage(page: IBrowserPage): Promise {
+ // Using Promise instead of async/await to prevent microtask
+ return new Promise((resolve, reject) => {
+ if (!page.mainFrame) {
+ resolve();
+ return;
+ }
+
+ const index = page.context.pages.indexOf(page);
+ if (index !== -1) {
+ page.context.pages.splice(index, 1);
+ }
+
+ BrowserFrameFactory.destroyFrame(page.mainFrame)
+ .then(() => {
+ (page.virtualConsolePrinter) = null;
+ (page.mainFrame) = null;
+ (page.context) = null;
+ resolve();
+ })
+ .catch((error) => reject(error));
+ });
+ }
+
+ /**
+ * Returns all frames.
+ *
+ * @param parentFrame Parent frame.
+ * @returns Frames, including the parent.
+ */
+ private static findFrames(parentFrame: IBrowserFrame): IBrowserFrame[] {
+ let frames = [parentFrame];
+ for (const frame of parentFrame.childFrames) {
+ frames = frames.concat(this.findFrames(frame));
+ }
+ return frames;
+ }
+}
diff --git a/packages/happy-dom/src/clipboard/Clipboard.ts b/packages/happy-dom/src/clipboard/Clipboard.ts
index 3b1980b71..097a679b8 100644
--- a/packages/happy-dom/src/clipboard/Clipboard.ts
+++ b/packages/happy-dom/src/clipboard/Clipboard.ts
@@ -1,5 +1,5 @@
import DOMException from '../exception/DOMException.js';
-import IWindow from '../window/IWindow.js';
+import IBrowserWindow from '../window/IBrowserWindow.js';
import ClipboardItem from './ClipboardItem.js';
import Blob from '../file/Blob.js';
@@ -10,7 +10,7 @@ import Blob from '../file/Blob.js';
* https://developer.mozilla.org/en-US/docs/Web/API/Clipboard.
*/
export default class Clipboard {
- #ownerWindow: IWindow;
+ #ownerWindow: IBrowserWindow;
#data: ClipboardItem[] = [];
/**
@@ -18,7 +18,7 @@ export default class Clipboard {
*
* @param ownerWindow Owner window.
*/
- constructor(ownerWindow: IWindow) {
+ constructor(ownerWindow: IBrowserWindow) {
this.#ownerWindow = ownerWindow;
}
diff --git a/packages/happy-dom/src/config/ElementTag.ts b/packages/happy-dom/src/config/ElementTag.ts
index d82fc5f84..dbcb19d98 100644
--- a/packages/happy-dom/src/config/ElementTag.ts
+++ b/packages/happy-dom/src/config/ElementTag.ts
@@ -1,155 +1,130 @@
-import HTMLElement from '../nodes/html-element/HTMLElement.js';
-import HTMLTemplateElement from '../nodes/html-template-element/HTMLTemplateElement.js';
-import HTMLFormElement from '../nodes/html-form-element/HTMLFormElement.js';
-import HTMLInputElement from '../nodes/html-input-element/HTMLInputElement.js';
-import HTMLTextAreaElement from '../nodes/html-text-area-element/HTMLTextAreaElement.js';
-import SVGSVGElement from '../nodes/svg-element/SVGSVGElement.js';
-import SVGElement from '../nodes/svg-element/SVGElement.js';
-import HTMLScriptElement from '../nodes/html-script-element/HTMLScriptElement.js';
-import HTMLImageElement from '../nodes/html-image-element/HTMLImageElement.js';
-import HTMLLinkElement from '../nodes/html-link-element/HTMLLinkElement.js';
-import HTMLStyleElement from '../nodes/html-style-element/HTMLStyleElement.js';
-import HTMLLabelElement from '../nodes/html-label-element/HTMLLabelElement.js';
-import HTMLSlotElement from '../nodes/html-slot-element/HTMLSlotElement.js';
-import HTMLMetaElement from '../nodes/html-meta-element/HTMLMetaElement.js';
-import HTMLBaseElement from '../nodes/html-base-element/HTMLBaseElement.js';
-import HTMLSelectElement from '../nodes/html-select-element/HTMLSelectElement.js';
-import HTMLOptionElement from '../nodes/html-option-element/HTMLOptionElement.js';
-import HTMLOptGroupElement from '../nodes/html-opt-group-element/HTMLOptGroupElement.js';
-import HTMLDialogElement from '../nodes/html-dialog-element/HTMLDialogElement.js';
-import HTMLButtonElement from '../nodes/html-button-element/HTMLButtonElement.js';
-import HTMLAudioElement from '../nodes/html-audio-element/HTMLAudioElement.js';
-import HTMLVideoElement from '../nodes/html-video-element/HTMLVideoElement.js';
-import HTMLAnchorElement from '../nodes/html-anchor-element/HTMLAnchorElement.js';
-import HTMLIFrameElement from '../nodes/html-iframe-element/HTMLIFrameElement.js';
-
-export default {
- A: HTMLAnchorElement,
- ABBR: HTMLElement,
- ADDRESS: HTMLElement,
- AREA: HTMLElement,
- ARTICLE: HTMLElement,
- ASIDE: HTMLElement,
- AUDIO: HTMLAudioElement,
- B: HTMLElement,
- BASE: HTMLBaseElement,
- BDI: HTMLElement,
- BDO: HTMLElement,
- BLOCKQUAOTE: HTMLElement,
- BODY: HTMLElement,
- TEMPLATE: HTMLTemplateElement,
- FORM: HTMLFormElement,
- INPUT: HTMLInputElement,
- TEXTAREA: HTMLTextAreaElement,
- SCRIPT: HTMLScriptElement,
- IMG: HTMLImageElement,
- LINK: HTMLLinkElement,
- STYLE: HTMLStyleElement,
- LABEL: HTMLLabelElement,
- SLOT: HTMLSlotElement,
- SVG: SVGSVGElement,
- G: SVGElement,
- CIRCLE: SVGElement,
- ELLIPSE: SVGElement,
- LINE: SVGElement,
- PATH: SVGElement,
- POLYGON: SVGElement,
- POLYLINE: SVGElement,
- RECT: SVGElement,
- STOP: SVGElement,
- USE: SVGElement,
- META: HTMLMetaElement,
- BLOCKQUOTE: HTMLElement,
- BR: HTMLElement,
- BUTTON: HTMLButtonElement,
- CANVAS: HTMLElement,
- CAPTION: HTMLElement,
- CITE: HTMLElement,
- CODE: HTMLElement,
- COL: HTMLElement,
- COLGROUP: HTMLElement,
- DATA: HTMLElement,
- DATALIST: HTMLElement,
- DD: HTMLElement,
- DEL: HTMLElement,
- DETAILS: HTMLElement,
- DFN: HTMLElement,
- DIALOG: HTMLDialogElement,
- DIV: HTMLElement,
- DL: HTMLElement,
- DT: HTMLElement,
- EM: HTMLElement,
- EMBED: HTMLElement,
- FIELDSET: HTMLElement,
- FIGCAPTION: HTMLElement,
- FIGURE: HTMLElement,
- FOOTER: HTMLElement,
- H1: HTMLElement,
- H2: HTMLElement,
- H3: HTMLElement,
- H4: HTMLElement,
- H5: HTMLElement,
- H6: HTMLElement,
- HEAD: HTMLElement,
- HEADER: HTMLElement,
- HGROUP: HTMLElement,
- HR: HTMLElement,
- HTML: HTMLElement,
- I: HTMLElement,
- IFRAME: HTMLIFrameElement,
- INS: HTMLElement,
- KBD: HTMLElement,
- LEGEND: HTMLElement,
- LI: HTMLElement,
- MAIN: HTMLElement,
- MAP: HTMLElement,
- MARK: HTMLElement,
- MATH: HTMLElement,
- MENU: HTMLElement,
- MENUITEM: HTMLElement,
- METER: HTMLElement,
- NAV: HTMLElement,
- NOSCRIPT: HTMLElement,
- OBJECT: HTMLElement,
- OL: HTMLElement,
- OPTGROUP: HTMLOptGroupElement,
- OPTION: HTMLOptionElement,
- OUTPUT: HTMLElement,
- P: HTMLElement,
- PARAM: HTMLElement,
- PICTURE: HTMLElement,
- PRE: HTMLElement,
- PROGRESS: HTMLElement,
- Q: HTMLElement,
- RB: HTMLElement,
- RP: HTMLElement,
- RT: HTMLElement,
- RTC: HTMLElement,
- RUBY: HTMLElement,
- S: HTMLElement,
- SAMP: HTMLElement,
- SECTION: HTMLElement,
- SELECT: HTMLSelectElement,
- SMALL: HTMLElement,
- SOURCE: HTMLElement,
- SPAN: HTMLElement,
- STRONG: HTMLElement,
- SUB: HTMLElement,
- SUMMARY: HTMLElement,
- SUP: HTMLElement,
- TABLE: HTMLElement,
- TBODY: HTMLElement,
- TD: HTMLElement,
- TFOOT: HTMLElement,
- TH: HTMLElement,
- THEAD: HTMLElement,
- TIME: HTMLElement,
- TITLE: HTMLElement,
- TR: HTMLElement,
- TRACK: HTMLElement,
- U: HTMLElement,
- UL: HTMLElement,
- VAR: HTMLElement,
- VIDEO: HTMLVideoElement,
- WBR: HTMLElement
+export default <{ [key: string]: string }>{
+ A: 'HTMLAnchorElement',
+ ABBR: 'HTMLElement',
+ ADDRESS: 'HTMLElement',
+ AREA: 'HTMLElement',
+ ARTICLE: 'HTMLElement',
+ ASIDE: 'HTMLElement',
+ AUDIO: 'HTMLAudioElement',
+ B: 'HTMLElement',
+ BASE: 'HTMLBaseElement',
+ BDI: 'HTMLElement',
+ BDO: 'HTMLElement',
+ BLOCKQUAOTE: 'HTMLElement',
+ BODY: 'HTMLElement',
+ TEMPLATE: 'HTMLTemplateElement',
+ FORM: 'HTMLFormElement',
+ INPUT: 'HTMLInputElement',
+ TEXTAREA: 'HTMLTextAreaElement',
+ SCRIPT: 'HTMLScriptElement',
+ IMG: 'HTMLImageElement',
+ LINK: 'HTMLLinkElement',
+ STYLE: 'HTMLStyleElement',
+ LABEL: 'HTMLLabelElement',
+ SLOT: 'HTMLSlotElement',
+ SVG: 'SVGSVGElement',
+ G: 'SVGElement',
+ CIRCLE: 'SVGElement',
+ ELLIPSE: 'SVGElement',
+ LINE: 'SVGElement',
+ PATH: 'SVGElement',
+ POLYGON: 'SVGElement',
+ POLYLINE: 'SVGElement',
+ RECT: 'SVGElement',
+ STOP: 'SVGElement',
+ USE: 'SVGElement',
+ META: 'HTMLMetaElement',
+ BLOCKQUOTE: 'HTMLElement',
+ BR: 'HTMLElement',
+ BUTTON: 'HTMLButtonElement',
+ CANVAS: 'HTMLElement',
+ CAPTION: 'HTMLElement',
+ CITE: 'HTMLElement',
+ CODE: 'HTMLElement',
+ COL: 'HTMLElement',
+ COLGROUP: 'HTMLElement',
+ DATA: 'HTMLElement',
+ DATALIST: 'HTMLElement',
+ DD: 'HTMLElement',
+ DEL: 'HTMLElement',
+ DETAILS: 'HTMLElement',
+ DFN: 'HTMLElement',
+ DIALOG: 'HTMLDialogElement',
+ DIV: 'HTMLElement',
+ DL: 'HTMLElement',
+ DT: 'HTMLElement',
+ EM: 'HTMLElement',
+ EMBED: 'HTMLElement',
+ FIELDSET: 'HTMLElement',
+ FIGCAPTION: 'HTMLElement',
+ FIGURE: 'HTMLElement',
+ FOOTER: 'HTMLElement',
+ H1: 'HTMLElement',
+ H2: 'HTMLElement',
+ H3: 'HTMLElement',
+ H4: 'HTMLElement',
+ H5: 'HTMLElement',
+ H6: 'HTMLElement',
+ HEAD: 'HTMLElement',
+ HEADER: 'HTMLElement',
+ HGROUP: 'HTMLElement',
+ HR: 'HTMLElement',
+ HTML: 'HTMLElement',
+ I: 'HTMLElement',
+ IFRAME: 'HTMLIFrameElement',
+ INS: 'HTMLElement',
+ KBD: 'HTMLElement',
+ LEGEND: 'HTMLElement',
+ LI: 'HTMLElement',
+ MAIN: 'HTMLElement',
+ MAP: 'HTMLElement',
+ MARK: 'HTMLElement',
+ MATH: 'HTMLElement',
+ MENU: 'HTMLElement',
+ MENUITEM: 'HTMLElement',
+ METER: 'HTMLElement',
+ NAV: 'HTMLElement',
+ NOSCRIPT: 'HTMLElement',
+ OBJECT: 'HTMLElement',
+ OL: 'HTMLElement',
+ OPTGROUP: 'HTMLOptGroupElement',
+ OPTION: 'HTMLOptionElement',
+ OUTPUT: 'HTMLElement',
+ P: 'HTMLElement',
+ PARAM: 'HTMLElement',
+ PICTURE: 'HTMLElement',
+ PRE: 'HTMLElement',
+ PROGRESS: 'HTMLElement',
+ Q: 'HTMLElement',
+ RB: 'HTMLElement',
+ RP: 'HTMLElement',
+ RT: 'HTMLElement',
+ RTC: 'HTMLElement',
+ RUBY: 'HTMLElement',
+ S: 'HTMLElement',
+ SAMP: 'HTMLElement',
+ SECTION: 'HTMLElement',
+ SELECT: 'HTMLSelectElement',
+ SMALL: 'HTMLElement',
+ SOURCE: 'HTMLElement',
+ SPAN: 'HTMLElement',
+ STRONG: 'HTMLElement',
+ SUB: 'HTMLElement',
+ SUMMARY: 'HTMLElement',
+ SUP: 'HTMLElement',
+ TABLE: 'HTMLElement',
+ TBODY: 'HTMLElement',
+ TD: 'HTMLElement',
+ TFOOT: 'HTMLElement',
+ TH: 'HTMLElement',
+ THEAD: 'HTMLElement',
+ TIME: 'HTMLElement',
+ TITLE: 'HTMLElement',
+ TR: 'HTMLElement',
+ TRACK: 'HTMLElement',
+ U: 'HTMLElement',
+ UL: 'HTMLElement',
+ VAR: 'HTMLElement',
+ VIDEO: 'HTMLVideoElement',
+ WBR: 'HTMLElement'
};
diff --git a/packages/happy-dom/src/console/VirtualConsole.ts b/packages/happy-dom/src/console/VirtualConsole.ts
index 5c23f54eb..56232288b 100644
--- a/packages/happy-dom/src/console/VirtualConsole.ts
+++ b/packages/happy-dom/src/console/VirtualConsole.ts
@@ -15,11 +15,11 @@ export default class VirtualConsole implements Console {
// This is not part of the browser specs.
public Console: ConsoleConstructor;
- private _printer: IVirtualConsolePrinter;
- private _count: { [label: string]: number } = {};
- private _time: { [label: string]: number } = {};
- private _groupID = 0;
- private _groups: IVirtualConsoleLogGroup[] = [];
+ #printer: IVirtualConsolePrinter;
+ #count: { [label: string]: number } = {};
+ #time: { [label: string]: number } = {};
+ #groupID = 0;
+ #groups: IVirtualConsoleLogGroup[] = [];
/**
* Constructor.
@@ -27,7 +27,7 @@ export default class VirtualConsole implements Console {
* @param printer Console printer.
*/
constructor(printer: IVirtualConsolePrinter) {
- this._printer = printer;
+ this.#printer = printer;
}
/**
@@ -38,11 +38,11 @@ export default class VirtualConsole implements Console {
*/
public assert(assertion: boolean, ...args: Array