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

How to simulate interactions with the canvas element for a vitest test? #809

Open
vcpablo opened this issue Feb 6, 2025 · 1 comment
Open

Comments

@vcpablo
Copy link

vcpablo commented Feb 6, 2025

Hello folks,

I'm currently using the signature_pad package along with Vue 3 + Formkit to create a component that allows the user to sign.
I want to create a unit test for a form that users the component but I'm not being able to make the library to trigger the endStroke event, which I'm listening on the component.

Here is a piece of the code and I hope someone could help me out with that:

SignaturePad.vue - the component that actually contain the signature_pad implementation

<script lang="ts">
import { FormKitFrameworkContext } from '@formkit/core'

type Props = {
  value?: string
}

export type Context = FormKitFrameworkContext & Props
</script>

<script setup lang="ts">
import SignaturePad from 'signature_pad'
import { computed, onMounted, ref, watch } from 'vue'

const props = defineProps<{
  context: Context
}>()

// For the signature pad lib to work properly, we need to make sure the canvas element has its dimensions set inline instead of using classes
const resizeCanvas = (canvas: HTMLCanvasElement): void => {
  const TAILWIND_SM_BREAKPOINT = 640
  const TAILWIND_SM_PADDING = 48

  const TAILWIND_MD_BREAKPOINT = 834
  const TAILWIND_MD_PADDING = 128

  const SIGNATURE_PAD_MINIMUM_WIDTH = 500

  if (window.innerWidth < TAILWIND_SM_BREAKPOINT) {
    canvas.width = window.innerWidth - TAILWIND_SM_PADDING
  } else if (window.innerWidth < TAILWIND_MD_BREAKPOINT) {
    canvas.width = window.innerWidth - TAILWIND_MD_PADDING
  } else {
    canvas.width = SIGNATURE_PAD_MINIMUM_WIDTH
  }
}

const attachEvents = (signaturePad, canvas): void => {
  signaturePad.addEventListener(
    'endStroke',
    () => {
      console.log('endStroke')
      const value = signaturePad.toDataURL()
      props.context.node.input(value)
    },
    { once: false },
  )

  window.onresize = () => {
    resizeCanvas(canvas)
  }
}

const initWatcher = (signaturePad): void => {
  watch(
    () => props.context.value,
    (current) => {
      if (!current) {
        signaturePad.clear()
      }
    },
  )
}

const classes = computed<string>(() => {
  const stateClass =
    props.context?.state?.validationVisible && !props.context?.state?.valid
      ? 'border-spf-danger'
      : 'border-spf-subtle'

  return `border ${props.context.class} ${stateClass}`
})

const signatureCanvas = ref(null)

onMounted(() => {
  const canvas = signatureCanvas.value

  if (canvas) {
    const signaturePad = new SignaturePad(canvas)
    console.log(signaturePad)

    attachEvents(signaturePad, canvas)
    resizeCanvas(canvas)
    initWatcher(signaturePad)

    if (props.context.value) {
      signaturePad.fromDataURL(props.context.value)
    }
  }
})
</script>
<template>
  <canvas
    id="signature-pad-canvas"
    ref="signatureCanvas"
    :class="classes"
    data-testid="signature-pad-canvas"
  />
</template>

SignaturePad.vue - the component is wrapper for Formkit creating a custom component that users the one above

<script setup lang="ts">
import { createInput } from '@formkit/vue'
import { BaseTextLink } from '@spectora/ui'
import SignaturePadInput from './SignaturePadInput.vue'

defineProps<{
  clearable?: boolean
  disabled?: boolean
  help?: string
  helpIcon?: string
  label?: string
  name: string
  readonly?: boolean
  validation?: string
  validationLabel?: string
  validationVisibility?: 'submit' | 'live' | 'blur' | 'dirty'
}>()

const signaturePad = createInput(SignaturePadInput, {
  props: ['value', 'class', 'required'],
})

const signature = defineModel<string>()

const handleClear = () => {
  signature.value = ''
}
</script>

<template>
  <FormKit
    v-model="signature"
    :disabled="disabled"
    :help="help"
    :help-icon="helpIcon"
    message-class="$reset style-caption text-spf-danger mt-2"
    :name="name"
    :readonly="readonly"
    :type="signaturePad"
    :validation="validation"
    :validation-label="validationLabel"
    :validation-visibility="validationVisibility"
    :value="signature"
  >
    <template #label>
      <div class="flex items-center justify-between">
        <label
          class="$reset text-spf-default style-label"
          :for="($attrs.id as string) || ''"
          >{{ label }}</label
        >
        <BaseTextLink
          v-if="clearable"
          :disabled="!signature"
          href="#"
          label="Clear Signature"
          size="small"
          @click="handleClear"
        />
      </div>
    </template>
  </FormKit>
</template>

Form.vue - the component that uses the custom signature pad

<FormKit
    :actions="false"
    :incomplete-message="false"
    type="form"
    @submit="handleSubmit"
    @submit-invalid="handleSubmitInvalid"
  >
    <div class="flex flex-col gap-7 pb-14">
      

      <SignaturePad
        id="terms-signature-pad"
        v-model="signature"
        class="h-[100px] w-full rounded-spf-md bg-spf-default"
        clearable
        label="Sign Here"
        name="signature"
        validation="required"
        validation-visibility="submit"
      />
    </div>
    <StepsFooter label="Terms & Conditions" @back="handleBack" />
  </FormKit>

Form.spec.ts - test file for the form with the code I got so far

it('should emit success event when hitting next having the terms and signing', async () => {
    render(TermsAndConditionsForm)

    // Trying to interact with the canvas element programatically
    const canvas = screen.getByTestId(
      'signature-pad-canvas',
    ) as HTMLCanvasElement
    const context = canvas?.getContext('2d')

    if (context) {
      canvas.dispatchEvent(new Event('mousedown'))
      // Set the starting point of the line
      context?.beginPath()
      context?.moveTo(50, 50) // Starting point (x, y)

      // Draw the line to the ending point
      context?.lineTo(200, 200) // Ending point (x, y)

      // Set the line color and width
      context.strokeStyle = 'black'
      context.lineWidth = 2

      // Render the line
      context?.stroke()
      canvas.dispatchEvent(new Event('mouseup'))
    } else {
      throw new Error('Canvas context is not available')
    }

    const nextButton = screen.getByRole('button', { name: /next/i })
    await user.click(nextButton)

    expect(screen.queryByText(/Signature is required./i)).toBeNull()
    
  })
@UziTech
Copy link
Collaborator

UziTech commented Feb 6, 2025

If you want an end to end test you should use pointer events instead of the canvas context to draw on the canvas. The pointer up event is what will dispatch the endStroke event. See our tests for how to do that.

If you want a unit test that does not also test the internals of signature_pad you can just dispatch the endStroke event in your test

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

2 participants