mirror of
https://github.com/novnc/noVNC.git
synced 2026-05-26 07:08:06 +00:00
Add permissions-exclusive async clipboard
Clipboard permissions must be supported, with states "granted" or "prompt" for both write and read.
This commit is contained in:
72
core/clipboard.js
Normal file
72
core/clipboard.js
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (c) 2025 The noVNC authors
|
||||
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
|
||||
*/
|
||||
|
||||
import * as Log from './util/logging.js';
|
||||
import { browserAsyncClipboardSupport } from './util/browser.js';
|
||||
|
||||
export default class AsyncClipboard {
|
||||
constructor(target) {
|
||||
this._target = target || null;
|
||||
|
||||
this._isAvailable = null;
|
||||
|
||||
this._eventHandlers = {
|
||||
'focus': this._handleFocus.bind(this),
|
||||
};
|
||||
|
||||
// ===== EVENT HANDLERS =====
|
||||
|
||||
this.onpaste = () => {};
|
||||
}
|
||||
|
||||
// ===== PRIVATE METHODS =====
|
||||
|
||||
async _ensureAvailable() {
|
||||
if (this._isAvailable !== null) return this._isAvailable;
|
||||
try {
|
||||
const status = await browserAsyncClipboardSupport();
|
||||
this._isAvailable = (status === 'available');
|
||||
} catch {
|
||||
this._isAvailable = false;
|
||||
}
|
||||
return this._isAvailable;
|
||||
}
|
||||
|
||||
async _handleFocus(event) {
|
||||
if (!(await this._ensureAvailable())) return;
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
this.onpaste(text);
|
||||
} catch (error) {
|
||||
Log.Error("Clipboard read failed: ", error);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== PUBLIC METHODS =====
|
||||
|
||||
writeClipboard(text) {
|
||||
// Can lazily check cached availability
|
||||
if (!this._isAvailable) return false;
|
||||
navigator.clipboard.writeText(text)
|
||||
.catch(error => Log.Error("Clipboard write failed: ", error));
|
||||
return true;
|
||||
}
|
||||
|
||||
grab() {
|
||||
if (!this._target) return;
|
||||
this._ensureAvailable()
|
||||
.then((isAvailable) => {
|
||||
if (isAvailable) {
|
||||
this._target.addEventListener('focus', this._eventHandlers.focus);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ungrab() {
|
||||
if (!this._target) return;
|
||||
this._target.removeEventListener('focus', this._eventHandlers.focus);
|
||||
}
|
||||
}
|
||||
29
core/rfb.js
29
core/rfb.js
@@ -15,6 +15,7 @@ import { clientToElement } from './util/element.js';
|
||||
import { setCapture } from './util/events.js';
|
||||
import EventTargetMixin from './util/eventtarget.js';
|
||||
import Display from "./display.js";
|
||||
import AsyncClipboard from "./clipboard.js";
|
||||
import Inflator from "./inflator.js";
|
||||
import Deflator from "./deflator.js";
|
||||
import Keyboard from "./input/keyboard.js";
|
||||
@@ -164,6 +165,7 @@ export default class RFB extends EventTargetMixin {
|
||||
this._sock = null; // Websock object
|
||||
this._display = null; // Display object
|
||||
this._flushing = false; // Display flushing state
|
||||
this._asyncClipboard = null; // Async clipboard object
|
||||
this._keyboard = null; // Keyboard input handler object
|
||||
this._gestures = null; // Gesture input handler object
|
||||
this._resizeObserver = null; // Resize observer object
|
||||
@@ -266,6 +268,9 @@ export default class RFB extends EventTargetMixin {
|
||||
throw exc;
|
||||
}
|
||||
|
||||
this._asyncClipboard = new AsyncClipboard(this._canvas);
|
||||
this._asyncClipboard.onpaste = this.clipboardPasteFrom.bind(this);
|
||||
|
||||
this._keyboard = new Keyboard(this._canvas);
|
||||
this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
|
||||
this._remoteCapsLock = null; // Null indicates unknown or irrelevant
|
||||
@@ -315,8 +320,10 @@ export default class RFB extends EventTargetMixin {
|
||||
this._rfbConnectionState === "connected") {
|
||||
if (viewOnly) {
|
||||
this._keyboard.ungrab();
|
||||
this._asyncClipboard.ungrab();
|
||||
} else {
|
||||
this._keyboard.grab();
|
||||
this._asyncClipboard.grab();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2208,7 +2215,10 @@ export default class RFB extends EventTargetMixin {
|
||||
this._setDesktopName(name);
|
||||
this._resize(width, height);
|
||||
|
||||
if (!this._viewOnly) { this._keyboard.grab(); }
|
||||
if (!this._viewOnly) {
|
||||
this._keyboard.grab();
|
||||
this._asyncClipboard.grab();
|
||||
}
|
||||
|
||||
this._fbDepth = 24;
|
||||
|
||||
@@ -2323,6 +2333,15 @@ export default class RFB extends EventTargetMixin {
|
||||
return this._fail("Unexpected SetColorMapEntries message");
|
||||
}
|
||||
|
||||
_writeClipboard(text) {
|
||||
if (this._viewOnly) return;
|
||||
if (this._asyncClipboard.writeClipboard(text)) return;
|
||||
// Fallback clipboard
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("clipboard", {detail: {text: text}})
|
||||
);
|
||||
}
|
||||
|
||||
_handleServerCutText() {
|
||||
Log.Debug("ServerCutText");
|
||||
|
||||
@@ -2342,9 +2361,7 @@ export default class RFB extends EventTargetMixin {
|
||||
return true;
|
||||
}
|
||||
|
||||
this.dispatchEvent(new CustomEvent(
|
||||
"clipboard",
|
||||
{ detail: { text: text } }));
|
||||
this._writeClipboard(text);
|
||||
|
||||
} else {
|
||||
//Extended msg.
|
||||
@@ -2480,9 +2497,7 @@ export default class RFB extends EventTargetMixin {
|
||||
|
||||
textData = textData.replaceAll("\r\n", "\n");
|
||||
|
||||
this.dispatchEvent(new CustomEvent(
|
||||
"clipboard",
|
||||
{ detail: { text: textData } }));
|
||||
this._writeClipboard(textData);
|
||||
}
|
||||
} else {
|
||||
return this._fail("Unexpected action in extended clipboard message: " + actions);
|
||||
|
||||
@@ -11,6 +11,39 @@
|
||||
import * as Log from './logging.js';
|
||||
import Base64 from '../base64.js';
|
||||
|
||||
// Async clipboard detection
|
||||
|
||||
/* Evaluates if there is browser support for the async clipboard API and
|
||||
* relevant clipboard permissions. Returns 'unsupported' if permission states
|
||||
* cannot be resolved. On the other hand, detecting 'granted' or 'prompt'
|
||||
* permission states for both read and write indicates full API support with no
|
||||
* imposed native browser paste prompt. Conversely, detecting 'denied' indicates
|
||||
* the user elected to disable clipboard.
|
||||
*/
|
||||
export async function browserAsyncClipboardSupport() {
|
||||
if (!(navigator?.permissions?.query &&
|
||||
navigator?.clipboard?.writeText &&
|
||||
navigator?.clipboard?.readText)) {
|
||||
return 'unsupported';
|
||||
}
|
||||
try {
|
||||
const writePerm = await navigator.permissions.query(
|
||||
{name: "clipboard-write", allowWithoutGesture: true});
|
||||
const readPerm = await navigator.permissions.query(
|
||||
{name: "clipboard-read", allowWithoutGesture: false});
|
||||
if (writePerm.state === "denied" || readPerm.state === "denied") {
|
||||
return 'denied';
|
||||
}
|
||||
if ((writePerm.state === "granted" || writePerm.state === "prompt") &&
|
||||
(readPerm.state === "granted" || readPerm.state === "prompt")) {
|
||||
return 'available';
|
||||
}
|
||||
} catch {
|
||||
return 'unsupported';
|
||||
}
|
||||
return 'unsupported';
|
||||
}
|
||||
|
||||
// Touch detection
|
||||
export let isTouchDevice = ('ontouchstart' in document.documentElement) ||
|
||||
// required for Chrome debugger
|
||||
|
||||
@@ -1,6 +1,74 @@
|
||||
import { isMac, isWindows, isIOS, isAndroid, isChromeOS,
|
||||
isSafari, isFirefox, isChrome, isChromium, isOpera, isEdge,
|
||||
isGecko, isWebKit, isBlink } from '../core/util/browser.js';
|
||||
isGecko, isWebKit, isBlink,
|
||||
browserAsyncClipboardSupport } from '../core/util/browser.js';
|
||||
|
||||
describe('Async clipboard', function () {
|
||||
"use strict";
|
||||
|
||||
beforeEach(function () {
|
||||
sinon.stub(navigator, "clipboard").value({
|
||||
writeText: sinon.stub(),
|
||||
readText: sinon.stub(),
|
||||
});
|
||||
sinon.stub(navigator, "permissions").value({
|
||||
query: sinon.stub().resolves({ state: "granted" })
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it("queries permissions with correct parameters", async function () {
|
||||
const queryStub = navigator.permissions.query;
|
||||
await browserAsyncClipboardSupport();
|
||||
expect(queryStub.firstCall).to.have.been.calledWithExactly({
|
||||
name: "clipboard-write",
|
||||
allowWithoutGesture: true
|
||||
});
|
||||
expect(queryStub.secondCall).to.have.been.calledWithExactly({
|
||||
name: "clipboard-read",
|
||||
allowWithoutGesture: false
|
||||
});
|
||||
});
|
||||
|
||||
it("is available when API present and permissions granted", async function () {
|
||||
navigator.permissions.query.resolves({ state: "granted" });
|
||||
const result = await browserAsyncClipboardSupport();
|
||||
expect(result).to.equal('available');
|
||||
});
|
||||
|
||||
it("is available when API present and permissions yield 'prompt'", async function () {
|
||||
navigator.permissions.query.resolves({ state: "prompt" });
|
||||
const result = await browserAsyncClipboardSupport();
|
||||
expect(result).to.equal('available');
|
||||
});
|
||||
|
||||
it("is unavailable when permissions denied", async function () {
|
||||
navigator.permissions.query.resolves({ state: "denied" });
|
||||
const result = await browserAsyncClipboardSupport();
|
||||
expect(result).to.equal('denied');
|
||||
});
|
||||
|
||||
it("is unavailable when permissions API fails", async function () {
|
||||
navigator.permissions.query.rejects(new Error("fail"));
|
||||
const result = await browserAsyncClipboardSupport();
|
||||
expect(result).to.equal('unsupported');
|
||||
});
|
||||
|
||||
it("is unavailable when write text API missing", async function () {
|
||||
navigator.clipboard.writeText = undefined;
|
||||
const result = await browserAsyncClipboardSupport();
|
||||
expect(result).to.equal('unsupported');
|
||||
});
|
||||
|
||||
it("is unavailable when read text API missing", async function () {
|
||||
navigator.clipboard.readText = undefined;
|
||||
const result = await browserAsyncClipboardSupport();
|
||||
expect(result).to.equal('unsupported');
|
||||
});
|
||||
});
|
||||
|
||||
describe('OS detection', function () {
|
||||
let origNavigator;
|
||||
|
||||
121
tests/test.clipboard.js
Normal file
121
tests/test.clipboard.js
Normal file
@@ -0,0 +1,121 @@
|
||||
import AsyncClipboard from '../core/clipboard.js';
|
||||
|
||||
describe('Async Clipboard', function () {
|
||||
"use strict";
|
||||
|
||||
let targetMock;
|
||||
let clipboard;
|
||||
|
||||
beforeEach(function () {
|
||||
sinon.stub(navigator, "clipboard").value({
|
||||
writeText: sinon.stub().resolves(),
|
||||
readText: sinon.stub().resolves(),
|
||||
});
|
||||
|
||||
sinon.stub(navigator, "permissions").value({
|
||||
query: sinon.stub(),
|
||||
});
|
||||
|
||||
targetMock = document.createElement("canvas");
|
||||
clipboard = new AsyncClipboard(targetMock);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
targetMock = null;
|
||||
clipboard = null;
|
||||
});
|
||||
|
||||
function stubClipboardPermissions(state) {
|
||||
navigator.permissions.query
|
||||
.withArgs({ name: 'clipboard-write', allowWithoutGesture: true })
|
||||
.resolves({ state: state });
|
||||
navigator.permissions.query
|
||||
.withArgs({ name: 'clipboard-read', allowWithoutGesture: false })
|
||||
.resolves({ state: state });
|
||||
}
|
||||
|
||||
function nextTick() {
|
||||
return new Promise(resolve => setTimeout(resolve, 0));
|
||||
}
|
||||
|
||||
it('grab() adds listener if permissions granted', async function () {
|
||||
stubClipboardPermissions('granted');
|
||||
|
||||
const addListenerSpy = sinon.spy(targetMock, 'addEventListener');
|
||||
clipboard.grab();
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(addListenerSpy.calledWith('focus')).to.be.true;
|
||||
});
|
||||
|
||||
it('grab() does not add listener if permissions denied', async function () {
|
||||
stubClipboardPermissions('denied');
|
||||
|
||||
const addListenerSpy = sinon.spy(targetMock, 'addEventListener');
|
||||
clipboard.grab();
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(addListenerSpy.calledWith('focus')).to.be.false;
|
||||
});
|
||||
|
||||
it('focus event triggers onpaste() if permissions granted', async function () {
|
||||
stubClipboardPermissions('granted');
|
||||
|
||||
const text = 'hello clipboard world';
|
||||
navigator.clipboard.readText.resolves(text);
|
||||
|
||||
const spyPromise = new Promise(resolve => clipboard.onpaste = resolve);
|
||||
|
||||
clipboard.grab();
|
||||
|
||||
await nextTick();
|
||||
|
||||
targetMock.dispatchEvent(new Event('focus'));
|
||||
|
||||
const res = await spyPromise;
|
||||
expect(res).to.equal(text);
|
||||
});
|
||||
|
||||
it('focus event does not trigger onpaste() if permissions denied', async function () {
|
||||
stubClipboardPermissions('denied');
|
||||
|
||||
const text = 'should not read';
|
||||
navigator.clipboard.readText.resolves(text);
|
||||
|
||||
clipboard.onpaste = sinon.spy();
|
||||
|
||||
clipboard.grab();
|
||||
|
||||
await nextTick();
|
||||
|
||||
targetMock.dispatchEvent(new Event('focus'));
|
||||
|
||||
expect(clipboard.onpaste.called).to.be.false;
|
||||
});
|
||||
|
||||
it('writeClipboard() calls navigator.clipboard.writeText() if permissions granted', async function () {
|
||||
stubClipboardPermissions('granted');
|
||||
clipboard._isAvailable = true;
|
||||
|
||||
const text = 'writing to clipboard';
|
||||
const result = clipboard.writeClipboard(text);
|
||||
|
||||
expect(navigator.clipboard.writeText.calledWith(text)).to.be.true;
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
|
||||
it('writeClipboard() does not call navigator.clipboard.writeText() if permissions denied', async function () {
|
||||
stubClipboardPermissions('denied');
|
||||
clipboard._isAvailable = false;
|
||||
|
||||
const text = 'should not write';
|
||||
const result = clipboard.writeClipboard(text);
|
||||
|
||||
expect(navigator.clipboard.writeText.called).to.be.false;
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
|
||||
});
|
||||
@@ -3467,17 +3467,48 @@ describe('Remote Frame Buffer protocol client', function () {
|
||||
});
|
||||
|
||||
describe('Normal clipboard handling receive', function () {
|
||||
it('should fire the clipboard callback with the retrieved text on ServerCutText', function () {
|
||||
it('should not dispatch a clipboard event following successful async write clipboard', async function () {
|
||||
client._viewOnly = false;
|
||||
client._asyncClipboard = {
|
||||
writeClipboard: sinon.stub().returns(true),
|
||||
};
|
||||
const expectedStr = 'cheese!';
|
||||
const data = [3, 0, 0, 0];
|
||||
push32(data, expectedStr.length);
|
||||
for (let i = 0; i < expectedStr.length; i++) { data.push(expectedStr.charCodeAt(i)); }
|
||||
const spy = sinon.spy();
|
||||
client.addEventListener("clipboard", spy);
|
||||
|
||||
const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
|
||||
|
||||
client._sock._websocket._receiveData(new Uint8Array(data));
|
||||
expect(spy).to.have.been.calledOnce;
|
||||
expect(spy.args[0][0].detail.text).to.equal(expectedStr);
|
||||
|
||||
expect(client._asyncClipboard.writeClipboard.calledOnceWith(
|
||||
expectedStr
|
||||
)).to.be.true;
|
||||
expect(dispatchEventSpy.calledWith(
|
||||
new CustomEvent("clipboard", {detail: {expectedStr: expectedStr}})
|
||||
)).to.be.false;
|
||||
});
|
||||
|
||||
it('should dispatch a clipboard event following unsuccessful async write clipboard', async function () {
|
||||
client._viewOnly = false;
|
||||
client._asyncClipboard = {
|
||||
writeClipboard: sinon.stub().returns(false),
|
||||
};
|
||||
const expectedStr = 'cheese!';
|
||||
const data = [3, 0, 0, 0];
|
||||
push32(data, expectedStr.length);
|
||||
for (let i = 0; i < expectedStr.length; i++) { data.push(expectedStr.charCodeAt(i)); }
|
||||
|
||||
const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
|
||||
|
||||
client._sock._websocket._receiveData(new Uint8Array(data));
|
||||
|
||||
expect(client._asyncClipboard.writeClipboard.calledOnceWith(
|
||||
expectedStr
|
||||
)).to.be.true;
|
||||
expect(dispatchEventSpy.calledOnceWith(
|
||||
new CustomEvent("clipboard", {detail: {expectedStr: expectedStr}})
|
||||
)).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3530,8 +3561,71 @@ describe('Remote Frame Buffer protocol client', function () {
|
||||
client._sock._websocket._receiveData(new Uint8Array(data));
|
||||
});
|
||||
|
||||
it('should not dispatch a clipboard event following successful async write clipboard', async function () {
|
||||
client._viewOnly = false;
|
||||
client._asyncClipboard = {
|
||||
writeClipboard: sinon.stub().returns(true),
|
||||
};
|
||||
let expectedData = "Schnitzel";
|
||||
let data = [3, 0, 0, 0];
|
||||
const flags = [0x10, 0x00, 0x00, 0x01];
|
||||
|
||||
let text = encodeUTF8("Schnitzel");
|
||||
let deflatedText = deflateWithSize(text);
|
||||
|
||||
// How much data we are sending.
|
||||
push32(data, toUnsigned32bit(-(4 + deflatedText.length)));
|
||||
|
||||
data = data.concat(flags);
|
||||
data = data.concat(Array.from(deflatedText));
|
||||
|
||||
const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
|
||||
|
||||
client._sock._websocket._receiveData(new Uint8Array(data));
|
||||
|
||||
expect(client._asyncClipboard.writeClipboard.calledOnceWith(
|
||||
expectedData
|
||||
)).to.be.true;
|
||||
expect(dispatchEventSpy.calledOnceWith(
|
||||
new CustomEvent("clipboard", {detail: {expectedData: expectedData}})
|
||||
)).to.be.false;
|
||||
});
|
||||
it('should dispatch a clipboard event following unsuccessful async write clipboard', async function () {
|
||||
client._viewOnly = false;
|
||||
client._asyncClipboard = {
|
||||
writeClipboard: sinon.stub().returns(false),
|
||||
};
|
||||
let expectedData = "Potatoes";
|
||||
let data = [3, 0, 0, 0];
|
||||
const flags = [0x10, 0x00, 0x00, 0x01];
|
||||
|
||||
let text = encodeUTF8("Potatoes");
|
||||
let deflatedText = deflateWithSize(text);
|
||||
|
||||
// How much data we are sending.
|
||||
push32(data, toUnsigned32bit(-(4 + deflatedText.length)));
|
||||
|
||||
data = data.concat(flags);
|
||||
data = data.concat(Array.from(deflatedText));
|
||||
|
||||
const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
|
||||
|
||||
client._sock._websocket._receiveData(new Uint8Array(data));
|
||||
|
||||
expect(client._asyncClipboard.writeClipboard.calledOnceWith(
|
||||
expectedData
|
||||
)).to.be.true;
|
||||
expect(dispatchEventSpy.calledOnceWith(
|
||||
new CustomEvent("clipboard", {detail: {expectedData: expectedData}})
|
||||
)).to.be.true;
|
||||
});
|
||||
|
||||
describe('Handle Provide', function () {
|
||||
it('should update clipboard with correct Unicode data from a Provide message', function () {
|
||||
it('should update clipboard with correct Unicode data from a Provide message', async function () {
|
||||
client._viewOnly = false;
|
||||
client._asyncClipboard = {
|
||||
writeClipboard: sinon.stub().returns(false),
|
||||
};
|
||||
let expectedData = "Aå漢字!";
|
||||
let data = [3, 0, 0, 0];
|
||||
const flags = [0x10, 0x00, 0x00, 0x01];
|
||||
@@ -3545,16 +3639,23 @@ describe('Remote Frame Buffer protocol client', function () {
|
||||
data = data.concat(flags);
|
||||
data = data.concat(Array.from(deflatedText));
|
||||
|
||||
const spy = sinon.spy();
|
||||
client.addEventListener("clipboard", spy);
|
||||
const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
|
||||
|
||||
client._sock._websocket._receiveData(new Uint8Array(data));
|
||||
expect(spy).to.have.been.calledOnce;
|
||||
expect(spy.args[0][0].detail.text).to.equal(expectedData);
|
||||
client.removeEventListener("clipboard", spy);
|
||||
|
||||
expect(client._asyncClipboard.writeClipboard.calledOnceWith(
|
||||
expectedData
|
||||
)).to.be.true;
|
||||
expect(dispatchEventSpy.calledOnceWith(
|
||||
new CustomEvent("clipboard", {detail: {expectedData: expectedData}})
|
||||
)).to.be.true;
|
||||
});
|
||||
|
||||
it('should update clipboard with correct escape characters from a Provide message ', function () {
|
||||
it('should update clipboard with correct escape characters from a Provide message ', async function () {
|
||||
client._viewOnly = false;
|
||||
client._asyncClipboard = {
|
||||
writeClipboard: sinon.stub().returns(false),
|
||||
};
|
||||
let expectedData = "Oh\nmy\n!";
|
||||
let data = [3, 0, 0, 0];
|
||||
const flags = [0x10, 0x00, 0x00, 0x01];
|
||||
@@ -3569,16 +3670,23 @@ describe('Remote Frame Buffer protocol client', function () {
|
||||
data = data.concat(flags);
|
||||
data = data.concat(Array.from(deflatedText));
|
||||
|
||||
const spy = sinon.spy();
|
||||
client.addEventListener("clipboard", spy);
|
||||
const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
|
||||
|
||||
client._sock._websocket._receiveData(new Uint8Array(data));
|
||||
expect(spy).to.have.been.calledOnce;
|
||||
expect(spy.args[0][0].detail.text).to.equal(expectedData);
|
||||
client.removeEventListener("clipboard", spy);
|
||||
|
||||
expect(client._asyncClipboard.writeClipboard.calledOnceWith(
|
||||
expectedData
|
||||
)).to.be.true;
|
||||
expect(dispatchEventSpy.calledOnceWith(
|
||||
new CustomEvent("clipboard", {detail: {expectedData: expectedData}})
|
||||
)).to.be.true;
|
||||
});
|
||||
|
||||
it('should be able to handle large Provide messages', function () {
|
||||
it('should be able to handle large Provide messages', async function () {
|
||||
client._viewOnly = false;
|
||||
client._asyncClipboard = {
|
||||
writeClipboard: sinon.stub().returns(false),
|
||||
};
|
||||
let expectedData = "hello".repeat(100000);
|
||||
let data = [3, 0, 0, 0];
|
||||
const flags = [0x10, 0x00, 0x00, 0x01];
|
||||
@@ -3593,13 +3701,16 @@ describe('Remote Frame Buffer protocol client', function () {
|
||||
data = data.concat(flags);
|
||||
data = data.concat(Array.from(deflatedText));
|
||||
|
||||
const spy = sinon.spy();
|
||||
client.addEventListener("clipboard", spy);
|
||||
const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
|
||||
|
||||
client._sock._websocket._receiveData(new Uint8Array(data));
|
||||
expect(spy).to.have.been.calledOnce;
|
||||
expect(spy.args[0][0].detail.text).to.equal(expectedData);
|
||||
client.removeEventListener("clipboard", spy);
|
||||
|
||||
expect(client._asyncClipboard.writeClipboard.calledOnceWith(
|
||||
expectedData
|
||||
)).to.be.true;
|
||||
expect(dispatchEventSpy.calledOnceWith(
|
||||
new CustomEvent("clipboard", {detail: {expectedData: expectedData}})
|
||||
)).to.be.true;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user