]> source.dussan.org Git - svg.js.git/commitdiff
added tests for transform.js, migrated tests for v3 of types to esm tests
authorUlrich-Matthias Schäfer <ulima.ums@googlemail.com>
Tue, 28 Apr 2020 04:19:38 +0000 (14:19 +1000)
committerUlrich-Matthias Schäfer <ulima.ums@googlemail.com>
Tue, 28 Apr 2020 04:19:38 +0000 (14:19 +1000)
25 files changed:
spec/RAFPlugin.js
spec/setupSVGDom.js
spec/spec/animation/Animator.js [new file with mode: 0644]
spec/spec/animation/Morphable.js [new file with mode: 0644]
spec/spec/animation/Queue.js [new file with mode: 0644]
spec/spec/animation/Runner.js [new file with mode: 0644]
spec/spec/animation/Timeline.js [new file with mode: 0644]
spec/spec/animation/easing.js [new file with mode: 0644]
spec/spec/animator.js [deleted file]
spec/spec/color.js [deleted file]
spec/spec/easing.js [deleted file]
spec/spec/modules/optional/transform.js [new file with mode: 0644]
spec/spec/morphing.js [deleted file]
spec/spec/number.js [deleted file]
spec/spec/point.js [deleted file]
spec/spec/pointarray.js [deleted file]
spec/spec/queue.js [deleted file]
spec/spec/runner.js [deleted file]
spec/spec/timeline.js [deleted file]
spec/spec/types/Color.js [new file with mode: 0644]
spec/spec/types/EventTarget.js
spec/spec/types/Matrix.js [new file with mode: 0644]
spec/spec/types/Number.js [new file with mode: 0644]
spec/spec/types/Point.js [new file with mode: 0644]
spec/spec/types/PointArray.js [new file with mode: 0644]

index 87ce48afdba8ad418ff59b6a21a94ea860eba596..3e82c707070c6b7d43173ddbc9389830024da118 100644 (file)
@@ -2,22 +2,19 @@
  * Jasmine RequestAnimationFrame: a set of helpers for testing funcionality
  * that uses requestAnimationFrame under the Jasmine BDD framework for JavaScript.
  */
