mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-10-26 06:19:53 +00:00
49efd640c1
Avoid resetting the canvas during visual viewport scroll events and only resize when the rendered viewport dimensions actually change. This prevents transient black bars while interacting with the remote desktop on tablets. Thanks: INSOLVE (Honorary); Marco Jakobs (@jacotec); MyNameisStitch (@MyNameisStitch); Redspin (@playerumpknow)
259 lines
8.5 KiB
JavaScript
259 lines
8.5 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('assert');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const vm = require('vm');
|
|
|
|
describe('mobile i18n keys', () => {
|
|
const langDir = path.join(__dirname, '../lang');
|
|
const locales = fs.readdirSync(langDir).filter(f => f.endsWith('.json'));
|
|
|
|
const requiredMobileNav = ['label', 'home', 'devices', 'remote', 'chat', 'more'];
|
|
const requiredRemote = [
|
|
'input_mode_touch',
|
|
'input_mode_touchpad',
|
|
'show_keyboard',
|
|
'special_keys',
|
|
'phone_unsupported_title',
|
|
'phone_unsupported_body',
|
|
'phone_unsupported_back_devices'
|
|
];
|
|
|
|
for (const file of locales) {
|
|
it(`${file} has mobile_nav and remote mobile keys`, () => {
|
|
const data = JSON.parse(fs.readFileSync(path.join(langDir, file), 'utf8'));
|
|
assert.ok(data.mobile_nav, `${file} missing mobile_nav`);
|
|
for (const key of requiredMobileNav) {
|
|
assert.ok(data.mobile_nav[key], `${file} mobile_nav.${key}`);
|
|
}
|
|
assert.ok(data.remote, `${file} missing remote`);
|
|
for (const key of requiredRemote) {
|
|
assert.ok(data.remote[key], `${file} remote.${key}`);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
function loadDeviceCapabilities(opts) {
|
|
opts = opts || {};
|
|
const width = opts.width !== undefined ? opts.width : 400;
|
|
const window = {
|
|
innerWidth: width,
|
|
innerHeight: opts.height || 800,
|
|
navigator: { maxTouchPoints: opts.touch ? 5 : 0 },
|
|
matchMedia: opts.matchMedia || (() => ({ matches: false, media: 'not all', addEventListener: () => {} })),
|
|
visualViewport: opts.visualViewport || null,
|
|
addEventListener: () => {},
|
|
dispatchEvent: () => true
|
|
};
|
|
const sandbox = {
|
|
window,
|
|
document: {
|
|
readyState: 'complete',
|
|
body: { classList: { toggle: () => {}, contains: () => false } },
|
|
documentElement: { clientWidth: width },
|
|
addEventListener: () => {}
|
|
},
|
|
DeviceCapabilities: null
|
|
};
|
|
sandbox.global = window;
|
|
const code = fs.readFileSync(path.join(__dirname, '../public/js/device-capabilities.js'), 'utf8');
|
|
vm.runInNewContext(code, sandbox);
|
|
return sandbox.window.DeviceCapabilities;
|
|
}
|
|
|
|
describe('DeviceCapabilities', () => {
|
|
it('exposes breakpoint constants and helpers', () => {
|
|
const DC = loadDeviceCapabilities({ width: 400, touch: true });
|
|
assert.strictEqual(DC.BP_PHONE, 767);
|
|
assert.strictEqual(DC.isPhone(), true);
|
|
assert.strictEqual(DC.isTablet(), false);
|
|
});
|
|
|
|
it('isPhone() is false when width is 0 (pre-layout / WebView)', () => {
|
|
const DC = loadDeviceCapabilities({ width: 0 });
|
|
assert.strictEqual(DC.isPhone(), false);
|
|
});
|
|
|
|
it('isPhone() is false on desktop with fine pointer and hover', () => {
|
|
const DC = loadDeviceCapabilities({
|
|
width: 1920,
|
|
matchMedia: (q) => ({
|
|
matches: q === '(hover: hover) and (pointer: fine)',
|
|
media: q,
|
|
addEventListener: () => {}
|
|
})
|
|
});
|
|
assert.strictEqual(DC.isPhone(), false);
|
|
});
|
|
|
|
it('isPhone() is true on small touch viewport without hover', () => {
|
|
const DC = loadDeviceCapabilities({ width: 400, touch: true });
|
|
assert.strictEqual(DC.isPhone(), true);
|
|
});
|
|
|
|
it('isPhone() is false on tablet width', () => {
|
|
const DC = loadDeviceCapabilities({ width: 900, touch: true });
|
|
assert.strictEqual(DC.isPhone(), false);
|
|
assert.strictEqual(DC.isTablet(), true);
|
|
});
|
|
|
|
it('getWidth uses visualViewport when innerWidth is 0', () => {
|
|
const DC = loadDeviceCapabilities({
|
|
width: 0,
|
|
visualViewport: { width: 820, height: 600, addEventListener: () => {} }
|
|
});
|
|
assert.strictEqual(DC.isPhone(), false);
|
|
assert.strictEqual(DC.isTablet(), true);
|
|
});
|
|
});
|
|
|
|
describe('RDTouch tablet input', () => {
|
|
function loadTouch() {
|
|
const sandbox = {
|
|
console,
|
|
performance: { now: () => 100 },
|
|
setTimeout,
|
|
clearTimeout,
|
|
RDInput: {
|
|
MOUSE_TYPE_DOWN: 1,
|
|
MOUSE_TYPE_UP: 2,
|
|
MOUSE_TYPE_WHEEL: 3,
|
|
MOUSE_BUTTON_LEFT: 1,
|
|
MOUSE_BUTTON_RIGHT: 2,
|
|
MOUSE_BUTTON_MIDDLE: 4,
|
|
},
|
|
};
|
|
const code = fs.readFileSync(
|
|
path.join(__dirname, '../public/js/rdclient/touch.js'),
|
|
'utf8'
|
|
);
|
|
vm.runInNewContext(code + '\nthis.RDTouch = RDTouch;', sandbox);
|
|
return sandbox.RDTouch;
|
|
}
|
|
|
|
function makeTouchHarness() {
|
|
const listeners = {};
|
|
const canvas = {
|
|
style: {},
|
|
addEventListener(type, handler) { listeners[type] = handler; },
|
|
removeEventListener() {},
|
|
getBoundingClientRect: () => ({ left: 0, top: 0 }),
|
|
};
|
|
const renderer = {
|
|
remoteWidth: 1920,
|
|
remoteHeight: 1080,
|
|
canvasToRemote: (x, y) => ({ x, y }),
|
|
};
|
|
const sent = [];
|
|
const Touch = loadTouch();
|
|
const touch = new Touch(canvas, renderer, (message) => sent.push(message));
|
|
touch.start();
|
|
return { touch, listeners, sent };
|
|
}
|
|
|
|
function eventFor(touchPoint, touches) {
|
|
return {
|
|
touches: touches || [touchPoint],
|
|
changedTouches: [touchPoint],
|
|
preventDefault() {},
|
|
};
|
|
}
|
|
|
|
it('maps a single direct touch into a left click', () => {
|
|
const { listeners, sent } = makeTouchHarness();
|
|
const point = { identifier: 1, clientX: 100, clientY: 120 };
|
|
|
|
listeners.touchstart(eventFor(point));
|
|
listeners.touchend(eventFor(point, []));
|
|
|
|
expect(sent.map((message) => message.mouseEvent.mask)).toEqual([9, 10]);
|
|
});
|
|
|
|
it('does not click after a touchpad drag', () => {
|
|
const { touch, listeners, sent } = makeTouchHarness();
|
|
touch.setMode('touchpad');
|
|
const start = { identifier: 1, clientX: 100, clientY: 120 };
|
|
const moved = { identifier: 1, clientX: 140, clientY: 120 };
|
|
|
|
listeners.touchstart(eventFor(start));
|
|
listeners.touchmove(eventFor(moved));
|
|
listeners.touchend(eventFor(moved, []));
|
|
|
|
expect(sent.some((message) => message.mouseEvent.mask === 9)).toBe(false);
|
|
expect(sent.some((message) => message.mouseEvent.mask === 10)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('RDRenderer mobile resize', () => {
|
|
function loadRenderer() {
|
|
const sandbox = {
|
|
console,
|
|
window: null,
|
|
devicePixelRatio: 2,
|
|
document: {
|
|
createElement: () => ({
|
|
getContext: () => ({})
|
|
})
|
|
}
|
|
};
|
|
sandbox.window = sandbox;
|
|
const code = fs.readFileSync(
|
|
path.join(__dirname, '../public/js/rdclient/renderer.js'),
|
|
'utf8'
|
|
);
|
|
vm.runInNewContext(code, sandbox);
|
|
return sandbox.RDRenderer;
|
|
}
|
|
|
|
function makeCanvas() {
|
|
let width = 800;
|
|
let height = 400;
|
|
let assignments = 0;
|
|
let rect = { width: 400, height: 200 };
|
|
const canvas = {
|
|
style: { width: '400px', height: '200px' },
|
|
parentElement: {
|
|
getBoundingClientRect: () => rect
|
|
},
|
|
getContext: () => ({
|
|
fillRect() {},
|
|
drawImage() {}
|
|
})
|
|
};
|
|
Object.defineProperties(canvas, {
|
|
width: {
|
|
get: () => width,
|
|
set: (value) => { width = value; assignments++; }
|
|
},
|
|
height: {
|
|
get: () => height,
|
|
set: (value) => { height = value; assignments++; }
|
|
}
|
|
});
|
|
return {
|
|
canvas,
|
|
setRect: (next) => { rect = next; },
|
|
getAssignments: () => assignments
|
|
};
|
|
}
|
|
|
|
it('does not clear the canvas when visual viewport size is unchanged', () => {
|
|
const Renderer = loadRenderer();
|
|
const harness = makeCanvas();
|
|
const renderer = new Renderer(harness.canvas);
|
|
let refreshes = 0;
|
|
renderer.onResizeRefresh = () => { refreshes++; };
|
|
|
|
renderer.resize();
|
|
expect(harness.getAssignments()).toBe(0);
|
|
expect(refreshes).toBe(0);
|
|
|
|
harness.setRect({ width: 420, height: 200 });
|
|
renderer.resize();
|
|
expect(harness.getAssignments()).toBe(2);
|
|
expect(refreshes).toBe(1);
|
|
});
|
|
});
|