/* * Copyright (C) 2025 - 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} from '@testing-library/react' import {vi} from 'vitest' import {CaptionRow, type CaptionRowProps} from '../CaptionRow' function renderComponent(props: CaptionRowProps) { return render() } describe('', () => { it('renders uploaded caption row with delete action', () => { const onDelete = vi.fn() renderComponent({ workflow_state: 'ready', captionName: 'English Caption', onDelete, }) expect(screen.getByText(/^english caption$/i)).toBeInTheDocument() expect(screen.getByText(/^delete english caption$/i)).toBeInTheDocument() fireEvent.click(screen.getByText(/^delete english caption$/i)) expect(onDelete).toHaveBeenCalledTimes(1) }) it('uploaded caption: shows download link with href and filename attributes', () => { const onDelete = vi.fn() renderComponent({ workflow_state: 'ready', captionName: 'Spanish Caption', url: 'https://example.com/es.srt', filename: 'spanish_es.srt', onDelete, }) const downloadLink = screen.getByText(/download spanish caption/i).closest('a') expect(downloadLink).toBeInTheDocument() expect(downloadLink).toHaveAttribute('href', 'https://example.com/es.srt') expect(downloadLink).toHaveAttribute('download', 'spanish_es.srt') }) it('uploaded caption: download button always shown in ready state', () => { renderComponent({ workflow_state: 'ready', captionName: 'Spanish Caption', onDelete: vi.fn(), }) expect(screen.getByText(/download spanish caption/i)).toBeInTheDocument() }) it('renders processing state properly displaying text', () => { renderComponent({ workflow_state: 'processing', captionName: 'French Caption', }) expect(screen.getByText(/french caption/i)).toBeInTheDocument() expect(screen.getByText(/processing\.\.\./i)).toBeInTheDocument() expect(screen.queryByText(/delete/i)).not.toBeInTheDocument() expect(screen.queryByText(/download/i)).not.toBeInTheDocument() }) it('failed upload: shows "Upload failed"', () => { renderComponent({ workflow_state: 'failed', captionName: 'German Caption', failedOperation: 'upload', }) expect(screen.getByText(/german caption/i)).toBeInTheDocument() expect(screen.getByText(/upload failed/i)).toBeInTheDocument() }) it('failed ASR: shows "Generation failed"', () => { renderComponent({ workflow_state: 'failed', captionName: 'German Caption', failedOperation: 'asr', }) expect(screen.getByLabelText(/german caption, generation failed/i)).toBeInTheDocument() }) it('failed delete: shows "Delete failed"', () => { renderComponent({ workflow_state: 'failed', captionName: 'German Caption', failedOperation: 'delete', }) expect(screen.getByLabelText(/german caption, delete failed/i)).toBeInTheDocument() }) it('server-side ASR failure (no failedOperation): shows "Generation failed"', () => { renderComponent({ workflow_state: 'failed', captionName: 'German Caption', asr: true, }) expect(screen.getByLabelText(/german caption, generation failed/i)).toBeInTheDocument() }) it('failed state: shows retry button and calls onRetry when clicked', () => { const onRetry = vi.fn() renderComponent({ workflow_state: 'failed', captionName: 'Spanish Caption', failedOperation: 'upload', onRetry, }) expect(screen.getByText(/^spanish caption$/i)).toBeInTheDocument() expect(screen.getByText(/^upload failed$/i)).toBeInTheDocument() const retryButton = screen.getByText(/^retry spanish caption$/i) expect(retryButton).toBeInTheDocument() fireEvent.click(retryButton) expect(onRetry).toHaveBeenCalledTimes(1) }) it('failed state: shows delete button and calls onDelete when clicked', () => { const onDelete = vi.fn() renderComponent({ workflow_state: 'failed', captionName: 'Spanish Caption', failedOperation: 'asr', onDelete, }) const deleteButton = screen.getByText(/delete spanish caption/i) expect(deleteButton).toBeInTheDocument() fireEvent.click(deleteButton) expect(onDelete).toHaveBeenCalledTimes(1) }) it('failed state: does not show delete button when onDelete is not provided', () => { renderComponent({ workflow_state: 'failed', captionName: 'French Caption', failedOperation: 'asr', }) expect(screen.queryByText(/delete french caption/i)).not.toBeInTheDocument() }) it('failed state: does not show retry button when onRetry is not provided', () => { renderComponent({ workflow_state: 'failed', captionName: 'French Caption', failedOperation: 'delete', }) expect(screen.queryByText(/retry/i)).not.toBeInTheDocument() }) it('processing state: row aria-label includes caption name and status', () => { renderComponent({ workflow_state: 'processing', captionName: 'French Caption', }) expect(screen.getByLabelText(/french caption, processing\.\.\./i)).toBeInTheDocument() }) it('failed state: row aria-label includes caption name and status text', () => { renderComponent({ workflow_state: 'failed', captionName: 'German Caption', failedOperation: 'asr', }) expect(screen.getByLabelText(/german caption, generation failed/i)).toBeInTheDocument() }) it('ready state: row aria-label is just the caption name', () => { renderComponent({ workflow_state: 'ready', captionName: 'English Caption', onDelete: vi.fn(), }) expect(screen.getByLabelText(/english caption/i)).toBeInTheDocument() }) it('inherited state: disabled delete action and has proper aria label on delete button', () => { const onDelete = vi.fn() renderComponent({ workflow_state: 'ready', captionName: 'Inherited Caption', isInherited: true, onDelete, }) expect(screen.getByText(/^inherited caption$/i)).toBeInTheDocument() const inheritedMessages = screen.getAllByText( /Captions inherited from a parent course cannot be removed/i, ) expect(inheritedMessages.length).toBeGreaterThan(0) }) })