ciscoheat / sveltekit-superforms

Making SvelteKit forms a pleasure to use!
https://superforms.rocks
MIT License
2.26k stars 67 forks source link

TypeError in superForm.js (v2.10.5): Cannot read properties of undefined (reading 'form') when testing with Vitest or Jest #394

Closed LMarshallAfzal closed 6 months ago

LMarshallAfzal commented 8 months ago

Description I'm encountering a "TypeError: Cannot read properties of undefined (reading 'form')" error within the sveltekit-superforms library (superForm.js) during testing with Vitest unit tests. This error prevents form data from being processed successfully within tests.

Steps to Reproduce (in Testing)

  1. Create a SvelteKit form using the superForm and superValidate functions.
  2. Within a test (e.g., using Vitest), submit the form with valid data.
  3. The error occurs within the superForm.js file (approximately line 181), where the code attempts to access the form property of an object named postedForm, which is undefined.

Important Note This issue does not appear when using the forms in a regular (non-test) environment.

Expected Behavior Form data should be processed correctly by the superForm function, both during testing and in standard application usage.

Actual Behavior The TypeError is thrown during testing, interrupting form processing within the test environment. Debugging indicates that the postedForm object is undefined when the problematic line executes.

Environment Svelte version: 4.2.7 SvelteKit version: 2.0.0 sveltekit-superforms version: 2.10.5 joi validation library version: 17.12.2 Node.js version: 21.7.1 Testing framework: Vitest (I had the same issue with Jest) Other relevant dependencies and versions (if applicable)

Error

TypeError: Cannot read properties of undefined (reading 'form')
 ❯ Module.superForm node_modules/sveltekit-superforms/dist/client/superForm.js:181:60
    180|                         form.message = clone(postedForm.message);
    181|                     }
    182|                     break;
       |                                      ^
    183|                 }
    184|             }
 ❯ instance src/routes/login/+page.svelte:8:37
 ❯ Module.init node_modules/svelte/src/runtime/internal/Component.js:130:5
 ❯ new Page src/routes/login/+page.svelte:887:25
 ❯ Module.render node_modules/@testing-library/svelte/src/pure.js:57:19
 ❯ src/lib/__tests__/login/LoginComponent.test.js:11:9

Test File

import { describe, test, expect, vi } from "vitest";
import { render, screen } from "@testing-library/svelte";
import LoginComponent from "../../../routes/login/+page.svelte";

vi.mock('form', () => ({
    resolve: vi.fn(),
}));

describe("Login", () => {
    test("renders login form", async () => {
        render(LoginComponent, {
            props: {
                data: { 
                    form: { 
                        email: 'johndoe@example.com',
                        password: 'Password123!'
                    }
                }
            }   
        });

        const emailInput = screen.getByLabelText("Email");
        const passwordInput = screen.getByLabelText("Password");
        const submitButton = screen.getByRole("button", { name: /sign in/i });

        expect(emailInput).toBeInTheDocument();
        expect(passwordInput).toBeInTheDocument();
        expect(submitButton).toBeInTheDocument();
    });
});

Component being tested

<script>
  import { superForm } from "sveltekit-superforms";

  export let data;

  let showAuthError = false;

  const { form, errors, enhance } = superForm(data.form);

  let selectedLoginMethod = "email"

