mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-05 15:28:29 +08:00
Added many unit tests to the observable module and fixed a bug.
This commit is contained in:
parent
cced7539c5
commit
a72b51511c
118
src/core/observable/Observable.test.ts
Normal file
118
src/core/observable/Observable.test.ts
Normal file
@ -0,0 +1,118 @@
|
||||
import { ChangeEvent, Observable } from "./Observable";
|
||||
import { SimpleEmitter } from "./SimpleEmitter";
|
||||
import { SimpleProperty } from "./property/SimpleProperty";
|
||||
import { DependentProperty } from "./property/DependentProperty";
|
||||
import { list_property, property } from "./index";
|
||||
import { FlatMappedProperty } from "./property/FlatMappedProperty";
|
||||
import { SimpleListProperty } from "./property/list/SimpleListProperty";
|
||||
import { DependentListProperty } from "./property/list/DependentListProperty";
|
||||
|
||||
// This suite tests every implementation of Observable.
|
||||
|
||||
function test_observable(
|
||||
name: string,
|
||||
create: () => {
|
||||
observable: Observable<any>;
|
||||
emit: () => void;
|
||||
},
|
||||
): void {
|
||||
test(`${name} should call observers when events are emitted`, () => {
|
||||
const { observable, emit } = create();
|
||||
const changes: ChangeEvent<any>[] = [];
|
||||
|
||||
observable.observe(c => {
|
||||
changes.push(c);
|
||||
});
|
||||
|
||||
emit();
|
||||
|
||||
expect(changes.length).toBe(1);
|
||||
|
||||
emit();
|
||||
emit();
|
||||
emit();
|
||||
|
||||
expect(changes.length).toBe(4);
|
||||
});
|
||||
|
||||
test(`${name} should not call observers after they are disposed`, () => {
|
||||
const { observable, emit } = create();
|
||||
const changes: ChangeEvent<any>[] = [];
|
||||
|
||||
const observer = observable.observe(c => {
|
||||
changes.push(c);
|
||||
});
|
||||
|
||||
emit();
|
||||
|
||||
expect(changes.length).toBe(1);
|
||||
|
||||
observer.dispose();
|
||||
|
||||
emit();
|
||||
emit();
|
||||
emit();
|
||||
|
||||
expect(changes.length).toBe(1);
|
||||
});
|
||||
}
|
||||
|
||||
test_observable(SimpleEmitter.name, () => {
|
||||
const observable = new SimpleEmitter();
|
||||
return {
|
||||
observable,
|
||||
emit: () => observable.emit({ value: 1 }),
|
||||
};
|
||||
});
|
||||
|
||||
test_observable(SimpleProperty.name, () => {
|
||||
const observable = new SimpleProperty(1);
|
||||
return {
|
||||
observable,
|
||||
emit: () => (observable.val += 1),
|
||||
};
|
||||
});
|
||||
|
||||
test_observable(DependentProperty.name, () => {
|
||||
const p = property(0);
|
||||
const observable = new DependentProperty([p], () => 2 * p.val);
|
||||
return {
|
||||
observable,
|
||||
emit: () => (p.val += 2),
|
||||
};
|
||||
});
|
||||
|
||||
test_observable(`${FlatMappedProperty.name} (dependent property emits)`, () => {
|
||||
const p = property({ x: property(5) });
|
||||
const observable = new FlatMappedProperty(p, v => v.x);
|
||||
return {
|
||||
observable,
|
||||
emit: () => (p.val = { x: property(p.val.x.val + 5) }),
|
||||
};
|
||||
});
|
||||
|
||||
test_observable(`${FlatMappedProperty.name} (nested property emits)`, () => {
|
||||
const p = property({ x: property(5) });
|
||||
const observable = new FlatMappedProperty(p, v => v.x);
|
||||
return {
|
||||
observable,
|
||||
emit: () => (p.val.x.val += 5),
|
||||
};
|
||||
});
|
||||
|
||||
test_observable(SimpleListProperty.name, () => {
|
||||
const observable = new SimpleListProperty<string>();
|
||||
return {
|
||||
observable,
|
||||
emit: () => observable.push("test"),
|
||||
};
|
||||
});
|
||||
|
||||
test_observable(DependentListProperty.name, () => {
|
||||
const list = list_property<number>();
|
||||
const observable = new DependentListProperty(list, x => x.map(v => 2 * v));
|
||||
return {
|
||||
observable,
|
||||
emit: () => list.push(10),
|
||||
};
|
||||
});
|
@ -27,8 +27,11 @@ export class FlatMappedProperty<T, U> extends AbstractMinimalProperty<U> impleme
|
||||
super();
|
||||
}
|
||||
|
||||
observe(observer: (event: PropertyChangeEvent<U>) => void): Disposable {
|
||||
const super_disposable = super.observe(observer);
|
||||
observe(
|
||||
observer: (event: PropertyChangeEvent<U>) => void,
|
||||
options?: { call_now?: boolean },
|
||||
): Disposable {
|
||||
const super_disposable = super.observe(observer, options);
|
||||
|
||||
if (this.dependency_disposable == undefined) {
|
||||
this.dependency_disposable = this.dependency.observe(() => {
|
||||
@ -40,8 +43,6 @@ export class FlatMappedProperty<T, U> extends AbstractMinimalProperty<U> impleme
|
||||
this.compute_and_observe();
|
||||
}
|
||||
|
||||
this.emit(this.get_val());
|
||||
|
||||
return {
|
||||
dispose: () => {
|
||||
super_disposable.dispose();
|
||||
|
139
src/core/observable/property/Property.test.ts
Normal file
139
src/core/observable/property/Property.test.ts
Normal file
@ -0,0 +1,139 @@
|
||||
import { SimpleProperty } from "./SimpleProperty";
|
||||
import { DependentProperty } from "./DependentProperty";
|
||||
import { list_property } from "../index";
|
||||
import { FlatMappedProperty } from "./FlatMappedProperty";
|
||||
import { SimpleListProperty } from "./list/SimpleListProperty";
|
||||
import { DependentListProperty } from "./list/DependentListProperty";
|
||||
import { is_property, Property, PropertyChangeEvent } from "./Property";
|
||||
import { is_list_property } from "./list/ListProperty";
|
||||
|
||||
// This suite tests every implementation of Property.
|
||||
|
||||
function test_property(
|
||||
name: string,
|
||||
create: () => {
|
||||
property: Property<any>;
|
||||
emit: () => void;
|
||||
},
|
||||
): void {
|
||||
test(`${name} should be a property according to is_property`, () => {
|
||||
const { property } = create();
|
||||
|
||||
expect(is_property(property)).toBe(true);
|
||||
});
|
||||
|
||||
test(`${name} should call observers immediately if added with call_now set to true`, () => {
|
||||
const { property } = create();
|
||||
const events: PropertyChangeEvent<any>[] = [];
|
||||
|
||||
property.observe(event => events.push(event), { call_now: true });
|
||||
|
||||
expect(events.length).toBe(1);
|
||||
});
|
||||
|
||||
test(`${name} should propagate updates to mapped properties`, () => {
|
||||
const { property, emit } = create();
|
||||
let i = 0;
|
||||
const mapped = property.map(() => i++);
|
||||
const events: PropertyChangeEvent<any>[] = [];
|
||||
|
||||
mapped.observe(event => events.push(event));
|
||||
|
||||
emit();
|
||||
|
||||
expect(events.length).toBe(1);
|
||||
});
|
||||
|
||||
test(`${name} should propagate updates to flat mapped properties`, () => {
|
||||
const { property, emit } = create();
|
||||
let i = 0;
|
||||
const flat_mapped = property.flat_map(() => new SimpleProperty(i++));
|
||||
const events: PropertyChangeEvent<any>[] = [];
|
||||
|
||||
flat_mapped.observe(event => events.push(event));
|
||||
|
||||
emit();
|
||||
|
||||
expect(events.length).toBe(1);
|
||||
});
|
||||
|
||||
test(`${name} should correctly set value and old_value in emitted PropertyChangeEvents`, () => {
|
||||
const { property, emit } = create();
|
||||
|
||||
const events: PropertyChangeEvent<any>[] = [];
|
||||
|
||||
property.observe(event => events.push(event));
|
||||
|
||||
const initial_value = property.val;
|
||||
|
||||
emit();
|
||||
|
||||
expect(events.length).toBe(1);
|
||||
expect(events[0].value).toBe(property.val);
|
||||
|
||||
if (!is_list_property(property)) {
|
||||
expect(events[0].old_value).toBe(initial_value);
|
||||
}
|
||||
|
||||
emit();
|
||||
|
||||
expect(events.length).toBe(2);
|
||||
expect(events[1].value).toBe(property.val);
|
||||
|
||||
if (!is_list_property(property)) {
|
||||
expect(events[1].old_value).toBe(events[0].value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test_property(SimpleProperty.name, () => {
|
||||
const property = new SimpleProperty(1);
|
||||
return {
|
||||
property,
|
||||
emit: () => (property.val += 1),
|
||||
};
|
||||
});
|
||||
|
||||
test_property(DependentProperty.name, () => {
|
||||
const p = new SimpleProperty(0);
|
||||
const property = new DependentProperty([p], () => 2 * p.val);
|
||||
return {
|
||||
property,
|
||||
emit: () => (p.val += 2),
|
||||
};
|
||||
});
|
||||
|
||||
test_property(`${FlatMappedProperty.name} (dependent property emits)`, () => {
|
||||
const p = new SimpleProperty({ x: new SimpleProperty(5) });
|
||||
const property = new FlatMappedProperty(p, v => v.x);
|
||||
return {
|
||||
property,
|
||||
emit: () => (p.val = { x: new SimpleProperty(p.val.x.val + 5) }),
|
||||
};
|
||||
});
|
||||
|
||||
test_property(`${FlatMappedProperty.name} (nested property emits)`, () => {
|
||||
const p = new SimpleProperty({ x: new SimpleProperty(5) });
|
||||
const property = new FlatMappedProperty(p, v => v.x);
|
||||
return {
|
||||
property,
|
||||
emit: () => (p.val.x.val += 5),
|
||||
};
|
||||
});
|
||||
|
||||
test_property(SimpleListProperty.name, () => {
|
||||
const property = new SimpleListProperty<string>();
|
||||
return {
|
||||
property,
|
||||
emit: () => property.push("test"),
|
||||
};
|
||||
});
|
||||
|
||||
test_property(DependentListProperty.name, () => {
|
||||
const list = list_property<number>();
|
||||
const property = new DependentListProperty(list, x => x.map(v => 2 * v));
|
||||
return {
|
||||
property,
|
||||
emit: () => list.push(10),
|
||||
};
|
||||
});
|
46
src/core/observable/property/WritableProperty.test.ts
Normal file
46
src/core/observable/property/WritableProperty.test.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { SimpleProperty } from "./SimpleProperty";
|
||||
import { SimpleListProperty } from "./list/SimpleListProperty";
|
||||
import { PropertyChangeEvent } from "./Property";
|
||||
import { WritableProperty } from "./WritableProperty";
|
||||
|
||||
// This suite tests every implementation of WritableProperty.
|
||||
|
||||
function test_writable_property<T>(
|
||||
name: string,
|
||||
create: () => {
|
||||
property: WritableProperty<T>;
|
||||
emit: () => void;
|
||||
create_val: () => T;
|
||||
},
|
||||
): void {
|
||||
test(`${name} should emit a PropertyChangeEvent when val is modified`, () => {
|
||||
const { property, create_val } = create();
|
||||
const events: PropertyChangeEvent<T>[] = [];
|
||||
|
||||
property.observe(event => events.push(event));
|
||||
|
||||
const new_val = create_val();
|
||||
property.val = new_val;
|
||||
|
||||
expect(events.length).toBe(1);
|
||||
expect(events[0].value).toEqual(new_val);
|
||||
});
|
||||
}
|
||||
|
||||
test_writable_property(SimpleProperty.name, () => {
|
||||
const property = new SimpleProperty(1);
|
||||
return {
|
||||
property,
|
||||
emit: () => (property.val += 1),
|
||||
create_val: () => property.val + 1,
|
||||
};
|
||||
});
|
||||
|
||||
test_writable_property(SimpleListProperty.name, () => {
|
||||
const property = new SimpleListProperty<string>();
|
||||
return {
|
||||
property,
|
||||
emit: () => property.push("test"),
|
||||
create_val: () => ["test"],
|
||||
};
|
||||
});
|
@ -3,7 +3,7 @@ import { PropertyChangeEvent } from "../Property";
|
||||
import { Disposable } from "../../Disposable";
|
||||
import { AbstractListProperty } from "./AbstractListProperty";
|
||||
|
||||
export class DependentListProperty<T> extends AbstractListProperty<T> implements ListProperty<T> {
|
||||
export class DependentListProperty<T> extends AbstractListProperty<T> {
|
||||
private readonly dependency: ListProperty<T>;
|
||||
private readonly transform: (values: readonly T[]) => T[];
|
||||
private dependency_disposable?: Disposable;
|
||||
|
55
src/core/observable/property/list/ListProperty.test.ts
Normal file
55
src/core/observable/property/list/ListProperty.test.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import {
|
||||
is_list_property,
|
||||
ListChangeType,
|
||||
ListProperty,
|
||||
ListPropertyChangeEvent,
|
||||
} from "./ListProperty";
|
||||
import { SimpleListProperty } from "./SimpleListProperty";
|
||||
import { DependentListProperty } from "./DependentListProperty";
|
||||
import { list_property } from "../../index";
|
||||
|
||||
// This suite tests every implementation of ListProperty.
|
||||
|
||||
function test_list_property(
|
||||
name: string,
|
||||
create: () => {
|
||||
property: ListProperty<any>;
|
||||
emit_list_change: () => void;
|
||||
},
|
||||
): void {
|
||||
test(`${name} should be a list property according to is_list_property`, () => {
|
||||
const { property } = create();
|
||||
|
||||
expect(is_list_property(property)).toBe(true);
|
||||
});
|
||||
|
||||
test(`${name} should propagate list changes to a filtered list`, () => {
|
||||
const { property, emit_list_change } = create();
|
||||
const filtered = property.filtered(() => true);
|
||||
const events: ListPropertyChangeEvent<any>[] = [];
|
||||
|
||||
filtered.observe_list(event => events.push(event));
|
||||
|
||||
emit_list_change();
|
||||
|
||||
expect(events.length).toBe(1);
|
||||
expect(events[0].type).toBe(ListChangeType.ListChange);
|
||||
});
|
||||
}
|
||||
|
||||
test_list_property(SimpleListProperty.name, () => {
|
||||
const property = new SimpleListProperty<string>();
|
||||
return {
|
||||
property,
|
||||
emit_list_change: () => property.push("test"),
|
||||
};
|
||||
});
|
||||
|
||||
test_list_property(DependentListProperty.name, () => {
|
||||
const list = list_property<number>();
|
||||
const property = new DependentListProperty(list, x => x.map(v => 2 * v));
|
||||
return {
|
||||
property,
|
||||
emit_list_change: () => list.push(10),
|
||||
};
|
||||
});
|
38
src/core/observable/property/list/SimpleListProperty.test.ts
Normal file
38
src/core/observable/property/list/SimpleListProperty.test.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { SimpleListProperty } from "./SimpleListProperty";
|
||||
import { ListChangeType, ListPropertyChangeEvent } from "./ListProperty";
|
||||
|
||||
test("constructor", () => {
|
||||
const list = new SimpleListProperty<number>(undefined, 1, 2, 3);
|
||||
|
||||
expect(list.val).toEqual([1, 2, 3]);
|
||||
expect(list.length.val).toBe(3);
|
||||
});
|
||||
|
||||
test("push", () => {
|
||||
const changes: ListPropertyChangeEvent<number>[] = [];
|
||||
const list = new SimpleListProperty<number>();
|
||||
|
||||
list.observe_list(change => changes.push(change));
|
||||
|
||||
list.push(9);
|
||||
|
||||
expect(list.val).toEqual([9]);
|
||||
expect(changes.length).toBe(1);
|
||||
expect(changes[0]).toEqual({
|
||||
type: ListChangeType.ListChange,
|
||||
index: 0,
|
||||
removed: [],
|
||||
inserted: [9],
|
||||
});
|
||||
|
||||
list.push(1, 2, 3);
|
||||
|
||||
expect(list.val).toEqual([9, 1, 2, 3]);
|
||||
expect(changes.length).toBe(2);
|
||||
expect(changes[1]).toEqual({
|
||||
type: ListChangeType.ListChange,
|
||||
index: 1,
|
||||
removed: [],
|
||||
inserted: [1, 2, 3],
|
||||
});
|
||||
});
|
@ -20,7 +20,7 @@ export class SimpleListProperty<T> extends AbstractListProperty<T>
|
||||
|
||||
/**
|
||||
* @param extract_observables - Extractor function called on each value in this list. Changes
|
||||
* to the returned observables will be propagated via update events.
|
||||
* to the returned observables will be propagated via ValueChange events.
|
||||
* @param values - Initial values of this list.
|
||||
*/
|
||||
constructor(extract_observables?: (element: T) => Observable<any>[], ...values: T[]) {
|
||||
|
Loading…
Reference in New Issue
Block a user