UNPKG

testplane

Version:

Tests framework based on mocha and wdio

261 lines 14.1 kB
"use strict"; 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