</script>

  <main class="flex justify-center items-center h-screen">
    <div class="w-full max-w-md">
      <h1 class="text-center text-3xl font-bold mb-4">Login</h1>

      {#if selectedLoginMethod === "email"}
        <form method="POST" class="space-y-4" use:enhance>

          <input type="hidden" name="loginMethod" bind:value={selectedLoginMethod} />
          <input type="email" placeholder="johndoe@site.co.uk" name="email" aria-invalid={$errors.email ? 'true' : undefined} bind:value={$form.email} class="input input-bordered input-primary w-full max-w-xl" />
          <input type="password" placeholder="Pa$$word123!" name="password" aria-invalid={$errors.password ? 'true' : undefined} bind:value={$form.password} class="input input-bordered input-primary w-full max-w-xl" />
          <a class="block mb-2 text-secondary hover:text-primary" href="/signup">New to the platform? Join now</a>
          <a class="block mb-2 text-secondary hover:text-primary" href="/reset_password">Forgot password</a>
          <button type="submit" class="btn btn-primary grow">
            Sign in
          </button>
        </form>
      {:else if selectedLoginMethod === "emailCode"}
        <form method="POST" class="space-y-4" use:enhance>
          {#if $errors.email}
            <div role="alert" class="alert alert-error">
                <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
                <span>{$errors.email}</span>
            </div>
          {/if}
          <input type="hidden" name="loginMethod" bind:value={selectedLoginMethod} />
          <input type="email" placeholder="johndoe@site.co.uk" name="email" bind:value={$form.email} required class="input input-bordered input-primary w-full max-w-xl" />
          <button type="submit" class="btn btn-primary grow">
            {selectedLoginMethod === "magicLink" ? "Send link" : "Send code"}
          </button>
        </form>
    </div>
  </main>

  <style>
    .button-container {
      display: flex;
      gap: 1rem;
    }
  </style>
ciscoheat commented 7 months ago

You need to mock the SvelteKit environment to make it work with vitest. Check the superForm tests how to do it: https://github.com/ciscoheat/sveltekit-superforms/blob/main/src/tests/superForm.test.ts#L355

andrewrisse commented 3 months ago

I am also having this issue, even after using the previously mentioned mocks. The code is fully functional, I just run into this issue when testing sveltekit-superforms with Vitest.

TypeError: Cannot read properties of undefined (reading '$set')
    at Module.applyAction (/.../node_modules/@sveltejs/kit/src/runtime/client/client.js:1955:8)
    at validationResponse (/.../node_modules/sveltekit-superforms/dist/client/superForm.js:1330:66)
import CreateApiKeyModal from '$components/modals/CreateApiKeyModal.svelte';
import { superValidate } from 'sveltekit-superforms';
import { yup } from 'sveltekit-superforms/adapters';
import type { APIKeysForm } from '$lib/types/apiKeys';
import { newAPIKeySchema } from '$schemas/apiKey';
import { toastStore } from '$stores';
import { faker } from '@faker-js/faker';
import userEvent from '@testing-library/user-event';
import { mockCreateApiKeyFormActionError } from '$lib/mocks/api-key-mocks';

describe('Create API Key Modal', () => {
  let form: APIKeysForm;

  it('closes the modal and pops a toast when there is an error with the form action', async () => {
    const keyName = faker.word.noun();
    const sixtyDays = new Date();
    sixtyDays.setDate(sixtyDays.getDate() + 60);

    mockCreateApiKeyFormActionError();

    const toastSpy = vi.spyOn(toastStore, 'addToast');

    form = await superValidate(yup(newAPIKeySchema));
    render(CreateApiKeyModal, { createApiKeyModalOpen: true, form });

    const modal = screen.getByTestId('create-api-key-modal');
    await userEvent.type(within(modal).getByRole('textbox'), keyName);
    await userEvent.click(screen.getByText('60 Days'));
    await userEvent.click(within(modal).getByRole('button', { name: /create/i }));

    expect(toastSpy).toHaveBeenCalledWith({
      kind: 'error',
      title: 'Creation Failed'
    });
    expect(modal).not.toBeInTheDocument();
  });
});

import { vi } from 'vitest';

vi.mock('svelte', async (original) => {
  const module = (await original()) as Record<string, unknown>;
  return {
    ...module,
    onDestroy: vi.fn()
  };
});

vi.mock('$app/stores', async () => {
  const { readable, writable } = await import('svelte/store');

  const getStores = () => ({
    navigating: readable(null),
    page: readable({ url: new URL('http://localhost'), params: {} }),
    session: writable(null),
    updated: readable(false)
  });

  const page: typeof import('$app/stores').page = {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    subscribe(fn: any) {
      return getStores().page.subscribe(fn);
    }
  };

  const navigating: typeof import('$app/stores').navigating = {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    subscribe(fn: any) {
      return getStores().navigating.subscribe(fn);
    }
  };

  return {
    getStores,
    navigating,
    page
  };
});
ekrata-main commented 1 week ago
TypeError: Cannot read properties of undefined (reading '$set')
    at Module.applyAction (/.../node_modules/@sveltejs/kit/src/runtime/client/client.js:1955:8)
    at validationResponse (/.../node_modules/sveltekit-superforms/dist/client/superForm.js:1330:66)

getting this too :(