412 lines
9.2 KiB
JavaScript
Executable File
412 lines
9.2 KiB
JavaScript
Executable File
|
|
import * as fs from 'fs/promises';
|
|
import * as node from '../source/node.js';
|
|
import * as path from 'path';
|
|
import * as url from 'url';
|
|
import * as worker_threads from 'worker_threads';
|
|
|
|
const mock = {};
|
|
|
|
mock.Context = class {
|
|
|
|
constructor(host, folder, identifier, stream, entries) {
|
|
this._host = host;
|
|
this._folder = folder;
|
|
this._identifier = identifier;
|
|
this._stream = stream;
|
|
this._entries = entries;
|
|
}
|
|
|
|
get identifier() {
|
|
return this._identifier;
|
|
}
|
|
|
|
get stream() {
|
|
return this._stream;
|
|
}
|
|
|
|
get entries() {
|
|
return this._entries;
|
|
}
|
|
|
|
async asset(file) {
|
|
return this._host.asset(file);
|
|
}
|
|
|
|
async fetch(file, encoding, base) {
|
|
return this._host.fetch(file, encoding, base === undefined ? this._folder : base);
|
|
}
|
|
|
|
async require(id) {
|
|
return this._host.require(id);
|
|
}
|
|
|
|
error(error, fatal) {
|
|
this._host.exception(error, fatal);
|
|
}
|
|
};
|
|
|
|
class CSSStyleDeclaration {
|
|
|
|
constructor() {
|
|
this._properties = new Map();
|
|
}
|
|
|
|
setProperty(name, value) {
|
|
this._properties.set(name, value);
|
|
}
|
|
|
|
removeProperty(name) {
|
|
this._properties.delete(name);
|
|
}
|
|
}
|
|
|
|
class DOMTokenList {
|
|
|
|
constructor(element) {
|
|
this._element = element;
|
|
}
|
|
|
|
add(...tokens) {
|
|
const value = this._element.getAttribute('class') || '';
|
|
const set = new Set(value.split(' ').concat(...tokens));
|
|
this._element.setAttribute('class', Array.from(set).filter((s) => s).join(' '));
|
|
}
|
|
|
|
contains(token) {
|
|
const value = this._element.getAttribute('class');
|
|
if (value === null || value.indexOf(token) === -1) {
|
|
return false;
|
|
}
|
|
return value.split(' ').some((s) => s === token);
|
|
}
|
|
}
|
|
|
|
class HTMLElement {
|
|
|
|
constructor(document) {
|
|
this._document = document;
|
|
this._childNodes = [];
|
|
this._attributes = new Map();
|
|
this._style = new CSSStyleDeclaration();
|
|
}
|
|
|
|
get style() {
|
|
return this._style;
|
|
|
|
}
|
|
|
|
get childNodes() {
|
|
return this._childNodes;
|
|
}
|
|
|
|
get firstChild() {
|
|
return this._childNodes.length > 0 ? this._childNodes[0] : null;
|
|
}
|
|
|
|
get lastChild() {
|
|
const index = this._childNodes.length - 1;
|
|
if (index >= 0) {
|
|
return this._childNodes[index];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
appendChild(node) {
|
|
this._childNodes.push(node);
|
|
}
|
|
|
|
insertBefore(newNode, referenceNode) {
|
|
const index = this._childNodes.indexOf(referenceNode);
|
|
if (index !== -1) {
|
|
this._childNodes.splice(index, 0, newNode);
|
|
}
|
|
}
|
|
|
|
removeChild(node) {
|
|
const index = this._childNodes.lastIndexOf(node);
|
|
if (index !== -1) {
|
|
this._childNodes.splice(index, 1);
|
|
this._document._removeElement(node);
|
|
}
|
|
}
|
|
|
|
replaceChildren(...nodes) {
|
|
for (const child of this._childNodes) {
|
|
this._document._removeElement(child);
|
|
}
|
|
this._childNodes = [...nodes];
|
|
}
|
|
|
|
replaceChild(newChild, oldChild) {
|
|
const index = this._childNodes.indexOf(oldChild);
|
|
if (index !== -1) {
|
|
this._childNodes[index] = newChild;
|
|
}
|
|
return oldChild;
|
|
}
|
|
|
|
setAttribute(name, value) {
|
|
if (name === 'id') {
|
|
this._document._updateElementId(this, value);
|
|
}
|
|
this._attributes.set(name, value);
|
|
}
|
|
|
|
hasAttribute(name) {
|
|
return this._attributes.has(name);
|
|
}
|
|
|
|
getAttribute(name) {
|
|
return this._attributes.has(name) ? this._attributes.get(name) : null;
|
|
}
|
|
|
|
getElementsByClassName(name) {
|
|
const elements = [];
|
|
for (const node of this._childNodes) {
|
|
if (node instanceof HTMLElement) {
|
|
elements.push(...node.getElementsByClassName(name));
|
|
if (node.classList.contains(name)) {
|
|
elements.push(node);
|
|
}
|
|
}
|
|
}
|
|
return elements;
|
|
}
|
|
|
|
addEventListener(/* event, callback */) {
|
|
}
|
|
|
|
removeEventListener(/* event, callback */) {
|
|
}
|
|
|
|
get classList() {
|
|
this._classList = this._classList || new DOMTokenList(this);
|
|
return this._classList;
|
|
}
|
|
|
|
getBBox() {
|
|
return { x: 0, y: 0, width: 10, height: 10 };
|
|
}
|
|
|
|
getBoundingClientRect() {
|
|
return { left: 0, top: 0, width: 0, height: 0 };
|
|
}
|
|
|
|
scrollTo() {
|
|
}
|
|
|
|
focus() {
|
|
}
|
|
}
|
|
|
|
class Document {
|
|
|
|
constructor() {
|
|
this._elements = new Map();
|
|
this._documentElement = new HTMLElement(this);
|
|
this._body = new HTMLElement(this);
|
|
this._documentElement.appendChild(this._body);
|
|
}
|
|
|
|
get documentElement() {
|
|
return this._documentElement;
|
|
}
|
|
|
|
get body() {
|
|
return this._body;
|
|
}
|
|
|
|
createElement(name) {
|
|
const element = new HTMLElement(this);
|
|
element.tagName = name.toUpperCase();
|
|
return element;
|
|
}
|
|
|
|
createElementNS(namespace, name) {
|
|
const element = new HTMLElement(this);
|
|
element.namespaceURI = namespace;
|
|
element.tagName = name;
|
|
return element;
|
|
}
|
|
|
|
createTextNode(text) {
|
|
const element = new HTMLElement(this);
|
|
element.textContent = text;
|
|
return element;
|
|
}
|
|
|
|
addEventListener(/* event, callback */) {
|
|
}
|
|
|
|
removeEventListener(/* event, callback */) {
|
|
}
|
|
|
|
getElementById(id) {
|
|
return this._elements.get(id) || null;
|
|
}
|
|
|
|
_removeElement(element) {
|
|
this._updateElementId(element, null);
|
|
for (const child of element.childNodes) {
|
|
this._removeElement(child);
|
|
}
|
|
}
|
|
|
|
_updateElementId(element, newId) {
|
|
const previous = element.getAttribute('id');
|
|
if (previous) {
|
|
this._elements.delete(previous);
|
|
}
|
|
if (newId) {
|
|
this._elements.set(newId, element);
|
|
}
|
|
}
|
|
}
|
|
|
|
class Window {
|
|
|
|
constructor() {
|
|
this._document = new Document();
|
|
}
|
|
|
|
get document() {
|
|
return this._document;
|
|
}
|
|
|
|
addEventListener(/* event, callback */) {
|
|
}
|
|
|
|
removeEventListener(/* event, callback */) {
|
|
}
|
|
|
|
requestAnimationFrame(callback) {
|
|
callback();
|
|
}
|
|
|
|
setTimeout(code, delay) {
|
|
return global.setTimeout(code, delay);
|
|
}
|
|
|
|
clearTimeout(timeoutID) {
|
|
global.clearTimeout(timeoutID);
|
|
}
|
|
}
|
|
|
|
mock.Host = class {
|
|
|
|
constructor(environment) {
|
|
this._environment = environment;
|
|
this._errors = [];
|
|
mock.Host.source = mock.Host.source || this._dirname('..', 'source');
|
|
if (!mock.Host.window) {
|
|
mock.Host.window = new Window();
|
|
const document = mock.Host.window.document;
|
|
for (const id of ['target', 'sidebar']) {
|
|
const element = document.createElement('div');
|
|
element.setAttribute('id', id);
|
|
document.body.appendChild(element);
|
|
}
|
|
}
|
|
}
|
|
|
|
async view(/* view */) {
|
|
}
|
|
|
|
async start() {
|
|
}
|
|
|
|
get window() {
|
|
return mock.Host.window;
|
|
}
|
|
|
|
get document() {
|
|
return this.window.document;
|
|
}
|
|
|
|
get errors() {
|
|
return this._errors;
|
|
}
|
|
|
|
get type() {
|
|
return 'Test';
|
|
}
|
|
|
|
environment(name) {
|
|
return this._environment[name];
|
|
}
|
|
|
|
update() {
|
|
}
|
|
|
|
screen(/* name */) {
|
|
}
|
|
|
|
async require(id) {
|
|
const file = path.join(mock.Host.source, `${id}.js`);
|
|
return await import(`file://${file}`);
|
|
}
|
|
|
|
worker(id) {
|
|
const file = path.join(mock.Host.source, `${id}.js`);
|
|
const worker = new worker_threads.Worker(file);
|
|
worker.addEventListener = (type, listener) => {
|
|
worker.on(type, (message) => listener({ data: message }));
|
|
};
|
|
return worker;
|
|
}
|
|
|
|
async asset(file) {
|
|
return this.fetch(file, 'utf-8', null);
|
|
}
|
|
|
|
async fetch(file, encoding, basename) {
|
|
const root = path.resolve(basename || mock.Host.source);
|
|
const pathname = path.resolve(root, file);
|
|
const relative = path.relative(root, pathname);
|
|
if (relative !== '' && (relative.startsWith('..') || path.isAbsolute(relative))) {
|
|
throw new Error(`The path '${pathname}' is invalid.`);
|
|
}
|
|
const exists = await this._access(pathname);
|
|
if (!exists) {
|
|
throw new Error(`The file '${file}' does not exist.`);
|
|
}
|
|
const stats = await fs.stat(pathname);
|
|
if (stats.isDirectory()) {
|
|
throw new Error(`The path '${file}' is a directory.`);
|
|
}
|
|
if (encoding) {
|
|
return await fs.readFile(pathname, encoding);
|
|
}
|
|
return new node.FileStream(pathname, 0, stats.size, stats.mtimeMs);
|
|
}
|
|
|
|
event(/* name, params */) {
|
|
}
|
|
|
|
exception(error /*, fatal */) {
|
|
this._errors.push(error);
|
|
}
|
|
|
|
message() {
|
|
}
|
|
|
|
async _access(path) {
|
|
try {
|
|
await fs.access(path);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
_dirname(...args) {
|
|
const file = url.fileURLToPath(import.meta.url);
|
|
const dir = path.dirname(file);
|
|
return path.join(dir, ...args);
|
|
}
|
|
};
|
|
|
|
export const Host = mock.Host;
|
|
export const Context = mock.Context;
|