|
1 | 1 | // TODO: port tests from packages/runtime-core/__tests__/hmr.spec.ts |
2 | 2 |
|
3 | | -import { type HMRRuntime, ref } from '@vue/runtime-dom' |
| 3 | +import { |
| 4 | + type HMRRuntime, |
| 5 | + nextTick, |
| 6 | + ref, |
| 7 | + toDisplayString, |
| 8 | +} from '@vue/runtime-dom' |
4 | 9 | import { makeRender } from './_utils' |
5 | 10 | import { |
6 | 11 | child, |
7 | 12 | createComponent, |
| 13 | + createComponentWithFallback, |
| 14 | + createInvoker, |
| 15 | + createSlot, |
| 16 | + defineVaporComponent, |
| 17 | + delegateEvents, |
| 18 | + next, |
8 | 19 | renderEffect, |
| 20 | + setInsertionState, |
9 | 21 | setText, |
10 | 22 | template, |
| 23 | + txt, |
| 24 | + withVaporCtx, |
11 | 25 | } from '@vue/runtime-vapor' |
12 | 26 |
|
13 | 27 | declare var __VUE_HMR_RUNTIME__: HMRRuntime |
14 | | -const { createRecord, reload } = __VUE_HMR_RUNTIME__ |
| 28 | +const { createRecord, rerender, reload } = __VUE_HMR_RUNTIME__ |
15 | 29 |
|
16 | 30 | const define = makeRender() |
17 | 31 |
|
| 32 | +const triggerEvent = (type: string, el: Element) => { |
| 33 | + const event = new Event(type, { bubbles: true }) |
| 34 | + el.dispatchEvent(event) |
| 35 | +} |
| 36 | +delegateEvents('click') |
| 37 | + |
| 38 | +beforeEach(() => { |
| 39 | + document.body.innerHTML = '' |
| 40 | +}) |
| 41 | + |
18 | 42 | describe('hot module replacement', () => { |
| 43 | + test('inject global runtime', () => { |
| 44 | + expect(createRecord).toBeDefined() |
| 45 | + expect(rerender).toBeDefined() |
| 46 | + expect(reload).toBeDefined() |
| 47 | + }) |
| 48 | + |
| 49 | + test('createRecord', () => { |
| 50 | + expect(createRecord('test1', {})).toBe(true) |
| 51 | + // if id has already been created, should return false |
| 52 | + expect(createRecord('test1', {})).toBe(false) |
| 53 | + }) |
| 54 | + |
| 55 | + test('rerender', async () => { |
| 56 | + const root = document.createElement('div') |
| 57 | + const parentId = 'test2-parent' |
| 58 | + const childId = 'test2-child' |
| 59 | + document.body.appendChild(root) |
| 60 | + |
| 61 | + const Child = defineVaporComponent({ |
| 62 | + __hmrId: childId, |
| 63 | + render() { |
| 64 | + const n1 = template('<div></div>', true)() as any |
| 65 | + setInsertionState(n1, null, true) |
| 66 | + createSlot('default', null) |
| 67 | + return n1 |
| 68 | + }, |
| 69 | + }) |
| 70 | + createRecord(childId, Child as any) |
| 71 | + |
| 72 | + const Parent = defineVaporComponent({ |
| 73 | + __hmrId: parentId, |
| 74 | + setup() { |
| 75 | + const count = ref(0) |
| 76 | + return { count } |
| 77 | + }, |
| 78 | + render(ctx) { |
| 79 | + const n3 = template('<div> </div>', true)() as any |
| 80 | + const n0 = child(n3) as any |
| 81 | + setInsertionState(n3, 1, true) |
| 82 | + createComponent(Child, null, { |
| 83 | + default: withVaporCtx(() => { |
| 84 | + const n1 = template(' ')() as any |
| 85 | + renderEffect(() => setText(n1, toDisplayString(ctx.count))) |
| 86 | + return n1 |
| 87 | + }), |
| 88 | + }) |
| 89 | + n3.$evtclick = createInvoker(() => ctx.count++) |
| 90 | + renderEffect(() => setText(n0, toDisplayString(ctx.count))) |
| 91 | + return n3 |
| 92 | + }, |
| 93 | + }) |
| 94 | + createRecord(parentId, Parent as any) |
| 95 | + |
| 96 | + // render(h(Parent), root) |
| 97 | + const { mount } = define(Parent).create() |
| 98 | + mount(root) |
| 99 | + expect(root.innerHTML).toBe(`<div>0<div>0<!--slot--></div></div>`) |
| 100 | + |
| 101 | + // Perform some state change. This change should be preserved after the |
| 102 | + // re-render! |
| 103 | + // triggerEvent(root.children[0] as TestElement, 'click') |
| 104 | + triggerEvent('click', root.children[0]) |
| 105 | + await nextTick() |
| 106 | + expect(root.innerHTML).toBe(`<div>1<div>1<!--slot--></div></div>`) |
| 107 | + |
| 108 | + // Update text while preserving state |
| 109 | + rerender(parentId, (ctx: any) => { |
| 110 | + const n3 = template('<div> </div>', true)() as any |
| 111 | + const n0 = child(n3) as any |
| 112 | + setInsertionState(n3, 1, true) |
| 113 | + createComponent(Child, null, { |
| 114 | + default: withVaporCtx(() => { |
| 115 | + const n1 = template(' ')() as any |
| 116 | + renderEffect(() => setText(n1, toDisplayString(ctx.count))) |
| 117 | + return n1 |
| 118 | + }), |
| 119 | + }) |
| 120 | + n3.$evtclick = createInvoker(() => ctx.count++) |
| 121 | + renderEffect(() => setText(n0, toDisplayString(ctx.count) + '!')) |
| 122 | + return n3 |
| 123 | + }) |
| 124 | + expect(root.innerHTML).toBe(`<div>1!<div>1<!--slot--></div></div>`) |
| 125 | + |
| 126 | + // Should force child update on slot content change |
| 127 | + rerender(parentId, (ctx: any) => { |
| 128 | + const n3 = template('<div> </div>', true)() as any |
| 129 | + const n0 = child(n3) as any |
| 130 | + setInsertionState(n3, 1, true) |
| 131 | + createComponent(Child, null, { |
| 132 | + default: withVaporCtx(() => { |
| 133 | + const n1 = template(' ')() as any |
| 134 | + renderEffect(() => setText(n1, toDisplayString(ctx.count) + '!')) |
| 135 | + return n1 |
| 136 | + }), |
| 137 | + }) |
| 138 | + n3.$evtclick = createInvoker(() => ctx.count++) |
| 139 | + renderEffect(() => setText(n0, toDisplayString(ctx.count) + '!')) |
| 140 | + return n3 |
| 141 | + }) |
| 142 | + expect(root.innerHTML).toBe(`<div>1!<div>1!<!--slot--></div></div>`) |
| 143 | + |
| 144 | + // Should force update element children despite block optimization |
| 145 | + rerender(parentId, (ctx: any) => { |
| 146 | + const n5 = template('<div> <span> </span></div>', true)() as any |
| 147 | + const n0 = child(n5) as any |
| 148 | + const n1 = next(n0) as any |
| 149 | + setInsertionState(n5, 2, true) |
| 150 | + createComponentWithFallback(Child, null, { |
| 151 | + default: withVaporCtx(() => { |
| 152 | + const n2 = template(' ')() as any |
| 153 | + renderEffect(() => setText(n2, toDisplayString(ctx.count) + '!')) |
| 154 | + return n2 |
| 155 | + }), |
| 156 | + }) |
| 157 | + const x1 = txt(n1) as any |
| 158 | + n5.$evtclick = createInvoker(() => ctx.count++) |
| 159 | + renderEffect(() => { |
| 160 | + const count = ctx.count |
| 161 | + setText(n0, toDisplayString(count)) |
| 162 | + setText(x1, toDisplayString(count)) |
| 163 | + }) |
| 164 | + return n5 |
| 165 | + }) |
| 166 | + expect(root.innerHTML).toBe( |
| 167 | + `<div>1<span>1</span><div>1!<!--slot--></div></div>`, |
| 168 | + ) |
| 169 | + |
| 170 | + // Should force update child slot elements |
| 171 | + rerender(parentId, (ctx: any) => { |
| 172 | + const n2 = template('<div></div>', true)() as any |
| 173 | + setInsertionState(n2, null, true) |
| 174 | + createComponentWithFallback(Child, null, { |
| 175 | + default: withVaporCtx(() => { |
| 176 | + const n0 = template('<span> </span>')() as any |
| 177 | + const x0 = txt(n0) as any |
| 178 | + renderEffect(() => setText(x0, toDisplayString(ctx.count))) |
| 179 | + return n0 |
| 180 | + }), |
| 181 | + }) |
| 182 | + n2.$evtclick = createInvoker(() => ctx.count++) |
| 183 | + return n2 |
| 184 | + }) |
| 185 | + expect(root.innerHTML).toBe( |
| 186 | + `<div><div><span>1</span><!--slot--></div></div>`, |
| 187 | + ) |
| 188 | + }) |
| 189 | + |
| 190 | + test('reload', async () => { |
| 191 | + // const root = nodeOps.createElement('div') |
| 192 | + // const childId = 'test3-child' |
| 193 | + // const unmountSpy = vi.fn() |
| 194 | + // const mountSpy = vi.fn() |
| 195 | + // const Child: ComponentOptions = { |
| 196 | + // __hmrId: childId, |
| 197 | + // data() { |
| 198 | + // return { count: 0 } |
| 199 | + // }, |
| 200 | + // unmounted: unmountSpy, |
| 201 | + // render: compileToFunction(`<div @click="count++">{{ count }}</div>`), |
| 202 | + // } |
| 203 | + // createRecord(childId, Child) |
| 204 | + // const Parent: ComponentOptions = { |
| 205 | + // render: () => h(Child), |
| 206 | + // } |
| 207 | + // render(h(Parent), root) |
| 208 | + // expect(serializeInner(root)).toBe(`<div>0</div>`) |
| 209 | + // reload(childId, { |
| 210 | + // __hmrId: childId, |
| 211 | + // data() { |
| 212 | + // return { count: 1 } |
| 213 | + // }, |
| 214 | + // mounted: mountSpy, |
| 215 | + // render: compileToFunction(`<div @click="count++">{{ count }}</div>`), |
| 216 | + // }) |
| 217 | + // await nextTick() |
| 218 | + // expect(serializeInner(root)).toBe(`<div>1</div>`) |
| 219 | + // expect(unmountSpy).toHaveBeenCalledTimes(1) |
| 220 | + // expect(mountSpy).toHaveBeenCalledTimes(1) |
| 221 | + }) |
| 222 | + |
19 | 223 | test('child reload + parent reload', async () => { |
20 | 224 | const root = document.createElement('div') |
21 | 225 | const childId = 'test1-child-reload' |
|
0 commit comments