Skip to content

Commit c090d40

Browse files
authored
Merge pull request #5718 from codeceptjs/feat/clipboard-actions
feat(helpers): add seeInClipboard, seeClipboardEquals and clearClipboard
2 parents 8922740 + 7081654 commit c090d40

13 files changed

Lines changed: 381 additions & 1 deletion

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Clears the system clipboard.
2+
3+
```js
4+
I.clearClipboard();
5+
I.seeClipboardEquals('');
6+
```
7+
8+
@returns {void} automatically synchronized promise through #recorder
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Grabs the text content of the system clipboard and returns it to test.
2+
Resumes test execution, so **should be used inside async function with `await`** operator.
3+
4+
```js
5+
I.click('Copy to clipboard');
6+
let url = await I.grabFromClipboard();
7+
```
8+
9+
Reading the clipboard requires a secure context (`https` or `localhost`) and is supported
10+
in Chromium-based browsers, where read access is granted automatically.
11+
12+
@returns {Promise<string>} the system clipboard contents.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Checks that the system clipboard is equal to the given text.
2+
3+
```js
4+
I.click('Copy to clipboard');
5+
I.seeClipboardEquals('https://codecept.io');
6+
```
7+
8+
Reading the clipboard requires a secure context (`https` or `localhost`) and is supported
9+
in Chromium-based browsers, where read access is granted automatically.
10+
11+
@param {string} text value to check.
12+
@returns {void} automatically synchronized promise through #recorder
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Checks that the system clipboard contains the given text.
2+
3+
```js
4+
I.click('Copy to clipboard');
5+
I.seeInClipboard('https://codecept.io');
6+
```
7+
8+
Reading the clipboard requires a secure context (`https` or `localhost`) and is supported
9+
in Chromium-based browsers, where read access is granted automatically.
10+
11+
@param {string} text value to check.
12+
@returns {void} automatically synchronized promise through #recorder

lib/helper/Appium.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1534,6 +1534,29 @@ class Appium extends Webdriver {
15341534
return this.browser.closeApp()
15351535
}
15361536

