Skip to content

How to set data with the Composition API ? #539

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
lucas-santosP opened this issue Apr 18, 2021 · 8 comments
Closed

How to set data with the Composition API ? #539

lucas-santosP opened this issue Apr 18, 2021 · 8 comments
Labels
question Further information is requested

Comments

@lucas-santosP
Copy link

As the documentation says here the setData method does not modify composition API setup data. So don't exist a way to do it when using the composition API ? I'm searching in the documentation for a while and haven't found a solution.

Example:

const wrapper = mount(Calculator);
await wrapper.setData({ memory: "5 + 9" });

expect(wrapper.html()).toContain("5 + 9");

The example above works fine with the options API, but with the composition API the setData returns this error:

TypeError: Cannot add property memory, object is not extensible

@lucas-santosP lucas-santosP changed the title setData with Composition API How to set data with the Composition API ? Apr 18, 2021
@lmiller1990
Copy link
Member

What are you expecting to happen? Can you share Calculator.vue?

If you are expecting to set the value of ref declared in setup, this will not work - setData only works with data. There isn't a good way to modify a value inside a function (like setup).

For setup, I'd generally just recommend interacting with the component via trigger or passing props to test it (like a user would).

@lmiller1990 lmiller1990 added the question Further information is requested label Apr 18, 2021
@lucas-santosP
Copy link
Author

The code is here. And yes, in my example memory is a ref variable declared in the setup function.

Using the wrapper.vm.memory = "something" wouldn't work too right ? It seems that doesn't trigger the reactivity but I guess this is an expected behavior...

I'm new to automated testing if what I say doesn't make sense, you can tell me. But the point of using setData, in this case, was to not have to get each button of each digit that I wanna add in the memory to test something else. I already did tests for the buttons clicks, I didn't wanna repeat them every time I wanna a value in memory.

@lmiller1990
Copy link
Member

lmiller1990 commented Apr 19, 2021

There is probably some way to make it work using .vm, but this is not generally a good idea. vm is basically like a private variable - it's not something you can normally access in your application. Test Utils exposes it, but the majority of the time you don't need it.

It's best to test things from the perspective of a user. I am not sure what Calculator.vue look likes, but if it had two fields that get added up, what I'd generally recommend is something like:

const wrapper = mount(Calculator)
await wrapper.find('#digit-1').setValue(1)
await wrapper.find('#digit-2').setValue(6)
await wrapper.find('#calculate').trigger('click')
expect(wrapper.find('#result').text()).toContain('7')

Rather than asserting against vm, you assert against what is rendered (after all, Vue components are for user interfaces).

How many digits do you want to assert? The only cases I can see would be positive/negative/0/non digit - so you'd need a total of four (or one test with four cases, I guess).

If you can share Calculate.vue I can give you more guidance.

@lucas-santosP
Copy link
Author

If you can share Calculate.vue I can give you more guidance.

The link of Calculator.vue was right there in my previous comment.
https://github.com/lucas-santosP/calculator/blob/main/src/components/Calculator.vue

How many digits do you want to assert?

The Calculator.vue has more than 15 buttons, cause each button is a digit or an operator, so this is why became repetitive and massive having to get each of them every time I want some value on memory.

If there is really no good way to define the data directly in this case, I was thinking about creating a helper function to get and trigger each button based on a string received via param... But if you can check the code, I’d like to know what you recommend.

@lmiller1990
Copy link
Member

lmiller1990 commented Apr 21, 2021

Neat calculator!

Here are some ideas. I have two suggestions. This is really just how I like to code, everyone has their own approach to this sort of thing.

Option 1: Separate logic with a composable

From what I can see you are trying to test calculateResult, eraseLastDigit etc. One option would just be to move these out of setup and allow them to receive arguments. You could make a composable:

export function useCalculate() {
  let memory = ref("");
  let error = ref(false);
  let clearOnNextDigit = ref(false);

  function calculateResult() {
    if (!memory.value) return;
    let mathExpression = memory.value.replace(/\s/g, ""); //remove spaces
    if (lastCharIsOperator(mathExpression)) {
      mathExpression = mathExpression.slice(0, mathExpression.length - 1);
    }
    try {
      mathExpression = mathExpression.replace(/\b0*((\d+\.\d+|\d+))\b/g, "$1"); // remove octal numeric
      memory.value = `${eval(mathExpression) || ""}`;
    } catch (_) {
      error.value = true;
      memory.value = "";
    } finally {
      clearOnNextDigit.value = true;
    }
  }

  return {
    memory,
    calculateResult,
    error
  }
}

Usage would now be something like:

<script>
import { useCalculate } from '../useCalculate'

export default {
  setup() {
    const { memory, error, calculateResult } = useCalculate()
  }
}
</script>

The actual code doesn't change much, just where it's located. Now the logic is a little more decoupled from the UI.

Now unit testing the logic is very easy:

it('adds numbers', () => {
  const { memory, calculateResult } = useCalculate()
  memory.value = '4 * 9'
  calculateResult()
  expect(meory.value).toBe('36)
})

This ideal is loosely knowledge as "functional core, imperative shell". The functional core is all your logic, the shell the UI layer, which is generally thin and simple. I made some content about this if you like video tutorials.

Option 2: Utilities functions and data-key attributes

EDIT: I literally just saw you have some tests like this. 🤦 I'll leave this here anyway, but I think this approach is fine.

This is a bit of work, though. If you prefer not to change the structure of the code too much, I'd recommend making some helpers. Note I'm adding data-key attributes.

<Button 
  variant="blue" 
  v-for="number in padNumbers[2]" 
  :key="number" 
  @click="addDigit(number)"
  :data-key="number"
>
  {{ number }}
</Button>

<Button 
  variant="green" 
  @click="addOperator('+')"
  :data-key="+"
>
  +
</Button>

Now you can write a helper and test like this. The main point is we test by interacting with the component, as opposed to surgically manipulating the internal values (like memory). These tests are much more resilient to change - you can trash the entire calculation logic and rewrite it, and as long as the UI doesn't change, everything will still work. That can give you confidence everything is working correctly. 💯

async function click(wrapper, char) {
  await wrapper.find(`[data-key=${char}]`).trigger('click') 
}

it('calculates', async () => {
  const wrapper = mount(Calculator)
  await click(wrapper, '9')
  await click(wrapper, '*')
  await click(wrapper, '4')
  // assert stuff, something like
  expect(wrapper.html()).toContain('36')
})

This is a cool code base, I like it. I do code review videos occasionally, would you mind if I used this as example?

@lmiller1990
Copy link
Member

Edit @lucas-santosP - I made a PR against your repo showing off another neat tool you might be interested in: lucas-santosP/calculator#3.

@lucas-santosP
Copy link
Author

lucas-santosP commented Apr 21, 2021

Neat calculator!

Thanks, and I liked the "functional core, imperative shell" concept I'll definitely check your video and will apply it in another branch to compare the two approaches.

EDIT: I literally just saw you have some tests like this. 🤦 I'll leave this here anyway, but I think this approach is fine.

Yeah 😆, I did it after I saw you saying that is not so good to check values using the wrapper.vm, so I change it and add some helpers functions to trigger the buttons... But I didn't know it was ok to do it, so your examples still help me understand.

This is a cool code base, I like it. I do code review videos occasionally, would you mind if I used this as example?

I'm wouldn't mind, I would love to see it.

@lucas-santosP
Copy link
Author

lucas-santosP commented Apr 21, 2021

So this issue main question was answered with your last comment therefore I'll close the issue, but if you have something to add feel free to reopen it.

Thank you for your replies, I really appreciate it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants