- Install Jest for unit testing with React Testing Library - Install Playwright for end-to-end testing - Configure Jest with proper TypeScript support and module mapping - Create test setup files and utilities for both unit and e2e tests Components: * Jest configuration with coverage thresholds * Playwright configuration with browser automation * Unit tests for LoginForm, AuthContext, and useSocketIO hook * E2E tests for authentication, dashboard, and agents workflows * GitHub Actions workflow for automated testing * Mock data and API utilities for consistent testing * Test documentation with best practices Testing features: - Unit tests with 70% coverage threshold - E2E tests with API mocking and user journey testing - CI/CD integration for automated test runs - Cross-browser testing support with Playwright - Authentication system testing end-to-end 🚀 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
538 lines
12 KiB
JavaScript
538 lines
12 KiB
JavaScript
/**
|
|
* These are commonly used parsers for CSS Values they take a string to parse
|
|
* and return a string after it's been converted, if needed
|
|
*/
|
|
"use strict";
|
|
|
|
const { resolve: resolveColor, utils } = require("@asamuzakjp/css-color");
|
|
const { asciiLowercase } = require("./utils/strings");
|
|
|
|
const { cssCalc, isColor, isGradient, splitValue } = utils;
|
|
|
|
// CSS global values
|
|
// @see https://drafts.csswg.org/css-cascade-5/#defaulting-keywords
|
|
const GLOBAL_VALUE = Object.freeze(["initial", "inherit", "unset", "revert", "revert-layer"]);
|
|
|
|
// Numeric data types
|
|
const NUM_TYPE = Object.freeze({
|
|
UNDEFINED: 0,
|
|
VAR: 1,
|
|
NUMBER: 2,
|
|
PERCENT: 4,
|
|
LENGTH: 8,
|
|
ANGLE: 0x10,
|
|
CALC: 0x20
|
|
});
|
|
|
|
// System colors
|
|
// @see https://drafts.csswg.org/css-color/#css-system-colors
|
|
// @see https://drafts.csswg.org/css-color/#deprecated-system-colors
|
|
const SYS_COLOR = Object.freeze([
|
|
"accentcolor",
|
|
"accentcolortext",
|
|
"activeborder",
|
|
"activecaption",
|
|
"activetext",
|
|
"appworkspace",
|
|
"background",
|
|
"buttonborder",
|
|
"buttonface",
|
|
"buttonhighlight",
|
|
"buttonshadow",
|
|
"buttontext",
|
|
"canvas",
|
|
"canvastext",
|
|
"captiontext",
|
|
"field",
|
|
"fieldtext",
|
|
"graytext",
|
|
"highlight",
|
|
"highlighttext",
|
|
"inactiveborder",
|
|
"inactivecaption",
|
|
"inactivecaptiontext",
|
|
"infobackground",
|
|
"infotext",
|
|
"linktext",
|
|
"mark",
|
|
"marktext",
|
|
"menu",
|
|
"menutext",
|
|
"scrollbar",
|
|
"selecteditem",
|
|
"selecteditemtext",
|
|
"threeddarkshadow",
|
|
"threedface",
|
|
"threedhighlight",
|
|
"threedlightshadow",
|
|
"threedshadow",
|
|
"visitedtext",
|
|
"window",
|
|
"windowframe",
|
|
"windowtext"
|
|
]);
|
|
|
|
// Regular expressions
|
|
const DIGIT = "(?:0|[1-9]\\d*)";
|
|
const NUMBER = `[+-]?(?:${DIGIT}(?:\\.\\d*)?|\\.\\d+)(?:e-?${DIGIT})?`;
|
|
const unitRegEx = new RegExp(`^(${NUMBER})([a-z]+|%)?$`, "i");
|
|
const urlRegEx = /^url\(\s*((?:[^)]|\\\))*)\s*\)$/;
|
|
const keywordRegEx = /^[a-z]+(?:-[a-z]+)*$/i;
|
|
const stringRegEx = /^("[^"]*"|'[^']*')$/;
|
|
const varRegEx = /^var\(/;
|
|
const varContainedRegEx = /(?<=[*/\s(])var\(/;
|
|
const calcRegEx =
|
|
/^(?:a?(?:cos|sin|tan)|abs|atan2|calc|clamp|exp|hypot|log|max|min|mod|pow|rem|round|sign|sqrt)\(/;
|
|
const functionRegEx = /^([a-z][a-z\d]*(?:-[a-z\d]+)*)\(/i;
|
|
|
|
const getNumericType = function getNumericType(val) {
|
|
if (varRegEx.test(val)) {
|
|
return NUM_TYPE.VAR;
|
|
}
|
|
if (calcRegEx.test(val)) {
|
|
return NUM_TYPE.CALC;
|
|
}
|
|
if (unitRegEx.test(val)) {
|
|
const [, , unit] = unitRegEx.exec(val);
|
|
if (!unit) {
|
|
return NUM_TYPE.NUMBER;
|
|
}
|
|
if (unit === "%") {
|
|
return NUM_TYPE.PERCENT;
|
|
}
|
|
if (/^(?:[cm]m|[dls]?v(?:[bhiw]|max|min)|in|p[ctx]|q|r?(?:[cl]h|cap|e[mx]|ic))$/i.test(unit)) {
|
|
return NUM_TYPE.LENGTH;
|
|
}
|
|
if (/^(?:deg|g?rad|turn)$/i.test(unit)) {
|
|
return NUM_TYPE.ANGLE;
|
|
}
|
|
}
|
|
return NUM_TYPE.UNDEFINED;
|
|
};
|
|
|
|
// Prepare stringified value.
|
|
exports.prepareValue = function prepareValue(value, globalObject = globalThis) {
|
|
// `null` is converted to an empty string.
|
|
// @see https://webidl.spec.whatwg.org/#LegacyNullToEmptyString
|
|
if (value === null) {
|
|
return "";
|
|
}
|
|
const type = typeof value;
|
|
switch (type) {
|
|
case "string":
|
|
return value.trim();
|
|
case "number":
|
|
return value.toString();
|
|
case "undefined":
|
|
return "undefined";
|
|
case "symbol":
|
|
throw new globalObject.TypeError("Can not convert symbol to string.");
|
|
default: {
|
|
const str = value.toString();
|
|
if (typeof str === "string") {
|
|
return str;
|
|
}
|
|
throw new globalObject.TypeError(`Can not convert ${type} to string.`);
|
|
}
|
|
}
|
|
};
|
|
|
|
exports.hasVarFunc = function hasVarFunc(val) {
|
|
return varRegEx.test(val) || varContainedRegEx.test(val);
|
|
};
|
|
|
|
exports.parseNumber = function parseNumber(val, restrictToPositive = false) {
|
|
if (val === "") {
|
|
return "";
|
|
}
|
|
const type = getNumericType(val);
|
|
switch (type) {
|
|
case NUM_TYPE.VAR:
|
|
return val;
|
|
case NUM_TYPE.CALC:
|
|
return cssCalc(val, {
|
|
format: "specifiedValue"
|
|
});
|
|
case NUM_TYPE.NUMBER: {
|
|
const num = parseFloat(val);
|
|
if (restrictToPositive && num < 0) {
|
|
return;
|
|
}
|
|
return `${num}`;
|
|
}
|
|
default:
|
|
if (varContainedRegEx.test(val)) {
|
|
return val;
|
|
}
|
|
}
|
|
};
|
|
|
|
exports.parseLength = function parseLength(val, restrictToPositive = false) {
|
|
if (val === "") {
|
|
return "";
|
|
}
|
|
const type = getNumericType(val);
|
|
switch (type) {
|
|
case NUM_TYPE.VAR:
|
|
return val;
|
|
case NUM_TYPE.CALC:
|
|
return cssCalc(val, {
|
|
format: "specifiedValue"
|
|
});
|
|
case NUM_TYPE.NUMBER:
|
|
if (parseFloat(val) === 0) {
|
|
return "0px";
|
|
}
|
|
return;
|
|
case NUM_TYPE.LENGTH: {
|
|
const [, numVal, unit] = unitRegEx.exec(val);
|
|
const num = parseFloat(numVal);
|
|
if (restrictToPositive && num < 0) {
|
|
return;
|
|
}
|
|
return `${num}${asciiLowercase(unit)}`;
|
|
}
|
|
default:
|
|
if (varContainedRegEx.test(val)) {
|
|
return val;
|
|
}
|
|
}
|
|
};
|
|
|
|
exports.parsePercent = function parsePercent(val, restrictToPositive = false) {
|
|
if (val === "") {
|
|
return "";
|
|
}
|
|
const type = getNumericType(val);
|
|
switch (type) {
|
|
case NUM_TYPE.VAR:
|
|
return val;
|
|
case NUM_TYPE.CALC:
|
|
return cssCalc(val, {
|
|
format: "specifiedValue"
|
|
});
|
|
case NUM_TYPE.NUMBER:
|
|
if (parseFloat(val) === 0) {
|
|
return "0%";
|
|
}
|
|
return;
|
|
case NUM_TYPE.PERCENT: {
|
|
const [, numVal, unit] = unitRegEx.exec(val);
|
|
const num = parseFloat(numVal);
|
|
if (restrictToPositive && num < 0) {
|
|
return;
|
|
}
|
|
return `${num}${asciiLowercase(unit)}`;
|
|
}
|
|
default:
|
|
if (varContainedRegEx.test(val)) {
|
|
return val;
|
|
}
|
|
}
|
|
};
|
|
|
|
// Either a length or a percent.
|
|
exports.parseMeasurement = function parseMeasurement(val, restrictToPositive = false) {
|
|
if (val === "") {
|
|
return "";
|
|
}
|
|
const type = getNumericType(val);
|
|
switch (type) {
|
|
case NUM_TYPE.VAR:
|
|
return val;
|
|
case NUM_TYPE.CALC:
|
|
return cssCalc(val, {
|
|
format: "specifiedValue"
|
|
});
|
|
case NUM_TYPE.NUMBER:
|
|
if (parseFloat(val) === 0) {
|
|
return "0px";
|
|
}
|
|
return;
|
|
case NUM_TYPE.LENGTH:
|
|
case NUM_TYPE.PERCENT: {
|
|
const [, numVal, unit] = unitRegEx.exec(val);
|
|
const num = parseFloat(numVal);
|
|
if (restrictToPositive && num < 0) {
|
|
return;
|
|
}
|
|
return `${num}${asciiLowercase(unit)}`;
|
|
}
|
|
default:
|
|
if (varContainedRegEx.test(val)) {
|
|
return val;
|
|
}
|
|
}
|
|
};
|
|
|
|
exports.parseAngle = function parseAngle(val, normalizeDeg = false) {
|
|
if (val === "") {
|
|
return "";
|
|
}
|
|
const type = getNumericType(val);
|
|
switch (type) {
|
|
case NUM_TYPE.VAR:
|
|
return val;
|
|
case NUM_TYPE.CALC:
|
|
return cssCalc(val, {
|
|
format: "specifiedValue"
|
|
});
|
|
case NUM_TYPE.NUMBER:
|
|
if (parseFloat(val) === 0) {
|
|
return "0deg";
|
|
}
|
|
return;
|
|
case NUM_TYPE.ANGLE: {
|
|
let [, numVal, unit] = unitRegEx.exec(val);
|
|
numVal = parseFloat(numVal);
|
|
unit = asciiLowercase(unit);
|
|
if (unit === "deg") {
|
|
if (normalizeDeg && numVal < 0) {
|
|
while (numVal < 0) {
|
|
numVal += 360;
|
|
}
|
|
}
|
|
numVal %= 360;
|
|
}
|
|
return `${numVal}${unit}`;
|
|
}
|
|
default:
|
|
if (varContainedRegEx.test(val)) {
|
|
return val;
|
|
}
|
|
}
|
|
};
|
|
|
|
exports.parseUrl = function parseUrl(val) {
|
|
if (val === "") {
|
|
return val;
|
|
}
|
|
const res = urlRegEx.exec(val);
|
|
if (!res) {
|
|
return;
|
|
}
|
|
let str = res[1];
|
|
// If it starts with single or double quotes, does it end with the same?
|
|
if ((str[0] === '"' || str[0] === "'") && str[0] !== str[str.length - 1]) {
|
|
return;
|
|
}
|
|
if (str[0] === '"' || str[0] === "'") {
|
|
str = str.substr(1, str.length - 2);
|
|
}
|
|
let urlstr = "";
|
|
let escaped = false;
|
|
for (let i = 0; i < str.length; i++) {
|
|
switch (str[i]) {
|
|
case "\\":
|
|
if (escaped) {
|
|
urlstr += "\\\\";
|
|
escaped = false;
|
|
} else {
|
|
escaped = true;
|
|
}
|
|
break;
|
|
case "(":
|
|
case ")":
|
|
case " ":
|
|
case "\t":
|
|
case "\n":
|
|
case "'":
|
|
if (!escaped) {
|
|
return;
|
|
}
|
|
urlstr += str[i];
|
|
escaped = false;
|
|
break;
|
|
case '"':
|
|
if (!escaped) {
|
|
return;
|
|
}
|
|
urlstr += '\\"';
|
|
escaped = false;
|
|
break;
|
|
default:
|
|
urlstr += str[i];
|
|
escaped = false;
|
|
}
|
|
}
|
|
return `url("${urlstr}")`;
|
|
};
|
|
|
|
exports.parseString = function parseString(val) {
|
|
if (val === "") {
|
|
return "";
|
|
}
|
|
if (!stringRegEx.test(val)) {
|
|
return;
|
|
}
|
|
val = val.substr(1, val.length - 2);
|
|
let str = "";
|
|
let escaped = false;
|
|
for (let i = 0; i < val.length; i++) {
|
|
switch (val[i]) {
|
|
case "\\":
|
|
if (escaped) {
|
|
str += "\\\\";
|
|
escaped = false;
|
|
} else {
|
|
escaped = true;
|
|
}
|
|
break;
|
|
case '"':
|
|
str += '\\"';
|
|
escaped = false;
|
|
break;
|
|
default:
|
|
str += val[i];
|
|
escaped = false;
|
|
}
|
|
}
|
|
return `"${str}"`;
|
|
};
|
|
|
|
exports.parseKeyword = function parseKeyword(val, validKeywords = []) {
|
|
if (val === "") {
|
|
return "";
|
|
}
|
|
if (varRegEx.test(val)) {
|
|
return val;
|
|
}
|
|
val = asciiLowercase(val.toString());
|
|
if (validKeywords.includes(val) || GLOBAL_VALUE.includes(val)) {
|
|
return val;
|
|
}
|
|
};
|
|
|
|
exports.parseColor = function parseColor(val) {
|
|
if (val === "") {
|
|
return "";
|
|
}
|
|
if (varRegEx.test(val)) {
|
|
return val;
|
|
}
|
|
if (/^[a-z]+$/i.test(val)) {
|
|
const v = asciiLowercase(val);
|
|
if (SYS_COLOR.includes(v)) {
|
|
return v;
|
|
}
|
|
}
|
|
const res = resolveColor(val, {
|
|
format: "specifiedValue"
|
|
});
|
|
if (res) {
|
|
return res;
|
|
}
|
|
return exports.parseKeyword(val);
|
|
};
|
|
|
|
exports.parseImage = function parseImage(val) {
|
|
if (val === "") {
|
|
return "";
|
|
}
|
|
if (varRegEx.test(val)) {
|
|
return val;
|
|
}
|
|
if (keywordRegEx.test(val)) {
|
|
return exports.parseKeyword(val, ["none"]);
|
|
}
|
|
const values = splitValue(val, {
|
|
delimiter: ",",
|
|
preserveComment: varContainedRegEx.test(val)
|
|
});
|
|
let isImage = Boolean(values.length);
|
|
for (let i = 0; i < values.length; i++) {
|
|
const image = values[i];
|
|
if (image === "") {
|
|
return "";
|
|
}
|
|
if (isGradient(image) || /^(?:none|inherit)$/i.test(image)) {
|
|
continue;
|
|
}
|
|
const imageUrl = exports.parseUrl(image);
|
|
if (imageUrl) {
|
|
values[i] = imageUrl;
|
|
} else {
|
|
isImage = false;
|
|
break;
|
|
}
|
|
}
|
|
if (isImage) {
|
|
return values.join(", ");
|
|
}
|
|
};
|
|
|
|
exports.parseFunction = function parseFunction(val) {
|
|
if (val === "") {
|
|
return {
|
|
name: null,
|
|
value: ""
|
|
};
|
|
}
|
|
if (functionRegEx.test(val) && val.endsWith(")")) {
|
|
if (varRegEx.test(val) || varContainedRegEx.test(val)) {
|
|
return {
|
|
name: "var",
|
|
value: val
|
|
};
|
|
}
|
|
const [, name] = functionRegEx.exec(val);
|
|
const value = val
|
|
.replace(new RegExp(`^${name}\\(`), "")
|
|
.replace(/\)$/, "")
|
|
.trim();
|
|
return {
|
|
name,
|
|
value
|
|
};
|
|
}
|
|
};
|
|
|
|
exports.parseShorthand = function parseShorthand(val, shorthandFor, preserve = false) {
|
|
const obj = {};
|
|
if (val === "" || exports.hasVarFunc(val)) {
|
|
for (const [property] of shorthandFor) {
|
|
obj[property] = "";
|
|
}
|
|
return obj;
|
|
}
|
|
const key = exports.parseKeyword(val);
|
|
if (key) {
|
|
if (key === "inherit") {
|
|
return obj;
|
|
}
|
|
return;
|
|
}
|
|
const parts = splitValue(val);
|
|
const shorthandArr = [...shorthandFor];
|
|
for (const part of parts) {
|
|
let partValid = false;
|
|
for (let i = 0; i < shorthandArr.length; i++) {
|
|
const [property, value] = shorthandArr[i];
|
|
if (value.isValid(part)) {
|
|
partValid = true;
|
|
obj[property] = value.parse(part);
|
|
if (!preserve) {
|
|
shorthandArr.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!partValid) {
|
|
return;
|
|
}
|
|
}
|
|
return obj;
|
|
};
|
|
|
|
// Returns `false` for global values, e.g. "inherit".
|
|
exports.isValidColor = function isValidColor(val) {
|
|
if (SYS_COLOR.includes(asciiLowercase(val))) {
|
|
return true;
|
|
}
|
|
return isColor(val);
|
|
};
|
|
|
|
// Splits value into an array.
|
|
// @see https://github.com/asamuzaK/cssColor/blob/main/src/js/util.ts
|
|
exports.splitValue = splitValue;
|