/*
* Copyright (C) 2026 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see .
*/
import {fireEvent, render, screen, waitFor} from '@testing-library/react'
import {vi} from 'vitest'
import {AutoCaptioning} from '../AutoCaptioning'
const LIVE_REGION_ID = 'flash_screenreader_holder'
const mockLanguages = [
{id: 'en', label: 'English'},
{id: 'es', label: 'Spanish'},
{id: 'fr', label: 'French'},
]
type AutoCaptioningProps = Parameters[0]
function renderComponent(props: Partial = {}) {
const liveRegion = document.getElementById(LIVE_REGION_ID)
const defaultProps: AutoCaptioningProps = {
onCancel: vi.fn(),
onPrimary: vi.fn(),
liveRegion: () => liveRegion,
languages: mockLanguages,
...props,
}
return render()
}
describe('', () => {
beforeEach(() => {
const liveRegion = document.createElement('div')
liveRegion.id = LIVE_REGION_ID
liveRegion.setAttribute('role', 'alert')
document.body.appendChild(liveRegion)
})
afterEach(() => {
const liveRegion = document.getElementById(LIVE_REGION_ID)
if (liveRegion) {
document.body.removeChild(liveRegion)
}
})
it('renders language dropdown with provided languages', () => {
renderComponent()
// Open the dropdown by clicking placeholder
const selectPlaceholder = screen.getByPlaceholderText('Select Language')
fireEvent.click(selectPlaceholder)
// Verify all languages are present
expect(screen.getByText('English')).toBeInTheDocument()
expect(screen.getByText('Spanish')).toBeInTheDocument()
expect(screen.getByText('French')).toBeInTheDocument()
})
it('shows validation error when no language selected', () => {
renderComponent()
// Click Request without selecting language
const requestButton = screen.getByText('Request')
fireEvent.click(requestButton)
// Verify error message appears (multiple instances: FormField + Alert)
const errorMessages = screen.getAllByText('Please select a language')
expect(errorMessages.length).toBeGreaterThan(0)
})
it('clears validation error when language is selected', () => {
renderComponent()
// Trigger validation error
const requestButton = screen.getByText('Request')
fireEvent.click(requestButton)
const errorMessages = screen.getAllByText('Please select a language')
expect(errorMessages.length).toBeGreaterThan(0)
// Select a language
const selectPlaceholder = screen.getByPlaceholderText('Select Language')
fireEvent.click(selectPlaceholder)
fireEvent.click(screen.getByText('English'))
// Verify error disappears
expect(screen.queryByText('Please select a language')).not.toBeInTheDocument()
})
it('calls onPrimary with selected language ID', () => {
const onPrimary = vi.fn()
renderComponent({onPrimary})
// Open dropdown and select English
const selectPlaceholder = screen.getByPlaceholderText('Select Language')
fireEvent.click(selectPlaceholder)
fireEvent.click(screen.getByText('English'))
// Click Request
const requestButton = screen.getByText('Request')
fireEvent.click(requestButton)
// Verify onPrimary called with 'en'
expect(onPrimary).toHaveBeenCalledWith('en')
expect(onPrimary).toHaveBeenCalledTimes(1)
})
it('calls onCancel when Cancel button clicked', () => {
const onCancel = vi.fn()
renderComponent({onCancel})
const cancelButton = screen.getByText('Cancel')
fireEvent.click(cancelButton)
expect(onCancel).toHaveBeenCalledTimes(1)
})
it('renders with empty language list', () => {
renderComponent({languages: []})
expect(screen.getByPlaceholderText('Select Language')).toBeInTheDocument()
expect(screen.getAllByText('Language Spoken in This Media*').length).toBeGreaterThan(0)
// Should still render but with empty dropdown
expect(screen.getByPlaceholderText('Select Language')).toBeInTheDocument()
expect(screen.getByText('Cancel')).toBeInTheDocument()
expect(screen.getByText('Request')).toBeInTheDocument()
})
describe('a11y', () => {
it('a11y: focuses language dropdown when language is missing on submit', () => {
renderComponent()
fireEvent.click(screen.getByText('Request'))
const languageInput = screen.getByPlaceholderText('Select Language')
expect(document.activeElement).toBe(languageInput)
})
it('a11y: sets aria-invalid and aria-describedby on language dropdown when error shown', () => {
renderComponent()
fireEvent.click(screen.getByText('Request'))
const languageInput = screen.getByPlaceholderText('Select Language')
expect(languageInput).toHaveAttribute('aria-invalid', 'true')
expect(languageInput).toHaveAttribute('aria-describedby', 'cc-asr-language-error')
})
it('a11y: clears aria-invalid and aria-describedby after selecting a language', () => {
renderComponent()
fireEvent.click(screen.getByText('Request'))
const languageInput = screen.getByPlaceholderText('Select Language')
expect(languageInput).toHaveAttribute('aria-invalid', 'true')
expect(languageInput).toHaveAttribute('aria-describedby', 'cc-asr-language-error')
fireEvent.click(languageInput)
fireEvent.click(screen.getByText('English'))
expect(languageInput).not.toHaveAttribute('aria-invalid')
expect(languageInput).not.toHaveAttribute('aria-describedby')
})
})
describe('Pendo tracking', () => {
const mockTrack = vi.fn()
beforeEach(() => {
;(window as any).canvasUsageMetrics = {track: mockTrack}
})
afterEach(() => {
delete (window as any).canvasUsageMetrics
})
it('fires canvas_caption_validation_error missing_language when Request clicked with no language', async () => {
renderComponent()
fireEvent.click(screen.getByText('Request'))
await waitFor(() => {
expect(mockTrack).toHaveBeenCalledWith('canvas_caption_validation_error', {
type: 'track',
flow_type: 'request_auto',
error_type: 'missing_language',
})
})
})
})
describe('.onDirtyStateChanged', () => {
describe('called with true', () => {
it('when language is selected', async () => {
const onDirtyStateChanged = vi.fn()
renderComponent({onDirtyStateChanged})
// Select a language - click on placeholder text to open, then click option
const selectPlaceholder = screen.getByPlaceholderText('Select Language')
fireEvent.click(selectPlaceholder)
fireEvent.click(screen.getByText('English'))
await waitFor(() => {
expect(onDirtyStateChanged).toHaveBeenCalledWith(true)
})
})
})
})
})