Dependencies update
This commit is contained in:
parent
1000611ec1
commit
b69db4e796
|
@ -1,3 +0,0 @@
|
|||
[submodule "src/common"]
|
||||
path = src/common
|
||||
url = https://code.thunderk.net/michael/tscommon.git
|
3
TODO.md
3
TODO.md
|
@ -4,7 +4,6 @@ To-Do-list
|
|||
Phaser 3 migration
|
||||
------------------
|
||||
|
||||
* Restore fullscreen mode (and add a fullscreen incentive before the menu)
|
||||
* Fix valuebar requiring to be in root display list
|
||||
* Restore unit tests about boundaries (in UITools)
|
||||
|
||||
|
@ -96,6 +95,7 @@ Artificial Intelligence
|
|||
Common UI
|
||||
---------
|
||||
|
||||
* Add a fullscreen incentive at game start
|
||||
* Fix calling setHoverClick several times on the same button not working as expected
|
||||
* Fix tooltip remaining when the hovered object is hidden by animations
|
||||
* If ProgressiveMessage animation performance is bad, show the text directly
|
||||
|
@ -108,6 +108,7 @@ Common UI
|
|||
Technical
|
||||
---------
|
||||
|
||||
* Use tk-serializer package (may need to switch to webpack)
|
||||
* Fix tooltips and input events on mobile
|
||||
* Pause timers when the game is paused (at least animation timers)
|
||||
* Pack sounds
|
||||
|
|
|
@ -2,13 +2,15 @@
|
|||
# Usage:
|
||||
# source activate_node
|
||||
|
||||
vdir="./.venv"
|
||||
expected="10.15.3"
|
||||
|
||||
if [ \! -f "./activate_node" ]
|
||||
then
|
||||
echo "Not in project directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
vdir="./.venv"
|
||||
test -x "${vdir}/bin/nodeenv" || ( virtualenv -p python3 "${vdir}" && "${vdir}/bin/pip" install --upgrade nodeenv )
|
||||
test -e "${vdir}/node/bin/activate" || "${vdir}/bin/nodeenv" --node=10.3.0 --force "${vdir}/node"
|
||||
test -x "${vdir}/bin/nodeenv" || ( python3 -m venv "${vdir}" && "${vdir}/bin/pip" install --upgrade nodeenv )
|
||||
test "$(${vdir}/node/bin/nodejs --version)" = "v${expected}" || "${vdir}/bin/nodeenv" --node=${expected} --force "${vdir}/node"
|
||||
source "${vdir}/node/bin/activate"
|
||||
|
|
File diff suppressed because it is too large
Load Diff
32
package.json
32
package.json
|
@ -18,29 +18,29 @@
|
|||
"author": "Michael Lemaire",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/jasmine": "^2.8.8",
|
||||
"@types/jasmine": "^3.3.12",
|
||||
"babel-polyfill": "6.26.0",
|
||||
"codecov": "^3.0.2",
|
||||
"codecov": "^3.3.0",
|
||||
"gamefroot-texture-packer": "github:Gamefroot/Gamefroot-Texture-Packer#f3687111afc94f80ea8f2877c188fb8e2004e8ff",
|
||||
"glob": "^7.1.2",
|
||||
"glob-watcher": "^5.0.1",
|
||||
"jasmine": "^3.1.0",
|
||||
"karma": "^2.0.2",
|
||||
"glob": "^7.1.3",
|
||||
"glob-watcher": "^5.0.3",
|
||||
"jasmine": "^3.4.0",
|
||||
"karma": "^4.1.0",
|
||||
"karma-coverage": "^1.1.2",
|
||||
"karma-jasmine": "^1.1.2",
|
||||
"karma-jasmine": "^2.0.1",
|
||||
"karma-phantomjs-launcher": "1.0.4",
|
||||
"karma-spec-reporter": "^0.0.32",
|
||||
"live-server": "1.2.0",
|
||||
"remap-istanbul": "^0.11.1",
|
||||
"runjs": "^4.3.2",
|
||||
"shelljs": "^0.8.2",
|
||||
"typescript": "^2.9.1",
|
||||
"uglify-js": "^3.4.0"
|
||||
"live-server": "1.2.1",
|
||||
"remap-istanbul": "^0.13.0",
|
||||
"runjs": "^4.4.2",
|
||||
"shelljs": "^0.8.3",
|
||||
"typescript": "^3.4.5",
|
||||
"uglify-js": "^3.5.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"jasmine-core": "^3.1.0",
|
||||
"parse": "^1.11.1",
|
||||
"phaser": "^3.10.1",
|
||||
"jasmine-core": "^3.4.0",
|
||||
"parse": "^2.4.0",
|
||||
"phaser": "^3.16.2",
|
||||
"process-pool": "^0.3.5"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,15 +41,18 @@ module TK.SpaceTac {
|
|||
// Current scaling
|
||||
scaling = 1
|
||||
|
||||
constructor(headless: boolean = false) {
|
||||
constructor(private testmode = false) {
|
||||
super({
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
type: headless ? Phaser.HEADLESS : Phaser.AUTO,
|
||||
type: Phaser.AUTO, // cannot really use HEADLESS because of bugs
|
||||
backgroundColor: '#000000',
|
||||
parent: '-space-tac',
|
||||
disableContextMenu: true,
|
||||
"render.autoResize": true,
|
||||
scale: {
|
||||
mode: Phaser.Scale.FIT,
|
||||
autoCenter: Phaser.Scale.CENTER_BOTH
|
||||
},
|
||||
});
|
||||
|
||||
this.storage = localStorage;
|
||||
|
@ -57,7 +60,7 @@ module TK.SpaceTac {
|
|||
this.session = new GameSession();
|
||||
this.session_token = null;
|
||||
|
||||
if (!headless) {
|
||||
if (!testmode) {
|
||||
this.events.on("blur", () => {
|
||||
this.scene.scenes.forEach(scene => this.scene.pause(scene));
|
||||
});
|
||||
|
@ -74,34 +77,17 @@ module TK.SpaceTac {
|
|||
this.scene.add('creation', UI.FleetCreationView);
|
||||
this.scene.add('universe', UI.UniverseMapView);
|
||||
|
||||
this.resizeToFitWindow();
|
||||
window.addEventListener("resize", () => this.resizeToFitWindow());
|
||||
|
||||
this.goToScene('boot');
|
||||
}
|
||||
}
|
||||
|
||||
resize(width: number, height: number, scaling?: number) {
|
||||
super.resize(width, height);
|
||||
|
||||
this.scaling = scaling ? scaling : 1;
|
||||
cfilter(this.scene.scenes, UI.BaseView).forEach(scene => scene.resize());
|
||||
}
|
||||
|
||||
resizeToFitWindow() {
|
||||
let width = window.innerWidth;
|
||||
let height = window.innerHeight;
|
||||
let scale = Math.min(width / 1920, height / 1080);
|
||||
this.resize(1920 * scale, 1080 * scale, scale);
|
||||
}
|
||||
|
||||
boot() {
|
||||
super.boot();
|
||||
this.options = new UI.GameOptions(this);
|
||||
}
|
||||
|
||||
get headless(): boolean {
|
||||
return this.config.renderType == Phaser.HEADLESS;
|
||||
get isTesting(): boolean {
|
||||
return this.testmode;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -124,7 +110,7 @@ module TK.SpaceTac {
|
|||
if (active) {
|
||||
let scene = this.scene.getScene(active);
|
||||
return (scene instanceof UI.BaseView) ? scene : null;
|
||||
} else if (this.headless) {
|
||||
} else if (this.isTesting) {
|
||||
return this.scene.scenes[0];
|
||||
} else {
|
||||
return null;
|
||||
|
@ -237,9 +223,7 @@ module TK.SpaceTac {
|
|||
* Check if the game is currently fullscreen
|
||||
*/
|
||||
isFullscreen(): boolean {
|
||||
// FIXME
|
||||
return false;
|
||||
//return this.scale.isFullScreen;
|
||||
return this.scale.isFullscreen;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -248,15 +232,13 @@ module TK.SpaceTac {
|
|||
* Returns true if the result is fullscreen
|
||||
*/
|
||||
toggleFullscreen(active: boolean | null = null): boolean {
|
||||
// FIXME
|
||||
/*if (active === false || (active !== true && this.isFullscreen())) {
|
||||
this.scale.stopFullScreen();
|
||||
if (active === false || (active !== true && this.isFullscreen())) {
|
||||
this.scale.stopFullscreen();
|
||||
return false;
|
||||
} else {
|
||||
this.scale.startFullScreen(true);
|
||||
this.scale.startFullscreen();
|
||||
return true;
|
||||
}*/
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 1425cb08935dd996a4c7a644ab793ff3b8355c9b
|
|
@ -0,0 +1,232 @@
|
|||
/// <reference path="DiffLog.ts" />
|
||||
|
||||
module TK.Specs {
|
||||
class TestState {
|
||||
counter = 0
|
||||
}
|
||||
|
||||
class TestDiff extends Diff<TestState> {
|
||||
private value: number
|
||||
constructor(value = 1) {
|
||||
super();
|
||||
this.value = value;
|
||||
}
|
||||
apply(state: TestState) {
|
||||
state.counter += this.value;
|
||||
}
|
||||
getReverse() {
|
||||
return new TestDiff(-this.value);
|
||||
}
|
||||
}
|
||||
|
||||
testing("DiffLog", test => {
|
||||
test.case("stores sequential events", check => {
|
||||
let log = new DiffLog<TestState>();
|
||||
check.equals(log.count(), 0);
|
||||
check.equals(log.get(0), null);
|
||||
check.equals(log.get(1), null);
|
||||
check.equals(log.get(2), null);
|
||||
|
||||
log.add(new TestDiff(2));
|
||||
check.equals(log.count(), 1);
|
||||
check.equals(log.get(0), new TestDiff(2));
|
||||
check.equals(log.get(1), null);
|
||||
check.equals(log.get(2), null);
|
||||
|
||||
log.add(new TestDiff(-4));
|
||||
check.equals(log.count(), 2);
|
||||
check.equals(log.get(0), new TestDiff(2));
|
||||
check.equals(log.get(1), new TestDiff(-4));
|
||||
check.equals(log.get(2), null);
|
||||
|
||||
log.clear(1);
|
||||
check.equals(log.count(), 1);
|
||||
check.equals(log.get(0), new TestDiff(2));
|
||||
|
||||
log.clear();
|
||||
check.equals(log.count(), 0);
|
||||
})
|
||||
})
|
||||
|
||||
testing("DiffLogClient", test => {
|
||||
test.case("adds diffs to the log", check => {
|
||||
let log = new DiffLog<TestState>();
|
||||
let state = new TestState();
|
||||
let client = new DiffLogClient(state, log);
|
||||
|
||||
check.equals(client.atEnd(), true, "client is empty, should be at end");
|
||||
check.equals(log.count(), 0, "log is empty initially");
|
||||
check.equals(state.counter, 0, "initial state is 0");
|
||||
|
||||
client.add(new TestDiff(3));
|
||||
check.equals(client.atEnd(), true, "client still at end");
|
||||
check.equals(log.count(), 1, "diff added to log");
|
||||
check.equals(state.counter, 3, "diff applied to state");
|
||||
|
||||
client.add(new TestDiff(2), false);
|
||||
check.equals(client.atEnd(), false, "client lapsing behind");
|
||||
check.equals(log.count(), 2, "diff added to log");
|
||||
check.equals(state.counter, 3, "diff not applied to state");
|
||||
})
|
||||
|
||||
test.case("initializes at current state (end of log)", check => {
|
||||
let state = new TestState();
|
||||
let log = new DiffLog<TestState>();
|
||||
log.add(new TestDiff(7));
|
||||
let client = new DiffLogClient(state, log);
|
||||
check.equals(client.atStart(), false);
|
||||
check.equals(client.atEnd(), true);
|
||||
check.equals(state.counter, 0);
|
||||
client.forward();
|
||||
check.equals(state.counter, 0);
|
||||
client.backward();
|
||||
check.equals(state.counter, -7);
|
||||
})
|
||||
|
||||
test.case("moves forward or backward in the log", check => {
|
||||
let log = new DiffLog<TestState>();
|
||||
let state = new TestState();
|
||||
let client = new DiffLogClient(state, log);
|
||||
|
||||
log.add(new TestDiff(7));
|
||||
log.add(new TestDiff(-2));
|
||||
log.add(new TestDiff(4));
|
||||
|
||||
check.equals(state.counter, 0, "initial state is 0");
|
||||
check.equals(client.atStart(), true, "client is at start");
|
||||
check.equals(client.atEnd(), false, "client is not at end");
|
||||
|
||||
client.forward();
|
||||
check.equals(state.counter, 7, "0+7 => 7");
|
||||
check.equals(client.atStart(), false, "client is not at start");
|
||||
check.equals(client.atEnd(), false, "client is not at end");
|
||||
|
||||
client.forward();
|
||||
check.equals(state.counter, 5, "7-2 => 5");
|
||||
check.equals(client.atStart(), false, "client is not at start");
|
||||
check.equals(client.atEnd(), false, "client is not at end");
|
||||
|
||||
client.forward();
|
||||
check.equals(state.counter, 9, "5+4 => 9");
|
||||
check.equals(client.atStart(), false, "client is not at start");
|
||||
check.equals(client.atEnd(), true, "client is at end");
|
||||
|
||||
client.forward();
|
||||
check.equals(state.counter, 9, "at end, still 9");
|
||||
check.equals(client.atStart(), false, "client is not at start");
|
||||
check.equals(client.atEnd(), true, "client is at end");
|
||||
|
||||
client.backward();
|
||||
check.equals(state.counter, 5, "9-4=>5");
|
||||
check.equals(client.atStart(), false, "client is not at start");
|
||||
check.equals(client.atEnd(), false, "client is not at end");
|
||||
|
||||
client.backward();
|
||||
check.equals(state.counter, 7, "5+2=>7");
|
||||
check.equals(client.atStart(), false, "client is not at start");
|
||||
check.equals(client.atEnd(), false, "client is not at end");
|
||||
|
||||
client.backward();
|
||||
check.equals(state.counter, 0, "7-7=>0");
|
||||
check.equals(client.atStart(), true, "client is back at start");
|
||||
check.equals(client.atEnd(), false, "client is not at end");
|
||||
|
||||
client.backward();
|
||||
check.equals(state.counter, 0, "at start, still 0");
|
||||
check.equals(client.atStart(), true, "client is at start");
|
||||
check.equals(client.atEnd(), false, "client is not at end");
|
||||
})
|
||||
|
||||
test.case("jumps to start or end of the log", check => {
|
||||
let log = new DiffLog<TestState>();
|
||||
let state = new TestState();
|
||||
let client = new DiffLogClient(state, log);
|
||||
|
||||
client.add(new TestDiff(7));
|
||||
log.add(new TestDiff(-2));
|
||||
log.add(new TestDiff(4));
|
||||
|
||||
check.equals(state.counter, 7, "initial state is 7");
|
||||
check.equals(client.atStart(), false, "client is not at start");
|
||||
check.equals(client.atEnd(), false, "client is not at end");
|
||||
|
||||
client.jumpToEnd();
|
||||
check.equals(state.counter, 9, "7-2+4=>9");
|
||||
check.equals(client.atStart(), false, "client is not at start");
|
||||
check.equals(client.atEnd(), true, "client at end");
|
||||
|
||||
client.jumpToEnd();
|
||||
check.equals(state.counter, 9, "still 9");
|
||||
check.equals(client.atStart(), false, "client is not at start");
|
||||
check.equals(client.atEnd(), true, "client at end");
|
||||
|
||||
client.jumpToStart();
|
||||
check.equals(state.counter, 0, "9-4+2-7=>0");
|
||||
check.equals(client.atStart(), true, "client is at start");
|
||||
check.equals(client.atEnd(), false, "client at not end");
|
||||
|
||||
client.jumpToStart();
|
||||
check.equals(state.counter, 0, "still 0");
|
||||
check.equals(client.atStart(), true, "client is at start");
|
||||
check.equals(client.atEnd(), false, "client at not end");
|
||||
})
|
||||
|
||||
test.case("truncate the log", check => {
|
||||
let log = new DiffLog<TestState>();
|
||||
let state = new TestState();
|
||||
let client = new DiffLogClient(state, log);
|
||||
|
||||
client.add(new TestDiff(7));
|
||||
client.add(new TestDiff(3));
|
||||
client.add(new TestDiff(5));
|
||||
|
||||
check.in("initial state", check => {
|
||||
check.equals(state.counter, 15, "state=15");
|
||||
check.equals(log.count(), 3, "count=3");
|
||||
});
|
||||
|
||||
client.backward();
|
||||
|
||||
check.in("after backward", check => {
|
||||
check.equals(state.counter, 10, "state=10");
|
||||
check.equals(log.count(), 3, "count=3");
|
||||
});
|
||||
|
||||
client.truncate();
|
||||
|
||||
check.in("after truncate", check => {
|
||||
check.equals(state.counter, 10, "state=10");
|
||||
check.equals(log.count(), 2, "count=2");
|
||||
});
|
||||
|
||||
client.truncate();
|
||||
|
||||
check.in("after another truncate", check => {
|
||||
check.equals(state.counter, 10, "state=10");
|
||||
check.equals(log.count(), 2, "count=2");
|
||||
});
|
||||
})
|
||||
|
||||
test.acase("plays the log continuously", async check => {
|
||||
let log = new DiffLog<TestState>();
|
||||
let state = new TestState();
|
||||
let client = new DiffLogClient(state, log);
|
||||
|
||||
let inter: number[] = [];
|
||||
let promise = client.play(diff => {
|
||||
inter.push((<any>diff).value);
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
log.add(new TestDiff(5));
|
||||
log.add(new TestDiff(-1));
|
||||
log.add(new TestDiff(2));
|
||||
client.stop(false);
|
||||
|
||||
await promise;
|
||||
|
||||
check.equals(state.counter, 6);
|
||||
check.equals(inter, [5, -1, 2]);
|
||||
})
|
||||
})
|
||||
}
|
|
@ -0,0 +1,249 @@
|
|||
/**
|
||||
* Framework to maintain a state from a log of changes
|
||||
*
|
||||
* This allows for repeatable, serializable and revertable state modifications.
|
||||
*/
|
||||
module TK {
|
||||
/**
|
||||
* Base class for a single diff.
|
||||
*
|
||||
* This represents an atomic change of the state, that can be applied, or reverted.
|
||||
*/
|
||||
export class Diff<T> {
|
||||
/**
|
||||
* Apply the diff on a given state
|
||||
*
|
||||
* By default it does nothing
|
||||
*/
|
||||
apply(state: T): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverts the diff from a given state
|
||||
*
|
||||
* By default it applies the reverse event
|
||||
*/
|
||||
revert(state: T): void {
|
||||
this.getReverse().apply(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reverse event
|
||||
*
|
||||
* By default it returns a stub event that does nothing
|
||||
*/
|
||||
protected getReverse(): Diff<T> {
|
||||
return new Diff<T>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collection of sequential diffs
|
||||
*/
|
||||
export class DiffLog<T> {
|
||||
private diffs: Diff<T>[] = []
|
||||
|
||||
/**
|
||||
* Add a single diff at the end of the log
|
||||
*/
|
||||
add(diff: Diff<T>): void {
|
||||
this.diffs.push(diff);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the diff at a specific index
|
||||
*/
|
||||
get(idx: number): Diff<T> | null {
|
||||
return this.diffs[idx] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the total count of diffs
|
||||
*/
|
||||
count(): number {
|
||||
return this.diffs.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean all stored diffs, starting at a given index
|
||||
*
|
||||
* The caller should be sure that no log client is beyond the cut index.
|
||||
*/
|
||||
clear(start = 0): void {
|
||||
this.diffs = this.diffs.slice(0, start);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Client for a DiffLog, able to go forward or backward in the log, applying diffs as needed
|
||||
*/
|
||||
export class DiffLogClient<T> {
|
||||
private state: T
|
||||
private log: DiffLog<T>
|
||||
private cursor = -1
|
||||
private playing = false
|
||||
private stopping = false
|
||||
private paused = false
|
||||
private timer = Timer.global
|
||||
|
||||
constructor(state: T, log: DiffLog<T>) {
|
||||
this.state = state;
|
||||
this.log = log;
|
||||
this.cursor = log.count() - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the log is currently playing
|
||||
*/
|
||||
isPlaying(): boolean {
|
||||
return this.playing && !this.paused && !this.stopping;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current diff pointed at
|
||||
*/
|
||||
getCurrent(): Diff<T> | null {
|
||||
return this.log.get(this.cursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a diff to the underlying log, applying it immediately if required
|
||||
*/
|
||||
add(diff: Diff<T>, apply = true): void {
|
||||
this.log.add(diff);
|
||||
if (apply) {
|
||||
this.jumpToEnd();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the underlying log continuously, until *stop* is called
|
||||
*
|
||||
* If *after_apply* is provided, it will be called after each diff is applied, and waited upon before resuming
|
||||
*/
|
||||
async play(after_apply?: (diff: Diff<T>) => Promise<void>): Promise<void> {
|
||||
if (this.playing) {
|
||||
console.error("DiffLogClient already playing", this);
|
||||
return;
|
||||
}
|
||||
|
||||
this.playing = true;
|
||||
this.stopping = false;
|
||||
|
||||
while (this.playing) {
|
||||
if (!this.paused) {
|
||||
let diff = this.forward();
|
||||
if (diff && after_apply) {
|
||||
await after_apply(diff);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.atEnd()) {
|
||||
if (this.stopping) {
|
||||
break;
|
||||
} else {
|
||||
await this.timer.sleep(50);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the previous *play*
|
||||
*/
|
||||
stop(immediate = true): void {
|
||||
if (!this.playing) {
|
||||
console.error("DiffLogClient not playing", this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (immediate) {
|
||||
this.playing = false;
|
||||
}
|
||||
this.stopping = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a step backward in time (revert one diff)
|
||||
*/
|
||||
backward(): Diff<T> | null {
|
||||
if (!this.atStart()) {
|
||||
this.cursor -= 1;
|
||||
this.paused = true;
|
||||
|
||||
let diff = this.log.get(this.cursor + 1);
|
||||
if (diff) {
|
||||
diff.revert(this.state);
|
||||
}
|
||||
return diff;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a step forward in time (apply one diff)
|
||||
*/
|
||||
forward(): Diff<T> | null {
|
||||
if (!this.atEnd()) {
|
||||
this.cursor += 1;
|
||||
if (this.atEnd()) {
|
||||
this.paused = false;
|
||||
}
|
||||
|
||||
let diff = this.log.get(this.cursor);
|
||||
if (diff) {
|
||||
diff.apply(this.state);
|
||||
}
|
||||
return diff;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Jump to the start of the log
|
||||
*
|
||||
* This will rewind all applied event
|
||||
*/
|
||||
jumpToStart() {
|
||||
while (!this.atStart()) {
|
||||
this.backward();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Jump to the end of the log
|
||||
*
|
||||
* This will apply all remaining event
|
||||
*/
|
||||
jumpToEnd() {
|
||||
while (!this.atEnd()) {
|
||||
this.forward();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we are currently at the start of the log
|
||||
*/
|
||||
atStart(): boolean {
|
||||
return this.cursor < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we are currently at the end of the log
|
||||
*/
|
||||
atEnd(): boolean {
|
||||
return this.cursor >= this.log.count() - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate all diffs after the current position
|
||||
*
|
||||
* This is useful when using the log to "undo" something
|
||||
*/
|
||||
truncate(): void {
|
||||
this.log.clear(this.cursor + 1);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
module TK {
|
||||
testing("Iterators", test => {
|
||||
function checkit<T>(base_iterator: Iterator<T>, ...values: (T | null)[]) {
|
||||
let iterator = base_iterator;
|
||||
values.forEach(value => {
|
||||
let [head, tail] = iterator();
|
||||
test.check.equals(head, value);
|
||||
iterator = tail;
|
||||
});
|
||||
|
||||
// second iteration to check for repeatability
|
||||
iterator = base_iterator;
|
||||
values.forEach(value => {
|
||||
let [head, tail] = iterator();
|
||||
test.check.equals(head, value);
|
||||
iterator = tail;
|
||||
});
|
||||
}
|
||||
|
||||
function checkarray<T>(iterator: Iterator<T>, values: T[]) {
|
||||
test.check.equals(imaterialize(iterator), values);
|
||||
|
||||
// second iteration to check for repeatability
|
||||
test.check.equals(imaterialize(iterator), values);
|
||||
}
|
||||
|
||||
test.case("calls a function for each yielded value", check => {
|
||||
let iterator = iarray([1, 2, 3]);
|
||||
let result: number[] = [];
|
||||
iforeach(iterator, bound(result, "push"));
|
||||
check.equals(result, [1, 2, 3]);
|
||||
|
||||
result = [];
|
||||
iforeach(iterator, i => {
|
||||
result.push(i);
|
||||
if (i == 2) {
|
||||
return null;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
check.equals(result, [1, 2]);
|
||||
|
||||
result = [];
|
||||
iforeach(iterator, i => {
|
||||
result.push(i);
|
||||
return i;
|
||||
}, 2);
|
||||
check.equals(result, [1, 2]);
|
||||
});
|
||||
|
||||
test.case("creates an iterator from an array", check => {
|
||||
checkit(iarray([]), null, null, null);
|
||||
checkit(iarray([1, 2, 3]), 1, 2, 3, null, null, null);
|
||||
});
|
||||
|
||||
test.case("creates an iterator from a single value", check => {
|
||||
checkarray(isingle(1), [1]);
|
||||
checkarray(isingle("a"), ["a"]);
|
||||
});
|
||||
|
||||
test.case("finds the first item passing a predicate", check => {
|
||||
check.equals(ifirst(iarray(<number[]>[]), i => i % 2 == 0), null);
|
||||
check.equals(ifirst(iarray([1, 2, 3]), i => i % 2 == 0), 2);
|
||||
check.equals(ifirst(iarray([1, 3, 5]), i => i % 2 == 0), null);
|
||||
});
|
||||
|
||||
test.case("finds the first item mapping to a value", check => {
|
||||
let predicate = (i: number) => i % 2 == 0 ? (i * 4).toString() : null
|
||||
check.equals(ifirstmap(iarray([]), predicate), null);
|
||||
check.equals(ifirstmap(iarray([1, 2, 3]), predicate), "8");
|
||||
check.equals(ifirstmap(iarray([1, 3, 5]), predicate), null);
|
||||
});
|
||||
|
||||
test.case("materializes an array from an iterator", check => {
|
||||
check.equals(imaterialize(iarray([1, 2, 3])), [1, 2, 3]);
|
||||
|
||||
check.throw(() => imaterialize(iarray([1, 2, 3, 4, 5]), 2), "Length limit on iterator materialize");
|
||||
});
|
||||
|
||||
test.case("creates an iterator in a range of integers", check => {
|
||||
checkarray(irange(4), [0, 1, 2, 3]);
|
||||
checkarray(irange(4, 1), [1, 2, 3, 4]);
|
||||
checkarray(irange(5, 3, 2), [3, 5, 7, 9, 11]);
|
||||
checkit(irange(), 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
|
||||
});
|
||||
|
||||
test.case("uses a step iterator to scan numbers", check => {
|
||||
checkit(istep(), 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
|
||||
checkit(istep(3), 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14);
|
||||
checkarray(istep(3, irepeat(1, 4)), [3, 4, 5, 6, 7]);
|
||||
checkarray(istep(8, IEMPTY), [8]);
|
||||
checkit(istep(1, irange()), 1, 1, 2, 4, 7, 11, 16);
|
||||
});
|
||||
|
||||
test.case("skips a number of values", check => {
|
||||
checkarray(iskip(irange(7), 3), [3, 4, 5, 6]);
|
||||
checkarray(iskip(irange(7), 12), []);
|
||||
checkarray(iskip(IEMPTY, 3), []);
|
||||
});
|
||||
|
||||
test.case("gets a value at an iterator position", check => {
|
||||
check.equals(iat(irange(), -1), null);
|
||||
check.equals(iat(irange(), 0), 0);
|
||||
check.equals(iat(irange(), 8), 8);
|
||||
check.equals(iat(irange(5), 8), null);
|
||||
check.equals(iat(IEMPTY, 0), null);
|
||||
});
|
||||
|
||||
test.case("chains iterators", check => {
|
||||
checkarray(ichain(), []);
|
||||
checkarray(ichain(irange(3)), [0, 1, 2]);
|
||||
checkarray(ichain(iarray([1, 2]), iarray([]), iarray([3, 4, 5])), [1, 2, 3, 4, 5]);
|
||||
});
|
||||
|
||||
test.case("chains iterator of iterators", check => {
|
||||
checkarray(ichainit(IEMPTY), []);
|
||||
checkarray(ichainit(iarray([iarray([1, 2, 3]), iarray([]), iarray([4, 5])])), [1, 2, 3, 4, 5]);
|
||||
});
|
||||
|
||||
test.case("repeats a value", check => {
|
||||
checkit(irepeat("a"), "a", "a", "a", "a");
|
||||
checkarray(irepeat("a", 3), ["a", "a", "a"]);
|
||||
});
|
||||
|
||||
test.case("loops an iterator", check => {
|
||||
checkarray(iloop(irange(3), 2), [0, 1, 2, 0, 1, 2]);
|
||||
checkit(iloop(irange(1)), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
|
||||
|
||||
let onloop = check.mockfunc("onloop");
|
||||
let iterator = iloop(irange(2), 3, onloop.func);
|
||||
function next() {
|
||||
let value;
|
||||
[value, iterator] = iterator();
|
||||
return value;
|
||||
}
|
||||
check.equals(next(), 0);
|
||||
check.called(onloop, 0);
|
||||
check.equals(next(), 1);
|
||||
check.called(onloop, 0);
|
||||
check.equals(next(), 0);
|
||||
check.called(onloop, 1);
|
||||
check.equals(next(), 1);
|
||||
check.called(onloop, 0);
|
||||
check.equals(next(), 0);
|
||||
check.called(onloop, 1);
|
||||
check.equals(next(), 1);
|
||||
check.called(onloop, 0);
|
||||
check.equals(next(), null);
|
||||
check.called(onloop, 0);
|
||||
});
|
||||
|
||||
test.case("maps an iterator", check => {
|
||||
checkarray(imap(IEMPTY, i => i * 2), []);
|
||||
checkarray(imap(irange(3), i => i * 2), [0, 2, 4]);
|
||||
});
|
||||
|
||||
test.case("filters an iterator", check => {
|
||||
checkarray(imap(IEMPTY, i => i % 3 == 0), []);
|
||||
checkarray(ifilter(irange(12), i => i % 3 == 0), [0, 3, 6, 9]);
|
||||
});
|
||||
|
||||
test.case("combines iterators", check => {
|
||||
let iterator = icombine(iarray([1, 2, 3]), iarray(["a", "b"]));
|
||||
checkarray(iterator, [[1, "a"], [1, "b"], [2, "a"], [2, "b"], [3, "a"], [3, "b"]]);
|
||||
});
|
||||
|
||||
test.case("zips iterators", check => {
|
||||
checkarray(izip(IEMPTY, IEMPTY), []);
|
||||
checkarray(izip(iarray([1, 2, 3]), iarray(["a", "b"])), [[1, "a"], [2, "b"]]);
|
||||
|
||||
checkarray(izipg(IEMPTY, IEMPTY), []);
|
||||
checkarray(izipg(iarray([1, 2, 3]), iarray(["a", "b"])), <[number | null, string | null][]>[[1, "a"], [2, "b"], [3, null]]);
|
||||
});
|
||||
|
||||
test.case("partitions iterators", check => {
|
||||
let [it1, it2] = ipartition(IEMPTY, () => true);
|
||||
checkarray(it1, []);
|
||||
checkarray(it2, []);
|
||||
|
||||
[it1, it2] = ipartition(irange(5), i => i % 2 == 0);
|
||||
checkarray(it1, [0, 2, 4]);
|
||||
checkarray(it2, [1, 3]);
|
||||
});
|
||||
|
||||
test.case("returns unique items", check => {
|
||||
checkarray(iunique(IEMPTY), []);
|
||||
checkarray(iunique(iarray([5, 3, 2, 3, 4, 5])), [5, 3, 2, 4]);
|
||||
checkarray(iunique(iarray([5, 3, 2, 3, 4, 5]), 4), [5, 3, 2, 4]);
|
||||
check.throw(() => imaterialize(iunique(iarray([5, 3, 2, 3, 4, 5]), 3)), "Unique count limit on iterator");
|
||||
});
|
||||
|
||||
test.case("uses ireduce for some common functions", check => {
|
||||
check.equals(isum(IEMPTY), 0);
|
||||
check.equals(isum(irange(4)), 6);
|
||||
|
||||
check.equals(icat(IEMPTY), "");
|
||||
check.equals(icat(iarray(["a", "bc", "d"])), "abcd");
|
||||
|
||||
check.equals(imin(IEMPTY), Infinity);
|
||||
check.equals(imin(iarray([3, 8, 2, 4])), 2);
|
||||
|
||||
check.equals(imax(IEMPTY), -Infinity);
|
||||
check.equals(imax(iarray([3, 8, 2, 4])), 8);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,363 @@
|
|||
/**
|
||||
* Lazy iterators to work on dynamic data sets without materializing them.
|
||||
*
|
||||
* They allow to work on infinite streams of values, with limited memory consumption.
|
||||
*
|
||||
* Functions in this file that do not return an Iterator are "materializing", meaning that they
|
||||
* may consume iterators up to the end, and will not work well on infinite iterators.
|
||||
*/
|
||||
module TK {
|
||||
/**
|
||||
* An iterator is a function without side effect, that returns the current value
|
||||
* and an iterator over the next values.
|
||||
*/
|
||||
export type Iterator<T> = () => [T | null, Iterator<T>];
|
||||
|
||||
function _getIEND(): [null, Iterator<any>] {
|
||||
return [null, _getIEND];
|
||||
}
|
||||
|
||||
/**
|
||||
* IEND is a return value for iterators, indicating end of iteration.
|
||||
*/
|
||||
export const IEND: [null, Iterator<any>] = [null, _getIEND];
|
||||
|
||||
/**
|
||||
* Empty iterator, returning IEND
|
||||
*/
|
||||
export const IEMPTY = () => IEND;
|
||||
|
||||
/**
|
||||
* Equivalent of Array.forEach for lazy iterators.
|
||||
*
|
||||
* If the callback returns *stopper*, the iteration is stopped.
|
||||
*/
|
||||
export function iforeach<T>(iterator: Iterator<T>, callback: (_: T) => any, stopper: any = null) {
|
||||
let value: T | null;
|
||||
[value, iterator] = iterator();
|
||||
while (value !== null) {
|
||||
let returned = callback(value);
|
||||
if (returned === stopper) {
|
||||
return;
|
||||
}
|
||||
[value, iterator] = iterator();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an iterator on an array
|
||||
*
|
||||
* The iterator will yield the next value each time it is called, then null when the array's end is reached.
|
||||
*/
|
||||
export function iarray<T>(array: T[], offset = 0): Iterator<T> {
|
||||
return () => {
|
||||
if (offset < array.length) {
|
||||
return [array[offset], iarray(array, offset + 1)];
|
||||
} else {
|
||||
return IEND;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an iterator yielding a single value
|
||||
*/
|
||||
export function isingle<T>(value: T): Iterator<T> {
|
||||
return iarray([value]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first item passing a predicate
|
||||
*/
|
||||
export function ifirst<T>(iterator: Iterator<T>, predicate: (item: T) => boolean): T | null {
|
||||
let result: T | null = null;
|
||||
iforeach(iterator, item => {
|
||||
if (predicate(item)) {
|
||||
result = item;
|
||||
return null;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first non-null result of a value-yielding predicate, applied to each iterator element
|
||||
*/
|
||||
export function ifirstmap<T1, T2>(iterator: Iterator<T1>, predicate: (item: T1) => T2 | null): T2 | null {
|
||||
let result: T2 | null = null;
|
||||
iforeach(iterator, item => {
|
||||
let mapped = predicate(item);
|
||||
if (mapped) {
|
||||
result = mapped;
|
||||
return null;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Materialize an array from consuming an iterator
|
||||
*
|
||||
* To avoid materializing infinite iterators (and bursting memory), the item count is limited to 1 million, and an
|
||||
* exception is thrown when this limit is reached.
|
||||
*/
|
||||
export function imaterialize<T>(iterator: Iterator<T>, limit = 1000000): T[] {
|
||||
let result: T[] = [];
|
||||
iforeach(iterator, value => {
|
||||
result.push(value);
|
||||
if (result.length >= limit) {
|
||||
throw new Error("Length limit on iterator materialize");
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over natural integers
|
||||
*
|
||||
* If *count* is not specified, the iterator is infinite
|
||||
*/
|
||||
export function irange(count: number = -1, start = 0, step = 1): Iterator<number> {
|
||||
return () => (count != 0) ? [start, irange(count - 1, start + step, step)] : IEND;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over numbers, by applying a step taken from an other iterator
|
||||
*
|
||||
* This iterator stops when the "step iterator" stops
|
||||
*
|
||||
* With no argument, istep() == irange()
|
||||
*/
|
||||
export function istep(start = 0, step = irepeat(1)): Iterator<number> {
|
||||
return () => {
|
||||
let [value, iterator] = step();
|
||||
return [start, value === null ? IEMPTY : istep(start + value, iterator)];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip a given number of values from an iterator, discarding them.
|
||||
*/
|
||||
export function iskip<T>(iterator: Iterator<T>, count = 1): Iterator<T> {
|
||||
let value: T | null;
|
||||
while (count--) {
|
||||
[value, iterator] = iterator();
|
||||
}
|
||||
return iterator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the value at a given position in the iterator
|
||||
*/
|
||||
export function iat<T>(iterator: Iterator<T>, position: number): T | null {
|
||||
if (position < 0) {
|
||||
return null;
|
||||
} else {
|
||||
if (position > 0) {
|
||||
iterator = iskip(iterator, position);
|
||||
}
|
||||
return iterator()[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Chain an iterator of iterators.
|
||||
*
|
||||
* This will yield values from the first yielded iterator, then the second one, and so on...
|
||||
*/
|
||||
export function ichainit<T>(iterators: Iterator<Iterator<T>>): Iterator<T> {
|
||||
return function () {
|
||||
let [iterators_head, iterators_tail] = iterators();
|
||||
if (iterators_head == null) {
|
||||
return IEND;
|
||||
} else {
|
||||
let [head, tail] = iterators_head();
|
||||
while (head == null) {
|
||||
[iterators_head, iterators_tail] = iterators_tail();
|
||||
if (iterators_head == null) {
|
||||
break;
|
||||
}
|
||||
[head, tail] = iterators_head();
|
||||
}
|
||||
return [head, ichain(tail, ichainit(iterators_tail))];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Chain iterators.
|
||||
*
|
||||
* This will yield values from the first iterator, then the second one, and so on...
|
||||
*/
|
||||
export function ichain<T>(...iterators: Iterator<T>[]): Iterator<T> {
|
||||
if (iterators.length == 0) {
|
||||
return IEMPTY;
|
||||
} else {
|
||||
return ichainit(iarray(iterators));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap an iterator, calling *onstart* when the first value of the wrapped iterator is yielded.
|
||||
*/
|
||||
function ionstart<T>(iterator: Iterator<T>, onstart: Function): Iterator<T> {
|
||||
return () => {
|
||||
let [head, tail] = iterator();
|
||||
if (head !== null) {
|
||||
onstart();
|
||||
}
|
||||
return [head, tail];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterator that repeats the same value.
|
||||
*/
|
||||
export function irepeat<T>(value: T, count = -1): Iterator<T> {
|
||||
return iloop(iarray([value]), count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loop an iterator for a number of times.
|
||||
*
|
||||
* If count is negative, if will loop forever (infinite iterator).
|
||||
*
|
||||
* onloop may be used to know when the iterator resets.
|
||||
*/
|
||||
export function iloop<T>(base: Iterator<T>, count = -1, onloop?: Function): Iterator<T> {
|
||||
if (count == 0) {
|
||||
return IEMPTY;
|
||||
} else {
|
||||
let next = onloop ? ionstart(base, onloop) : base;
|
||||
return ichainit(() => [base, iarray([iloop(next, count - 1)])]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterator version of "map".
|
||||
*/
|
||||
export function imap<T1, T2>(iterator: Iterator<T1>, mapfunc: (_: T1) => T2): Iterator<T2> {
|
||||
return () => {
|
||||
let [head, tail] = iterator();
|
||||
if (head === null) {
|
||||
return IEND;
|
||||
} else {
|
||||
return [mapfunc(head), imap(tail, mapfunc)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterator version of "reduce".
|
||||
*/
|
||||
export function ireduce<T>(iterator: Iterator<T>, reduce: (item1: T, item2: T) => T, init: T): T {
|
||||
let result = init;
|
||||
iforeach(iterator, item => {
|
||||
result = reduce(result, item);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterator version of "filter".
|
||||
*/
|
||||
export function ifilter<T>(iterator: Iterator<T>, filterfunc: (_: T) => boolean): Iterator<T> {
|
||||
return () => {
|
||||
let [value, iter] = iterator();
|
||||
while (value !== null && !filterfunc(value)) {
|
||||
[value, iter] = iter();
|
||||
}
|
||||
return [value, ifilter(iter, filterfunc)];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine two iterators.
|
||||
*
|
||||
* This iterates through the second one several times, so if one iterator may be infinite,
|
||||
* it should be the first one.
|
||||
*/
|
||||
export function icombine<T1, T2>(it1: Iterator<T1>, it2: Iterator<T2>): Iterator<[T1, T2]> {
|
||||
return ichainit(imap(it1, v1 => imap(it2, (v2): [T1, T2] => [v1, v2])));
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance two iterators at the same time, yielding item pairs
|
||||
*
|
||||
* Iteration will stop at the first of the two iterators that stops.
|
||||
*/
|
||||
export function izip<T1, T2>(it1: Iterator<T1>, it2: Iterator<T2>): Iterator<[T1, T2]> {
|
||||
return () => {
|
||||
let [val1, nit1] = it1();
|
||||
let [val2, nit2] = it2();
|
||||
if (val1 !== null && val2 !== null) {
|
||||
return [[val1, val2], izip(nit1, nit2)];
|
||||
} else {
|
||||
return IEND;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance two iterators at the same time, yielding item pairs (greedy version)
|
||||
*
|
||||
* Iteration will stop when both iterators are consumed, returning partial couples (null in the peer) if needed.
|
||||
*/
|
||||
export function izipg<T1, T2>(it1: Iterator<T1>, it2: Iterator<T2>): Iterator<[T1 | null, T2 | null]> {
|
||||
return () => {
|
||||
let [val1, nit1] = it1();
|
||||
let [val2, nit2] = it2();
|
||||
if (val1 === null && val2 === null) {
|
||||
return IEND;
|
||||
} else {
|
||||
return [[val1, val2], izipg(nit1, nit2)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Partition in two iterators, one with values that pass the predicate, the other with values that don't
|
||||
*/
|
||||
export function ipartition<T>(it: Iterator<T>, predicate: (item: T) => boolean): [Iterator<T>, Iterator<T>] {
|
||||
return [ifilter(it, predicate), ifilter(it, x => !predicate(x))];
|
||||
}
|
||||
|
||||
/**
|
||||
* Yield items from an iterator only once.
|
||||
*
|
||||
* Beware that even if this function is not materializing, it keeps track of yielded item, and may choke on
|
||||
* infinite or very long streams. Thus, no more than *limit* items will be yielded (an error is thrown
|
||||
* when this limit is reached).
|
||||
*
|
||||
* This function is O(n²)
|
||||
*/
|
||||
export function iunique<T>(it: Iterator<T>, limit = 1000000): Iterator<T> {
|
||||
function internal(it: Iterator<T>, limit: number, done: T[]): Iterator<T> {
|
||||
let [value, iterator] = it();
|
||||
while (value !== null && contains(done, value)) {
|
||||
[value, iterator] = iterator();
|
||||
}
|
||||
if (value === null) {
|
||||
return IEMPTY;
|
||||
} else if (limit <= 0) {
|
||||
throw new Error("Unique count limit on iterator");
|
||||
} else {
|
||||
let head = value;
|
||||
return () => [head, internal(it, limit - 1, done.concat([head]))];
|
||||
}
|
||||
}
|
||||
return internal(it, limit, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Common reduce shortcuts
|
||||
*/
|
||||
export const isum = (iterator: Iterator<number>) => ireduce(iterator, (a, b) => a + b, 0);
|
||||
export const icat = (iterator: Iterator<string>) => ireduce(iterator, (a, b) => a + b, "");
|
||||
export const imin = (iterator: Iterator<number>) => ireduce(iterator, Math.min, Infinity);
|
||||
export const imax = (iterator: Iterator<number>) => ireduce(iterator, Math.max, -Infinity);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
# tscommon
|
||||
|
||||
Typescript tools shared between projects
|
|
@ -0,0 +1,127 @@
|
|||
/// <reference path="RObject.ts" />
|
||||
|
||||
module TK.Specs {
|
||||
export class TestRObject extends RObject {
|
||||
x: number
|
||||
constructor(x = RandomGenerator.global.random()) {
|
||||
super();
|
||||
this.x = x;
|
||||
}
|
||||
};
|
||||
|
||||
testing("RObject", test => {
|
||||
test.setup(function () {
|
||||
(<any>RObject)._next_id = 0;
|
||||
})
|
||||
|
||||
test.case("gets a sequential id", check => {
|
||||
let o1 = new TestRObject();
|
||||
check.equals(o1.id, 0);
|
||||
let o2 = new TestRObject();
|
||||
let o3 = new TestRObject();
|
||||
check.equals(o2.id, 1);
|
||||
check.equals(o3.id, 2);
|
||||
|
||||
check.equals(rid(o3), 2);
|
||||
check.equals(rid(o3.id), 2);
|
||||
})
|
||||
|
||||
test.case("checks object identity", check => {
|
||||
let o1 = new TestRObject(1);
|
||||
let o2 = new TestRObject(1);
|
||||
let o3 = duplicate(o1, TK.Specs);
|
||||
|
||||
check.equals(o1.is(o1), true, "o1 is o1");
|
||||
check.equals(o1.is(o2), false, "o1 is not o2");
|
||||
check.equals(o1.is(o3), true, "o1 is o3");
|
||||
check.equals(o1.is(null), false, "o1 is not null");
|
||||
|
||||
check.equals(o2.is(o1), false, "o2 is not o1");
|
||||
check.equals(o2.is(o2), true, "o2 is o2");
|
||||
check.equals(o2.is(o3), false, "o2 is not o3");
|
||||
check.equals(o2.is(null), false, "o2 is not null");
|
||||
|
||||
check.equals(o3.is(o1), true, "o3 is o1");
|
||||
check.equals(o3.is(o2), false, "o3 is not o2");
|
||||
check.equals(o3.is(o3), true, "o3 is o3");
|
||||
check.equals(o3.is(null), false, "o3 is not null");
|
||||
})
|
||||
|
||||
test.case("resets global id on unserialize", check => {
|
||||
let o1 = new TestRObject();
|
||||
check.equals(o1.id, 0);
|
||||
let o2 = new TestRObject();
|
||||
check.equals(o2.id, 1);
|
||||
|
||||
let serializer = new Serializer(TK.Specs);
|
||||
let packed = serializer.serialize({ objs: [o1, o2] });
|
||||
|
||||
(<any>RObject)._next_id = 0;
|
||||
|
||||
check.equals(new TestRObject().id, 0);
|
||||
let unpacked = serializer.unserialize(packed);
|
||||
check.equals(unpacked, { objs: [o1, o2] });
|
||||
check.equals(new TestRObject().id, 2);
|
||||
serializer.unserialize(packed);
|
||||
check.equals(new TestRObject().id, 3);
|
||||
})
|
||||
})
|
||||
|
||||
testing("RObjectContainer", test => {
|
||||
test.case("stored objects and get them by their id", check => {
|
||||
let o1 = new TestRObject();
|
||||
let store = new RObjectContainer([o1]);
|
||||
|
||||
let o2 = new TestRObject();
|
||||
check.same(store.get(o1.id), o1);
|
||||
check.equals(store.get(o2.id), null);
|
||||
|
||||
store.add(o2);
|
||||
check.same(store.get(o1.id), o1);
|
||||
check.same(store.get(o2.id), o2);
|
||||
})
|
||||
|
||||
test.case("lists contained objects", check => {
|
||||
let store = new RObjectContainer<TestRObject>();
|
||||
let o1 = store.add(new TestRObject());
|
||||
let o2 = store.add(new TestRObject());
|
||||
|
||||
check.equals(store.count(), 2, "count=2");
|
||||
|
||||
let objects = store.list();
|
||||
check.equals(objects.length, 2, "list length=2");
|
||||
check.contains(objects, o1, "list contains o1");
|
||||
check.contains(objects, o2, "list contains o2");
|
||||
|
||||
objects = imaterialize(store.iterator());
|
||||
check.equals(objects.length, 2, "list length=2");
|
||||
check.contains(objects, o1, "list contains o1");
|
||||
check.contains(objects, o2, "list contains o2");
|
||||
|
||||
let ids = store.ids();
|
||||
check.equals(ids.length, 2, "ids length=2");
|
||||
check.contains(ids, o1.id, "list contains o1.id");
|
||||
check.contains(ids, o2.id, "list contains o2.id");
|
||||
})
|
||||
|
||||
test.case("removes objects", check => {
|
||||
let store = new RObjectContainer<TestRObject>();
|
||||
let o1 = store.add(new TestRObject());
|
||||
let o2 = store.add(new TestRObject());
|
||||
|
||||
check.in("initial", check => {
|
||||
check.equals(store.count(), 2, "count=2");
|
||||
check.same(store.get(o1.id), o1, "o1 present");
|
||||
check.same(store.get(o2.id), o2, "o2 present");
|
||||
});
|
||||
|
||||
store.remove(o1);
|
||||
|
||||
check.in("removed o1", check => {
|
||||
check.equals(store.count(), 1, "count=1");
|
||||
check.same(store.get(o1.id), null, "o1 missing");
|
||||
check.same(store.get(o2.id), o2, "o2 present");
|
||||
});
|
||||
})
|
||||
})
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
module TK {
|
||||
export type RObjectId = number
|
||||
|
||||
/**
|
||||
* Returns the id of an object
|
||||
*/
|
||||
export function rid(obj: RObject | RObjectId): number {
|
||||
return (obj instanceof RObject) ? obj.id : obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Referenced objects, with unique ID.
|
||||
*
|
||||
* Objects extending this class will have an automatic unique ID, and may be tracked from an RObjectContainer.
|
||||
*/
|
||||
export class RObject {
|
||||
readonly id: RObjectId = RObject._next_id++
|
||||
private static _next_id = 0
|
||||
|
||||
postUnserialize() {
|
||||
if (this.id >= RObject._next_id) {
|
||||
RObject._next_id = this.id + 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that two objects are the same (only by comparing their ID)
|
||||
*/
|
||||
is(other: RObject | RObjectId | null): boolean {
|
||||
if (other === null) {
|
||||
return false;
|
||||
} else if (other instanceof RObject) {
|
||||
return this.id === other.id;
|
||||
} else {
|
||||
return this.id === other;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Container to track referenced objects
|
||||
*/
|
||||
export class RObjectContainer<T extends RObject> {
|
||||
private objects: { [index: number]: T } = {}
|
||||
|
||||
constructor(objects: T[] = []) {
|
||||
objects.forEach(obj => this.add(obj));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an object to the container
|
||||
*/
|
||||
add(object: T): T {
|
||||
this.objects[object.id] = object;
|
||||
return object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an object from the container
|
||||
*/
|
||||
remove(object: T): void {
|
||||
delete this.objects[object.id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an object from the container by its id
|
||||
*/
|
||||
get(id: RObjectId): T | null {
|
||||
return this.objects[id] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of objects
|
||||
*/
|
||||
count(): number {
|
||||
return this.list().length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all contained ids (list)
|
||||
*/
|
||||
ids(): RObjectId[] {
|
||||
return this.list().map(obj => obj.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all contained objects (list)
|
||||
*/
|
||||
list(): T[] {
|
||||
return values(this.objects);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all contained objects (iterator)
|
||||
*/
|
||||
iterator(): Iterator<T> {
|
||||
return iarray(this.list());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
module TK {
|
||||
testing("RandomGenerator", test => {
|
||||
test.case("produces floats", check => {
|
||||
var gen = new RandomGenerator();
|
||||
|
||||
var i = 100;
|
||||
while (i--) {
|
||||
var value = gen.random();
|
||||
check.greaterorequal(value, 0);
|
||||
check.greater(1, value);
|
||||
}
|
||||
});
|
||||
|
||||
test.case("produces integers", check => {
|
||||
var gen = new RandomGenerator();
|
||||
|
||||
var i = 100;
|
||||
while (i--) {
|
||||
var value = gen.randInt(5, 12);
|
||||
check.equals(Math.round(value), value);
|
||||
check.greater(value, 4);
|
||||
check.greater(13, value);
|
||||
}
|
||||
});
|
||||
|
||||
test.case("chooses from an array", check => {
|
||||
var gen = new RandomGenerator();
|
||||
|
||||
check.equals(gen.choice([5]), 5);
|
||||
|
||||
var i = 100;
|
||||
while (i--) {
|
||||
var value = gen.choice(["test", "thing"]);
|
||||
check.contains(["thing", "test"], value);
|
||||
}
|
||||
});
|
||||
|
||||
test.case("samples from an array", check => {
|
||||
var gen = new RandomGenerator();
|
||||
|
||||
var i = 100;
|
||||
while (i-- > 1) {
|
||||
var input = [1, 2, 3, 4, 5];
|
||||
var sample = gen.sample(input, i % 5 + 1);
|
||||
check.same(sample.length, i % 5 + 1);
|
||||
sample.forEach((num, idx) => {
|
||||
check.contains(input, num);
|
||||
check.notcontains(sample.filter((ival, iidx) => iidx != idx), num);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test.case("choose from weighted ranges", check => {
|
||||
let gen = new RandomGenerator();
|
||||
|
||||
check.equals(gen.weighted([]), -1);
|
||||
check.equals(gen.weighted([1]), 0);
|
||||
check.equals(gen.weighted([0, 1, 0]), 1);
|
||||
check.equals(gen.weighted([0, 12, 0]), 1);
|
||||
|
||||
gen = new SkewedRandomGenerator([0, 0.5, 0.7, 0.8, 0.9999]);
|
||||
check.equals(gen.weighted([4, 3, 0, 2, 1]), 0);
|
||||
check.equals(gen.weighted([4, 3, 0, 2, 1]), 1);
|
||||
check.equals(gen.weighted([4, 3, 0, 2, 1]), 3);
|
||||
check.equals(gen.weighted([4, 3, 0, 2, 1]), 3);
|
||||
check.equals(gen.weighted([4, 3, 0, 2, 1]), 4);
|
||||
});
|
||||
|
||||
test.case("generates ids", check => {
|
||||
let gen = new SkewedRandomGenerator([0, 0.4, 0.2, 0.1, 0.3, 0.8]);
|
||||
check.equals(gen.id(6, "abcdefghij"), "aecbdi");
|
||||
});
|
||||
|
||||
test.case("can be skewed", check => {
|
||||
var gen = new SkewedRandomGenerator([0, 0.5, 0.2, 0.9]);
|
||||
|
||||
check.equals(gen.random(), 0);
|
||||
check.equals(gen.random(), 0.5);
|
||||
check.equals(gen.randInt(4, 8), 5);
|
||||
check.equals(gen.random(), 0.9);
|
||||
|
||||
var value = gen.random();
|
||||
check.greaterorequal(value, 0);
|
||||
check.greater(1, value);
|
||||
|
||||
gen = new SkewedRandomGenerator([0.7], true);
|
||||
check.equals(gen.random(), 0.7);
|
||||
check.equals(gen.random(), 0.7);
|
||||
check.equals(gen.random(), 0.7);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
module TK {
|
||||
/*
|
||||
* Random generator.
|
||||
*/
|
||||
export class RandomGenerator {
|
||||
static global: RandomGenerator = new RandomGenerator();
|
||||
|
||||
postUnserialize() {
|
||||
this.random = Math.random;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a random number in the (0.0 included -> 1.0 excluded) range
|
||||
*/
|
||||
random = Math.random;
|
||||
|
||||
/**
|
||||
* Get a random number in the (*from* included -> *to* included) range
|
||||
*/
|
||||
randInt(from: number, to: number): number {
|
||||
return Math.floor(this.random() * (to - from + 1)) + from;
|
||||
}
|
||||
|
||||
/**
|
||||
* Choose a random item in an array
|
||||
*/
|
||||
choice<T>(input: T[]): T {
|
||||
return input[this.randInt(0, input.length - 1)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Choose a random sample of items from an array
|
||||
*/
|
||||
sample<T>(input: T[], count: number): T[] {
|
||||
var minput = input.slice();
|
||||
var result: T[] = [];
|
||||
while (count--) {
|
||||
var idx = this.randInt(0, minput.length - 1);
|
||||
result.push(minput[idx]);
|
||||
minput.splice(idx, 1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a random boolean (coin toss)
|
||||
*/
|
||||
bool(): boolean {
|
||||
return this.randInt(0, 1) == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the range in which the number falls, ranges being weighted
|
||||
*/
|
||||
weighted(weights: number[]): number {
|
||||
if (weights.length == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
let total = sum(weights);
|
||||
if (total == 0) {
|
||||
return 0;
|
||||
} else {
|
||||
let cumul = 0;
|
||||
weights = weights.map(weight => {
|
||||
cumul += weight / total;
|
||||
return cumul;
|
||||
});
|
||||
let r = this.random();
|
||||
for (let i = 0; i < weights.length; i++) {
|
||||
if (r < weights[i]) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return weights.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random id string, composed of ascii characters
|
||||
*/
|
||||
id(length: number, chars?: string): string {
|
||||
if (!chars) {
|
||||
chars = range(94).map(i => String.fromCharCode(i + 33)).join("");
|
||||
}
|
||||
return range(length).map(() => this.choice(<any>chars)).join("");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Random generator that produces a series of fixed numbers before going back to random ones.
|
||||
*/
|
||||
export class SkewedRandomGenerator extends RandomGenerator {
|
||||
i = 0;
|
||||
suite: number[];
|
||||
loop: boolean;
|
||||
|
||||
constructor(suite: number[], loop = false) {
|
||||
super();
|
||||
|
||||
this.suite = suite;
|
||||
this.loop = loop;
|
||||
}
|
||||
|
||||
random = () => {
|
||||
var result = this.suite[this.i];
|
||||
this.i += 1;
|
||||
if (this.loop && this.i == this.suite.length) {
|
||||
this.i = 0;
|
||||
}
|
||||
return (typeof result == "undefined") ? Math.random() : result;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
module TK.Specs {
|
||||
export class TestSerializerObj1 {
|
||||
a: number;
|
||||
constructor(a = 0) {
|
||||
this.a = a;
|
||||
}
|
||||
}
|
||||
|
||||
export class TestSerializerObj2 {
|
||||
a = () => 1
|
||||
b = [(obj: any) => 2]
|
||||
}
|
||||
|
||||
export class TestSerializerObj3 {
|
||||
a = [1, 2];
|
||||
postUnserialize() {
|
||||
remove(this.a, 2);
|
||||
}
|
||||
}
|
||||
|
||||
testing("Serializer", test => {
|
||||
function checkReversability(obj: any, namespace = TK.Specs): any {
|
||||
var serializer = new Serializer(TK.Specs);
|
||||
var data = serializer.serialize(obj);
|
||||
serializer = new Serializer(TK.Specs);
|
||||
var loaded = serializer.unserialize(data);
|
||||
test.check.equals(loaded, obj);
|
||||
return loaded;
|
||||
}
|
||||
|
||||
test.case("serializes simple objects", check => {
|
||||
var obj = {
|
||||
"a": 5,
|
||||
"b": null,
|
||||
"c": [{ "a": 2 }, "test"]
|
||||
};
|
||||
checkReversability(obj);
|
||||
});
|
||||
|
||||
test.case("restores objects constructed from class", check => {
|
||||
var loaded = checkReversability(new TestSerializerObj1(5));
|
||||
check.equals(loaded.a, 5);
|
||||
check.same(loaded instanceof TestSerializerObj1, true, "not a TestSerializerObj1 instance");
|
||||
});
|
||||
|
||||
test.case("stores one version of the same object", check => {
|
||||
var a = new TestSerializerObj1(8);
|
||||
var b = new TestSerializerObj1(8);
|
||||
var c = {
|
||||
'r': a,
|
||||
's': ["test", a],
|
||||
't': a,
|
||||
'u': b
|
||||
};
|
||||
var loaded = checkReversability(c);
|
||||
check.same(loaded.t, loaded.r);
|
||||
check.same(loaded.s[1], loaded.r);
|
||||
check.notsame(loaded.u, loaded.r);
|
||||
});
|
||||
|
||||
test.case("handles circular references", check => {
|
||||
var a: any = { b: {} };
|
||||
a.b.c = a;
|
||||
|
||||
var loaded = checkReversability(a);
|
||||
});
|
||||
|
||||
test.case("ignores some classes", check => {
|
||||
var serializer = new Serializer(TK.Specs);
|
||||
serializer.addIgnoredClass("TestSerializerObj1");
|
||||
|
||||
var data = serializer.serialize({ a: 5, b: new TestSerializerObj1() });
|
||||
var loaded = serializer.unserialize(data);
|
||||
|
||||
check.equals(loaded, { a: 5, b: undefined });
|
||||
});
|
||||
|
||||
test.case("ignores functions", check => {
|
||||
let serializer = new Serializer(TK.Specs);
|
||||
let data = serializer.serialize({ obj: new TestSerializerObj2() });
|
||||
let loaded = serializer.unserialize(data);
|
||||
|
||||
let expected = <any>new TestSerializerObj2();
|
||||
expected.a = undefined;
|
||||
expected.b[0] = undefined;
|
||||
check.equals(loaded, { obj: expected });
|
||||
});
|
||||
|
||||
test.case("calls specific postUnserialize", check => {
|
||||
let serializer = new Serializer(TK.Specs);
|
||||
let data = serializer.serialize({ obj: new TestSerializerObj3() });
|
||||
let loaded = serializer.unserialize(data);
|
||||
|
||||
let expected = new TestSerializerObj3();
|
||||
expected.a = [1];
|
||||
check.equals(loaded, { obj: expected });
|
||||
});
|
||||
|
||||
test.case("finds TS namespace, even from sub-namespace", check => {
|
||||
checkReversability(new Timer());
|
||||
checkReversability(new RandomGenerator());
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
module TK {
|
||||
|
||||
function isObject(value: any): boolean {
|
||||
return value instanceof Object && !Array.isArray(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* A typescript object serializer.
|
||||
*/
|
||||
export class Serializer {
|
||||
namespace: any;
|
||||
ignored: string[] = [];
|
||||
|
||||
constructor(namespace: any = TK) {
|
||||
this.namespace = namespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a class to the ignore list
|
||||
*/
|
||||
addIgnoredClass(name: string) {
|
||||
this.ignored.push(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an object from a constructor name
|
||||
*/
|
||||
constructObject(ctype: string): Object {
|
||||
if (ctype == "Object") {
|
||||
return {};
|
||||
} else {
|
||||
let cl = this.namespace[ctype];
|
||||
if (cl) {
|
||||
return Object.create(cl.prototype);
|
||||
} else {
|
||||
cl = (<any>TK)[ctype];
|
||||
if (cl) {
|
||||
return Object.create(cl.prototype);
|
||||
} else {
|
||||
console.error("Can't find class", ctype);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize an object to a string
|
||||
*/
|
||||
serialize(obj: any): string {
|
||||
// Collect objects
|
||||
var objects: Object[] = [];
|
||||
var stats: any = {};
|
||||
crawl(obj, value => {
|
||||
if (isObject(value)) {
|
||||
var vtype = classname(value);
|
||||
if (vtype != "" && this.ignored.indexOf(vtype) < 0) {
|
||||
stats[vtype] = (stats[vtype] || 0) + 1;
|
||||
add(objects, value);
|
||||
return value;
|
||||
} else {
|
||||
return TK.STOP_CRAWLING;
|
||||
}
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
});
|
||||
//console.log("Serialize stats", stats);
|
||||
|
||||
// Serialize objects list, transforming deeper objects to links
|
||||
var fobjects = objects.map(value => <Object>{ $c: classname(value), $f: merge({}, value) });
|
||||
return JSON.stringify(fobjects, (key, value) => {
|
||||
if (key != "$f" && isObject(value) && !value.hasOwnProperty("$c") && !value.hasOwnProperty("$i")) {
|
||||
return { $i: objects.indexOf(value) };
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unserialize a string to an object
|
||||
*/
|
||||
unserialize(data: string): any {
|
||||
// Unserialize objects list
|
||||
var fobjects = JSON.parse(data);
|
||||
|
||||
// Reconstruct objects
|
||||
var objects = fobjects.map((objdata: any) => merge(this.constructObject(objdata['$c']), objdata['$f']));
|
||||
|
||||
// Reconnect links
|
||||
crawl(objects, value => {
|
||||
if (value instanceof Object && value.hasOwnProperty('$i')) {
|
||||
return objects[value['$i']];
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}, true);
|
||||
|
||||
// Post unserialize hooks
|
||||
crawl(objects[0], value => {
|
||||
if (value instanceof Object && typeof value.postUnserialize == "function") {
|
||||
value.postUnserialize();
|
||||
}
|
||||
});
|
||||
|
||||
// First object was the root
|
||||
return objects[0];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,330 @@
|
|||
/**
|
||||
* Various testing functions.
|
||||
*/
|
||||
module TK {
|
||||
export type FakeClock = { forward: (milliseconds: number) => void }
|
||||
export type Mock<F extends Function> = { func: F, getCalls: () => any[][], reset: () => void }
|
||||
|
||||
/**
|
||||
* Main test suite descriptor
|
||||
*/
|
||||
export function testing(desc: string, body: (test: TestSuite) => void) {
|
||||
if (typeof describe != "undefined") {
|
||||
describe(desc, () => {
|
||||
beforeEach(() => jasmine.addMatchers(CUSTOM_MATCHERS));
|
||||
|
||||
let test = new TestSuite(desc);
|
||||
body(test);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test suite (group of test cases)
|
||||
*/
|
||||
export class TestSuite {
|
||||
private desc: string
|
||||
constructor(desc: string) {
|
||||
this.desc = desc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a setup step for each case of the suite
|
||||
*/
|
||||
setup(body: Function, cleanup?: Function): void {
|
||||
beforeEach(() => body());
|
||||
if (cleanup) {
|
||||
afterEach(() => cleanup());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an asynchronous setup step for each case of the suite
|
||||
*/
|
||||
asetup(body: () => Promise<void>, cleanup?: Function): void {
|
||||
beforeEach((done) => body().then(done));
|
||||
if (cleanup) {
|
||||
afterEach(() => cleanup());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Describe a single test case
|
||||
*/
|
||||
case(desc: string, body: (ctx: TestContext) => void): void {
|
||||
it(desc, () => {
|
||||
console.debug(`${this.desc} ${desc}`);
|
||||
body(new TestContext())
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Describe an asynchronous test case
|
||||
*/
|
||||
acase<T>(desc: string, body: (ctx: TestContext) => Promise<T>): void {
|
||||
it(desc, done => {
|
||||
console.debug(`${this.desc} ${desc}`);
|
||||
body(new TestContext()).then(done).catch(done.fail);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup fake clock for the suite
|
||||
*/
|
||||
clock(): FakeClock {
|
||||
let current = 0;
|
||||
|
||||
beforeEach(function () {
|
||||
current = 0;
|
||||
jasmine.clock().install();
|
||||
spyOn(Timer, "nowMs").and.callFake(() => current);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
jasmine.clock().uninstall();
|
||||
});
|
||||
|
||||
return {
|
||||
forward: milliseconds => {
|
||||
current += milliseconds;
|
||||
jasmine.clock().tick(milliseconds);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Out-of-context assertion helpers
|
||||
*
|
||||
* It is better to use in-context checks, for better information
|
||||
*/
|
||||
get check(): TestContext {
|
||||
return new TestContext();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A test context, with assertion helpers
|
||||
*/
|
||||
export class TestContext {
|
||||
info: string[];
|
||||
|
||||
constructor(info: string[] = []) {
|
||||
this.info = info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a sub context (adds information for all assertions done with this context)
|
||||
*/
|
||||
sub(info: string): TestContext {
|
||||
return new TestContext(this.info.concat([info]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a body in a sub context
|
||||
*/
|
||||
in(info: string, body: (ctx: TestContext) => void): void {
|
||||
body(this.sub(info));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a message, with context information added
|
||||
*/
|
||||
message(message?: string): string | undefined {
|
||||
let parts = this.info;
|
||||
if (message) {
|
||||
parts = parts.concat([message]);
|
||||
}
|
||||
return parts.length ? parts.join(" - ") : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch an object's method with a mock
|
||||
*
|
||||
* Replacement may be:
|
||||
* - undefined to call through
|
||||
* - null to not call anything
|
||||
* - a fake function to call instead
|
||||
*
|
||||
* All patches are removed at the end of a case
|
||||
*/
|
||||
patch<T extends Object, K extends keyof T, F extends T[K] & Function>(obj: T, method: K, replacement?: F | null): Mock<F> {
|
||||
let spy = spyOn(obj, <any>method);
|
||||
if (replacement === null) {
|
||||
spy.and.stub();
|
||||
} else if (replacement) {
|
||||
spy.and.callFake(<any>replacement);
|
||||
} else {
|
||||
spy.and.callThrough();
|
||||
}
|
||||
|
||||
return {
|
||||
func: <any>spy,
|
||||
getCalls: () => spy.calls.all().map(info => info.args),
|
||||
reset: () => spy.calls.reset()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mock function
|
||||
*/
|
||||
mockfunc<F extends Function>(name = "mock", replacement?: F): Mock<F> {
|
||||
let spy = jasmine.createSpy(name, <any>replacement);
|
||||
|
||||
if (replacement) {
|
||||
spy = spy.and.callThrough();
|
||||
}
|
||||
|
||||
return {
|
||||
func: <any>spy,
|
||||
getCalls: () => spy.calls.all().map(info => info.args),
|
||||
reset: () => spy.calls.reset()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that a mock have been called a given number of times, or with specific args
|
||||
*/
|
||||
called(mock: Mock<any>, calls: number | any[][], reset = true): void {
|
||||
if (typeof calls == "number") {
|
||||
expect(mock.getCalls().length).toEqual(calls, this.message());
|
||||
} else {
|
||||
expect(mock.getCalls()).toEqual(calls, this.message());
|
||||
}
|
||||
|
||||
if (reset) {
|
||||
mock.reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that a function call throws an error
|
||||
*/
|
||||
throw(call: Function, error?: string | Error): void {
|
||||
if (typeof error == "undefined") {
|
||||
expect(call).toThrow();
|
||||
} else if (typeof error == "string") {
|
||||
expect(call).toThrowError(error);
|
||||
} else {
|
||||
expect(call).toThrow(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that an object is an instance of a given type
|
||||
*/
|
||||
instance<T>(obj: any, classref: { new(...args: any[]): T }, message: string): obj is T {
|
||||
let result = obj instanceof classref;
|
||||
expect(result).toBe(true, this.message(message));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that two references are the same object
|
||||
*/
|
||||
same<T extends Object>(ref1: T | null | undefined, ref2: T | null | undefined, message?: string): void {
|
||||
expect(ref1).toBe(ref2, this.message(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that two references are not the same object
|
||||
*/
|
||||
notsame<T extends Object>(ref1: T | null, ref2: T | null, message?: string): void {
|
||||
expect(ref1).not.toBe(ref2, this.message(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that two values are equal, in the sense of deep comparison
|
||||
*/
|
||||
equals<T>(val1: T, val2: T, message?: string): void {
|
||||
expect(val1).toEqual(val2, this.message(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that two values differs, in the sense of deep comparison
|
||||
*/
|
||||
notequals<T>(val1: T, val2: T, message?: string): void {
|
||||
expect(val1).not.toEqual(val2, this.message(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that a numerical value is close to another, at a given number of digits precision
|
||||
*/
|
||||
nears(val1: number, val2: number, precision = 8, message?: string): void {
|
||||
if (precision != Math.round(precision)) {
|
||||
throw new Error(`'nears' precision should be integer, not {precision}`);
|
||||
}
|
||||
expect(val1).toBeCloseTo(val2, precision, this.message(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that a numerical value is greater than another
|
||||
*/
|
||||
greater(val1: number, val2: number, message?: string): void {
|
||||
expect(val1).toBeGreaterThan(val2, this.message(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that a numerical value is greater than or equal to another
|
||||
*/
|
||||
greaterorequal(val1: number, val2: number, message?: string): void {
|
||||
expect(val1).toBeGreaterThanOrEqual(val2, this.message(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that a string matches a regex
|
||||
*/
|
||||
regex(pattern: RegExp, value: string, message?: string): void {
|
||||
expect(value).toMatch(pattern, this.message(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that an array contains an item
|
||||
*/
|
||||
contains<T>(array: T[], item: T, message?: string): void {
|
||||
expect(array).toContain(item, this.message(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that an array does not contain an item
|
||||
*/
|
||||
notcontains<T>(array: T[], item: T, message?: string): void {
|
||||
expect(array).not.toContain(item, this.message(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check than an object contains a set of properties
|
||||
*/
|
||||
containing<T>(val: T, props: Partial<T>, message?: string): void {
|
||||
expect(val).toEqual(jasmine.objectContaining(props), this.message(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fail the whole case
|
||||
*/
|
||||
fail(message?: string): void {
|
||||
fail(this.message(message));
|
||||
}
|
||||
}
|
||||
|
||||
const CUSTOM_MATCHERS = {
|
||||
toEqual: function (util: any, customEqualityTesters: any) {
|
||||
customEqualityTesters = customEqualityTesters || [];
|
||||
|
||||
return {
|
||||
compare: function (actual: any, expected: any, message?: string) {
|
||||
let result: any = { pass: false };
|
||||
let diffBuilder = (<any>jasmine).DiffBuilder();
|
||||
|
||||
result.pass = util.equals(actual, expected, customEqualityTesters, diffBuilder);
|
||||
|
||||
result.message = diffBuilder.getMessage();
|
||||
if (message) {
|
||||
result.message += " " + message;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
module TK.Specs {
|
||||
testing("Timer", test => {
|
||||
let clock = test.clock();
|
||||
|
||||
test.case("schedules and cancels future calls", check => {
|
||||
let timer = new Timer();
|
||||
|
||||
let called: any[] = [];
|
||||
let callback = (item: any) => called.push(item);
|
||||
|
||||
let s1 = timer.schedule(50, () => callback(1));
|
||||
let s2 = timer.schedule(150, () => callback(2));
|
||||
let s3 = timer.schedule(250, () => callback(3));
|
||||
|
||||
check.equals(called, []);
|
||||
clock.forward(100);
|
||||
check.equals(called, [1]);
|
||||
timer.cancel(s1);
|
||||
check.equals(called, [1]);
|
||||
clock.forward(100);
|
||||
check.equals(called, [1, 2]);
|
||||
timer.cancel(s3);
|
||||
clock.forward(100);
|
||||
check.equals(called, [1, 2]);
|
||||
clock.forward(1000);
|
||||
check.equals(called, [1, 2]);
|
||||
});
|
||||
|
||||
test.case("may cancel all scheduled", check => {
|
||||
let timer = new Timer();
|
||||
|
||||
let called: any[] = [];
|
||||
let callback = (item: any) => called.push(item);
|
||||
|
||||
timer.schedule(150, () => callback(1));
|
||||
timer.schedule(50, () => callback(2));
|
||||
timer.schedule(500, () => callback(3));
|
||||
|
||||
check.equals(called, []);
|
||||
|
||||
clock.forward(100);
|
||||
|
||||
check.equals(called, [2]);
|
||||
|
||||
clock.forward(100);
|
||||
|
||||
check.equals(called, [2, 1]);
|
||||
|
||||
timer.cancelAll();
|
||||
|
||||
clock.forward(1000);
|
||||
|
||||
check.equals(called, [2, 1]);
|
||||
|
||||
timer.schedule(50, () => callback(4));
|
||||
timer.schedule(150, () => callback(5));
|
||||
|
||||
clock.forward(100);
|
||||
|
||||
check.equals(called, [2, 1, 4]);
|
||||
|
||||
timer.cancelAll(true);
|
||||
|
||||
clock.forward(100);
|
||||
|
||||
check.equals(called, [2, 1, 4]);
|
||||
|
||||
timer.schedule(50, () => callback(6));
|
||||
|
||||
clock.forward(100);
|
||||
|
||||
check.equals(called, [2, 1, 4]);
|
||||
});
|
||||
|
||||
test.case("may switch to synchronous mode", check => {
|
||||
let timer = new Timer(true);
|
||||
let called: any[] = [];
|
||||
let callback = (item: any) => called.push(item);
|
||||
|
||||
timer.schedule(50, () => callback(1));
|
||||
check.equals(called, [1]);
|
||||
});
|
||||
|
||||
test.acase("sleeps asynchronously", async check => {
|
||||
let timer = new Timer();
|
||||
let x = 1;
|
||||
|
||||
let promise = timer.sleep(500).then(() => {
|
||||
x++;
|
||||
});
|
||||
check.equals(x, 1);
|
||||
|
||||
clock.forward(300);
|
||||
check.equals(x, 1);
|
||||
|
||||
clock.forward(300);
|
||||
check.equals(x, 1);
|
||||
|
||||
await promise;
|
||||
check.equals(x, 2);
|
||||
});
|
||||
|
||||
test.case("gives current time in milliseconds", check => {
|
||||
check.equals(Timer.nowMs(), 0);
|
||||
|
||||
clock.forward(5);
|
||||
|
||||
check.equals(Timer.nowMs(), 5);
|
||||
let t = Timer.nowMs();
|
||||
|
||||
clock.forward(10);
|
||||
|
||||
check.equals(Timer.nowMs(), 15);
|
||||
check.equals(Timer.fromMs(t), 10);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
module TK {
|
||||
/**
|
||||
* Timing utility.
|
||||
*
|
||||
* This extends the standard setTimeout feature.
|
||||
*/
|
||||
export class Timer {
|
||||
// Global timer shared by the whole project
|
||||
static global = new Timer();
|
||||
|
||||
// Global synchronous timer for unit tests
|
||||
static synchronous = new Timer(true);
|
||||
|
||||
private sync = false;
|
||||
|
||||
private locked = false;
|
||||
|
||||
private scheduled: any[] = [];
|
||||
|
||||
constructor(sync = false) {
|
||||
this.sync = sync;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current timestamp in milliseconds
|
||||
*/
|
||||
static nowMs(): number {
|
||||
return (new Date()).getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the elapsed time in milliseconds since a previous timestamp
|
||||
*/
|
||||
static fromMs(previous: number): number {
|
||||
return this.nowMs() - previous;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the timer is synchronous
|
||||
*/
|
||||
isSynchronous() {
|
||||
return this.sync;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel all scheduled calls.
|
||||
*
|
||||
* If lock=true, no further scheduling will be allowed.
|
||||
*/
|
||||
cancelAll(lock = false) {
|
||||
this.locked = lock;
|
||||
|
||||
let scheduled = this.scheduled;
|
||||
this.scheduled = [];
|
||||
|
||||
scheduled.forEach(handle => clearTimeout(handle));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a scheduled callback.
|
||||
*/
|
||||
cancel(scheduled: any) {
|
||||
if (remove(this.scheduled, scheduled)) {
|
||||
clearTimeout(scheduled);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a callback to be called later (time is in milliseconds).
|
||||
*/
|
||||
schedule(delay: number, callback: Function): any {
|
||||
if (this.locked) {
|
||||
return null;
|
||||
} else if (this.sync || delay <= 0) {
|
||||
callback();
|
||||
return null;
|
||||
} else {
|
||||
let handle = setTimeout(() => {
|
||||
remove(this.scheduled, handle);
|
||||
callback();
|
||||
}, delay);
|
||||
add(this.scheduled, handle);
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously sleep a given time.
|
||||
*/
|
||||
async sleep(ms: number): Promise<any> {
|
||||
return new Promise(resolve => {
|
||||
this.schedule(ms, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
postUnserialize() {
|
||||
this.scheduled = [];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
module TK.Specs {
|
||||
testing("Toggle", test => {
|
||||
let on_calls = 0;
|
||||
let off_calls = 0;
|
||||
let clock = test.clock();
|
||||
|
||||
test.setup(function () {
|
||||
on_calls = 0;
|
||||
off_calls = 0;
|
||||
});
|
||||
|
||||
function newToggle(): Toggle {
|
||||
return new Toggle(() => on_calls++, () => off_calls++);
|
||||
}
|
||||
|
||||
function checkstate(on: number, off: number) {
|
||||
test.check.same(on_calls, on);
|
||||
test.check.same(off_calls, off);
|
||||
on_calls = 0;
|
||||
off_calls = 0;
|
||||
}
|
||||
|
||||
test.case("toggles on and off", check => {
|
||||
let toggle = newToggle();
|
||||
let client = toggle.manipulate("test");
|
||||
checkstate(0, 0);
|
||||
|
||||
let result = client(true);
|
||||
check.equals(result, true);
|
||||
checkstate(1, 0);
|
||||
|
||||
result = client(true);
|
||||
check.equals(result, true);
|
||||
checkstate(0, 0);
|
||||
|
||||
clock.forward(10000000);
|
||||
checkstate(0, 0);
|
||||
|
||||
result = client(false);
|
||||
check.equals(result, false);
|
||||
checkstate(0, 1);
|
||||
|
||||
result = client(false);
|
||||
check.equals(result, false);
|
||||
checkstate(0, 0);
|
||||
|
||||
clock.forward(10000000);
|
||||
checkstate(0, 0);
|
||||
|
||||
result = client(true);
|
||||
check.equals(result, true);
|
||||
checkstate(1, 0);
|
||||
|
||||
let client2 = toggle.manipulate("test2");
|
||||
result = client2(true);
|
||||
check.equals(result, true);
|
||||
checkstate(0, 0);
|
||||
|
||||
result = client(false);
|
||||
check.equals(result, true);
|
||||
checkstate(0, 0);
|
||||
|
||||
result = client2(false);
|
||||
check.equals(result, false);
|
||||
checkstate(0, 1);
|
||||
})
|
||||
|
||||
test.case("switches between on and off", check => {
|
||||
let toggle = newToggle();
|
||||
let client = toggle.manipulate("test");
|
||||
checkstate(0, 0);
|
||||
|
||||
let result = client();
|
||||
check.equals(result, true);
|
||||
checkstate(1, 0);
|
||||
|
||||
result = client();
|
||||
check.equals(result, false);
|
||||
checkstate(0, 1);
|
||||
|
||||
result = client();
|
||||
check.equals(result, true);
|
||||
checkstate(1, 0);
|
||||
|
||||
let client2 = toggle.manipulate("test2");
|
||||
checkstate(0, 0);
|
||||
|
||||
result = client2();
|
||||
check.equals(result, true);
|
||||
checkstate(0, 0);
|
||||
|
||||
result = client();
|
||||
check.equals(result, true);
|
||||
checkstate(0, 0);
|
||||
|
||||
result = client2();
|
||||
check.equals(result, false);
|
||||
checkstate(0, 1);
|
||||
})
|
||||
|
||||
test.case("toggles on for a given time", check => {
|
||||
let toggle = newToggle();
|
||||
let client = toggle.manipulate("test");
|
||||
checkstate(0, 0);
|
||||
|
||||
let result = client(100);
|
||||
check.equals(result, true);
|
||||
checkstate(1, 0);
|
||||
|
||||
check.equals(toggle.isOn(), true);
|
||||
checkstate(0, 0);
|
||||
clock.forward(60);
|
||||
check.equals(toggle.isOn(), true);
|
||||
checkstate(0, 0);
|
||||
clock.forward(60);
|
||||
check.equals(toggle.isOn(), false);
|
||||
checkstate(0, 1);
|
||||
|
||||
result = client(100);
|
||||
check.equals(result, true);
|
||||
checkstate(1, 0);
|
||||
result = client(200);
|
||||
check.equals(result, true);
|
||||
checkstate(0, 0);
|
||||
clock.forward(150);
|
||||
check.equals(toggle.isOn(), true);
|
||||
checkstate(0, 0);
|
||||
clock.forward(150);
|
||||
check.equals(toggle.isOn(), false);
|
||||
checkstate(0, 1);
|
||||
|
||||
let client2 = toggle.manipulate("test2");
|
||||
result = client(100);
|
||||
check.equals(result, true);
|
||||
checkstate(1, 0);
|
||||
result = client2(200);
|
||||
check.equals(result, true);
|
||||
checkstate(0, 0);
|
||||
clock.forward(150);
|
||||
check.equals(toggle.isOn(), true);
|
||||
checkstate(0, 0);
|
||||
clock.forward(150);
|
||||
check.equals(toggle.isOn(), false);
|
||||
checkstate(0, 1);
|
||||
|
||||
result = client(100);
|
||||
check.equals(result, true);
|
||||
checkstate(1, 0);
|
||||
result = client(true);
|
||||
check.equals(result, true);
|
||||
checkstate(0, 0);
|
||||
check.equals(toggle.isOn(), true);
|
||||
clock.forward(2000);
|
||||
check.equals(toggle.isOn(), true);
|
||||
checkstate(0, 0);
|
||||
})
|
||||
})
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
module TK {
|
||||
/**
|
||||
* Client for Toggle object, allowing to manipulate it
|
||||
*
|
||||
* *state* may be:
|
||||
* - a boolean to require on or off
|
||||
* - a number to require on for the duration in milliseconds
|
||||
* - undefined to switch between on and off (based on the client state, not the toggle state)
|
||||
*
|
||||
* The function returns the actual state after applying the requirement
|
||||
*/
|
||||
export type ToggleClient = (state?: boolean | number) => boolean
|
||||
|
||||
/**
|
||||
* A toggle between two states (on and off).
|
||||
*
|
||||
* A toggle will be on if at least one ToggleClient requires it to be on.
|
||||
*/
|
||||
export class Toggle {
|
||||
private on: Function
|
||||
private off: Function
|
||||
private status = false
|
||||
private clients: string[] = []
|
||||
private scheduled: { [client: string]: any } = {}
|
||||
private timer = Timer.global
|
||||
|
||||
constructor(on: Function = () => null, off: Function = () => null) {
|
||||
this.on = on;
|
||||
this.off = off;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current state is on
|
||||
*/
|
||||
isOn(): boolean {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a client to manipulate the toggle's state
|
||||
*/
|
||||
manipulate(client: string): ToggleClient {
|
||||
return state => {
|
||||
if (this.scheduled[client]) {
|
||||
this.timer.cancel(this.scheduled[client]);
|
||||
this.scheduled[client] = null;
|
||||
}
|
||||
|
||||
if (typeof state == "undefined") {
|
||||
if (contains(this.clients, client)) {
|
||||
this.stop(client);
|
||||
} else {
|
||||
this.start(client);
|
||||
}
|
||||
} else if (typeof state == "number") {
|
||||
if (state > 0) {
|
||||
this.start(client);
|
||||
this.scheduled[client] = this.timer.schedule(state, () => this.stop(client));
|
||||
} else {
|
||||
this.stop(client);
|
||||
}
|
||||
} else if (!state) {
|
||||
this.stop(client);
|
||||
} else {
|
||||
this.start(client);
|
||||
}
|
||||
return this.status;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the toggle for a client (set the status *on*)
|
||||
*/
|
||||
private start(client: string) {
|
||||
add(this.clients, client);
|
||||
if (!this.status) {
|
||||
this.status = true;
|
||||
this.on();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the toggle (set the status *off*)
|
||||
*/
|
||||
private stop(client: string) {
|
||||
remove(this.clients, client);
|
||||
if (this.status && this.clients.length == 0) {
|
||||
this.status = false;
|
||||
this.off();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,517 @@
|
|||
module TK.Specs {
|
||||
testing("Tools", test => {
|
||||
test.case("returns boolean equivalent", check => {
|
||||
check.same(bool(null), false, "null");
|
||||
check.same(bool(undefined), false, "undefined");
|
||||
|
||||
check.same(bool(false), false, "false");
|
||||
check.same(bool(true), true, "true");
|
||||
|
||||
check.same(bool(0), false, "0");
|
||||
check.same(bool(1), true, "1");
|
||||
check.same(bool(-1), true, "-1");
|
||||
|
||||
check.same(bool(""), false, "\"\"");
|
||||
check.same(bool(" "), true, "\" \"");
|
||||
check.same(bool("abc"), true, "\"abc\"");
|
||||
|
||||
check.same(bool([]), false, "[]");
|
||||
check.same(bool([1]), true, "[1]");
|
||||
check.same(bool([null, "a"]), true, "[null, \"a\"]");
|
||||
|
||||
check.same(bool({}), false, "{}");
|
||||
check.same(bool({ a: 5 }), true, "{a: 5}");
|
||||
check.same(bool(new Timer()), true, "new Timer()");
|
||||
});
|
||||
|
||||
test.case("coalesces to a default value", check => {
|
||||
check.equals(coalesce(0, 2), 0);
|
||||
check.equals(coalesce(5, 2), 5);
|
||||
check.equals(coalesce(null, 2), 2);
|
||||
check.equals(coalesce(undefined, 2), 2);
|
||||
|
||||
check.equals(coalesce("", "a"), "");
|
||||
check.equals(coalesce("b", "a"), "b");
|
||||
check.equals(coalesce(null, "a"), "a");
|
||||
check.equals(coalesce(undefined, "a"), "a");
|
||||
});
|
||||
|
||||
test.case("do basic function composition", check => {
|
||||
check.equals(fmap((x: number) => x * 2, x => x + 1)(3), 8);
|
||||
});
|
||||
|
||||
test.case("function composes a fallback value for null results", check => {
|
||||
let f = nnf(-1, x => x > 3 ? null : x);
|
||||
check.equals(f(2), 2);
|
||||
check.equals(f(3), 3);
|
||||
check.equals(f(4), -1);
|
||||
});
|
||||
|
||||
test.case("checks for null value", check => {
|
||||
let value: number | null = 5;
|
||||
check.equals(nn(value), 5);
|
||||
value = 0;
|
||||
check.equals(nn(value), 0);
|
||||
value = null;
|
||||
check.throw(() => nn(value), "Null value");
|
||||
});
|
||||
|
||||
test.case("removes null values from arrays", check => {
|
||||
let value: (number | null)[] = [];
|
||||
check.equals(nna(value), []);
|
||||
value = [1, 2];
|
||||
check.equals(nna(value), [1, 2]);
|
||||
value = [1, null, 3];
|
||||
check.equals(nna(value), [1, 3]);
|
||||
});
|
||||
|
||||
test.case("cast-checks objects", check => {
|
||||
let obj = new RObject();
|
||||
check.equals(as(RObject, obj), obj);
|
||||
check.throw(() => as(Timer, obj), new Error("Bad cast"));
|
||||
});
|
||||
|
||||
test.case("compare values", check => {
|
||||
check.equals(cmp(8, 5), 1);
|
||||
check.same(cmp(5, 8), -1);
|
||||
check.equals(cmp(5, 5), 0);
|
||||
|
||||
check.equals(cmp("c", "b"), 1);
|
||||
check.same(cmp("b", "c"), -1);
|
||||
check.equals(cmp("b", "b"), 0);
|
||||
|
||||
check.same(cmp(8, 5, true), -1);
|
||||
check.equals(cmp(5, 8, true), 1);
|
||||
check.equals(cmp(5, 5, true), 0);
|
||||
});
|
||||
|
||||
test.case("clamp values in a range", check => {
|
||||
check.equals(clamp(5, 3, 8), 5);
|
||||
check.equals(clamp(1, 3, 8), 3);
|
||||
check.equals(clamp(10, 3, 8), 8);
|
||||
});
|
||||
|
||||
test.case("interpolates values linearly", check => {
|
||||
check.equals(lerp(0, 0, 4), 0);
|
||||
check.equals(lerp(0.5, 0, 4), 2);
|
||||
check.equals(lerp(1, 0, 4), 4);
|
||||
check.equals(lerp(2, 0, 4), 8);
|
||||
check.same(lerp(-1, 0, 4), -4);
|
||||
check.equals(lerp(0.5, 3, 4), 3.5);
|
||||
check.equals(lerp(0.5, 3, 3), 3);
|
||||
check.equals(lerp(0.5, 3, 2), 2.5);
|
||||
});
|
||||
|
||||
test.case("duplicates objects", check => {
|
||||
check.equals(duplicate(null), null);
|
||||
check.equals(duplicate(5), 5);
|
||||
check.equals(duplicate("test"), "test");
|
||||
check.equals(duplicate({ a: 4 }), { a: 4 });
|
||||
check.equals(duplicate([1, "test"]), [1, "test"]);
|
||||
check.equals(duplicate(new TestSerializerObj1(6), <any>TK.Specs), new TestSerializerObj1(6));
|
||||
let original = new TestRObject(4);
|
||||
check.equals(duplicate(original, <any>TK.Specs), original);
|
||||
check.notsame(duplicate(original, <any>TK.Specs), original);
|
||||
});
|
||||
|
||||
test.case("copies arrays", check => {
|
||||
var array = [1, 2, "test", null, { "a": 5 }];
|
||||
var copied = acopy(array);
|
||||
|
||||
check.equals(copied, array);
|
||||
check.notsame(copied, array);
|
||||
check.same(copied[4], array[4]);
|
||||
|
||||
check.equals(array[2], "test");
|
||||
check.equals(copied[2], "test");
|
||||
array[2] = "notest";
|
||||
check.equals(array[2], "notest");
|
||||
check.equals(copied[2], "test");
|
||||
copied[2] = "ok";
|
||||
check.equals(array[2], "notest");
|
||||
check.equals(copied[2], "ok");
|
||||
|
||||
check.equals(array.length, 5);
|
||||
check.equals(copied.length, 5);
|
||||
remove(copied, 2);
|
||||
check.equals(array.length, 5);
|
||||
check.equals(copied.length, 4);
|
||||
});
|
||||
|
||||
test.case("iterates through sorted arrays", check => {
|
||||
var result: number[] = [];
|
||||
itersorted([1, 2, 3], item => item, item => result.push(item));
|
||||
check.equals(result, [1, 2, 3]);
|
||||
|
||||
result = [];
|
||||
itersorted([1, 2, 3], item => -item, item => result.push(item));
|
||||
check.equals(result, [3, 2, 1]);
|
||||
});
|
||||
|
||||
test.case("checks if an array contains an item", check => {
|
||||
check.equals(contains([], 5), false);
|
||||
|
||||
check.equals(contains([3, 5, 8], 5), true);
|
||||
check.equals(contains([3, 5, 8], 4), false);
|
||||
|
||||
check.equals(contains([5, 5, 5], 5), true);
|
||||
|
||||
check.equals(contains([3, null, 8], null), true);
|
||||
|
||||
check.equals(contains(["a", "b"], "b"), true);
|
||||
check.equals(contains(["a", "b"], "c"), false);
|
||||
});
|
||||
|
||||
test.case("capitalizes strings", check => {
|
||||
check.equals(capitalize("test"), "Test");
|
||||
check.equals(capitalize("test second"), "Test second");
|
||||
});
|
||||
|
||||
test.case("produces range of numbers", check => {
|
||||
check.equals(range(-1), []);
|
||||
check.equals(range(0), []);
|
||||
check.equals(range(1), [0]);
|
||||
check.equals(range(2), [0, 1]);
|
||||
check.equals(range(5), [0, 1, 2, 3, 4]);
|
||||
});
|
||||
|
||||
test.case("zips arrays", check => {
|
||||
check.equals(zip([], []), []);
|
||||
check.equals(zip([], [1]), []);
|
||||
check.equals(zip([0], [1]), [[0, 1]]);
|
||||
check.equals(zip([0, 2, 4], [1, 3]), [[0, 1], [2, 3]]);
|
||||
check.equals(zip([0, 1], ["a", "b"]), [[0, "a"], [1, "b"]]);
|
||||
});
|
||||
|
||||
test.case("unzips arrays", check => {
|
||||
check.equals(unzip([]), [[], []]);
|
||||
check.equals(unzip([[1, "a"]]), [[1], ["a"]]);
|
||||
check.equals(unzip([[1, "a"], [2, "b"]]), [[1, 2], ["a", "b"]]);
|
||||
});
|
||||
|
||||
test.case("partitions arrays by a predicate", check => {
|
||||
check.equals(binpartition([], (i: number) => i % 2 == 0), [[], []]);
|
||||
check.equals(binpartition([1, 2, 3, 4], i => i % 2 == 0), [[2, 4], [1, 3]]);
|
||||
});
|
||||
|
||||
test.case("produces neighbor tuples from array", check => {
|
||||
check.equals(neighbors([]), []);
|
||||
check.equals(neighbors([1]), []);
|
||||
check.equals(neighbors([1, 2]), [[1, 2]]);
|
||||
check.equals(neighbors([1, 2, 3]), [[1, 2], [2, 3]]);
|
||||
check.equals(neighbors([1, 2, 3, 4]), [[1, 2], [2, 3], [3, 4]]);
|
||||
|
||||
check.equals(neighbors([], true), []);
|
||||
check.equals(neighbors([1], true), [[1, 1]]);
|
||||
check.equals(neighbors([1, 2], true), [[1, 2], [2, 1]]);
|
||||
check.equals(neighbors([1, 2, 3], true), [[1, 2], [2, 3], [3, 1]]);
|
||||
check.equals(neighbors([1, 2, 3, 4], true), [[1, 2], [2, 3], [3, 4], [4, 1]]);
|
||||
});
|
||||
|
||||
test.case("filters list with type guards", check => {
|
||||
let result = tfilter(<(number | string)[]>[1, "a", 2, "b"], (x): x is number => typeof x == "number");
|
||||
check.equals(result, [1, 2]);
|
||||
|
||||
let o1 = new RObject();
|
||||
let o2 = new RObject();
|
||||
let o3 = new RObjectContainer();
|
||||
check.equals(cfilter([1, "a", o1, 2, o2, o3, "b"], RObject), [o1, o2]);
|
||||
});
|
||||
|
||||
test.case("flattens lists of lists", check => {
|
||||
check.equals(flatten([]), []);
|
||||
check.equals(flatten([[]]), []);
|
||||
check.equals(flatten([[], []]), []);
|
||||
check.equals(flatten([[1], []]), [1]);
|
||||
check.equals(flatten([[], [1]]), [1]);
|
||||
check.equals(flatten([[1], [2]]), [1, 2]);
|
||||
check.equals(flatten([[1, 2], [3, 4], [], [5]]), [1, 2, 3, 4, 5]);
|
||||
});
|
||||
|
||||
test.case("counts items in array", check => {
|
||||
check.equals(counter([]), []);
|
||||
check.equals(counter(["a"]), [["a", 1]]);
|
||||
check.equals(counter(["a", "b"]), [["a", 1], ["b", 1]]);
|
||||
check.equals(counter(["a", "b", "a"]), [["a", 2], ["b", 1]]);
|
||||
});
|
||||
|
||||
test.case("find the first array item to pass a predicate", check => {
|
||||
check.equals(first([1, 2, 3], i => i % 2 == 0), 2);
|
||||
check.equals(first([1, 2, 3], i => i % 4 == 0), null);
|
||||
|
||||
check.equals(any([1, 2, 3], i => i % 2 == 0), true);
|
||||
check.equals(any([1, 2, 3], i => i % 4 == 0), false);
|
||||
});
|
||||
|
||||
test.case("creates a simple iterator over an array", check => {
|
||||
let i = iterator([1, 2, 3]);
|
||||
check.equals(i(), 1);
|
||||
check.equals(i(), 2);
|
||||
check.equals(i(), 3);
|
||||
check.equals(i(), null);
|
||||
check.equals(i(), null);
|
||||
|
||||
i = iterator([]);
|
||||
check.equals(i(), null);
|
||||
check.equals(i(), null);
|
||||
});
|
||||
|
||||
test.case("iterates an object keys and values", check => {
|
||||
var obj = {
|
||||
"a": 1,
|
||||
"c": [2.5],
|
||||
"b": null
|
||||
};
|
||||
check.equals(keys(obj), ["a", "c", "b"]);
|
||||
check.equals(values(obj), [1, [2.5], null]);
|
||||
var result: { [key: string]: any } = {};
|
||||
iteritems(obj, (key, value) => { result[key] = value; });
|
||||
check.equals(result, obj);
|
||||
|
||||
check.equals(dict(items(obj)), obj);
|
||||
});
|
||||
|
||||
test.case("iterates an enum", check => {
|
||||
enum Test {
|
||||
ZERO,
|
||||
ONE,
|
||||
TWO
|
||||
};
|
||||
|
||||
var result: any[] = [];
|
||||
iterenum(Test, item => result.push(item));
|
||||
check.equals(result, [0, 1, 2]);
|
||||
});
|
||||
|
||||
test.case("create a dict from an array of couples", check => {
|
||||
check.equals(dict([]), {});
|
||||
check.equals(dict([["5", 3], ["4", 1], ["5", 8]]), { "5": 8, "4": 1 });
|
||||
});
|
||||
|
||||
test.case("create an index from an array", check => {
|
||||
check.equals(index([2, 3, 4], i => (i - 1).toString()), { "1": 2, "2": 3, "3": 4 });
|
||||
});
|
||||
|
||||
test.case("add an item in an array", check => {
|
||||
var result;
|
||||
var array = [1];
|
||||
|
||||
result = add(array, 8);
|
||||
check.equals(array, [1, 8]);
|
||||
check.equals(result, true);
|
||||
|
||||
result = add(array, 2);
|
||||
check.equals(array, [1, 8, 2]);
|
||||
check.equals(result, true);
|
||||
|
||||
result = add(array, 8);
|
||||
check.equals(array, [1, 8, 2]);
|
||||
check.equals(result, false);
|
||||
});
|
||||
|
||||
test.case("removes an item from an array", check => {
|
||||
var array = [1, 2, 3];
|
||||
var result = remove(array, 1);
|
||||
check.equals(array, [2, 3]);
|
||||
check.equals(result, true);
|
||||
result = remove(array, 1);
|
||||
check.equals(array, [2, 3]);
|
||||
check.equals(result, false);
|
||||
result = remove(array, 2);
|
||||
check.equals(array, [3]);
|
||||
check.equals(result, true);
|
||||
result = remove(array, 3);
|
||||
check.equals(array, []);
|
||||
check.equals(result, true);
|
||||
result = remove(array, 3);
|
||||
check.equals(array, []);
|
||||
check.equals(result, false);
|
||||
});
|
||||
|
||||
test.case("checks objects equality", check => {
|
||||
check.equals(equals({}, {}), true);
|
||||
check.equals(equals({ "a": 1 }, { "a": 1 }), true);
|
||||
check.equals(equals({ "a": 1 }, { "a": 2 }), false);
|
||||
check.equals(equals({ "a": 1 }, { "b": 1 }), false);
|
||||
check.equals(equals({ "a": 1 }, { "a": null }), false);
|
||||
});
|
||||
|
||||
test.case("combinate filters", check => {
|
||||
var filter = andfilter((item: number) => item > 5, (item: number) => item < 12);
|
||||
check.equals(filter(4), false);
|
||||
check.equals(filter(5), false);
|
||||
check.equals(filter(6), true);
|
||||
check.equals(filter(8), true);
|
||||
check.equals(filter(11), true);
|
||||
check.equals(filter(12), false);
|
||||
check.equals(filter(13), false);
|
||||
});
|
||||
|
||||
test.case("get a class name", check => {
|
||||
class Test {
|
||||
}
|
||||
var a = new Test();
|
||||
check.equals(classname(a), "Test");
|
||||
});
|
||||
|
||||
test.case("find lowest item of an array", check => {
|
||||
check.equals(lowest(["aaa", "b", "cc"], s => s.length), "b");
|
||||
});
|
||||
|
||||
test.case("binds callbacks", check => {
|
||||
class Test {
|
||||
prop = 5;
|
||||
meth() {
|
||||
return this.prop + 1;
|
||||
}
|
||||
}
|
||||
var inst = new Test();
|
||||
var double = (getter: () => number): number => getter() * 2;
|
||||
check.throw(() => double(inst.meth));
|
||||
check.equals(double(bound(inst, "meth")), 12);
|
||||
});
|
||||
|
||||
test.case("computes progress between two boundaries", check => {
|
||||
check.equals(progress(-1.0, 0.0, 1.0), 0.0);
|
||||
check.equals(progress(0.0, 0.0, 1.0), 0.0);
|
||||
check.equals(progress(0.4, 0.0, 1.0), 0.4);
|
||||
check.equals(progress(1.8, 0.0, 1.0), 1.0);
|
||||
check.equals(progress(1.5, 0.5, 2.5), 0.5);
|
||||
});
|
||||
|
||||
test.case("copies full javascript objects", check => {
|
||||
class TestObj {
|
||||
a: string;
|
||||
b: any;
|
||||
constructor() {
|
||||
this.a = "test";
|
||||
this.b = { c: 5.1, d: ["unit", "test", 5] };
|
||||
}
|
||||
get(): string {
|
||||
return this.a;
|
||||
}
|
||||
}
|
||||
|
||||
var ini = new TestObj();
|
||||
|
||||
var cop = copy(ini);
|
||||
|
||||
check.notsame(cop, ini);
|
||||
check.equals(cop, ini);
|
||||
|
||||
check.equals(cop.get(), "test");
|
||||
});
|
||||
|
||||
test.case("merges objects", check => {
|
||||
check.equals(merge({}, {}), {});
|
||||
check.equals(merge(<any>{ a: 1 }, { b: 2 }), { a: 1, b: 2 });
|
||||
check.equals(merge(<any>{ a: 1 }, { a: 3, b: 2 }), { a: 3, b: 2 });
|
||||
check.equals(merge(<any>{ a: 1, b: 2 }, { a: undefined }), { a: undefined, b: 2 });
|
||||
});
|
||||
|
||||
test.case("crawls through objects", check => {
|
||||
var obj = {
|
||||
"a": 1,
|
||||
"b": "test",
|
||||
"c": {
|
||||
"a": <any[]>[2, "thing", { "a": 3, "b": {} }],
|
||||
"b": null,
|
||||
"c": undefined,
|
||||
"d": false
|
||||
}
|
||||
};
|
||||
/*(<any>obj).jasmineToString = () => "obj1";
|
||||
(<any>obj.c).jasmineToString = () => "obj2";
|
||||
(<any>obj.c.a[2]).jasmineToString = () => "obj3";
|
||||
(<any>obj.c.a[2].b).jasmineToString = () => "obj4";
|
||||
(<any>obj.c.a).jasmineToString = () => "array1";*/
|
||||
|
||||
var crawled: any[] = [];
|
||||
crawl(obj, val => crawled.push(val));
|
||||
check.equals(crawled, [obj, 1, "test", obj.c, obj.c.a, 2, "thing", obj.c.a[2], 3, obj.c.a[2].b, false]);
|
||||
check.equals(obj.a, 1);
|
||||
|
||||
// replace mode
|
||||
crawl(obj, val => typeof val == "number" ? 5 : val, true);
|
||||
check.equals(obj, { a: 5, b: "test", c: { a: [5, "thing", { a: 5, b: {} }], b: null, c: undefined, d: false } });
|
||||
});
|
||||
|
||||
test.case("get minimal item of an array", check => {
|
||||
check.equals(min([5, 1, 8]), 1);
|
||||
});
|
||||
|
||||
test.case("get maximal item of an array", check => {
|
||||
check.equals(max([5, 12, 8]), 12);
|
||||
});
|
||||
|
||||
test.case("get sum of an array", check => {
|
||||
check.equals(sum([5, 1, 8]), 14);
|
||||
});
|
||||
|
||||
test.case("get average of an array", check => {
|
||||
check.equals(avg([4, 2, 9]), 5);
|
||||
});
|
||||
|
||||
test.case("converts to same sign", check => {
|
||||
check.equals(samesign(2, 1), 2);
|
||||
check.equals(samesign(2, -1), -2);
|
||||
check.equals(samesign(-2, 1), 2);
|
||||
check.equals(samesign(-2, -1), -2);
|
||||
});
|
||||
|
||||
test.case("sorts an array", check => {
|
||||
let base = ["aa", "bbb", "c", "dddd"];
|
||||
check.equals(sorted(base, (a, b) => cmp(a.length, b.length)), ["c", "aa", "bbb", "dddd"]);
|
||||
check.equals(base, ["aa", "bbb", "c", "dddd"]);
|
||||
});
|
||||
|
||||
test.case("sorts an array, with function applied to each element", check => {
|
||||
check.equals(sortedBy([-8, 4, -2, 6], Math.abs), [-2, 4, 6, -8]);
|
||||
check.equals(sortedBy([-8, 4, -2, 6], Math.abs, true), [-8, 6, 4, -2]);
|
||||
});
|
||||
|
||||
test.case("get minimal item of an array, with function applied to each element", check => {
|
||||
check.equals(minBy([-8, 4, -2, 6], Math.abs), -2);
|
||||
});
|
||||
|
||||
test.case("get maximal item of an array, with function applied to each element", check => {
|
||||
check.equals(maxBy([-8, 4, -2, 6], Math.abs), -8);
|
||||
});
|
||||
|
||||
test.case("filter out duplicates in array", check => {
|
||||
check.equals(unique([]), []);
|
||||
check.equals(unique([1, 2, 3]), [1, 2, 3]);
|
||||
check.equals(unique([1, 2, 3, 2, 1]), [1, 2, 3]);
|
||||
});
|
||||
|
||||
test.case("get the union between two arrays", check => {
|
||||
check.equals(union([], []), []);
|
||||
check.equals(union([], [5]), [5]);
|
||||
check.equals(union([4], []), [4]);
|
||||
check.equals(union([4], [5]), [4, 5]);
|
||||
check.equals(union([1, 2, 4, 8], [3, 2, 8, 7]), [1, 2, 4, 8, 3, 7]);
|
||||
});
|
||||
|
||||
test.case("get the difference between two arrays", check => {
|
||||
check.equals(difference([], []), []);
|
||||
check.equals(difference([], [5]), []);
|
||||
check.equals(difference([1, 2, 4, 8], [2, 8, 7]), [1, 4]);
|
||||
});
|
||||
|
||||
test.case("get the intersection of two arrays", check => {
|
||||
check.equals(intersection([], []), []);
|
||||
check.equals(intersection([], [5]), []);
|
||||
check.equals(intersection([4], []), []);
|
||||
check.equals(intersection([6], [7]), []);
|
||||
check.equals(intersection([1, 8, 2], [2, 8, 4]), [8, 2]);
|
||||
});
|
||||
|
||||
test.case("get the disjunct union of two arrays", check => {
|
||||
check.equals(disjunctunion([], []), []);
|
||||
check.equals(disjunctunion([], [5]), [5]);
|
||||
check.equals(disjunctunion([4], []), [4]);
|
||||
check.equals(disjunctunion([6], [7]), [6, 7]);
|
||||
check.equals(disjunctunion([1, 8, 2], [2, 8, 4]), [1, 4]);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,629 @@
|
|||
/**
|
||||
* Various utility functions.
|
||||
*/
|
||||
module TK {
|
||||
/**
|
||||
* Functions that does nothing (useful for default callbacks)
|
||||
*/
|
||||
export function nop(): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Identity function (returns the sole argument untouched)
|
||||
*/
|
||||
export function identity<T>(input: T): T {
|
||||
return input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a value for a boolean equivalent
|
||||
*/
|
||||
export function bool<T>(value: T | null | undefined): value is T;
|
||||
export function bool(value: any): boolean {
|
||||
if (!value) {
|
||||
return false;
|
||||
} else if (typeof value == "object") {
|
||||
return Object.keys(value).length > 0;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a default value if the given one is undefined
|
||||
*/
|
||||
export function coalesce(value: string | null | undefined, fallback: string): string;
|
||||
export function coalesce(value: number | null | undefined, fallback: number): number;
|
||||
export function coalesce<T>(value: T | null | undefined, fallback: T): T {
|
||||
if (typeof value == "undefined" || value === null) {
|
||||
return fallback;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for an object being an instance of a type, an returns casted version
|
||||
*
|
||||
* Throws an error on failure to cast
|
||||
*/
|
||||
export function as<T>(classref: { new(...args: any[]): T }, obj: any): T {
|
||||
if (obj instanceof classref) {
|
||||
return obj;
|
||||
} else {
|
||||
console.error("Bad cast", obj, classref);
|
||||
throw new Error("Bad cast");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a functor on the result of another function
|
||||
*/
|
||||
export function fmap<U, V>(g: (arg: U) => V, f: (...args: any[]) => U): (...args: any[]) => V {
|
||||
// TODO variadic typing, as soon as supported by typescript
|
||||
return (...args) => g(f(...args));
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a default value to nulls or undefineds returned by a function
|
||||
*/
|
||||
export function nnf<T>(fallback: T, f: (...args: any[]) => T | null): (...args: any[]) => T {
|
||||
return fmap(val => val === null ? fallback : val, f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value if null, throwing an exception if its the case
|
||||
*/
|
||||
export function nn<T>(value: T | null): T {
|
||||
if (value === null) {
|
||||
throw new Error("Null value");
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove null values from an array
|
||||
*/
|
||||
export function nna<T>(array: (T | null)[]): T[] {
|
||||
return <T[]>array.filter(item => item !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare operator, that can be used in sort() calls.
|
||||
*/
|
||||
export function cmp(a: any, b: any, reverse = false): number {
|
||||
if (a > b) {
|
||||
return reverse ? -1 : 1;
|
||||
} else if (a < b) {
|
||||
return reverse ? 1 : -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamp a value in a range.
|
||||
*/
|
||||
export function clamp<T>(value: T, min: T, max: T): T {
|
||||
if (value < min) {
|
||||
return min;
|
||||
} else if (value > max) {
|
||||
return max;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a linear interpolation between two values (factor is between 0 and 1).
|
||||
*/
|
||||
export function lerp(factor: number, min: number, max: number): number {
|
||||
return min + (max - min) * factor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a deep copy of any object.
|
||||
*
|
||||
* Serializer is used for this, and needs a namespace to work.
|
||||
*
|
||||
* Please be aware that contained RObjects will be duplicated, but keep their ID, thus breaking the uniqueness.
|
||||
*/
|
||||
export function duplicate<T>(obj: T, namespace: Object = TK): T {
|
||||
let serializer = new Serializer(namespace);
|
||||
let serialized = serializer.serialize({ dat: obj });
|
||||
return serializer.unserialize(serialized).dat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a shallow copy of an array.
|
||||
*/
|
||||
export function acopy<T>(input: T[]): T[] {
|
||||
return input.slice();
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a function for each member of an array, sorted by a key.
|
||||
*/
|
||||
export function itersorted<T>(input: T[], keyfunc: (item: T) => any, callback: (item: T) => void): void {
|
||||
var array = acopy(input);
|
||||
array.sort((item1, item2) => cmp(keyfunc(item1), keyfunc(item2)));
|
||||
array.forEach(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Capitalize the first letter of an input string.
|
||||
*/
|
||||
export function capitalize(input: string): string {
|
||||
return input.charAt(0).toLocaleUpperCase() + input.slice(1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if an array contains an item.
|
||||
*/
|
||||
export function contains<T>(array: T[], item: T): boolean {
|
||||
return array.indexOf(item) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce an n-sized array, with integers counting from 0
|
||||
*/
|
||||
export function range(n: number): number[] {
|
||||
var result: number[] = [];
|
||||
for (var i = 0; i < n; i++) {
|
||||
result.push(i);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce an array of couples, build from the common length of two arrays
|
||||
*/
|
||||
export function zip<T1, T2>(array1: T1[], array2: T2[]): [T1, T2][] {
|
||||
var result: [T1, T2][] = [];
|
||||
var n = (array1.length > array2.length) ? array2.length : array1.length;
|
||||
for (var i = 0; i < n; i++) {
|
||||
result.push([array1[i], array2[i]]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce two arrays, build from an array of couples
|
||||
*/
|
||||
export function unzip<T1, T2>(array: [T1, T2][]): [T1[], T2[]] {
|
||||
return [array.map(x => x[0]), array.map(x => x[1])];
|
||||
}
|
||||
|
||||
/**
|
||||
* Partition a list by a predicate, returning the items that pass the predicate, then the ones that don't pass it
|
||||
*/
|
||||
export function binpartition<T>(array: T[], predicate: (item: T) => boolean): [T[], T[]] {
|
||||
let pass: T[] = [];
|
||||
let fail: T[] = [];
|
||||
array.forEach(item => (predicate(item) ? pass : fail).push(item));
|
||||
return [pass, fail];
|
||||
}
|
||||
|
||||
/**
|
||||
* Yields the neighbors tuple list
|
||||
*/
|
||||
export function neighbors<T>(array: T[], wrap = false): [T, T][] {
|
||||
var result: [T, T][] = [];
|
||||
if (array.length > 0) {
|
||||
var previous = array[0];
|
||||
for (var i = 1; i < array.length; i++) {
|
||||
result.push([previous, array[i]]);
|
||||
previous = array[i];
|
||||
}
|
||||
if (wrap) {
|
||||
result.push([previous, array[0]]);
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type filter, to return a list of instances of a given type
|
||||
*/
|
||||
export function tfilter<T>(array: any[], filter: (item: any) => item is T): T[] {
|
||||
return array.filter(filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Class filter, to return a list of instances of a given type
|
||||
*/
|
||||
export function cfilter<T>(array: any[], classref: { new(...args: any[]): T }): T[] {
|
||||
return array.filter((item): item is T => item instanceof classref);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten a list of lists
|
||||
*/
|
||||
export function flatten<T>(array: T[][]): T[] {
|
||||
return array.reduce((a, b) => a.concat(b), []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count each element in an array
|
||||
*/
|
||||
export function counter<T>(array: T[], equals: (a: T, b: T) => boolean = (a, b) => a === b): [T, number][] {
|
||||
var result: [T, number][] = [];
|
||||
array.forEach(item => {
|
||||
var found = first(result, iter => equals(iter[0], item));
|
||||
if (found) {
|
||||
found[1]++;
|
||||
} else {
|
||||
result.push([item, 1]);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the first element of the array that matches the predicate, null if not found
|
||||
*/
|
||||
export function first<T>(array: T[], predicate: (item: T) => boolean): T | null {
|
||||
for (var i = 0; i < array.length; i++) {
|
||||
if (predicate(array[i])) {
|
||||
return array[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether if any element in the array matches the predicate
|
||||
*/
|
||||
export function any<T>(array: T[], predicate: (item: T) => boolean): boolean {
|
||||
return first(array, predicate) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an iterator over an array
|
||||
*
|
||||
* An iterator is a function yielding the next value each time, until the end of array where it yields null.
|
||||
*
|
||||
* For more powerful iterators, see Iterators
|
||||
*/
|
||||
export function iterator<T>(array: T[]): () => T | null {
|
||||
let i = 0;
|
||||
return () => (i < array.length) ? array[i++] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate a list of (key, value) in an object.
|
||||
*/
|
||||
export function iteritems<T>(obj: { [key: string]: T }, func: (key: string, value: T) => void) {
|
||||
for (var key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
func(key, obj[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform an dictionary object to a list of couples (key, value).
|
||||
*/
|
||||
export function items<T>(obj: { [key: string]: T }): [string, T][] {
|
||||
let result: [string, T][] = [];
|
||||
iteritems(obj, (key, value) => result.push([key, value]));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of keys from an object.
|
||||
*/
|
||||
export function keys<T extends object>(obj: T): (Extract<keyof T, string>)[] {
|
||||
var result: (Extract<keyof T, string>)[] = [];
|
||||
for (var key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
result.push(key);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of values from an object.
|
||||
*/
|
||||
export function values<T>(obj: { [key: string]: T }): T[] {
|
||||
var result: T[] = [];
|
||||
for (var key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
result.push(obj[key]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate an enum values.
|
||||
*/
|
||||
export function iterenum<T>(obj: T, callback: (item: number) => void) {
|
||||
for (var val in obj) {
|
||||
var parsed = parseInt(val, 10);
|
||||
if (!isNaN(parsed)) {
|
||||
callback(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a dictionary from a list of couples
|
||||
*/
|
||||
export function dict<T>(array: [string, T][]): { [index: string]: T } {
|
||||
let result: { [index: string]: T } = {};
|
||||
array.forEach(([key, value]) => result[key] = value);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a dictionnary index from a list of objects
|
||||
*/
|
||||
export function index<T>(array: T[], keyfunc: (obj: T) => string): { [key: string]: T } {
|
||||
var result: { [key: string]: T } = {};
|
||||
array.forEach(obj => result[keyfunc(obj)] = obj);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an item to the end of a list, only if not already there
|
||||
*/
|
||||
export function add<T>(array: T[], item: T): boolean {
|
||||
if (!contains(array, item)) {
|
||||
array.push(item);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Remove an item from a list if found. Return true if changed.
|
||||
*/
|
||||
export function remove<T>(array: T[], item: T): boolean {
|
||||
var idx = array.indexOf(item);
|
||||
if (idx >= 0) {
|
||||
array.splice(idx, 1);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two standard objects are equal.
|
||||
*/
|
||||
export function equals<T>(obj1: { [key: string]: T }, obj2: { [key: string]: T }): boolean {
|
||||
return JSON.stringify(obj1) == JSON.stringify(obj2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a function on any couple formed from combining two arrays.
|
||||
*/
|
||||
export function combicall<T>(array1: T[], array2: T[], callback: (item1: T, item2: T) => void): void {
|
||||
array1.forEach(item1 => array2.forEach(item2 => callback(item1, item2)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Combinate two filter functions (predicates), with a boolean and.
|
||||
*/
|
||||
export function andfilter<T>(filt1: (item: T) => boolean, filt2: (item: T) => boolean): (item: T) => boolean {
|
||||
return (item: T) => filt1(item) && filt2(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the class name of an object.
|
||||
*/
|
||||
export function classname(obj: Object): string {
|
||||
return (<any>obj.constructor).name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the lowest item of an array, using a mapping function.
|
||||
*/
|
||||
export function lowest<T>(array: T[], rating: (item: T) => number): T {
|
||||
var rated = array.map((item: T): [T, number] => [item, rating(item)]);
|
||||
rated.sort((a, b) => cmp(a[1], b[1]));
|
||||
return rated[0][0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a function bound to an object.
|
||||
*
|
||||
* This is useful to pass the bound function as callback directly.
|
||||
*/
|
||||
export function bound<T, K extends keyof T>(obj: T, func: K): T[K] {
|
||||
let attr = obj[func];
|
||||
if (attr instanceof Function) {
|
||||
return attr.bind(obj);
|
||||
} else {
|
||||
return <any>(() => attr);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a 0.0-1.0 factor of progress between two limits.
|
||||
*/
|
||||
export function progress(value: number, min: number, max: number) {
|
||||
var result = (value - min) / (max - min);
|
||||
return clamp(result, 0.0, 1.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy all fields of an object in another (shallow copy)
|
||||
*/
|
||||
export function copyfields<T>(src: Partial<T>, dest: Partial<T>) {
|
||||
for (let key in src) {
|
||||
if (src.hasOwnProperty(key)) {
|
||||
dest[key] = src[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy an object (only a shallow copy of immediate properties)
|
||||
*/
|
||||
export function copy<T>(object: T): T {
|
||||
let objectCopy = <T>Object.create(object.constructor.prototype);
|
||||
copyfields(object, objectCopy);
|
||||
return objectCopy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge an object into another
|
||||
*/
|
||||
export function merge<T>(base: T, incoming: Partial<T>): T {
|
||||
let result = copy(base);
|
||||
copyfields(incoming, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
export const STOP_CRAWLING = {};
|
||||
|
||||
/**
|
||||
* Recursively crawl through an object, yielding any defined value found along the way
|
||||
*
|
||||
* If *replace* is set to true, the current object is replaced (in array or object attribute) by the result of the callback
|
||||
*
|
||||
* *memo* is used to prevent circular references to be traversed
|
||||
*/
|
||||
export function crawl(obj: any, callback: (item: any) => any, replace = false, memo: any[] = []) {
|
||||
if (obj instanceof Object && !Array.isArray(obj)) {
|
||||
if (memo.indexOf(obj) >= 0) {
|
||||
return obj;
|
||||
} else {
|
||||
memo.push(obj);
|
||||
}
|
||||
}
|
||||
|
||||
if (obj !== undefined && obj !== null && typeof obj != "function") {
|
||||
let result = callback(obj);
|
||||
|
||||
if (result === STOP_CRAWLING) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
let subresult = obj.map(value => crawl(value, callback, replace, memo));
|
||||
if (replace) {
|
||||
subresult.forEach((value, index) => {
|
||||
obj[index] = value;
|
||||
});
|
||||
}
|
||||
} else if (obj instanceof Object) {
|
||||
let subresult: any = {};
|
||||
iteritems(obj, (key, value) => {
|
||||
subresult[key] = crawl(value, callback, replace, memo);
|
||||
});
|
||||
if (replace) {
|
||||
copyfields(subresult, obj);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} else {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the minimal value of an array
|
||||
*/
|
||||
export function min<T>(array: T[]): T {
|
||||
return array.reduce((a, b) => a < b ? a : b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the maximal value of an array
|
||||
*/
|
||||
export function max<T>(array: T[]): T {
|
||||
return array.reduce((a, b) => a > b ? a : b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the sum of an array
|
||||
*/
|
||||
export function sum(array: number[]): number {
|
||||
return array.reduce((a, b) => a + b, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the average of an array
|
||||
*/
|
||||
export function avg(array: number[]): number {
|
||||
return sum(array) / array.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return value, with the same sign as base
|
||||
*/
|
||||
export function samesign(value: number, base: number): number {
|
||||
return Math.abs(value) * (base < 0 ? -1 : 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a copy of the array, sorted by a cmp function (equivalent of javascript sort)
|
||||
*/
|
||||
export function sorted<T>(array: T[], cmpfunc: (v1: T, v2: T) => number): T[] {
|
||||
return acopy(array).sort(cmpfunc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a copy of the array, sorted by the result of a function applied to each item
|
||||
*/
|
||||
export function sortedBy<T1, T2>(array: T1[], func: (val: T1) => T2, reverse = false): T1[] {
|
||||
return sorted(array, (a, b) => cmp(func(a), func(b), reverse));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the minimum of an array transformed by a function
|
||||
*/
|
||||
export function minBy<T1, T2>(array: T1[], func: (val: T1) => T2): T1 {
|
||||
return array.reduce((a, b) => func(a) < func(b) ? a : b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the maximum of an array transformed by a function
|
||||
*/
|
||||
export function maxBy<T1, T2>(array: T1[], func: (val: T1) => T2): T1 {
|
||||
return array.reduce((a, b) => func(a) > func(b) ? a : b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a copy of an array, containing each value only once
|
||||
*/
|
||||
export function unique<T>(array: T[]): T[] {
|
||||
return array.filter((value, index, self) => self.indexOf(value) === index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the union of two arrays (items in either array)
|
||||
*/
|
||||
export function union<T>(array1: T[], array2: T[]): T[] {
|
||||
return array1.concat(difference(array2, array1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the difference between two arrays (items in the first, but not in the second)
|
||||
*/
|
||||
export function difference<T>(array1: T[], array2: T[]): T[] {
|
||||
return array1.filter(value => !contains(array2, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the intersection of two arrays (items in both arrays)
|
||||
*/
|
||||
export function intersection<T>(array1: T[], array2: T[]): T[] {
|
||||
return array1.filter(value => contains(array2, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the disjunctive union of two arrays (items not in both arrays)
|
||||
*/
|
||||
export function disjunctunion<T>(array1: T[], array2: T[]): T[] {
|
||||
return difference(union(array1, array2), intersection(array1, array2));
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -55,7 +55,7 @@ module TK.SpaceTac.UI {
|
|||
console.log(`Starting scene ${classname(this)}`);
|
||||
|
||||
this.gameui = <MainUI>this.sys.game;
|
||||
this.timer = new Timer(this.gameui.headless);
|
||||
this.timer = new Timer(this.gameui.isTesting);
|
||||
this.animations = new Animations(this.tweens);
|
||||
this.particles = new UIParticles(this);
|
||||
this.inputs = new InputManager(this);
|
||||
|
@ -93,8 +93,6 @@ module TK.SpaceTac.UI {
|
|||
this.messages = new Messages(this);
|
||||
this.dialogs_opened = [];
|
||||
|
||||
this.resize();
|
||||
|
||||
// Browser console variable (for debugging purpose)
|
||||
if (typeof window != "undefined") {
|
||||
let session = this.gameui.session;
|
||||
|
@ -107,20 +105,6 @@ module TK.SpaceTac.UI {
|
|||
}
|
||||
}
|
||||
|
||||
resize() {
|
||||
if (this.gameui) {
|
||||
if (this.layers) {
|
||||
this.layers.setScale(this.gameui.scaling);
|
||||
}
|
||||
if (this.dialogs_layer) {
|
||||
this.dialogs_layer.setScale(this.gameui.scaling);
|
||||
}
|
||||
if (this.cameras.main) {
|
||||
this.cameras.main.setViewport(0, 0, 1920 * this.gameui.scaling, 1080 * this.gameui.scaling);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get options() {
|
||||
return this.gameui.options;
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ module TK.SpaceTac.UI.Specs {
|
|||
if (scene instanceof BaseView) {
|
||||
testgame.multistorage = new Multi.FakeRemoteStorage();
|
||||
let connection = new Multi.Connection(RandomGenerator.global.id(12), testgame.multistorage);
|
||||
check.patch(scene, "getConnection", () => connection);
|
||||
check.patch(scene as BaseView, "getConnection", () => connection);
|
||||
}
|
||||
|
||||
let orig_create = bound(scene, "create");
|
||||
|
|
|
@ -92,6 +92,7 @@ module TK.SpaceTac.UI {
|
|||
*/
|
||||
setupMouseCapture() {
|
||||
let view = this.view;
|
||||
let button_down = false;
|
||||
|
||||
let background = new UIBuilder(view, this.container).image("battle-arena-background");
|
||||
background.setName("mouse-capture");
|
||||
|
@ -99,8 +100,12 @@ module TK.SpaceTac.UI {
|
|||
|
||||
// Capture clicks on background
|
||||
background.setInteractive();
|
||||
background.on("pointerdown", (pointer: Phaser.Input.Pointer) => {
|
||||
button_down = (pointer.buttons == 1);
|
||||
});
|
||||
background.on("pointerup", (pointer: Phaser.Input.Pointer) => {
|
||||
if (pointer.buttons == 1) {
|
||||
if (button_down) {
|
||||
button_down = false;
|
||||
this.callbacks_click.forEach(callback => callback());
|
||||
}
|
||||
});
|
||||
|
|
|
@ -54,7 +54,7 @@ module TK.SpaceTac.UI {
|
|||
* Start log processing
|
||||
*/
|
||||
start() {
|
||||
if (!this.view.gameui.headless) {
|
||||
if (!this.view.gameui.isTesting) {
|
||||
this.log.play(async diff => {
|
||||
while (this.view.isPaused()) {
|
||||
await this.view.timer.sleep(500);
|
||||
|
|
|
@ -25,7 +25,8 @@ module TK.SpaceTac.UI.Specs {
|
|||
check.equals(sheet.text_name && sheet.text_name.text, "Ship 1");
|
||||
|
||||
let portrait = as(UIButton, sheet.group_portraits.getAt(1));
|
||||
portrait.emit("pointerup");
|
||||
portrait.emit("pointerdown", { buttons: 1 });
|
||||
portrait.emit("pointerup", { buttons: 1 });
|
||||
|
||||
check.equals(sheet.text_name && sheet.text_name.text, "Ship 2");
|
||||
});
|
||||
|
|
|
@ -46,7 +46,7 @@ module TK.SpaceTac.UI {
|
|||
}
|
||||
});
|
||||
|
||||
if (!this.game.headless) {
|
||||
if (!this.game.isTesting) {
|
||||
this.view.input.keyboard.on("keyup", (event: KeyboardEvent) => {
|
||||
if (this.debug) {
|
||||
console.log(event);
|
||||
|
@ -147,6 +147,7 @@ module TK.SpaceTac.UI {
|
|||
let enternext: Function | null = null;
|
||||
let entercalled = false;
|
||||
let cursorinside = false;
|
||||
let leftbutton = false;
|
||||
let destroyed = false;
|
||||
|
||||
obj.setDataEnabled();
|
||||
|
@ -234,6 +235,7 @@ module TK.SpaceTac.UI {
|
|||
|
||||
obj.on("pointerdown", (pointer?: Phaser.Input.Pointer) => {
|
||||
if (destroyed || (pointer && pointer.buttons != 1)) return;
|
||||
leftbutton = true;
|
||||
|
||||
if (UITools.isVisible(obj)) {
|
||||
holdstart = Timer.nowMs();
|
||||
|
@ -247,7 +249,8 @@ module TK.SpaceTac.UI {
|
|||
});
|
||||
|
||||
obj.on("pointerup", (pointer?: Phaser.Input.Pointer) => {
|
||||
if (destroyed || (pointer && pointer.buttons != 1)) return;
|
||||
if (destroyed || !leftbutton) return;
|
||||
leftbutton = false;
|
||||
|
||||
if (!cursorinside) {
|
||||
effectiveleave();
|
||||
|
|
|
@ -38,7 +38,7 @@ module TK.SpaceTac.UI.Specs {
|
|||
return component;
|
||||
}
|
||||
|
||||
function checktext(path: (number | string)[], attrs?: Partial<UIText>, style?: Partial<Phaser.GameObjects.Text.TextStyle>): UIText {
|
||||
function checktext(path: (number | string)[], attrs?: Partial<UIText>, style?: Partial<Phaser.GameObjects.TextStyle>): UIText {
|
||||
let text = checkcomp(path, UIText, "", attrs);
|
||||
|
||||
if (typeof style != "undefined") {
|
||||
|
@ -116,8 +116,8 @@ module TK.SpaceTac.UI.Specs {
|
|||
builder.clear();
|
||||
builder.text("", 0, 0, {});
|
||||
builder.text("", 0, 0, { size: 61 });
|
||||
checktext(["View layers", "base", 0], undefined, { fontFamily: "16pt SpaceTac" });
|
||||
checktext(["View layers", "base", 1], undefined, { fontFamily: "61pt SpaceTac" });
|
||||
checktext(["View layers", "base", 0], undefined, { fontFamily: "SpaceTac", fontSize: "16pt" });
|
||||
checktext(["View layers", "base", 1], undefined, { fontFamily: "SpaceTac", fontSize: "61pt" });
|
||||
|
||||
builder.clear();
|
||||
builder.text("", 0, 0, {});
|
||||
|
@ -140,8 +140,8 @@ module TK.SpaceTac.UI.Specs {
|
|||
builder.clear();
|
||||
builder.text("", 0, 0, {});
|
||||
builder.text("", 0, 0, { bold: true });
|
||||
checktext(["View layers", "base", 0], undefined, { fontFamily: "16pt SpaceTac" });
|
||||
checktext(["View layers", "base", 1], undefined, { fontFamily: "bold 16pt SpaceTac" });
|
||||
checktext(["View layers", "base", 0], undefined, { fontFamily: "SpaceTac", fontSize: "16pt", fontStyle: "" });
|
||||
checktext(["View layers", "base", 1], undefined, { fontFamily: "SpaceTac", fontSize: "16pt", fontStyle: "bold" });
|
||||
|
||||
builder.clear();
|
||||
builder.text("", 0, 0, {});
|
||||
|
@ -208,10 +208,10 @@ module TK.SpaceTac.UI.Specs {
|
|||
builder.text("t3");
|
||||
builder.text("t4", undefined, undefined, { bold: true });
|
||||
|
||||
checktext(["View layers", "base", 0], { text: "t1" }, { fontFamily: "16pt SpaceTac" });
|
||||
checktext(["View layers", "base", 1], { text: "t2" }, { fontFamily: "bold 16pt SpaceTac" });
|
||||
checktext(["View layers", "base", 2], { text: "t3" }, { fontFamily: "16pt SpaceTac" });
|
||||
checktext(["View layers", "base", 3], { text: "t4" }, { fontFamily: "bold 16pt SpaceTac" });
|
||||
checktext(["View layers", "base", 0], { text: "t1" }, { fontFamily: "SpaceTac", fontSize: "16pt", fontStyle: "" });
|
||||
checktext(["View layers", "base", 1], { text: "t2" }, { fontFamily: "SpaceTac", fontSize: "16pt", fontStyle: "bold" });
|
||||
checktext(["View layers", "base", 2], { text: "t3" }, { fontFamily: "SpaceTac", fontSize: "16pt", fontStyle: "" });
|
||||
checktext(["View layers", "base", 3], { text: "t4" }, { fontFamily: "SpaceTac", fontSize: "16pt", fontStyle: "bold" });
|
||||
})
|
||||
|
||||
test.case("allows to change text or image content", check => {
|
||||
|
|
Loading…
Reference in New Issue