1537+
/**
1538+
* {{> clearClipboard }}
1539+
*
1540+
* Appium: support both Android and iOS
1541+
*/
1542+
async clearClipboard() {
1543+
if (typeof this.browser.setClipboard !== 'function') return super.clearClipboard()
1544+
return this.browser.setClipboard('', 'plaintext')
1545+
}
1546+
1547+
/**
1548+
* {{> grabFromClipboard }}
1549+
*
1550+
* Appium: support both Android and iOS
1551+
*/
1552+
async grabFromClipboard() {
1553+
if (typeof this.browser.getClipboard !== 'function') return super.grabFromClipboard()
1554+
const encoded = await this.browser.getClipboard('plaintext')
1555+
const clipboard = Buffer.from(encoded || '', 'base64').toString('utf8')
1556+
this.debugSection('Clipboard', clipboard)
1557+
return clipboard
1558+
}
1559+
15371560
/**
15381561
* {{> appendField }}
15391562
*

lib/helper/CDPBrowser.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { isColorProperty, convertColorToRGBA } from '../colorUtils.js'
1818
import WebElement from '../element/WebElement.js'
1919
import CDPElementHandle from './extras/CDPElementHandle.js'
2020
import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js'
21+
import { CLIPBOARD_READ_TIMEOUT_MS, readClipboardScript, writeClipboardScript, clipboardExpression } from './extras/clipboard.js'
2122
import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, flushNetworkTraffics } from './network/actions.js'
2223
import { assembleApng, isPng } from './extras/apngAssembler.js'
2324

@@ -1940,6 +1941,89 @@ class CDPBrowser extends Helper {
19401941
}
19411942
}
19421943

1944+
/**
1945+
* Checks that the system clipboard contains the given text.
1946+
*
1947+
* ```js
1948+
* I.click('Copy to clipboard');
1949+
* I.seeInClipboard('https://codecept.io');
1950+
* ```
1951+
*
1952+
* Reading the clipboard requires a secure context (`https` or `localhost`).
1953+
*
1954+
* @param {string} text value to check.
1955+
* @returns {Promise<void>}
1956+
*/
1957+
async seeInClipboard(text) {
1958+
const clipboard = await this.grabFromClipboard()
1959+
return stringIncludes('clipboard').assert(text, clipboard)
1960+
}
1961+
1962+
/**
1963+
* Checks that the system clipboard is equal to the given text.
1964+
*
1965+
* ```js
1966+
* I.click('Copy to clipboard');
1967+
* I.seeClipboardEquals('https://codecept.io');
1968+
* ```
1969+
*
1970+
* Reading the clipboard requires a secure context (`https` or `localhost`).
1971+
*
1972+
* @param {string} text value to check.
1973+
* @returns {Promise<void>}
1974+
*/
1975+
async seeClipboardEquals(text) {
1976+
const clipboard = await this.grabFromClipboard()
1977+
return equals('clipboard').assert(clipboard, text)
1978+
}
1979+
1980+
/**
1981+
* Clears the system clipboard.
1982+
*
1983+
* ```js
1984+
* I.clearClipboard();
1985+
* I.seeClipboardEquals('');
1986+
* ```
1987+
*
1988+
* @returns {Promise<void>}
1989+
*/
1990+
async clearClipboard() {
1991+
await this._grantClipboardAccess()
1992+
await this._evaluate(clipboardExpression(writeClipboardScript, ''))
1993+
}
1994+
1995+
/**
1996+
* Grabs the text content of the system clipboard.
1997+
* Resumes test execution, so **should be used inside async function with `await`** operator.
1998+
*
1999+
* ```js
2000+
* I.click('Copy to clipboard');
2001+
* const url = await I.grabFromClipboard();
2002+
* ```
2003+
*
2004+
* @returns {Promise<string>} the system clipboard contents.
2005+
*/
2006+
async grabFromClipboard() {
2007+
await this._grantClipboardAccess()
2008+
const clipboard = await this._evaluate(clipboardExpression(readClipboardScript, CLIPBOARD_READ_TIMEOUT_MS))
2009+
this.debugSection('Clipboard', clipboard)
2010+
return clipboard
2011+
}
2012+
2013+
/**
2014+
* Brings the current target to front and grants it clipboard read/write access, so
2015+
* `navigator.clipboard` does not reject with a permission or focus error. Failures are ignored:
2016+
* a browser without `Browser.grantPermissions` surfaces its own error from the read instead.
2017+
*
2018+
* @protected
2019+
*/
2020+
async _grantClipboardAccess() {
2021+
await this.cdp.send('Page.bringToFront', {}, this.sessionId).catch(() => null)
2022+
const origin = await this._evaluate('window.location.origin').catch(() => null)
2023+
if (!origin || !origin.startsWith('http')) return
2024+
await this.cdp.send('Browser.grantPermissions', { origin, permissions: ['clipboardReadWrite', 'clipboardSanitizedWrite'] }).catch(() => null)
2025+
}
2026+
19432027
/**
19442028
* Saves a screenshot to the output folder (set in codecept.conf.ts or codecept.conf.js).
19452029
* Filename is relative to the output folder.

lib/helper/Playwright.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Locator from '../locator.js'
88
import recorder from '../recorder.js'
99
import store from '../store.js'
1010
import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js'
11+
import { CLIPBOARD_READ_TIMEOUT_MS, readClipboardScript, writeClipboardScript } from './extras/clipboard.js'
1112
import { includes as stringIncludes } from '../assert/include.js'
1213
import { urlEquals, equals } from '../assert/equal.js'
1314
import { empty } from '../assert/empty.js'
@@ -2667,6 +2668,49 @@ class Playwright extends Helper {
26672668
return this.browserContext.clearCookies()
26682669
}
26692670

2671+
/**
2672+
* {{> seeInClipboard }}
2673+
*/
2674+
async seeInClipboard(text) {
2675+
const clipboard = await this.grabFromClipboard()
2676+
stringIncludes('clipboard').assert(text, clipboard)
2677+
}
2678+
2679+
/**
2680+
* {{> seeClipboardEquals }}
2681+
*/
2682+
async seeClipboardEquals(text) {
2683+
const clipboard = await this.grabFromClipboard()
2684+
return equals('clipboard').assert(clipboard, text)
2685+
}
2686+
2687+
/**
2688+
* {{> grabFromClipboard }}
2689+
*/
2690+
async grabFromClipboard() {
2691+
await this._grantClipboardAccess()
2692+
const clipboard = await this.page.evaluate(readClipboardScript, CLIPBOARD_READ_TIMEOUT_MS)
2693+
this.debugSection('Clipboard', clipboard)
2694+
return clipboard
2695+
}
2696+
2697+
/**
2698+
* {{> clearClipboard }}
2699+
*/
2700+
async clearClipboard() {
2701+
await this._grantClipboardAccess()
2702+
return this.page.evaluate(writeClipboardScript, '')
2703+
}
2704+
2705+
async _grantClipboardAccess() {
2706+
await this.page.bringToFront().catch(() => {})
2707+
if (this.options.browser !== 'chromium') return
2708+
await this.page
2709+
.context()
2710+
.grantPermissions(['clipboard-read', 'clipboard-write'])
2711+
.catch(() => {})
2712+
}
2713+
26702714
/**
26712715
* Executes a script on the page:
26722716
*

lib/helper/Puppeteer.js

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import Locator from '../locator.js'
99
import recorder from '../recorder.js'
1010
import store from '../store.js'
1111
import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js'
12+
import { CLIPBOARD_READ_TIMEOUT_MS, readClipboardScript, writeClipboardScript } from './extras/clipboard.js'
1213
import { includes as stringIncludes } from '../assert/include.js'
1314
import { urlEquals, equals } from '../assert/equal.js'
1415
import { empty } from '../assert/empty.js'
@@ -1968,6 +1969,55 @@ class Puppeteer extends Helper {
19681969
return this.page.deleteCookie(cookie[0])
19691970
}
19701971

1972+
/**
1973+
* {{> seeInClipboard }}
1974+
*/
1975+
async seeInClipboard(text) {
1976+
const clipboard = await this.grabFromClipboard()
1977+
stringIncludes('clipboard').assert(text, clipboard)
1978+
}
1979+
1980+
/**
1981+
* {{> seeClipboardEquals }}
1982+
*/
1983+
async seeClipboardEquals(text) {
1984+
const clipboard = await this.grabFromClipboard()
1985+
return equals('clipboard').assert(clipboard, text)
1986+
}
1987+
1988+
/**
1989+
* {{> grabFromClipboard }}
1990+
*/
1991+
async grabFromClipboard() {
1992+
await this._grantClipboardAccess()
1993+
const clipboard = await this.page.evaluate(readClipboardScript, CLIPBOARD_READ_TIMEOUT_MS)
1994+
this.debugSection('Clipboard', clipboard)
1995+
return clipboard
1996+
}
1997+
1998+
/**
1999+
* {{> clearClipboard }}
2000+
*/
2001+
async clearClipboard() {
2002+
await this._grantClipboardAccess()
2003+
return this.page.evaluate(writeClipboardScript, '')
2004+
}
2005+
2006+
async _grantClipboardAccess() {
2007+
await this.page.bringToFront().catch(() => {})
2008+
let origin
2009+
try {
2010+
origin = new URL(this.page.url()).origin
2011+
} catch (err) {
2012+
return
2013+
}
2014+
if (!origin.startsWith('http')) return
2015+
await this.page
2016+
.browserContext()
2017+
.overridePermissions(origin, ['clipboard-read', 'clipboard-write', 'clipboard-sanitized-write'])
2018+
.catch(() => {})
2019+
}
2020+
19712021
/**
19722022
* If a function returns a Promise, tt will wait for its resolution.
19732023
*
@@ -3717,4 +3767,3 @@ async function proceedSelect(context, el, option) {
37173767

37183768
return this._waitForAction()
37193769
}
3720-

lib/helper/WebDriver.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { includes as stringIncludes } from '../assert/include.js'
1111
import { urlEquals, equals } from '../assert/equal.js'
1212
import store from '../store.js'
1313
import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js'
14+
import { CLIPBOARD_READ_TIMEOUT_MS, readClipboardScript, writeClipboardScript, clipboardAsyncScript } from './extras/clipboard.js'
1415
import output from '../output.js'
1516
const { debug } = output
1617
import { empty } from '../assert/empty.js'
@@ -2091,6 +2092,48 @@ class WebDriver extends Helper {
20912092
return cookie[0]
20922093
}
20932094

2095+
/**
2096+
* {{> seeInClipboard }}
2097+
*/
2098+
async seeInClipboard(text) {
2099+
const clipboard = await this.grabFromClipboard()
2100+
return stringIncludes('clipboard').assert(text, clipboard)
2101+
}
2102+
2103+
/**
2104+
* {{> seeClipboardEquals }}
2105+
*/
2106+
async seeClipboardEquals(text) {
2107+
const clipboard = await this.grabFromClipboard()
2108+
return equals('clipboard').assert(clipboard, text)
2109+
}
2110+
2111+
/**
2112+
* {{> grabFromClipboard }}
2113+
*/
2114+
async grabFromClipboard() {
2115+
await this._grantClipboardAccess()
2116+
const result = (await this.browser.executeAsync(clipboardAsyncScript(readClipboardScript), CLIPBOARD_READ_TIMEOUT_MS)) || {}
2117+
if (result.error) throw new Error(`Could not read the clipboard: ${result.error}`)
2118+
this.debugSection('Clipboard', result.value)
2119+
return result.value
2120+
}
2121+
2122+
/**
2123+
* {{> clearClipboard }}
2124+
*/
2125+
async clearClipboard() {
2126+
await this._grantClipboardAccess()
2127+
const result = (await this.browser.executeAsync(clipboardAsyncScript(writeClipboardScript), '')) || {}
2128+
if (result.error) throw new Error(`Could not write to the clipboard: ${result.error}`)
2129+
}
2130+
2131+
async _grantClipboardAccess() {
2132+
if (typeof this.browser.setPermissions !== 'function') return
2133+
await this.browser.setPermissions({ name: 'clipboard-read' }, 'granted').catch(() => {})
2134+
await this.browser.setPermissions({ name: 'clipboard-write' }, 'granted').catch(() => {})
2135+
}
2136+
20942137
/**
20952138
* {{> waitForCookie }}
20962139
*/

