webpack/test/helpers/FakeDocument.js

286 lines
6.1 KiB
JavaScript

"use strict";
const fs = require("fs");
const path = require("path");
/**
* @this {FakeDocument}
* @param {string} property property
* @returns {EXPECTED_ANY} value
*/
function getPropertyValue(property) {
return this[property];
}
class FakeDocument {
constructor(basePath) {
this.head = this.createElement("head");
this.body = this.createElement("body");
this.baseURI = "https://test.cases/path/index.html";
this._elementsByTagName = new Map([
["head", [this.head]],
["body", [this.body]]
]);
this._basePath = basePath;
}
createElement(type) {
return new FakeElement(this, type, this._basePath);
}
_onElementAttached(element) {
const type = element._type;
let list = this._elementsByTagName.get(type);
if (list === undefined) {
list = [];
this._elementsByTagName.set(type, list);
}
list.push(element);
}
_onElementRemoved(element) {
const type = element._type;
const list = this._elementsByTagName.get(type);
const idx = list.indexOf(element);
list.splice(idx, 1);
}
getElementsByTagName(name) {
return this._elementsByTagName.get(name) || [];
}
getComputedStyle(element) {
const style = { getPropertyValue };
const links = this.getElementsByTagName("link");
for (const link of links) {
for (const rule of link.sheet.cssRules) {
if (rule.selectorText === element._type) {
Object.assign(style, rule.style);
}
}
}
return style;
}
}
class FakeElement {
constructor(document, type, basePath) {
this._document = document;
this._type = type;
this._children = [];
this._attributes = Object.create(null);
this._src = undefined;
this._href = undefined;
this.parentNode = undefined;
this.sheet = type === "link" ? new FakeSheet(this, basePath) : undefined;
}
_attach(node) {
this._document._onElementAttached(node);
this._children.push(node);
node.parentNode = this;
}
_load(node) {
if (node._type === "link") {
setTimeout(() => {
if (node.onload) node.onload({ type: "load", target: node });
}, 100);
} else if (node._type === "script" && this._document.onScript) {
Promise.resolve().then(() => {
this._document.onScript(node.src);
});
}
}
insertBefore(node) {
this._attach(node);
this._load(node);
}
appendChild(node) {
this._attach(node);
this._load(node);
}
removeChild(node) {
const idx = this._children.indexOf(node);
if (idx >= 0) {
this._children.splice(idx, 1);
this._document._onElementRemoved(node);
node.parentNode = undefined;
}
}
setAttribute(name, value) {
if (this._type === "link" && name === "href") {
this.href(value);
} else {
this._attributes[name] = value;
}
}
removeAttribute(name) {
delete this._attributes[name];
}
getAttribute(name) {
if (this._type === "link" && name === "href") {
return this.href;
}
return this._attributes[name];
}
_toRealUrl(value) {
if (/^\//.test(value)) {
return `https://test.cases${value}`;
} else if (/^\.\.\//.test(value)) {
return `https://test.cases${value.slice(2)}`;
} else if (/^\.\//.test(value)) {
return `https://test.cases/path${value.slice(1)}`;
} else if (/^\w+:\/\//.test(value)) {
return value;
} else if (/^\/\//.test(value)) {
return `https:${value}`;
}
return `https://test.cases/path/${value}`;
}
set src(value) {
if (this._type === "script") {
this._src = this._toRealUrl(value);
}
}
get src() {
return this._src;
}
set href(value) {
if (this._type === "link") {
this._href = this._toRealUrl(value);
}
}
get href() {
return this._href;
}
}
class FakeSheet {
constructor(element, basePath) {
this._element = element;
this._basePath = basePath;
}
get css() {
let css = fs.readFileSync(
path.resolve(
this._basePath,
this._element.href
.replace(/^https:\/\/test\.cases\/path\//, "")
.replace(/^https:\/\/example\.com\//, "")
),
"utf8"
);
css = css.replace(/@import url\("([^"]+)"\);/g, (match, url) => {
if (!/^https:\/\/test\.cases\/path\//.test(url)) {
return `@import url("${url}");`;
}
if (url.startsWith("#")) {
return url;
}
return fs.readFileSync(
path.resolve(
this._basePath,
url.replace(/^https:\/\/test\.cases\/path\//, "")
),
"utf8"
);
});
return css;
}
get cssRules() {
const walkCssTokens = require("../../lib/css/walkCssTokens");
const rules = [];
let currentRule = { getPropertyValue };
let selector;
let last = 0;
const processDeclaration = (str) => {
const colon = str.indexOf(":");
if (colon > 0) {
const property = str.slice(0, colon).trim();
const value = str.slice(colon + 1);
currentRule[property] = value;
}
};
const filepath = /file:\/\//.test(this._element.href)
? new URL(this._element.href)
: path.resolve(
this._basePath,
this._element.href
.replace(/^https:\/\/test\.cases\/path\//, "")
.replace(/^https:\/\/example\.com\/public\/path\//, "")
.replace(/^https:\/\/example\.com\//, "")
);
let css = fs.readFileSync(filepath, "utf8");
css = css
// Remove comments
// @ts-expect-error we use es2018 for such tests
.replace(/\/\*.*?\*\//gms, "")
.replace(/@import url\("([^"]+)"\);/g, (match, url) => {
if (!/^https:\/\/test\.cases\/path\//.test(url)) {
return url;
}
if (url.startsWith("#")) {
return url;
}
return fs.readFileSync(
path.resolve(
this._basePath,
url.replace(/^https:\/\/test\.cases\/path\//, "")
),
"utf8"
);
});
walkCssTokens(css, 0, {
leftCurlyBracket(source, start, end) {
if (selector === undefined) {
selector = source.slice(last, start).trim();
last = end;
}
return end;
},
rightCurlyBracket(source, start, end) {
processDeclaration(source.slice(last, start));
last = end;
rules.push({ selectorText: selector, style: currentRule });
selector = undefined;
currentRule = { getPropertyValue };
return end;
},
semicolon(source, start, end) {
processDeclaration(source.slice(last, start));
last = end;
return end;
}
});
return rules;
}
}
FakeDocument.FakeSheet = FakeSheet;
FakeDocument.FakeElement = FakeDocument;
module.exports = FakeDocument;