Skip to content

Commit 2976b8c

Browse files
author
Tobias
committed
Add async clipboard unittesting
1 parent c4047f4 commit 2976b8c

File tree

4 files changed

+179
-30
lines changed

4 files changed

+179
-30
lines changed

core/rfb.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
import { toUnsigned32bit, toSigned32bit } from './util/int.js';
1111
import * as Log from './util/logging.js';
1212
import { encodeUTF8, decodeUTF8 } from './util/strings.js';
13-
import { dragThreshold, supportsWebCodecsH264Decode, isAsyncClipboardAvailable
14-
} from './util/browser.js';
13+
import { dragThreshold, supportsWebCodecsH264Decode,
14+
isAsyncClipboardAvailable } from './util/browser.js';
1515
import { clientToElement } from './util/element.js';
1616
import { setCapture } from './util/events.js';
1717
import EventTargetMixin from './util/eventtarget.js';
@@ -271,6 +271,7 @@ export default class RFB extends EventTargetMixin {
271271

272272
this._clipboard = new Clipboard(this._canvas);
273273
this._clipboard.onRead = this.clipboardPasteFrom.bind(this);
274+
this._isAsyncClipboardAvailable = isAsyncClipboardAvailable;
274275

275276
this._keyboard = new Keyboard(this._canvas);
276277
this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
@@ -322,14 +323,14 @@ export default class RFB extends EventTargetMixin {
322323
if (viewOnly) {
323324
this._keyboard.ungrab();
324325
(async () => {
325-
if (await isAsyncClipboardAvailable()) {
326+
if (await this._isAsyncClipboardAvailable()) {
326327
this._clipboard.ungrab();
327328
}
328329
})();
329330
} else {
330331
this._keyboard.grab();
331332
(async () => {
332-
if (await isAsyncClipboardAvailable()) {
333+
if (await this._isAsyncClipboardAvailable()) {
333334
this._clipboard.grab();
334335
}
335336
})();
@@ -2227,7 +2228,7 @@ export default class RFB extends EventTargetMixin {
22272228
if (!this._viewOnly) {
22282229
this._keyboard.grab();
22292230
(async () => {
2230-
if (await isAsyncClipboardAvailable()) {
2231+
if (await this._isAsyncClipboardAvailable()) {
22312232
this._clipboard.grab();
22322233
}
22332234
})();
@@ -2349,7 +2350,7 @@ export default class RFB extends EventTargetMixin {
23492350
_writeClipboard(text) {
23502351
if (this._viewOnly) return;
23512352
(async () => {
2352-
if (await isAsyncClipboardAvailable()) {
2353+
if (await this._isAsyncClipboardAvailable()) {
23532354
await this._clipboard.writeClipboard(text);
23542355
} else {
23552356
// Dispatch event for the alternative clipboard

tests/test.browser.js

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,55 @@
11
import { isMac, isWindows, isIOS, isAndroid, isChromeOS,
22
isSafari, isFirefox, isChrome, isChromium, isOpera, isEdge,
3-
isGecko, isWebKit, isBlink } from '../core/util/browser.js';
3+
isGecko, isWebKit, isBlink,
4+
isAsyncClipboardAvailable } from '../core/util/browser.js';
5+
6+
describe('Async clipboard', function () {
7+
"use strict";
8+
9+
beforeEach(function () {
10+
sinon.stub(navigator.permissions, "query").resolves({ state: "granted" });
11+
});
12+
13+
afterEach(function () {
14+
sinon.restore();
15+
});
16+
17+
it("is available when API present and permissions granted", async function () {
18+
navigator.permissions.query.resolves({ state: "granted" });
19+
const result = await isAsyncClipboardAvailable();
20+
expect(result).to.be.true;
21+
});
22+
23+
it("is available when API present and permissions yield 'prompt'", async function () {
24+
navigator.permissions.query.resolves({ state: "prompt" });
25+
const result = await isAsyncClipboardAvailable();
26+
expect(result).to.be.true;
27+
});
28+
29+
it("is unavailable when permissions denied", async function () {
30+
navigator.permissions.query.resolves({ state: "denied" });
31+
const result = await isAsyncClipboardAvailable();
32+
expect(result).to.be.false;
33+
});
34+
35+
it("is unavailable when permissions API fails", async function () {
36+
navigator.permissions.query.rejects(new Error("fail"));
37+
const result = await isAsyncClipboardAvailable();
38+
expect(result).to.be.false;
39+
});
40+
41+
it("is unavailable when write text API missing", async function () {
42+
sinon.stub(navigator.clipboard, "writeText").value(undefined);
43+
const result = await isAsyncClipboardAvailable();
44+
expect(result).to.be.false;
45+
});
46+
47+
it("is unavailable when read text API missing", async function () {
48+
sinon.stub(navigator.clipboard, "readText").value(undefined);
49+
const result = await isAsyncClipboardAvailable();
50+
expect(result).to.be.false;
51+
});
52+
});
453

554
describe('OS detection', function () {
655
let origNavigator;

tests/test.clipboard.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import Clipboard from '../core/clipboard.js';
2+
3+
describe('Async Clipboard API', function () {
4+
"use strict";
5+
6+
let targetMock;
7+
let clipboard;
8+
9+
beforeEach(function () {
10+
targetMock = document.createElement("canvas");
11+
clipboard = new Clipboard(targetMock);
12+
});
13+
14+
afterEach(function () {
15+
sinon.restore();
16+
targetMock = null;
17+
clipboard = null;
18+
});
19+
20+
it("writeClipboard calls navigator.clipboard.writeText", async function () {
21+
const text = "writing some text";
22+
sinon.stub(navigator.clipboard, "writeText").resolves(text);
23+
await clipboard.writeClipboard(text);
24+
sinon.assert.calledOnceWithExactly(navigator.clipboard.writeText, text);
25+
});
26+
27+
it("_handleFocus calls navigator.clipboard.readText", async function () {
28+
sinon.stub(navigator.clipboard, "readText").resolves();
29+
await clipboard._handleFocus(new Event("focus"));
30+
sinon.assert.calledOnce(navigator.clipboard.readText);
31+
});
32+
33+
it("_handleFocus triggers onRead with read text", async function () {
34+
const text = "random text 123";
35+
sinon.stub(navigator.clipboard, "readText").resolves(text);
36+
clipboard.onRead = sinon.spy();
37+
await clipboard._handleFocus(new Event("focus"));
38+
sinon.assert.calledOnceWithExactly(clipboard.onRead, text);
39+
});
40+
});

tests/test.rfb.js

Lines changed: 82 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3466,18 +3466,62 @@ describe('Remote Frame Buffer protocol client', function () {
34663466
});
34673467
});
34683468

3469+
describe('_writeClipboard()', function () {
3470+
3471+
it('calls clipboard.writeClipboard if async clipboard is available', async function () {
3472+
client._isAsyncClipboardAvailable = async () => true;
3473+
client._clipboard = { writeClipboard: sinon.stub().resolves() };
3474+
const text = 'text to system clipboard';
3475+
await client._writeClipboard(text);
3476+
sinon.assert.calledOnceWithExactly(client._clipboard.writeClipboard, text);
3477+
});
3478+
3479+
it('dispatches a clipboard event if async clipboard is unavailable', async function () {
3480+
client._isAsyncClipboardAvailable = async () => false;
3481+
const text = 'text to clipboard textarea';
3482+
const spyPromise = new Promise((resolve) => {
3483+
const handler = (e) => {
3484+
client.removeEventListener("clipboard", handler);
3485+
resolve(e);
3486+
};
3487+
client.addEventListener('clipboard', handler);
3488+
});
3489+
await client._writeClipboard(text);
3490+
const event = await spyPromise;
3491+
expect(event.detail.text).to.equal(text);
3492+
});
3493+
3494+
it('does nothing in view only mode', async function () {
3495+
client._viewOnly = true;
3496+
client._clipboard = { writeClipboard: sinon.stub() };
3497+
const text = 'text that will not be written';
3498+
let called = false;
3499+
client.addEventListener('clipboard', () => called = true);
3500+
await client._writeClipboard(text);
3501+
sinon.assert.notCalled(client._clipboard.writeClipboard);
3502+
expect(called).to.be.false;
3503+
});
3504+
});
3505+
34693506
describe('Normal clipboard handling receive', function () {
3470-
it('should fire the clipboard callback with the retrieved text on ServerCutText', function () {
3507+
it('should fire the clipboard callback with the retrieved text on ServerCutText', async function () {
3508+
client._isAsyncClipboardAvailable = async () => false;
34713509
const expectedStr = 'cheese!';
34723510
const data = [3, 0, 0, 0];
34733511
push32(data, expectedStr.length);
34743512
for (let i = 0; i < expectedStr.length; i++) { data.push(expectedStr.charCodeAt(i)); }
3475-
const spy = sinon.spy();
3476-
client.addEventListener("clipboard", spy);
3513+
3514+
const spyPromise = new Promise((resolve) => {
3515+
const handler = (e) => {
3516+
client.removeEventListener("clipboard", handler);
3517+
resolve(e);
3518+
};
3519+
client.addEventListener("clipboard", handler);
3520+
});
34773521

34783522
client._sock._websocket._receiveData(new Uint8Array(data));
3479-
expect(spy).to.have.been.calledOnce;
3480-
expect(spy.args[0][0].detail.text).to.equal(expectedStr);
3523+
const event = await spyPromise;
3524+
expect(event.detail.text).to.equal(expectedStr);
34813525
});
34823526
});
34833527

@@ -3531,7 +3575,8 @@ describe('Remote Frame Buffer protocol client', function () {
35313575
});
35323576

35333577
describe('Handle Provide', function () {
3534-
it('should update clipboard with correct Unicode data from a Provide message', function () {
3578+
it('should update clipboard with correct Unicode data from a Provide message', async function () {
3579+
client._isAsyncClipboardAvailable = async () => false;
35353580
let expectedData = "Aå漢字!";
35363581
let data = [3, 0, 0, 0];
35373582
const flags = [0x10, 0x00, 0x00, 0x01];
@@ -3545,16 +3590,21 @@ describe('Remote Frame Buffer protocol client', function () {
35453590
data = data.concat(flags);
35463591
data = data.concat(Array.from(deflatedText));
35473592

3548-
const spy = sinon.spy();
3549-
client.addEventListener("clipboard", spy);
3593+
const spyPromise = new Promise((resolve) => {
3594+
const handler = (e) => {
3595+
client.removeEventListener("clipboard", handler);
3596+
resolve(e);
3597+
};
3598+
client.addEventListener("clipboard", handler);
3599+
});
35503600

35513601
client._sock._websocket._receiveData(new Uint8Array(data));
3552-
expect(spy).to.have.been.calledOnce;
3553-
expect(spy.args[0][0].detail.text).to.equal(expectedData);
3554-
client.removeEventListener("clipboard", spy);
3602+
const event = await spyPromise;
3603+
expect(event.detail.text).to.equal(expectedData);
35553604
});
35563605

3557-
it('should update clipboard with correct escape characters from a Provide message ', function () {
3606+
it('should update clipboard with correct escape characters from a Provide message ', async function () {
3607+
client._isAsyncClipboardAvailable = async () => false;
35583608
let expectedData = "Oh\nmy\n!";
35593609
let data = [3, 0, 0, 0];
35603610
const flags = [0x10, 0x00, 0x00, 0x01];
@@ -3569,16 +3619,21 @@ describe('Remote Frame Buffer protocol client', function () {
35693619
data = data.concat(flags);
35703620
data = data.concat(Array.from(deflatedText));
35713621

3572-
const spy = sinon.spy();
3573-
client.addEventListener("clipboard", spy);
3622+
const spyPromise = new Promise((resolve) => {
3623+
const handler = (e) => {
3624+
client.removeEventListener("clipboard", handler);
3625+
resolve(e);
3626+
};
3627+
client.addEventListener("clipboard", handler);
3628+
});
35743629

35753630
client._sock._websocket._receiveData(new Uint8Array(data));
3576-
expect(spy).to.have.been.calledOnce;
3577-
expect(spy.args[0][0].detail.text).to.equal(expectedData);
3578-
client.removeEventListener("clipboard", spy);
3631+
const event = await spyPromise;
3632+
expect(event.detail.text).to.equal(expectedData);
35793633
});
35803634

3581-
it('should be able to handle large Provide messages', function () {
3635+
it('should be able to handle large Provide messages', async function () {
3636+
client._isAsyncClipboardAvailable = async () => false;
35823637
let expectedData = "hello".repeat(100000);
35833638
let data = [3, 0, 0, 0];
35843639
const flags = [0x10, 0x00, 0x00, 0x01];
@@ -3593,13 +3648,17 @@ describe('Remote Frame Buffer protocol client', function () {
35933648
data = data.concat(flags);
35943649
data = data.concat(Array.from(deflatedText));
35953650

3596-
const spy = sinon.spy();
3597-
client.addEventListener("clipboard", spy);
3651+
const spyPromise = new Promise((resolve) => {
3652+
const handler = (e) => {
3653+
client.removeEventListener("clipboard", handler);
3654+
resolve(e);
3655+
};
3656+
client.addEventListener("clipboard", handler);
3657+
});
35983658

35993659
client._sock._websocket._receiveData(new Uint8Array(data));
3600-
expect(spy).to.have.been.calledOnce;
3601-
expect(spy.args[0][0].detail.text).to.equal(expectedData);
3602-
client.removeEventListener("clipboard", spy);
3660+
const event = await spyPromise;
3661+
expect(event.detail.text).to.equal(expectedData);
36033662
});
36043663

36053664
});

0 commit comments

Comments
 (0)