Public Access
Selecting output and right-clicking it opened the WebView's own context menu with Copy on it, which is correct behaviour for a web page and wrong for a terminal. What a terminal's right button has meant since the Windows console got QuickEdit, and since PuTTY shipped, is this: with something selected it copies and drops the selection, with nothing selected it pastes. No menu, no second click. The gesture can only live in the page. The selection is drawn into a canvas the host never sees, and once a terminal has focus the host's window receives no input events at all — the same reason the Ctrl+plus chords are handled there. ◆ THE CLIPBOARD IS THE HOST'S IN BOTH DIRECTIONS, AND THE READING IS WHY. navigator.clipboard.readText() is gated behind the clipboard-read permission in every Chromium, and an embedded WebView with no PermissionRequested handler answers a permission request by putting a prompt on the screen. Trading a context menu for a permission dialog is not this change. So the page asks and the host answers, exactly as requestFontSizeStep does. Writing does not go the other way either, although navigator.clipboard.writeText would have worked from the page on Windows: a clipboard that is the host's in one direction and the page's in the other is two mechanisms to keep working across WebView2, WebKitGTK and Android's WebView. The host has owned a clipboard delegate since the keychain grew its COPY buttons, so each App.axaml.cs grows a ClipboardReader beside its ClipboardWriter and MainWindowViewModel takes it. Three opcodes: client ClipboardCopy and ClipboardPaste, server Clipboard. ◆ Clipboard IS NOT Paste, AND THE DIFFERENCE IS THE BRACKETING. Paste carries a snippet off the sidebar, and the page deliberately *types* a single-line one rather than bracketing it, because bash marks a bracketed paste as an active region and a snippet should read as though it had been typed. A clipboard paste is the case those markers are for — readline uses them to decide it has been pasted into — so Clipboard is always bracketed and never followed by an Enter. One opcode for both would have needed a flag meaning "which of the two kinds of paste is this", on a payload whose existing flag byte means something else. Both client frames are dispatched before the session lookup, which is what keeps the gesture working in a pane whose shell has just ended: squinting at the message a remote left behind is exactly when somebody copies the line. Dispatch was past MA0051's length once it grew a second such branch, so the frames no pump needs to see moved into Announce() and the font-size chord went with them. ◆ CAPTURED AND STOPPED, RATHER THAN MERELY PREVENTED. preventDefault alone suppresses the menu and still lets xterm's own contextmenu handler run, which moves its helper textarea under the pointer at z-index 1000 and leaves the selection sitting in it, selected. Both of those exist to serve the native menu. Taking the event in the capture phase on the pane and stopping it there is what keeps the gesture wholly the page's. rightClickSelectsWord is now false, which on macOS is a change of default rather than a restatement of it: xterm sets it true there alone, so a right-click would have selected the word under the pointer and copied that instead of pasting — wrong on one platform only, which is the worst way for it to be wrong. The renderer harness grows the pane it has been inviting since RendererReconnectionTests: a fake Terminal, the two addons, and a real UTF-8 codec, because both directions run through the page's own TextEncoder and TextDecoder and a Latin-1 stub would pass a test with an em dash in the selection. Its warning about declaring names terminal.js declares now covers the half that does not SyntaxError — a harness frame() was silently replaced by the page's frame() and failed six tests on a type error a long way from the cause. Manual checks 1.11 and 11.13 are what is left for a person: that a real contextmenu event reaches a capture listener, that no native menu is drawn once it is prevented, and that xterm's real paste() brackets. 11.13 also records what is unverified — whether Android's WebView delivers a pointer's secondary click as a contextmenu event at all. ClipboardReader is wired on that head regardless, because the alternative is a head where the paste half silently does nothing and nobody finds out; that head is not compiled here either, this machine having no android workload. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
552 lines
18 KiB
JavaScript
552 lines
18 KiB
JavaScript
'use strict';
|
|
|
|
/*
|
|
The world terminal.js is loaded into by RendererReconnectionTests.
|
|
|
|
It fakes exactly what the page's transport touches and nothing else: a WebSocket whose every step is
|
|
driven from the test, a clock that only moves when a test moves it, and the two DOM objects the page
|
|
reads at load.
|
|
|
|
◆ AND, SINCE RendererClipboardTests, A PANE.
|
|
|
|
Which the reconnection tests never needed and this file used to say so, inviting whoever first opened a
|
|
session to add the Terminal, the fit addon and the WebGL addon rather than guessing at them in advance.
|
|
They are below, and they are stubs with a memory: a fake terminal records what was pasted into it, what
|
|
was typed into it and what it was asked to focus, because those three are the whole of what the page does
|
|
to a pane and therefore the whole of what a test can ask about. The selection is settable from a test,
|
|
since a selection is made with a mouse in a canvas and there is no canvas here.
|
|
|
|
What a fake terminal is NOT is xterm. It cannot say whether paste() brackets its argument or whether
|
|
getSelection() trims a row's right padding — that is xterm's behaviour, exercised by xterm's own suite and
|
|
by docs/manual-checks.md 1.11 here. What these tests can prove is which of the two the page calls, with
|
|
what, and addressed to which session, and that is where every mistake in this area has actually been.
|
|
|
|
◆ NOTHING HERE MAY DECLARE A NAME terminal.js ALSO DECLARES.
|
|
|
|
Both files are evaluated as scripts into the same global scope, and a collision goes one of two ways.
|
|
Against one of the page's `const`s — its own `root`, say — it is a SyntaxError before a line of either
|
|
runs, which is the good outcome: it is impossible to miss. That is why the page's two elements are
|
|
`pageRoot` and `pageBanner` below and reached through getElementById, which is how the page reaches them
|
|
anyway.
|
|
|
|
Against one of the page's *functions* there is no error at all. The page is evaluated second, so its
|
|
declaration silently replaces this file's and every test calling that name gets the page's function
|
|
instead — which is how `frame()` here, meant to read a sent frame back, quietly returned the ArrayBuffer
|
|
that terminal.js's own frame() builds instead, and failed six tests on a type error a long way from the
|
|
cause. Hence `sentFrames`/`sentFrame` below: prefixed on purpose, not for tidiness.
|
|
|
|
◆ AND THE FAKE SOCKET'S BEHAVIOUR IS THE PART TO GET RIGHT.
|
|
|
|
close() is the one method with a rule that is not obvious and that the tests lean on: a socket closed
|
|
while it is still CONNECTING never fires 'close' at all, per the WebSocket specification, while one
|
|
closed after it opened does. The page relies on exactly that — connect() closes the attempt it abandons
|
|
and expects to hear nothing back from it — so a fake that fired 'close' either way would report the loop
|
|
the page's guards exist to prevent, and one that fired it neither way would hide it.
|
|
*/
|
|
|
|
var clock = { now: 0, next: 1, timers: {} };
|
|
|
|
/** Every socket the page has opened, in order, live or dead. */
|
|
var sockets = [];
|
|
|
|
/** Every frame the page has sent, in order, as arrays of byte values. */
|
|
var outbound = [];
|
|
|
|
/** Every fake terminal the page has opened on a pane, in the order it opened them. */
|
|
var terminals = [];
|
|
|
|
/** What the last right-click did about the browser's own menu. See rightClick(). */
|
|
var gesture = { prevented: 0, stopped: 0 };
|
|
|
|
function listeners(target) {
|
|
target.handlers = {};
|
|
|
|
target.addEventListener = function (name, handler) {
|
|
if (!target.handlers[name]) {
|
|
target.handlers[name] = [];
|
|
}
|
|
|
|
target.handlers[name].push(handler);
|
|
};
|
|
|
|
target.fire = function (name, event) {
|
|
var handlers = target.handlers[name] || [];
|
|
|
|
for (var i = 0; i < handlers.length; i++) {
|
|
handlers[i](event || {});
|
|
}
|
|
};
|
|
|
|
return target;
|
|
}
|
|
|
|
function FakeSocket(url, protocols) {
|
|
this.url = url;
|
|
this.protocols = protocols;
|
|
this.readyState = FakeSocket.CONNECTING;
|
|
this.binaryType = '';
|
|
|
|
listeners(this);
|
|
sockets.push(this);
|
|
}
|
|
|
|
FakeSocket.CONNECTING = 0;
|
|
FakeSocket.OPEN = 1;
|
|
FakeSocket.CLOSING = 2;
|
|
FakeSocket.CLOSED = 3;
|
|
|
|
/**
|
|
* Records rather than discards, because half of what the page does on a right-click is a frame.
|
|
*
|
|
* Kept as raw bytes and decoded on the way out — see sentFrame() — so that a payload the page wrote as
|
|
* UTF-8 is read back as UTF-8 rather than as whatever a stringification would have made of it.
|
|
*/
|
|
FakeSocket.prototype.send = function (buffer) {
|
|
var bytes = new Uint8Array(buffer);
|
|
var copy = [];
|
|
|
|
for (var i = 0; i < bytes.length; i++) {
|
|
copy.push(bytes[i]);
|
|
}
|
|
|
|
outbound.push(copy);
|
|
};
|
|
|
|
/** What the page calls. See the note above on why a connecting socket goes quietly. */
|
|
FakeSocket.prototype.close = function () {
|
|
if (this.readyState === FakeSocket.CLOSED) {
|
|
return;
|
|
}
|
|
|
|
var wasConnecting = this.readyState === FakeSocket.CONNECTING;
|
|
this.readyState = FakeSocket.CLOSED;
|
|
|
|
if (!wasConnecting) {
|
|
this.fire('close');
|
|
}
|
|
};
|
|
|
|
var pageBanner = { textContent: 'Connecting…' };
|
|
|
|
var pageRoot = listeners({
|
|
dataset: { token: 'test-token', socket: 'ws://127.0.0.1:1/socket' },
|
|
appendChild: function () {},
|
|
});
|
|
|
|
var document = listeners({
|
|
hidden: false,
|
|
getElementById: function (id) { return id === 'root' ? pageRoot : pageBanner; },
|
|
createElement: function () { return newElement(); },
|
|
});
|
|
|
|
/**
|
|
* A pane, as far as the page can tell.
|
|
*
|
|
* With real dimensions, and not because anything would fail without them — which is the point. The page's
|
|
* guard is `clientWidth < MINIMUM_FITTABLE_PIXELS`, and an undefined width compares false against every
|
|
* number, so a fake with no dimensions slips past a rule it ought to have been caught by and gets fitted
|
|
* anyway. A fake that passes for the wrong reason stops matching the page the day that guard is written
|
|
* differently.
|
|
*
|
|
* Listeners, because the right-click handler is registered on this and there is nowhere else to fire it
|
|
* from.
|
|
*/
|
|
function newElement() {
|
|
return listeners({
|
|
dataset: {},
|
|
style: {},
|
|
clientWidth: 800,
|
|
clientHeight: 600,
|
|
remove: function () {},
|
|
});
|
|
}
|
|
|
|
var window = listeners({});
|
|
|
|
// The page warns through this on paths no reconnection test reaches. Present so that a test which does
|
|
// reach one fails on its own assertion rather than on a missing global.
|
|
var console = { warn: function () {}, log: function () {} };
|
|
|
|
function setTimeout(callback, delay) {
|
|
var id = clock.next++;
|
|
|
|
clock.timers[id] = { at: clock.now + (delay || 0), callback: callback };
|
|
|
|
return id;
|
|
}
|
|
|
|
function clearTimeout(id) {
|
|
delete clock.timers[id];
|
|
}
|
|
|
|
function ResizeObserver() {
|
|
this.observe = function () {};
|
|
}
|
|
|
|
// ── The pane's three globals, which the page reaches by the names the UMD builds attach ──────────────
|
|
|
|
/**
|
|
* A terminal that remembers what was done to it.
|
|
*
|
|
* `selection` is a plain field rather than anything computed, and settable from a test through select():
|
|
* a selection is made by dragging a mouse across a canvas, which is the one part of this there is no
|
|
* standing in for. What the tests are about is what the page does with whatever is selected.
|
|
*/
|
|
function FakeTerminal(options) {
|
|
this.options = options || {};
|
|
this.cols = 80;
|
|
this.rows = 24;
|
|
this.selection = '';
|
|
this.pastes = [];
|
|
this.inputs = [];
|
|
this.writes = [];
|
|
this.focuses = 0;
|
|
this.disposed = false;
|
|
this.pane = null;
|
|
|
|
terminals.push(this);
|
|
}
|
|
|
|
FakeTerminal.prototype.loadAddon = function () {};
|
|
FakeTerminal.prototype.open = function (pane) { this.pane = pane; };
|
|
FakeTerminal.prototype.attachCustomKeyEventHandler = function (handler) { this.keys = handler; };
|
|
FakeTerminal.prototype.onData = function (handler) { this.data = handler; };
|
|
FakeTerminal.prototype.onResize = function (handler) { this.resized = handler; };
|
|
FakeTerminal.prototype.focus = function () { this.focuses++; };
|
|
FakeTerminal.prototype.paste = function (text) { this.pastes.push(text); };
|
|
FakeTerminal.prototype.input = function (text) { this.inputs.push(text); };
|
|
FakeTerminal.prototype.getSelection = function () { return this.selection; };
|
|
FakeTerminal.prototype.clearSelection = function () { this.selection = ''; };
|
|
FakeTerminal.prototype.dispose = function () { this.disposed = true; };
|
|
|
|
/**
|
|
* The write callback is the acknowledgement, so it is called rather than kept.
|
|
*
|
|
* Synchronously, which a real xterm does not promise — it renders first. The page's contract is that the
|
|
* acknowledgement is sent from this callback and not before, and calling it straight away is what lets a
|
|
* test assert that without needing a frame clock.
|
|
*/
|
|
FakeTerminal.prototype.write = function (data, done) {
|
|
this.writes.push(data);
|
|
|
|
if (done) {
|
|
done();
|
|
}
|
|
};
|
|
|
|
var Terminal = FakeTerminal;
|
|
|
|
var FitAddon = {
|
|
FitAddon: function () {
|
|
this.fit = function () {};
|
|
},
|
|
};
|
|
|
|
var WebglAddon = {
|
|
WebglAddon: function () {
|
|
this.onContextLoss = function () {};
|
|
this.dispose = function () {};
|
|
},
|
|
};
|
|
|
|
// ── UTF-8, because the page encodes and decodes every payload through these ──────────────────────────
|
|
//
|
|
// Written out rather than stubbed as one byte per character. The page sends a selection as UTF-8 and reads
|
|
// a clipboard as UTF-8, so a harness that treated the two as Latin-1 would pass a test with an em dash in
|
|
// the selection and ship a page that mangled it.
|
|
|
|
function TextEncoder() {
|
|
this.encode = function (text) {
|
|
var bytes = [];
|
|
|
|
for (var i = 0; i < text.length; i++) {
|
|
var point = text.charCodeAt(i);
|
|
|
|
// A surrogate pair is one code point in two units. Combined here, because encoding each half on its
|
|
// own is exactly the mojibake this exists to catch.
|
|
if (point >= 0xd800 && point <= 0xdbff && i + 1 < text.length) {
|
|
var low = text.charCodeAt(i + 1);
|
|
|
|
if (low >= 0xdc00 && low <= 0xdfff) {
|
|
point = 0x10000 + ((point - 0xd800) << 10) + (low - 0xdc00);
|
|
i++;
|
|
}
|
|
}
|
|
|
|
if (point < 0x80) {
|
|
bytes.push(point);
|
|
} else if (point < 0x800) {
|
|
bytes.push(0xc0 | (point >> 6), 0x80 | (point & 0x3f));
|
|
} else if (point < 0x10000) {
|
|
bytes.push(0xe0 | (point >> 12), 0x80 | ((point >> 6) & 0x3f), 0x80 | (point & 0x3f));
|
|
} else {
|
|
bytes.push(
|
|
0xf0 | (point >> 18),
|
|
0x80 | ((point >> 12) & 0x3f),
|
|
0x80 | ((point >> 6) & 0x3f),
|
|
0x80 | (point & 0x3f));
|
|
}
|
|
}
|
|
|
|
return new Uint8Array(bytes);
|
|
};
|
|
}
|
|
|
|
function TextDecoder() {
|
|
this.decode = function (bytes) {
|
|
var text = '';
|
|
|
|
for (var i = 0; i < bytes.length;) {
|
|
var first = bytes[i];
|
|
var length = first < 0x80 ? 1 : first < 0xe0 ? 2 : first < 0xf0 ? 3 : 4;
|
|
var point = length === 1 ? first : first & (0x7f >> length);
|
|
|
|
for (var n = 1; n < length; n++) {
|
|
point = (point << 6) | (bytes[i + n] & 0x3f);
|
|
}
|
|
|
|
i += length;
|
|
|
|
if (point > 0xffff) {
|
|
point -= 0x10000;
|
|
text += String.fromCharCode(0xd800 + (point >> 10), 0xdc00 + (point & 0x3ff));
|
|
} else {
|
|
text += String.fromCharCode(point);
|
|
}
|
|
}
|
|
|
|
return text;
|
|
};
|
|
}
|
|
|
|
// ── What the tests drive the page with ──────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Runs every timer due within the next `ms`, in the order they fall due.
|
|
*
|
|
* One at a time and re-scanned each round rather than collected up front, because a timer's callback
|
|
* routinely schedules the next one — which is the whole shape of the page's retry — and a snapshot taken
|
|
* before the first callback ran would miss it.
|
|
*/
|
|
function advance(ms) {
|
|
var target = clock.now + ms;
|
|
|
|
for (;;) {
|
|
var dueId = null;
|
|
var due = null;
|
|
|
|
for (var id in clock.timers) {
|
|
var timer = clock.timers[id];
|
|
|
|
if (timer.at <= target && (due === null || timer.at < due.at)) {
|
|
due = timer;
|
|
dueId = id;
|
|
}
|
|
}
|
|
|
|
if (due === null) {
|
|
break;
|
|
}
|
|
|
|
delete clock.timers[dueId];
|
|
clock.now = due.at;
|
|
due.callback();
|
|
}
|
|
|
|
clock.now = target;
|
|
}
|
|
|
|
/** How many sockets the page has opened since it loaded. */
|
|
function attempts() {
|
|
return sockets.length;
|
|
}
|
|
|
|
/** What the one status element says — the banner the user sees. */
|
|
function banner() {
|
|
return pageBanner.textContent;
|
|
}
|
|
|
|
/** How many frames the page has sent since it loaded. */
|
|
function sentFrames() {
|
|
return outbound.length;
|
|
}
|
|
|
|
/**
|
|
* One frame the page sent, as "opcode/session/payload", the payload decoded as UTF-8.
|
|
*
|
|
* One string rather than three values because it crosses into C# as one, and a test asserting on
|
|
* "5/7/whoami" reads as the frame it is. Resize frames land here too and read as rubbish; nothing asserts
|
|
* on those, and rendering them some other way would mean a second shape for a reader to learn.
|
|
*/
|
|
function sentFrame(index) {
|
|
var bytes = outbound[index];
|
|
var session = ((bytes[1] << 24) | (bytes[2] << 16) | (bytes[3] << 8) | bytes[4]) >>> 0;
|
|
|
|
return bytes[0] + '/' + session + '/' + new TextDecoder().decode(bytes.slice(5));
|
|
}
|
|
|
|
/** Hands the page a frame, on whichever socket is its current one. */
|
|
function deliverFrame(opcode, sessionId, payload) {
|
|
var bytes = new Uint8Array(5 + payload.length);
|
|
var header = new DataView(bytes.buffer);
|
|
|
|
header.setUint8(0, opcode);
|
|
header.setUint32(1, sessionId);
|
|
bytes.set(payload, 5);
|
|
|
|
sockets[sockets.length - 1].fire('message', { data: bytes.buffer });
|
|
}
|
|
|
|
/** SessionOpened, which is what makes the page build a pane. */
|
|
function openSession(sessionId) {
|
|
deliverFrame(2, sessionId, []);
|
|
}
|
|
|
|
/** Clipboard: the host's answer to a right-click paste. */
|
|
function deliverClipboard(sessionId, text) {
|
|
deliverFrame(8, sessionId, new TextEncoder().encode(text));
|
|
}
|
|
|
|
/** Paste: a snippet off the sidebar, which is deliberately not the same frame. */
|
|
function deliverSnippet(sessionId, text, execute) {
|
|
var body = new TextEncoder().encode(text);
|
|
var payload = new Uint8Array(1 + body.length);
|
|
|
|
payload[0] = execute ? 1 : 0;
|
|
payload.set(body, 1);
|
|
|
|
deliverFrame(6, sessionId, payload);
|
|
}
|
|
|
|
/** The pane behind one session, found the way the page labelled it. */
|
|
function terminalFor(sessionId) {
|
|
for (var i = 0; i < terminals.length; i++) {
|
|
if (terminals[i].pane && terminals[i].pane.dataset.sessionId === String(sessionId)) {
|
|
return terminals[i];
|
|
}
|
|
}
|
|
|
|
throw new Error('No pane for session ' + sessionId + '.');
|
|
}
|
|
|
|
/** Drags a selection across that pane, which is the one thing a fake terminal cannot do for itself. */
|
|
function select(sessionId, text) {
|
|
terminalFor(sessionId).selection = text;
|
|
}
|
|
|
|
/** What is selected in that pane now — how a test sees that a copy dropped it. */
|
|
function selection(sessionId) {
|
|
return terminalFor(sessionId).selection;
|
|
}
|
|
|
|
/**
|
|
* The right button, over that pane.
|
|
*
|
|
* The event carries only the two methods the page calls on it, and counts the calls: suppressing the
|
|
* browser's own menu is half of what this gesture has to do, and a handler that copied correctly and left
|
|
* the menu up would be a regression no assertion about frames could see.
|
|
*/
|
|
function rightClick(sessionId) {
|
|
gesture = { prevented: 0, stopped: 0 };
|
|
|
|
terminalFor(sessionId).pane.fire('contextmenu', {
|
|
preventDefault: function () { gesture.prevented++; },
|
|
stopPropagation: function () { gesture.stopped++; },
|
|
});
|
|
}
|
|
|
|
/**
|
|
* The right button somewhere that is not a pane — the status banner, in practice.
|
|
*
|
|
* Fired on the document rather than on an element, which is where the page's fallback rule lives: a pane
|
|
* stops the event before it gets here, so this is the path only a click that missed every pane takes.
|
|
*/
|
|
function rightClickOffAPane() {
|
|
gesture = { prevented: 0, stopped: 0 };
|
|
|
|
document.fire('contextmenu', {
|
|
preventDefault: function () { gesture.prevented++; },
|
|
stopPropagation: function () { gesture.stopped++; },
|
|
});
|
|
}
|
|
|
|
/** Whether the last right-click took the event away from the browser, and from xterm. */
|
|
function prevented() {
|
|
return gesture.prevented;
|
|
}
|
|
|
|
function stopped() {
|
|
return gesture.stopped;
|
|
}
|
|
|
|
/** Everything paste() was given for that pane, joined — the bracketed route. */
|
|
function pasted(sessionId) {
|
|
return terminalFor(sessionId).pastes.join('|');
|
|
}
|
|
|
|
/** Everything input() was given for that pane, joined — the typed route. */
|
|
function typed(sessionId) {
|
|
return terminalFor(sessionId).inputs.join('|');
|
|
}
|
|
|
|
/** How many times that pane has been asked for the caret. */
|
|
function focuses(sessionId) {
|
|
return terminalFor(sessionId).focuses;
|
|
}
|
|
|
|
/** Whether that attempt has been closed, by the page or by the far end. */
|
|
function isClosed(index) {
|
|
return sockets[index].readyState === FakeSocket.CLOSED;
|
|
}
|
|
|
|
/** The host accepted the upgrade. */
|
|
function accept(index) {
|
|
sockets[index].readyState = FakeSocket.OPEN;
|
|
sockets[index].fire('open');
|
|
}
|
|
|
|
/** An established socket goes away and the page is told. */
|
|
function drop(index) {
|
|
sockets[index].readyState = FakeSocket.CLOSED;
|
|
sockets[index].fire('close');
|
|
}
|
|
|
|
/** An attempt that never connects: 'error' then 'close', as a refused connection reports itself. */
|
|
function fail(index) {
|
|
sockets[index].readyState = FakeSocket.CLOSED;
|
|
sockets[index].fire('error');
|
|
sockets[index].fire('close');
|
|
}
|
|
|
|
/**
|
|
* A close arriving for a socket that died earlier — the host's takeover abort, landing late.
|
|
*
|
|
* Separate from drop() because the point of it is the delay: the socket is already dead by the time the
|
|
* event is delivered, which is what a suspended renderer's queued events look like on the way back.
|
|
*/
|
|
function deliverLateClose(index) {
|
|
sockets[index].readyState = FakeSocket.CLOSED;
|
|
sockets[index].fire('close');
|
|
}
|
|
|
|
/** The screen the page is on goes away, and comes back. */
|
|
function becomeHidden() {
|
|
document.hidden = true;
|
|
document.fire('visibilitychange');
|
|
}
|
|
|
|
function becomeVisible() {
|
|
document.hidden = false;
|
|
document.fire('visibilitychange');
|
|
}
|
|
|
|
function takeFocus() {
|
|
window.fire('focus');
|
|
}
|
|
|
|
function comeOnline() {
|
|
window.fire('online');
|
|
}
|
|
|
|
var WebSocket = FakeSocket;
|