mirror of
https://github.com/cypress-io/cypress.git
synced 2026-02-22 15:11:00 -06:00
chore(runner,runner-ct): reduce duplication with packages/runner-shared (#16866)
* move snapshot-controls to shared package * share visit-failure component * share blank-contents * share message component * move message styling to shared * stub scss in unit tests * remove whitespace * make dup files match * share selector playground * share script error * share automation-disconnected * remove old file * share no-automation * share error messages * share errors * make iframe model files similar * share iframe-model * share selector playground * share style * share highlight in selector playground * share dom file * update import * share aut-iframe * wip: shared event manager * remove CT event manager * move studio to shared runner package * fix tests * use shared event manager in CT runner * comment back in code * rename viewporth width/height to width/height * fix ts errors * share viewport info * share header * revert changed test * remove old code * fix tests and move test to shared package * move tests to shared package * make container files similar * share container in runner * share container * move test * move spec * update tsconfig] * update headeR * fix styling * style * refactor * refactor * reduce public modules * Update packages/runner-shared/src/event-manager.js Co-authored-by: Zach Bloomquist <github@chary.us> * fix percy regression * fix regression in style * improve types, try reverting style * add runner-shared tests to pipeline Co-authored-by: Zach Bloomquist <github@chary.us> Co-authored-by: Barthélémy Ledoux <bart@cypress.io>
This commit is contained in:
@@ -46,7 +46,7 @@
|
||||
"stop-only": "npx stop-only --skip .cy,.publish,.projects,node_modules,dist,dist-test,fixtures,lib,bower_components,src,__snapshots__ --exclude e2e.ts,cypress-tests.ts,unwritten.spec.ts",
|
||||
"stop-only-all": "yarn stop-only --folder packages",
|
||||
"pretest": "yarn ensure-deps",
|
||||
"test": "yarn lerna exec yarn test --scope cypress --scope \"'@packages/{electron,extension,https-proxy,launcher,net-stubbing,network,proxy,rewriter,runner,socket}'\"",
|
||||
"test": "yarn lerna exec yarn test --scope cypress --scope \"'@packages/{electron,extension,https-proxy,launcher,net-stubbing,network,proxy,rewriter,runner,runner-shared,socket}'\"",
|
||||
"test-debug": "lerna exec yarn test-debug --ignore \"'@packages/{desktop-gui,driver,root,static,web-config}'\"",
|
||||
"pretest-e2e": "yarn ensure-deps",
|
||||
"test-e2e": "lerna exec yarn test-e2e --ignore \"'@packages/{desktop-gui,driver,root,static,web-config}'\"",
|
||||
|
||||
3
packages/driver/index.d.ts
vendored
3
packages/driver/index.d.ts
vendored
@@ -2,4 +2,5 @@
|
||||
/// <reference path="../ts/index.d.ts" />
|
||||
export const $Cypress: Cypress.Cypress
|
||||
|
||||
export default $Cypress
|
||||
export const $: typeof JQuery
|
||||
export default $Cypress
|
||||
@@ -3,7 +3,7 @@ import React from 'react'
|
||||
import { mount } from '@cypress/react'
|
||||
import RunnerCt from '../../src/app/RunnerCt'
|
||||
import '@packages/runner/src/main.scss'
|
||||
import eventManager from '../../src/lib/event-manager'
|
||||
import { eventManager } from '@packages/runner-shared'
|
||||
import { testSpecFile } from '../fixtures/testSpecFile'
|
||||
import { makeState, fakeConfig, getPort } from './utils'
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react'
|
||||
import { mount } from '@cypress/react'
|
||||
import RunnerCt from '../../src/app/RunnerCt'
|
||||
import '@packages/runner/src/main.scss'
|
||||
import eventManager from '../../src/lib/event-manager'
|
||||
import { eventManager } from '@packages/runner-shared'
|
||||
import { testSpecFile } from '../fixtures/testSpecFile'
|
||||
import { makeState, fakeConfig, getPort } from './utils'
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import React from 'react'
|
||||
import { mount } from '@cypress/react'
|
||||
import ScriptError from '../../src/errors/script-error'
|
||||
import { ScriptError } from '@packages/runner-shared'
|
||||
|
||||
describe('ScriptError', () => {
|
||||
it('renders an error', () => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
|
||||
import State from '../lib/state'
|
||||
import { Hidden } from '../lib/Hidden'
|
||||
import { namedObserver } from '../lib/mobx'
|
||||
import { namedObserver } from '@packages/runner-shared'
|
||||
import { PLUGIN_BAR_HEIGHT } from './RunnerCt'
|
||||
|
||||
import styles from './RunnerCt.module.scss'
|
||||
|
||||
@@ -3,10 +3,8 @@ import cs from 'classnames'
|
||||
import { ReporterHeaderProps } from '@packages/reporter/src/header/header'
|
||||
import { Reporter } from '@packages/reporter/src/main'
|
||||
|
||||
import errorMessages from '../errors/error-messages'
|
||||
import EventManager from '../lib/event-manager'
|
||||
import { errorMessages, namedObserver, eventManager as EventManager } from '@packages/runner-shared'
|
||||
import State from '../lib/state'
|
||||
import { namedObserver } from '../lib/mobx'
|
||||
import { ReporterHeader } from './ReporterHeader'
|
||||
import { NoSpec } from './NoSpec'
|
||||
|
||||
@@ -15,7 +13,10 @@ import styles from './RunnerCt.module.scss'
|
||||
interface ReporterContainerProps {
|
||||
state: State
|
||||
eventManager: typeof EventManager
|
||||
config: Cypress.RuntimeConfigOptions
|
||||
config: {
|
||||
configFile: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export const ReporterContainer = namedObserver('ReporterContainer',
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ReporterHeaderProps } from '@packages/reporter/src/header/header'
|
||||
import Stats from '@packages/reporter/src/header/stats'
|
||||
import Controls from '@packages/reporter/src/header/controls'
|
||||
import { StatsStore } from '@packages/reporter/src/header/stats-store'
|
||||
import { namedObserver } from '../lib/mobx'
|
||||
import { namedObserver } from '@packages/runner-shared'
|
||||
import styles from './ReporterHeader.module.scss'
|
||||
|
||||
export const EmptyReporterHeader: React.FC = () => {
|
||||
|
||||
@@ -14,13 +14,12 @@ library.add(fas)
|
||||
library.add(fab)
|
||||
|
||||
import State from '../lib/state'
|
||||
import EventManager from '../lib/event-manager'
|
||||
import { eventManager as EventManager, namedObserver } from '@packages/runner-shared'
|
||||
import { useGlobalHotKey } from '../lib/useHotKey'
|
||||
import { animationFrameDebounce } from '../lib/debounce'
|
||||
import { LeftNavMenu } from './LeftNavMenu'
|
||||
import { SpecContent } from './SpecContent'
|
||||
import { hideIfScreenshotting, hideSpecsListIfNecessary } from '../lib/hideGuard'
|
||||
import { namedObserver } from '../lib/mobx'
|
||||
import { SpecList } from './SpecList/SpecList'
|
||||
import { NoSpec } from './NoSpec'
|
||||
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import cs from 'classnames'
|
||||
import * as React from 'react'
|
||||
import SplitPane from 'react-split-pane'
|
||||
import { Message, namedObserver, eventManager as EventManager, Header } from '@packages/runner-shared'
|
||||
|
||||
import Header from '../header/header'
|
||||
import { Iframes } from '../iframe/iframes'
|
||||
import { animationFrameDebounce } from '../lib/debounce'
|
||||
import { Message } from '../message/message'
|
||||
import { KeyboardHelper } from './KeyboardHelper'
|
||||
import { NoSpec } from './NoSpec'
|
||||
import { Plugins } from './Plugins'
|
||||
import { ReporterContainer } from './ReporterContainer'
|
||||
import { PLUGIN_BAR_HEIGHT } from './RunnerCt'
|
||||
import State from '../lib/state'
|
||||
import EventManager from '../lib/event-manager'
|
||||
import { hideIfScreenshotting, hideReporterIfNecessary } from '../lib/hideGuard'
|
||||
|
||||
import styles from './RunnerCt.module.scss'
|
||||
import { namedObserver } from '../lib/mobx'
|
||||
|
||||
interface SpecContentProps {
|
||||
state: State
|
||||
eventManager: typeof EventManager
|
||||
config: Cypress.RuntimeConfigOptions
|
||||
config: {
|
||||
configFile: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
interface SpecContentWrapperProps {
|
||||
@@ -62,7 +62,7 @@ export const SpecContent = namedObserver('SpecContent', (props: SpecContentProps
|
||||
},
|
||||
)}
|
||||
>
|
||||
<Header {...props} />
|
||||
<Header {...props} runner='ct' />
|
||||
{props.state.spec
|
||||
? <Iframes {...props} />
|
||||
: (
|
||||
@@ -70,7 +70,19 @@ export const SpecContent = namedObserver('SpecContent', (props: SpecContentProps
|
||||
<KeyboardHelper />
|
||||
</NoSpec>
|
||||
)}
|
||||
<Message state={props.state} />
|
||||
<Message
|
||||
state={{
|
||||
messageTitle: props.state.messageTitle,
|
||||
messageControls: props.state.messageControls,
|
||||
messageDescription: props.state.messageDescription,
|
||||
messageType: props.state.messageType,
|
||||
messageStyles: {
|
||||
state: props.state.messageStyles.state,
|
||||
styles: props.state.messageStyles.styles,
|
||||
messageType: props.state.messageType,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Plugins
|
||||
key="plugins"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react'
|
||||
import { runInAction } from 'mobx'
|
||||
import EventManager from '../lib/event-manager'
|
||||
import { eventManager as EventManager } from '@packages/runner-shared'
|
||||
import State from '../lib/state'
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
export default {
|
||||
reporterError (err, specPath) {
|
||||
if (!err) return null
|
||||
|
||||
switch (err.type) {
|
||||
case 'BUNDLE_ERROR':
|
||||
return {
|
||||
title: 'Oops...we found an error preparing this test file:',
|
||||
link: 'https://on.cypress.io/we-found-an-error-preparing-your-test-file',
|
||||
callout: specPath,
|
||||
message: `
|
||||
This occurred while Cypress was compiling and bundling your test code. This is usually caused by:
|
||||
|
||||
* A missing file or dependency
|
||||
* A syntax error in the file or one of its dependencies
|
||||
|
||||
Fix the error in your code and re-run your tests.
|
||||
`,
|
||||
}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
import cs from 'classnames'
|
||||
|
||||
import { action, observable } from 'mobx'
|
||||
import { observer } from 'mobx-react'
|
||||
import React, { Component } from 'react'
|
||||
import Tooltip from '@cypress/react-tooltip'
|
||||
import State from '../lib/state'
|
||||
|
||||
import { configFileFormatted } from '../lib/config-file-formatted'
|
||||
import SelectorPlayground from '../selector-playground/selector-playground'
|
||||
import selectorPlaygroundModel from '../selector-playground/selector-playground-model'
|
||||
|
||||
interface HeaderProps {
|
||||
state: State
|
||||
config: Cypress.RuntimeConfigOptions
|
||||
}
|
||||
|
||||
@observer
|
||||
export default class Header extends Component<HeaderProps> {
|
||||
headerRef = React.createRef()
|
||||
|
||||
@observable showingViewportMenu = false
|
||||
|
||||
render () {
|
||||
const { state, config } = this.props
|
||||
|
||||
return (
|
||||
<header
|
||||
ref={this.headerRef}
|
||||
className={cs({
|
||||
'showing-selector-playground': selectorPlaygroundModel.isOpen,
|
||||
'display-none': state.screenshotting,
|
||||
})}
|
||||
>
|
||||
<div className='sel-url-wrap'>
|
||||
<Tooltip
|
||||
title='Open Selector Playground'
|
||||
visible={selectorPlaygroundModel.isOpen ? false : null}
|
||||
wrapperClassName='selector-playground-toggle-tooltip-wrapper'
|
||||
className='cy-tooltip'
|
||||
>
|
||||
<button
|
||||
aria-label='Open Selector Playground'
|
||||
className='header-button selector-playground-toggle'
|
||||
disabled={state.isLoading || state.isRunning}
|
||||
onClick={this._togglePlaygroundOpen}
|
||||
>
|
||||
<i aria-hidden="true" className='fas fa-crosshairs' />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<ul className='menu'>
|
||||
<li className={cs('viewport-info', { 'menu-open': this.showingViewportMenu })}>
|
||||
<button onClick={this._toggleViewportMenu}>
|
||||
{`${state.viewportWidth} `}
|
||||
<span className='the-x'>x</span>
|
||||
{` ${state.viewportHeight}`}
|
||||
<i className='fas fa-fw fa-info-circle'></i>
|
||||
</button>
|
||||
<div className='popup-menu viewport-menu'>
|
||||
{/* eslint-disable react/jsx-one-expression-per-line */}
|
||||
<p>The <strong>viewport</strong> determines the width and height of your application. By default the viewport will be
|
||||
<strong>{state.defaults.viewportWidth}px</strong> by
|
||||
<strong>{state.defaults.viewportHeight}px</strong> unless specified by a
|
||||
<code>cy.viewport</code> command.
|
||||
</p>
|
||||
<p>Additionally you can override the default viewport dimensions by specifying these values in your {configFileFormatted(config.configFile)}.</p>
|
||||
<pre>{/* eslint-disable indent */}
|
||||
{`{
|
||||
"viewportWidth": ${state.defaults.viewportWidth},
|
||||
"viewportHeight": ${state.defaults.viewportHeight}
|
||||
}`}
|
||||
</pre>
|
||||
{/* eslint-enable indent */}
|
||||
<p>
|
||||
<a href='https://on.cypress.io/viewport' target='_blank' rel='noreferrer'>
|
||||
<i className='fas fa-info-circle'></i>
|
||||
Read more about viewport here.
|
||||
</a>
|
||||
</p>
|
||||
{/* eslint-enable react/jsx-one-expression-per-line */}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<SelectorPlayground model={selectorPlaygroundModel} />
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.previousSelectorPlaygroundOpen = selectorPlaygroundModel.isOpen
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
if (selectorPlaygroundModel.isOpen !== this.previousSelectorPlaygroundOpen) {
|
||||
this.props.state.updateWindowDimensions({
|
||||
headerHeight: this.headerRef.current.offsetHeight,
|
||||
})
|
||||
|
||||
this.previousSelectorPlaygroundOpen = selectorPlaygroundModel.isOpen
|
||||
}
|
||||
}
|
||||
|
||||
_togglePlaygroundOpen = () => {
|
||||
selectorPlaygroundModel.toggleOpen()
|
||||
}
|
||||
|
||||
@action _toggleViewportMenu = () => {
|
||||
this.showingViewportMenu = !this.showingViewportMenu
|
||||
}
|
||||
}
|
||||
@@ -1,399 +0,0 @@
|
||||
import _ from 'lodash'
|
||||
import { $ } from '@packages/driver'
|
||||
|
||||
import dom from '../lib/dom'
|
||||
import logger from '../lib/logger'
|
||||
import eventManager from '../lib/event-manager'
|
||||
import visitFailure from './visit-failure'
|
||||
import blankContents from './blank-contents'
|
||||
import selectorPlaygroundModel from '../selector-playground/selector-playground-model'
|
||||
|
||||
export default class AutIframe {
|
||||
constructor (config) {
|
||||
this.config = config
|
||||
this.debouncedToggleSelectorPlayground = _.debounce(this.toggleSelectorPlayground, 300)
|
||||
}
|
||||
|
||||
create () {
|
||||
this.$iframe = $('<iframe>', {
|
||||
id: `Your App: '${this.config.projectName}'`,
|
||||
class: 'aut-iframe',
|
||||
})
|
||||
|
||||
return this.$iframe
|
||||
}
|
||||
|
||||
showBlankContents () {
|
||||
this._showContents(blankContents())
|
||||
}
|
||||
|
||||
showVisitFailure = (props) => {
|
||||
this._showContents(visitFailure(props))
|
||||
}
|
||||
|
||||
_showContents (contents) {
|
||||
this._body().html(contents)
|
||||
}
|
||||
|
||||
_contents () {
|
||||
return this.$iframe && this.$iframe.contents()
|
||||
}
|
||||
|
||||
_window () {
|
||||
return this.$iframe.prop('contentWindow')
|
||||
}
|
||||
|
||||
_document () {
|
||||
return this.$iframe.prop('contentDocument')
|
||||
}
|
||||
|
||||
_body () {
|
||||
return this._contents() && this._contents().find('body')
|
||||
}
|
||||
|
||||
detachDom = () => {
|
||||
const Cypress = eventManager.getCypress()
|
||||
|
||||
if (!Cypress) return
|
||||
|
||||
return Cypress.cy.detachDom(this._contents())
|
||||
}
|
||||
|
||||
restoreDom = (snapshot) => {
|
||||
const Cypress = eventManager.getCypress()
|
||||
const { headStyles, bodyStyles } = Cypress ? Cypress.cy.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
|
||||
this._body().remove()
|
||||
this._insertBodyStyles(body.get(), bodyStyles)
|
||||
$html.append(body.get())
|
||||
|
||||
this.debouncedToggleSelectorPlayground(selectorPlaygroundModel.isEnabled)
|
||||
}
|
||||
|
||||
_replaceHtmlAttrs ($html, htmlAttrs) {
|
||||
let oldAttrs = {}
|
||||
|
||||
// remove all attributes
|
||||
if ($html[0]) {
|
||||
oldAttrs = _.map($html[0].attributes, (attr) => {
|
||||
return attr.name
|
||||
})
|
||||
}
|
||||
|
||||
_.each(oldAttrs, (attr) => {
|
||||
$html.removeAttr(attr)
|
||||
})
|
||||
|
||||
// set the ones specified
|
||||
_.each(htmlAttrs, (value, key) => {
|
||||
$html.attr(key, value)
|
||||
})
|
||||
}
|
||||
|
||||
_replaceHeadStyles (styles = []) {
|
||||
const $head = this._contents().find('head')
|
||||
const existingStyles = $head.find('link[rel="stylesheet"],style')
|
||||
|
||||
_.each(styles, (style, index) => {
|
||||
if (style.href) {
|
||||
// make a best effort at not disturbing <link> stylesheets
|
||||
// if possible by checking to see if the existing head has a
|
||||
// stylesheet with the same href in the same position
|
||||
this._replaceLink($head, existingStyles[index], style)
|
||||
} else {
|
||||
// for <style> tags, just replace them completely since the contents
|
||||
// could be different and it shouldn't cause a FOUC since
|
||||
// there's no http request involved
|
||||
this._replaceStyle($head, existingStyles[index], style)
|
||||
}
|
||||
})
|
||||
|
||||
// remove any extra stylesheets
|
||||
if (existingStyles.length > styles.length) {
|
||||
existingStyles.slice(styles.length).remove()
|
||||
}
|
||||
}
|
||||
|
||||
_replaceLink ($head, existingStyle, style) {
|
||||
const linkTag = this._linkTag(style)
|
||||
|
||||
if (!existingStyle) {
|
||||
// no existing style at this index, so no more styles at all in
|
||||
// the head, so just append it
|
||||
$head.append(linkTag)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (existingStyle.href !== style.href) {
|
||||
$(existingStyle).replaceWith(linkTag)
|
||||
}
|
||||
}
|
||||
|
||||
_replaceStyle ($head, existingStyle, style) {
|
||||
const styleTag = this._styleTag(style)
|
||||
|
||||
if (existingStyle) {
|
||||
$(existingStyle).replaceWith(styleTag)
|
||||
} else {
|
||||
// no existing style at this index, so no more styles at all in
|
||||
// the head, so just append it
|
||||
$head.append(styleTag)
|
||||
}
|
||||
}
|
||||
|
||||
_insertBodyStyles ($body, styles = []) {
|
||||
_.each(styles, (style) => {
|
||||
$body.append(style.href ? this._linkTag(style) : this._styleTag(style))
|
||||
})
|
||||
}
|
||||
|
||||
_linkTag (style) {
|
||||
return `<link rel="stylesheet" href="${style.href}" />`
|
||||
}
|
||||
|
||||
_styleTag (style) {
|
||||
return `<style>${style}</style>`
|
||||
}
|
||||
|
||||
highlightEl = ({ body }, { $el, coords, highlightAttr, scrollBy }) => {
|
||||
this.removeHighlights()
|
||||
|
||||
if (body) {
|
||||
$el = body.get().find(`[${highlightAttr}]`)
|
||||
} else {
|
||||
body = { get: () => this._body() }
|
||||
}
|
||||
|
||||
// normalize
|
||||
const el = $el.get(0)
|
||||
const $body = body.get()
|
||||
|
||||
body = $body.get(0)
|
||||
|
||||
// scroll the top of the element into view
|
||||
if (el) {
|
||||
el.scrollIntoView()
|
||||
// if we have a scrollBy on our command
|
||||
// then we need to additional scroll the window
|
||||
// by these offsets
|
||||
if (scrollBy) {
|
||||
this.$iframe.prop('contentWindow').scrollBy(scrollBy.x, scrollBy.y)
|
||||
}
|
||||
}
|
||||
|
||||
$el.each((__, element) => {
|
||||
const $_el = $(element)
|
||||
|
||||
// bail if our el no longer exists in the parent body
|
||||
if (!$.contains(body, element)) return
|
||||
|
||||
// switch to using outerWidth + outerHeight
|
||||
// because we want to highlight our element even
|
||||
// if it only has margin and zero content height / width
|
||||
const dimensions = dom.getOuterSize($_el)
|
||||
|
||||
// dont show anything if our element displaces nothing
|
||||
if (dimensions.width === 0 || dimensions.height === 0 || $_el.css('display') === 'none') {
|
||||
return
|
||||
}
|
||||
|
||||
dom.addElementBoxModelLayers($_el, $body).attr('data-highlight-el', true)
|
||||
})
|
||||
|
||||
if (coords) {
|
||||
requestAnimationFrame(() => {
|
||||
dom.addHitBoxLayer(coords, $body).attr('data-highlight-hitbox', true)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
removeHighlights = () => {
|
||||
this._contents() && this._contents().find('.__cypress-highlight').remove()
|
||||
}
|
||||
|
||||
toggleSelectorPlayground = (isEnabled) => {
|
||||
const $body = this._body()
|
||||
|
||||
if (!$body) return
|
||||
|
||||
if (isEnabled) {
|
||||
$body.on('mouseenter', this._resetShowHighlight)
|
||||
$body.on('mousemove', this._onSelectorMouseMove)
|
||||
$body.on('mouseleave', this._clearHighlight)
|
||||
} else {
|
||||
$body.off('mouseenter', this._resetShowHighlight)
|
||||
$body.off('mousemove', this._onSelectorMouseMove)
|
||||
$body.off('mouseleave', this._clearHighlight)
|
||||
if (this._highlightedEl) {
|
||||
this._clearHighlight()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_resetShowHighlight = () => {
|
||||
selectorPlaygroundModel.setShowingHighlight(false)
|
||||
}
|
||||
|
||||
_onSelectorMouseMove = (e) => {
|
||||
const $body = this._body()
|
||||
|
||||
if (!$body) return
|
||||
|
||||
let el = e.target
|
||||
let $el = $(el)
|
||||
|
||||
const $ancestorHighlight = $el.closest('.__cypress-selector-playground')
|
||||
|
||||
if ($ancestorHighlight.length) {
|
||||
$el = $ancestorHighlight
|
||||
}
|
||||
|
||||
if ($ancestorHighlight.length || $el.hasClass('__cypress-selector-playground')) {
|
||||
const $highlight = $el
|
||||
|
||||
$highlight.css('display', 'none')
|
||||
el = this._document().elementFromPoint(e.clientX, e.clientY)
|
||||
$el = $(el)
|
||||
$highlight.css('display', 'block')
|
||||
}
|
||||
|
||||
if (this._highlightedEl === el) return
|
||||
|
||||
this._highlightedEl = el
|
||||
|
||||
const Cypress = eventManager.getCypress()
|
||||
|
||||
const selector = Cypress.SelectorPlayground.getSelector($el)
|
||||
|
||||
dom.addOrUpdateSelectorPlaygroundHighlight({
|
||||
$el,
|
||||
selector,
|
||||
$body,
|
||||
showTooltip: true,
|
||||
onClick: () => {
|
||||
selectorPlaygroundModel.setNumElements(1)
|
||||
selectorPlaygroundModel.resetMethod()
|
||||
selectorPlaygroundModel.setSelector(selector)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
_clearHighlight = () => {
|
||||
const $body = this._body()
|
||||
|
||||
if (!$body) return
|
||||
|
||||
dom.addOrUpdateSelectorPlaygroundHighlight({ $el: null, $body })
|
||||
if (this._highlightedEl) {
|
||||
this._highlightedEl = null
|
||||
}
|
||||
}
|
||||
|
||||
toggleSelectorHighlight (isShowingHighlight) {
|
||||
if (!isShowingHighlight) {
|
||||
this._clearHighlight()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const Cypress = eventManager.getCypress()
|
||||
|
||||
const $el = this.getElements(Cypress.dom)
|
||||
|
||||
selectorPlaygroundModel.setValidity(!!$el)
|
||||
|
||||
if ($el) {
|
||||
selectorPlaygroundModel.setNumElements($el.length)
|
||||
|
||||
if ($el.length) {
|
||||
dom.scrollIntoView(this._window(), $el[0])
|
||||
}
|
||||
}
|
||||
|
||||
dom.addOrUpdateSelectorPlaygroundHighlight({
|
||||
$el: $el && $el.length ? $el : null,
|
||||
selector: selectorPlaygroundModel.selector,
|
||||
$body: this._body(),
|
||||
showTooltip: false,
|
||||
})
|
||||
}
|
||||
|
||||
getElements (cypressDom) {
|
||||
const { selector, method } = selectorPlaygroundModel
|
||||
const $contents = this._contents()
|
||||
|
||||
if (!$contents || !selector) return
|
||||
|
||||
return dom.getElementsForSelector({
|
||||
method,
|
||||
selector,
|
||||
cypressDom,
|
||||
$root: $contents,
|
||||
})
|
||||
}
|
||||
|
||||
printSelectorElementsToConsole () {
|
||||
logger.clearLog()
|
||||
|
||||
const Cypress = eventManager.getCypress()
|
||||
|
||||
const $el = this.getElements(Cypress.dom)
|
||||
|
||||
const command = `cy.${selectorPlaygroundModel.method}('${selectorPlaygroundModel.selector}')`
|
||||
|
||||
if (!$el) {
|
||||
return logger.logFormatted({
|
||||
Command: command,
|
||||
Yielded: 'Nothing',
|
||||
})
|
||||
}
|
||||
|
||||
logger.logFormatted({
|
||||
Command: command,
|
||||
Elements: $el.length,
|
||||
Yielded: Cypress.dom.getElements($el),
|
||||
})
|
||||
}
|
||||
|
||||
beforeScreenshot = (config) => {
|
||||
// could fail if iframe is cross-origin, so fail gracefully
|
||||
try {
|
||||
if (config.disableTimersAndAnimations) {
|
||||
dom.addCssAnimationDisabler(this._body())
|
||||
}
|
||||
|
||||
_.each(config.blackout, (selector) => {
|
||||
dom.addBlackout(this._body(), selector)
|
||||
})
|
||||
} catch (err) {
|
||||
/* eslint-disable no-console */
|
||||
console.error('Failed to modify app dom:')
|
||||
console.error(err)
|
||||
/* eslint-disable no-console */
|
||||
}
|
||||
}
|
||||
|
||||
afterScreenshot = (config) => {
|
||||
// could fail if iframe is cross-origin, so fail gracefully
|
||||
try {
|
||||
if (config.disableTimersAndAnimations) {
|
||||
dom.removeCssAnimationDisabler(this._body())
|
||||
}
|
||||
|
||||
dom.removeBlackouts(this._body())
|
||||
} catch (err) {
|
||||
/* eslint-disable no-console */
|
||||
console.error('Failed to modify app dom:')
|
||||
console.error(err)
|
||||
/* eslint-disable no-console */
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
import _ from 'lodash'
|
||||
import { action } from 'mobx'
|
||||
|
||||
import eventManager from '../lib/event-manager'
|
||||
import selectorPlaygroundModel from '../selector-playground/selector-playground-model'
|
||||
|
||||
export default class IframeModel {
|
||||
constructor ({ state, detachDom, restoreDom, highlightEl, snapshotControls }) {
|
||||
this.state = state
|
||||
this.detachDom = detachDom
|
||||
this.restoreDom = restoreDom
|
||||
this.highlightEl = highlightEl
|
||||
this.snapshotControls = snapshotControls
|
||||
|
||||
this._reset()
|
||||
}
|
||||
|
||||
listen () {
|
||||
eventManager.on('run:start', action('run:start', this._beforeRun))
|
||||
eventManager.on('run:end', action('run:end', this._afterRun))
|
||||
|
||||
eventManager.on('viewport:changed', action('viewport:changed', this._updateViewport))
|
||||
eventManager.on('config', action('config', (config) => {
|
||||
this._updateViewport(_.map(config, 'viewportHeight', 'viewportWidth'))
|
||||
}))
|
||||
|
||||
eventManager.on('url:changed', action('url:changed', this._updateUrl))
|
||||
eventManager.on('page:loading', action('page:loading', this._updateLoadingUrl))
|
||||
|
||||
eventManager.on('show:snapshot', action('show:snapshot', this._setSnapshots))
|
||||
eventManager.on('hide:snapshot', action('hide:snapshot', this._clearSnapshots))
|
||||
|
||||
eventManager.on('pin:snapshot', action('pin:snapshot', this._pinSnapshot))
|
||||
eventManager.on('unpin:snapshot', action('unpin:snapshot', this._unpinSnapshot))
|
||||
}
|
||||
|
||||
_beforeRun = () => {
|
||||
this.state.isLoading = false
|
||||
this.state.isRunning = true
|
||||
this.state.resetUrl()
|
||||
selectorPlaygroundModel.setEnabled(false)
|
||||
this._reset()
|
||||
this._clearMessage()
|
||||
}
|
||||
|
||||
_afterRun = () => {
|
||||
this.state.isRunning = false
|
||||
}
|
||||
|
||||
_updateViewport = ({ viewportWidth, viewportHeight }, cb) => {
|
||||
this.state.updateAutViewportDimensions({ viewportWidth, viewportHeight })
|
||||
|
||||
if (cb) {
|
||||
this.state.setCallbackAfterUpdate(cb)
|
||||
}
|
||||
}
|
||||
|
||||
_updateUrl = (url) => {
|
||||
this.state.url = url
|
||||
}
|
||||
|
||||
_updateLoadingUrl = (isLoadingUrl) => {
|
||||
this.state.isLoadingUrl = isLoadingUrl
|
||||
}
|
||||
|
||||
_clearMessage = () => {
|
||||
this.state.clearMessage()
|
||||
}
|
||||
|
||||
_setSnapshots = (snapshotProps) => {
|
||||
if (this.isSnapshotPinned) return
|
||||
|
||||
if (this.state.isRunning) {
|
||||
return this._testsRunningError()
|
||||
}
|
||||
|
||||
const { snapshots } = snapshotProps
|
||||
|
||||
if (!snapshots || !snapshots.length) {
|
||||
this._clearSnapshots()
|
||||
this._setMissingSnapshotMessage()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
this.state.highlightUrl = true
|
||||
|
||||
if (!this.originalState) {
|
||||
this._storeOriginalState()
|
||||
}
|
||||
|
||||
this.detachedId = snapshotProps.id
|
||||
|
||||
this._updateViewport(snapshotProps)
|
||||
this._updateUrl(snapshotProps.url)
|
||||
|
||||
clearInterval(this.intervalId)
|
||||
|
||||
const revert = action('revert:snapshot', this._showSnapshot)
|
||||
|
||||
if (snapshots.length > 1) {
|
||||
let i = 0
|
||||
|
||||
this.intervalId = setInterval(() => {
|
||||
if (this.isSnapshotPinned) return
|
||||
|
||||
i += 1
|
||||
if (!snapshots[i]) {
|
||||
i = 0
|
||||
}
|
||||
|
||||
revert(snapshots[i], snapshotProps)
|
||||
}, 800)
|
||||
}
|
||||
|
||||
revert(snapshots[0], snapshotProps)
|
||||
}
|
||||
|
||||
_showSnapshot = (snapshot, snapshotProps) => {
|
||||
this.state.messageTitle = 'DOM Snapshot'
|
||||
this.state.messageDescription = snapshot.name
|
||||
this.state.messageType = ''
|
||||
|
||||
this._restoreDom(snapshot, snapshotProps)
|
||||
}
|
||||
|
||||
_restoreDom (snapshot, snapshotProps) {
|
||||
this.restoreDom(snapshot)
|
||||
|
||||
if (snapshotProps.$el) {
|
||||
this.highlightEl(snapshot, snapshotProps)
|
||||
}
|
||||
}
|
||||
|
||||
_clearSnapshots = () => {
|
||||
if (this.isSnapshotPinned) return
|
||||
|
||||
clearInterval(this.intervalId)
|
||||
|
||||
this.state.highlightUrl = false
|
||||
|
||||
if (!this.originalState || !this.originalState.body) {
|
||||
return this._clearMessage()
|
||||
}
|
||||
|
||||
const previousDetachedId = this.detachedId
|
||||
|
||||
// process on next tick so we don't restore the dom if we're
|
||||
// about to receive another 'show:snapshot' event, else that would
|
||||
// be a huge waste
|
||||
setTimeout(action('clear:snapshots:next:tick', () => {
|
||||
// we want to only restore the dom if we haven't received
|
||||
// another snapshot by the time this function runs
|
||||
if (previousDetachedId !== this.detachedId) return
|
||||
|
||||
this._updateViewport(this.originalState)
|
||||
this._updateUrl(this.originalState.url)
|
||||
this.restoreDom(this.originalState.snapshot)
|
||||
this._clearMessage()
|
||||
|
||||
this.originalState = null
|
||||
this.detachedId = null
|
||||
}))
|
||||
}
|
||||
|
||||
_pinSnapshot = (snapshotProps) => {
|
||||
const { snapshots } = snapshotProps
|
||||
|
||||
if (!snapshots || !snapshots.length) {
|
||||
eventManager.snapshotUnpinned()
|
||||
this._setMissingSnapshotMessage()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
clearInterval(this.intervalId)
|
||||
|
||||
this.isSnapshotPinned = true
|
||||
|
||||
this.state.snapshot.showingHighlights = true
|
||||
this.state.snapshot.stateIndex = 0
|
||||
|
||||
this.state.messageTitle = 'DOM Snapshot'
|
||||
this.state.messageDescription = 'pinned'
|
||||
this.state.messageType = 'info'
|
||||
this.state.messageControls = this.snapshotControls(snapshotProps)
|
||||
|
||||
this._restoreDom(snapshots[0], snapshotProps)
|
||||
}
|
||||
|
||||
_setMissingSnapshotMessage () {
|
||||
this.state.messageTitle = 'The snapshot is missing. Displaying current state of the DOM.'
|
||||
this.state.messageDescription = ''
|
||||
this.state.messageType = 'warning'
|
||||
}
|
||||
|
||||
_unpinSnapshot = () => {
|
||||
this.isSnapshotPinned = false
|
||||
this.state.messageTitle = 'DOM Snapshot'
|
||||
this.state.messageDescription = ''
|
||||
this.state.messageControls = null
|
||||
}
|
||||
|
||||
_testsRunningError () {
|
||||
this.state.messageTitle = 'Cannot show Snapshot while tests are running'
|
||||
this.state.messageType = 'warning'
|
||||
}
|
||||
|
||||
_storeOriginalState () {
|
||||
const finalSnapshot = this.detachDom()
|
||||
|
||||
if (!finalSnapshot) return
|
||||
|
||||
const { body, htmlAttrs } = finalSnapshot
|
||||
|
||||
this.originalState = {
|
||||
body,
|
||||
htmlAttrs,
|
||||
snapshot: finalSnapshot,
|
||||
url: this.state.url,
|
||||
viewportWidth: this.state.viewportWidth,
|
||||
viewportHeight: this.state.viewportHeight,
|
||||
}
|
||||
}
|
||||
|
||||
_reset () {
|
||||
this.detachedId = null
|
||||
this.intervalId = null
|
||||
this.originalState = null
|
||||
this.isSnapshotPinned = false
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,18 @@ import cs from 'classnames'
|
||||
import { action, when, autorun } from 'mobx'
|
||||
import React, { useRef, useEffect } from 'react'
|
||||
import { default as $Cypress } from '@packages/driver'
|
||||
import {
|
||||
SnapshotControls,
|
||||
ScriptError,
|
||||
namedObserver,
|
||||
IframeModel,
|
||||
selectorPlaygroundModel,
|
||||
AutIframe,
|
||||
eventManager as EventManager,
|
||||
} from '@packages/runner-shared'
|
||||
|
||||
import State from '../../src/lib/state'
|
||||
import AutIframe from './aut-iframe'
|
||||
import { ScriptError } from '../errors/script-error'
|
||||
import SnapshotControls from './snapshot-controls'
|
||||
import IframeModel from './iframe-model'
|
||||
import selectorPlaygroundModel from '../selector-playground/selector-playground-model'
|
||||
import styles from '../app/RunnerCt.module.scss'
|
||||
import eventManager from '../lib/event-manager'
|
||||
import { namedObserver } from '../lib/mobx'
|
||||
import './iframes.scss'
|
||||
|
||||
export function getSpecUrl ({ namespace, spec }, prefix = '') {
|
||||
@@ -20,7 +22,7 @@ export function getSpecUrl ({ namespace, spec }, prefix = '') {
|
||||
|
||||
interface IFramesProps {
|
||||
state: State
|
||||
eventManager: typeof eventManager
|
||||
eventManager: typeof EventManager
|
||||
config: Cypress.RuntimeConfigOptions
|
||||
}
|
||||
|
||||
@@ -168,7 +170,7 @@ export const Iframes = namedObserver('Iframes', ({
|
||||
state.callbackAfterUpdate?.()
|
||||
})
|
||||
|
||||
const { viewportHeight, viewportWidth, scriptError, scale, screenshotting } = state
|
||||
const { height, width, scriptError, scale, screenshotting } = state
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -188,8 +190,8 @@ export const Iframes = namedObserver('Iframes', ({
|
||||
})
|
||||
}
|
||||
style={{
|
||||
height: viewportHeight,
|
||||
width: viewportWidth,
|
||||
height,
|
||||
width,
|
||||
transform: `scale(${screenshotting ? 1 : scale})`,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import cs from 'classnames'
|
||||
import _ from 'lodash'
|
||||
import { action } from 'mobx'
|
||||
import { observer } from 'mobx-react'
|
||||
import React, { Component } from 'react'
|
||||
import Tooltip from '@cypress/react-tooltip'
|
||||
|
||||
@observer
|
||||
class SnapshotControls extends Component {
|
||||
render () {
|
||||
return (
|
||||
<span
|
||||
className={cs('snapshot-controls', {
|
||||
'showing-selection': this.props.state.snapshot.showingHighlights,
|
||||
})}
|
||||
>
|
||||
{this._selectionToggle()}
|
||||
{this._states()}
|
||||
<Tooltip title='Unpin' className='cy-tooltip'>
|
||||
<button className='unpin' onClick={this._unpin}>
|
||||
<i className='fas fa-times' />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
_selectionToggle () {
|
||||
if (!this.props.snapshotProps.$el) return null
|
||||
|
||||
const showingHighlights = this.props.state.snapshot.showingHighlights
|
||||
|
||||
return (
|
||||
<Tooltip title={`${showingHighlights ? 'Hide' : 'Show'} Highlights`} className='cy-tooltip'>
|
||||
<button className='toggle-selection' onClick={this._toggleHighlights}>
|
||||
<i className='far fa-object-group' />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
_states () {
|
||||
const { snapshots } = this.props.snapshotProps
|
||||
|
||||
if (snapshots.length < 2) return null
|
||||
|
||||
return (
|
||||
<span className='snapshot-state-picker'>
|
||||
{_.map(snapshots, (snapshot, index) => (
|
||||
<button
|
||||
key={snapshot.name ?? index}
|
||||
className={cs({
|
||||
'state-is-selected': this.props.state.snapshot.stateIndex === index,
|
||||
})}
|
||||
href="#"
|
||||
onClick={this._changeState(index)}
|
||||
>
|
||||
{snapshot.name ?? index + 1}
|
||||
</button>
|
||||
))}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
_unpin = () => {
|
||||
this.props.eventManager.snapshotUnpinned()
|
||||
}
|
||||
|
||||
@action _toggleHighlights = () => {
|
||||
this.props.onToggleHighlights(this.props.snapshotProps)
|
||||
}
|
||||
|
||||
_changeState = (index) => action('change:snapshot:state', () => {
|
||||
this.props.onStateChange(this.props.snapshotProps, index)
|
||||
})
|
||||
}
|
||||
|
||||
export default SnapshotControls
|
||||
@@ -1,33 +0,0 @@
|
||||
import React from 'react'
|
||||
import { isUndefined } from 'lodash'
|
||||
|
||||
const configFileFormatted = (configFile) => {
|
||||
if (configFile === false) {
|
||||
return (
|
||||
<>
|
||||
{/* eslint-disable-next-line react/jsx-one-expression-per-line */ }
|
||||
<code>cypress.json</code> file (currently disabled by <code>--config-file false</code>)
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (isUndefined(configFile) || configFile === 'cypress.json') {
|
||||
return (
|
||||
<>
|
||||
{/* eslint-disable-next-line react/jsx-one-expression-per-line */ }
|
||||
<code>cypress.json</code> file
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* eslint-disable-next-line react/jsx-one-expression-per-line */ }
|
||||
custom config file <code>{configFile}</code>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
configFileFormatted,
|
||||
}
|
||||
@@ -1,457 +0,0 @@
|
||||
import _ from 'lodash'
|
||||
|
||||
import { $ } from '@packages/driver'
|
||||
import selectorPlaygroundHighlight from '@packages/runner/src/selector-playground/highlight'
|
||||
// The '!' tells webpack to disable normal loaders, and keep loaders with `enforce: 'pre'` and `enforce: 'post'`
|
||||
// This disables the CSSExtractWebpackPlugin and allows us to get the CSS as a raw string instead of saving it to a separate file.
|
||||
import selectorPlaygroundCSS from '!@packages/runner/src/selector-playground/selector-playground.scss'
|
||||
|
||||
const styles = (styleString) => {
|
||||
return styleString.replace(/\s*\n\s*/g, '')
|
||||
}
|
||||
|
||||
const resetStyles = `
|
||||
border: none !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
`
|
||||
|
||||
function addHitBoxLayer (coords, $body) {
|
||||
$body = $body || $('body')
|
||||
|
||||
const height = 10
|
||||
const width = 10
|
||||
|
||||
const dotHeight = 4
|
||||
const dotWidth = 4
|
||||
|
||||
const top = coords.y - height / 2
|
||||
const left = coords.x - width / 2
|
||||
|
||||
const dotTop = height / 2 - dotHeight / 2
|
||||
const dotLeft = width / 2 - dotWidth / 2
|
||||
|
||||
const boxStyles = styles(`
|
||||
${resetStyles}
|
||||
position: absolute;
|
||||
top: ${top}px;
|
||||
left: ${left}px;
|
||||
width: ${width}px;
|
||||
height: ${height}px;
|
||||
background-color: red;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 5px #333;
|
||||
z-index: 2147483647;
|
||||
`)
|
||||
|
||||
const $box = $(`<div class="__cypress-highlight" style="${boxStyles}" />`)
|
||||
|
||||
const wrapper = $(`<div style="${styles(resetStyles)} position: relative" />`).appendTo($box)
|
||||
|
||||
const dotStyles = styles(`
|
||||
${resetStyles}
|
||||
position: absolute;
|
||||
top: ${dotTop}px;
|
||||
left: ${dotLeft}px;
|
||||
height: ${dotHeight}px;
|
||||
width: ${dotWidth}px;
|
||||
height: ${dotHeight}px;
|
||||
background-color: pink;
|
||||
border-radius: 5px;
|
||||
`)
|
||||
|
||||
$(`<div style="${dotStyles}">`).appendTo(wrapper)
|
||||
|
||||
return $box.appendTo($body)
|
||||
}
|
||||
|
||||
function addElementBoxModelLayers ($el, $body) {
|
||||
$body = $body || $('body')
|
||||
|
||||
const dimensions = getElementDimensions($el)
|
||||
|
||||
const $container = $('<div class="__cypress-highlight">')
|
||||
.css({
|
||||
opacity: 0.7,
|
||||
position: 'absolute',
|
||||
zIndex: 2147483647,
|
||||
})
|
||||
|
||||
const layers = {
|
||||
Content: '#9FC4E7',
|
||||
Padding: '#C1CD89',
|
||||
Border: '#FCDB9A',
|
||||
Margin: '#F9CC9D',
|
||||
}
|
||||
|
||||
// create the margin / bottom / padding layers
|
||||
_.each(layers, (color, attr) => {
|
||||
let obj
|
||||
|
||||
switch (attr) {
|
||||
case 'Content':
|
||||
// rearrange the contents offset so
|
||||
// its inside of our border + padding
|
||||
obj = {
|
||||
width: dimensions.width,
|
||||
height: dimensions.height,
|
||||
top: dimensions.offset.top + dimensions.borderTop + dimensions.paddingTop,
|
||||
left: dimensions.offset.left + dimensions.borderLeft + dimensions.paddingLeft,
|
||||
}
|
||||
|
||||
break
|
||||
default:
|
||||
obj = {
|
||||
width: getDimensionsFor(dimensions, attr, 'width'),
|
||||
height: getDimensionsFor(dimensions, attr, 'height'),
|
||||
top: dimensions.offset.top,
|
||||
left: dimensions.offset.left,
|
||||
}
|
||||
}
|
||||
|
||||
// if attr is margin then we need to additional
|
||||
// subtract what the actual marginTop + marginLeft
|
||||
// values are, since offset disregards margin completely
|
||||
if (attr === 'Margin') {
|
||||
obj.top -= dimensions.marginTop
|
||||
obj.left -= dimensions.marginLeft
|
||||
}
|
||||
|
||||
if (attr === 'Padding') {
|
||||
obj.top += dimensions.borderTop
|
||||
obj.left += dimensions.borderLeft
|
||||
}
|
||||
|
||||
// bail if the dimensions of this layer match the previous one
|
||||
// so we dont create unnecessary layers
|
||||
if (dimensionsMatchPreviousLayer(obj, $container)) return
|
||||
|
||||
return createLayer($el, attr, color, $container, obj)
|
||||
})
|
||||
|
||||
$container.appendTo($body)
|
||||
|
||||
$container.children().each((index, el) => {
|
||||
const $el = $(el)
|
||||
const top = $el.data('top')
|
||||
const left = $el.data('left')
|
||||
|
||||
// dont ask... for some reason we
|
||||
// have to run offset twice!
|
||||
_.times(2, () => {
|
||||
return $el.offset({ top, left })
|
||||
})
|
||||
})
|
||||
|
||||
return $container
|
||||
}
|
||||
|
||||
function getOrCreateSelectorHelperDom ($body) {
|
||||
let $container = $body.find('.__cypress-selector-playground')
|
||||
|
||||
if ($container.length) {
|
||||
const shadowRoot = $container[0].shadowRoot
|
||||
|
||||
return {
|
||||
$container,
|
||||
shadowRoot,
|
||||
$reactContainer: $(shadowRoot).find('.react-container'),
|
||||
}
|
||||
}
|
||||
|
||||
$container = $('<div />')
|
||||
.addClass('__cypress-selector-playground')
|
||||
.css({ position: 'static' })
|
||||
.appendTo($body)
|
||||
|
||||
const shadowRoot = $container[0].attachShadow({ mode: 'open' })
|
||||
|
||||
const $reactContainer = $('<div />')
|
||||
.addClass('react-container')
|
||||
.appendTo(shadowRoot)
|
||||
|
||||
$('<style />', { html: selectorPlaygroundCSS.toString() }).prependTo(shadowRoot)
|
||||
|
||||
return { $container, shadowRoot, $reactContainer }
|
||||
}
|
||||
|
||||
function addOrUpdateSelectorPlaygroundHighlight ({ $el, $body, selector, showTooltip, onClick }) {
|
||||
const { $container, shadowRoot, $reactContainer } = getOrCreateSelectorHelperDom($body)
|
||||
|
||||
if (!$el) {
|
||||
selectorPlaygroundHighlight.unmount($reactContainer[0])
|
||||
$reactContainer.off('click')
|
||||
$container.remove()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const borderSize = 2
|
||||
|
||||
const styles = $el.map((__, el) => {
|
||||
const $el = $(el)
|
||||
const offset = $el.offset()
|
||||
|
||||
return {
|
||||
position: 'absolute',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
width: $el.outerWidth(),
|
||||
height: $el.outerHeight(),
|
||||
top: offset.top - borderSize,
|
||||
left: offset.left - borderSize,
|
||||
transform: $el.css('transform'),
|
||||
zIndex: getZIndex($el),
|
||||
}
|
||||
}).get()
|
||||
|
||||
if ($el.length === 1) {
|
||||
$reactContainer
|
||||
.off('click')
|
||||
.on('click', onClick)
|
||||
}
|
||||
|
||||
selectorPlaygroundHighlight.render($reactContainer[0], {
|
||||
selector,
|
||||
appendTo: shadowRoot,
|
||||
showTooltip,
|
||||
styles,
|
||||
})
|
||||
}
|
||||
|
||||
function createLayer ($el, attr, color, container, dimensions) {
|
||||
const transform = $el.css('transform')
|
||||
|
||||
const css = {
|
||||
transform,
|
||||
width: dimensions.width,
|
||||
height: dimensions.height,
|
||||
position: 'absolute',
|
||||
zIndex: getZIndex($el),
|
||||
backgroundColor: color,
|
||||
}
|
||||
|
||||
return $('<div>')
|
||||
.css(css)
|
||||
.attr('data-top', dimensions.top)
|
||||
.attr('data-left', dimensions.left)
|
||||
.attr('data-layer', attr)
|
||||
.prependTo(container)
|
||||
}
|
||||
|
||||
function dimensionsMatchPreviousLayer (obj, container) {
|
||||
// since we're prepending to the container that
|
||||
// means the previous layer is actually the first child element
|
||||
const previousLayer = container.children().first().get(0)
|
||||
|
||||
// bail if there is no previous layer
|
||||
if (!previousLayer) {
|
||||
return
|
||||
}
|
||||
|
||||
return obj.width === previousLayer.offsetWidth &&
|
||||
obj.height === previousLayer.offsetHeight
|
||||
}
|
||||
|
||||
function getDimensionsFor (dimensions, attr, dimension) {
|
||||
return dimensions[`${dimension}With${attr}`]
|
||||
}
|
||||
|
||||
function getZIndex (el) {
|
||||
if (/^(auto|0)$/.test(el.css('zIndex'))) {
|
||||
return 2147483647
|
||||
}
|
||||
|
||||
return _.toNumber(el.css('zIndex'))
|
||||
}
|
||||
|
||||
function getElementDimensions ($el) {
|
||||
const el = $el.get(0)
|
||||
|
||||
const { offsetHeight, offsetWidth } = el
|
||||
|
||||
const box = {
|
||||
offset: $el.offset(), // offset disregards margin but takes into account border + padding
|
||||
// dont use jquery here for width/height because it uses getBoundingClientRect() which returns scaled values.
|
||||
// TODO: switch back to using jquery when upgrading to jquery 3.4+
|
||||
paddingTop: getPadding($el, 'top'),
|
||||
paddingRight: getPadding($el, 'right'),
|
||||
paddingBottom: getPadding($el, 'bottom'),
|
||||
paddingLeft: getPadding($el, 'left'),
|
||||
borderTop: getBorder($el, 'top'),
|
||||
borderRight: getBorder($el, 'right'),
|
||||
borderBottom: getBorder($el, 'bottom'),
|
||||
borderLeft: getBorder($el, 'left'),
|
||||
marginTop: getMargin($el, 'top'),
|
||||
marginRight: getMargin($el, 'right'),
|
||||
marginBottom: getMargin($el, 'bottom'),
|
||||
marginLeft: getMargin($el, 'left'),
|
||||
}
|
||||
|
||||
// NOTE: offsetWidth/height always give us content + padding + border, so subtract them
|
||||
// to get the true "clientHeight" and "clientWidth".
|
||||
// we CANNOT just use "clientHeight" and "clientWidth" because those always return 0
|
||||
// for inline elements >_<
|
||||
//
|
||||
box.width = offsetWidth - (box.paddingLeft + box.paddingRight + box.borderLeft + box.borderRight)
|
||||
box.height = offsetHeight - (box.paddingTop + box.paddingBottom + box.borderTop + box.borderBottom)
|
||||
|
||||
// innerHeight: Get the current computed height for the first
|
||||
// element in the set of matched elements, including padding but not border.
|
||||
|
||||
// outerHeight: Get the current computed height for the first
|
||||
// element in the set of matched elements, including padding, border,
|
||||
// and optionally margin. Returns a number (without 'px') representation
|
||||
// of the value or null if called on an empty set of elements.
|
||||
box.heightWithPadding = box.height + box.paddingTop + box.paddingBottom
|
||||
|
||||
box.heightWithBorder = box.heightWithPadding + box.borderTop + box.borderBottom
|
||||
|
||||
box.heightWithMargin = box.heightWithBorder + box.marginTop + box.marginBottom
|
||||
|
||||
box.widthWithPadding = box.width + box.paddingLeft + box.paddingRight
|
||||
|
||||
box.widthWithBorder = box.widthWithPadding + box.borderLeft + box.borderRight
|
||||
|
||||
box.widthWithMargin = box.widthWithBorder + box.marginLeft + box.marginRight
|
||||
|
||||
return box
|
||||
}
|
||||
|
||||
function getNumAttrValue ($el, attr) {
|
||||
// nuke anything thats not a number or a negative symbol
|
||||
const num = _.toNumber($el.css(attr).replace(/[^0-9\.-]+/, ''))
|
||||
|
||||
if (!_.isFinite(num)) {
|
||||
throw new Error('Element attr did not return a valid number')
|
||||
}
|
||||
|
||||
return num
|
||||
}
|
||||
|
||||
function getPadding ($el, dir) {
|
||||
return getNumAttrValue($el, `padding-${dir}`)
|
||||
}
|
||||
|
||||
function getBorder ($el, dir) {
|
||||
return getNumAttrValue($el, `border-${dir}-width`)
|
||||
}
|
||||
|
||||
function getMargin ($el, dir) {
|
||||
return getNumAttrValue($el, `margin-${dir}`)
|
||||
}
|
||||
|
||||
function getOuterSize ($el) {
|
||||
return {
|
||||
width: $el.outerWidth(true),
|
||||
height: $el.outerHeight(true),
|
||||
}
|
||||
}
|
||||
|
||||
function isInViewport (win, el) {
|
||||
let rect = el.getBoundingClientRect()
|
||||
|
||||
return (
|
||||
rect.top >= 0 &&
|
||||
rect.left >= 0 &&
|
||||
rect.bottom <= win.innerHeight &&
|
||||
rect.right <= win.innerWidth
|
||||
)
|
||||
}
|
||||
|
||||
function scrollIntoView (win, el) {
|
||||
if (!el || isInViewport(win, el)) return
|
||||
|
||||
el.scrollIntoView()
|
||||
}
|
||||
|
||||
const sizzleRe = /sizzle/i
|
||||
|
||||
function getElementsForSelector ({ $root, selector, method, cypressDom }) {
|
||||
let $el = null
|
||||
|
||||
try {
|
||||
if (method === 'contains') {
|
||||
$el = $root.find(cypressDom.getContainsSelector(selector))
|
||||
if ($el.length) {
|
||||
$el = cypressDom.getFirstDeepestElement($el)
|
||||
}
|
||||
} else {
|
||||
$el = $root.find(selector)
|
||||
}
|
||||
} catch (err) {
|
||||
// if not a sizzle error, ignore it and let $el be null
|
||||
if (!sizzleRe.test(err.stack)) throw err
|
||||
}
|
||||
|
||||
return $el
|
||||
}
|
||||
|
||||
function addCssAnimationDisabler ($body) {
|
||||
$(`
|
||||
<style id="__cypress-animation-disabler">
|
||||
*, *:before, *:after {
|
||||
transition-property: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
</style>
|
||||
`).appendTo($body)
|
||||
}
|
||||
|
||||
function removeCssAnimationDisabler ($body) {
|
||||
$body.find('#__cypress-animation-disabler').remove()
|
||||
}
|
||||
|
||||
function addBlackoutForElement ($body, $el) {
|
||||
const dimensions = getElementDimensions($el)
|
||||
const width = dimensions.widthWithBorder
|
||||
const height = dimensions.heightWithBorder
|
||||
const top = dimensions.offset.top
|
||||
const left = dimensions.offset.left
|
||||
|
||||
const style = styles(`
|
||||
${resetStyles}
|
||||
position: absolute;
|
||||
top: ${top}px;
|
||||
left: ${left}px;
|
||||
width: ${width}px;
|
||||
height: ${height}px;
|
||||
background-color: black;
|
||||
z-index: 2147483647;
|
||||
`)
|
||||
|
||||
$(`<div class="__cypress-blackout" style="${style}">`).appendTo($body)
|
||||
}
|
||||
|
||||
function addBlackout ($body, selector) {
|
||||
let $el
|
||||
|
||||
try {
|
||||
$el = $body.find(selector)
|
||||
if (!$el.length) return
|
||||
} catch (err) {
|
||||
// if it's an invalid selector, just ignore it
|
||||
return
|
||||
}
|
||||
|
||||
$el.each(function () {
|
||||
addBlackoutForElement($body, $(this))
|
||||
})
|
||||
}
|
||||
|
||||
function removeBlackouts ($body) {
|
||||
$body.find('.__cypress-blackout').remove()
|
||||
}
|
||||
|
||||
export default {
|
||||
addBlackout,
|
||||
removeBlackouts,
|
||||
addElementBoxModelLayers,
|
||||
addHitBoxLayer,
|
||||
addOrUpdateSelectorPlaygroundHighlight,
|
||||
addCssAnimationDisabler,
|
||||
removeCssAnimationDisabler,
|
||||
getElementsForSelector,
|
||||
getOuterSize,
|
||||
scrollIntoView,
|
||||
}
|
||||
@@ -1,491 +0,0 @@
|
||||
import _ from 'lodash'
|
||||
import { EventEmitter } from 'events'
|
||||
import Promise from 'bluebird'
|
||||
import { action } from 'mobx'
|
||||
|
||||
import { client } from '@packages/socket'
|
||||
|
||||
import automation from './automation'
|
||||
import logger from './logger'
|
||||
|
||||
import $Cypress, { $ } from '@packages/driver'
|
||||
|
||||
const ws = client.connect({
|
||||
path: '/__socket.io',
|
||||
transports: ['websocket'],
|
||||
})
|
||||
|
||||
ws.on('connect', () => {
|
||||
ws.emit('runner:connected')
|
||||
})
|
||||
|
||||
const driverToReporterEvents = 'paused before:firefox:force:gc after:firefox:force:gc'.split(' ')
|
||||
const driverToLocalAndReporterEvents = 'run:start run:end'.split(' ')
|
||||
const driverToSocketEvents = 'backend:request automation:request mocha recorder:frame'.split(' ')
|
||||
const driverTestEvents = 'test:before:run:async test:after:run'.split(' ')
|
||||
const driverToLocalEvents = 'viewport:changed config stop url:changed page:loading visit:failed'.split(' ')
|
||||
const socketRerunEvents = 'runner:restart watched:file:changed'.split(' ')
|
||||
const socketToDriverEvents = 'net:event script:error'.split(' ')
|
||||
|
||||
const localBus = new EventEmitter()
|
||||
const reporterBus = new EventEmitter()
|
||||
|
||||
// NOTE: this is exposed for testing, ideally we should only expose this if a test flag is set
|
||||
window.runnerWs = ws
|
||||
|
||||
// NOTE: this is for testing Cypress-in-Cypress, window.Cypress is undefined here
|
||||
// unless Cypress has been loaded into the AUT frame
|
||||
if (window.Cypress) {
|
||||
window.eventManager = { reporterBus, localBus }
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Cypress.Cypress}
|
||||
*/
|
||||
let Cypress
|
||||
|
||||
const eventManager = {
|
||||
reporterBus,
|
||||
|
||||
getCypress () {
|
||||
return Cypress
|
||||
},
|
||||
|
||||
addGlobalListeners (state, connectionInfo) {
|
||||
const rerun = () => {
|
||||
if (!this) {
|
||||
// if the tests have been reloaded
|
||||
// then nothing to rerun
|
||||
return
|
||||
}
|
||||
|
||||
return this._reRun(state)
|
||||
}
|
||||
|
||||
ws.emit('is:automation:client:connected', connectionInfo, action('automationEnsured', (isConnected) => {
|
||||
state.automation = isConnected ? automation.CONNECTED : automation.MISSING
|
||||
ws.on('automation:disconnected', action('automationDisconnected', () => {
|
||||
state.automation = automation.DISCONNECTED
|
||||
}))
|
||||
}))
|
||||
|
||||
ws.on('change:to:url', (url) => {
|
||||
window.location.href = url
|
||||
})
|
||||
|
||||
ws.on('automation:push:message', (msg, data = {}) => {
|
||||
if (!Cypress) return
|
||||
|
||||
switch (msg) {
|
||||
case 'change:cookie':
|
||||
Cypress.Cookies.log(data.message, data.cookie, data.removed)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
ws.on('component:specs:changed', (specs) => {
|
||||
state.setSpecs(specs)
|
||||
})
|
||||
|
||||
ws.on('dev-server:hmr:error', (error) => {
|
||||
Cypress.stop()
|
||||
localBus.emit('script:error', error)
|
||||
})
|
||||
|
||||
_.each(socketRerunEvents, (event) => {
|
||||
ws.on(event, rerun)
|
||||
})
|
||||
|
||||
_.each(socketToDriverEvents, (event) => {
|
||||
ws.on(event, (...args) => {
|
||||
Cypress.emit(event, ...args)
|
||||
})
|
||||
})
|
||||
|
||||
const logCommand = (logId) => {
|
||||
const consoleProps = Cypress.runner.getConsolePropsForLogById(logId)
|
||||
|
||||
logger.logFormatted(consoleProps)
|
||||
}
|
||||
|
||||
reporterBus.on('runner:console:error', ({ err, commandId }) => {
|
||||
if (!Cypress) return
|
||||
|
||||
if (commandId || err) logger.clearLog()
|
||||
|
||||
if (commandId) logCommand(commandId)
|
||||
|
||||
if (err) logger.logError(err.stack)
|
||||
})
|
||||
|
||||
reporterBus.on('runner:console:log', (logId) => {
|
||||
if (!Cypress) return
|
||||
|
||||
logger.clearLog()
|
||||
logCommand(logId)
|
||||
})
|
||||
|
||||
reporterBus.on('focus:tests', this.focusTests)
|
||||
|
||||
reporterBus.on('get:user:editor', (cb) => {
|
||||
ws.emit('get:user:editor', cb)
|
||||
})
|
||||
|
||||
reporterBus.on('set:user:editor', (editor) => {
|
||||
ws.emit('set:user:editor', editor)
|
||||
})
|
||||
|
||||
reporterBus.on('runner:restart', rerun)
|
||||
|
||||
function sendEventIfSnapshotProps (logId, event) {
|
||||
if (!Cypress) return
|
||||
|
||||
const snapshotProps = Cypress.runner.getSnapshotPropsForLogById(logId)
|
||||
|
||||
if (snapshotProps) {
|
||||
localBus.emit(event, snapshotProps)
|
||||
}
|
||||
}
|
||||
|
||||
reporterBus.on('runner:show:snapshot', (logId) => {
|
||||
sendEventIfSnapshotProps(logId, 'show:snapshot')
|
||||
})
|
||||
|
||||
reporterBus.on('runner:hide:snapshot', this._hideSnapshot.bind(this))
|
||||
|
||||
reporterBus.on('runner:pin:snapshot', (logId) => {
|
||||
sendEventIfSnapshotProps(logId, 'pin:snapshot')
|
||||
})
|
||||
|
||||
reporterBus.on('runner:unpin:snapshot', this._unpinSnapshot.bind(this))
|
||||
|
||||
reporterBus.on('runner:resume', () => {
|
||||
if (!Cypress) return
|
||||
|
||||
Cypress.emit('resume:all')
|
||||
})
|
||||
|
||||
reporterBus.on('runner:next', () => {
|
||||
if (!Cypress) return
|
||||
|
||||
Cypress.emit('resume:next')
|
||||
})
|
||||
|
||||
reporterBus.on('runner:stop', () => {
|
||||
if (!Cypress) return
|
||||
|
||||
Cypress.stop()
|
||||
})
|
||||
|
||||
reporterBus.on('save:state', (state) => {
|
||||
this.saveState(state)
|
||||
})
|
||||
|
||||
reporterBus.on('external:open', (url) => {
|
||||
ws.emit('external:open', url)
|
||||
})
|
||||
|
||||
reporterBus.on('open:file', (url) => {
|
||||
ws.emit('open:file', url)
|
||||
})
|
||||
|
||||
const $window = $(window)
|
||||
|
||||
// when we actually unload then
|
||||
// nuke all of the cookies again
|
||||
// so we clear out unload
|
||||
$window.on('unload', () => {
|
||||
this._clearAllCookies()
|
||||
})
|
||||
|
||||
// when our window triggers beforeunload
|
||||
// we know we've change the URL and we need
|
||||
// to clear our cookies
|
||||
// additionally we set unload to true so
|
||||
// that Cypress knows not to set any more
|
||||
// cookies
|
||||
$window.on('beforeunload', () => {
|
||||
reporterBus.emit('reporter:restart:test:run')
|
||||
|
||||
this._clearAllCookies()
|
||||
this._setUnload()
|
||||
})
|
||||
},
|
||||
|
||||
start (config) {
|
||||
if (config.socketId) {
|
||||
ws.emit('app:connect', config.socketId)
|
||||
}
|
||||
},
|
||||
|
||||
setup (config) {
|
||||
Cypress = this.Cypress = $Cypress.create(config)
|
||||
|
||||
// expose Cypress globally
|
||||
// since CT AUT shares the window with the spec, we don't want to overwrite
|
||||
// our spec Cypress instance with the component's Cypress instance
|
||||
if (window.top === window) {
|
||||
window.Cypress = Cypress
|
||||
}
|
||||
|
||||
this._addCypressListeners(Cypress)
|
||||
|
||||
ws.emit('watch:test:file', config.spec)
|
||||
},
|
||||
|
||||
isBrowser (browserName) {
|
||||
if (!this.Cypress) return false
|
||||
|
||||
return this.Cypress.isBrowser(browserName)
|
||||
},
|
||||
|
||||
initialize ($autIframe, config) {
|
||||
performance.mark('initialize-start')
|
||||
|
||||
return Cypress.initialize({
|
||||
$autIframe,
|
||||
onSpecReady: () => {
|
||||
// get the current runnable in case we reran mid-test due to a visit
|
||||
// to a new domain
|
||||
ws.emit('get:existing:run:state', (state = {}) => {
|
||||
if (!Cypress.runner) {
|
||||
// the tests have been reloaded
|
||||
return
|
||||
}
|
||||
|
||||
const runnables = Cypress.runner.normalizeAll(state.tests)
|
||||
const run = () => {
|
||||
performance.mark('initialize-end')
|
||||
performance.measure('initialize', 'initialize-start', 'initialize-end')
|
||||
|
||||
this._runDriver(state)
|
||||
}
|
||||
|
||||
reporterBus.emit('runnables:ready', runnables)
|
||||
|
||||
if (state.numLogs) {
|
||||
Cypress.runner.setNumLogs(state.numLogs)
|
||||
}
|
||||
|
||||
if (state.startTime) {
|
||||
Cypress.runner.setStartTime(state.startTime)
|
||||
}
|
||||
|
||||
if (config.isTextTerminal && !state.currentId) {
|
||||
// we are in run mode and it's the first load
|
||||
// store runnables in backend and maybe send to dashboard
|
||||
return ws.emit('set:runnables:and:maybe:record:tests', runnables, run)
|
||||
}
|
||||
|
||||
if (state.currentId) {
|
||||
// if we have a currentId it means
|
||||
// we need to tell the Cypress to skip
|
||||
// ahead to that test
|
||||
Cypress.runner.resumeAtTest(state.currentId, state.emissions)
|
||||
}
|
||||
|
||||
run()
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
_addCypressListeners (Cypress) {
|
||||
Cypress.on('message', (msg, data, cb) => {
|
||||
ws.emit('client:request', msg, data, cb)
|
||||
})
|
||||
|
||||
_.each(driverToSocketEvents, (event) => {
|
||||
Cypress.on(event, (...args) => {
|
||||
return ws.emit(event, ...args)
|
||||
})
|
||||
})
|
||||
|
||||
Cypress.on('collect:run:state', () => {
|
||||
if (Cypress.env('NO_COMMAND_LOG')) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
reporterBus.emit('reporter:collect:run:state', resolve)
|
||||
})
|
||||
})
|
||||
|
||||
Cypress.on('log:added', (log) => {
|
||||
const displayProps = Cypress.runner.getDisplayPropsForLog(log)
|
||||
|
||||
reporterBus.emit('reporter:log:add', displayProps)
|
||||
})
|
||||
|
||||
Cypress.on('log:changed', (log) => {
|
||||
const displayProps = Cypress.runner.getDisplayPropsForLog(log)
|
||||
|
||||
reporterBus.emit('reporter:log:state:changed', displayProps)
|
||||
})
|
||||
|
||||
Cypress.on('before:screenshot', (config, cb) => {
|
||||
const beforeThenCb = () => {
|
||||
localBus.emit('before:screenshot', config)
|
||||
cb()
|
||||
}
|
||||
|
||||
if (Cypress.env('NO_COMMAND_LOG')) {
|
||||
return beforeThenCb()
|
||||
}
|
||||
|
||||
const wait = !config.appOnly && config.waitForCommandSynchronization
|
||||
|
||||
if (!config.appOnly) {
|
||||
reporterBus.emit('test:set:state', _.pick(config, 'id', 'isOpen'), wait ? beforeThenCb : undefined)
|
||||
}
|
||||
|
||||
if (!wait) beforeThenCb()
|
||||
})
|
||||
|
||||
Cypress.on('after:screenshot', (config) => {
|
||||
localBus.emit('after:screenshot', config)
|
||||
})
|
||||
|
||||
_.each(driverToReporterEvents, (event) => {
|
||||
Cypress.on(event, (...args) => {
|
||||
reporterBus.emit(event, ...args)
|
||||
})
|
||||
})
|
||||
|
||||
_.each(driverTestEvents, (event) => {
|
||||
Cypress.on(event, (test, cb) => {
|
||||
reporterBus.emit(event, test, cb)
|
||||
})
|
||||
})
|
||||
|
||||
_.each(driverToLocalAndReporterEvents, (event) => {
|
||||
Cypress.on(event, (...args) => {
|
||||
localBus.emit(event, ...args)
|
||||
reporterBus.emit(event, ...args)
|
||||
})
|
||||
})
|
||||
|
||||
_.each(driverToLocalEvents, (event) => {
|
||||
Cypress.on(event, (...args) => {
|
||||
return localBus.emit(event, ...args)
|
||||
})
|
||||
})
|
||||
|
||||
Cypress.on('script:error', (err) => {
|
||||
Cypress.stop()
|
||||
localBus.emit('script:error', err)
|
||||
})
|
||||
},
|
||||
|
||||
_runDriver (state) {
|
||||
performance.mark('run-s')
|
||||
Cypress.run(() => {
|
||||
performance.mark('run-e')
|
||||
performance.measure('run', 'run-s', 'run-e')
|
||||
})
|
||||
|
||||
reporterBus.emit('reporter:start', {
|
||||
firefoxGcInterval: Cypress.getFirefoxGcInterval(),
|
||||
startTime: Cypress.runner.getStartTime(),
|
||||
numPassed: state.passed,
|
||||
numFailed: state.failed,
|
||||
numPending: state.pending,
|
||||
autoScrollingEnabled: state.autoScrollingEnabled,
|
||||
scrollTop: state.scrollTop,
|
||||
})
|
||||
},
|
||||
|
||||
stop () {
|
||||
localBus.removeAllListeners()
|
||||
ws.off()
|
||||
},
|
||||
|
||||
_reRun (state) {
|
||||
if (!Cypress) return
|
||||
|
||||
state.setIsLoading(true)
|
||||
|
||||
// when we are re-running we first
|
||||
// need to stop cypress always
|
||||
Cypress.stop()
|
||||
|
||||
return this._restart()
|
||||
.then(() => {
|
||||
// this probably isn't 100% necessary
|
||||
// since Cypress will fall out of scope
|
||||
// but we want to be aggressive here
|
||||
// and force GC early and often
|
||||
Cypress.removeAllListeners()
|
||||
|
||||
localBus.emit('restart')
|
||||
})
|
||||
},
|
||||
|
||||
_restart () {
|
||||
return new Promise((resolve) => {
|
||||
reporterBus.once('reporter:restarted', resolve)
|
||||
reporterBus.emit('reporter:restart:test:run')
|
||||
})
|
||||
},
|
||||
|
||||
emit (event, ...args) {
|
||||
localBus.emit(event, ...args)
|
||||
},
|
||||
|
||||
on (event, ...args) {
|
||||
localBus.on(event, ...args)
|
||||
},
|
||||
|
||||
off (event, ...args) {
|
||||
localBus.off(event, ...args)
|
||||
},
|
||||
|
||||
notifyRunningSpec (specFile) {
|
||||
ws.emit('spec:changed', specFile)
|
||||
},
|
||||
|
||||
focusTests () {
|
||||
ws.emit('focus:tests')
|
||||
},
|
||||
|
||||
snapshotUnpinned () {
|
||||
this._unpinSnapshot()
|
||||
this._hideSnapshot()
|
||||
reporterBus.emit('reporter:snapshot:unpinned')
|
||||
},
|
||||
|
||||
_unpinSnapshot () {
|
||||
localBus.emit('unpin:snapshot')
|
||||
},
|
||||
|
||||
_hideSnapshot () {
|
||||
localBus.emit('hide:snapshot')
|
||||
},
|
||||
|
||||
launchBrowser (browser) {
|
||||
ws.emit('reload:browser', window.location.toString(), browser && browser.name)
|
||||
},
|
||||
|
||||
// clear all the cypress specific cookies
|
||||
// whenever our app starts
|
||||
// and additional when we stop running our tests
|
||||
_clearAllCookies () {
|
||||
if (!Cypress) return
|
||||
|
||||
Cypress.Cookies.clearCypressCookies()
|
||||
},
|
||||
|
||||
_setUnload () {
|
||||
if (!Cypress) return
|
||||
|
||||
Cypress.Cookies.setCy('unload', true)
|
||||
},
|
||||
|
||||
saveState (state) {
|
||||
ws.emit('save:app:state', state)
|
||||
},
|
||||
}
|
||||
|
||||
export default eventManager
|
||||
@@ -1,8 +1,8 @@
|
||||
import { action, computed, observable } from 'mobx'
|
||||
import _ from 'lodash'
|
||||
import automation from './automation'
|
||||
import { UIPlugin } from '../plugins/UIPlugin'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { automation } from '@packages/runner-shared'
|
||||
import {
|
||||
DEFAULT_REPORTER_WIDTH,
|
||||
LEFT_NAV_WIDTH,
|
||||
@@ -21,16 +21,13 @@ interface Defaults {
|
||||
messageType: string
|
||||
messageControls: unknown
|
||||
|
||||
width: number
|
||||
height: number
|
||||
|
||||
reporterWidth: number | null
|
||||
pluginsHeight: number | null
|
||||
specListWidth: number | null
|
||||
isSpecsListOpen: boolean
|
||||
|
||||
viewportHeight: number
|
||||
viewportWidth: number
|
||||
height: number
|
||||
width: number
|
||||
|
||||
url: string
|
||||
highlightUrl: boolean
|
||||
@@ -48,11 +45,8 @@ const _defaults: Defaults = {
|
||||
messageType: '',
|
||||
messageControls: null,
|
||||
|
||||
width: 500,
|
||||
height: 500,
|
||||
|
||||
viewportHeight: 500,
|
||||
viewportWidth: 500,
|
||||
width: 500,
|
||||
|
||||
pluginsHeight: PLUGIN_BAR_HEIGHT,
|
||||
|
||||
@@ -111,9 +105,6 @@ export default class State {
|
||||
@observable windowWidth = 0
|
||||
@observable windowHeight = 0
|
||||
|
||||
@observable viewportWidth = _defaults.viewportWidth
|
||||
@observable viewportHeight = _defaults.viewportHeight
|
||||
|
||||
@observable automation = automation.CONNECTING
|
||||
|
||||
@observable.ref scriptError: string | undefined
|
||||
@@ -175,10 +166,10 @@ export default class State {
|
||||
return 1
|
||||
}
|
||||
|
||||
if (autAreaWidth < this.viewportWidth || autAreaHeight < this.viewportHeight) {
|
||||
if (autAreaWidth < this.width || autAreaHeight < this.height) {
|
||||
return Math.min(
|
||||
autAreaWidth / this.viewportWidth,
|
||||
autAreaHeight / this.viewportHeight,
|
||||
autAreaWidth / this.width,
|
||||
autAreaHeight / this.height,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -218,9 +209,9 @@ export default class State {
|
||||
this.screenshotting = screenshotting
|
||||
}
|
||||
|
||||
@action updateAutViewportDimensions (dimensions: { viewportWidth: number, viewportHeight: number }) {
|
||||
this.viewportHeight = dimensions.viewportHeight
|
||||
this.viewportWidth = dimensions.viewportWidth
|
||||
@action updateDimensions (width: number, height: number) {
|
||||
this.height = height
|
||||
this.width = width
|
||||
}
|
||||
|
||||
@action toggleIsSpecsListOpen () {
|
||||
@@ -249,7 +240,11 @@ export default class State {
|
||||
this.specListWidth = width
|
||||
}
|
||||
|
||||
@action updateWindowDimensions ({ windowWidth, windowHeight }: { windowWidth?: number, windowHeight?: number }) {
|
||||
@action updateWindowDimensions ({
|
||||
windowWidth,
|
||||
windowHeight,
|
||||
headerHeight,
|
||||
}: { windowWidth?: number, windowHeight?: number, headerHeight?: number }) {
|
||||
if (windowWidth) {
|
||||
this.windowWidth = windowWidth
|
||||
}
|
||||
@@ -257,6 +252,10 @@ export default class State {
|
||||
if (windowHeight) {
|
||||
this.windowHeight = windowHeight
|
||||
}
|
||||
|
||||
if (headerHeight) {
|
||||
this.headerHeight = headerHeight
|
||||
}
|
||||
}
|
||||
|
||||
@action clearMessage () {
|
||||
@@ -330,7 +329,7 @@ export default class State {
|
||||
}
|
||||
|
||||
runMultiMode = async () => {
|
||||
const eventManager = require('./event-manager').default
|
||||
const eventManager = require('@packages/runner-shared').eventManager
|
||||
const waitForRunEnd = () => new Promise((res) => eventManager.on('run:end', res))
|
||||
|
||||
this.setSpec(null)
|
||||
|
||||
@@ -4,8 +4,9 @@ import { render } from 'react-dom'
|
||||
import { utils as driverUtils } from '@packages/driver'
|
||||
import defaultEvents from '@packages/reporter/src/lib/events'
|
||||
|
||||
import App from './app/RunnerCt'
|
||||
import State from './lib/state'
|
||||
import Container from './app/container'
|
||||
import { Container, eventManager } from '@packages/runner-shared'
|
||||
import util from './lib/util'
|
||||
|
||||
// to support async/await
|
||||
@@ -62,9 +63,20 @@ const Runner = {
|
||||
Runner.state = state
|
||||
Runner.configureMobx = configure
|
||||
|
||||
state.updateAutViewportDimensions({ viewportWidth: config.viewportWidth, viewportHeight: config.viewportHeight })
|
||||
state.updateDimensions(config.viewportWidth, config.viewportHeight)
|
||||
|
||||
render(<Container config={config} state={state} />, el)
|
||||
const container = (
|
||||
<Container
|
||||
config={config}
|
||||
runner='ct'
|
||||
state={state}
|
||||
App={App}
|
||||
hasSpecFile={util.hasSpecFile}
|
||||
eventManager={eventManager}
|
||||
/>
|
||||
)
|
||||
|
||||
render(container, el)
|
||||
})()
|
||||
},
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
/* Strict Type-Checking Options */
|
||||
// "traceResolution": true,
|
||||
"strict": false,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
/**
|
||||
* Skip type checking of all declaration files (*.d.ts).
|
||||
@@ -56,6 +56,8 @@
|
||||
},
|
||||
"include": [
|
||||
"./lib/*.ts",
|
||||
"./src*.ts",
|
||||
"./src*.tsx",
|
||||
"./index.ts",
|
||||
"./index.d.ts",
|
||||
"./../ts/index.d.ts"
|
||||
|
||||
@@ -21,20 +21,31 @@ babelLoader.use.options.plugins.push([require.resolve('babel-plugin-prismjs'), {
|
||||
css: false,
|
||||
}])
|
||||
|
||||
let pngRule
|
||||
// @ts-ignore
|
||||
const nonPngRules = _.filter(commonConfig.module.rules, (rule) => {
|
||||
// @ts-ignore
|
||||
if (rule.test.toString().includes('png')) {
|
||||
pngRule = rule
|
||||
|
||||
return false
|
||||
const { pngRule, nonPngRules } = commonConfig!.module!.rules!.reduce<{
|
||||
nonPngRules: webpack.RuleSetRule[]
|
||||
pngRule: webpack.RuleSetRule | undefined
|
||||
}>((acc, rule) => {
|
||||
if (rule?.test?.toString().includes('png')) {
|
||||
return {
|
||||
...acc,
|
||||
pngRule: rule,
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
return {
|
||||
...acc,
|
||||
nonPngRules: [...acc.nonPngRules, rule],
|
||||
}
|
||||
}, {
|
||||
nonPngRules: [],
|
||||
pngRule: undefined,
|
||||
})
|
||||
|
||||
pngRule.use[0].options = {
|
||||
if (!pngRule || !pngRule.use) {
|
||||
throw Error('Could not find png loader')
|
||||
}
|
||||
|
||||
(pngRule.use as webpack.RuleSetLoader[])[0].options = {
|
||||
name: '[name].[ext]',
|
||||
outputPath: 'img',
|
||||
publicPath: '/__cypress/runner/img/',
|
||||
|
||||
137
packages/runner-shared/.eslintrc.json
Normal file
137
packages/runner-shared/.eslintrc.json
Normal file
@@ -0,0 +1,137 @@
|
||||
{
|
||||
"plugins": [
|
||||
"cypress",
|
||||
"@cypress/dev"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:@cypress/dev/general",
|
||||
"plugin:@cypress/dev/tests",
|
||||
"plugin:@cypress/dev/react",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"../reporter/src/.eslintrc.json"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"env": {
|
||||
"cypress/globals": true
|
||||
},
|
||||
"rules": {
|
||||
"react/display-name": "off",
|
||||
"react/function-component-definition": [
|
||||
"error",
|
||||
{
|
||||
"namedComponents": "arrow-function",
|
||||
"unnamedComponents": "arrow-function"
|
||||
}
|
||||
],
|
||||
"react/jsx-boolean-value": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"react/jsx-closing-bracket-location": [
|
||||
"error",
|
||||
"line-aligned"
|
||||
],
|
||||
"react/jsx-closing-tag-location": "error",
|
||||
"react/jsx-curly-brace-presence": [
|
||||
"error",
|
||||
{
|
||||
"props": "never",
|
||||
"children": "never"
|
||||
}
|
||||
],
|
||||
"react/jsx-curly-newline": "error",
|
||||
"react/jsx-filename-extension": [
|
||||
"warn",
|
||||
{
|
||||
"extensions": [
|
||||
".js",
|
||||
".jsx",
|
||||
".tsx"
|
||||
]
|
||||
}
|
||||
],
|
||||
"react/jsx-first-prop-new-line": "error",
|
||||
"react/jsx-max-props-per-line": [
|
||||
"error",
|
||||
{
|
||||
"maximum": 1,
|
||||
"when": "multiline"
|
||||
}
|
||||
],
|
||||
"react/jsx-no-bind": [
|
||||
"error",
|
||||
{
|
||||
"ignoreDOMComponents": true
|
||||
}
|
||||
],
|
||||
"react/jsx-no-useless-fragment": "error",
|
||||
"react/jsx-one-expression-per-line": [
|
||||
"error",
|
||||
{
|
||||
"allow": "literal"
|
||||
}
|
||||
],
|
||||
"react/jsx-sort-props": [
|
||||
"error",
|
||||
{
|
||||
"callbacksLast": true,
|
||||
"ignoreCase": true,
|
||||
"noSortAlphabetically": true,
|
||||
"reservedFirst": true
|
||||
}
|
||||
],
|
||||
"react/jsx-tag-spacing": [
|
||||
"error",
|
||||
{
|
||||
"closingSlash": "never",
|
||||
"beforeSelfClosing": "always"
|
||||
}
|
||||
],
|
||||
"react/jsx-wrap-multilines": [
|
||||
"error",
|
||||
{
|
||||
"declaration": "parens-new-line",
|
||||
"assignment": "parens-new-line",
|
||||
"return": "parens-new-line",
|
||||
"arrow": "parens-new-line",
|
||||
"condition": "parens-new-line",
|
||||
"logical": "parens-new-line",
|
||||
"prop": "parens-new-line"
|
||||
}
|
||||
],
|
||||
"react/no-array-index-key": "error",
|
||||
"react/no-unescaped-entities": "off",
|
||||
"react/prop-types": "off",
|
||||
"quote-props": [
|
||||
"error",
|
||||
"as-needed"
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"lib/*"
|
||||
],
|
||||
"rules": {
|
||||
"no-console": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"**/*.json"
|
||||
],
|
||||
"rules": {
|
||||
"quotes": "off",
|
||||
"comma-dangle": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": "*.tsx",
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"react/jsx-no-bind": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
32
packages/runner-shared/package.json
Normal file
32
packages/runner-shared/package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@packages/runner-shared",
|
||||
"version": "0.0.0-development",
|
||||
"private": true,
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"test": "yarn test-unit",
|
||||
"test-unit": "mocha --config test/.mocharc.json src/**/*.spec.* --exit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cypress/react-tooltip": "0.5.3",
|
||||
"ansi-to-html": "0.6.14",
|
||||
"classnames": "2.3.1",
|
||||
"lodash": "4.17.21",
|
||||
"mobx": "5.15.4",
|
||||
"mobx-react": "6.1.8",
|
||||
"react": "16.8.6",
|
||||
"react-dom": "16.8.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@packages/driver": "0.0.0-development",
|
||||
"@packages/socket": "0.0.0-development",
|
||||
"@packages/web-config": "0.0.0-development",
|
||||
"chai": "4.2.0",
|
||||
"chai-enzyme": "1.0.0-beta.1",
|
||||
"enzyme": "3.11.0",
|
||||
"enzyme-adapter-react-16": "1.15.2",
|
||||
"mocha": "7.0.1",
|
||||
"sinon": "7.5.0",
|
||||
"sinon-chai": "3.3.0"
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import React from 'react'
|
||||
import { shallow } from 'enzyme'
|
||||
import sinon from 'sinon'
|
||||
|
||||
import AutomationDisconnected from './automation-disconnected'
|
||||
import { AutomationDisconnected } from '.'
|
||||
|
||||
describe('<AutomationDisconnected />', () => {
|
||||
it('renders the message', () => {
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
|
||||
export default ({ onReload }) => (
|
||||
export const AutomationDisconnected = ({ onReload }) => (
|
||||
<div className='runner automation-failure'>
|
||||
<div className='automation-message automation-disconnected'>
|
||||
<p>Whoops, the Cypress extension has disconnected.</p>
|
||||
17
packages/runner-shared/src/automation-element/index.tsx
Normal file
17
packages/runner-shared/src/automation-element/index.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react'
|
||||
|
||||
export const automationElementId = '__cypress-string'
|
||||
|
||||
interface AutomationElementProps {
|
||||
randomString: string
|
||||
}
|
||||
|
||||
export const AutomationElement: React.FC<AutomationElementProps> = ({
|
||||
randomString,
|
||||
}) => {
|
||||
return (
|
||||
<div id={automationElementId} style={{ display: 'none' }}>
|
||||
{randomString}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export default {
|
||||
export const automation = {
|
||||
CONNECTING: 'CONNECTING',
|
||||
MISSING: 'MISSING',
|
||||
CONNECTED: 'CONNECTED',
|
||||
@@ -1,4 +1,4 @@
|
||||
export default () => {
|
||||
export const blankContents = () => {
|
||||
return `
|
||||
<style>
|
||||
body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img,a img{border:none;}address,caption,cite,code,dfn,em,strong,th,var,optgroup{font-style:inherit;font-weight:inherit;}del,ins{text-decoration:none;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym{border:0;font-variant:normal;}sup{vertical-align:baseline;}sub{vertical-align:baseline;}legend{color:#000;}
|
||||
40
packages/runner-shared/src/config-file-formatted.tsx
Normal file
40
packages/runner-shared/src/config-file-formatted.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from 'react'
|
||||
import { isUndefined } from 'lodash'
|
||||
|
||||
const configFileFormatted = (configFile) => {
|
||||
if (configFile === false) {
|
||||
return (
|
||||
<>
|
||||
<code>cypress.json</code>
|
||||
{' '}
|
||||
file (currently disabled by
|
||||
{' '}
|
||||
<code>--config-file false</code>
|
||||
)
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (isUndefined(configFile) || configFile === 'cypress.json') {
|
||||
return (
|
||||
<>
|
||||
<code>cypress.json</code>
|
||||
{' '}
|
||||
file
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
custom config file
|
||||
<code>
|
||||
{configFile}
|
||||
</code>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
configFileFormatted,
|
||||
}
|
||||
@@ -2,16 +2,18 @@ import React from 'react'
|
||||
import { mount, shallow } from 'enzyme'
|
||||
import sinon from 'sinon'
|
||||
|
||||
import App from './app'
|
||||
import automation from '../lib/automation'
|
||||
import AutomationDisconnected from '../errors/automation-disconnected'
|
||||
import NoAutomation from '../errors/no-automation'
|
||||
import NoSpec from '../errors/no-spec'
|
||||
import State from '../lib/state'
|
||||
import App from '@packages/runner/src/app/app'
|
||||
import { AutomationDisconnected } from '../automation-disconnected'
|
||||
import { automation } from '../automation'
|
||||
import { NoAutomation } from '../no-automation'
|
||||
import { automationElementId } from '../automation-element'
|
||||
import NoSpec from '@packages/runner/src/errors/no-spec'
|
||||
|
||||
import Container, { automationElementId } from './container'
|
||||
import { Container } from '.'
|
||||
|
||||
const createProps = () => ({
|
||||
runner: 'e2e',
|
||||
hasSpecFile: sinon.stub(),
|
||||
config: {
|
||||
browsers: [],
|
||||
integrationFolder: '',
|
||||
@@ -19,6 +21,15 @@ const createProps = () => ({
|
||||
projectName: '',
|
||||
viewportHeight: 0,
|
||||
viewportWidth: 0,
|
||||
spec: {
|
||||
name: 'test/spec.js',
|
||||
relative: './this/is/a/test/spec.js',
|
||||
absolute: '/Users/me/code/this/is/a/test/spec.js',
|
||||
},
|
||||
state: {
|
||||
autoScrollingEnabled: true,
|
||||
reporterWidth: 300,
|
||||
},
|
||||
},
|
||||
eventManager: {
|
||||
addGlobalListeners: sinon.spy(),
|
||||
@@ -28,11 +39,19 @@ const createProps = () => ({
|
||||
emit: () => {},
|
||||
on: () => {},
|
||||
},
|
||||
on: () => {},
|
||||
start: () => {},
|
||||
setup: () => {},
|
||||
},
|
||||
state: new State(),
|
||||
util: {
|
||||
hasSpecFile: sinon.stub(),
|
||||
state: {
|
||||
automation: undefined,
|
||||
defaults: {
|
||||
width: 500,
|
||||
height: 500,
|
||||
},
|
||||
},
|
||||
App,
|
||||
NoSpec,
|
||||
})
|
||||
|
||||
describe('<Container />', () => {
|
||||
@@ -53,12 +72,11 @@ describe('<Container />', () => {
|
||||
const props = createProps()
|
||||
|
||||
props.state.automation = automation.CONNECTING
|
||||
component = shallow(<Container {...props} />)
|
||||
component = mount(<Container {...props} />)
|
||||
})
|
||||
|
||||
it('renders the automation element alone', () => {
|
||||
expect(component.find(`#${automationElementId}`)).to.exist
|
||||
expect(component.find(`#${automationElementId}`).parent()).not.to.exist
|
||||
})
|
||||
})
|
||||
|
||||
@@ -117,7 +135,7 @@ describe('<Container />', () => {
|
||||
beforeEach(() => {
|
||||
props = createProps()
|
||||
props.state.automation = automation.CONNECTED
|
||||
props.util.hasSpecFile.returns(false)
|
||||
props.hasSpecFile.returns(false)
|
||||
component = shallow(<Container {...props} />)
|
||||
})
|
||||
|
||||
@@ -125,35 +143,11 @@ describe('<Container />', () => {
|
||||
expect(component.find(NoSpec)).to.have.prop('config', props.config)
|
||||
})
|
||||
|
||||
it('renders the automation element', () => {
|
||||
expect(component.find(`#${automationElementId}`)).to.exist
|
||||
})
|
||||
|
||||
it('renders the app when hash changes with and has a spec file', () => {
|
||||
props.util.hasSpecFile.returns(true)
|
||||
props.hasSpecFile.returns(true)
|
||||
component.find(NoSpec).prop('onHashChange')()
|
||||
component.update()
|
||||
expect(component.find(App)).to.exist
|
||||
})
|
||||
})
|
||||
|
||||
describe('when automation is connected and there is a spec file', () => {
|
||||
let props
|
||||
let component
|
||||
|
||||
beforeEach(() => {
|
||||
props = createProps()
|
||||
props.state.automation = automation.CONNECTED
|
||||
props.util.hasSpecFile.returns(true)
|
||||
component = shallow(<Container {...props} />)
|
||||
})
|
||||
|
||||
it('renders <App />', () => {
|
||||
expect(component.find(App)).to.exist
|
||||
})
|
||||
|
||||
it('renders the automation element', () => {
|
||||
expect(component.find(`#${automationElementId}`)).to.exist
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,20 +1,13 @@
|
||||
import { observer } from 'mobx-react'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { Component } from 'react'
|
||||
|
||||
import automation from '../lib/automation'
|
||||
import eventManager from '../lib/event-manager'
|
||||
import State from '../lib/state'
|
||||
import util from '../lib/util'
|
||||
|
||||
import RunnerCt from './RunnerCt'
|
||||
import AutomationDisconnected from '../errors/automation-disconnected'
|
||||
import NoAutomation from '../errors/no-automation'
|
||||
|
||||
const automationElementId = '__cypress-string'
|
||||
import { AutomationDisconnected } from '../automation-disconnected'
|
||||
import { automation } from '../automation'
|
||||
import { NoAutomation } from '../no-automation'
|
||||
import { automationElementId, AutomationElement } from '../automation-element'
|
||||
|
||||
@observer
|
||||
class Container extends Component {
|
||||
export class Container extends Component {
|
||||
constructor (...args) {
|
||||
super(...args)
|
||||
|
||||
@@ -40,30 +33,34 @@ class Container extends Component {
|
||||
return this._automationDisconnected()
|
||||
case automation.CONNECTED:
|
||||
default:
|
||||
return this._app()
|
||||
if (this.props.runner === 'e2e') {
|
||||
return this.props.hasSpecFile()
|
||||
? this._app()
|
||||
: this._noSpec()
|
||||
}
|
||||
|
||||
if (this.props.runner === 'ct') {
|
||||
return this._app()
|
||||
}
|
||||
|
||||
throw Error(`runner prop is required and must be 'e2e' or 'ct'. You passed: ${this.props.runner}.`)
|
||||
}
|
||||
}
|
||||
|
||||
_automationElement () {
|
||||
return (
|
||||
<div id={automationElementId} style={{ display: 'none' }}>
|
||||
{this.randomString}
|
||||
</div>
|
||||
<AutomationElement randomString={this.randomString} />
|
||||
)
|
||||
}
|
||||
|
||||
_app () {
|
||||
return (
|
||||
<RunnerCt {...this.props}>
|
||||
{this._automationElement()}
|
||||
</RunnerCt>
|
||||
)
|
||||
}
|
||||
const { App, ...rest } = this.props
|
||||
|
||||
_checkSpecFile = () => {
|
||||
if (this.props.util.hasSpecFile()) {
|
||||
this.forceUpdate()
|
||||
}
|
||||
return (
|
||||
<App {...rest}>
|
||||
{this._automationElement()}
|
||||
</App>
|
||||
)
|
||||
}
|
||||
|
||||
_noAutomation () {
|
||||
@@ -82,18 +79,23 @@ class Container extends Component {
|
||||
_automationDisconnected () {
|
||||
return <AutomationDisconnected onReload={this.props.eventManager.launchBrowser} />
|
||||
}
|
||||
|
||||
// This two functions, _noSpec anad _checkSpecFile, are only used by the E2E runner.
|
||||
// TODO: remove any runner specific code from this file.
|
||||
_noSpec () {
|
||||
const { NoSpec } = this.props
|
||||
|
||||
return (
|
||||
<NoSpec config={this.props.config} onHashChange={this._checkSpecFile}>
|
||||
{this._automationElement()}
|
||||
</NoSpec>
|
||||
)
|
||||
}
|
||||
|
||||
// This is only used by the E2E runner.
|
||||
_checkSpecFile = () => {
|
||||
if (this.props.hasSpecFile()) {
|
||||
this.forceUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Container.defaultProps = {
|
||||
eventManager,
|
||||
util,
|
||||
}
|
||||
|
||||
Container.propTypes = {
|
||||
config: PropTypes.object.isRequired,
|
||||
state: PropTypes.instanceOf(State),
|
||||
}
|
||||
|
||||
export { automationElementId }
|
||||
|
||||
export default Container
|
||||
@@ -1,10 +1,10 @@
|
||||
import _ from 'lodash'
|
||||
|
||||
import { $ } from '@packages/driver'
|
||||
import selectorPlaygroundHighlight from '../selector-playground/highlight'
|
||||
import { selectorPlaygroundHighlight } from './selector-playground/highlight'
|
||||
// The '!' tells webpack to disable normal loaders, and keep loaders with `enforce: 'pre'` and `enforce: 'post'`
|
||||
// This disables the CSSExtractWebpackPlugin and allows us to get the CSS as a raw string instead of saving it to a separate file.
|
||||
import selectorPlaygroundCSS from '!../selector-playground/selector-playground.scss'
|
||||
import selectorPlaygroundCSS from '!./selector-playground/selector-playground.scss'
|
||||
|
||||
const styles = (styleString) => {
|
||||
return styleString.replace(/\s*\n\s*/g, '')
|
||||
@@ -443,7 +443,7 @@ function removeBlackouts ($body) {
|
||||
$body.find('.__cypress-blackout').remove()
|
||||
}
|
||||
|
||||
export default {
|
||||
export const dom = {
|
||||
addBlackout,
|
||||
removeBlackouts,
|
||||
addElementBoxModelLayers,
|
||||
@@ -1,4 +1,4 @@
|
||||
export default {
|
||||
export const errorMessages = {
|
||||
reporterError (err, specPath) {
|
||||
if (!err) return null
|
||||
|
||||
@@ -5,10 +5,10 @@ import { action } from 'mobx'
|
||||
|
||||
import { client } from '@packages/socket'
|
||||
|
||||
import automation from './automation'
|
||||
import logger from './logger'
|
||||
import studioRecorder from '../studio/studio-recorder'
|
||||
import selectorPlaygroundModel from '../selector-playground/selector-playground-model'
|
||||
import { studioRecorder } from './studio'
|
||||
import { automation } from './automation'
|
||||
import { logger } from './logger'
|
||||
import { selectorPlaygroundModel } from './selector-playground'
|
||||
|
||||
import $Cypress, { $ } from '@packages/driver'
|
||||
|
||||
@@ -26,7 +26,7 @@ const driverToLocalAndReporterEvents = 'run:start run:end'.split(' ')
|
||||
const driverToSocketEvents = 'backend:request automation:request mocha recorder:frame'.split(' ')
|
||||
const driverTestEvents = 'test:before:run:async test:after:run'.split(' ')
|
||||
const driverToLocalEvents = 'viewport:changed config stop url:changed page:loading visit:failed'.split(' ')
|
||||
const socketRerunEvents = 'runner:restart'.split(' ')
|
||||
const socketRerunEvents = 'runner:restart watched:file:changed'.split(' ')
|
||||
const socketToDriverEvents = 'net:event script:error'.split(' ')
|
||||
const localToReporterEvents = 'reporter:log:add reporter:log:state:changed reporter:log:remove'.split(' ')
|
||||
|
||||
@@ -47,7 +47,7 @@ if (window.Cypress) {
|
||||
*/
|
||||
let Cypress
|
||||
|
||||
const eventManager = {
|
||||
export const eventManager = {
|
||||
reporterBus,
|
||||
|
||||
getCypress () {
|
||||
@@ -99,6 +99,15 @@ const eventManager = {
|
||||
rerun()
|
||||
})
|
||||
|
||||
ws.on('component:specs:changed', (specs) => {
|
||||
state.setSpecs(specs)
|
||||
})
|
||||
|
||||
ws.on('dev-server:hmr:error', (error) => {
|
||||
Cypress.stop()
|
||||
localBus.emit('script:error', error)
|
||||
})
|
||||
|
||||
_.each(socketRerunEvents, (event) => {
|
||||
ws.on(event, rerun)
|
||||
})
|
||||
@@ -290,9 +299,13 @@ const eventManager = {
|
||||
Cypress = this.Cypress = $Cypress.create(config)
|
||||
|
||||
// expose Cypress globally
|
||||
window.Cypress = Cypress
|
||||
// since CT AUT shares the window with the spec, we don't want to overwrite
|
||||
// our spec Cypress instance with the component's Cypress instance
|
||||
if (window.top === window) {
|
||||
window.Cypress = Cypress
|
||||
}
|
||||
|
||||
this._addListeners()
|
||||
this._addListeners(Cypress)
|
||||
|
||||
ws.emit('watch:test:file', config.spec)
|
||||
},
|
||||
@@ -585,6 +598,10 @@ const eventManager = {
|
||||
localBus.on(event, ...args)
|
||||
},
|
||||
|
||||
off (event, ...args) {
|
||||
localBus.off(event, ...args)
|
||||
},
|
||||
|
||||
notifyRunningSpec (specFile) {
|
||||
ws.emit('spec:changed', specFile)
|
||||
},
|
||||
@@ -630,5 +647,3 @@ const eventManager = {
|
||||
ws.emit('save:app:state', state)
|
||||
},
|
||||
}
|
||||
|
||||
export default eventManager
|
||||
@@ -5,12 +5,10 @@ import sinon from 'sinon'
|
||||
import driver from '@packages/driver'
|
||||
import Tooltip from '@cypress/react-tooltip'
|
||||
|
||||
import eventManager from '../lib/event-manager'
|
||||
import selectorPlaygroundModel from '../selector-playground/selector-playground-model'
|
||||
import studioRecorder from '../studio/studio-recorder'
|
||||
|
||||
import Header from './header'
|
||||
import Studio from '../studio/studio'
|
||||
import { eventManager } from '../event-manager'
|
||||
import { Studio, studioRecorder } from '../studio'
|
||||
import { selectorPlaygroundModel } from '../selector-playground'
|
||||
import { Header } from '.'
|
||||
|
||||
const getState = (props) => _.extend({
|
||||
defaults: {},
|
||||
@@ -21,6 +19,7 @@ const propsWithState = (stateProps, configProps = {}) =>
|
||||
({
|
||||
state: getState(stateProps),
|
||||
config: configProps,
|
||||
runner: 'e2e',
|
||||
})
|
||||
|
||||
describe('<Header />', () => {
|
||||
@@ -297,7 +296,7 @@ describe('<Header />', () => {
|
||||
|
||||
describe('viewport info', () => {
|
||||
it('has menu-open class on button click', () => {
|
||||
const component = shallow(<Header {...propsWithState()} />)
|
||||
const component = mount(<Header {...propsWithState()} />)
|
||||
|
||||
component.find('.viewport-info button').simulate('click')
|
||||
expect(component.find('.viewport-info')).to.have.className('menu-open')
|
||||
@@ -305,14 +304,14 @@ describe('<Header />', () => {
|
||||
|
||||
it('displays width, height, and display scale', () => {
|
||||
const state = { width: 1, height: 2, displayScale: 3 }
|
||||
const component = shallow(<Header {...propsWithState(state)} />)
|
||||
const component = mount(<Header {...propsWithState(state)} />)
|
||||
|
||||
expect(component.find('.viewport-info button').text()).to.contain('1 x 2 (3%)')
|
||||
})
|
||||
|
||||
it('displays default width and height in menu', () => {
|
||||
const state = { defaults: { width: 4, height: 5 } }
|
||||
const component = shallow(<Header {...propsWithState(state)} />)
|
||||
const component = mount(<Header {...propsWithState(state)} />)
|
||||
|
||||
expect(component.find('.viewport-menu pre').text()).to.contain('"viewportWidth": 4')
|
||||
expect(component.find('.viewport-menu pre').text()).to.contain('"viewportHeight": 5')
|
||||
242
packages/runner-shared/src/header/index.tsx
Normal file
242
packages/runner-shared/src/header/index.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
import cs from 'classnames'
|
||||
|
||||
import { action, computed, observable } from 'mobx'
|
||||
import { observer } from 'mobx-react'
|
||||
import React, { Component, createRef } from 'react'
|
||||
import Tooltip from '@cypress/react-tooltip'
|
||||
import { $ } from '@packages/driver'
|
||||
|
||||
import { ViewportInfo } from '../viewport-info'
|
||||
import { SelectorPlayground } from '../selector-playground/SelectorPlayground'
|
||||
import { selectorPlaygroundModel } from '../selector-playground'
|
||||
import { Studio, studioRecorder } from '../studio'
|
||||
import { eventManager } from '../event-manager'
|
||||
|
||||
interface BaseState {
|
||||
isLoading: boolean
|
||||
isRunning: boolean
|
||||
width: number
|
||||
height: number
|
||||
displayScale: number | undefined
|
||||
defaults: {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
updateWindowDimensions: (payload: { headerHeight: number }) => void
|
||||
}
|
||||
|
||||
interface StateCT {
|
||||
runner: 'ct'
|
||||
state: {
|
||||
screenshotting: boolean
|
||||
} & BaseState
|
||||
}
|
||||
|
||||
interface StateE2E {
|
||||
runner: 'e2e'
|
||||
state: {
|
||||
url: string
|
||||
isLoadingUrl: boolean
|
||||
highlightUrl: boolean
|
||||
} & BaseState
|
||||
}
|
||||
|
||||
interface HeaderBaseProps {
|
||||
config: {
|
||||
configFile: string
|
||||
[k: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
type CtHeaderProps = StateCT & HeaderBaseProps
|
||||
|
||||
type E2EHeaderProps = StateE2E & HeaderBaseProps
|
||||
|
||||
type HeaderProps = CtHeaderProps | E2EHeaderProps
|
||||
|
||||
@observer
|
||||
export class Header extends Component<HeaderProps> {
|
||||
@observable showingViewportMenu = false
|
||||
@observable urlInput = ''
|
||||
@observable previousSelectorPlaygroundOpen: boolean = false
|
||||
@observable previousRecorderIsOpen: boolean = false
|
||||
|
||||
urlInputRef = createRef<HTMLInputElement>()
|
||||
headerRef = createRef<HTMLHeadElement>()
|
||||
|
||||
get studioForm () {
|
||||
if (this.props.runner !== 'e2e') {
|
||||
return
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
className={cs('url-container', {
|
||||
loading: this.props.runner === 'e2e' && this.props.state.isLoadingUrl,
|
||||
highlighted: this.props.runner === 'e2e' && this.props.state.highlightUrl,
|
||||
'menu-open': this._studioNeedsUrl,
|
||||
})}
|
||||
onSubmit={this._visitUrlInput}
|
||||
>
|
||||
<input
|
||||
ref={this.urlInputRef}
|
||||
type='text'
|
||||
className={cs('url', { 'input-active': this._studioNeedsUrl })}
|
||||
value={this._studioNeedsUrl ? this.urlInput : this.props.state.url}
|
||||
readOnly={!this._studioNeedsUrl}
|
||||
onChange={this._onUrlInput}
|
||||
onClick={this._openUrl}
|
||||
/>
|
||||
<div className='popup-menu url-menu'>
|
||||
<p>
|
||||
<strong>Please enter a valid URL to visit.</strong>
|
||||
</p>
|
||||
<div className='menu-buttons'>
|
||||
<button type='button' className='btn-cancel' onClick={this._cancelStudio}>Cancel</button>
|
||||
<button type='submit' className='btn-submit' disabled={!this.urlInput}>
|
||||
{`Go `}
|
||||
<i className='fas fa-arrow-right' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<span className='loading-container'>
|
||||
...loading
|
||||
{' '}
|
||||
<i className='fas fa-spinner fa-pulse' />
|
||||
</span>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { config, state } = this.props
|
||||
|
||||
return (
|
||||
<header
|
||||
ref={this.headerRef}
|
||||
className={cs({
|
||||
'showing-selector-playground': selectorPlaygroundModel.isOpen,
|
||||
'showing-studio': studioRecorder.isOpen,
|
||||
'display-none': this.props.runner === 'ct' && this.props.state.screenshotting,
|
||||
})}
|
||||
>
|
||||
<div className='sel-url-wrap'>
|
||||
<Tooltip
|
||||
title='Open Selector Playground'
|
||||
visible={selectorPlaygroundModel.isOpen || studioRecorder.isOpen ? false : null}
|
||||
wrapperClassName='selector-playground-toggle-tooltip-wrapper'
|
||||
className='cy-tooltip'
|
||||
>
|
||||
<button
|
||||
aria-label='Open Selector Playground'
|
||||
className='header-button selector-playground-toggle'
|
||||
disabled={this.props.state.isLoading || state.isRunning || studioRecorder.isOpen}
|
||||
onClick={this._togglePlaygroundOpen}
|
||||
>
|
||||
<i aria-hidden="true" className='fas fa-crosshairs' />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<div className={cs('menu-cover', { 'menu-cover-display': this._studioNeedsUrl })} />
|
||||
{this.studioForm}
|
||||
</div>
|
||||
|
||||
<ViewportInfo
|
||||
showingViewportMenu={this.showingViewportMenu}
|
||||
width={state.width}
|
||||
height={state.height}
|
||||
config={config}
|
||||
displayScale={this.props.runner === 'e2e' ? state.displayScale : undefined}
|
||||
defaults={{
|
||||
width: state.defaults.width,
|
||||
height: state.defaults.height,
|
||||
}}
|
||||
toggleViewportMenu={this._toggleViewportMenu}
|
||||
/>
|
||||
|
||||
<SelectorPlayground
|
||||
model={selectorPlaygroundModel}
|
||||
eventManager={eventManager}
|
||||
/>
|
||||
{this.props.runner === 'e2e' &&
|
||||
<Studio model={studioRecorder} hasUrl={!!this.props.state.url} />}
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
@action componentDidMount () {
|
||||
this.previousSelectorPlaygroundOpen = selectorPlaygroundModel.isOpen
|
||||
this.previousRecorderIsOpen = studioRecorder.isOpen
|
||||
|
||||
this.urlInput = this.props.config.baseUrl ? `${this.props.config.baseUrl}/` : ''
|
||||
}
|
||||
|
||||
@action componentDidUpdate () {
|
||||
if (selectorPlaygroundModel.isOpen !== this.previousSelectorPlaygroundOpen) {
|
||||
this._updateWindowDimensions()
|
||||
this.previousSelectorPlaygroundOpen = selectorPlaygroundModel.isOpen
|
||||
}
|
||||
|
||||
if (studioRecorder.isOpen !== this.previousRecorderIsOpen) {
|
||||
this._updateWindowDimensions()
|
||||
this.previousRecorderIsOpen = studioRecorder.isOpen
|
||||
}
|
||||
|
||||
if (this._studioNeedsUrl && this.urlInputRef.current) {
|
||||
this.urlInputRef.current.focus()
|
||||
}
|
||||
}
|
||||
|
||||
_togglePlaygroundOpen = () => {
|
||||
selectorPlaygroundModel.toggleOpen()
|
||||
}
|
||||
|
||||
@action _toggleViewportMenu = () => {
|
||||
this.showingViewportMenu = !this.showingViewportMenu
|
||||
}
|
||||
|
||||
_updateWindowDimensions = () => {
|
||||
if (!this.headerRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
this.props.state.updateWindowDimensions({
|
||||
headerHeight: $(this.headerRef.current).outerHeight(),
|
||||
})
|
||||
}
|
||||
|
||||
_openUrl = () => {
|
||||
if (this._studioNeedsUrl || this.props.runner !== 'e2e') {
|
||||
return
|
||||
}
|
||||
|
||||
window.open(this.props.state.url)
|
||||
}
|
||||
|
||||
@computed get _studioNeedsUrl () {
|
||||
if (this.props.runner !== 'e2e') {
|
||||
return
|
||||
}
|
||||
|
||||
return studioRecorder.needsUrl && !this.props.state.url
|
||||
}
|
||||
|
||||
@action _onUrlInput = (e) => { // : React.FormEvent<HTMLInputElement>) => {
|
||||
if (!this._studioNeedsUrl) return
|
||||
|
||||
this.urlInput = e.target.value
|
||||
}
|
||||
|
||||
@action _visitUrlInput = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!this._studioNeedsUrl) return
|
||||
|
||||
studioRecorder.visitUrl(this.urlInput)
|
||||
|
||||
this.urlInput = ''
|
||||
}
|
||||
|
||||
_cancelStudio = () => {
|
||||
eventManager.emit('studio:cancel')
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
import _ from 'lodash'
|
||||
import { $ } from '@packages/driver'
|
||||
import { blankContents } from '../blank-contents'
|
||||
import { visitFailure } from '../visit-failure'
|
||||
import { selectorPlaygroundModel } from '../selector-playground'
|
||||
import { eventManager } from '../event-manager'
|
||||
import { dom } from '../dom'
|
||||
import { logger } from '../logger'
|
||||
import { studioRecorder } from '../studio'
|
||||
|
||||
import dom from '../lib/dom'
|
||||
import logger from '../lib/logger'
|
||||
import eventManager from '../lib/event-manager'
|
||||
import visitFailure from './visit-failure'
|
||||
import blankContents from './blank-contents'
|
||||
import selectorPlaygroundModel from '../selector-playground/selector-playground-model'
|
||||
import studioRecorder from '../studio/studio-recorder'
|
||||
|
||||
export default class AutIframe {
|
||||
export class AutIframe {
|
||||
constructor (config) {
|
||||
this.config = config
|
||||
this.debouncedToggleSelectorPlayground = _.debounce(this.toggleSelectorPlayground, 300)
|
||||
@@ -1,15 +1,14 @@
|
||||
import _ from 'lodash'
|
||||
import { action } from 'mobx'
|
||||
|
||||
import eventManager from '../lib/event-manager'
|
||||
import selectorPlaygroundModel from '../selector-playground/selector-playground-model'
|
||||
import studioRecorder from '../studio/studio-recorder'
|
||||
import { selectorPlaygroundModel } from '../selector-playground'
|
||||
import { studioRecorder } from '../studio'
|
||||
import { eventManager } from '../event-manager'
|
||||
|
||||
export default class IframeModel {
|
||||
constructor ({ state, detachDom, removeHeadStyles, restoreDom, highlightEl, snapshotControls }) {
|
||||
export class IframeModel {
|
||||
constructor ({ state, detachDom, restoreDom, highlightEl, snapshotControls }) {
|
||||
this.state = state
|
||||
this.detachDom = detachDom
|
||||
this.removeHeadStyles = removeHeadStyles
|
||||
this.restoreDom = restoreDom
|
||||
this.highlightEl = highlightEl
|
||||
this.snapshotControls = snapshotControls
|
||||
@@ -229,6 +228,8 @@ export default class IframeModel {
|
||||
htmlAttrs,
|
||||
snapshot: finalSnapshot,
|
||||
url: this.state.url,
|
||||
// TODO: use same attr for both runner and runner-ct states.
|
||||
// these refer to the same thing - the viewport dimensions.
|
||||
viewportWidth: this.state.width,
|
||||
viewportHeight: this.state.height,
|
||||
}
|
||||
3
packages/runner-shared/src/iframe/index.ts
Normal file
3
packages/runner-shared/src/iframe/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './iframe-model'
|
||||
|
||||
export * from './aut-iframe'
|
||||
35
packages/runner-shared/src/index.ts
Normal file
35
packages/runner-shared/src/index.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
export * from './snapshot-controls'
|
||||
|
||||
export * from './visit-failure'
|
||||
|
||||
export * from './blank-contents'
|
||||
|
||||
export * from './message'
|
||||
|
||||
export * from './selector-playground'
|
||||
|
||||
export * from './script-error'
|
||||
|
||||
export * from './mobx'
|
||||
|
||||
export * from './error-messages'
|
||||
|
||||
export * from './iframe'
|
||||
|
||||
export * from './dom'
|
||||
|
||||
export * from './logger'
|
||||
|
||||
export * from './event-manager'
|
||||
|
||||
export * from './automation'
|
||||
|
||||
export * from './studio'
|
||||
|
||||
export * from './viewport-info'
|
||||
|
||||
export * from './config-file-formatted'
|
||||
|
||||
export * from './header'
|
||||
|
||||
export * from './container'
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import _ from 'lodash'
|
||||
|
||||
export default {
|
||||
export const logger = {
|
||||
log (...args) {
|
||||
console.log(...args)
|
||||
},
|
||||
@@ -1,10 +1,20 @@
|
||||
import cs from 'classnames'
|
||||
import { observer } from 'mobx-react'
|
||||
import React, { forwardRef } from 'react'
|
||||
import State from '../lib/state'
|
||||
import './message.scss'
|
||||
|
||||
interface MessageProps {
|
||||
state: State
|
||||
state: {
|
||||
messageTitle?: string
|
||||
messageControls?: unknown
|
||||
messageDescription: string
|
||||
messageType?: string
|
||||
messageStyles: {
|
||||
state: string
|
||||
styles: React.CSSProperties
|
||||
messageType: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const Message = observer(forwardRef<HTMLDivElement, MessageProps>(({ state }, ref) => {
|
||||
@@ -1,3 +1,5 @@
|
||||
@import '../variables.scss';
|
||||
|
||||
.runner {
|
||||
.message-container {
|
||||
display: flex;
|
||||
4
packages/runner-shared/src/mixins.scss
Normal file
4
packages/runner-shared/src/mixins.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
@mixin button-active {
|
||||
background-color: #e9e9e9;
|
||||
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
|
||||
}
|
||||
@@ -48,7 +48,7 @@ const browserPicker = (browsers, onLaunchBrowser) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default ({ browsers, onLaunchBrowser }) => (
|
||||
export const NoAutomation = ({ browsers, onLaunchBrowser }) => (
|
||||
<div className='runner automation-failure'>
|
||||
<div className='automation-message'>
|
||||
<p>Whoops, we can't run your tests.</p>
|
||||
@@ -3,7 +3,7 @@ import React from 'react'
|
||||
import { shallow } from 'enzyme'
|
||||
import sinon from 'sinon'
|
||||
|
||||
import NoAutomation from './no-automation'
|
||||
import { NoAutomation } from '.'
|
||||
|
||||
const noBrowsers = []
|
||||
const browsersWithChosen = [
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { namedObserver } from '../lib/mobx'
|
||||
import { namedObserver } from '../mobx'
|
||||
const ansiToHtml = require('ansi-to-html')
|
||||
|
||||
const convert = new ansiToHtml({
|
||||
@@ -24,5 +24,3 @@ export const ScriptError: React.FC<{ error: string }> = namedObserver('ScriptErr
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
export default ScriptError
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import { shallow } from 'enzyme'
|
||||
|
||||
import ScriptError from './script-error'
|
||||
import { ScriptError } from '@packages/runner-shared'
|
||||
|
||||
describe('<ScriptError />', () => {
|
||||
it('renders nothing when there is no script error', () => {
|
||||
@@ -12,9 +12,9 @@ describe('<ScriptError />', () => {
|
||||
})
|
||||
|
||||
it('renders ansi as colors', () => {
|
||||
const state = { error: { error: `Webpack Compilation Error
|
||||
const state = { error: `Webpack Compilation Error
|
||||
[0m [90m 11 | [39m it([32m'is true for actual jquery instances'[39m[33m,[39m () [33m=>[39m [0m
|
||||
@ multi ./cypress/integration/dom/jquery_spec.js main[0]` } }
|
||||
@ multi ./cypress/integration/dom/jquery_spec.js main[0]` }
|
||||
const component = shallow(<ScriptError {...state} />)
|
||||
const { dangerouslySetInnerHTML } = component.props()
|
||||
|
||||
@@ -4,8 +4,7 @@ import { action, observable } from 'mobx'
|
||||
import { observer } from 'mobx-react'
|
||||
import React, { Component } from 'react'
|
||||
import Tooltip from '@cypress/react-tooltip'
|
||||
|
||||
import eventManager from '../lib/event-manager'
|
||||
import './selector-playground.scss'
|
||||
|
||||
const defaultCopyText = 'Copy to clipboard'
|
||||
const defaultPrintText = 'Print to console'
|
||||
@@ -113,7 +112,7 @@ class SelectorPlayground extends Component {
|
||||
{' Learn more'}
|
||||
</a>
|
||||
<button className='close' onClick={this._togglePlaygroundOpen}>
|
||||
x
|
||||
x
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
@@ -155,7 +154,7 @@ x
|
||||
>
|
||||
<button onClick={this._toggleMethodPicker}>
|
||||
<i className='fas fa-caret-down'></i>
|
||||
{` cy. ${model.method}`}
|
||||
{` cy.${model.method}`}
|
||||
</button>
|
||||
<div className='method-picker'>
|
||||
{_.map(methods, (method) => (
|
||||
@@ -216,7 +215,7 @@ x
|
||||
}
|
||||
|
||||
_printToConsole = () => {
|
||||
eventManager.emit('print:selector:elements:to:console')
|
||||
this.props.eventManager.emit('print:selector:elements:to:console')
|
||||
this._setPrintText('Printed!')
|
||||
}
|
||||
|
||||
@@ -244,4 +243,4 @@ x
|
||||
}
|
||||
}
|
||||
|
||||
export default SelectorPlayground
|
||||
export { SelectorPlayground }
|
||||
@@ -31,7 +31,7 @@ function renderHighlight (container, props) {
|
||||
render(<Highlight {...props} />, container)
|
||||
}
|
||||
|
||||
export default {
|
||||
export const selectorPlaygroundHighlight = {
|
||||
render: renderHighlight,
|
||||
unmount: unmountComponentAtNode,
|
||||
}
|
||||
1
packages/runner-shared/src/selector-playground/index.js
Normal file
1
packages/runner-shared/src/selector-playground/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export * from './selector-playground-model'
|
||||
@@ -77,4 +77,4 @@ class SelectorPlaygroundModel {
|
||||
}
|
||||
}
|
||||
|
||||
export default new SelectorPlaygroundModel()
|
||||
export const selectorPlaygroundModel = new SelectorPlaygroundModel()
|
||||
@@ -4,9 +4,7 @@ import { mount, shallow } from 'enzyme'
|
||||
import sinon from 'sinon'
|
||||
|
||||
import Tooltip from '@cypress/react-tooltip'
|
||||
|
||||
import eventManager from '../lib/event-manager'
|
||||
import SelectorPlayground from './selector-playground'
|
||||
import { SelectorPlayground } from './SelectorPlayground'
|
||||
|
||||
const createModel = (props) => _.extend({
|
||||
method: 'get',
|
||||
@@ -184,8 +182,15 @@ describe('<SelectorPlayground />', () => {
|
||||
})
|
||||
|
||||
it('prints to console when clicked', () => {
|
||||
sinon.stub(eventManager, 'emit')
|
||||
const component = mount(<SelectorPlayground model={model} />)
|
||||
const eventManager = {
|
||||
emit: sinon.stub(),
|
||||
}
|
||||
const component = mount(
|
||||
<SelectorPlayground
|
||||
model={model}
|
||||
eventManager={eventManager}
|
||||
/>,
|
||||
)
|
||||
|
||||
component.find('.print-to-console').simulate('click')
|
||||
expect(eventManager.emit).to.be.calledWith('print:selector:elements:to:console')
|
||||
@@ -194,14 +199,31 @@ describe('<SelectorPlayground />', () => {
|
||||
})
|
||||
|
||||
it('sets tooltip text to "Printed!" when successful', () => {
|
||||
const component = mount(<SelectorPlayground model={model} />)
|
||||
const eventManager = {
|
||||
emit: sinon.stub(),
|
||||
}
|
||||
const component = mount(
|
||||
<SelectorPlayground
|
||||
model={model}
|
||||
eventManager={eventManager}
|
||||
/>,
|
||||
)
|
||||
|
||||
component.find('.print-to-console').simulate('click')
|
||||
expect(component.find(Tooltip).at(3)).to.have.prop('title', 'Printed!')
|
||||
})
|
||||
|
||||
it('resets tooltip text when mousing out of button', () => {
|
||||
const component = mount(<SelectorPlayground model={model} />)
|
||||
const eventManager = {
|
||||
emit: sinon.stub(),
|
||||
}
|
||||
const component = mount(
|
||||
<SelectorPlayground
|
||||
model={model}
|
||||
eventManager={eventManager}
|
||||
/>,
|
||||
)
|
||||
|
||||
const randomEl = document.createElement('div')
|
||||
|
||||
component.find('.print-to-console').simulate('click')
|
||||
1
packages/runner-shared/src/snapshot-controls/index.ts
Normal file
1
packages/runner-shared/src/snapshot-controls/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './snapshot-controls'
|
||||
@@ -4,6 +4,7 @@ import { action } from 'mobx'
|
||||
import { observer } from 'mobx-react'
|
||||
import React, { Component } from 'react'
|
||||
import Tooltip from '@cypress/react-tooltip'
|
||||
import './snapshot-controls.scss'
|
||||
|
||||
@observer
|
||||
class SnapshotControls extends Component {
|
||||
@@ -48,10 +49,10 @@ class SnapshotControls extends Component {
|
||||
<span className='snapshot-state-picker'>
|
||||
{_.map(snapshots, (snapshot, index) => (
|
||||
<button
|
||||
key={snapshot.name || index}
|
||||
className={cs({
|
||||
'state-is-selected': this.props.state.snapshot.stateIndex === index,
|
||||
})}
|
||||
key={snapshot.name || index}
|
||||
href="#"
|
||||
onClick={this._changeState(index)}
|
||||
>
|
||||
@@ -75,4 +76,4 @@ class SnapshotControls extends Component {
|
||||
})
|
||||
}
|
||||
|
||||
export default SnapshotControls
|
||||
export { SnapshotControls }
|
||||
@@ -1,3 +1,6 @@
|
||||
@import "../variables.scss";
|
||||
@import "../mixins.scss";
|
||||
|
||||
.runner {
|
||||
.snapshot-controls {
|
||||
display: flex;
|
||||
|
Before Width: | Height: | Size: 387 KiB After Width: | Height: | Size: 387 KiB |
5
packages/runner-shared/src/studio/index.js
Normal file
5
packages/runner-shared/src/studio/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './studio'
|
||||
|
||||
export * from './studio-modals'
|
||||
|
||||
export * from './studio-recorder'
|
||||
@@ -3,9 +3,9 @@ import { observer } from 'mobx-react'
|
||||
import React, { Component } from 'react'
|
||||
import { Dialog } from '@reach/dialog'
|
||||
import VisuallyHidden from '@reach/visually-hidden'
|
||||
import { eventManager } from '@packages/runner-shared'
|
||||
|
||||
import eventManager from '../lib/event-manager'
|
||||
import studioRecorder from './studio-recorder'
|
||||
import { studioRecorder } from './studio-recorder'
|
||||
|
||||
@observer
|
||||
export class StudioInstructionsModal extends Component {
|
||||
@@ -19,7 +19,10 @@ export class StudioInstructionsModal extends Component {
|
||||
>
|
||||
<div className='body'>
|
||||
<h1 className='title'>
|
||||
<i className='fas fa-magic icon' /> Studio <span className='beta'>BETA</span>
|
||||
<i className='fas fa-magic icon' />
|
||||
{' '}
|
||||
Studio
|
||||
<span className='beta'>BETA</span>
|
||||
</h1>
|
||||
<div className='content center'>
|
||||
<div className='text'>
|
||||
@@ -27,15 +30,29 @@ export class StudioInstructionsModal extends Component {
|
||||
</div>
|
||||
<div className='text center-box'>
|
||||
<ul>
|
||||
<li><pre>.check()</pre></li>
|
||||
<li><pre>.click()</pre></li>
|
||||
<li><pre>.select()</pre></li>
|
||||
<li><pre>.type()</pre></li>
|
||||
<li><pre>.uncheck()</pre></li>
|
||||
<li>
|
||||
<pre>.check()</pre>
|
||||
</li>
|
||||
<li>
|
||||
<pre>.click()</pre>
|
||||
</li>
|
||||
<li>
|
||||
<pre>.select()</pre>
|
||||
</li>
|
||||
<li>
|
||||
<pre>.type()</pre>
|
||||
</li>
|
||||
<li>
|
||||
<pre>.uncheck()</pre>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className='text'>
|
||||
This feature is currently in Beta and we will be adding more commands and abilities in the future. Your <a href='https://on.cypress.io/studio-beta' target='_blank'>feedback</a> will be highly influential to our team.
|
||||
This feature is currently in Beta and we will be adding more commands and abilities in the future. Your
|
||||
{' '}
|
||||
<a href='https://on.cypress.io/studio-beta' target='_blank' rel="noreferrer">feedback</a>
|
||||
{' '}
|
||||
will be highly influential to our team.
|
||||
</div>
|
||||
</div>
|
||||
<div className='controls'>
|
||||
@@ -44,7 +61,7 @@ export class StudioInstructionsModal extends Component {
|
||||
</div>
|
||||
<button className='close-button' onClick={this.props.close}>
|
||||
<VisuallyHidden>Close</VisuallyHidden>
|
||||
<span aria-hidden>
|
||||
<span aria-hidden={true}>
|
||||
<i className='fas fa-times' />
|
||||
</span>
|
||||
</button>
|
||||
@@ -65,10 +82,13 @@ export class StudioInitModal extends Component {
|
||||
>
|
||||
<div className='body'>
|
||||
<h1 className='title'>
|
||||
<i className='fas fa-magic icon' /> Studio <span className='beta'>BETA</span>
|
||||
<i className='fas fa-magic icon' />
|
||||
{' '}
|
||||
Studio
|
||||
<span className='beta'>BETA</span>
|
||||
</h1>
|
||||
<div className='gif'>
|
||||
<img src={require('../../static/studio.gif')} alt='Studio' />
|
||||
<img src={require('../static/studio.gif')} alt='Studio' />
|
||||
</div>
|
||||
<div className='content center'>
|
||||
<div className='text'>
|
||||
@@ -81,7 +101,7 @@ export class StudioInitModal extends Component {
|
||||
</div>
|
||||
<button className='close-button' onClick={this._close}>
|
||||
<VisuallyHidden>Close</VisuallyHidden>
|
||||
<span aria-hidden>
|
||||
<span aria-hidden={true}>
|
||||
<i className='fas fa-times' />
|
||||
</span>
|
||||
</button>
|
||||
@@ -111,13 +131,15 @@ export class StudioSaveModal extends Component {
|
||||
>
|
||||
<div className='body'>
|
||||
<h1 className='title'>
|
||||
<i className='fas fa-magic icon' /> Save New Test
|
||||
<i className='fas fa-magic icon' />
|
||||
{' '}
|
||||
Save New Test
|
||||
</h1>
|
||||
<div className='content'>
|
||||
<form onSubmit={this._save}>
|
||||
<div className='text'>
|
||||
<label className='text-strong' htmlFor='testName'>Test Name</label>
|
||||
<input id='testName' type='text' value={this.name} onChange={this._onInputChange} required />
|
||||
<input id='testName' type='text' value={this.name} required={true} onChange={this._onInputChange} />
|
||||
</div>
|
||||
<div className='center'>
|
||||
<button className='btn-main' type='submit' disabled={!this.name}>
|
||||
@@ -129,7 +151,7 @@ export class StudioSaveModal extends Component {
|
||||
</div>
|
||||
<button className='close-button' onClick={studioRecorder.closeSaveModal}>
|
||||
<VisuallyHidden>Close</VisuallyHidden>
|
||||
<span aria-hidden>
|
||||
<span aria-hidden={true}>
|
||||
<i className='fas fa-times' />
|
||||
</span>
|
||||
</button>
|
||||
@@ -158,4 +180,4 @@ const StudioModals = () => (
|
||||
</>
|
||||
)
|
||||
|
||||
export default StudioModals
|
||||
export { StudioModals }
|
||||
@@ -2,10 +2,10 @@ import React from 'react'
|
||||
import { shallow } from 'enzyme'
|
||||
import sinon from 'sinon'
|
||||
import { Dialog } from '@reach/dialog'
|
||||
import { eventManager } from '../event-manager'
|
||||
|
||||
import StudioModals, { StudioInstructionsModal, StudioInitModal, StudioSaveModal } from './studio-modals'
|
||||
import studioRecorder from './studio-recorder'
|
||||
import eventManager from '../lib/event-manager'
|
||||
import { StudioModals, StudioInstructionsModal, StudioInitModal, StudioSaveModal } from './studio-modals'
|
||||
import { studioRecorder } from './studio-recorder'
|
||||
|
||||
describe('<StudioModals />', () => {
|
||||
beforeEach(() => {
|
||||
@@ -27,7 +27,7 @@ describe('<StudioModals />', () => {
|
||||
|
||||
describe('<StudioInstructionsModal />', () => {
|
||||
it('passes open prop to dialog', () => {
|
||||
const component = shallow(<StudioInstructionsModal open={false} close={() => {}} />)
|
||||
const component = shallow(<StudioInstructionsModal open={false} close={sinon.stub()} />)
|
||||
|
||||
expect(component.find(Dialog)).to.have.prop('isOpen', false)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { action, computed, observable } from 'mobx'
|
||||
import { $ } from '@packages/driver'
|
||||
import $driverUtils from '@packages/driver/src/cypress/utils'
|
||||
|
||||
import eventManager from '../lib/event-manager'
|
||||
import { eventManager } from '@packages/runner-shared'
|
||||
|
||||
const saveErrorMessage = (message) => {
|
||||
return `\
|
||||
@@ -542,4 +541,4 @@ export class StudioRecorder {
|
||||
}
|
||||
}
|
||||
|
||||
export default new StudioRecorder()
|
||||
export const studioRecorder = new StudioRecorder()
|
||||
@@ -1,9 +1,9 @@
|
||||
import sinon from 'sinon'
|
||||
import $ from 'jquery'
|
||||
import driver from '@packages/driver'
|
||||
import { eventManager } from '@packages/runner-shared'
|
||||
|
||||
import studioRecorder, { StudioRecorder } from './studio-recorder'
|
||||
import eventManager from '../lib/event-manager'
|
||||
import { StudioRecorder, studioRecorder } from './studio-recorder'
|
||||
|
||||
const createEvent = (props) => {
|
||||
return {
|
||||
@@ -42,7 +42,7 @@ describe('StudioRecorder', () => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('exports a singleton by default', () => {
|
||||
it('exports a singleton by named export', () => {
|
||||
expect(studioRecorder).to.be.instanceOf(StudioRecorder)
|
||||
})
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { Component } from 'react'
|
||||
import Tooltip from '@cypress/react-tooltip'
|
||||
import cs from 'classnames'
|
||||
import { eventManager } from '@packages/runner-shared'
|
||||
|
||||
import eventManager from '../lib/event-manager'
|
||||
import { StudioInstructionsModal } from './studio-modals'
|
||||
|
||||
class Studio extends Component {
|
||||
@@ -17,19 +17,22 @@ class Studio extends Component {
|
||||
|
||||
return (
|
||||
<div className='header-popup studio'>
|
||||
{/* eslint-disable react/jsx-no-bind */}
|
||||
<StudioInstructionsModal open={this.state.modalOpen} close={() => this.setState({ modalOpen: false })} />
|
||||
<div className='text-block'>
|
||||
<span className={cs('icon', { 'is-active': model.isActive && !model.isFailed && hasUrl })}>
|
||||
<i className='fas' />
|
||||
</span>{' '}
|
||||
<span className='title'>Studio</span>{' '}
|
||||
</span>
|
||||
{' '}
|
||||
<span className='title'>Studio</span>
|
||||
{' '}
|
||||
<span className='beta'>Beta</span>
|
||||
</div>
|
||||
<div className='text-block'>
|
||||
<a href='#' onClick={this._showModal} className={cs('available-commands', { 'link-disabled': model.isLoading })}>Available Commands</a>
|
||||
<a href='#' className={cs('available-commands', { 'link-disabled': model.isLoading })} onClick={this._showModal}>Available Commands</a>
|
||||
</div>
|
||||
<div className='text-block'>
|
||||
<a href={!model.isLoading ? 'https://on.cypress.io/studio-beta' : undefined} target='_blank' className={cs('give-feedback', { 'link-disabled': model.isLoading })}>Give Feedback</a>
|
||||
<a href={!model.isLoading ? 'https://on.cypress.io/studio-beta' : undefined} target='_blank' className={cs('give-feedback', { 'link-disabled': model.isLoading })} rel="noreferrer">Give Feedback</a>
|
||||
</div>
|
||||
<div className='studio-controls'>
|
||||
<Tooltip
|
||||
@@ -98,4 +101,4 @@ class Studio extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default Studio
|
||||
export { Studio }
|
||||
@@ -1,12 +1,11 @@
|
||||
import React from 'react'
|
||||
import { shallow } from 'enzyme'
|
||||
import sinon from 'sinon'
|
||||
|
||||
import Tooltip from '@cypress/react-tooltip'
|
||||
|
||||
import Studio from './studio'
|
||||
import { Studio } from './studio'
|
||||
import { StudioInstructionsModal } from './studio-modals'
|
||||
import eventManager from '../lib/event-manager'
|
||||
import { eventManager } from '../event-manager'
|
||||
|
||||
const createModel = (props) => {
|
||||
return {
|
||||
21
packages/runner-shared/src/variables.scss
Normal file
21
packages/runner-shared/src/variables.scss
Normal file
@@ -0,0 +1,21 @@
|
||||
$message-height: 33px;
|
||||
|
||||
%clearfix {
|
||||
&:before,
|
||||
&:after {
|
||||
content: "";
|
||||
display: table;
|
||||
}
|
||||
&:after {
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
$success: #08c18d;
|
||||
$error: #e94f5f;
|
||||
$link-text: #3380FF;
|
||||
$reporter-min-width: 450px;
|
||||
$message-height: 33px;
|
||||
$font-mono: Consolas, Monaco, 'Andale Mono', monospace;
|
||||
$font-sans: 'Mulish', 'Helvetica Neue', 'Arial', sans-serif;
|
||||
$open-sans: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
73
packages/runner-shared/src/viewport-info/index.tsx
Normal file
73
packages/runner-shared/src/viewport-info/index.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import React from 'react'
|
||||
import cs from 'classnames'
|
||||
|
||||
import { namedObserver } from '../mobx'
|
||||
import { configFileFormatted } from '../config-file-formatted'
|
||||
|
||||
interface ViewportInfoProps {
|
||||
showingViewportMenu: boolean
|
||||
width: number
|
||||
height: number
|
||||
displayScale: number | undefined
|
||||
config: {
|
||||
configFile: unknown // Similar to Cypress.RuntimeConfigOptions, but has some extra properties
|
||||
[key: string]: unknown
|
||||
}
|
||||
defaults: {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
toggleViewportMenu: () => void
|
||||
}
|
||||
|
||||
export const ViewportInfo: React.FC<ViewportInfoProps> = namedObserver('ViewportInfo', ({
|
||||
showingViewportMenu,
|
||||
width,
|
||||
height,
|
||||
displayScale,
|
||||
defaults,
|
||||
config,
|
||||
toggleViewportMenu,
|
||||
}: ViewportInfoProps) => {
|
||||
return (
|
||||
<ul className='menu'>
|
||||
<li className={cs('viewport-info', { 'menu-open': showingViewportMenu })}>
|
||||
<button onClick={toggleViewportMenu}>
|
||||
{width}
|
||||
{' '}
|
||||
<span className='the-x'>x</span>
|
||||
{' '}
|
||||
{height}
|
||||
{' '}
|
||||
<span className='viewport-scale'>
|
||||
{displayScale && `(${displayScale}%)`}
|
||||
</span>
|
||||
<i className='fas fa-fw fa-info-circle'></i>
|
||||
</button>
|
||||
<div className='popup-menu viewport-menu'>
|
||||
{/* eslint-disable react/jsx-one-expression-per-line */}
|
||||
<p>The <strong>viewport</strong> determines the width and height of your application. By default the viewport will be
|
||||
<strong>{` ${defaults.width}`}px</strong> by
|
||||
<strong>{` ${defaults.height}`}px</strong> unless specified by a
|
||||
{' '}<code>cy.viewport</code> command.
|
||||
</p>
|
||||
<p>Additionally you can override the default viewport dimensions by specifying these values in your {configFileFormatted(config.configFile)}.</p>
|
||||
<pre>{/* eslint-disable indent */}
|
||||
{`{
|
||||
"viewportWidth": ${defaults.width},
|
||||
"viewportHeight": ${defaults.height}
|
||||
}`}
|
||||
</pre>
|
||||
{/* eslint-enable indent */}
|
||||
<p>
|
||||
<a href='https://on.cypress.io/viewport' target='_blank' rel='noreferrer'>
|
||||
<i className='fas fa-info-circle'></i>
|
||||
Read more about viewport here.
|
||||
</a>
|
||||
</p>
|
||||
{/* eslint-enable react/jsx-one-expression-per-line */}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
)
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
export default (props) => {
|
||||
export const visitFailure = (props) => {
|
||||
const { status, statusText, contentType } = props
|
||||
|
||||
const getContentType = () => {
|
||||
5
packages/runner-shared/test/.mocharc.json
Normal file
5
packages/runner-shared/test/.mocharc.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"file": "test/helper.js",
|
||||
"require": "../web-config/node-register",
|
||||
"extension": "ts,jsx,tsx,js"
|
||||
}
|
||||
45
packages/runner-shared/test/helper.js
Normal file
45
packages/runner-shared/test/helper.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { returnMockRequire, register } from '@packages/web-config/node-jsdom-setup'
|
||||
import sinon from 'sinon'
|
||||
|
||||
const driverMock = {}
|
||||
|
||||
register({
|
||||
enzyme: require('enzyme'),
|
||||
EnzymeAdapter: require('enzyme-adapter-react-16'),
|
||||
chaiEnzyme: require('chai-enzyme'),
|
||||
requireOverride (depPath) {
|
||||
if (depPath === '@packages/driver') {
|
||||
return driverMock
|
||||
}
|
||||
|
||||
// TODO: refactor w/ regex
|
||||
if (depPath.includes('.gif')) {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const io = returnMockRequire('@packages/socket/lib/browser', { client: {} })
|
||||
|
||||
io.client.connect = sinon.stub().returns({ emit: () => {}, on: () => {} })
|
||||
|
||||
const _useFakeTimers = sinon.useFakeTimers
|
||||
let timers = []
|
||||
|
||||
sinon.useFakeTimers = function (...args) {
|
||||
const ret = _useFakeTimers.apply(this, args)
|
||||
|
||||
timers.push(ret)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
driverMock.$ = sinon.stub().throws('$ called without being stubbed')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
timers.forEach((clock) => {
|
||||
return clock.restore()
|
||||
})
|
||||
|
||||
timers = []
|
||||
})
|
||||
7
packages/runner-shared/tsconfig.json
Normal file
7
packages/runner-shared/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../ts/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
"experimentalDecorators": true
|
||||
}
|
||||
}
|
||||
@@ -6,16 +6,17 @@ import React, { Component } from 'react'
|
||||
import { findDOMNode } from 'react-dom'
|
||||
import { Reporter } from '@packages/reporter'
|
||||
import { $ } from '@packages/driver'
|
||||
import {
|
||||
Message,
|
||||
errorMessages,
|
||||
StudioModals,
|
||||
Header,
|
||||
} from '@packages/runner-shared'
|
||||
|
||||
import errorMessages from '../errors/error-messages'
|
||||
import util from '../lib/util'
|
||||
import State from '../lib/state'
|
||||
|
||||
import Header from '../header/header'
|
||||
import Iframes from '../iframe/iframes'
|
||||
import Message from '../message/message'
|
||||
import Resizer from './resizer'
|
||||
import StudioModals from '../studio/studio-modals'
|
||||
|
||||
@observer
|
||||
class App extends Component {
|
||||
@@ -53,7 +54,7 @@ class App extends Component {
|
||||
className='runner container'
|
||||
style={{ left: this.props.state.absoluteReporterWidth }}
|
||||
>
|
||||
<Header ref='header' {...this.props} />
|
||||
<Header ref='header' runner='e2e' {...this.props} />
|
||||
<Iframes ref='iframes' {...this.props} />
|
||||
<Message ref='message' state={this.props.state} />
|
||||
{this.props.children}
|
||||
@@ -250,7 +251,6 @@ App.propTypes = {
|
||||
on: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
state: PropTypes.instanceOf(State).isRequired,
|
||||
}
|
||||
|
||||
export default App
|
||||
|
||||
@@ -3,8 +3,8 @@ import { shallow } from 'enzyme'
|
||||
import sinon from 'sinon'
|
||||
import driver from '@packages/driver'
|
||||
|
||||
import { Message } from '@packages/runner-shared'
|
||||
import * as reporter from '@packages/reporter'
|
||||
import Message from '../message/message'
|
||||
import State from '../lib/state'
|
||||
|
||||
const Reporter = reporter.Reporter = () => <div />
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import { observer } from 'mobx-react'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { Component } from 'react'
|
||||
|
||||
import automation from '../lib/automation'
|
||||
import eventManager from '../lib/event-manager'
|
||||
import State from '../lib/state'
|
||||
import util from '../lib/util'
|
||||
|
||||
import App from './app'
|
||||
import AutomationDisconnected from '../errors/automation-disconnected'
|
||||
import NoAutomation from '../errors/no-automation'
|
||||
import NoSpec from '../errors/no-spec'
|
||||
|
||||
const automationElementId = '__cypress-string'
|
||||
|
||||
@observer
|
||||
class Container extends Component {
|
||||
constructor (...args) {
|
||||
super(...args)
|
||||
|
||||
this.randomString = `${Math.random()}`
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.props.eventManager.addGlobalListeners(this.props.state, {
|
||||
element: automationElementId,
|
||||
string: this.randomString,
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
switch (this.props.state.automation) {
|
||||
case automation.CONNECTING:
|
||||
return this._automationElement()
|
||||
case automation.MISSING:
|
||||
return this._noAutomation()
|
||||
case automation.DISCONNECTED:
|
||||
return this._automationDisconnected()
|
||||
case automation.CONNECTED:
|
||||
default:
|
||||
return this.props.util.hasSpecFile() ? this._app() : this._noSpec()
|
||||
}
|
||||
}
|
||||
|
||||
_automationElement () {
|
||||
return (
|
||||
<div id={automationElementId} style={{ display: 'none' }}>
|
||||
{this.randomString}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
_app () {
|
||||
return (
|
||||
<App {...this.props}>
|
||||
{this._automationElement()}
|
||||
</App>
|
||||
)
|
||||
}
|
||||
|
||||
_noSpec () {
|
||||
return (
|
||||
<NoSpec config={this.props.config} onHashChange={this._checkSpecFile}>
|
||||
{this._automationElement()}
|
||||
</NoSpec>
|
||||
)
|
||||
}
|
||||
|
||||
_checkSpecFile = () => {
|
||||
if (this.props.util.hasSpecFile()) {
|
||||
this.forceUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
_noAutomation () {
|
||||
return (
|
||||
<NoAutomation
|
||||
browsers={this.props.config.browsers}
|
||||
onLaunchBrowser={(browser) => this.props.eventManager.launchBrowser(browser)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
_automationDisconnected () {
|
||||
return <AutomationDisconnected onReload={this.props.eventManager.launchBrowser} />
|
||||
}
|
||||
}
|
||||
|
||||
Container.defaultProps = {
|
||||
eventManager,
|
||||
util,
|
||||
}
|
||||
|
||||
Container.propTypes = {
|
||||
config: PropTypes.object.isRequired,
|
||||
state: PropTypes.instanceOf(State),
|
||||
}
|
||||
|
||||
export { automationElementId }
|
||||
|
||||
export default Container
|
||||
@@ -1,19 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
export default ({ onReload }) => (
|
||||
<div className='runner automation-failure'>
|
||||
<div className='automation-message automation-disconnected'>
|
||||
<p>Whoops, the Cypress extension has disconnected.</p>
|
||||
<p className='muted'>Cypress cannot run tests without this extension.</p>
|
||||
<button onClick={onReload}>
|
||||
<i className='fas fa-sync-alt'></i> Reload the Browser
|
||||
</button>
|
||||
<div className='helper-line'>
|
||||
<a href='https://on.cypress.io/launching-browsers' target='_blank'>
|
||||
<i className='fas fa-question-circle'></i>
|
||||
Why am I seeing this message?
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -1,61 +0,0 @@
|
||||
import _ from 'lodash'
|
||||
import React from 'react'
|
||||
import { BrowserIcon, Dropdown } from '@packages/ui-components'
|
||||
|
||||
const displayName = (name) => _.capitalize(name)
|
||||
|
||||
const noBrowsers = () => (
|
||||
<div>
|
||||
<p className='muted'>
|
||||
<small>
|
||||
We couldn't find any supported browsers capable of running Cypress on your machine.
|
||||
</small>
|
||||
</p>
|
||||
<a href='https://www.google.com/chrome/browser/desktop' className='btn btn-primary btn-lg' target='_blank' rel='noopener noreferrer'>
|
||||
<i className='fas fa-chrome'></i>
|
||||
Download Chrome
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
|
||||
const browser = (browser) => (
|
||||
<span>
|
||||
<BrowserIcon browserName={browser.displayName} />
|
||||
<span>Run {displayName(browser.displayName)} {browser.majorVersion}</span>
|
||||
</span>
|
||||
)
|
||||
|
||||
const browserPicker = (browsers, onLaunchBrowser) => {
|
||||
const chosenBrowser = _.find(browsers, { default: true }) || browsers[0]
|
||||
const otherBrowsers = _(browsers)
|
||||
.without(chosenBrowser)
|
||||
.map((browser) => _.extend({}, browser, { key: browser.name + browser.version }))
|
||||
.value()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p className='muted'>This browser was not launched through Cypress. Tests cannot run.</p>
|
||||
<Dropdown
|
||||
chosen={chosenBrowser}
|
||||
others={otherBrowsers}
|
||||
onSelect={onLaunchBrowser}
|
||||
renderItem={browser}
|
||||
keyProperty='key'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ({ browsers, onLaunchBrowser }) => (
|
||||
<div className='runner automation-failure'>
|
||||
<div className='automation-message'>
|
||||
<p>Whoops, we can't run your tests.</p>
|
||||
{browsers.length ? browserPicker(browsers, onLaunchBrowser) : noBrowsers()}
|
||||
<div className='helper-line'>
|
||||
<a className='helper-docs-link' href='https://on.cypress.io/launching-browsers' target='_blank'>
|
||||
<i className='fas fa-question-circle'></i> Why am I seeing this message?
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react'
|
||||
import eventManager from '../lib/event-manager'
|
||||
import { eventManager } from '@packages/runner-shared'
|
||||
|
||||
class NoSpec extends Component {
|
||||
componentDidMount () {
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { observer } from 'mobx-react'
|
||||
import React from 'react'
|
||||
const ansiToHtml = require('ansi-to-html')
|
||||
|
||||
const convert = new ansiToHtml({
|
||||
fg: '#000',
|
||||
bg: '#fff',
|
||||
newline: false,
|
||||
escapeXML: true,
|
||||
stream: false,
|
||||
})
|
||||
const ScriptError = observer(({ error }) => {
|
||||
if (!error) return null
|
||||
|
||||
const errorHTML = convert.toHtml(error.error)
|
||||
|
||||
return (
|
||||
<pre className='script-error' dangerouslySetInnerHTML={{ __html: errorHTML }}>
|
||||
</pre>
|
||||
)
|
||||
})
|
||||
|
||||
export default ScriptError
|
||||
@@ -1,175 +0,0 @@
|
||||
import cs from 'classnames'
|
||||
|
||||
import { action, computed, observable } from 'mobx'
|
||||
import { observer } from 'mobx-react'
|
||||
import React, { Component, createRef } from 'react'
|
||||
import Tooltip from '@cypress/react-tooltip'
|
||||
import { $ } from '@packages/driver'
|
||||
|
||||
import { configFileFormatted } from '../lib/config-file-formatted'
|
||||
import SelectorPlayground from '../selector-playground/selector-playground'
|
||||
import selectorPlaygroundModel from '../selector-playground/selector-playground-model'
|
||||
import Studio from '../studio/studio'
|
||||
import studioRecorder from '../studio/studio-recorder'
|
||||
import eventManager from '../lib/event-manager'
|
||||
|
||||
@observer
|
||||
export default class Header extends Component {
|
||||
@observable showingViewportMenu = false
|
||||
@observable urlInput = ''
|
||||
urlInputRef = createRef()
|
||||
|
||||
render () {
|
||||
const { state, config } = this.props
|
||||
|
||||
return (
|
||||
<header
|
||||
ref='header'
|
||||
className={cs({
|
||||
'showing-selector-playground': selectorPlaygroundModel.isOpen,
|
||||
'showing-studio': studioRecorder.isOpen,
|
||||
})}
|
||||
>
|
||||
<div className='sel-url-wrap'>
|
||||
<Tooltip
|
||||
title='Open Selector Playground'
|
||||
visible={selectorPlaygroundModel.isOpen || studioRecorder.isOpen ? false : null}
|
||||
wrapperClassName='selector-playground-toggle-tooltip-wrapper'
|
||||
className='cy-tooltip'
|
||||
>
|
||||
<button
|
||||
aria-label='Open Selector Playground'
|
||||
className='header-button selector-playground-toggle'
|
||||
onClick={this._togglePlaygroundOpen}
|
||||
disabled={state.isLoading || state.isRunning || studioRecorder.isOpen}
|
||||
>
|
||||
<i aria-hidden="true" className='fas fa-crosshairs' />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<div className={cs('menu-cover', { 'menu-cover-display': this._studioNeedsUrl })} />
|
||||
<form
|
||||
className={cs('url-container', {
|
||||
'loading': state.isLoadingUrl,
|
||||
'highlighted': state.highlightUrl,
|
||||
'menu-open': this._studioNeedsUrl,
|
||||
})}
|
||||
onSubmit={this._visitUrlInput}
|
||||
>
|
||||
<input
|
||||
ref={this.urlInputRef}
|
||||
type='text'
|
||||
className={cs('url', { 'input-active': this._studioNeedsUrl })}
|
||||
value={this._studioNeedsUrl ? this.urlInput : state.url}
|
||||
readOnly={!this._studioNeedsUrl}
|
||||
onChange={this._onUrlInput}
|
||||
onClick={this._openUrl}
|
||||
/>
|
||||
<div className='popup-menu url-menu'>
|
||||
<p><strong>Please enter a valid URL to visit.</strong></p>
|
||||
<div className='menu-buttons'>
|
||||
<button type='button' className='btn-cancel' onClick={this._cancelStudio}>Cancel</button>
|
||||
<button type='submit' className='btn-submit' disabled={!this.urlInput}>Go <i className='fas fa-arrow-right' /></button>
|
||||
</div>
|
||||
</div>
|
||||
<span className='loading-container'>
|
||||
...loading <i className='fas fa-spinner fa-pulse' />
|
||||
</span>
|
||||
</form>
|
||||
</div>
|
||||
<ul className='menu'>
|
||||
<li className={cs('viewport-info', { 'menu-open': this.showingViewportMenu })}>
|
||||
<button onClick={this._toggleViewportMenu}>
|
||||
{state.width} <span className='the-x'>x</span> {state.height} <span className='viewport-scale'>({state.displayScale}%)</span>
|
||||
<i className='fas fa-fw fa-info-circle' />
|
||||
</button>
|
||||
<div className='popup-menu viewport-menu'>
|
||||
<p>The <strong>viewport</strong> determines the width and height of your application. By default the viewport will be <strong>{state.defaults.width}px</strong> by <strong>{state.defaults.height}px</strong> unless specified by a <code>cy.viewport</code> command.</p>
|
||||
<p>Additionally you can override the default viewport dimensions by specifying these values in your {configFileFormatted(config.configFile)}.</p>
|
||||
<pre>{/* eslint-disable indent */}
|
||||
{`{
|
||||
"viewportWidth": ${state.defaults.width},
|
||||
"viewportHeight": ${state.defaults.height}
|
||||
}`}
|
||||
</pre>{/* eslint-enable indent */}
|
||||
<p>
|
||||
<a href='https://on.cypress.io/viewport' target='_blank'>
|
||||
<i className='fas fa-info-circle' />
|
||||
Read more about viewport here.
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<SelectorPlayground model={selectorPlaygroundModel} />
|
||||
<Studio model={studioRecorder} hasUrl={!!state.url} />
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
@action componentDidMount () {
|
||||
this.previousSelectorPlaygroundOpen = selectorPlaygroundModel.isOpen
|
||||
this.previousRecorderIsOpen = studioRecorder.isOpen
|
||||
|
||||
this.urlInput = this.props.config.baseUrl ? `${this.props.config.baseUrl}/` : ''
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
if (selectorPlaygroundModel.isOpen !== this.previousSelectorPlaygroundOpen) {
|
||||
this._updateWindowDimensions()
|
||||
this.previousSelectorPlaygroundOpen = selectorPlaygroundModel.isOpen
|
||||
}
|
||||
|
||||
if (studioRecorder.isOpen !== this.previousRecorderIsOpen) {
|
||||
this._updateWindowDimensions()
|
||||
this.previousRecorderIsOpen = studioRecorder.isOpen
|
||||
}
|
||||
|
||||
if (this._studioNeedsUrl) {
|
||||
this.urlInputRef.current.focus()
|
||||
}
|
||||
}
|
||||
|
||||
_updateWindowDimensions = () => {
|
||||
this.props.state.updateWindowDimensions({
|
||||
headerHeight: $(this.refs.header).outerHeight(),
|
||||
})
|
||||
}
|
||||
|
||||
_togglePlaygroundOpen = () => {
|
||||
selectorPlaygroundModel.toggleOpen()
|
||||
}
|
||||
|
||||
_openUrl = () => {
|
||||
if (this._studioNeedsUrl) return
|
||||
|
||||
window.open(this.props.state.url)
|
||||
}
|
||||
|
||||
@computed get _studioNeedsUrl () {
|
||||
return studioRecorder.needsUrl && !this.props.state.url
|
||||
}
|
||||
|
||||
@action _onUrlInput = (e) => {
|
||||
if (!this._studioNeedsUrl) return
|
||||
|
||||
this.urlInput = e.target.value
|
||||
}
|
||||
|
||||
@action _visitUrlInput = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!this._studioNeedsUrl) return
|
||||
|
||||
studioRecorder.visitUrl(this.urlInput)
|
||||
|
||||
this.urlInput = ''
|
||||
}
|
||||
|
||||
_cancelStudio = () => {
|
||||
eventManager.emit('studio:cancel')
|
||||
}
|
||||
|
||||
@action _toggleViewportMenu = () => {
|
||||
this.showingViewportMenu = !this.showingViewportMenu
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
export default () => {
|
||||
return `
|
||||
<style>
|
||||
body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img,a img{border:none;}address,caption,cite,code,dfn,em,strong,th,var,optgroup{font-style:inherit;font-weight:inherit;}del,ins{text-decoration:none;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym{border:0;font-variant:normal;}sup{vertical-align:baseline;}sub{vertical-align:baseline;}legend{color:#000;}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
color: #333;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: #EEE;
|
||||
border-radius: 6px;
|
||||
padding: 30px 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
svg {
|
||||
display: inline-block;
|
||||
stroke-width: 0;
|
||||
stroke: currentColor;
|
||||
fill: currentColor;
|
||||
margin: 20px 0 10px;
|
||||
width: 62px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 21px;
|
||||
font-weight: 200;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
kbd {
|
||||
background-color: #777;
|
||||
border-radius: 3px;
|
||||
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25);
|
||||
color: #fff;
|
||||
display: inline-block;
|
||||
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
font-size: 90%;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #FFF;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover,
|
||||
a:focus,
|
||||
a:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
ul {
|
||||
background-color: #DDD;
|
||||
border-radius: 10px;
|
||||
text-align: left;
|
||||
margin: 0 auto 10px;
|
||||
max-width: 220px;
|
||||
padding: 20px 20px 20px 40px;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style: decimal;
|
||||
list-style-position: outside;
|
||||
margin: 4px 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class='container'>
|
||||
<svg viewBox="0 0 30 32">
|
||||
<path d="M25.143 17.714v8.571q0 0.464-0.339 0.804t-0.804 0.339h-6.857v-6.857h-4.571v6.857h-6.857q-0.464 0-0.804-0.339t-0.339-0.804v-8.571q0-0.018 0.009-0.054t0.009-0.054l10.268-8.464 10.268 8.464q0.018 0.036 0.018 0.107zM29.125 16.482l-1.107 1.321q-0.143 0.161-0.375 0.196h-0.054q-0.232 0-0.375-0.125l-12.357-10.304-12.357 10.304q-0.214 0.143-0.429 0.125-0.232-0.036-0.375-0.196l-1.107-1.321q-0.143-0.179-0.125-0.42t0.196-0.384l12.839-10.696q0.571-0.464 1.357-0.464t1.357 0.464l4.357 3.643v-3.482q0-0.25 0.161-0.411t0.411-0.161h3.429q0.25 0 0.411 0.161t0.161 0.411v7.286l3.911 3.25q0.179 0.143 0.196 0.384t-0.125 0.42z"></path>
|
||||
</svg>
|
||||
<p>This is the default blank page.</p>
|
||||
<p>To test your web application:</p>
|
||||
<ul>
|
||||
<li>Start your app's server</li>
|
||||
<li>
|
||||
<kbd>
|
||||
<a href='https://on.cypress.io/visit' target='_blank'>cy.visit()</a>
|
||||
</kbd>
|
||||
your app
|
||||
</li>
|
||||
<li>Begin writing tests</li>
|
||||
</ul>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -3,15 +3,16 @@ import { action, autorun } from 'mobx'
|
||||
import { observer } from 'mobx-react'
|
||||
import React, { Component } from 'react'
|
||||
import { $ } from '@packages/driver'
|
||||
import {
|
||||
SnapshotControls,
|
||||
ScriptError,
|
||||
IframeModel,
|
||||
selectorPlaygroundModel,
|
||||
AutIframe,
|
||||
logger,
|
||||
studioRecorder,
|
||||
} from '@packages/runner-shared'
|
||||
|
||||
import AutIframe from './aut-iframe'
|
||||
import ScriptError from '../errors/script-error'
|
||||
import SnapshotControls from './snapshot-controls'
|
||||
|
||||
import IframeModel from './iframe-model'
|
||||
import logger from '../lib/logger'
|
||||
import selectorPlaygroundModel from '../selector-playground/selector-playground-model'
|
||||
import studioRecorder from '../studio/studio-recorder'
|
||||
import util from '../lib/util'
|
||||
|
||||
@observer
|
||||
@@ -102,7 +103,6 @@ export default class Iframes extends Component {
|
||||
|
||||
this.iframeModel = new IframeModel({
|
||||
state: this.props.state,
|
||||
removeHeadStyles: this.autIframe.removeHeadStyles,
|
||||
restoreDom: this.autIframe.restoreDom,
|
||||
highlightEl: this.autIframe.highlightEl,
|
||||
detachDom: this.autIframe.detachDom,
|
||||
@@ -122,7 +122,13 @@ export default class Iframes extends Component {
|
||||
}
|
||||
|
||||
@action _setScriptError = (err) => {
|
||||
this.props.state.scriptError = err
|
||||
if (err && 'error' in err) {
|
||||
this.props.state.scriptError = err.error
|
||||
}
|
||||
|
||||
if (!err) {
|
||||
this.props.state.scriptError = null
|
||||
}
|
||||
}
|
||||
|
||||
_run = (config) => {
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
export default (props) => {
|
||||
const { status, statusText, contentType } = props
|
||||
|
||||
const getContentType = () => {
|
||||
if (!contentType) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return `(${contentType})`
|
||||
}
|
||||
|
||||
const getStatus = () => {
|
||||
if (!status) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return `<p>${status} - ${statusText} ${getContentType()}</p>`
|
||||
}
|
||||
|
||||
return `
|
||||
<style>
|
||||
body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img,a img{border:none;}address,caption,cite,code,dfn,em,strong,th,var,optgroup{font-style:inherit;font-weight:inherit;}del,ins{text-decoration:none;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym{border:0;font-variant:normal;}sup{vertical-align:baseline;}sub{vertical-align:baseline;}legend{color:#000;}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
color: #333;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: #fcf8e3;
|
||||
border-radius: 6px;
|
||||
padding: 48px 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
svg {
|
||||
display: inline-block;
|
||||
fill: currentColor;
|
||||
margin: 20px 0 10px;
|
||||
stroke-width: 0;
|
||||
stroke: currentColor;
|
||||
width: 62px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 21px;
|
||||
font-weight: 200;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #476fc9;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover,
|
||||
a:focus,
|
||||
a:active {
|
||||
color: #2c4d97;
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container">
|
||||
<svg viewBox="0 0 32 30">
|
||||
<path d="M18.286 24.554v-3.393q0-0.25-0.17-0.42t-0.402-0.17h-3.429q-0.232 0-0.402 0.17t-0.17 0.42v3.393q0 0.25 0.17 0.42t0.402 0.17h3.429q0.232 0 0.402-0.17t0.17-0.42zM18.25 17.875l0.321-8.196q0-0.214-0.179-0.339-0.232-0.196-0.429-0.196h-3.929q-0.196 0-0.429 0.196-0.179 0.125-0.179 0.375l0.304 8.161q0 0.179 0.179 0.295t0.429 0.116h3.304q0.25 0 0.42-0.116t0.188-0.295zM18 1.196l13.714 25.143q0.625 1.125-0.036 2.25-0.304 0.518-0.83 0.821t-1.134 0.304h-27.429q-0.607 0-1.134-0.304t-0.83-0.821q-0.661-1.125-0.036-2.25l13.714-25.143q0.304-0.554 0.839-0.875t1.161-0.321 1.161 0.321 0.839 0.875z"></path>
|
||||
</svg>
|
||||
<p>Sorry, we could not load:</p>
|
||||
<p>
|
||||
<a href="${props.url}" target="_blank" rel="noopener noreferrer">${props.url}</a>
|
||||
</p>
|
||||
${getStatus()}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export default {
|
||||
CONNECTING: 'CONNECTING',
|
||||
MISSING: 'MISSING',
|
||||
CONNECTED: 'CONNECTED',
|
||||
DISCONNECTED: 'DISCONNECTED',
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import React from 'react'
|
||||
import { isUndefined } from 'lodash'
|
||||
|
||||
const configFileFormatted = (configFile) => {
|
||||
if (configFile === false) {
|
||||
return <><code>cypress.json</code> file (currently disabled by <code>--config-file false</code>)</>
|
||||
}
|
||||
|
||||
if (isUndefined(configFile) || configFile === 'cypress.json') {
|
||||
return <><code>cypress.json</code> file</>
|
||||
}
|
||||
|
||||
return <>custom config file <code>{configFile}</code></>
|
||||
}
|
||||
|
||||
export {
|
||||
configFileFormatted,
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import _ from 'lodash'
|
||||
|
||||
export default {
|
||||
log (...args) {
|
||||
console.log(...args)
|
||||
},
|
||||
|
||||
logError (...args) {
|
||||
console.error(...args)
|
||||
},
|
||||
|
||||
clearLog () {
|
||||
if (console.clear) console.clear()
|
||||
},
|
||||
|
||||
logFormatted (consoleProps) {
|
||||
if (_.isEmpty(consoleProps)) return
|
||||
|
||||
this._logValues(consoleProps)
|
||||
this._logGroups(consoleProps)
|
||||
this._logTable(consoleProps)
|
||||
},
|
||||
|
||||
_logValues (consoleProps) {
|
||||
const formattedLog = this._formatted(_.omit(consoleProps, 'groups', 'table'))
|
||||
|
||||
_.each(formattedLog, (value, key) => {
|
||||
// don't log empty strings
|
||||
// _.trim([]) returns '' but we want to log empty arrays, so account for that
|
||||
if (_.trim(value) === '' && !_.isArray(value)) return
|
||||
|
||||
this.log(`%c${key}`, 'font-weight: bold', value)
|
||||
})
|
||||
},
|
||||
|
||||
_formatted (consoleProps) {
|
||||
const maxKeyLength = this._getMaxKeyLength(consoleProps)
|
||||
|
||||
return _.reduce(consoleProps, (memo, value, key) => {
|
||||
const append = ': '
|
||||
|
||||
key = _.chain(key + append).capitalize().padEnd(maxKeyLength + append.length, ' ').value()
|
||||
memo[key] = value
|
||||
|
||||
return memo
|
||||
}, {})
|
||||
},
|
||||
|
||||
_getMaxKeyLength (obj) {
|
||||
const lengths = _(obj).keys().map('length').value()
|
||||
|
||||
return Math.max(...lengths)
|
||||
},
|
||||
|
||||
_logGroups (consoleProps) {
|
||||
const groups = this._getGroups(consoleProps)
|
||||
|
||||
_.each(groups, (group) => {
|
||||
console.groupCollapsed(group.name)
|
||||
_.each(group.items, (value, key) => {
|
||||
if (group.label === false) {
|
||||
this.log(value)
|
||||
} else {
|
||||
this.log(`%c${key}`, 'color: blue', value)
|
||||
}
|
||||
})
|
||||
|
||||
console.groupEnd()
|
||||
})
|
||||
},
|
||||
|
||||
_getGroups (consoleProps) {
|
||||
const groups = _.result(consoleProps, 'groups')
|
||||
|
||||
if (!groups) return
|
||||
|
||||
return _.map(groups, (group) => {
|
||||
group.items = this._formatted(group.items)
|
||||
|
||||
return group
|
||||
})
|
||||
},
|
||||
|
||||
_logTable (consoleProps) {
|
||||
if (isMultiEntryTable(consoleProps.table)) {
|
||||
_.each(
|
||||
_.sortBy(consoleProps.table, (val, key) => key),
|
||||
(table) => {
|
||||
return this._logTable({ table })
|
||||
},
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const table = this._getTable(consoleProps)
|
||||
|
||||
if (!table) return
|
||||
|
||||
if (_.isArray(table)) {
|
||||
console.table(table)
|
||||
} else {
|
||||
console.groupCollapsed(table.name)
|
||||
console.table(table.data, table.columns)
|
||||
console.groupEnd()
|
||||
}
|
||||
},
|
||||
|
||||
_getTable (consoleProps) {
|
||||
const table = _.result(consoleProps, 'table')
|
||||
|
||||
if (!table) return
|
||||
|
||||
return table
|
||||
},
|
||||
}
|
||||
|
||||
const isMultiEntryTable = (table) => !_.isFunction(table) && !_.some(_.keys(table).map(isNaN).filter(Boolean), true)
|
||||
@@ -1,5 +1,5 @@
|
||||
import { action, computed, observable } from 'mobx'
|
||||
import automation from './automation'
|
||||
import { automation } from '@packages/runner-shared'
|
||||
|
||||
const _defaults = {
|
||||
messageTitle: null,
|
||||
|
||||
@@ -3,8 +3,11 @@ import React from 'react'
|
||||
import { render } from 'react-dom'
|
||||
import { utils as driverUtils } from '@packages/driver'
|
||||
|
||||
import App from './app/app'
|
||||
import NoSpec from './errors/no-spec'
|
||||
import State from './lib/state'
|
||||
import Container from './app/container'
|
||||
import { Container, eventManager } from '@packages/runner-shared'
|
||||
import util from './lib/util'
|
||||
|
||||
configure({ enforceActions: 'always' })
|
||||
|
||||
@@ -22,7 +25,19 @@ const Runner = {
|
||||
|
||||
state.updateDimensions(config.viewportWidth, config.viewportHeight)
|
||||
|
||||
render(<Container config={config} state={state} />, el)
|
||||
const container = (
|
||||
<Container
|
||||
config={config}
|
||||
runner='e2e'
|
||||
state={state}
|
||||
App={App}
|
||||
NoSpec={NoSpec}
|
||||
hasSpecFile={util.hasSpecFile}
|
||||
eventManager={eventManager}
|
||||
/>
|
||||
)
|
||||
|
||||
render(container, el)
|
||||
})()
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import cs from 'classnames'
|
||||
import { observer } from 'mobx-react'
|
||||
import React, { forwardRef } from 'react'
|
||||
|
||||
export default observer(forwardRef(({ state }, ref) => {
|
||||
if (!state.messageTitle) return null
|
||||
|
||||
function controls () {
|
||||
if (!state.messageControls) return null
|
||||
|
||||
return (
|
||||
<div className='message-controls'>
|
||||
{state.messageControls}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cs(
|
||||
'message-container',
|
||||
`message-${state.messageStyles.state}`,
|
||||
`message-type-${state.messageType}`,
|
||||
{ 'message-has-description': !!state.messageDescription },
|
||||
)}
|
||||
style={state.messageStyles.styles}
|
||||
>
|
||||
<div className='message'>
|
||||
<span className='title'>{state.messageTitle}</span>
|
||||
<span className='description'>{state.messageDescription}</span>
|
||||
</div>
|
||||
{controls()}
|
||||
</div>
|
||||
)
|
||||
}))
|
||||
@@ -1,37 +0,0 @@
|
||||
import _ from 'lodash'
|
||||
import React from 'react'
|
||||
import { render, unmountComponentAtNode } from 'react-dom'
|
||||
import Tooltip from '@cypress/react-tooltip'
|
||||
|
||||
const Highlight = ({ selector, appendTo, styles, showTooltip = true }) => {
|
||||
return (
|
||||
<div>
|
||||
{_.map(styles, (style, i) => {
|
||||
// indicates that tooltip should change if one of these props change
|
||||
const updateCue = _.values(_.pick(style, 'width', 'height', 'top', 'left', 'transform')).join()
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
key={i}
|
||||
title={selector}
|
||||
visible={showTooltip}
|
||||
placement='top-start'
|
||||
appendTo={appendTo}
|
||||
updateCue={updateCue}
|
||||
>
|
||||
<div className='highlight' style={style} />
|
||||
</Tooltip>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function renderHighlight (container, props) {
|
||||
render(<Highlight {...props} />, container)
|
||||
}
|
||||
|
||||
export default {
|
||||
render: renderHighlight,
|
||||
unmount: unmountComponentAtNode,
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import { action, computed, observable } from 'mobx'
|
||||
|
||||
const methods = ['get', 'contains']
|
||||
|
||||
class SelectorPlaygroundModel {
|
||||
methods = methods
|
||||
|
||||
@observable getSelector = 'body'
|
||||
@observable containsSelector = 'Hello, World'
|
||||
@observable isOpen = false
|
||||
@observable isEnabled = false
|
||||
@observable isShowingHighlight = false
|
||||
@observable isValid = true
|
||||
@observable numElements = 0
|
||||
@observable method = methods[0]
|
||||
|
||||
@computed get selector () {
|
||||
return this.method === 'get' ? this.getSelector : this.containsSelector
|
||||
}
|
||||
|
||||
@computed get infoHelp () {
|
||||
if (!this.isValid) {
|
||||
return 'Invalid selector'
|
||||
}
|
||||
|
||||
return this.numElements === 1 ? '1 matched element' : `${this.numElements} matched elements`
|
||||
}
|
||||
|
||||
@action toggleEnabled () {
|
||||
this.setEnabled(!this.isEnabled)
|
||||
}
|
||||
|
||||
@action setEnabled (isEnabled) {
|
||||
this.isEnabled = isEnabled
|
||||
|
||||
if (!this.isEnabled) {
|
||||
this.isShowingHighlight = false
|
||||
}
|
||||
}
|
||||
|
||||
@action toggleOpen () {
|
||||
this.setOpen(!this.isOpen)
|
||||
}
|
||||
|
||||
@action setOpen (isOpen) {
|
||||
this.isOpen = isOpen
|
||||
|
||||
this.setEnabled(this.isOpen)
|
||||
}
|
||||
|
||||
@action setShowingHighlight (isShowingHighlight) {
|
||||
this.isShowingHighlight = isShowingHighlight
|
||||
}
|
||||
|
||||
@action setSelector (selector) {
|
||||
if (this.method === 'get') {
|
||||
this.getSelector = selector
|
||||
} else {
|
||||
this.containsSelector = selector
|
||||
}
|
||||
}
|
||||
|
||||
@action setNumElements (numElements) {
|
||||
this.numElements = numElements
|
||||
}
|
||||
|
||||
@action setValidity (isValid) {
|
||||
this.isValid = isValid
|
||||
}
|
||||
|
||||
@action setMethod (method) {
|
||||
this.method = method
|
||||
}
|
||||
|
||||
@action resetMethod () {
|
||||
this.method = methods[0]
|
||||
}
|
||||
}
|
||||
|
||||
export default new SelectorPlaygroundModel()
|
||||
@@ -1,237 +0,0 @@
|
||||
import _ from 'lodash'
|
||||
import cs from 'classnames'
|
||||
import { action, observable } from 'mobx'
|
||||
import { observer } from 'mobx-react'
|
||||
import React, { Component } from 'react'
|
||||
import Tooltip from '@cypress/react-tooltip'
|
||||
|
||||
import eventManager from '../lib/event-manager'
|
||||
|
||||
const defaultCopyText = 'Copy to clipboard'
|
||||
const defaultPrintText = 'Print to console'
|
||||
|
||||
// mouseleave fires when entering a child element, so make sure we're
|
||||
// actually leaving the button and not just hovering over a child
|
||||
const fixMouseOut = (fn, getTarget) => (e) => {
|
||||
if (
|
||||
!e.relatedTarget
|
||||
|| e.relatedTarget.parentNode === getTarget()
|
||||
|| e.relatedTarget === getTarget()
|
||||
) return
|
||||
|
||||
fn(e)
|
||||
}
|
||||
|
||||
@observer
|
||||
class SelectorPlayground extends Component {
|
||||
@observable copyText = defaultCopyText
|
||||
@observable printText = defaultPrintText
|
||||
@observable showingMethodPicker = false
|
||||
|
||||
render () {
|
||||
const { model } = this.props
|
||||
const selectorText = `cy.${model.method}('${model.selector}')`
|
||||
|
||||
return (
|
||||
<div className={cs('header-popup selector-playground', `method-${model.method}`, {
|
||||
'no-elements': !model.numElements,
|
||||
'invalid-selector': !model.isValid,
|
||||
})}>
|
||||
<div className='selector'>
|
||||
<Tooltip
|
||||
title='Click an element to see a suggested selector'
|
||||
className='cy-tooltip'
|
||||
>
|
||||
<button
|
||||
className={`highlight-toggle ${model.isEnabled ? 'active' : ''}`}
|
||||
onClick={this._toggleEnablingSelectorPlayground}>
|
||||
<span className='fa-stack'>
|
||||
<i className='far fa-square fa-stack-1x'></i>
|
||||
<i className='fas fa-mouse-pointer fa-stack-1x'></i>
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<div
|
||||
className='wrap'
|
||||
onMouseOver={this._setHighlight(true)}
|
||||
>
|
||||
{this._methodSelector()}
|
||||
<span>(</span>
|
||||
<span>{'\''}</span>
|
||||
<div className='selector-input'>
|
||||
<input
|
||||
ref={(node) => this._input = node}
|
||||
name={`${model.isEnabled}` /* fixes issue with not resizing when opening/closing selector playground */}
|
||||
value={model.selector}
|
||||
onChange={this._updateSelector}
|
||||
onFocus={this._setHighlight(true)}
|
||||
/>
|
||||
</div>
|
||||
<span>{'\''}</span>
|
||||
<span>)</span>
|
||||
<input ref='copyText' className='copy-backer' value={selectorText} readOnly />
|
||||
<Tooltip title={model.infoHelp || ''} className='cy-tooltip'>
|
||||
<span className='info num-elements'>
|
||||
{model.isValid ?
|
||||
model.numElements :
|
||||
<i className='fas fa-exclamation-triangle'></i>
|
||||
}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Tooltip title={this.copyText || ''} updateCue={`${selectorText}${this.copyText}`} className='cy-tooltip'>
|
||||
<button
|
||||
ref={(node) => this._copyButton = node}
|
||||
className='copy-to-clipboard'
|
||||
onClick={this._copyToClipboard}
|
||||
disabled={!model.numElements || !model.isValid}
|
||||
onMouseOut={fixMouseOut(this._resetCopyText, () => this._copyButton)}
|
||||
>
|
||||
<i className='far fa-copy' />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip title={this.printText || ''} updateCue={`${selectorText}${this.printText}`} className='cy-tooltip'>
|
||||
<button
|
||||
ref={(node) => this._printButton = node}
|
||||
className='print-to-console'
|
||||
onClick={this._printToConsole}
|
||||
disabled={!model.numElements || !model.isValid}
|
||||
onMouseOut={fixMouseOut(this._resetPrintText, () => this._printButton)}
|
||||
>
|
||||
<i className='fas fa-terminal' />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<a className='selector-info' href='https://on.cypress.io/selector-playground' target="_blank">
|
||||
<i className='fas fa-question-circle'></i>{' '}
|
||||
Learn more
|
||||
</a>
|
||||
<button className='close' onClick={this._togglePlaygroundOpen}>x</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this._previousIsEnabled = this.props.model.isEnabled
|
||||
this._previousMethod = this.props.model.method
|
||||
|
||||
document.body.addEventListener('click', this._onOutsideClick, false)
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
if (
|
||||
(this.props.model.isEnabled !== this._previousIsEnabled)
|
||||
|| (this.props.model.method !== this._previousMethod)
|
||||
) {
|
||||
if (this.props.model.isEnabled) {
|
||||
this._focusAndSelectInputText()
|
||||
}
|
||||
|
||||
this._previousIsEnabled = this.props.model.isEnabled
|
||||
this._previousMethod = this.props.model.method
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
document.body.removeEventListener('click', this._onOutsideClick)
|
||||
}
|
||||
|
||||
_methodSelector () {
|
||||
const { model } = this.props
|
||||
const methods = _.filter(model.methods, (method) => method !== model.method)
|
||||
|
||||
return (
|
||||
<span className={cs('method', {
|
||||
'is-showing': this.showingMethodPicker,
|
||||
})}>
|
||||
<button onClick={this._toggleMethodPicker}>
|
||||
<i className='fas fa-caret-down'></i>{' '}
|
||||
cy.{model.method}
|
||||
</button>
|
||||
<div className='method-picker'>
|
||||
{_.map(methods, (method) => (
|
||||
<div key={method} onClick={() => this._setMethod(method)}>
|
||||
cy.{method}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
_focusAndSelectInputText () {
|
||||
this._input.focus()
|
||||
this._input.select()
|
||||
}
|
||||
|
||||
_onOutsideClick = () => {
|
||||
this._setShowingMethodPicker(false)
|
||||
}
|
||||
|
||||
_toggleMethodPicker = () => {
|
||||
this._setShowingMethodPicker(!this.showingMethodPicker)
|
||||
}
|
||||
|
||||
@action _setShowingMethodPicker (isShowing) {
|
||||
this.showingMethodPicker = isShowing
|
||||
}
|
||||
|
||||
@action _setMethod (method) {
|
||||
if (method !== this.props.model.method) {
|
||||
this.props.model.setMethod(method)
|
||||
}
|
||||
}
|
||||
|
||||
_setHighlight = (isShowing) => () => {
|
||||
this.props.model.setShowingHighlight(isShowing)
|
||||
}
|
||||
|
||||
_copyToClipboard = () => {
|
||||
try {
|
||||
this.refs.copyText.select()
|
||||
const successful = document.execCommand('copy')
|
||||
|
||||
this._setCopyText(successful ? 'Copied!' : 'Oops, unable to copy')
|
||||
} catch (err) {
|
||||
this._setCopyText('Oops, unable to copy')
|
||||
}
|
||||
}
|
||||
|
||||
@action _setCopyText (text) {
|
||||
this.copyText = text
|
||||
}
|
||||
|
||||
_resetCopyText = () => {
|
||||
this._setCopyText(defaultCopyText)
|
||||
}
|
||||
|
||||
_printToConsole = () => {
|
||||
eventManager.emit('print:selector:elements:to:console')
|
||||
this._setPrintText('Printed!')
|
||||
}
|
||||
|
||||
@action _setPrintText (text) {
|
||||
this.printText = text
|
||||
}
|
||||
|
||||
_resetPrintText = () => {
|
||||
this._setPrintText(defaultPrintText)
|
||||
}
|
||||
|
||||
_toggleEnablingSelectorPlayground = () => {
|
||||
this.props.model.toggleEnabled()
|
||||
}
|
||||
|
||||
_togglePlaygroundOpen = () => {
|
||||
this.props.model.toggleOpen()
|
||||
}
|
||||
|
||||
_updateSelector = (e) => {
|
||||
const { model } = this.props
|
||||
|
||||
model.setSelector(e.target.value)
|
||||
model.setShowingHighlight(true)
|
||||
}
|
||||
}
|
||||
|
||||
export default SelectorPlayground
|
||||
@@ -111,6 +111,10 @@ export const register = ({
|
||||
return args[0]
|
||||
}
|
||||
|
||||
if (args[0].endsWith('.scss')) {
|
||||
return args[0]
|
||||
}
|
||||
|
||||
const ret = _load.apply(this, browserPkg)
|
||||
|
||||
return ret
|
||||
|
||||
@@ -55,7 +55,7 @@ const stats = {
|
||||
timings: true,
|
||||
}
|
||||
|
||||
function makeSassLoaders ({ modules }): RuleSetRule {
|
||||
function makeSassLoaders ({ modules }: { modules: boolean }): RuleSetRule {
|
||||
const exclude = [/node_modules/]
|
||||
|
||||
if (!modules) exclude.push(/\.modules?\.s[ac]ss$/i)
|
||||
|
||||
Reference in New Issue
Block a user