testplane
Version:
Tests framework based on mocha and wdio
261 lines • 14.1 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CSSSelectivity = void 0;
const lodash_1 = require("lodash");
const node_path_1 = __importDefault(require("node:path"));
const node_url_1 = require("node:url");
const constants_1 = require("../../../error-snippets/constants");
const utils_1 = require("./utils");
const fs_cache_1 = require("./fs-cache");
const debug_1 = require("./debug");
class CSSSelectivity {
constructor(cdp, cdpSessionId, wdSessionId, sourceRoot, mapSourceMapUrl) {
this._cssOnStyleSheetAddedFn = null;
this._stylesSourceMap = {};
this._styleSheetIdToSourceMapUrl = {};
this._coverageResult = [];
this._cdp = cdp;
this._cdpSessionId = cdpSessionId;
this._wdSessionId = wdSessionId;
this._sourceRoot = sourceRoot;
this._mapSourceMapUrl = mapSourceMapUrl;
}
_processStyle({ header: { styleSheetId, sourceURL, sourceMapURL } }, cdpSessionId) {
if (!this._cdpSessionId || cdpSessionId !== this._cdpSessionId) {
return;
}
if (!sourceMapURL || sourceURL.startsWith("chrome-error://")) {
this._stylesSourceMap[styleSheetId] ||= null;
return;
}
if (this._stylesSourceMap[styleSheetId]) {
return;
}
let sourceMapResolvedUrl = (0, node_url_1.resolve)(sourceURL, sourceMapURL);
if (!URL.canParse(sourceMapResolvedUrl)) {
this._stylesSourceMap[styleSheetId] ||= null;
return;
}
const mapResult = this._mapSourceMapUrl
? this._mapSourceMapUrl({ type: "css", sourceUrl: sourceURL, sourceMapUrl: sourceMapResolvedUrl })
: true;
if (!mapResult) {
this._stylesSourceMap[styleSheetId] ||= null;
return;
}
if (mapResult !== true) {
this._styleSheetIdToSourceMapUrl[styleSheetId] = sourceMapResolvedUrl = mapResult;
}
// Embedded source maps are not cached on file system because of their large cache key
if ((0, utils_1.isDataProtocol)(sourceMapResolvedUrl)) {
this._styleSheetIdToSourceMapUrl[styleSheetId] = null;
this._stylesSourceMap[styleSheetId] ||= (0, utils_1.fetchTextWithBrowserFallback)(sourceMapResolvedUrl, this._cdp.runtime, this._cdpSessionId).catch((err) => err);
}
else {
this._styleSheetIdToSourceMapUrl[styleSheetId] = sourceMapResolvedUrl;
this._stylesSourceMap[styleSheetId] ||= (0, fs_cache_1.hasCachedSelectivityFile)(fs_cache_1.CacheType.Asset, sourceMapResolvedUrl).then(isCached => {
return isCached
? true
: (0, utils_1.fetchTextWithBrowserFallback)(sourceMapResolvedUrl, this._cdp.runtime, this._cdpSessionId)
.then(data => (0, fs_cache_1.setCachedSelectivityFile)(fs_cache_1.CacheType.Asset, sourceMapResolvedUrl, data)
.then(() => true)
.catch(err => {
(0, debug_1.debugSelectivity)(`Couldn't offload asset from "${sourceMapResolvedUrl}" to fs-cache: %O`, err);
return data;
}))
.catch((err) => err);
});
}
}
// If we haven't got "styleSheetAdded" event for the script, pull up styles + source map manually
_ensureStylesAreLoading(ruleUsage) {
ruleUsage.forEach(({ styleSheetId }) => {
if (Object.hasOwn(this._stylesSourceMap, styleSheetId)) {
return;
}
this._stylesSourceMap[styleSheetId] ||= (async () => {
// In case SourceMapUrl was restored from session-cache, but SourceMap itself wasn't
if (this._styleSheetIdToSourceMapUrl[styleSheetId]) {
const sourceMapUrl = this._styleSheetIdToSourceMapUrl[styleSheetId];
const cachedSourceMap = await (0, fs_cache_1.getCachedSelectivityFile)(fs_cache_1.CacheType.Asset, sourceMapUrl);
if (cachedSourceMap) {
return cachedSourceMap;
}
const sourceMap = await (0, utils_1.fetchTextWithBrowserFallback)(sourceMapUrl, this._cdp.runtime, this._cdpSessionId).catch((err) => err);
if (!(sourceMap instanceof Error)) {
(0, fs_cache_1.setCachedSelectivityFile)(fs_cache_1.CacheType.Asset, sourceMapUrl, sourceMap).catch(() => { });
}
return sourceMap;
}
const sourceCode = await this._cdp.css
.getStyleSheetText(this._cdpSessionId, styleSheetId)
.then(res => res.text)
.catch((err) => err);
if (sourceCode instanceof Error) {
return sourceCode;
}
const sourceMapsStartIndex = sourceCode.lastIndexOf(constants_1.CSS_SOURCE_MAP_URL_COMMENT);
const sourceMapsEndIndex = sourceCode.indexOf("*/", sourceMapsStartIndex);
// Source maps are not generated for this source file
if (sourceMapsStartIndex === -1) {
return null;
}
const sourceMapURL = sourceMapsEndIndex === -1
? sourceCode.slice(sourceMapsStartIndex + constants_1.CSS_SOURCE_MAP_URL_COMMENT.length)
: sourceCode.slice(sourceMapsStartIndex + constants_1.CSS_SOURCE_MAP_URL_COMMENT.length, sourceMapsEndIndex);
// If we encounter css stylesheet, that was not reported by "styleSheetAdded"
// We can only get sourcemaps if they are inlined
// Otherwise, we can't resolve actual sourcemaps url because we dont know css styles url itself.
if (!(0, utils_1.isDataProtocol)(sourceMapURL)) {
return new Error([
`Missed stylesheet url for stylesheet id ${styleSheetId}.`,
"Looks like Chrome Devtools 'styleSheetAdded' event was lost",
"It could happen due to network instability",
"Switching to inline sourcemaps for CSS will help at the cost of increased RAM usage",
].join("\n"));
}
return (0, utils_1.fetchTextWithBrowserFallback)(sourceMapURL, this._cdp.runtime, this._cdpSessionId).catch((err) => err);
})();
});
}
async _waitForLoadingStyles() {
await Promise.allSettled(Object.values(this._stylesSourceMap));
}
async _saveSessionCache() {
const sessionCache = {};
for (const styleSheetId in this._styleSheetIdToSourceMapUrl) {
const sourceMapUrl = this._styleSheetIdToSourceMapUrl[styleSheetId];
const sourceMap = await this._stylesSourceMap[styleSheetId];
sessionCache[styleSheetId] = {
sourceMapUrl,
sourceMap: sourceMap instanceof Error ? sourceMap.message : sourceMap,
isError: sourceMap instanceof Error,
};
}
return (0, fs_cache_1.setCachedSelectivityFile)(fs_cache_1.CacheType.CssSessionCache, this._wdSessionId, JSON.stringify(sessionCache), {
overwrite: true,
}).catch(err => {
(0, debug_1.debugSelectivity)(`Couldn't save session cache for session '%s': %O`, this._wdSessionId, err);
});
}
async _loadSessionCache() {
const sessionCacheString = await (0, fs_cache_1.getCachedSelectivityFile)(fs_cache_1.CacheType.CssSessionCache, this._wdSessionId);
if (!sessionCacheString) {
return;
}
let sessionCache = null;
try {
sessionCache = JSON.parse(sessionCacheString);
}
catch (err) {
(0, debug_1.debugSelectivity)("CSS Session cache is invalid JSON for session '%s': %O", this._wdSessionId, err);
}
if (!sessionCache) {
return;
}
for (const styleSheetId in sessionCache) {
const cachedData = sessionCache[styleSheetId];
this._styleSheetIdToSourceMapUrl[styleSheetId] = cachedData.sourceMapUrl;
if (!cachedData.isError) {
this._stylesSourceMap[styleSheetId] = Promise.resolve(cachedData.sourceMap);
}
else {
// If source map url was received, we can try to restore cache
// If cache can't be restored, session should be considered as broken
if (!cachedData.sourceMapUrl) {
const originalError = cachedData.sourceMap;
const errorMessage = [
"Selectivity: session is broken. Couldn't restore source map from previous test:",
"\t- " + originalError.split("\n").pop(),
].join("\n");
// Not throwing the error right away because we might not need
this._stylesSourceMap[styleSheetId] = Promise.resolve(new Error(errorMessage));
}
}
}
}
async start() {
const cssOnStyleSheetAdded = (this._cssOnStyleSheetAddedFn = this._processStyle.bind(this));
this._cdp.css.on("styleSheetAdded", cssOnStyleSheetAdded);
const [startTrackingResult, loadSessionCacheResult] = await Promise.all([
this._cdp.css.startRuleUsageTracking(this._cdpSessionId).catch((err) => err),
this._loadSessionCache().catch((err) => err),
]);
if (startTrackingResult instanceof Error) {
throw startTrackingResult;
}
if (loadSessionCacheResult instanceof Error) {
(0, debug_1.debugSelectivity)("Couldn't load session cache for session '%s': %O", this._wdSessionId, loadSessionCacheResult);
}
}
async takeCoverageSnapshot() {
const coveragePart = await this._cdp.css.takeCoverageDelta(this._cdpSessionId);
this._ensureStylesAreLoading(coveragePart.coverage);
await this._waitForLoadingStyles();
this._coverageResult.push(...coveragePart.coverage);
}
/** @param drop only performs cleanup without providing actual deps. Should be "true" if test is failed */
async stop(drop) {
try {
if (drop) {
return null;
}
const coverageLastPart = await this._cdp.css.stopRuleUsageTracking(this._cdpSessionId);
const coverageStyles = [...this._coverageResult, ...coverageLastPart.ruleUsage];
this._ensureStylesAreLoading(coverageLastPart.ruleUsage);
const totalDependingSourceFiles = new Set();
const grouppedByStyleSheetCoverage = (0, lodash_1.groupBy)(coverageStyles, "styleSheetId");
const styleSheetIds = Object.keys(grouppedByStyleSheetCoverage);
await Promise.all(styleSheetIds.map(async (styleSheetId) => {
const sourceMap = await this._stylesSourceMap[styleSheetId];
const sourceMapUrl = this._styleSheetIdToSourceMapUrl[styleSheetId];
if (!sourceMap) {
return;
}
if (sourceMap instanceof Error) {
throw new Error([
`CSS Selectivity: Couldn't load source maps for stylesheet id ${styleSheetId}:`,
String(sourceMap),
].join("\n"));
}
if ((0, utils_1.isCachedOnFs)(sourceMap) && !sourceMapUrl) {
throw new Error("Assertation failed: souce map url has to present if source maps are fs-cached");
}
const sourceMapString = (0, utils_1.isCachedOnFs)(sourceMap)
? await (0, fs_cache_1.getCachedSelectivityFile)(fs_cache_1.CacheType.Asset, sourceMapUrl)
: sourceMap;
if (!sourceMapString) {
throw new Error(`CSS Selectivity: fs-cache is broken for ${sourceMapUrl}`);
}
const rawSourceMap = (0, utils_1.patchSourceMapSources)(JSON.parse(sourceMapString), this._sourceRoot);
// We could check "if stylesheet was used" with utils.extractSourceFilesDeps
// But we dont, because if stylesheet was not used, it could be used after change
// So its safe to think "if stylesheet was loaded, it was used"
rawSourceMap.sources.forEach(sourceFilePath => {
// Ignore generated postcss styles:
// https://github.com/postcss/postcss/blob/eae46db765d752cf8f40c4fa2b0b85030079c43d/lib/map-generator.js#L122
if (!sourceFilePath || sourceFilePath === "<no source>") {
return;
}
// "Each entry is either a string that is a (potentially relative) URL", so we are using posix.jojn
// https://tc39.es/ecma426/#sec-source-map-format
// Except for file path with protocol ("turbopack://", "file://")
const sourceRootBasedPath = (0, utils_1.hasProtocol)(sourceFilePath)
? sourceFilePath
: node_path_1.default.posix.join(rawSourceMap.sourceRoot || "", sourceFilePath);
totalDependingSourceFiles.add(sourceRootBasedPath);
});
}));
await this._saveSessionCache();
return totalDependingSourceFiles;
}
finally {
this._cssOnStyleSheetAddedFn && this._cdp.css.off("styleSheetAdded", this._cssOnStyleSheetAddedFn);
}
}
}
exports.CSSSelectivity = CSSSelectivity;
//# sourceMappingURL=css-selectivity.js.map