Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/webapi/clearClipboard.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Clears the system clipboard.

```js
I.clearClipboard();
I.seeClipboardEquals('');
```

@returns {void} automatically synchronized promise through #recorder
12 changes: 12 additions & 0 deletions docs/webapi/grabFromClipboard.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Grabs the text content of the system clipboard and returns it to test.
Resumes test execution, so **should be used inside async function with `await`** operator.

```js
I.click('Copy to clipboard');
let url = await I.grabFromClipboard();
```

Reading the clipboard requires a secure context (`https` or `localhost`) and is supported
in Chromium-based browsers, where read access is granted automatically.

@returns {Promise<string>} the system clipboard contents.
12 changes: 12 additions & 0 deletions docs/webapi/seeClipboardEquals.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Checks that the system clipboard is equal to the given text.

```js
I.click('Copy to clipboard');
I.seeClipboardEquals('https://codecept.io');
```

Reading the clipboard requires a secure context (`https` or `localhost`) and is supported
in Chromium-based browsers, where read access is granted automatically.

@param {string} text value to check.
@returns {void} automatically synchronized promise through #recorder
12 changes: 12 additions & 0 deletions docs/webapi/seeInClipboard.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Checks that the system clipboard contains the given text.

```js
I.click('Copy to clipboard');
I.seeInClipboard('https://codecept.io');
```

Reading the clipboard requires a secure context (`https` or `localhost`) and is supported
in Chromium-based browsers, where read access is granted automatically.

@param {string} text value to check.
@returns {void} automatically synchronized promise through #recorder
23 changes: 23 additions & 0 deletions lib/helper/Appium.js
Original file line number Diff line number Diff line change
Expand Up @@ -1534,6 +1534,29 @@ class Appium extends Webdriver {
return this.browser.closeApp()
}

/**
* {{> clearClipboard }}
*
* Appium: support both Android and iOS
*/
async clearClipboard() {
if (typeof this.browser.setClipboard !== 'function') return super.clearClipboard()
return this.browser.setClipboard('', 'plaintext')
}

/**
* {{> grabFromClipboard }}
*
* Appium: support both Android and iOS
*/
async grabFromClipboard() {
if (typeof this.browser.getClipboard !== 'function') return super.grabFromClipboard()
const encoded = await this.browser.getClipboard('plaintext')
const clipboard = Buffer.from(encoded || '', 'base64').toString('utf8')
this.debugSection('Clipboard', clipboard)
return clipboard
}

/**
* {{> appendField }}
*
Expand Down
84 changes: 84 additions & 0 deletions lib/helper/CDPBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { isColorProperty, convertColorToRGBA } from '../colorUtils.js'
import WebElement from '../element/WebElement.js'
import CDPElementHandle from './extras/CDPElementHandle.js'
import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js'
import { CLIPBOARD_READ_TIMEOUT_MS, readClipboardScript, writeClipboardScript, clipboardExpression } from './extras/clipboard.js'
import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, flushNetworkTraffics } from './network/actions.js'
import { assembleApng, isPng } from './extras/apngAssembler.js'

Expand Down Expand Up @@ -1940,6 +1941,89 @@ class CDPBrowser extends Helper {
}
}

/**
* Checks that the system clipboard contains the given text.
*
* ```js
* I.click('Copy to clipboard');
* I.seeInClipboard('https://codecept.io');
* ```
*
* Reading the clipboard requires a secure context (`https` or `localhost`).
*
* @param {string} text value to check.
* @returns {Promise<void>}
*/
async seeInClipboard(text) {
const clipboard = await this.grabFromClipboard()
return stringIncludes('clipboard').assert(text, clipboard)
}

/**
* Checks that the system clipboard is equal to the given text.
*
* ```js
* I.click('Copy to clipboard');
* I.seeClipboardEquals('https://codecept.io');
* ```
*
* Reading the clipboard requires a secure context (`https` or `localhost`).
*
* @param {string} text value to check.
* @returns {Promise<void>}
*/
async seeClipboardEquals(text) {
const clipboard = await this.grabFromClipboard()
return equals('clipboard').assert(clipboard, text)
}

/**
* Clears the system clipboard.
*
* ```js
* I.clearClipboard();
* I.seeClipboardEquals('');
* ```
*
* @returns {Promise<void>}
*/
async clearClipboard() {
await this._grantClipboardAccess()
await this._evaluate(clipboardExpression(writeClipboardScript, ''))
}

