Merge branch 'clipboard-async' of https://github.com/tobfah/noVNC
This commit is contained in:
commit
fb7e891841
28
app/ui.js
28
app/ui.js
|
|
@ -9,7 +9,7 @@
|
||||||
import * as Log from '../core/util/logging.js';
|
import * as Log from '../core/util/logging.js';
|
||||||
import _, { l10n } from './localization.js';
|
import _, { l10n } from './localization.js';
|
||||||
import { isTouchDevice, isMac, isIOS, isAndroid, isChromeOS, isSafari,
|
import { isTouchDevice, isMac, isIOS, isAndroid, isChromeOS, isSafari,
|
||||||
hasScrollbarGutter, dragThreshold }
|
hasScrollbarGutter, dragThreshold, browserAsyncClipboardSupport }
|
||||||
from '../core/util/browser.js';
|
from '../core/util/browser.js';
|
||||||
import { setCapture, getPointerEvent } from '../core/util/events.js';
|
import { setCapture, getPointerEvent } from '../core/util/events.js';
|
||||||
import KeyTable from "../core/input/keysym.js";
|
import KeyTable from "../core/input/keysym.js";
|
||||||
|
|
@ -1103,6 +1103,7 @@ const UI = {
|
||||||
UI.rfb.showDotCursor = UI.getSetting('show_dot');
|
UI.rfb.showDotCursor = UI.getSetting('show_dot');
|
||||||
|
|
||||||
UI.updateViewOnly(); // requires UI.rfb
|
UI.updateViewOnly(); // requires UI.rfb
|
||||||
|
UI.updateClipboard();
|
||||||
},
|
},
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
|
|
@ -1754,6 +1755,31 @@ const UI = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateClipboard() {
|
||||||
|
browserAsyncClipboardSupport()
|
||||||
|
.then((support) => {
|
||||||
|
if (support === 'unsupported') {
|
||||||
|
// Use fallback clipboard panel
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (support === 'denied' || support === 'available') {
|
||||||
|
UI.closeClipboardPanel();
|
||||||
|
document.getElementById('noVNC_clipboard_button')
|
||||||
|
.classList.add('noVNC_hidden');
|
||||||
|
document.getElementById('noVNC_clipboard_button')
|
||||||
|
.removeEventListener('click', UI.toggleClipboardPanel);
|
||||||
|
document.getElementById('noVNC_clipboard_text')
|
||||||
|
.removeEventListener('change', UI.clipboardSend);
|
||||||
|
if (UI.rfb) {
|
||||||
|
UI.rfb.removeEventListener('clipboard', UI.clipboardReceive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Treat as unsupported
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
updateShowDotCursor() {
|
updateShowDotCursor() {
|
||||||
if (!UI.rfb) return;
|
if (!UI.rfb) return;
|
||||||
UI.rfb.showDotCursor = UI.getSetting('show_dot');
|
UI.rfb.showDotCursor = UI.getSetting('show_dot');
|
||||||
|
|
|
||||||
|
|
@ -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 { setCapture } from './util/events.js';
|
||||||
import EventTargetMixin from './util/eventtarget.js';
|
import EventTargetMixin from './util/eventtarget.js';
|
||||||
import Display from "./display.js";
|
import Display from "./display.js";
|
||||||
|
import AsyncClipboard from "./clipboard.js";
|
||||||
import Inflator from "./inflator.js";
|
import Inflator from "./inflator.js";
|
||||||
import Deflator from "./deflator.js";
|
import Deflator from "./deflator.js";
|
||||||
import Keyboard from "./input/keyboard.js";
|
import Keyboard from "./input/keyboard.js";
|
||||||
|
|
@ -164,6 +165,7 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._sock = null; // Websock object
|
this._sock = null; // Websock object
|
||||||
this._display = null; // Display object
|
this._display = null; // Display object
|
||||||
this._flushing = false; // Display flushing state
|
this._flushing = false; // Display flushing state
|
||||||
|
this._asyncClipboard = null; // Async clipboard object
|
||||||
this._keyboard = null; // Keyboard input handler object
|
this._keyboard = null; // Keyboard input handler object
|
||||||
this._gestures = null; // Gesture input handler object
|
this._gestures = null; // Gesture input handler object
|
||||||
this._resizeObserver = null; // Resize observer object
|
this._resizeObserver = null; // Resize observer object
|
||||||
|
|
@ -266,6 +268,9 @@ export default class RFB extends EventTargetMixin {
|
||||||
throw exc;
|
throw exc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._asyncClipboard = new AsyncClipboard(this._canvas);
|
||||||
|
this._asyncClipboard.onpaste = this.clipboardPasteFrom.bind(this);
|
||||||
|
|
||||||
this._keyboard = new Keyboard(this._canvas);
|
this._keyboard = new Keyboard(this._canvas);
|
||||||
this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
|
this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
|
||||||
this._remoteCapsLock = null; // Null indicates unknown or irrelevant
|
this._remoteCapsLock = null; // Null indicates unknown or irrelevant
|
||||||
|
|
@ -315,8 +320,10 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._rfbConnectionState === "connected") {
|
this._rfbConnectionState === "connected") {
|
||||||
if (viewOnly) {
|
if (viewOnly) {
|
||||||
this._keyboard.ungrab();
|
this._keyboard.ungrab();
|
||||||
|
this._asyncClipboard.ungrab();
|
||||||
} else {
|
} else {
|
||||||
this._keyboard.grab();
|
this._keyboard.grab();
|
||||||
|
this._asyncClipboard.grab();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2208,7 +2215,10 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._setDesktopName(name);
|
this._setDesktopName(name);
|
||||||
this._resize(width, height);
|
this._resize(width, height);
|
||||||
|
|
||||||
if (!this._viewOnly) { this._keyboard.grab(); }
|
if (!this._viewOnly) {
|
||||||
|
this._keyboard.grab();
|
||||||
|
this._asyncClipboard.grab();
|
||||||
|
}
|
||||||
|
|
||||||
this._fbDepth = 24;
|
this._fbDepth = 24;
|
||||||
|
|
||||||
|
|
@ -2323,6 +2333,15 @@ export default class RFB extends EventTargetMixin {
|
||||||
return this._fail("Unexpected SetColorMapEntries message");
|
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() {
|
_handleServerCutText() {
|
||||||
Log.Debug("ServerCutText");
|
Log.Debug("ServerCutText");
|
||||||
|
|
||||||
|
|
@ -2342,9 +2361,7 @@ export default class RFB extends EventTargetMixin {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dispatchEvent(new CustomEvent(
|
this._writeClipboard(text);
|
||||||
"clipboard",
|
|
||||||
{ detail: { text: text } }));
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
//Extended msg.
|
//Extended msg.
|
||||||
|
|
@ -2480,9 +2497,7 @@ export default class RFB extends EventTargetMixin {
|
||||||
|
|
||||||
textData = textData.replaceAll("\r\n", "\n");
|
textData = textData.replaceAll("\r\n", "\n");
|
||||||
|
|
||||||
this.dispatchEvent(new CustomEvent(
|
this._writeClipboard(textData);
|
||||||
"clipboard",
|
|
||||||
{ detail: { text: textData } }));
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return this._fail("Unexpected action in extended clipboard message: " + actions);
|
return this._fail("Unexpected action in extended clipboard message: " + actions);
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,39 @@
|
||||||
import * as Log from './logging.js';
|
import * as Log from './logging.js';
|
||||||
import Base64 from '../base64.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
|
// Touch detection
|
||||||
export let isTouchDevice = ('ontouchstart' in document.documentElement) ||
|
export let isTouchDevice = ('ontouchstart' in document.documentElement) ||
|
||||||
// required for Chrome debugger
|
// required for Chrome debugger
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ keysym values.
|
||||||
* __Display__ (core/display.js): Efficient 2D rendering abstraction
|
* __Display__ (core/display.js): Efficient 2D rendering abstraction
|
||||||
layered on the HTML5 canvas element.
|
layered on the HTML5 canvas element.
|
||||||
|
|
||||||
|
* __Clipboard__ (core/clipboard.js): Clipboard event handler.
|
||||||
|
|
||||||
* __Websock__ (core/websock.js): Websock client from websockify
|
* __Websock__ (core/websock.js): Websock client from websockify
|
||||||
with transparent binary data support.
|
with transparent binary data support.
|
||||||
[Websock API](https://github.com/novnc/websockify-js/wiki/websock.js) wiki page.
|
[Websock API](https://github.com/novnc/websockify-js/wiki/websock.js) wiki page.
|
||||||
|
|
@ -25,10 +27,10 @@ with transparent binary data support.
|
||||||
|
|
||||||
## 1.2 Callbacks
|
## 1.2 Callbacks
|
||||||
|
|
||||||
For the Mouse, Keyboard and Display objects the callback functions are
|
For the Mouse, Keyboard, Display, and Clipboard objects, the callback
|
||||||
assigned to configuration attributes, just as for the RFB object. The
|
functions are assigned to configuration attributes, just as for the RFB
|
||||||
WebSock module has a method named 'on' that takes two parameters: the
|
object. The WebSock module has a method named 'on' that takes two
|
||||||
callback event name, and the callback function.
|
parameters: the callback event name, and the callback function.
|
||||||
|
|
||||||
## 2. Modules
|
## 2. Modules
|
||||||
|
|
||||||
|
|
@ -81,3 +83,23 @@ None
|
||||||
| blitImage | (x, y, width, height, arr, offset, from_queue) | Blit pixels (of R,G,B,A) to the display
|
| blitImage | (x, y, width, height, arr, offset, from_queue) | Blit pixels (of R,G,B,A) to the display
|
||||||
| drawImage | (img, x, y) | Draw image and track damage
|
| drawImage | (img, x, y) | Draw image and track damage
|
||||||
| autoscale | (containerWidth, containerHeight) | Scale the display
|
| autoscale | (containerWidth, containerHeight) | Scale the display
|
||||||
|
|
||||||
|
## 2.3 Clipboard module
|
||||||
|
|
||||||
|
### 2.3.1 Configuration attributes
|
||||||
|
|
||||||
|
None
|
||||||
|
|
||||||
|
### 2.3.2 Methods
|
||||||
|
|
||||||
|
| name | parameters | description
|
||||||
|
| ------------------ | ----------------- | ------------
|
||||||
|
| writeClipboard | (text) | An async write text to clipboard
|
||||||
|
| grab | () | Begin capturing clipboard events
|
||||||
|
| ungrab | () | Stop capturing clipboard events
|
||||||
|
|
||||||
|
### 2.3.3 Callbacks
|
||||||
|
|
||||||
|
| name | parameters | description
|
||||||
|
| ------- | ---------- | ------------
|
||||||
|
| onpaste | (text) | Called following a target focus event and an async clipboard read
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,74 @@
|
||||||
import { isMac, isWindows, isIOS, isAndroid, isChromeOS,
|
import { isMac, isWindows, isIOS, isAndroid, isChromeOS,
|
||||||
isSafari, isFirefox, isChrome, isChromium, isOpera, isEdge,
|
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 () {
|
describe('OS detection', function () {
|
||||||
let origNavigator;
|
let origNavigator;
|
||||||
|
|
|
||||||
|
|
@ -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 () {
|
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 expectedStr = 'cheese!';
|
||||||
const data = [3, 0, 0, 0];
|
const data = [3, 0, 0, 0];
|
||||||
push32(data, expectedStr.length);
|
push32(data, expectedStr.length);
|
||||||
for (let i = 0; i < expectedStr.length; i++) { data.push(expectedStr.charCodeAt(i)); }
|
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));
|
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));
|
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 () {
|
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 expectedData = "Aå漢字!";
|
||||||
let data = [3, 0, 0, 0];
|
let data = [3, 0, 0, 0];
|
||||||
const flags = [0x10, 0x00, 0x00, 0x01];
|
const flags = [0x10, 0x00, 0x00, 0x01];
|
||||||
|
|
@ -3545,16 +3639,23 @@ describe('Remote Frame Buffer protocol client', function () {
|
||||||
data = data.concat(flags);
|
data = data.concat(flags);
|
||||||
data = data.concat(Array.from(deflatedText));
|
data = data.concat(Array.from(deflatedText));
|
||||||
|
|
||||||
const spy = sinon.spy();
|
const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
|
||||||
client.addEventListener("clipboard", spy);
|
|
||||||
|
|
||||||
client._sock._websocket._receiveData(new Uint8Array(data));
|
client._sock._websocket._receiveData(new Uint8Array(data));
|
||||||
expect(spy).to.have.been.calledOnce;
|
|
||||||
expect(spy.args[0][0].detail.text).to.equal(expectedData);
|
expect(client._asyncClipboard.writeClipboard.calledOnceWith(
|
||||||
client.removeEventListener("clipboard", spy);
|
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 expectedData = "Oh\nmy\n!";
|
||||||
let data = [3, 0, 0, 0];
|
let data = [3, 0, 0, 0];
|
||||||
const flags = [0x10, 0x00, 0x00, 0x01];
|
const flags = [0x10, 0x00, 0x00, 0x01];
|
||||||
|
|
@ -3569,16 +3670,23 @@ describe('Remote Frame Buffer protocol client', function () {
|
||||||
data = data.concat(flags);
|
data = data.concat(flags);
|
||||||
data = data.concat(Array.from(deflatedText));
|
data = data.concat(Array.from(deflatedText));
|
||||||
|
|
||||||
const spy = sinon.spy();
|
const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
|
||||||
client.addEventListener("clipboard", spy);
|
|
||||||
|
|
||||||
client._sock._websocket._receiveData(new Uint8Array(data));
|
client._sock._websocket._receiveData(new Uint8Array(data));
|
||||||
expect(spy).to.have.been.calledOnce;
|
|
||||||
expect(spy.args[0][0].detail.text).to.equal(expectedData);
|
expect(client._asyncClipboard.writeClipboard.calledOnceWith(
|
||||||
client.removeEventListener("clipboard", spy);
|
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 expectedData = "hello".repeat(100000);
|
||||||
let data = [3, 0, 0, 0];
|
let data = [3, 0, 0, 0];
|
||||||
const flags = [0x10, 0x00, 0x00, 0x01];
|
const flags = [0x10, 0x00, 0x00, 0x01];
|
||||||
|
|
@ -3593,13 +3701,16 @@ describe('Remote Frame Buffer protocol client', function () {
|
||||||
data = data.concat(flags);
|
data = data.concat(flags);
|
||||||
data = data.concat(Array.from(deflatedText));
|
data = data.concat(Array.from(deflatedText));
|
||||||
|
|
||||||
const spy = sinon.spy();
|
const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
|
||||||
client.addEventListener("clipboard", spy);
|
|
||||||
|
|
||||||
client._sock._websocket._receiveData(new Uint8Array(data));
|
client._sock._websocket._receiveData(new Uint8Array(data));
|
||||||
expect(spy).to.have.been.calledOnce;
|
|
||||||
expect(spy.args[0][0].detail.text).to.equal(expectedData);
|
expect(client._asyncClipboard.writeClipboard.calledOnceWith(
|
||||||
client.removeEventListener("clipboard", spy);
|
expectedData
|
||||||
|
)).to.be.true;
|
||||||
|
expect(dispatchEventSpy.calledOnceWith(
|
||||||
|
new CustomEvent("clipboard", {detail: {expectedData: expectedData}})
|
||||||
|
)).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue