import Vue from 'vue' describe('Component keep-alive', () => { let components, one, two, el beforeEach(() => { one = { template: '
one
', created: vi.fn(), mounted: vi.fn(), activated: vi.fn(), deactivated: vi.fn(), destroyed: vi.fn() } two = { template: '
two
', created: vi.fn(), mounted: vi.fn(), activated: vi.fn(), deactivated: vi.fn(), destroyed: vi.fn() } components = { one, two } el = document.createElement('div') document.body.appendChild(el) }) function assertHookCalls(component, callCounts) { expect([ component.created.mock.calls.length, component.mounted.mock.calls.length, component.activated.mock.calls.length, component.deactivated.mock.calls.length, component.destroyed.mock.calls.length ]).toEqual(callCounts) } it('should work', done => { const vm = new Vue({ template: `
`, data: { view: 'one', ok: true }, components }).$mount() expect(vm.$el.textContent).toBe('one') assertHookCalls(one, [1, 1, 1, 0, 0]) assertHookCalls(two, [0, 0, 0, 0, 0]) vm.view = 'two' waitForUpdate(() => { expect(vm.$el.textContent).toBe('two') assertHookCalls(one, [1, 1, 1, 1, 0]) assertHookCalls(two, [1, 1, 1, 0, 0]) vm.view = 'one' }) .then(() => { expect(vm.$el.textContent).toBe('one') assertHookCalls(one, [1, 1, 2, 1, 0]) assertHookCalls(two, [1, 1, 1, 1, 0]) vm.view = 'two' }) .then(() => { expect(vm.$el.textContent).toBe('two') assertHookCalls(one, [1, 1, 2, 2, 0]) assertHookCalls(two, [1, 1, 2, 1, 0]) vm.ok = false // teardown }) .then(() => { expect(vm.$el.textContent).toBe('') assertHookCalls(one, [1, 1, 2, 2, 1]) assertHookCalls(two, [1, 1, 2, 2, 1]) }) .then(done) }) it('should invoke hooks on the entire sub tree', done => { one.template = '' one.components = { two } const vm = new Vue({ template: `
`, data: { ok: true }, components }).$mount() expect(vm.$el.textContent).toBe('two') assertHookCalls(one, [1, 1, 1, 0, 0]) assertHookCalls(two, [1, 1, 1, 0, 0]) vm.ok = false waitForUpdate(() => { expect(vm.$el.textContent).toBe('') assertHookCalls(one, [1, 1, 1, 1, 0]) assertHookCalls(two, [1, 1, 1, 1, 0]) vm.ok = true }) .then(() => { expect(vm.$el.textContent).toBe('two') assertHookCalls(one, [1, 1, 2, 1, 0]) assertHookCalls(two, [1, 1, 2, 1, 0]) vm.ok = false }) .then(() => { expect(vm.$el.textContent).toBe('') assertHookCalls(one, [1, 1, 2, 2, 0]) assertHookCalls(two, [1, 1, 2, 2, 0]) }) .then(done) }) it('should handle nested keep-alive hooks properly', done => { one.template = '' one.data = () => ({ ok: true }) one.components = { two } const vm = new Vue({ template: `
`, data: { ok: true }, components }).$mount() const oneInstance = vm.$refs.one expect(vm.$el.textContent).toBe('two') assertHookCalls(one, [1, 1, 1, 0, 0]) assertHookCalls(two, [1, 1, 1, 0, 0]) vm.ok = false waitForUpdate(() => { expect(vm.$el.textContent).toBe('') assertHookCalls(one, [1, 1, 1, 1, 0]) assertHookCalls(two, [1, 1, 1, 1, 0]) }) .then(() => { vm.ok = true }) .then(() => { expect(vm.$el.textContent).toBe('two') assertHookCalls(one, [1, 1, 2, 1, 0]) assertHookCalls(two, [1, 1, 2, 1, 0]) }) .then(() => { // toggle sub component when activated oneInstance.ok = false }) .then(() => { expect(vm.$el.textContent).toBe('') assertHookCalls(one, [1, 1, 2, 1, 0]) assertHookCalls(two, [1, 1, 2, 2, 0]) }) .then(() => { oneInstance.ok = true }) .then(() => { expect(vm.$el.textContent).toBe('two') assertHookCalls(one, [1, 1, 2, 1, 0]) assertHookCalls(two, [1, 1, 3, 2, 0]) }) .then(() => { vm.ok = false }) .then(() => { expect(vm.$el.textContent).toBe('') assertHookCalls(one, [1, 1, 2, 2, 0]) assertHookCalls(two, [1, 1, 3, 3, 0]) }) .then(() => { // toggle sub component when parent is deactivated oneInstance.ok = false }) .then(() => { expect(vm.$el.textContent).toBe('') assertHookCalls(one, [1, 1, 2, 2, 0]) assertHookCalls(two, [1, 1, 3, 3, 0]) // should not be affected }) .then(() => { oneInstance.ok = true }) .then(() => { expect(vm.$el.textContent).toBe('') assertHookCalls(one, [1, 1, 2, 2, 0]) assertHookCalls(two, [1, 1, 3, 3, 0]) // should not be affected }) .then(() => { vm.ok = true }) .then(() => { expect(vm.$el.textContent).toBe('two') assertHookCalls(one, [1, 1, 3, 2, 0]) assertHookCalls(two, [1, 1, 4, 3, 0]) }) .then(() => { oneInstance.ok = false vm.ok = false }) .then(() => { expect(vm.$el.textContent).toBe('') assertHookCalls(one, [1, 1, 3, 3, 0]) assertHookCalls(two, [1, 1, 4, 4, 0]) }) .then(() => { vm.ok = true }) .then(() => { expect(vm.$el.textContent).toBe('') assertHookCalls(one, [1, 1, 4, 3, 0]) assertHookCalls(two, [1, 1, 4, 4, 0]) // should remain inactive }) .then(done) }) function sharedAssertions(vm, done) { expect(vm.$el.textContent).toBe('one') assertHookCalls(one, [1, 1, 1, 0, 0]) assertHookCalls(two, [0, 0, 0, 0, 0]) vm.view = 'two' waitForUpdate(() => { expect(vm.$el.textContent).toBe('two') assertHookCalls(one, [1, 1, 1, 1, 0]) assertHookCalls(two, [1, 1, 0, 0, 0]) vm.view = 'one' }) .then(() => { expect(vm.$el.textContent).toBe('one') assertHookCalls(one, [1, 1, 2, 1, 0]) assertHookCalls(two, [1, 1, 0, 0, 1]) vm.view = 'two' }) .then(() => { expect(vm.$el.textContent).toBe('two') assertHookCalls(one, [1, 1, 2, 2, 0]) assertHookCalls(two, [2, 2, 0, 0, 1]) vm.ok = false // teardown }) .then(() => { expect(vm.$el.textContent).toBe('') assertHookCalls(one, [1, 1, 2, 2, 1]) assertHookCalls(two, [2, 2, 0, 0, 2]) }) .then(done) } it('include (string)', done => { const vm = new Vue({ template: `
`, data: { view: 'one', ok: true }, components }).$mount() sharedAssertions(vm, done) }) it('include (regex)', done => { const vm = new Vue({ template: `
`, data: { view: 'one', ok: true }, components }).$mount() sharedAssertions(vm, done) }) it('include (array)', done => { const vm = new Vue({ template: `
`, data: { view: 'one', ok: true }, components }).$mount() sharedAssertions(vm, done) }) it('exclude (string)', done => { const vm = new Vue({ template: `
`, data: { view: 'one', ok: true }, components }).$mount() sharedAssertions(vm, done) }) it('exclude (regex)', done => { const vm = new Vue({ template: `
`, data: { view: 'one', ok: true }, components }).$mount() sharedAssertions(vm, done) }) it('exclude (array)', done => { const vm = new Vue({ template: `
`, data: { view: 'one', ok: true }, components }).$mount() sharedAssertions(vm, done) }) it('include + exclude', done => { const vm = new Vue({ template: `
`, data: { view: 'one', ok: true }, components }).$mount() sharedAssertions(vm, done) }) it('prune cache on include/exclude change', done => { const vm = new Vue({ template: `
`, data: { view: 'one', include: 'one,two' }, components }).$mount() vm.view = 'two' waitForUpdate(() => { assertHookCalls(one, [1, 1, 1, 1, 0]) assertHookCalls(two, [1, 1, 1, 0, 0]) vm.include = 'two' }) .then(() => { assertHookCalls(one, [1, 1, 1, 1, 1]) assertHookCalls(two, [1, 1, 1, 0, 0]) vm.view = 'one' }) .then(() => { assertHookCalls(one, [2, 2, 1, 1, 1]) assertHookCalls(two, [1, 1, 1, 1, 0]) }) .then(done) }) it('prune cache on include/exclude change + view switch', done => { const vm = new Vue({ template: `
`, data: { view: 'one', include: 'one,two' }, components }).$mount() vm.view = 'two' waitForUpdate(() => { assertHookCalls(one, [1, 1, 1, 1, 0]) assertHookCalls(two, [1, 1, 1, 0, 0]) vm.include = 'one' vm.view = 'one' }) .then(() => { assertHookCalls(one, [1, 1, 2, 1, 0]) // two should be pruned assertHookCalls(two, [1, 1, 1, 1, 1]) }) .then(done) }) it('should not prune currently active instance', done => { const vm = new Vue({ template: `
`, data: { view: 'one', include: 'one,two' }, components }).$mount() vm.include = 'two' waitForUpdate(() => { assertHookCalls(one, [1, 1, 1, 0, 0]) assertHookCalls(two, [0, 0, 0, 0, 0]) vm.view = 'two' }) .then(() => { assertHookCalls(one, [1, 1, 1, 0, 1]) assertHookCalls(two, [1, 1, 1, 0, 0]) }) .then(done) }) // #3882 it('deeply nested keep-alive should be destroyed properly', done => { one.template = `
` one.components = { two } const vm = new Vue({ template: `
`, data: { ok: true }, components: { parent: { template: `
`, components: { one } } } }).$mount() assertHookCalls(one, [1, 1, 1, 0, 0]) assertHookCalls(two, [1, 1, 1, 0, 0]) vm.ok = false waitForUpdate(() => { assertHookCalls(one, [1, 1, 1, 1, 1]) assertHookCalls(two, [1, 1, 1, 1, 1]) }).then(done) }) // #4237 it('should update latest props/listeners for a re-activated component', done => { const one = { props: ['prop'], template: `
one {{ prop }}
` } const two = { props: ['prop'], template: `
two {{ prop }}
` } const vm = new Vue({ data: { view: 'one', n: 1 }, template: `
`, components: { one, two } }).$mount() expect(vm.$el.textContent).toBe('one 1') vm.n++ waitForUpdate(() => { expect(vm.$el.textContent).toBe('one 2') vm.view = 'two' }) .then(() => { expect(vm.$el.textContent).toBe('two 2') }) .then(done) }) it('max', done => { const spyA = vi.fn() const spyB = vi.fn() const spyC = vi.fn() const spyAD = vi.fn() const spyBD = vi.fn() const spyCD = vi.fn() function assertCount(calls) { expect([ spyA.mock.calls.length, spyAD.mock.calls.length, spyB.mock.calls.length, spyBD.mock.calls.length, spyC.mock.calls.length, spyCD.mock.calls.length ]).toEqual(calls) } const vm = new Vue({ template: ` `, data: { n: 'aa' }, components: { aa: { template: '
a
', created: spyA, destroyed: spyAD }, bb: { template: '
bbb
', created: spyB, destroyed: spyBD }, cc: { template: '
ccc
', created: spyC, destroyed: spyCD } } }).$mount() assertCount([1, 0, 0, 0, 0, 0]) vm.n = 'bb' waitForUpdate(() => { assertCount([1, 0, 1, 0, 0, 0]) vm.n = 'cc' }) .then(() => { // should prune A because max cache reached assertCount([1, 1, 1, 0, 1, 0]) vm.n = 'bb' }) .then(() => { // B should be reused, and made latest assertCount([1, 1, 1, 0, 1, 0]) vm.n = 'aa' }) .then(() => { // C should be pruned because B was used last so C is the oldest cached assertCount([2, 1, 1, 0, 1, 1]) }) .then(done) }) it('max=1', done => { const spyA = vi.fn() const spyB = vi.fn() const spyC = vi.fn() const spyAD = vi.fn() const spyBD = vi.fn() const spyCD = vi.fn() function assertCount(calls) { expect([ spyA.mock.calls.length, spyAD.mock.calls.length, spyB.mock.calls.length, spyBD.mock.calls.length, spyC.mock.calls.length, spyCD.mock.calls.length ]).toEqual(calls) } const vm = new Vue({ template: ` `, data: { n: 'aa' }, components: { aa: { template: '
a
', created: spyA, destroyed: spyAD }, bb: { template: '
bbb
', created: spyB, destroyed: spyBD }, cc: { template: '
ccc
', created: spyC, destroyed: spyCD } } }).$mount() assertCount([1, 0, 0, 0, 0, 0]) vm.n = 'bb' waitForUpdate(() => { // should prune A because max cache reached assertCount([1, 1, 1, 0, 0, 0]) vm.n = 'cc' }) .then(() => { // should prune B because max cache reached assertCount([1, 1, 1, 1, 1, 0]) vm.n = 'bb' }) .then(() => { // B is recreated assertCount([1, 1, 2, 1, 1, 1]) vm.n = 'aa' }) .then(() => { // B is destroyed and A recreated assertCount([2, 1, 2, 2, 1, 1]) }) .then(done) }) it('should warn unknown component inside', () => { new Vue({ template: `` }).$mount() expect(`Unknown custom element: `).toHaveBeenWarned() }) // #6938 it('should not cache anonymous component when include is specified', done => { const Foo = { name: 'foo', template: `
foo
`, created: vi.fn() } const Bar = { template: `
bar
`, created: vi.fn() } const Child = { functional: true, render(h, ctx) { return h(ctx.props.view ? Foo : Bar) } } const vm = new Vue({ template: ` `, data: { view: true }, components: { Child } }).$mount() function assert(foo, bar) { expect(Foo.created.mock.calls.length).toBe(foo) expect(Bar.created.mock.calls.length).toBe(bar) } expect(vm.$el.textContent).toBe('foo') assert(1, 0) vm.view = false waitForUpdate(() => { expect(vm.$el.textContent).toBe('bar') assert(1, 1) vm.view = true }) .then(() => { expect(vm.$el.textContent).toBe('foo') assert(1, 1) vm.view = false }) .then(() => { expect(vm.$el.textContent).toBe('bar') assert(1, 2) }) .then(done) }) it('should cache anonymous components if include is not specified', done => { const Foo = { template: `
foo
`, created: vi.fn() } const Bar = { template: `
bar
`, created: vi.fn() } const Child = { functional: true, render(h, ctx) { return h(ctx.props.view ? Foo : Bar) } } const vm = new Vue({ template: ` `, data: { view: true }, components: { Child } }).$mount() function assert(foo, bar) { expect(Foo.created.mock.calls.length).toBe(foo) expect(Bar.created.mock.calls.length).toBe(bar) } expect(vm.$el.textContent).toBe('foo') assert(1, 0) vm.view = false waitForUpdate(() => { expect(vm.$el.textContent).toBe('bar') assert(1, 1) vm.view = true }) .then(() => { expect(vm.$el.textContent).toBe('foo') assert(1, 1) vm.view = false }) .then(() => { expect(vm.$el.textContent).toBe('bar') assert(1, 1) }) .then(done) }) // #7105 it('should not destroy active instance when pruning cache', done => { const Foo = { template: `
foo
`, destroyed: vi.fn() } const vm = new Vue({ template: `
`, data: { include: ['foo'] }, components: { Foo } }).$mount() // condition: a render where a previous component is reused vm.include = ['foo'] waitForUpdate(() => { vm.include = [''] }) .then(() => { expect(Foo.destroyed).not.toHaveBeenCalled() }) .then(done) }) })