Search code examples
javascriptunit-testingsveltevitest

Got error "Function called outside component initialization" when testing Svelte component with Vitest


I want to test the following Svelte component with Vitest:

<script context="module">
    import {push} from 'svelte-spa-router'
    import {onDestroy} from 'svelte'

    let time = 1000*120;
    let interval = 1000;
    let running = true;
        
    export const twoDigits = (number) => {
        return number.toLocaleString('en-US', {
            minimumIntegerDigits: 2,
            useGrouping: false
        })
    }
    
    export const timeString = (ms) => {
        let seconds = ms / 1000;
        const minutes = Math.floor(seconds / 60);
        seconds = seconds % 60;
        const timeString = twoDigits(minutes) + ":" + twoDigits(seconds);
        return timeString
    }
    
    let timeShown = timeString(time)
    
    export const startCountdown = () => {
        setTimeout(()=>{
            time -= interval;
            timeShown = timeString(time);
            if (time > 0 && running) {
                startCountdown();
            } else if (time > 0 && !running) {
                console.log("Countdown stopped")
            } else {
                console.log("Done!")
                setTimeout( () => {
                    push('/gameover')
                },1000)
            }
        }, interval)
    }

    startCountdown()

    onDestroy(()=>{
        running = false;
    })

</script>

<div id="countdown">
    {timeShown}
</div>

Unfortunately, I get an error saying Function called outside component initialization as soon as I try to import the component in my test file with import Countdown from "../lib/Countdown.svelte".

The problem could have something to do with the onDestroy function but I don't know, how to fix this.

stacktrace:

Error: Function called outside component initialization
 ❯ get_current_component node_modules/svelte/src/runtime/internal/lifecycle.js:14:32
     14| 
     15| /**
     16|  * Schedules a callback to run immediately before the component is updated after any state chan…       |                      ^
     17|  *
     18|  * The first time the callback runs will be before the initial `onMount`
 ❯ Module.onDestroy node_modules/svelte/src/runtime/internal/lifecycle.js:77:2
 ❯ src/lib/Countdown.svelte:47:5
 ❯ src/test/countdown.test.js:3:31

vite.config.js

import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'

// https://vitejs.dev/config/
export default defineConfig({
  base: "",
  test: {
    environment: "jsdom"
  },
  plugins: [svelte()],
})

At the moment there is only a trivial test in the test file since the import of Countdown produces the error:

import {test, expect} from "vitest"
import { onDestroy } from "svelte"
import Countdown from "../lib/Countdown.svelte"

const hello = "hello"

test("something", ()=>{
    expect(hello).toBe("hello")
})

Solution

  • The component is broken because of this:

    <script context="module">
    

    That code is independent of all instances, hence the error on calling onDestroy, which is an component lifecycle hook. The attribute context="module" should not be there.

    Also note that onDestroy runs on the server during server-side rendering. If you use SSR, it probably should be changed to this:

    onMount(() => {
      startCountdown()
    
      return () => {
        running = false;
      };
    })
    

    This way no timers are involved on the server at all.