| /* | |
| * 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. | |
| */ | |
| (function (context) { | |
| var DEFAULT_DATA_TABLE_LIMIT = 8; | |
| var objToString = Object.prototype.toString; | |
| var TYPED_ARRAY = { | |
| '[object Int8Array]': 1, | |
| '[object Uint8Array]': 1, | |
| '[object Uint8ClampedArray]': 1, | |
| '[object Int16Array]': 1, | |
| '[object Uint16Array]': 1, | |
| '[object Int32Array]': 1, | |
| '[object Uint32Array]': 1, | |
| '[object Float32Array]': 1, | |
| '[object Float64Array]': 1 | |
| }; | |
| var params = {}; | |
| var parts = location.search.slice(1).split('&'); | |
| for (var i = 0; i < parts.length; ++i) { | |
| var kv = parts[i].split('='); | |
| params[kv[0]] = kv[1]; | |
| } | |
| if ('__SEED_RANDOM__' in params) { | |
| require(['../node_modules/seedrandom/seedrandom.js'], function (seedrandom) { | |
| var myRandom = new seedrandom('echarts-random'); | |
| // Fixed random generator | |
| Math.random = function () { | |
| return myRandom(); | |
| }; | |
| }); | |
| } | |
| var testHelper = {}; | |
| /** | |
| * @param {Object} opt | |
| * @param {string|string[]} [opt.title] If array, each item is on a single line. | |
| * Can use '**abc**', means <strong>abc</strong>. | |
| * @param {Option} opt.option The chart option. | |
| * | |
| * @param {number} [opt.width] Optional. Specify a different chart width. | |
| * @param {number} [opt.height] Optional. Specify a different chart height. | |
| * @param {boolean} [opt.notMerge] Optional. `chart.setOption(option, {norMerge});` | |
| * @param {boolean} [opt.lazyUpdate] Optional. `chart.setOption(option, {lazyUpdate});` | |
| * @param {boolean} [opt.autoResize=true] Optional. Enable chart auto response to window resize. | |
| * @param {Function} [opt.onResize] Optional. Available when `opt.autoResize` or `opt.draggable` is true. | |
| * @param {string} [opt.renderer] Optional. 'canvas' or 'svg'. DO NOT set it in formmal test cases; | |
| * leave it controlled by __ECHARTS__DEFAULT__RENDERER__ for visual testing. | |
| * | |
| * @param {boolean} [opt.draggable] Optional. Add a draggable button to mutify the chart size. | |
| * This feature require "test/lib/draggable.js" | |
| * | |
| * @param {string} [opt.inputsStyle='normal'] Optional, can be 'normal', 'compact'. | |
| * Can be either `inputsStyle` or `buttonsStyle`. | |
| * @param {number} [opt.inputsHeight] Optional. By default not fix height. If specified, a scroll | |
| * bar will be displayed if overflow the height. In visual test, once a height changed | |
| * by adding something, the subsequent position will be changed, leading to test failures. | |
| * Fixing the height helps avoid this. | |
| * Can be either `inputsHeight` or `buttonsHeight`. | |
| * @param {boolean} [opt.saveInputsInitialState] Optional. | |
| * Required by `chart.__testHelper.restoreInputsToInitialState` | |
| * @param {InputDefine[]|InputDefine|()=>InputDefine[]} [opt.inputs] Optional. | |
| * See also `testHelper.createInputsSimply()` | |
| * definitions of button/range/select/br/hr. | |
| * They are the same: `opt.buttons` `opt.button`, `opt.inputs`, `opt.input`. | |
| * It can be a function that return inputs definitions, like: | |
| * inputs: chart => { return [{text: 'xxx', onclick: fn}, ...]; } | |
| * Inputs can be these types: | |
| * [ | |
| * { | |
| * // A button (default). | |
| * text: 'xxx', | |
| * // They are the same: `onclick`, `click` (capital insensitive) | |
| * onclick: fn, | |
| * disabled: false, // Optional. | |
| * prevent: { // Optional. | |
| * recordInputs: false, // Optional. | |
| * inputsState: false, // Optional. | |
| * }, | |
| * }, | |
| * { | |
| * // A range slider (HTML <input type="range">). | |
| * type: 'range', // They are the same: 'range' 'slider' | |
| * id: 'some_id', // Optional. Can be used in `switchGroup`. | |
| * text: 'xxx', // Optional | |
| * min: 0, // Optional | |
| * max: 100, // Optional | |
| * value: 30, // Optional. Must be a number. | |
| * step: 1, // Optional | |
| * suffix: '%', // Optional. e.g., '%' means the number is displayed as '33%' | |
| * disabled: false, // Optional. | |
| * prevent: { // Optional. | |
| * recordInputs: false, // Optional. | |
| * inputsState: false, // Optional. | |
| * }, | |
| * // They are the same: `oninput` `input` | |
| * // `onchange` `change` `onchanged` `changed` | |
| * // `onselect` `select` (capital insensitive) | |
| * onchange: function () { console.log(this.value); } | |
| * }, | |
| * { | |
| * // A select (HTML <select>...</select>). | |
| * type: 'select', // They are the same: 'select' 'selection' | |
| * id: 'some_id', // Optional. Can be used in `getState` and `setState`. | |
| * // Either `values` or `options` can be used. | |
| * // Items in `values` or `options[i].value` can be any type, like `true`, `123`, etc. | |
| * values: ['a', 'b', 'c'], | |
| * options: [ | |
| * {text: 'a', value: 123}, | |
| * {value: {some: {some: 456}}}, // `text` can be omitted and auto generated by `value`. | |
| * {text: 'c', input: ...}, // `input` can be used as shown below. | |
| * ... | |
| * ], | |
| * // `options[i]` can nest other input type, currently only support `type: range`: | |
| * options: [ | |
| * {value: undefined}, | |
| * {text: 'c', input: { | |
| * type: 'range', | |
| * // ... Other properties of `range` input except `onchange` and `text`. | |
| * // When this option is not selected, the range input will be disabled. | |
| * }}, | |
| * // If more than one options have internal `input`, `id` (option id) must be specified. | |
| * // It can be visited by `onchange() { if (this.optionId) {...} }`. | |
| * {text: 'd', id: 'some_option_id', input: {...}} | |
| * ], | |
| * optionIndex: 0, // Optional. Or `valueIndex`. The initial value index. | |
| * // By default, the first option. | |
| * value: 'cval', // Optional. The initial value. By default, the first option. | |
| * // Can be any type, like `true`, `123`, etc. | |
| * // But can only be JS primitive type, as `===` is used internally. | |
| * text: 'xxx', // Optional. | |
| * disabled: false, // Optional. | |
| * prevent: { // Optional. | |
| * recordInputs: false, // Optional. | |
| * inputsState: false, // Optional. | |
| * }, | |
| * // They are the same: `oninput` `input` | |
| * // `onchange` `change` `onchanged` `changed` | |
| * // `onselect` `select` (capital insensitive) | |
| * onchange: function () { console.log(this.value, this.optionId); } | |
| * }, | |
| * { | |
| * // Group inputs. Only one group can be displayed at a time with in a group set. | |
| * type: 'groups', // They are the same: 'groups' 'group' 'groupset' | |
| * // `inputsHeight` is mandatory in group set to avoid height change to affects visual testing | |
| * // when switching groups. It will be applied to all groups. | |
| * inputsHeight, | |
| * // `inputsHeight` will be applied to all groups. | |
| * inputsStyle, | |
| * disabled: false, // Optional. Controlls all groups inside, | |
| * // unless `group.disabled` or `input.disbles` specified. | |
| * prevent: { // Optional. | |
| * recordInputs: false, // Optional. | |
| * inputsState: false, // Optional. | |
| * }, | |
| * groups: [{ | |
| * id: 'group_A', | |
| * text: 'xxx', // Optional. Or `title`. Displayed in the header line of the group content. | |
| * disabled: false, // Optional. Controlls all inputs inside, unless `input.disabled` specified. | |
| * inputs: [{...}, {...}, ...], | |
| * }, { | |
| * id: 'group_B', | |
| * inputs: [{...}, {...}, ...], | |
| * }, ...] | |
| * // Group switching API: @see chart.__testHelper.switchGroup(groupId); | |
| * }, | |
| * { | |
| * // A line break. | |
| * // They are the same: `br` `lineBreak` `break` `wrap` `newLine` `endOfLine` `carriageReturn` | |
| * // `lineFeed` `lineSeparator` `nextLine` (capital insensitive) | |
| * type: 'br', | |
| * }, | |
| * { | |
| * // A separate line. | |
| * type: 'hr', | |
| * text: 'xxx', // Optional. Display text on the split line. | |
| * }, | |
| * // ... | |
| * ] | |
| * ----------------------------- Inputs related API ----------------------------------- | |
| * @function chart.__testHelper.switchGroup Switch group. | |
| * chart.__testHelper.switchGroup( | |
| * groupId: string, | |
| * opt?: { | |
| * recordInputs: boolean, // Optional. @see `chart.__testHelper.recordInputs`. | |
| * } | |
| * ); | |
| * | |
| * @function chart.__testHelper.disableInputs Disable the specified inputs. | |
| * chart.__testHelper.disableInputs(opt: { | |
| * disabled: boolean, // disables/enables | |
| * inputId: string, // Optional. id or id array. disables/enables the id-specified inputs. | |
| * groupId: string, // Optional. id or id array. disables/enables the inputs within the group. | |
| * recordInputs: boolean, // Optional. @see `chart.__testHelper.recordInputs`. | |
| * }) | |
| * | |
| * @function chart.__testHelper.recordInputs | |
| * @see `prevent` in `inputs` to prevent record. | |
| * chart.__testHelper.recordInputs(opt: { // start record inputs operations for replay. | |
| * action: 'start' | |
| * }) | |
| * chart.__testHelper.recordInputs(opt: { // stop record inputs operations and output. | |
| * action: 'stop', | |
| * outputType?: 'clipboard' | 'console', // Optional. 'clipboard' by default. | |
| * printObjectOpt?: {} // Optional. the opt of `testHelper.printObject`. | |
| * }) | |
| * Note: if some API `chart.__testHelper.xxx` has parameter `recordInputs`, it indicates that wether | |
| * record this call. It is `false` by default, and: | |
| * - When this API is called in a callback function of an input where no `prevent.recordInputs` is | |
| * declared, this option should be kept `false`. (This is the most cases.) | |
| * - Otherwise it should be `true`. | |
| * @function chart.__testHelper.replayInputs | |
| * chart.__testHelper.replayInputs(inputsRecord) | |
| * | |
| * (TL;DR) NOTE: Currently, echarts can not be restored to the initial state by | |
| * `setOption({..., xxx: undefined})` or `setOption({..., xxx: 'auto'})` in most options. | |
| * That is, the initial state can only be obtained by: | |
| * - either "not specified in echarts option" from the beginning; | |
| * - or "sepecify the exact default value to match the internal default value in echarts option". | |
| * | |
| * @function chart.__testHelper.getInputsState Get the current state of `inputs`. | |
| * chart.__testHelper.getInputsState() | |
| * e.g., result: {some_id_1: 'value1', some_id_2: 'value2'} | |
| * @see `prevent` in `inputs` to prevent. | |
| * @function chart.__testHelper.setInputsState Set the current state of `inputs`. | |
| * chart.__testHelper.setInputsState(state) | |
| * @function chart.__testHelper.restoreInputsToInitialState | |
| * chart.__testHelper.restoreInputsToInitialState() | |
| * @see opt.saveInputsInitialState which must be specified as true for this API. | |
| * ------------------------------------------------------------------------------------ | |
| * | |
| * @param {BoundingRectOpt} [opt.boundingRect] Optional. | |
| * @typedef {boolean | {color?: string, slient: boolean}} BoundingRectOpt | |
| * Enable display bounding rect for zrender elements. | |
| * - `true`: Simply display the bounding rects. | |
| * - `opt.boundingRect.color`: a string to indicate the color, like 'red', 'rgba(0,0,0,0.2)', '#fff'. | |
| * - `opt.boundingRect.silent`: by default `false`; | |
| * if `false`, click on the bounding rect, window.$0 will be assigned the original zrender element. | |
| * - Can be switched dynamically by: | |
| * // Update BoundingRectOpt, typically used to show/hide bounding rects. | |
| * @function chart.__testHelper.boundingRect | |
| * chart.__testHelper.boundingRect(opt: BoundingRectOpt); | |
| * chart.__testHelper.boundingRect(); // Use the last BoundingRectOpt. | |
| * | |
| * @param {boolean} [opt.recordCanvas] Optional. 'test/lib/canteen.js' is required. | |
| * @param {boolean} [opt.recordVideo] Optional. | |
| * | |
| * @param {Object} [opt.info] Optional. info object to display. | |
| * @api info can be updated by `chart.__testHelper.updateInfo(someInfoObj, 'some_info_key');` | |
| * @param {string} [opt.infoStyle] Optional. Can be 'overlay'. | |
| * @param {string} [opt.infoKey] Optional. | |
| * @param {Object|Array} [opt.dataTable] Optional. | |
| * @param {Array.<Object|Array>} [opt.dataTables] Optional. Multiple dataTables. | |
| * @param {number} [opt.dataTableLimit=DEFAULT_DATA_TABLE_LIMIT] Optional. | |
| */ | |
| testHelper.create = function (echarts, domOrId, opt) { | |
| var dom = getDom(domOrId); | |
| if (!dom) { | |
| return; | |
| } | |
| var errMsgPrefix = '[testHelper dom: ' + domOrId + ']'; | |
| var titleContainer = document.createElement('div'); | |
| var left = document.createElement('div'); | |
| var chartContainerWrapper = document.createElement('div'); | |
| var chartContainer = document.createElement('div'); | |
| var inputsContainer = document.createElement('div'); | |
| var dataTableContainer = document.createElement('div'); | |
| var infoContainer = document.createElement('div'); | |
| var recordCanvasContainer = document.createElement('div'); | |
| var recordVideoContainer = document.createElement('div'); | |
| var boundingRectsContainer = document.createElement('div'); | |
| titleContainer.className = 'test-title'; | |
| dom.className = 'test-chart-block'; | |
| left.className = 'test-chart-block-left'; | |
| chartContainerWrapper.className = 'test-chart-wrapper'; | |
| chartContainer.className = 'test-chart'; | |
| dataTableContainer.className = 'test-data-table'; | |
| infoContainer.className = 'test-info'; | |
| boundingRectsContainer.className = 'test-bounding-rects'; | |
| boundingRectsContainer.style.display = 'none'; | |
| recordCanvasContainer.className = 'record-canvas'; | |
| recordVideoContainer.className = 'record-video'; | |
| if (opt.info) { | |
| if (opt.infoStyle === 'overlay') { | |
| infoContainer.className += ' test-info-overlay'; | |
| } | |
| else { | |
| dom.className += ' test-chart-block-has-right'; | |
| infoContainer.className += ' test-chart-block-right'; | |
| } | |
| } | |
| left.appendChild(recordCanvasContainer); | |
| left.appendChild(recordVideoContainer); | |
| left.appendChild(inputsContainer); | |
| left.appendChild(dataTableContainer); | |
| left.appendChild(chartContainerWrapper); | |
| chartContainerWrapper.appendChild(chartContainer); | |
| chartContainerWrapper.appendChild(boundingRectsContainer); | |
| dom.appendChild(infoContainer); | |
| dom.appendChild(left); | |
| dom.parentNode.insertBefore(titleContainer, dom); | |
| initTestTitle(opt, titleContainer, dom); | |
| var chart = testHelper.createChart(echarts, chartContainer, opt.option, opt, opt.setOptionOpts, errMsgPrefix); | |
| initDataTables(opt, dataTableContainer); | |
| if (chart) { | |
| chart.__testHelper = {}; | |
| initInputs(chart, opt, inputsContainer, errMsgPrefix); | |
| initUpdateInfo(opt, chart, infoContainer); | |
| initRecordCanvas(opt, chart, recordCanvasContainer); | |
| if (opt.recordVideo) { | |
| testHelper.createRecordVideo(chart, recordVideoContainer); | |
| } | |
| initShowBoundingRects(chart, echarts, opt, boundingRectsContainer); | |
| if (opt.info) { | |
| infoContainer.style.maxHeight = Math.round(chart.getHeight() * 0.9) + 'px'; | |
| } | |
| } | |
| return chart; | |
| }; | |
| function initTestTitle(opt, titleContainer, dom) { | |
| var optTitle = opt.title; | |
| if (optTitle) { | |
| if (optTitle instanceof Array) { | |
| optTitle = optTitle.join('\n'); | |
| } | |
| titleContainer.innerHTML = '<div class="test-title-inner">' | |
| + encodeHTML(optTitle) | |
| .replace(/\*\*([^*]+?)\*\*/g, '<strong>$1</strong>') | |
| .replace(/\n/g, '<br>') | |
| + '</div>'; | |
| } | |
| titleContainer.setAttribute('title', dom.getAttribute('id')); | |
| titleContainer.addEventListener('click', function () { | |
| // Convenient for locating test case code. | |
| console.log('[TEST CASE DOM ID]: ' + dom.getAttribute('id')); | |
| }); | |
| var hovering = false; | |
| titleContainer.addEventListener('mouseover', function () { | |
| if (!hovering) { | |
| hovering = true; | |
| if (_controlFrameSingleton) { | |
| cssAddClass(_controlFrameSingleton.btnPanel, 'control-frame-btn-panel-avoid-occlusion'); | |
| } | |
| } | |
| }); | |
| titleContainer.addEventListener('mouseout', function (event) { | |
| if (hovering) { | |
| hovering = false; | |
| if (_controlFrameSingleton) { | |
| cssRemoveClass(_controlFrameSingleton.btnPanel, 'control-frame-btn-panel-avoid-occlusion'); | |
| } | |
| } | |
| }); | |
| } | |
| function initUpdateInfo(opt, chart, infoContainer) { | |
| assert(chart.__testHelper); | |
| if (opt.info) { | |
| updateInfo(opt.info, opt.infoKey); | |
| } | |
| function updateInfo(info, infoKey) { | |
| infoContainer.innerHTML = createObjectHTML(info, infoKey || 'info'); | |
| } | |
| chart.__testHelper.updateInfo = updateInfo; | |
| } | |
| function initInputs(chart, opt, inputsContainer, errMsgPrefix) { | |
| assert(chart.__testHelper); | |
| var NAMES_ON_INPUT_CHANGE = makeFlexibleNames([ | |
| 'input', 'on-input', 'change', 'on-change', 'changed', 'on-changed', 'select', 'on-select' | |
| ]); | |
| var NAMES_ON_CLICK = makeFlexibleNames([ | |
| 'click', 'on-click' | |
| ]); | |
| var NAMES_TYPE_BUTTON = makeFlexibleNames(['button', 'btn']); | |
| var NAMES_TYPE_RANGE = makeFlexibleNames(['range', 'slider']); | |
| var NAMES_TYPE_SELECT = makeFlexibleNames(['select', 'selection']); | |
| var NAMES_TYPE_BR = makeFlexibleNames([ | |
| 'br', 'line-break', 'break', 'wrap', 'new-line', 'end-of-line', | |
| 'carriage-return', 'line-feed', 'line-separator', 'next-line' | |
| ]); | |
| var NAMES_TYPE_HR = makeFlexibleNames([ | |
| 'hr', 'horizontal-line', 'divider', 'separate-line' | |
| ]); | |
| var NAMES_TYPE_GROUP_SET = makeFlexibleNames(['group', 'groups', 'group-set']); | |
| /** | |
| * key: inputId, | |
| * value: { | |
| * id: inputId, | |
| * disable?, | |
| * switchGroup?, | |
| * setState?, | |
| * getState?, | |
| * } | |
| */ | |
| var _inputsDict = {}; | |
| var NAMES_RECORD_INPUTS_ACTION_START = makeFlexibleNames(['start', 'begin']); | |
| var NAMES_RECORD_INPUTS_ACTION_STOP = makeFlexibleNames(['stop', 'end', 'finish']); | |
| var _inputsRecord = null; | |
| /** | |
| * key: inputId | |
| * value: @see makeInputRecorder | |
| */ | |
| var _inputRecorderWrapperMap = {}; | |
| var _INPUTS_RECORD_VERSION = '1.0.0'; | |
| var NANES_PREVENT_INPUTS_STATE = makeFlexibleNames([ | |
| 'inputs-state', 'input-state', 'inputs-states', 'input-states' | |
| ]); | |
| var NANES_PREVENT_RECORD_INPUTS = makeFlexibleNames([ | |
| 'record-inputs', 'record-input', | |
| 'input-record', 'inputs-record', | |
| ]); | |
| var _initStateBackup = null; | |
| initInputsContainer(inputsContainer, opt); | |
| var inputsDefineList = retrieveInputDefineList(opt); | |
| dealInitEachInput(inputsDefineList, inputsContainer); | |
| // --- Input operation related API --- | |
| chart.__testHelper.switchGroup | |
| = makeSwitchGroup(); | |
| chart.__testHelper.disableInputs | |
| = chart.__testHelper.disableInput | |
| = makeDisableInputs(); | |
| // --- Input meta related API --- | |
| chart.__testHelper.recordInputs | |
| = recordInputs; | |
| chart.__testHelper.replayInputs | |
| = chart.__testHelper.replayInput | |
| = replayInputs; | |
| chart.__testHelper.getInputsState | |
| = chart.__testHelper.getInputState | |
| = getInputsState; | |
| chart.__testHelper.setInputsState | |
| = chart.__testHelper.setInputState | |
| = setInputsState; | |
| chart.__testHelper.restoreInputsToInitialState | |
| = restoreInputsToInitialState; | |
| if (opt.saveInputsInitialState) { | |
| _initStateBackup = chart.__testHelper.getInputsState(); | |
| } | |
| return; | |
| function makeDisableInputs() { | |
| var inputRecorderWrapper = makeInputRecorder(); | |
| inputRecorderWrapper.setupInputId('__\0testHelper_disableInputs'); | |
| var disableInputsWithRecordInputs = inputRecorderWrapper.inputRecorder.wrapUserInputListener({ | |
| listener: disableInputs, | |
| op: 'disableInputs' | |
| }); | |
| /** | |
| * @param {string|Array.<string>?} opt.groupId | |
| * @param {string|Array.<string>?} opt.inputId | |
| * @param {boolean} opt.recordInputs | |
| */ | |
| return function (opt) { | |
| opt.recordInputs | |
| ? disableInputsWithRecordInputs(opt) | |
| : disableInputs(opt); | |
| } | |
| function disableInputs(opt) { | |
| assert(opt, '[disableInputs] requires parameters.'); | |
| var groupId = opt.groupId; | |
| var inputId = opt.inputId; | |
| assert( | |
| groupId != null || inputId != null, | |
| '[disableInputs] requires `groupId` or/and `inputId`.' | |
| ); | |
| var inputIdList = []; | |
| if (inputId != null) { | |
| if (getType(inputId) !== 'array') { | |
| inputId = [inputId]; | |
| } | |
| for (var idx = 0; idx < inputId.length; idx++) { | |
| var id = inputId[idx]; | |
| findInputCreatedAndCheck(id, {throw: true}); | |
| inputIdList.push(id); | |
| } | |
| } | |
| if (groupId != null) { | |
| if (getType(groupId) !== 'array') { | |
| groupId = [groupId]; | |
| } | |
| for (var idx = 0; idx < groupId.length; idx++) { | |
| inputIdList = inputIdList.concat(retrieveAndVerifyGroup(groupId[idx]).idList); | |
| } | |
| } | |
| var disabled = opt.disabled; | |
| for (var idx = 0; idx < inputIdList.length; idx++) { | |
| var id = inputIdList[idx]; | |
| if (_inputsDict[id].disable) { | |
| _inputsDict[id].disable({disabled: disabled}); | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * @param {string} opt.action 'start' or 'stop'. | |
| * @param {string} opt.outputType Optional. 'clipboard' or 'console'. | |
| * @param {Object} opt.printObjectOpt Optional. The opt of `testHelper.printObject`. | |
| */ | |
| function recordInputs(opt) { | |
| var action = opt.action; | |
| assert( | |
| NAMES_RECORD_INPUTS_ACTION_START.indexOf(action) >= 0 | |
| || NAMES_RECORD_INPUTS_ACTION_STOP.indexOf(action) >= 0, | |
| 'Invalide recordInputs action: ' + action + '. Should be ' | |
| + NAMES_RECORD_INPUTS_ACTION_START + ' ' + NAMES_RECORD_INPUTS_ACTION_STOP | |
| ); | |
| if (NAMES_RECORD_INPUTS_ACTION_START.indexOf(action) >= 0) { | |
| _inputsRecord = { | |
| version: _INPUTS_RECORD_VERSION, | |
| startTime: +(new Date()), | |
| operations: [], | |
| }; | |
| } | |
| else if (NAMES_RECORD_INPUTS_ACTION_STOP.indexOf(action) >= 0) { | |
| if (_inputsRecord == null) { | |
| console.error( | |
| 'Inputs record is not started. Please call' | |
| + ' `chart.__testHelper.recordInputs({action: "start"})` first.' | |
| ); | |
| return; | |
| } | |
| _inputsRecord.endTime = +(new Date()); | |
| var inputsRecord = _inputsRecord; | |
| _inputsRecord = null; | |
| outputInputsRecord(inputsRecord); | |
| return inputsRecord; | |
| } | |
| function outputInputsRecord(record) { | |
| if (opt.outputType === 'console') { | |
| console.log(testHelper.printObject(record, opt.printObjectOpt)); | |
| } | |
| else { | |
| testHelper.clipboard(record, opt.printObjectOpt); | |
| } | |
| } | |
| } | |
| function replayInputs(inputsRecord) { | |
| assert( | |
| inputsRecord.version === _INPUTS_RECORD_VERSION, | |
| 'Not supported inputs record version. expect' + _INPUTS_RECORD_VERSION + ' Need to re-record.' | |
| ); | |
| for (var idx = 0; idx < inputsRecord.operations.length; idx++) { | |
| var opItem = inputsRecord.operations[idx]; | |
| findInputCreatedAndCheck(opItem.id, {throw: true}); | |
| assert( | |
| !shouldPrevent(opItem.id, NANES_PREVENT_RECORD_INPUTS), | |
| 'Input (id:' + opItem.id + ') has prevented recording. This may caused by test case change.' | |
| ); | |
| var inputRecorderWrapper = _inputRecorderWrapperMap[opItem.id]; | |
| assert(inputRecorderWrapper); | |
| assert(getType(opItem.op) === 'string', 'Invalid op: ' + opItem.op); | |
| var listenerDefine = inputRecorderWrapper.listenerDefineMap[opItem.op]; | |
| assert( | |
| listenerDefine, | |
| 'Can not find listener by op: ' + opItem.op + ' This may caused by test case change.' | |
| ); | |
| var prepared = {this: [], arguments: {}}; | |
| if (listenerDefine.prepareReplay) { | |
| prepared = listenerDefine.prepareReplay(opItem.args); | |
| assert( | |
| isObject(prepared) | |
| && prepared.hasOwnProperty('this') | |
| && getType(prepared.arguments) === 'array', | |
| '`prepareReplay` must return an object: {this: any, arguments: []}.' | |
| ); | |
| } | |
| listenerDefine.listener.apply(prepared.this, prepared.arguments); | |
| } | |
| } | |
| function makeInputRecorder() { | |
| var _inputId = null; | |
| var inputRecorderWrapper = { | |
| setupInputId: function (inputId) { | |
| _inputId = inputId; | |
| _inputRecorderWrapperMap[inputId] = inputRecorderWrapper; | |
| }, | |
| inputRecorder: { | |
| wrapUserInputListener: wrapUserInputListener | |
| }, | |
| /** | |
| * key: op, | |
| */ | |
| listenerDefineMap: {}, | |
| }; | |
| return inputRecorderWrapper; | |
| function wrapUserInputListener(listenerDefine) { | |
| assert( | |
| getType(listenerDefine.listener) === 'function', | |
| 'Must provide a function `listener`.' | |
| ); | |
| assert( | |
| getType(listenerDefine.op) === 'string', | |
| 'Must provide an `op` string to identify this listener.' | |
| ); | |
| assert( | |
| !inputRecorderWrapper.listenerDefineMap[listenerDefine.op], | |
| '`op` ' + listenerDefine.op + ' overlapped.' | |
| ); | |
| inputRecorderWrapper.listenerDefineMap[listenerDefine.op] = listenerDefine; | |
| return function wrappedListener() { | |
| assert(_inputId != null); | |
| if (_inputsRecord && !shouldPrevent(_inputId, NANES_PREVENT_RECORD_INPUTS)) { | |
| var recordWrapper = {id: _inputId, op: listenerDefine.op}; | |
| if (listenerDefine.createRecordArgs) { | |
| recordWrapper.args = listenerDefine.createRecordArgs.apply(this, arguments); | |
| } | |
| _inputsRecord.operations.push(recordWrapper); | |
| } | |
| return listenerDefine.listener.apply(this, arguments); | |
| }; | |
| } | |
| } | |
| function setInputsState(state) { | |
| var changedCreatedList = []; | |
| for (var id in state) { | |
| if (state.hasOwnProperty(id)) { | |
| var inputCreated = findInputCreatedAndCheck(id, {log: true}); | |
| if (!inputCreated) { | |
| continue; | |
| } | |
| if (shouldPrevent(id, NANES_PREVENT_INPUTS_STATE) || !inputCreated.setState) { | |
| continue; | |
| } | |
| inputCreated.setState(state[id]); | |
| changedCreatedList.push(inputCreated); | |
| } | |
| } | |
| } | |
| function getInputsState() { | |
| var result = {}; | |
| for (var id in _inputsDict) { | |
| if (_inputsDict.hasOwnProperty(id)) { | |
| var inputCreated = _inputsDict[id]; | |
| if (shouldPrevent(id, NANES_PREVENT_INPUTS_STATE) || !inputCreated.getState) { | |
| continue; | |
| } | |
| if (inputCreated.idCanNotPersist) { | |
| throw new Error( | |
| errMsgPrefix + '[getInputsState]. Please specify an id explicitly or unique text' | |
| + ' for input:' + printObject(inputCreated.__inputDefine) | |
| ); | |
| } | |
| result[id] = inputCreated.getState(); | |
| } | |
| } | |
| return result; | |
| } | |
| function restoreInputsToInitialState() { | |
| assert( | |
| _initStateBackup != null, | |
| 'opt.saveInputsInitialState must be true to use `restoreInputsToInitialState`.' | |
| ); | |
| setInputsState(_initStateBackup); | |
| } | |
| function initInputsContainer(container, define, features) { | |
| assert(container.tagName.toLowerCase() === 'div'); | |
| container.innerHTML = ''; | |
| var ignoreFixHeight = features && features.ignoreFixHeight; | |
| var ignoreInputsStyle = features && features.ignoreInputsStyle; | |
| var inputsHeight = retrieveValue(define.inputsHeight, define.buttonsHeight, null); | |
| if (inputsHeight != null) { | |
| inputsHeight = parseFloat(inputsHeight); | |
| } | |
| var classNameArr = []; | |
| if (features && features.className) { | |
| classNameArr.push(features.className); | |
| } | |
| if (!ignoreInputsStyle) { | |
| classNameArr.push( | |
| 'test-inputs', | |
| 'test-buttons', // deprecated but backward compat. | |
| 'test-inputs-style-' + (define.inputsStyle || define.buttonsStyle || 'normal') | |
| ); | |
| } | |
| if (!ignoreFixHeight && inputsHeight != null) { | |
| classNameArr.push('test-inputs-fix-height'); | |
| container.style.cssText += [ | |
| 'height:' + inputsHeight + 'px' | |
| ].join(';') + ';'; | |
| } | |
| container.className = classNameArr.join(' '); | |
| } | |
| function dealInitEachInput(inputsDefineList, inputsContainer) { | |
| var idList = []; | |
| for (var i = 0; i < inputsDefineList.length; i++) { | |
| var inputDefine = inputsDefineList[i]; | |
| var inputRecorderWrapper = makeInputRecorder(); | |
| var inputCreated = createInputByDefine( | |
| inputDefine, | |
| inputRecorderWrapper.inputRecorder | |
| ); | |
| if (!inputCreated) { | |
| continue; | |
| } | |
| for (var j = 0; j < inputCreated.elList.length; j++) { | |
| inputsContainer.appendChild(inputCreated.elList[j]); | |
| } | |
| var id = storeToInputDict(inputDefine, inputCreated, inputRecorderWrapper.setupInputId); | |
| idList.push(id); | |
| } | |
| return idList; | |
| } | |
| function storeToInputDict(inputDefine, inputCreated, inputRecorderSetupInputId) { | |
| var id = retrieveId(inputDefine, 'id'); | |
| if (id != null) { | |
| id = '' + id; | |
| if (_inputsDict[id]) { | |
| throw new Error(errMsgPrefix + ' Duplicate input id: ' + id); | |
| } | |
| } | |
| if (id == null) { | |
| var text = retrieveValue(inputDefine.text, '') + ''; | |
| if (text) { | |
| var textBasedId = '__inputs|' + text + '|'; | |
| if (!_inputsDict[textBasedId]) { | |
| id = textBasedId; | |
| } | |
| } | |
| } | |
| if (id == null) { | |
| id = generateNonPersistentId('__inputs_non_persist'); | |
| assert(!_inputsDict[id]); | |
| inputCreated.idCanNotPersist = true; | |
| } | |
| inputCreated.id = id; | |
| inputCreated.__inputDefine = inputDefine; | |
| _inputsDict[id] = inputCreated; | |
| if (inputRecorderSetupInputId) { | |
| inputRecorderSetupInputId(id); | |
| } | |
| return id; | |
| } | |
| function retrieveAndVerifyGroup(groupId) { | |
| var groupCreated = _inputsDict[groupId]; | |
| assert(groupCreated, 'Can not find group by id: ' + groupId); | |
| assert(groupCreated.groupParent, 'This is not a group. id: ' + groupId); | |
| return groupCreated; | |
| } | |
| function makeSwitchGroup() { | |
| var inputRecorderWrapper = makeInputRecorder(); | |
| inputRecorderWrapper.setupInputId('__\0testHelper_switchGroup'); | |
| var switchGroupWithRecordInputs = inputRecorderWrapper.inputRecorder.wrapUserInputListener({ | |
| listener: dealSwitchGroup, | |
| op: 'switchGroup' | |
| }); | |
| return function (groupId, opt) { | |
| (opt && opt.recordInputs) | |
| ? switchGroupWithRecordInputs(groupId, opt) | |
| : dealSwitchGroup(groupId); | |
| }; | |
| function dealSwitchGroup(groupId) { | |
| var groupCreatedToShow = retrieveAndVerifyGroup(groupId); | |
| var groupSetCreated = groupCreatedToShow.groupParent; | |
| groupSetCreated.switchGroup(groupId); | |
| } | |
| } | |
| function showHideGroupInGroupSet(groupCreated, showOrHide) { | |
| groupCreated.inputsContainerEl.style.display = showOrHide | |
| ? 'block' : 'none'; | |
| var groupDefine = groupCreated.groupDefine; | |
| groupCreated.groupSetTextEl.innerHTML = showOrHide | |
| ? encodeHTML(retrieveValue(groupDefine.text, groupDefine.title, '')) | |
| : ''; | |
| } | |
| function shouldPrevent(inputId, names) { | |
| var prevent = _inputsDict[inputId].__inputDefine.prevent || {}; | |
| for (var idx = 0; idx < names.length; idx++) { | |
| if (prevent[names[idx]]) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| function findInputCreatedAndCheck(inputId, errorHandling) { | |
| var inputCreated = _inputsDict[inputId]; | |
| if (!inputCreated) { | |
| var errMsg = errMsgPrefix + ' No input found by id: ' + inputId + '. May caused by test case change.'; | |
| if (errorHandling.log) { | |
| console.error(errMsg); | |
| } | |
| else if (errorHandling.throw) { | |
| throw new Error(errMsg); | |
| } | |
| else { | |
| throw new Error('internal failure.') | |
| } | |
| } | |
| return inputCreated; | |
| } | |
| function retrieveInputDefineList(define) { | |
| var defineList = retrieveValue(define.buttons, define.button, define.input, define.inputs); | |
| if (typeof defineList === 'function') { | |
| defineList = defineList(chart); | |
| } | |
| if (!(defineList instanceof Array)) { | |
| defineList = defineList ? [defineList] : []; | |
| } | |
| return defineList; | |
| } | |
| function getInputsTextHTML(inputDefine, defaultText) { | |
| return encodeHTML(retrieveValue(inputDefine.name, inputDefine.text, defaultText)); | |
| } | |
| function getBtnEventListener(inputDefine, names) { | |
| for (var idx = 0; idx < names.length; idx++) { | |
| if (inputDefine[names[idx]]) { | |
| return inputDefine[names[idx]]; | |
| } | |
| } | |
| } | |
| function retrieveId(inputDefine, idPropName) { | |
| if (inputDefine && inputDefine[idPropName] != null) { | |
| var type = getType(inputDefine[idPropName]); | |
| if (type !== 'string' && type != 'number') { | |
| throw new Error(errMsgPrefix + ' id must be string or number.'); | |
| } | |
| return inputDefine[idPropName] + ''; | |
| } | |
| } | |
| function createInputByDefine(inputDefine, inputRecorder) { | |
| if (!inputDefine) { | |
| return; | |
| } | |
| var inputType = inputDefine.hasOwnProperty('type') ? inputDefine.type : 'button'; | |
| if (arrayIndexOf(NAMES_TYPE_RANGE, inputType) >= 0) { | |
| return createRangeInput(inputDefine, null, inputRecorder); | |
| } | |
| else if (arrayIndexOf(NAMES_TYPE_SELECT, inputType) >= 0) { | |
| return createSelectInput(inputDefine, inputRecorder); | |
| } | |
| else if (arrayIndexOf(NAMES_TYPE_BR, inputType) >= 0) { | |
| return createBr(inputDefine, inputRecorder); | |
| } | |
| else if (arrayIndexOf(NAMES_TYPE_HR, inputType) >= 0) { | |
| return createHr(inputDefine, inputRecorder); | |
| } | |
| else if (arrayIndexOf(NAMES_TYPE_BUTTON, inputType) >= 0) { | |
| return createButtonInput(inputDefine, inputRecorder); | |
| } | |
| else if (arrayIndexOf(NAMES_TYPE_GROUP_SET, inputType) >= 0) { | |
| return createGroupSetInput(inputDefine, inputRecorder); | |
| } | |
| else { | |
| throw new Error(errMsgPrefix + ' Unsupported button type: ' + inputType); | |
| } | |
| } | |
| function createRangeInput(inputDefine, internallyForceDef, inputRecorder) { | |
| var _currVal = +retrieveValue(inputDefine.value, 0); | |
| var _disabled = false; | |
| var _step = +retrieveValue(inputDefine.step, 1); | |
| var _minVal = +retrieveValue(inputDefine.min, 0); | |
| var _maxVal = +retrieveValue(inputDefine.max, 100); | |
| var _precision = Math.max( | |
| getPrecision(_minVal), | |
| getPrecision(_maxVal), | |
| getPrecision(_currVal), | |
| getPrecision(_step) | |
| ); | |
| var _noDeltaButtons = !!inputDefine.noDeltaButtons; // Only for backward compat. | |
| var _rangeInputWrapperEl; | |
| var _rangeInputListener; | |
| var _rangeInputEl; | |
| var _rangeInputValueEl; | |
| var _opSuffix = internallyForceDef && internallyForceDef.id || ''; | |
| dealInitRangeInput(); | |
| return { | |
| elList: [_rangeInputWrapperEl], | |
| disable: resetRangeInputDisabled, | |
| getState: getRangeInputState, | |
| setState: setRangeInputState, | |
| }; | |
| function dealInitRangeInput() { | |
| _rangeInputWrapperEl = document.createElement('span'); | |
| resetRangeInputWrapperCSS(_rangeInputWrapperEl, false); | |
| _rangeInputListener = internallyForceDef | |
| ? getBtnEventListener(internallyForceDef, NAMES_ON_INPUT_CHANGE) | |
| : getBtnEventListener(inputDefine, NAMES_ON_INPUT_CHANGE); | |
| if (!_rangeInputListener) { | |
| throw new Error( | |
| errMsgPrefix + ' No listener (either ' | |
| + NAMES_ON_INPUT_CHANGE.join(', ') + ') specified for slider.' | |
| ); | |
| } | |
| var sliderTextEl = document.createElement('span'); | |
| sliderTextEl.className = 'test-inputs-slider-text'; | |
| sliderTextEl.innerHTML = internallyForceDef | |
| ? getInputsTextHTML(internallyForceDef, '') | |
| : getInputsTextHTML(inputDefine, ''); | |
| _rangeInputWrapperEl.appendChild(sliderTextEl); | |
| function createRangeInputDeltaBtn(btnName, delta) { | |
| if (_noDeltaButtons) { return; } | |
| var sliderLRBtnEl = document.createElement('div'); | |
| sliderLRBtnEl.className = 'test-inputs-slider-btn-incdec test-inputs-slider-btn-' + btnName; | |
| _rangeInputWrapperEl.appendChild(sliderLRBtnEl); | |
| sliderLRBtnEl.addEventListener('click', inputRecorder.wrapUserInputListener({ | |
| listener: function () { | |
| if (_disabled) { return; } | |
| // 0.1 + 0.2 = 0.30000000000000004 | |
| _currVal = round(_currVal + delta, _precision); | |
| updateRangeInputViewValue(_currVal); | |
| dispatchRangeInputChangedEvent(); | |
| }, | |
| op: btnName + _opSuffix | |
| })); | |
| } | |
| createRangeInputDeltaBtn('decrease', -_step); | |
| createRangeInputDeltaBtn('increase', _step); | |
| _rangeInputEl = document.createElement('input'); | |
| _rangeInputEl.className = 'test-inputs-slider-input'; | |
| _rangeInputEl.setAttribute('type', 'range'); | |
| _rangeInputEl.addEventListener('input', inputRecorder.wrapUserInputListener({ | |
| listener: function () { | |
| if (_disabled) { return; } | |
| _currVal = +this.value; | |
| updateRangeInputViewValue(_currVal); | |
| dispatchRangeInputChangedEvent(); | |
| }, | |
| op: 'slide' + _opSuffix, | |
| createRecordArgs: function () { | |
| return [+this.value]; | |
| }, | |
| prepareReplay: function (recordArgs) { | |
| _rangeInputEl.value = recordArgs[0]; | |
| return { | |
| this: _rangeInputEl, | |
| arguments: [] | |
| }; | |
| } | |
| })); | |
| _rangeInputEl.setAttribute('min', _minVal); | |
| _rangeInputEl.setAttribute('max', _maxVal); | |
| _rangeInputEl.setAttribute('value', _currVal); | |
| _rangeInputEl.setAttribute('step', _step); | |
| _rangeInputWrapperEl.appendChild(_rangeInputEl); | |
| _rangeInputValueEl = document.createElement('span'); | |
| _rangeInputValueEl.className = 'test-inputs-slider-value'; | |
| _rangeInputWrapperEl.appendChild(_rangeInputValueEl); | |
| updateRangeInputViewValue(_currVal); | |
| resetRangeInputDisabled(inputDefine); | |
| } | |
| function updateRangeInputViewValue(newVal) { | |
| _rangeInputEl.value = +newVal; | |
| _rangeInputValueEl.innerHTML = encodeHTML(newVal + '' + (inputDefine.suffix || '')); | |
| } | |
| function resetRangeInputWrapperCSS(wrapperEl, disabled) { | |
| wrapperEl.className = 'test-inputs-slider' | |
| + (internallyForceDef ? ' test-inputs-slider-sub' : '') | |
| + (disabled ? ' test-inputs-slider-disabled' : ''); | |
| + (_noDeltaButtons ? ' test-inputs-slider-no-delta-buttons' : ''); | |
| } | |
| function setRangeInputState(state) { | |
| if (!isObject(state)) { | |
| console.error( | |
| errMsgPrefix + ' Range input state must be object rather than ' + printObject(state) | |
| + ' May caused by test case change.' | |
| ); | |
| return; | |
| } | |
| var newVal = +state.value; | |
| if (!isFinite(newVal)) { | |
| console.error( | |
| errMsgPrefix + ' Range input state.value must be number rather than ' + printObject(state) | |
| + ' May caused by test case change.' | |
| ); | |
| return; | |
| } | |
| _currVal = newVal; | |
| resetRangeInputDisabled({disabled: state.disabled}); | |
| updateRangeInputViewValue(_currVal); | |
| } | |
| function getRangeInputState() { | |
| return { | |
| value: _currVal, | |
| disabled: _disabled, | |
| }; | |
| } | |
| function resetRangeInputDisabled(opt) { | |
| _disabled = !!opt.disabled; | |
| _rangeInputEl.disabled = _disabled; | |
| resetRangeInputWrapperCSS(_rangeInputWrapperEl, _disabled); | |
| } | |
| function dispatchRangeInputChangedEvent() { | |
| if (_disabled) { return; } | |
| var target = {value: _currVal}; | |
| _rangeInputListener.call(target, {target: target}); | |
| } | |
| } // End of createRangeInput | |
| function createSelectInput(inputDefine, inputRecorder) { | |
| var selectCtx = { | |
| _optionList: [], | |
| _selectWrapperEl: null, | |
| _selectEl: null, | |
| _optionIdxToSubInput: [], | |
| _el: null, | |
| _disabled: false, | |
| }; | |
| var _SAMPLE_SELECT_DEFINITION = [ | |
| '{', | |
| ' type: "select",', | |
| ' text?: "my select:",', | |
| ' options: [', | |
| ' {text?: string, value: any},', | |
| ' {text?: string, input: {type: "range", ...}},', | |
| ' {text?: string, id: "some_option_id", input: {type: "range", ...}},', | |
| ' ...,', | |
| ' ],', | |
| ' onchange() { ... },', | |
| '}' | |
| ].join('\n'); | |
| createSelectInputElements(); | |
| var _selectListener = getBtnEventListener(inputDefine, NAMES_ON_INPUT_CHANGE); | |
| assert( | |
| _selectListener, | |
| errMsgPrefix + ' No listener specified for select. Should have either one of ' | |
| + NAMES_ON_INPUT_CHANGE.join(', ') + '.' | |
| ); | |
| initSelectInputOptions(inputDefine); | |
| selectCtx._selectEl.addEventListener('change', inputRecorder.wrapUserInputListener({ | |
| listener: function dispatchSelectInputChangedEvent() { | |
| if (selectCtx._disabled) { return; } | |
| resetSelectInputSubInputsDisabled(); | |
| triggerUserSelectChangedEvent(); | |
| }, | |
| op: 'select', | |
| createRecordArgs: function () { | |
| return [getSelectInputOptionIndex()]; | |
| }, | |
| prepareReplay: function (recordArgs) { | |
| var optionIndex = recordArgs[0]; | |
| validateOptionIndex(optionIndex); | |
| selectCtx._selectEl.value = optionIndex; | |
| return { | |
| this: selectCtx._selectEl, | |
| arguments: [] | |
| }; | |
| } | |
| })); | |
| setSelectInputInitValue(inputDefine); | |
| resetSelectInputDisabled(inputDefine); | |
| return { | |
| elList: [selectCtx._el], | |
| disable: resetSelectInputDisabled, | |
| getState: getSelectInputState, | |
| setState: setSelectInputState, | |
| }; | |
| function createSelectInputElements() { | |
| var selectWrapperEl = document.createElement('span'); | |
| selectCtx._selectWrapperEl = selectWrapperEl; | |
| resetSelectInputWrapperCSS(selectWrapperEl, false); | |
| var textEl = document.createElement('span'); | |
| textEl.className = 'test-inputs-select-text'; | |
| textEl.innerHTML = getInputsTextHTML(inputDefine, ''); | |
| selectWrapperEl.appendChild(textEl); | |
| var selectEl = document.createElement('select'); | |
| selectEl.className = 'test-inputs-select-select'; | |
| selectWrapperEl.appendChild(selectEl); | |
| selectCtx._el = selectWrapperEl; | |
| selectCtx._selectEl = selectEl; | |
| } | |
| function resetSelectInputWrapperCSS(selectWrapperEl, disabled) { | |
| selectWrapperEl.className = 'test-inputs-select' | |
| + (disabled ? ' test-inputs-select-disabled' : ''); | |
| } | |
| function initSelectInputOptions(inputDefine) { | |
| // optionDef can be {text, value} or just value | |
| // (value can be null/undefined/array/object/... everything). | |
| // Convinient but might cause ambiguity when a value happens to be {text, value}, but rarely happen. | |
| if (inputDefine.options) { | |
| var innerInputCount = 0; | |
| for (var optionIdx = 0; optionIdx < inputDefine.options.length; optionIdx++) { | |
| var optionDef = inputDefine.options[optionIdx]; | |
| assert(isObject(optionDef), [ | |
| errMsgPrefix + ' Select option definition should be an object, such as,', | |
| _SAMPLE_SELECT_DEFINITION | |
| ].join('\n')); | |
| assert(optionDef.hasOwnProperty('value') || isObject(optionDef.input), [ | |
| errMsgPrefix + ' Select option definition should contain prop' | |
| + ' either `value` or `option`, such as,', | |
| _SAMPLE_SELECT_DEFINITION | |
| ].join('\n')); | |
| var text = getType(optionDef.text) === 'string' | |
| ? optionDef.text | |
| : makeSelectInputTextByValue(optionDef); | |
| selectCtx._optionList.push({ | |
| value: optionDef.value, | |
| input: optionDef.input, | |
| id: optionDef.id, | |
| text: text | |
| }); | |
| if (optionDef.input) { | |
| innerInputCount++; | |
| } | |
| assert(innerInputCount < 2 || optionDef.id != null, [ | |
| errMsgPrefix + ' If more than one inner input in a select,' | |
| + ' option id must be specified. ', | |
| _SAMPLE_SELECT_DEFINITION | |
| ].join('\n')); | |
| } | |
| } | |
| else if (inputDefine.values) { | |
| for (var optionIdx = 0; optionIdx < inputDefine.values.length; optionIdx++) { | |
| var value = inputDefine.values[optionIdx]; | |
| selectCtx._optionList.push({ | |
| value: value, | |
| text: makeSelectInputTextByValue({value: value}) | |
| }); | |
| } | |
| } | |
| if (!selectCtx._optionList.length) { | |
| throw new Error(errMsgPrefix + ' No options specified for select.'); | |
| } | |
| for (var optionIdx = 0; optionIdx < selectCtx._optionList.length; optionIdx++) { | |
| var optionDef = selectCtx._optionList[optionIdx]; | |
| selectCtx._optionList[optionIdx] = optionDef; | |
| var optionEl = document.createElement('option'); | |
| optionEl.innerHTML = encodeHTML(optionDef.text); | |
| // HTML select.value is always string. But it would be more convenient to | |
| // convert it to user's raw input value type. | |
| // (The input raw value can be null/undefined/array/object/... everything). | |
| optionEl.value = optionIdx; | |
| selectCtx._selectEl.appendChild(optionEl); | |
| if (optionDef.input) { | |
| if (arrayIndexOf(NAMES_TYPE_RANGE, optionDef.input.type) < 0) { | |
| throw new Error(errMsgPrefix + ' Sub input only supported for range input.'); | |
| } | |
| var rangeInputCreated = createRangeInput(optionDef.input, { | |
| text: '', | |
| id: optionDef.id, | |
| onchange: function () { | |
| if (selectCtx._disabled) { return; } | |
| triggerUserSelectChangedEvent(); | |
| } | |
| }, inputRecorder); | |
| for (var idx = 0; idx < rangeInputCreated.elList.length; idx++) { | |
| selectCtx._el.appendChild(rangeInputCreated.elList[idx]); | |
| } | |
| selectCtx._optionIdxToSubInput[optionIdx] = rangeInputCreated; | |
| } | |
| } | |
| } | |
| function resetSelectInputDisabled(opt) { | |
| selectCtx._disabled = !!opt.disabled; | |
| selectCtx._selectEl.disabled = selectCtx._disabled; | |
| resetSelectInputWrapperCSS(selectCtx._selectWrapperEl, selectCtx._disabled); | |
| resetSelectInputSubInputsDisabled(); | |
| } | |
| function getSelectInputState() { | |
| var optionIndex = getSelectInputOptionIndex(); | |
| var state = {}; | |
| state.optionIndex = optionIndex; | |
| state.disabled = selectCtx._disabled; | |
| if (selectCtx._optionIdxToSubInput.length) { // Make literal state short to save space. | |
| state.optionStateMap = {}; | |
| for (var optionIdx = 0; optionIdx < selectCtx._optionIdxToSubInput.length; optionIdx++) { | |
| if (selectCtx._optionIdxToSubInput[optionIdx]) { | |
| state.optionStateMap[optionIdx] = selectCtx._optionIdxToSubInput[optionIdx].getState(); | |
| } | |
| } | |
| } | |
| return state; | |
| } | |
| function setSelectInputState(state) { | |
| if (!isObject(state)) { | |
| console.error( | |
| errMsgPrefix + ' Invalid select input state: ' + printObject(state) | |
| + ' May caused by test case change.' | |
| ); | |
| return; | |
| } | |
| if (!validateOptionIndex(state.optionIndex)) { | |
| return; | |
| } | |
| var optionStateMap = state.optionStateMap || {}; | |
| for (var optionIdx in optionStateMap) { | |
| if (state.optionStateMap.hasOwnProperty(optionIdx)) { | |
| var subInput = selectCtx._optionIdxToSubInput[optionIdx]; | |
| if (!subInput) { | |
| console.error( | |
| errMsgPrefix + ' Invalid select input state: ' + printObject(state) | |
| + ' Can not find a sub-input by optionIndex: ' + optionIdx + '.' | |
| + ' May caused by test case change.' | |
| ); | |
| return; | |
| } | |
| } | |
| } | |
| for (var optionIdx in optionStateMap) { | |
| if (state.optionStateMap.hasOwnProperty(optionIdx)) { | |
| var subInput = selectCtx._optionIdxToSubInput[optionIdx]; | |
| subInput.setState(state.optionStateMap[optionIdx]); | |
| } | |
| } | |
| resetSelectInputDisabled({disabled: state.disabled}); | |
| resetSelectInputOptionIndex(state.optionIndex); | |
| } | |
| function validateOptionIndex(optionIndex) { | |
| if (getType(optionIndex) !== 'number' | |
| || optionIndex < 0 | |
| || optionIndex >= selectCtx._optionList.length | |
| ) { | |
| console.error( | |
| errMsgPrefix + ' Invalid select, optionIndex: ' + optionIndex + ' is out if range.' | |
| + ' May caused by test case change.' | |
| ); | |
| return false; | |
| } | |
| return true; | |
| } | |
| function setSelectInputInitValue(inputDefine) { | |
| var initOptionIdx = 0; | |
| var initOptionIdxOpt = retrieveValue(inputDefine.optionIndex, inputDefine.valueIndex, undefined); | |
| if (initOptionIdxOpt != null) { | |
| if (initOptionIdxOpt < 0 || initOptionIdxOpt >= selectCtx._optionList.length) { | |
| throw new Error(errMsgPrefix + ' Invalid optionIndex: ' + initOptionIdxOpt); | |
| } | |
| selectCtx._selectEl.value = selectCtx._optionList[initOptionIdxOpt].value; | |
| initOptionIdx = initOptionIdxOpt; | |
| } | |
| else if (inputDefine.hasOwnProperty('value')) { | |
| var found = false; | |
| for (var idx = 0; idx < selectCtx._optionList.length; idx++) { | |
| if (!selectCtx._optionList[idx].input | |
| && selectCtx._optionList[idx].value === inputDefine.value | |
| ) { | |
| found = true; | |
| initOptionIdx = idx; | |
| } | |
| } | |
| if (!found) { | |
| throw new Error(errMsgPrefix + ' Value not found in select options: ' + inputDefine.value); | |
| } | |
| } | |
| resetSelectInputOptionIndex(initOptionIdx); | |
| } | |
| function resetSelectInputOptionIndex(optionIdx) { | |
| selectCtx._selectEl.value = optionIdx; | |
| resetSelectInputSubInputsDisabled(); | |
| } | |
| function getSelectInputOptionIndex() { | |
| return +selectCtx._selectEl.value; | |
| } | |
| function getSelectInputValueByOptionIndex(optionIdx) { | |
| return selectCtx._optionList[optionIdx].input | |
| ? selectCtx._optionIdxToSubInput[optionIdx].getState().value | |
| : selectCtx._optionList[optionIdx].value; | |
| } | |
| function triggerUserSelectChangedEvent() { | |
| var optionIdx = getSelectInputOptionIndex(); | |
| var value = getSelectInputValueByOptionIndex(optionIdx); | |
| var optionId = selectCtx._optionList[optionIdx].id; | |
| var target = {value: value, optionId: optionId}; | |
| _selectListener.call(target, {target: target}); | |
| } | |
| function resetSelectInputSubInputsDisabled() { | |
| var optionIdx = getSelectInputOptionIndex(); | |
| for (var i = 0; i < selectCtx._optionIdxToSubInput.length; i++) { | |
| var subInput = selectCtx._optionIdxToSubInput[i]; | |
| if (subInput) { | |
| var disabled = selectCtx._disabled | |
| ? true // Disable all options. | |
| : i !== optionIdx // Disable all except current selected option. | |
| subInput.disable({disabled: disabled}); | |
| } | |
| } | |
| } | |
| function makeSelectInputTextByValue(optionDef) { | |
| if (optionDef.hasOwnProperty('value')) { | |
| return printObject(optionDef.value, { | |
| arrayLineBreak: false, objectLineBreak: false, indent: 0, lineBreak: '' | |
| }); | |
| } | |
| else if (optionDef.input) { | |
| return 'range input'; | |
| } | |
| } | |
| } // End of createSelectInput | |
| function createGroupSetInput(groupSetDefine) { | |
| assert( | |
| getType(groupSetDefine.inputsHeight) === 'number', | |
| '`inputsHeight` is mandatory on groupSet to avoid height change' | |
| + ' to affects visual testing when switching groups.' | |
| ) | |
| assert( | |
| getType(groupSetDefine.groups) === 'array', | |
| '.groups must be an array.' | |
| ); | |
| assert( | |
| groupSetDefine.groups.length > 0, | |
| 'groupset.group must have at least one group' | |
| ); | |
| var groupSetEl = document.createElement('div'); | |
| initInputsContainer(groupSetEl, groupSetDefine, { | |
| ignoreInputsStyle: true, | |
| className: 'test-inputs-groupset', | |
| }); | |
| var groupSetMarginBottomEl = document.createElement('div'); | |
| groupSetMarginBottomEl.className = 'test-inputs-groupset-margin-bottom'; | |
| var groupSetTextEl = document.createElement('div'); | |
| groupSetTextEl.className = 'test-inputs-groupset-text'; | |
| groupSetEl.appendChild(groupSetTextEl); | |
| var groupSetCreated = { | |
| currentGroupIndex: 0, | |
| elList: [groupSetEl, groupSetMarginBottomEl], | |
| children: [], | |
| getState: getGroupSetInputState, | |
| setState: setGroupSetInputState, | |
| switchGroup: switchGroup | |
| }; | |
| for (var groupIdx = 0; groupIdx < groupSetDefine.groups.length; groupIdx++) { | |
| var groupDefine = groupSetDefine.groups[groupIdx]; | |
| assert(groupDefine, 'groupset.group must not be undefined/null.'); | |
| var groupChildInputsContainer = document.createElement('div'); | |
| initInputsContainer(groupChildInputsContainer, groupSetDefine, { | |
| ignoreFixHeight: true, | |
| className: 'test-inputs-groupset-group', | |
| }); | |
| groupSetEl.appendChild(groupChildInputsContainer); | |
| var groupChildId = retrieveId(groupDefine, 'id'); | |
| if (groupChildId == null) { | |
| throw new Error('In group child input, id must be specified.'); | |
| } | |
| var groupCreated = { | |
| groupParent: groupSetCreated, | |
| inputsContainerEl: groupChildInputsContainer, | |
| groupSetTextEl: groupSetTextEl, | |
| groupDefine: groupDefine, | |
| idList: null, | |
| groupIndex: groupSetCreated.children.length | |
| }; | |
| groupSetCreated.children.push(groupCreated); | |
| storeToInputDict(groupDefine, groupCreated); | |
| var inputsDefineList = retrieveInputDefineList(groupDefine).slice(); | |
| // Cascade `disabled`. | |
| for (var inputIdx = 0; inputIdx < inputsDefineList.length; inputIdx++) { | |
| var inputDefine = inputsDefineList[inputIdx]; | |
| if (!inputDefine) { | |
| continue; | |
| } | |
| assert(isObject(inputDefine)); | |
| inputsDefineList[inputIdx] = inputDefine = Object.assign({}, inputDefine); | |
| inputDefine.disabled = retrieveValue( | |
| inputDefine.disabled, groupDefine.disabled, groupSetDefine.disabled | |
| ); | |
| } | |
| groupCreated.idList = dealInitEachInput(inputsDefineList, groupChildInputsContainer); | |
| showHideGroupInGroupSet(groupCreated, false); | |
| } | |
| showHideGroupInGroupSet(groupSetCreated.children[groupSetCreated.currentGroupIndex], true); | |
| return groupSetCreated; | |
| function switchGroup(groupId) { | |
| var groupCreatedToShow = retrieveAndVerifyGroup(groupId); | |
| if (groupCreatedToShow.groupIndex === groupCreatedToShow.groupParent.currentGroupIndex) { | |
| return; | |
| } | |
| var groupCreatedToHide = groupCreatedToShow.groupParent.children[ | |
| groupCreatedToShow.groupParent.currentGroupIndex | |
| ]; | |
| showHideGroupInGroupSet(groupCreatedToHide, false); | |
| showHideGroupInGroupSet(groupCreatedToShow, true); | |
| groupCreatedToShow.groupParent.currentGroupIndex = groupCreatedToShow.groupIndex; | |
| } | |
| function getGroupSetInputState() { | |
| var state = {currentGroupIndex: groupSetCreated.currentGroupIndex}; | |
| return state; | |
| } | |
| function setGroupSetInputState(state) { | |
| if (!isObject(state)) { | |
| console.error( | |
| errMsgPrefix + ' Invalid group set state: ' + printObject(state) | |
| + ' May caused by test case change.' | |
| ); | |
| return; | |
| } | |
| var currentGroupIndex = state.currentGroupIndex; | |
| if (getType(currentGroupIndex) !== 'number' | |
| || currentGroupIndex < 0 | |
| || currentGroupIndex >= groupSetCreated.children.length | |
| ) { | |
| console.error( | |
| errMsgPrefix + ' Invalid group set currentGroupIndex: ' + currentGroupIndex | |
| + ' May caused by test case change.' | |
| ); | |
| return; | |
| } | |
| switchGroup(currentGroupIndex); | |
| } | |
| } // End of createGroupSetInput | |
| function createButtonInput(inputDefine, inputRecorder) { | |
| var _btnDisabled = false; | |
| var btn = document.createElement('button'); | |
| btn.innerHTML = getInputsTextHTML(inputDefine, 'button'); | |
| var _btnListener = getBtnEventListener(inputDefine, NAMES_ON_CLICK); | |
| assert(_btnListener, 'No button onclick provided.'); | |
| btn.addEventListener('click', inputRecorder.wrapUserInputListener({ | |
| listener: function () { | |
| if (_btnDisabled) { return; } | |
| return _btnListener.apply(this, arguments); | |
| }, | |
| op: 'click' | |
| })); | |
| resetButtonInputDisabled(inputDefine); | |
| return { | |
| elList: [btn], | |
| disable: resetButtonInputDisabled, | |
| setState: setButtonInputState, | |
| getState: getButtonInputState | |
| }; | |
| function resetButtonInputDisabled(opt) { | |
| _btnDisabled = !!opt.disabled; | |
| btn.disabled = _btnDisabled; | |
| } | |
| function getButtonInputState() { | |
| return {disabled: _btnDisabled}; | |
| } | |
| function setButtonInputState(state) { | |
| if (!isObject(state)) { | |
| console.error( | |
| errMsgPrefix + ' Button input state must be object rather than ' + printObject(state) | |
| + ' May caused by test case change.' | |
| ); | |
| return; | |
| } | |
| resetButtonInputDisabled(state); | |
| } | |
| } // End of createButtonInput | |
| function createBr(inputDefine) { | |
| return {elList: [document.createElement('br')]}; | |
| } | |
| function createHr(inputDefine) { | |
| var _hrWrapperEl = document.createElement('div'); | |
| _hrWrapperEl.className = 'test-inputs-hr' | |
| var textEl = document.createElement('span'); | |
| textEl.className = 'test-inputs-hr-text'; | |
| _hrWrapperEl.appendChild(textEl); | |
| var text = textEl.innerHTML = getInputsTextHTML(inputDefine, ''); | |
| textEl.style.display = text ? 'block' : 'none'; | |
| return { | |
| elList: [_hrWrapperEl] | |
| }; | |
| } | |
| } // End of initInputs | |
| function initRecordCanvas(opt, chart, recordCanvasContainer) { | |
| if (!opt.recordCanvas) { | |
| return; | |
| } | |
| recordCanvasContainer.innerHTML = '' | |
| + '<button>Show Canvas Record</button>' | |
| + '<button>Clear Canvas Record</button>' | |
| + '<div class="content-area"><textarea></textarea><br><button>Close</button></div>'; | |
| var buttons = recordCanvasContainer.getElementsByTagName('button'); | |
| var canvasRecordButton = buttons[0]; | |
| var clearButton = buttons[1]; | |
| var closeButton = buttons[2]; | |
| var recordArea = recordCanvasContainer.getElementsByTagName('textarea')[0]; | |
| var contentAraa = recordArea.parentNode; | |
| canvasRecordButton.addEventListener('click', function () { | |
| var content = []; | |
| eachCtx(function (zlevel, ctx) { | |
| content.push('\nLayer zlevel: ' + zlevel, '\n\n'); | |
| if (typeof ctx.stack !== 'function') { | |
| alert('Missing: <script src="test/lib/canteen.js"></script>'); | |
| return; | |
| } | |
| var stack = ctx.stack(); | |
| for (var i = 0; i < stack.length; i++) { | |
| var line = stack[i]; | |
| content.push(JSON.stringify(line), ',\n'); | |
| } | |
| }); | |
| contentAraa.style.display = 'block'; | |
| recordArea.value = content.join(''); | |
| }); | |
| clearButton.addEventListener('click', function () { | |
| eachCtx(function (zlevel, ctx) { | |
| ctx.clear(); | |
| }); | |
| recordArea.value = 'Cleared.'; | |
| }); | |
| closeButton.addEventListener('click', function () { | |
| contentAraa.style.display = 'none'; | |
| }); | |
| function eachCtx(cb) { | |
| var painter = chart.getZr().painter; | |
| if (painter.type !== 'canvas') { | |
| return; | |
| } | |
| var layers = painter.getLayers(); | |
| for (var zlevel in layers) { | |
| if (layers.hasOwnProperty(zlevel)) { | |
| var layer = layers[zlevel]; | |
| var canvas = layer.dom; | |
| var ctx = canvas.getContext('2d'); | |
| cb(zlevel, ctx); | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * @param {EChartsInstance} chart | |
| * @param {Parameter<testHelper.create, 2>['boundingRect']} opt.boundingRect | |
| */ | |
| function initShowBoundingRects(chart, echarts, opt, boundingRectsContainer) { | |
| assert(chart.__testHelper); | |
| var _bRectZr; | |
| var _bRectGroup; | |
| // @type Parameter<testHelper.create, 2>['boundingRect'] | |
| var _currBoundingRectOpt = false; | |
| chart.__testHelper.updateBoundingRects | |
| = chart.__testHelper.updateBoundingRect | |
| = chart.__testHelper.boundingRect | |
| = chart.__testHelper.boundingRects | |
| = updateBoundingRects; | |
| updateBoundingRects(opt.boundingRect); | |
| return; | |
| function updateBoundingRects(opt) { | |
| if (arguments.length > 0) { | |
| _currBoundingRectOpt = opt; | |
| } // If no opt, keep the last one. | |
| _currBoundingRectOpt | |
| ? buildBoundingRects(_currBoundingRectOpt) | |
| : disableBoundingRects(); | |
| } | |
| function ensureBoundingRectsFacilities() { | |
| // zr requires size non-zero. | |
| boundingRectsContainer.style.width = chart.getWidth() + 'px'; | |
| boundingRectsContainer.style.height = chart.getHeight() + 'px'; | |
| if (_bRectZr) { | |
| _bRectZr.resize(); | |
| return; | |
| } | |
| _bRectGroup = new echarts.graphic.Group(); | |
| _bRectGroup.__testHelperBoundingRectsRoot = true; | |
| _bRectGroup.on('click', function (event) { | |
| var target = event.target; | |
| if (!target || !target.__testHelperBoundingRectTarget) { | |
| return; | |
| } | |
| var wrapper = { | |
| boundingRect: target, | |
| rawElement: target.__testHelperBoundingRectTarget | |
| }; | |
| console.log('boundingRect:', wrapper.boundingRect); | |
| console.log('rawElement:', wrapper.rawElement); | |
| window.$0 = wrapper; | |
| }); | |
| _bRectZr = echarts.zrender.init(boundingRectsContainer); | |
| _bRectZr.add(_bRectGroup); | |
| } | |
| function disableBoundingRects() { | |
| chart.off('finished', updateBoundingRects); | |
| boundingRectsContainer.style.display = 'none'; | |
| if (_bRectGroup) { | |
| _bRectGroup.removeAll(); | |
| } | |
| } | |
| function buildBoundingRects(boundingRectOpt) { | |
| ensureBoundingRectsFacilities(); | |
| boundingRectOpt = isObject(boundingRectOpt) ? boundingRectOpt : {}; | |
| boundingRectsContainer.style.display = 'block'; | |
| _bRectGroup.removeAll(); | |
| var strokeColor = boundingRectOpt.color || 'rgba(0,0,255,0.5)'; | |
| var silent = boundingRectOpt.silent != null ? boundingRectOpt.silent : false; | |
| boundingRectsContainer.style.pointerEvent = silent ? 'none' : 'auto'; | |
| var roots = chart.getZr().storage.getRoots(); | |
| for (var rootIdx = 0; rootIdx < roots.length; rootIdx++) { | |
| travelGroupAndBuildRects(roots[rootIdx], _bRectGroup); | |
| } | |
| // Follow chart update and resize. | |
| chart.on('finished', updateBoundingRects); | |
| return; | |
| function travelGroupAndBuildRects(el, visualRectGroupParent) { | |
| if (el.childrenRef) { // group or text | |
| var visualRectGroup = createVisualRectGroup(el, visualRectGroupParent) | |
| var children = el.childrenRef(); | |
| for (var idx = 0; idx < children.length; idx++) { | |
| var child = children[idx]; | |
| travelGroupAndBuildRects(child, visualRectGroup); | |
| } | |
| } | |
| // Both display ZRText and TSpan bounding rect for debuging. | |
| if (!el.isGroup) { | |
| createVisualRectForEl(el, visualRectGroupParent); | |
| } | |
| function createVisualRectForEl(el, visualRectGroup) { | |
| createRectForDisplayable(el, visualRectGroup); | |
| var textContent = el.getTextContent(); | |
| var textGuildLine = el.getTextGuideLine(); | |
| var textConfig = el.textConfig; | |
| if (textContent || textGuildLine) { | |
| var isLocal = textConfig && textConfig.local; | |
| var targetVisualGroup = isLocal ? visualRectGroup : _bRectGroup; | |
| textContent && createRectForDisplayable(textContent, targetVisualGroup, true); | |
| textGuildLine && createRectForDisplayable(textGuildLine, targetVisualGroup, true); | |
| } | |
| } | |
| function createVisualRectGroup(fromEl, visualRectGroupParent) { | |
| var visualRectGroup = new echarts.graphic.Group(); | |
| copyTransformAttrs(visualRectGroup, fromEl); | |
| visualRectGroupParent.add(visualRectGroup); | |
| return visualRectGroup; | |
| } | |
| function createRectForDisplayable(el, visualRectGroup, useInnerTransformable) { | |
| var elRawRect = el.getBoundingRect(); | |
| var visualRect = new echarts.graphic.Rect({ | |
| shape: {x: elRawRect.x, y: elRawRect.y, width: elRawRect.width, height: elRawRect.height}, | |
| style: {fill: null, stroke: strokeColor, lineWidth: 1, strokeNoScale: true}, | |
| silent: silent, | |
| z: Number.MAX_SAFE_INTEGER | |
| }); | |
| visualRect.__testHelperBoundingRectTarget = el; | |
| var transAttrSource = el; | |
| if (useInnerTransformable && el.innerTransformable) { | |
| transAttrSource = el.innerTransformable; | |
| } | |
| copyTransformAttrs(visualRect, transAttrSource); | |
| visualRectGroup.add(visualRect); | |
| } | |
| } | |
| function copyTransformAttrs(target, source) { | |
| target.x = source.x; | |
| target.y = source.y; | |
| target.rotation = source.rotation; | |
| target.scaleX = source.scaleX; | |
| target.scaleY = source.scaleY; | |
| target.originX = source.originX; | |
| target.originY = source.originY; | |
| target.skewX = source.skewX; | |
| target.skewY = source.skewY; | |
| target.anchorX = source.anchorX; | |
| target.anchorY = source.anchorY; | |
| } | |
| } | |
| } | |
| /** | |
| * Simply create inputs - all of them are 'select' or 'br'. | |
| * This is the most commonly used case. | |
| * | |
| * Usage: | |
| * ```js | |
| * const _ctx = { | |
| * xAxis_axisTick_interval: { | |
| * text: 'xAxis.axisTick.interval:', // Any text | |
| * value: 'NOT_SET', // Initial value of the "select" input. | |
| * values: ['NOT_SET', 2, 0, 1] // Options of the "select" input. | |
| * __extra: 123 // Pass to updateChart directly. | |
| * }, | |
| * xAxis_axisLabel_interval: { | |
| * text: 'xAxis.axisLabel.interval:', | |
| * value: 'NOT_SET', | |
| * values: ['NOT_SET', 2, 0, 1] | |
| * }, | |
| * br0: {}, // Can create a 'br' input. | |
| * // 'br'/'BR' is reserved keys. | |
| * dataZoomOrMinMax: { | |
| * text: 'use:', // Can omit. | |
| * value: 'use_xAxis_min_max', | |
| * values: ['use_xAxis_min_max', 'use_dataZoom'], | |
| * }, | |
| * br1: {}, // Create another 'br' input. | |
| * hr1: { // Create a 'hr' input. | |
| * text: 'some label on hr' | |
| * }, | |
| * }; | |
| * function createOption() { | |
| * return {...}; | |
| * } | |
| * function updateChart(currentInputOption) { | |
| * chart.setOption(createOption(), {notMerge: true}); | |
| * console.log(currentInputOption.__extra); // 123 or null/undefined. | |
| * } | |
| * var chart = testHelper.create(echarts, 'chart4', { | |
| * option: createOption(), | |
| * inputsStyle: 'compact', | |
| * inputs: testHelper.createInputsSimply(_ctx, updateChart) | |
| * }; | |
| * ``` | |
| */ | |
| testHelper.createInputsSimply = function (_ctx, updateChart) { | |
| var inputs = []; | |
| for (var key in _ctx) { | |
| if (_ctx.hasOwnProperty(key)) { | |
| inputs.push(createInput(key)); | |
| } | |
| } | |
| function createInput(key) { | |
| if (/^br[0-9]*$/i.test(key)) { | |
| // Can be typically `{br: {}, br0: {}, br1: {}, BR: {}, BR1: {}}`. | |
| return { | |
| type: 'br' | |
| }; | |
| } | |
| else if (/^hr[0-9]*$/i.test(key)) { | |
| // Can be typically `{br: {}, br0: {}, br1: {}, BR: {}, BR1: {}}`. | |
| return { | |
| type: 'hr', | |
| text: _ctx[key].text | |
| }; | |
| } | |
| else { | |
| var input = { | |
| type: 'select', | |
| text: _ctx[key].text || (key + ':'), | |
| onchange: function () { | |
| var newValue = this.value; | |
| if (_ctx[key].hasOwnProperty('valueIndex')) { | |
| _ctx[key].valueIndex = findValueIndex(); | |
| } | |
| else if (_ctx[key].hasOwnProperty('optionIndex')) { | |
| _ctx[key].optionIndex = findValueIndex(); | |
| } | |
| else { | |
| _ctx[key].value = newValue; | |
| } | |
| function findValueIndex() { | |
| if (_ctx[key].values) { | |
| return arrayIndexOf(_ctx[key].values, newValue); | |
| } | |
| else if (_ctx[key].options) { | |
| for (var i = 0; i < _ctx[key].options.length; i++) { | |
| if (_ctx[key].options[i].value === newValue) { | |
| return i; | |
| } | |
| } | |
| return -1; | |
| } | |
| } | |
| updateChart(_ctx[key]); | |
| } | |
| }; | |
| function assignIfExisting(prop) { | |
| if (_ctx[key].hasOwnProperty(prop)) { | |
| input[prop] = _ctx[key][prop]; | |
| } | |
| } | |
| assignIfExisting('value'); | |
| assignIfExisting('valueIndex'); | |
| assignIfExisting('optionIndex'); | |
| assignIfExisting('values'); | |
| assignIfExisting('options'); | |
| return input; | |
| } | |
| } | |
| return inputs; | |
| }; | |
| testHelper.createRecordVideo = function (chart, recordVideoContainer) { | |
| var button = document.createElement('button'); | |
| button.innerHTML = 'Start Recording'; | |
| recordVideoContainer.appendChild(button); | |
| var recorder = new VideoRecorder(chart); | |
| var isRecording = false; | |
| button.onclick = function () { | |
| isRecording ? recorder.stop() : recorder.start(); | |
| button.innerHTML = (isRecording ? 'Start' : 'Stop') + ' Recording'; | |
| isRecording = !isRecording; | |
| } | |
| } | |
| /** | |
| * @param {ECharts} echarts | |
| * @param {HTMLElement|string} domOrId | |
| * @param {Object} option | |
| * @param {boolean|number} opt If number, means height | |
| * @param {boolean} opt.lazyUpdate | |
| * @param {boolean} opt.notMerge | |
| * @param {boolean} opt.useCoarsePointer | |
| * @param {boolean} opt.pointerSize | |
| * @param {number} opt.width | |
| * @param {number} opt.height | |
| * @param {boolean} opt.draggable | |
| * @param {string} opt.renderer 'canvas' or 'svg' | |
| * @param {string} errMsgPrefix | |
| */ | |
| testHelper.createChart = function (echarts, domOrId, option, opt, errMsgPrefix) { | |
| if (typeof opt === 'number') { | |
| opt = {height: opt}; | |
| } | |
| else { | |
| opt = opt || {}; | |
| } | |
| var dom = getDom(domOrId); | |
| if (dom) { | |
| if (opt.width != null) { | |
| dom.style.width = opt.width + 'px'; | |
| } | |
| if (opt.height != null) { | |
| dom.style.height = opt.height + 'px'; | |
| } | |
| var theme = opt.theme && opt.theme !== 'none' ? opt.theme : null; | |
| if (theme == null && window.__ECHARTS__DEFAULT__THEME__) { | |
| theme = window.__ECHARTS__DEFAULT__THEME__; | |
| } | |
| if (typeof theme === 'string') { | |
| require(['theme/' + theme]); | |
| } | |
| var chart = echarts.init(dom, theme, { | |
| renderer: opt.renderer, | |
| useCoarsePointer: opt.useCoarsePointer, | |
| pointerSize: opt.pointerSize | |
| }); | |
| if (opt.draggable) { | |
| if (!window.draggable) { | |
| throw new Error( | |
| errMsgPrefix + ' Pleasse add the script in HTML: \n' | |
| + '<script src="lib/draggable.js"></script>' | |
| ); | |
| } | |
| window.draggable.init(dom, chart, {throttle: 70, onResize: opt.onResize}); | |
| } | |
| option && chart.setOption(option, { | |
| lazyUpdate: opt.lazyUpdate, | |
| notMerge: opt.notMerge | |
| }); | |
| var isAutoResize = opt.autoResize == null ? true : opt.autoResize; | |
| if (isAutoResize) { | |
| testHelper.resizable(chart, {onResize: opt.onResize}); | |
| } | |
| return chart; | |
| } | |
| }; | |
| /** | |
| * @usage | |
| * ```js | |
| * testHelper.printAssert(chart, function (assert) { | |
| * // If any error thrown here, a "checked: Fail" will be printed on the chart; | |
| * // Otherwise, "checked: Pass" will be printed on the chart. | |
| * assert(condition1); | |
| * assert(condition2); | |
| * assert(condition3); | |
| * }); | |
| * ``` | |
| * `testHelper.printAssert` can be called multiple times for one chart instance. | |
| * For each call, one result (fail or pass) will be printed. | |
| * | |
| * @param chartOrDomId {EChartsInstance | string} | |
| * @param checkFn {Function} param: a function `assert`. | |
| */ | |
| testHelper.printAssert = function (chartOrDomId, checkerFn) { | |
| if (!chartOrDomId) { | |
| return; | |
| } | |
| var hostDOMEl; | |
| var chart; | |
| if (typeof chartOrDomId === 'string') { | |
| hostDOMEl = document.getElementById(chartOrDomId); | |
| } | |
| else { | |
| chart = chartOrDomId; | |
| hostDOMEl = chartOrDomId.getDom(); | |
| } | |
| var failErr; | |
| function assert(cond, msg) { | |
| if (!cond) { | |
| throw new Error(msg || 'printAssert error'); | |
| } | |
| } | |
| try { | |
| checkerFn(assert); | |
| } | |
| catch (err) { | |
| console.error(err); | |
| failErr = err; | |
| } | |
| var printAssertRecord = hostDOMEl.__printAssertRecord || (hostDOMEl.__printAssertRecord = []); | |
| var resultDom = document.createElement('div'); | |
| resultDom.innerHTML = failErr ? 'checked: Fail' : 'checked: Pass'; | |
| var fontSize = 40; | |
| resultDom.style.cssText = [ | |
| 'position: absolute;', | |
| 'left: 20px;', | |
| 'pointer-events: none;', | |
| 'font-size: ' + fontSize + 'px;', | |
| 'z-index: ' + (failErr ? 99999 : 88888) + ';', | |
| 'color: ' + (failErr ? 'rgba(150,0,0,0.8)' : 'rgba(0,150,0,0.8)') + ';', | |
| ].join(''); | |
| printAssertRecord.push(resultDom); | |
| hostDOMEl.appendChild(resultDom); | |
| relayoutResult(); | |
| function relayoutResult() { | |
| var chartHeight = chart ? chart.getHeight() : hostDOMEl.offsetHeight; | |
| var lineHeight = Math.min(fontSize + 10, (chartHeight - 20) / printAssertRecord.length); | |
| for (var i = 0; i < printAssertRecord.length; i++) { | |
| var record = printAssertRecord[i]; | |
| record.style.top = (10 + i * lineHeight) + 'px'; | |
| } | |
| } | |
| }; | |
| var _dummyRequestAnimationFrameMounted = false; | |
| var _controlFrameSingleton = null; | |
| /** | |
| * Usage: | |
| * ```js | |
| * testHelper.controlFrame({pauseAt: 60}); | |
| * // Then load echarts.js (must after controlFrame called) | |
| * ``` | |
| * | |
| * @param {Object} [opt] | |
| * @param {number} [opt.puaseAt] If specified `pauseAt`, auto pause at the frame. | |
| * @param {Function} [opt.onFrame] | |
| */ | |
| testHelper.controlFrame = function (opt) { | |
| if (_controlFrameSingleton) { | |
| throw new Error('`testHelper.controlFrame` can only be called once.'); | |
| } | |
| _controlFrameSingleton = {}; | |
| opt = opt || {}; | |
| var pauseAt = opt.pauseAt; | |
| pauseAt == null && (pauseAt = 0); | |
| var _running = true; | |
| var _pendingCbList = []; | |
| var _frameNumber = 0; | |
| var _mounted = false; | |
| var _mask = null; | |
| function getRunBtnText() { | |
| return _running ? 'Pause' : 'Resume'; | |
| } | |
| var buttons = [{ | |
| text: getRunBtnText(), | |
| onclick: function () { | |
| buttons[0].el.innerHTML = getRunBtnText(); | |
| _running ? pause() : run(); | |
| } | |
| }, { | |
| text: 'Next Frame', | |
| onclick: nextFrame | |
| }]; | |
| var btnPanel = document.createElement('div'); | |
| btnPanel.className = 'control-frame-btn-panel' | |
| var infoEl = document.createElement('div'); | |
| infoEl.className = 'control-frame-info'; | |
| btnPanel.appendChild(infoEl); | |
| document.body.appendChild(btnPanel); | |
| for (var i = 0; i < buttons.length; i++) { | |
| var button = buttons[i]; | |
| var btnEl = button.el = document.createElement('button'); | |
| btnEl.innerHTML = button.text; | |
| btnEl.addEventListener('click', button.onclick); | |
| btnPanel.appendChild(btnEl); | |
| } | |
| _controlFrameSingleton.btnPanel = btnPanel; | |
| if (_dummyRequestAnimationFrameMounted) { | |
| throw new Error('Do not support `controlFrame` twice'); | |
| } | |
| _dummyRequestAnimationFrameMounted = true; | |
| var raf = window.requestAnimationFrame; | |
| window.requestAnimationFrame = function (cb) { | |
| _pendingCbList.push(cb); | |
| if (_running && !_mounted) { | |
| _mounted = true; | |
| raf(nextFrame); | |
| } | |
| }; | |
| function run() { | |
| setRunning(true); | |
| nextFrame(); | |
| } | |
| function pause() { | |
| setRunning(false); | |
| } | |
| function setRunning(nextRunning) { | |
| if (!_running && nextRunning) { | |
| addOrRemoveMask(false); | |
| } | |
| else if (_running && !nextRunning) { | |
| addOrRemoveMask(true); | |
| } | |
| _running = nextRunning; | |
| } | |
| function nextFrame() { | |
| opt.onFrame && opt.onFrame(_frameNumber); | |
| if (pauseAt != null && _frameNumber === pauseAt) { | |
| setRunning(false); | |
| pauseAt = null; | |
| } | |
| infoEl.innerHTML = 'Frame: ' + _frameNumber + ' ( ' + (_running ? 'Running' : 'Paused') + ' )'; | |
| buttons[0].el.innerHTML = getRunBtnText(); | |
| _mounted = false; | |
| var pending = _pendingCbList; | |
| _pendingCbList = []; | |
| for (var i = 0; i < pending.length; i++) { | |
| pending[i](); | |
| } | |
| _frameNumber++; | |
| } | |
| function addOrRemoveMask(addOrRemove) { | |
| if (addOrRemove && !_mask) { | |
| _mask = document.createElement('div'); | |
| _mask.className = 'control-frame-mask'; | |
| document.body.appendChild(_mask); | |
| } | |
| else if (!addOrRemove && _mask) { | |
| document.body.removeChild(_mask); | |
| _mask = null; | |
| } | |
| } | |
| }; // End of `controlFrame` | |
| /** | |
| * Print zrender canvas layer draw operations on frame. | |
| * | |
| * @usage | |
| * ```js | |
| * var recordContainer = document.getElementById('record'); | |
| * testHelper.controlFrame({ | |
| * pauseAt: 30, | |
| * onFrame: function (frameNumber) { | |
| * testHelper.printCanvasLayerDrawOperationsOnFrame(chart, frameNumber, recordContainer); | |
| * } | |
| * }); | |
| * ``` | |
| */ | |
| testHelper.printCanvasLayerDrawOperationsOnFrame = function ( | |
| chart, // Mandatory. | |
| frameNumber, // Mandatory. | |
| recordContainer, // Mandatory. | |
| cellMax // Optional; max cell count to display | |
| ) { | |
| if (!chart) { | |
| return; | |
| } | |
| var _drawOpCtx = chart.__printCanvasLayerDrawOperationsOnFrame_ctx | |
| || (chart.__printCanvasLayerDrawOperationsOnFrame_ctx = initPrintDrawOpCtx(recordContainer)); | |
| // key: layer key; value: ops array. | |
| var _lastOps = {}; | |
| doPrint(); | |
| function initPrintDrawOpCtx(recordContainer) { | |
| if (window.Canteen) { | |
| window.Canteen.globals.STACK_SIZE = 100000000; | |
| } | |
| else { | |
| console.error && console.error('canteen.js is required but not imported'); | |
| } | |
| recordContainer.innerHTML = [ | |
| '<div class="print-canvas-layer-draw-operations-on-frame-record-title">', | |
| 'NOTE: In "incremental layers" (layer zr_N.1), ', | |
| 'canvas instruction count per frame (<span class="print-canvas-layer-draw-operations-on-frame-cmd-count">red number</span>) should be the same per frame;', | |
| '<br>In "normal layers" (layer zr_N.0 or zr_N.2), should be no incremental canvas instructions per frame.', | |
| '</div>' | |
| ].join(''); | |
| recordContainer.className = 'print-canvas-layer-draw-operations-on-frame-record'; | |
| return { | |
| layersInfoMap: {}, | |
| recordContainer: recordContainer, | |
| CELL_MAX: cellMax || 90, | |
| }; | |
| } | |
| function doPrint() { | |
| _drawOpCtx.lastOps = {}; | |
| var painter = chart.getZr().painter; | |
| if (painter.type !== 'canvas') { | |
| return; | |
| } | |
| var layers = painter.getLayers(); | |
| for (var layerId in layers) { | |
| if (layers.hasOwnProperty(layerId)) { | |
| printSingleCanvasLayer(layerId, layers[layerId], frameNumber); | |
| } | |
| } | |
| } | |
| function printSingleCanvasLayer(layerId, layer, frameNumber) { | |
| var layerInfo = _drawOpCtx.layersInfoMap[layerId]; | |
| if (!layerInfo) { | |
| layerInfo = _drawOpCtx.layersInfoMap[layerId] = { | |
| recordLineCellCount: 0, | |
| recordLineTitle: document.createElement('span'), | |
| recordLineAvg: document.createElement('span'), | |
| recordLineContainer: document.createElement('div') | |
| }; | |
| layerInfo.stackLengthRecord = []; | |
| layerInfo.stackLengthSumInWindow = 0; | |
| layerInfo.stackLengthMax = 0; | |
| layerInfo.recordLineTitle.innerHTML = 'layer ' + layerId + ': '; | |
| layerInfo.recordLineTitle.className = 'print-canvas-layer-draw-operations-on-frame-record-line-title'; | |
| layerInfo.recordLineAvg.className = 'print-canvas-layer-draw-operations-on-frame-record-line-title'; | |
| layerInfo.recordLineContainer.className = 'print-canvas-layer-draw-operations-on-frame-record-line'; | |
| var titleLineDom = document.createElement('div'); | |
| titleLineDom.appendChild(layerInfo.recordLineTitle); | |
| titleLineDom.appendChild(layerInfo.recordLineAvg); | |
| _drawOpCtx.recordContainer.appendChild(titleLineDom); | |
| _drawOpCtx.recordContainer.appendChild(layerInfo.recordLineContainer); | |
| } | |
| var canvas = layer.dom; | |
| var ctx = canvas.getContext('2d'); | |
| var stackLength = getStackLength(ctx); | |
| var thisStackLength = stackLength; | |
| if (thisStackLength > layerInfo.stackLengthMax) { | |
| layerInfo.stackLengthMax = thisStackLength; | |
| } | |
| layerInfo.stackLengthRecord.push(thisStackLength); | |
| layerInfo.stackLengthSumInWindow += thisStackLength; | |
| if (layerInfo.stackLengthRecord.length > _drawOpCtx.CELL_MAX) { | |
| layerInfo.stackLengthSumInWindow -= layerInfo.stackLengthRecord[0]; | |
| layerInfo.stackLengthRecord.shift(); | |
| } | |
| var avgStackLength = (layerInfo.stackLengthSumInWindow / layerInfo.stackLengthRecord.length).toFixed(2); | |
| layerInfo.recordLineAvg.innerHTML = | |
| ' (avg: ' + avgStackLength | |
| + ' = ' + layerInfo.stackLengthSumInWindow + ' / ' + layerInfo.stackLengthRecord.length | |
| + ' max: ' + layerInfo.stackLengthMax + ')'; | |
| var cell; | |
| if (layerInfo.recordLineCellCount > _drawOpCtx.CELL_MAX) { | |
| cell = layerInfo.recordLineContainer.firstChild; | |
| } | |
| else { | |
| cell = document.createElement('span'); | |
| layerInfo.recordLineCellCount++; | |
| } | |
| cell.innerHTML = frameNumber + ':<span class="print-canvas-layer-draw-operations-on-frame-cmd-count">' + thisStackLength + '</span> '; | |
| layerInfo.recordLineContainer.appendChild(cell); | |
| if (ctx.stack) { | |
| _lastOps[layerId] = ctx.stack().slice(); | |
| } | |
| if (ctx.clear) { | |
| ctx.clear(); | |
| } | |
| } | |
| function getStackLength(ctx) { | |
| return ctx.stack ? ctx.stack().length : 0; | |
| } | |
| return _lastOps; | |
| }; // End of `printCanvasLayerDrawOperationsOnFrame` | |
| testHelper.resizable = function (chart, opt) { | |
| opt = opt || {}; | |
| var dom = chart.getDom(); | |
| var width = dom.clientWidth; | |
| var height = dom.clientHeight; | |
| function resize() { | |
| var newWidth = dom.clientWidth; | |
| var newHeight = dom.clientHeight; | |
| if (width !== newWidth || height !== newHeight) { | |
| chart.resize(); | |
| if (chart.__testHelper && chart.__testHelper.updateBoundingRects) { | |
| chart.__testHelper.updateBoundingRects(); | |
| } | |
| width = newWidth; | |
| height = newHeight; | |
| if (opt.onResize) { | |
| opt.onResize(); | |
| } | |
| } | |
| } | |
| if (window.attachEvent) { | |
| // Use builtin resize in IE | |
| window.attachEvent('onresize', resize); | |
| } | |
| else if (window.addEventListener) { | |
| window.addEventListener('resize', resize, false); | |
| } | |
| }; | |
| // Clean params specified by `cleanList` and seed a param specifid by `newVal` in URL. | |
| testHelper.setURLParam = function (cleanList, newVal) { | |
| var params = getParamListFromURL(); | |
| for (var i = params.length - 1; i >= 0; i--) { | |
| for (var j = 0; j < cleanList.length; j++) { | |
| if (params[i] === cleanList[j]) { | |
| params.splice(i, 1); | |
| } | |
| } | |
| } | |
| newVal && params.push(newVal); | |
| params.sort(); | |
| location.search = params.join('&'); | |
| }; | |
| // Whether has param `val` in URL. | |
| testHelper.hasURLParam = function (val) { | |
| var params = getParamListFromURL(); | |
| for (var i = params.length - 1; i >= 0; i--) { | |
| if (params[i] === val) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| }; | |
| // Nodejs `path.resolve`. | |
| testHelper.resolve = function () { | |
| var resolvedPath = ''; | |
| var resolvedAbsolute; | |
| for (var i = arguments.length - 1; i >= 0 && !resolvedAbsolute; i--) { | |
| var path = arguments[i]; | |
| if (path) { | |
| resolvedPath = path + '/' + resolvedPath; | |
| resolvedAbsolute = path[0] === '/'; | |
| } | |
| } | |
| if (!resolvedAbsolute) { | |
| throw new Error('At least one absolute path should be input.'); | |
| } | |
| // Normalize the path | |
| resolvedPath = normalizePathArray(resolvedPath.split('/'), false).join('/'); | |
| return '/' + resolvedPath; | |
| }; | |
| var encodeHTML = testHelper.encodeHTML = function (source) { | |
| return String(source) | |
| .replace(/&/g, '&') | |
| .replace(/</g, '<') | |
| .replace(/>/g, '>') | |
| .replace(/"/g, '"') | |
| .replace(/'/g, '''); | |
| }; | |
| var encodeJSObjectKey = function (source, quotationMark) { | |
| source = '' + source; | |
| if (!/^[a-zA-Z$_][a-zA-Z0-9$_]*$/.test(source)) { | |
| source = convertStringToJSLiteral(source, quotationMark); | |
| } | |
| return source; | |
| }; | |
| var convertStringToJSLiteral = function (str, quotationMark) { | |
| // assert(getType(str) === 'string'); | |
| // assert(quotationMark === '"' || quotationMark === "'"); | |
| str = JSON.stringify(str); // escapse \n\r or others. | |
| if (quotationMark === "'") { | |
| str = "'" + str.slice(1, str.length - 1).replace(/'/g, "\\'") + "'"; | |
| } | |
| return str; | |
| } | |
| /** | |
| * @usage | |
| * var result = retrieveValue(val, defaultVal); | |
| * var result = retrieveValue(val1, val2, defaultVal); | |
| */ | |
| var retrieveValue = testHelper.retrieveValue = function () { | |
| for (var i = 0, len = arguments.length; i < len; i++) { | |
| var val = arguments[i]; | |
| if (val != null) { | |
| return val; | |
| } | |
| } | |
| }; | |
| /** | |
| * @public | |
| * @return {string} Current url dir. | |
| */ | |
| testHelper.dir = function () { | |
| return location.origin + testHelper.resolve(location.pathname, '..'); | |
| }; | |
| /** | |
| * Not accurate. | |
| * @param {*} type | |
| * @return {string} 'function', 'array', 'typedArray', 'regexp', | |
| * 'date', 'object', 'boolean', 'number', 'string' | |
| */ | |
| var getType = testHelper.getType = function (value) { | |
| var type = typeof value; | |
| var typeStr = objToString.call(value); | |
| return !!TYPED_ARRAY[objToString.call(value)] | |
| ? 'typedArray' | |
| : typeof value === 'function' | |
| ? 'function' | |
| : typeStr === '[object Array]' | |
| ? 'array' | |
| : typeStr === '[object Number]' | |
| ? 'number' | |
| : typeStr === '[object Boolean]' | |
| ? 'boolean' | |
| : typeStr === '[object String]' | |
| ? 'string' | |
| : typeStr === '[object RegExp]' | |
| ? 'regexp' | |
| : typeStr === '[object Date]' | |
| ? 'date' | |
| : !!value && type === 'object' | |
| ? 'object' | |
| : null; | |
| }; | |
| function containsDOMElement(parent, child, includeSelf) { | |
| if (!includeSelf && child) { | |
| child = child.parentNode; | |
| } | |
| while (child) { | |
| if (child === parent) { | |
| return true; | |
| } | |
| child = child.parentNode; | |
| } | |
| return false; | |
| } | |
| function cssHasClass(el, className) { | |
| return cssNormalizeClass(el.className).indexOf(className) >= 0; | |
| } | |
| function cssAddClass(el, className) { | |
| if (!cssHasClass(el, className)) { | |
| el.className = cssNormalizeClass(el.className + ' ' + className); | |
| } | |
| } | |
| function cssRemoveClass(el, className) { | |
| var classes = (' ' + el.className + ' ').replace(' ' + className + ' ', function () { | |
| return ' '; // Use a cb to String.prototype.replace to avoid `$` issue. | |
| }); | |
| el.className = cssNormalizeClass(classes); | |
| } | |
| function cssNormalizeClass(classes) { | |
| return classes.replace(/^\s+|\s+$/g, '').replace(/\s+/g, ' '); | |
| } | |
| /** | |
| * JSON.stringify(obj, null, 2) will vertically layout array, which takes too much space. | |
| * Can print like: | |
| * [ | |
| * {name: 'xxx', value: 123}, | |
| * {name: 'xxx', value: 123}, | |
| * {name: 'xxx', value: 123} | |
| * ] | |
| * { | |
| * arr: [33, 44, 55], | |
| * str: 'xxx' | |
| * } | |
| * | |
| * @param {*} object | |
| * @param {opt|string} [opt] If string, means key. | |
| * @param {string} [opt.key=''] Top level key, if given, print like: 'someKey: [asdf]' | |
| * @param {number} [opt.lineBreakMaxColumn=80] If the content in a single line is greater than | |
| * `maxColumn` (indent is not included), line break. | |
| * @param {boolean} [opt.objectLineBreak=undefined] Whether to line break. undefined/null means auto. | |
| * @param {boolean} [opt.arrayLineBreak=undefined] Whether to line break. undefined/null means auto. | |
| * @param {string} [opt.indent=4] | |
| * @param {string} [opt.marginLeft=0] Spaces number for margin left of the entire text. | |
| * @param {string} [opt.lineBreak='\n'] | |
| * @param {string} [opt.quotationMark="'"] "'" or '"'. | |
| * @return {string} | |
| */ | |
| var printObject = testHelper.printObject = function (obj, opt) { | |
| opt = typeof opt === 'string' | |
| ? {key: opt} | |
| : (opt || {}); | |
| var indent = opt.indent != null ? opt.indent : 4; | |
| var lineBreak = opt.lineBreak != null ? opt.lineBreak : '\n'; | |
| var quotationMark = ({'"': '"', "'": "'"})[opt.quotationMark] || "'"; | |
| var marginLeft = opt.marginLeft || 0; | |
| var lineBreakMaxColumn = opt.lineBreakMaxColumn || 80; | |
| var forceObjectLineBreak = opt.objectLineBreak === true || opt.objectLineBreak === false; | |
| var forceArrayLineBreak = opt.arrayLineBreak === true || opt.arrayLineBreak === false; | |
| return (new Array(marginLeft + 1)).join(' ') + doPrint(obj, opt.key, 0).str; | |
| function doPrint(obj, key, depth) { | |
| var codeIndent = (new Array(depth * indent + marginLeft + 1)).join(' '); | |
| var subCodeIndent = (new Array((depth + 1) * indent + marginLeft + 1)).join(' '); | |
| var hasLineBreak = false; | |
| // [ | |
| // 11, 22, 33, 44, 55, 66, // This is a partial break. | |
| // 77, 88, 99 | |
| // ] | |
| var preventParentArrayPartiallyBreak = false; | |
| var preStr = ''; | |
| if (key != null) { | |
| preStr += encodeJSObjectKey(key, quotationMark) + ': '; | |
| } | |
| var str; | |
| var objType = getType(obj); | |
| switch (objType) { | |
| case 'function': | |
| hasLineBreak = true; | |
| preventParentArrayPartiallyBreak = true; | |
| var fnStr = obj.toString(); | |
| var isMethodShorthand = key != null && isMethodShorthandNotAccurate(fnStr, obj.name, key); | |
| str = (isMethodShorthand ? '' : preStr) + fnStr; | |
| break; | |
| case 'regexp': | |
| case 'date': | |
| str = preStr + quotationMark + obj + quotationMark; | |
| break; | |
| case 'array': | |
| case 'typedArray': | |
| if (forceArrayLineBreak) { | |
| hasLineBreak = !!opt.arrayLineBreak; | |
| } | |
| // If no break line in array, print in single line, like [12, 23, 34]. | |
| // else, each item takes a line. | |
| var childBuilder = []; | |
| var maxColumnWithoutLineBreak = preStr.length; | |
| var canPartiallyBreak = true; | |
| for (var i = 0, len = obj.length; i < len; i++) { | |
| var subResult = doPrint(obj[i], null, depth + 1); | |
| childBuilder.push(subResult.str); | |
| if (subResult.hasLineBreak) { | |
| hasLineBreak = true; | |
| } | |
| else { | |
| maxColumnWithoutLineBreak += subResult.str.length + 2; // `2` is ', '.length | |
| } | |
| if (subResult.preventParentArrayPartiallyBreak) { | |
| preventParentArrayPartiallyBreak = true; | |
| canPartiallyBreak = false | |
| } | |
| } | |
| if (obj.length > 3) { | |
| // `3` is an arbitrary value, considering a path array: | |
| // [ | |
| // [1,2], [3,4], [5,6], | |
| // [7,8], [9,10] | |
| // ] | |
| preventParentArrayPartiallyBreak = true; | |
| } | |
| if (!forceObjectLineBreak && maxColumnWithoutLineBreak > lineBreakMaxColumn) { | |
| hasLineBreak = true; | |
| } | |
| var tail = hasLineBreak ? lineBreak : ''; | |
| var subPre = hasLineBreak ? subCodeIndent : ''; | |
| var endPre = hasLineBreak ? codeIndent : ''; | |
| var delimiterInline = ', '; | |
| var delimiterBreak = ',' + lineBreak + subCodeIndent; | |
| if (!childBuilder.length) { | |
| str = preStr + '[]'; | |
| } | |
| else { | |
| var subContentStr = ''; | |
| var subContentMaxColumn = 0; | |
| if (canPartiallyBreak && hasLineBreak) { | |
| for (var idx = 0; idx < childBuilder.length; idx++) { | |
| var childStr = childBuilder[idx]; | |
| subContentMaxColumn += childStr.length + delimiterInline.length; | |
| if (idx === childBuilder.length - 1) { | |
| subContentStr += childStr; | |
| } | |
| else if (subContentMaxColumn > lineBreakMaxColumn) { | |
| subContentStr += childStr + delimiterBreak; | |
| subContentMaxColumn = 0; | |
| } | |
| else { | |
| subContentStr += childStr + delimiterInline; | |
| } | |
| } | |
| } | |
| else { | |
| subContentStr = childBuilder.join(hasLineBreak ? delimiterBreak : delimiterInline); | |
| } | |
| str = '' | |
| + preStr + '[' + tail | |
| + subPre + subContentStr + tail | |
| + endPre + ']'; | |
| } | |
| break; | |
| case 'object': | |
| if (forceObjectLineBreak) { | |
| hasLineBreak = !!opt.objectLineBreak; | |
| } | |
| var childBuilder = []; | |
| var maxColumnWithoutLineBreak = preStr.length; | |
| var keyCount = 0; | |
| for (var i in obj) { | |
| if (obj.hasOwnProperty(i)) { | |
| keyCount++; | |
| var subResult = doPrint(obj[i], i, depth + 1); | |
| childBuilder.push(subResult.str); | |
| if (subResult.hasLineBreak) { | |
| hasLineBreak = true; | |
| } | |
| else { | |
| maxColumnWithoutLineBreak += subResult.str.length + 2; // `2` is ', '.length | |
| } | |
| if (subResult.preventParentArrayPartiallyBreak) { | |
| preventParentArrayPartiallyBreak = true; | |
| } | |
| } | |
| } | |
| if (keyCount > 1) { | |
| // `3` is an arbitrary value, considering case like: | |
| // [ | |
| // {name: 'xx'}, {name: 'yy'}, {name: 'zz'}, | |
| // {name: 'aa'}, {name: 'bb'} | |
| // ] | |
| preventParentArrayPartiallyBreak = true; | |
| } | |
| if (!forceObjectLineBreak && maxColumnWithoutLineBreak > lineBreakMaxColumn) { | |
| hasLineBreak = true; | |
| } | |
| if (!childBuilder.length) { | |
| str = preStr + '{}'; | |
| } | |
| else { | |
| str = '' | |
| + preStr + '{' + (hasLineBreak ? lineBreak : '') | |
| + (hasLineBreak ? subCodeIndent : '') | |
| + childBuilder.join(',' + (hasLineBreak ? lineBreak + subCodeIndent: ' ')) | |
| + (hasLineBreak ? lineBreak: '') | |
| + (hasLineBreak ? codeIndent : '') + '}'; | |
| } | |
| break; | |
| case 'boolean': | |
| case 'number': | |
| str = preStr + obj + ''; | |
| break; | |
| case 'string': | |
| str = preStr + convertStringToJSLiteral(obj, quotationMark); | |
| break; | |
| default: | |
| str = preStr + obj + ''; | |
| preventParentArrayPartiallyBreak = true; | |
| } | |
| return { | |
| str: str, | |
| hasLineBreak: hasLineBreak, | |
| isMethodShorthand: isMethodShorthand, | |
| preventParentArrayPartiallyBreak: preventParentArrayPartiallyBreak | |
| }; | |
| } | |
| /** | |
| * Simple implementation for detecting method shorthand, such as, | |
| * ({abc() { return 1; }}).abc is a method shorthand and needs to | |
| * be serialized as `{abc() { return 1; }}` rather than `{abc: abc() { return 1; }}`. | |
| * Those cases can be detected: | |
| * ({abc() { console.log('=>'); return 1; }}).abc expected: IS_SHORTHAND | |
| * ({abc(x, y = 5) { return 1; }}).abc expected: IS_SHORTHAND | |
| * ({$ab_c() { return 1; }}).$ab_c expected: IS_SHORTHAND | |
| * ({*abc() { return 1; }}).abc expected: IS_SHORTHAND | |
| * ({* abc() { return 1; }}).abc expected: IS_SHORTHAND | |
| * ({async abc() { return 1; }}).abc expected: IS_SHORTHAND | |
| * ({*abc() { yield 1; }}).abc expected: IS_SHORTHAND | |
| * ({abc(x, y) { return x + y; }}).abc expected: IS_SHORTHAND | |
| * ({abc: function abc() { return 1; }}).abc expected: NOT_SHORTHAND | |
| * ({abc: function def() { return 1; }}).abc expected: NOT_SHORTHAND | |
| * ({abc: function() { return 1; }}).abc expected: NOT_SHORTHAND | |
| * ({abc: function* () { return 1; }}).abc expected: NOT_SHORTHAND | |
| * ({abc: function (aa, bb) { return 1; }}).abc expected: NOT_SHORTHAND | |
| * ({abc: function (aa, bb = 5) { return 1; }}).abc expected: NOT_SHORTHAND | |
| * ({abc: async () => { return 1; }}).abc expected: NOT_SHORTHAND | |
| * ({abc: () => { return 1; }}).abc expected: NOT_SHORTHAND | |
| * ({abc: (aa, bb = 5) => { return 1; }}).abc expected: NOT_SHORTHAND | |
| * FIXME: fail at some rare cases, such as: | |
| * Literal string involved, like: | |
| * ({"ab-() ' =>c"() { return 1; }})["ab-() ' =>c"] expected: IS_SHORTHAND | |
| * ({async "ab-c"() { return 1; }})["ab-c"] expected: IS_SHORTHAND | |
| * Computed property name involved, like: | |
| * ({[some]() { return 1; }})[some] expected: IS_SHORTHAND | |
| */ | |
| function isMethodShorthandNotAccurate(fnStr, fnName, objKey) { | |
| // Assert fnStr, fnName, objKey is a string. | |
| if (fnName !== objKey) { | |
| return false; | |
| } | |
| var matched = fnStr.match(/^\s*(async\s+)?(function\s*)?(\*\s*)?([a-zA-Z$_][a-zA-Z0-9$_]*)?\s*\(/); | |
| if (!matched) { | |
| return false; | |
| } | |
| if (matched[2]) { // match 'function' | |
| return false; | |
| } | |
| // May enhanced by /(['"])(?:(?=(\\?))\2.)*?\1/; to match literal string, | |
| // such as "ab-c", "a\nc". But this simple impl does not cover it. | |
| if (!matched[4] || matched[4] !== objKey) { // match "maybe function name" | |
| return false; | |
| } | |
| return true; | |
| } | |
| }; | |
| /** | |
| * Usage: | |
| * ```js | |
| * // Print all elements that has `style.text`: | |
| * var str = testHelper.stringifyElements(chart, { | |
| * attr: ['z', 'z2', 'style.text', 'style.fill', 'style.stroke'], | |
| * filter: el => el.style && el.style.text | |
| * }); | |
| * ``` | |
| * | |
| * @param {EChart} chart | |
| * @param {Object} [opt] | |
| * @param {string|Array.<string>} [opt.attr] Only print the given attrName; | |
| * For example: 'z2' or ['z2', 'style.fill', 'style.stroke'] | |
| * @param {function} [opt.filter] print a subtree only if any satisfied node exists. | |
| * param: el, return: boolean | |
| */ | |
| var stringifyElements = testHelper.stringifyElements = function (chart, opt) { | |
| if (!chart) { | |
| return; | |
| } | |
| opt = opt || {}; | |
| var attrNameList = opt.attr; | |
| if (getType(attrNameList) !== 'array') { | |
| attrNameList = attrNameList ? [attrNameList] : []; | |
| } | |
| var zr = chart.getZr(); | |
| var roots = zr.storage.getRoots(); | |
| var plainRoots = []; | |
| retrieve(roots, plainRoots); | |
| var elsStr = printObject(plainRoots, {indent: 2}); | |
| return elsStr; | |
| // Only retrieve the value of the given attrName. | |
| function retrieve(elList, plainNodes) { | |
| var anySatisfied = false; | |
| for (var i = 0; i < elList.length; i++) { | |
| var el = elList[i]; | |
| var thisElSatisfied = !opt.filter || opt.filter(el); | |
| var plainNode = {}; | |
| copyElment(plainNode, el); | |
| var textContent = el.getTextContent(); | |
| if (textContent) { | |
| plainNode.textContent = {}; | |
| copyElment(plainNode.textContent, textContent); | |
| } | |
| var thisSubAnySatisfied = false; | |
| if (el.isGroup) { | |
| plainNode.children = []; | |
| thisSubAnySatisfied = retrieve(el.childrenRef(), plainNode.children); | |
| } | |
| if (thisElSatisfied || thisSubAnySatisfied) { | |
| plainNodes.push(plainNode); | |
| anySatisfied = true; | |
| } | |
| } | |
| return anySatisfied; | |
| } | |
| function copyElment(plainNode, el) { | |
| for (var i = 0; i < attrNameList.length; i++) { | |
| var attrName = attrNameList[i]; | |
| var attrParts = attrName.split('.'); | |
| var partsLen = attrParts.length; | |
| if (!partsLen) { | |
| continue; | |
| } | |
| var elInner = el; | |
| var plainInner = plainNode; | |
| for (var j = 0; j < partsLen - 1 && elInner; j++) { | |
| var attr = attrParts[j]; | |
| elInner = el[attr]; | |
| if (elInner) { | |
| plainInner = plainInner[attr] || (plainInner[attr] = {}); | |
| } | |
| } | |
| var attr = attrParts[partsLen - 1]; | |
| if (elInner && elInner.hasOwnProperty(attr)) { | |
| plainInner[attr] = elInner[attr]; | |
| } | |
| } | |
| } | |
| }; | |
| /** | |
| * Usage: | |
| * ```js | |
| * // Print all elements that has `style.text`: | |
| * testHelper.printElements(chart, { | |
| * attr: ['z', 'z2', 'style.text', 'style.fill', 'style.stroke'], | |
| * filter: el => el.style && el.style.text | |
| * }); | |
| * ``` | |
| * | |
| * @see `stringifyElements`. | |
| */ | |
| var printElements = testHelper.printElements = function (chart, opt) { | |
| var elsStr = testHelper.stringifyElements(chart, opt); | |
| console.log(elsStr); | |
| }; | |
| /** | |
| * Usage: | |
| * ```js | |
| * // Print all elements that has `style.text`: | |
| * testHelper.retrieveElements(chart, { | |
| * filter: el => el.style && el.style.text | |
| * }); | |
| * ``` | |
| * | |
| * @param {EChart} chart | |
| * @param {Object} [opt] | |
| * @param {function} [opt.filter] print a subtree only if any satisfied node exists. | |
| * param: el, return: boolean | |
| * @return {Array.<Element>} | |
| */ | |
| var retrieveElements = testHelper.retrieveElements = function (chart, opt) { | |
| if (!chart) { | |
| return; | |
| } | |
| opt = opt || {}; | |
| var attrNameList = opt.attr; | |
| if (getType(attrNameList) !== 'array') { | |
| attrNameList = attrNameList ? [attrNameList] : []; | |
| } | |
| var zr = chart.getZr(); | |
| var roots = zr.storage.getRoots(); | |
| var result = []; | |
| retrieve(roots); | |
| function retrieve(elList) { | |
| for (var i = 0; i < elList.length; i++) { | |
| var el = elList[i]; | |
| if (!opt.filter || opt.filter(el)) { | |
| result.push(el); | |
| } | |
| if (el.isGroup) { | |
| retrieve(el.childrenRef()); | |
| } | |
| } | |
| } | |
| return result; | |
| }; | |
| // opt: {record: JSON, width: number, height: number} | |
| testHelper.reproduceCanteen = function (opt) { | |
| var canvas = document.createElement('canvas'); | |
| canvas.style.width = opt.width + 'px'; | |
| canvas.style.height = opt.height + 'px'; | |
| var dpr = Math.max(window.devicePixelRatio || 1, 1); | |
| canvas.width = opt.width * dpr; | |
| canvas.height = opt.height * dpr; | |
| var ctx = canvas.getContext('2d'); | |
| var record = opt.record; | |
| for (var i = 0; i < record.length; i++) { | |
| var line = record[i]; | |
| if (line.attr) { | |
| if (!line.hasOwnProperty('val')) { | |
| alertIllegal(line); | |
| } | |
| ctx[line.attr] = line.val; | |
| } | |
| else if (line.method) { | |
| if (!line.hasOwnProperty('arguments')) { | |
| alertIllegal(line); | |
| } | |
| ctx[line.method].apply(ctx, line.arguments); | |
| } | |
| else { | |
| alertIllegal(line); | |
| } | |
| } | |
| function alertIllegal(line) { | |
| throw new Error('Illegal line: ' + JSON.stringify(line)); | |
| } | |
| document.body.appendChild(canvas); | |
| }; | |
| function initDataTables(opt, dataTableContainer) { | |
| var dataTables = opt.dataTables; | |
| if (!dataTables && opt.dataTable) { | |
| dataTables = [opt.dataTable]; | |
| } | |
| if (dataTables) { | |
| var tableHTML = []; | |
| for (var i = 0; i < dataTables.length; i++) { | |
| tableHTML.push(createDataTableHTML(dataTables[i], opt)); | |
| } | |
| dataTableContainer.innerHTML = tableHTML.join(''); | |
| } | |
| } | |
| function createDataTableHTML(data, opt) { | |
| var sourceFormat = detectSourceFormat(data); | |
| var dataTableLimit = opt.dataTableLimit || DEFAULT_DATA_TABLE_LIMIT; | |
| if (!sourceFormat) { | |
| return ''; | |
| } | |
| var html = ['<table><tbody>']; | |
| if (sourceFormat === 'arrayRows') { | |
| for (var i = 0; i < data.length && i <= dataTableLimit; i++) { | |
| var line = data[i]; | |
| var htmlLine = ['<tr>']; | |
| for (var j = 0; j < line.length; j++) { | |
| var val = i === dataTableLimit ? '...' : line[j]; | |
| htmlLine.push('<td>' + encodeHTML(val) + '</td>'); | |
| } | |
| htmlLine.push('</tr>'); | |
| html.push(htmlLine.join('')); | |
| } | |
| } | |
| else if (sourceFormat === 'objectRows') { | |
| for (var i = 0; i < data.length && i <= dataTableLimit; i++) { | |
| var line = data[i]; | |
| var htmlLine = ['<tr>']; | |
| for (var key in line) { | |
| if (line.hasOwnProperty(key)) { | |
| var keyText = i === dataTableLimit ? '...' : key; | |
| htmlLine.push('<td class="test-data-table-key">' + encodeHTML(keyText) + '</td>'); | |
| var val = i === dataTableLimit ? '...' : line[key]; | |
| htmlLine.push('<td>' + encodeHTML(val) + '</td>'); | |
| } | |
| } | |
| htmlLine.push('</tr>'); | |
| html.push(htmlLine.join('')); | |
| } | |
| } | |
| else if (sourceFormat === 'keyedColumns') { | |
| for (var key in data) { | |
| var htmlLine = ['<tr>']; | |
| htmlLine.push('<td class="test-data-table-key">' + encodeHTML(key) + '</td>'); | |
| if (data.hasOwnProperty(key)) { | |
| var col = data[key] || []; | |
| for (var i = 0; i < col.length && i <= dataTableLimit; i++) { | |
| var val = i === dataTableLimit ? '...' : col[i]; | |
| htmlLine.push('<td>' + encodeHTML(val) + '</td>'); | |
| } | |
| } | |
| htmlLine.push('</tr>'); | |
| html.push(htmlLine.join('')); | |
| } | |
| } | |
| html.push('</tbody></table>'); | |
| return html.join(''); | |
| } | |
| function detectSourceFormat(data) { | |
| if (data.length) { | |
| for (var i = 0, len = data.length; i < len; i++) { | |
| var item = data[i]; | |
| if (item == null) { | |
| continue; | |
| } | |
| else if (item.length) { | |
| return 'arrayRows'; | |
| } | |
| else if (typeof data === 'object') { | |
| return 'objectRows'; | |
| } | |
| } | |
| } | |
| else if (typeof data === 'object') { | |
| return 'keyedColumns'; | |
| } | |
| } | |
| function createObjectHTML(obj, key) { | |
| var html = isObject(obj) | |
| ? encodeHTML(printObject(obj, key)) | |
| : obj | |
| ? obj.toString() | |
| : ''; | |
| return [ | |
| '<pre class="test-print-object">', | |
| html, | |
| '</pre>' | |
| ].join(''); | |
| } | |
| var getDom = testHelper.getDom = function (domOrId) { | |
| return getType(domOrId) === 'string' ? document.getElementById(domOrId) : domOrId; | |
| } | |
| // resolves . and .. elements in a path array with directory names there | |
| // must be no slashes or device names (c:\) in the array | |
| // (so also no leading and trailing slashes - it does not distinguish | |
| // relative and absolute paths) | |
| function normalizePathArray(parts, allowAboveRoot) { | |
| var res = []; | |
| for (var i = 0; i < parts.length; i++) { | |
| var p = parts[i]; | |
| // ignore empty parts | |
| if (!p || p === '.') { | |
| continue; | |
| } | |
| if (p === '..') { | |
| if (res.length && res[res.length - 1] !== '..') { | |
| res.pop(); | |
| } else if (allowAboveRoot) { | |
| res.push('..'); | |
| } | |
| } else { | |
| res.push(p); | |
| } | |
| } | |
| return res; | |
| } | |
| function getParamListFromURL() { | |
| var params = location.search.replace('?', ''); | |
| return params ? params.split('&') : []; | |
| } | |
| function isObject(value) { | |
| // Avoid a V8 JIT bug in Chrome 19-20. | |
| // See https://code.google.com/p/v8/issues/detail?id=2291 for more details. | |
| var type = typeof value; | |
| return type === 'function' || (!!value && type === 'object'); | |
| } | |
| function arrayIndexOf(arr, value) { | |
| if (arr.indexOf) { | |
| return arr.indexOf(value); | |
| } | |
| for (var i = 0; i < arr.length; i++) { | |
| if (arr[i] === value) { | |
| return i; | |
| } | |
| } | |
| return -1; | |
| } | |
| var assert = testHelper.assert = function (cond, msg) { | |
| if (!cond) { | |
| throw new Error(msg || 'Assertion failed.'); | |
| } | |
| } | |
| function makeFlexibleNames(dashedNames) { | |
| var nameMap = {}; | |
| for (var i = 0; i < dashedNames.length; i++) { | |
| var name = dashedNames[i]; | |
| var tmpNames = []; | |
| tmpNames.push(name); | |
| tmpNames.push(name.replace(/-/g, '')); | |
| tmpNames.push(name.replace(/-/g, '_')); | |
| tmpNames.push(name.replace(/-([a-zA-Z0-9])/g, function (_, wf) { | |
| return wf.toUpperCase(); | |
| })); | |
| for (var j = 0; j < tmpNames.length; j++) { | |
| nameMap[tmpNames[j]] = 1; | |
| nameMap[tmpNames[j].toUpperCase()] = 1; | |
| nameMap[tmpNames[j].toLowerCase()] = 1; | |
| } | |
| } | |
| var names = []; | |
| for (var name in nameMap) { | |
| if (nameMap.hasOwnProperty(name)) { | |
| names.push(name); | |
| } | |
| } | |
| return names; | |
| } | |
| /** | |
| * Copied from src/util/number.ts | |
| */ | |
| function getPrecision(val) { | |
| val = +val; | |
| if (isNaN(val)) { | |
| return 0; | |
| } | |
| // It is much faster than methods converting number to string as follows | |
| // let tmp = val.toString(); | |
| // return tmp.length - 1 - tmp.indexOf('.'); | |
| // especially when precision is low | |
| // Notice: | |
| // (1) If the loop count is over about 20, it is slower than `getPrecisionSafe`. | |
| // (see https://jsbench.me/2vkpcekkvw/1) | |
| // (2) If the val is less than for example 1e-15, the result may be incorrect. | |
| // (see test/ut/spec/util/number.test.ts `getPrecision_equal_random`) | |
| if (val > 1e-14) { | |
| var e = 1; | |
| for (var i = 0; i < 15; i++, e *= 10) { | |
| if (Math.round(val * e) / e === val) { | |
| return i; | |
| } | |
| } | |
| } | |
| return getPrecisionSafe(val); | |
| } | |
| /** | |
| * Copied from src/util/number.ts | |
| * Get precision with slow but safe method | |
| */ | |
| function getPrecisionSafe(val) { | |
| // toLowerCase for: '3.4E-12' | |
| var str = val.toString().toLowerCase(); | |
| // Consider scientific notation: '3.4e-12' '3.4e+12' | |
| var eIndex = str.indexOf('e'); | |
| var exp = eIndex > 0 ? +str.slice(eIndex + 1) : 0; | |
| var significandPartLen = eIndex > 0 ? eIndex : str.length; | |
| var dotIndex = str.indexOf('.'); | |
| var decimalPartLen = dotIndex < 0 ? 0 : significandPartLen - 1 - dotIndex; | |
| return Math.max(0, decimalPartLen - exp); | |
| } | |
| /** | |
| * Copied from src/util/number.ts | |
| */ | |
| function round(x, precision, returnStr) { | |
| if (precision == null) { | |
| precision = 10; | |
| } | |
| // Avoid range error | |
| precision = Math.min(Math.max(0, precision), ROUND_SUPPORTED_PRECISION_MAX); | |
| // PENDING: 1.005.toFixed(2) is '1.00' rather than '1.01' | |
| x = (+x).toFixed(precision); | |
| return (returnStr ? x : +x); | |
| } | |
| // Although chrome already enlarge this number to 100 for `toFixed`, but | |
| // we sill follow the spec for compatibility. | |
| var ROUND_SUPPORTED_PRECISION_MAX = 20; | |
| function objectNoOtherNotNullUndefinedPropExcept(obj, exceptProps) { | |
| if (!obj) { | |
| return false; | |
| } | |
| for (var key in obj) { | |
| if (obj.hasOwnProperty(key) && arrayIndexOf(exceptProps, key) < 0 && obj[key] != null) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| var copyToClipboard = function (text) { | |
| if (typeof navigator === 'undefined' || !navigator.clipboard || !navigator.clipboard.writeText) { | |
| console.error('[clipboard] Can not copy to clipboard.'); | |
| return; | |
| } | |
| return navigator.clipboard.writeText(text).then(function () { | |
| console.log('[clipboard] Text copied to clipboard.'); | |
| }).catch(function (err) { | |
| console.error('[clipboard] Failed to copy text: ', err); // Just print for easy to use. | |
| return err; | |
| }); | |
| }; | |
| /** | |
| * A shortcut for both stringify and copy to clipboard. | |
| * | |
| * @param {any} val Any val to stringify and copy to clipboard. | |
| * @param {Object?} printObjectOpt Optional. | |
| */ | |
| testHelper.clipboard = function (val, printObjectOpt) { | |
| var literal = testHelper.printObject(val, printObjectOpt); | |
| if (document.hasFocus()) { | |
| copyToClipboard(literal); | |
| } | |
| else { | |
| // Handle the error: | |
| // NotAllowedError: Failed to execute 'writeText' on 'Clipboard': Document is not focused. | |
| ensureClipboardButton(); | |
| updateClipboardButton(literal) | |
| console.log( | |
| '⚠️ [clipboard] Please click the new button that appears on the top-left corner of the screen' | |
| + ' to copy to clipboard.' | |
| ); | |
| } | |
| function updateClipboardButton(text) { | |
| var button = __tmpClipboardButttonWrapper.button; | |
| button.innerHTML = 'Click me to copy to clipboard'; | |
| button.style.display = 'block'; | |
| __tmpClipboardButttonWrapper.text = text; | |
| } | |
| function ensureClipboardButton() { | |
| var button = __tmpClipboardButttonWrapper.button; | |
| if (button != null) { | |
| return; | |
| } | |
| __tmpClipboardButttonWrapper.button = button = document.createElement('div'); | |
| button.style.cssText = [ | |
| 'height: 80px;', | |
| 'line-height: 80px;', | |
| 'padding: 10px 20px;', | |
| 'margin: 5px;', | |
| 'text-align: center;', | |
| 'position: fixed;', | |
| 'top: 10px;', | |
| 'left: 10px;', | |
| 'z-index: 9999;', | |
| 'cursor: pointer;', | |
| 'color: #fff;', | |
| 'background-color: #333;', | |
| 'border: 2px solid #eee;', | |
| 'border-radius: 5px;', | |
| 'font-size: 18px;', | |
| 'font-weight: bold;', | |
| 'font-family: sans-serif;', | |
| 'box-shadow: 0 4px 10px rgba(0, 0, 0, 0.8);' | |
| ].join(''); | |
| document.body.appendChild(button); | |
| button.addEventListener('click', function () { | |
| copyToClipboard(__tmpClipboardButttonWrapper.text).then(function (err) { | |
| if (!err) { | |
| button.style.display = 'none'; | |
| } | |
| else { | |
| button.innerHTML = 'error, see console log.'; | |
| } | |
| }); | |
| }); | |
| } | |
| // Do not return the text, because it may be too long for a console.log. | |
| }; | |
| var __tmpClipboardButttonWrapper = {}; | |
| // It may be changed by test case changing. Do not use it as a persistent id. | |
| var _idBase = 1; | |
| function generateNonPersistentId(prefix) { | |
| return (prefix || '') + '' + (_idBase++); | |
| } | |
| function VideoRecorder(chart) { | |
| this.start = startRecording; | |
| this.stop = stopRecording; | |
| var recorder = null; | |
| var oldRefreshImmediately = chart.getZr().refreshImmediately; | |
| function startRecording() { | |
| // Normal resolution or high resolution? | |
| var compositeCanvas = document.createElement('canvas'); | |
| var width = chart.getWidth(); | |
| var height = chart.getHeight(); | |
| compositeCanvas.width = width; | |
| compositeCanvas.height = height; | |
| var compositeCtx = compositeCanvas.getContext('2d'); | |
| chart.getZr().refreshImmediately = function () { | |
| var ret = oldRefreshImmediately.apply(this, arguments); | |
| var canvasList = chart.getDom().querySelectorAll('canvas'); | |
| compositeCtx.fillStyle = '#fff'; | |
| compositeCtx.fillRect(0, 0, width, height); | |
| for (var i = 0; i < canvasList.length; i++) { | |
| compositeCtx.drawImage(canvasList[i], 0, 0, width, height); | |
| } | |
| return ret; | |
| } | |
| var stream = compositeCanvas.captureStream(25); | |
| recorder = new MediaRecorder(stream, { mimeType: 'video/webm' }); | |
| var videoData = []; | |
| recorder.ondataavailable = function (event) { | |
| if (event.data && event.data.size) { | |
| videoData.push(event.data); | |
| } | |
| }; | |
| recorder.onstop = function () { | |
| var url = URL.createObjectURL(new Blob(videoData, { type: 'video/webm' })); | |
| var a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'recording.webm'; | |
| a.click(); | |
| setTimeout(function () { | |
| window.URL.revokeObjectURL(url); | |
| }, 100); | |
| }; | |
| recorder.start(); | |
| } | |
| function stopRecording() { | |
| if (recorder) { | |
| chart.getZr().refreshImmediately = oldRefreshImmediately; | |
| recorder.stop(); | |
| } | |
| } | |
| } | |
| /** | |
| * Print layer info to chart internal zrender. (For visual testing). | |
| */ | |
| // testHelper.printZRCanvasPainterLayersInfoForTest = function (echarts, chart) { | |
| // var zr = chart.getZr(); | |
| // start(); | |
| // function retrieveLayerInfo() { | |
| // // CAVEAT: Accessing internal data structure, may change. | |
| // // See `CanvasPainterInternal` | |
| // var painter = zr.painter; | |
| // var painterInternal = painter._i; | |
| // var layerStackInfo = echarts.util.clone(painterInternal.layerStack); | |
| // // Respect to the original content of `painterInternal.layers`. | |
| // var layersMapInfo = {}; | |
| // for (var ii in painterInternal.layers) { | |
| // if (painterInternal.layers.hasOwnProperty(ii)) { | |
| // layersMapInfo[ii] = {}; | |
| // var layersPerZLevel2 = painterInternal.layers[ii]; | |
| // for (var jj in layersPerZLevel2) { | |
| // if (layersPerZLevel2.hasOwnProperty(jj)) { | |
| // var layer = layersPerZLevel2[jj]; | |
| // // CAVEAT: Accessing internal data structure, may change. | |
| // var cursorStack = layer.__cursorStack; | |
| // var cursors = layer.__cursors; | |
| // var cursorsInfo = {}; | |
| // cursors.each(function (cursor, key) { | |
| // cursorsInfo[key] = echarts.util.clone(cursor); | |
| // }); | |
| // layersMapInfo[ii][jj] = { | |
| // layerId: layer.id, | |
| // cursorStack: echarts.util.clone(cursorStack), | |
| // cursors: cursorsInfo | |
| // }; | |
| // } | |
| // } | |
| // } | |
| // } | |
| // var hoverLayerInfo = painterInternal.hoverLayer | |
| // ? {layerId: painterInternal.hoverLayer.id} | |
| // : null; | |
| // return { | |
| // painterInternal: painterInternal, | |
| // layersMapInfo: layersMapInfo, | |
| // hoverLayerInfo: hoverLayerInfo, | |
| // layerStackInfo: layerStackInfo, | |
| // }; | |
| // } | |
| // function ensureResultDOM() { | |
| // var hostDOMEl = chart.getDom(); | |
| // var resultDom = chart.__zrLayerInfo; | |
| // if (!resultDom) { | |
| // resultDom = chart.__zrLayerInfo = document.createElement('pre'); | |
| // hostDOMEl.appendChild(resultDom); | |
| // resultDom.style.cssText = [ | |
| // 'position: absolute;', | |
| // 'top: 10px;', | |
| // 'left: 5px;', | |
| // 'pointer-events: none;', | |
| // 'font-size: 9px;', | |
| // 'z-index: 9999;', | |
| // 'color: #000;', | |
| // 'width: 220px;', | |
| // 'line-height: 1;', | |
| // 'font-family: Arial;', | |
| // 'background-color: rgba(255,255,255,0.5);', | |
| // 'border: 1px solid #000;', | |
| // 'padding: 2px;' | |
| // ].join(''); | |
| // } | |
| // return resultDom; | |
| // } | |
| // function print1(allInfo, resultDom) { | |
| // console.log(allInfo.painterInternal); // For debug | |
| // } | |
| // function print2(allInfo, resultDom) { | |
| // var htmlArr = [ | |
| // 'chart.on("finished") <br>', | |
| // 'layerStack: ' + testHelper.printObject(allInfo.layerStackInfo) + '<br>', | |
| // 'layersMap: ' + testHelper.printObject(allInfo.layersMapInfo) | |
| // ]; | |
| // // console.log(allInfo.layersMapInfo); | |
| // resultDom.innerHTML = htmlArr.join(''); | |
| // } | |
| // function start() { | |
| // var resultDom = ensureResultDOM(); | |
| // var lastRendered = []; | |
| // console.log('You can visit window.__info'); | |
| // chart.on('rendered', function () { | |
| // var allInfo = retrieveLayerInfo(); | |
| // // print1(allInfo, resultDom); | |
| // lastRendered.push({ | |
| // layerStackInfo: allInfo.layerStackInfo, | |
| // hoverLayerInfo: allInfo.hoverLayerInfo, | |
| // layersMapInfo: allInfo.layersMapInfo | |
| // }); | |
| // // console.log('rendered'); | |
| // }); | |
| // chart.on('finished', function () { | |
| // // console.log('chart.on("finished")'); | |
| // var allInfo = retrieveLayerInfo(); | |
| // print2(allInfo, resultDom); | |
| // // console.log(lastRendered); | |
| // // console.log(allInfo.painterInternal); | |
| // window.__info = lastRendered.slice(); | |
| // lastRendered.length = 0; | |
| // }); | |
| // } | |
| // }; | |
| testHelper.getCanvasLayerDom = function (chart, zlevel, zlevel2) { | |
| var zr = chart.getZr(); | |
| var painter = zr.painter; | |
| if (painter.type !== 'canvas') { | |
| return; | |
| } | |
| // CAVEAT: Accessing internal data structure, may change. | |
| // See `CanvasPainterInternal` | |
| var painterInternal = painter._i; | |
| var layersPerZLevel = painterInternal.layers[zlevel]; | |
| var layer = layersPerZLevel && layersPerZLevel[zlevel2]; | |
| return layer ? layer.dom : null; | |
| }; | |
| context.testHelper = testHelper; | |
| })(window); |