├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── debugTests.js ├── doop.ts ├── package.json ├── readme.md ├── spec ├── DoopReduxSpec.ts ├── DoopSpec.ts └── support │ ├── jasmine.d.ts │ └── jasmine.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | built 2 | node_modules 3 | .DS_Store 4 | .vscode -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielearwicker/doop/9ae9733f9011776769b0375b3c60b8aeb20e4bc3/.npmignore -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "stable" 4 | - "4.1" 5 | - "4.0" 6 | - "0.12" 7 | - "0.11" 8 | - "0.10" 9 | before_install: 10 | - npm install -g typescript@beta 11 | - npm install -g jasmine 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Daniel Earwicker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /debugTests.js: -------------------------------------------------------------------------------- 1 | var Jasmine = require("jasmine"); 2 | 3 | var jasmine = new Jasmine(); 4 | 5 | jasmine.loadConfigFile('spec/support/jasmine.json'); 6 | 7 | jasmine.execute(); 8 | -------------------------------------------------------------------------------- /doop.ts: -------------------------------------------------------------------------------- 1 | /** Storage implementation is very simple: uses an array indexed 2 | by order of declaration. Cloning an array is very fast, see: 3 | https://github.com/facebook/immutable-js/issues/286 4 | */ 5 | function makeDoopDescriptor(index: number, target: any, key: string) { 6 | 7 | const original = target[key]; 8 | 9 | let defaultValue: any; 10 | 11 | // This implements the getter/setter for each Doop property 12 | function Doop(val: any) { 13 | 14 | if (arguments.length == 0) { 15 | // Get: this is easy, just fetch from our secret array 16 | const val = this.$__Doop__$ && this.$__Doop__$[index]; 17 | return (val === undefined) ? defaultValue : val; 18 | } 19 | 20 | // Set: first make sure our secret array exists 21 | if (!this.$__Doop__$) { 22 | this.$__Doop__$ = []; 23 | } 24 | 25 | // While running inside constructor, just mutate the value 26 | if (this.$__Doop__$Constructing) { 27 | this.$__Doop__$[index] = val; 28 | 29 | // And return the same (albeit mutated) instance 30 | return this; 31 | } 32 | 33 | // Make a new instance based on prototype 34 | const revision = Object.create(target); 35 | 36 | // Copy the secret array from the original object 37 | const copy = revision.$__Doop__$ = this.$__Doop__$.slice(0); 38 | 39 | // Mutate the new secret array 40 | copy[index] = val; 41 | 42 | // Return the mutated clone 43 | return revision; 44 | } 45 | 46 | const mapper: Mapper = original && original.$__Doop__$Mapper; 47 | const isField: Mapper = original && original.$__Doop__$Field; 48 | let reducers: any; 49 | 50 | if (mapper || isField) { 51 | reducers = target.$__Doop__$Reducers; 52 | 53 | if (isField) { 54 | defaultValue = original.$__Doop__$Init; 55 | (Doop as any).$__Doop__$Field = true; 56 | (Doop as any).$__Doop__$Init = original.$__Doop__$Init; 57 | } 58 | 59 | (Doop as any).self = (container: Cursor): Cursor => { 60 | return cursor( 61 | container()[key](), 62 | (action: Action) => container({ 63 | type: key + ".self", 64 | payload: action, 65 | $: undefined 66 | })); 67 | } 68 | 69 | reducers[key + ".self"] = (container: any, childAction: any) => { 70 | const child = container[key](); 71 | const reduce = child.$__Doop__$Reduce; 72 | if (reduce) { 73 | const newChild = reduce(child, childAction); 74 | return container[key](newChild); 75 | } 76 | return container; 77 | } 78 | } 79 | 80 | if (mapper) { 81 | defaultValue = mapper.empty; 82 | (Doop as any).$__Doop__$Mapper = mapper; 83 | 84 | const defaultItem = (Doop as any).$__Doop__$DefaultItem = original.$__Doop__$DefaultItem; 85 | 86 | (Doop as any).item = (container: Cursor, address: any): Cursor => { 87 | const val = mapper.get(container()[key](), address); 88 | 89 | return cursor(val === undefined ? defaultItem : val, 90 | (itemAction: Action) => { 91 | container({ 92 | type: key + ".item", 93 | payload: { address, itemAction }, 94 | $: undefined 95 | }); 96 | } 97 | ); 98 | }; 99 | 100 | (Doop as any).remove = (address: any): Action => { 101 | return { 102 | type: key + ".item", 103 | payload: { address, undefined }, 104 | $: undefined 105 | }; 106 | }; 107 | 108 | reducers[key + ".item"] = (container: any, { address, itemAction }: any) => { 109 | const collection = container[key](); 110 | const item = mapper.get(collection, address); 111 | const defaulted = item === undefined ? defaultItem : item; 112 | const reduce = defaulted.$__Doop__$Reduce; 113 | if (reduce) { 114 | const newItem = itemAction === undefined ? undefined : reduce(defaulted, itemAction); 115 | const newCollection = mapper.set(collection, address, newItem); 116 | return container[key](newCollection); 117 | } 118 | return container; 119 | } 120 | } 121 | 122 | return { 123 | writable: true, 124 | enumerable: true, 125 | configurable: true, 126 | value: Doop 127 | }; 128 | } 129 | 130 | function clone(obj: any) { 131 | return obj && JSON.parse(JSON.stringify(obj)); 132 | } 133 | 134 | function assign(target: Target, source: Source): Target & Source { 135 | Object.keys(source).forEach(key => { 136 | (target as any)[key] = (source as any)[key]; 137 | }); 138 | return target as Target & Source; 139 | } 140 | 141 | /** This overload is used in property declarations. Maybe prefer 'field 'as it 142 | supports supplying an initial value. 143 | */ 144 | export function doop(): Doop; 145 | 146 | /** This overload acts as a decorator on a class, indicating that it is 147 | immutable. 148 | */ 149 | export function doop(target: any): any; 150 | 151 | /** This overload acts as a decorator on a property getter, and converts it into 152 | an ordinary function which (this is the beautiful hack) is syntactically 153 | compatible with a getter that returns the Doop interface. 154 | */ 155 | export function doop( 156 | target: any, 157 | propertyKey: string | symbol, 158 | descriptor: TypedPropertyDescriptor): TypedPropertyDescriptor | void; 159 | 160 | export function doop( 161 | target?: any, 162 | propertyKey?: string | symbol, 163 | descriptor?: TypedPropertyDescriptor): TypedPropertyDescriptor | void { 164 | 165 | if (!target) { 166 | // We're being used to declare the property 167 | return undefined; 168 | } 169 | 170 | if (!propertyKey) { 171 | const wrapper = function(...args: any[]) { 172 | // During construction, set the flag so Doop setters can mutate 173 | this.$__Doop__$Constructing = (this.$__Doop__$Constructing || 0) + 1; 174 | try { 175 | target.apply(this, args); 176 | } finally { 177 | this.$__Doop__$Constructing--; 178 | } 179 | return this; 180 | } 181 | 182 | const prototype = wrapper.prototype = /*Object.create(*/target.prototype/*)*/; 183 | prototype.$__Doop__$Reducers = {}; 184 | 185 | Object.keys(target).forEach(key => { 186 | let staticField = target[key]; 187 | if (staticField && staticField.$__Doop__$ActionName) { 188 | 189 | staticField = staticField.$__Doop__$ActionName(key); 190 | 191 | if (staticField.$__Doop__$Reducer) { 192 | prototype.$__Doop__$Reducers[key] = staticField.$__Doop__$Reducer; 193 | } 194 | } 195 | (wrapper as any)[key] = staticField; 196 | }); 197 | 198 | const indices = prototype.$__Doop__$Indices = clone(prototype.$__Doop__$Indices); 199 | 200 | if (indices) { 201 | /** Redefine inherited Doop properties. Why? See makeDoopDescriptor. 202 | when the user sets a property we clone the object, which starts 203 | with giving the prototype to Object.create. This better not be 204 | the base class's prototype, or we'll get an instance of the base 205 | class. So we just create new definitions at every level of 206 | inheritance. 207 | */ 208 | for (const key of Object.keys(indices)) { 209 | Object.defineProperty(prototype, key, makeDoopDescriptor(indices[key], prototype, key)); 210 | } 211 | } 212 | 213 | prototype.$__Doop__$Reduce = (state: any, action: any) => { 214 | const reduce = prototype.$__Doop__$Reducers[action.type]; 215 | return reduce ? reduce(state, action.payload) : state; 216 | }; 217 | 218 | prototype.toJSON = function() { 219 | const obj: any = {}; 220 | for (const key of Object.keys(indices)) { 221 | obj[key] = this[key](); 222 | } 223 | return obj; 224 | } 225 | 226 | return wrapper; 227 | } 228 | 229 | // Allocate an index on this type 230 | const index = target.$__Doop__$Count || 0; 231 | target.$__Doop__$Count = index + 1; 232 | 233 | // Create the indices map so we can redefine properties in inheriting classes 234 | const indices = target.$__Doop__$Indices || (target.$__Doop__$Indices = {}); 235 | indices[propertyKey] = index; 236 | } 237 | 238 | /** As far as TypeScript is concerned, a Doop property is a getter that returns 239 | an instance of some object that supports this interface. 240 | 241 | But at runtime we reconfigure it to be an ordinary function that implements 242 | this interface directly, so that `this` refers to the owner object. 243 | */ 244 | export interface Doop { 245 | (): V; 246 | (newValue: V): O; 247 | } 248 | 249 | /** 250 | * An action that can be sent to a cursor (or Redux store) 251 | */ 252 | export interface Action { 253 | type: string; 254 | payload: Payload; 255 | $: Target; 256 | } 257 | 258 | function makeAction(name: string, reduce: (state: State, payload: Payload) => State) { 259 | 260 | const func = (payload: Payload) => { 261 | return { type: name, payload: payload, $: undefined as any }; 262 | } 263 | 264 | let merged: { 265 | (payload: Payload): Action; 266 | $__Doop__$ActionName(newName: string): typeof merged; 267 | $__Doop__$Reducer: typeof reduce; 268 | }; 269 | 270 | merged = assign(func, { 271 | $__Doop__$ActionName(newName: string) { 272 | return newName === name ? merged : makeAction(newName, reduce); 273 | }, 274 | $__Doop__$Reducer: reduce 275 | }); 276 | 277 | return merged; 278 | } 279 | 280 | export function action(reduce: (state: State, payload: Payload) => State) 281 | : (payload: Payload) => Action; 282 | 283 | export function action(reduce: (state: State) => State): () => Action; 284 | 285 | export function action(reduce: (state: State, payload: Payload) => State) 286 | : (payload: Payload) => Action { 287 | 288 | return makeAction("(noname)", reduce); 289 | } 290 | 291 | export interface Cursor { 292 | (): State; 293 | (action: Action): void; 294 | } 295 | 296 | function cursor( 297 | snapshot: State, 298 | dispatch: (action: Action) => void 299 | ): Cursor { 300 | 301 | return (action?: Action) => { 302 | if (action) { 303 | dispatch(action); 304 | } 305 | return snapshot; 306 | }; 307 | } 308 | 309 | export interface Mapper { 310 | readonly empty: Collection; 311 | get(container: Collection, address: Address): Item; 312 | set(container: Collection, address: Address, value: Item): Collection; 313 | } 314 | 315 | export function arraySet(array: T[], index: number, value: T) { 316 | const clone = array.slice(0); 317 | clone[index] = value; 318 | return clone; 319 | } 320 | 321 | export const arrayMapper: Mapper = { 322 | empty: [], 323 | get(array, index) { return array[index]; }, 324 | set: arraySet 325 | }; 326 | 327 | export function merge(first: Target, second: Source): Target & Source { 328 | return assign(assign({}, first), second); 329 | } 330 | 331 | const objectMapper: Mapper = { 332 | empty: {}, 333 | get(obj, key) { return obj[key]; }, 334 | set(obj, key, value) { 335 | if (value === undefined) { 336 | var clone = assign({}, obj); 337 | delete clone[key]; 338 | return clone; 339 | } 340 | return merge(obj, { [key]: value }); 341 | } 342 | }; 343 | 344 | export const objectMapperString: Mapper = objectMapper; 345 | export const objectMapperNumber: Mapper = objectMapper; 346 | 347 | export interface Field extends Doop { 348 | self(container: Cursor): Cursor; 349 | } 350 | 351 | export function field(init?: Type) { 352 | // posts actions using field name 353 | return { $__Doop__$Field: true, $__Doop__$Init: init } as any as Field; 354 | } 355 | 356 | export interface CollectionField extends Field { 357 | item(container: Cursor, address: Address): Cursor; 358 | remove(address: Address): Action; 359 | } 360 | 361 | export function collection( 362 | mapper: Mapper, 363 | defaultItem?: Item 364 | ) { 365 | // posts actions that include the address in the payload 366 | return { 367 | $__Doop__$Mapper: mapper, 368 | $__Doop__$DefaultItem: defaultItem 369 | } as any as CollectionField; 370 | } 371 | 372 | export function createReducer(init: State) { 373 | return (state: State, action: Action) => { 374 | if (!state) { 375 | return init; 376 | } 377 | const reducer = (state as any).$__Doop__$Reduce; 378 | if (reducer) { 379 | return reducer(state, action) as State; 380 | } 381 | return state; 382 | }; 383 | } 384 | 385 | export interface CommonStore { 386 | subscribe(handler: () => void): { () : void; }; 387 | } 388 | 389 | export interface SimpleStore extends CommonStore { 390 | cursor(): Cursor; 391 | } 392 | 393 | export function createStore(init: State): SimpleStore { 394 | 395 | const reducer = createReducer(init); 396 | const subscribers: (() => void)[] = []; 397 | let state = reducer(undefined as any, { type: "$__Doop__$initAction" } as Action);; 398 | 399 | function dispatch(action: Action) { 400 | const newState = reducer(state, action); 401 | if (newState !== state) { 402 | state = newState; 403 | subscribers.forEach(s => s()); 404 | } 405 | } 406 | 407 | return { 408 | cursor() { 409 | return cursor(state, dispatch); 410 | }, 411 | subscribe(handler: () => void) { 412 | subscribers.push(handler); 413 | return () => { 414 | const index = subscribers.indexOf(handler); 415 | if (index !== -1) { 416 | subscribers.splice(index); 417 | } 418 | } 419 | } 420 | }; 421 | } 422 | 423 | // Redux integration 424 | export interface ReduxStore extends CommonStore { 425 | getState(): State; 426 | dispatch(action: Action): void; 427 | } 428 | 429 | export function cursorFromStore(store: ReduxStore) { 430 | return cursor(store.getState(), store.dispatch); 431 | } 432 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "doop", 3 | "version": "2.2.1", 4 | "description": "Succinct immmutable record classes and Redux for TypeScript", 5 | "main": "built/doop.js", 6 | "typings": "built/doop.d.ts", 7 | "scripts": { 8 | "test": "jasmine", 9 | "prepublish": "tsc" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/danielearwicker/doop.git" 14 | }, 15 | "keywords": [ 16 | "immutable", 17 | "decorators", 18 | "typescript" 19 | ], 20 | "author": "Daniel Earwicker ", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/danielearwicker/doop/issues" 24 | }, 25 | "homepage": "https://github.com/danielearwicker/doop#readme", 26 | "devDependencies": { 27 | "jasmine": "^2.4.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/danielearwicker/doop.svg?branch=master)](https://travis-ci.org/danielearwicker/doop) 2 | 3 | ## doop 4 | 5 | ... is a way of writing immutable classes in TypeScript, with a fairly minimal syntax for declaring properties (you only have to state the property name and type once), and convenient/fast clone-with-update, all with static type checking and support for inheritance. 6 | 7 | I'm going to pretend the name stands for: 8 | 9 | Declarative Object-Oriented Persistence 10 | 11 | * *Persistence* in the sense of immutable data structures, nothing to do with 12 | I/O serialization. 13 | 14 | * *Declarative* in that decorators are used to drive metaprogramming that takes 15 | care of all the behind-the-scenes implementation stuff for you. 16 | 17 | But really it's a [Futurama reference](http://futurama.wikia.com/wiki/Democratic_Order_of_Planets). 18 | 19 | ## Installation 20 | 21 | npm install doop 22 | 23 | The package includes TypeScript declarations so you don't need to install them separately. 24 | 25 | How to declare a class with three properties: 26 | 27 | ```typescript 28 | import { doop } from "../doop"; 29 | 30 | @doop 31 | class Animal { 32 | 33 | @doop 34 | get hasTail() { return doop() } 35 | 36 | @doop 37 | get legs() { return doop(); } 38 | 39 | @doop 40 | get food() { return doop(); } 41 | 42 | constructor() { 43 | this.hasTail(true).legs(2); 44 | } 45 | 46 | describe() { 47 | const tail = this.hasTail() ? "a" : "no"; 48 | return `Has ${this.legs()} legs, ${tail} tail and likes to eat ${this.food()}.`; 49 | } 50 | } 51 | ``` 52 | 53 | And here's how you'd use it: 54 | 55 | ```typescript 56 | const a = new Animal(); 57 | expect(a.legs()).toEqual(2); // jasmine spec-style assertion 58 | 59 | // create a modified clone 60 | const b = a.legs(4); 61 | expect(b.legs()).toEqual(4); 62 | 63 | // original object is unaffected 64 | expect(a.legs()).toEqual(2); 65 | ``` 66 | 67 | That is, you call the property with no arguments to get the value, and you call it with one argument to create a new, separate instance of the class with that property's value modified but all other `doop` properties having the same value as the original instance. Cloning is super-fast. 68 | 69 | (Avoid defining any ordinary instance properties on the same class; they will not be copied and will have the value `undefined` on a new cloned instance.) 70 | 71 | ## More details 72 | 73 | See the [background info](http://danielearwicker.github.io/Introducing_doop.html). 74 | 75 | 76 | -------------------------------------------------------------------------------- /spec/DoopReduxSpec.ts: -------------------------------------------------------------------------------- 1 | import { doop } from "../doop"; 2 | import * as Doop from "../doop"; 3 | 4 | @doop 5 | class Publisher { 6 | 7 | @doop 8 | get name() { return Doop.field() } 9 | 10 | @doop 11 | get reputation() { return Doop.field(3) } 12 | 13 | static readonly upvote = Doop.action( 14 | pub => pub.reputation(Math.min(5, pub.reputation() + 1))); 15 | 16 | static readonly downvote = Doop.action( 17 | pub => pub.reputation(Math.max(1, pub.reputation() - 1))); 18 | } 19 | 20 | @doop 21 | class ForeignPublisher extends Publisher { 22 | 23 | static readonly upvote = Publisher.downvote; 24 | 25 | static readonly downvote = Publisher.upvote; 26 | } 27 | 28 | @doop 29 | class Book { 30 | 31 | @doop 32 | get title() { return Doop.field() } 33 | 34 | @doop 35 | get price() { return Doop.field() } 36 | 37 | @doop 38 | get publisher() { return Doop.field(new Publisher()) } 39 | 40 | static readonly setTitleAndPrice = 41 | Doop.action<{title: string, price: number}, Book>( 42 | (book, {title, price}) => book.title(title).price(price)); 43 | 44 | static readonly publish = Doop.action( 45 | (book, name) => book.publisher(new Publisher().name(name))); 46 | 47 | static readonly publishOverseas = Doop.action( 48 | (book, name) => book.publisher(new ForeignPublisher().name(name))); 49 | } 50 | 51 | @doop 52 | class Shelf { 53 | 54 | @doop 55 | get books() { return Doop.collection(Doop.objectMapperNumber) } 59 | constructor() { 60 | // NB. don't need to initialise collections 61 | } 62 | 63 | static readonly addBook = Doop.action( 64 | (shelf, id) => shelf.books(Doop.merge(shelf.books(), { [id]: new Book() }))); 65 | } 66 | 67 | const enableLogging = true; 68 | 69 | function log(message: string, data: any) { 70 | if (!enableLogging) { 71 | return; 72 | } 73 | console.log(`--------------- ${message} ---------------`); 74 | console.log(JSON.stringify(data, null, 4)); 75 | } 76 | 77 | function createTestStore(init: State) { 78 | const store = Doop.createStore(init); 79 | 80 | store.subscribe(() => { 81 | log("changed", store.cursor()()); 82 | }); 83 | 84 | return store; 85 | } 86 | 87 | describe("doop", () => { 88 | 89 | it("supports a basic test of setup", () => { 90 | 91 | const root = createTestStore(new Shelf()); 92 | 93 | root.cursor()(Shelf.addBook(8001)); 94 | 95 | const firstBook = root.cursor()().books.item(root.cursor(), 8001); 96 | 97 | firstBook(Book.setTitleAndPrice({ title: "1985", price: 2.99 })); 98 | firstBook(Book.publish("Penguin")); 99 | 100 | const firstBookPublisher = firstBook().publisher.self(firstBook); 101 | expect(firstBookPublisher().reputation()).toEqual(3); 102 | 103 | firstBookPublisher(Publisher.upvote()); 104 | 105 | const rc1 = root.cursor(); 106 | expect(rc1().books()[8001].price()).toEqual(2.99); 107 | expect(rc1().books()[8001].publisher().name()).toEqual("Penguin"); 108 | expect(rc1().books()[8001].publisher().reputation()).toEqual(4); 109 | 110 | // Sets new ForeignPublisher, reputation back to 3 111 | firstBook(Book.publishOverseas("Der Schtumphenpressen")); 112 | 113 | // Upvote is now downward! 114 | firstBookPublisher(Publisher.upvote()); 115 | 116 | const rc2 = root.cursor(); 117 | expect(rc2().books()[8001].publisher().name()).toEqual("Der Schtumphenpressen"); 118 | expect(rc2().books()[8001].publisher().reputation()).toEqual(2); 119 | }); 120 | }); 121 | 122 | @doop 123 | class Node { 124 | 125 | @doop 126 | get value() { return Doop.field() } 127 | 128 | @doop 129 | get children() { return Doop.collection(Doop.objectMapperString, new Node()) } 133 | 134 | static readonly SetValue = Doop.action( 135 | (node, value) => node.value(value) 136 | ); 137 | 138 | static bindPath = (cursor: Doop.Cursor, [head, ...tail]: string[]): Doop.Cursor => 139 | head === undefined ? cursor : Node.bindPath(cursor().children.item(cursor, head), tail); 140 | } 141 | 142 | /* Known issue: due to order in which TS runs decorators vs static initializers 143 | @doop 144 | class StaticInit { 145 | 146 | @doop 147 | get something() { return Doop.field() } 148 | 149 | static Test = new StaticInit().something(5); 150 | } 151 | */ 152 | 153 | describe("doop", () => { 154 | 155 | /* 156 | it("allows static members to instantiate itself", () => { 157 | 158 | expect(StaticInit.Test.something()).toEqual(5); 159 | }); 160 | */ 161 | 162 | it("supports creation on demand", () => { 163 | 164 | const store = createTestStore(new Node()); 165 | 166 | // We can bind to a path in a single operation, without making any updates yet 167 | const z1 = Node.bindPath(store.cursor(), ["x", "y", "z"]); 168 | 169 | // Send an action to the cursor, the whole path is created now 170 | z1(Node.SetValue("p")); 171 | 172 | // Re-bind so we can see the update 173 | const z2 = Node.bindPath(store.cursor(), ["x", "y", "z"]); 174 | expect(z2().value()).toEqual("p"); 175 | 176 | // Also a built-in action to remove from the collection (depending on the 177 | // mapper used, it might just store an undefined in the slot). 178 | const y1 = Node.bindPath(store.cursor(), ["x", "y"]); 179 | y1(y1().children.remove("z")); 180 | 181 | expect(JSON.stringify(store.cursor()())).toEqual(`{"children":{"x":{"children":{"y":{"children":{}}}}}}`); 182 | }); 183 | }); 184 | -------------------------------------------------------------------------------- /spec/DoopSpec.ts: -------------------------------------------------------------------------------- 1 | import { doop } from "../doop"; 2 | 3 | @doop 4 | class Animal { 5 | 6 | @doop 7 | get hasFur() { return doop() } 8 | 9 | @doop 10 | get hasTail() { return doop() } 11 | 12 | @doop 13 | get legs() { return doop() } 14 | 15 | @doop 16 | get food() { return doop() } 17 | 18 | constructor() { 19 | this.hasTail(true).legs(2); 20 | } 21 | 22 | describe() { 23 | const tail = this.hasTail() ? "a" : "no"; 24 | return `Has ${this.legs()} legs, ${tail} tail and likes to eat ${this.food()}.`; 25 | } 26 | } 27 | 28 | @doop 29 | class Farm { 30 | 31 | @doop 32 | get owner() { return doop() } 33 | 34 | @doop 35 | get bestPig() { return doop() } 36 | } 37 | 38 | @doop 39 | class Piglet extends Animal { 40 | 41 | @doop 42 | get haycorns() { return doop() } 43 | 44 | @doop 45 | get ofLittleBrain() { return doop() } 46 | 47 | constructor() { 48 | super(); 49 | } 50 | 51 | describe() { 52 | return super.describe() + " And is nervous."; 53 | } 54 | } 55 | 56 | @doop 57 | class Bear extends Animal { 58 | 59 | @doop 60 | get ofLittleBrain() { return doop() } 61 | 62 | constructor(ofLittleBrain: boolean) { 63 | super(); 64 | this.hasTail(false) 65 | } 66 | 67 | describe() { 68 | return super.describe() + ( 69 | this.ofLittleBrain() 70 | ? " And is of very little brain." 71 | : " And is quite smart."); 72 | } 73 | } 74 | 75 | @doop 76 | class Pooh extends Bear { 77 | 78 | @doop 79 | get isHumming() { return doop() } 80 | 81 | constructor() { 82 | super(true); 83 | } 84 | } 85 | 86 | @doop 87 | class SuperPiglet extends Piglet { 88 | 89 | @doop 90 | get flying() { return doop() } 91 | } 92 | 93 | @doop 94 | class Empty { } 95 | 96 | @doop 97 | class NoConstructor { 98 | 99 | @doop 100 | get message() { return doop() } 101 | } 102 | 103 | @doop 104 | class Mixed { 105 | 106 | @doop 107 | get x() { return doop() } 108 | 109 | y = 25; 110 | } 111 | 112 | describe("doop", () => { 113 | 114 | it("allows mutation inside constructor", () => { 115 | const a = new Animal(); 116 | expect(a.hasFur()).toEqual(undefined); 117 | expect(a.food()).toEqual(undefined); 118 | expect(a.hasTail()).toEqual(true); 119 | expect(a.legs()).toEqual(2); 120 | }); 121 | 122 | it("supports inheritance", () => { 123 | const b = new Bear(false); 124 | expect(b.hasFur()).toEqual(undefined); 125 | expect(b.food()).toEqual(undefined); 126 | expect(b.hasTail()).toEqual(false); 127 | expect(b.legs()).toEqual(2); 128 | }); 129 | 130 | it("supports multiple levels of inheritance", () => { 131 | const b = new Pooh(); 132 | expect(b.hasFur()).toEqual(undefined); 133 | expect(b.food()).toEqual(undefined); 134 | expect(b.hasTail()).toEqual(false); 135 | expect(b.legs()).toEqual(2); 136 | expect(b.isHumming()).toEqual(undefined); 137 | 138 | expect(b.describe()).toEqual("Has 2 legs, no tail and likes to eat undefined. And is quite smart."); 139 | }); 140 | 141 | it("ignores mutation attempt outside constructor", () => { 142 | const a = new Animal(); 143 | expect(a.legs()).toEqual(2); 144 | a.legs(4); 145 | expect(a.legs()).toEqual(2); 146 | 147 | expect(a.describe()).toEqual("Has 2 legs, a tail and likes to eat undefined."); 148 | }); 149 | 150 | it("returns new revisions on set of property", () => { 151 | const a = new Animal(); 152 | expect(a.legs()).toEqual(2); 153 | const b = a.legs(4); 154 | expect(a.legs()).toEqual(2); 155 | expect(b.legs()).toEqual(4); 156 | 157 | expect(b.describe()).toEqual("Has 4 legs, a tail and likes to eat undefined."); 158 | }); 159 | 160 | it("doesn't muddle up values between properties", () => { 161 | const p = new Piglet(); 162 | expect(p.hasFur()).toEqual(undefined); 163 | expect(p.food()).toEqual(undefined); 164 | expect(p.hasTail()).toEqual(true); 165 | expect(p.legs()).toEqual(2); 166 | 167 | const p2 = p.hasFur(true).food("Haycorns").hasTail(false).legs(15); 168 | expect(p.hasFur()).toEqual(undefined); 169 | expect(p.food()).toEqual(undefined); 170 | expect(p.hasTail()).toEqual(true); 171 | expect(p.legs()).toEqual(2); 172 | expect(p2.hasFur()).toEqual(true); 173 | expect(p2.food()).toEqual("Haycorns"); 174 | expect(p2.hasTail()).toEqual(false); 175 | expect(p2.legs()).toEqual(15); 176 | 177 | const p3 = p2.food("Mash").hasTail(true).legs(800); 178 | expect(p.hasFur()).toEqual(undefined); 179 | expect(p.food()).toEqual(undefined); 180 | expect(p.hasTail()).toEqual(true); 181 | expect(p.legs()).toEqual(2); 182 | expect(p2.hasFur()).toEqual(true); 183 | expect(p2.food()).toEqual("Haycorns"); 184 | expect(p2.hasTail()).toEqual(false); 185 | expect(p2.legs()).toEqual(15); 186 | expect(p3.hasFur()).toEqual(true); 187 | expect(p3.food()).toEqual("Mash"); 188 | expect(p3.hasTail()).toEqual(true); 189 | expect(p3.legs()).toEqual(800); 190 | 191 | expect(p.describe()).toEqual("Has 2 legs, a tail and likes to eat undefined. And is nervous."); 192 | expect(p2.describe()).toEqual("Has 15 legs, no tail and likes to eat Haycorns. And is nervous."); 193 | expect(p3.describe()).toEqual("Has 800 legs, a tail and likes to eat Mash. And is nervous."); 194 | }); 195 | 196 | it("tolerates an empty class", () => new Empty()); 197 | 198 | it("tolerates a constructorless class", () => { 199 | const o = new NoConstructor().message("hi"); 200 | expect(o.message()).toEqual("hi"); 201 | }); 202 | 203 | it("doesn't clone any plain instance properties", () => { 204 | const m = new Mixed(); 205 | expect(m.x()).toEqual(undefined); 206 | expect(m.y).toEqual(25); 207 | 208 | const m2 = m.x(42); 209 | expect(m2.x()).toEqual(42); 210 | expect(m2.y).toEqual(undefined); // instance fields go to undefined 211 | 212 | // original unaffected 213 | expect(m.x()).toEqual(undefined); 214 | expect(m.y).toEqual(25); 215 | }); 216 | 217 | it("doesn't confuse properties in same position in peer classes", () => { 218 | const p = new SuperPiglet().haycorns(32); 219 | expect(p.haycorns()).toEqual(32); 220 | 221 | const p2 = p.ofLittleBrain(false); 222 | expect(p2.ofLittleBrain()).toEqual(false); 223 | expect(p2.haycorns()).toEqual(32); 224 | }); 225 | }); 226 | -------------------------------------------------------------------------------- /spec/support/jasmine.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for Jasmine 2.2 2 | // Project: http://jasmine.github.io/ 3 | // Definitions by: Boris Yankov , Theodore Brown , David Pärsson 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | 7 | // For ddescribe / iit use : https://github.com/borisyankov/DefinitelyTyped/blob/master/karma-jasmine/karma-jasmine.d.ts 8 | 9 | declare function describe(description: string, specDefinitions: () => void): void; 10 | declare function fdescribe(description: string, specDefinitions: () => void): void; 11 | declare function xdescribe(description: string, specDefinitions: () => void): void; 12 | 13 | declare function it(expectation: string, assertion?: () => void, timeout?: number): void; 14 | declare function it(expectation: string, assertion?: (done: () => void) => void, timeout?: number): void; 15 | declare function fit(expectation: string, assertion?: () => void, timeout?: number): void; 16 | declare function fit(expectation: string, assertion?: (done: () => void) => void, timeout?: number): void; 17 | declare function xit(expectation: string, assertion?: () => void, timeout?: number): void; 18 | declare function xit(expectation: string, assertion?: (done: () => void) => void, timeout?: number): void; 19 | 20 | /** If you call the function pending anywhere in the spec body, no matter the expectations, the spec will be marked pending. */ 21 | declare function pending(reason?: string): void; 22 | 23 | declare function beforeEach(action: () => void, timeout?: number): void; 24 | declare function beforeEach(action: (done: () => void) => void, timeout?: number): void; 25 | declare function afterEach(action: () => void, timeout?: number): void; 26 | declare function afterEach(action: (done: () => void) => void, timeout?: number): void; 27 | 28 | declare function beforeAll(action: () => void, timeout?: number): void; 29 | declare function beforeAll(action: (done: () => void) => void, timeout?: number): void; 30 | declare function afterAll(action: () => void, timeout?: number): void; 31 | declare function afterAll(action: (done: () => void) => void, timeout?: number): void; 32 | 33 | declare function expect(spy: Function): jasmine.Matchers; 34 | declare function expect(actual: any): jasmine.Matchers; 35 | 36 | declare function fail(e?: any): void; 37 | 38 | declare function spyOn(object: any, method: string): jasmine.Spy; 39 | 40 | declare function runs(asyncMethod: Function): void; 41 | declare function waitsFor(latchMethod: () => boolean, failureMessage?: string, timeout?: number): void; 42 | declare function waits(timeout?: number): void; 43 | 44 | declare module jasmine { 45 | 46 | var clock: () => Clock; 47 | 48 | function any(aclass: any): Any; 49 | function anything(): Any; 50 | function arrayContaining(sample: any[]): ArrayContaining; 51 | function objectContaining(sample: any): ObjectContaining; 52 | function createSpy(name: string, originalFn?: Function): Spy; 53 | function createSpyObj(baseName: string, methodNames: any[]): any; 54 | function createSpyObj(baseName: string, methodNames: any[]): T; 55 | function pp(value: any): string; 56 | function getEnv(): Env; 57 | function addCustomEqualityTester(equalityTester: CustomEqualityTester): void; 58 | function addMatchers(matchers: CustomMatcherFactories): void; 59 | function stringMatching(str: string): Any; 60 | function stringMatching(str: RegExp): Any; 61 | 62 | interface Any { 63 | 64 | new (expectedClass: any): any; 65 | 66 | jasmineMatches(other: any): boolean; 67 | jasmineToString(): string; 68 | } 69 | 70 | // taken from TypeScript lib.core.es6.d.ts, applicable to CustomMatchers.contains() 71 | interface ArrayLike { 72 | length: number; 73 | [n: number]: T; 74 | } 75 | 76 | interface ArrayContaining { 77 | new (sample: any[]): any; 78 | 79 | asymmetricMatch(other: any): boolean; 80 | jasmineToString(): string; 81 | } 82 | 83 | interface ObjectContaining { 84 | new (sample: any): any; 85 | 86 | jasmineMatches(other: any, mismatchKeys: any[], mismatchValues: any[]): boolean; 87 | jasmineToString(): string; 88 | } 89 | 90 | interface Block { 91 | 92 | new (env: Env, func: SpecFunction, spec: Spec): any; 93 | 94 | execute(onComplete: () => void): void; 95 | } 96 | 97 | interface WaitsBlock extends Block { 98 | new (env: Env, timeout: number, spec: Spec): any; 99 | } 100 | 101 | interface WaitsForBlock extends Block { 102 | new (env: Env, timeout: number, latchFunction: SpecFunction, message: string, spec: Spec): any; 103 | } 104 | 105 | interface Clock { 106 | install(): void; 107 | uninstall(): void; 108 | /** Calls to any registered callback are triggered when the clock is ticked forward via the jasmine.clock().tick function, which takes a number of milliseconds. */ 109 | tick(ms: number): void; 110 | mockDate(date?: Date): void; 111 | } 112 | 113 | interface CustomEqualityTester { 114 | (first: any, second: any): boolean; 115 | } 116 | 117 | interface CustomMatcher { 118 | compare(actual: T, expected: T): CustomMatcherResult; 119 | compare(actual: any, expected: any): CustomMatcherResult; 120 | } 121 | 122 | interface CustomMatcherFactory { 123 | (util: MatchersUtil, customEqualityTesters: Array): CustomMatcher; 124 | } 125 | 126 | interface CustomMatcherFactories { 127 | [index: string]: CustomMatcherFactory; 128 | } 129 | 130 | interface CustomMatcherResult { 131 | pass: boolean; 132 | message?: string; 133 | } 134 | 135 | interface MatchersUtil { 136 | equals(a: any, b: any, customTesters?: Array): boolean; 137 | contains(haystack: ArrayLike | string, needle: any, customTesters?: Array): boolean; 138 | buildFailureMessage(matcherName: string, isNot: boolean, actual: any, ...expected: Array): string; 139 | } 140 | 141 | interface Env { 142 | setTimeout: any; 143 | clearTimeout: void; 144 | setInterval: any; 145 | clearInterval: void; 146 | updateInterval: number; 147 | 148 | currentSpec: Spec; 149 | 150 | matchersClass: Matchers; 151 | 152 | version(): any; 153 | versionString(): string; 154 | nextSpecId(): number; 155 | addReporter(reporter: Reporter): void; 156 | execute(): void; 157 | describe(description: string, specDefinitions: () => void): Suite; 158 | // ddescribe(description: string, specDefinitions: () => void): Suite; Not a part of jasmine. Angular team adds these 159 | beforeEach(beforeEachFunction: () => void): void; 160 | beforeAll(beforeAllFunction: () => void): void; 161 | currentRunner(): Runner; 162 | afterEach(afterEachFunction: () => void): void; 163 | afterAll(afterAllFunction: () => void): void; 164 | xdescribe(desc: string, specDefinitions: () => void): XSuite; 165 | it(description: string, func: () => void): Spec; 166 | // iit(description: string, func: () => void): Spec; Not a part of jasmine. Angular team adds these 167 | xit(desc: string, func: () => void): XSpec; 168 | compareRegExps_(a: RegExp, b: RegExp, mismatchKeys: string[], mismatchValues: string[]): boolean; 169 | compareObjects_(a: any, b: any, mismatchKeys: string[], mismatchValues: string[]): boolean; 170 | equals_(a: any, b: any, mismatchKeys: string[], mismatchValues: string[]): boolean; 171 | contains_(haystack: any, needle: any): boolean; 172 | addCustomEqualityTester(equalityTester: CustomEqualityTester): void; 173 | addMatchers(matchers: CustomMatcherFactories): void; 174 | specFilter(spec: Spec): boolean; 175 | } 176 | 177 | interface FakeTimer { 178 | 179 | new (): any; 180 | 181 | reset(): void; 182 | tick(millis: number): void; 183 | runFunctionsWithinRange(oldMillis: number, nowMillis: number): void; 184 | scheduleFunction(timeoutKey: any, funcToCall: () => void, millis: number, recurring: boolean): void; 185 | } 186 | 187 | interface HtmlReporter { 188 | new (): any; 189 | } 190 | 191 | interface HtmlSpecFilter { 192 | new (): any; 193 | } 194 | 195 | interface Result { 196 | type: string; 197 | } 198 | 199 | interface NestedResults extends Result { 200 | description: string; 201 | 202 | totalCount: number; 203 | passedCount: number; 204 | failedCount: number; 205 | 206 | skipped: boolean; 207 | 208 | rollupCounts(result: NestedResults): void; 209 | log(values: any): void; 210 | getItems(): Result[]; 211 | addResult(result: Result): void; 212 | passed(): boolean; 213 | } 214 | 215 | interface MessageResult extends Result { 216 | values: any; 217 | trace: Trace; 218 | } 219 | 220 | interface ExpectationResult extends Result { 221 | matcherName: string; 222 | passed(): boolean; 223 | expected: any; 224 | actual: any; 225 | message: string; 226 | trace: Trace; 227 | } 228 | 229 | interface Trace { 230 | name: string; 231 | message: string; 232 | stack: any; 233 | } 234 | 235 | interface PrettyPrinter { 236 | 237 | new (): any; 238 | 239 | format(value: any): void; 240 | iterateObject(obj: any, fn: (property: string, isGetter: boolean) => void): void; 241 | emitScalar(value: any): void; 242 | emitString(value: string): void; 243 | emitArray(array: any[]): void; 244 | emitObject(obj: any): void; 245 | append(value: any): void; 246 | } 247 | 248 | interface StringPrettyPrinter extends PrettyPrinter { 249 | } 250 | 251 | interface Queue { 252 | 253 | new (env: any): any; 254 | 255 | env: Env; 256 | ensured: boolean[]; 257 | blocks: Block[]; 258 | running: boolean; 259 | index: number; 260 | offset: number; 261 | abort: boolean; 262 | 263 | addBefore(block: Block, ensure?: boolean): void; 264 | add(block: any, ensure?: boolean): void; 265 | insertNext(block: any, ensure?: boolean): void; 266 | start(onComplete?: () => void): void; 267 | isRunning(): boolean; 268 | next_(): void; 269 | results(): NestedResults; 270 | } 271 | 272 | interface Matchers { 273 | 274 | new (env: Env, actual: any, spec: Env, isNot?: boolean): any; 275 | 276 | env: Env; 277 | actual: any; 278 | spec: Env; 279 | isNot?: boolean; 280 | message(): any; 281 | 282 | toBe(expected: any, expectationFailOutput?: any): boolean; 283 | toEqual(expected: any, expectationFailOutput?: any): boolean; 284 | toMatch(expected: string | RegExp, expectationFailOutput?: any): boolean; 285 | toBeDefined(expectationFailOutput?: any): boolean; 286 | toBeUndefined(expectationFailOutput?: any): boolean; 287 | toBeNull(expectationFailOutput?: any): boolean; 288 | toBeNaN(): boolean; 289 | toBeTruthy(expectationFailOutput?: any): boolean; 290 | toBeFalsy(expectationFailOutput?: any): boolean; 291 | toHaveBeenCalled(): boolean; 292 | toHaveBeenCalledWith(...params: any[]): boolean; 293 | toHaveBeenCalledTimes(expected: number): boolean; 294 | toContain(expected: any, expectationFailOutput?: any): boolean; 295 | toBeLessThan(expected: number, expectationFailOutput?: any): boolean; 296 | toBeGreaterThan(expected: number, expectationFailOutput?: any): boolean; 297 | toBeCloseTo(expected: number, precision: any, expectationFailOutput?: any): boolean; 298 | toThrow(expected?: any): boolean; 299 | toThrowError(message?: string | RegExp): boolean; 300 | toThrowError(expected?: Error, message?: string | RegExp): boolean; 301 | not: Matchers; 302 | 303 | Any: Any; 304 | } 305 | 306 | interface Reporter { 307 | reportRunnerStarting(runner: Runner): void; 308 | reportRunnerResults(runner: Runner): void; 309 | reportSuiteResults(suite: Suite): void; 310 | reportSpecStarting(spec: Spec): void; 311 | reportSpecResults(spec: Spec): void; 312 | log(str: string): void; 313 | } 314 | 315 | interface MultiReporter extends Reporter { 316 | addReporter(reporter: Reporter): void; 317 | } 318 | 319 | interface Runner { 320 | 321 | new (env: Env): any; 322 | 323 | execute(): void; 324 | beforeEach(beforeEachFunction: SpecFunction): void; 325 | afterEach(afterEachFunction: SpecFunction): void; 326 | beforeAll(beforeAllFunction: SpecFunction): void; 327 | afterAll(afterAllFunction: SpecFunction): void; 328 | finishCallback(): void; 329 | addSuite(suite: Suite): void; 330 | add(block: Block): void; 331 | specs(): Spec[]; 332 | suites(): Suite[]; 333 | topLevelSuites(): Suite[]; 334 | results(): NestedResults; 335 | } 336 | 337 | interface SpecFunction { 338 | (spec?: Spec): void; 339 | } 340 | 341 | interface SuiteOrSpec { 342 | id: number; 343 | env: Env; 344 | description: string; 345 | queue: Queue; 346 | } 347 | 348 | interface Spec extends SuiteOrSpec { 349 | 350 | new (env: Env, suite: Suite, description: string): any; 351 | 352 | suite: Suite; 353 | 354 | afterCallbacks: SpecFunction[]; 355 | spies_: Spy[]; 356 | 357 | results_: NestedResults; 358 | matchersClass: Matchers; 359 | 360 | getFullName(): string; 361 | results(): NestedResults; 362 | log(arguments: any): any; 363 | runs(func: SpecFunction): Spec; 364 | addToQueue(block: Block): void; 365 | addMatcherResult(result: Result): void; 366 | expect(actual: any): any; 367 | waits(timeout: number): Spec; 368 | waitsFor(latchFunction: SpecFunction, timeoutMessage?: string, timeout?: number): Spec; 369 | fail(e?: any): void; 370 | getMatchersClass_(): Matchers; 371 | addMatchers(matchersPrototype: CustomMatcherFactories): void; 372 | finishCallback(): void; 373 | finish(onComplete?: () => void): void; 374 | after(doAfter: SpecFunction): void; 375 | execute(onComplete?: () => void): any; 376 | addBeforesAndAftersToQueue(): void; 377 | explodes(): void; 378 | spyOn(obj: any, methodName: string, ignoreMethodDoesntExist: boolean): Spy; 379 | removeAllSpies(): void; 380 | } 381 | 382 | interface XSpec { 383 | id: number; 384 | runs(): void; 385 | } 386 | 387 | interface Suite extends SuiteOrSpec { 388 | 389 | new (env: Env, description: string, specDefinitions: () => void, parentSuite: Suite): any; 390 | 391 | parentSuite: Suite; 392 | 393 | getFullName(): string; 394 | finish(onComplete?: () => void): void; 395 | beforeEach(beforeEachFunction: SpecFunction): void; 396 | afterEach(afterEachFunction: SpecFunction): void; 397 | beforeAll(beforeAllFunction: SpecFunction): void; 398 | afterAll(afterAllFunction: SpecFunction): void; 399 | results(): NestedResults; 400 | add(suiteOrSpec: SuiteOrSpec): void; 401 | specs(): Spec[]; 402 | suites(): Suite[]; 403 | children(): any[]; 404 | execute(onComplete?: () => void): void; 405 | } 406 | 407 | interface XSuite { 408 | execute(): void; 409 | } 410 | 411 | interface Spy { 412 | (...params: any[]): any; 413 | 414 | identity: string; 415 | and: SpyAnd; 416 | calls: Calls; 417 | mostRecentCall: { args: any[]; }; 418 | argsForCall: any[]; 419 | wasCalled: boolean; 420 | } 421 | 422 | interface SpyAnd { 423 | /** By chaining the spy with and.callThrough, the spy will still track all calls to it but in addition it will delegate to the actual implementation. */ 424 | callThrough(): Spy; 425 | /** By chaining the spy with and.returnValue, all calls to the function will return a specific value. */ 426 | returnValue(val: any): Spy; 427 | /** By chaining the spy with and.callFake, all calls to the spy will delegate to the supplied function. */ 428 | callFake(fn: Function): Spy; 429 | /** By chaining the spy with and.throwError, all calls to the spy will throw the specified value. */ 430 | throwError(msg: string): Spy; 431 | /** When a calling strategy is used for a spy, the original stubbing behavior can be returned at any time with and.stub. */ 432 | stub(): Spy; 433 | } 434 | 435 | interface Calls { 436 | /** By chaining the spy with calls.any(), will return false if the spy has not been called at all, and then true once at least one call happens. **/ 437 | any(): boolean; 438 | /** By chaining the spy with calls.count(), will return the number of times the spy was called **/ 439 | count(): number; 440 | /** By chaining the spy with calls.argsFor(), will return the arguments passed to call number index **/ 441 | argsFor(index: number): any[]; 442 | /** By chaining the spy with calls.allArgs(), will return the arguments to all calls **/ 443 | allArgs(): any[]; 444 | /** By chaining the spy with calls.all(), will return the context (the this) and arguments passed all calls **/ 445 | all(): CallInfo[]; 446 | /** By chaining the spy with calls.mostRecent(), will return the context (the this) and arguments for the most recent call **/ 447 | mostRecent(): CallInfo; 448 | /** By chaining the spy with calls.first(), will return the context (the this) and arguments for the first call **/ 449 | first(): CallInfo; 450 | /** By chaining the spy with calls.reset(), will clears all tracking for a spy **/ 451 | reset(): void; 452 | } 453 | 454 | interface CallInfo { 455 | /** The context (the this) for the call */ 456 | object: any; 457 | /** All arguments passed to the call */ 458 | args: any[]; 459 | /** The return value of the call */ 460 | returnValue: any; 461 | } 462 | 463 | interface Util { 464 | inherit(childClass: Function, parentClass: Function): any; 465 | formatException(e: any): any; 466 | htmlEscape(str: string): string; 467 | argsToArray(args: any): any; 468 | extend(destination: any, source: any): any; 469 | } 470 | 471 | interface JsApiReporter extends Reporter { 472 | 473 | started: boolean; 474 | finished: boolean; 475 | result: any; 476 | messages: any; 477 | 478 | new (): any; 479 | 480 | suites(): Suite[]; 481 | summarize_(suiteOrSpec: SuiteOrSpec): any; 482 | results(): any; 483 | resultsForSpec(specId: any): any; 484 | log(str: any): any; 485 | resultsForSpecs(specIds: any): any; 486 | summarizeResult_(result: any): any; 487 | } 488 | 489 | interface Jasmine { 490 | Spec: Spec; 491 | clock: Clock; 492 | util: Util; 493 | } 494 | 495 | export var HtmlReporter: HtmlReporter; 496 | export var HtmlSpecFilter: HtmlSpecFilter; 497 | export var DEFAULT_TIMEOUT_INTERVAL: number; 498 | } 499 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "built/spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": false 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "isolatedModules": false, 7 | "jsx": "react", 8 | "experimentalDecorators": true, 9 | "emitDecoratorMetadata": true, 10 | "declaration": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "removeComments": false, 14 | "noLib": false, 15 | "preserveConstEnums": true, 16 | "outDir": "built", 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "suppressImplicitAnyIndexErrors": false, 20 | "sourceMap": true 21 | }, 22 | "exclude": [ 23 | "node_modules", 24 | "built" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------