lib/helper/extras/clipboard.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
export const CLIPBOARD_READ_TIMEOUT_MS = 5000
2+
3+
export function readClipboardScript(timeout) {
4+
if (!navigator.clipboard || !navigator.clipboard.readText) {
5+
throw new Error('Clipboard API is not available on this page, it requires a secure context (https or localhost)')
6+
}
7+
return Promise.race([navigator.clipboard.readText(), new Promise((resolve, reject) => setTimeout(() => reject(new Error('timed out while reading the clipboard')), timeout))])
8+
}
9+
10+
export function writeClipboardScript(text) {
11+
if (!navigator.clipboard || !navigator.clipboard.writeText) {
12+
throw new Error('Clipboard API is not available on this page, it requires a secure context (https or localhost)')
13+
}
14+
return navigator.clipboard.writeText(text)
15+
}
16+
17+
export function clipboardExpression(script, arg) {
18+
return `(${script.toString()})(${JSON.stringify(arg)})`
19+
}
20+
21+
export function clipboardAsyncScript(script) {
22+
return `
23+
var done = arguments[arguments.length - 1]
24+
var fail = function (err) { done({ error: (err && err.message) || String(err) }) }
25+
try {
26+
(${script.toString()})(arguments[0]).then(function (value) { done({ value: value }) }, fail)
27+
} catch (err) {
28+
fail(err)
29+
}
30+
`
31+
}

0 commit comments

Comments
 (0)