/**
* Grabs the text content of the system clipboard.
* Resumes test execution, so **should be used inside async function with `await`** operator.
*
* ```js
* I.click('Copy to clipboard');
* const url = await I.grabFromClipboard();
* ```
*
* @returns {Promise<string>} the system clipboard contents.
*/
async grabFromClipboard() {
await this._grantClipboardAccess()
const clipboard = await this._evaluate(clipboardExpression(readClipboardScript, CLIPBOARD_READ_TIMEOUT_MS))
this.debugSection('Clipboard', clipboard)
return clipboard
}

/**
* Brings the current target to front and grants it clipboard read/write access, so
* `navigator.clipboard` does not reject with a permission or focus error. Failures are ignored:
* a browser without `Browser.grantPermissions` surfaces its own error from the read instead.
*
* @protected
*/
async _grantClipboardAccess() {
await this.cdp.send('Page.bringToFront', {}, this.sessionId).catch(() => null)
const origin = await this._evaluate('window.location.origin').catch(() => null)
if (!origin || !origin.startsWith('http')) return
await this.cdp.send('Browser.grantPermissions', { origin, permissions: ['clipboardReadWrite', 'clipboardSanitizedWrite'] }).catch(() => null)
}

/**
* Saves a screenshot to the output folder (set in codecept.conf.ts or codecept.conf.js).
* Filename is relative to the output folder.
Expand Down
44 changes: 44 additions & 0 deletions lib/helper/Playwright.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Locator from '../locator.js'
import recorder from '../recorder.js'
import store from '../store.js'
import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js'
import { CLIPBOARD_READ_TIMEOUT_MS, readClipboardScript, writeClipboardScript } from './extras/clipboard.js'
import { includes as stringIncludes } from '../assert/include.js'
import { urlEquals, equals } from '../assert/equal.js'
import { empty } from '../assert/empty.js'
Expand Down Expand Up @@ -2659,6 +2660,49 @@ class Playwright extends Helper {
return this.browserContext.clearCookies()
}

/**
* {{> seeInClipboard }}
*/
async seeInClipboard(text) {
const clipboard = await this.grabFromClipboard()
stringIncludes('clipboard').assert(text, clipboard)
}

/**
* {{> seeClipboardEquals }}
*/
async seeClipboardEquals(text) {
const clipboard = await this.grabFromClipboard()
return equals('clipboard').assert(clipboard, text)
}

/**
* {{> grabFromClipboard }}
*/
async grabFromClipboard() {
await this._grantClipboardAccess()
const clipboard = await this.page.evaluate(readClipboardScript, CLIPBOARD_READ_TIMEOUT_MS)
this.debugSection('Clipboard', clipboard)
return clipboard
}

/**
* {{> clearClipboard }}
*/
async clearClipboard() {
await this._grantClipboardAccess()
return this.page.evaluate(writeClipboardScript, '')
}

async _grantClipboardAccess() {
await this.page.bringToFront().catch(() => {})
if (this.options.browser !== 'chromium') return
await this.page
.context()
.grantPermissions(['clipboard-read', 'clipboard-write'])
.catch(() => {})
}

/**
* Executes a script on the page:
*
Expand Down
51 changes: 50 additions & 1 deletion lib/helper/Puppeteer.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Locator from '../locator.js'
import recorder from '../recorder.js'
import store from '../store.js'
import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js'
import { CLIPBOARD_READ_TIMEOUT_MS, readClipboardScript, writeClipboardScript } from './extras/clipboard.js'
import { includes as stringIncludes } from '../assert/include.js'
import { urlEquals, equals } from '../assert/equal.js'
import { empty } from '../assert/empty.js'
Expand Down Expand Up @@ -1960,6 +1961,55 @@ class Puppeteer extends Helper {
return this.page.deleteCookie(cookie[0])
}

/**
* {{> seeInClipboard }}
*/
async seeInClipboard(text) {
const clipboard = await this.grabFromClipboard()
stringIncludes('clipboard').assert(text, clipboard)
}

/**
* {{> seeClipboardEquals }}
*/
async seeClipboardEquals(text) {
const clipboard = await this.grabFromClipboard()
return equals('clipboard').assert(clipboard, text)
}

