skip to Main Content

In Cypress I am changing the viewport of my Veu3 app after I preform any other test and it gives the error: "(uncaught execption) TypeError a.value is null".

The minimum app that produces the error:

<script setup>
  import { ref, onMounted } from 'vue'
  const a = ref(null)
  
  onMounted (() => {
    function onWindowResize() {
      window.addEventListener("resize", () => {
        //a.value always needs to refresh when the window resizes to different content
        a.value.textContent = 'Text displayed here will be decided on the window size'
      })
    }
    onWindowResize()
  })
</script>

<template>
<div>
  <span ref="a" data-testid="test1">M</span>
  <a> ..more</a>
</div>
</template>

With the following test. The interesting thing to note here is that if you don’t include the first test the Error is not there.

import Test from '../Test.vue'

describe('run 2 test', () => {
  it('The error only appears if I run a test before I run the viewport change', () => {
    cy.mount(Test)
    cy.get('[data-testid="test1"]').get('a').should('contain', ' ..more')
  })

  it('The error originates on the first viewport changes it appears', () => {
    cy.mount(Test)
//so it appears here
    cy.viewport(550, 750)

    let charCount = 0
    cy.get('[data-testid="test1"]').then(($span) => {
      charCount = String($span.text()).length;
    })
    cy.viewport(400, 400)
    cy.get('[data-testid="test1"]').then(($span) => {
      const newCharCount = String($span.text()).length;
      expect(newCharCount).to.be.eq(charCount)
    })
  })
})

I found this anwser to the question: "Vue3 TypeError ref.value is null" and tried adding defineExpose() yet it doesn’t change a thing. I tried re-declaring the const a = ref(null) within the onMounted() also no luck. Been searching but can’t find a solution any help would be appreciated since a lot of the test will revolve around viewport changes.

The error causes the rest of the application, which depends on the a.value to fail.

UPDATE
Apparently it is me who can’t see what is wrong. So, I would like to understand why a.value should not exist in unMounted() (even thought this is an example in de Vue docs). Also why it does work in de browser without any errors. And why does it work in Cypress is no tests precede the test?

I would really like to understand and find a way around this.

3

