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