@desklink/react-native 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +162 -16
- package/app.plugin.js +16 -0
- package/expo-module.config.json +4 -1
- package/ios/DesklinkInput.podspec +22 -0
- package/ios/DesklinkInputModule.swift +20 -0
- package/ios/DesklinkInputView.swift +209 -0
- package/package.json +9 -3
- package/src/DesktopView.ios.spec.tsx +354 -0
- package/src/DesktopView.ios.tsx +215 -29
- package/src/hardwareKeys.ts +61 -0
- package/src/native.ios.ts +53 -5
- package/src/protocol.ts +6 -2
- package/src/useDesktopSession.ts +83 -15
- package/src/webrtc.ios.ts +10 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import TestRenderer from 'react-test-renderer';
|
|
4
|
+
|
|
5
|
+
/** `act` refuses to flush state updates unless React is told this is a test. */
|
|
6
|
+
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Drives the real iOS view — its gestures, keyboard and control channel — and
|
|
10
|
+
* reads the exact messages the desktop receives. Only the platform underneath
|
|
11
|
+
* is stood in for: `react-native` primitives are host strings, the WebRTC
|
|
12
|
+
* stack arrives through the bundler's lazy require, and the peer hands over a
|
|
13
|
+
* data channel that records what it is asked to send. Behaviour here must
|
|
14
|
+
* match `DesktopView.kt`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
vi.mock('react-native', () => ({
|
|
18
|
+
Keyboard: { addListener: () => ({ remove: () => undefined }), dismiss: () => undefined },
|
|
19
|
+
StyleSheet: { create: (styles: unknown) => styles },
|
|
20
|
+
TextInput: 'TextInput',
|
|
21
|
+
View: 'View',
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
/** Whether the build carries the package's native input view; by default it stands for one without. */
|
|
25
|
+
const nativeInput = { installed: false };
|
|
26
|
+
vi.mock('expo-modules-core', () => ({
|
|
27
|
+
requireOptionalNativeModule: (name: string) => (nativeInput.installed && name === 'DesklinkInput' ? {} : null),
|
|
28
|
+
requireNativeViewManager: () => 'DesklinkInput',
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
// The bundler's inline require, standing in: it goes through the stubbed global so the spec sees when it runs.
|
|
32
|
+
vi.mock('./webrtc.ios', () => ({ requireWebRTC: () => (globalThis as unknown as { require: (id: string) => unknown }).require('react-native-webrtc') }));
|
|
33
|
+
|
|
34
|
+
class MockStream {
|
|
35
|
+
addTrack = vi.fn();
|
|
36
|
+
toURL() { return 'stream'; }
|
|
37
|
+
}
|
|
38
|
+
class MockPeer {
|
|
39
|
+
ontrack?: (event: { track: { kind: string }; streams: never[] }) => void;
|
|
40
|
+
ondatachannel?: (event: { channel: unknown }) => void;
|
|
41
|
+
oniceconnectionstatechange?: () => void;
|
|
42
|
+
iceConnectionState = 'new';
|
|
43
|
+
localDescription = { sdp: 'answer' };
|
|
44
|
+
setRemoteDescription = vi.fn(async () => undefined);
|
|
45
|
+
addIceCandidate = vi.fn(async () => undefined);
|
|
46
|
+
createAnswer = vi.fn(async () => ({ type: 'answer', sdp: 'answer' }));
|
|
47
|
+
setLocalDescription = vi.fn(async () => undefined);
|
|
48
|
+
close = vi.fn();
|
|
49
|
+
constructor() { peers.push(this); }
|
|
50
|
+
}
|
|
51
|
+
const peers: MockPeer[] = [];
|
|
52
|
+
function MockRTCView() { return null; }
|
|
53
|
+
|
|
54
|
+
/** What the bundler's lazy require handed out, in order. */
|
|
55
|
+
const required: string[] = [];
|
|
56
|
+
|
|
57
|
+
const BOUNDS = { width: 390, height: 844 };
|
|
58
|
+
const DESKTOP = { width: 1280, height: 720 };
|
|
59
|
+
// fit = min(390/1280, 844/720); the picture is 390 wide, centred vertically.
|
|
60
|
+
const SCALE = Math.min(BOUNDS.width / DESKTOP.width, BOUNDS.height / DESKTOP.height);
|
|
61
|
+
const ORIGIN_Y = (BOUNDS.height - DESKTOP.height * SCALE) / 2;
|
|
62
|
+
|
|
63
|
+
type Touch = { locationX: number; locationY: number; timestamp?: number };
|
|
64
|
+
function touch(x: number, y: number, timestamp?: number): Touch { return { locationX: x, locationY: y, timestamp }; }
|
|
65
|
+
|
|
66
|
+
async function openView(options: { gestures?: 'desktop' | 'browser' | 'device'; accessibilityLabel?: string; native?: boolean } = {}) {
|
|
67
|
+
nativeInput.installed = options.native === true;
|
|
68
|
+
const { desktopInputEnabled, nativeDesklink } = await import('./native.ios');
|
|
69
|
+
const { DesktopView } = await import('./DesktopView.ios');
|
|
70
|
+
const id = nativeDesklink.createSession('[]')!;
|
|
71
|
+
const peer = peers.at(-1)!;
|
|
72
|
+
const sent: Record<string, unknown>[] = [];
|
|
73
|
+
peer.ondatachannel!({
|
|
74
|
+
channel: { readyState: 'open', send: (value: string) => { sent.push(JSON.parse(value)); }, close: () => undefined },
|
|
75
|
+
});
|
|
76
|
+
nativeDesklink.setInputEnabled(id, true);
|
|
77
|
+
expect(desktopInputEnabled(id)).toBe(true);
|
|
78
|
+
let renderer: TestRenderer.ReactTestRenderer | null = null;
|
|
79
|
+
TestRenderer.act(() => {
|
|
80
|
+
renderer = TestRenderer.create(
|
|
81
|
+
<DesktopView sessionId={id} gestures={options.gestures} accessibilityLabel={options.accessibilityLabel} />,
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
const outer = () => renderer!.root.findByType(options.native ? 'DesklinkInput' as never : 'View').props as {
|
|
85
|
+
onLayout: (event: { nativeEvent: { layout: { width: number; height: number } } }) => void;
|
|
86
|
+
onResponderGrant: (event: { nativeEvent: { touches: Touch[] } }) => void;
|
|
87
|
+
onResponderMove: (event: { nativeEvent: { touches: Touch[] } }) => void;
|
|
88
|
+
onResponderRelease: (event: { nativeEvent: { touches: Touch[] } }) => void;
|
|
89
|
+
[key: string]: unknown;
|
|
90
|
+
};
|
|
91
|
+
TestRenderer.act(() => {
|
|
92
|
+
outer().onLayout({ nativeEvent: { layout: { ...BOUNDS } } });
|
|
93
|
+
nativeDesklink.setSurfaceSize(id, DESKTOP.width, DESKTOP.height);
|
|
94
|
+
peer.ontrack!({ track: { kind: 'video' }, streams: [] });
|
|
95
|
+
});
|
|
96
|
+
const grant = (at: Touch[]) => TestRenderer.act(() => { outer().onResponderGrant({ nativeEvent: { touches: at } }); });
|
|
97
|
+
const move = (at: Touch[]) => TestRenderer.act(() => { outer().onResponderMove({ nativeEvent: { touches: at } }); });
|
|
98
|
+
const release = (at: Touch[] = []) => TestRenderer.act(() => { outer().onResponderRelease({ nativeEvent: { touches: at } }); });
|
|
99
|
+
const submit = () => TestRenderer.act(() => {
|
|
100
|
+
renderer!.root.findByType('TextInput').props.onSubmitEditing();
|
|
101
|
+
});
|
|
102
|
+
const rtcStyle = () => renderer!.root.findByType(MockRTCView).props.style as { left: number; top: number; width: number; height: number };
|
|
103
|
+
/** What the native input view reports, as its events. */
|
|
104
|
+
const native = {
|
|
105
|
+
key: (usage: number, down: boolean, characters = '', modifiers: string[] = []) => TestRenderer.act(() => {
|
|
106
|
+
(outer().onKey as (event: unknown) => void)({ nativeEvent: { usage, down, characters, modifiers } });
|
|
107
|
+
}),
|
|
108
|
+
pointer: (phase: string, x: number, y: number, buttons = 0, timestamp?: number) => TestRenderer.act(() => {
|
|
109
|
+
(outer().onPointer as (event: unknown) => void)({ nativeEvent: { phase, x, y, buttons, timestamp } });
|
|
110
|
+
}),
|
|
111
|
+
wheel: (phase: string, dx: number, dy: number, x = 195, y = 420) => TestRenderer.act(() => {
|
|
112
|
+
(outer().onWheel as (event: unknown) => void)({ nativeEvent: { phase, dx, dy, x, y } });
|
|
113
|
+
}),
|
|
114
|
+
};
|
|
115
|
+
return { id, nativeDesklink, renderer: renderer!, sent, outer, grant, move, release, submit, rtcStyle, native };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
beforeEach(() => {
|
|
119
|
+
peers.length = 0;
|
|
120
|
+
required.length = 0;
|
|
121
|
+
// The device bundler evaluates `react-native-webrtc` on first require;
|
|
122
|
+
// nothing in this package may require it at import time.
|
|
123
|
+
vi.stubGlobal('require', (mod: string) => {
|
|
124
|
+
required.push(mod);
|
|
125
|
+
if (mod === 'react-native-webrtc') return { MediaStream: MockStream, RTCPeerConnection: MockPeer, RTCView: MockRTCView };
|
|
126
|
+
throw new Error(`unexpected require: ${mod}`);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
afterEach(() => { vi.unstubAllGlobals(); });
|
|
130
|
+
|
|
131
|
+
describe('DesktopView.ios parity with DesktopView.kt', () => {
|
|
132
|
+
it('ignores a one-finger drag that starts in the letterbox', async () => {
|
|
133
|
+
const { sent, grant, move, release } = await openView();
|
|
134
|
+
// y = 10 is above the fitted picture (it starts at ORIGIN_Y).
|
|
135
|
+
grant([touch(10, 10)]);
|
|
136
|
+
move([touch(50, 50)]);
|
|
137
|
+
release();
|
|
138
|
+
expect(sent.filter((message) => message.kind === 'pointer')).toEqual([]);
|
|
139
|
+
// Sanity: the same drag on the picture carries the pointer.
|
|
140
|
+
grant([touch(195, 420)]);
|
|
141
|
+
move([touch(250, 430)]);
|
|
142
|
+
release();
|
|
143
|
+
expect(sent.filter((message) => message.kind === 'pointer' && message.phase === 'move')).not.toEqual([]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('zooms around the pinch focus and re-clamps after zooming out', async () => {
|
|
147
|
+
const { rtcStyle, grant, move } = await openView();
|
|
148
|
+
grant([touch(100, 400)]);
|
|
149
|
+
move([touch(100, 400), touch(140, 400)]);
|
|
150
|
+
move([touch(80, 400), touch(160, 400)]);
|
|
151
|
+
expect(rtcStyle().width).toBeCloseTo(DESKTOP.width * SCALE * 2, 0);
|
|
152
|
+
// The desktop point under the focus (120, 400) stays under it.
|
|
153
|
+
expect(rtcStyle().left).toBeCloseTo(-120, 0);
|
|
154
|
+
// Pinching back to 1x recentres: no stale offset left behind.
|
|
155
|
+
move([touch(100, 400), touch(140, 400)]);
|
|
156
|
+
expect(rtcStyle().width).toBeCloseTo(BOUNDS.width, 0);
|
|
157
|
+
expect(rtcStyle().left).toBeCloseTo(0, 5);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('accumulates wheel below the send threshold and moves the pointer to the scroll focus', async () => {
|
|
161
|
+
const { sent, grant, move, release } = await openView();
|
|
162
|
+
grant([touch(190, 420)]);
|
|
163
|
+
move([touch(190, 420), touch(200, 420)]);
|
|
164
|
+
move([touch(192, 421), touch(202, 421)]);
|
|
165
|
+
move([touch(194, 422), touch(204, 422)]);
|
|
166
|
+
move([touch(196, 423), touch(206, 423)]);
|
|
167
|
+
// The last step crosses the slop into a scroll, but its own single
|
|
168
|
+
// pixel stays below the 0.05 detent threshold: held back.
|
|
169
|
+
move([touch(197, 424), touch(207, 424)]);
|
|
170
|
+
const wheels = () => sent.filter((message) => message.kind === 'wheel');
|
|
171
|
+
expect(wheels()).toEqual([]);
|
|
172
|
+
// A second pixel crosses it: one fractional wheel step goes out.
|
|
173
|
+
move([touch(198, 424), touch(208, 424)]);
|
|
174
|
+
expect(wheels()).toHaveLength(1);
|
|
175
|
+
expect(wheels()[0].dx).toBeCloseTo(-2 / SCALE / 120, 5);
|
|
176
|
+
expect(wheels()[0].dy).toBeCloseTo(-1 / SCALE / 120, 5);
|
|
177
|
+
// The desktop scrolled whatever was under its pointer.
|
|
178
|
+
expect(sent.filter((message) => message.kind === 'pointer' && message.phase === 'move')).not.toEqual([]);
|
|
179
|
+
release();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('releases a held device drag when the profile switches away', async () => {
|
|
183
|
+
const first = await openView({ gestures: 'device' });
|
|
184
|
+
first.grant([touch(195, 420)]);
|
|
185
|
+
expect(first.sent.at(-1)).toMatchObject({ kind: 'pointer', phase: 'down', button: 1 });
|
|
186
|
+
const { DesktopView } = await import('./DesktopView.ios');
|
|
187
|
+
TestRenderer.act(() => {
|
|
188
|
+
first.renderer.update(<DesktopView sessionId={first.id} gestures="desktop" />);
|
|
189
|
+
});
|
|
190
|
+
expect(first.sent.at(-1)).toMatchObject({ kind: 'release_all' });
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('sends Enter through capture while a sticky modifier is held', async () => {
|
|
194
|
+
const { id, nativeDesklink, sent, submit } = await openView();
|
|
195
|
+
const keys: Record<string, unknown>[] = [];
|
|
196
|
+
const listener = nativeDesklink.addListener!('onSessionEvent', (event) => {
|
|
197
|
+
if (event.sessionId === id && event.name === 'keyboard') keys.push(event.payload);
|
|
198
|
+
});
|
|
199
|
+
nativeDesklink.captureKeyboard(id, true);
|
|
200
|
+
submit();
|
|
201
|
+
listener.remove();
|
|
202
|
+
expect(keys).toEqual([{ key: 'Enter' }]);
|
|
203
|
+
expect(sent.filter((message) => message.kind === 'key')).toEqual([]);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('puts the cursor mark tip on the pointer with a hotspot', async () => {
|
|
207
|
+
const { renderer, grant, move } = await openView();
|
|
208
|
+
grant([touch(195, 420)]);
|
|
209
|
+
move([touch(250, 430)]);
|
|
210
|
+
const cursor = renderer.root.findAllByType('View')[1].props.style as unknown[];
|
|
211
|
+
const mark = cursor[1] as { left: number; top: number };
|
|
212
|
+
const desktop = { x: Math.floor(250 / SCALE), y: Math.floor((430 - ORIGIN_Y) / SCALE) };
|
|
213
|
+
expect(mark.left).toBeCloseTo((desktop.x + 0.5) * SCALE - 3, 1);
|
|
214
|
+
expect(mark.top).toBeCloseTo(ORIGIN_Y + (desktop.y + 0.5) * SCALE - 3, 1);
|
|
215
|
+
expect(renderer.root.findAllByType('View')[1].props.accessible).toBe(false);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('exposes the surface to VoiceOver only with a label', async () => {
|
|
219
|
+
const labelled = await openView({ accessibilityLabel: 'Desktop' });
|
|
220
|
+
expect(labelled.outer().accessible).toBe(true);
|
|
221
|
+
const unlabelled = await openView();
|
|
222
|
+
expect(unlabelled.outer().accessible).toBe(false);
|
|
223
|
+
expect(unlabelled.renderer.root.findByType('TextInput').props.accessible).toBe(false);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('keeps WebRTC out of the import and loads it on first render', async () => {
|
|
227
|
+
vi.resetModules();
|
|
228
|
+
await import('./DesktopView.ios');
|
|
229
|
+
expect(required).not.toContain('react-native-webrtc');
|
|
230
|
+
await openView();
|
|
231
|
+
expect(required).toContain('react-native-webrtc');
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe('DesktopView.ios hardware keyboard and pointer', () => {
|
|
236
|
+
/** Desktop pixels under a point of the view. */
|
|
237
|
+
const pixel = (x: number, y: number) => ({ x: Math.floor(x / SCALE), y: Math.floor((y - ORIGIN_Y) / SCALE) });
|
|
238
|
+
const pointers = (sent: Record<string, unknown>[]) => sent.filter((message) => message.kind === 'pointer')
|
|
239
|
+
.map(({ phase, x, y, button }) => ({ phase, x, y, button }));
|
|
240
|
+
|
|
241
|
+
it('keeps a plain view where the build has no native input view', async () => {
|
|
242
|
+
const { renderer } = await openView();
|
|
243
|
+
expect(renderer.root.findAllByType('DesklinkInput' as never)).toEqual([]);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('arms the native view only while control is enabled', async () => {
|
|
247
|
+
const { id, nativeDesklink, outer } = await openView({ native: true });
|
|
248
|
+
expect(outer().enabled).toBe(true);
|
|
249
|
+
TestRenderer.act(() => { nativeDesklink.setInputEnabled(id, false); });
|
|
250
|
+
expect(outer().enabled).toBe(false);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('sends arrows, Esc, Tab and a Command chord from the hardware keyboard', async () => {
|
|
254
|
+
const { sent, native } = await openView({ native: true });
|
|
255
|
+
native.key(0x52, true); native.key(0x52, false);
|
|
256
|
+
native.key(0x29, true); native.key(0x29, false);
|
|
257
|
+
native.key(0x2b, true, '\t'); native.key(0x2b, false, '\t');
|
|
258
|
+
native.key(0xe3, true, '', ['Meta']);
|
|
259
|
+
native.key(0x06, true, 'c', ['Meta']);
|
|
260
|
+
native.key(0x06, false, 'c', ['Meta']);
|
|
261
|
+
native.key(0xe3, false);
|
|
262
|
+
native.key(0x0b, true, 'h'); native.key(0x0b, false, 'h');
|
|
263
|
+
expect(sent.map(({ seq: _seq, ...message }) => message)).toEqual([
|
|
264
|
+
{ kind: 'key', name: 'ArrowUp', modifiers: [], down: true }, { kind: 'key', name: 'ArrowUp', modifiers: [], down: false },
|
|
265
|
+
{ kind: 'key', name: 'Escape', modifiers: [], down: true }, { kind: 'key', name: 'Escape', modifiers: [], down: false },
|
|
266
|
+
{ kind: 'key', name: 'Tab', modifiers: [], down: true }, { kind: 'key', name: 'Tab', modifiers: [], down: false },
|
|
267
|
+
{ kind: 'key', name: 'Meta', modifiers: ['Meta'], down: true },
|
|
268
|
+
{ kind: 'key', character: 'c', modifiers: ['Meta'], down: true }, { kind: 'key', character: 'c', modifiers: ['Meta'], down: false },
|
|
269
|
+
{ kind: 'key', name: 'Meta', modifiers: [], down: false },
|
|
270
|
+
{ kind: 'text', text: 'h' },
|
|
271
|
+
]);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('hands hardware keys to the session while a sticky modifier waits', async () => {
|
|
275
|
+
const { id, nativeDesklink, sent, native } = await openView({ native: true });
|
|
276
|
+
const keys: Record<string, unknown>[] = [];
|
|
277
|
+
const listener = nativeDesklink.addListener!('onSessionEvent', (event) => {
|
|
278
|
+
if (event.sessionId === id && event.name === 'keyboard') keys.push(event.payload);
|
|
279
|
+
});
|
|
280
|
+
nativeDesklink.captureKeyboard(id, true);
|
|
281
|
+
native.key(0x19, true, 'v');
|
|
282
|
+
native.key(0x28, true, '\r');
|
|
283
|
+
listener.remove();
|
|
284
|
+
expect(keys).toEqual([{ text: 'v' }, { key: 'Enter' }]);
|
|
285
|
+
expect(sent).toEqual([]);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('hovers the desktop pointer without a button or a drawn mark, and not from the letterbox', async () => {
|
|
289
|
+
const { sent, native, renderer } = await openView({ native: true });
|
|
290
|
+
native.pointer('hover', 10, 10);
|
|
291
|
+
native.pointer('hover', 200, 400);
|
|
292
|
+
expect(pointers(sent)).toEqual([{ phase: 'move', ...pixel(200, 400), button: undefined }]);
|
|
293
|
+
// The system draws the trackpad's own pointer.
|
|
294
|
+
expect(renderer.root.findAllByType('View')).toEqual([]);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('drags with a trackpad press the native view reported before touch saw it', async () => {
|
|
298
|
+
const { sent, native, grant, move, release } = await openView({ native: true });
|
|
299
|
+
native.pointer('down', 100, 400, 1, 5000);
|
|
300
|
+
grant([touch(100, 400, 5000)]);
|
|
301
|
+
native.pointer('move', 160, 410, 1);
|
|
302
|
+
move([touch(160, 410, 5010)]);
|
|
303
|
+
native.pointer('up', 160, 410, 0);
|
|
304
|
+
release();
|
|
305
|
+
expect(pointers(sent)).toEqual([
|
|
306
|
+
{ phase: 'down', ...pixel(100, 400), button: 1 },
|
|
307
|
+
{ phase: 'move', ...pixel(160, 410), button: 1 },
|
|
308
|
+
{ phase: 'up', ...pixel(160, 410), button: 1 },
|
|
309
|
+
]);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('right-clicks with a secondary press touch saw first, and no long press follows', async () => {
|
|
313
|
+
vi.useFakeTimers();
|
|
314
|
+
try {
|
|
315
|
+
const { sent, native, grant, release } = await openView({ native: true });
|
|
316
|
+
grant([touch(100, 400, 6000)]);
|
|
317
|
+
native.pointer('down', 100, 400, 2, 6000);
|
|
318
|
+
TestRenderer.act(() => { vi.advanceTimersByTime(600); });
|
|
319
|
+
native.pointer('up', 100, 400, 0);
|
|
320
|
+
release();
|
|
321
|
+
expect(pointers(sent)).toEqual([
|
|
322
|
+
{ phase: 'down', ...pixel(100, 400), button: 3 },
|
|
323
|
+
{ phase: 'up', ...pixel(100, 400), button: 3 },
|
|
324
|
+
]);
|
|
325
|
+
} finally {
|
|
326
|
+
vi.useRealTimers();
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('does not press twice when a device-profile touch already pressed for the trackpad', async () => {
|
|
331
|
+
const { sent, native, grant, release } = await openView({ native: true, gestures: 'device' });
|
|
332
|
+
grant([touch(100, 400, 7000)]);
|
|
333
|
+
native.pointer('down', 100, 400, 1, 7000);
|
|
334
|
+
native.pointer('up', 100, 400, 0);
|
|
335
|
+
release();
|
|
336
|
+
expect(pointers(sent)).toEqual([
|
|
337
|
+
{ phase: 'down', ...pixel(100, 400), button: 1 },
|
|
338
|
+
{ phase: 'up', ...pixel(100, 400), button: 1 },
|
|
339
|
+
]);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('scrolls the desktop under the pointer with a trackpad, content following the fingers', async () => {
|
|
343
|
+
const { sent, native } = await openView({ native: true });
|
|
344
|
+
native.wheel('begin', 0, -30, 200, 400);
|
|
345
|
+
native.wheel('move', 0, -0.01, 200, 400);
|
|
346
|
+
native.wheel('end', 0, 0, 200, 400);
|
|
347
|
+
expect(pointers(sent)).toEqual([{ phase: 'move', ...pixel(200, 400), button: undefined }]);
|
|
348
|
+
const wheels = sent.filter((message) => message.kind === 'wheel');
|
|
349
|
+
expect(wheels).toHaveLength(2);
|
|
350
|
+
expect(wheels[0].dy).toBeCloseTo(30 / SCALE / 120, 5);
|
|
351
|
+
// What stayed below the send threshold goes out when the scroll ends.
|
|
352
|
+
expect(wheels[1].dy).toBeCloseTo(0.01 / SCALE / 120, 5);
|
|
353
|
+
});
|
|
354
|
+
});
|