diff --git a/test/unit/specs/directives/for_spec.js b/test/unit/specs/directives/for_spec.js
new file mode 100644
index 000000000..fbe72ef15
--- /dev/null
+++ b/test/unit/specs/directives/for_spec.js
@@ -0,0 +1,980 @@
+var _ = require('../../../../src/util')
+var Vue = require('../../../../src/vue')
+
+if (_.inBrowser) {
+ describe('v-for', function () {
+
+ var el
+ beforeEach(function () {
+ el = document.createElement('div')
+ spyOn(_, 'warn')
+ })
+
+ it('objects', function (done) {
+ var vm = new Vue({
+ el: el,
+ data: {
+ items: [{a: 1}, {a: 2}]
+ },
+ template: '
{{$index}} {{item.a}}
'
+ })
+ assertMutations(vm, el, done)
+ })
+
+ it('primitives', function (done) {
+ var vm = new Vue({
+ el: el,
+ data: {
+ items: [2, 1, 2]
+ },
+ template: '{{$index}} {{item}}
'
+ })
+ assertPrimitiveMutations(vm, el, done)
+ })
+
+ it('object of objects', function (done) {
+ var vm = new Vue({
+ el: el,
+ data: {
+ items: {
+ a: {a: 1},
+ b: {a: 2}
+ }
+ },
+ template: '{{$index}} {{$key}} {{item.a}}
'
+ })
+ assertObjectMutations(vm, el, done)
+ })
+
+ it('object of primitives', function (done) {
+ var vm = new Vue({
+ el: el,
+ data: {
+ items: {
+ a: 1,
+ b: 2
+ }
+ },
+ template: '{{$index}} {{$key}} {{item}}
'
+ })
+ assertObjectPrimitiveMutations(vm, el, done)
+ })
+
+ it('array of arrays', function () {
+ var vm = new Vue({
+ el: el,
+ data: {
+ items: [[1, 1], [2, 2], [3, 3]]
+ },
+ template: '{{$index}} {{item}}
'
+ })
+ var markup = vm.items.map(function (item, i) {
+ return '' + i + ' ' + item.toString() + '
'
+ }).join('')
+ expect(el.innerHTML).toBe(markup)
+ })
+
+ it('repeating object with filter', function () {
+ new Vue({
+ el: el,
+ data: {
+ items: {
+ a: { msg: 'aaa' },
+ b: { msg: 'bbb' }
+ }
+ },
+ template: '{{item.msg}}
'
+ })
+ expect(el.innerHTML).toBe('aaa
')
+ })
+
+ it('component', function (done) {
+ var vm = new Vue({
+ el: el,
+ data: {
+ items: [{a: 1}, {a: 2}]
+ },
+ template: '',
+ components: {
+ test: {
+ props: ['index', 'item'],
+ template: '{{index}} {{item.a}}
',
+ replace: true
+ }
+ }
+ })
+ assertMutations(vm, el, done)
+ })
+
+ it('v-component', function (done) {
+ var vm = new Vue({
+ el: el,
+ data: {
+ items: [{a: 1}, {a: 2}]
+ },
+ template: '',
+ components: {
+ test: {
+ props: ['index', 'item'],
+ template: '{{index}} {{item.a}}
',
+ replace: true
+ }
+ }
+ })
+ assertMutations(vm, el, done)
+ })
+
+ it('component with inline-template', function (done) {
+ var vm = new Vue({
+ el: el,
+ data: {
+ items: [{a: 1}, {a: 2}]
+ },
+ template:
+ '' +
+ '{{index}} {{item.a}}' +
+ '',
+ components: {
+ test: {
+ props: ['index', 'item']
+ }
+ }
+ })
+ assertMutations(vm, el, done)
+ })
+
+ it('component with primitive values', function (done) {
+ var vm = new Vue({
+ el: el,
+ data: {
+ items: [2, 1, 2]
+ },
+ template: '',
+ components: {
+ test: {
+ props: ['index', 'value'],
+ template: '{{index}} {{value}}
',
+ replace: true
+ }
+ }
+ })
+ assertPrimitiveMutations(vm, el, done)
+ })
+
+ it('component with object of objects', function (done) {
+ var vm = new Vue({
+ el: el,
+ data: {
+ items: {
+ a: {a: 1},
+ b: {a: 2}
+ }
+ },
+ template: '',
+ components: {
+ test: {
+ props: ['key', 'index', 'value'],
+ template: '{{index}} {{key}} {{value.a}}
',
+ replace: true
+ }
+ }
+ })
+ assertObjectMutations(vm, el, done)
+ })
+
+ it('nested loops', function () {
+ new Vue({
+ el: el,
+ data: {
+ items: [
+ { items: [{a: 1}, {a: 2}], a: 1 },
+ { items: [{a: 3}, {a: 4}], a: 2 }
+ ]
+ },
+ template: '' +
+ '
{{$index}} {{subItem.a}} {{$parent.$index}} {{item.a}}
' +
+ '
'
+ })
+ expect(el.innerHTML).toBe(
+ '' +
+ ''
+ )
+ })
+
+ it('nested loops on object', function () {
+ new Vue({
+ el: el,
+ data: {
+ listHash: {
+ listA: [{a: 1}, {a: 2}],
+ listB: [{a: 1}, {a: 2}]
+ }
+ },
+ template:
+ '' +
+ '{{$key}}' +
+ '
{{item.a}}
' +
+ '
'
+ })
+ function output (key) {
+ var key1 = key === 'listA' ? 'listB' : 'listA'
+ return '' +
+ ''
+ }
+ expect(el.innerHTML === output('listA') || el.innerHTML === output('listB')).toBeTruthy()
+ })
+
+ it('dynamic component type based on instance data', function () {
+ new Vue({
+ el: el,
+ template: '',
+ data: {
+ list: [
+ { type: 'a' },
+ { type: 'b' },
+ { type: 'c' }
+ ]
+ },
+ components: {
+ 'view-a': {
+ template: 'AAA'
+ },
+ 'view-b': {
+ template: 'BBB'
+ },
+ 'view-c': {
+ template: 'CCC'
+ }
+ }
+ })
+ expect(el.innerHTML).toBe('AAABBBCCC')
+ // primitive
+ el = document.createElement('div')
+ new Vue({
+ el: el,
+ template: '',
+ data: {
+ list: ['a', 'b', 'c']
+ },
+ components: {
+ 'view-a': {
+ template: 'AAA'
+ },
+ 'view-b': {
+ template: 'BBB'
+ },
+ 'view-c': {
+ template: 'CCC'
+ }
+ }
+ })
+ expect(el.innerHTML).toBe('AAABBBCCC')
+ })
+
+ // it('block repeat', function (done) {
+ // var vm = new Vue({
+ // el: el,
+ // template: '{{a}}
{{a + 1}}
',
+ // data: {
+ // list: [
+ // { a: 1 },
+ // { a: 2 },
+ // { a: 3 }
+ // ]
+ // }
+ // })
+ // assertMarkup()
+ // vm.list.reverse()
+ // _.nextTick(function () {
+ // assertMarkup()
+ // vm.list.splice(1, 1)
+ // _.nextTick(function () {
+ // assertMarkup()
+ // vm.list.splice(1, 0, { a: 2 })
+ // _.nextTick(function () {
+ // assertMarkup()
+ // done()
+ // })
+ // })
+ // })
+
+ // function assertMarkup () {
+ // var markup = vm.list.map(function (item) {
+ // return '' + item.a + '
' + (item.a + 1) + '
'
+ // }).join('')
+ // expect(el.innerHTML).toBe(markup)
+ // }
+ // })
+
+ // it('block repeat with component', function (done) {
+ // var vm = new Vue({
+ // el: el,
+ // template: '',
+ // data: {
+ // list: [
+ // { a: 1 },
+ // { a: 2 },
+ // { a: 3 }
+ // ]
+ // },
+ // components: {
+ // test: {
+ // props: ['a'],
+ // template: '{{a}}'
+ // }
+ // }
+ // })
+ // assertMarkup()
+ // vm.list.reverse()
+ // _.nextTick(function () {
+ // assertMarkup()
+ // vm.list.splice(1, 1)
+ // _.nextTick(function () {
+ // assertMarkup()
+ // vm.list.splice(1, 0, { a: 2 })
+ // _.nextTick(function () {
+ // assertMarkup()
+ // done()
+ // })
+ // })
+ // })
+
+ // function assertMarkup () {
+ // var markup = vm.list.map(function (item) {
+ // return '' + item.a + ''
+ // }).join('')
+ // expect(el.innerHTML).toBe(markup)
+ // }
+ // })
+
+ // it('array filters', function (done) {
+ // var vm = new Vue({
+ // el: el,
+ // template: '{{id}}
',
+ // data: {
+ // filterKey: 'hi!',
+ // sortKey: 'id',
+ // list: [
+ // { id: 1, id2: 4, msg: 'hi!' },
+ // { id: 2, id2: 3, msg: 'na' },
+ // { id: 3, id2: 2, msg: 'hi!' },
+ // { id: 4, id2: 1, msg: 'na' }
+ // ]
+ // }
+ // })
+ // assertMarkup()
+
+ // go(
+ // function () {
+ // vm.filterKey = 'na'
+ // }, assertMarkup
+ // )
+ // .then(
+ // function () {
+ // vm.sortKey = 'id2'
+ // }, assertMarkup
+ // )
+ // .then(
+ // function () {
+ // vm.list[0].id2 = 0
+ // }, assertMarkup
+ // )
+ // .then(
+ // function () {
+ // vm.list.push({ id: 0, id2: 4, msg: 'na' })
+ // }, assertMarkup
+ // )
+ // .then(
+ // function () {
+ // vm.list = [
+ // { id: 33, id2: 4, msg: 'hi!' },
+ // { id: 44, id2: 3, msg: 'na' }
+ // ]
+ // }, assertMarkup
+ // )
+ // .run(done)
+
+ // function assertMarkup () {
+ // var markup = vm.list
+ // .filter(function (item) {
+ // return item.msg === vm.filterKey
+ // })
+ // .sort(function (a, b) {
+ // return a[vm.sortKey] > b[vm.sortKey] ? -1 : 1
+ // })
+ // .map(function (item) {
+ // return '' + item.id + '
'
+ // }).join('')
+ // expect(el.innerHTML).toBe(markup)
+ // }
+ // })
+
+ // it('orderBy supporting $key for object repeaters', function (done) {
+ // var vm = new Vue({
+ // el: el,
+ // template: '{{$value}}
',
+ // data: {
+ // sortKey: '$key',
+ // obj: {
+ // c: 1,
+ // a: 3,
+ // b: 2
+ // }
+ // }
+ // })
+ // expect(el.innerHTML).toBe('3
2
1
')
+ // vm.sortKey = '$value'
+ // _.nextTick(function () {
+ // expect(el.innerHTML).toBe('1
2
3
')
+ // done()
+ // })
+ // })
+
+ // it('orderBy supporting $value for primitive arrays', function () {
+ // new Vue({
+ // el: el,
+ // template: '{{$value}}
',
+ // data: {
+ // list: [3, 2, 1]
+ // }
+ // })
+ // expect(el.innerHTML).toBe('1
2
3
')
+ // })
+
+ // it('track by id', function (done) {
+
+ // assertTrackBy('', '{{msg}}', function () {
+ // assertTrackBy('', '{{item.msg}}', done)
+ // })
+
+ // function assertTrackBy (template, componentTemplate, next) {
+ // var vm = new Vue({
+ // el: el,
+ // template: template,
+ // data: {
+ // list: [
+ // { id: 1, msg: 'hi' },
+ // { id: 2, msg: 'ha' },
+ // { id: 3, msg: 'ho' }
+ // ]
+ // },
+ // components: {
+ // test: {
+ // template: componentTemplate
+ // }
+ // }
+ // })
+ // assertMarkup()
+ // var oldVms = vm.$children.slice()
+ // // swap the data with different objects, but with
+ // // the same ID!
+ // vm.list = [
+ // { id: 1, msg: 'wa' },
+ // { id: 2, msg: 'wo' }
+ // ]
+ // _.nextTick(function () {
+ // assertMarkup()
+ // // should reuse old vms!
+ // var i = 2
+ // while (i--) {
+ // expect(vm.$children[i]).toBe(oldVms[i])
+ // }
+ // next()
+ // })
+
+ // function assertMarkup () {
+ // var markup = vm.list.map(function (item) {
+ // return '' + item.msg + ''
+ // }).join('')
+ // expect(el.innerHTML).toBe(markup)
+ // }
+ // }
+ // })
+
+ // it('track by $index', function (done) {
+ // var vm = new Vue({
+ // el: el,
+ // data: {
+ // items: [{a: 1}, {a: 2}]
+ // },
+ // template: '{{$index}} {{a}}
'
+ // })
+
+ // assertMarkup()
+ // var el1 = el.children[0]
+ // var el2 = el.children[1]
+ // vm.items = [{a: 3}, {a: 2}, {a: 1}]
+ // _.nextTick(function () {
+ // assertMarkup()
+ // // should mutate the DOM in-place
+ // expect(el.children[0]).toBe(el1)
+ // expect(el.children[1]).toBe(el2)
+ // done()
+ // })
+
+ // function assertMarkup () {
+ // expect(el.innerHTML).toBe(vm.items.map(function (item, i) {
+ // return '' + i + ' ' + item.a + '
'
+ // }).join(''))
+ // }
+ // })
+
+ // it('warn duplicate objects', function () {
+ // var obj = {}
+ // new Vue({
+ // el: el,
+ // template: '',
+ // data: {
+ // items: [obj, obj]
+ // }
+ // })
+ // expect(hasWarned(_, 'Duplicate objects')).toBe(true)
+ // })
+
+ // it('warn duplicate trackby id', function () {
+ // new Vue({
+ // el: el,
+ // template: '',
+ // data: {
+ // items: [{id: 1}, {id: 1}]
+ // }
+ // })
+ // expect(hasWarned(_, 'Duplicate track-by key')).toBe(true)
+ // })
+
+ // it('warn v-if', function () {
+ // new Vue({
+ // el: el,
+ // template: '',
+ // data: {
+ // items: []
+ // }
+ // })
+ // expect(hasWarned(_, 'Don\'t use v-if')).toBe(true)
+ // })
+
+ // it('repeat number', function () {
+ // new Vue({
+ // el: el,
+ // template: '{{$index}} {{$value}}
'
+ // })
+ // expect(el.innerHTML).toBe('0 0
1 1
2 2
')
+ // })
+
+ // it('repeat string', function () {
+ // new Vue({
+ // el: el,
+ // template: '{{$index}} {{$value}}
'
+ // })
+ // expect(el.innerHTML).toBe('0 v
1 u
2 e
')
+ // })
+
+ // it('teardown', function () {
+ // var vm = new Vue({
+ // el: el,
+ // template: '',
+ // data: {
+ // items: [{a: 1}, {a: 2}]
+ // }
+ // })
+ // vm._directives[0].unbind()
+ // expect(vm.$children.length).toBe(0)
+ // })
+
+ // it('with transition', function (done) {
+ // document.body.appendChild(el)
+ // var vm = new Vue({
+ // el: el,
+ // template: '{{a}}
',
+ // data: {
+ // items: [{a: 1}, {a: 2}, {a: 3}]
+ // },
+ // transitions: {
+ // test: {
+ // leave: function (el, done) {
+ // setTimeout(done, 0)
+ // }
+ // }
+ // }
+ // })
+ // vm.items.splice(1, 1, {a: 4})
+ // setTimeout(function () {
+ // expect(el.innerHTML).toBe(
+ // '1
' +
+ // '4
' +
+ // '3
'
+ // )
+ // document.body.removeChild(el)
+ // done()
+ // }, 100)
+ // })
+
+ // it('sync $value/alias changes back to original array/object', function (done) {
+ // var vm = new Vue({
+ // el: el,
+ // template:
+ // '{{$value}}
' +
+ // '{{$value}}
' +
+ // '{{val}}
',
+ // data: {
+ // items: ['a', true],
+ // obj: { foo: 'a', bar: 'b' },
+ // vals: [1, null]
+ // }
+ // })
+ // vm.$children[0].$value = 'c'
+ // vm.$children[1].$value = 'd'
+ // var key = vm.$children[2].$key
+ // vm.$children[2].$value = 'e'
+ // vm.$children[4].val = 3
+ // vm.$children[5].val = 4
+ // _.nextTick(function () {
+ // expect(vm.items[0]).toBe('c')
+ // expect(vm.items[1]).toBe('d')
+ // expect(vm.obj[key]).toBe('e')
+ // expect(vm.vals[0]).toBe(3)
+ // expect(vm.vals[1]).toBe(4)
+ // done()
+ // })
+ // })
+
+ // it('warn $value sync with filters', function (done) {
+ // var vm = new Vue({
+ // el: el,
+ // template: '',
+ // data: {
+ // items: ['a', 'b']
+ // }
+ // })
+ // vm.$children[0].$value = 'c'
+ // _.nextTick(function () {
+ // expect(hasWarned(_, 'use an Array of Objects instead')).toBe(true)
+ // done()
+ // })
+ // })
+
+ // it('nested track by', function (done) {
+ // assertTrackBy('', function () {
+ // assertTrackBy('', done)
+ // })
+
+ // function assertTrackBy (template, next) {
+ // var vm = new Vue({
+ // el: el,
+ // data: {
+ // list: [
+ // { id: 1, msg: 'hi', list: [
+ // { id: 1, msg: 'hi foo' }
+ // ] },
+ // { id: 2, msg: 'ha', list: [] },
+ // { id: 3, msg: 'ho', list: [] }
+ // ]
+ // },
+ // template: template
+ // })
+ // assertMarkup()
+
+ // var oldVms = vm.$children.slice()
+
+ // vm.list = [
+ // { id: 1, msg: 'wa', list: [
+ // { id: 1, msg: 'hi foo' },
+ // { id: 2, msg: 'hi bar' }
+ // ] },
+ // { id: 2, msg: 'wo', list: [] }
+ // ]
+
+ // _.nextTick(function () {
+ // assertMarkup()
+ // // should reuse old vms!
+ // var i = 2
+ // while (i--) {
+ // expect(vm.$children[i]).toBe(oldVms[i])
+ // }
+ // expect(vm.$children[0].$children[0]).toBe(oldVms[0].$children[0])
+ // next()
+ // })
+
+ // function assertMarkup () {
+ // var markup = vm.list.map(function (item) {
+ // var sublist = item.list.map(function (item) {
+ // return '' + item.msg + '
'
+ // }).join('')
+ // return '' + item.msg + sublist + '
'
+ // }).join('')
+ // expect(el.innerHTML).toBe(markup)
+ // }
+ // }
+ // })
+
+ // it('switch between object-converted & array mode', function (done) {
+ // var obj = {
+ // a: { msg: 'AA' },
+ // b: { msg: 'BB' }
+ // }
+ // var arr = [obj.b, obj.a]
+ // var vm = new Vue({
+ // el: el,
+ // template: '{{msg}}
',
+ // data: {
+ // obj: obj
+ // }
+ // })
+ // expect(el.innerHTML).toBe(Object.keys(obj).map(function (key) {
+ // return '' + obj[key].msg + '
'
+ // }).join(''))
+ // vm.obj = arr
+ // _.nextTick(function () {
+ // expect(el.innerHTML).toBe('BB
AA
')
+ // // make sure it cleared the cache
+ // expect(vm._directives[0].cache.a).toBeNull()
+ // expect(vm._directives[0].cache.b).toBeNull()
+ // done()
+ // })
+ // })
+
+ })
+}
+
+/**
+ * Simple helper for chained async asssertions
+ *
+ * @param {Function} fn - the data manipulation function
+ * @param {Function} cb - the assertion fn to be called on nextTick
+ */
+
+function go (fn, cb) {
+ return {
+ stack: [{fn: fn, cb: cb}],
+ then: function (fn, cb) {
+ this.stack.push({fn: fn, cb: cb})
+ return this
+ },
+ run: function (done) {
+ var self = this
+ var step = this.stack.shift()
+ if (!step) return done()
+ step.fn()
+ _.nextTick(function () {
+ step.cb()
+ self.run(done)
+ })
+ }
+ }
+}
+
+/**
+ * Assert mutation and markup correctness for v-repeat on
+ * an Array of Objects
+ */
+
+function assertMutations (vm, el, done) {
+ assertMarkup()
+ var poppedItem
+ go(
+ function () {
+ vm.items.push({a: 3})
+ },
+ assertMarkup
+ )
+ .then(
+ function () {
+ vm.items.shift()
+ },
+ assertMarkup
+ )
+ .then(
+ function () {
+ vm.items.reverse()
+ },
+ assertMarkup
+ )
+ .then(
+ function () {
+ poppedItem = vm.items.pop()
+ },
+ assertMarkup
+ )
+ .then(
+ function () {
+ vm.items.unshift(poppedItem)
+ },
+ assertMarkup
+ )
+ .then(
+ function () {
+ vm.items.sort(function (a, b) {
+ return a.a > b.a ? 1 : -1
+ })
+ },
+ assertMarkup
+ )
+ .then(
+ function () {
+ vm.items.splice(1, 1, {a: 5})
+ },
+ assertMarkup
+ )
+ // test swapping the array
+ .then(
+ function () {
+ vm.items = [{a: 0}, {a: 1}, {a: 2}]
+ },
+ assertMarkup
+ )
+ .run(done)
+
+ function assertMarkup () {
+ var tag = el.children[0].tagName.toLowerCase()
+ var markup = vm.items.map(function (item, i) {
+ var el = '<' + tag + '>' + i + ' ' + item.a + '' + tag + '>'
+ return el
+ }).join('')
+ expect(el.innerHTML).toBe(markup)
+ }
+}
+
+/**
+ * Assert mutation and markup correctness for v-repeat on
+ * an Array of primitive values
+ */
+
+function assertPrimitiveMutations (vm, el, done) {
+ assertMarkup()
+ go(
+ function () {
+ // check duplicate
+ vm.items.push(2, 2, 3)
+ },
+ assertMarkup
+ )
+ .then(
+ function () {
+ vm.items.shift()
+ },
+ assertMarkup
+ )
+ .then(
+ function () {
+ vm.items.reverse()
+ },
+ assertMarkup
+ )
+ .then(
+ function () {
+ vm.items.pop()
+ },
+ assertMarkup
+ )
+ .then(
+ function () {
+ vm.items.unshift(3)
+ },
+ assertMarkup
+ )
+ .then(
+ function () {
+ vm.items.sort(function (a, b) {
+ return a > b ? 1 : -1
+ })
+ },
+ assertMarkup
+ )
+ .then(
+ function () {
+ vm.items.splice(1, 1, 5)
+ },
+ assertMarkup
+ )
+ // test swapping the array
+ .then(
+ function () {
+ vm.items = [1, 2, 2]
+ },
+ assertMarkup
+ )
+ .run(done)
+
+ function assertMarkup () {
+ var markup = vm.items.map(function (item, i) {
+ return '' + i + ' ' + item + '
'
+ }).join('')
+ expect(el.innerHTML).toBe(markup)
+ }
+}
+
+/**
+ * Assert mutation and markup correctness for v-repeat on
+ * an Object of Objects
+ */
+
+function assertObjectMutations (vm, el, done) {
+ assertMarkup()
+ go(
+ function () {
+ vm.items.a = {a: 3}
+ },
+ assertMarkup
+ )
+ .then(
+ function () {
+ vm.items = {
+ c: {a: 1},
+ d: {a: 2}
+ }
+ },
+ assertMarkup
+ )
+ .then(
+ function () {
+ vm.items.$add('a', {a: 3})
+ },
+ assertMarkup
+ )
+ .run(done)
+
+ function assertMarkup () {
+ var markup = Object.keys(vm.items).map(function (key, i) {
+ return '' + i + ' ' + key + ' ' + vm.items[key].a + '
'
+ }).join('')
+ expect(el.innerHTML).toBe(markup)
+ }
+}
+
+/**
+ * Assert mutation and markup correctness for v-repeat on
+ * an Object of primitive values
+ */
+
+function assertObjectPrimitiveMutations (vm, el, done) {
+ assertMarkup()
+ go(
+ function () {
+ vm.items.a = 3
+ },
+ assertMarkup
+ )
+ .then(
+ function () {
+ vm.items = {
+ c: 1,
+ d: 2
+ }
+ },
+ assertMarkup
+ )
+ .then(
+ function () {
+ vm.items.$add('a', 3)
+ },
+ assertMarkup
+ )
+ .run(done)
+
+ function assertMarkup () {
+ var markup = Object.keys(vm.items).map(function (key, i) {
+ return '' + i + ' ' + key + ' ' + vm.items[key] + '
'
+ }).join('')
+ expect(el.innerHTML).toBe(markup)
+ }
+}