UNPKG

@revoloo/cypress6

Version:

Cypress.io end to end testing tool

217 lines (166 loc) 6.21 kB
const path = require('path') const url = require('url') const _ = require('lodash') const md5 = require('md5') const LimitedMap = require('../util/limited_map') const $utils = require('../cypress/utils') const anyUrlInCssRe = /url\((['"])([^'"]*)\1\)/gm const screenStylesheetRe = /(screen|all)/ const reduceText = (arr, fn) => { return _.reduce(arr, ((memo, item) => { return memo += fn(item) }), '') } const isScreenStylesheet = (stylesheet) => { const media = stylesheet.getAttribute('media') return !_.isString(media) || screenStylesheetRe.test(media) } const getDocumentStylesheets = (doc) => { if (!doc) return {} return _.transform(doc.styleSheets, (memo, stylesheet) => { memo[stylesheet.href] = stylesheet }, {}) } const makePathsAbsoluteToDocCache = new LimitedMap(50) const makePathsAbsoluteToDoc = $utils.memoize((styles, doc) => { if (!_.isString(styles)) return styles return styles.replace(anyUrlInCssRe, (_1, _2, filePath) => { //// the href getter will always resolve an absolute path taking into //// account things like the current URL and the <base> tag const a = doc.createElement('a') a.href = filePath return `url('${a.href}')` }) }, makePathsAbsoluteToDocCache) const makePathsAbsoluteToStylesheetCache = new LimitedMap(50) const makePathsAbsoluteToStylesheet = $utils.memoize((styles, href) => { if (!_.isString(styles)) { return styles } const stylesheetPath = href.replace(path.basename(href), '') return styles.replace(anyUrlInCssRe, (_1, _2, filePath) => { const absPath = url.resolve(stylesheetPath, filePath) return `url('${absPath}')` }) }, makePathsAbsoluteToStylesheetCache) const getExternalCssContents = (href, stylesheet) => { //// some browsers may throw a SecurityError if the stylesheet is cross-domain //// https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet#Notes //// for others, it will just be null try { const rules = stylesheet.rules || stylesheet.cssRules if (rules) { const contents = reduceText(rules, (rule) => { return rule.cssText }) return makePathsAbsoluteToStylesheet(contents, href) } return null } catch (e) { return null } } const getInlineCssContents = (stylesheet, $$) => { if (!stylesheet.sheet) return $$(stylesheet).text() const rules = stylesheet.sheet.cssRules return reduceText(rules, (rule) => { return rule.cssText }) } const create = ($$, state) => { const cssIdToContentsMap = new WeakMap() const cssHashedContentsToIdMap = new LimitedMap() const cssHrefToIdMap = new LimitedMap() const cssHrefToModifiedMap = new LimitedMap() let newWindow = false //// we invalidate the cache when css is modified by javascript const onCssModified = (href) => { cssHrefToModifiedMap.set(href, { modified: true }) } //// the lifecycle of a stylesheet is the lifecycle of the window //// so track this to know when to re-evaluate the cache in case //// of css being modified by javascript const onBeforeWindowLoad = () => { newWindow = true } const getStyleId = (href, stylesheet) => { const hrefModified = cssHrefToModifiedMap.get(href) || {} const existing = cssHrefToIdMap.get(href) //// if we've loaded a new window and the css was invalidated due to javascript //// we need to re-evaluate since this time around javascript might not change the css if (existing && !hrefModified.modified && !(newWindow && hrefModified.modifiedLast)) { return existing } const cssContents = getExternalCssContents(href, stylesheet) if (cssContents == null) { return } const hashedCssContents = md5(cssContents) //// if we already have these css contents stored, don't store them again const existingId = cssHashedContentsToIdMap.get(hashedCssContents) //// id just needs to be a new object reference //// we add the href for debuggability const id = existingId || { hrefId: href } cssHrefToIdMap.set(href, id) //// if we already have these css contents stored, don't store them again if (!existingId) { cssHashedContentsToIdMap.set(hashedCssContents, id) cssIdToContentsMap.set(id, cssContents) } if (hrefModified.modified) { hrefModified.modifiedLast = true } else if (newWindow) { hrefModified.modifiedLast = false } hrefModified.modified = false cssHrefToModifiedMap.set(href, hrefModified) return id } const getStyleIdsFor = (doc, $$, stylesheets, location) => { let styles = $$(location).find('link[rel=\'stylesheet\'],style') styles = _.filter(styles, isScreenStylesheet) return _.map(styles, (stylesheet) => { //// in cases where we can get the CSS as a string, make the paths //// absolute so that when they're restored by appending them to the page //// in <style> tags, background images and fonts still properly load const href = stylesheet.href //// if there's an href, it's a link tag //// return the CSS rules as a string, or, if cross-domain, //// a reference to the stylesheet's href if (href) { return getStyleId(href, stylesheets[href]) || { href } } //// otherwise, it's a style tag, and we can just grab its content const cssContents = getInlineCssContents(stylesheet, $$) return makePathsAbsoluteToDoc(cssContents, doc) }) } const getStyleIds = () => { const doc = state('document') const stylesheets = getDocumentStylesheets(doc) const styleIds = { headStyleIds: getStyleIdsFor(doc, $$, stylesheets, 'head'), bodyStyleIds: getStyleIdsFor(doc, $$, stylesheets, 'body'), } //// after getting the all the styles on the page, it's no longer a new window newWindow = false return styleIds } const getStylesByIds = (ids) => { return _.map(ids, (idOrCss) => { if (_.isString(idOrCss)) { return idOrCss } return cssIdToContentsMap.get(idOrCss) || { href: idOrCss.href } }) } return { getStyleIds, getStylesByIds, onCssModified, onBeforeWindowLoad, } } module.exports = { create, }