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( + '

0 1 0 1

1 2 0 1

' + + '

0 3 1 2

1 4 1 2

' + ) + }) + + 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 '
' + key + '

1

2

' + + '
' + key1 + '

1

2

' + } + 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: '', + // 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('
{{msg}}
{{msg}}
', function () { + // assertTrackBy('
{{msg}}
{{msg}}
', 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 + '' + 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) + } +}