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 a proper way to mount a component with v-model in quasar-app-extension-testing-e2e-cypress #385

Open
FelixNumworks opened this issue Sep 18, 2024 · 1 comment

Comments

@FelixNumworks
Copy link

FelixNumworks commented Sep 18, 2024

This is a feature request

Problem when mounting components with a v-model

When using cy.mount in my component tests, I have difficulty correctly mounting the component with a v-model binding.

My old way of doing it was like this:

cy.mount(MyComponent, {
  props: {
    modelValue: myModel,
  },
});

The issue is that while the model is passed as a prop, the onUpdate:modelValue event isn't configured, causing unexpected behavior with Vue's reactivity system.

Expected solution

I'd like the mount comand to allow me to do this easily:

cy.mount(MyComponent, {
  props: {
     ...
  },
  models: {
    modelValue: myModel
  }
});

Example:

Take this simple DoubleCounter.vue component

<template>
  <button data-cy="mutateButton" @click="mutateModel">Increment by mutating model</button>
  <button data-cy="replaceButton" @click="replaceModel">Increment by replacing model</button>
  <p data-cy="count">{{ myModel.count }}</p>
</template>

<script setup lang="ts">
import { PropType } from 'vue';

const myModel = defineModel({
  type: Object as PropType<{ count: number }>,
  required: true,
});

function replaceModel() {
  myModel.value = { count: myModel.value.count + 1 };
}

function mutateModel() {
  myModel.value.count += 1;
}
</script>

Clicking the mutateButton works as expected, but once the replaceButton is clicked, the component becomes buggy — it updates the model, but the DOM doesn't reflect the changes.

it('doesnt work when mounting the component with a model props', () => {
  const model = { count: 0 };
  cy.mount(DoubleCounter, {
    props: {
      modelValue: model,
    },
  });

  cy.dataCy('mutateButton').click();
  cy.dataCy('count').should('have.text', '1');

  cy.dataCy('replaceButton').click();
  cy.dataCy('count').should('have.text', '2');

  cy.dataCy('mutateButton').click();
  cy.dataCy('count').should('have.text', '3'); //  <----- FAILS 😢
});

Current solution

As per vue-test-utils documention, I can manually set the onUpdate:modelValue. Unfortunately, cy.mount doesn't provide direct access to the VueWrapper, so I had to find a workaround.

This is what I came up with:

it('does work when mounting the component with a model props and an update prop', () => {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  let componentWrapper: any;

  const model = { count: 0 };
  cy.mount(DoubleCounter, {
    props: {
      modelValue: model,
      'onUpdate:modelValue': (updatedModel: { count: number }) => {
        componentWrapper?.setProps({ modelValue: updatedModel });
      },
    },
  }).then((component) => {
    componentWrapper = component.wrapper;
  });

  cy.dataCy('mutateButton').click();
  cy.dataCy('count').should('have.text', '1');

  cy.dataCy('replaceButton').click();
  cy.dataCy('count').should('have.text', '2');

  cy.dataCy('mutateButton').click();
  cy.dataCy('count').should('have.text', '3'); // <---- WORKS 🥳
});

This is tedious and could be impemented in the mount command directly.

Thank you :)

@FelixNumworks
Copy link
Author

Here is my overwritten mount command:

Cypress.Commands.overwrite('mount', (originalFn, component, mountOptions) => {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  let componentWrapper: any;

  if (mountOptions?.props && component.emits) {
    for (const emit of component.emits as string[]) {
      if (!emit.startsWith('update:')) continue;
      const modelName = emit.replace('update:', '');
      if (!mountOptions.props[modelName]) continue;
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      mountOptions.props[`onUpdate:${modelName}`] = (updatedModel: any) => {
        componentWrapper?.setProps({ [modelName]: updatedModel });
      };
    }
  }

  return originalFn(component, mountOptions).then(({ wrapper, component }) => {
    componentWrapper = wrapper;
    return { wrapper, component };
  });
});

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

No branches or pull requests

1 participant