| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import * as timeline from './timeline'; |
| |
| function waitTime(time) { |
| return new Promise(resolve => { |
| setTimeout(() => { |
| resolve(); |
| }, time); |
| }); |
| }; |
| |
| export class ActionPlayback { |
| |
| constructor() { |
| this._timer = 0; |
| this._current = 0; |
| |
| this._ops = []; |
| this._currentOpIndex = 0; |
| |
| this._isLastOpMousewheel = false; |
| } |
| |
| getContext() { |
| return { |
| elapsedTime: this._elapsedTime, |
| currentOpIndex: this._currentOpIndex, |
| isLastOpMouseWheel: this._isLastOpMousewheel |
| } |
| } |
| |
| _reset() { |
| this._currentOpIndex = 0; |
| this._current = Date.now(); |
| this._elapsedTime = 0; |
| this._isLastOpMousewheel = false; |
| } |
| |
| _restoreContext(ctx) { |
| this._elapsedTime = ctx.elapsedTime; |
| this._currentOpIndex = ctx.currentOpIndex; |
| this._isLastOpMousewheel = ctx.isLastOpMouseWheel; |
| } |
| |
| async runAction(action, ctxToRestore) { |
| this.stop(); |
| |
| if (!action.ops.length) { |
| return; |
| } |
| |
| this._ops = action.ops.slice().sort((a, b) => { |
| return a.time - b.time; |
| }); |
| let firstOp = this._ops[0]; |
| this._ops.forEach(op => { |
| op.time -= firstOp.time; |
| }); |
| |
| this._reset(); |
| |
| if (ctxToRestore) { |
| this._restoreContext(ctxToRestore); |
| // Usually restore context happens when page is reloaded after mouseup. |
| // In this case the _currentOpIndex is not increased yet. |
| this._currentOpIndex++; |
| } |
| |
| let self = this; |
| |
| async function takeScreenshot() { |
| // Pause timeline when doing screenshot to avoid screenshot needs taking a while. |
| timeline.pause(); |
| await __VRT_ACTION_SCREENSHOT__(action); |
| timeline.resume(); |
| } |
| |
| return new Promise((resolve, reject) => { |
| async function tick() { |
| let current = Date.now(); |
| let dTime = current - self._current; |
| self._elapsedTime += dTime; |
| self._current = current; |
| |
| try { |
| // Execute all if there are multiple ops in one frame. |
| do { |
| const executed = await self._update(takeScreenshot); |
| if (!executed) { |
| break; |
| } |
| } while (true); |
| } |
| catch (e) { |
| // Stop running and throw error. |
| reject(e); |
| return; |
| } |
| |
| if (self._currentOpIndex >= self._ops.length) { |
| // Finished |
| resolve(); |
| } |
| else { |
| self._timer = setTimeout(tick, 0); |
| } |
| } |
| tick(); |
| }); |
| } |
| |
| |
| stop() { |
| if (this._timer) { |
| clearTimeout(this._timer); |
| this._timer = 0; |
| } |
| } |
| |
| async _update(takeScreenshot) { |
| let op = this._ops[this._currentOpIndex]; |
| |
| if (!op || (op.time > this._elapsedTime)) { |
| // Not yet. |
| return; |
| } |
| |
| let screenshotTaken = false; |
| switch (op.type) { |
| case 'mousedown': |
| // Pause timeline to avoid frame not sync. |
| timeline.pause(); |
| await __VRT_MOUSE_MOVE__(op.x, op.y); |
| await __VRT_MOUSE_DOWN__(); |
| timeline.resume(); |
| break; |
| case 'mouseup': |
| timeline.pause(); |
| await __VRT_MOUSE_MOVE__(op.x, op.y); |
| await __VRT_MOUSE_UP__(); |
| if (window.__VRT_RELOAD_TRIGGERED__) { |
| return; |
| } |
| timeline.resume(); |
| break; |
| case 'mousemove': |
| timeline.pause(); |
| await __VRT_MOUSE_MOVE__(op.x, op.y); |
| timeline.resume(); |
| break; |
| case 'mousewheel': |
| let element = document.elementFromPoint(op.x, op.y); |
| // Here dispatch mousewheel event because echarts used it. |
| // TODO Consider upgrade? |
| let event = new WheelEvent('mousewheel', { |
| // PENDING |
| // Needs inverse delta? |
| deltaY: op.deltaY, |
| clientX: op.x, clientY: op.y, |
| // Needs bubble to parent container |
| bubbles: true |
| }); |
| element.dispatchEvent(event); |
| this._isLastOpMousewheel = true; |
| break; |
| case 'screenshot': |
| await takeScreenshot(); |
| screenshotTaken = true; |
| break; |
| case 'valuechange': |
| document.querySelector(op.selector).value = op.value; |
| break; |
| } |
| |
| this._currentOpIndex++; |
| |
| // If next op is an auto screenshot |
| let nextOp = this._ops[this._currentOpIndex]; |
| if (nextOp && nextOp.type === 'screenshot-auto') { |
| let delay = nextOp.delay == null ? 400 : nextOp.delay; |
| // TODO Configuration time |
| await waitTime(delay); |
| await takeScreenshot(); |
| screenshotTaken = true; |
| this._currentOpIndex++; |
| } |
| |
| if (this._isLastOpMousewheel && op.type !== 'mousewheel') { |
| // Only take screenshot after mousewheel finished |
| if (!screenshotTaken) { |
| await takeScreenshot(); |
| } |
| this._isLastOpMousewheel = false; |
| } |
| |
| return true; |
| } |
| }; |