validated-changeset
Version:
Changesets for your local state
1,585 lines (1,283 loc) • 112 kB
text/typescript
import { Changeset } from '../src';
import get from '../src/utils/get-deep';
import set from '../src/utils/set-deep';
import lookupValidator from '../src/utils/validator-lookup';
let dummyModel: any;
const exampleArray: Array<any> = [];
const dummyValidations: Record<string, any> = {
age(_k: string, value: any) {
return !!value ? value < 120 || '' : true;
},
name(_k: string, value: any) {
return (!!value && value.length > 3) || 'too short';
},
email(_k: string, value: any) {
const errors = [];
if (value && value.length < 5) {
errors.push('too short');
}
if (value && !value.includes('@')) {
errors.push('not an email');
}
if (errors.length < 1) {
return true;
}
return errors.length > 1 ? errors : errors[0];
},
password(_k: string, value: unknown) {
return !!value || ['foo', 'bar'];
},
passwordConfirmation(
_k: string,
newValue: unknown,
_oldValue: unknown,
{ password: changedPassword }: { password: string },
{ password }: { password: string }
) {
return (
(!!newValue && (changedPassword === newValue || password === newValue)) ||
"password doesn't match"
);
},
async(_k: string, value: unknown) {
return Promise.resolve(value || '');
},
options(_k: string, value: unknown) {
return !!value;
},
org: {
isCompliant(_k: string, value: unknown) {
return !!value || 'not provided';
},
usa: {
ny: [
(_k: string, value: unknown) => {
return !!value || 'must be present';
},
(_k: string, value: string) => {
return /[A-z]/.test(value) ? true : 'only letters work';
}
]
}
},
size: {
value(_k: string, value: unknown) {
return typeof value === 'number' || 'not a valid size.value';
}
}
};
function dummyValidator({
key,
newValue,
oldValue,
changes,
content
}: {
key: string;
newValue: unknown;
oldValue: unknown;
changes: any;
content: any;
}) {
const validatorFn = get(dummyValidations, key);
if (typeof validatorFn === 'function') {
return validatorFn(newValue, oldValue, changes, content);
}
}
describe('Unit | Utility | changeset', () => {
beforeEach(() => {
dummyModel = {
save() {
return Promise.resolve(this);
},
exampleArray
};
});
afterEach(() => {
dummyModel = {};
});
/**
* #toString
*/
it('content can be an empty hash', () => {
expect.assertions(1);
const emptyObject = {};
const dummyChangeset = Changeset(emptyObject, lookupValidator(dummyValidations));
expect(dummyChangeset.toString()).toEqual('changeset:[object Object]');
});
/**
* #error
*/
it('#error returns the error object and keeps changes', () => {
const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations));
const expectedResult = { name: { validation: 'too short', value: 'a' } };
dummyChangeset.set('name', 'a');
expect(dummyChangeset.error).toEqual(expectedResult);
expect(dummyChangeset.error.name).toEqual(expectedResult.name);
expect(dummyChangeset.get('error.name')).toEqual(expectedResult.name);
expect(dummyChangeset.change).toEqual({ name: 'a' });
expect(dummyChangeset.change.name).toEqual('a');
expect(dummyChangeset.get('change.name')).toEqual('a');
});
it('#error can use custom validator', () => {
const dummyChangeset = Changeset(dummyModel, dummyValidator);
const expectedResult = { name: { validation: 'too short', value: 'a' } };
dummyChangeset.set('name', 'a');
expect(dummyChangeset.error).toEqual(expectedResult);
expect(dummyChangeset.error.name).toEqual(expectedResult.name);
expect(dummyChangeset.get('error.name')).toEqual(expectedResult.name);
expect(dummyChangeset.change).toEqual({ name: 'a' });
expect(dummyChangeset.change.name).toEqual('a');
expect(dummyChangeset.get('change.name')).toEqual('a');
});
it('can get nested values in the error object', function () {
const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations));
const expectedResult = { validation: 'too short', value: 'a' };
dummyChangeset.set('name', 'a');
expect(dummyChangeset.get('error.name')).toEqual(expectedResult);
});
it('can can work with an array of nested validations', function () {
const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations));
const expectedResult = { validation: ['too short', 'not an email'], value: 'a' };
dummyChangeset.set('email', 'a');
expect(dummyChangeset.get('error.email')).toEqual(expectedResult);
});
/**
* #change
*/
it('#change returns the changes object', () => {
const dummyChangeset = Changeset(dummyModel);
const expectedResult = { name: 'a' };
dummyChangeset.set('name', 'a');
expect(dummyChangeset.change).toEqual(expectedResult);
});
it('#change supports `undefined`', () => {
const model = { name: 'a' };
const dummyChangeset = Changeset(model);
const expectedResult = { name: undefined };
dummyChangeset.set('name', undefined);
expect(dummyChangeset.change).toEqual(expectedResult);
});
it('#change works with arrays', () => {
const dummyChangeset = Changeset(dummyModel);
const newArray = [...exampleArray, 'new'];
const expectedResult = { exampleArray: newArray };
dummyChangeset.set('exampleArray', newArray);
expect(dummyChangeset.change).toEqual(expectedResult);
});
/**
* #errors
*/
it('#errors returns the error object and keeps changes', () => {
const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations));
let expectedResult = [{ key: 'name', validation: 'too short', value: 'a' }];
dummyChangeset.set('name', 'a');
expect(dummyChangeset.errors).toEqual(expectedResult);
expect(dummyChangeset.get('errors')).toEqual(expectedResult);
});
it('can get nested values in the errors object', () => {
const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations));
dummyChangeset.set('unknown', 'wat');
dummyChangeset.set('org.usa.ny', '');
dummyChangeset.set('name', '');
let expectedErrors = [
{ key: 'org.usa.ny', validation: ['must be present', 'only letters work'], value: '' },
{ key: 'name', validation: 'too short', value: '' }
];
expect(dummyChangeset.get('errors')).toEqual(expectedErrors);
dummyChangeset.set('org.usa.ny', '1');
expectedErrors = [
{ key: 'org.usa.ny', validation: ['only letters work'], value: '1' },
{ key: 'name', validation: 'too short', value: '' }
];
expect(dummyChangeset.get('errors')).toEqual(expectedErrors);
});
/**
* #changes
*/
/**
* #data
*/
it('data reads the changeset CONTENT', () => {
const dummyChangeset = Changeset(dummyModel);
expect(dummyChangeset.data).toEqual(dummyModel);
});
/**
* #isValid
*/
it('does not validate with default options', () => {
const dummyChangeset = Changeset(
dummyModel,
lookupValidator(dummyValidations),
dummyValidations,
{}
);
expect(dummyChangeset.get('isValid')).toBeTruthy();
});
it('does not validate when the initValidate option is set to false', () => {
const dummyChangeset = Changeset(
dummyModel,
lookupValidator(dummyValidations),
dummyValidations,
{ initValidate: false }
);
expect(dummyChangeset.get('isValid')).toBeTruthy();
});
it('validates when the initValidate option is set to true', () => {
const dummyChangeset = Changeset(
dummyModel,
lookupValidator(dummyValidations),
dummyValidations,
{ initValidate: true }
);
expect(dummyChangeset.get('isValid')).toBeFalsy();
});
it('validates when the initValidate option is set to true and the initial changeset data is valid', () => {
dummyModel.name = 'Bobby';
let validations: Record<string, any> = {
name(_k: string, value: any) {
return (!!value && value.length > 3) || 'too short';
}
};
const dummyChangeset = Changeset(dummyModel, lookupValidator(validations), validations, {
initValidate: true
});
expect(dummyChangeset.get('isValid')).toBeTruthy();
});
/**
* #isInvalid
*/
/**
* #isPristine
*/
it("isPristine returns true if changes are equal to content's values", () => {
dummyModel.name = 'Bobby';
dummyModel.thing = 123;
dummyModel.nothing = null;
const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations));
dummyChangeset.set('name', 'Bobby');
dummyChangeset.set('nothing', null);
expect(dummyChangeset.get('isPristine')).toBeTruthy();
});
it("isPristine returns false if changes are not equal to content's values", () => {
dummyModel.name = 'Bobby';
const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations));
dummyChangeset.set('name', 'Bobby');
dummyChangeset.set('thing', 123);
expect(dummyChangeset.get('isPristine')).toBeFalsy();
});
it('isPristine works with `null` values', () => {
dummyModel.name = null;
dummyModel.age = 15;
const dummyChangeset = Changeset(dummyModel);
expect(dummyChangeset.get('isPristine')).toBeTruthy();
dummyChangeset.set('name', 'Kenny');
expect(dummyChangeset.get('isPristine')).toBeFalsy();
dummyChangeset.set('name', null);
expect(dummyChangeset.get('isPristine')).toBeTruthy();
});
it('isPristine returns true if changes not in user provided changesetKeys', () => {
dummyModel.name = 'Bobby';
dummyModel.thing = 123;
dummyModel.nothing = null;
const changesetKeys = ['name'];
const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations), null, {
changesetKeys
});
dummyChangeset.set('nothing', 'something');
expect(dummyChangeset.get('isPristine')).toBe(true);
expect(dummyChangeset.get('isDirty')).toBe(false);
});
it('isPristine returns true if nested changes not in user provided changesetKeys', () => {
dummyModel.obj = { name: 'Bobby' };
const changesetKeys = ['name'];
const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations), null, {
changesetKeys
});
dummyChangeset.set('obj.name', 'something');
expect(dummyChangeset.get('isPristine')).toBe(true);
expect(dummyChangeset.get('isDirty')).toBe(false);
});
it('isPristine returns false if set a key in changesetKeys', () => {
dummyModel.name = 'Bobby';
dummyModel.thing = 123;
dummyModel.nothing = null;
const changesetKeys = ['razataz'];
const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations), null, {
changesetKeys
});
dummyChangeset.set('razataz', 'boom');
expect(dummyChangeset.get('isPristine')).toBe(false);
expect(dummyChangeset.get('isDirty')).toBe(true);
});
it('isPristine returns false if nested changes in user provided changesetKeys', () => {
const changesetKeys = ['org'];
const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations), null, {
changesetKeys
});
dummyChangeset.set('org.usa.ny', 'NYE');
expect(dummyChangeset.get('isPristine')).toBe(false);
expect(dummyChangeset.get('isDirty')).toBe(true);
});
it('isPristine returns true if nested path does not match at the deepest level', () => {
const changesetKeys = ['org.usa.ny'];
const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations), null, {
changesetKeys
});
dummyChangeset.set('org.usa', 'USA');
expect(dummyChangeset.get('isPristine')).toBe(true);
expect(dummyChangeset.get('isDirty')).toBe(false);
});
/**
* #isDirty
*/
it('#set dirties changeset', () => {
const dummyChangeset = Changeset(dummyModel);
dummyChangeset.set('name', 'foo');
expect(dummyChangeset.isDirty).toBe(true);
});
it('#isDirty after set', () => {
const dummyChangeset = Changeset(dummyModel);
dummyChangeset.set('name', 'foo');
expect(dummyChangeset.isDirty).toBe(true);
});
it('#isDirty reset after execute', () => {
dummyModel.name = {};
const dummyChangeset = Changeset(dummyModel);
dummyChangeset['name'] = {
short: 'foo'
};
expect(dummyChangeset.get('isDirty')).toBe(true);
dummyChangeset.execute();
expect(dummyChangeset.get('isDirty')).toBe(false);
});
it('#isDirty reset after rollback', () => {
dummyModel.name = {};
const dummyChangeset = Changeset(dummyModel);
dummyChangeset['name'] = {
short: 'foo'
};
expect(dummyChangeset.get('isDirty')).toBe(true);
dummyChangeset.rollback();
expect(dummyChangeset.get('isDirty')).toBe(false);
});
it('#isDirty is false when no set', () => {
dummyModel['name'] = { nick: 'bar' };
const dummyChangeset = Changeset(dummyModel);
dummyChangeset.name;
expect(dummyChangeset.isDirty).toBe(false);
});
it('#isDirty is false when no set with deep values', () => {
dummyModel['details'] = { name: { nick: 'bar' } };
const dummyChangeset = Changeset(dummyModel);
dummyChangeset.get('details.name');
expect(dummyChangeset.isDirty).toBe(false);
expect(dummyChangeset.change).toEqual({});
});
it('#isDirty is true when set with deep values', () => {
class Dog {
value: any;
constructor(value: any) {
this.value = value;
}
}
dummyModel['details'] = { name: {} };
const dummyChangeset = Changeset(dummyModel);
dummyChangeset.get('details.name');
const dogKlass = new Dog({ nickname: 'bar' });
dummyChangeset['details'] = { name: dogKlass };
expect(dummyChangeset.isDirty).toBe(true);
expect(dummyChangeset.change).toEqual({ details: { name: dogKlass } });
});
it('#set does not dirty changeset with same date', () => {
dummyModel.createTime = new Date('2013-05-01');
const dummyChangeset = Changeset(dummyModel);
dummyChangeset.set('createTime', new Date('2013-05-01'));
expect(dummyChangeset.isDirty).toBe(false);
});
/**
* #get
*/
it('#get proxies to content', () => {
dummyModel.name = 'Jim Bob';
const dummyChangeset = Changeset(dummyModel);
const result = dummyChangeset.name;
expect(result).toBe('Jim Bob');
});
it('#get proxies to content prototype', () => {
class Dog {
name?: string;
}
Dog.prototype.name = 'Jim Bob';
const dummyChangeset = Changeset(new Dog());
const result = dummyChangeset.name;
expect(result).toBe('Jim Bob');
});
it('#get returns the content when the proxied content is a class', () => {
class Moment {
date: unknown;
constructor(date: Date) {
this.date = date;
}
}
const d = new Date('2015');
const momentInstance = new Moment(d);
const c = Changeset({
startDate: momentInstance
});
const newValue = c.get('startDate');
expect(newValue.date).toEqual(momentInstance.date);
expect(newValue.content instanceof Moment).toBeTruthy();
expect(newValue.date).toBe(d);
});
it('#get handles changes that are non primitives', () => {
class Moment {
_isUTC: any;
date: unknown;
constructor(date: Date) {
this.date = date;
this._isUTC = false;
}
}
const d = new Date('2015');
const momentInstance = new Moment(d);
momentInstance._isUTC = true;
const c = Changeset({
startDate: momentInstance
});
let newValue = c.get('startDate');
expect(newValue.date).toEqual(momentInstance.date);
expect(newValue.content instanceof Moment).toBeTruthy();
expect(newValue.date).toBe(d);
expect(newValue._isUTC).toEqual(true);
const newD = new Date('2020');
const newMomentInstance = new Moment(newD);
c.set('startDate', newMomentInstance);
newValue = c.get('startDate');
newMomentInstance._isUTC = undefined;
expect(newValue).toEqual(newMomentInstance);
expect(newValue instanceof Moment).toBeTruthy();
expect(newValue.date).toBe(newD);
expect(newValue._isUTC).toBeUndefined();
});
it('#get merges sibling keys from CONTENT with CHANGES', () => {
class Moment {
_isUTC: boolean;
date: unknown;
constructor(date: Date) {
this.date = date;
this._isUTC = false;
}
}
const d = new Date('2015');
const momentInstance = new Moment(d);
momentInstance._isUTC = true;
const c = Changeset({
startDate: momentInstance
});
let newValue = c.get('startDate');
expect(newValue.date).toEqual(momentInstance.date);
expect(newValue.content instanceof Moment).toBeTruthy();
expect(newValue.date).toBe(d);
expect(newValue._isUTC).toEqual(true);
const newD = new Date('2020');
c.set('startDate.date', newD);
newValue = c.get('startDate');
expect(newValue.date).toEqual(newD);
expect(newValue.content instanceof Moment).toBeTruthy();
expect(newValue.date).toBe(newD);
expect(newValue._isUTC).toBe(true);
});
it('#get returns change if present', () => {
dummyModel.name = 'Jim Bob';
const dummyChangeset = Changeset(dummyModel);
dummyChangeset['name'] = 'Milton Waddams';
const result = dummyChangeset.name;
expect(result).toBe('Milton Waddams');
});
it('#get handles change without Change value', () => {
const shallowValidations = { ...dummyValidations };
shallowValidations.profile = (_k: string, newValue: any) => {
return typeof newValue === 'undefined' ? 'Cannot be blank' : true;
};
dummyModel.profile = {};
const c = Changeset(dummyModel, lookupValidator(shallowValidations));
c.profile;
c.validate('profile');
let result: any = c.error.profile;
expect(result).toBe(undefined);
c.set('profile', undefined);
c.validate('profile');
result = c.error.profile.validation;
expect(result).toBe('Cannot be blank');
});
it('#get returns change that is a blank value', () => {
dummyModel.name = 'Jim Bob';
const dummyChangeset = Changeset(dummyModel);
dummyChangeset['name'] = '';
const result = dummyChangeset.name;
expect(result).toBe('');
});
it('#get returns change that is has undefined as value', () => {
dummyModel.name = 'Jim Bob';
const dummyChangeset = Changeset(dummyModel);
dummyChangeset['name'] = undefined;
const result = dummyChangeset.name;
expect(result).toBeUndefined();
});
it('#get nested objects can contain arrays', () => {
expect.assertions(7);
dummyModel.name = 'Bob';
dummyModel.contact = {
emails: ['bob@email.com', 'the_bob@email.com']
};
expect(get(dummyModel, 'contact.emails')).toEqual(['bob@email.com', 'the_bob@email.com']);
const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations));
expect(dummyChangeset.get('name')).toBe('Bob');
expect(dummyChangeset.get('contact.emails')).toEqual(['bob@email.com', 'the_bob@email.com']);
dummyChangeset.set('contact.emails', ['fred@email.com', 'the_fred@email.com']);
expect(dummyChangeset.get('contact.emails')).toEqual(['fred@email.com', 'the_fred@email.com']);
dummyChangeset.rollback();
expect(dummyChangeset.get('contact.emails')).toEqual(['bob@email.com', 'the_bob@email.com']);
dummyChangeset.set('contact.emails', ['fred@email.com', 'the_fred@email.com']);
expect(dummyChangeset.get('contact.emails')).toEqual(['fred@email.com', 'the_fred@email.com']);
dummyChangeset.execute();
expect(dummyModel.contact.emails).toEqual(['fred@email.com', 'the_fred@email.com']);
});
it('#getted Object proxies to underlying method', () => {
class Dog {
breed: string;
constructor(b: string) {
this.breed = b;
}
bark() {
return `woof i'm a ${this.breed}`;
}
}
const model: Record<string, any> = {
foo: {
bar: {
dog: new Dog('shiba inu, wow')
}
}
};
{
const c = Changeset(model);
const actual = c.get('foo.bar.dog');
const expectedResult = "woof i'm a shiba inu, wow";
expect(actual.bark()).toEqual(expectedResult);
}
{
const c = Changeset(model);
const actual = get(c, 'foo.bar.dog');
const expectedResult = get(model, 'foo.bar.dog');
expect(actual).toEqual(expectedResult);
}
{
const c = Changeset(model);
const actual = get(c, 'foo.bar.dog');
const expectedResult = get(model, 'foo.bar.dog');
expect(actual).toEqual(expectedResult);
}
});
it('#get proxies to underlying array properties', () => {
dummyModel.users = ['user1', 'user2'];
const dummyChangeset = Changeset(dummyModel);
expect((dummyChangeset.users as Array<string>).length).toBe(2);
});
it('#get works if content is undefined for nested key', () => {
const model: Record<string, any> = {};
const c = Changeset(model);
c.set('foo.bar.cat', {
color: 'red'
});
const cat = c.get('foo.bar.cat');
expect(cat.color).toEqual('red');
});
it('#get works with toString override', () => {
dummyModel.toString = function () {
return 'mine';
};
const dummyChangeset = Changeset(dummyModel);
dummyChangeset['name'] = undefined;
const result = dummyChangeset.toString();
expect(result).toEqual('changeset:mine');
});
it('#get prioritizes own methods/getters', () => {
dummyModel.trigger = function (arg: any) {
expect(arg).toEqual('mine');
};
const dummyChangeset = Changeset(dummyModel);
dummyChangeset['name'] = undefined;
dummyChangeset.trigger('mine');
});
/**
* #set
*/
it('#set adds a change if valid', () => {
const expectedChanges = [{ key: 'name', value: 'foo' }];
const dummyChangeset = Changeset(dummyModel);
dummyChangeset.set('name', 'foo');
const changes = dummyChangeset.changes;
expect(dummyModel.name).toBeUndefined();
expect(dummyChangeset.get('name')).toEqual('foo');
expect(changes).toEqual(expectedChanges);
expect(dummyChangeset.isDirty).toBe(true);
expect(dummyChangeset.change).toEqual({ name: 'foo' });
});
it('#set adds a change with plain assignment without existing values', () => {
dummyModel['name'] = { nick: 'bar' };
const dummyChangeset = Changeset(dummyModel);
const proxy: any = dummyChangeset.name;
proxy['nick'] = 'foo';
expect(dummyChangeset.get('name.nick')).toEqual('foo');
const expectedChanges = [{ key: 'name.nick', value: 'foo' }];
const changes = dummyChangeset.changes;
expect(changes).toEqual(expectedChanges);
});
it('#set adds a change with plain assignment', () => {
dummyModel['name'] = 'bar';
const dummyChangeset = Changeset(dummyModel);
dummyChangeset['name'] = 'foo';
const changes = dummyChangeset.changes;
expect(dummyModel.name).toBe('bar');
expect(dummyChangeset.name).toEqual('foo');
const expectedChanges = [{ key: 'name', value: 'foo' }];
expect(changes).toEqual(expectedChanges);
});
it('#set adds a date', () => {
const d = new Date();
const expectedChanges = [{ key: 'dateOfBirth', value: d }];
const dummyChangeset = Changeset(dummyModel);
dummyChangeset.set('dateOfBirth', d);
const changes = dummyChangeset.changes;
expect(dummyModel.dateOfBirth).toBeUndefined();
expect(dummyChangeset.get('dateOfBirth')).toEqual(d);
expect(changes).toEqual(expectedChanges);
});
it('#set adds a date if already set on model', () => {
const model = { dateOfBirth: new Date() };
const dummyChangeset = Changeset(model);
const d = new Date('March 25, 1990');
dummyChangeset.set('dateOfBirth', d);
const changes = dummyChangeset.changes;
expect(dummyModel.dateOfBirth).toBeUndefined();
expect(dummyChangeset.get('dateOfBirth')).toEqual(d);
expect(dummyChangeset.dateOfBirth).toEqual(d);
const expectedChanges = [{ key: 'dateOfBirth', value: d }];
expect(changes).toEqual(expectedChanges);
});
it('#set Ember.set works', () => {
const expectedChanges = [{ key: 'name', value: 'foo' }];
const dummyChangeset = Changeset(dummyModel);
dummyChangeset['name'] = 'foo';
expect(dummyModel.name).toBeUndefined();
expect(dummyChangeset.get('name')).toBe('foo');
const changes = dummyChangeset.changes;
expect(changes).toEqual(expectedChanges);
dummyChangeset.execute();
expect(dummyModel.name).toBe('foo');
expect(dummyChangeset.get('name')).toBe('foo');
});
it('#set works for nested', () => {
const expectedChanges = [{ key: 'name', value: { short: 'foo' } }];
dummyModel.name = {};
dummyModel.org = {};
const dummyChangeset = Changeset(dummyModel);
dummyChangeset['name'] = {
short: 'foo'
};
expect(dummyChangeset.get('name.short')).toBe('foo');
expect(dummyModel.name).toEqual({});
const changes = dummyChangeset.changes;
expect(changes).toEqual(expectedChanges);
expect(dummyChangeset.name).toEqual({ short: 'foo' });
expect(dummyChangeset.org).toEqual({});
dummyChangeset.execute();
expect(dummyModel.name.short).toBe('foo');
});
it('#set overrides', () => {
const expectedChanges = [{ key: 'age', value: '90' }];
let dummyChangeset = Changeset({ age: '10' });
dummyChangeset.set('age', '80');
dummyChangeset.set('age', '10');
dummyChangeset.set('age', '90');
const changes = dummyChangeset.changes;
expect(dummyModel.age).toBeUndefined();
expect(dummyChangeset.get('age')).toEqual('90');
expect(changes).toEqual(expectedChanges);
expect(dummyChangeset.isDirty).toBe(true);
expect(dummyChangeset.change).toEqual({ age: '90' });
});
test('#set Ember.set with Object actually does work TWICE for nested', () => {
set(dummyModel, 'name', {});
let title1 = { id: 'Mr', description: 'Mister' };
let title2 = { id: 'Mrs', description: 'Missus' };
let dummyChangeset: any = Changeset(dummyModel);
set(dummyChangeset, 'name.title', title1);
expect(get(dummyModel, 'name.title.id')).toBeUndefined();
expect(dummyChangeset.name.title.id).toEqual('Mr');
expect(dummyChangeset.get('name.title.id')).toEqual('Mr');
let changes = get(dummyChangeset, 'changes');
expect(changes).toEqual([{ key: 'name.title', value: title1 }]);
set(dummyChangeset, 'name.title', title2);
expect(get(dummyModel, 'name.title.id')).toBeUndefined();
expect(dummyChangeset.name.title.id).toEqual('Mrs');
expect(dummyChangeset.get('name.title.id')).toEqual('Mrs');
changes = get(dummyChangeset, 'changes');
expect(changes).toEqual([{ key: 'name.title', value: title2 }]);
dummyChangeset.execute();
expect(dummyModel.name.title.id).toEqual('Mrs');
});
test('#set with Object should work TWICE for nested', () => {
set(dummyModel, 'name', {});
let title1 = { id: 'Mr', description: 'Mister' };
let title2 = { id: 'Mrs', description: 'Missus' };
let dummyChangeset: any = Changeset(dummyModel);
dummyChangeset.set('name.title', title1);
expect(get(dummyModel, 'name.title.id')).toBeUndefined();
expect(dummyChangeset.name.title.id).toEqual('Mr');
expect(dummyChangeset.get('name.title.id')).toEqual('Mr');
let changes = dummyChangeset.changes;
expect(changes).toEqual([{ key: 'name.title', value: title1 }]);
dummyChangeset.set('name.title', title2);
expect(get(dummyModel, 'name.title.id')).toBeUndefined();
expect(dummyChangeset.name.title.id).toEqual('Mrs');
expect(dummyChangeset.get('name.title.id')).toEqual('Mrs');
changes = dummyChangeset.changes;
expect(changes).toEqual([{ key: 'name.title', value: title2 }]);
dummyChangeset.execute();
expect(dummyModel.name.title.id).toEqual('Mrs');
});
describe('arrays within nested objects', () => {
describe('#set', () => {
let initialData: { contact: { emails?: string[] } } = { contact: { emails: [] } };
beforeEach(() => {
initialData = { contact: { emails: ['bob@email.com'] } };
});
it('works with boolean values', () => {
let initialData = { contact: { emails: [{}, {}] } };
const changeset = Changeset(initialData);
changeset.set('contact.emails.2', { nested: false });
expect(changeset.get('contact.emails.2')).toEqual({ nested: false });
changeset.set('contact.emails.2.nested', true);
expect(changeset.get('contact.emails.2')).toEqual({ nested: true });
changeset.set('contact.emails.2.nested', false);
expect(changeset.get('contact.emails.2')).toEqual({ nested: false });
});
it('nested objects cannot create arrays when we have no hints', () => {
initialData.contact = {};
const changeset = Changeset(initialData);
expect(changeset.get('contact.emails')).toEqual(undefined);
changeset.set('contact.emails.0', 'fred@email.com');
expect(changeset.get('contact.emails.0')).toEqual('fred@email.com');
expect(changeset.get('contact.emails')).toEqual({ '0': 'fred@email.com' });
});
it('works with validations', () => {
const changeset = Changeset(
initialData,
lookupValidator({
contact: {
emails: [
(_k: string, value: any) => {
if (value.includes('fred')) {
return 'Fred is banned';
}
}
]
}
})
);
expect(changeset.isValid).toEqual(true);
changeset.set('contact.emails.0', 'fred@email.com');
expect(changeset.isValid).toEqual(false);
expect(changeset.isDirty).toEqual(true);
expect(changeset.errors).toEqual([
{ key: 'contact.emails.0', validation: 'Fred is banned', value: 'fred@email.com' }
]);
});
it('can be rolled back', () => {
const changeset = Changeset(initialData);
changeset.set('contact.emails.0', 'fred@email.com');
expect(changeset.get('contact.emails.0')).toEqual('fred@email.com');
expect(changeset.changes).toEqual([{ key: 'contact.emails.0', value: 'fred@email.com' }]);
expect(changeset.get('contact.emails').unwrap()).toEqual(['fred@email.com']);
changeset.rollback();
expect(changeset.get('contact.emails.0')).toEqual('bob@email.com');
expect(changeset.changes).toEqual([]);
expect(changeset.get('contact.emails')).toEqual(['bob@email.com']);
});
it('can be saved', () => {
const changeset = Changeset(initialData);
changeset.set('contact.emails.0', 'fred@email.com');
expect(changeset.get('contact.emails.0')).toEqual('fred@email.com');
expect(changeset.changes).toEqual([{ key: 'contact.emails.0', value: 'fred@email.com' }]);
changeset.save();
expect(changeset.get('contact.emails.0')).toEqual('fred@email.com');
expect(changeset.changes).toEqual([]);
});
it('can add items to the array', () => {
const changeset = Changeset(initialData);
changeset.set('contact.emails.1', 'fred@email.com');
expect(changeset.get('contact.emails.1')).toEqual('fred@email.com');
expect(changeset.get('contact.emails').unwrap()).toEqual([
'bob@email.com',
'fred@email.com'
]);
expect(changeset.changes).toEqual([{ key: 'contact.emails.1', value: 'fred@email.com' }]);
changeset.set('contact.emails.3', 'greg@email.com');
expect(changeset.get('contact.emails.3')).toEqual('greg@email.com');
expect(changeset.get('contact.emails').unwrap()).toEqual([
'bob@email.com',
'fred@email.com',
undefined,
'greg@email.com'
]);
expect(changeset.changes).toEqual([
{ key: 'contact.emails.1', value: 'fred@email.com' },
{ key: 'contact.emails.3', value: 'greg@email.com' }
]);
expect(changeset.change).toEqual({
contact: { emails: { 1: 'fred@email.com', 3: 'greg@email.com' } }
});
});
it('can remove items from the array', () => {
const changeset = Changeset(initialData);
changeset.set('contact.emails.1', 'fred@email.com');
expect(changeset.get('contact.emails.1')).toEqual('fred@email.com');
expect(changeset.get('contact.emails').unwrap()).toEqual([
'bob@email.com',
'fred@email.com'
]);
expect(changeset.changes).toEqual([{ key: 'contact.emails.1', value: 'fred@email.com' }]);
changeset.set('contact.emails.0', null);
expect(changeset.get('contact.emails.0')).toEqual(null);
expect(changeset.get('contact.emails').unwrap()).toEqual([null, 'fred@email.com']);
expect(changeset.changes).toEqual([
{ key: 'contact.emails.0', value: null },
{ key: 'contact.emails.1', value: 'fred@email.com' }
]);
changeset.set('contact.emails.1', null);
expect(changeset.get('contact.emails').unwrap()).toEqual([null, null]);
expect(changeset.changes).toEqual([
{ key: 'contact.emails.0', value: null },
{ key: 'contact.emails.1', value: null }
]);
});
it('can add an item to an index in an array where that item was previously removed', () => {
const deepObj = (email: string) => ({
emails: {
primary: email
}
});
const bob = deepObj('bob@email.com');
const fred = deepObj('fred@email.com');
const sanHolo = deepObj('sanholo@email.com');
const changeset = Changeset({
contacts: [bob, fred]
});
// "Delete" array element
changeset.set('contacts.0', null);
expect(changeset.isDirty).toBeTruthy();
expect(changeset.get('contacts.0')).toEqual(null);
expect(changeset.get('contacts')).toEqual([null, fred]);
expect(changeset.changes).toEqual([{ key: 'contacts.0', value: null }]);
// Set array element to entirely new object
changeset.set('contacts.0', sanHolo);
expect(changeset.isDirty).toBeTruthy();
expect(changeset.get('contacts')).toEqual([sanHolo, fred]);
expect(changeset.get('contacts.0.emails.primary')).toEqual('sanholo@email.com');
expect(changeset.changes).toEqual([{ key: 'contacts.0', value: sanHolo }]);
// "Delete" array element again
changeset.set('contacts.0', null);
expect(changeset.isDirty).toBeTruthy();
expect(changeset.get('contacts.0')).toEqual(null);
expect(changeset.get('contacts')).toEqual([null, fred]);
expect(changeset.changes).toEqual([{ key: 'contacts.0', value: null }]);
// Revert everything
changeset.rollback();
expect(changeset.isDirty).toBeFalsy();
expect(changeset.changes).toEqual([]);
expect(changeset.get('contacts')).toEqual([bob, fred]);
});
xit(`negative values are not allowed`, () => {
// This test is currently disabled because setDeep doesn't have a reference to the
// original array and setDeep is where we'd throw on invalid key values
const changeset = Changeset(initialData);
expect(changeset.get('contact.emails')).toEqual(['bob@email.com']);
expect(() => {
changeset.set('contact.emails.-1', 'fred@email.com');
}).toThrow(
'Negative indices are not allowed as arrays do not serialize values at negative indices'
);
});
});
});
describe('arrays as values of top level objects', () => {
let initialData: { emails: Record<string, string>[] } = { emails: [] };
beforeEach(() => {
initialData = { emails: [{ primary: 'bob@email.com' }] };
});
it('can modify properties on an entry', () => {
const changeset = Changeset(initialData);
changeset.set('emails.0.primary', 'fun@email.com');
expect(changeset.get('emails.0.primary')).toEqual('fun@email.com');
expect(changeset.get('emails')).toEqual([{ primary: 'fun@email.com' }]);
expect(changeset.changes).toEqual([{ key: 'emails.0.primary', value: 'fun@email.com' }]);
});
it('can add properties to an entry', () => {
const changeset = Changeset(initialData);
changeset.set('emails.0.funEmail', 'fun@email.com');
expect(changeset.get('emails.0.funEmail')).toEqual('fun@email.com');
expect(changeset.changes).toEqual([{ key: 'emails.0.funEmail', value: 'fun@email.com' }]);
expect(changeset.get('emails')).toEqual([
{ primary: 'bob@email.com', funEmail: 'fun@email.com' }
]);
});
it('can add new properties to new entries', () => {
const changeset = Changeset(initialData);
changeset.set('emails.1.funEmail', 'fun@email.com');
changeset.set('emails.1.primary', 'primary@email.com');
expect(changeset.get('emails.1.funEmail')).toEqual('fun@email.com');
expect(changeset.get('emails.1.primary')).toEqual('primary@email.com');
expect(changeset.get('emails')).toEqual([
{ primary: 'bob@email.com' },
{ primary: 'primary@email.com', funEmail: 'fun@email.com' }
]);
expect(changeset.changes).toEqual([
{ key: 'emails.1.funEmail', value: 'fun@email.com' },
{ key: 'emails.1.primary', value: 'primary@email.com' }
]);
});
it('can add a new object all at once, and edit it', () => {
const changeset = Changeset(initialData);
changeset.set('emails.1', {
funEmail: 'fun@email.com',
primary: 'primary@email.com'
});
expect(changeset.get('emails.1.funEmail')).toEqual('fun@email.com');
expect(changeset.get('emails.1.primary')).toEqual('primary@email.com');
expect(changeset.get('emails')).toEqual([
{ primary: 'bob@email.com' },
{ primary: 'primary@email.com', funEmail: 'fun@email.com' }
]);
expect(changeset.changes).toEqual([
{
key: 'emails.1',
value: { funEmail: 'fun@email.com', primary: 'primary@email.com' }
}
]);
changeset.set('emails.1.primary', 'primary2@email.com');
expect(changeset.get('emails.1.primary')).toEqual('primary2@email.com');
expect(changeset.changes).toEqual([
{
key: 'emails.1',
value: {
primary: 'primary2@email.com',
funEmail: 'fun@email.com'
}
}
]);
expect(changeset.get('emails')).toEqual([
{ primary: 'bob@email.com' },
{ primary: 'primary2@email.com', funEmail: 'fun@email.com' }
]);
});
it('can edit a new object that was added after deleting an array entry', () => {
const changeset = Changeset({
emails: [
{
fun: 'fun0@email.com',
primary: 'primary0@email.com'
},
{
fun: 'fun1@email.com',
primary: 'primary1@email.com'
}
]
});
changeset.set('emails.1', null);
expect(changeset.get('emails.0.fun')).toEqual('fun0@email.com');
expect(changeset.get('emails.0.primary')).toEqual('primary0@email.com');
expect(changeset.get('emails')).toEqual([
{
fun: 'fun0@email.com',
primary: 'primary0@email.com'
},
null
]);
expect(changeset.changes).toEqual([
{
key: 'emails.1',
value: null
}
]);
changeset.set('emails.1', {
fun: 'brandNew@email.com',
primary: 'brandNewPrimary@email.com'
});
expect(changeset.get('emails')).toEqual([
{
fun: 'fun0@email.com',
primary: 'primary0@email.com'
},
{
fun: 'brandNew@email.com',
primary: 'brandNewPrimary@email.com'
}
]);
expect(changeset.changes).toEqual([
{
key: 'emails.1',
value: {
fun: 'brandNew@email.com',
primary: 'brandNewPrimary@email.com'
}
}
]);
});
it('can edit an object with a key of value after another array entry has been deleted', () => {
const changeset = Changeset({
emails: [
{
fun: 'fun0@email.com',
primary: 'primary0@email.com',
value: 'the value'
},
{
fun: 'fun1@email.com',
primary: 'primary1@email.com',
value: 'some value'
}
]
});
changeset.set('emails.1', null);
expect(changeset.get('emails')).toEqual([
{
fun: 'fun0@email.com',
primary: 'primary0@email.com',
value: 'the value'
},
null
]);
expect(changeset.changes).toEqual([
{
key: 'emails.1',
value: null
}
]);
expect(changeset.get('emails.0.fun')).toEqual('fun0@email.com');
expect(changeset.get('emails.0.primary')).toEqual('primary0@email.com');
// does not need to be unwrapped
expect(changeset.get('emails.0.value')).toEqual('the value');
});
});
describe('arrays of objects within nested objects', () => {
describe('#set', () => {
let initialData: { contact: { emails: Record<string, string>[] } } = {
contact: { emails: [] }
};
beforeEach(() => {
initialData = { contact: { emails: [{ primary: 'bob@email.com' }] } };
});
it('can modify properties on an entry', () => {
const changeset = Changeset(initialData);
changeset.set('contact.emails.0.primary', 'fun@email.com');
expect(changeset.get('contact.emails.0.primary')).toEqual('fun@email.com');
expect(changeset.get('contact.emails').unwrap()).toEqual([{ primary: 'fun@email.com' }]);
expect(changeset.changes).toEqual([
{ key: 'contact.emails.0.primary', value: 'fun@email.com' }
]);
});
it('can add properties to an entry', () => {
const changeset = Changeset(initialData);
changeset.set('contact.emails.0.funEmail', 'fun@email.com');
expect(changeset.get('contact.emails.0.funEmail')).toEqual('fun@email.com');
expect(changeset.changes).toEqual([
{ key: 'contact.emails.0.funEmail', value: 'fun@email.com' }
]);
expect(changeset.get('contact.emails').unwrap()).toEqual([
{ primary: 'bob@email.com', funEmail: 'fun@email.com' }
]);
});
it('can add new properties to new entries', () => {
const changeset = Changeset(initialData);
changeset.set('contact.emails.1.funEmail', 'fun@email.com');
changeset.set('contact.emails.1.primary', 'primary@email.com');
expect(changeset.get('contact.emails.1.funEmail')).toEqual('fun@email.com');
expect(changeset.get('contact.emails.1.primary')).toEqual('primary@email.com');
expect(changeset.get('contact.emails').unwrap()).toEqual([
{ primary: 'bob@email.com' },
{ primary: 'primary@email.com', funEmail: 'fun@email.com' }
]);
expect(changeset.changes).toEqual([
{ key: 'contact.emails.1.funEmail', value: 'fun@email.com' },
{ key: 'contact.emails.1.primary', value: 'primary@email.com' }
]);
});
it('can add a new object all at once, and edit it', () => {
const changeset = Changeset(initialData);
changeset.set('contact.emails.1', {
funEmail: 'fun@email.com',
primary: 'primary@email.com'
});
expect(changeset.get('contact.emails.1.funEmail')).toEqual('fun@email.com');
expect(changeset.get('contact.emails.1.primary')).toEqual('primary@email.com');
expect(changeset.get('contact.emails').unwrap()).toEqual([
{ primary: 'bob@email.com' },
{ primary: 'primary@email.com', funEmail: 'fun@email.com' }
]);
expect(changeset.changes).toEqual([
{
key: 'contact.emails.1',
value: { funEmail: 'fun@email.com', primary: 'primary@email.com' }
}
]);
changeset.set('contact.emails.1.primary', 'primary2@email.com');
expect(changeset.get('contact.emails.1.primary')).toEqual('primary2@email.com');
expect(changeset.changes).toEqual([
{
key: 'contact.emails.1',
value: {
primary: 'primary2@email.com',
funEmail: 'fun@email.com'
}
}
]);
expect(changeset.get('contact.emails').unwrap()).toEqual([
{ primary: 'bob@email.com' },
{ primary: 'primary2@email.com', funEmail: 'fun@email.com' }
]);
});
it('can edit a new object that was added after deleting an array entry', () => {
const changeset = Changeset({
contacts: {
emails: [
{
fun: 'fun0@email.com',
primary: 'primary0@email.com'
},
{
fun: 'fun1@email.com',
primary: 'primary1@email.com'
}
]
}
});
changeset.set('contacts.emails.1', null);
expect(changeset.get('contacts.emails').unwrap()).toEqual([
{
fun: 'fun0@email.com',
primary: 'primary0@email.com'
},
null
]);
});
});
});
it('#set works for nested when the root key is "value"', () => {
dummyModel.value = {};
dummyModel.org = {};
const dummyChangeset = Changeset(dummyModel);
dummyChangeset.set('value.short', 'foo');
expect(dummyChangeset.get('value.short')).toBe('foo');
expect(dummyModel.value).toEqual({});
const changes = dummyChangeset.changes;
const expectedChanges = [{ key: 'value.short', value: 'foo' }];
expect(changes).toEqual(expectedChanges);
expect(dummyChangeset.value).toEqual({ short: 'foo' });
expect(dummyChangeset.org).toEqual({});
dummyChangeset.execute();
expect(dummyModel.value.short).toBe('foo');
});
it('nested objects can be replaced with different ones without changing the nested return values', () => {
dummyModel['org'] = { usa: { ny: 'ny' } };
const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations));
dummyChangeset.set('org', { usa: { ca: 'ca' } });
expect(dummyChangeset.get('org')).toEqual({ usa: { ca: 'ca', ny: undefined } });
expect(dummyChangeset.get('org.usa')).toEqual({ ca: 'ca', ny: undefined });
expect(dummyChangeset.get('org.usa.ca')).toBe('ca');
expect(dummyChangeset.get('org.usa.ny')).toBeUndefined();
});
it('nested objects can be replaced with different ones as classes', () => {
class Country {
details: object;
constructor(details: object) {
this.details = details;
}
}
dummyModel['org'] = new Country({ usa: { ny: 'ny' } });
const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations));
dummyChangeset.set('org', new Country({ usa: { ca: 'ca' } }));
expect(dummyChangeset.get('org')).toEqual(new Country({ usa: { ca: 'ca', ny: undefined } }));
expect(dummyChangeset.get('org.details')).toEqual({ usa: { ca: 'ca', ny: undefined } });
expect(dummyChangeset.get('org.details.usa')).toEqual({ ca: 'ca', ny: undefined });
expect(dummyChangeset.get('org.details.usa.ca')).toBe('ca');
expect(dummyChangeset.get('org.details.usa.ny')).toBeUndefined();
});
it('#set doesnt lose sibling keys', () => {
dummyModel['org'] = {
usa: {
mn: 'mn',
ny: 'ny',
nz: 'nz'
},
landArea: 100
};
const c: Record<string, any> = Changeset(dummyModel);
c.set('org.usa.ny', 'NY');
expect(dummyModel.org.usa.ny).toBe('ny');
expect(c.org.usa.ny).toBe('NY');
expect(c.get('org.usa.ny')).toBe('NY');
expect(c.get('org.usa.mn')).toBe('mn');
expect(c.get('org.usa.nz')).toBe('nz');
expect(c.get('org.landArea')).toBe(100);
// set again
c.set('org.usa.ny', 'nye');
expect(dummyModel.org.usa.ny).toBe('ny');
expect(c.org.usa.ny).toBe('nye');
expect(c.get('org.usa.ny')).toBe('nye');
expect(c.get('org.usa.mn')).toBe('mn');
expect(c.get('org.usa.nz')).toBe('nz');
expect(c.get('org.landArea')).toBe(100);
});
it('#set adds a change if the key is an object', () => {
dummyModel['org'] = {
usa: {
mn: 'mn',
ny: 'ny',
nz: 'nz'
},
landArea: 100
};
const c: any = Changeset(dummyModel);
c.set('org.usa.ny', 'NY');
expect(dummyModel.org.usa.ny).toBe('ny');
expect(c.org.usa.ny).toBe('NY');
expect(c.get('org.usa.ny')).toBe('NY');
expect(c.get('org.usa.mn')).toBe('mn');
expect(c.get('org.usa.nz')).toBe('nz');
expect(c.get('org.landArea')).toBe(100);
const expectedChanges = [{ key: 'org.usa.ny', value: 'NY' }];
const changes = c.changes;
expect(changes).toEqual(expectedChanges);
});
it('#set use native setters with nested doesnt work', () => {
dummyModel['org'] = {
usa: {
ny: 'ny'
}
};
const c = Changeset(dummyModel);
set(c, 'org.usa.ny', 'foo');
expect(dummyModel.org.usa.ny).toBe('foo');
expect(c.get('org.usa.ny')).toBe('foo');
const changes = c.changes;
expect(changes).toEqual([]);
});
it('#set use native setters at single level', () => {
dummyModel.org = 'ny';
const c = Changeset(dummyModel);
c.org = 'foo';
expect(dummyModel.org).toBe('ny');
expect(c.or