Chart.js/test/specs/helpers.config.tests.js

814 lines
23 KiB
JavaScript
Raw Permalink Normal View History

describe('Chart.helpers.config', function() {
const {getHoverColor, _createResolver, _attachContext} = Chart.helpers;
describe('_createResolver', function() {
it('should resolve to raw values', function() {
const defaults = {
color: 'red',
backgroundColor: 'green',
hoverColor: (ctx, options) => getHoverColor(options.color)
};
const options = {
color: 'blue'
};
const resolver = _createResolver([options, defaults]);
expect(resolver.color).toEqual('blue');
expect(resolver.backgroundColor).toEqual('green');
expect(resolver.hoverColor).toEqual(defaults.hoverColor);
});
it('should resolve to parent scopes, when _fallback is true', function() {
const descriptors = {
_fallback: true
};
const defaults = {
root: true,
sub: {
child: true
}
};
const options = {
child: 'sub default comes before this',
opt: 'opt'
};
const resolver = _createResolver([options, defaults, descriptors]);
const sub = resolver.sub;
expect(sub.root).toEqual(true);
expect(sub.child).toEqual(true);
expect(sub.opt).toEqual('opt');
});
it('should support overriding options', function() {
const defaults = {
option1: 'defaults1',
option2: 'defaults2',
option3: 'defaults3',
};
const options = {
option1: 'options1',
option2: 'options2'
};
const overrides = {
option1: 'override1'
};
const resolver = _createResolver([options, defaults]);
expect(resolver).toEqualOptions({
option1: 'options1',
option2: 'options2',
option3: 'defaults3'
});
expect(resolver.override(overrides)).toEqualOptions({
option1: 'override1',
option2: 'options2',
option3: 'defaults3'
});
});
it('should support common object methods', function() {
const defaults = {
option1: 'defaults'
};
class Options {
constructor() {
this.option2 = 'options';
}
get getter() {
return 'options getter';
}
}
const options = new Options();
const resolver = _createResolver([options, defaults]);
expect(Object.prototype.hasOwnProperty.call(resolver, 'option2')).toBeTrue();
expect(Object.prototype.hasOwnProperty.call(resolver, 'option1')).toBeFalse();
expect(Object.prototype.hasOwnProperty.call(resolver, 'getter')).toBeFalse();
expect(Object.prototype.hasOwnProperty.call(resolver, 'nonexistent')).toBeFalse();
expect(Object.keys(resolver)).toEqual(['option2']);
expect(Object.getOwnPropertyNames(resolver)).toEqual(['option2', 'option1']);
expect('option2' in resolver).toBeTrue();
expect('option1' in resolver).toBeTrue();
expect('getter' in resolver).toBeFalse();
expect('nonexistent' in resolver).toBeFalse();
expect(resolver instanceof Options).toBeTrue();
expect(resolver.getter).toEqual('options getter');
});
2021-02-19 07:30:39 +01:00
it('should not fail on when options are frozen', function() {
function create() {
const defaults = Object.freeze({default: true});
const options = Object.freeze({value: true});
return _createResolver([options, defaults]);
}
expect(create).not.toThrow();
});
2021-02-19 07:30:39 +01:00
describe('_fallback', function() {
it('should follow simple _fallback', function() {
const defaults = {
interaction: {
mode: 'test',
priority: 'fall'
},
hover: {
_fallback: 'interaction',
priority: 'main'
}
};
const options = {
interaction: {
a: 1
},
hover: {
b: 2
}
};
const resolver = _createResolver([options, defaults]);
expect(resolver.hover).toEqualOptions({
mode: 'test',
priority: 'main',
a: 1,
b: 2
});
});
it('should support _fallback as function', function() {
const descriptors = {
_fallback: (prop, value) => prop === 'hover' && value.shouldFall && 'interaction',
};
const defaults = {
interaction: {
mode: 'test',
priority: 'fall'
},
hover: {
priority: 'main'
}
};
const options = {
interaction: {
a: 1
},
hover: {
shouldFall: true,
b: 2
}
};
const resolver = _createResolver([options, defaults, descriptors]);
expect(resolver.hover).toEqualOptions({
mode: 'test',
priority: 'main',
a: 1,
b: 2
});
});
it('should not fallback by default', function() {
2021-02-19 07:30:39 +01:00
const defaults = {
hover: {
a: 'defaults.hover'
},
controllers: {
y: 'defaults.controllers',
bar: {
z: 'defaults.controllers.bar',
hover: {
b: 'defaults.controllers.bar.hover'
}
}
},
x: 'defaults root'
};
const options = {
x: 'options',
hover: {
c: 'options.hover',
sub: {
f: 'options.hover.sub'
}
},
controllers: {
y: 'options.controllers',
bar: {
z: 'options.controllers.bar',
hover: {
d: 'options.controllers.bar.hover',
sub: {
e: 'options.controllers.bar.hover.sub'
}
}
}
}
};
const resolver = _createResolver([options, options.controllers.bar, options.controllers, defaults.controllers.bar, defaults.controllers, defaults]);
expect(resolver.hover).toEqualOptions({
a: 'defaults.hover',
b: 'defaults.controllers.bar.hover',
c: 'options.hover',
d: 'options.controllers.bar.hover',
e: undefined,
f: undefined,
x: undefined,
y: undefined,
z: undefined
});
expect(resolver.hover.sub).toEqualOptions({
a: undefined,
b: undefined,
c: undefined,
d: undefined,
e: 'options.controllers.bar.hover.sub',
f: 'options.hover.sub',
x: undefined,
y: undefined,
z: undefined
});
});
it('should fallback to specific scope', function() {
const defaults = {
hover: {
_fallback: 'hover',
a: 'defaults.hover'
},
controllers: {
y: 'defaults.controllers',
bar: {
z: 'defaults.controllers.bar',
hover: {
b: 'defaults.controllers.bar.hover'
}
}
},
x: 'defaults root'
};
const options = {
x: 'options',
hover: {
c: 'options.hover',
sub: {
f: 'options.hover.sub'
}
},
controllers: {
y: 'options.controllers',
bar: {
z: 'options.controllers.bar',
hover: {
d: 'options.controllers.bar.hover',
sub: {
e: 'options.controllers.bar.hover.sub'
}
}
}
}
};
const resolver = _createResolver([options, options.controllers.bar, options.controllers, defaults.controllers.bar, defaults.controllers, defaults]);
expect(resolver.hover).toEqualOptions({
a: 'defaults.hover',
b: 'defaults.controllers.bar.hover',
c: 'options.hover',
d: 'options.controllers.bar.hover',
e: undefined,
f: undefined,
x: undefined,
y: undefined,
z: undefined
});
expect(resolver.hover.sub).toEqualOptions({
a: 'defaults.hover',
b: 'defaults.controllers.bar.hover',
c: 'options.hover',
d: 'options.controllers.bar.hover',
e: 'options.controllers.bar.hover.sub',
f: 'options.hover.sub',
x: undefined,
y: undefined,
z: undefined
});
});
it('should fallback throuhg multiple routes', function() {
const descriptors = {
_fallback: 'level1',
level1: {
_fallback: 'root'
},
level2: {
_fallback: 'level1'
}
};
2021-02-19 07:30:39 +01:00
const defaults = {
root: {
a: 'root'
},
level1: {
b: 'level1',
},
level2: {
level1: {
g: 'level2.level1'
},
c: 'level2',
sublevel1: {
d: 'sublevel1'
},
sublevel2: {
e: 'sublevel2',
level1: {
f: 'sublevel2.level1'
}
}
}
};
const resolver = _createResolver([defaults, descriptors]);
2021-02-19 07:30:39 +01:00
expect(resolver.level1).toEqualOptions({
a: 'root',
b: 'level1',
c: undefined
});
expect(resolver.level2).toEqualOptions({
a: 'root',
b: 'level1',
c: 'level2',
d: undefined
});
expect(resolver.level2.sublevel1).toEqualOptions({
a: 'root',
b: 'level1',
c: undefined,
2021-02-19 07:30:39 +01:00
d: 'sublevel1',
e: undefined,
f: undefined,
g: 'level2.level1'
});
expect(resolver.level2.sublevel2).toEqualOptions({
a: 'root',
b: 'level1',
c: undefined,
2021-02-19 07:30:39 +01:00
d: undefined,
e: 'sublevel2',
f: undefined,
g: 'level2.level1'
});
expect(resolver.level2.sublevel2.level1).toEqualOptions({
a: 'root',
b: 'level1',
c: undefined,
2021-02-19 07:30:39 +01:00
d: undefined,
e: undefined,
2021-02-19 07:30:39 +01:00
f: 'sublevel2.level1',
g: undefined // same key only included from immediate parents and root
});
});
it('should fallback through multiple routes (animations)', function() {
const descriptors = {
animations: {
_fallback: 'animation',
},
};
const defaults = {
animation: {
duration: 1000,
easing: 'easeInQuad'
},
animations: {
colors: {
properties: ['color', 'backgroundColor'],
type: 'color'
},
numbers: {
properties: ['x', 'y'],
type: 'number'
}
},
transitions: {
resize: {
animation: {
duration: 0
}
},
show: {
animation: {
duration: 400
},
animations: {
colors: {
from: 'transparent'
}
}
}
}
};
const options = {
animation: {
easing: 'linear'
},
animations: {
colors: {
properties: ['color', 'borderColor', 'backgroundColor'],
},
duration: {
properties: ['a', 'b'],
type: 'boolean'
}
}
};
const show = _createResolver([options, defaults.transitions.show, defaults, descriptors]);
expect(show.animation).toEqualOptions({
duration: 400,
easing: 'linear'
});
expect(show.animations.colors._scopes).toEqual([
options.animations.colors,
defaults.transitions.show.animations.colors,
defaults.animations.colors,
options.animation,
defaults.transitions.show.animation,
defaults.animation
]);
expect(show.animations.colors).toEqualOptions({
duration: 400,
from: 'transparent',
easing: 'linear',
type: 'color',
properties: ['color', 'borderColor', 'backgroundColor']
2021-02-19 07:30:39 +01:00
});
expect(show.animations.duration).toEqualOptions({
duration: 400,
easing: 'linear',
type: 'boolean',
properties: ['a', 'b']
});
expect(Object.getOwnPropertyNames(show.animations).filter(k => Chart.helpers.isObject(show.animations[k]))).toEqual([
'colors',
'duration',
'numbers',
]);
const def = _createResolver([options, defaults, descriptors]);
expect(def.animation).toEqualOptions({
duration: 1000,
easing: 'linear'
});
expect(def.animations.colors._scopes).toEqual([
options.animations.colors,
defaults.animations.colors,
options.animation,
defaults.animation
]);
expect(def.animations.colors).toEqualOptions({
duration: 1000,
easing: 'linear',
type: 'color',
properties: ['color', 'borderColor', 'backgroundColor']
});
expect(def.animations.duration).toEqualOptions({
duration: 1000,
easing: 'linear',
type: 'boolean',
properties: ['a', 'b']
});
expect(Object.getOwnPropertyNames(def.animations).filter(k => Chart.helpers.isObject(show.animations[k]))).toEqual([
'colors',
'duration',
'numbers',
]);
2021-02-19 07:30:39 +01:00
});
});
describe('setting values', function() {
it('should set values to first scope', function() {
const defaults = {
value: true
};
const options = {};
const resolver = _createResolver([options, defaults]);
resolver.value = false;
expect(options.value).toBeFalse();
expect(defaults.value).toBeTrue();
});
it('should set values of sub-objects to first scope', function() {
const defaults = {
sub: {
value: true
}
};
const options = {};
const resolver = _createResolver([options, defaults]);
resolver.sub.value = false;
expect(options.sub.value).toBeFalse();
expect(defaults.sub.value).toBeTrue();
});
it('should throw when setting a value and options is frozen', function() {
const defaults = Object.freeze({default: true});
const options = Object.freeze({value: true});
const resolver = _createResolver([options, defaults]);
function set() {
resolver.value = false;
}
expect(set).toThrow();
});
2021-02-19 07:30:39 +01:00
});
});
describe('_attachContext', function() {
it('should resolve to final values', function() {
const defaults = {
color: 'red',
backgroundColor: 'green',
hoverColor: (ctx, options) => getHoverColor(options.color)
};
const options = {
color: ['white', 'blue']
};
const resolver = _createResolver([options, defaults]);
const opts = _attachContext(resolver, {index: 1});
expect(opts.color).toEqual('blue');
expect(opts.backgroundColor).toEqual('green');
expect(opts.hoverColor).toEqual(getHoverColor('blue'));
});
it('should thrown on recursion', function() {
const options = {
foo: (ctx, opts) => opts.bar,
bar: (ctx, opts) => opts.xyz,
xyz: (ctx, opts) => opts.foo
};
const resolver = _createResolver([options]);
const opts = _attachContext(resolver, {test: true});
expect(function() {
return opts.foo;
}).toThrowError('Recursion detected: foo->bar->xyz->foo');
});
it('should support scriptable options in subscopes', function() {
const defaults = {
elements: {
point: {
backgroundColor: 'red'
}
}
};
const options = {
elements: {
point: {
borderColor: (ctx, opts) => getHoverColor(opts.backgroundColor)
}
}
};
const resolver = _createResolver([options, defaults]);
const opts = _attachContext(resolver, {});
expect(opts.elements.point.borderColor).toEqual(getHoverColor('red'));
expect(opts.elements.point.backgroundColor).toEqual('red');
});
it('same resolver should be usable with multiple contexts', function() {
const defaults = {
animation: {
delay: 10
}
};
const options = {
animation: (ctx) => ctx.index === 0 ? {duration: 1000} : {duration: 500}
};
const resolver = _createResolver([options, defaults]);
const opts1 = _attachContext(resolver, {index: 0});
const opts2 = _attachContext(resolver, {index: 1});
expect(opts1.animation.duration).toEqual(1000);
expect(opts1.animation.delay).toEqual(10);
expect(opts2.animation.duration).toEqual(500);
expect(opts2.animation.delay).toEqual(10);
});
it('should fall back from object returned from scriptable option', function() {
const defaults = {
mainScope: {
main: true,
subScope: {
sub: true
}
}
};
const options = {
mainScope: (ctx) => ({
mainTest: ctx.contextValue,
subScope: {
subText: 'a'
}
})
};
const opts = _attachContext(_createResolver([options, defaults]), {contextValue: 'test'});
expect(opts.mainScope).toEqualOptions({
main: true,
mainTest: 'test',
subScope: {
sub: true,
subText: 'a'
}
});
});
it('should resolve array of non-indexable objects properly', function() {
const defaults = {
label: {
value: 42,
text: (ctx) => ctx.text
},
labels: {
_fallback: 'label',
_indexable: false
}
};
const options = {
labels: [{text: 'a'}, {text: 'b'}, {value: 1}]
};
const opts = _attachContext(_createResolver([options, defaults]), {text: 'context'});
expect(opts).toEqualOptions({
labels: [
{
text: 'a',
value: 42
},
{
text: 'b',
value: 42
},
{
text: 'context',
value: 1
}
]
});
});
it('should support overriding options', function() {
const options = {
fn1: ctx => ctx.index,
fn2: ctx => ctx.type
};
const override = {
fn1: ctx => ctx.index * 2
};
const opts = _attachContext(_createResolver([options]), {index: 2, type: 'test'});
expect(opts).toEqualOptions({
fn1: 2,
fn2: 'test'
});
expect(opts.override(override)).toEqualOptions({
fn1: 4,
fn2: 'test'
});
});
it('should support changing context', function() {
const opts = _attachContext(_createResolver([{fn: ctx => ctx.test}]), {test: 1});
expect(opts.fn).toEqual(1);
expect(opts.setContext({test: 2}).fn).toEqual(2);
expect(opts.fn).toEqual(1);
});
it('should support common object methods', function() {
const defaults = {
option1: 'defaults'
};
class Options {
constructor() {
this.option2 = () => 'options';
}
get getter() {
return 'options getter';
}
}
const options = new Options();
const resolver = _createResolver([options, defaults]);
const opts = _attachContext(resolver, {index: 1});
expect(Object.prototype.hasOwnProperty.call(opts, 'option2')).toBeTrue();
expect(Object.prototype.hasOwnProperty.call(opts, 'option1')).toBeFalse();
expect(Object.prototype.hasOwnProperty.call(opts, 'getter')).toBeFalse();
expect(Object.prototype.hasOwnProperty.call(opts, 'nonexistent')).toBeFalse();
expect(Object.keys(opts)).toEqual(['option2']);
expect(Object.getOwnPropertyNames(opts)).toEqual(['option2', 'option1']);
expect('option2' in opts).toBeTrue();
expect('option1' in opts).toBeTrue();
expect('getter' in opts).toBeFalse();
expect('nonexistent' in opts).toBeFalse();
expect(opts instanceof Options).toBeTrue();
expect(opts.getter).toEqual('options getter');
expect('test' in opts).toBeFalse();
expect(opts.test).toBeUndefined();
opts.test = true;
expect('test' in opts).toBeTrue();
expect(opts.test).toBeTrue();
delete opts.test;
expect('test' in opts).toBeFalse();
opts.test = (ctx) => ctx.index;
expect('test' in opts).toBeTrue();
expect(opts.test).toBe(1);
delete opts.test;
expect('test' in opts).toBeFalse();
});
it('should not create proxy for adapters', function() {
const defaults = {
scales: {
time: {
adapters: {
date: {
locale: {
method: (arg) => arg === undefined ? 'ok' : 'fail'
}
}
}
}
}
};
const resolver = _createResolver([{}, defaults]);
const opts = _attachContext(resolver, {index: 1});
const fn = opts.scales.time.adapters.date.locale.method;
expect(typeof fn).toBe('function');
expect(fn()).toEqual('ok');
});
it('should properly set value to object in array of objects', function() {
const defaults = {};
const options = {
annotations: [{
value: 10
}, {
value: 20
}]
};
const resolver = _attachContext(_createResolver([options, defaults]), {test: true});
expect(resolver.annotations[0].value).toEqual(10);
resolver.annotations[0].value = 15;
expect(options.annotations[0].value).toEqual(15);
expect(options.annotations[1].value).toEqual(20);
});
describe('_indexable and _scriptable', function() {
it('should default to true', function() {
const options = {
array: [1, 2, 3],
func: (ctx) => ctx.index * 10
};
const opts = _attachContext(_createResolver([options]), {index: 1});
expect(opts.array).toEqual(2);
expect(opts.func).toEqual(10);
});
it('should allow false', function() {
const fn = () => 'test';
const options = {
_indexable: false,
_scriptable: false,
array: [1, 2, 3],
func: fn
};
const opts = _attachContext(_createResolver([options]), {index: 1});
expect(opts.array).toEqual([1, 2, 3]);
expect(opts.func).toEqual(fn);
expect(opts.func()).toEqual('test');
});
it('should allow function', function() {
const fn = () => 'test';
const options = {
_indexable: (prop) => prop !== 'array',
_scriptable: (prop) => prop === 'func',
array: [1, 2, 3],
array2: ['a', 'b', 'c'],
func: fn
};
const opts = _attachContext(_createResolver([options]), {index: 1});
expect(opts.array).toEqual([1, 2, 3]);
expect(opts.func).toEqual('test');
expect(opts.array2).toEqual('b');
});
});
});
});