Skip to content

Commit

Permalink
Copy to clipboard example (#709)
Browse files Browse the repository at this point in the history
* working on clipboard copy example

* working clipboard

* add example with clipboard copy and paste

* add the new recipe to the root files

* show how to spy on the clipboard method

* add more clipboard tests

* split the longer test into several smaller ones

* add cypress-repeat

* add cy.screenshot

* skip tests when not in Electron

* add test that puts text into clipboard

* add permissions spec

* grant clipboard permission in chrome using CDP

* add chrome on CI tests

* playing with code

* bump the chrome browser image

* add link to the recorded video

* remove screenshots

* add more notes about permissions

* add more comments
  • Loading branch information
bahmutov authored May 26, 2021
1 parent 2eb4760 commit 275b9d2
Show file tree
Hide file tree
Showing 11 changed files with 390 additions and 3 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Recipe | Description
[File download](./examples/testing-dom__download) | Download and validate CSV, Excel, text, Zip, and image files
[Page reloads](./examples/testing-dom__page-reloads) | Avoiding `while` loop when dealing with randomness
[Pagination](./examples/testing-dom__pagination) | Clicking the "Next" link until we reach the last page
[Clipboard](./examples/testing-dom__clipboard) | Copy and paste text into the clipboard from the test

## Logging in recipes

Expand Down
12 changes: 9 additions & 3 deletions circle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ jobs:
parallelism: 10
working_directory: ~/app
docker:
- image: cypress/browsers:node14.7.0-chrome84
- image: cypress/browsers:node14.16.0-chrome90-ff88
environment:
TERM: xterm
# no need to record these runs yet
Expand All @@ -149,7 +149,7 @@ jobs:
parallelism: 1
working_directory: ~/app
docker:
- image: cypress/browsers:node12.14.0-chrome79-ff71
- image: cypress/browsers:node14.16.0-chrome90-ff88
environment:
TERM: xterm
# no need to record these runs yet
Expand All @@ -165,7 +165,7 @@ jobs:
parallelism: 10
working_directory: ~/app
docker:
- image: cypress/browsers:node12.18.3-chrome89-ff86
- image: cypress/browsers:node14.16.0-chrome90-ff88
environment:
TERM: xterm
# no need to record these runs yet
Expand Down Expand Up @@ -314,6 +314,8 @@ jobs:
<<: *defaults
testing-dom__pagination:
<<: *defaults
testing-dom__clipboard:
<<: *defaults
unit-testing__application-code:
<<: *defaults
server-communication__bootstrapping-your-app:
Expand Down Expand Up @@ -605,6 +607,9 @@ all_jobs: &all_jobs
requires:
- build
repeat: 5
- testing-dom__clipboard:
requires:
- build
- unit-testing__application-code:
requires:
- build
Expand Down Expand Up @@ -726,6 +731,7 @@ all_jobs: &all_jobs
- testing-dom__sorting-table
- testing-dom__page-reloads
- testing-dom__pagination
- testing-dom__clipboard
- unit-testing__application-code
# "meta" jobs
- test-examples
Expand Down
22 changes: 22 additions & 0 deletions examples/testing-dom__clipboard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Clipboard
> Copy / paste text example
The widget to copy text is `@github/clipboard-copy-element` custom element that comes from [github/github-elements](https://github.com/github/github-elements)

![Copy / paste test](./images/copy-paste.gif)

See the [cypress/integration/spec.js](./cypress/integration/spec.js) file. The test currently work only in Electron where the clipboard permission is granted when Cypress starts it.

The page [index.html](./index.html) shows the copy button on "mouseover" event. When the text is copied to the clipboard, it shows a [tiny toast](https://github.com/bahmutov/tiny-toast) popup.

See the [Mozilla Clipboard docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Interact_with_the_clipboard)

## Other browsers

Work in progress, mostly because the default permissions prompt the user to allow the page to access the clipboard.

## Videos

We show how to test the clipboard access from Cypress in these videos:

- [Access the clipboard from Cypress test using Electron browser](https://youtu.be/SExmed1dCL4)
7 changes: 7 additions & 0 deletions examples/testing-dom__clipboard/cypress.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"fixturesFolder": false,
"supportFile": false,
"pluginsFile": false,
"viewportWidth": 400,
"viewportHeight": 300
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/// <reference types="cypress" />

// Permissions API specifically for clipboard information
// https://web.dev/async-clipboard/
// https://developer.mozilla.org/en-US/docs/Web/API/Permissions/query

/* eslint-env browser */
describe('Clipboard permissions', () => {
// Electron has access to the clipboard
// https://www.electronjs.org/docs/api/clipboard#clipboard
it('are granted in Electron', { browser: 'electron' }, () => {
cy.visit('index.html') // yields the window object
.its('navigator.permissions')
// permission names taken from
// https://w3c.github.io/permissions/#enumdef-permissionname
.invoke('query', { name: 'clipboard-read' })
.its('state')
.should('equal', 'granted')
})

// we can safely query the current permission status in Chrome
it('can be queried in Chrome', { browser: 'chrome' }, () => {
cy.visit('index.html') // yields the window object
.its('navigator.permissions')
// permission names taken from
// https://w3c.github.io/permissions/#enumdef-permissionname
.invoke('query', { name: 'clipboard-read' })
// by default it is "prompt" which shows a popup asking
// the user if the site can have access to the clipboard
// if the user allows, then next time it will be "granted"
// If the user denies access to the clipboard, on the next
// run the state will be "denied"
.its('state').should('be.oneOf', ['prompt', 'granted', 'denied'])
})

it('can be granted in Chrome', { browser: 'chrome' }, () => {
// use the Chrome debugger protocol to grant the current browser window
// access to the clipboard from the current origin
// https://chromedevtools.github.io/devtools-protocol/tot/Browser/#method-grantPermissions
// We are using cy.wrap to wait for the promise returned
// from the Cypress.automation call, so the test continues
// after the clipboard permission has been granted
cy.wrap(Cypress.automation('remote:debugger:protocol', {
command: 'Browser.grantPermissions',
params: {
permissions: ['clipboardReadWrite', 'clipboardSanitizedWrite'],
// make the permission tighter by allowing the current origin only
// like "http://localhost:56978"
origin: window.location.origin,
},
}))

cy.visit('index.html') // yields the window object
.its('navigator.permissions')
// permission names taken from
// https://w3c.github.io/permissions/#enumdef-permissionname
.invoke('query', { name: 'clipboard-read' })
.its('state').should('equal', 'granted')

// now reading the clipboard from test will work
// but only via navigator.clipboard - the document.execCommand
// does nothing.
cy.get('code').trigger('mouseover')
cy.get('[aria-label="Copy"]').click()
// confirm the clipboard's contents
cy.window().its('navigator.clipboard')
.invoke('readText')
.should('equal', 'npm install -D cypress')

// TODO how can we paste the clipboard into the text area?
// right now the document.execCommand('paste') does not
// do anything in the Chrome browser
})
})
92 changes: 92 additions & 0 deletions examples/testing-dom__clipboard/cypress/integration/spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/// <reference types="cypress" />

// access to the clipboard reliably works in Electron browser
// in other browsers, there are popups asking for permission
// thus we should only run these tests in Electron
describe('Clipboard', { browser: 'electron' }, () => {
it('copies text to clipboard', () => {
cy.visit('index.html')
cy.get('code').trigger('mouseover')
cy.get('[aria-label="Copy"]').click()

// let's check the copied text
// on Chrome this operation will prompt the browser
// to ask the user for permission:
// http://localhost:port wants to
// See text and images copied the clipboard
cy.window().its('navigator.clipboard')
.invoke('readText')
.should('equal', 'npm install -D cypress')
})

it('shows the popup', () => {
cy.visit('index.html')
cy.get('code').trigger('mouseover')
cy.get('[aria-label="Copy"]').click()

cy.contains('.tinyToast', 'Copied!').should('be.visible')
// the toast then goes away in less than 2 seconds
cy.get('.tinyToast', { timeout: 2000 }).should('not.exist')
})

it('can set the clipboard text in the text area', () => {
cy.visit('index.html')
cy.get('code').trigger('mouseover')
cy.get('[aria-label="Copy"]').click()

// let's check the copied text
cy.window().its('navigator.clipboard')
.invoke('readText')
.should('equal', 'npm install -D cypress')
.then((text) => {
// paste the text from the clipboard into the text area
cy.get('#paste-here').click().invoke('val', text)
})
})

it('spies on the clipboard methods', () => {
cy.visit('index.html')
cy.get('code').trigger('mouseover')
cy.window().its('navigator.clipboard').then((clipboard) => {
cy.spy(clipboard, 'writeText').as('writeText')
})

cy.get('[aria-label="Copy"]').click()
cy.get('@writeText')
.should('have.been.calledOnceWith', 'npm install -D cypress')
})

it('falls back to document.execCommand if navigator does not support clipboard', () => {
cy.visit('index.html', {
onBeforeLoad (win) {
// tip: to correctly delete a property from
// the navigator, must delete it from its prototype
delete win.navigator.__proto__.clipboard
},
})

cy.document().then((doc) => cy.spy(doc, 'execCommand').as('execCommand'))
cy.get('code').trigger('mouseover')
cy.get('[aria-label="Copy"]').click()
cy.get('@execCommand').should('have.been.calledOnceWith', 'copy')

// we can paste the clipboard text
cy.get('#paste-here').focus()
cy.document().invoke('execCommand', 'paste')
cy.get('#paste-here').should('have.value', 'npm install -D cypress')
})

it('writes text into clipboard', () => {
cy.visit('index.html')
// the document has to have focus before we can
// write our text into the clipboard
cy.get('#paste-here').focus()
cy.window()
.its('navigator.clipboard')
.invoke('writeText', 'this is a test')

// paste the clipboard into the text area
cy.document().invoke('execCommand', 'paste')
cy.get('#paste-here').should('have.value', 'this is a test')
})
})
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
114 changes: 114 additions & 0 deletions examples/testing-dom__clipboard/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<head>
<script src="https://unpkg.com/@github/clipboard-copy-element@latest" defer></script>
<script src="https://unpkg.com/[email protected]/dist/tiny-toast.js" defer></script>
<style>
pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #f6f8fa;
border-radius: 6px;
}

.snippet-clipboard-content {
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;
font-size: 16px;
line-height: 1.5;
word-wrap: break-word;
box-sizing: border-box;
position: relative!important;
}
.zeroclipboard-container {
display: none;
animation: fade-out .2s both;
}

.right-0 {
right: 0!important;
}
.top-0 {
top: 0!important;
}
.position-relative {
position: relative!important;
}
.position-absolute {
position: absolute!important;
}
.p-0 {
padding:0!important;
}
.m-2 {
margin: 8px!important;
}
.ClipboardButton {
position: relative;
}
.btn {
position: relative;
display: inline-block;
padding: 5px 16px;
font-size: 14px;
font-weight: 500;
line-height: 20px;
white-space: nowrap;
vertical-align: middle;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
border: 1px solid;
border-radius: 6px;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
.btn .octicon {
color: lightgray;
margin-right: 4px;
vertical-align: text-bottom;
}
.octicon {
display: inline-block;
overflow: visible!important;
vertical-align: text-bottom;
}
</style>
</head>
<body>
<h1>Clipboard</h1>

<div class="snippet-clipboard-content position-relative">
<pre><code>$ npm install -D cypress</code></pre>
<div class="zeroclipboard-container position-absolute right-0 top-0">
<clipboard-copy aria-label="Copy"
class="ClipboardButton btn js-clipboard-copy m-2 p-0 tooltipped-no-delay"
data-copy-feedback="Copied!"
data-tooltip-direction="w"
value="npm install -D cypress" tabindex="0" role="button">
<svg aria-hidden="true" viewBox="0 0 16 16" version="1.1" data-view-component="true" height="16" width="16" class="octicon octicon-clippy js-clipboard-clippy-icon m-2">
<path fill-rule="evenodd" d="M5.75 1a.75.75 0 00-.75.75v3c0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75v-3a.75.75 0 00-.75-.75h-4.5zm.75 3V2.5h3V4h-3zm-2.874-.467a.75.75 0 00-.752-1.298A1.75 1.75 0 002 3.75v9.5c0 .966.784 1.75 1.75 1.75h8.5A1.75 1.75 0 0014 13.25v-9.5a1.75 1.75 0 00-.874-1.515.75.75 0 10-.752 1.298.25.25 0 01.126.217v9.5a.25.25 0 01-.25.25h-8.5a.25.25 0 01-.25-.25v-9.5a.25.25 0 01.126-.217z"></path>
</svg>
</clipboard-copy>
</div>
</div>

<textarea id="paste-here" rows="4" cols="40"></textarea>

<script>
const contents = document.querySelector('.snippet-clipboard-content')
const clipboardContainer = contents.querySelector('.zeroclipboard-container')

contents.addEventListener('mouseover', (e) => {
clipboardContainer.style.display = 'inherit'
})
contents.addEventListener('mouseout', (e) => {
clipboardContainer.style.display = 'none'
})
document.addEventListener('clipboard-copy', function(event) {
tinyToast.show('Copied!').hide(1500)
})
</script>
</body>
16 changes: 16 additions & 0 deletions examples/testing-dom__clipboard/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "clipboard",
"version": "1.0.0",
"description": "Copy / paste text example",
"private": true,
"scripts": {
"cypress:open": "../../node_modules/.bin/cypress open",
"cypress:run": "../../node_modules/.bin/cypress run",
"start": "echo nothing to start",
"test:ci": "npm run cypress:run",
"test:ci:chrome": "../../node_modules/.bin/cypress run --browser chrome",
"test:ci:chrome:headless": "../../node_modules/.bin/cypress run --browser chrome --headless",
"test:ci:record": "npm run cypress:run -- --record",
"test:repeat": "../../node_modules/.bin/cypress-repeat -n 5"
}
}
Loading

0 comments on commit 275b9d2

Please sign in to comment.