test/unit/features/component/component-keep-alive.spec.ts TYPESCRIPT 834 lines View on github.com → Search inside
1import Vue from 'vue'23describe('Component keep-alive', () => {4  let components, one, two, el5  beforeEach(() => {6    one = {7      template: '<div>one</div>',8      created: vi.fn(),9      mounted: vi.fn(),10      activated: vi.fn(),11      deactivated: vi.fn(),12      destroyed: vi.fn()13    }14    two = {15      template: '<div>two</div>',16      created: vi.fn(),17      mounted: vi.fn(),18      activated: vi.fn(),19      deactivated: vi.fn(),20      destroyed: vi.fn()21    }22    components = {23      one,24      two25    }26    el = document.createElement('div')27    document.body.appendChild(el)28  })2930  function assertHookCalls(component, callCounts) {31    expect([32      component.created.mock.calls.length,33      component.mounted.mock.calls.length,34      component.activated.mock.calls.length,35      component.deactivated.mock.calls.length,36      component.destroyed.mock.calls.length37    ]).toEqual(callCounts)38  }3940  it('should work', done => {41    const vm = new Vue({42      template: `43        <div v-if="ok">44          <keep-alive>45            <component :is="view"></component>46          </keep-alive>47        </div>48      `,49      data: {50        view: 'one',51        ok: true52      },53      components54    }).$mount()55    expect(vm.$el.textContent).toBe('one')56    assertHookCalls(one, [1, 1, 1, 0, 0])57    assertHookCalls(two, [0, 0, 0, 0, 0])58    vm.view = 'two'59    waitForUpdate(() => {60      expect(vm.$el.textContent).toBe('two')61      assertHookCalls(one, [1, 1, 1, 1, 0])62      assertHookCalls(two, [1, 1, 1, 0, 0])63      vm.view = 'one'64    })65      .then(() => {66        expect(vm.$el.textContent).toBe('one')67        assertHookCalls(one, [1, 1, 2, 1, 0])68        assertHookCalls(two, [1, 1, 1, 1, 0])69        vm.view = 'two'70      })71      .then(() => {72        expect(vm.$el.textContent).toBe('two')73        assertHookCalls(one, [1, 1, 2, 2, 0])74        assertHookCalls(two, [1, 1, 2, 1, 0])75        vm.ok = false // teardown76      })77      .then(() => {78        expect(vm.$el.textContent).toBe('')79        assertHookCalls(one, [1, 1, 2, 2, 1])80        assertHookCalls(two, [1, 1, 2, 2, 1])81      })82      .then(done)83  })8485  it('should invoke hooks on the entire sub tree', done => {86    one.template = '<two/>'87    one.components = { two }8889    const vm = new Vue({90      template: `91        <div>92          <keep-alive>93            <one v-if="ok"/>94          </keep-alive>95        </div>96      `,97      data: {98        ok: true99      },100      components101    }).$mount()102103    expect(vm.$el.textContent).toBe('two')104    assertHookCalls(one, [1, 1, 1, 0, 0])105    assertHookCalls(two, [1, 1, 1, 0, 0])106    vm.ok = false107    waitForUpdate(() => {108      expect(vm.$el.textContent).toBe('')109      assertHookCalls(one, [1, 1, 1, 1, 0])110      assertHookCalls(two, [1, 1, 1, 1, 0])111      vm.ok = true112    })113      .then(() => {114        expect(vm.$el.textContent).toBe('two')115        assertHookCalls(one, [1, 1, 2, 1, 0])116        assertHookCalls(two, [1, 1, 2, 1, 0])117        vm.ok = false118      })119      .then(() => {120        expect(vm.$el.textContent).toBe('')121        assertHookCalls(one, [1, 1, 2, 2, 0])122        assertHookCalls(two, [1, 1, 2, 2, 0])123      })124      .then(done)125  })126127  it('should handle nested keep-alive hooks properly', done => {128    one.template = '<keep-alive><two v-if="ok" /></keep-alive>'129    one.data = () => ({ ok: true })130    one.components = { two }131132    const vm = new Vue({133      template: `134        <div>135          <keep-alive>136            <one v-if="ok" ref="one" />137          </keep-alive>138        </div>139      `,140      data: {141        ok: true142      },143      components144    }).$mount()145146    const oneInstance = vm.$refs.one147    expect(vm.$el.textContent).toBe('two')148    assertHookCalls(one, [1, 1, 1, 0, 0])149    assertHookCalls(two, [1, 1, 1, 0, 0])150    vm.ok = false151    waitForUpdate(() => {152      expect(vm.$el.textContent).toBe('')153      assertHookCalls(one, [1, 1, 1, 1, 0])154      assertHookCalls(two, [1, 1, 1, 1, 0])155    })156      .then(() => {157        vm.ok = true158      })159      .then(() => {160        expect(vm.$el.textContent).toBe('two')161        assertHookCalls(one, [1, 1, 2, 1, 0])162        assertHookCalls(two, [1, 1, 2, 1, 0])163      })164      .then(() => {165        // toggle sub component when activated166        oneInstance.ok = false167      })168      .then(() => {169        expect(vm.$el.textContent).toBe('')170        assertHookCalls(one, [1, 1, 2, 1, 0])171        assertHookCalls(two, [1, 1, 2, 2, 0])172      })173      .then(() => {174        oneInstance.ok = true175      })176      .then(() => {177        expect(vm.$el.textContent).toBe('two')178        assertHookCalls(one, [1, 1, 2, 1, 0])179        assertHookCalls(two, [1, 1, 3, 2, 0])180      })181      .then(() => {182        vm.ok = false183      })184      .then(() => {185        expect(vm.$el.textContent).toBe('')186        assertHookCalls(one, [1, 1, 2, 2, 0])187        assertHookCalls(two, [1, 1, 3, 3, 0])188      })189      .then(() => {190        // toggle sub component when parent is deactivated191        oneInstance.ok = false192      })193      .then(() => {194        expect(vm.$el.textContent).toBe('')195        assertHookCalls(one, [1, 1, 2, 2, 0])196        assertHookCalls(two, [1, 1, 3, 3, 0]) // should not be affected197      })198      .then(() => {199        oneInstance.ok = true200      })201      .then(() => {202        expect(vm.$el.textContent).toBe('')203        assertHookCalls(one, [1, 1, 2, 2, 0])204        assertHookCalls(two, [1, 1, 3, 3, 0]) // should not be affected205      })206      .then(() => {207        vm.ok = true208      })209      .then(() => {210        expect(vm.$el.textContent).toBe('two')211        assertHookCalls(one, [1, 1, 3, 2, 0])212        assertHookCalls(two, [1, 1, 4, 3, 0])213      })214      .then(() => {215        oneInstance.ok = false216        vm.ok = false217      })218      .then(() => {219        expect(vm.$el.textContent).toBe('')220        assertHookCalls(one, [1, 1, 3, 3, 0])221        assertHookCalls(two, [1, 1, 4, 4, 0])222      })223      .then(() => {224        vm.ok = true225      })226      .then(() => {227        expect(vm.$el.textContent).toBe('')228        assertHookCalls(one, [1, 1, 4, 3, 0])229        assertHookCalls(two, [1, 1, 4, 4, 0]) // should remain inactive230      })231      .then(done)232  })233234  function sharedAssertions(vm, done) {235    expect(vm.$el.textContent).toBe('one')236    assertHookCalls(one, [1, 1, 1, 0, 0])237    assertHookCalls(two, [0, 0, 0, 0, 0])238    vm.view = 'two'239    waitForUpdate(() => {240      expect(vm.$el.textContent).toBe('two')241      assertHookCalls(one, [1, 1, 1, 1, 0])242      assertHookCalls(two, [1, 1, 0, 0, 0])243      vm.view = 'one'244    })245      .then(() => {246        expect(vm.$el.textContent).toBe('one')247        assertHookCalls(one, [1, 1, 2, 1, 0])248        assertHookCalls(two, [1, 1, 0, 0, 1])249        vm.view = 'two'250      })251      .then(() => {252        expect(vm.$el.textContent).toBe('two')253        assertHookCalls(one, [1, 1, 2, 2, 0])254        assertHookCalls(two, [2, 2, 0, 0, 1])255        vm.ok = false // teardown256      })257      .then(() => {258        expect(vm.$el.textContent).toBe('')259        assertHookCalls(one, [1, 1, 2, 2, 1])260        assertHookCalls(two, [2, 2, 0, 0, 2])261      })262      .then(done)263  }264265  it('include (string)', done => {266    const vm = new Vue({267      template: `268        <div v-if="ok">269          <keep-alive include="one">270            <component :is="view"></component>271          </keep-alive>272        </div>273      `,274      data: {275        view: 'one',276        ok: true277      },278      components279    }).$mount()280    sharedAssertions(vm, done)281  })282283  it('include (regex)', done => {284    const vm = new Vue({285      template: `286        <div v-if="ok">287          <keep-alive :include="/^one$/">288            <component :is="view"></component>289          </keep-alive>290        </div>291      `,292      data: {293        view: 'one',294        ok: true295      },296      components297    }).$mount()298    sharedAssertions(vm, done)299  })300301  it('include (array)', done => {302    const vm = new Vue({303      template: `304        <div v-if="ok">305          <keep-alive :include="['one']">306            <component :is="view"></component>307          </keep-alive>308        </div>309      `,310      data: {311        view: 'one',312        ok: true313      },314      components315    }).$mount()316    sharedAssertions(vm, done)317  })318319  it('exclude (string)', done => {320    const vm = new Vue({321      template: `322        <div v-if="ok">323          <keep-alive exclude="two">324            <component :is="view"></component>325          </keep-alive>326        </div>327      `,328      data: {329        view: 'one',330        ok: true331      },332      components333    }).$mount()334    sharedAssertions(vm, done)335  })336337  it('exclude (regex)', done => {338    const vm = new Vue({339      template: `340        <div v-if="ok">341          <keep-alive :exclude="/^two$/">342            <component :is="view"></component>343          </keep-alive>344        </div>345      `,346      data: {347        view: 'one',348        ok: true349      },350      components351    }).$mount()352    sharedAssertions(vm, done)353  })354355  it('exclude (array)', done => {356    const vm = new Vue({357      template: `358        <div v-if="ok">359          <keep-alive :exclude="['two']">360            <component :is="view"></component>361          </keep-alive>362        </div>363      `,364      data: {365        view: 'one',366        ok: true367      },368      components369    }).$mount()370    sharedAssertions(vm, done)371  })372373  it('include + exclude', done => {374    const vm = new Vue({375      template: `376        <div v-if="ok">377          <keep-alive include="one,two" exclude="two">378            <component :is="view"></component>379          </keep-alive>380        </div>381      `,382      data: {383        view: 'one',384        ok: true385      },386      components387    }).$mount()388    sharedAssertions(vm, done)389  })390391  it('prune cache on include/exclude change', done => {392    const vm = new Vue({393      template: `394        <div>395          <keep-alive :include="include">396            <component :is="view"></component>397          </keep-alive>398        </div>399      `,400      data: {401        view: 'one',402        include: 'one,two'403      },404      components405    }).$mount()406407    vm.view = 'two'408    waitForUpdate(() => {409      assertHookCalls(one, [1, 1, 1, 1, 0])410      assertHookCalls(two, [1, 1, 1, 0, 0])411      vm.include = 'two'412    })413      .then(() => {414        assertHookCalls(one, [1, 1, 1, 1, 1])415        assertHookCalls(two, [1, 1, 1, 0, 0])416        vm.view = 'one'417      })418      .then(() => {419        assertHookCalls(one, [2, 2, 1, 1, 1])420        assertHookCalls(two, [1, 1, 1, 1, 0])421      })422      .then(done)423  })424425  it('prune cache on include/exclude change + view switch', done => {426    const vm = new Vue({427      template: `428        <div>429          <keep-alive :include="include">430            <component :is="view"></component>431          </keep-alive>432        </div>433      `,434      data: {435        view: 'one',436        include: 'one,two'437      },438      components439    }).$mount()440441    vm.view = 'two'442    waitForUpdate(() => {443      assertHookCalls(one, [1, 1, 1, 1, 0])444      assertHookCalls(two, [1, 1, 1, 0, 0])445      vm.include = 'one'446      vm.view = 'one'447    })448      .then(() => {449        assertHookCalls(one, [1, 1, 2, 1, 0])450        // two should be pruned451        assertHookCalls(two, [1, 1, 1, 1, 1])452      })453      .then(done)454  })455456  it('should not prune currently active instance', done => {457    const vm = new Vue({458      template: `459        <div>460          <keep-alive :include="include">461            <component :is="view"></component>462          </keep-alive>463        </div>464      `,465      data: {466        view: 'one',467        include: 'one,two'468      },469      components470    }).$mount()471472    vm.include = 'two'473    waitForUpdate(() => {474      assertHookCalls(one, [1, 1, 1, 0, 0])475      assertHookCalls(two, [0, 0, 0, 0, 0])476      vm.view = 'two'477    })478      .then(() => {479        assertHookCalls(one, [1, 1, 1, 0, 1])480        assertHookCalls(two, [1, 1, 1, 0, 0])481      })482      .then(done)483  })484485  // #3882486  it('deeply nested keep-alive should be destroyed properly', done => {487    one.template = `<div><keep-alive><two></two></keep-alive></div>`488    one.components = { two }489    const vm = new Vue({490      template: `<div><parent v-if="ok"></parent></div>`,491      data: { ok: true },492      components: {493        parent: {494          template: `<div><keep-alive><one></one></keep-alive></div>`,495          components: { one }496        }497      }498    }).$mount()499500    assertHookCalls(one, [1, 1, 1, 0, 0])501    assertHookCalls(two, [1, 1, 1, 0, 0])502503    vm.ok = false504    waitForUpdate(() => {505      assertHookCalls(one, [1, 1, 1, 1, 1])506      assertHookCalls(two, [1, 1, 1, 1, 1])507    }).then(done)508  })509510  // #4237511  it('should update latest props/listeners for a re-activated component', done => {512    const one = {513      props: ['prop'],514      template: `<div>one {{ prop }}</div>`515    }516    const two = {517      props: ['prop'],518      template: `<div>two {{ prop }}</div>`519    }520    const vm = new Vue({521      data: { view: 'one', n: 1 },522      template: `523        <div>524          <keep-alive>525            <component :is="view" :prop="n"></component>526          </keep-alive>527        </div>528      `,529      components: { one, two }530    }).$mount()531532    expect(vm.$el.textContent).toBe('one 1')533    vm.n++534    waitForUpdate(() => {535      expect(vm.$el.textContent).toBe('one 2')536      vm.view = 'two'537    })538      .then(() => {539        expect(vm.$el.textContent).toBe('two 2')540      })541      .then(done)542  })543544  it('max', done => {545    const spyA = vi.fn()546    const spyB = vi.fn()547    const spyC = vi.fn()548    const spyAD = vi.fn()549    const spyBD = vi.fn()550    const spyCD = vi.fn()551552    function assertCount(calls) {553      expect([554        spyA.mock.calls.length,555        spyAD.mock.calls.length,556        spyB.mock.calls.length,557        spyBD.mock.calls.length,558        spyC.mock.calls.length,559        spyCD.mock.calls.length560      ]).toEqual(calls)561    }562563    const vm = new Vue({564      template: `565        <keep-alive max="2">566          <component :is="n"></component>567        </keep-alive>568      `,569      data: {570        n: 'aa'571      },572      components: {573        aa: {574          template: '<div>a</div>',575          created: spyA,576          destroyed: spyAD577        },578        bb: {579          template: '<div>bbb</div>',580          created: spyB,581          destroyed: spyBD582        },583        cc: {584          template: '<div>ccc</div>',585          created: spyC,586          destroyed: spyCD587        }588      }589    }).$mount()590591    assertCount([1, 0, 0, 0, 0, 0])592    vm.n = 'bb'593    waitForUpdate(() => {594      assertCount([1, 0, 1, 0, 0, 0])595      vm.n = 'cc'596    })597      .then(() => {598        // should prune A because max cache reached599        assertCount([1, 1, 1, 0, 1, 0])600        vm.n = 'bb'601      })602      .then(() => {603        // B should be reused, and made latest604        assertCount([1, 1, 1, 0, 1, 0])605        vm.n = 'aa'606      })607      .then(() => {608        // C should be pruned because B was used last so C is the oldest cached609        assertCount([2, 1, 1, 0, 1, 1])610      })611      .then(done)612  })613614  it('max=1', done => {615    const spyA = vi.fn()616    const spyB = vi.fn()617    const spyC = vi.fn()618    const spyAD = vi.fn()619    const spyBD = vi.fn()620    const spyCD = vi.fn()621622    function assertCount(calls) {623      expect([624        spyA.mock.calls.length,625        spyAD.mock.calls.length,626        spyB.mock.calls.length,627        spyBD.mock.calls.length,628        spyC.mock.calls.length,629        spyCD.mock.calls.length630      ]).toEqual(calls)631    }632633    const vm = new Vue({634      template: `635        <keep-alive max="1">636          <component :is="n"></component>637        </keep-alive>638      `,639      data: {640        n: 'aa'641      },642      components: {643        aa: {644          template: '<div>a</div>',645          created: spyA,646          destroyed: spyAD647        },648        bb: {649          template: '<div>bbb</div>',650          created: spyB,651          destroyed: spyBD652        },653        cc: {654          template: '<div>ccc</div>',655          created: spyC,656          destroyed: spyCD657        }658      }659    }).$mount()660661    assertCount([1, 0, 0, 0, 0, 0])662    vm.n = 'bb'663    waitForUpdate(() => {664      // should prune A because max cache reached665      assertCount([1, 1, 1, 0, 0, 0])666      vm.n = 'cc'667    })668      .then(() => {669        // should prune B because max cache reached670        assertCount([1, 1, 1, 1, 1, 0])671        vm.n = 'bb'672      })673      .then(() => {674        // B is recreated675        assertCount([1, 1, 2, 1, 1, 1])676        vm.n = 'aa'677      })678      .then(() => {679        // B is destroyed and A recreated680        assertCount([2, 1, 2, 2, 1, 1])681      })682      .then(done)683  })684685  it('should warn unknown component inside', () => {686    new Vue({687      template: `<keep-alive><foo/></keep-alive>`688    }).$mount()689    expect(`Unknown custom element: <foo>`).toHaveBeenWarned()690  })691692  // #6938693  it('should not cache anonymous component when include is specified', done => {694    const Foo = {695      name: 'foo',696      template: `<div>foo</div>`,697      created: vi.fn()698    }699700    const Bar = {701      template: `<div>bar</div>`,702      created: vi.fn()703    }704705    const Child = {706      functional: true,707      render(h, ctx) {708        return h(ctx.props.view ? Foo : Bar)709      }710    }711712    const vm = new Vue({713      template: `714        <keep-alive include="foo">715          <child :view="view"></child>716        </keep-alive>717      `,718      data: {719        view: true720      },721      components: { Child }722    }).$mount()723724    function assert(foo, bar) {725      expect(Foo.created.mock.calls.length).toBe(foo)726      expect(Bar.created.mock.calls.length).toBe(bar)727    }728729    expect(vm.$el.textContent).toBe('foo')730    assert(1, 0)731    vm.view = false732    waitForUpdate(() => {733      expect(vm.$el.textContent).toBe('bar')734      assert(1, 1)735      vm.view = true736    })737      .then(() => {738        expect(vm.$el.textContent).toBe('foo')739        assert(1, 1)740        vm.view = false741      })742      .then(() => {743        expect(vm.$el.textContent).toBe('bar')744        assert(1, 2)745      })746      .then(done)747  })748749  it('should cache anonymous components if include is not specified', done => {750    const Foo = {751      template: `<div>foo</div>`,752      created: vi.fn()753    }754755    const Bar = {756      template: `<div>bar</div>`,757      created: vi.fn()758    }759760    const Child = {761      functional: true,762      render(h, ctx) {763        return h(ctx.props.view ? Foo : Bar)764      }765    }766767    const vm = new Vue({768      template: `769        <keep-alive>770          <child :view="view"></child>771        </keep-alive>772      `,773      data: {774        view: true775      },776      components: { Child }777    }).$mount()778779    function assert(foo, bar) {780      expect(Foo.created.mock.calls.length).toBe(foo)781      expect(Bar.created.mock.calls.length).toBe(bar)782    }783784    expect(vm.$el.textContent).toBe('foo')785    assert(1, 0)786    vm.view = false787    waitForUpdate(() => {788      expect(vm.$el.textContent).toBe('bar')789      assert(1, 1)790      vm.view = true791    })792      .then(() => {793        expect(vm.$el.textContent).toBe('foo')794        assert(1, 1)795        vm.view = false796      })797      .then(() => {798        expect(vm.$el.textContent).toBe('bar')799        assert(1, 1)800      })801      .then(done)802  })803804  // #7105805  it('should not destroy active instance when pruning cache', done => {806    const Foo = {807      template: `<div>foo</div>`,808      destroyed: vi.fn()809    }810    const vm = new Vue({811      template: `812        <div>813          <keep-alive :include="include">814            <foo/>815          </keep-alive>816        </div>817      `,818      data: {819        include: ['foo']820      },821      components: { Foo }822    }).$mount()823    // condition: a render where a previous component is reused824    vm.include = ['foo']825    waitForUpdate(() => {826      vm.include = ['']827    })828      .then(() => {829        expect(Foo.destroyed).not.toHaveBeenCalled()830      })831      .then(done)832  })833})

Findings

✓ No findings reported for this file.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.