Skip to content

Unable to use direct-vuex #146

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

Closed
kosciak9 opened this issue May 26, 2020 · 8 comments
Closed

Unable to use direct-vuex #146

kosciak9 opened this issue May 26, 2020 · 8 comments
Labels
bug Something isn't working

Comments

@kosciak9
Copy link

Describe the bug Because of the way store is initialized, I'm unable to use direct-vuex stores. Short summary, direct-vuex is a "wrapper" for vuex to provide typing for store. Thanks to it you can do this.$store.dispatch.myAction(myPayload) happily.

To Reproduce Steps to reproduce the behavior:

  • create store via createDirectStore from direct-vuex.
  • result of this function is already a Vuex.Store so initialization in render fails (getters should be function)

Expected behavior

I can use direct-vuex stores.

I'm skipping few sections as I already know the solution (or at least where problem originates):

Problematic code here.

if (store) {
    const Vuex = require('vuex')
    localVue.use(Vuex)
    vuexStore = new Vuex.Store(store)
  }

I do realize why it's written this way.

Question to the maintainers is, how do you want to implement it. Or whenever you want to implement it at all! There could be boolean switch in additionalOptions to skip store initialization (like initializeStore, by default set to true, to not break API), or some other options like directVuexStore which you can use to add to the Vue instance instead of plain store. I'm happy to help with this in any way you need.

@kosciak9 kosciak9 added the bug Something isn't working label May 26, 2020
@cilice
Copy link
Contributor

cilice commented May 27, 2020

@kosciak9
I think you can skip the store helper if you need more control over the situation.

const { store } = createDirectStore({
  // … store implementation here …
});

render(Comp, {}, localVue => {
  localVue.use(Vuex);

  return {
    store
  };
});

@kosciak9
Copy link
Author

Hey, you are right, it works quite well. I was trying to fight it for quite a long time and seems like I missed the most obvious answer. :)

Short code example for anyone with same issue (TypeScript, but didn't actually test it):

 it("directVuex example", async () => {
    // you have to .use(Vuex) before createDirectStore
    const localVue = createLocalVue();
    localVue.use(Vuex);

    const increment = jest.fn();
    const decrement = jest.fn();
    const { store } = createDirectStore({
      state: () => {
        return { counter: 0 };
      },
      getters: {
        counter: state => state.counter,
      },
      mutations: {
        increment,
        decrement
      },
    });

    const { getByLabelText } = render(
      ExampleComponent,
      {
        localVue,
      },
      () => ({
        store: store.original,
      })
    );

    await userEvent.click(getByLabelText("Add one"));

    expect(increment).toHaveBeenCalled();

    await userEvent.click(getByLabelText("Remove one"));

    expect(decrement).not.toHaveBeenCalled();
  });

@afontcu
Copy link
Member

afontcu commented May 27, 2020

Glad you got it working!

Instead of importing createLocalVue and passing in localVue as an option in the second parameter, you could use the callback (as pointed out by @cilice) which exposes the Vue instance for the component (source)

@kosciak9
Copy link
Author

Yeah, but you would have to use it like this:

const increment = jest.fn();
const decrement = jest.fn();

const { getByLabelText } = render(
  ExampleComponent,
  {
    localVue,
  },
  (localVue) => {
    localVue.use(Vuex);

    const { store } = createDirectStore({
      state: () => {
        return { counter: 0 };
      },
      getters: {
        counter: (state) => state.counter,
      },
      mutations: {
        increment,
        decrement,
      },
    });

    return { store: store.original };
  }
);

As .use(Vuex) gotta happen before createDirectStore. From my understanding, you're now losing access to store and cannot manipulate state directly. While I understand that it aligns with the way testing-library works and encourages you to work, we're separating our Vuex logic from our UI logic (Server and UI state) and test them separately.

@afontcu
Copy link
Member

afontcu commented May 27, 2020

Isn't the following an option? I've never used direct-vuex before, so I might be wrong:

const increment = jest.fn();
const decrement = jest.fn();

const { getByLabelText } = render(
  ExampleComponent,
  {},  // <-- without importing and passing localVue
  (vue) => {
    vue.use(Vuex);

    const { store } = createDirectStore({
      state: () => {
        return { counter: 0 };
      },
      getters: {
        counter: (state) => state.counter,
      },
      mutations: {
        increment,
        decrement,
      },
    });

    return { store: store.original };
  }
);

@kosciak9
Copy link
Author

Seems like we have same idea :D I accidentally didn't remove localVue from options but otherwise code is the same. What about my concerns? Looking at what's render is returning, seems like store is not directly accessible anymore.

@cilice
Copy link
Contributor

cilice commented May 27, 2020

@kosciak9

Besides that, I’d actually advice against doing tests like this. Here, you hardcouple your implementation with the output.

What if you rename your action at some point to incrementBy? The important unit here is the Dom output and the caused side effects, like requests. Handling of Vuex happens totally inside the component.

Also, I’m not suggesting not to test your store at all - I think it deserves to be tested - just not within the component.

The value lies in the output and expects on them in the Dom and not an the Vuex actions called.

@kosciak9
Copy link
Author

kosciak9 commented May 27, 2020

While I understand your point of view and also dislike this way of testing, sometimes you have to draw a line between units that you're testing, even if those units are quite huge.

In our case we abstracted any communication with APIs to Vuex, and provide just one or two actions that will check input and refetch accordingly. UI components are decoupled from anything besides looking at fetchState and actual data. That's why mocked stores are mostly just one getter and one action. It's a more complicated props setup, that allows us to do this.$store.direct stuff.

I'm not sure if that's the greatest approach but it allows us to think in smaller blocks (UI, Server) and separate logic.

We do have tests that use our "real" store the way user is using them, but such a big test failing won't give you quick insight into what have you actually broke.

It's like they say in Python's Zen:
Special cases aren't special enough to break the rules.
Although practicality beats purity.

I feel practicality here beats purity of DOM testing. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants