最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Viewport change of Vue3 app in Cypress gives TypeError ref.value is null - Stack Overflow

programmeradmin0浏览0评论

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.

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.

Share Improve this question edited Oct 13, 2023 at 8:30 St. Jan asked Oct 10, 2023 at 8:01 St. JanSt. Jan 2015 silver badges25 bronze badges 7
  • 1 Just a ment - cy.get('[data-testid="test1"]').get('a') is not really effective, since the two .get()s are disconnected. If you want to incorporate a traversal in the query, it would be cy.get('[data-testid="test1"]').next('a') since <a> is a sibling of <span>. Not that it causes the problem, but just saying you may not be running the query you expect. – A.Pearsson Commented Oct 10, 2023 at 8:28
  • THX, will take note:) – St. Jan Commented Oct 10, 2023 at 8:33
  • The solution given by @Aladin Spaz doesn't take in to account that the value of a.value.textContext needs to be updated on every occasion. It just stops updating it. In the Vue Docs vuejs/api/position-api-lifecycle.html#onmounted the example that is given of the use of onMounted() is exactly what I am doing here without the eventListener. Also the text does pass in Cypress if no test precedes the test.... – St. Jan Commented Oct 12, 2023 at 12:19
  • 3 That makes no sense - you can't update an element that does not exist! – TesterDick Commented Oct 12, 2023 at 20:15
  • I don;t understand why the element doesn't exist. could you explain? vuejs/api/position-api-lifecycle.html#onmounted here it shows the same situation as an example. Furthermore why does it work without errors if there is no test preceding this test? – St. Jan Commented Oct 13, 2023 at 6:24
 |  Show 2 more ments

3 Answers 3

Reset to default 8

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
      })
  })
})

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.

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 ponent 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.

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 mand cypress run --ponent --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:

Why

So why exactly did that happen? It seems like when the first test finishes, the ponent 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.

That would only mean that the ponent 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 ponent mounting on the second test, but also the first one. This is just my hunch, but for some reason, the first ponent isn't unmounted properly (the ponent 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 unfortable 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 ponent 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>
发布评论

评论列表(0)

  1. 暂无评论