Skip to content

Commit

Permalink
feat: Add extensions to get and set Windows clipboard (#192)
Browse files Browse the repository at this point in the history
  • Loading branch information
mykola-mokhnach authored Aug 20, 2023
1 parent 0fc9648 commit 8a1791d
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 0 deletions.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,31 @@ text | string | no | Non-empty string of Unicode text to type (surrogate charact
virtualKeyCode | number | no | Valid virtual key code. The list of supported key codes is available at [Virtual-Key Codes](https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes) page. Either this property or `pause` or `text` must be provided. | 0x10
down | boolean | no | This property only makes sense in combination with `virtualKeyCode`. If set to `true` then the corresponding key will be depressed, `false` - released. By default the key is just pressed once. ! Do not forget to release depressed keys in your automated tests. | true

### windows: setClipboard

Sets Windows clipboard content to the given text or a PNG image.

#### Arguments

Name | Type | Required | Description | Example
--- | --- | --- | --- | ---
b64Content | string | yes | Base64-encoded content of the clipboard to be set | `QXBwaXVt`
contentType | 'plaintext' or 'image' | no | Set to 'plaintext' in order to set the given text to the clipboard (the default value). Set to 'image' if `b64Content` contains a base64-encoded payload of a PNG image. | image

### windows: getClipboard

Retrieves Windows clipboard content.

#### Arguments

Name | Type | Required | Description | Example
--- | --- | --- | --- | ---
contentType | 'plaintext' or 'image' | no | Set to 'plaintext' in order to set the given text to the clipboard (the default value). Set to 'image' to retrieve a base64-encoded payload of a PNG image. | image

#### Returns

Base-64 encoded content of the Windows clipboard.


## Environment Variables

Expand Down
104 changes: 104 additions & 0 deletions lib/commands/clipboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { exec } from 'teen_process';
import { errors } from 'appium/driver';
import { requireArgs } from '../utils';
import _ from 'lodash';

const commands = {};

/**
* @typedef {'plaintext' | 'image'} ContentTypeEnum
*/

/**
* @type { Record<ContentTypeEnum, ContentTypeEnum>}
*/
const CONTENT_TYPE = Object.freeze({
plaintext: 'plaintext',
image: 'image',
});

// https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/set-clipboard?view=powershell-7.3

/**
* @typedef {Object} SetClipboardOptions
* @property {string} b64Content base64-encoded clipboard content to set
* @property {ContentTypeEnum} contentType [text] The clipboard content type to set
*/

/**
* Sets the Windows clipboard to the given string or PNG-image.
*
* @param {Partial<SetClipboardOptions>} opts
*/
commands.windowsSetClipboard = async function windowsSetClipboard (opts = {}) {
const {
b64Content,
contentType = CONTENT_TYPE.plaintext,
} = requireArgs(['b64Content'], opts);
if (b64Content && Buffer.from(b64Content, 'base64').toString('base64') !== b64Content) {
throw new errors.InvalidArgumentError(`The 'b64Content' argument must be a valid base64-encoded string`);
}
switch (contentType) {
case CONTENT_TYPE.plaintext:
return await exec('powershell', ['-command',
`$str=[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${b64Content}'));`,
'Set-Clipboard -Value $str'
]);
case CONTENT_TYPE.image:
return await exec('powershell', ['-command',
`$img=[Drawing.Bitmap]::FromStream([IO.MemoryStream][Convert]::FromBase64String('${b64Content}'));`,
'[System.Windows.Forms.Clipboard]::SetImage($img);',
'$img.Dispose();'
]);
default:
throw new errors.InvalidArgumentError(
`The clipboard content type '${contentType}' is not known. ` +
`Only the following content types are supported: ${_.values(CONTENT_TYPE)}`
);
}
};

// https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/get-clipboard?view=powershell-7.3

/**
* @typedef {Object} GetClipboardOptions
* @property {ContentTypeEnum} contentType [plaintext] The clipboard content type to get.
* Only PNG images are supported for extraction if set to 'image'.
*/

/**
* Returns the Windows clipboard content as base64-encoded string.
*
* @param {Partial<GetClipboardOptions>} opts
* @returns {Promise<string>} base64-encoded content of the clipboard
*/
commands.windowsGetClipboard = async function windowsGetClipboard (opts = {}) {
const {
contentType = CONTENT_TYPE.plaintext,
} = opts;
switch (contentType) {
case CONTENT_TYPE.plaintext: {
const {stdout} = await exec('powershell', ['-command',
'$str=Get-Clipboard;',
'[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($str));'
]);
return _.trim(stdout);
}
case CONTENT_TYPE.image: {
const {stdout} = await exec('powershell', ['-command',
'$s=New-Object System.IO.MemoryStream;',
'[System.Windows.Forms.Clipboard]::GetImage().Save($s,[System.Drawing.Imaging.ImageFormat]::Png);',
'[System.Convert]::ToBase64String($s.ToArray());'
]);
return _.trim(stdout);
}
default:
throw new errors.InvalidArgumentError(
`The clipboard content type '${contentType}' is not known. ` +
`Only the following content types are supported: ${_.values(CONTENT_TYPE)}`
);
}
};

export { commands };
export default commands;
3 changes: 3 additions & 0 deletions lib/commands/execute.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ const EXTENSION_COMMANDS_MAPPING = {
clickAndDrag: 'windowsClickAndDrag',
hover: 'windowsHover',
keys: 'windowsKeys',

setClipboard: 'windowsSetClipboard',
getClipboard: 'windowsGetClipboard',
};

commands.execute = async function execute (script, args) {
Expand Down
2 changes: 2 additions & 0 deletions lib/commands/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import executeCmds from './execute';
import fileMovementExtensions from './file-movement';
import appManagementExtensions from './app-management';
import gesturesCmds from './gestures';
import clipboardCmds from './clipboard';


const commands = {};
Expand All @@ -21,6 +22,7 @@ Object.assign(
fileMovementExtensions,
appManagementExtensions,
gesturesCmds,
clipboardCmds,
// add other command types here
);

Expand Down
19 changes: 19 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { util} from 'appium/support';
import { promisify } from 'node:util';
import { exec } from 'node:child_process';
import B from 'bluebird';
import _ from 'lodash';
import { errors } from 'appium/driver';

const execAsync = promisify(exec);

Expand All @@ -24,3 +26,20 @@ export async function shellExec(cmd, args = [], opts = {}) {
return await B.resolve(execAsync(fullCmd, opts))
.timeout(timeoutMs, `The command '${fullCmd}' timed out after ${timeoutMs}ms`);
}

/**
* Assert the presence of particular keys in the given object
*
* @template {Object} T
* @param {keyof T|(keyof T)[]} argNames one or more key names
* @param {T} opts the object to check
* @returns {T} the same given object
*/
export function requireArgs (argNames, opts) {
for (const argName of (_.isArray(argNames) ? argNames : [argNames])) {
if (!_.has(opts, argName)) {
throw new errors.InvalidArgumentError(`'${String(argName)}' argument must be provided`);
}
}
return opts;
}

0 comments on commit 8a1791d

Please sign in to comment.