2018-05-04 08:03:47 +08:00
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|
|
|
|
|
|
2017-05-04 14:19:44 +08:00
|
|
|
(function (context) {
|
|
|
|
|
|
2018-01-11 15:53:11 +08:00
|
|
|
var DEFAULT_DATA_TABLE_LIMIT = 8;
|
2017-05-04 14:19:44 +08:00
|
|
|
|
2018-01-11 15:53:11 +08:00
|
|
|
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
|
|
|
|
|
};
|
2018-01-10 18:13:08 +08:00
|
|
|
|
2021-11-19 16:45:19 +08:00
|
|
|
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 () {
|
2025-05-24 18:07:57 +08:00
|
|
|
return myRandom();
|
2021-11-19 16:45:19 +08:00
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-11 15:53:11 +08:00
|
|
|
var testHelper = {};
|
2018-01-10 18:13:08 +08:00
|
|
|
|
2017-05-04 14:19:44 +08:00
|
|
|
|
2018-01-11 15:53:11 +08:00
|
|
|
/**
|
2018-02-23 03:01:27 +08:00
|
|
|
* @param {Object} opt
|
2025-05-24 18:07:57 +08:00
|
|
|
* @param {string|string[]} [opt.title] If array, each item is on a single line.
|
2018-09-13 23:07:11 +08:00
|
|
|
* Can use '**abc**', means <strong>abc</strong>.
|
2025-05-24 18:07:57 +08:00
|
|
|
* @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.
|
2025-06-12 16:22:43 +08:00
|
|
|
* @param {Function} [opt.onResize] Optional. Available when `opt.autoResize` or `opt.draggable` is true.
|
2025-05-24 18:07:57 +08:00
|
|
|
* @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.
|
|
|
|
|
* 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
|
2025-06-12 15:58:28 +08:00
|
|
|
* suffix: '%', // Optional. e.g., '%' means the number is displayed as '33%'
|
2025-05-24 18:07:57 +08:00
|
|
|
* 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.
|
2025-06-12 03:07:15 +08:00
|
|
|
* }},
|
|
|
|
|
* // 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: {...}}
|
2025-05-24 18:07:57 +08:00
|
|
|
* ],
|
|
|
|
|
* 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)
|
2025-06-12 03:07:15 +08:00
|
|
|
* onchange: function () { console.log(this.value, this.optionId); }
|
2025-05-24 18:07:57 +08:00
|
|
|
* },
|
|
|
|
|
* {
|
|
|
|
|
* // 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.infoKey='option'] 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.
|
2018-01-11 15:53:11 +08:00
|
|
|
*/
|
|
|
|
|
testHelper.create = function (echarts, domOrId, opt) {
|
|
|
|
|
var dom = getDom(domOrId);
|
2017-08-14 16:32:35 +08:00
|
|
|
|
2018-01-11 15:53:11 +08:00
|
|
|
if (!dom) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2017-08-14 16:32:35 +08:00
|
|
|
|
2025-05-24 18:07:57 +08:00
|
|
|
var errMsgPrefix = '[testHelper dom: ' + domOrId + ']';
|
|
|
|
|
|
|
|
|
|
var titleContainer = document.createElement('div');
|
2018-01-11 15:53:11 +08:00
|
|
|
var left = document.createElement('div');
|
2025-05-24 18:07:57 +08:00
|
|
|
var chartContainerWrapper = document.createElement('div');
|
2018-01-11 15:53:11 +08:00
|
|
|
var chartContainer = document.createElement('div');
|
2025-05-24 18:07:57 +08:00
|
|
|
var inputsContainer = document.createElement('div');
|
2018-01-11 15:53:11 +08:00
|
|
|
var dataTableContainer = document.createElement('div');
|
|
|
|
|
var infoContainer = document.createElement('div');
|
2018-12-12 01:25:16 +08:00
|
|
|
var recordCanvasContainer = document.createElement('div');
|
2021-06-21 22:23:58 +08:00
|
|
|
var recordVideoContainer = document.createElement('div');
|
2025-05-24 18:07:57 +08:00
|
|
|
var boundingRectsContainer = document.createElement('div');
|
2017-05-04 14:19:44 +08:00
|
|
|
|
2025-05-24 18:07:57 +08:00
|
|
|
titleContainer.setAttribute('title', dom.getAttribute('id'));
|
2017-05-04 14:19:44 +08:00
|
|
|
|
2025-05-24 18:07:57 +08:00
|
|
|
titleContainer.className = 'test-title';
|
2018-01-11 15:53:11 +08:00
|
|
|
dom.className = 'test-chart-block';
|
|
|
|
|
left.className = 'test-chart-block-left';
|
2025-05-24 18:07:57 +08:00
|
|
|
chartContainerWrapper.className = 'test-chart-wrapper';
|
2018-01-11 15:53:11 +08:00
|
|
|
chartContainer.className = 'test-chart';
|
|
|
|
|
dataTableContainer.className = 'test-data-table';
|
|
|
|
|
infoContainer.className = 'test-info';
|
2025-05-24 18:07:57 +08:00
|
|
|
boundingRectsContainer.className = 'test-bounding-rects';
|
|
|
|
|
boundingRectsContainer.style.display = 'none';
|
2018-12-12 01:25:16 +08:00
|
|
|
recordCanvasContainer.className = 'record-canvas';
|
2021-06-21 22:23:58 +08:00
|
|
|
recordVideoContainer.className = 'record-video';
|
2018-01-10 18:13:08 +08:00
|
|
|
|
2018-01-11 15:53:11 +08:00
|
|
|
if (opt.info) {
|
|
|
|
|
dom.className += ' test-chart-block-has-right';
|
|
|
|
|
infoContainer.className += ' test-chart-block-right';
|
|
|
|
|
}
|
2018-01-10 18:13:08 +08:00
|
|
|
|
2018-12-12 01:25:16 +08:00
|
|
|
left.appendChild(recordCanvasContainer);
|
2021-06-21 22:23:58 +08:00
|
|
|
left.appendChild(recordVideoContainer);
|
2025-05-24 18:07:57 +08:00
|
|
|
left.appendChild(inputsContainer);
|
2018-01-11 15:53:11 +08:00
|
|
|
left.appendChild(dataTableContainer);
|
2025-05-24 18:07:57 +08:00
|
|
|
left.appendChild(chartContainerWrapper);
|
|
|
|
|
chartContainerWrapper.appendChild(chartContainer);
|
|
|
|
|
chartContainerWrapper.appendChild(boundingRectsContainer);
|
2018-01-11 15:53:11 +08:00
|
|
|
dom.appendChild(infoContainer);
|
|
|
|
|
dom.appendChild(left);
|
2025-05-24 18:07:57 +08:00
|
|
|
dom.parentNode.insertBefore(titleContainer, dom);
|
2018-01-10 18:13:08 +08:00
|
|
|
|
2025-05-24 18:07:57 +08:00
|
|
|
initTestTitle(opt, titleContainer);
|
|
|
|
|
|
|
|
|
|
var chart = testHelper.createChart(echarts, chartContainer, opt.option, opt, opt.setOptionOpts, errMsgPrefix);
|
|
|
|
|
chart.__testHelper = {};
|
|
|
|
|
|
|
|
|
|
initDataTables(opt, dataTableContainer);
|
2018-02-23 03:01:27 +08:00
|
|
|
|
2025-05-24 18:07:57 +08:00
|
|
|
if (chart) {
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return chart;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function initTestTitle(opt, titleContainer) {
|
2018-02-23 03:01:27 +08:00
|
|
|
var optTitle = opt.title;
|
|
|
|
|
if (optTitle) {
|
|
|
|
|
if (optTitle instanceof Array) {
|
|
|
|
|
optTitle = optTitle.join('\n');
|
|
|
|
|
}
|
2025-05-24 18:07:57 +08:00
|
|
|
titleContainer.innerHTML = '<div class="test-title-inner">'
|
|
|
|
|
+ encodeHTML(optTitle)
|
2018-09-13 23:07:11 +08:00
|
|
|
.replace(/\*\*([^*]+?)\*\*/g, '<strong>$1</strong>')
|
|
|
|
|
.replace(/\n/g, '<br>')
|
2018-02-23 03:01:27 +08:00
|
|
|
+ '</div>';
|
2018-01-11 15:53:11 +08:00
|
|
|
}
|
2025-05-24 18:07:57 +08:00
|
|
|
}
|
2018-02-23 03:01:27 +08:00
|
|
|
|
2025-05-24 18:07:57 +08:00
|
|
|
function initUpdateInfo(opt, chart, infoContainer) {
|
|
|
|
|
assert(chart.__testHelper);
|
2018-02-23 03:01:27 +08:00
|
|
|
|
2025-05-24 18:07:57 +08:00
|
|
|
if (opt.info) {
|
|
|
|
|
updateInfo(opt.info, opt.infoKey);
|
2018-01-11 15:53:11 +08:00
|
|
|
}
|
2025-05-24 18:07:57 +08:00
|
|
|
|
|
|
|
|
function updateInfo(info, infoKey) {
|
|
|
|
|
infoContainer.innerHTML = createObjectHTML(info, infoKey || 'option');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
2018-01-11 15:53:11 +08:00
|
|
|
}
|
|
|
|
|
}
|
2018-02-23 03:01:27 +08:00
|
|
|
|
2025-05-24 18:07:57 +08:00
|
|
|
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);
|
|
|
|
|
};
|
|
|
|
|
}
|
2018-02-23 03:01:27 +08:00
|
|
|
}
|
2025-05-24 18:07:57 +08:00
|
|
|
|
|
|
|
|
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);
|
2018-02-23 03:01:27 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-24 18:07:57 +08:00
|
|
|
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;
|
2020-12-10 21:43:08 +08:00
|
|
|
}
|
|
|
|
|
|
2025-05-24 18:07:57 +08:00
|
|
|
function restoreInputsToInitialState() {
|
|
|
|
|
assert(
|
|
|
|
|
_initStateBackup != null,
|
|
|
|
|
'opt.saveInputsInitialState must be true to use `restoreInputsToInitialState`.'
|
|
|
|
|
);
|
|
|
|
|
setInputsState(_initStateBackup);
|
2018-01-11 15:53:11 +08:00
|
|
|
}
|
2018-01-10 18:13:08 +08:00
|
|
|
|
2025-05-24 18:07:57 +08:00
|
|
|
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);
|
|
|
|
|
}
|
2020-04-29 23:26:04 +08:00
|
|
|
|
2025-05-24 18:07:57 +08:00
|
|
|
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(' ');
|
2021-06-21 22:23:58 +08:00
|
|
|
}
|
|
|
|
|
|
2025-05-24 18:07:57 +08:00
|
|
|
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;
|
|
|
|
|
}
|
2020-12-10 21:43:08 +08:00
|
|
|
|
2025-05-24 18:07:57 +08:00
|
|
|
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;
|
2025-06-12 03:07:15 +08:00
|
|
|
var _opSuffix = internallyForceDef && internallyForceDef.id || '';
|
2025-05-24 18:07:57 +08:00
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
},
|
2025-06-12 03:07:15 +08:00
|
|
|
op: btnName + _opSuffix
|
2025-05-24 18:07:57 +08:00
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
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();
|
|
|
|
|
},
|
2025-06-12 03:07:15 +08:00
|
|
|
op: 'slide' + _opSuffix,
|
2025-05-24 18:07:57 +08:00
|
|
|
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;
|
2025-06-12 15:58:28 +08:00
|
|
|
_rangeInputValueEl.innerHTML = encodeHTML(newVal + '' + (inputDefine.suffix || ''));
|
2025-05-24 18:07:57 +08:00
|
|
|
}
|
|
|
|
|
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", ...}},',
|
2025-06-12 03:07:15 +08:00
|
|
|
' {text?: string, id: "some_option_id", input: {type: "range", ...}},',
|
2025-05-24 18:07:57 +08:00
|
|
|
' ...,',
|
|
|
|
|
' ],',
|
|
|
|
|
' 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) {
|
2025-06-12 03:07:15 +08:00
|
|
|
var innerInputCount = 0;
|
2025-05-24 18:07:57 +08:00
|
|
|
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,
|
2025-06-12 03:07:15 +08:00
|
|
|
id: optionDef.id,
|
2025-05-24 18:07:57 +08:00
|
|
|
text: text
|
|
|
|
|
});
|
2025-06-12 03:07:15 +08:00
|
|
|
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'));
|
2025-05-24 18:07:57 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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: '',
|
2025-06-12 03:07:15 +08:00
|
|
|
id: optionDef.id,
|
2025-05-24 18:07:57 +08:00
|
|
|
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++) {
|
2025-06-12 03:07:15 +08:00
|
|
|
if (!selectCtx._optionList[idx].input
|
|
|
|
|
&& selectCtx._optionList[idx].value === inputDefine.value
|
|
|
|
|
) {
|
2025-05-24 18:07:57 +08:00
|
|
|
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);
|
2025-06-12 03:07:15 +08:00
|
|
|
var optionId = selectCtx._optionList[optionIdx].id;
|
|
|
|
|
var target = {value: value, optionId: optionId};
|
2025-05-24 18:07:57 +08:00
|
|
|
_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
|
2020-04-29 23:26:04 +08:00
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
}
|
2018-12-12 01:25:16 +08:00
|
|
|
});
|
2020-04-29 23:26:04 +08:00
|
|
|
contentAraa.style.display = 'block';
|
|
|
|
|
recordArea.value = content.join('');
|
|
|
|
|
});
|
|
|
|
|
clearButton.addEventListener('click', function () {
|
|
|
|
|
eachCtx(function (zlevel, ctx) {
|
|
|
|
|
ctx.clear();
|
2018-12-12 01:25:16 +08:00
|
|
|
});
|
2020-04-29 23:26:04 +08:00
|
|
|
recordArea.value = 'Cleared.';
|
|
|
|
|
});
|
|
|
|
|
closeButton.addEventListener('click', function () {
|
|
|
|
|
contentAraa.style.display = 'none';
|
|
|
|
|
});
|
2018-12-12 01:25:16 +08:00
|
|
|
|
|
|
|
|
function eachCtx(cb) {
|
|
|
|
|
var layers = chart.getZr().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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-04-29 23:26:04 +08:00
|
|
|
}
|
2018-01-10 18:13:08 +08:00
|
|
|
|
2025-05-24 18:07:57 +08:00
|
|
|
/**
|
|
|
|
|
* @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;
|
|
|
|
|
|
2025-07-11 23:09:50 +08:00
|
|
|
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];
|
2025-05-24 18:07:57 +08:00
|
|
|
travelGroupAndBuildRects(child, visualRectGroup);
|
|
|
|
|
}
|
2025-07-11 23:09:50 +08:00
|
|
|
}
|
|
|
|
|
// Both display ZRText and TSpan bounding rect for debuging.
|
|
|
|
|
if (!el.isGroup) {
|
|
|
|
|
createVisualRectForEl(el, visualRectGroupParent);
|
|
|
|
|
}
|
2025-05-24 18:07:57 +08:00
|
|
|
|
2025-07-11 23:09:50 +08:00
|
|
|
function createVisualRectForEl(el, visualRectGroup) {
|
|
|
|
|
createRectForDisplayable(el, visualRectGroup);
|
|
|
|
|
var textContent = el.getTextContent();
|
|
|
|
|
var textGuildLine = el.getTextGuideLine();
|
|
|
|
|
var textConfig = el.textConfig;
|
2025-05-24 18:07:57 +08:00
|
|
|
if (textContent || textGuildLine) {
|
2025-07-11 23:09:50 +08:00
|
|
|
var isLocal = textConfig && textConfig.local;
|
|
|
|
|
var targetVisualGroup = isLocal ? visualRectGroup : _bRectGroup;
|
|
|
|
|
textContent && createRectForDisplayable(textContent, targetVisualGroup, true);
|
|
|
|
|
textGuildLine && createRectForDisplayable(textGuildLine, targetVisualGroup, true);
|
2025-05-24 18:07:57 +08:00
|
|
|
}
|
2025-07-11 23:09:50 +08:00
|
|
|
}
|
2025-05-24 18:07:57 +08:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-17 11:18:49 +08:00
|
|
|
testHelper.createRecordVideo = function (chart, recordVideoContainer) {
|
2021-06-21 22:23:58 +08:00
|
|
|
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();
|
2025-05-24 18:07:57 +08:00
|
|
|
button.innerHTML = (isRecording ? 'Start' : 'Stop') + ' Recording';
|
2021-06-21 22:23:58 +08:00
|
|
|
|
|
|
|
|
isRecording = !isRecording;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-11 15:53:11 +08:00
|
|
|
/**
|
2018-12-12 01:25:16 +08:00
|
|
|
* @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
|
2022-05-26 16:25:14 +08:00
|
|
|
* @param {boolean} opt.useCoarsePointer
|
|
|
|
|
* @param {boolean} opt.pointerSize
|
2018-12-12 01:25:16 +08:00
|
|
|
* @param {number} opt.width
|
|
|
|
|
* @param {number} opt.height
|
|
|
|
|
* @param {boolean} opt.draggable
|
2023-12-04 17:41:51 +08:00
|
|
|
* @param {string} opt.renderer 'canvas' or 'svg'
|
2025-05-24 18:07:57 +08:00
|
|
|
* @param {string} errMsgPrefix
|
2018-01-11 15:53:11 +08:00
|
|
|
*/
|
2025-05-24 18:07:57 +08:00
|
|
|
testHelper.createChart = function (echarts, domOrId, option, opt, errMsgPrefix) {
|
2018-01-11 15:53:11 +08:00
|
|
|
if (typeof opt === 'number') {
|
|
|
|
|
opt = {height: opt};
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
opt = opt || {};
|
|
|
|
|
}
|
2018-01-10 18:13:08 +08:00
|
|
|
|
2018-01-11 15:53:11 +08:00
|
|
|
var dom = getDom(domOrId);
|
2018-01-10 18:13:08 +08:00
|
|
|
|
2018-01-11 15:53:11 +08:00
|
|
|
if (dom) {
|
|
|
|
|
if (opt.width != null) {
|
|
|
|
|
dom.style.width = opt.width + 'px';
|
2018-01-10 18:13:08 +08:00
|
|
|
}
|
2018-01-11 15:53:11 +08:00
|
|
|
if (opt.height != null) {
|
|
|
|
|
dom.style.height = opt.height + 'px';
|
2018-01-10 18:13:08 +08:00
|
|
|
}
|
|
|
|
|
|
2025-05-26 15:27:01 +08:00
|
|
|
var theme = opt.theme && opt.theme !== 'none' ? opt.theme : null;
|
|
|
|
|
if (theme == null && window.__ECHARTS__DEFAULT__THEME__) {
|
|
|
|
|
theme = window.__ECHARTS__DEFAULT__THEME__;
|
|
|
|
|
}
|
|
|
|
|
if (theme) {
|
2025-06-12 03:07:15 +08:00
|
|
|
require(['theme/' + theme]);
|
2025-05-26 15:27:01 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var chart = echarts.init(dom, theme, {
|
2023-12-04 17:41:51 +08:00
|
|
|
renderer: opt.renderer,
|
2022-05-26 16:25:14 +08:00
|
|
|
useCoarsePointer: opt.useCoarsePointer,
|
|
|
|
|
pointerSize: opt.pointerSize
|
2022-05-24 15:25:34 +08:00
|
|
|
});
|
2018-01-10 18:13:08 +08:00
|
|
|
|
2018-01-11 15:53:11 +08:00
|
|
|
if (opt.draggable) {
|
2019-10-31 15:19:32 +08:00
|
|
|
if (!window.draggable) {
|
|
|
|
|
throw new Error(
|
2025-05-24 18:07:57 +08:00
|
|
|
errMsgPrefix + ' Pleasse add the script in HTML: \n'
|
2019-10-31 15:19:32 +08:00
|
|
|
+ '<script src="lib/draggable.js"></script>'
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-06-12 16:22:43 +08:00
|
|
|
window.draggable.init(dom, chart, {throttle: 70, onResize: opt.onResize});
|
2017-11-05 03:37:53 +08:00
|
|
|
}
|
|
|
|
|
|
2018-03-07 18:04:27 +08:00
|
|
|
option && chart.setOption(option, {
|
|
|
|
|
lazyUpdate: opt.lazyUpdate,
|
|
|
|
|
notMerge: opt.notMerge
|
|
|
|
|
});
|
2019-09-09 14:14:24 +08:00
|
|
|
|
2019-11-28 18:28:58 +08:00
|
|
|
var isAutoResize = opt.autoResize == null ? true : opt.autoResize;
|
2019-09-09 14:14:24 +08:00
|
|
|
if (isAutoResize) {
|
2025-06-12 16:22:43 +08:00
|
|
|
testHelper.resizable(chart, {onResize: opt.onResize});
|
2019-09-09 11:34:43 +08:00
|
|
|
}
|
2017-11-05 03:37:53 +08:00
|
|
|
|
2018-01-11 15:53:11 +08:00
|
|
|
return chart;
|
2018-01-10 18:13:08 +08:00
|
|
|
}
|
|
|
|
|
};
|
2018-01-08 20:10:10 +08:00
|
|
|
|
2020-07-07 23:02:06 +08:00
|
|
|
/**
|
|
|
|
|
* @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.
|
|
|
|
|
*
|
2020-07-14 18:09:46 +08:00
|
|
|
* @param chartOrDomId {EChartsInstance | string}
|
2020-07-07 23:02:06 +08:00
|
|
|
* @param checkFn {Function} param: a function `assert`.
|
|
|
|
|
*/
|
2020-07-14 18:09:46 +08:00
|
|
|
testHelper.printAssert = function (chartOrDomId, checkerFn) {
|
**There are these issues existing before this commit:**
(1). If no dimensions specified on dataset, series can not really share one storage instance. (Because the each series will create its own dimension name (like ['x', 'y', 'value'], ['x', 'value', 'y'], and the storage hash is based on those names. So the hash can not match).
(2). Each time `setOption` update series (but not change `dataset`), new data stack dimensions (and corresponding chunks) will be keep added to the shared data storage, and then the chunks will be more and more.
(3). When "unused dimension omit" happen, the index of SeriesData['dimensions'] and SeriesData['getDimension'] is not the dimensionIndex that users known. But there are someplace still use dimensionIndex to visit them. (especially in visualMap and other similar cases that user can input dimension index via option).
(4). If user only specify type but no name in dimensions, their will be some bug when "unused dimension omit" happen. (Because unused dimensions will not auto-generate dimension name by `createDimensions` and so that it has no dimension name in storage, and can not be queried by dimension name).
(5). If different series option specify its own `dimensions` but share one `dataset`, the `source` get by `sourceManager` is different `source` instances in each series. Those `source` instances contain different `dimensionDefine` but reference the same data. And then a data storage created by based on s
**This commit try resolve those issues by this way:**
1. Do not save the "dimName->dimIndex map" in data storage any more. Because:
1. In fact data storage do not need this map to read/write data.
2. dimNames are usually created based on each series info (like ['x', 'y', 'value'], ['x', 'value', 'y'], ...) if not specified by user. So they are different between series. But even those series have different generated dimension names, they can still share one storage, because essentially they visit the same dataset source by dimIndex.
2. Make `SeriesDimensionDefine` (that is, each item of `SeriesData['dimensionInfos']`) contain `storageDimensionIndex` to indicate its corresponding data store dimension index. And alway user `storageDimensionIndex` to visit dat storage rather than dimName. `storageDimensionIndex` is created in `createDimension`.
3. create a new structure `SeriesDimensionRequest` for each series. It contains the info generated by `createDimension` (like dimensionDefineList, whether dimension omitted, source for this series).
3.1 `sourceManager` use `seriesDimensionRequest` to find the shared storage by generate storage dimensions and hash based on the `dimCount` and `dimensionDefineList` (which are created by `createDimension` and saved in `seriesDimensionRequest`).
3.2 `dataStack` add "data stack dimensions" to `dimensionDefineList` in `seriesDimensionRequest`.
3.3 `seriesData` use `seriesDimensionRequest` to init its dimensions, and use `seriesDimensionRequest` query dimName by dimIndex from source, or query dimIndex by dimName from source for "omitted dimension". If different series option specify its own `dimensions` but share one `dataset`, the `source` get by `sourceManager` is different `source` instances in each series. Those `source` instances contain different `dimensionDefine` but reference the same "raw data". The data storage generated based on the "raw data" can be shared between series, but the `dimensionDefine` should not be shared. `SeriesDimensionRequest` encapsulate these mess and work each series to query dimension name or index.
3.4 `seriesDimensionRequest` do not create new data structure as possible as it can, but reference shared data structure (like `source` instance). So it will not cost memory issue.
4. Change the previous `storage.appendDimension` to `storage.ensureCalculationDimension` for data stack. That is, if its dimension has been created, reuse it.
5. Remove previous `canUse` method. Whether a storage can be shared by series is all determined by hash. The hash is generated in two ways:
5.1 For source format "arrayRows" (i.e., [[12, 33], [55, 99], ...]), dimension name do not need to be added to hash, because this kind of data actually visited by index. If two series have different dimension name (like 'x', 'y') for single index, they also can share the storage.
5.2 For source format "objectRows" (i.e, [{a: 12, b: 33}, {b: 55, a: 99}, ...]), property name 'a', 'b' will be added to hash, because this kind of data actually visited by property name.
5.3 And as before, dimension type, ordinal meta id, source header, series layout will also be added to hash.
6. Make `DataStorage` method immutable:
`DataStorage['filterSelf']` -> `DataStorage['filter']`
`DataStorage['selectRange']`
**PENDING:**
1. Should deprecate `dimName = seriesData.getDimension(dimLoose)` and `series.get(dimName)` but always use `dimIdx = seriesData.getDimensionIndex(dimLoose)` and `dataStorage.get(dimIdx)` instead. For examples:
```js
// Previously
const val = data.get(data.getDimension(dim), dataIdx);
// Now
const val = data.getStorage().get(data.getDimensionIndex(dim), dataIdx);
```
`seriesData.getDimension(dimLoose)` has a feature that convert dimIdx to dimName, which is not essentially necessary (because dimIdx can be used to visit data directly), but this feature require a "dimIdx->dimName map" in `SeriesData` (why? because when some dimensions are omitted, we can not use dimIdx on `SeriesData['dimensions']` directly).
2. Radar has bug when using `series.encode`. This commit do not fix the bug but keep as it is.
2021-08-20 04:31:15 +08:00
|
|
|
if (!chartOrDomId) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-14 18:09:46 +08:00
|
|
|
var hostDOMEl;
|
|
|
|
|
var chart;
|
|
|
|
|
if (typeof chartOrDomId === 'string') {
|
|
|
|
|
hostDOMEl = document.getElementById(chartOrDomId);
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
chart = chartOrDomId;
|
|
|
|
|
hostDOMEl = chartOrDomId.getDom();
|
|
|
|
|
}
|
2020-07-07 23:02:06 +08:00
|
|
|
var failErr;
|
|
|
|
|
function assert(cond) {
|
|
|
|
|
if (!cond) {
|
|
|
|
|
throw new Error();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
checkerFn(assert);
|
|
|
|
|
}
|
|
|
|
|
catch (err) {
|
|
|
|
|
console.error(err);
|
|
|
|
|
failErr = err;
|
|
|
|
|
}
|
2020-07-14 18:09:46 +08:00
|
|
|
var printAssertRecord = hostDOMEl.__printAssertRecord || (hostDOMEl.__printAssertRecord = []);
|
2020-07-07 23:02:06 +08:00
|
|
|
|
|
|
|
|
var resultDom = document.createElement('div');
|
|
|
|
|
resultDom.innerHTML = failErr ? 'checked: Fail' : 'checked: Pass';
|
|
|
|
|
var fontSize = 40;
|
|
|
|
|
resultDom.style.cssText = [
|
|
|
|
|
'position: absolute;',
|
|
|
|
|
'left: 20px;',
|
2025-05-24 18:07:57 +08:00
|
|
|
'pointer-events: none;',
|
2020-07-07 23:02:06 +08:00
|
|
|
'font-size: ' + fontSize + 'px;',
|
|
|
|
|
'z-index: ' + (failErr ? 99999 : 88888) + ';',
|
2025-05-24 18:07:57 +08:00
|
|
|
'color: ' + (failErr ? 'rgba(150,0,0,0.8)' : 'rgba(0,150,0,0.8)') + ';',
|
2020-07-07 23:02:06 +08:00
|
|
|
].join('');
|
|
|
|
|
printAssertRecord.push(resultDom);
|
2020-07-14 18:09:46 +08:00
|
|
|
hostDOMEl.appendChild(resultDom);
|
2020-07-07 23:02:06 +08:00
|
|
|
|
|
|
|
|
relayoutResult();
|
|
|
|
|
|
|
|
|
|
function relayoutResult() {
|
2020-07-14 18:09:46 +08:00
|
|
|
var chartHeight = chart ? chart.getHeight() : hostDOMEl.offsetHeight;
|
2020-07-07 23:02:06 +08:00
|
|
|
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';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2018-01-08 20:10:10 +08:00
|
|
|
|
2020-04-29 23:26:04 +08:00
|
|
|
var _dummyRequestAnimationFrameMounted = false;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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) {
|
|
|
|
|
opt = opt || {};
|
|
|
|
|
var pauseAt = opt.pauseAt;
|
|
|
|
|
pauseAt == null && (pauseAt = 0);
|
|
|
|
|
|
|
|
|
|
var _running = true;
|
|
|
|
|
var _pendingCbList = [];
|
|
|
|
|
var _frameNumber = 0;
|
|
|
|
|
var _mounted = false;
|
|
|
|
|
|
2020-04-30 03:52:59 +08:00
|
|
|
function getRunBtnText() {
|
|
|
|
|
return _running ? 'pause' : 'run';
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-29 23:26:04 +08:00
|
|
|
var buttons = [{
|
2020-04-30 03:52:59 +08:00
|
|
|
text: getRunBtnText(),
|
|
|
|
|
onclick: function () {
|
|
|
|
|
buttons[0].el.innerHTML = getRunBtnText();
|
|
|
|
|
_running ? pause() : run();
|
|
|
|
|
}
|
2020-04-29 23:26:04 +08:00
|
|
|
}, {
|
|
|
|
|
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];
|
2020-04-30 03:52:59 +08:00
|
|
|
var btnEl = button.el = document.createElement('button');
|
2020-04-29 23:26:04 +08:00
|
|
|
btnEl.innerHTML = button.text;
|
|
|
|
|
btnEl.addEventListener('click', button.onclick);
|
|
|
|
|
btnPanel.appendChild(btnEl);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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() {
|
|
|
|
|
_running = true;
|
|
|
|
|
nextFrame();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function pause() {
|
|
|
|
|
_running = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function nextFrame() {
|
|
|
|
|
opt.onFrame && opt.onFrame(_frameNumber);
|
|
|
|
|
|
|
|
|
|
if (pauseAt != null && _frameNumber === pauseAt) {
|
|
|
|
|
_running = false;
|
|
|
|
|
pauseAt = null;
|
|
|
|
|
}
|
2020-04-30 03:52:59 +08:00
|
|
|
infoEl.innerHTML = 'Frame: ' + _frameNumber + ' ( ' + (_running ? 'Running' : 'Paused') + ' )';
|
|
|
|
|
buttons[0].el.innerHTML = getRunBtnText();
|
2020-04-29 23:26:04 +08:00
|
|
|
|
|
|
|
|
_mounted = false;
|
|
|
|
|
var pending = _pendingCbList;
|
|
|
|
|
_pendingCbList = [];
|
|
|
|
|
for (var i = 0; i < pending.length; i++) {
|
|
|
|
|
pending[i]();
|
|
|
|
|
}
|
|
|
|
|
_frameNumber++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-12 16:22:43 +08:00
|
|
|
testHelper.resizable = function (chart, opt) {
|
|
|
|
|
opt = opt || {};
|
2019-11-28 18:28:58 +08:00
|
|
|
var dom = chart.getDom();
|
|
|
|
|
var width = dom.clientWidth;
|
|
|
|
|
var height = dom.clientHeight;
|
2019-09-10 01:14:31 +08:00
|
|
|
function resize() {
|
2019-11-28 18:28:58 +08:00
|
|
|
var newWidth = dom.clientWidth;
|
|
|
|
|
var newHeight = dom.clientHeight;
|
2019-09-10 01:14:31 +08:00
|
|
|
if (width !== newWidth || height !== newHeight) {
|
|
|
|
|
chart.resize();
|
2025-05-24 18:07:57 +08:00
|
|
|
if (chart.__testHelper && chart.__testHelper.updateBoundingRects) {
|
|
|
|
|
chart.__testHelper.updateBoundingRects();
|
|
|
|
|
}
|
2019-09-10 01:14:31 +08:00
|
|
|
width = newWidth;
|
|
|
|
|
height = newHeight;
|
2025-06-12 16:22:43 +08:00
|
|
|
|
|
|
|
|
if (opt.onResize) {
|
|
|
|
|
opt.onResize();
|
|
|
|
|
}
|
2019-09-10 01:14:31 +08:00
|
|
|
}
|
|
|
|
|
}
|
2018-01-11 15:53:11 +08:00
|
|
|
if (window.attachEvent) {
|
2019-09-10 01:14:31 +08:00
|
|
|
// Use builtin resize in IE
|
2025-05-24 18:07:57 +08:00
|
|
|
window.attachEvent('onresize', resize);
|
2019-09-10 01:14:31 +08:00
|
|
|
}
|
|
|
|
|
else if (window.addEventListener) {
|
|
|
|
|
window.addEventListener('resize', resize, false);
|
2018-01-10 18:13:08 +08:00
|
|
|
}
|
2018-01-11 15:53:11 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 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);
|
2018-01-08 20:10:10 +08:00
|
|
|
}
|
|
|
|
|
}
|
2018-01-10 18:13:08 +08:00
|
|
|
}
|
2018-01-11 15:53:11 +08:00
|
|
|
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;
|
2018-01-08 20:10:10 +08:00
|
|
|
}
|
|
|
|
|
}
|
2018-01-11 15:53:11 +08:00
|
|
|
return false;
|
|
|
|
|
};
|
2018-01-10 18:13:08 +08:00
|
|
|
|
2018-01-11 15:53:11 +08:00
|
|
|
// Nodejs `path.resolve`.
|
|
|
|
|
testHelper.resolve = function () {
|
|
|
|
|
var resolvedPath = '';
|
|
|
|
|
var resolvedAbsolute;
|
2018-01-08 20:10:10 +08:00
|
|
|
|
2018-01-11 15:53:11 +08:00
|
|
|
for (var i = arguments.length - 1; i >= 0 && !resolvedAbsolute; i--) {
|
|
|
|
|
var path = arguments[i];
|
|
|
|
|
if (path) {
|
|
|
|
|
resolvedPath = path + '/' + resolvedPath;
|
|
|
|
|
resolvedAbsolute = path[0] === '/';
|
2018-01-08 20:10:10 +08:00
|
|
|
}
|
|
|
|
|
}
|
2018-01-11 15:53:11 +08:00
|
|
|
|
|
|
|
|
if (!resolvedAbsolute) {
|
|
|
|
|
throw new Error('At least one absolute path should be input.');
|
2018-01-08 20:10:10 +08:00
|
|
|
}
|
|
|
|
|
|
2018-01-11 15:53:11 +08:00
|
|
|
// Normalize the path
|
|
|
|
|
resolvedPath = normalizePathArray(resolvedPath.split('/'), false).join('/');
|
2018-01-10 18:13:08 +08:00
|
|
|
|
2018-01-11 15:53:11 +08:00
|
|
|
return '/' + resolvedPath;
|
|
|
|
|
};
|
2018-01-10 18:13:08 +08:00
|
|
|
|
2025-05-24 18:07:57 +08:00
|
|
|
var encodeHTML = testHelper.encodeHTML = function (source) {
|
2018-01-11 15:53:11 +08:00
|
|
|
return String(source)
|
|
|
|
|
.replace(/&/g, '&')
|
|
|
|
|
.replace(/</g, '<')
|
|
|
|
|
.replace(/>/g, '>')
|
|
|
|
|
.replace(/"/g, '"')
|
|
|
|
|
.replace(/'/g, ''');
|
|
|
|
|
};
|
|
|
|
|
|
2025-05-24 18:07:57 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2018-01-11 15:53:11 +08:00
|
|
|
/**
|
|
|
|
|
* @public
|
|
|
|
|
* @return {string} Current url dir.
|
|
|
|
|
*/
|
|
|
|
|
testHelper.dir = function () {
|
|
|
|
|
return location.origin + testHelper.resolve(location.pathname, '..');
|
2018-01-10 18:13:08 +08:00
|
|
|
};
|
2018-01-11 15:53:11 +08:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Not accurate.
|
|
|
|
|
* @param {*} type
|
|
|
|
|
* @return {string} 'function', 'array', 'typedArray', 'regexp',
|
|
|
|
|
* 'date', 'object', 'boolean', 'number', 'string'
|
|
|
|
|
*/
|
|
|
|
|
var getType = testHelper.getType = function (value) {
|
2018-01-10 18:13:08 +08:00
|
|
|
var type = typeof value;
|
|
|
|
|
var typeStr = objToString.call(value);
|
|
|
|
|
|
|
|
|
|
return !!TYPED_ARRAY[objToString.call(value)]
|
|
|
|
|
? 'typedArray'
|
2025-05-24 18:07:57 +08:00
|
|
|
: typeof value === 'function'
|
2018-01-10 18:13:08 +08:00
|
|
|
? '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;
|
2018-01-11 15:53:11 +08:00
|
|
|
};
|
2018-01-10 18:13:08 +08:00
|
|
|
|
2018-01-11 15:53:11 +08:00
|
|
|
/**
|
|
|
|
|
* 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]'
|
2025-05-24 18:07:57 +08:00
|
|
|
* @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.
|
2018-01-11 15:53:11 +08:00
|
|
|
* @param {string} [opt.indent=4]
|
2025-05-24 18:07:57 +08:00
|
|
|
* @param {string} [opt.marginLeft=0] Spaces number for margin left of the entire text.
|
2018-01-11 15:53:11 +08:00
|
|
|
* @param {string} [opt.lineBreak='\n']
|
2025-05-24 18:07:57 +08:00
|
|
|
* @param {string} [opt.quotationMark="'"] "'" or '"'.
|
2018-01-11 15:53:11 +08:00
|
|
|
*/
|
|
|
|
|
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';
|
2025-05-24 18:07:57 +08:00
|
|
|
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;
|
2018-01-11 15:53:11 +08:00
|
|
|
|
2025-05-24 18:07:57 +08:00
|
|
|
return (new Array(marginLeft + 1)).join(' ') + doPrint(obj, opt.key, 0).str;
|
2018-01-10 18:13:08 +08:00
|
|
|
|
|
|
|
|
function doPrint(obj, key, depth) {
|
2025-05-24 18:07:57 +08:00
|
|
|
var codeIndent = (new Array(depth * indent + marginLeft + 1)).join(' ');
|
|
|
|
|
var subCodeIndent = (new Array((depth + 1) * indent + marginLeft + 1)).join(' ');
|
2018-01-10 18:13:08 +08:00
|
|
|
var hasLineBreak = false;
|
2025-05-24 18:07:57 +08:00
|
|
|
// [
|
|
|
|
|
// 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) + ': ';
|
|
|
|
|
}
|
2018-01-10 18:13:08 +08:00
|
|
|
var str;
|
|
|
|
|
|
|
|
|
|
var objType = getType(obj);
|
|
|
|
|
|
|
|
|
|
switch (objType) {
|
|
|
|
|
case 'function':
|
|
|
|
|
hasLineBreak = true;
|
2025-05-24 18:07:57 +08:00
|
|
|
preventParentArrayPartiallyBreak = true;
|
|
|
|
|
var fnStr = obj.toString();
|
|
|
|
|
var isMethodShorthand = key != null && isMethodShorthandNotAccurate(fnStr, obj.name, key);
|
|
|
|
|
str = (isMethodShorthand ? '' : preStr) + fnStr;
|
2018-01-10 18:13:08 +08:00
|
|
|
break;
|
|
|
|
|
case 'regexp':
|
|
|
|
|
case 'date':
|
2018-01-11 15:53:11 +08:00
|
|
|
str = preStr + quotationMark + obj + quotationMark;
|
2018-01-10 18:13:08 +08:00
|
|
|
break;
|
|
|
|
|
case 'array':
|
|
|
|
|
case 'typedArray':
|
2025-05-24 18:07:57 +08:00
|
|
|
if (forceArrayLineBreak) {
|
|
|
|
|
hasLineBreak = !!opt.arrayLineBreak;
|
|
|
|
|
}
|
2018-01-10 18:13:08 +08:00
|
|
|
// If no break line in array, print in single line, like [12, 23, 34].
|
|
|
|
|
// else, each item takes a line.
|
|
|
|
|
var childBuilder = [];
|
2025-05-24 18:07:57 +08:00
|
|
|
var maxColumnWithoutLineBreak = preStr.length;
|
|
|
|
|
var canPartiallyBreak = true;
|
2018-01-10 18:13:08 +08:00
|
|
|
for (var i = 0, len = obj.length; i < len; i++) {
|
|
|
|
|
var subResult = doPrint(obj[i], null, depth + 1);
|
|
|
|
|
childBuilder.push(subResult.str);
|
2025-05-24 18:07:57 +08:00
|
|
|
|
2018-01-10 18:13:08 +08:00
|
|
|
if (subResult.hasLineBreak) {
|
|
|
|
|
hasLineBreak = true;
|
|
|
|
|
}
|
2025-05-24 18:07:57 +08:00
|
|
|
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;
|
2018-01-10 18:13:08 +08:00
|
|
|
}
|
|
|
|
|
var tail = hasLineBreak ? lineBreak : '';
|
|
|
|
|
var subPre = hasLineBreak ? subCodeIndent : '';
|
|
|
|
|
var endPre = hasLineBreak ? codeIndent : '';
|
2025-05-24 18:07:57 +08:00
|
|
|
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 + ']';
|
|
|
|
|
}
|
2018-01-10 18:13:08 +08:00
|
|
|
break;
|
|
|
|
|
case 'object':
|
2025-05-24 18:07:57 +08:00
|
|
|
if (forceObjectLineBreak) {
|
|
|
|
|
hasLineBreak = !!opt.objectLineBreak;
|
|
|
|
|
}
|
2018-01-10 18:13:08 +08:00
|
|
|
var childBuilder = [];
|
2025-05-24 18:07:57 +08:00
|
|
|
var maxColumnWithoutLineBreak = preStr.length;
|
|
|
|
|
var keyCount = 0;
|
2018-01-10 18:13:08 +08:00
|
|
|
for (var i in obj) {
|
|
|
|
|
if (obj.hasOwnProperty(i)) {
|
2025-05-24 18:07:57 +08:00
|
|
|
keyCount++;
|
2018-01-10 18:13:08 +08:00
|
|
|
var subResult = doPrint(obj[i], i, depth + 1);
|
2018-01-11 15:53:11 +08:00
|
|
|
childBuilder.push(subResult.str);
|
2025-05-24 18:07:57 +08:00
|
|
|
|
2018-01-11 15:53:11 +08:00
|
|
|
if (subResult.hasLineBreak) {
|
|
|
|
|
hasLineBreak = true;
|
|
|
|
|
}
|
2025-05-24 18:07:57 +08:00
|
|
|
else {
|
|
|
|
|
maxColumnWithoutLineBreak += subResult.str.length + 2; // `2` is ', '.length
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (subResult.preventParentArrayPartiallyBreak) {
|
|
|
|
|
preventParentArrayPartiallyBreak = true;
|
|
|
|
|
}
|
2018-01-10 18:13:08 +08:00
|
|
|
}
|
|
|
|
|
}
|
2025-05-24 18:07:57 +08:00
|
|
|
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 : '') + '}';
|
|
|
|
|
}
|
2018-01-10 18:13:08 +08:00
|
|
|
break;
|
|
|
|
|
case 'boolean':
|
|
|
|
|
case 'number':
|
|
|
|
|
str = preStr + obj + '';
|
|
|
|
|
break;
|
|
|
|
|
case 'string':
|
2025-05-24 18:07:57 +08:00
|
|
|
str = preStr + convertStringToJSLiteral(obj, quotationMark);
|
2018-01-10 18:13:08 +08:00
|
|
|
break;
|
|
|
|
|
default:
|
2018-01-14 13:13:59 +08:00
|
|
|
str = preStr + obj + '';
|
2025-05-24 18:07:57 +08:00
|
|
|
preventParentArrayPartiallyBreak = true;
|
2018-01-10 18:13:08 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
str: str,
|
2025-05-24 18:07:57 +08:00
|
|
|
hasLineBreak: hasLineBreak,
|
|
|
|
|
isMethodShorthand: isMethodShorthand,
|
|
|
|
|
preventParentArrayPartiallyBreak: preventParentArrayPartiallyBreak
|
2018-01-10 18:13:08 +08:00
|
|
|
};
|
|
|
|
|
}
|
2025-05-24 18:07:57 +08:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-11 15:53:11 +08:00
|
|
|
};
|
|
|
|
|
|
2020-04-24 00:20:04 +08:00
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
*/
|
2025-05-24 18:07:57 +08:00
|
|
|
var stringifyElements = testHelper.stringifyElements = function (chart, opt) {
|
2020-04-24 00:20:04 +08:00
|
|
|
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];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
2018-01-11 15:53:11 +08:00
|
|
|
|
2020-04-24 00:20:04 +08:00
|
|
|
/**
|
|
|
|
|
* 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`.
|
|
|
|
|
*/
|
2025-05-24 18:07:57 +08:00
|
|
|
var printElements = testHelper.printElements = function (chart, opt) {
|
2020-04-24 00:20:04 +08:00
|
|
|
var elsStr = testHelper.stringifyElements(chart, opt);
|
|
|
|
|
console.log(elsStr);
|
|
|
|
|
};
|
2018-01-11 15:53:11 +08:00
|
|
|
|
2020-04-29 23:26:04 +08:00
|
|
|
/**
|
|
|
|
|
* 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>}
|
|
|
|
|
*/
|
2025-05-24 18:07:57 +08:00
|
|
|
var retrieveElements = testHelper.retrieveElements = function (chart, opt) {
|
2020-04-29 23:26:04 +08:00
|
|
|
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;
|
|
|
|
|
};
|
|
|
|
|
|
2020-04-24 00:46:02 +08:00
|
|
|
// 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);
|
|
|
|
|
};
|
|
|
|
|
|
2025-05-24 18:07:57 +08:00
|
|
|
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('');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-11 15:53:11 +08:00
|
|
|
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];
|
2025-05-24 18:07:57 +08:00
|
|
|
htmlLine.push('<td>' + encodeHTML(val) + '</td>');
|
2018-01-11 15:53:11 +08:00
|
|
|
}
|
|
|
|
|
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;
|
2025-05-24 18:07:57 +08:00
|
|
|
htmlLine.push('<td class="test-data-table-key">' + encodeHTML(keyText) + '</td>');
|
2018-01-11 15:53:11 +08:00
|
|
|
var val = i === dataTableLimit ? '...' : line[key];
|
2025-05-24 18:07:57 +08:00
|
|
|
htmlLine.push('<td>' + encodeHTML(val) + '</td>');
|
2018-01-11 15:53:11 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
htmlLine.push('</tr>');
|
|
|
|
|
html.push(htmlLine.join(''));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (sourceFormat === 'keyedColumns') {
|
|
|
|
|
for (var key in data) {
|
|
|
|
|
var htmlLine = ['<tr>'];
|
2025-05-24 18:07:57 +08:00
|
|
|
htmlLine.push('<td class="test-data-table-key">' + encodeHTML(key) + '</td>');
|
2018-01-11 15:53:11 +08:00
|
|
|
if (data.hasOwnProperty(key)) {
|
|
|
|
|
var col = data[key] || [];
|
|
|
|
|
for (var i = 0; i < col.length && i <= dataTableLimit; i++) {
|
|
|
|
|
var val = i === dataTableLimit ? '...' : col[i];
|
2025-05-24 18:07:57 +08:00
|
|
|
htmlLine.push('<td>' + encodeHTML(val) + '</td>');
|
2018-01-11 15:53:11 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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';
|
|
|
|
|
}
|
2018-01-10 18:13:08 +08:00
|
|
|
}
|
|
|
|
|
|
2018-01-11 15:53:11 +08:00
|
|
|
function createObjectHTML(obj, key) {
|
2020-12-10 21:43:08 +08:00
|
|
|
var html = isObject(obj)
|
2025-05-24 18:07:57 +08:00
|
|
|
? encodeHTML(printObject(obj, key))
|
2020-12-10 21:43:08 +08:00
|
|
|
: obj
|
|
|
|
|
? obj.toString()
|
|
|
|
|
: '';
|
|
|
|
|
|
2018-01-11 15:53:11 +08:00
|
|
|
return [
|
|
|
|
|
'<pre class="test-print-object">',
|
2020-12-10 21:43:08 +08:00
|
|
|
html,
|
2018-01-11 15:53:11 +08:00
|
|
|
'</pre>'
|
|
|
|
|
].join('');
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-29 05:01:33 +08:00
|
|
|
var getDom = testHelper.getDom = function (domOrId) {
|
2018-01-11 15:53:11 +08:00
|
|
|
return getType(domOrId) === 'string' ? document.getElementById(domOrId) : domOrId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2017-11-05 03:37:53 +08:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-11 16:00:21 +08:00
|
|
|
function getParamListFromURL() {
|
|
|
|
|
var params = location.search.replace('?', '');
|
|
|
|
|
return params ? params.split('&') : [];
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-10 21:43:08 +08:00
|
|
|
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.
|
2021-06-21 22:23:58 +08:00
|
|
|
var type = typeof value;
|
2020-12-10 21:43:08 +08:00
|
|
|
return type === 'function' || (!!value && type === 'object');
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-24 18:07:57 +08:00
|
|
|
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++);
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-21 22:23:58 +08:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-04 14:19:44 +08:00
|
|
|
context.testHelper = testHelper;
|
|
|
|
|
|
2025-05-24 18:07:57 +08:00
|
|
|
})(window);
|