Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to add new methods to Wrapper #611

Closed
ascii-soup opened this issue May 15, 2018 · 9 comments
Closed

Add ability to add new methods to Wrapper #611

ascii-soup opened this issue May 15, 2018 · 9 comments

Comments

@ascii-soup
Copy link

ascii-soup commented May 15, 2018

What problem does this feature solve?

I think it would be nice to be able to extend the Wrapper class with new methods (helper methods, effectively) to increase readability / reduce test-specific boilerplate.

For example, using Jest, I've added some new matchers to keep things clean whilst testing components. Contrived example below:

test('renders a table cell', () => {
    const component = shallow(MyComponent);
    expect(component).toHaveElement('td');
});

It would be nice to be able to add similar helper methods to the Wrapper object so that tests could be written at a slightly higher level of abstraction. See a before and after below:

// Before
test('hides icon on hover', () => {
    const component = shallow(MyComponent);
    component.trigger('mouseover');
    expect(component.find('i').isVisible()).toBe(false);
});

// After
test('hides icon on hover', () => {
    const component = shallow(MyComponent);
    expect(component.hover().find('i').isVisible()).toBe(false);
});

// With both extended Wrapper and custom Jest matcher
test('hides icon on hover', () => {
    const component = shallow(MyComponent);
    expect(component.hover()).not.toHaveVisible('i');
});

Although the above examples are somewhat contrived for the sake of example, I think they lead to test code that more clearly documents the component under test.

What does the proposed API look like?

As for an example of how this might look in terms of the API, I'm not too sure; Wrapper is not exposed for reasons I think I agree with (from other issues on the topic). An exported function that provides this ability might work? I'm not much of a JS expert so please forgive me if I don't understand why the following example is unfeasible.

import { extend_wrapper } from "@vue/test-utils";

extend_wrapper({
    hover() {
        return this.trigger('mouseover');
    }
});

I'm interested to hear thoughts on this, thanks for taking the time to read.

@eddyerburgh
Copy link
Member

I can see that this would be useful, but it also encourages adding non-standard methods to the wrapper object.

I'm not convinced either way and would like to hear other opinions.

@38elements
Copy link
Contributor

I read the example of usage which adds an alias of wrapper.trigger('mouseover').
I do not think this feature is necessary.
But, I think if many people need to add some methods to Wrapper, it is necessary.
I think that exporting Wrapper class as below is better than this approach, since it is simple.

#328 (comment)

@shortdiv
Copy link
Contributor

I'm not sure I see the use case beyond method extends being aliases to existing methods supported by test-utils as @38elements mentioned. If there are methods that aren't currently supported by test utils, we should add them to the library instead of allowing wrapper to be extended.

I also don't think a wrapper extend is more readable or concise, and as @eddyerburgh mentioned it could result in users adding non standard methods to wrapper object, which could result in weird/unexpected bugs.

@ascii-soup
Copy link
Author

I realise my examples were quite brief. Perhaps they don't illustrate the usefulness very well.

It's hard to come up with examples that don't rely on code that I cannot share, but I'll try something a little closer to what I'm trying to achieve.

Imagine you have a particular interaction that requires a couple of steps. In domain terms, this is 'add a record'. In terms of the component, it requires hovering to show some buttons, then clicking a button.

Part of the requirement is that once an interaction like this has taken place (say for example it opens a popup window) the buttons should remain showing (as if hovered) and the row should remain highlighted.

Two tests I'd like to write would be:

  1. Keep showing the buttons after mouseover, click, mouseout
  2. Keep the row highlighted after mouseover, click, mouseout

Example code for the two tests below:

test('keeps hover open whilst adding a record', () => {
    const component = shallow(MyComponent);

    component.trigger('mouseover');
    component.find('.add-btn').trigger('click');
    component.trigger('mouseout');

    expect(component.find('.hidden-buttons').isVisible()).toBe(true);
});

test('keeps row highlighted whilst adding a record', () => {
    const component = shallow(MyComponent);

    component.trigger('mouseover');
    component.find('.add-btn').trigger('click');
    component.trigger('mouseout');

    expect(component.find('tr').classes()).toContain('highlight');
});

