Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { act, fireEvent, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
import { expect, test, vi } from 'vitest'
import { render } from 'vitest-browser-react'
import { EpisodeArtworkPreview } from '#app/components/calls/episode-artwork-preview.tsx'

test('publish anonymously tooltip opens on hover', async () => {
const user = userEvent.setup()
render(
const screen = await render(
<EpisodeArtworkPreview
title="My episode title"
email="person@example.com"
Expand All @@ -22,12 +20,14 @@ test('publish anonymously tooltip opens on hover', async () => {
const tooltipButton = screen.getByRole('button', {
name: 'What does publish anonymously mean?',
})
const tooltip = screen.getByRole('tooltip')
const checkbox = screen.getByRole('checkbox', { name: /publish anonymously/i })

expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()
await user.hover(tooltipButton)
expect(screen.getByRole('tooltip')).toHaveTextContent('If you check this')
await user.unhover(tooltipButton)
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()
await expect.poll(() => tooltip.query()).toBeNull()
await tooltipButton.hover()
await expect.element(tooltip).toHaveTextContent('If you check this')
await checkbox.hover()
await expect.poll(() => tooltip.query()).toBeNull()
})

test('episode artwork preview dims while the next image suspends', async () => {
Expand Down Expand Up @@ -60,26 +60,25 @@ test('episode artwork preview dims while the next image suspends', async () => {
}

try {
render(<Example />)
const screen = await render(<Example />)

const checkbox = screen.getByRole('checkbox', { name: /publish anonymously/i })
const initialImg = screen.getByAltText('Episode artwork preview')
const initialSrc = initialImg.getAttribute('src')
const checkbox = screen.getByRole('checkbox', {
name: /publish anonymously/i,
})
const img = screen.getByAltText('Episode artwork preview')
const initialSrc = img.element().getAttribute('src')
expect(initialSrc).toBeTruthy()

await act(async () => {
fireEvent.click(checkbox)
expect(checkbox).toBeChecked()
await checkbox.click()
await expect.element(checkbox).toBeChecked()

const pendingImg = screen.getByAltText('Episode artwork preview')
expect(pendingImg).toHaveClass('opacity-60')
expect(pendingImg).toHaveAttribute('src', initialSrc)
await expect.element(img).toHaveClass('opacity-60')
await expect.element(img).toHaveAttribute('src', initialSrc)

await vi.advanceTimersByTimeAsync(1500)
})
const loadedImg = screen.getByAltText('Episode artwork preview')
expect(loadedImg).toHaveClass('opacity-100')
expect(loadedImg.getAttribute('src')).not.toBe(initialSrc)
await vi.advanceTimersByTimeAsync(1500)

await expect.element(img).toHaveClass('opacity-100')
expect(img.element().getAttribute('src')).not.toBe(initialSrc)
} finally {
vi.unstubAllGlobals()
vi.useRealTimers()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { render } from 'vitest-browser-react'

const { mockNavigate, mockRevalidate, mockUseRootData } = vi.hoisted(() => ({
mockNavigate: vi.fn(),
Expand All @@ -20,12 +20,7 @@ vi.mock('#app/utils/use-root-data.ts', () => ({
useRootData: () => mockUseRootData(),
}))

// In Vitest, the Vite macro plugin isn't installed, so mock the macro helper.
vi.mock('vite-env-only/macros', () => ({
serverOnly$: (fn: unknown) => fn,
}))

import { RecordingForm } from '#app/routes/resources/calls/save.tsx'
import { RecordingForm } from '#app/components/calls/recording-form.tsx'

describe('RecordingForm', () => {
it('recovers when FileReader.readAsDataURL throws synchronously', async () => {
Expand Down Expand Up @@ -57,26 +52,23 @@ describe('RecordingForm', () => {
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})

try {
const { container } = render(
const screen = await render(
<RecordingForm audio={new Blob(['audio'])} intent="create-call" />,
)

fireEvent.change(screen.getByLabelText('Title'), {
target: { value: 'A valid title' },
})

const submitButton = screen.getByRole('button', {
name: 'Submit Recording',
})
const form = container.querySelector('form')
expect(form).not.toBeNull()
fireEvent.submit(form as HTMLFormElement)

await waitFor(() => expect(submitButton).toBeEnabled())
expect(submitButton).toHaveTextContent('Submit Recording')
expect(
screen.getByText('Unable to read recording. Please try again.'),
).toBeInTheDocument()
await screen.getByLabelText('Title').fill('A valid title')
await screen.getByRole('button', { name: 'Submit Recording' }).click()

await expect
.element(screen.getByRole('button', { name: 'Submit Recording' }))
.toBeEnabled()
await expect
.element(screen.getByRole('button', { name: 'Submit Recording' }))
.toHaveTextContent('Submit Recording')
await expect
.element(
screen.getByText('Unable to read recording. Please try again.'),
)
.toBeVisible()
expect(readAsDataURL).toHaveBeenCalledTimes(1)
expect(addEventListener).toHaveBeenCalledWith(
'loadend',
Expand Down Expand Up @@ -150,18 +142,13 @@ describe('RecordingForm', () => {
.mockImplementation(() => {})

try {
const { container } = render(
const screen = await render(
<RecordingForm audio={new Blob(['audio'])} intent="create-call" />,
)
await screen.getByLabelText('Title').fill('My First Call')
await screen.getByRole('button', { name: 'Submit Recording' }).click()

fireEvent.change(screen.getByLabelText('Title'), {
target: { value: 'My First Call' },
})
const form = container.querySelector('form')
expect(form).not.toBeNull()
fireEvent.submit(form as HTMLFormElement)

await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1))
await expect.poll(() => fetchMock.mock.calls.length).toBe(1)
const [requestUrl, requestInit] = fetchMock.mock.calls[0] ?? []
expect(requestUrl).toBe('/resources/calls/save')
expect(requestInit?.method).toBe('POST')
Expand All @@ -178,10 +165,9 @@ describe('RecordingForm', () => {
expect(requestBody.get('title')).toBe('My First Call')
expect(requestBody.get('notes')).toBe('')

await waitFor(() =>
expect(mockNavigate).toHaveBeenCalledWith(
'/calls/record/fake-call-id?ok=1#done',
),
await expect.poll(() => mockNavigate.mock.calls.length).toBe(1)
expect(mockNavigate).toHaveBeenCalledWith(
'/calls/record/fake-call-id?ok=1#done',
)
expect(mockRevalidate).not.toHaveBeenCalled()
expect(jsonMock).not.toHaveBeenCalled()
Expand Down Expand Up @@ -258,7 +244,7 @@ describe('RecordingForm', () => {
const audio = new Blob(['audio'])

try {
const { container, rerender } = render(
const screen = await render(
<RecordingForm
audio={audio}
intent="create-call"
Expand All @@ -269,15 +255,13 @@ describe('RecordingForm', () => {
/>,
)

const form = container.querySelector('form')
expect(form).not.toBeNull()
fireEvent.submit(form as HTMLFormElement)
await screen.getByRole('button', { name: 'Submit Recording' }).click()

await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1))
await screen.findByText('Title is required')
await expect.poll(() => fetchMock.mock.calls.length).toBe(1)
await expect.element(screen.getByText('Title is required')).toBeVisible()

// Simulate parent rerendering with a fresh but equivalent data object.
rerender(
await screen.rerender(
<RecordingForm
audio={audio}
intent="create-call"
Expand All @@ -288,7 +272,7 @@ describe('RecordingForm', () => {
/>,
)

await screen.findByText('Title is required')
await expect.element(screen.getByText('Title is required')).toBeVisible()
} finally {
createObjectURL.mockRestore()
revokeObjectURL.mockRestore()
Expand All @@ -301,6 +285,8 @@ describe('RecordingForm', () => {
mockUseRootData.mockReturnValue({
requestInfo: { flyPrimaryInstance: null },
})
const fetchMock = vi.fn()
vi.stubGlobal('fetch', fetchMock as unknown as typeof fetch)
const createObjectURL = vi
.spyOn(URL, 'createObjectURL')
.mockReturnValue('blob:recording')
Expand All @@ -309,52 +295,56 @@ describe('RecordingForm', () => {
.mockImplementation(() => {})

try {
const { container } = render(
const screen = await render(
<RecordingForm audio={new Blob(['audio'])} intent="create-call" />,
)

const form = container.querySelector('form')
expect(form).not.toBeNull()
expect(form).toHaveAttribute('novalidate')
const form = document.querySelector('form')
expect(form?.hasAttribute('novalidate')).toBe(true)

const titleInput = screen.getByLabelText('Title')
expect(titleInput).toHaveAttribute('maxLength', '80')
expect(screen.getByText('80 characters left')).toBeInTheDocument()
const titleId = titleInput.getAttribute('id')
const titleInput = document.querySelector(
'input[name="title"]',
) as HTMLInputElement | null
expect(titleInput?.maxLength).toBe(80)
await expect.element(screen.getByText('80 characters left')).toBeVisible()
const titleId = titleInput?.getAttribute('id')
expect(titleId).toBeTruthy()
expect(titleInput).toHaveAttribute(
'aria-describedby',
expect(titleInput?.getAttribute('aria-describedby')).toBe(
`${titleId}-countdown`,
)

fireEvent.change(titleInput, { target: { value: 'abcd' } })
expect(screen.getByText('76 characters left')).toBeInTheDocument()
expect(
screen.queryByText('Title must be at least 5 characters'),
).not.toBeInTheDocument()
await screen.getByLabelText('Title').fill('abcd')
await expect
.element(screen.getByText('76 characters left'))
.toBeVisible()
expect(document.body.textContent).not.toContain(
'Title must be at least 5 characters',
)

fireEvent.blur(titleInput)
expect(
screen.getByText('Title must be at least 5 characters'),
).toBeInTheDocument()
// Locators don't expose a dedicated `focus()` helper; click focuses.
await screen.getByLabelText('Notes (optional)').click()
await expect
.element(screen.getByText('Title must be at least 5 characters'))
.toBeVisible()

const notesInput = screen.getByLabelText('Notes (optional)')
expect(notesInput).toHaveAttribute('maxLength', '5000')
const notesInput = document.querySelector(
'textarea[name="notes"]',
) as HTMLTextAreaElement | null
expect(notesInput?.maxLength).toBe(5000)

// Submit should surface validation and should not attempt to upload audio
// when validation fails.
fireEvent.change(titleInput, { target: { value: '' } })
fireEvent.submit(form as HTMLFormElement)
await screen.findByText('Title is required')
expect(titleInput.getAttribute('aria-describedby')).toContain(
`${titleId}-error`,
)
expect(titleInput.getAttribute('aria-describedby')).toContain(
`${titleId}-countdown`,
)
await screen.getByLabelText('Title').fill('')
await screen.getByRole('button', { name: 'Submit Recording' }).click()
await expect.element(screen.getByText('Title is required')).toBeVisible()
expect(fetchMock).not.toHaveBeenCalled()
const describedBy = titleInput?.getAttribute('aria-describedby') ?? ''
expect(describedBy).toContain(`${titleId}-error`)
expect(describedBy).toContain(`${titleId}-countdown`)
} finally {
createObjectURL.mockRestore()
revokeObjectURL.mockRestore()
vi.unstubAllGlobals()
}
})
})
Loading
Loading