test: 100% coverage for observer

This commit is contained in:
Evan You 2018-09-21 09:52:46 -04:00
parent bf38fea313
commit bb0e15de4d
6 changed files with 65 additions and 38 deletions

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
dist dist
.DS_Store .DS_Store
node_modules node_modules
coverage
explorations explorations
TODOs.md TODOs.md

View File

@ -6,7 +6,8 @@
"scripts": { "scripts": {
"dev": "node scripts/dev.js", "dev": "node scripts/dev.js",
"build": "node scripts/build.js", "build": "node scripts/build.js",
"lint": "prettier --write --parser typescript 'packages/**/*.ts'" "lint": "prettier --write --parser typescript 'packages/**/*.ts'",
"test": "jest"
}, },
"gitHooks": { "gitHooks": {
"pre-commit": "lint-staged", "pre-commit": "lint-staged",

View File

@ -621,6 +621,10 @@ describe('observer/autorun', () => {
stop(runner) stop(runner)
obj.prop = 3 obj.prop = 3
expect(dummy).toBe(2) expect(dummy).toBe(2)
// stopped runner should still be manually callable
runner()
expect(dummy).toBe(3)
}) })
it('markNonReactive', () => { it('markNonReactive', () => {

View File

@ -52,16 +52,26 @@ describe('observer/immutable', () => {
observed.bar.baz = 3 observed.bar.baz = 3
expect(observed.bar.baz).toBe(2) expect(observed.bar.baz).toBe(2)
expect(warn).toHaveBeenCalledTimes(2) expect(warn).toHaveBeenCalledTimes(2)
delete observed.foo
expect(observed.foo).toBe(1)
expect(warn).toHaveBeenCalledTimes(3)
delete observed.bar.baz
expect(observed.bar.baz).toBe(2)
expect(warn).toHaveBeenCalledTimes(4)
}) })
it('should allow mutation when unlocked', () => { it('should allow mutation when unlocked', () => {
const observed = immutable({ foo: 1, bar: { baz: 2 } }) const observed: any = immutable({ foo: 1, bar: { baz: 2 } })
unlock() unlock()
observed.foo = 2 observed.prop = 2
observed.bar.baz = 3 observed.bar.qux = 3
delete observed.bar.baz
delete observed.foo
lock() lock()
expect(observed.foo).toBe(2) expect(observed.prop).toBe(2)
expect(observed.bar.baz).toBe(3) expect(observed.foo).toBeUndefined()
expect(observed.bar.qux).toBe(3)
expect('baz' in observed.bar).toBe(false)
expect(warn).not.toHaveBeenCalled() expect(warn).not.toHaveBeenCalled()
}) })
@ -190,7 +200,9 @@ describe('observer/immutable', () => {
lock() lock()
}) })
}) })
;[Map, WeakMap].forEach((Collection: any) => {
const maps = [Map, WeakMap]
maps.forEach((Collection: any) => {
describe(Collection.name, () => { describe(Collection.name, () => {
test('should make nested values immutable', () => { test('should make nested values immutable', () => {
const key1 = {} const key1 = {}
@ -224,22 +236,25 @@ describe('observer/immutable', () => {
test('should allow mutation & trigger autorun when unlocked', () => { test('should allow mutation & trigger autorun when unlocked', () => {
const map = immutable(new Collection()) const map = immutable(new Collection())
const isWeak = Collection === WeakMap
const key = {} const key = {}
let dummy let dummy
autorun(() => { autorun(() => {
dummy = map.get(key) dummy = map.get(key) + (isWeak ? 0 : map.size)
}) })
expect(dummy).toBeUndefined() expect(dummy).toBeNaN()
unlock() unlock()
map.set(key, 1) map.set(key, 1)
lock() lock()
expect(dummy).toBe(1) expect(dummy).toBe(isWeak ? 1 : 2)
expect(map.get(key)).toBe(1) expect(map.get(key)).toBe(1)
expect(warn).not.toHaveBeenCalled() expect(warn).not.toHaveBeenCalled()
}) })
}) })
}) })
;[Set, WeakSet].forEach((Collection: any) => {
const sets = [Set, WeakSet]
sets.forEach((Collection: any) => {
describe(Collection.name, () => { describe(Collection.name, () => {
test('should make nested values immutable', () => { test('should make nested values immutable', () => {
const key1 = {} const key1 = {}

View File

@ -11,18 +11,21 @@ const builtInSymbols = new Set(
.filter(value => typeof value === 'symbol') .filter(value => typeof value === 'symbol')
) )
function get( function makeGetter(isImmutable: boolean) {
target: any, return function get(target: any, key: string | symbol, receiver: any) {
key: string | symbol,
receiver: any,
toObservable: (t: any) => any
) {
const res = Reflect.get(target, key, receiver) const res = Reflect.get(target, key, receiver)
if (typeof key === 'symbol' && builtInSymbols.has(key)) { if (typeof key === 'symbol' && builtInSymbols.has(key)) {
return res return res
} }
track(target, OperationTypes.GET, key) track(target, OperationTypes.GET, key)
return res !== null && typeof res === 'object' ? toObservable(res) : res return res !== null && typeof res === 'object'
? isImmutable
? // need to lazy access immutable and observable here to avoid
// circular dependency
immutable(res)
: observable(res)
: res
}
} }
function set( function set(
@ -37,6 +40,7 @@ function set(
const result = Reflect.set(target, key, value, receiver) const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original // don't trigger if target is something up in the prototype chain of original
if (target === unwrap(receiver)) { if (target === unwrap(receiver)) {
/* istanbul ignore else */
if (__DEV__) { if (__DEV__) {
const extraInfo = { oldValue, newValue: value } const extraInfo = { oldValue, newValue: value }
if (!hadKey) { if (!hadKey) {
@ -60,6 +64,7 @@ function deleteProperty(target: any, key: string | symbol): boolean {
const oldValue = target[key] const oldValue = target[key]
const result = Reflect.deleteProperty(target, key) const result = Reflect.deleteProperty(target, key)
if (hadKey) { if (hadKey) {
/* istanbul ignore else */
if (__DEV__) { if (__DEV__) {
trigger(target, OperationTypes.DELETE, key, { oldValue }) trigger(target, OperationTypes.DELETE, key, { oldValue })
} else { } else {
@ -81,8 +86,7 @@ function ownKeys(target: any): (string | number | symbol)[] {
} }
export const mutableHandlers: ProxyHandler<any> = { export const mutableHandlers: ProxyHandler<any> = {
get: (target: any, key: string | symbol, receiver: any) => get: makeGetter(false),
get(target, key, receiver, observable),
set, set,
deleteProperty, deleteProperty,
has, has,
@ -90,8 +94,7 @@ export const mutableHandlers: ProxyHandler<any> = {
} }
export const immutableHandlers: ProxyHandler<any> = { export const immutableHandlers: ProxyHandler<any> = {
get: (target: any, key: string | symbol, receiver: any) => get: makeGetter(true),
get(target, key, receiver, LOCKED ? immutable : observable),
set(target: any, key: string | symbol, value: any, receiver: any): boolean { set(target: any, key: string | symbol, value: any, receiver: any): boolean {
if (LOCKED) { if (LOCKED) {

View File

@ -34,6 +34,7 @@ function add(value: any) {
const hadKey = proto.has.call(target, value) const hadKey = proto.has.call(target, value)
const result = proto.add.call(target, value) const result = proto.add.call(target, value)
if (!hadKey) { if (!hadKey) {
/* istanbul ignore else */
if (__DEV__) { if (__DEV__) {
trigger(target, OperationTypes.ADD, value, { value }) trigger(target, OperationTypes.ADD, value, { value })
} else { } else {
@ -51,6 +52,7 @@ function set(key: any, value: any) {
const oldValue = proto.get.call(target, key) const oldValue = proto.get.call(target, key)
const result = proto.set.call(target, key, value) const result = proto.set.call(target, key, value)
if (value !== oldValue) { if (value !== oldValue) {
/* istanbul ignore else */
if (__DEV__) { if (__DEV__) {
const extraInfo = { oldValue, newValue: value } const extraInfo = { oldValue, newValue: value }
if (!hadKey) { if (!hadKey) {
@ -77,6 +79,7 @@ function deleteEntry(key: any) {
// forward the operation before queueing reactions // forward the operation before queueing reactions
const result = proto.delete.call(target, key) const result = proto.delete.call(target, key)
if (hadKey) { if (hadKey) {
/* istanbul ignore else */
if (__DEV__) { if (__DEV__) {
trigger(target, OperationTypes.DELETE, key, { oldValue }) trigger(target, OperationTypes.DELETE, key, { oldValue })
} else { } else {
@ -94,6 +97,7 @@ function clear() {
// forward the operation before queueing reactions // forward the operation before queueing reactions
const result = proto.clear.call(target) const result = proto.clear.call(target)
if (hadItems) { if (hadItems) {
/* istanbul ignore else */
if (__DEV__) { if (__DEV__) {
trigger(target, OperationTypes.CLEAR, void 0, { oldTarget }) trigger(target, OperationTypes.CLEAR, void 0, { oldTarget })
} else { } else {
@ -158,22 +162,21 @@ const immutableInstrumentations: any = {
} }
}) })
function getInstrumented( function makeInstrumentationGetter(instrumentations: any) {
return function getInstrumented(
target: any, target: any,
key: string | symbol, key: string | symbol,
receiver: any, receiver: any
instrumentations: any
) { ) {
target = instrumentations.hasOwnProperty(key) ? instrumentations : target target = instrumentations.hasOwnProperty(key) ? instrumentations : target
return Reflect.get(target, key, receiver) return Reflect.get(target, key, receiver)
} }
}
export const mutableCollectionHandlers: ProxyHandler<any> = { export const mutableCollectionHandlers: ProxyHandler<any> = {
get: (target: any, key: string | symbol, receiver: any) => get: makeInstrumentationGetter(mutableInstrumentations)
getInstrumented(target, key, receiver, mutableInstrumentations)
} }
export const immutableCollectionHandlers: ProxyHandler<any> = { export const immutableCollectionHandlers: ProxyHandler<any> = {
get: (target: any, key: string | symbol, receiver: any) => get: makeInstrumentationGetter(immutableInstrumentations)
getInstrumented(target, key, receiver, immutableInstrumentations)
} }