From 9f343b049a815c9832445067b9a419fbc448d428 Mon Sep 17 00:00:00 2001 From: Lachlan Miller Date: Fri, 25 Sep 2020 19:25:24 +1000 Subject: [PATCH 1/7] feat: isVisible --- src/domWrapper.ts | 5 ++++ src/utils/isElementVisible.ts | 33 ++++++++++++++++++++++++ tests/isVisible.spec.ts | 48 +++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 src/utils/isElementVisible.ts create mode 100644 tests/isVisible.spec.ts diff --git a/src/domWrapper.ts b/src/domWrapper.ts index 503346399..b72f3d29a 100644 --- a/src/domWrapper.ts +++ b/src/domWrapper.ts @@ -2,6 +2,7 @@ import { nextTick } from 'vue' import { createWrapperError } from './errorWrapper' import { TriggerOptions, createDOMEvent } from './createDomEvent' +import { isElementVisible } from './utils/isElementVisible' export class DOMWrapper { element: ElementType @@ -36,6 +37,10 @@ export class DOMWrapper { return true } + isVisible() { + return isElementVisible(this.element) + } + text() { return this.element.textContent?.trim() } diff --git a/src/utils/isElementVisible.ts b/src/utils/isElementVisible.ts new file mode 100644 index 000000000..575cff72f --- /dev/null +++ b/src/utils/isElementVisible.ts @@ -0,0 +1,33 @@ +/*! + * isElementVisible + * Adapted from https://github.com/testing-library/jest-dom + * Licensed under the MIT License. + */ + +type StylableElement = HTMLElement | SVGElement + +function isStyleVisible(element: T) { + const { display, visibility, opacity } = element.style + return ( + display !== 'none' && + visibility !== 'hidden' && + visibility !== 'collapse' && + opacity !== '0' + ) +} + +function isAttributeVisible(element: T) { + return ( + !element.hasAttribute('hidden') && + (element.nodeName === 'DETAILS' ? element.hasAttribute('open') : true) + ) +} + +export function isElementVisible(element: T) { + return ( + element.nodeName !== '#comment' && + isStyleVisible(element) && + isAttributeVisible(element) && + (!element.parentElement || isElementVisible(element.parentElement)) + ) +} diff --git a/tests/isVisible.spec.ts b/tests/isVisible.spec.ts new file mode 100644 index 000000000..008ed3194 --- /dev/null +++ b/tests/isVisible.spec.ts @@ -0,0 +1,48 @@ +import { mount } from '../src' + +describe('isVisible', () => { + const Comp = { + template: `
`, + props: { + show: { + type: Boolean + } + } + } + + it('returns false when element hidden via v-show', () => { + const wrapper = mount(Comp, { + props: { + show: false + } + }) + + expect(wrapper.find('span').isVisible()).toBe(false) + }) + + it('returns true when element is visible via v-show', () => { + const wrapper = mount(Comp, { + props: { + show: true + } + }) + + expect(wrapper.find('span').isVisible()).toBe(true) + }) + + it('element becomes hidden reactively', async () => { + const Comp = { + template: ` + + + + Item: {{ item }} + + + + `, + methods: { + add() { + this.items.push(2) + }, + remove() { + this.items.splice(1) // back to [1] + } + }, + data() { + return { + items: [1] + } + } + } + const wrapper = mount(Comp) + + expect(wrapper.html()).toContain('Item: 1') + await wrapper.find('#add').trigger('click') + expect(wrapper.html()).toContain('Item: 1') + expect(wrapper.html()).toContain('Item: 2') + await wrapper.find('#remove').trigger('click') + expect(wrapper.html()).toContain('Item: 1') + expect(wrapper.html()).not.toContain('Item: 2') + }) }) From 5f375c5ee1f73cae56a197afaac3dcc7a8ca901f Mon Sep 17 00:00:00 2001 From: Lachlan Miller Date: Fri, 25 Sep 2020 21:54:35 +1000 Subject: [PATCH 3/7] feat: allow opting out of stubbed transitions --- src/config.ts | 11 ++++++++--- src/mount.ts | 9 +++------ src/stubs.ts | 6 +++++- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/config.ts b/src/config.ts index 48eaad5cb..c7e9c31b6 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,6 +1,6 @@ +import { ComponentPublicInstance } from 'vue' import { GlobalMountOptions } from './types' import { VueWrapper } from './vueWrapper' -import { ComponentPublicInstance } from 'vue' interface GlobalConfigOptions { global: GlobalMountOptions @@ -19,7 +19,7 @@ interface Plugin { } class Pluggable { - installedPlugins = [] as Array + installedPlugins: Plugin[] = [] install( handler: ( @@ -55,7 +55,12 @@ class Pluggable { } export const config: GlobalConfigOptions = { - global: {}, + global: { + stubs: { + transition: true, + 'transition-group': true + } + }, plugins: { VueWrapper: new Pluggable(), DOMWrapper: new Pluggable() diff --git a/src/mount.ts b/src/mount.ts index ecc7d376f..ada6d81b5 100644 --- a/src/mount.ts +++ b/src/mount.ts @@ -350,12 +350,9 @@ export function mount( app.mixin(attachEmitListener()) // stubs - if (global.stubs || options?.shallow) { - stubComponents(global.stubs, options?.shallow) - } else { - // still apply default stub of Transition and Transition Group - stubComponents() - } + // even if we are using `mount`, we will still + // stub out Transition and Transition Group by default. + stubComponents(global.stubs, options?.shallow) // mount the app! const vm = app.mount(el) diff --git a/src/stubs.ts b/src/stubs.ts index 82ef1374a..d49348ddf 100644 --- a/src/stubs.ts +++ b/src/stubs.ts @@ -92,7 +92,11 @@ export function stubComponents( // 1. a HTML tag (div, span...) // 2. An object of component options, such as { name: 'foo', render: [Function], props: {...} } // Depending what it is, we do different things. - if (type === Transition || type === TransitionGroup) { + if (type === Transition && stubs['transition']) { + return [createTransitionStub({ props: undefined }), undefined, children] + } + + if (type === TransitionGroup && stubs['transition-group']) { return [createTransitionStub({ props: undefined }), undefined, children] } From 3b4c1365ee1f08f26b35baa486e44b426eddc350 Mon Sep 17 00:00:00 2001 From: Lachlan Miller Date: Fri, 25 Sep 2020 22:40:21 +1000 Subject: [PATCH 4/7] feat: allow opting out of stubbed transition and transition-group --- src/stubs.ts | 24 +++++++-- tests/mountingOptions/stubs.global.spec.ts | 58 +++++++++++++++++++++- 2 files changed, 75 insertions(+), 7 deletions(-) diff --git a/src/stubs.ts b/src/stubs.ts index d49348ddf..55a856671 100644 --- a/src/stubs.ts +++ b/src/stubs.ts @@ -36,12 +36,15 @@ const createStub = ({ name, props }: StubOptions): ComponentOptions => { return defineComponent({ name: name || anonName, render, props }) } -const createTransitionStub = ({ props }: StubOptions): ComponentOptions => { +const createTransitionStub = ({ + name, + props +}: StubOptions): ComponentOptions => { const render = (ctx: ComponentPublicInstance) => { - return h('transition-stub', {}, ctx.$slots) + return h(name, {}, ctx.$slots) } - return defineComponent({ name: 'transition-stub', render, props }) + return defineComponent({ name, render, props }) } const resolveComponentStubByName = ( @@ -93,11 +96,22 @@ export function stubComponents( // 2. An object of component options, such as { name: 'foo', render: [Function], props: {...} } // Depending what it is, we do different things. if (type === Transition && stubs['transition']) { - return [createTransitionStub({ props: undefined }), undefined, children] + return [ + createTransitionStub({ name: 'transition-stub', props: undefined }), + undefined, + children + ] } if (type === TransitionGroup && stubs['transition-group']) { - return [createTransitionStub({ props: undefined }), undefined, children] + return [ + createTransitionStub({ + name: 'transition-group-stub', + props: undefined + }), + undefined, + children + ] } if ( diff --git a/tests/mountingOptions/stubs.global.spec.ts b/tests/mountingOptions/stubs.global.spec.ts index 2d5f7bbe6..1fe7280fb 100644 --- a/tests/mountingOptions/stubs.global.spec.ts +++ b/tests/mountingOptions/stubs.global.spec.ts @@ -6,12 +6,13 @@ import ComponentWithoutName from '../components/ComponentWithoutName.vue' import ComponentWithSlots from '../components/ComponentWithSlots.vue' describe('mounting options: stubs', () => { + let configStubsSave = config.global.stubs beforeEach(() => { - config.global.stubs = {} + config.global.stubs = configStubsSave }) afterEach(() => { - config.global.stubs = {} + config.global.stubs = configStubsSave }) it('handles Array syntax', () => { @@ -308,6 +309,59 @@ describe('mounting options: stubs', () => { expect(wrapper.html()).toBe('') }) + it('stubs transition by default', () => { + const Comp = { + template: `
` + } + const wrapper = mount(Comp) + + expect(wrapper.html()).toBe( + '
' + ) + }) + + it('opts out of stubbing transition by default', () => { + const Comp = { + template: `
` + } + const wrapper = mount(Comp, { + global: { + stubs: { + transition: false + } + } + }) + + // Vue removes at run-time and does it's magic, so should not + // appear in the html when it isn't stubbed. + expect(wrapper.html()).toBe('
') + }) + + it('opts out of stubbing transition-group by default', () => { + const Comp = { + template: `
` + } + const wrapper = mount(Comp, { + global: { + stubs: { + 'transition-group': false + } + } + }) + + // Vue removes at run-time and does it's magic, so should not + // appear in the html when it isn't stubbed. + expect(wrapper.html()).toBe('
') + }) + + it('stubs transition-group by default', () => { + const Comp = { + template: `
` + } + const wrapper = mount(Comp) + expect(wrapper.find('#content').exists()).toBe(true) + }) + describe('stub slots', () => { const Component = { name: 'Parent', From 912f4bec0ac1e1c52776f01fd0beee99300eaf28 Mon Sep 17 00:00:00 2001 From: Lachlan Miller Date: Fri, 25 Sep 2020 22:53:32 +1000 Subject: [PATCH 5/7] refactor: add comments --- src/stubs.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/stubs.ts b/src/stubs.ts index 55a856671..42ad49217 100644 --- a/src/stubs.ts +++ b/src/stubs.ts @@ -91,10 +91,8 @@ export function stubComponents( transformVNodeArgs((args, instance: ComponentInternalInstance | null) => { const [nodeType, props, children, patchFlag, dynamicProps] = args const type = nodeType as VNodeTypes - // args[0] can either be: - // 1. a HTML tag (div, span...) - // 2. An object of component options, such as { name: 'foo', render: [Function], props: {...} } - // Depending what it is, we do different things. + + // stub transition by default via config.global.stubs if (type === Transition && stubs['transition']) { return [ createTransitionStub({ name: 'transition-stub', props: undefined }), @@ -103,6 +101,7 @@ export function stubComponents( ] } + // stub transition-group by default via config.global.stubs if (type === TransitionGroup && stubs['transition-group']) { return [ createTransitionStub({ @@ -114,6 +113,10 @@ export function stubComponents( ] } + // args[0] can either be: + // 1. a HTML tag (div, span...) + // 2. An object of component options, such as { name: 'foo', render: [Function], props: {...} } + // Depending what it is, we do different things. if ( isHTMLElement(type) || isCommentOrFragment(type) || From f1144a8b5ef57ee148af6c2403405fc30190780d Mon Sep 17 00:00:00 2001 From: Lachlan Miller Date: Fri, 25 Sep 2020 23:31:12 +1000 Subject: [PATCH 6/7] feat: allow passing renderDefaultSlot to config mounting options --- src/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types.ts b/src/types.ts index 890a412ab..df204dfaf 100644 --- a/src/types.ts +++ b/src/types.ts @@ -28,6 +28,7 @@ export type GlobalMountOptions = { components?: Record directives?: Record stubs?: Record + renderStubDefaultSlot?: boolean } export interface VueWrapperMeta { From eeb74c354c09f1859f28768adb74083b6ee1714e Mon Sep 17 00:00:00 2001 From: Lachlan Miller Date: Sat, 26 Sep 2020 18:19:09 +1000 Subject: [PATCH 7/7] test: add tests for parent --- tests/isVisible.spec.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/isVisible.spec.ts b/tests/isVisible.spec.ts index 64f657a0d..bce409303 100644 --- a/tests/isVisible.spec.ts +++ b/tests/isVisible.spec.ts @@ -30,6 +30,15 @@ describe('isVisible', () => { expect(wrapper.find('span').isVisible()).toBe(true) }) + it('returns false when element parent is invisible via v-show', () => { + const Comp = { + template: `
` + } + const wrapper = mount(Comp) + + expect(wrapper.find('span').isVisible()).toBe(false) + }) + it('element becomes hidden reactively', async () => { const Comp = { template: `