Skip to content

Commit bc29903

Browse files
DavertMikclaude
andcommitted
feat(helpers): add grabFromClipboard, share clipboard scripts
Promotes the private clipboard read to a public `grabFromClipboard()` action on every browser helper, and moves the in-page clipboard scripts that were duplicated across four helpers into `lib/helper/extras/clipboard.js`. The shared scripts are promise-based and self-contained, so each helper consumes them the way its engine expects: Playwright and Puppeteer pass the function to `page.evaluate`, CDPBrowser interpolates it into a `Runtime.evaluate` expression, and WebDriver wraps it in a W3C async script body rather than keeping a second callback-style copy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 6774c6f commit bc29903

8 files changed

Lines changed: 114 additions & 119 deletions

File tree

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.

‎lib/helper/Appium.js‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1544,8 +1544,13 @@ class Appium extends Webdriver {
15441544
return this.browser.setClipboard('', 'plaintext')
15451545
}
15461546

1547-
async _grabClipboard() {
1548-
if (typeof this.browser.getClipboard !== 'function') return super._grabClipboard()
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()
15491554
const encoded = await this.browser.getClipboard('plaintext')
15501555
const clipboard = Buffer.from(encoded || '', 'base64').toString('utf8')
15511556
this.debugSection('Clipboard', clipboard)

‎lib/helper/CDPBrowser.js‎

Lines changed: 14 additions & 24 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

@@ -73,8 +74,6 @@ const PAGE_LOAD_GRACE_MS = 300
7374
// (common) non-navigating case, not a guess at how long a real navigation takes.
7475
const ACTION_SETTLE_GRACE_MS = 20
7576

76-
const CLIPBOARD_READ_TIMEOUT_MS = 5000
77-
7877
/**
7978
* CDPBrowser drives a browser directly over the raw Chrome DevTools Protocol, without depending
8079
* on Puppeteer, Playwright, or WebDriver. It opens its own WebSocket connection (via `CDPConnection`),
@@ -1956,7 +1955,7 @@ class CDPBrowser extends Helper {
19561955
* @returns {Promise<void>}
19571956
*/
19581957
async seeInClipboard(text) {
1959-
const clipboard = await this._grabClipboard()
1958+
const clipboard = await this.grabFromClipboard()
19601959
return stringIncludes('clipboard').assert(text, clipboard)
19611960
}
19621961

@@ -1974,7 +1973,7 @@ class CDPBrowser extends Helper {
19741973
* @returns {Promise<void>}
19751974
*/
19761975
async seeClipboardEquals(text) {
1977-
const clipboard = await this._grabClipboard()
1976+
const clipboard = await this.grabFromClipboard()
19781977
return equals('clipboard').assert(clipboard, text)
19791978
}
19801979

@@ -1990,18 +1989,23 @@ class CDPBrowser extends Helper {
19901989
*/
19911990
async clearClipboard() {
19921991
await this._grantClipboardAccess()
1993-
await this._evaluate(`(${writeClipboardScript.toString()})('')`)
1992+
await this._evaluate(clipboardExpression(writeClipboardScript, ''))
19941993
}
19951994

19961995
/**
1997-
* Reads the system clipboard through `navigator.clipboard`, granting read access first.
1996+
* Grabs the text content of the system clipboard.
1997+
* Resumes test execution, so **should be used inside async function with `await`** operator.
19981998
*
1999-
* @returns {Promise<string>} the current clipboard contents.
2000-
* @protected
1999+
* ```js
2000+
* I.click('Copy to clipboard');
2001+
* const url = await I.grabFromClipboard();
2002+
* ```
2003+
*
2004+
* @returns {Promise<string>} the system clipboard contents.
20012005
*/
2002-
async _grabClipboard() {
2006+
async grabFromClipboard() {
20032007
await this._grantClipboardAccess()
2004-
const clipboard = await this._evaluate(`(${readClipboardScript.toString()})(${CLIPBOARD_READ_TIMEOUT_MS})`)
2008+
const clipboard = await this._evaluate(clipboardExpression(readClipboardScript, CLIPBOARD_READ_TIMEOUT_MS))
20052009
this.debugSection('Clipboard', clipboard)
20062010
return clipboard
20072011
}
@@ -3083,18 +3087,4 @@ class CDPBrowser extends Helper {
30833087
}
30843088
}
30853089

3086-
function readClipboardScript(timeout) {
3087-
if (!navigator.clipboard || !navigator.clipboard.readText) {
3088-
throw new Error('Clipboard API is not available on this page, it requires a secure context (https or localhost)')
3089-
}
3090-
return Promise.race([navigator.clipboard.readText(), new Promise((resolve, reject) => setTimeout(() => reject(new Error('timed out while reading the clipboard')), timeout))])
3091-
}
3092-
3093-
function writeClipboardScript(text) {
3094-
if (!navigator.clipboard || !navigator.clipboard.writeText) {
3095-
throw new Error('Clipboard API is not available on this page, it requires a secure context (https or localhost)')
3096-
}
3097-
return navigator.clipboard.writeText(text)
3098-
}
3099-
31003090
export default CDPBrowser

‎lib/helper/Playwright.js‎

Lines changed: 13 additions & 25 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'
@@ -2663,31 +2664,34 @@ class Playwright extends Helper {
26632664
* {{> seeInClipboard }}
26642665
*/
26652666
async seeInClipboard(text) {
2666-
const clipboard = await this._grabClipboard()
2667+
const clipboard = await this.grabFromClipboard()
26672668
stringIncludes('clipboard').assert(text, clipboard)
26682669
}
26692670

26702671
/**
26712672
* {{> seeClipboardEquals }}
26722673
*/
26732674
async seeClipboardEquals(text) {
2674-
const clipboard = await this._grabClipboard()
2675+
const clipboard = await this.grabFromClipboard()
26752676
return equals('clipboard').assert(clipboard, text)
26762677
}
26772678

26782679
/**
2679-
* {{> clearClipboard }}
2680+
* {{> grabFromClipboard }}
26802681
*/
2681-
async clearClipboard() {
2682+
async grabFromClipboard() {
26822683
await this._grantClipboardAccess()
2683-
return this.page.evaluate(writeClipboardScript, '')
2684+
const clipboard = await this.page.evaluate(readClipboardScript, CLIPBOARD_READ_TIMEOUT_MS)
2685+
this.debugSection('Clipboard', clipboard)
2686+
return clipboard
26842687
}
26852688

2686-
async _grabClipboard() {
2689+
/**
2690+
* {{> clearClipboard }}
2691+
*/
2692+
async clearClipboard() {
26872693
await this._grantClipboardAccess()
2688-
const clipboard = await this.page.evaluate(readClipboardScript, CLIPBOARD_TIMEOUT)
2689-
this.debugSection('Clipboard', clipboard)
2690-
return clipboard
2694+
return this.page.evaluate(writeClipboardScript, '')
26912695
}
26922696

26932697
async _grantClipboardAccess() {
@@ -4942,19 +4946,3 @@ async function elToString(el, numberOfElements) {
49424946
.trim() + '...'
49434947
)
49444948
}
4945-
4946-
const CLIPBOARD_TIMEOUT = 5000
4947-
4948-
function readClipboardScript(timeout) {
4949-
if (!navigator.clipboard || !navigator.clipboard.readText) {
4950-
throw new Error('Clipboard API is not available on this page, it requires a secure context (https or localhost)')
4951-
}
4952-
return Promise.race([navigator.clipboard.readText(), new Promise((resolve, reject) => setTimeout(() => reject(new Error('timed out while reading the clipboard')), timeout))])
4953-
}
4954-
4955-
function writeClipboardScript(text) {
4956-
if (!navigator.clipboard || !navigator.clipboard.writeText) {
4957-
throw new Error('Clipboard API is not available on this page, it requires a secure context (https or localhost)')
4958-
}
4959-
return navigator.clipboard.writeText(text)
4960-
}

‎lib/helper/Puppeteer.js‎

Lines changed: 13 additions & 25 deletions
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'
@@ -1964,31 +1965,34 @@ class Puppeteer extends Helper {
19641965
* {{> seeInClipboard }}
19651966
*/
19661967
async seeInClipboard(text) {
1967-
const clipboard = await this._grabClipboard()
1968+
const clipboard = await this.grabFromClipboard()
19681969
stringIncludes('clipboard').assert(text, clipboard)
19691970
}
19701971

19711972
/**
19721973
* {{> seeClipboardEquals }}
19731974
*/
19741975
async seeClipboardEquals(text) {
1975-
const clipboard = await this._grabClipboard()
1976+
const clipboard = await this.grabFromClipboard()
19761977
return equals('clipboard').assert(clipboard, text)
19771978
}
19781979

19791980
/**
1980-
* {{> clearClipboard }}
1981+
* {{> grabFromClipboard }}
19811982
*/
1982-
async clearClipboard() {
1983+
async grabFromClipboard() {
19831984
await this._grantClipboardAccess()
1984-
return this.page.evaluate(writeClipboardScript, '')
1985+
const clipboard = await this.page.evaluate(readClipboardScript, CLIPBOARD_READ_TIMEOUT_MS)
1986+
this.debugSection('Clipboard', clipboard)
1987+
return clipboard
19851988
}
19861989

1987-
async _grabClipboard() {
1990+
/**
1991+
* {{> clearClipboard }}
1992+
*/
1993+
async clearClipboard() {
19881994
await this._grantClipboardAccess()
1989-
const clipboard = await this.page.evaluate(readClipboardScript, CLIPBOARD_TIMEOUT)
1990-
this.debugSection('Clipboard', clipboard)
1991-
return clipboard
1995+
return this.page.evaluate(writeClipboardScript, '')
19921996
}
19931997

19941998
async _grantClipboardAccess() {
@@ -3755,19 +3759,3 @@ async function proceedSelect(context, el, option) {
37553759

37563760
return this._waitForAction()
37573761
}
3758-
3759-
const CLIPBOARD_TIMEOUT = 5000
3760-
3761-
function readClipboardScript(timeout) {
3762-
if (!navigator.clipboard || !navigator.clipboard.readText) {
3763-
throw new Error('Clipboard API is not available on this page, it requires a secure context (https or localhost)')
3764-
}
3765-
return Promise.race([navigator.clipboard.readText(), new Promise((resolve, reject) => setTimeout(() => reject(new Error('timed out while reading the clipboard')), timeout))])
3766-
}
3767-
3768-
function writeClipboardScript(text) {
3769-
if (!navigator.clipboard || !navigator.clipboard.writeText) {
3770-
throw new Error('Clipboard API is not available on this page, it requires a secure context (https or localhost)')
3771-
}
3772-
return navigator.clipboard.writeText(text)
3773-
}

‎lib/helper/WebDriver.js‎

Lines changed: 15 additions & 42 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'
@@ -2095,35 +2096,38 @@ class WebDriver extends Helper {
20952096
* {{> seeInClipboard }}
20962097
*/
20972098
async seeInClipboard(text) {
2098-
const clipboard = await this._grabClipboard()
2099+
const clipboard = await this.grabFromClipboard()
20992100
return stringIncludes('clipboard').assert(text, clipboard)
21002101
}
21012102

21022103
/**
21032104
* {{> seeClipboardEquals }}
21042105
*/
21052106
async seeClipboardEquals(text) {
2106-
const clipboard = await this._grabClipboard()
2107+
const clipboard = await this.grabFromClipboard()
21072108
return equals('clipboard').assert(clipboard, text)
21082109
}
21092110

21102111
/**
2111-
* {{> clearClipboard }}
2112+
* {{> grabFromClipboard }}
21122113
*/
2113-
async clearClipboard() {
2114-
await this._grantClipboardAccess()
2115-
const result = (await this.browser.executeAsync(writeClipboardScript, '')) || {}
2116-
if (result.error) throw new Error(`Could not write to the clipboard: ${result.error}`)
2117-
}
2118-
2119-
async _grabClipboard() {
2114+
async grabFromClipboard() {
21202115
await this._grantClipboardAccess()
2121-
const result = (await this.browser.executeAsync(readClipboardScript, CLIPBOARD_TIMEOUT)) || {}
2116+
const result = (await this.browser.executeAsync(clipboardAsyncScript(readClipboardScript), CLIPBOARD_READ_TIMEOUT_MS)) || {}
21222117
if (result.error) throw new Error(`Could not read the clipboard: ${result.error}`)
21232118
this.debugSection('Clipboard', result.value)
21242119
return result.value
21252120
}
21262121

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+
21272131
async _grantClipboardAccess() {
21282132
if (typeof this.browser.setPermissions !== 'function') return
21292133
await this.browser.setPermissions({ name: 'clipboard-read' }, 'granted').catch(() => {})
@@ -3649,35 +3653,4 @@ async function proceedSelectOption(elem, option) {
36493653
return forEachAsync(els, clickOptionFn)
36503654
}
36513655

3652-
const CLIPBOARD_TIMEOUT = 5000
3653-
3654-
function readClipboardScript(timeout, done) {
3655-
let finished = false
3656-
const finish = result => {
3657-
if (finished) return
3658-
finished = true
3659-
done(result)
3660-
}
3661-
if (!navigator.clipboard || !navigator.clipboard.readText) {
3662-
finish({ error: 'Clipboard API is not available on this page, it requires a secure context (https or localhost)' })
3663-
return
3664-
}
3665-
setTimeout(() => finish({ error: 'timed out while reading the clipboard' }), timeout)
3666-
navigator.clipboard.readText().then(
3667-
text => finish({ value: text }),
3668-
err => finish({ error: (err && err.message) || String(err) }),
3669-
)
3670-
}
3671-
3672-
function writeClipboardScript(text, done) {
3673-
if (!navigator.clipboard || !navigator.clipboard.writeText) {
3674-
done({ error: 'Clipboard API is not available on this page, it requires a secure context (https or localhost)' })
3675-
return
3676-
}
3677-
navigator.clipboard.writeText(text).then(
3678-
() => done({}),
3679-
err => done({ error: (err && err.message) || String(err) }),
3680-
)
3681-
}
3682-
36833656
export { WebDriver as default }

‎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)