function target_test(...args) { if (args.length !== 2 && args.length !== 3) { console.error(`target_test expected 2 or 3 arguments but got ${args.length}.`); return; } const impliedOptions = args.length == 2; let options = impliedOptions ? { } : args[0]; let continutation = args[impliedOptions ? 0 : 1]; let description = args[impliedOptions ? 1 : 2]; options.name = options.name || "div"; options.x = options.x || 0; options.y = options.y || 0; options.width = options.width || "100%"; options.height = options.height || "100%"; async_test(test => { continutation(makeTarget(test, options), test); }, description); } function makeTarget(test, options) { const target = document.body.appendChild(document.createElement(options.name)); target.setAttribute("style", ` position: absolute; left: ${options.x}; top: ${options.y}; width: ${options.width}; height: ${options.height}; `); test.add_cleanup(() => target.remove()); return target; } class EventTracker { constructor(target, eventNames) { this.target = target; this.events = []; this.pointerIdToTouchIdMap = {}; for (let eventName of eventNames) target.addEventListener(eventName, this); } clear() { this.events = []; } handleEvent(event) { if (event instanceof PointerEvent) this._handlePointerEvent(event); else if (event instanceof MouseEvent) this._handleMouseEvent(event); else if (event instanceof TouchEvent) this._handleTouchEvent(event); } _handlePointerEvent(event) { if (!this.pointerIdToTouchIdMap[event.pointerId]) this.pointerIdToTouchIdMap[event.pointerId] = Object.keys(this.pointerIdToTouchIdMap).length + 1; this.events.push({ id: this.pointerIdToTouchIdMap[event.pointerId], type: event.type, x: event.clientX, y: event.clientY, pressure: event.pressure, isPrimary: event.isPrimary, isTrusted: event.isTrusted, button: event.button, buttons: event.buttons }); } _handleMouseEvent(event) { this.events.push({ type: event.type, x: event.clientX, y: event.clientY, }); } _handleTouchEvent(event) { this.events.push({ type: event.type }); } assertMatchesEvents(expectedEvents) { assert_true(!!this.events.length, "Event tracker saw some events."); assert_equals(this.events.length, expectedEvents.length, "Expected events and actual events have the same length."); for (let i = 0; i < expectedEvents.length; ++i) { const expectedEvent = expectedEvents[i]; const actualEvent = this.events[i]; for (let property of Object.getOwnPropertyNames(expectedEvent)) assert_equals(actualEvent[property], expectedEvent[property], `Property ${property} matches for event at index ${i}.`); } } } const ui = new (class UIController { constructor() { this.fingers = {}; } finger() { const id = Object.keys(this.fingers).length + 1; return this.fingers[id] = new Finger(id); } swipe(from, to) { const durationInSeconds = 0.1; return new Promise(resolve => this._run('dragFromPointToPoint', `${from.x}, ${from.y}, ${to.x}, ${to.y}, ${durationInSeconds}`).then(() => setTimeout(resolve, durationInSeconds * 1000) )); } tap(options) { return this._run('singleTapAtPoint', `${options.x}, ${options.y}`); } doubleTap(options) { return this._run('doubleTapAtPoint', `${options.x}, ${options.y}, 0`); } doubleTapToZoom(options) { const durationInSeconds = 0.35; return new Promise(resolve => this._run('doubleTapAtPoint', `${options.x}, ${options.y}, 0`).then(() => setTimeout(resolve, durationInSeconds * 1000) )); } pinchOut(options) { options.x = options.x || 0; options.y = options.y || 0; const startPoint = { x: options.x + options.width, y: options.y + options.height }; const endPoint = { x: options.x + options.width * options.scale, y: options.y + options.height * options.scale }; function step(factor) { return { x: endPoint.x + (startPoint.x - endPoint.x) * (1 - factor), y: endPoint.y + (startPoint.y - endPoint.y) * (1 - factor) }; } const one = this.finger(); const two = this.finger(); return this.sequence([ one.begin({ x: options.x, y: options.y }), two.begin(step(0)), two.move(step(0.2)), two.move(step(0.4)), two.move(step(0.6)), two.move(step(0.8)), two.move(step(1)), one.end(), two.end() ]); } sequence(touches) { const activeFingers = {}; return this._runEvents(touches.map((touches, index) => { if (!Array.isArray(touches)) touches = [touches]; const processedIDs = {}; // Update the list of active touches. touches.forEach(touch => { processedIDs[touch.id] = true; if (touch.phase === "ended") delete activeFingers[touch.id]; else activeFingers[touch.id] = { x: touch.x, y: touch.y }; }); // Now go through the active touches and check that they're all listed in the new touches. for (let id in activeFingers) { if (!processedIDs[id]) touches.push(this.fingers[id].stationary(activeFingers[id])); } return { inputType : "hand", timeOffset : index * 0.05, coordinateSpace : "content", touches : touches } })); } tapStylus(options) { options.azimuthAngle = options.azimuthAngle || 0; options.altitudeAngle = options.altitudeAngle || 0; options.pressure = options.pressure || 0; return this._run('stylusTapAtPoint', `${options.x}, ${options.y}, ${options.azimuthAngle}, ${options.altitudeAngle}, ${options.pressure}`); } _runEvents(events) { return this._run('sendEventStream', `'${JSON.stringify({ events })}'`); } _run(command, args) { const script = `uiController.${command}(${args}, () => uiController.uiScriptComplete());`; return new Promise(resolve => testRunner.runUIScript(script, resolve)); } })(); class Finger { constructor(id) { this.id = id; } begin(options) { return this._action("began", options.x || 0, options.y || 0); } move(options) { return this._action("moved", options.x || 0, options.y || 0); } end(options) { return this._action("ended", this._lastX, this._lastY); } stationary(options) { return this._action("stationary", options.x || this._lastX, options.y || this._lastY, options.pressure || 0); } _action(phase, x, y, pressure = 0) { this._lastX = x; this._lastY = y; return { inputType: "finger", id: this.id, phase, x, y, pressure }; } }