Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(studio): add ability to copy commands to clipboard #16912

Merged
merged 17 commits into from Jun 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
89 changes: 72 additions & 17 deletions packages/reporter/cypress/integration/tests_spec.ts
Expand Up @@ -6,6 +6,16 @@ describe('tests', () => {
let runner: EventEmitter
let runnables: RootRunnable

const addStudioCommand = () => {
addCommand(runner, {
hookId: 'r3-studio',
name: 'get',
message: '#studio-command',
state: 'success',
isStudio: true,
})
}

beforeEach(() => {
cy.fixture('runnables').then((_runnables) => {
runnables = _runnables
Expand Down Expand Up @@ -170,24 +180,29 @@ describe('tests', () => {
.find('.studio-controls').as('studioControls')
})

it('is visible with save button when test passed', () => {
it('is visible with save and copy button when test passed', () => {
cy.get('@studioControls').should('be.visible')
cy.get('@studioControls').find('.studio-save').should('be.visible')
cy.get('@studioControls').find('.studio-copy').should('be.visible')

cy.percySnapshot()
})

it('is visible without save button if test failed', () => {
it('is visible without save and copy button if test failed', () => {
cy.contains('test 2')
.parents('.collapsible').first()
.find('.studio-controls').should('be.visible')

cy.contains('test 2')
.parents('.collapsible').first()
.find('.studio-save').should('not.be.visible')

cy.contains('test 2')
.parents('.collapsible').first()
.find('.studio-copy').should('not.be.visible')
})

it('is visible without save button if test was skipped', () => {
it('is visible without save and copy button if test was skipped', () => {
cy.contains('nested suite 1')
.parents('.collapsible').first()
.contains('test 1').click()
Expand All @@ -196,6 +211,7 @@ describe('tests', () => {
.should('be.visible')

cy.get('@pendingControls').find('.studio-save').should('not.be.visible')
cy.get('@pendingControls').find('.studio-copy').should('not.be.visible')
})

it('is not visible while test is running', () => {
Expand All @@ -214,31 +230,70 @@ describe('tests', () => {
cy.wrap(runner.emit).should('be.calledWith', 'studio:cancel')
})

describe('copy button', () => {
it('is disabled without tooltip when there are no commands', () => {
cy.get('@studioControls')
.find('.studio-copy')
.should('be.disabled')
.parent('span')
.trigger('mouseover')

cy.get('.cy-tooltip').should('not.exist')
})

it('is enabled with tooltip when there are commands', () => {
addStudioCommand()

cy.get('@studioControls')
.find('.studio-copy')
.should('not.be.disabled')
.trigger('mouseover')

cy.get('.cy-tooltip').should('have.text', 'Copy Commands to Clipboard')
})

it('is emits studio:copy:to:clipboard when clicked', () => {
addStudioCommand()

cy.stub(runner, 'emit')

cy.get('@studioControls').find('.studio-copy').click()

cy.wrap(runner.emit).should('be.calledWith', 'studio:copy:to:clipboard')
})

it('displays success state after commands are copied', () => {
addStudioCommand()

cy.stub(runner, 'emit').callsFake((event, callback) => {
if (event === 'studio:copy:to:clipboard') {
callback('')
}
})

cy.get('@studioControls')
.find('.studio-copy')
.click()
.should('have.class', 'studio-copy-success')
.trigger('mouseover')

cy.get('.cy-tooltip').should('have.text', 'Commands Copied!')
})
})

describe('save button', () => {
it('is disabled without commands', () => {
cy.get('@studioControls').find('.studio-save').should('be.disabled')
})

it('is enabled when there are commands', () => {
addCommand(runner, {
hookId: 'r3-studio',
name: 'get',
message: '#studio-command',
state: 'success',
isStudio: true,
})
addStudioCommand()

cy.get('@studioControls').find('.studio-save').should('not.be.disabled')
})

it('is emits studio:save when clicked', () => {
addCommand(runner, {
hookId: 'r3-studio',
name: 'get',
message: '#studio-command',
state: 'success',
isStudio: true,
})
addStudioCommand()

cy.stub(runner, 'emit')

Expand Down
4 changes: 4 additions & 0 deletions packages/reporter/src/lib/events.ts
Expand Up @@ -240,6 +240,10 @@ const events: Events = {
localBus.on('studio:save', () => {
runner.emit('studio:save')
})

localBus.on('studio:copy:to:clipboard', (cb) => {
runner.emit('studio:copy:to:clipboard', cb)
})
},

emit (event, ...args) {
Expand Down
36 changes: 33 additions & 3 deletions packages/reporter/src/runnables/runnables.scss
Expand Up @@ -237,7 +237,7 @@
.studio-controls {
display: flex;

.studio-save {
.studio-save, .studio-copy {
display: block;
}
}
Expand Down Expand Up @@ -397,7 +397,7 @@
outline: none;
}

&:active {
&:not(.studio-copy):active {
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
}
}
Expand All @@ -411,11 +411,41 @@
}
}

.studio-copy-wrapper {
margin-left: auto;

.studio-copy {
color: #3386d4;
display: none;
font-size: 16px;
padding: 3px 10px;

i {
width: 15px;
}

&:hover, &:focus {
color: darken(#3386d4, 5%);
}

&.studio-copy-success {
color: $pass;
}

&[disabled],
&[disabled]:hover,
&[disabled]:active {
color: #3386d4;
opacity: 0.5;
}
}
}

.studio-save {
background-color: #3386d4;
color: #fff;
display: none;
margin-left: auto;
margin-left: 4px;

&:hover {
background-color: darken(#3386d4, 10%);
Expand Down
44 changes: 43 additions & 1 deletion packages/reporter/src/test/test.tsx
Expand Up @@ -2,6 +2,7 @@ import { observer } from 'mobx-react'
import React, { Component, createRef, RefObject, MouseEvent } from 'react'
// @ts-ignore
import Tooltip from '@cypress/react-tooltip'
import cs from 'classnames'

import events, { Events } from '../lib/events'
import appState, { AppState } from '../lib/app-state'
Expand All @@ -18,12 +19,20 @@ interface StudioControlsProps {
model: TestModel
}

interface StudioControlsState {
copySuccess: boolean
}

@observer
class StudioControls extends Component<StudioControlsProps> {
class StudioControls extends Component<StudioControlsProps, StudioControlsState> {
static defaultProps = {
events,
}

state = {
copySuccess: false,
}

_cancel = (e: MouseEvent) => {
e.preventDefault()

Expand All @@ -36,12 +45,45 @@ class StudioControls extends Component<StudioControlsProps> {
this.props.events.emit('studio:save')
}

_copy = (e: MouseEvent) => {
e.preventDefault()

this.props.events.emit('studio:copy:to:clipboard', () => {
this.setState({ copySuccess: true })
})
}

_endCopySuccess = () => {
if (this.state.copySuccess) {
this.setState({ copySuccess: false })
}
}

render () {
const { studioIsNotEmpty } = this.props.model
const { copySuccess } = this.state

return (
<div className='studio-controls'>
<button className='studio-cancel' onClick={this._cancel}>Cancel</button>
<Tooltip
title={copySuccess ? 'Commands Copied!' : 'Copy Commands to Clipboard'}
className='cy-tooltip'
wrapperClassName='studio-copy-wrapper'
visible={!studioIsNotEmpty ? false : null}
updateCue={copySuccess}
>
<button
className={cs('studio-copy', {
'studio-copy-success': copySuccess,
})}
disabled={!studioIsNotEmpty}
onClick={this._copy}
onMouseLeave={this._endCopySuccess}
>
<i className={copySuccess ? 'fas fa-check' : 'far fa-copy'} />
</button>
</Tooltip>
<button className='studio-save' disabled={!studioIsNotEmpty} onClick={this._save}>Save Commands</button>
</div>
)
Expand Down
15 changes: 15 additions & 0 deletions packages/runner-shared/src/event-manager.js
Expand Up @@ -250,11 +250,19 @@ export const eventManager = {
studioRecorder.startSave()
})

reporterBus.on('studio:copy:to:clipboard', (cb) => {
this._studioCopyToClipboard(cb)
})

localBus.on('studio:start', () => {
studioRecorder.closeInitModal()
rerun()
})

localBus.on('studio:copy:to:clipboard', (cb) => {
this._studioCopyToClipboard(cb)
})

localBus.on('studio:save', (saveInfo) => {
ws.emit('studio:save', saveInfo, (err) => {
if (err) {
Expand Down Expand Up @@ -594,6 +602,13 @@ export const eventManager = {
return displayProps
},

_studioCopyToClipboard (cb) {
ws.emit('studio:get:commands:text', studioRecorder.logs, (commandsText) => {
studioRecorder.copyToClipboard(commandsText)
.then(cb)
})
},

emit (event, ...args) {
localBus.emit(event, ...args)
},
Expand Down
2 changes: 1 addition & 1 deletion packages/runner-shared/src/studio/studio-modals.jsx
Expand Up @@ -3,7 +3,7 @@ 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 '../event-manager'

import { studioRecorder } from './studio-recorder'

Expand Down
25 changes: 24 additions & 1 deletion packages/runner-shared/src/studio/studio-recorder.js
@@ -1,7 +1,7 @@
import { action, computed, observable } from 'mobx'
import { $ } from '@packages/driver'
import $driverUtils from '@packages/driver/src/cypress/utils'
import { eventManager } from '@packages/runner-shared'
import { eventManager } from '../event-manager'

const saveErrorMessage = (message) => {
return `\
Expand Down Expand Up @@ -261,6 +261,29 @@ export class StudioRecorder {
this._clearPreviousMouseEvent()
}

copyToClipboard = (commandsText) => {
// clipboard API is not supported without secure context
if (window.isSecureContext && navigator.clipboard) {
return navigator.clipboard.writeText(commandsText)
}

// fallback to creating invisible textarea
// create the textarea in our document rather than this._body
// as to not interfere with the app in the aut
const textArea = document.createElement('textarea')

textArea.value = commandsText
textArea.style.position = 'fixed'
textArea.style.opacity = 0

document.body.appendChild(textArea)
textArea.select()
document.execCommand('copy')
textArea.remove()

return Promise.resolve()
}

_trustEvent = (event) => {
// only capture events sent by the actual user
// but disable the check if we're in a test
Expand Down