creevey
Version:
Cross-browser screenshot testing tool for Storybook with fancy UI Runner
482 lines (402 loc) • 19.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.calcStatus = calcStatus;
exports.getTestPath = getTestPath;
exports.getSuiteByPath = getSuiteByPath;
exports.getTestByPath = getTestByPath;
exports.getTestsByStoryId = getTestsByStoryId;
exports.checkSuite = checkSuite;
exports.treeifyTests = treeifyTests;
exports.getCheckedTests = getCheckedTests;
exports.updateTestStatus = updateTestStatus;
exports.removeTests = removeTests;
exports.filterTests = filterTests;
exports.openSuite = openSuite;
exports.flattenSuite = flattenSuite;
exports.countTestsStatus = countTestsStatus;
exports.getConnectionUrl = getConnectionUrl;
exports.getImageUrl = getImageUrl;
exports.getBorderSize = getBorderSize;
exports.useLoadImages = useLoadImages;
exports.useResizeObserver = useResizeObserver;
exports.useApplyScale = useApplyScale;
exports.useCalcScale = useCalcScale;
exports.useTheme = useTheme;
exports.setSearchParams = setSearchParams;
exports.getTestPathFromSearch = getTestPathFromSearch;
exports.useForceUpdate = useForceUpdate;
var _theming = require("@storybook/theming");
var _qs = require("qs");
var _react = require("react");
var _types = require("../../types");
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
function _iterableToArrayLimit(arr, i) { var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _toArray(arr) { return _arrayWithHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableRest(); }
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); }
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); }
function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); }
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
var statusUpdatesMap = new Map([[undefined, /(unknown|success|failed|pending|running)/], ['unknown', /(success|failed|pending|running)/], ['success', /(failed|pending|running)/], ['failed', /(pending|running)/], ['pending', /running/]]);
function makeEmptySuiteNode() {
var path = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
return {
path: path,
skip: true,
opened: false,
checked: true,
indeterminate: false,
children: {}
};
}
function calcStatus(oldStatus, newStatus) {
var _statusUpdatesMap$get;
return newStatus && (_statusUpdatesMap$get = statusUpdatesMap.get(oldStatus)) !== null && _statusUpdatesMap$get !== void 0 && _statusUpdatesMap$get.test(newStatus) ? newStatus : oldStatus;
}
function getTestPath(test) {
var browser = test.browser,
testName = test.testName,
storyPath = test.storyPath;
return [].concat(_toConsumableArray(storyPath), [testName, browser]).filter(_types.isDefined);
}
function getSuiteByPath(suite, path) {
return path.reduce(function (suiteOrTest, pathToken) {
return (0, _types.isTest)(suiteOrTest) ? suiteOrTest : suiteOrTest === null || suiteOrTest === void 0 ? void 0 : suiteOrTest.children[pathToken];
}, suite);
}
function getTestByPath(suite, path) {
var _getSuiteByPath;
var test = (_getSuiteByPath = getSuiteByPath(suite, path)) !== null && _getSuiteByPath !== void 0 ? _getSuiteByPath : suite;
return (0, _types.isTest)(test) ? test : null;
}
function getTestsByStoryId(suite, storyId) {
return Object.values(suite.children).filter(_types.isDefined).flatMap(function (suiteOrTest) {
if ((0, _types.isTest)(suiteOrTest)) return suiteOrTest.storyId === storyId ? suiteOrTest : [];
return getTestsByStoryId(suiteOrTest, storyId);
}).filter(_types.isDefined);
}
function checkTests(suiteOrTest, checked) {
suiteOrTest.checked = checked;
if (!(0, _types.isTest)(suiteOrTest)) {
suiteOrTest.indeterminate = false;
Object.values(suiteOrTest.children).filter(_types.isDefined).forEach(function (child) {
return checkTests(child, checked);
});
}
}
function updateChecked(suite) {
var children = Object.values(suite.children).filter(_types.isDefined).filter(function (child) {
return !child.skip;
});
var checkedEvery = children.every(function (test) {
return test.checked;
});
var checkedSome = children.some(function (test) {
return test.checked;
});
var indeterminate = children.some(function (test) {
return (0, _types.isTest)(test) ? false : test.indeterminate;
}) || !checkedEvery && checkedSome;
var checked = indeterminate || suite.checked == checkedEvery ? suite.checked : checkedEvery;
suite.checked = checked;
suite.indeterminate = indeterminate;
}
function checkSuite(suite, path, checked) {
var subSuite = getSuiteByPath(suite, path);
if (subSuite) checkTests(subSuite, checked);
path.slice(0, -1).map(function (_, index, tokens) {
return tokens.slice(0, tokens.length - index);
}).forEach(function (parentPath) {
var parentSuite = getSuiteByPath(suite, parentPath);
if ((0, _types.isTest)(parentSuite)) return;
if (parentSuite) updateChecked(parentSuite);
});
updateChecked(suite);
}
function treeifyTests(testsById) {
var rootSuite = makeEmptySuiteNode();
rootSuite.opened = true;
Object.values(testsById).forEach(function (test) {
if (!test) return;
var _getTestPath$reverse = getTestPath(test).reverse(),
_getTestPath$reverse2 = _toArray(_getTestPath$reverse),
browser = _getTestPath$reverse2[0],
testPath = _getTestPath$reverse2.slice(1);
var lastSuite = testPath.reverse().reduce(function (suite, token) {
var subSuite = suite.children[token] || makeEmptySuiteNode([].concat(_toConsumableArray(suite.path), [token]));
subSuite.status = calcStatus(subSuite.status, test.status);
if (!test.skip) subSuite.skip = false;
if (!subSuite.skip) suite.skip = false;
suite.children[token] = subSuite;
suite.status = calcStatus(suite.status, subSuite.status);
if ((0, _types.isTest)(subSuite)) {
throw new Error("Suite and Test should not have same path '".concat(JSON.stringify(getTestPath(subSuite)), "'"));
}
return subSuite;
}, rootSuite);
lastSuite.children[browser] = _objectSpread(_objectSpread({}, test), {}, {
checked: true
});
});
return rootSuite;
}
function getCheckedTests(suite) {
return Object.values(suite.children).filter(_types.isDefined).flatMap(function (suiteOrTest) {
if ((0, _types.isTest)(suiteOrTest)) return suiteOrTest.checked ? suiteOrTest : [];
if (!suiteOrTest.checked && !suiteOrTest.indeterminate) return [];
return getCheckedTests(suiteOrTest);
});
}
function updateTestStatus(suite, path, update) {
var _suite$children$title;
var title = path.shift();
if (!title) return;
var suiteOrTest = (_suite$children$title = suite.children[title]) !== null && _suite$children$title !== void 0 ? _suite$children$title : suite.children[title] = _objectSpread(_objectSpread({}, path.length == 0 ? update : makeEmptySuiteNode([].concat(_toConsumableArray(suite.path), [title]))), {}, {
checked: suite.checked
});
if ((0, _types.isTest)(suiteOrTest)) {
var _test$results;
var test = suiteOrTest;
var skip = update.skip,
status = update.status,
results = update.results,
approved = update.approved;
if ((0, _types.isDefined)(skip)) test.skip = skip;
if ((0, _types.isDefined)(status)) test.status = status;
if ((0, _types.isDefined)(results)) test.results ? (_test$results = test.results).push.apply(_test$results, _toConsumableArray(results)) : test.results = results;
if ((0, _types.isDefined)(approved)) Object.entries(approved).forEach(function (_ref) {
var _ref2 = _slicedToArray(_ref, 2),
image = _ref2[0],
retry = _ref2[1];
return retry !== undefined && ((test.approved = test.approved || {})[image] = retry);
});
} else {
var subSuite = suiteOrTest;
updateTestStatus(subSuite, path, update);
}
suite.skip = Object.values(suite.children).filter(_types.isDefined).map(function (_ref3) {
var skip = _ref3.skip;
return skip;
}).every(Boolean);
suite.status = Object.values(suite.children).filter(_types.isDefined).map(function (_ref4) {
var status = _ref4.status;
return status;
}).reduce(calcStatus);
}
function removeTests(suite, path) {
var _suiteOrTest$children;
var title = path.shift();
if (!title) return;
var suiteOrTest = suite.children[title];
if (suiteOrTest && !(0, _types.isTest)(suiteOrTest)) removeTests(suiteOrTest, path);
if ((0, _types.isTest)(suiteOrTest) || Object.keys((_suiteOrTest$children = suiteOrTest === null || suiteOrTest === void 0 ? void 0 : suiteOrTest.children) !== null && _suiteOrTest$children !== void 0 ? _suiteOrTest$children : {}).length == 0) delete suite.children[title];
if (Object.keys(suite.children).length == 0) return;
updateChecked(suite);
suite.skip = Object.values(suite.children).filter(_types.isDefined).map(function (_ref5) {
var skip = _ref5.skip;
return skip;
}).every(Boolean);
suite.status = Object.values(suite.children).filter(_types.isDefined).map(function (_ref6) {
var status = _ref6.status;
return status;
}).reduce(calcStatus);
}
function filterTests(suite, filter) {
var status = filter.status,
subStrings = filter.subStrings;
if (!status && !subStrings.length) return suite;
var filteredSuite = _objectSpread(_objectSpread({}, suite), {}, {
children: {}
});
Object.entries(suite.children).forEach(function (_ref7) {
var _ref8 = _slicedToArray(_ref7, 2),
title = _ref8[0],
suiteOrTest = _ref8[1];
if (!suiteOrTest || suiteOrTest.skip) return;
if (!status && subStrings.some(function (subString) {
return title.toLowerCase().includes(subString);
})) {
filteredSuite.children[title] = suiteOrTest;
} else if ((0, _types.isTest)(suiteOrTest)) {
if (status && suiteOrTest.status && ['pending', 'running', status].includes(suiteOrTest.status)) filteredSuite.children[title] = suiteOrTest;
} else {
var filteredSubSuite = filterTests(suiteOrTest, filter);
if (Object.keys(filteredSubSuite.children).length == 0) return;
filteredSuite.children[title] = filteredSubSuite;
}
});
return filteredSuite;
}
function openSuite(suite, path, opened) {
var subSuite = path.reduce(function (suiteOrTest, pathToken) {
if (suiteOrTest && !(0, _types.isTest)(suiteOrTest)) {
if (opened) suiteOrTest.opened = opened;
return suiteOrTest.children[pathToken];
}
}, suite);
if (subSuite && !(0, _types.isTest)(subSuite)) subSuite.opened = opened;
}
function flattenSuite(suite) {
if (!suite.opened) return [];
return Object.entries(suite.children).flatMap(function (_ref9) {
var _ref10 = _slicedToArray(_ref9, 2),
title = _ref10[0],
subSuite = _ref10[1];
return subSuite ? [{
title: title,
suite: subSuite
}].concat(_toConsumableArray((0, _types.isTest)(subSuite) ? [] : flattenSuite(subSuite))) : [];
});
}
function countTestsStatus(suite) {
var successCount = 0;
var failedCount = 0;
var skippedCount = 0;
var pendingCount = 0;
var cases = Object.values(suite.children).filter(_types.isDefined);
var suiteOrTest;
while (suiteOrTest = cases.pop()) {
if ((0, _types.isTest)(suiteOrTest)) {
if (suiteOrTest.skip) skippedCount++;
if (suiteOrTest.status === 'success') successCount++;
if (suiteOrTest.status === 'failed') failedCount++;
if (suiteOrTest.status === 'pending') pendingCount++;
} else {
cases.push.apply(cases, _toConsumableArray(Object.values(suiteOrTest.children).filter(_types.isDefined)));
}
}
return {
successCount: successCount,
failedCount: failedCount,
skippedCount: skippedCount,
pendingCount: pendingCount
};
}
function getConnectionUrl() {
return [window.location.hostname, typeof __CREEVEY_SERVER_PORT__ == 'undefined' ? window.location.port : __CREEVEY_SERVER_PORT__].filter(Boolean).join(':');
}
function getImageUrl(path, imageName) {
// path => [kind, story, test, browser]
var browser = path.slice(-1)[0];
var imagesUrl = window.location.host ? "".concat(window.location.protocol, "//").concat(getConnectionUrl()).concat(window.location.pathname == '/' ? '/report' : window.location.pathname.split('/').slice(0, -1).join('/'), "/").concat(encodeURI(path.slice(0, -1).join('/'))) : encodeURI(path.slice(0, -1).join('/'));
return imageName == browser ? imagesUrl : "".concat(imagesUrl, "/").concat(encodeURI(browser));
}
function getBorderSize(element) {
// NOTE Firefox returns empty string for `borderWidth` prop
var borderSize = parseFloat(getComputedStyle(element).borderTopWidth);
return Number.isNaN(borderSize) ? 0 : borderSize;
}
function useLoadImages(s1, s2, s3) {
var _useState = (0, _react.useState)(false),
_useState2 = _slicedToArray(_useState, 2),
loaded = _useState2[0],
setLoaded = _useState2[1];
(0, _react.useEffect)(function () {
setLoaded(false);
void Promise.all([s1, s2, s3].map(function (url) {
return new Promise(function (resolve) {
var image = document.createElement('img');
image.src = url;
image.onload = resolve;
image.onerror = resolve;
});
})).then(function () {
return setLoaded(true);
});
}, [s1, s2, s3]);
return loaded;
}
/**
* Uses the ResizeObserver API to observe changes within the given HTML Element DOM Rect.
*
* @returns dimensions of element's content box (which means without paddings and border width)
*/
function useResizeObserver(elementRef, onResize) {
var debounceTimeout = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 16;
var observerRef = (0, _react.useRef)(null);
(0, _react.useEffect)(function () {
if (!elementRef.current) return;
observerRef.current = new ResizeObserver(onResize);
observerRef.current.observe(elementRef.current);
return function () {
var _observerRef$current;
return (_observerRef$current = observerRef.current) === null || _observerRef$current === void 0 ? void 0 : _observerRef$current.disconnect();
};
}, [debounceTimeout, elementRef, onResize]);
}
function useApplyScale(imageRef, scale, dependency) {
(0, _react.useLayoutEffect)(function () {
if (!imageRef.current) return;
var image = imageRef.current;
var borderSize = getBorderSize(image);
image.style.height = "".concat(image.naturalHeight * scale + borderSize * 2, "px");
}, [imageRef, scale, dependency]);
}
function useCalcScale(diffImageRef, loaded) {
var _useState3 = (0, _react.useState)(1),
_useState4 = _slicedToArray(_useState3, 2),
scale = _useState4[0],
setScale = _useState4[1];
var calcScale = (0, _react.useCallback)(function () {
var diffImage = diffImageRef.current;
if (!diffImage || !loaded) return setScale(1);
var borderSize = getBorderSize(diffImage);
var ratio = (diffImage.getBoundingClientRect().width - borderSize * 2) / diffImage.naturalWidth;
setScale(Math.min(1, ratio));
}, [diffImageRef, loaded]);
useResizeObserver(diffImageRef, calcScale);
(0, _react.useLayoutEffect)(calcScale, [calcScale]);
return scale;
}
var CREEVEY_THEME = 'Creevey_theme';
function isTheme(theme) {
return (0, _types.isDefined)(theme) && Object.prototype.hasOwnProperty.call(_theming.themes, theme);
}
function initialTheme() {
var theme = localStorage.getItem(CREEVEY_THEME);
return isTheme(theme) ? theme : 'light';
}
function useTheme() {
var _useState5 = (0, _react.useState)(initialTheme()),
_useState6 = _slicedToArray(_useState5, 2),
theme = _useState6[0],
setTheme = _useState6[1];
(0, _react.useEffect)(function () {
localStorage.setItem(CREEVEY_THEME, theme);
}, [theme]);
return [theme, setTheme];
}
function setSearchParams(testPath) {
var pageUrl = "?".concat((0, _qs.stringify)({
testPath: testPath
}));
window.history.pushState({
testPath: testPath
}, '', pageUrl);
}
function getTestPathFromSearch() {
var _parse = (0, _qs.parse)(window.location.search.slice(1)),
testPath = _parse.testPath; //@ts-expect-error: This expression is not callable.
if (Array.isArray(testPath) && testPath.every(function (token) {
return typeof token == 'string';
})) {
return testPath;
}
return [];
}
function useForceUpdate() {
var _useState7 = (0, _react.useState)({}),
_useState8 = _slicedToArray(_useState7, 2),
update = _useState8[1];
return (0, _react.useCallback)(function () {
return update({});
}, []);
}