/**
* {{> grabFromClipboard }}
*/
async grabFromClipboard() {
await this._grantClipboardAccess()
const clipboard = await this.page.evaluate(readClipboardScript, CLIPBOARD_READ_TIMEOUT_MS)
this.debugSection('Clipboard', clipboard)
return clipboard
}

/**
* {{> clearClipboard }}
*/
async clearClipboard() {
await this._grantClipboardAccess()
return this.page.evaluate(writeClipboardScript, '')
}

async _grantClipboardAccess() {
await this.page.bringToFront().catch(() => {})
let origin
try {
origin = new URL(this.page.url()).origin
} catch (err) {
return
}
if (!origin.startsWith('http')) return
await this.page
.browserContext()
.overridePermissions(origin, ['clipboard-read', 'clipboard-write', 'clipboard-sanitized-write'])
.catch(() => {})
}

/**
* If a function returns a Promise, tt will wait for its resolution.
*
Expand Down Expand Up @@ -3709,4 +3759,3 @@ async function proceedSelect(context, el, option) {

return this._waitForAction()
}

43 changes: 43 additions & 0 deletions lib/helper/WebDriver.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { includes as stringIncludes } from '../assert/include.js'
import { urlEquals, equals } from '../assert/equal.js'
import store from '../store.js'
import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js'
import { CLIPBOARD_READ_TIMEOUT_MS, readClipboardScript, writeClipboardScript, clipboardAsyncScript } from './extras/clipboard.js'
import output from '../output.js'
const { debug } = output
import { empty } from '../assert/empty.js'
Expand Down Expand Up @@ -2091,6 +2092,48 @@ class WebDriver extends Helper {
return cookie[0]
}

/**
* {{> seeInClipboard }}
*/
async seeInClipboard(text) {
const clipboard = await this.grabFromClipboard()
return stringIncludes('clipboard').assert(text, clipboard)
}

/**
* {{> seeClipboardEquals }}
*/
async seeClipboardEquals(text) {
const clipboard = await this.grabFromClipboard()
return equals('clipboard').assert(clipboard, text)
}

/**
* {{> grabFromClipboard }}
*/
async grabFromClipboard() {
await this._grantClipboardAccess()
const result = (await this.browser.executeAsync(clipboardAsyncScript(readClipboardScript), CLIPBOARD_READ_TIMEOUT_MS)) || {}
if (result.error) throw new Error(`Could not read the clipboard: ${result.error}`)
this.debugSection('Clipboard', result.value)
return result.value
}

/**
* {{> clearClipboard }}
*/
async clearClipboard() {
await this._grantClipboardAccess()
const result = (await this.browser.executeAsync(clipboardAsyncScript(writeClipboardScript), '')) || {}
if (result.error) throw new Error(`Could not write to the clipboard: ${result.error}`)
}

async _grantClipboardAccess() {
if (typeof this.browser.setPermissions !== 'function') return
await this.browser.setPermissions({ name: 'clipboard-read' }, 'granted').catch(() => {})
await this.browser.setPermissions({ name: 'clipboard-write' }, 'granted').catch(() => {})
}

/**
* {{> waitForCookie }}
*/
Expand Down
31 changes: 31 additions & 0 deletions lib/helper/extras/clipboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export const CLIPBOARD_READ_TIMEOUT_MS = 5000

export function readClipboardScript(timeout) {
if (!navigator.clipboard || !navigator.clipboard.readText) {
throw new Error('Clipboard API is not available on this page, it requires a secure context (https or localhost)')
}
return Promise.race([navigator.clipboard.readText(), new Promise((resolve, reject) => setTimeout(() => reject(new Error('timed out while reading the clipboard')), timeout))])
}

export function writeClipboardScript(text) {
if (!navigator.clipboard || !navigator.clipboard.writeText) {
throw new Error('Clipboard API is not available on this page, it requires a secure context (https or localhost)')
}
return navigator.clipboard.writeText(text)
}

export function clipboardExpression(script, arg) {
return `(${script.toString()})(${JSON.stringify(arg)})`
}

export function clipboardAsyncScript(script) {
return `
var done = arguments[arguments.length - 1]
var fail = function (err) { done({ error: (err && err.message) || String(err) }) }
try {
(${script.toString()})(arguments[0]).then(function (value) { done({ value: value }) }, fail)
} catch (err) {
fail(err)
}
`
}
Loading
Loading