From 80acfa5030a0f54c27987e00e6502d64e41f8aa0 Mon Sep 17 00:00:00 2001 From: Doctor Wu Date: Sun, 16 Jun 2024 23:08:18 +0800 Subject: [PATCH] test(runtime-vapor): add directive test case (#231) --- .../__tests__/directives/directives.spec.ts | 295 ++++++++++++++++++ 1 file changed, 295 insertions(+) create mode 100644 packages/runtime-vapor/__tests__/directives/directives.spec.ts diff --git a/packages/runtime-vapor/__tests__/directives/directives.spec.ts b/packages/runtime-vapor/__tests__/directives/directives.spec.ts new file mode 100644 index 000000000..881d80dc5 --- /dev/null +++ b/packages/runtime-vapor/__tests__/directives/directives.spec.ts @@ -0,0 +1,295 @@ +import { ref } from '@vue/reactivity' +import { + type ComponentInternalInstance, + type DirectiveBinding, + type DirectiveHook, + createComponent, + getCurrentInstance, + nextTick, + renderEffect, + setText, + template, + withDirectives, +} from '@vue/runtime-vapor' +import { makeRender } from '../_utils' + +const define = makeRender() + +describe('directives', () => { + it('should work', async () => { + const count = ref(0) + + function assertBindings(binding: DirectiveBinding) { + expect(binding.value).toBe(count.value) + expect(binding.arg).toBe('foo') + expect(binding.instance).toBe(_instance) + expect(binding.modifiers && binding.modifiers.ok).toBe(true) + } + + const beforeMount = vi.fn(((el, binding) => { + expect(el.tagName).toBe('DIV') + // should not be inserted yet + expect(el.parentNode).toBe(null) + expect(root.children.length).toBe(0) + + assertBindings(binding) + + // expect(vnode).toBe(_vnode) + // expect(prevVNode).toBe(null) + }) as DirectiveHook) + + const mounted = vi.fn(((el, binding) => { + expect(el.tagName).toBe('DIV') + // should be inserted now + expect(el.parentNode).toBe(root) + expect(root.children[0]).toBe(el) + + assertBindings(binding) + }) as DirectiveHook) + + const beforeUpdate = vi.fn(((el, binding) => { + expect(el.tagName).toBe('DIV') + expect(el.parentNode).toBe(root) + expect(root.children[0]).toBe(el) + + // node should not have been updated yet + expect(el.firstChild?.textContent).toBe(`${count.value - 1}`) + + assertBindings(binding) + }) as DirectiveHook) + + const updated = vi.fn(((el, binding) => { + expect(el.tagName).toBe('DIV') + expect(el.parentNode).toBe(root) + expect(root.children[0]).toBe(el) + + // node should have been updated + expect(el.firstChild?.textContent).toBe(`${count.value}`) + + assertBindings(binding) + }) as DirectiveHook) + + const beforeUnmount = vi.fn(((el, binding) => { + expect(el.tagName).toBe('DIV') + // should be removed now + expect(el.parentNode).toBe(root) + expect(root.children[0]).toBe(el) + + assertBindings(binding) + }) as DirectiveHook) + + const unmounted = vi.fn(((el, binding) => { + expect(el.tagName).toBe('DIV') + // should have been removed + expect(el.parentNode).toBe(null) + expect(root.children.length).toBe(0) + + assertBindings(binding) + }) as DirectiveHook) + + const dir = { + beforeMount, + mounted, + beforeUpdate, + updated, + beforeUnmount, + unmounted, + } + + let _instance: ComponentInternalInstance | null = null + const { render } = define({ + setup() { + _instance = getCurrentInstance() + }, + render() { + const n0 = template('
')() + renderEffect(() => setText(n0, count.value)) + withDirectives(n0, [ + [ + dir, + // value + () => count.value, + // argument + 'foo', + // modifiers + { ok: true }, + ], + ]) + return n0 + }, + }) + + const root = document.createElement('div') + + render(null, root) + expect(beforeMount).toHaveBeenCalledTimes(1) + expect(mounted).toHaveBeenCalledTimes(1) + + count.value++ + await nextTick() + expect(beforeUpdate).toHaveBeenCalledTimes(1) + expect(updated).toHaveBeenCalledTimes(1) + + render(null, root) + expect(beforeUnmount).toHaveBeenCalledTimes(1) + expect(unmounted).toHaveBeenCalledTimes(1) + }) + + it('should work with a function directive', async () => { + const count = ref(0) + + function assertBindings(binding: DirectiveBinding) { + expect(binding.value).toBe(count.value) + expect(binding.arg).toBe('foo') + expect(binding.instance).toBe(_instance) + expect(binding.modifiers && binding.modifiers.ok).toBe(true) + } + + const fn = vi.fn(((el, binding) => { + expect(el.tagName).toBe('DIV') + expect(el.parentNode).toBe(root) + + assertBindings(binding) + }) as DirectiveHook) + + let _instance: ComponentInternalInstance | null = null + const { render } = define({ + setup() { + _instance = getCurrentInstance() + }, + render() { + const n0 = template('
')() + renderEffect(() => setText(n0, count.value)) + withDirectives(n0, [ + [ + fn, + // value + () => count.value, + // argument + 'foo', + // modifiers + { ok: true }, + ], + ]) + return n0 + }, + }) + + const root = document.createElement('div') + render(null, root) + + expect(fn).toHaveBeenCalledTimes(1) + + count.value++ + await nextTick() + expect(fn).toHaveBeenCalledTimes(2) + }) + + // #2298 + it('directive merging on component root', () => { + const d1 = { + mounted: vi.fn(), + } + const d2 = { + mounted: vi.fn(), + } + const Comp = { + render() { + const n0 = template('
')() + withDirectives(n0, [[d2]]) + return n0 + }, + } + + const { render } = define({ + name: 'App', + render() { + const n0 = createComponent(Comp) + withDirectives(n0, [[d1]]) + return n0 + }, + }) + + const root = document.createElement('div') + render(null, root) + expect(d1.mounted).toHaveBeenCalled() + expect(d2.mounted).toHaveBeenCalled() + }) + + test('should disable tracking inside directive lifecycle hooks', async () => { + const count = ref(0) + const text = ref('') + const beforeUpdate = vi.fn(() => count.value++) + + const { render } = define({ + render() { + const n0 = template('

')() + renderEffect(() => setText(n0, text.value)) + withDirectives(n0, [ + [ + { + beforeUpdate, + }, + ], + ]) + return n0 + }, + }) + + const root = document.createElement('div') + render(null, root) + expect(beforeUpdate).toHaveBeenCalledTimes(0) + expect(count.value).toBe(0) + + text.value = 'foo' + await nextTick() + expect(beforeUpdate).toHaveBeenCalledTimes(1) + expect(count.value).toBe(1) + }) + + test('should receive exposeProxy for closed instances', async () => { + let res: string + const { render } = define({ + setup(_, { expose }) { + expose({ + msg: 'Test', + }) + }, + render() { + const n0 = template('

Lore Ipsum

')() + withDirectives(n0, [ + [ + { + mounted(el, { instance }) { + res = (instance.exposed as any).msg as string + }, + }, + ], + ]) + return n0 + }, + }) + const root = document.createElement('div') + render(null, root) + expect(res!).toBe('Test') + }) + + test('should not throw with unknown directive', async () => { + const d1 = { + mounted: vi.fn(), + } + const { render } = define({ + name: 'App', + render() { + const n0 = template('
')() + // simulates the code generated on an unknown directive + withDirectives(n0, [[undefined], [d1]]) + return n0 + }, + }) + + const root = document.createElement('div') + render(null, root) + expect(d1.mounted).toHaveBeenCalled() + }) +})