var _ = require('src/util') var Vue = require('src') describe('v-for', function () { var el beforeEach(function () { el = document.createElement('div') }) 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: [1, 2, 3] }, 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('filter converting array to object', function () { new Vue({ el: el, data: { items: [ { msg: 'aaa' }, { msg: 'bbb' } ] }, template: '
{{item.msg}} {{$key}}
', filters: { test: function (val) { return { a: val[0], b: val[1] } } } }) expect(el.innerHTML).toBe('
aaa a
bbb b
') }) 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('is 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: [1, 2, 3] }, 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('fragment loop', 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('fragment loop 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: '
{{item.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 + '
' }) .slice(0, 2) .join('') expect(el.innerHTML).toBe(markup) } }) it('orderBy supporting $key for object repeaters', function (done) { var vm = new Vue({ el: el, template: '
{{val}}
', data: { sortKey: '$key', obj: { c: 1, a: 3, b: 2 } } }) expect(el.innerHTML).toBe('
3
2
1
') vm.sortKey = 'val' _.nextTick(function () { expect(el.innerHTML).toBe('
1
2
3
') done() }) }) it('orderBy supporting alias for primitive arrays', function () { new Vue({ el: el, template: '
{{val}}
', data: { list: [3, 2, 1] } }) expect(el.innerHTML).toBe('
1
2
3
') }) it('track by id', function (done) { var vm = new Vue({ el: el, template: '', data: { list: [ { id: 1, msg: 'hi' }, { id: 2, msg: 'ha' }, { id: 3, msg: 'ho' } ] }, components: { test: { props: ['item'], template: '{{item.msg}}' } } }) 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]) } done() }) 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}} {{item.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('primitive values track by $index', function (done) { var vm = new Vue({ el: el, data: { items: [1, 2, 3] }, template: '
{{$index}} {{item}}
' }) assertPrimitiveMutationsWithDuplicates(vm, el, done) }) it('warn missing alias', function () { new Vue({ el: el, template: '
' }) expect('Alias is required in v-for').toHaveBeenWarned() }) it('warn duplicate objects', function () { var obj = {} new Vue({ el: el, template: '
', data: { items: [obj, obj] } }) expect('Duplicate value').toHaveBeenWarned() }) it('warn duplicate objects on diff', function (done) { var obj = {} var vm = new Vue({ el: el, template: '
', data: { items: [obj] } }) expect(getWarnCount()).toBe(0) vm.items.push(obj) _.nextTick(function () { expect('Duplicate value').toHaveBeenWarned() done() }) }) it('warn duplicate trackby id', function () { new Vue({ el: el, template: '
', data: { items: [{id: 1}, {id: 1}] } }) expect('Duplicate value').toHaveBeenWarned() }) it('key val syntax with object', function (done) { var vm = new Vue({ el: el, template: '
{{$index}} {{key}} {{val.a}}
', data: { items: { a: {a: 1}, b: {a: 2} } } }) assertObjectMutations(vm, el, done) }) it('key val syntax with array', function (done) { var vm = new Vue({ el: el, template: '
{{i}} {{item.a}}
', data: { items: [{a: 1}, {a: 2}] } }) assertMutations(vm, el, done) }) it('key val syntax with nested v-for s', function () { new Vue({ el: el, template: '
{{key}} {{subkey}} {{subval}}
', data: { items: {'a': {'b': 'c'}} } }) expect(el.innerHTML).toBe('
a b c
') }) it('repeat number', function () { new Vue({ el: el, template: '
{{$index}} {{n}}
' }) expect(el.innerHTML).toBe('
0 0
1 1
2 2
') }) it('repeat string', function () { new Vue({ el: el, template: '
{{$index}} {{letter}}
' }) 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: '
{{item.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('v-model binding on alias', function () { var vm = new Vue({ el: el, template: '
' + '
', data: { items: ['a'], obj: { foo: 'a' } } }) var a = getInput(1) a.value = 'b' trigger(a, 'input') expect(vm.items[0]).toBe('b') var b = getInput(2) b.value = 'bar' trigger(b, 'input') expect(vm.obj.foo).toBe('bar') function getInput (x) { return vm.$el.querySelector('div:nth-child(' + x + ') input') } }) it('warn v-model on alias with filters', function () { var vm = new Vue({ el: el, template: '
' + '' + '
', data: { items: ['a', 'b'] } }) trigger(vm.$el.querySelector('input'), 'input') expect('It seems you are using two-way binding').toHaveBeenWarned() }) it('nested track by', function (done) { var vm = new Vue({ el: el, template: '
' + '{{item.msg}}' + '
' + '{{subItem.msg}}' + '
' + '
', data: { list: [ { id: 1, msg: 'hi', list: [ { id: 1, msg: 'hi foo' } ] }, { id: 2, msg: 'ha', list: [] }, { id: 3, msg: 'ho', list: [] } ] } }) assertMarkup() var oldNodes = el.children var oldInnerNodes = el.children[0].children 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 frags! var i = 2 while (i--) { expect(el.children[i]).toBe(oldNodes[i]) } expect(el.children[0].children[0]).toBe(oldInnerNodes[0]) done() }) 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: '
{{item.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() }) }) it('call attach/detach for contained components', function (done) { document.body.appendChild(el) var attachSpy = jasmine.createSpy('attach') var detachSpy = jasmine.createSpy('detach') var vm = new Vue({ el: el, template: '', data: { items: [1, 2] }, components: { test: { attached: attachSpy, detached: detachSpy } } }) expect(attachSpy.calls.count()).toBe(2) expect(detachSpy.calls.count()).toBe(0) vm.items.push(3) _.nextTick(function () { expect(attachSpy.calls.count()).toBe(3) expect(detachSpy.calls.count()).toBe(0) vm.items.pop() _.nextTick(function () { expect(attachSpy.calls.count()).toBe(3) expect(detachSpy.calls.count()).toBe(1) vm.items = [] _.nextTick(function () { expect(attachSpy.calls.count()).toBe(3) expect(detachSpy.calls.count()).toBe(3) done() }) }) }) }) it('access parent\'s $refs', function () { var vm = new Vue({ el: document.createElement('div'), template: '
{{$refs.c1.d}}
', components: { c1: { template: '
', data: function () { return { d: 1 } } } } }) expect(vm.$refs.c1 instanceof Vue).toBe(true) expect(vm.$refs.c1.$el.innerHTML).toContain('
1
1
') }) it('access parent scope\'s $els', function (done) { var vm = new Vue({ el: document.createElement('div'), template: '
{{ready ? $els.a.getAttribute("data-d") : 0}}
', data: { ready: false } }) expect(vm.$els.a.nodeType).toBe(1) expect(vm.$els.a.innerHTML).toContain('
0
0
') vm.ready = true vm.$nextTick(function () { expect(vm.$els.a.innerHTML).toContain('
1
1
') 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-for 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.push(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-for on * an Array of primitive values */ function assertPrimitiveMutations (vm, el, done) { assertMarkup() go( function () { vm.items.push(4) }, assertMarkup ) .then( function () { vm.items.shift() }, assertMarkup ) .then( function () { vm.items.reverse() }, assertMarkup ) .then( function () { vm.items.pop() }, assertMarkup ) .then( function () { vm.items.unshift(1) }, 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, 3] }, 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-for on * an Array of primitive values when using track-by="$index" */ function assertPrimitiveMutationsWithDuplicates (vm, el, done) { assertMarkup() go( function () { 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-for 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 () { _.set(vm.items, '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-for 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 () { _.set(vm.items, '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) } } /** * Helper for triggering events */ function trigger (target, event, process) { var e = document.createEvent('HTMLEvents') e.initEvent(event, true, true) if (process) process(e) target.dispatchEvent(e) return e }