-;(function () {
+function RAFPlugin (jasmine) {
 
   var index = 0
   var callbacks = []
 
-  function MockRAF (global) {
-    this.realRAF = global.requestAnimationFrame
-    this.realCAF = global.cancelAnimationFrame
-    this.realPerf = global.performance
+  function MockRAF () {
     this.nextTime = 0
 
     var _this = this
 
     /**
-         * Mock for window.requestAnimationFrame
-         */
+    * Mock for window.requestAnimationFrame
+    */
     this.mockRAF = function (fn) {
       if (typeof fn !== 'function') {
         throw new Error('You should pass a function to requestAnimationFrame')
@@ -29,8 +26,8 @@
     }
 
     /**
-         * Mock for window.cancelAnimationFrame
-         */
+    * Mock for window.cancelAnimationFrame
+    */
     this.mockCAF = function (requestID) {
       callbacks.splice(requestID, 1)
     }
     }
 
     /**
-         * Install request animation frame mocks.
-         */
-    this.install = function () {
+    * Install request animation frame mocks.
+    */
+    this.install = function (global) {
+      _this.realRAF = global.requestAnimationFrame
+      _this.realCAF = global.cancelAnimationFrame
+      _this.realPerf = global.performance
       global.requestAnimationFrame = _this.mockRAF
       global.cancelAnimationFrame = _this.mockCAF
       global.performance = _this.mockPerf
     }
 
     /**
-         * Uninstall request animation frame mocks.
-         */
-    this.uninstall = function () {
+    * Uninstall request animation frame mocks.
+    */
+    this.uninstall = function (global) {
       global.requestAnimationFrame = _this.realRAF
       global.cancelAnimationFrame = _this.realCAF
       global.performance = _this.realPerf
@@ -62,8 +62,8 @@
     }
 
     /**
-         * Simulate animation frame readiness.
-         */
+    * Simulate animation frame readiness.
+    */
     this.tick = function (dt) {
       _this.nextTime += (dt || 1)
 
     }
   }
 
-  jasmine.RequestAnimationFrame = new MockRAF(window)
-}())
+  jasmine.RequestAnimationFrame = new MockRAF()
+}
+
+// if (!module) {
+RAFPlugin(jasmine)
+// } else {
+//   module.exports.RAFPlugin = RAFPlugin
+
+// }
index 72e5383e1d02846bb09c18ce0f6fed278c30dab4..22bf56597507d873e1d2b793c4ec576926337867 100644 (file)
@@ -1,3 +1,4 @@
+import './RAFPlugin.js'
 import { createHTMLWindow } from 'svgdom'
 
 /* globals beforeEach, afterEach, jasmine */
diff --git a/spec/spec/animation/Animator.js b/spec/spec/animation/Animator.js
new file mode 100644 (file)
index 0000000..80f2eab
--- /dev/null
@@ -0,0 +1,55 @@
+/* globals describe, expect, it, beforeEach, afterEach, jasmine */
+
+import { Animator, Queue } from '../../../src/main.js'
+import { getWindow } from '../../../src/utils/window.js'
+
+describe('Animator.js', function () {
+
+  beforeEach(function () {
+    jasmine.RequestAnimationFrame.install(getWindow())
+    Animator.timeouts = new Queue()
+    Animator.frames = new Queue()
+    Animator.nextDraw = null
+  })
+
+  afterEach(function () {
+    jasmine.RequestAnimationFrame.uninstall(getWindow())
+  })
+
+  describe('timeout()', function () {
+    it('calls a function after a specific time', function () {
+
+      var spy = jasmine.createSpy('tester')
+      Animator.timeout(spy, 100)
+
+      jasmine.RequestAnimationFrame.tick(99)
+      expect(spy).not.toHaveBeenCalled()
+      jasmine.RequestAnimationFrame.tick()
+      expect(spy).toHaveBeenCalled()
+    })
+  })
+
+  describe('cancelTimeout()', function () {
+    it('cancels a timeout which was created with timeout()', function () {
+      var spy = jasmine.createSpy('tester')
+      var id = Animator.timeout(spy, 100)
+      Animator.clearTimeout(id)
+
+      expect(spy).not.toHaveBeenCalled()
+      jasmine.RequestAnimationFrame.tick(100)
+      expect(spy).not.toHaveBeenCalled()
+    })
+  })
+
+  describe('frame()', function () {
+    it('calls a function at the next animationFrame', function () {
+      var spy = jasmine.createSpy('tester')
+
+      Animator.frame(spy)
+      expect(spy).not.toHaveBeenCalled()
+      jasmine.RequestAnimationFrame.tick()
+      expect(spy).toHaveBeenCalled()
+    })
+  })
+
+})
diff --git a/spec/spec/animation/Morphable.js b/spec/spec/animation/Morphable.js
new file mode 100644 (file)
index 0000000..4b0e2f1
--- /dev/null
@@ -0,0 +1,167 @@
+/* globals describe, expect, it, jasmine */
+
+import { Morphable, NonMorphable, ObjectBag, Color, Box, Matrix, PointArray, PathArray, TransformBag, Number as SVGNumber, Array as SVGArray } from '../../../src/main.js'
+
+const { objectContaining, arrayContaining, any } = jasmine
+
+describe('Morphable.js', function () {
+  describe('constructors', function () {
+
+    it('Morphable with SVGNumber', function () {
+      var morpher = new Morphable().from(10).to(5)
+
+      expect(morpher).toEqual(any(Morphable))
+      expect(morpher.type()).toBe(SVGNumber)
+      expect(morpher.at(0.5)).toEqual(any(SVGNumber))
+      expect(morpher.at(0.5).valueOf()).toBe(7.5)
+    })
+
+    it('Morphable with String', function () {
+      var morpher = new Morphable().from('foo').to('bar')
+
+      expect(morpher).toEqual(any(Morphable))
+      expect(morpher.type()).toBe(NonMorphable)
+      expect(morpher.at(0.5)).toEqual(any(NonMorphable))
+      expect(morpher.at(0.5).valueOf()).toBe('foo')
+      expect(morpher.at(1).valueOf()).toBe('bar')
+    })
+
+    it('Morphable with Object', function () {
+      var morpher = new Morphable().from({ a: 5, b: 10 }).to({ a: 10, b: 20 })
+
+      expect(morpher).toEqual(any(Morphable))
+      expect(morpher.type()).toBe(ObjectBag)
+      expect(morpher.at(0.5)).toEqual(any(Object))
+      expect(morpher.at(0.5).valueOf()).toEqual(objectContaining({ a: 7.5, b: 15 }))
+    })
+
+    it('Creates a morphable out of an SVGNumber', function () {
+      var morpher = new SVGNumber(5).to(10)
+
+      expect(morpher).toEqual(any(Morphable))
+      expect(morpher.type()).toBe(SVGNumber)
+      expect(morpher.at(0.5)).toEqual(any(SVGNumber))
+      expect(morpher.at(0.5).valueOf()).toBe(7.5)
+    })
+
+    it('Creates a morphable out of an Color', function () {
+      var morpher = new Color('#fff').to('#000')
+
+      expect(morpher).toEqual(any(Morphable))
+      expect(morpher.type()).toBe(Color)
+      expect(morpher.at(0.5)).toEqual(any(Color))
+      expect(morpher.at(0.5).toHex()).toBe('#808080')
+    })
+
+    it('Creates a morphable out of an Box', function () {
+      var morpher = new Box(1, 2, 3, 4).to([ 5, 6, 7, 8 ])
+
+      expect(morpher).toEqual(any(Morphable))
+      expect(morpher.type()).toBe(Box)
+      expect(morpher.at(0.5)).toEqual(any(Box))
+      expect(morpher.at(0.5)).toEqual(objectContaining({ x: 3, y: 4, width: 5, height: 6 }))
+    })
+
+    it('Creates a morphable out of an Matrix', function () {
+      var morpher = new Matrix(1, 2, 3, 4, 5, 6).to([ 3, 4, 5, 6, 7, 8 ])
+
+      expect(morpher).toEqual(any(Morphable))
+      expect(morpher.type()).toBe(Matrix)
+      expect(morpher.at(0.5)).toEqual(any(Matrix))
+      expect(morpher.at(0.5)).toEqual(objectContaining(new Matrix(2, 3, 4, 5, 6, 7)))
+    })
+
+    it('Creates a morphable out of an Array', function () {
+      var morpher = new SVGArray([ 1, 2, 3, 4, 5, 6 ]).to([ 3, 4, 5, 6, 7, 8 ])
+
+      expect(morpher).toEqual(any(Morphable))
+      expect(morpher.type()).toBe(SVGArray)
+      expect(morpher.at(0.5)).toEqual(any(SVGArray))
+      expect(morpher.at(0.5).toArray()).toEqual(arrayContaining([ 2, 3, 4, 5, 6, 7 ]))
+    })
+
+    it('Creates a morphable out of an PointArray', function () {
+      var morpher = new PointArray([ 1, 2, 3, 4, 5, 6 ]).to([ 3, 4, 5, 6, 7, 8 ])
+
+      expect(morpher).toEqual(any(Morphable))
+      expect(morpher.type()).toBe(PointArray)
+      expect(morpher.at(0.5)).toEqual(any(PointArray))
+      expect(morpher.at(0.5).toArray()).toEqual(arrayContaining([ 2, 3, 4, 5, 6, 7 ]))
+    })
+
+    it('Creates a morphable out of an PathArray', function () {
+      var morpher = new PathArray([ 'M', 1, 2, 'L', 3, 4, 'L', 5, 6 ]).to([ 'M', 3, 4, 'L', 5, 6, 'L', 7, 8 ])
+
+      expect(morpher).toEqual(any(Morphable))
+      expect(morpher.type()).toBe(PathArray)
+      expect(morpher.at(0.5)).toEqual(any(PathArray))
+      expect(morpher.at(0.5).toArray()).toEqual(arrayContaining([ 'M', 2, 3, 'L', 4, 5, 'L', 6, 7 ]))
+    })
+
+    it('Creates a morphable out of an NonMorphable', function () {
+      var morpher = new NonMorphable('foo').to('bar')
+
+      expect(morpher).toEqual(any(Morphable))
+      expect(morpher.type()).toBe(NonMorphable)
+      expect(morpher.at(0.5)).toEqual(any(NonMorphable))
+      expect(morpher.at(0.5).valueOf()).toBe('foo')
+      expect(morpher.at(1).valueOf()).toBe('bar')
+    })
+
+    it('Creates a morphable out of an TransformBag', function () {
+      var morpher = new TransformBag({ rotate: 0, translateX: 0 })
+        .to({ rotate: 50, translateX: 20 })
+
+      expect(morpher).toEqual(any(Morphable))
+      expect(morpher.type()).toBe(TransformBag)
+      expect(morpher.at(0.5)).toEqual(any(TransformBag))
+
+      expect(morpher.at(0.5)).toEqual(objectContaining({ rotate: 25, translateX: 10 }))
+    })
+
+    it('Creates a morphable out of an ObjectBag', function () {
+      var morpher = new ObjectBag({ a: 5, b: 10 }).to({ a: 10, b: 20 })
+
+      expect(morpher).toEqual(any(Morphable))
+      expect(morpher.type()).toBe(ObjectBag)
+      expect(morpher.at(0.5)).toEqual(any(Object))
+      expect(morpher.at(0.5).valueOf()).toEqual(objectContaining({ a: 7.5, b: 15 }))
+    })
+  })
+
+  describe('from()', function () {
+    it('sets the type of the runner', function () {
+      var morpher = new Morphable().from(5)
+      expect(morpher.type()).toBe(SVGNumber)
+    })
+
+    it('sets the from attribute to an array representation of the morphable type', function () {
+      var morpher = new Morphable().from(5)
+      expect(morpher.from()).toEqual(arrayContaining([ 5 ]))
+    })
+  })
+
+  describe('type()', function () {
+    it('sets the type of the runner', function () {
+      var morpher = new Morphable().type(SVGNumber)
+      expect(morpher._type).toBe(SVGNumber)
+    })
+
+    it('gets the type of the runner', function () {
+      var morpher = new Morphable().type(SVGNumber)
+      expect(morpher.type()).toBe(SVGNumber)
+    })
+  })
+
+  describe('to()', function () {
+    it('sets the type of the runner', function () {
+      var morpher = new Morphable().to(5)
+      expect(morpher.type()).toBe(SVGNumber)
+    })
+
+    it('sets the from attribute to an array representation of the morphable type', function () {
+      var morpher = new Morphable().to(5)
+      expect(morpher.to()).toEqual(arrayContaining([ 5 ]))
+    })
+  })
+})
diff --git a/spec/spec/animation/Queue.js b/spec/spec/animation/Queue.js
new file mode 100644 (file)
index 0000000..4282bf9
--- /dev/null
@@ -0,0 +1,87 @@
+/* globals describe, expect, it */
+
+import { Queue } from '../../../src/main.js'
+
+describe('Queue.js', function () {
+
+  describe('first ()', function () {
+
+    it('returns null if no item in the queue', function () {
+      var queue = new Queue()
+      expect(queue.first()).toEqual(null)
+    })
+
+    it('returns the first value in the queue', function () {
+      var queue = new Queue()
+      queue.push(1)
+      expect(queue.first()).toBe(1)
+      queue.push(2)
+      expect(queue.first()).toBe(1)
+    })
+  })
+
+  describe('last ()', function () {
+
+    it('returns null if no item in the queue', function () {
+      var queue = new Queue()
+      expect(queue.last()).toEqual(null)
+    })
+
+    it('returns the last value added', function () {
+      var queue = new Queue()
+      queue.push(1)
+      expect(queue.last()).toBe(1)
+      queue.push(2)
+      expect(queue.last()).toBe(2)
+    })
+  })
+
+  describe('push ()', function () {
+
+    it('adds an element to the end of the queue', function () {
+      var queue = new Queue()
+      queue.push(1)
+      queue.push(2)
+      queue.push(3)
+
+      expect(queue.first()).toBe(1)
+      expect(queue.last()).toBe(3)
+    })
+  })
+
+  describe('remove ()', function () {
+    it('removes the given item from the queue', function () {
+      var queue = new Queue()
+      queue.push(1)
+      queue.push(2)
+      var item = queue.push(3)
+
+      queue.remove(item)
+
+      expect(queue.last()).toBe(2)
+      expect(queue.first()).toBe(1)
+    })
+  })
+
+  describe('shift ()', function () {
+    it('returns nothing if queue is empty', function () {
+      var queue = new Queue()
+      var val = queue.shift()
+      expect(val).toBeFalsy()
+    })
+
+    it('returns the first item of the queue and removes it', function () {
+      var queue = new Queue()
+      queue.push(1)
+      queue.push(2)
+      queue.push(3)
+
+      var val = queue.shift()
+
+      expect(queue.last()).toBe(3)
+      expect(queue.first()).toBe(2)
+
+      expect(val).toBe(1)
+    })
+  })
+})
diff --git a/spec/spec/animation/Runner.js b/spec/spec/animation/Runner.js
new file mode 100644 (file)
index 0000000..63d1bb8
--- /dev/null
@@ -0,0 +1,842 @@
+/* globals describe, expect, it, beforeEach, afterEach, spyOn, jasmine */
+
+import { Runner, defaults, Ease, Controller, SVG, Timeline } from '../../../src/main.js'
+import { getWindow } from '../../../src/utils/window.js'
+
+const { createSpy, objectContaining, arrayContaining } = jasmine
+
+describe('Runner.js', () => {
+
+  var initFn = createSpy('initFn')
+  var runFn = createSpy('runFn')
+
+  beforeEach(() => {
+    jasmine.RequestAnimationFrame.install(getWindow())
+    initFn.calls.reset()
+    runFn.calls.reset()
+  })
+
+  afterEach(() => {
+    jasmine.RequestAnimationFrame.uninstall(getWindow())
+  })
+
+  describe('sanitise()', () => {
+    it('can handle all form of input', () => {
+      var fn = Runner.sanitise
+
+      expect(fn(200, 200, 'now')).toEqual(objectContaining({
+        duration: 200,
+        delay: 200,
+        when: 'now',
+        times: 1,
+        wait: 0,
+        swing: false
+      }))
+
+      expect(fn(200, 200)).toEqual(objectContaining({
+        duration: 200,
+        delay: 200,
+        when: 'last',
+        times: 1,
+        wait: 0,
+        swing: false
+      }))
+
+      expect(fn(200)).toEqual(objectContaining({
+        duration: 200,
+        delay: defaults.timeline.delay,
+        when: 'last',
+        times: 1,
+        wait: 0,
+        swing: false
+      }))
+
+      expect(fn(runFn)).toEqual(objectContaining({
+        duration: runFn,
+        delay: defaults.timeline.delay,
+        when: 'last',
+        times: 1,
+        wait: 0,
+        swing: false
+      }))
+
+      expect(fn({ delay: 200 })).toEqual(objectContaining({
+        duration: defaults.timeline.duration,
+        delay: 200,
+        when: 'last',
+        times: 1,
+        wait: 0,
+        swing: false
+      }))
+
+      expect(fn({ times: 3, delay: 200, when: 'now', swing: true, wait: 200 })).toEqual(objectContaining({
+        duration: defaults.timeline.duration,
+        delay: 200,
+        when: 'now',
+        times: 3,
+        wait: 200,
+        swing: true
+      }))
+    })
+  })
+
+  describe('())', () => {
+    it('creates a runner with defaults', () => {
+      var runner = new Runner()
+      expect(runner instanceof Runner).toBe(true)
+      expect(runner._duration).toBe(defaults.timeline.duration)
+      expect(runner._stepper instanceof Ease).toBe(true)
+    })
+
+    it('creates a runner with duration set', () => {
+      var runner = new Runner(1000)
+      expect(runner instanceof Runner).toBe(true)
+      expect(runner._duration).toBe(1000)
+      expect(runner._stepper instanceof Ease).toBe(true)
+    })
+
+    it('creates a runner with controller set', () => {
+      var runner = new Runner(runFn)
+      expect(runner instanceof Runner).toBe(true)
+      expect(runner._duration).toBeFalsy()
+      expect(runner._stepper instanceof Controller).toBe(true)
+    })
+  })
+
+  describe('constructors', () => {
+    // FIXME: Not possible to spy like this in es6
+    // describe('animate()',  () => {
+    //   it('creates a runner with the element set and schedules it on the timeline',  () => {
+    //     var orginalRunner = Runner
+    //     spyOn(SVG, 'Runner').and.callFake(()  =>{
+    //       return new orginalRunner()
+    //     })
+    //
+    //     var element = SVG('<rect />')
+    //     var runner = element.animate()
+    //     expect(Runner).toHaveBeenCalled();
+    //     expect(runner instanceof Runner)
+    //     expect(runner.element()).toBe(element)
+    //     expect(runner.timeline()).toBe(element.timeline())
+    //   })
+    // })
+
+    describe('delay()', () => {
+      it('calls animate with correct parameters', () => {
+        var element = SVG('<rect />')
+
+        spyOn(element, 'animate')
+        element.delay(100, 'now')
+        expect(element.animate).toHaveBeenCalledWith(0, 100, 'now')
+      })
+    })
+  })
+
+  describe('queue()', () => {
+    it('adds another closure to the runner', () => {
+      var runner = new Runner()
+      runner.queue(initFn, runFn, true)
+
+      expect(runner._queue[0]).toEqual(objectContaining({
+        initialiser: initFn,
+        initialised: false,
+        runner: runFn,
+        finished: false
+      }))
+    })
+  })
+
+  describe('step()', () => {
+
+    it('returns itself', () => {
+      var runner = new Runner()
+      expect(runner.step()).toBe(runner)
+    })
+
+    it('calls initFn once and runFn at every step', () => {
+      var runner = new Runner()
+      runner.queue(initFn, runFn, false)
+
+      runner.step()
+      expect(initFn).toHaveBeenCalled()
+      expect(runFn).toHaveBeenCalled()
+
+      runner.step()
+      expect(initFn.calls.count()).toBe(1)
+      expect(runFn.calls.count()).toBe(2)
+    })
+
+    it('calls initFn on every step if its declaritive', () => {
+      var runner = new Runner(new Controller())
+      runner.queue(initFn, runFn, true)
+
+      runner.step()
+      expect(initFn).toHaveBeenCalled()
+      expect(runFn).toHaveBeenCalled()
+
+      runner.step()
+      expect(initFn.calls.count()).toBe(2)
+      expect(runFn.calls.count()).toBe(2)
+    })
+
+    function getLoop (r) {
+      var loopDuration = r._duration + r._wait
+      var loopsDone = Math.floor(r._time / loopDuration)
+      return loopsDone
+    }
+
+    // step in time
+    it('steps forward a certain time', () => {
+      var spy = createSpy('stepper')
+      var r = new Runner(1000).loop(10, false, 100)
+      r.queue(null, spy)
+
+      r.step(300) // should be 0.3s
+      expect(spy).toHaveBeenCalledWith(0.3)
+      expect(getLoop(r)).toBe(0)
+
+      r.step(300) // should be 0.6s
+      expect(spy).toHaveBeenCalledWith(0.6)
+      expect(getLoop(r)).toBe(0)
+
+      r.step(600) // should be 0.1s
+      expect(spy).toHaveBeenCalledWith(0.1)
+      expect(getLoop(r)).toBe(1)
+
+      r.step(-300) // should be 0.9s
+      expect(spy).toHaveBeenCalledWith(0.9)
+      expect(getLoop(r)).toBe(0)
+
+      r.step(2000) // should be 0.7s
+      expect(spy).toHaveBeenCalledWith(0.7)
+      expect(getLoop(r)).toBe(2)
+
+      r.step(-2000) // should be 0.9s
+      expect(spy).toHaveBeenCalledWith(0.9)
+      expect(getLoop(r)).toBe(0)
+    })
+
+    it('handles dts which are bigger than the animation time', () => {
+      var runner = new Runner(1000)
+      runner.queue(initFn, runFn, true)
+
+      runner.step(1100)
+      expect(initFn).toHaveBeenCalled()
+      expect(runFn).toHaveBeenCalledWith(1)
+    })
+
+    describe('looping', () => {
+      describe('without wait', () => {
+        describe('unreversed', () => {
+          describe('nonswinging', () => {
+            it('does behave correctly at the end of an even loop', () => {
+              var spy = createSpy('stepper')
+              var runner = new Runner(1000).loop(6, false)
+              runner.queue(null, spy)
+
+              runner.step(5750)
+              expect(spy).toHaveBeenCalledWith(0.75)
+              runner.step(250)
+              expect(spy).toHaveBeenCalledWith(1)
+            })
+
+            it('does behave correctly at the end of an uneven loop', () => {
+              var spy = createSpy('stepper')
+              var runner = new Runner(1000).loop(5, false)
+              runner.queue(null, spy)
+
+              runner.step(4750)
+              expect(spy).toHaveBeenCalledWith(0.75)
+              runner.step(250)
+              expect(spy).toHaveBeenCalledWith(1)
+            })
+          })
+
+          describe('swinging', () => {
+            it('does behave correctly at the end of an even loop', () => {
+              var spy = createSpy('stepper')
+              var runner = new Runner(1000).loop(6, true)
+              runner.queue(null, spy)
+
+              runner.step(5750)
+              expect(spy).toHaveBeenCalledWith(0.25)
+              runner.step(250)
+              expect(spy).toHaveBeenCalledWith(0)
+            })
+
+            it('does behave correctly at the end of an uneven loop', () => {
+              var spy = createSpy('stepper')
+              var runner = new Runner(1000).loop(5, true)
+              runner.queue(null, spy)
+
+              runner.step(4750)
+              expect(spy).toHaveBeenCalledWith(0.75)
+              runner.step(250)
+              expect(spy).toHaveBeenCalledWith(1)
+            })
+          })
+        })
+
+        describe('reversed', () => {
+          describe('nonswinging', () => {
+            it('does behave correctly at the end of an even loop', () => {
+              var spy = createSpy('stepper')
+              var runner = new Runner(1000).loop(6, false).reverse()
+              runner.queue(null, spy)
+
+              runner.step(5750)
+              expect(spy).toHaveBeenCalledWith(0.25)
+              runner.step(250)
+              expect(spy).toHaveBeenCalledWith(0)
+            })
+
+            it('does behave correctly at the end of an uneven loop', () => {
+              var spy = createSpy('stepper')
+              var runner = new Runner(1000).loop(5, false).reverse()
+              runner.queue(null, spy)
+
+              runner.step(4750)
+              expect(spy).toHaveBeenCalledWith(0.25)
+              runner.step(250)
+              expect(spy).toHaveBeenCalledWith(0)
+            })
+          })
+
+          describe('swinging', () => {
+            it('does behave correctly at the end of an even loop', () => {
+              var spy = createSpy('stepper')
+              var runner = new Runner(1000).loop(6, true).reverse()
+              runner.queue(null, spy)
+
+              runner.step(5750)
+              expect(spy).toHaveBeenCalledWith(0.75)
+              runner.step(250)
+              expect(spy).toHaveBeenCalledWith(1)
+            })
+
+            it('does behave correctly at the end of an uneven loop', () => {
+              var spy = createSpy('stepper')
+              var runner = new Runner(1000).loop(5, true).reverse()
+              runner.queue(null, spy)
+
+              runner.step(4750)
+              expect(spy).toHaveBeenCalledWith(0.25)
+              runner.step(250)
+              expect(spy).toHaveBeenCalledWith(0)
+            })
+          })
+        })
+      })
+
+      describe('with wait', () => {
+        describe('unreversed', () => {
+          describe('nonswinging', () => {
+            it('does behave correctly at the end of an even loop', () => {
+              var spy = createSpy('stepper')
+              var runner = new Runner(1000).loop(6, false, 100)
+              runner.queue(null, spy)
+
+              runner.step(5450)
+              expect(spy).toHaveBeenCalledWith(1)
+              spy.calls.reset()
+
+              runner.step(800)
+              expect(spy).toHaveBeenCalledWith(0.75)
+              runner.step(250)
+              expect(spy).toHaveBeenCalledWith(1)
+            })
+
+            it('does behave correctly at the end of an uneven loop', () => {
+              var spy = createSpy('stepper')
+              var runner = new Runner(1000).loop(5, false, 100)
+              runner.queue(null, spy)
+
+              runner.step(4350)
+              expect(spy).toHaveBeenCalledWith(1)
+              spy.calls.reset()
+
+              runner.step(800)
+              expect(spy).toHaveBeenCalledWith(0.75)
+              runner.step(250)
+              expect(spy).toHaveBeenCalledWith(1)
+            })
+          })
+
+          describe('swinging', () => {
+            it('does behave correctly at the end of an even loop', () => {
+              var spy = createSpy('stepper')
+              var runner = new Runner(1000).loop(6, true, 100)
+              runner.queue(null, spy)
+
+              runner.step(5450)
+              expect(spy).toHaveBeenCalledWith(1)
+              spy.calls.reset()
+
+              runner.step(800)
+              expect(spy).toHaveBeenCalledWith(0.25)
+              runner.step(250)
+              expect(spy).toHaveBeenCalledWith(0)
+            })
+
+            it('does behave correctly at the end of an uneven loop', () => {
+              var spy = createSpy('stepper')
+              var runner = new Runner(1000).loop(5, true, 100)
+              runner.queue(null, spy)
+
+              runner.step(4350)
+              expect(spy).toHaveBeenCalledWith(0)
+              spy.calls.reset()
+
+              runner.step(800)
+              expect(spy).toHaveBeenCalledWith(0.75)
+
+              runner.step(250)
+              expect(spy).toHaveBeenCalledWith(1)
+            })
+          })
+        })
+
+        describe('reversed', () => {
+          describe('nonswinging', () => {
+            it('does behave correctly at the end of an even loop', () => {
+              var spy = createSpy('stepper')
+              var runner = new Runner(1000).loop(6, false, 100).reverse()
+              runner.queue(null, spy)
+
+              runner.step(5450)
+              expect(spy).toHaveBeenCalledWith(0)
+              spy.calls.reset()
+
+              runner.step(800)
+              expect(spy).toHaveBeenCalledWith(0.25)
+              runner.step(250)
+              expect(spy).toHaveBeenCalledWith(0)
+            })
+
+            it('does behave correctly at the end of an uneven loop', () => {
+              var spy = createSpy('stepper')
+              var runner = new Runner(1000).loop(5, false, 100).reverse()
+              runner.queue(null, spy)
+
+              runner.step(4350)
+              expect(spy).toHaveBeenCalledWith(0)
+              spy.calls.reset()
+
+              runner.step(800)
+              expect(spy).toHaveBeenCalledWith(0.25)
+              runner.step(250)
+              expect(spy).toHaveBeenCalledWith(0)
+            })
+          })
+
+          describe('swinging', () => {
+            it('does behave correctly at the end of an even loop', () => {
+              var spy = createSpy('stepper')
+              var runner = new Runner(1000).loop(6, true, 100).reverse()
+              runner.queue(null, spy)
+
+              runner.step(5450)
+              expect(spy).toHaveBeenCalledWith(0)
+              spy.calls.reset()
+
+              runner.step(800)
+              expect(spy).toHaveBeenCalledWith(0.75)
+              runner.step(250)
+              expect(spy).toHaveBeenCalledWith(1)
+            })
+
+            it('does behave correctly at the end of an uneven loop', () => {
+              var spy = createSpy('stepper')
+              var runner = new Runner(1000).loop(5, true, 100).reverse()
+              runner.queue(null, spy)
+
+              runner.step(4350)
+              expect(spy).toHaveBeenCalledWith(1)
+              spy.calls.reset()
+
+              runner.step(800)
+              expect(spy).toHaveBeenCalledWith(0.25)
+              runner.step(250)
+              expect(spy).toHaveBeenCalledWith(0)
+            })
+          })
+        })
+      })
+    })
+
+  })
+
+  describe('active()', () => {
+    it('acts as a getter without parameters', () => {
+      var runner = new Runner()
+      expect(runner.active()).toBe(true)
+    })
+
+    it('disables the runner when false is passed', () => {
+      var runner = new Runner()
+      expect(runner.active(false)).toBe(runner)
+      expect(runner.active()).toBe(false)
+    })
+
+    it('enables the runner when true is passed', () => {
+      var runner = new Runner()
+      expect(runner.active(false)).toBe(runner)
+      expect(runner.active(true)).toBe(runner)
+      expect(runner.active()).toBe(true)
+    })
+  })
+
+  describe('duration()', () => {
+    it('return the full duration of the runner including all loops and waits', () => {
+      var runner = new Runner(800).loop(10, true, 200)
+      expect(runner.duration()).toBe(9800)
+    })
+  })
+
+  describe('loop()', () => {
+    it('makes this runner looping', () => {
+      var runner = new Runner(1000).loop(5)
+      expect(runner.duration()).toBe(5000)
+    })
+  })
+
+  describe('time()', () => {
+    it('returns itself', () => {
+      var runner = new Runner()
+      expect(runner.time(0)).toBe(runner)
+    })
+
+    it('acts as a getter with no parameter passed', () => {
+      var runner = new Runner()
+      expect(runner.time()).toBe(0)
+    })
+
+    it('reschedules the runner to a new time', () => {
+      var runner = new Runner()
+      runner.time(10)
+
+      expect(runner.time()).toBe(10)
+    })
+
+    it('calls step to reschedule', () => {
+      var runner = new Runner()
+      spyOn(runner, 'step')
+      runner.time(10)
+
+      expect(runner.step).toHaveBeenCalledWith(10)
+    })
+  })
+
+  describe('loops()', () => {
+    it('get the loops of a runner', () => {
+      var spy = createSpy('stepper')
+      var runner = new Runner(1000).queue(null, spy)
+
+      runner.step(300)
+      expect(spy).toHaveBeenCalledWith(0.3)
+
+      expect(runner.loops()).toBe(0.3)
+    })
+    it('sets the loops of the runner', () => {
+      var spy = createSpy('stepper')
+      var runner = new Runner(1000).queue(null, spy)
+
+      expect(runner.loops(0.5).loops()).toBe(0.5)
+      expect(spy).toHaveBeenCalledWith(0.5)
+
+      expect(runner.loops(0.1).loops()).toBe(0.1)
+      expect(spy).toHaveBeenCalledWith(0.1)
+
+      expect(runner.loops(1.5).loops()).toBe(1)
+      expect(spy).toHaveBeenCalledWith(1)
+    })
+    it('sets the loops of the runner in a loop', () => {
+      var spy = createSpy('stepper')
+      var runner = new Runner(1000).loop(5, true, 500).queue(null, spy)
+
+      expect(runner.loops(1.3).loops()).toBe(1.3)
+      expect(spy).toHaveBeenCalledWith(0.7)
+
+      expect(runner.loops(0.3).loops()).toBe(0.3)
+    })
+  })
+
+  describe('progress()', () => {
+    it('gets the progress of a runner', () => {
+      var spy = createSpy('stepper')
+      var runner = new Runner(1000).queue(null, spy)
+
+      runner.step(300)
+      expect(spy).toHaveBeenCalledWith(0.3)
+
+      expect(runner.progress()).toBe(0.3)
+    })
+
+    it('gets the progress of a runner when looping', () => {
+      var spy = createSpy('stepper')
+      var runner = new Runner(800).queue(null, spy).loop(10, false, 200) // duration should be 9800
+
+      // middle of animation, in the middle of wait time
+      runner.step(4900)
+      expect(runner.progress()).toBe(0.5)
+      expect(spy).toHaveBeenCalledWith(1)
+
+      // start of next loop
+      runner.step(100)
+      expect(spy).toHaveBeenCalledWith(0)
+
+      // move 400 into current loop which is 0.5 progress
+      // the progress value is 5400 / 9800
+      runner.step(400)
+      expect(spy).toHaveBeenCalledWith(0.5)
+      expect(runner.progress()).toBe(5400 / 9800)
+    })
+
+    it('sets the progress of a runner', () => {
+      var spy = createSpy('stepper')
+      var runner = new Runner(1000).queue(null, spy)
+
+      expect(runner.progress(0.5).progress()).toBe(0.5)
+      expect(spy).toHaveBeenCalledWith(0.5)
+    })
+
+    it('sets the progress of a runner when looping', () => {
+      var spy = createSpy('stepper')
+      var runner = new Runner(800).queue(null, spy).loop(10, false, 200)
+
+      // progress 0.5 somewhere in the middle of wait time
+      expect(runner.progress(0.5).progress()).toBe(0.5)
+      expect(spy).toHaveBeenCalledWith(1)
+
+      // start of next loop
+      runner.step(100)
+      expect(spy).toHaveBeenCalledWith(0)
+
+      // should move 0.5 into the next loop
+      expect(runner.progress(5400 / 9800).progress()).toBe(5400 / 9800)
+      expect(spy.calls.mostRecent().args[0]).toBeCloseTo(0.5)
+    })
+  })
+
+  describe('position()', () => {
+
+    it('gets the position of a runner', () => {
+      var spy = createSpy('stepper')
+      var runner = new Runner(1000).queue(null, spy)
+
+      runner.step(300)
+      expect(spy).toHaveBeenCalledWith(0.3)
+
+      expect(runner.position()).toBe(0.3)
+    })
+
+    it('gets the position of a runner when looping', () => {
+      var spy = createSpy('stepper')
+      var runner = new Runner(1000).loop(5, true, 100).queue(null, spy)
+
+      runner.step(1200)
+      expect(spy).toHaveBeenCalledWith(0.9)
+
+      expect(runner.position()).toBe(0.9)
+    })
+
+    it('sets the position of a runner', () => {
+      var spy = createSpy('stepper')
+      var runner = new Runner(1000).queue(null, spy)
+
+      expect(runner.position(0.5).position()).toBe(0.5)
+      expect(spy).toHaveBeenCalledWith(0.5)
+    })
+
+    it('sets the position of a runner in a loop', () => {
+      var spy = createSpy('stepper')
+      var runner = new Runner(1000).loop(5, true, 100).queue(null, spy)
+
+      runner.step(1200)
+      expect(runner.position(0.4).position()).toBe(0.4)
+      expect(spy).toHaveBeenCalledWith(0.4)
+
+      expect(runner.position(0).position()).toBe(0)
+      expect(spy).toHaveBeenCalledWith(0)
+
+      expect(runner.position(1).position()).toBe(1)
+      expect(spy).toHaveBeenCalledWith(1)
+    })
+  })
+
+  describe('element()', () => {
+    it('returns the element bound to this runner if any', () => {
+      var runner1 = new Runner()
+      expect(runner1.element()).toBe(null)
+
+      var element = SVG('<rect />')
+      var runner2 = element.animate()
+      expect(runner2.element()).toBe(element)
+    })
+
+    it('sets an element to be bound to the runner', () => {
+      var runner = new Runner()
+      var element = SVG('<rect />')
+      expect(runner.element(element)).toBe(runner)
+      expect(runner.element()).toBe(element)
+    })
+  })
+
+  describe('timeline()', () => {
+    it('returns the timeline bound to this runner if any', () => {
+      var runner1 = new Runner()
+      expect(runner1.element()).toBe(null)
+
+      var element = SVG('<rect />')
+      var runner2 = element.animate()
+      expect(runner2.timeline()).toBe(element.timeline())
+    })
+
+    it('sets a timeline to be bound to the runner', () => {
+      var runner = new Runner()
+      var timeline = new Timeline()
+      expect(runner.timeline(timeline)).toBe(runner)
+      expect(runner.timeline()).toBe(timeline)
+    })
+  })
+
+  describe('schedule()', () => {
+    it('schedules the runner on a timeline', () => {
+      var runner = new Runner()
+      var timeline = new Timeline()
+      var spy = spyOn(timeline, 'schedule').and.callThrough()
+
+      expect(runner.schedule(timeline, 200, 'now')).toBe(runner)
+      expect(runner.timeline()).toBe(timeline)
+      expect(spy).toHaveBeenCalledWith(runner, 200, 'now')
+    })
+
+    it('schedules the runner on its own timeline', () => {
+      var runner = new Runner()
+      var timeline = new Timeline()
+      var spy = spyOn(timeline, 'schedule')
+      runner.timeline(timeline)
+
+      expect(runner.schedule(200, 'now')).toBe(runner)
+      expect(runner.timeline()).toBe(timeline)
+      expect(spy).toHaveBeenCalledWith(runner, 200, 'now')
+    })
+  })
+
+  describe('unschedule()', () => {
+    it('unschedules this runner from its timeline', () => {
+      var runner = new Runner()
+      var timeline = new Timeline()
+      var spy = spyOn(timeline, 'unschedule').and.callThrough()
+
+      expect(runner.schedule(timeline, 200, 'now')).toBe(runner)
+      expect(runner.unschedule()).toBe(runner)
+      expect(spy).toHaveBeenCalledWith(runner)
+      expect(runner.timeline()).toBe(null)
+    })
+  })
+
+  describe('animate()', () => {
+    it('creates a new runner scheduled after the first', () => {
+      var runner = new Runner(1000)
+      var timeline = new Timeline()
+
+      runner.schedule(timeline)
+
+      var runner2 = runner.animate(500, 1000)
+
+      var t = timeline.time()
+
+      expect(runner2.timeline()).toBe(timeline)
+      expect(runner2.time()).toBe(0)
+
+      expect(timeline.schedule()).toEqual(arrayContaining([
+        objectContaining({ start: t, duration: 1000, end: t + 1000, runner: runner }),
+        objectContaining({ start: t + 2000, duration: 500, end: t + 2500, runner: runner2 })
+      ]))
+    })
+  })
+
+  describe('delay()', () => {
+    it('calls animate with delay parameters', () => {
+      var runner = new Runner(1000)
+      spyOn(runner, 'animate')
+
+      runner.delay(500)
+      expect(runner.animate).toHaveBeenCalledWith(0, 500)
+    })
+  })
+
+  describe('during()', () => {
+    it('returns itself', () => {
+      var runner = new Runner()
+      expect(runner.during(runFn)).toBe(runner)
+    })
+
+    it('calls queue passing only a function to call on every step', () => {
+      var runner = new Runner()
+      spyOn(runner, 'queue')
+      runner.during(runFn)
+
+      expect(runner.queue).toHaveBeenCalledWith(null, runFn)
+    })
+  })
+
+  // describe('after()',  () => {
+  //   it('returns itself',  () => {
+  //     var runner = new Runner()
+  //     expect(runner.after(runFn)).toBe(runner)
+  //   })
+  //
+  //   it('binds a function to the after event',  () => {
+  //     var runner = new Runner()
+  //     spyOn(runner, 'on')
+  //     runner.after(runFn)
+  //
+  //     expect(runner.on).toHaveBeenCalledWith('finish', runFn)
+  //   })
+  // })
+  //
+  // describe('finish()',  () => {
+  //   it('returns itself',  () => {
+  //     var runner = new Runner()
+  //     expect(runner.finish()).toBe(runner)
+  //   })
+  //
+  //   it('calls step with Infinity as argument',  () => {
+  //     var runner = new Runner()
+  //     spyOn(runner, 'step')
+  //     runner.finish()
+  //
+  //     expect(runner.step).toHaveBeenCalledWith(Infinity)
+  //   })
+  // })
+
+  describe('reverse()', () => {
+    it('returns itself', () => {
+      var runner = new Runner()
+      expect(runner.reverse()).toBe(runner)
+    })
+
+    it('reverses the runner', () => {
+      var spy = createSpy('stepper')
+      var runner = new Runner(1000).reverse().queue(null, spy)
+      runner.step(750)
+      expect(spy).toHaveBeenCalledWith(0.25)
+    })
+  })
+
+  describe('ease()', () => {
+    it('returns itself', () => {
+      var runner = new Runner()
+      expect(runner.ease(() => {})).toBe(runner)
+    })
+
+    it('creates an easing Controller from the easing function', () => {
+      var runner = new Runner()
+      runner.ease(() => {})
+
+      expect(runner._stepper instanceof Ease).toBe(true)
+    })
+  })
+})
diff --git a/spec/spec/animation/Timeline.js b/spec/spec/animation/Timeline.js
new file mode 100644 (file)
index 0000000..1acc663
--- /dev/null
@@ -0,0 +1,117 @@
+/* globals describe, expect, it, beforeEach, container */
+
+import { Timeline, SVG } from '../../../src/main.js'
+
+describe('Timeline.js', () => {
+  describe('getEndTimeOfTimeline', () => {
+    it('returns 0 if no runners are scheduled', () => {
+      const timeline = new Timeline()
+      const endTime = timeline.getEndTimeOfTimeline()
+      expect(endTime).toEqual(0)
+    })
+  })
+
+  describe('finish - issue #964', () => {
+    let canvas
+
+    beforeEach(() => {
+      canvas = SVG().addTo(container)
+    })
+
+    it('places all elements at the right position - single runner', () => {
+      const timeline = new Timeline()
+
+      const rect = canvas.rect(20, 20)
+      rect.timeline(timeline)
+      rect.animate().move(100, 200)
+
+      timeline.finish()
+      expect(rect.x()).toEqual(100)
+      expect(rect.y()).toEqual(200)
+    })
+
+    it('places all elements at the right position - runner that finishes latest is in first position', () => {
+      const timeline = new Timeline()
+
+      const rect1 = canvas.rect(10, 10)
+      rect1.timeline(timeline)
+
+      const rect2 = canvas.rect(10, 10)
+      rect2.timeline(timeline)
+
+      const rect3 = canvas.rect(10, 10)
+      rect3.timeline(timeline)
+
+      rect1.animate(2000, 0, 'now').move(100, 200)
+      rect2.animate(1000, 0, 'now').move(100, 200)
+      rect3.animate(1000, 500, 'now').move(100, 200)
+
+      timeline.finish()
+
+      expect(rect1.x()).toEqual(100)
+      expect(rect1.y()).toEqual(200)
+
+      expect(rect2.x()).toEqual(100)
+      expect(rect2.y()).toEqual(200)
+
+      expect(rect3.x()).toEqual(100)
+      expect(rect3.y()).toEqual(200)
+    })
+
+    it('places all elements at the right position - runner that finishes latest is in middle position', () => {
+      const timeline = new Timeline()
+
+      const rect1 = canvas.rect(10, 10)
+      rect1.timeline(timeline)
+
+      const rect2 = canvas.rect(10, 10)
+      rect2.timeline(timeline)
+
+      const rect3 = canvas.rect(10, 10)
+      rect3.timeline(timeline)
+
+      rect2.animate(1000, 0, 'now').move(100, 200)
+      rect1.animate(2000, 0, 'now').move(100, 200)
+      rect3.animate(1000, 500, 'now').move(100, 200)
+
+      timeline.finish()
+
+      expect(rect1.x()).toEqual(100)
+      expect(rect1.y()).toEqual(200)
+
+      expect(rect2.x()).toEqual(100)
+      expect(rect2.y()).toEqual(200)
+
+      expect(rect3.x()).toEqual(100)
+      expect(rect3.y()).toEqual(200)
+    })
+
+    it('places all elements at the right position - runner that finishes latest is in last position', () => {
+      const timeline = new Timeline()
+
+      const rect1 = canvas.rect(10, 10)
+      rect1.timeline(timeline)
+
+      const rect2 = canvas.rect(10, 10)
+      rect2.timeline(timeline)
+
+      const rect3 = canvas.rect(10, 10)
+      rect3.timeline(timeline)
+
+      rect2.animate(1000, 0, 'now').move(100, 200)
+      rect3.animate(1000, 500, 'now').move(100, 200)
+      rect1.animate(2000, 0, 'now').move(100, 200)
+
+      timeline.finish()
+
+      expect(rect1.x()).toEqual(100)
+      expect(rect1.y()).toEqual(200)
+
+      expect(rect2.x()).toEqual(100)
+      expect(rect2.y()).toEqual(200)
+
+      expect(rect3.x()).toEqual(100)
+      expect(rect3.y()).toEqual(200)
+    })
+  })
+})
diff --git a/spec/spec/animation/easing.js b/spec/spec/animation/easing.js
new file mode 100644 (file)
index 0000000..ab9f51c
--- /dev/null
@@ -0,0 +1,26 @@
+/* globals describe, expect, it */
+
+import { easing } from '../../../src/main.js'
+
+describe('easing', () => {
+  var easedValues = {
+    '-': 0.5,
+    '<>': 0.5,
+    '>': 0.7071,
+    '<': 0.2929
+  }
+
+  ;[ '-', '<>', '<', '>' ].forEach((el) => {
+    describe(el, () => {
+      it('is 0 at 0', () => {
+        expect(easing[el](0)).toBe(0)
+      })
+      it('is 1 at 1', () => {
+        expect(Math.round(easing[el](1) * 1000) / 1000).toBe(1) // we need to round cause for some reason at some point 1==0.999999999
+      })
+      it('is eased at 0.5', () => {
+        expect(easing[el](0.5)).toBeCloseTo(easedValues[el])
+      })
+    })
+  })
+})
diff --git a/spec/spec/animator.js b/spec/spec/animator.js
deleted file mode 100644 (file)
index 5ccdcca..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-describe('SVG.Animator', function () {
-
-  beforeEach(function () {
-    jasmine.RequestAnimationFrame.install()
-    SVG.Animator.timeouts = new SVG.Queue()
-    SVG.Animator.frames = new SVG.Queue()
-    SVG.Animator.nextDraw = null
-  })
-
-  afterEach(function () {
-    jasmine.RequestAnimationFrame.uninstall()
-  })
-
-  describe('timeout()', function () {
-    it('calls a function after a specific time', function () {
-
-      var spy = jasmine.createSpy('tester')
-      var id = SVG.Animator.timeout(spy, 100)
-
-      jasmine.RequestAnimationFrame.tick(99)
-      expect(spy).not.toHaveBeenCalled()
-      jasmine.RequestAnimationFrame.tick()
-      expect(spy).toHaveBeenCalled()
-    })
-  })
-
-  describe('cancelTimeout()', function () {
-    it('cancels a timeout which was created with timeout()', function () {
-      var spy = jasmine.createSpy('tester')
-      var id = SVG.Animator.timeout(spy, 100)
-      SVG.Animator.clearTimeout(id)
-
-      expect(spy).not.toHaveBeenCalled()
-      jasmine.RequestAnimationFrame.tick(100)
-      expect(spy).not.toHaveBeenCalled()
-    })
-  })
-
-  describe('frame()', function () {
-    it('calls a function at the next animationFrame', function () {
-      var spy = jasmine.createSpy('tester')
-
-      SVG.Animator.frame(spy)
-      expect(spy).not.toHaveBeenCalled()
-      jasmine.RequestAnimationFrame.tick()
-      expect(spy).toHaveBeenCalled()
-    })
-  })
-
-})
diff --git a/spec/spec/color.js b/spec/spec/color.js
deleted file mode 100644 (file)
index 1f0dfbd..0000000
+++ /dev/null
@@ -1,270 +0,0 @@
-
-describe('Color', function() {
-       var color
-
-       beforeEach(function() {
-               color = new SVG.Color({ r: 0, g: 102, b: 255 })
-       })
-
-       describe ('construct: constructs a color in different formats', () => {
-
-               it ('constructs a color from an object in the correct color space', () => {
-
-                       // Try in rgb
-                       let color = new SVG.Color({ r: 255, g: 0, b: 128 })
-                       expect(color.r).toBe(255)
-                       expect(color.g).toBe(0)
-                       expect(color.b).toBe(128)
-                       expect(color.space).toBe('rgb')
-
-                       // Try in cmyk
-                       let color2 = new SVG.Color({ c: 20, y: 15, m: 10, k: 5 })
-                       expect(color2.c).toBe(20)
-                       expect(color2.m).toBe(10)
-                       expect(color2.y).toBe(15)
-                       expect(color2.k).toBe(5)
-                       expect(color2.space).toBe('cmyk')
-               })
-
-               it ('constructs a color from an array', () => {
-                       let color = new SVG.Color([ 30, 24, 50 ])
-                       expect( color.r ).toBe( 30 )
-                       expect( color.g ).toBe( 24 )
-                       expect( color.b ).toBe( 50 )
-                       expect( color.space ).toBe('rgb')
-               })
-
-               it ('constructs a color from an array with space in array', () => {
-                       let color = new SVG.Color([ 50, 50, 5, 'lab' ])
-                       expect( color.l ).toBe( 50 )
-                       expect( color.a ).toBe( 50 )
-                       expect( color.b ).toBe( 5 )
-                       expect( color.space ).toBe('lab')
-               })
-
-               it ('constructs a color from an array with space given', () => {
-                       let color = new SVG.Color([ 50, 50, 5], 'lab' )
-                       expect( color.l ).toBe( 50 )
-                       expect( color.a ).toBe( 50 )
-                       expect( color.b ).toBe( 5 )
-                       expect( color.space ).toBe('lab')
-               })
-
-               it('correclty parses an rgb string', () => {
-                       let color = new SVG.Color('rgb(255,0,128)')
-                       expect(color.r).toBe(255)
-                       expect(color.g).toBe(0)
-                       expect(color.b).toBe(128)
-               })
-
-               it('correclty parses a 3 digit hex string', () => {
-                       color = new SVG.Color('#f06')
-                       expect(color.r).toBe(255)
-                       expect(color.g).toBe(0)
-                       expect(color.b).toBe(102)
-               })
-
-               it('correclty parses a 6 digit hex string', () => {
-                       color = new SVG.Color('#0066ff')
-                       expect(color.r).toBe(0)
-                       expect(color.g).toBe(102)
-                       expect(color.b).toBe(255)
-               })
-
-       })
-
-       describe ('input and output: Importing and exporting colors', () => {
-               describe('toHex()', function() {
-                       it('returns a hex color', function() {
-                               expect(color.toHex()).toBe('#0066ff')
-                       })
-               })
-
-               describe('toRgb()', function() {
-                       it('returns a rgb string color', function() {
-                               expect(color.toRgb()).toBe('rgb(0,102,255)')
-                       })
-               })
-       })
-
-       describe('color spaces: The color spaces supported by our library', () => {
-
-               describe('lab()', () => {
-                       it ('can convert rgb to lab', () => {
-                               let color = new SVG.Color( 255, 0, 128 )
-                               let lab = color.lab()
-                               expect( lab.l ).toBeCloseTo( 54.88, 1 )
-                               expect( lab.a ).toBeCloseTo( 84.55, 1 )
-                               expect( lab.b ).toBeCloseTo( 4.065, 1 )
-                               expect( lab.space ).toBe('lab')
-                       })
-
-                       it ('can convert from lab to rgb', () => {
-                               let lab = new SVG.Color( 54.88, 84.55, 4.065, 'lab' )
-                               let rgb = lab.rgb()
-                               expect( rgb.r ).toBeCloseTo( 255, 0 )
-                               expect( rgb.g ).toBeCloseTo( 0, 0 )
-                               expect( rgb.b ).toBeCloseTo( 128, 0 )
-                               expect( rgb.space ).toBe('rgb')
-                       })
-
-                       it ('is invertable', () => {
-                               let { r, g, b } = new SVG.Color( 255, 0, 128 ).lab().rgb()
-                               expect ( r ).toBeCloseTo( 255, 0 )
-                               expect ( g ).toBeCloseTo( 0, 0 )
-                               expect ( b ).toBeCloseTo( 128, 0 )
-                       })
-
-                       it('handles black', () => {
-                               let color = new SVG.Color(0, 0, 0).lab().rgb()
-                               expect( color.r ).toBeCloseTo(0, 0)
-                               expect( color.g ).toBeCloseTo(0, 0)
-                               expect( color.b ).toBeCloseTo(0, 0)
-                               expect( color.toHex() ).toBe('#000000')
-                       })
-
-                       it('handles white', () => {
-                               let color = new SVG.Color(255, 255, 255).lab().rgb()
-                               expect( color.r ).toBeCloseTo(255, 0)
-                               expect( color.g ).toBeCloseTo(255, 0)
-                               expect( color.b ).toBeCloseTo(255, 0)
-                               expect( color.toHex() ).toBe('#ffffff')
-                       })
-               })
-
-               describe('lch()', () => {
-                       it ('can convert rgb to lch', () => {
-                               let color = new SVG.Color( 255, 0, 128 )
-                               let lch = color.lch()
-                               expect( lch.l ).toBeCloseTo( 54.88, 1 )
-                               expect( lch.c ).toBeCloseTo( 84.65, 1 )
-                               expect( lch.h ).toBeCloseTo( 2.75, 1 )
-                               expect( lch.space ).toBe('lch')
-                       })
-
-                       it ('can convert from lch to rgb', () => {
-                               let lch = new SVG.Color( 54.88, 84.65, 2.75, 'lch' )
-                               let rgb = lch.rgb()
-                               expect( rgb.r ).toBeCloseTo( 255, 0 )
-                               expect( rgb.g ).toBeCloseTo( 0, 0 )
-                               expect( rgb.b ).toBeCloseTo( 128, 0 )
-                               expect( rgb.space ).toBe('rgb')
-                       })
-
-                       it ('is invertable', () => {
-                               let { r, g, b } = new SVG.Color( 255, 0, 128 ).lch().rgb()
-                               expect ( r ).toBeCloseTo( 255, 0 )
-                               expect ( g ).toBeCloseTo( 0, 0 )
-                               expect ( b ).toBeCloseTo( 128, 0 )
-                       })
-
-                       it('handles black', () => {
-                               let color = new SVG.Color(0, 0, 0).lch().rgb()
-                               expect( color.r ).toBeCloseTo(0, 0)
-                               expect( color.g ).toBeCloseTo(0, 0)
-                               expect( color.b ).toBeCloseTo(0, 0)
-                               expect( color.toHex() ).toBe('#000000')
-                       })
-
-                       it('handles white', () => {
-                               let color = new SVG.Color(255, 255, 255).lch().rgb()
-                               expect( color.r ).toBeCloseTo(255, 0)
-                               expect( color.g ).toBeCloseTo(255, 0)
-                               expect( color.b ).toBeCloseTo(255, 0)
-                               expect( color.toHex() ).toBe('#ffffff')
-                       })
-               })
-
-               describe('hsl()', () => {
-
-                       it ('can convert from rgb to hsl', () => {
-                               let color = new SVG.Color( 255, 0, 128 )
-                               let hsl = color.hsl()
-                               expect( hsl.h ).toBeCloseTo( 329.88, 1 )
-                               expect( hsl.s ).toBeCloseTo( 100, 1 )
-                               expect( hsl.l ).toBeCloseTo( 50, 1 )
-                               expect( hsl.space ).toBe('hsl')
-                       })
-
-                       it ('can convert from hsl to rgb', () => {
-                               let hsl = new SVG.Color( 329.88, 100, 50, 'hsl' )
-                               let rgb = hsl.rgb()
-                               expect( rgb.r ).toBeCloseTo( 255, 0 )
-                               expect( rgb.g ).toBeCloseTo( 0, 0 )
-                               expect( rgb.b ).toBeCloseTo( 128, 0 )
-                               expect( rgb.space ).toBe('rgb')
-                       })
-
-                       it ('is invertable', () => {
-                               let { r, g, b } = new SVG.Color( 255, 0, 128 ).hsl().rgb()
-                               expect ( r ).toBeCloseTo( 255, 0 )
-                               expect ( g ).toBeCloseTo( 0, 0 )
-                               expect ( b ).toBeCloseTo( 128, 0 )
-                       })
-
-                       it('handles black', () => {
-                               let color = new SVG.Color(0, 0, 0).hsl().rgb()
-                               expect( color.r ).toBeCloseTo(0, 0)
-                               expect( color.g ).toBeCloseTo(0, 0)
-                               expect( color.b ).toBeCloseTo(0, 0)
-                               expect( color.toHex() ).toBe('#000000')
-                       })
-
-                       it('handles white', () => {
-                               let color = new SVG.Color(255, 255, 255).hsl().rgb()
-                               expect( color.r ).toBeCloseTo(255, 0)
-                               expect( color.g ).toBeCloseTo(255, 0)
-                               expect( color.b ).toBeCloseTo(255, 0)
-                               expect( color.toHex() ).toBe('#ffffff')
-                       })
-               })
-
-               describe('cmyk()', () => {
-
-                       it ('can convert from rgb to cmyk', () => {
-                               let color = new SVG.Color( 255, 0, 128 )
-                               let cmyk = color.cmyk()
-                               expect( cmyk.c ).toBeCloseTo( 0, 1 )
-                               expect( cmyk.m ).toBeCloseTo( 1, 1 )
-                               expect( cmyk.y ).toBeCloseTo( 0.49, 1 )
-                               expect( cmyk.k ).toBeCloseTo( 0, 1 )
-                               expect( cmyk.space ).toBe('cmyk')
-                       })
-
-                       it ('can convert from cmyk to rgb', () => {
-                               let color = new SVG.Color( 0, 1, 0.49, 0, 'cmyk' )
-                               let rgb = color.rgb()
-                               expect( rgb.r ).toBeCloseTo( 255, -1 )
-                               expect( rgb.g ).toBeCloseTo( 0, -1 )
-                               expect( rgb.b ).toBeCloseTo( 128, -1 )
-                               expect( rgb.space ).toBe('rgb')
-                       })
-
-                       it ('is invertable', () => {
-                               let { r, g, b } = new SVG.Color( 255, 0, 128 ).cmyk().rgb()
-                               expect ( r ).toBeCloseTo( 255, 0 )
-                               expect ( g ).toBeCloseTo( 0, 0 )
-                               expect ( b ).toBeCloseTo( 128, 0 )
-                       })
-
-                       it('handles black', () => {
-                               let color = new SVG.Color(0, 0, 0).cmyk().rgb()
-                               expect( color.r ).toBeCloseTo(0, 0)
-                               expect( color.g ).toBeCloseTo(0, 0)
-                               expect( color.b ).toBeCloseTo(0, 0)
-                               expect( color.toHex() ).toBe('#000000')
-                       })
-
-                       it('handles white', () => {
-                               let color = new SVG.Color(255, 255, 255).cmyk().rgb()
-                               expect( color.r ).toBeCloseTo(255, 0)
-                               expect( color.g ).toBeCloseTo(255, 0)
-                               expect( color.b ).toBeCloseTo(255, 0)
-                               expect( color.toHex() ).toBe('#ffffff')
-                       })
-
-               })
-
-       })
-
-})
diff --git a/spec/spec/easing.js b/spec/spec/easing.js
deleted file mode 100644 (file)
index 04690ac..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-describe('SVG.easing', function() {
-  var easedValues = {
-    '-':0.5,
-    '<>':0.5,
-    '>':0.7071,
-    '<':0.2929,
-  }
-
-  ;['-', '<>', '<', '>'].forEach(function(el) {
-    describe(el, function() {
-      it('is 0 at 0', function() {
-        expect(SVG.easing[el](0)).toBe(0)
-      })
-      it('is 1 at 1', function() {
-        expect(Math.round(SVG.easing[el](1)*1000)/1000).toBe(1) // we need to round cause for some reason at some point 1==0.999999999
-      })
-      it('is eased at 0.5', function() {
-        expect(SVG.easing[el](0.5)).toBeCloseTo(easedValues[el])
-      })
-    })
-  })
-})
diff --git a/spec/spec/modules/optional/transform.js b/spec/spec/modules/optional/transform.js
new file mode 100644 (file)
index 0000000..db59784
--- /dev/null
@@ -0,0 +1,141 @@
+/* globals describe, expect, it, spyOn, container */
+
+import { Rect, Matrix, SVG } from '../../../../src/main.js'
+
+describe('transform.js', () => {
+  describe('untransform()', () => {
+    it('returns itself', () => {
+      const rect = new Rect()
+      expect(rect.untransform()).toBe(rect)
+    })
+
+    it('deletes the transform attribute', () => {
+      const rect = new Rect()
+      expect(rect.untransform().attr('transform')).toBe(undefined)
+    })
+  })
+
+  describe('matrixify()', () => {
+    it('reduces all transformations of the transform list into one matrix - 1', () => {
+      const rect = new Rect().attr('transform', 'matrix(1, 0, 1, 1, 0, 1)')
+      expect(rect.matrixify()).toEqual(new Matrix(1, 0, 1, 1, 0, 1))
+    })
+
+    it('reduces all transformations of the transform list into one matrix - 2', () => {
+      const rect = new Rect().attr('transform', 'translate(10, 20) rotate(45)')
+      expect(rect.matrixify()).toEqual(new Matrix().rotate(45).translate(10, 20))
+    })
+
+    it('reduces all transformations of the transform list into one matrix - 3', () => {
+      const rect = new Rect().attr('transform', 'translate(10, 20) rotate(45) skew(1,2) skewX(10) skewY(20)')
+      expect(rect.matrixify()).toEqual(new Matrix().skewY(20).skewX(10).skew(1, 2).rotate(45).translate(10, 20))
+    })
+  })
+
+  describe('toParent()', () => {
+    it('returns itself', () => {
+      const canvas = SVG().addTo(container)
+      const g = canvas.group()
+      const rect = g.rect(100, 100)
+      expect(rect.toParent(canvas)).toBe(rect)
+    })
+
+    it('does nothing if the parent is the the current element', () => {
+      const canvas = SVG().addTo(container)
+      const g = canvas.group()
+      const parent = g.parent()
+      const position = g.position()
+      g.toParent(g)
+      expect(g.parent()).toBe(parent)
+      expect(g.position()).toBe(position)
+    })
+
+    it('moves an element to a different container without changing its visual representation - 1', () => {
+      const canvas = SVG().addTo(container)
+      const g = canvas.group().matrix(1, 0, 1, 1, 0, 1)
+      const rect = g.rect(100, 100)
+      rect.toParent(canvas)
+      expect(rect.matrix()).toEqual(new Matrix(1, 0, 1, 1, 0, 1))
+      expect(rect.parent()).toBe(canvas)
+    })
+
+    it('moves an element to a different container without changing its visual representation - 2', () => {
+      const canvas = SVG().addTo(container)
+      const g = canvas.group().translate(10, 20)
+      const rect = g.rect(100, 100)
+      const g2 = canvas.group().rotate(10)
+      rect.toParent(g2)
+      const actual = rect.matrix()
+      const expected = new Matrix().translate(10, 20).rotate(-10)
+
+      // funny enough the dom seems to shorten the floats and precision gets lost
+      ;[ ...'abcdef' ].forEach(prop => expect(actual[prop]).toBeCloseTo(expected[prop], 5))
+    })
+
+    it('inserts the element at the specified position', () => {
+      const canvas = SVG().addTo(container)
+      const g = canvas.group()
+      const rect = g.rect(100, 100)
+      canvas.rect(100, 100)
+      canvas.rect(100, 100)
+      expect(rect.toParent(canvas, 2).position()).toBe(2)
+    })
+  })
+
+  describe('toRoot()', () => {
+    it('calls toParent() with root node', () => {
+      const canvas = SVG().addTo(container)
+      const g = canvas.group().matrix(1, 0, 1, 1, 0, 1)
+      const rect = g.rect(100, 100)
+      const spy = spyOn(rect, 'toParent')
+      rect.toRoot(3)
+      expect(spy).toHaveBeenCalledWith(canvas, 3)
+    })
+  })
+
+  describe('transform()', () => {
+    it('acts as full getter with no argument', () => {
+      const rect = new Rect().attr('transform', 'translate(10, 20) rotate(45)')
+      const actual = rect.transform()
+      const expected = new Matrix().rotate(45).translate(10, 20).decompose()
+
+      expect(actual).toEqual(expected)
+    })
+
+    it('returns a single transformation value when string was passed', () => {
+      const rect = new Rect().attr('transform', 'translate(10, 20) rotate(45)')
+      expect(rect.transform('rotate')).toBe(45)
+      expect(rect.transform('translateX')).toBe(10)
+      expect(rect.transform('translateY')).toBe(20)
+    })
+
+    it('sets the transformation with an object', () => {
+      const rect = new Rect().transform({ rotate: 45, translate: [ 10, 20 ] })
+      expect(rect.transform('rotate')).toBe(45)
+      expect(rect.transform('translateX')).toBe(10)
+      expect(rect.transform('translateY')).toBe(20)
+    })
+
+    it('performs a relative transformation with flag=true', () => {
+      const rect = new Rect().transform({ rotate: 45, translate: [ 10, 20 ] }).transform({ rotate: 10 }, true)
+      expect(rect.transform('rotate')).toBeCloseTo(55, 5) // rounding errors
+      expect(rect.transform('translateX')).toBe(10)
+      expect(rect.transform('translateY')).toBe(20)
+    })
+
+    it('performs a relative transformation with flag=other matrix', () => {
+      const rect = new Rect().transform({ rotate: 45, translate: [ 10, 20 ] }).transform({ rotate: 10 }, new Matrix().rotate(30))
+      expect(rect.transform('rotate')).toBeCloseTo(40, 5) // rounding errors
+      expect(rect.transform('translateX')).toBe(0)
+      expect(rect.transform('translateY')).toBe(0)
+    })
+
+    it('performs a relative transformation with flag=other element', () => {
+      const referenceElement = new Rect().transform({ rotate: 30 })
+      const rect = new Rect().transform({ rotate: 45, translate: [ 10, 20 ] }).transform({ rotate: 10 }, referenceElement)
+      expect(rect.transform('rotate')).toBeCloseTo(40, 5) // rounding errors
+      expect(rect.transform('translateX')).toBe(0)
+      expect(rect.transform('translateY')).toBe(0)
+    })
+  })
+})
diff --git a/spec/spec/morphing.js b/spec/spec/morphing.js
deleted file mode 100644 (file)
index 505440f..0000000
+++ /dev/null
@@ -1,161 +0,0 @@
-describe('Morphing', function () {
-  describe('constructors', function () {
-
-    it('SVG.Morphable with Number', function () {
-      var morpher = new SVG.Morphable().from(10).to(5)
-
-      expect(morpher instanceof SVG.Morphable).toBe(true)
-      expect(morpher.type()).toBe(SVG.Number)
-      expect(morpher.at(0.5) instanceof SVG.Number).toBe(true)
-      expect(morpher.at(0.5).valueOf()).toBe(7.5)
-    })
-
-    it('SVG.Morphable with String', function () {
-      var morpher = new SVG.Morphable().from('foo').to('bar')
-
-      expect(morpher instanceof SVG.Morphable).toBe(true)
-      expect(morpher.type()).toBe(SVG.NonMorphable)
-      expect(morpher.at(0.5) instanceof SVG.NonMorphable).toBe(true)
-      expect(morpher.at(0.5).valueOf()).toBe('foo')
-      expect(morpher.at(1).valueOf()).toBe('bar')
-    })
-
-    it('SVG.Morphable with Object', function () {
-      var morpher = new SVG.Morphable().from({a:5, b: 10}).to({a: 10, b: 20})
-
-      expect(morpher instanceof SVG.Morphable).toBe(true)
-      expect(morpher.type()).toBe(SVG.ObjectBag)
-      expect(morpher.at(0.5) instanceof Object).toBe(true)
-      expect(morpher.at(0.5).valueOf()).toEqual(jasmine.objectContaining({a: 7.5, b: 15}))
-    })
-
-    it('Creates a morphable out of an SVG.Number', function () {
-      var morpher = new SVG.Number(5).to(10)
-
-      expect(morpher instanceof SVG.Morphable).toBe(true)
-      expect(morpher.type()).toBe(SVG.Number)
-      expect(morpher.at(0.5) instanceof SVG.Number).toBe(true)
-      expect(morpher.at(0.5).valueOf()).toBe(7.5)
-    })
-
-    it('Creates a morphable out of an SVG.Color', function () {
-      var morpher = new SVG.Color('#fff').to('#000')
-
-      expect(morpher instanceof SVG.Morphable).toBe(true)
-      expect(morpher.type()).toBe(SVG.Color)
-      expect(morpher.at(0.5) instanceof SVG.Color).toBe(true)
-      expect(morpher.at(0.5).toHex()).toBe('#808080')
-    })
-
-    it('Creates a morphable out of an SVG.Box', function () {
-      var morpher = new SVG.Box(1, 2, 3, 4).to([5, 6, 7, 8])
-
-      expect(morpher instanceof SVG.Morphable).toBe(true)
-      expect(morpher.type()).toBe(SVG.Box)
-      expect(morpher.at(0.5) instanceof SVG.Box).toBe(true)
-      expect(morpher.at(0.5)).toEqual(jasmine.objectContaining({x: 3, y: 4, width: 5, height: 6}))
-    })
-
-    it('Creates a morphable out of an SVG.Matrix', function () {
-      var morpher = new SVG.Matrix(1, 2, 3, 4, 5, 6).to([3, 4, 5, 6, 7, 8])
-
-      expect(morpher instanceof SVG.Morphable).toBe(true)
-      expect(morpher.type()).toBe(SVG.Matrix)
-      expect(morpher.at(0.5) instanceof SVG.Matrix).toBe(true)
-      expect(morpher.at(0.5)).toEqual(jasmine.objectContaining(new SVG.Matrix(2, 3, 4, 5, 6, 7)))
-    })
-
-    it('Creates a morphable out of an SVG.Array', function () {
-      var morpher = new SVG.Array([1,2,3,4,5,6]).to([3,4,5,6,7,8])
-
-      expect(morpher instanceof SVG.Morphable).toBe(true)
-      expect(morpher.type()).toBe(SVG.Array)
-      expect(morpher.at(0.5) instanceof SVG.Array).toBe(true)
-      expect(morpher.at(0.5).toArray()).toEqual(jasmine.arrayContaining([2, 3, 4, 5, 6, 7]))
-    })
-
-    it('Creates a morphable out of an SVG.PointArray', function () {
-      var morpher = new SVG.PointArray([1, 2, 3, 4, 5, 6]).to([3, 4, 5, 6, 7, 8])
-
-      expect(morpher instanceof SVG.Morphable).toBe(true)
-      expect(morpher.type()).toBe(SVG.PointArray)
-      expect(morpher.at(0.5) instanceof SVG.PointArray).toBe(true)
-      expect(morpher.at(0.5).toArray()).toEqual(jasmine.arrayContaining([2, 3, 4, 5, 6, 7]))
-    })
-
-    it('Creates a morphable out of an SVG.PathArray', function () {
-      var morpher = new SVG.PathArray(['M', 1, 2, 'L', 3, 4, 'L', 5, 6]).to(['M', 3, 4, 'L', 5, 6, 'L', 7, 8])
-
-      expect(morpher instanceof SVG.Morphable).toBe(true)
-      expect(morpher.type()).toBe(SVG.PathArray)
-      expect(morpher.at(0.5) instanceof SVG.PathArray).toBe(true)
-      expect(morpher.at(0.5).toArray()).toEqual(jasmine.arrayContaining(['M', 2, 3, 'L', 4, 5, 'L', 6, 7]))
-    })
-
-    it('Creates a morphable out of an SVG.NonMorphable', function () {
-      var morpher = new SVG.NonMorphable('foo').to('bar')
-
-      expect(morpher instanceof SVG.Morphable).toBe(true)
-      expect(morpher.type()).toBe(SVG.NonMorphable)
-      expect(morpher.at(0.5) instanceof SVG.NonMorphable).toBe(true)
-      expect(morpher.at(0.5).valueOf()).toBe('foo')
-      expect(morpher.at(1).valueOf()).toBe('bar')
-    })
-
-    it('Creates a morphable out of an SVG.TransformBag', function () {
-      var morpher = new SVG.TransformBag({rotate: 0, translateX: 0})
-        .to({rotate: 50, translateX: 20})
-
-      expect(morpher instanceof SVG.Morphable).toBe(true)
-      expect(morpher.type()).toBe(SVG.TransformBag)
-      expect(morpher.at(0.5) instanceof SVG.TransformBag).toBe(true)
-
-      expect(morpher.at(0.5)).toEqual(jasmine.objectContaining({rotate: 25, translateX: 10}))
-    })
-
-    it('Creates a morphable out of an SVG.ObjectBag', function () {
-      var morpher = new SVG.ObjectBag({a:5, b: 10}).to({a: 10, b: 20})
-
-      expect(morpher instanceof SVG.Morphable).toBe(true)
-      expect(morpher.type()).toBe(SVG.ObjectBag)
-      expect(morpher.at(0.5) instanceof Object).toBe(true)
-      expect(morpher.at(0.5).valueOf()).toEqual(jasmine.objectContaining({a: 7.5, b: 15}))
-    })
-  })
-
-  describe('from()', function () {
-    it('sets the type of the runner', function () {
-      var morpher = new SVG.Morphable().from(5)
-      expect(morpher.type()).toBe(SVG.Number)
-    })
-
-    it('sets the from attribute to an array representation of the morphable type', function () {
-      var morpher = new SVG.Morphable().from(5)
-      expect(morpher.from()).toEqual(jasmine.arrayContaining([5]))
-    })
-  })
-
-  describe('type()', function () {
-    it('sets the type of the runner', function () {
-      var morpher = new SVG.Morphable().type(SVG.Number)
-      expect(morpher._type).toBe(SVG.Number)
-    })
-
-    it('gets the type of the runner', function () {
-      var morpher = new SVG.Morphable().type(SVG.Number)
-      expect(morpher.type()).toBe(SVG.Number)
-    })
-  })
-
-  describe('to()', function () {
-    it('sets the type of the runner', function () {
-      var morpher = new SVG.Morphable().to(5)
-      expect(morpher.type()).toBe(SVG.Number)
-    })
-
-    it('sets the from attribute to an array representation of the morphable type', function () {
-      var morpher = new SVG.Morphable().to(5)
-      expect(morpher.to()).toEqual(jasmine.arrayContaining([5]))
-    })
-  })
-})
diff --git a/spec/spec/number.js b/spec/spec/number.js
deleted file mode 100644 (file)
index eb98fd5..0000000
+++ /dev/null
@@ -1,178 +0,0 @@
-describe('Number', function() {
-  var number
-
-  beforeEach(function() {
-    number = new SVG.Number
-  })
-
-  describe('new', function() {
-    it('is zero', function() {
-      expect(number.value).toBe(0)
-    })
-    it('has a blank unit', function() {
-      expect(number.unit).toBe('')
-    })
-    it('accepts the unit as a second argument', function() {
-      number = new SVG.Number(30, '%')
-      expect(number.value).toBe(30)
-      expect(number.unit).toBe('%')
-    })
-    it('parses a pixel value', function() {
-      number = new SVG.Number('20px')
-      expect(number.value).toBe(20)
-      expect(number.unit).toBe('px')
-    })
-    it('parses a percent value', function() {
-      number = new SVG.Number('99%')
-      expect(number.value).toBe(0.99)
-      expect(number.unit).toBe('%')
-    })
-    it('parses a seconds value', function() {
-      number = new SVG.Number('2s')
-      expect(number.value).toBe(2000)
-      expect(number.unit).toBe('s')
-    })
-    it('parses a negative percent value', function() {
-      number = new SVG.Number('-89%')
-      expect(number.value).toBe(-0.89)
-      expect(number.unit).toBe('%')
-    })
-    it('falls back to 0 if given value is NaN', function() {
-      number = new SVG.Number(NaN)
-      expect(number.value).toBe(0)
-    })
-    it('falls back to maximum value if given number is positive infinite', function() {
-      number = new SVG.Number(1.7976931348623157E+10308)
-      expect(number.value).toBe(3.4e+38)
-    })
-    it('falls back to minimum value if given number is negative infinite', function() {
-      number = new SVG.Number(-1.7976931348623157E+10308)
-      expect(number.value).toBe(-3.4e+38)
-    })
-  })
-
-  describe('toString()', function() {
-    it('converts the number to a string', function() {
-      expect(number.toString()).toBe('0')
-    })
-    it('appends the unit', function() {
-      number.value = 1.21
-      number.unit = 'px'
-      expect(number.toString()).toBe('1.21px')
-    })
-    it('converts percent values properly', function() {
-      number.value = 1.36
-      number.unit = '%'
-      expect(number.toString()).toBe('136%')
-    })
-    it('converts second values properly', function() {
-      number.value = 2500
-      number.unit = 's'
-      expect(number.toString()).toBe('2.5s')
-    })
-  })
-
-  describe('valueOf()', function() {
-    it('returns a numeric value for default units', function() {
-      expect(typeof number.valueOf()).toBe('number')
-      number = new SVG.Number('12')
-      expect(typeof number.valueOf()).toBe('number')
-      number = new SVG.Number(13)
-      expect(typeof number.valueOf()).toBe('number')
-    })
-    it('returns a numeric value for pixel units', function() {
-      number = new SVG.Number('10px')
-      expect(typeof number.valueOf()).toBe('number')
-    })
-    it('returns a numeric value for percent units', function() {
-      number = new SVG.Number('20%')
-      expect(typeof number.valueOf()).toBe('number')
-    })
-    it('converts to a primitive when multiplying', function() {
-      number.value = 80
-      expect(number * 4).toBe(320)
-    })
-  })
-
-  describe('plus()', function() {
-    it('returns a new instance', function() {
-      expect(number.plus(4.5)).not.toBe(number)
-      expect(number.plus(4.5) instanceof SVG.Number).toBeTruthy()
-    })
-    it('adds a given number', function() {
-      expect(number.plus(3.5).valueOf()).toBe(3.5)
-    })
-    it('adds a given percentage value', function() {
-      expect(number.plus('225%').valueOf()).toBe(2.25)
-    })
-    it('adds a given pixel value', function() {
-      expect(number.plus('83px').valueOf()).toBe(83)
-    })
-    it('use the unit of this number as the unit of the returned number by default', function (){
-      expect(new SVG.Number('12s').plus('3%').unit).toBe('s')
-    })
-    it('use the unit of the passed number as the unit of the returned number when this number as no unit', function() {
-      expect(number.plus('15%').unit).toBe('%')
-    })
-  })
-
-  describe('minus()', function() {
-    it('subtracts a given number', function() {
-      expect(number.minus(3.7).valueOf()).toBe(-3.7)
-    })
-    it('subtracts a given percentage value', function() {
-      expect(number.minus('223%').valueOf()).toBe(-2.23)
-    })
-    it('subtracts a given pixel value', function() {
-      expect(number.minus('85px').valueOf()).toBe(-85)
-    })
-    it('use the unit of this number as the unit of the returned number by default', function (){
-      expect(new SVG.Number('12s').minus('3%').unit).toBe('s')
-    })
-    it('use the unit of the passed number as the unit of the returned number when this number as no unit', function() {
-      expect(number.minus('15%').unit).toBe('%')
-    })
-  })
-
-  describe('times()', function() {
-    beforeEach(function() {
-      number = number.plus(4)
-    })
-    it('multiplies with a given number', function() {
-      expect(number.times(3).valueOf()).toBe(12)
-    })
-    it('multiplies with a given percentage value', function() {
-      expect(number.times('110%').valueOf()).toBe(4.4)
-    })
-    it('multiplies with a given pixel value', function() {
-      expect(number.times('85px').valueOf()).toBe(340)
-    })
-    it('use the unit of this number as the unit of the returned number by default', function (){
-      expect(new SVG.Number('12s').times('3%').unit).toBe('s')
-    })
-    it('use the unit of the passed number as the unit of the returned number when this number as no unit', function() {
-      expect(number.times('15%').unit).toBe('%')
-    })
-  })
-
-  describe('divide()', function() {
-    beforeEach(function() {
-      number = number.plus(90)
-    })
-    it('divides by a given number', function() {
-      expect(number.divide(3).valueOf()).toBe(30)
-    })
-    it('divides by a given percentage value', function() {
-      expect(number.divide('3000%').valueOf()).toBe(3)
-    })
-    it('divides by a given pixel value', function() {
-      expect(number.divide('45px').valueOf()).toBe(2)
-    })
-    it('use the unit of this number as the unit of the returned number by default', function (){
-      expect(new SVG.Number('12s').divide('3%').unit).toBe('s')
-    })
-    it('use the unit of the passed number as the unit of the returned number when this number as no unit', function() {
-      expect(number.divide('15%').unit).toBe('%')
-    })
-  })
-})
diff --git a/spec/spec/point.js b/spec/spec/point.js
deleted file mode 100644 (file)
index 8be944a..0000000
+++ /dev/null
@@ -1,74 +0,0 @@
-describe('Point', function() {
-  var point
-
-  describe('initialization', function() {
-
-    describe('without a source', function() {
-
-      beforeEach(function() {
-        point = new SVG.Point
-      })
-
-      it('creates a new point with default values', function() {
-        expect(point.x).toBe(0)
-        expect(point.y).toBe(0)
-      })
-
-    })
-
-    describe('with x and y given', function() {
-      it('creates a point with given values', function() {
-        var point = new SVG.Point(2,4)
-
-        expect(point.x).toBe(2)
-        expect(point.y).toBe(4)
-      })
-    })
-
-    describe('with only x given', function() {
-      it('sets the y value to 0', function() {
-        var point = new SVG.Point(7)
-
-        expect(point.x).toBe(7)
-        expect(point.y).toBe(0)
-      })
-    })
-
-    describe('with array given', function() {
-      it('creates a point from array', function() {
-        var point = new SVG.Point([2,4])
-
-        expect(point.x).toBe(2)
-        expect(point.y).toBe(4)
-      })
-    })
-
-    describe('with object given', function() {
-      it('creates a point from object', function() {
-        var point = new SVG.Point({x:2,y:4})
-
-        expect(point.x).toBe(2)
-        expect(point.y).toBe(4)
-      })
-    })
-
-    describe('with SVG.Point given', function() {
-      it('creates a point from SVG.Point', function() {
-        var point = new SVG.Point(new SVG.Point(2,4))
-
-        expect(point.x).toBe(2)
-        expect(point.y).toBe(4)
-      })
-    })
-  })
-
-  describe('clone()', function() {
-    it('returns cloned point', function() {
-      var point1 = new SVG.Point(1,1)
-        , point2 = point1.clone()
-
-      expect(point1).toEqual(point2)
-      expect(point1).not.toBe(point2)
-    })
-  })
-})
diff --git a/spec/spec/pointarray.js b/spec/spec/pointarray.js
deleted file mode 100644 (file)
index f50f981..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-describe('PointArray', function() {
-  const squareString = '0,0 1,0 1,1 0,1';
-  const square = new SVG.PointArray(squareString)
-
-  describe('toString()', function() {
-    it('round trips with string', () => {
-      expect(square.toString()).toEqual(squareString)
-    })
-  })
-
-  describe('transform()', function() {
-    it('translates correctly', () => {
-      const translation = new SVG.Matrix().translate(2,1)
-      const newSquare = square.transform(translation)
-      expect(newSquare.toString()).toEqual('2,1 3,1 3,2 2,2')
-    })
-
-    it('transforms like Point', () => {
-      const matrix = new SVG.Matrix(1, 2, 3, 4, 5, 6)
-      const newSquare = square.transform(matrix)
-      for (let i = 0; i < square.length; i++) {
-        const squarePoint = new SVG.Point(square[i])
-        const newSquarePoint = new SVG.Point(newSquare[i])
-        expect(squarePoint.transform(matrix)).toEqual(newSquarePoint)
-      }
-    })
-  })
-})
diff --git a/spec/spec/queue.js b/spec/spec/queue.js
deleted file mode 100644 (file)
index 531b900..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-
-describe ('SVG.Queue()', function () {
-
-  describe('first ()', function () {
-
-    it('returns null if no item in the queue', function () {
-      var queue = new SVG.Queue()
-      expect(queue.first()).toEqual(null)
-    })
-
-    it ('returns the first value in the queue', function () {
-      var queue = new SVG.Queue()
-      queue.push(1)
-      expect(queue.first()).toBe(1)
-      queue.push(2)
-      expect(queue.first()).toBe(1)
-    })
-  })
-
-  describe('last ()', function () {
-
-    it ('returns null if no item in the queue', function () {
-      var queue = new SVG.Queue()
-      expect(queue.last()).toEqual(null)
-    })
-
-    it ('returns the last value added', function () {
-      var queue = new SVG.Queue()
-      queue.push(1)
-      expect(queue.last()).toBe(1)
-      queue.push(2)
-      expect(queue.last()).toBe(2)
-    })
-  })
-
-  describe('push ()', function () {
-
-    it ('adds an element to the end of the queue', function () {
-      var queue = new SVG.Queue()
-      queue.push(1)
-      queue.push(2)
-      queue.push(3)
-
-      expect(queue.first()).toBe(1)
-      expect(queue.last()).toBe(3)
-    })
-  })
-
-  describe('remove ()', function () {
-    it('removes the given item from the queue', function () {
-      var queue = new SVG.Queue()
-      queue.push(1)
-      queue.push(2)
-      var item = queue.push(3)
-
-      queue.remove(item)
-
-      expect(queue.last()).toBe(2)
-      expect(queue.first()).toBe(1)
-    })
-  })
-
-  describe('shift ()', function () {
-    it('returns nothing if queue is empty', function () {
-      var queue = new SVG.Queue()
-      var val = queue.shift()
-      expect(val).toBeFalsy()
-    })
-
-    it('returns the first item of the queue and removes it', function () {
-      var queue = new SVG.Queue()
-      queue.push(1)
-      queue.push(2)
-      queue.push(3)
-
-      var val = queue.shift()
-
-      expect(queue.last()).toBe(3)
-      expect(queue.first()).toBe(2)
-
-      expect(val).toBe(1)
-    })
-  })
-})
diff --git a/spec/spec/runner.js b/spec/spec/runner.js
deleted file mode 100644 (file)
index 2cda176..0000000
+++ /dev/null
@@ -1,836 +0,0 @@
-describe('SVG.Runner', function () {
-
-  var initFn = jasmine.createSpy('initFn')
-  var runFn = jasmine.createSpy('runFn')
-
-  beforeEach(function () {
-    initFn.calls.reset()
-    runFn.calls.reset()
-  })
-
-  describe('sanitise()', function () {
-    it('can handle all form of input', function () {
-      var fn = SVG.Runner.sanitise
-
-      expect(fn(200, 200, 'now')).toEqual(jasmine.objectContaining({
-        duration: 200,
-        delay: 200,
-        when: 'now',
-        times: 1,
-        wait: 0,
-        swing: false
-      }))
-
-      expect(fn(200, 200)).toEqual(jasmine.objectContaining({
-        duration: 200,
-        delay: 200,
-        when: 'last',
-        times: 1,
-        wait: 0,
-        swing: false
-      }))
-
-      expect(fn(200)).toEqual(jasmine.objectContaining({
-        duration: 200,
-        delay: SVG.defaults.timeline.delay,
-        when: 'last',
-        times: 1,
-        wait: 0,
-        swing: false
-      }))
-
-      expect(fn(runFn)).toEqual(jasmine.objectContaining({
-        duration: runFn,
-        delay: SVG.defaults.timeline.delay,
-        when: 'last',
-        times: 1,
-        wait: 0,
-        swing: false
-      }))
-
-      expect(fn({delay: 200})).toEqual(jasmine.objectContaining({
-        duration: SVG.defaults.timeline.duration,
-        delay: 200,
-        when: 'last',
-        times: 1,
-        wait: 0,
-        swing: false
-      }))
-
-      expect(fn({times: 3, delay: 200, when: 'now', swing: true, wait: 200})).toEqual(jasmine.objectContaining({
-        duration: SVG.defaults.timeline.duration,
-        delay: 200,
-        when: 'now',
-        times: 3,
-        wait: 200,
-        swing: true
-      }))
-    })
-  })
-
-  describe('())', function () {
-    it('creates a runner with defaults', function () {
-      var runner = new SVG.Runner()
-      expect(runner instanceof SVG.Runner).toBe(true)
-      expect(runner._duration).toBe(SVG.defaults.timeline.duration)
-      expect(runner._stepper instanceof SVG.Ease).toBe(true)
-    })
-
-    it('creates a runner with duration set', function () {
-      var runner = new SVG.Runner(1000)
-      expect(runner instanceof SVG.Runner).toBe(true)
-      expect(runner._duration).toBe(1000)
-      expect(runner._stepper instanceof SVG.Ease).toBe(true)
-    })
-
-    it('creates a runner with controller set', function () {
-      var runner = new SVG.Runner(runFn)
-      expect(runner instanceof SVG.Runner).toBe(true)
-      expect(runner._duration).toBeFalsy()
-      expect(runner._stepper instanceof SVG.Controller).toBe(true)
-    })
-  })
-
-  describe('constructors', function () {
-    // FIXME: Not possible to spy like this in es6
-    // describe('animate()', function () {
-    //   it('creates a runner with the element set and schedules it on the timeline', function () {
-    //     var orginalRunner = SVG.Runner
-    //     spyOn(SVG, 'Runner').and.callFake(function() {
-    //       return new orginalRunner()
-    //     })
-    //
-    //     var element = SVG('<rect />')
-    //     var runner = element.animate()
-    //     expect(SVG.Runner).toHaveBeenCalled();
-    //     expect(runner instanceof SVG.Runner)
-    //     expect(runner.element()).toBe(element)
-    //     expect(runner.timeline()).toBe(element.timeline())
-    //   })
-    // })
-
-    describe('delay()', function () {
-      it('calls animate with correct parameters', function () {
-        var element = SVG('<rect />')
-
-        spyOn(element, 'animate')
-        element.delay(100, 'now')
-        expect(element.animate).toHaveBeenCalledWith(0, 100, 'now')
-      })
-    })
-  })
-
-  describe('queue()', function () {
-    it('adds another closure to the runner', function () {
-      var runner = new SVG.Runner()
-      runner.queue(initFn, runFn, true)
-
-      expect(runner._queue[0]).toEqual(jasmine.objectContaining({
-        initialiser: initFn,
-        initialised: false,
-        runner: runFn,
-        finished: false
-      }))
-    })
-  })
-
-
-  describe('step()', function () {
-
-    it('returns itself', function () {
-      var runner = new SVG.Runner()
-      expect(runner.step()).toBe(runner)
-    })
-
-    it('calls initFn once and runFn at every step', function() {
-      var runner = new SVG.Runner()
-      runner.queue(initFn, runFn, false)
-
-      runner.step()
-      expect(initFn).toHaveBeenCalled()
-      expect(runFn).toHaveBeenCalled()
-
-      runner.step()
-      expect(initFn.calls.count()).toBe(1)
-      expect(runFn.calls.count()).toBe(2)
-    })
-
-    it('calls initFn on every step if its declaritive', function() {
-      var runner = new SVG.Runner(new SVG.Controller())
-      runner.queue(initFn, runFn, true)
-
-      runner.step()
-      expect(initFn).toHaveBeenCalled()
-      expect(runFn).toHaveBeenCalled()
-
-      runner.step()
-      expect(initFn.calls.count()).toBe(2)
-      expect(runFn.calls.count()).toBe(2)
-    })
-
-    function getLoop(r) {
-      var loopDuration = r._duration + r._wait
-      var loopsDone = Math.floor(r._time / loopDuration)
-      return loopsDone
-    }
-
-    // step in time
-    it('steps forward a certain time', function () {
-      var spy = jasmine.createSpy('stepper')
-      var r = new SVG.Runner(1000).loop(10, false, 100)
-      r.queue(null, spy)
-
-      r.step(300) // should be 0.3s
-      expect(spy).toHaveBeenCalledWith(0.3)
-      expect(getLoop(r)).toBe(0)
-
-      r.step(300) // should be 0.6s
-      expect(spy).toHaveBeenCalledWith(0.6)
-      expect(getLoop(r)).toBe(0)
-
-      r.step(600) // should be 0.1s
-      expect(spy).toHaveBeenCalledWith(0.1)
-      expect(getLoop(r)).toBe(1)
-
-      r.step(-300) // should be 0.9s
-      expect(spy).toHaveBeenCalledWith(0.9)
-      expect(getLoop(r)).toBe(0)
-
-      r.step(2000) // should be 0.7s
-      expect(spy).toHaveBeenCalledWith(0.7)
-      expect(getLoop(r)).toBe(2)
-
-      r.step(-2000) // should be 0.9s
-      expect(spy).toHaveBeenCalledWith(0.9)
-      expect(getLoop(r)).toBe(0)
-    })
-
-    it('handles dts which are bigger than the animation time', function () {
-      var runner = new SVG.Runner(1000)
-      runner.queue(initFn, runFn, true)
-
-      runner.step(1100)
-      expect(initFn).toHaveBeenCalled()
-      expect(runFn).toHaveBeenCalledWith(1)
-    })
-
-
-    describe('looping', function () {
-      describe('without wait', function () {
-        describe('unreversed', function () {
-          describe('nonswinging', function () {
-            it('does behave correctly at the end of an even loop', function () {
-              var spy = jasmine.createSpy('stepper')
-              var runner = new SVG.Runner(1000).loop(6, false)
-              runner.queue(null, spy)
-
-              runner.step(5750)
-              expect(spy).toHaveBeenCalledWith(0.75)
-              runner.step(250)
-              expect(spy).toHaveBeenCalledWith(1)
-            })
-
-            it('does behave correctly at the end of an uneven loop', function () {
-              var spy = jasmine.createSpy('stepper')
-              var runner = new SVG.Runner(1000).loop(5, false)
-              runner.queue(null, spy)
-
-              runner.step(4750)
-              expect(spy).toHaveBeenCalledWith(0.75)
-              runner.step(250)
-              expect(spy).toHaveBeenCalledWith(1)
-            })
-          })
-
-          describe('swinging', function () {
-            it('does behave correctly at the end of an even loop', function () {
-              var spy = jasmine.createSpy('stepper')
-              var runner = new SVG.Runner(1000).loop(6, true)
-              runner.queue(null, spy)
-
-              runner.step(5750)
-              expect(spy).toHaveBeenCalledWith(0.25)
-              runner.step(250)
-              expect(spy).toHaveBeenCalledWith(0)
-            })
-
-            it('does behave correctly at the end of an uneven loop', function () {
-              var spy = jasmine.createSpy('stepper')
-              var runner = new SVG.Runner(1000).loop(5, true)
-              runner.queue(null, spy)
-
-              runner.step(4750)
-              expect(spy).toHaveBeenCalledWith(0.75)
-              runner.step(250)
-              expect(spy).toHaveBeenCalledWith(1)
-            })
-          })
-        })
-
-        describe('reversed', function () {
-          describe('nonswinging', function () {
-            it('does behave correctly at the end of an even loop', function () {
-              var spy = jasmine.createSpy('stepper')
-              var runner = new SVG.Runner(1000).loop(6, false).reverse()
-              runner.queue(null, spy)
-
-              runner.step(5750)
-              expect(spy).toHaveBeenCalledWith(0.25)
-              runner.step(250)
-              expect(spy).toHaveBeenCalledWith(0)
-            })
-
-            it('does behave correctly at the end of an uneven loop', function () {
-              var spy = jasmine.createSpy('stepper')
-              var runner = new SVG.Runner(1000).loop(5, false).reverse()
-              runner.queue(null, spy)
-
-              runner.step(4750)
-              expect(spy).toHaveBeenCalledWith(0.25)
-              runner.step(250)
-              expect(spy).toHaveBeenCalledWith(0)
-            })
-          })
-
-          describe('swinging', function () {
-            it('does behave correctly at the end of an even loop', function () {
-              var spy = jasmine.createSpy('stepper')
-              var runner = new SVG.Runner(1000).loop(6, true).reverse()
-              runner.queue(null, spy)
-
-              runner.step(5750)
-              expect(spy).toHaveBeenCalledWith(0.75)
-              runner.step(250)
-              expect(spy).toHaveBeenCalledWith(1)
-            })
-
-            it('does behave correctly at the end of an uneven loop', function () {
-              var spy = jasmine.createSpy('stepper')
-              var runner = new SVG.Runner(1000).loop(5, true).reverse()
-              runner.queue(null, spy)
-
-              runner.step(4750)
-              expect(spy).toHaveBeenCalledWith(0.25)
-              runner.step(250)
-              expect(spy).toHaveBeenCalledWith(0)
-            })
-          })
-        })
-      })
-
-
-      describe('with wait', function () {
-        describe('unreversed', function () {
-          describe('nonswinging', function () {
-            it('does behave correctly at the end of an even loop', function () {
-              var spy = jasmine.createSpy('stepper')
-              var runner = new SVG.Runner(1000).loop(6, false, 100)
-              runner.queue(null, spy)
-
-              runner.step(5450)
-              expect(spy).toHaveBeenCalledWith(1)
-              spy.calls.reset()
-
-              runner.step(800)
-              expect(spy).toHaveBeenCalledWith(0.75)
-              runner.step(250)
-              expect(spy).toHaveBeenCalledWith(1)
-            })
-
-            it('does behave correctly at the end of an uneven loop', function () {
-              var spy = jasmine.createSpy('stepper')
-              var runner = new SVG.Runner(1000).loop(5, false, 100)
-              runner.queue(null, spy)
-
-              runner.step(4350)
-              expect(spy).toHaveBeenCalledWith(1)
-              spy.calls.reset()
-
-              runner.step(800)
-              expect(spy).toHaveBeenCalledWith(0.75)
-              runner.step(250)
-              expect(spy).toHaveBeenCalledWith(1)
-            })
-          })
-
-          describe('swinging', function () {
-            it('does behave correctly at the end of an even loop', function () {
-              var spy = jasmine.createSpy('stepper')
-              var runner = new SVG.Runner(1000).loop(6, true, 100)
-              runner.queue(null, spy)
-
-              runner.step(5450)
-              expect(spy).toHaveBeenCalledWith(1)
-              spy.calls.reset()
-
-              runner.step(800)
-              expect(spy).toHaveBeenCalledWith(0.25)
-              runner.step(250)
-              expect(spy).toHaveBeenCalledWith(0)
-            })
-
-            it('does behave correctly at the end of an uneven loop', function () {
-              var spy = jasmine.createSpy('stepper')
-              var runner = new SVG.Runner(1000).loop(5, true, 100)
-              runner.queue(null, spy)
-
-              runner.step(4350)
-              expect(spy).toHaveBeenCalledWith(0)
-              spy.calls.reset()
-
-              runner.step(800)
-              expect(spy).toHaveBeenCalledWith(0.75)
-
-              runner.step(250)
-              expect(spy).toHaveBeenCalledWith(1)
-            })
-          })
-        })
-
-        describe('reversed', function () {
-          describe('nonswinging', function () {
-            it('does behave correctly at the end of an even loop', function () {
-              var spy = jasmine.createSpy('stepper')
-              var runner = new SVG.Runner(1000).loop(6, false, 100).reverse()
-              runner.queue(null, spy)
-
-              runner.step(5450)
-              expect(spy).toHaveBeenCalledWith(0)
-              spy.calls.reset()
-
-              runner.step(800)
-              expect(spy).toHaveBeenCalledWith(0.25)
-              runner.step(250)
-              expect(spy).toHaveBeenCalledWith(0)
-            })
-
-            it('does behave correctly at the end of an uneven loop', function () {
-              var spy = jasmine.createSpy('stepper')
-              var runner = new SVG.Runner(1000).loop(5, false, 100).reverse()
-              runner.queue(null, spy)
-
-              runner.step(4350)
-              expect(spy).toHaveBeenCalledWith(0)
-              spy.calls.reset()
-
-              runner.step(800)
-              expect(spy).toHaveBeenCalledWith(0.25)
-              runner.step(250)
-              expect(spy).toHaveBeenCalledWith(0)
-            })
-          })
-
-          describe('swinging', function () {
-            it('does behave correctly at the end of an even loop', function () {
-              var spy = jasmine.createSpy('stepper')
-              var runner = new SVG.Runner(1000).loop(6, true, 100).reverse()
-              runner.queue(null, spy)
-
-              runner.step(5450)
-              expect(spy).toHaveBeenCalledWith(0)
-              spy.calls.reset()
-
-              runner.step(800)
-              expect(spy).toHaveBeenCalledWith(0.75)
-              runner.step(250)
-              expect(spy).toHaveBeenCalledWith(1)
-            })
-
-            it('does behave correctly at the end of an uneven loop', function () {
-              var spy = jasmine.createSpy('stepper')
-              var runner = new SVG.Runner(1000).loop(5, true, 100).reverse()
-              runner.queue(null, spy)
-
-              runner.step(4350)
-              expect(spy).toHaveBeenCalledWith(1)
-              spy.calls.reset()
-
-              runner.step(800)
-              expect(spy).toHaveBeenCalledWith(0.25)
-              runner.step(250)
-              expect(spy).toHaveBeenCalledWith(0)
-            })
-          })
-        })
-      })
-    })
-
-
-  })
-
-
-  describe('active()', function () {
-    it('acts as a getter without parameters', function () {
-      var runner = new SVG.Runner()
-      expect(runner.active()).toBe(true)
-    })
-
-    it('disables the runner when false is passed', function () {
-      var runner = new SVG.Runner()
-      expect(runner.active(false)).toBe(runner)
-      expect(runner.active()).toBe(false)
-    })
-
-    it('enables the runner when true is passed', function () {
-      var runner = new SVG.Runner()
-      expect(runner.active(false)).toBe(runner)
-      expect(runner.active(true)).toBe(runner)
-      expect(runner.active()).toBe(true)
-    })
-  })
-
-  describe('duration()', function () {
-    it('return the full duration of the runner including all loops and waits', function () {
-      var runner = new SVG.Runner(800).loop(10, true, 200)
-      expect(runner.duration()).toBe(9800)
-    })
-  })
-
-  describe('loop()', function () {
-    it('makes this runner looping', function () {
-      var runner = new SVG.Runner(1000).loop(5)
-      expect(runner.duration()).toBe(5000)
-    })
-  })
-
-  describe('time()', function () {
-    it('returns itself', function () {
-      var runner = new SVG.Runner()
-      expect(runner.time(0)).toBe(runner)
-    })
-
-    it('acts as a getter with no parameter passed', function () {
-      var runner = new SVG.Runner()
-      expect(runner.time()).toBe(0)
-    })
-
-    it('reschedules the runner to a new time', function () {
-      var runner = new SVG.Runner()
-      runner.time(10)
-
-      expect(runner.time()).toBe(10)
-    })
-
-    it('calls step to reschedule', function () {
-      var runner = new SVG.Runner()
-      spyOn(runner, 'step')
-      runner.time(10)
-
-      expect(runner.step).toHaveBeenCalledWith(10)
-    })
-  })
-
-  describe('loops()', function () {
-    it('get the loops of a runner', function () {
-      var spy = jasmine.createSpy('stepper')
-      var runner = new SVG.Runner(1000).queue(null, spy)
-
-      runner.step(300)
-      expect(spy).toHaveBeenCalledWith(0.3)
-
-      expect(runner.loops()).toBe(0.3)
-    })
-    it('sets the loops of the runner', function () {
-      var spy = jasmine.createSpy('stepper')
-      var runner = new SVG.Runner(1000).queue(null, spy)
-
-      expect(runner.loops(0.5).loops()).toBe(0.5)
-      expect(spy).toHaveBeenCalledWith(0.5)
-
-      expect(runner.loops(0.1).loops()).toBe(0.1)
-      expect(spy).toHaveBeenCalledWith(0.1)
-
-      expect(runner.loops(1.5).loops()).toBe(1)
-      expect(spy).toHaveBeenCalledWith(1)
-    })
-    it('sets the loops of the runner in a loop', function () {
-      var spy = jasmine.createSpy('stepper')
-      var runner = new SVG.Runner(1000).loop(5, true, 500).queue(null, spy)
-
-      expect(runner.loops(1.3).loops()).toBe(1.3)
-      expect(spy).toHaveBeenCalledWith(0.7)
-
-      expect(runner.loops(0.3).loops()).toBe(0.3)
-    })
-  })
-
-  describe('progress()', function () {
-    it('gets the progress of a runner', function () {
-      var spy = jasmine.createSpy('stepper')
-      var runner = new SVG.Runner(1000).queue(null, spy)
-
-      runner.step(300)
-      expect(spy).toHaveBeenCalledWith(0.3)
-
-      expect(runner.progress()).toBe(0.3)
-    })
-
-    it('gets the progress of a runner when looping', function () {
-      var spy = jasmine.createSpy('stepper')
-      var runner = new SVG.Runner(800).queue(null, spy).loop(10, false, 200) // duration should be 9800
-
-      // middle of animation, in the middle of wait time
-      runner.step(4900)
-      expect(runner.progress()).toBe(0.5)
-      expect(spy).toHaveBeenCalledWith(1)
-
-      // start of next loop
-      runner.step(100)
-      expect(spy).toHaveBeenCalledWith(0)
-
-      // move 400 into current loop which is 0.5 progress
-      // the progress value is 5400 / 9800
-      runner.step(400)
-      expect(spy).toHaveBeenCalledWith(0.5)
-      expect(runner.progress()).toBe(5400 / 9800)
-    })
-
-    it('sets the progress of a runner', function () {
-      var spy = jasmine.createSpy('stepper')
-      var runner = new SVG.Runner(1000).queue(null, spy)
-
-      expect(runner.progress(0.5).progress()).toBe(0.5)
-      expect(spy).toHaveBeenCalledWith(0.5)
-    })
-
-    it('sets the progress of a runner when looping', function () {
-      var spy = jasmine.createSpy('stepper')
-      var runner = new SVG.Runner(800).queue(null, spy).loop(10, false, 200)
-
-      // progress 0.5 somewhere in the middle of wait time
-      expect(runner.progress(0.5).progress()).toBe(0.5)
-      expect(spy).toHaveBeenCalledWith(1)
-
-      // start of next loop
-      runner.step(100)
-      expect(spy).toHaveBeenCalledWith(0)
-
-      // should move 0.5 into the next loop
-      expect(runner.progress(5400 / 9800).progress()).toBe(5400 / 9800)
-      expect(spy.calls.mostRecent().args[0]).toBeCloseTo(0.5)
-    })
-  })
-
-  describe('position()', function () {
-
-    it('gets the position of a runner', function () {
-      var spy = jasmine.createSpy('stepper')
-      var runner = new SVG.Runner(1000).queue(null, spy)
-
-      runner.step(300)
-      expect(spy).toHaveBeenCalledWith(0.3)
-
-      expect(runner.position()).toBe(0.3)
-    })
-
-    it('gets the position of a runner when looping', function () {
-      var spy = jasmine.createSpy('stepper')
-      var runner = new SVG.Runner(1000).loop(5, true, 100).queue(null, spy)
-
-      runner.step(1200)
-      expect(spy).toHaveBeenCalledWith(0.9)
-
-      expect(runner.position()).toBe(0.9)
-    })
-
-    it('sets the position of a runner', function () {
-      var spy = jasmine.createSpy('stepper')
-      var runner = new SVG.Runner(1000).queue(null, spy)
-
-      expect(runner.position(0.5).position()).toBe(0.5)
-      expect(spy).toHaveBeenCalledWith(0.5)
-    })
-
-    it('sets the position of a runner in a loop', function () {
-      var spy = jasmine.createSpy('stepper')
-      var runner = new SVG.Runner(1000).loop(5, true, 100).queue(null, spy)
-
-      runner.step(1200)
-      expect(runner.position(0.4).position()).toBe(0.4)
-      expect(spy).toHaveBeenCalledWith(0.4)
-
-      expect(runner.position(0).position()).toBe(0)
-      expect(spy).toHaveBeenCalledWith(0)
-
-      expect(runner.position(1).position()).toBe(1)
-      expect(spy).toHaveBeenCalledWith(1)
-    })
-  })
-
-  describe('element()', function () {
-    it('returns the element bound to this runner if any', function () {
-      var runner1 = new SVG.Runner()
-      expect(runner1.element()).toBe(null)
-
-      var element = SVG('<rect />')
-      var runner2 = element.animate()
-      expect(runner2.element()).toBe(element)
-    })
-
-    it('sets an element to be bound to the runner', function () {
-      var runner = new SVG.Runner()
-      var element = SVG('<rect />')
-      expect(runner.element(element)).toBe(runner)
-      expect(runner.element()).toBe(element)
-    })
-  })
-
-  describe('timeline()', function () {
-    it('returns the timeline bound to this runner if any', function () {
-      var runner1 = new SVG.Runner()
-      expect(runner1.element()).toBe(null)
-
-      var element = SVG('<rect />')
-      var runner2 = element.animate()
-      expect(runner2.timeline()).toBe(element.timeline())
-    })
-
-    it('sets a timeline to be bound to the runner', function () {
-      var runner = new SVG.Runner()
-      var timeline = new SVG.Timeline()
-      expect(runner.timeline(timeline)).toBe(runner)
-      expect(runner.timeline()).toBe(timeline)
-    })
-  })
-
-  describe('schedule()', function () {
-    it('schedules the runner on a timeline', function () {
-      var runner = new SVG.Runner()
-      var timeline = new SVG.Timeline()
-      var spy = spyOn(timeline, 'schedule').and.callThrough()
-
-      expect(runner.schedule(timeline, 200, 'now')).toBe(runner)
-      expect(runner.timeline()).toBe(timeline)
-      expect(spy).toHaveBeenCalledWith(runner, 200, 'now')
-    })
-
-    it('schedules the runner on its own timeline', function () {
-      var runner = new SVG.Runner()
-      var timeline = new SVG.Timeline()
-      var spy = spyOn(timeline, 'schedule')
-      runner.timeline(timeline)
-
-      expect(runner.schedule(200, 'now')).toBe(runner)
-      expect(runner.timeline()).toBe(timeline)
-      expect(spy).toHaveBeenCalledWith(runner, 200, 'now')
-    })
-  })
-
-  describe('unschedule()', function () {
-    it('unschedules this runner from its timeline', function () {
-      var runner = new SVG.Runner()
-      var timeline = new SVG.Timeline()
-      var spy = spyOn(timeline, 'unschedule').and.callThrough()
-
-      expect(runner.schedule(timeline, 200, 'now')).toBe(runner)
-      expect(runner.unschedule()).toBe(runner)
-      expect(spy).toHaveBeenCalledWith(runner)
-      expect(runner.timeline()).toBe(null)
-    })
-  })
-
-
-  describe('animate()', function () {
-    it('creates a new runner scheduled after the first', function () {
-      var runner = new SVG.Runner(1000)
-      var timeline = new SVG.Timeline()
-
-      runner.schedule(timeline)
-
-      var runner2 = runner.animate(500, 1000)
-
-      var t = timeline.time()
-
-      expect(runner2.timeline()).toBe(timeline)
-      expect(runner2.time()).toBe(0)
-
-      expect(timeline.schedule()).toEqual(jasmine.arrayContaining([
-        jasmine.objectContaining({start: t, duration: 1000, end: t+1000, runner: runner}),
-        jasmine.objectContaining({start: t+2000, duration: 500, end: t+2500, runner: runner2})
-      ]))
-    })
-  })
-
-  describe('delay()', function () {
-    it('calls animate with delay parameters', function () {
-      var runner = new SVG.Runner(1000)
-      spyOn(runner, 'animate')
-
-      runner.delay(500)
-      expect(runner.animate).toHaveBeenCalledWith(0, 500)
-    })
-  })
-
-  describe('during()', function () {
-    it('returns itself', function () {
-      var runner = new SVG.Runner()
-      expect(runner.during(runFn)).toBe(runner)
-    })
-
-    it('calls queue passing only a function to call on every step', function () {
-      var runner = new SVG.Runner()
-      spyOn(runner, 'queue')
-      runner.during(runFn)
-
-      expect(runner.queue).toHaveBeenCalledWith(null, runFn)
-    })
-  })
-
-  // describe('after()', function () {
-  //   it('returns itself', function () {
-  //     var runner = new SVG.Runner()
-  //     expect(runner.after(runFn)).toBe(runner)
-  //   })
-  //
-  //   it('binds a function to the after event', function () {
-  //     var runner = new SVG.Runner()
-  //     spyOn(runner, 'on')
-  //     runner.after(runFn)
-  //
-  //     expect(runner.on).toHaveBeenCalledWith('finish', runFn)
-  //   })
-  // })
-  //
-  // describe('finish()', function () {
-  //   it('returns itself', function () {
-  //     var runner = new SVG.Runner()
-  //     expect(runner.finish()).toBe(runner)
-  //   })
-  //
-  //   it('calls step with Infinity as argument', function () {
-  //     var runner = new SVG.Runner()
-  //     spyOn(runner, 'step')
-  //     runner.finish()
-  //
-  //     expect(runner.step).toHaveBeenCalledWith(Infinity)
-  //   })
-  // })
-
-  describe('reverse()', function () {
-    it('returns itself', function () {
-      var runner = new SVG.Runner()
-      expect(runner.reverse()).toBe(runner)
-    })
-
-    it('reverses the runner', function () {
-      var spy = jasmine.createSpy('stepper')
-      var runner = new SVG.Runner(1000).reverse().queue(null, spy)
-      runner.step(750)
-      expect(spy).toHaveBeenCalledWith(0.25)
-    })
-  })
-
-  describe('ease()', function () {
-    it('returns itself', function () {
-      var runner = new SVG.Runner()
-      expect(runner.ease(function () {})).toBe(runner)
-    })
-
-    it('creates an easing Controller from the easing function', function () {
-      var runner = new SVG.Runner()
-      runner.ease(function () {})
-
-      expect(runner._stepper instanceof SVG.Ease).toBe(true)
-    })
-  })
-})
diff --git a/spec/spec/timeline.js b/spec/spec/timeline.js
deleted file mode 100644 (file)
index f28a85f..0000000
+++ /dev/null
@@ -1,112 +0,0 @@
-describe('timeline', function() {
-    describe('getEndTimeOfTimeline', function() {
-        it('returns 0 if no runners are scheduled', function() {
-            const timeline = new SVG.Timeline();
-            endTime = timeline.getEndTimeOfTimeline();
-            expect(endTime).toEqual(0);
-        })
-    })
-
-    describe('finish - issue #964', function() {
-        beforeEach(function() {
-            draw.clear()
-            draw.attr('viewBox', null)
-        })
-
-        it('places all elements at the right position - single runner', function() {
-            const timeline = new SVG.Timeline()
-
-            const rect = draw.rect(20,20)
-            rect.timeline(timeline)
-            rect.animate().move(100, 200)
-
-            timeline.finish()
-            expect(rect.x()).toEqual(100);
-            expect(rect.y()).toEqual(200);
-        })
-
-        it('places all elements at the right position - runner that finishes latest is in first position', function() {
-            const timeline = new SVG.Timeline()
-
-            const rect1 = draw.rect(10, 10)
-            rect1.timeline(timeline)
-            
-            const rect2 = draw.rect(10, 10)
-            rect2.timeline(timeline);
-
-            const rect3 = draw.rect(10, 10)
-            rect3.timeline(timeline);
-
-            rect1.animate(2000, 0, 'now').move(100, 200)
-            rect2.animate(1000, 0, 'now').move(100, 200)
-            rect3.animate(1000, 500, 'now').move(100, 200)
-
-            timeline.finish()
-
-            expect(rect1.x()).toEqual(100);
-            expect(rect1.y()).toEqual(200);
-
-            expect(rect2.x()).toEqual(100);
-            expect(rect2.y()).toEqual(200);
-
-            expect(rect3.x()).toEqual(100);
-            expect(rect3.y()).toEqual(200);
-        })
-
-        it('places all elements at the right position - runner that finishes latest is in middle position', function() {
-            const timeline = new SVG.Timeline()
-
-            const rect1 = draw.rect(10, 10)
-            rect1.timeline(timeline)
-            
-            const rect2 = draw.rect(10, 10)
-            rect2.timeline(timeline);
-
-            const rect3 = draw.rect(10, 10)
-            rect3.timeline(timeline);
-
-            rect2.animate(1000, 0, 'now').move(100, 200)
-            rect1.animate(2000, 0, 'now').move(100, 200)
-            rect3.animate(1000, 500, 'now').move(100, 200)
-
-            timeline.finish()
-
-            expect(rect1.x()).toEqual(100);
-            expect(rect1.y()).toEqual(200);
-
-            expect(rect2.x()).toEqual(100);
-            expect(rect2.y()).toEqual(200);
-
-            expect(rect3.x()).toEqual(100);
-            expect(rect3.y()).toEqual(200);
-        })
-
-        it('places all elements at the right position - runner that finishes latest is in last position', function() {
-            const timeline = new SVG.Timeline()
-
-            const rect1 = draw.rect(10, 10)
-            rect1.timeline(timeline)
-            
-            const rect2 = draw.rect(10, 10)
-            rect2.timeline(timeline);
-
-            const rect3 = draw.rect(10, 10)
-            rect3.timeline(timeline);
-
-            rect2.animate(1000, 0, 'now').move(100, 200)
-            rect3.animate(1000, 500, 'now').move(100, 200)
-            rect1.animate(2000, 0, 'now').move(100, 200)
-
-            timeline.finish()
-
-            expect(rect1.x()).toEqual(100);
-            expect(rect1.y()).toEqual(200);
-
-            expect(rect2.x()).toEqual(100);
-            expect(rect2.y()).toEqual(200);
-
-            expect(rect3.x()).toEqual(100);
-            expect(rect3.y()).toEqual(200);
-        })
-    })    
-})
\ No newline at end of file
diff --git a/spec/spec/types/Color.js b/spec/spec/types/Color.js
new file mode 100644 (file)
index 0000000..ede8863
--- /dev/null
@@ -0,0 +1,273 @@
+/* globals describe, expect, it, beforeEach */
+
+import { Color } from '../../../src/main.js'
+
+describe('Color.js', () => {
+  var color
+
+  beforeEach(() => {
+    color = new Color({ r: 0, g: 102, b: 255 })
+  })
+
+  describe('construct: constructs a color in different formats', () => {
+
+    it('constructs a color from an object in the correct color space', () => {
+
+      // Try in rgb
+      const color = new Color({ r: 255, g: 0, b: 128 })
+      expect(color.r).toBe(255)
+      expect(color.g).toBe(0)
+      expect(color.b).toBe(128)
+      expect(color.space).toBe('rgb')
+
+      // Try in cmyk
+      const color2 = new Color({ c: 20, y: 15, m: 10, k: 5 })
+      expect(color2.c).toBe(20)
+      expect(color2.m).toBe(10)
+      expect(color2.y).toBe(15)
+      expect(color2.k).toBe(5)
+      expect(color2.space).toBe('cmyk')
+    })
+
+    it('constructs a color from an array', () => {
+      const color = new Color([ 30, 24, 50 ])
+      expect(color.r).toBe(30)
+      expect(color.g).toBe(24)
+      expect(color.b).toBe(50)
+      expect(color.space).toBe('rgb')
+    })
+
+    it('constructs a color from an array with space in array', () => {
+      const color = new Color([ 50, 50, 5, 'lab' ])
+      expect(color.l).toBe(50)
+      expect(color.a).toBe(50)
+      expect(color.b).toBe(5)
+      expect(color.space).toBe('lab')
+    })
+
+    it('constructs a color from an array with space given', () => {
+      const color = new Color([ 50, 50, 5 ], 'lab')
+      expect(color.l).toBe(50)
+      expect(color.a).toBe(50)
+      expect(color.b).toBe(5)
+      expect(color.space).toBe('lab')
+    })
+
+    it('correclty parses an rgb string', () => {
+      const color = new Color('rgb(255,0,128)')
+      expect(color.r).toBe(255)
+      expect(color.g).toBe(0)
+      expect(color.b).toBe(128)
+    })
+
+    it('correclty parses a 3 digit hex string', () => {
+      color = new Color('#f06')
+      expect(color.r).toBe(255)
+      expect(color.g).toBe(0)
+      expect(color.b).toBe(102)
+    })
+
+    it('correclty parses a 6 digit hex string', () => {
+      color = new Color('#0066ff')
+      expect(color.r).toBe(0)
+      expect(color.g).toBe(102)
+      expect(color.b).toBe(255)
+    })
+
+  })
+
+  describe('input and output: Importing and exporting colors', () => {
+    describe('toHex()', () => {
+      it('returns a hex color', () => {
+        expect(color.toHex()).toBe('#0066ff')
+      })
+    })
+
+    describe('toRgb()', () => {
+      it('returns a rgb string color', () => {
+        expect(color.toRgb()).toBe('rgb(0,102,255)')
+      })
+    })
+  })
+
+  describe('color spaces: The color spaces supported by our library', () => {
+
+    describe('lab()', () => {
+      it('can convert rgb to lab', () => {
+        const color = new Color(255, 0, 128)
+        const lab = color.lab()
+        expect(lab.l).toBeCloseTo(54.88, 1)
+        expect(lab.a).toBeCloseTo(84.55, 1)
+        expect(lab.b).toBeCloseTo(4.065, 1)
+        expect(lab.space).toBe('lab')
+      })
+
+      it('can convert from lab to rgb', () => {
+        const lab = new Color(54.88, 84.55, 4.065, 'lab')
+        const rgb = lab.rgb()
+        expect(rgb.r).toBeCloseTo(255, 0)
+        expect(rgb.g).toBeCloseTo(0, 0)
+        expect(rgb.b).toBeCloseTo(128, 0)
+        expect(rgb.space).toBe('rgb')
+      })
+
+      it('is invertable', () => {
+        const { r, g, b } = new Color(255, 0, 128).lab().rgb()
+        expect(r).toBeCloseTo(255, 0)
+        expect(g).toBeCloseTo(0, 0)
+        expect(b).toBeCloseTo(128, 0)
+      })
+
+      it('handles black', () => {
+        const color = new Color(0, 0, 0).lab().rgb()
+        expect(color.r).toBeCloseTo(0, 0)
+        expect(color.g).toBeCloseTo(0, 0)
+        expect(color.b).toBeCloseTo(0, 0)
+        expect(color.toHex()).toBe('#000000')
+      })
+
+      it('handles white', () => {
+        const color = new Color(255, 255, 255).lab().rgb()
+        expect(color.r).toBeCloseTo(255, 0)
+        expect(color.g).toBeCloseTo(255, 0)
+        expect(color.b).toBeCloseTo(255, 0)
+        expect(color.toHex()).toBe('#ffffff')
+      })
+    })
+
+    describe('lch()', () => {
+      it('can convert rgb to lch', () => {
+        const color = new Color(255, 0, 128)
+        const lch = color.lch()
+        expect(lch.l).toBeCloseTo(54.88, 1)
+        expect(lch.c).toBeCloseTo(84.65, 1)
+        expect(lch.h).toBeCloseTo(2.75, 1)
+        expect(lch.space).toBe('lch')
+      })
+
+      it('can convert from lch to rgb', () => {
+        const lch = new Color(54.88, 84.65, 2.75, 'lch')
+        const rgb = lch.rgb()
+        expect(rgb.r).toBeCloseTo(255, 0)
+        expect(rgb.g).toBeCloseTo(0, 0)
+        expect(rgb.b).toBeCloseTo(128, 0)
+        expect(rgb.space).toBe('rgb')
+      })
+
+      it('is invertable', () => {
+        const { r, g, b } = new Color(255, 0, 128).lch().rgb()
+        expect(r).toBeCloseTo(255, 0)
+        expect(g).toBeCloseTo(0, 0)
+        expect(b).toBeCloseTo(128, 0)
+      })
+
+      it('handles black', () => {
+        const color = new Color(0, 0, 0).lch().rgb()
+        expect(color.r).toBeCloseTo(0, 0)
+        expect(color.g).toBeCloseTo(0, 0)
+        expect(color.b).toBeCloseTo(0, 0)
+        expect(color.toHex()).toBe('#000000')
+      })
+
+      it('handles white', () => {
+        const color = new Color(255, 255, 255).lch().rgb()
+        expect(color.r).toBeCloseTo(255, 0)
+        expect(color.g).toBeCloseTo(255, 0)
+        expect(color.b).toBeCloseTo(255, 0)
+        expect(color.toHex()).toBe('#ffffff')
+      })
+    })
+
+    describe('hsl()', () => {
+
+      it('can convert from rgb to hsl', () => {
+        const color = new Color(255, 0, 128)
+        const hsl = color.hsl()
+        expect(hsl.h).toBeCloseTo(329.88, 1)
+        expect(hsl.s).toBeCloseTo(100, 1)
+        expect(hsl.l).toBeCloseTo(50, 1)
+        expect(hsl.space).toBe('hsl')
+      })
+
+      it('can convert from hsl to rgb', () => {
+        const hsl = new Color(329.88, 100, 50, 'hsl')
+        const rgb = hsl.rgb()
+        expect(rgb.r).toBeCloseTo(255, 0)
+        expect(rgb.g).toBeCloseTo(0, 0)
+        expect(rgb.b).toBeCloseTo(128, 0)
+        expect(rgb.space).toBe('rgb')
+      })
+
+      it('is invertable', () => {
+        const { r, g, b } = new Color(255, 0, 128).hsl().rgb()
+        expect(r).toBeCloseTo(255, 0)
+        expect(g).toBeCloseTo(0, 0)
+        expect(b).toBeCloseTo(128, 0)
+      })
+
+      it('handles black', () => {
+        const color = new Color(0, 0, 0).hsl().rgb()
+        expect(color.r).toBeCloseTo(0, 0)
+        expect(color.g).toBeCloseTo(0, 0)
+        expect(color.b).toBeCloseTo(0, 0)
+        expect(color.toHex()).toBe('#000000')
+      })
+
+      it('handles white', () => {
+        const color = new Color(255, 255, 255).hsl().rgb()
+        expect(color.r).toBeCloseTo(255, 0)
+        expect(color.g).toBeCloseTo(255, 0)
+        expect(color.b).toBeCloseTo(255, 0)
+        expect(color.toHex()).toBe('#ffffff')
+      })
+    })
+
+    describe('cmyk()', () => {
+
+      it('can convert from rgb to cmyk', () => {
+        const color = new Color(255, 0, 128)
+        const cmyk = color.cmyk()
+        expect(cmyk.c).toBeCloseTo(0, 1)
+        expect(cmyk.m).toBeCloseTo(1, 1)
+        expect(cmyk.y).toBeCloseTo(0.49, 1)
+        expect(cmyk.k).toBeCloseTo(0, 1)
+        expect(cmyk.space).toBe('cmyk')
+      })
+
+      it('can convert from cmyk to rgb', () => {
+        const color = new Color(0, 1, 0.49, 0, 'cmyk')
+        const rgb = color.rgb()
+        expect(rgb.r).toBeCloseTo(255, -1)
+        expect(rgb.g).toBeCloseTo(0, -1)
+        expect(rgb.b).toBeCloseTo(128, -1)
+        expect(rgb.space).toBe('rgb')
+      })
+
+      it('is invertable', () => {
+        const { r, g, b } = new Color(255, 0, 128).cmyk().rgb()
+        expect(r).toBeCloseTo(255, 0)
+        expect(g).toBeCloseTo(0, 0)
+        expect(b).toBeCloseTo(128, 0)
+      })
+
+      it('handles black', () => {
+        const color = new Color(0, 0, 0).cmyk().rgb()
+        expect(color.r).toBeCloseTo(0, 0)
+        expect(color.g).toBeCloseTo(0, 0)
+        expect(color.b).toBeCloseTo(0, 0)
+        expect(color.toHex()).toBe('#000000')
+      })
+
+      it('handles white', () => {
+        const color = new Color(255, 255, 255).cmyk().rgb()
+        expect(color.r).toBeCloseTo(255, 0)
+        expect(color.g).toBeCloseTo(255, 0)
+        expect(color.b).toBeCloseTo(255, 0)
+        expect(color.toHex()).toBe('#ffffff')
+      })
+
+    })
+
+  })
+
+})
index cfc7f0272be427c25daefa4622898df758181988..912924ab7cf823a2f380e58c41c583c9c556f57d 100644 (file)
@@ -1,8 +1,6 @@
 /* globals describe, expect, it, spyOn, jasmine */
 
-import {
-  EventTarget
-} from '../../../src/main.js'
+import { EventTarget } from '../../../src/main.js'
 import { getWindow } from '../../../src/utils/window.js'
 
 const { any, objectContaining, createSpy } = jasmine
diff --git a/spec/spec/types/Matrix.js b/spec/spec/types/Matrix.js
new file mode 100644 (file)
index 0000000..33500fa
--- /dev/null
@@ -0,0 +1,381 @@
+/* globals describe, expect, it, jasmine */
+
+import { Matrix, Rect } from '../../../src/main.js'
+
+const { objectContaining } = jasmine
+
+describe('Matrix.js', () => {
+  const comp = { a: 2, b: 0, c: 0, d: 2, e: 100, f: 50 }
+
+  describe('initialization', () => {
+
+    it('creates a new matrix with default values', () => {
+      const matrix = new Matrix()
+      expect(matrix).toEqual(objectContaining(
+        { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 }
+      ))
+    })
+
+    it('parses the current transform matrix from an element', () => {
+      const rect = new Rect().transform(comp)
+      const matrix = new Matrix(rect)
+      expect(matrix).toEqual(objectContaining(comp))
+    })
+
+    it('parses a string value correctly', () => {
+      const matrix = new Matrix('2, 0, 0, 2, 100, 50')
+      expect(matrix).toEqual(objectContaining(comp))
+    })
+
+    it('parses an array correctly', () => {
+      const matrix = new Matrix([ 2, 0, 0, 2, 100, 50 ])
+      expect(matrix).toEqual(objectContaining(comp))
+    })
+
+    it('parses an object correctly', () => {
+      const matrix = new Matrix(comp)
+      expect(matrix).toEqual(objectContaining(comp))
+    })
+
+    it('parses a transform object correctly', () => {
+      const matrix = new Matrix({ scale: 2, translate: [ 100, 50 ] })
+      expect(matrix).toEqual(objectContaining(comp))
+    })
+
+    it('parses 6 arguments correctly', () => {
+      const matrix = new Matrix(2, 0, 0, 2, 100, 50)
+      expect(matrix).toEqual(objectContaining(comp))
+    })
+  })
+
+  describe('toString()', () => {
+    it('exports correctly to a string', () => {
+      expect(new Matrix().toString()).toBe('matrix(1,0,0,1,0,0)')
+    })
+  })
+
+  describe('compose()', () => {
+    it('composes a matrix to form the correct result', () => {
+      const composed = new Matrix().compose({
+        scaleX: 3, scaleY: 20, shear: 4, rotate: 50, translateX: 23, translateY: 52
+      })
+
+      const expected = new Matrix().scale(3, 20).shear(4).rotate(50).translate(23, 52)
+      expect(composed).toEqual(expected)
+    })
+  })
+
+  describe('decompose()', () => {
+    it('decomposes a matrix properly', () => {
+      var matrix = new Matrix().scale(3, 2.5).shear(4).rotate(30).translate(20, 30)
+      var decomposed = matrix.decompose()
+      expect(decomposed.scaleX).toBeCloseTo(3)
+      expect(decomposed.scaleY).toBeCloseTo(2.5)
+      expect(decomposed.shear).toBeCloseTo(4)
+      expect(decomposed.rotate).toBeCloseTo(30)
+      expect(decomposed.translateX).toBeCloseTo(20)
+      expect(decomposed.translateY).toBeCloseTo(30)
+    })
+
+    it('can be recomposed to the same matrix', () => {
+      var matrix = new Matrix().scale(3, 2.5).shear(4).rotate(30).translate(20, 30)
+      var decomposed = matrix.decompose()
+      var composed = new Matrix().compose(decomposed)
+      expect(matrix.a).toBeCloseTo(composed.a)
+      expect(matrix.b).toBeCloseTo(composed.b)
+      expect(matrix.c).toBeCloseTo(composed.c)
+      expect(matrix.d).toBeCloseTo(composed.d)
+      expect(matrix.e).toBeCloseTo(composed.e)
+      expect(matrix.f).toBeCloseTo(composed.f)
+    })
+  })
+
+  describe('clone()', () => {
+    it('returns a clone of the matrix', () => {
+      var matrix = new Matrix(2, 0, 0, 5, 0, 0)
+      var clone = matrix.clone()
+      expect(matrix).not.toBe(clone)
+      for (var i in 'abcdef') {
+        expect(matrix[i]).toEqual(clone[i])
+      }
+    })
+  })
+
+  describe('multiply()', () => {
+    it('multiplies two matrices', () => {
+      var matrix1 = new Matrix(1, 4, 2, 5, 3, 6)
+      var matrix2 = new Matrix(7, 8, 8, 7, 9, 6)
+      var matrix3 = matrix1.multiply(matrix2)
+
+      expect(matrix1.toString()).toBe('matrix(1,4,2,5,3,6)')
+      expect(matrix2.toString()).toBe('matrix(7,8,8,7,9,6)')
+      expect(matrix3.toString()).toBe('matrix(23,68,22,67,24,72)')
+    })
+
+    it('accepts matrices in any form', () => {
+      var matrix1 = new Matrix(1, 4, 2, 5, 3, 6)
+      var matrix2 = matrix1.multiply('7,8,8,7,9,6')
+
+      expect(matrix1.toString()).toBe('matrix(1,4,2,5,3,6)')
+      expect(matrix2.toString()).toBe('matrix(23,68,22,67,24,72)')
+    })
+  })
+
+  describe('inverse()', () => {
+    it('inverses matrix', () => {
+
+      var matrix1 = new Matrix(2, 0, 0, 5, 4, 3)
+      var matrix2 = matrix1.inverse()
+      var abcdef = [ 0.5, 0, 0, 0.2, -2, -0.6 ]
+
+      for (var i in 'abcdef') {
+        expect(matrix2['abcdef'[i]]).toBeCloseTo(abcdef[i])
+      }
+    })
+  })
+
+  describe('translate()', () => {
+    it('translates matrix by given x and y values', () => {
+      var matrix = new Matrix(1, 0, 0, 1, 4, 3).translate(10, 12.5)
+      expect(matrix.e).toBe(14)
+      expect(matrix.f).toBe(15.5)
+    })
+
+    it('does nothing if you give it no x or y value', () => {
+      var matrix = new Matrix(1, 2, 3, 4, 5, 6).translate()
+      expect(matrix.e).toBe(5)
+      expect(matrix.f).toBe(6)
+    })
+  })
+
+  describe('scale()', () => {
+    it('performs a uniformal scale with one value', () => {
+      var matrix = new Matrix(1, 0, 0, 1, 4, 3).scale(3)
+
+      expect(matrix.a).toBe(3)
+      expect(matrix.d).toBe(3)
+      expect(matrix.e).toBe(4 * 3)
+      expect(matrix.f).toBe(3 * 3)
+    })
+    it('performs a non-uniformal scale with two values', () => {
+      var matrix = new Matrix(1, 0, 0, 1, 4, 3).scale(2.5, 3.5)
+
+      expect(matrix.a).toBe(2.5)
+      expect(matrix.d).toBe(3.5)
+      expect(matrix.e).toBe(4 * 2.5)
+      expect(matrix.f).toBe(3 * 3.5)
+    })
+    it('performs a uniformal scale at a given center point with three values', () => {
+      var matrix = new Matrix(1, 3, 2, 3, 4, 3).scale(3, 2, 3)
+
+      expect(matrix.a).toBe(3)
+      expect(matrix.b).toBe(9)
+      expect(matrix.c).toBe(6)
+      expect(matrix.d).toBe(9)
+      expect(matrix.e).toBe(8)
+      expect(matrix.f).toBe(3)
+    })
+    it('performs a non-uniformal scale at a given center point with four values', () => {
+      var matrix = new Matrix(1, 3, 2, 3, 4, 3).scale(3, 2, 2, 3)
+
+      expect(matrix.a).toBe(3)
+      expect(matrix.b).toBe(6)
+      expect(matrix.c).toBe(6)
+      expect(matrix.d).toBe(6)
+      expect(matrix.e).toBe(8)
+      expect(matrix.f).toBe(3)
+    })
+  })
+
+  describe('rotate()', () => {
+    it('performs a rotation with one argument', () => {
+      var matrix = new Matrix(1, 3, 2, 3, 4, 3).rotate(30)
+
+      expect(matrix.a).toBeCloseTo(-0.6339746)
+      expect(matrix.b).toBeCloseTo(3.09807621)
+      expect(matrix.c).toBeCloseTo(0.23205081)
+      expect(matrix.d).toBeCloseTo(3.59807621)
+      expect(matrix.e).toBeCloseTo(1.96410162)
+      expect(matrix.f).toBeCloseTo(4.59807621)
+    })
+    it('performs a rotation around a given point with three arguments', () => {
+      var matrix = new Matrix(1, 3, 2, 3, 4, 3).rotate(30, 2, 3)
+
+      expect(matrix.a).toBeCloseTo(-0.633974596216)
+      expect(matrix.b).toBeCloseTo(3.09807621135)
+      expect(matrix.c).toBeCloseTo(0.232050807569)
+      expect(matrix.d).toBeCloseTo(3.59807621135)
+      expect(matrix.e).toBeCloseTo(3.73205080757)
+      expect(matrix.f).toBeCloseTo(4.0)
+    })
+  })
+
+  describe('flip()', () => {
+    describe('with x given', () => {
+      it('performs a flip over the horizontal axis with one argument', () => {
+        var matrix = new Matrix(1, 0, 0, 1, 4, 3).flip('x')
+
+        expect(matrix.a).toBe(-1)
+        expect(matrix.d).toBe(1)
+        expect(matrix.e).toBe(-4)
+        expect(matrix.f).toBe(3)
+      })
+      it('performs a flip over the horizontal axis over a given point with two arguments', () => {
+        var matrix = new Matrix(1, 0, 0, 1, 4, 3).flip('x', 150)
+
+        expect(matrix.a).toBe(-1)
+        expect(matrix.d).toBe(1)
+        expect(matrix.e).toBe(296)
+        expect(matrix.f).toBe(3)
+      })
+    })
+    describe('with y given', () => {
+      it('performs a flip over the vertical axis with one argument', () => {
+        var matrix = new Matrix(1, 0, 0, 1, 4, 3).flip('y')
+
+        expect(matrix.a).toBe(1)
+        expect(matrix.d).toBe(-1)
+        expect(matrix.e).toBe(4)
+        expect(matrix.f).toBe(-3)
+      })
+      it('performs a flip over the vertical axis over a given point with two arguments', () => {
+        var matrix = new Matrix(1, 0, 0, 1, 4, 3).flip('y', 100)
+
+        expect(matrix.a).toBe(1)
+        expect(matrix.d).toBe(-1)
+        expect(matrix.e).toBe(4)
+        expect(matrix.f).toBe(197)
+      })
+    })
+    describe('with no axis given', () => {
+      it('performs a flip over the horizontal and vertical axis with no argument', () => {
+        var matrix = new Matrix(1, 0, 0, 1, 4, 3).flip()
+
+        expect(matrix.a).toBe(-1)
+        expect(matrix.d).toBe(-1)
+        expect(matrix.e).toBe(-4)
+        expect(matrix.f).toBe(-3)
+      })
+      it('performs a flip over the horizontal and vertical axis over a given point with one argument that represent both coordinates', () => {
+        var matrix = new Matrix(1, 0, 0, 1, 4, 3).flip(100)
+
+        expect(matrix.a).toBe(-1)
+        expect(matrix.d).toBe(-1)
+        expect(matrix.e).toBe(196)
+        expect(matrix.f).toBe(197)
+      })
+      it('performs a flip over the horizontal and vertical axis over a given point with two arguments', () => {
+        var matrix = new Matrix(1, 0, 0, 1, 4, 3).flip(50, 100)
+
+        expect(matrix.a).toBe(-1)
+        expect(matrix.d).toBe(-1)
+        expect(matrix.e).toBe(96)
+        expect(matrix.f).toBe(197)
+      })
+    })
+  })
+
+  describe('skew()', () => {
+    it('performs a uniformal skew with one value', () => {
+      var matrix = new Matrix(1, 0, 0, 1, 4, 3).skew(30)
+
+      expect(matrix.a).toBe(1)
+      expect(matrix.b).toBeCloseTo(0.57735026919)
+      expect(matrix.c).toBeCloseTo(0.57735026919)
+      expect(matrix.d).toBe(1)
+      expect(matrix.e).toBeCloseTo(5.73205080757)
+      expect(matrix.f).toBeCloseTo(5.30940107676)
+    })
+
+    it('performs a non-uniformal skew with two values', () => {
+      var matrix = new Matrix(1, 0, 0, 1, 4, 3).skew(30, 20)
+
+      expect(matrix.a).toBe(1)
+      expect(matrix.b).toBeCloseTo(0.363970234266)
+      expect(matrix.c).toBeCloseTo(0.57735026919)
+      expect(matrix.d).toBe(1)
+      expect(matrix.e).toBeCloseTo(5.73205080757)
+      expect(matrix.f).toBeCloseTo(4.45588093706)
+    })
+
+    it('performs a uniformal skew at a given center point with three values', () => {
+      var matrix = new Matrix(1, 0, 0, 1, 4, 3).skew(30, 150, 100)
+
+      expect(matrix.a).toBe(1)
+      expect(matrix.b).toBeCloseTo(0.57735026919)
+      expect(matrix.c).toBeCloseTo(0.57735026919)
+      expect(matrix.d).toBe(1)
+      expect(matrix.e).toBeCloseTo(-52.0029761114)
+      expect(matrix.f).toBeCloseTo(-81.2931393017)
+    })
+
+    it('performs a non-uniformal skew at a given center point with four values', () => {
+      var matrix = new Matrix(1, 0, 0, 1, 4, 3).skew(30, 20, 150, 100)
+
+      expect(matrix.a).toBe(1.0)
+      expect(matrix.b).toBeCloseTo(0.363970234266)
+      expect(matrix.c).toBeCloseTo(0.57735026919)
+      expect(matrix.d).toBe(1.0)
+      expect(matrix.e).toBeCloseTo(-52.0029761114)
+      expect(matrix.f).toBeCloseTo(-50.1396542029)
+    })
+
+    it('can be chained', () => {
+      var matrix = new Matrix(1, 0, 0, 1, 4, 3).skew(20, 30).skew(30, 20)
+      expect(matrix.a).toBeCloseTo(1.33333333333)
+      expect(matrix.b).toBeCloseTo(0.941320503456)
+      expect(matrix.c).toBeCloseTo(0.941320503456)
+      expect(matrix.d).toBeCloseTo(1.13247433143)
+      expect(matrix.e).toBeCloseTo(8.1572948437)
+      expect(matrix.f).toBeCloseTo(7.16270500812)
+    })
+  })
+
+  describe('skewX', () => {
+    it('performs a skew along the x axis with one value', () => {
+      var matrix = new Matrix(1, 0, 0, 1, 4, 3).skewX(30)
+
+      expect(matrix.a).toBe(1)
+      expect(matrix.b).toBe(0)
+      expect(matrix.c).toBeCloseTo(0.57735026919)
+      expect(matrix.d).toBe(1)
+      expect(matrix.e).toBeCloseTo(5.73205080757)
+      expect(matrix.f).toBe(3)
+    })
+
+    it('performs a skew along the x axis at a given center point with three values', () => {
+      var matrix = new Matrix(1, 0, 0, 1, 4, 3).skewX(30, 150, 100)
+
+      expect(matrix.a).toBe(1)
+      expect(matrix.b).toBe(0)
+      expect(matrix.c).toBeCloseTo(0.57735026919)
+      expect(matrix.d).toBe(1)
+      expect(matrix.e).toBeCloseTo(-52.0029761114)
+      expect(matrix.f).toBe(3)
+    })
+  })
+
+  describe('skewY', () => {
+    it('performs a skew along the y axis with one value', () => {
+      var matrix = new Matrix(1, 0, 0, 1, 4, 3).skewY(30)
+
+      expect(matrix.a).toBe(1)
+      expect(matrix.b).toBeCloseTo(0.57735026919)
+      expect(matrix.c).toBe(0)
+      expect(matrix.d).toBe(1)
+      expect(matrix.e).toBe(4)
+      expect(matrix.f).toBeCloseTo(5.30940107676)
+    })
+
+    it('performs a skew along the y axis at a given center point with three values', () => {
+      var matrix = new Matrix(1, 0, 0, 1, 4, 3).skewY(30, 150, 100)
+
+      expect(matrix.a).toBe(1)
+      expect(matrix.b).toBeCloseTo(0.57735026919)
+      expect(matrix.c).toBe(0)
+      expect(matrix.d).toBe(1)
+      expect(matrix.e).toBe(4)
+      expect(matrix.f).toBeCloseTo(-81.2931393017)
+    })
+  })
+})
diff --git a/spec/spec/types/Number.js b/spec/spec/types/Number.js
new file mode 100644 (file)
index 0000000..d7ef2fd
--- /dev/null
@@ -0,0 +1,184 @@
+/* globals describe, expect, it, beforeEach, jasmine */
+
+import { Number as SVGNumber } from '../../../src/main.js'
+
+const { any } = jasmine
+
+describe('Number.js', () => {
+  var number
+
+  beforeEach(() => {
+    number = new SVGNumber()
+  })
+
+  describe('new', () => {
+    it('is zero', () => {
+      expect(number.value).toBe(0)
+    })
+    it('has a blank unit', () => {
+      expect(number.unit).toBe('')
+    })
+    it('accepts the unit as a second argument', () => {
+      number = new SVGNumber(30, '%')
+      expect(number.value).toBe(30)
+      expect(number.unit).toBe('%')
+    })
+    it('parses a pixel value', () => {
+      number = new SVGNumber('20px')
+      expect(number.value).toBe(20)
+      expect(number.unit).toBe('px')
+    })
+    it('parses a percent value', () => {
+      number = new SVGNumber('99%')
+      expect(number.value).toBe(0.99)
+      expect(number.unit).toBe('%')
+    })
+    it('parses a seconds value', () => {
+      number = new SVGNumber('2s')
+      expect(number.value).toBe(2000)
+      expect(number.unit).toBe('s')
+    })
+    it('parses a negative percent value', () => {
+      number = new SVGNumber('-89%')
+      expect(number.value).toBe(-0.89)
+      expect(number.unit).toBe('%')
+    })
+    it('falls back to 0 if given value is NaN', () => {
+      number = new SVGNumber(NaN)
+      expect(number.value).toBe(0)
+    })
+    it('falls back to maximum value if given number is positive infinite', () => {
+      number = new SVGNumber(1.7976931348623157E+10308)
+      expect(number.value).toBe(3.4e+38)
+    })
+    it('falls back to minimum value if given number is negative infinite', () => {
+      number = new SVGNumber(-1.7976931348623157E+10308)
+      expect(number.value).toBe(-3.4e+38)
+    })
+  })
+
+  describe('toString()', () => {
+    it('converts the number to a string', () => {
+      expect(number.toString()).toBe('0')
+    })
+    it('appends the unit', () => {
+      number.value = 1.21
+      number.unit = 'px'
+      expect(number.toString()).toBe('1.21px')
+    })
+    it('converts percent values properly', () => {
+      number.value = 1.36
+      number.unit = '%'
+      expect(number.toString()).toBe('136%')
+    })
+    it('converts second values properly', () => {
+      number.value = 2500
+      number.unit = 's'
+      expect(number.toString()).toBe('2.5s')
+    })
+  })
+
+  describe('valueOf()', () => {
+    it('returns a numeric value for default units', () => {
+      expect(typeof number.valueOf()).toBe('number')
+      number = new SVGNumber('12')
+      expect(typeof number.valueOf()).toBe('number')
+      number = new SVGNumber(13)
+      expect(typeof number.valueOf()).toBe('number')
+    })
+    it('returns a numeric value for pixel units', () => {
+      number = new SVGNumber('10px')
+      expect(typeof number.valueOf()).toBe('number')
+    })
+    it('returns a numeric value for percent units', () => {
+      number = new SVGNumber('20%')
+      expect(typeof number.valueOf()).toBe('number')
+    })
+    it('converts to a primitive when multiplying', () => {
+      number.value = 80
+      expect(number * 4).toBe(320)
+    })
+  })
+
+  describe('plus()', () => {
+    it('returns a new instance', () => {
+      expect(number.plus(4.5)).not.toBe(number)
+      expect(number.plus(4.5)).toEqual(any(SVGNumber))
+    })
+    it('adds a given number', () => {
+      expect(number.plus(3.5).valueOf()).toBe(3.5)
+    })
+    it('adds a given percentage value', () => {
+      expect(number.plus('225%').valueOf()).toBe(2.25)
+    })
+    it('adds a given pixel value', () => {
+      expect(number.plus('83px').valueOf()).toBe(83)
+    })
+    it('use the unit of this number as the unit of the returned number by default', () => {
+      expect(new SVGNumber('12s').plus('3%').unit).toBe('s')
+    })
+    it('use the unit of the passed number as the unit of the returned number when this number as no unit', () => {
+      expect(number.plus('15%').unit).toBe('%')
+    })
+  })
+
+  describe('minus()', () => {
+    it('subtracts a given number', () => {
+      expect(number.minus(3.7).valueOf()).toBe(-3.7)
+    })
+    it('subtracts a given percentage value', () => {
+      expect(number.minus('223%').valueOf()).toBe(-2.23)
+    })
+    it('subtracts a given pixel value', () => {
+      expect(number.minus('85px').valueOf()).toBe(-85)
+    })
+    it('use the unit of this number as the unit of the returned number by default', () => {
+      expect(new SVGNumber('12s').minus('3%').unit).toBe('s')
+    })
+    it('use the unit of the passed number as the unit of the returned number when this number as no unit', () => {
+      expect(number.minus('15%').unit).toBe('%')
+    })
+  })
+
+  describe('times()', () => {
+    beforeEach(() => {
+      number = number.plus(4)
+    })
+    it('multiplies with a given number', () => {
+      expect(number.times(3).valueOf()).toBe(12)
+    })
+    it('multiplies with a given percentage value', () => {
+      expect(number.times('110%').valueOf()).toBe(4.4)
+    })
+    it('multiplies with a given pixel value', () => {
+      expect(number.times('85px').valueOf()).toBe(340)
+    })
+    it('use the unit of this number as the unit of the returned number by default', () => {
+      expect(new SVGNumber('12s').times('3%').unit).toBe('s')
+    })
+    it('use the unit of the passed number as the unit of the returned number when this number as no unit', () => {
+      expect(number.times('15%').unit).toBe('%')
+    })
+  })
+
+  describe('divide()', () => {
+    beforeEach(() => {
+      number = number.plus(90)
+    })
+    it('divides by a given number', () => {
+      expect(number.divide(3).valueOf()).toBe(30)
+    })
+    it('divides by a given percentage value', () => {
+      expect(number.divide('3000%').valueOf()).toBe(3)
+    })
+    it('divides by a given pixel value', () => {
+      expect(number.divide('45px').valueOf()).toBe(2)
+    })
+    it('use the unit of this number as the unit of the returned number by default', () => {
+      expect(new SVGNumber('12s').divide('3%').unit).toBe('s')
+    })
+    it('use the unit of the passed number as the unit of the returned number when this number as no unit', () => {
+      expect(number.divide('15%').unit).toBe('%')
+    })
+  })
+})
diff --git a/spec/spec/types/Point.js b/spec/spec/types/Point.js
new file mode 100644 (file)
index 0000000..ee0bbd4
--- /dev/null
@@ -0,0 +1,78 @@
+/* globals describe, expect, it, beforeEach */
+
+import { Point } from '../../../src/main.js'
+
+describe('Point.js', () => {
+  var point
+
+  describe('initialization', () => {
+
+    describe('without a source', () => {
+
+      beforeEach(() => {
+        point = new Point()
+      })
+
+      it('creates a new point with default values', () => {
+        expect(point.x).toBe(0)
+        expect(point.y).toBe(0)
+      })
+
+    })
+
+    describe('with x and y given', () => {
+      it('creates a point with given values', () => {
+        var point = new Point(2, 4)
+
+        expect(point.x).toBe(2)
+        expect(point.y).toBe(4)
+      })
+    })
+
+    describe('with only x given', () => {
+      it('sets the y value to 0', () => {
+        var point = new Point(7)
+
+        expect(point.x).toBe(7)
+        expect(point.y).toBe(0)
+      })
+    })
+
+    describe('with array given', () => {
+      it('creates a point from array', () => {
+        var point = new Point([ 2, 4 ])
+
+        expect(point.x).toBe(2)
+        expect(point.y).toBe(4)
+      })
+    })
+
+    describe('with object given', () => {
+      it('creates a point from object', () => {
+        var point = new Point({ x: 2, y: 4 })
+
+        expect(point.x).toBe(2)
+        expect(point.y).toBe(4)
+      })
+    })
+
+    describe('with Point given', () => {
+      it('creates a point from Point', () => {
+        var point = new Point(new Point(2, 4))
+
+        expect(point.x).toBe(2)
+        expect(point.y).toBe(4)
+      })
+    })
+  })
+
+  describe('clone()', () => {
+    it('returns cloned point', () => {
+      var point1 = new Point(1, 1)
+      var point2 = point1.clone()
+
+      expect(point1).toEqual(point2)
+      expect(point1).not.toBe(point2)
+    })
+  })
+})
diff --git a/spec/spec/types/PointArray.js b/spec/spec/types/PointArray.js
new file mode 100644 (file)
index 0000000..da8675e
--- /dev/null
@@ -0,0 +1,32 @@
+/* globals describe, expect, it */
+
+import { PointArray, Matrix, Point } from '../../../src/main.js'
+
+describe('PointArray', () => {
+  const squareString = '0,0 1,0 1,1 0,1'
+  const square = new PointArray(squareString)
+
+  describe('toString()', () => {
+    it('round trips with string', () => {
+      expect(square.toString()).toEqual(squareString)
+    })
+  })
+
+  describe('transform()', () => {
+    it('translates correctly', () => {
+      const translation = new Matrix().translate(2, 1)
+      const newSquare = square.transform(translation)
+      expect(newSquare.toString()).toEqual('2,1 3,1 3,2 2,2')
+    })
+
+    it('transforms like Point', () => {
+      const matrix = new Matrix(1, 2, 3, 4, 5, 6)
+      const newSquare = square.transform(matrix)
+      for (let i = 0; i < square.length; i++) {
+        const squarePoint = new Point(square[i])
+        const newSquarePoint = new Point(newSquare[i])
+        expect(squarePoint.transform(matrix)).toEqual(newSquarePoint)
+      }
+    })
+  })
+})