Answers


  1. When I use a ref in React, it’s always good practice to check the ref.value before accessing/assigning it.

    So if I put a guard inside the resize listener, the error

    (uncaught exception)TypeError: Cannot set properties of null (setting ‘textContent’)

    goes away.

    <script setup>
      import { ref, onMounted } from 'vue'
      const a = ref(null)
      
      onMounted (() => {
        function onWindowResize() {
          window.addEventListener("resize", () => {
    
            // check that the ref is linked to the element before using it
            if (a.value) {
              a.value.textContent = 'resize'
            }
          })
        }
        onWindowResize()
      })
    </script>
    

    Was not sure if the logic of the test still holds, though, because it may be throwing away a resize event.

    So I added a check for the text "resize" (which is set by the listener), and it passes.

    The rest of your test doesn’t pass, which follows because now the resizer is functioning.

    it('The error originates on the first viewport changes it appears', () => {
      cy.mount(Test)
    
      cy.viewport(550, 750)
    
      let charCount = 0
      cy.get('[data-testid="test1"]').then(($span) => {
    
        charCount = $span.text().length; 
    
        cy.viewport(400, 400)
    
        cy.get('[data-testid="test1"]')
          .should('have.text', 'resize')               // passes
          .then(($span) => {
            const newCharCount = $span.text().length;
            expect(newCharCount).to.be.eq(charCount)   // now failing because resize occurred
          })
      })
    })
    

    enter image description here

    Also, there’s a warning in the dev console that the resize event is being deprecated and will be removed (chrome browser).

    It suggests

    Consider using MutationObserver instead.

    Login or Signup to reply.
  2. I think your test is the wrong way round, if the resize changes the text so expect(newCharCount).to.be.eq(charCount) would not pass, because now the char count is larger. See a.value.textContent = ...

    Did you make a typo there?

        let charCount = 0
        cy.get('[data-testid="test1"]').then(($span) => {
          charCount = String($span.text()).length;
        })
        cy.viewport(400, 400)
        cy.get('[data-testid="test1"]').then(($span) => {
          const newCharCount = String($span.text()).length;
          expect(newCharCount).to.be.gt(charCount)     <-- size has increased 
        })
    

    Adding a guard is the correct action to stop the uncaught:exception.

    Window resize works independently of the Vue app, the error is occuring after the component is unmounted.

    Adding some debugging:

    <script setup>
      import { ref, onMounted, onUnmounted } from 'vue'
    
      const el = ref(null)
      
      let isMounted = false
    
      onMounted (() => {
        isMounted = true
        console.log('Mounted: ref el', el.value, isMounted)
        window.addEventListener("resize", () => {
          if (isMounted) {
            el.value.textContent = 'some new text'
          }
        })
      })
    
      onUnmounted (() => {
        isMounted = false
        console.log('Unmounted: ref el', el.value, isMounted)
      })
    </script>
    

    Console:

    • Mounted: ref el <span data-testid=​"test1">​M​​ true
    • Unmounted: ref el null false
    • Mounted: ref el <span data-testid=​"test1">​some new text​​ true

    Shows ref el is pointing at the span when mounted (as expected).

    Technically in onBeforeUnmount() you should remove the listener and that will also get rid of the uncaught:exception.

    Since resize listener is deprecated, change it to MutationObserver instead.

    Alternatively, use useElementSize from @vueuse/core.

    Login or Signup to reply.
  3. I’m not sure about the versions of cypress that you’re using or how you run it, but what I did to reproduce your problem was to use vue 3, and cypress 13, and run the test with this command cypress run --component --headed --no-exit --browser chrome. And the error that I found is TypeError: Cannot set properties of null (setting 'textContent').

    What happened

    Assuming my way of reproducing is correct, I can assure you that the error that you found "TypeError ref.value is null" is actually happening on the first test, not the second test. Want some proof? try to use these codes:

    <script setup>
      import { ref, onMounted } from 'vue'
      const a = ref(null)
      const props = defineProps(['testname'])
    
      onMounted (() => {
        function onWindowResize() {
          window.addEventListener("resize", () => {
            //a.value always needs to refresh when the window resizes to different content
            try {
              a.value.textContent = 'Text displayed here will be decided on the window size'
              console.log(`success changing textContent on ${props.testname}`)
            } catch (e) {
              console.error(e)
              console.log(`error happens on ${props.testname}`)
            }
          })
        }
        onWindowResize()
      })
    </script>
    
    <template>
    <div>
      <span ref="a" data-testid="test1">M</span>
      <a> ..more</a>
    </div>
    </template>
    
    describe('run 2 test', () => {
      it('The error only appears if I run a test before I run the viewport change', () => {
        cy.mount(Test, { props: { testname: "test1" } })
        cy.get('[data-testid="test1"]').get('a').should('contain', ' ..more')
      })
    
      it('The error originates on the first viewport changes it appears', () => {
        cy.mount(Test, { props: { testname: "test2" } })
        //so it appears here
        cy.viewport(550, 750)
    
        let charCount = 0
        cy.get('[data-testid="test1"]').then(($span) => {
          charCount = String($span.text()).length;
        })
        cy.viewport(400, 400)
        cy.get('[data-testid="test1"]').then(($span) => {
          const newCharCount = String($span.text()).length;
          expect(newCharCount).to.be.eq(charCount)
        })
      })
    })
    

    If you run with those codes, you’ll see clearly that the error happens on the first test:

    cypress error viewport change

    Why

    So why exactly did that happen? It seems like when the first test finishes, the component doesn’t get unmounted, even after the second test starts to run, want some proof? try to update the script setup to this:

    <script setup>
      import { ref, onMounted, onBeforeUnmount } from 'vue'
      const a = ref(null)
      const props = defineProps(['testname'])
    
      function intervalFunction() {
        console.log(`running on ${props.testname}`)
      }
      onMounted (() => {
        setInterval(intervalFunction, 1000);
        function onWindowResize() {
          window.addEventListener("resize", () => {
            //a.value always needs to refresh when the window resizes to different content
            try {
              a.value.textContent = 'Text displayed here will be decided on the window size'
              console.log(`success changing textContent on ${props.testname}`)
            } catch (e) {
              console.error(e)
              console.log(`error happens on ${props.testname}`)
            }
          })
        }
        onWindowResize()
      })
    
      onBeforeUnmount(() => {
        clearInterval(intervalFunction)
      })
    </script>
    

    Now run the test again, and on the console, you’ll see that the interval prints don’t stop even when it should be cleared on the onBeforeUnmount lifecycle hook and after the test is finished.

    cypress mount

    That would only mean that the component persists and is still affected by the next test. In other words, when this line cy.viewport(550, 750) is executed, it doesn’t just affect the component mounting on the second test, but also the first one. This is just my hunch, but for some reason, the first component isn’t unmounted properly (the component has no more elements after the first test is done, but maybe some of it is still there and cypress somehow still affects it)

    So why is it like that? to be honest i’m not sure myself, but i’d say that error is a false alarm for the test, because that error happens when the test (the first one) is already executed, so in my opinion, you can just ignore it. If you feel uncomfortable with the error, you could just wrap the value assignment inside a try catch like what I show above.

    Or maybe you can unmount the component by following this guide.

    EDIT

    Actually, after some tweaking, you simply need to remove the window resize event listener when unmounting:

    <script setup>
    import { ref, onMounted, onBeforeUnmount } from "vue";
    const a = ref(null);
    
    function resizeCallback() {
      //a.value always needs to refresh when the window resizes to different content
      a.value.textContent = "Text displayed here will be decided on the window size";
    }
    
    onMounted(() => {
      function onWindowResize() {
        window.addEventListener("resize", resizeCallback);
      }
      onWindowResize();
    });
    
    onBeforeUnmount(() => {
      window.removeEventListener("resize", resizeCallback);
    });
    </script>
    
    <template>
      <div>
        <span ref="a" data-testid="test1">M</span>
        <a> ..more</a>
      </div>
    </template>
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search