mirror of
https://github.com/cypress-io/cypress.git
synced 2026-04-22 07:00:22 -05:00
Reduce stylesheet-related memory consumption and improve performance (#4068)
* Memoize getCssRulesString by href to improve perf and memory address #2366 * Add tests * Move tests around in preparation for more * Memoize makePathsAbsoluteToStylesheet as well * Serve large dynamic css file rather than massive static file * Update readme to be explicit about directories to run npm scripts - This confused me each time I needed to get tests running again with the watchers * Fix bug that causes occasional test failure * rename spec to match issue instead of PR * update memoization of snapshot css to account for changes by javascript * improve snapshot css memory usage and performance * fix linting error * try re-ordering the tests * add existential check to html element * add back html file * limit memoized caches to 50 items * simplify generated css code * refactor listening to css modification and before window load * add/revise css snapshot tests remove tests that needed manual verificaiton in favor of integration tests * use lodash instead of Array.from * rename function for consistency * fix limited map first key implementation * fix log spec Co-authored-by: Chris Breiding <chrisbreiding@gmail.com>
This commit is contained in:
committed by
Brian Mann
parent
e44fc780dd
commit
d2e098206c
@@ -26,6 +26,7 @@ If you're developing on the driver, you'll want to run in the normal Cypress GUI
|
||||
|
||||
```bash
|
||||
## run in cypress GUI mode
|
||||
cd packages/driver
|
||||
npm run cypress:open
|
||||
```
|
||||
|
||||
@@ -50,6 +51,7 @@ The driver uses a node server to test all of its edge cases, so first start that
|
||||
|
||||
```bash
|
||||
## boot the driver's server
|
||||
cd packages/driver
|
||||
npm start
|
||||
```
|
||||
|
||||
|
||||
@@ -47,8 +47,9 @@
|
||||
"jsdom": "13.2.0",
|
||||
"lodash": "4.17.11",
|
||||
"lolex": "4.1.0",
|
||||
"methods": "1.1.2",
|
||||
"md5": "2.2.1",
|
||||
"method-override": "3.0.0",
|
||||
"methods": "1.1.2",
|
||||
"minimatch": "3.0.4",
|
||||
"minimist": "1.2.0",
|
||||
"mocha": "cypress-io/mocha#58f6eac05e664fc6b69aa9fba70f1f6b5531a900",
|
||||
|
||||
@@ -83,5 +83,4 @@ module.exports = {
|
||||
|
||||
contentWindow.alert = callbacks.onAlert
|
||||
contentWindow.confirm = callbacks.onConfirm
|
||||
|
||||
}
|
||||
|
||||
@@ -1,91 +1,26 @@
|
||||
path = require("path")
|
||||
url = require("url")
|
||||
_ = require("lodash")
|
||||
$ = require("jquery")
|
||||
|
||||
anyUrlInCssRe = /url\((['"])([^'"]*)\1\)/gm
|
||||
$SnapshotsCss = require("./snapshots_css")
|
||||
|
||||
HIGHLIGHT_ATTR = "data-cypress-el"
|
||||
|
||||
reduceText = (arr, fn) ->
|
||||
_.reduce arr, ((memo, item) -> memo += fn(item)), ""
|
||||
|
||||
getCssRulesString = (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
|
||||
if rules = stylesheet.rules or stylesheet.cssRules
|
||||
reduceText rules, (rule) -> rule.cssText
|
||||
else
|
||||
null
|
||||
catch e
|
||||
null
|
||||
|
||||
screenStylesheetRe = /(screen|all)/
|
||||
|
||||
isScreenStylesheet = (stylesheet) ->
|
||||
media = stylesheet.getAttribute("media")
|
||||
return not _.isString(media) or screenStylesheetRe.test(media)
|
||||
|
||||
getStylesFor = (doc, $$, stylesheets, location) ->
|
||||
styles = $$(location).find("link[rel='stylesheet'],style")
|
||||
styles = _.filter(styles, isScreenStylesheet)
|
||||
|
||||
_.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
|
||||
if 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
|
||||
makePathsAbsoluteToStylesheet(
|
||||
getCssRulesString(stylesheets[stylesheet.href]),
|
||||
stylesheet.href
|
||||
) or {
|
||||
href: stylesheet.href
|
||||
}
|
||||
else
|
||||
## otherwise, it's a style tag, and we can just grab its content
|
||||
styleRules = if stylesheet.sheet
|
||||
then Array.prototype.slice.call(stylesheet.sheet.cssRules).map((rule) -> rule.cssText).join("")
|
||||
else $$(stylesheet).text()
|
||||
|
||||
makePathsAbsoluteToDoc(doc, styleRules)
|
||||
|
||||
getDocumentStylesheets = (document = {}) ->
|
||||
_.reduce document.styleSheets, (memo, stylesheet) ->
|
||||
memo[stylesheet.href] = stylesheet
|
||||
return memo
|
||||
, {}
|
||||
|
||||
makePathsAbsoluteToStylesheet = (styles, stylesheetHref) ->
|
||||
return styles if not _.isString(styles)
|
||||
|
||||
stylesheetPath = stylesheetHref.replace(path.basename(stylesheetHref), '')
|
||||
styles.replace anyUrlInCssRe, (_1, _2, filePath) ->
|
||||
absPath = url.resolve(stylesheetPath, filePath)
|
||||
return "url('#{absPath}')"
|
||||
|
||||
makePathsAbsoluteToDoc = (doc, styles) ->
|
||||
return styles if not _.isString(styles)
|
||||
|
||||
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
|
||||
a = doc.createElement("a")
|
||||
a.href = filePath
|
||||
return "url('#{a.href}')"
|
||||
|
||||
create = ($$, state) ->
|
||||
getStyles = ->
|
||||
doc = state("document")
|
||||
stylesheets = getDocumentStylesheets(doc)
|
||||
snapshotsCss = $SnapshotsCss.create($$, state)
|
||||
snapshotsMap = new WeakMap()
|
||||
|
||||
return {
|
||||
headStyles: getStylesFor(doc, $$, stylesheets, "head")
|
||||
bodyStyles: getStylesFor(doc, $$, stylesheets, "body")
|
||||
}
|
||||
getHtmlAttrs = (htmlEl) ->
|
||||
tmpHtmlEl = document.createElement("html")
|
||||
|
||||
_.transform htmlEl?.attributes, (memo, attr) ->
|
||||
return if not attr.specified
|
||||
|
||||
try
|
||||
## if we can successfully set the attributethen set it on memo
|
||||
## because it's possible the attribute is completely invalid
|
||||
tmpHtmlEl.setAttribute(attr.name, attr.value)
|
||||
memo[attr.name] = attr.value
|
||||
, {}
|
||||
|
||||
replaceIframes = (body) ->
|
||||
## remove iframes because we don't want extra requests made, JS run, etc
|
||||
@@ -150,25 +85,52 @@ create = ($$, state) ->
|
||||
"""
|
||||
$placeholder[0].src = "data:text/html;charset=utf-8,#{encodeURI(contents)}"
|
||||
|
||||
createSnapshot = ($el) ->
|
||||
getStyles = (snapshot) ->
|
||||
styleIds = snapshotsMap.get(snapshot)
|
||||
|
||||
return {} if not styleIds
|
||||
|
||||
return {
|
||||
headStyles: snapshotsCss.getStylesByIds(styleIds.headStyleIds)
|
||||
bodyStyles: snapshotsCss.getStylesByIds(styleIds.bodyStyleIds)
|
||||
}
|
||||
|
||||
detachDom = (iframeContents) ->
|
||||
{ headStyleIds, bodyStyleIds } = snapshotsCss.getStyleIds()
|
||||
htmlAttrs = getHtmlAttrs(iframeContents.find('html')[0])
|
||||
$body = iframeContents.find('body')
|
||||
|
||||
$body.find('script,link[rel="stylesheet"],style').remove()
|
||||
|
||||
snapshot = {
|
||||
name: "final state"
|
||||
htmlAttrs
|
||||
body: $body.detach()
|
||||
}
|
||||
|
||||
snapshotsMap.set(snapshot, { headStyleIds, bodyStyleIds })
|
||||
|
||||
return snapshot
|
||||
|
||||
createSnapshot = (name, $elToHighlight) ->
|
||||
## create a unique selector for this el
|
||||
$el.attr(HIGHLIGHT_ATTR, true) if $el?.attr
|
||||
$elToHighlight.attr(HIGHLIGHT_ATTR, true) if $elToHighlight?.attr
|
||||
|
||||
## TODO: throw error here if cy is undefined!
|
||||
|
||||
body = $$("body").clone()
|
||||
$body = $$("body").clone()
|
||||
|
||||
## for the head and body, get an array of all CSS,
|
||||
## whether it's links or style tags
|
||||
## if it's same-origin, it will get the actual styles as a string
|
||||
## it it's cross-domain, it will get a reference to the link's href
|
||||
{headStyles, bodyStyles} = getStyles()
|
||||
{ headStyleIds, bodyStyleIds } = snapshotsCss.getStyleIds()
|
||||
|
||||
## replaces iframes with placeholders
|
||||
replaceIframes(body)
|
||||
replaceIframes($body)
|
||||
|
||||
## remove tags we don't want in body
|
||||
body.find("script,link[rel='stylesheet'],style").remove()
|
||||
$body.find("script,link[rel='stylesheet'],style").remove()
|
||||
|
||||
## here we need to figure out if we're in a remote manual environment
|
||||
## if so we need to stringify the DOM:
|
||||
@@ -184,32 +146,31 @@ create = ($$, state) ->
|
||||
## which would reduce memory, and some CPU operations
|
||||
|
||||
## now remove it after we clone
|
||||
$el.removeAttr(HIGHLIGHT_ATTR) if $el?.removeAttr
|
||||
|
||||
tmpHtmlEl = document.createElement("html")
|
||||
$elToHighlight.removeAttr(HIGHLIGHT_ATTR) if $elToHighlight?.removeAttr
|
||||
|
||||
## preserve attributes on the <html> tag
|
||||
htmlAttrs = _.reduce $$("html")[0]?.attributes, (memo, attr) ->
|
||||
if attr.specified
|
||||
try
|
||||
## if we can successfully set the attribute
|
||||
## then set it on memo because its possible
|
||||
## the attribute is completely invalid
|
||||
tmpHtmlEl.setAttribute(attr.name, attr.value)
|
||||
memo[attr.name] = attr.value
|
||||
htmlAttrs = getHtmlAttrs($$("html")[0])
|
||||
|
||||
memo
|
||||
, {}
|
||||
snapshot = {
|
||||
name
|
||||
htmlAttrs
|
||||
body: $body
|
||||
}
|
||||
|
||||
return {body, htmlAttrs, headStyles, bodyStyles}
|
||||
snapshotsMap.set(snapshot, { headStyleIds, bodyStyleIds })
|
||||
|
||||
return snapshot
|
||||
|
||||
return {
|
||||
createSnapshot
|
||||
|
||||
## careful renaming or removing this method, the runner depends on it
|
||||
detachDom
|
||||
|
||||
getStyles
|
||||
|
||||
getDocumentStylesheets
|
||||
onCssModified: snapshotsCss.onCssModified
|
||||
|
||||
onBeforeWindowLoad: snapshotsCss.onBeforeWindowLoad
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
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,
|
||||
}
|
||||
@@ -29,7 +29,7 @@ $utils = require("./cypress/utils")
|
||||
|
||||
proxies = {
|
||||
runner: "getStartTime getTestsState getEmissions setNumLogs countByTestState getDisplayPropsForLog getConsolePropsForLogById getSnapshotPropsForLogById getErrorByTestId setStartTime resumeAtTest normalizeAll".split(" ")
|
||||
cy: "getStyles".split(" ")
|
||||
cy: "detachDom getStyles".split(" ")
|
||||
}
|
||||
|
||||
jqueryProxyFn = ->
|
||||
@@ -395,6 +395,9 @@ class $Cypress
|
||||
when "app:window:unload"
|
||||
@emit("window:unload", args[0])
|
||||
|
||||
when "app:css:modified"
|
||||
@emit("css:modified", args[0])
|
||||
|
||||
when "spec:script:error"
|
||||
@emit("script:error", args...)
|
||||
|
||||
|
||||
@@ -169,6 +169,16 @@ create = (specWindow, Cypress, Cookies, state, config, log) ->
|
||||
contentWindow.document.hasFocus = ->
|
||||
focused.documentHasFocus.call(@)
|
||||
|
||||
cssModificationSpy = (original, args...) ->
|
||||
snapshots.onCssModified(@href)
|
||||
original.apply(@, args)
|
||||
|
||||
insertRule = contentWindow.CSSStyleSheet.prototype.insertRule
|
||||
deleteRule = contentWindow.CSSStyleSheet.prototype.deleteRule
|
||||
|
||||
contentWindow.CSSStyleSheet.prototype.insertRule = _.wrap(insertRule, cssModificationSpy)
|
||||
contentWindow.CSSStyleSheet.prototype.deleteRule = _.wrap(deleteRule, cssModificationSpy)
|
||||
|
||||
enqueue = (obj) ->
|
||||
## if we have a nestedIndex it means we're processing
|
||||
## nested commands and need to splice them into the
|
||||
@@ -912,6 +922,8 @@ create = (specWindow, Cypress, Cookies, state, config, log) ->
|
||||
|
||||
wrapNativeMethods(contentWindow)
|
||||
|
||||
snapshots.onBeforeWindowLoad()
|
||||
|
||||
timers.wrap(contentWindow)
|
||||
|
||||
onSpecWindowUncaughtException: ->
|
||||
@@ -954,8 +966,11 @@ create = (specWindow, Cypress, Cookies, state, config, log) ->
|
||||
## When the function returns true, this prevents the firing of the default event handler.
|
||||
return true
|
||||
|
||||
getStyles: ->
|
||||
snapshots.getStyles()
|
||||
detachDom: (args...) ->
|
||||
snapshots.detachDom(args...)
|
||||
|
||||
getStyles: (args...) ->
|
||||
snapshots.getStyles(args...)
|
||||
|
||||
setRunnable: (runnable, hookName) ->
|
||||
## when we're setting a new runnable
|
||||
|
||||
@@ -269,20 +269,12 @@ Log = (cy, state, config, obj) ->
|
||||
at: null
|
||||
next: null
|
||||
|
||||
{body, htmlAttrs, headStyles, bodyStyles} = cy.createSnapshot(@get("$el"))
|
||||
|
||||
obj = {
|
||||
name: name
|
||||
body: body
|
||||
htmlAttrs: htmlAttrs
|
||||
headStyles: headStyles
|
||||
bodyStyles: bodyStyles
|
||||
}
|
||||
snapshot = cy.createSnapshot(name, @get("$el"))
|
||||
|
||||
snapshots = @get("snapshots") ? []
|
||||
|
||||
## insert at index 'at' or whatever is the next position
|
||||
snapshots[options.at or snapshots.length] = obj
|
||||
snapshots[options.at or snapshots.length] = snapshot
|
||||
|
||||
@set("snapshots", snapshots)
|
||||
|
||||
|
||||
@@ -363,4 +363,20 @@ module.exports = {
|
||||
values
|
||||
|
||||
run(0)
|
||||
|
||||
memoize: (func, cacheInstance = new Map()) ->
|
||||
memoized = (args...) ->
|
||||
key = args[0]
|
||||
cache = memoized.cache
|
||||
|
||||
return cache.get(key) if cache.has(key)
|
||||
|
||||
result = func.apply(this, args)
|
||||
memoized.cache = cache.set(key, result) || cache
|
||||
|
||||
return result
|
||||
|
||||
memoized.cache = cacheInstance
|
||||
|
||||
return memoized
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
const _ = require('lodash')
|
||||
|
||||
// IE doesn't support Array.from or Map.prototype.keys
|
||||
const getMapKeys = (map) => {
|
||||
if (_.isFunction(Array.from) && _.isFunction(map.keys)) {
|
||||
return Array.from(map.keys())
|
||||
}
|
||||
|
||||
const keys = []
|
||||
|
||||
map.forEach((key) => {
|
||||
keys.push(key)
|
||||
})
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
class LimitedMap extends Map {
|
||||
constructor (limit = 100) {
|
||||
super()
|
||||
|
||||
this._limit = limit
|
||||
}
|
||||
|
||||
set (key, value) {
|
||||
if (this.size === this._limit) {
|
||||
const firstKey = _.first(getMapKeys(this))
|
||||
|
||||
this.delete(firstKey)
|
||||
}
|
||||
|
||||
return super.set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LimitedMap
|
||||
@@ -6,6 +6,13 @@
|
||||
<body>
|
||||
Some generic content
|
||||
|
||||
<div>
|
||||
<span class="foo">Foo</span>
|
||||
<span class="bar">Bar</span>
|
||||
<span class="baz">Baz</span>
|
||||
<span class="qux">Qux</span>
|
||||
</div>
|
||||
|
||||
<a href="/">link</a>
|
||||
|
||||
<a id="hashchange" href="#hashchange">hashchange</a>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.bar {
|
||||
color: red;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.baz {
|
||||
color: purple;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.print {
|
||||
color: black;
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
const $ = Cypress.$.bind(Cypress)
|
||||
const $SnapshotsCss = require('../../../../src/cy/snapshots_css')
|
||||
|
||||
const normalizeStyles = (styles) => {
|
||||
return styles
|
||||
.replace(/\s+/gm, '')
|
||||
.replace(/['"]/gm, '\'')
|
||||
}
|
||||
|
||||
const addStyles = (styles, to) => {
|
||||
return new Promise((resolve) => {
|
||||
$(styles)
|
||||
.load(() => {
|
||||
return resolve()
|
||||
})
|
||||
.appendTo(cy.$$(to))
|
||||
})
|
||||
}
|
||||
|
||||
describe('driver/src/cy/snapshots_css', () => {
|
||||
let snapshotCss
|
||||
|
||||
beforeEach(() => {
|
||||
snapshotCss = $SnapshotsCss.create(cy.$$, cy.state)
|
||||
|
||||
cy.viewport(400, 600)
|
||||
cy.visit('/fixtures/generic.html').then(() => {
|
||||
return Cypress.Promise.all([
|
||||
addStyles('<link rel="stylesheet" href="/fixtures/generic_styles.css" />', 'head'),
|
||||
addStyles('<style>p { color: blue; }</style>', 'head'),
|
||||
addStyles('<link media="screen" rel="stylesheet" href="http://localhost:3501/fixtures/generic_styles.css" />', 'head'),
|
||||
addStyles('<link media="print" rel="stylesheet" href="/fixtures/generic_styles_print.css" />', 'head'),
|
||||
addStyles('<link media="all" rel="stylesheet" href="/fixtures/generic_styles_2.css" />', 'body'),
|
||||
addStyles('<link rel="stylesheet" href="/fixtures/generic_styles_3.css" />', 'body'),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
context('.getStyleIds', () => {
|
||||
it('returns IDs for cached CSS contents', () => {
|
||||
const { headStyleIds, bodyStyleIds } = snapshotCss.getStyleIds()
|
||||
const another = snapshotCss.getStyleIds()
|
||||
|
||||
expect(headStyleIds).to.have.length(3)
|
||||
expect(headStyleIds[0]).to.eql({ hrefId: 'http://localhost:3500/fixtures/generic_styles.css' })
|
||||
|
||||
expect(bodyStyleIds).to.have.length(2)
|
||||
expect(bodyStyleIds).to.eql([{ hrefId: 'http://localhost:3500/fixtures/generic_styles_2.css' }, { hrefId: 'http://localhost:3500/fixtures/generic_styles_3.css' }])
|
||||
// IDs for 2 of the same stylesheets should have referential equality
|
||||
expect(headStyleIds[0]).to.equal(another.headStyleIds[0])
|
||||
})
|
||||
|
||||
it('returns strings for inline stylesheets', () => {
|
||||
const { headStyleIds } = snapshotCss.getStyleIds()
|
||||
|
||||
expect(headStyleIds[1]).to.equal('p { color: blue; }')
|
||||
})
|
||||
|
||||
it('returns { href } object for cross-origin stylesheets', () => {
|
||||
const { headStyleIds } = snapshotCss.getStyleIds()
|
||||
|
||||
expect(headStyleIds[2]).to.eql({ href: 'http://localhost:3501/fixtures/generic_styles.css' })
|
||||
})
|
||||
|
||||
it('works for media-less stylesheets', () => {
|
||||
const { headStyleIds } = snapshotCss.getStyleIds()
|
||||
|
||||
expect(headStyleIds[0]).to.eql({ hrefId: 'http://localhost:3500/fixtures/generic_styles.css' })
|
||||
})
|
||||
|
||||
it('works for media=screen stylesheets', () => {
|
||||
const { headStyleIds } = snapshotCss.getStyleIds()
|
||||
|
||||
expect(headStyleIds[2]).to.eql({ href: 'http://localhost:3501/fixtures/generic_styles.css' })
|
||||
})
|
||||
|
||||
it('works for media=all stylesheets', () => {
|
||||
const { bodyStyleIds } = snapshotCss.getStyleIds()
|
||||
|
||||
expect(bodyStyleIds[0]).to.eql({ hrefId: 'http://localhost:3500/fixtures/generic_styles_2.css' })
|
||||
})
|
||||
|
||||
it('ignores other media stylesheets', () => {
|
||||
const { headStyleIds } = snapshotCss.getStyleIds()
|
||||
|
||||
expect(headStyleIds).to.have.length(3)
|
||||
})
|
||||
|
||||
it('returns new id if css has been modified', () => {
|
||||
const idsBefore = snapshotCss.getStyleIds()
|
||||
|
||||
cy.state('document').styleSheets[0].insertRule('.qux { color: orange; }')
|
||||
snapshotCss.onCssModified('http://localhost:3500/fixtures/generic_styles.css')
|
||||
const idsAfter = snapshotCss.getStyleIds()
|
||||
|
||||
expect(idsBefore.headStyleIds).to.have.length(3)
|
||||
expect(idsAfter.headStyleIds).to.have.length(3)
|
||||
expect(idsAfter.headStyleIds[0]).to.eql({ hrefId: 'http://localhost:3500/fixtures/generic_styles.css' })
|
||||
// same href, but id should be referentially NOT equal
|
||||
expect(idsBefore.headStyleIds[0]).not.to.equal(idsAfter.headStyleIds[0])
|
||||
})
|
||||
|
||||
it('returns same id after css has been modified until a new window', () => {
|
||||
|
||||
cy.state('document').styleSheets[0].insertRule('.qux { color: orange; }')
|
||||
snapshotCss.onCssModified('http://localhost:3500/fixtures/generic_styles.css')
|
||||
const ids1 = snapshotCss.getStyleIds()
|
||||
const ids2 = snapshotCss.getStyleIds()
|
||||
const ids3 = snapshotCss.getStyleIds()
|
||||
|
||||
expect(ids1.headStyleIds[0]).to.eql({ hrefId: 'http://localhost:3500/fixtures/generic_styles.css' })
|
||||
expect(ids1.headStyleIds[0]).to.equal(ids2.headStyleIds[0])
|
||||
expect(ids2.headStyleIds[0]).to.equal(ids3.headStyleIds[0])
|
||||
|
||||
cy.state('document').styleSheets[0].deleteRule(0) // need to change contents or they will map to same id
|
||||
snapshotCss.onBeforeWindowLoad()
|
||||
const ids4 = snapshotCss.getStyleIds()
|
||||
|
||||
expect(ids4.headStyleIds[0]).to.eql({ hrefId: 'http://localhost:3500/fixtures/generic_styles.css' })
|
||||
expect(ids3.headStyleIds[0]).not.to.equal(ids4.headStyleIds[0])
|
||||
})
|
||||
|
||||
it('returns same id if css has been modified but yields same contents', () => {
|
||||
const ids1 = snapshotCss.getStyleIds()
|
||||
|
||||
cy.state('document').styleSheets[0].insertRule('.qux { color: orange; }')
|
||||
snapshotCss.onCssModified('http://localhost:3500/fixtures/generic_styles.css')
|
||||
cy.state('document').styleSheets[0].deleteRule(0)
|
||||
snapshotCss.onCssModified('http://localhost:3500/fixtures/generic_styles.css')
|
||||
|
||||
const ids2 = snapshotCss.getStyleIds()
|
||||
|
||||
expect(ids2.headStyleIds[0]).to.eql({ hrefId: 'http://localhost:3500/fixtures/generic_styles.css' })
|
||||
expect(ids1.headStyleIds[0]).to.equal(ids2.headStyleIds[0])
|
||||
})
|
||||
})
|
||||
|
||||
context('.getStylesByIds', () => {
|
||||
let getStyles
|
||||
|
||||
beforeEach(() => {
|
||||
getStyles = () => {
|
||||
const { headStyleIds, bodyStyleIds } = snapshotCss.getStyleIds()
|
||||
const headStyles = snapshotCss.getStylesByIds(headStyleIds)
|
||||
const bodyStyles = snapshotCss.getStylesByIds(bodyStyleIds)
|
||||
|
||||
return { headStyles, bodyStyles }
|
||||
}
|
||||
})
|
||||
|
||||
it('returns array of css styles for given ids', () => {
|
||||
const { headStyles, bodyStyles } = getStyles()
|
||||
|
||||
expect(headStyles[0]).to.equal('.foo { color: green; }')
|
||||
expect(headStyles[1]).to.equal('p { color: blue; }')
|
||||
expect(bodyStyles[0]).to.eql('.bar { color: red; }')
|
||||
expect(bodyStyles[1]).to.eql('.baz { color: purple; }')
|
||||
})
|
||||
|
||||
it('returns { href } object for cross-origin stylesheets', () => {
|
||||
const { headStyles } = getStyles()
|
||||
|
||||
expect(headStyles[2]).to.eql({ href: 'http://localhost:3501/fixtures/generic_styles.css' })
|
||||
})
|
||||
|
||||
it('includes rules injected by JavaScript', () => {
|
||||
const styleEl = document.createElement('style')
|
||||
|
||||
$(styleEl).appendTo(cy.$$('head'))
|
||||
styleEl.sheet.insertRule('.foo { color: red; }', 0)
|
||||
|
||||
const { headStyles } = getStyles()
|
||||
|
||||
expect(headStyles[3]).to.equal('.foo { color: red; }')
|
||||
})
|
||||
|
||||
it('replaces CSS paths of style tags with absolute paths', () => {
|
||||
const styles = `
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'Some Font';
|
||||
src: url('../fonts/some-font.eot');
|
||||
src: url('../fonts/some-font.eot?#iefix') format('embedded-opentype'), url('../fonts/some-font.woff2') format('woff2'), url('../fonts/some-font.woff') format('woff'), url('../fonts/some-font.ttf') format('truetype'), url('../fonts/some-font.svg#glyphicons_halflingsregular') format('svg');
|
||||
}
|
||||
</style>
|
||||
`
|
||||
|
||||
$(styles).appendTo(cy.$$('head'))
|
||||
|
||||
const { headStyles } = getStyles()
|
||||
|
||||
expect(normalizeStyles(headStyles[3])).to.include(normalizeStyles(`
|
||||
@font-face {
|
||||
font-family: "Some Font";
|
||||
src: url('http://localhost:3500/fonts/some-font.eot?#iefix') format("embedded-opentype"), url('http://localhost:3500/fonts/some-font.woff2') format("woff2"), url('http://localhost:3500/fonts/some-font.woff') format("woff"), url('http://localhost:3500/fonts/some-font.ttf') format("truetype"), url('http://localhost:3500/fonts/some-font.svg#glyphicons_halflingsregular') format("svg");
|
||||
}
|
||||
`))
|
||||
})
|
||||
|
||||
it('replaces CSS paths of local stylesheets with absolute paths', () => {
|
||||
return addStyles('<link rel="stylesheet" href="nested/with_paths.css" />', 'head').then(() => {
|
||||
const { headStyles } = getStyles()
|
||||
|
||||
expect(normalizeStyles(headStyles[3])).to.include(normalizeStyles(`
|
||||
@font-face {
|
||||
font-family: 'Some Font';
|
||||
src: url('http://localhost:3500/fixtures/fonts/some-font.eot?#iefix') format('embedded-opentype'), url('http://localhost:3500/fixtures/fonts/some-font.woff2') format('woff2'), url('http://localhost:3500/fixtures/fonts/some-font.woff') format('woff'), url('http://localhost:3500/fixtures/fonts/some-font.ttf') format('truetype'), url('http://localhost:3500/fixtures/fonts/some-font.svg#glyphicons_halflingsregular') format('svg');
|
||||
}
|
||||
`))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,11 +1,6 @@
|
||||
$ = Cypress.$.bind(Cypress)
|
||||
$Snapshots = require("../../../../src/cy/snapshots")
|
||||
|
||||
normalizeStyles = (styles) ->
|
||||
styles
|
||||
.replace(/\s+/gm, "")
|
||||
.replace(/['"]/gm, "'")
|
||||
|
||||
describe "driver/src/cy/snapshots", ->
|
||||
context "invalid snapshot html", ->
|
||||
beforeEach ->
|
||||
@@ -53,19 +48,19 @@ describe "driver/src/cy/snapshots", ->
|
||||
it "does not clone scripts", ->
|
||||
$("<script type='text/javascript' />").appendTo(cy.$$("body"))
|
||||
|
||||
{ body } = cy.createSnapshot(@$el)
|
||||
{ body } = cy.createSnapshot(null, @$el)
|
||||
expect(body.find("script")).not.to.exist
|
||||
|
||||
it "does not clone css stylesheets", ->
|
||||
$("<link rel='stylesheet' />").appendTo(cy.$$("body"))
|
||||
|
||||
{ body } = cy.createSnapshot(@$el)
|
||||
{ body } = cy.createSnapshot(null, @$el)
|
||||
expect(body.find("link")).not.to.exist
|
||||
|
||||
it "does not clone style tags", ->
|
||||
$("<style>.foo { color: blue }</style>").appendTo(cy.$$("body"))
|
||||
|
||||
{ body } = cy.createSnapshot(@$el)
|
||||
{ body } = cy.createSnapshot(null, @$el)
|
||||
expect(body.find("style")).not.to.exist
|
||||
|
||||
it "preserves classes on the <html> tag", ->
|
||||
@@ -74,215 +69,56 @@ describe "driver/src/cy/snapshots", ->
|
||||
$html[0].id = "baz"
|
||||
$html.css("margin", "10px")
|
||||
|
||||
{ htmlAttrs } = cy.createSnapshot(@$el)
|
||||
{ htmlAttrs } = cy.createSnapshot(null, @$el)
|
||||
expect(htmlAttrs).to.eql({
|
||||
class: "foo bar"
|
||||
id: "baz"
|
||||
style: "margin: 10px;"
|
||||
})
|
||||
|
||||
it "provides contents of style tags in head", ->
|
||||
$("<style>.foo { color: red }</style>").appendTo(cy.$$("head"))
|
||||
|
||||
{ headStyles } = cy.createSnapshot(@$el)
|
||||
expect(headStyles[0]).to.include(".foo { color: red; }")
|
||||
|
||||
it "provides contents of style tags in head for injected rules", ->
|
||||
styleEl = document.createElement("style");
|
||||
$(styleEl).appendTo(cy.$$("head"))
|
||||
styleEl.sheet.insertRule(".foo { color: red; }", 0)
|
||||
|
||||
{ headStyles } = cy.createSnapshot(@$el)
|
||||
expect(headStyles[0]).to.include(".foo { color: red; }")
|
||||
|
||||
it "provides contents of local stylesheet links in head", (done) ->
|
||||
onLoad = ->
|
||||
## need to for appended stylesheet to load
|
||||
{ headStyles } = cy.createSnapshot(@$el)
|
||||
|
||||
expect(headStyles[0]).to.include(".foo { color: green; }")
|
||||
done()
|
||||
|
||||
$("<link rel='stylesheet' href='generic_styles.css' />")
|
||||
.load(onLoad)
|
||||
.appendTo(cy.$$("head"))
|
||||
|
||||
it "provides media-less stylesheets", (done) ->
|
||||
onLoad = ->
|
||||
## need to for appended stylesheet to load
|
||||
{ headStyles } = cy.createSnapshot(@$el)
|
||||
|
||||
expect(headStyles[0]).to.include(".foo { color: green; }")
|
||||
done()
|
||||
|
||||
$("<link rel='stylesheet' href='generic_styles.css' />")
|
||||
.load(onLoad)
|
||||
.appendTo(cy.$$("head"))
|
||||
|
||||
it "provides media=screen stylesheets", (done) ->
|
||||
onLoad = ->
|
||||
## need to for appended stylesheet to load
|
||||
{ headStyles } = cy.createSnapshot(@$el)
|
||||
|
||||
expect(headStyles[0]).to.include(".foo { color: green; }")
|
||||
done()
|
||||
|
||||
$("<link rel='stylesheet' href='generic_styles.css' media='screen' />")
|
||||
.load(onLoad)
|
||||
.appendTo(cy.$$("head"))
|
||||
|
||||
it "provides media=all stylesheets", (done) ->
|
||||
onLoad = ->
|
||||
## need to for appended stylesheet to load
|
||||
{ headStyles } = cy.createSnapshot(@$el)
|
||||
|
||||
expect(headStyles[0]).to.include(".foo { color: green; }")
|
||||
done()
|
||||
|
||||
$("<link rel='stylesheet' href='generic_styles.css' media='all' />")
|
||||
.load(onLoad)
|
||||
.appendTo(cy.$$("head"))
|
||||
|
||||
it "does not provide non-screen stylesheets", (done) ->
|
||||
onLoad = ->
|
||||
## need to for appended stylesheet to load
|
||||
{ headStyles } = cy.createSnapshot(@$el)
|
||||
|
||||
expect(headStyles).to.have.length(0)
|
||||
done()
|
||||
|
||||
$("<link rel='stylesheet' href='generic_styles.css' media='print' />")
|
||||
.load(onLoad)
|
||||
.appendTo(cy.$$("head"))
|
||||
|
||||
it "provides object with href of external stylesheets in head", (done) ->
|
||||
onLoad = ->
|
||||
## need to for appended stylesheet to load
|
||||
{ headStyles } = cy.createSnapshot(@$el)
|
||||
|
||||
expect(headStyles[0]).to.deep.eq({href: "http://localhost:3501/fixtures/generic_styles.css"})
|
||||
done()
|
||||
|
||||
$("<link rel='stylesheet' href='http://localhost:3501/fixtures/generic_styles.css' />")
|
||||
.load(onLoad)
|
||||
.appendTo(cy.$$("head"))
|
||||
|
||||
it "provides contents of style tags in body", ->
|
||||
$("<style>.foo { color: red }</style>").appendTo(cy.$$("body"))
|
||||
|
||||
{bodyStyles} = cy.createSnapshot(@$el)
|
||||
expect(bodyStyles[bodyStyles.length - 1]).to.include(".foo { color: red; }")
|
||||
|
||||
it "provides contents of local stylesheet links in body", (done) ->
|
||||
onLoad = ->
|
||||
## need to for appended stylesheet to load
|
||||
{bodyStyles} = cy.createSnapshot(@$el)
|
||||
expect(bodyStyles[bodyStyles.length - 1]).to.include(".foo { color: green; }")
|
||||
done()
|
||||
|
||||
$("<link rel='stylesheet' href='generic_styles.css' />")
|
||||
.load(onLoad)
|
||||
.appendTo(cy.$$("body"))
|
||||
|
||||
it "provides object with href of external stylesheets in body", (done) ->
|
||||
onLoad = ->
|
||||
## need to for appended stylesheet to load
|
||||
{bodyStyles} = cy.createSnapshot(@$el)
|
||||
expect(bodyStyles[bodyStyles.length - 1]).to.eql({href: "http://localhost:3501/fixtures/generic_styles.css"})
|
||||
done()
|
||||
|
||||
$("<link rel='stylesheet' href='http://localhost:3501/fixtures/generic_styles.css' />")
|
||||
.load(onLoad)
|
||||
.appendTo(cy.$$("body"))
|
||||
|
||||
it "sets data-cypress-el attr", ->
|
||||
attr = cy.spy(@$el, "attr")
|
||||
cy.createSnapshot(@$el)
|
||||
cy.createSnapshot(null, @$el)
|
||||
expect(attr).to.be.calledWith("data-cypress-el", true)
|
||||
|
||||
it "removes data-cypress-el attr", ->
|
||||
cy.createSnapshot(@$el)
|
||||
cy.createSnapshot(null, @$el)
|
||||
expect(@$el.attr("data-cypress-el")).to.be.undefined
|
||||
|
||||
it "replaces CSS paths of style tags with absolute paths", ->
|
||||
styles = """
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'Some Font';
|
||||
src: url('../fonts/some-font.eot');
|
||||
src: url('../fonts/some-font.eot?#iefix') format('embedded-opentype'), url('../fonts/some-font.woff2') format('woff2'), url('../fonts/some-font.woff') format('woff'), url('../fonts/some-font.ttf') format('truetype'), url('../fonts/some-font.svg#glyphicons_halflingsregular') format('svg');
|
||||
}
|
||||
</style>
|
||||
"""
|
||||
$(styles).appendTo(cy.$$("head"))
|
||||
|
||||
## need to wait a tick for appended stylesheet to take affect
|
||||
{ headStyles } = cy.createSnapshot(@$el)
|
||||
expect(headStyles[0].replace(/\s+/gm, "")).to.include """
|
||||
@font-face {
|
||||
font-family: "Some Font";
|
||||
src: url('http://localhost:3500/fonts/some-font.eot?#iefix') format("embedded-opentype"), url('http://localhost:3500/fonts/some-font.woff2') format("woff2"), url('http://localhost:3500/fonts/some-font.woff') format("woff"), url('http://localhost:3500/fonts/some-font.ttf') format("truetype"), url('http://localhost:3500/fonts/some-font.svg#glyphicons_halflingsregular') format("svg");
|
||||
}
|
||||
""".replace(/\s+/gm, "")
|
||||
|
||||
it "replaces CSS paths of local stylesheets with absolute paths", (done) ->
|
||||
loadFn = ->
|
||||
## need to wait a tick for appended stylesheet to take affect
|
||||
{ headStyles } = cy.createSnapshot(@$el)
|
||||
expect(normalizeStyles(headStyles[0])).to.include(normalizeStyles("""
|
||||
@font-face {
|
||||
font-family: 'Some Font';
|
||||
src: url('http://localhost:3500/fixtures/fonts/some-font.eot?#iefix') format('embedded-opentype'), url('http://localhost:3500/fixtures/fonts/some-font.woff2') format('woff2'), url('http://localhost:3500/fixtures/fonts/some-font.woff') format('woff'), url('http://localhost:3500/fixtures/fonts/some-font.ttf') format('truetype'), url('http://localhost:3500/fixtures/fonts/some-font.svg#glyphicons_halflingsregular') format('svg');
|
||||
}
|
||||
"""))
|
||||
done()
|
||||
|
||||
$("<link rel='stylesheet' href='nested/with_paths.css' />")
|
||||
.load(loadFn)
|
||||
.appendTo(cy.$$("head"))
|
||||
|
||||
context "iframes", ->
|
||||
it "replaces with placeholders that have src in content", ->
|
||||
$("<iframe src='generic.html' />").appendTo(cy.$$("body"))
|
||||
|
||||
{ body } = cy.createSnapshot(@$el)
|
||||
{ body } = cy.createSnapshot(null, @$el)
|
||||
expect(body.find("iframe").length).to.equal(1)
|
||||
expect(body.find("iframe")[0].src).to.include("generic.html")
|
||||
|
||||
it "placeholders have same id", ->
|
||||
$("<iframe id='foo-bar' />").appendTo(cy.$$("body"))
|
||||
|
||||
{ body } = cy.createSnapshot(@$el)
|
||||
{ body } = cy.createSnapshot(null, @$el)
|
||||
expect(body.find("iframe")[0].id).to.equal("foo-bar")
|
||||
|
||||
it "placeholders have same classes", ->
|
||||
$("<iframe class='foo bar' />").appendTo(cy.$$("body"))
|
||||
|
||||
{ body } = cy.createSnapshot(@$el)
|
||||
{ body } = cy.createSnapshot(null, @$el)
|
||||
expect(body.find("iframe")[0].className).to.equal("foo bar")
|
||||
|
||||
it "placeholders have inline styles", ->
|
||||
$("<iframe style='margin: 40px' />").appendTo(cy.$$("body"))
|
||||
|
||||
{ body } = cy.createSnapshot(@$el)
|
||||
{ body } = cy.createSnapshot(null, @$el)
|
||||
expect(body.find("iframe").css("margin")).to.equal("40px")
|
||||
|
||||
it "placeholders have width set to outer width", ->
|
||||
$("<iframe style='width: 40px; padding: 20px; border: solid 5px' />").appendTo(cy.$$("body"))
|
||||
|
||||
{ body } = cy.createSnapshot(@$el)
|
||||
{ body } = cy.createSnapshot(null, @$el)
|
||||
expect(body.find("iframe").css("width")).to.equal("90px")
|
||||
|
||||
it "placeholders have height set to outer height", ->
|
||||
$("<iframe style='height: 40px; padding: 10px; border: solid 5px' />").appendTo(cy.$$("body"))
|
||||
|
||||
{ body } = cy.createSnapshot(@$el)
|
||||
{ body } = cy.createSnapshot(null, @$el)
|
||||
expect(body.find("iframe").css("height")).to.equal("70px")
|
||||
|
||||
context ".getDocumentStylesheets", ->
|
||||
it "returns empty obj when no document", ->
|
||||
fn = ->
|
||||
|
||||
snapshot = $Snapshots.create(fn, fn)
|
||||
|
||||
expect(snapshot.getDocumentStylesheets(null)).to.deep.eq({})
|
||||
|
||||
@@ -19,7 +19,7 @@ describe('src/cypress/log', function () {
|
||||
const log = this.log({ '$el': div })
|
||||
const result = log.snapshot()
|
||||
|
||||
expect(this.cy.createSnapshot).to.be.calledWith(div)
|
||||
expect(this.cy.createSnapshot).to.be.calledWith(undefined, div)
|
||||
expect(result).to.equal(log)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
LimitedMap = require("../../../../src/util/limited_map")
|
||||
|
||||
_ = Cypress._
|
||||
$utils = Cypress.utils
|
||||
Promise = Cypress.Promise
|
||||
@@ -58,3 +60,45 @@ describe "driver/src/cypress/utils", ->
|
||||
expect(err2.message).to.eq("\n\nbar")
|
||||
|
||||
expect(err2.stack).to.eq("Error: \n\nbar\n" + stack)
|
||||
|
||||
context ".memoize", ->
|
||||
it "runs the function the first time", ->
|
||||
fn = cy.stub().returns("output")
|
||||
memoizedFn = $utils.memoize(fn)
|
||||
result = memoizedFn("input")
|
||||
expect(fn).to.be.calledWith("input")
|
||||
expect(result).to.equal("output")
|
||||
|
||||
it "runs the function for unique first arguments", ->
|
||||
fn = cy.stub().returns("output")
|
||||
memoizedFn = $utils.memoize(fn)
|
||||
result1 = memoizedFn("input-1")
|
||||
result2 = memoizedFn("input-2")
|
||||
expect(fn).to.be.calledWith("input-1")
|
||||
expect(fn).to.be.calledWith("input-2")
|
||||
expect(fn).to.be.calledTwice
|
||||
expect(result1).to.equal("output")
|
||||
expect(result2).to.equal("output")
|
||||
|
||||
it "returns cached return value if first argument is the same", ->
|
||||
fn = cy.stub().returns("output")
|
||||
memoizedFn = $utils.memoize(fn)
|
||||
result1 = memoizedFn("input")
|
||||
result2 = memoizedFn("input")
|
||||
expect(fn).to.be.calledWith("input")
|
||||
expect(fn).to.be.calledOnce
|
||||
expect(result1).to.equal("output")
|
||||
expect(result2).to.equal("output")
|
||||
|
||||
it "accepts a cache instance to use as the second argument", ->
|
||||
fn = cy.stub().returns("output")
|
||||
## LimitedMap(2) only holds on to 2 items at a time and clears older ones
|
||||
memoizedFn = $utils.memoize(fn, new LimitedMap(2))
|
||||
memoizedFn("input-1")
|
||||
memoizedFn("input-2")
|
||||
expect(fn).to.be.calledTwice
|
||||
memoizedFn("input-3")
|
||||
expect(fn).to.be.calledThrice
|
||||
memoizedFn("input-1")
|
||||
## cache for input-1 is cleared, so it calls the function again
|
||||
expect(fn.callCount).to.be.equal(4)
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
const LimitedMap = require('../../../../src/util/limited_map')
|
||||
|
||||
const _ = Cypress._
|
||||
|
||||
describe('driver/src/util/limited_map', () => {
|
||||
it('has all the methods of a Map', () => {
|
||||
const limitedMap = new LimitedMap()
|
||||
|
||||
expect(limitedMap.set).to.be.a('function')
|
||||
expect(limitedMap.get).to.be.a('function')
|
||||
expect(limitedMap.delete).to.be.a('function')
|
||||
expect(limitedMap.has).to.be.a('function')
|
||||
expect(limitedMap.size).to.be.a('number')
|
||||
})
|
||||
|
||||
it('remove old entries when over limit, default 100', () => {
|
||||
const limitedMap = new LimitedMap()
|
||||
|
||||
_.each(_.times(100), (i) => {
|
||||
limitedMap.set(`foo-${i}`, i)
|
||||
})
|
||||
expect(limitedMap.size).to.equal(100)
|
||||
expect(Array.from(limitedMap.values())[0]).to.equal(0)
|
||||
expect(Array.from(limitedMap.values())[99]).to.equal(99)
|
||||
|
||||
limitedMap.set(`foo-${100}`, 100)
|
||||
expect(limitedMap.size).to.equal(100)
|
||||
expect(Array.from(limitedMap.values())[0]).to.equal(1)
|
||||
expect(Array.from(limitedMap.values())[99]).to.equal(100)
|
||||
|
||||
limitedMap.set(`foo-${101}`, 101)
|
||||
expect(limitedMap.size).to.equal(100)
|
||||
expect(Array.from(limitedMap.values())[0]).to.equal(2)
|
||||
expect(Array.from(limitedMap.values())[99]).to.equal(101)
|
||||
|
||||
limitedMap.set(`foo-${102}`, 102)
|
||||
expect(limitedMap.size).to.equal(100)
|
||||
expect(Array.from(limitedMap.values())[0]).to.equal(3)
|
||||
expect(Array.from(limitedMap.values())[99]).to.equal(102)
|
||||
})
|
||||
|
||||
it('accepts limit as first parameter', () => {
|
||||
const limitedMap = new LimitedMap(5)
|
||||
|
||||
_.each(_.times(5), (i) => {
|
||||
limitedMap.set(`foo-${i}`, i)
|
||||
})
|
||||
expect(limitedMap.size).to.equal(5)
|
||||
expect(Array.from(limitedMap.values())[0]).to.equal(0)
|
||||
expect(Array.from(limitedMap.values())[4]).to.equal(4)
|
||||
|
||||
limitedMap.set(`foo-${5}`, 5)
|
||||
expect(limitedMap.size).to.equal(5)
|
||||
expect(Array.from(limitedMap.values())[0]).to.equal(1)
|
||||
expect(Array.from(limitedMap.values())[4]).to.equal(5)
|
||||
|
||||
limitedMap.set(`foo-${6}`, 6)
|
||||
expect(limitedMap.size).to.equal(5)
|
||||
expect(Array.from(limitedMap.values())[0]).to.equal(2)
|
||||
expect(Array.from(limitedMap.values())[4]).to.equal(6)
|
||||
|
||||
})
|
||||
})
|
||||
@@ -51,38 +51,22 @@ export default class AutIframe {
|
||||
return this._contents() && this._contents().find('body')
|
||||
}
|
||||
|
||||
detachDom = (Cypress) => {
|
||||
const contents = this._contents()
|
||||
const { headStyles, bodyStyles } = Cypress.getStyles()
|
||||
let htmlAttrs = {}
|
||||
detachDom = () => {
|
||||
const Cypress = eventManager.getCypress()
|
||||
|
||||
if (contents.find('html')[0]) {
|
||||
htmlAttrs = _.transform(contents.find('html')[0].attributes, (memo, attr) => {
|
||||
if (attr.specified) {
|
||||
memo[attr.name] = attr.value
|
||||
}
|
||||
}, {})
|
||||
}
|
||||
if (!Cypress) return
|
||||
|
||||
const $body = contents.find('body')
|
||||
|
||||
$body.find('script,link[rel="stylesheet"],style').remove()
|
||||
|
||||
return {
|
||||
body: $body.detach(),
|
||||
htmlAttrs,
|
||||
headStyles,
|
||||
bodyStyles,
|
||||
}
|
||||
return Cypress.detachDom(this._contents())
|
||||
}
|
||||
|
||||
restoreDom = ({ body, htmlAttrs, headStyles, bodyStyles }) => {
|
||||
restoreDom = (snapshot) => {
|
||||
const Cypress = eventManager.getCypress()
|
||||
const { headStyles, bodyStyles } = Cypress ? Cypress.getStyles(snapshot) : {}
|
||||
const { body, htmlAttrs } = snapshot
|
||||
const contents = this._contents()
|
||||
|
||||
const $html = contents.find('html')
|
||||
|
||||
this._replaceHtmlAttrs($html, htmlAttrs)
|
||||
|
||||
this._replaceHeadStyles(headStyles)
|
||||
|
||||
// remove the old body and replace with restored one
|
||||
@@ -113,7 +97,7 @@ export default class AutIframe {
|
||||
})
|
||||
}
|
||||
|
||||
_replaceHeadStyles (styles) {
|
||||
_replaceHeadStyles (styles = []) {
|
||||
const $head = this._contents().find('head')
|
||||
const existingStyles = $head.find('link[rel="stylesheet"],style')
|
||||
|
||||
@@ -165,7 +149,7 @@ export default class AutIframe {
|
||||
}
|
||||
}
|
||||
|
||||
_insertBodyStyles ($body, styles) {
|
||||
_insertBodyStyles ($body, styles = []) {
|
||||
_.each(styles, (style) => {
|
||||
$body.append(style.href ? this._linkTag(style) : this._styleTag(style))
|
||||
})
|
||||
|
||||
@@ -156,7 +156,7 @@ export default class IframeModel {
|
||||
|
||||
this._updateViewport(this.originalState)
|
||||
this._updateUrl(this.originalState.url)
|
||||
this.restoreDom(this.originalState)
|
||||
this.restoreDom(this.originalState.snapshot)
|
||||
this._clearMessage()
|
||||
|
||||
this.originalState = null
|
||||
@@ -208,17 +208,16 @@ export default class IframeModel {
|
||||
}
|
||||
|
||||
_storeOriginalState () {
|
||||
const originalState = this.detachDom()
|
||||
const finalSnapshot = this.detachDom()
|
||||
|
||||
if (!originalState) return
|
||||
if (!finalSnapshot) return
|
||||
|
||||
const { body, htmlAttrs, headStyles, bodyStyles } = originalState
|
||||
const { body, htmlAttrs } = finalSnapshot
|
||||
|
||||
this.originalState = {
|
||||
body,
|
||||
htmlAttrs,
|
||||
headStyles,
|
||||
bodyStyles,
|
||||
snapshot: finalSnapshot,
|
||||
url: this.state.url,
|
||||
viewportWidth: this.state.width,
|
||||
viewportHeight: this.state.height,
|
||||
|
||||
@@ -9,7 +9,6 @@ import AutIframe from './aut-iframe'
|
||||
import ScriptError from '../errors/script-error'
|
||||
import SnapshotControls from './snapshot-controls'
|
||||
|
||||
import eventManager from '../lib/event-manager'
|
||||
import IframeModel from './iframe-model'
|
||||
import logger from '../lib/logger'
|
||||
import selectorPlaygroundModel from '../selector-playground/selector-playground-model'
|
||||
@@ -74,13 +73,7 @@ export default class Iframes extends Component {
|
||||
removeHeadStyles: this.autIframe.removeHeadStyles,
|
||||
restoreDom: this.autIframe.restoreDom,
|
||||
highlightEl: this.autIframe.highlightEl,
|
||||
detachDom: () => {
|
||||
const Cypress = eventManager.getCypress()
|
||||
|
||||
if (Cypress) {
|
||||
return this.autIframe.detachDom(Cypress)
|
||||
}
|
||||
},
|
||||
detachDom: this.autIframe.detachDom,
|
||||
snapshotControls: (snapshotProps) => (
|
||||
<SnapshotControls
|
||||
eventManager={this.props.eventManager}
|
||||
|
||||
Reference in New Issue
Block a user