mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-11-21 23:20:20 +00:00
ed76e1ed4b
Stop relying on Temporal, at least temporarily. The classes used here will soon be removed (until they are implemented again from scratch).
698 lines
23 KiB
JavaScript
698 lines
23 KiB
JavaScript
var describe;
|
|
var test;
|
|
var expect;
|
|
var withinSameSecond;
|
|
|
|
// Stores the results of each test and suite. Has a terrible
|
|
// name to avoid name collision.
|
|
var __TestResults__ = {};
|
|
|
|
// So test names like "toString" don't automatically produce an error
|
|
Object.setPrototypeOf(__TestResults__, null);
|
|
|
|
// This array is used to communicate with the C++ program. It treats
|
|
// each message in this array as a separate message. Has a terrible
|
|
// name to avoid name collision.
|
|
var __UserOutput__ = [];
|
|
|
|
// We also rebind console.log here to use the array above
|
|
console.log = (...args) => {
|
|
__UserOutput__.push(args.join(" "));
|
|
};
|
|
|
|
class ExpectationError extends Error {
|
|
constructor(message) {
|
|
super(message);
|
|
this.name = "ExpectationError";
|
|
}
|
|
}
|
|
|
|
// Use an IIFE to avoid polluting the global namespace as much as possible
|
|
(() => {
|
|
const deepEquals = (a, b) => {
|
|
if (Object.is(a, b)) return true; // Handles identical references and primitives
|
|
if ((a !== null && b === null) || (a === null && b !== null)) return false;
|
|
if (Array.isArray(a)) return Array.isArray(b) && deepArrayEquals(a, b);
|
|
if (typeof a === "object") return typeof b === "object" && deepObjectEquals(a, b);
|
|
};
|
|
|
|
const deepArrayEquals = (a, b) => {
|
|
if (a.length !== b.length) return false;
|
|
for (let i = 0; i < a.length; ++i) {
|
|
if (!deepEquals(a[i], b[i])) return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
const deepObjectEquals = (a, b) => {
|
|
const keysA = Reflect.ownKeys(a);
|
|
const keysB = Reflect.ownKeys(b);
|
|
|
|
if (keysA.length !== keysB.length) return false;
|
|
for (let key of keysA) {
|
|
if (!deepEquals(a[key], b[key])) return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
const valueToString = value => {
|
|
try {
|
|
if (value === 0 && 1 / value < 0) {
|
|
return "-0";
|
|
}
|
|
return String(value);
|
|
} catch {
|
|
// e.g for objects without a prototype, the above throws.
|
|
return Object.prototype.toString.call(value);
|
|
}
|
|
};
|
|
|
|
class Expector {
|
|
constructor(target, inverted) {
|
|
this.target = target;
|
|
this.inverted = !!inverted;
|
|
}
|
|
|
|
get not() {
|
|
return new Expector(this.target, !this.inverted);
|
|
}
|
|
|
|
toBe(value) {
|
|
this.__doMatcher(() => {
|
|
this.__expect(
|
|
Object.is(this.target, value),
|
|
() =>
|
|
`toBe: expected _${valueToString(value)}_, got _${valueToString(
|
|
this.target
|
|
)}_`
|
|
);
|
|
});
|
|
}
|
|
|
|
toBeCloseTo(value, precision = 5) {
|
|
this.__expect(
|
|
typeof this.target === "number",
|
|
() => `toBeCloseTo: expected target of type number, got ${typeof this.target}`
|
|
);
|
|
this.__expect(
|
|
typeof value === "number",
|
|
() => `toBeCloseTo: expected argument of type number, got ${typeof value}`
|
|
);
|
|
this.__expect(
|
|
typeof precision === "number",
|
|
() => `toBeCloseTo: expected precision of type number, got ${typeof precision}`
|
|
);
|
|
|
|
const epsilon = 10 ** -precision / 2;
|
|
this.__doMatcher(() => {
|
|
this.__expect(Math.abs(this.target - value) < epsilon);
|
|
});
|
|
}
|
|
|
|
toHaveLength(length) {
|
|
this.__expect(
|
|
typeof this.target.length === "number",
|
|
() => "toHaveLength: target.length not of type number"
|
|
);
|
|
|
|
this.__doMatcher(() => {
|
|
this.__expect(Object.is(this.target.length, length));
|
|
});
|
|
}
|
|
|
|
toHaveSize(size) {
|
|
this.__expect(
|
|
typeof this.target.size === "number",
|
|
() => "toHaveSize: target.size not of type number"
|
|
);
|
|
|
|
this.__doMatcher(() => {
|
|
this.__expect(Object.is(this.target.size, size));
|
|
});
|
|
}
|
|
|
|
toHaveProperty(property, value) {
|
|
this.__doMatcher(() => {
|
|
let object = this.target;
|
|
|
|
if (typeof property === "string" && property.includes(".")) {
|
|
let propertyArray = [];
|
|
|
|
while (property.includes(".")) {
|
|
let index = property.indexOf(".");
|
|
propertyArray.push(property.substring(0, index));
|
|
if (index + 1 >= property.length) break;
|
|
property = property.substring(index + 1, property.length);
|
|
}
|
|
|
|
propertyArray.push(property);
|
|
|
|
property = propertyArray;
|
|
}
|
|
|
|
if (Array.isArray(property)) {
|
|
for (let key of property) {
|
|
this.__expect(
|
|
object !== undefined && object !== null,
|
|
"got undefined or null as array key"
|
|
);
|
|
object = object[key];
|
|
}
|
|
} else {
|
|
object = object[property];
|
|
}
|
|
|
|
this.__expect(object !== undefined, "should not be undefined");
|
|
if (value !== undefined)
|
|
this.__expect(
|
|
deepEquals(object, value),
|
|
`value does not equal property ${valueToString(object)} vs ${valueToString(
|
|
value
|
|
)}`
|
|
);
|
|
});
|
|
}
|
|
|
|
toBeDefined() {
|
|
this.__doMatcher(() => {
|
|
this.__expect(
|
|
this.target !== undefined,
|
|
() => "toBeDefined: expected target to be defined, got undefined"
|
|
);
|
|
});
|
|
}
|
|
|
|
toBeInstanceOf(class_) {
|
|
this.__doMatcher(() => {
|
|
this.__expect(
|
|
this.target instanceof class_,
|
|
`Expected ${valueToString(this.target)} to be instance of ${class_.name}`
|
|
);
|
|
});
|
|
}
|
|
|
|
toBeNull() {
|
|
this.__doMatcher(() => {
|
|
this.__expect(
|
|
this.target === null,
|
|
`Expected target to be null got ${valueToString(this.target)}`
|
|
);
|
|
});
|
|
}
|
|
|
|
toBeUndefined() {
|
|
this.__doMatcher(() => {
|
|
this.__expect(
|
|
this.target === undefined,
|
|
() =>
|
|
`toBeUndefined: expected target to be undefined, got _${valueToString(
|
|
this.target
|
|
)}_`
|
|
);
|
|
});
|
|
}
|
|
|
|
toBeNaN() {
|
|
this.__doMatcher(() => {
|
|
this.__expect(
|
|
isNaN(this.target),
|
|
() => `toBeNaN: expected target to be NaN, got _${valueToString(this.target)}_`
|
|
);
|
|
});
|
|
}
|
|
|
|
toBeTrue(customDetails = undefined) {
|
|
this.__doMatcher(() => {
|
|
this.__expect(
|
|
this.target === true,
|
|
() =>
|
|
`toBeTrue: expected target to be true, got _${valueToString(this.target)}_${
|
|
customDetails ? ` (${customDetails})` : ""
|
|
}`
|
|
);
|
|
});
|
|
}
|
|
|
|
toBeFalse(customDetails = undefined) {
|
|
this.__doMatcher(() => {
|
|
this.__expect(
|
|
this.target === false,
|
|
() =>
|
|
`toBeFalse: expected target to be false, got _${valueToString(
|
|
this.target
|
|
)}_${customDetails ?? ""}`
|
|
);
|
|
});
|
|
}
|
|
|
|
__validateNumericComparisonTypes(value) {
|
|
this.__expect(typeof this.target === "number" || typeof this.target === "bigint");
|
|
this.__expect(typeof value === "number" || typeof value === "bigint");
|
|
this.__expect(typeof this.target === typeof value);
|
|
}
|
|
|
|
toBeLessThan(value) {
|
|
this.__validateNumericComparisonTypes(value);
|
|
|
|
this.__doMatcher(() => {
|
|
this.__expect(this.target < value);
|
|
});
|
|
}
|
|
|
|
toBeLessThanOrEqual(value) {
|
|
this.__validateNumericComparisonTypes(value);
|
|
|
|
this.__doMatcher(() => {
|
|
this.__expect(this.target <= value);
|
|
});
|
|
}
|
|
|
|
toBeGreaterThan(value) {
|
|
this.__validateNumericComparisonTypes(value);
|
|
|
|
this.__doMatcher(() => {
|
|
this.__expect(this.target > value);
|
|
});
|
|
}
|
|
|
|
toBeGreaterThanOrEqual(value) {
|
|
this.__validateNumericComparisonTypes(value);
|
|
|
|
this.__doMatcher(() => {
|
|
this.__expect(this.target >= value);
|
|
});
|
|
}
|
|
|
|
toContain(item) {
|
|
this.__doMatcher(() => {
|
|
for (let element of this.target) {
|
|
if (item === element) return;
|
|
}
|
|
|
|
throw new ExpectationError();
|
|
});
|
|
}
|
|
|
|
toContainEqual(item) {
|
|
this.__doMatcher(() => {
|
|
for (let element of this.target) {
|
|
if (deepEquals(item, element)) return;
|
|
}
|
|
|
|
throw new ExpectationError();
|
|
});
|
|
}
|
|
|
|
toEqual(value) {
|
|
this.__doMatcher(() => {
|
|
this.__expect(
|
|
deepEquals(this.target, value),
|
|
() =>
|
|
`Expected _${valueToString(value)}_, but got _${valueToString(
|
|
this.target
|
|
)}_`
|
|
);
|
|
});
|
|
}
|
|
|
|
toThrow(value) {
|
|
this.__expect(typeof this.target === "function");
|
|
this.__expect(
|
|
typeof value === "string" ||
|
|
typeof value === "function" ||
|
|
typeof value === "object" ||
|
|
value === undefined
|
|
);
|
|
|
|
this.__doMatcher(() => {
|
|
let threw = true;
|
|
try {
|
|
this.target();
|
|
threw = false;
|
|
} catch (e) {
|
|
if (typeof value === "string") {
|
|
this.__expect(
|
|
e.message.includes(value),
|
|
`Expected ${this.target.toString()} to throw and message to include "${value}" but message "${
|
|
e.message
|
|
}" did not contain it`
|
|
);
|
|
} else if (typeof value === "function") {
|
|
this.__expect(
|
|
e instanceof value,
|
|
`Expected ${this.target.toString()} to throw and be of type ${value} but it threw ${e}`
|
|
);
|
|
} else if (typeof value === "object") {
|
|
this.__expect(
|
|
e.message === value.message,
|
|
`Expected ${this.target.toString()} to throw and message to be ${value} but it threw with message ${
|
|
e.message
|
|
}`
|
|
);
|
|
}
|
|
}
|
|
this.__expect(
|
|
threw,
|
|
`Expected ${this.target.toString()} to throw but it didn't throw anything`
|
|
);
|
|
});
|
|
}
|
|
|
|
pass(message) {
|
|
// FIXME: This does nothing. If we want to implement things
|
|
// like assertion count, this will have to do something
|
|
}
|
|
|
|
// jest-extended
|
|
fail(message) {
|
|
this.__doMatcher(() => {
|
|
this.__expect(false, message);
|
|
});
|
|
}
|
|
|
|
// jest-extended
|
|
toThrowWithMessage(class_, message) {
|
|
this.__expect(typeof this.target === "function");
|
|
this.__expect(class_ !== undefined);
|
|
this.__expect(message !== undefined);
|
|
|
|
this.__doMatcher(() => {
|
|
try {
|
|
this.target();
|
|
this.__expect(false, () => "toThrowWithMessage: target function did not throw");
|
|
} catch (e) {
|
|
this.__expect(
|
|
e instanceof class_,
|
|
() =>
|
|
`toThrowWithMessage: expected error to be instance of ${valueToString(
|
|
class_.name
|
|
)}, got ${valueToString(e.name)}`
|
|
);
|
|
this.__expect(
|
|
e.message.includes(message),
|
|
() =>
|
|
`toThrowWithMessage: expected error message to include _${valueToString(
|
|
message
|
|
)}_, got _${valueToString(e.message)}_`
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Test for syntax errors; target must be a string
|
|
toEval() {
|
|
this.__expect(typeof this.target === "string");
|
|
const success = canParseSource(this.target);
|
|
this.__expect(
|
|
this.inverted ? !success : success,
|
|
() =>
|
|
`Expected _${valueToString(this.target)}_ ` +
|
|
(this.inverted ? "not to eval but it did" : "to eval but it didn't")
|
|
);
|
|
}
|
|
|
|
// Must compile regardless of inverted-ness
|
|
toEvalTo(value) {
|
|
this.__expect(typeof this.target === "string");
|
|
|
|
let result;
|
|
|
|
try {
|
|
result = eval(this.target);
|
|
} catch (e) {
|
|
throw new ExpectationError(
|
|
`Expected _${valueToString(this.target)}_ to eval but it failed with ${e}`
|
|
);
|
|
}
|
|
|
|
this.__doMatcher(() => {
|
|
this.__expect(
|
|
deepEquals(value, result),
|
|
() =>
|
|
`Expected _${valueToString(this.target)}_ to eval to ` +
|
|
`_${valueToString(value)}_ but got _${valueToString(result)}_`
|
|
);
|
|
});
|
|
}
|
|
|
|
toHaveConfigurableProperty(property) {
|
|
this.__expect(this.target !== undefined && this.target !== null);
|
|
let d = Object.getOwnPropertyDescriptor(this.target, property);
|
|
this.__expect(d !== undefined);
|
|
|
|
this.__doMatcher(() => {
|
|
this.__expect(d.configurable);
|
|
});
|
|
}
|
|
|
|
toHaveEnumerableProperty(property) {
|
|
this.__expect(this.target !== undefined && this.target !== null);
|
|
let d = Object.getOwnPropertyDescriptor(this.target, property);
|
|
this.__expect(d !== undefined);
|
|
|
|
this.__doMatcher(() => {
|
|
this.__expect(d.enumerable);
|
|
});
|
|
}
|
|
|
|
toHaveWritableProperty(property) {
|
|
this.__expect(this.target !== undefined && this.target !== null);
|
|
let d = Object.getOwnPropertyDescriptor(this.target, property);
|
|
this.__expect(d !== undefined);
|
|
|
|
this.__doMatcher(() => {
|
|
this.__expect(d.writable);
|
|
});
|
|
}
|
|
|
|
toHaveValueProperty(property, value) {
|
|
this.__expect(this.target !== undefined && this.target !== null);
|
|
let d = Object.getOwnPropertyDescriptor(this.target, property);
|
|
this.__expect(d !== undefined);
|
|
|
|
this.__doMatcher(() => {
|
|
this.__expect(d.value !== undefined);
|
|
if (value !== undefined) this.__expect(deepEquals(value, d.value));
|
|
});
|
|
}
|
|
|
|
toHaveGetterProperty(property) {
|
|
this.__expect(this.target !== undefined && this.target !== null);
|
|
let d = Object.getOwnPropertyDescriptor(this.target, property);
|
|
this.__expect(d !== undefined);
|
|
|
|
this.__doMatcher(() => {
|
|
this.__expect(d.get !== undefined);
|
|
});
|
|
}
|
|
|
|
toHaveSetterProperty(property) {
|
|
this.__expect(this.target !== undefined && this.target !== null);
|
|
let d = Object.getOwnPropertyDescriptor(this.target, property);
|
|
this.__expect(d !== undefined);
|
|
|
|
this.__doMatcher(() => {
|
|
this.__expect(d.set !== undefined);
|
|
});
|
|
}
|
|
|
|
toBeIteratorResultWithValue(value) {
|
|
this.__expect(this.target !== undefined && this.target !== null);
|
|
this.__doMatcher(() => {
|
|
this.__expect(
|
|
this.target.done === false,
|
|
() =>
|
|
`toGiveIteratorResultWithValue: expected 'done' to be _false_ got ${valueToString(
|
|
this.target.done
|
|
)}`
|
|
);
|
|
this.__expect(
|
|
deepEquals(value, this.target.value),
|
|
() =>
|
|
`toGiveIteratorResultWithValue: expected 'value' to be _${valueToString(
|
|
value
|
|
)}_ got ${valueToString(this.target.value)}`
|
|
);
|
|
});
|
|
}
|
|
|
|
toBeIteratorResultDone() {
|
|
this.__expect(this.target !== undefined && this.target !== null);
|
|
this.__doMatcher(() => {
|
|
this.__expect(
|
|
this.target.done === true,
|
|
() =>
|
|
`toGiveIteratorResultDone: expected 'done' to be _true_ got ${valueToString(
|
|
this.target.done
|
|
)}`
|
|
);
|
|
this.__expect(
|
|
this.target.value === undefined,
|
|
() =>
|
|
`toGiveIteratorResultDone: expected 'value' to be _undefined_ got ${valueToString(
|
|
this.target.value
|
|
)}`
|
|
);
|
|
});
|
|
}
|
|
|
|
__doMatcher(matcher) {
|
|
if (!this.inverted) {
|
|
matcher();
|
|
} else {
|
|
let threw = false;
|
|
try {
|
|
matcher();
|
|
} catch (e) {
|
|
if (e.name === "ExpectationError") threw = true;
|
|
}
|
|
if (!threw) throw new ExpectationError("not: test didn't fail");
|
|
}
|
|
}
|
|
|
|
__expect(value, details) {
|
|
if (value !== true) {
|
|
if (details !== undefined) {
|
|
if (details instanceof Function) throw new ExpectationError(details());
|
|
else throw new ExpectationError(details);
|
|
} else {
|
|
throw new ExpectationError();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
expect = value => new Expector(value);
|
|
|
|
// describe is able to lump test results inside of it by using this context
|
|
// variable. Top level tests have the default suite message
|
|
const defaultSuiteMessage = "__$$TOP_LEVEL$$__";
|
|
let suiteMessage = defaultSuiteMessage;
|
|
|
|
describe = (message, callback) => {
|
|
suiteMessage = message;
|
|
if (!__TestResults__[suiteMessage]) __TestResults__[suiteMessage] = {};
|
|
try {
|
|
callback();
|
|
} catch (e) {
|
|
__TestResults__[suiteMessage][defaultSuiteMessage] = {
|
|
result: "fail",
|
|
details: String(e),
|
|
duration: 0,
|
|
};
|
|
}
|
|
suiteMessage = defaultSuiteMessage;
|
|
};
|
|
|
|
test = (message, callback) => {
|
|
if (!__TestResults__[suiteMessage]) __TestResults__[suiteMessage] = {};
|
|
|
|
const suite = __TestResults__[suiteMessage];
|
|
if (Object.prototype.hasOwnProperty.call(suite, message)) {
|
|
suite[message] = {
|
|
result: "fail",
|
|
details: "Another test with the same message did already run",
|
|
duration: 0,
|
|
};
|
|
return;
|
|
}
|
|
|
|
const start = Date.now();
|
|
const time_ms = () => Date.now() - start;
|
|
|
|
try {
|
|
callback();
|
|
suite[message] = {
|
|
result: "pass",
|
|
duration: time_ms(),
|
|
};
|
|
} catch (e) {
|
|
suite[message] = {
|
|
result: "fail",
|
|
details: String(e),
|
|
duration: time_ms(),
|
|
};
|
|
}
|
|
};
|
|
|
|
test.skip = (message, callback) => {
|
|
if (typeof callback !== "function")
|
|
throw new Error("test.skip has invalid second argument (must be a function)");
|
|
|
|
if (!__TestResults__[suiteMessage]) __TestResults__[suiteMessage] = {};
|
|
|
|
const suite = __TestResults__[suiteMessage];
|
|
if (Object.prototype.hasOwnProperty.call(suite, message)) {
|
|
suite[message] = {
|
|
result: "fail",
|
|
details: "Another test with the same message did already run",
|
|
duration: 0,
|
|
};
|
|
return;
|
|
}
|
|
|
|
suite[message] = {
|
|
result: "skip",
|
|
duration: 0,
|
|
};
|
|
};
|
|
|
|
test.xfail = (message, callback) => {
|
|
if (!__TestResults__[suiteMessage]) __TestResults__[suiteMessage] = {};
|
|
|
|
const suite = __TestResults__[suiteMessage];
|
|
if (Object.prototype.hasOwnProperty.call(suite, message)) {
|
|
suite[message] = {
|
|
result: "fail",
|
|
details: "Another test with the same message did already run",
|
|
duration: 0,
|
|
};
|
|
return;
|
|
}
|
|
|
|
const start = Date.now();
|
|
const time_ms = () => Date.now() - start;
|
|
|
|
try {
|
|
callback();
|
|
suite[message] = {
|
|
result: "fail",
|
|
details: "Expected test to fail, but it passed",
|
|
duration: time_ms(),
|
|
};
|
|
} catch (e) {
|
|
suite[message] = {
|
|
result: "xfail",
|
|
duration: time_ms(),
|
|
};
|
|
}
|
|
};
|
|
|
|
test.xfailIf = (condition, message, callback) => {
|
|
condition ? test.xfail(message, callback) : test(message, callback);
|
|
};
|
|
|
|
withinSameSecond = callback => {
|
|
let callbackDuration;
|
|
for (let tries = 0; tries < 5; tries++) {
|
|
const start = Date.now();
|
|
const result = callback();
|
|
const end = Date.now();
|
|
|
|
if (start / 1000 != end / 1000) {
|
|
callbackDuration = end - start;
|
|
continue;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
throw new ExpectationError(
|
|
`Tried to execute callback '${callback}' 5 times within the same second but ` +
|
|
`failed. Make sure the callback does as little work as possible (the last run ` +
|
|
`took ${callbackDuration}ms) and the machine is not overloaded. If you see this ` +
|
|
`error appearing in the CI it is most likely a flaky failure!`
|
|
);
|
|
};
|
|
})();
|