blob: f9e0a0541a9cca8729d9aaad05c42cade3f0ac62 [file] [edit]
export const LAYOUT_MODES = Object.freeze(["getBoundingClientRect", "getBoundingRectAndElementFromPoint"]);
export class Params {
viewport = {
width: 800,
height: 600,
};
// Enable a detailed developer menu to change the current Params.
developerMode = false;
startAutomatically = false;
iterationCount = 10;
suites = [];
// A list of tags to filter suites
tags = ["default"];
// Toggle running a dummy suite once before the normal test suites.
useWarmupSuite = false;
// toggle async type vs default raf type.
useAsyncSteps = false;
// Change how a test measurement is triggered and async time is measured:
// "timer": The classic (as in Speedometer 2.x) way using setTimeout
// "raf": Using rAF callbacks, both for triggering the sync part and for measuring async time.
measurementMethod = "raf";
// Wait time after suite preparation in ms.
waitAfterSetup = 0;
// Wait time before the sync step in ms.
waitBeforeSync = 0;
// Wait after running a suite.
waitAfterSuite = 0;
// Warmup time before the sync step in ms.
warmupBeforeSync = 0;
// Seed for shuffling the execution order of suites.
// "off": do not shuffle
// "generate": generate a random seed
// <integer>: use the provided integer as a seed
shuffleSeed = "off";
// Param to tweak the relative complexity of all suites.
// The default is 1.0, and for suites supporting this param, the duration
// roughly scales with the complexity.
complexity = 1.0;
// Choices: "getBoundingClientRect" or "getBoundingRectAndElementFromPoint"
layoutMode = LAYOUT_MODES[0];
// Measure more workload prepare time.
measurePrepare = false;
// External config url to override internal tests.
config = "";
constructor(searchParams = undefined, warnUnused = false) {
if (searchParams)
this._copyFromSearchParams(searchParams, warnUnused);
if (!this.developerMode) {
Object.freeze(this.viewport);
Object.freeze(this);
}
}
_parseNumber(value, errorMessage, minValue = 0) {
const number = Number(value);
if (!Number.isFinite(number) && errorMessage)
throw new Error(`Invalid ${errorMessage} param: '${value}', expected Number.`);
if (number < minValue)
throw new Error(`Invalid ${errorMessage} param: '${value}', value must be >= ${minValue}.`);
return number;
}
_parseInt(value, errorMessage, minValue = 0) {
const number = this._parseNumber(value, errorMessage, minValue);
if (!Number.isInteger(number) && errorMessage)
throw new Error(`Invalid ${errorMessage} param: '${value}', expected int.`);
return parseInt(number);
}
_copyFromSearchParams(searchParams, warnUnused) {
this.viewport = this._parseViewport(searchParams);
this.startAutomatically = this._parseBooleanParam(searchParams, "startAutomatically");
this.iterationCount = this._parseIntParam(searchParams, "iterationCount", 1);
this.suites = this._parseSuites(searchParams);
this.tags = this._parseTags(searchParams);
this.developerMode = this._parseBooleanParam(searchParams, "developerMode");
this.useWarmupSuite = this._parseBooleanParam(searchParams, "useWarmupSuite");
this.useAsyncSteps = this._parseBooleanParam(searchParams, "useAsyncSteps");
this.waitAfterSetup = this._parseIntParam(searchParams, "waitAfterSetup", 0);
this.waitBeforeSync = this._parseIntParam(searchParams, "waitBeforeSync", 0);
this.waitAfterSuite = this._parseIntParam(searchParams, "waitAfterSuite", 0);
this.warmupBeforeSync = this._parseIntParam(searchParams, "warmupBeforeSync", 0);
this.measurementMethod = this._parseEnumParam(searchParams, "measurementMethod", ["raf"]);
this.shuffleSeed = this._parseShuffleSeed(searchParams);
this.complexity = this._parserNumberParam(searchParams, "complexity", 0);
this.layoutMode = this._parseEnumParam(searchParams, "layoutMode", LAYOUT_MODES);
this.measurePrepare = this._parseBooleanParam(searchParams, "measurePrepare");
this.config = this._parseConfig(searchParams);
if (warnUnused) {
const unused = Array.from(searchParams.keys());
if (unused.length > 0)
console.error(`Got unused search params: ${unused.join(", ")}`);
}
}
_parseBooleanParam(searchParams, paramKey) {
if (!searchParams.has(paramKey))
return false;
searchParams.delete(paramKey);
return true;
}
_parserNumberParam(searchParams, paramKey, minValue) {
if (!searchParams.has(paramKey))
return defaultParams[paramKey];
const parsedValue = this._parseNumber(searchParams.get(paramKey), "waitBeforeSync", minValue);
searchParams.delete(paramKey);
return parsedValue;
}
_parseIntParam(searchParams, paramKey, minValue) {
if (!searchParams.has(paramKey))
return defaultParams[paramKey];
const parsedValue = this._parseInt(searchParams.get(paramKey), "waitBeforeSync", minValue);
searchParams.delete(paramKey);
return parsedValue;
}
_parseViewport(searchParams) {
if (!searchParams.has("viewport"))
return defaultParams.viewport;
const viewportParam = searchParams.get("viewport");
const [width, height] = viewportParam.split("x");
const viewport = {
width: this._parseInt(width, "viewport.width"),
height: this._parseInt(height, "viewport.height"),
};
if (this.viewport.width < 800 || this.viewport.height < 600)
throw new Error(`Invalid viewport param: ${viewportParam}`);
searchParams.delete("viewport");
return viewport;
}
_parseSuites(searchParams) {
if (searchParams.has("suite") || searchParams.has("suites")) {
if (searchParams.has("suite") && searchParams.has("suites"))
throw new Error("Params 'suite' and 'suites' can not be used together.");
const value = searchParams.get("suite") || searchParams.get("suites");
const suites = value.split(",");
if (suites.length === 0)
throw new Error("No suites selected");
searchParams.delete("suite");
searchParams.delete("suites");
return suites;
}
return defaultParams.suites;
}
_parseTags(searchParams) {
if (!searchParams.has("tags"))
return defaultParams.tags;
if (this.suites.length)
throw new Error("'suites' and 'tags' cannot be used together.");
const tags = searchParams.get("tags").split(",");
searchParams.delete("tags");
return tags;
}
_parseEnumParam(searchParams, paramKey, enumArray) {
if (!searchParams.has(paramKey))
return defaultParams[paramKey];
const value = searchParams.get(paramKey);
if (!enumArray.includes(value))
throw new Error(`Got invalid ${paramKey}: '${value}', choices are ${enumArray}`);
searchParams.delete(paramKey);
return value;
}
_parseShuffleSeed(searchParams) {
if (!searchParams.has("shuffleSeed"))
return defaultParams.shuffleSeed;
let shuffleSeed = searchParams.get("shuffleSeed");
if (shuffleSeed !== "off") {
if (shuffleSeed === "generate") {
shuffleSeed = Math.floor((Math.random() * 1) << 16);
console.log(`Generated a random suite order seed: ${shuffleSeed}`);
} else {
shuffleSeed = parseInt(shuffleSeed);
}
if (!Number.isInteger(shuffleSeed))
throw new Error(`Invalid shuffle seed: '${shuffleSeed}', must be either 'off', 'generate' or an integer.`);
}
searchParams.delete("shuffleSeed");
return shuffleSeed;
}
_parseConfig(searchParams) {
const config = searchParams.get("config") ?? "";
searchParams.delete("config");
if (config && !isValidJsonUrl(config))
throw new Error("Invalid config url passed in.");
return config;
}
toCompleteSearchParamsObject() {
return this.toSearchParamsObject(false);
}
toSearchParamsObject(filter = true) {
const rawUrlParams = { __proto__: null };
for (const [key, value] of Object.entries(this)) {
// Handle composite values separately.
if (key === "viewport" || key === "suites" || key === "tags")
continue;
// Skip over default values.
if (filter && value === defaultParams[key])
continue;
rawUrlParams[key] = value;
}
if (this.viewport.width !== defaultParams.viewport.width || this.viewport.height !== defaultParams.viewport.height)
rawUrlParams.viewport = `${this.viewport.width}x${this.viewport.height}`;
if (this.suites.length) {
rawUrlParams.suites = this.suites.join(",");
} else if (this.tags.length) {
if (!(this.tags.length === 1 && this.tags[0] === "default"))
rawUrlParams.tags = this.tags.join(",");
} else {
rawUrlParams.suites = "";
}
return new URLSearchParams(rawUrlParams);
}
toSearchParams() {
return this.toSearchParamsObject().toString();
}
isDefault() {
return this === defaultParams;
}
}
function isValidJsonUrl(url) {
if (typeof url !== "string" || url.length === 0)
return false;
try {
new URL(url, "http://www.example.com");
return true;
} catch (error) {
return false;
}
}
export const defaultParams = new Params();
export let paramsError = null;
let maybeCustomParams = defaultParams;
if (globalThis?.location?.search) {
const searchParams = new URLSearchParams(globalThis.location.search);
try {
maybeCustomParams = new Params(searchParams, true);
} catch (e) {
paramsError = e;
console.error("Invalid URL Param", e, "\nUsing defaults as fallback:", maybeCustomParams);
}
}
export const params = maybeCustomParams;