The mechanics of how the 'add record' popup is shown don't really matter to the test; they are at too low a level of abstraction. We wish to be able to say "When I start adding a record, the row should be highlighted" or "When I start adding a record, the buttons should remain showing".

If, for example, the component were to change slightly and the interaction now requires also clicking on a menu item that drops down from the button, all the tests that specify these mechanics will need to be updated.

Consider the below as an alternative (including custom Jest matchers as before):

test('keeps hover open whilst adding a record', () => {
    const component = shallow(MyComponent);
    
    component.beginAddingRecord();
    
    expect(component).toHaveVisible('.hidden-buttons');
});

test('keeps row highlighted whilst adding a record', () => {
    const component = shallow(MyComponent);
    
    component.beginAddingRecord();
    
    expect(component.find('tr')).toBeHighlighted();
});

In my opinion these are nicer tests since the operate at a higher level of abstraction and are written in terms of the component's desired behaviour, rather than in terms of the mechanics of how that behaviour is triggered.

Hopefully these examples show that whilst it is effectively aliasing, it can be used to help abstract away 'details' that may not be important to the test you are writing.

@shortdiv
Copy link
Contributor

Thanks for the example! I think I have a better understanding of what you're proposing. I'm still not 100% convinced of having a method alias like beginAddingRecord to batch mouse events. However, I think having the ability to add custom matchers like toBeHighlighted and toHaveVisible makes a lot of sense and would be pretty useful.

@ascii-soup
Copy link
Author

Let's not get too wrapped up in the fact that I've used mouse events in my examples. You could just as easily add methods that set props, introspect state, do anything that you would/could do with a wrapper normally. The key point is that it allows me to add methods that are applicable to my domain, not general usage.

const component = shallow(MyInvoiceComponent);
component.pay(); // This is a method on the component itself
let isBilled = component.isBilled(); // This is a domain-specific test helper on the wrapper
expect(isBilled).toBeTruthy();
const component = shallow(MyEntityComponent);
let isBeingEdited = component.isBeingEdited(); // This is a domain-specific test helper on the wrapper
expect(isBeingEdited).toBeTruthy();
const component = shallow(MyTreeLikeComponent);
let hasOpenParent = component.hasOpenParent(); // This is a domain-specific test helper on the wrapper
expect(hasOpenParent).toBeFalsy();
const component = shallow(MyComponentThatCanBeInvalidated);

// Without helper we are relying on component internals in each test
component.setProps({invalid: true, error_message: 'Testing invalidation'});

// With helper, we can concentrate on behaviour
component.invalidateWithMessage('Testing invalidation');

// We can also abstract more complex operations that are specific to our components
let popupHasClosed = !component.findPopup().visible();
expect(popupHasClosed).toBeTruthy();

Another use case, besides in-house development, would be someone who authors a library of Vue components (think Vuetify or similar) that have particular attributes, say, every component can be either large or small, fixed or floating, and can have an error state.

It would be nice if the component library authors could provide another package, my-vue-components-library-vue-test-utils-wrappers with handy methods like isFloating(), isSmall(), hasErrors() etc to be used within your own projects that utilise these components.

I'm happy to keep monkey-patching these sort of helpers in as I do at the moment by wrapping 'shallow()' and just adding the methods to the returned wrapper object, but it would be nice to have an official mechanism to do so, particularly because I can then just 'drop-in' extra methods at the bootstrap stage, rather than having to modify my tests to use my own wrapper functions.

@eddyerburgh
Copy link
Member

The next beta will export Wrapper and WrapperArray, so you can add properties to their prototype:

import { Wrapper } from '@vue/test-utils'

Wrapper.prototype.someCustomMethod = () => {}

I'm going to leave it undocumented for now, because I don't think we should encourage most users to add custom methods.

@begueradj
Copy link

For a <v-tooltip /> component, how to access the displayed information (text) when we trigger the mousenter event ?

@afontcu
Copy link
Member

afontcu commented Jan 29, 2020

Hi @begueradj! The issue was closed a year and a half ago, so it would be way better if you could open up a new one if you are facing an issue.

If you have a general question, it would be better if you could submit it to the forum or the official chat.

Thanks! :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants