Skip to content
Open
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
16 changes: 15 additions & 1 deletion components/features/tts/ReadAloudPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export function ReadAloudPanel({
const [isReadAloudOpen, setIsReadAloudOpen] = useState(false)
const [ttsLoading, setTtsLoading] = useState(false)
const [ttsError, setTtsError] = useState<string | null>(null)
/** Autoplay blocked (e.g. browser policy) — not a generation failure; separate from `ttsError`. */
const [ttsPlaybackHint, setTtsPlaybackHint] = useState<string | null>(null)
const [ttsAudioUrl, setTtsAudioUrl] = useState<string | null>(null)
const [voiceGender, setVoiceGender] = useState<Gender>('feminine')
const audioRef = useRef<HTMLAudioElement | null>(null)
Expand All @@ -53,11 +55,15 @@ export function ReadAloudPanel({
useEffect(() => {
if (!ttsAudioUrl || !audioRef.current) return

setTtsPlaybackHint(null)

const maybePlay = async () => {
try {
await audioRef.current?.play()
} catch {
setTtsError('Audio is ready. Tap Play in AI Read Aloud to start playback.')
setTtsPlaybackHint(
'Audio is ready. Tap Play in AI Read Aloud to start playback.'
)
}
}

Expand All @@ -69,6 +75,7 @@ export function ReadAloudPanel({

setTtsLoading(true)
setTtsError(null)
setTtsPlaybackHint(null)
try {
const response = await fetch('/api/tts', {
method: 'POST',
Expand Down Expand Up @@ -169,6 +176,13 @@ export function ReadAloudPanel({
)}
</div>

{ttsPlaybackHint && (
<Alert>
<AlertTitle>Ready to play</AlertTitle>
<AlertDescription>{ttsPlaybackHint}</AlertDescription>
</Alert>
)}

{ttsError && (
<Alert variant="destructive">
<AlertTitle>Read Aloud failed</AlertTitle>
Expand Down
44 changes: 44 additions & 0 deletions cypress.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { defineConfig } from "cypress";
import path from "path";

type CypressWebpackConfig = {
resolve?: {
alias?: Record<string, unknown>
} & Record<string, unknown>
} & Record<string, unknown>

export default defineConfig({
projectId: '51aa3d',
allowCypressEnv: false,

e2e: {
baseUrl: 'http://localhost:3000',
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
supportFile: 'cypress/support/e2e.ts',
},

component: {
devServer: {
framework: "next",
bundler: "webpack",
webpackConfig: async (config: CypressWebpackConfig = {}) => {
return {
...config,
resolve: {
...(config.resolve ?? {}),
alias: {
...(config.resolve?.alias ?? {}),
'next/navigation': path.resolve(__dirname, 'cypress/mocks/next-navigation.ts'),
'@/app/actions/logging': path.resolve(__dirname, 'cypress/mocks/logging.ts'),
}
}
}
},
},
setupNodeEvents(on, config) {
void on
process.env.CYPRESS_COMPONENT_TEST = "true"
return config
},
},
});
41 changes: 41 additions & 0 deletions cypress/component/LandingPage.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react'
import { UploadForm } from '@/components/upload-form'

const SOURCE_LANGUAGES = [
'Detect language', 'English', 'Spanish', 'French', 'German',
'Chinese (Simplified)', 'Chinese (Traditional)', 'Japanese', 'Korean',
'Portuguese', 'Italian', 'Russian', 'Arabic', 'Hindi', 'Dutch',
'Polish', 'Swedish', 'Turkish', 'Vietnamese',
]

const TARGET_LANGUAGES = SOURCE_LANGUAGES.filter(l => l !== 'Detect language')

describe('<UploadForm /> - All Languages', () => {

it('renders all source language options', () => {
cy.mount(<UploadForm />)
cy.contains('button', 'Detect language').click()

SOURCE_LANGUAGES.forEach(lang => {
cy.get('[role="option"]').contains(lang).should('exist')
})
})

it('renders all target language options', () => {
cy.mount(<UploadForm />)
cy.contains('button', 'Spanish').click()

TARGET_LANGUAGES.forEach(lang => {
cy.get('[role="option"]').contains(lang).should('exist')
})
})

TARGET_LANGUAGES.forEach(lang => {
it(`can select ${lang} as the target language`, () => {
cy.mount(<UploadForm />)
cy.contains('button', 'Spanish').click()
cy.get('[role="option"]').contains(lang).click()
cy.contains('button', lang).should('exist')
})
})
})
227 changes: 227 additions & 0 deletions cypress/component/UploadForm.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import React from 'react'
import { UploadForm } from '@/components/upload-form'

describe('<UploadForm /> - File Upload', () => {

it('renders empty dropzone initially', () => {
cy.mount(<UploadForm />)
cy.contains('Drag & drop or choose a file').should('exist')
cy.contains('Browse files').should('exist')
cy.get('button[type="submit"]').should('be.disabled')
})

it('displays file info after selection', () => {
cy.mount(<UploadForm />)

cy.get('input[type="file"]').selectFile({
contents: Cypress.Buffer.from('test content'),
fileName: 'test-doc.pdf',
mimeType: 'application/pdf',
}, { force: true })

cy.contains('test-doc.pdf').should('exist')
cy.get('button[type="submit"]').should('not.be.disabled')
})

it('shows file size in KB', () => {
cy.mount(<UploadForm />)

const content = 'x'.repeat(2048)
cy.get('input[type="file"]').selectFile({
contents: Cypress.Buffer.from(content),
fileName: 'sized-file.pdf',
mimeType: 'application/pdf',
}, { force: true })

cy.contains('2 KB').should('exist')
})

it('can remove selected file', () => {
cy.mount(<UploadForm />)

cy.get('input[type="file"]').selectFile({
contents: Cypress.Buffer.from('test'),
fileName: 'remove-me.pdf',
mimeType: 'application/pdf',
}, { force: true })

cy.contains('remove-me.pdf').should('exist')
cy.get('[aria-label="Remove file"]').click()
cy.contains('remove-me.pdf').should('not.exist')
cy.get('button[type="submit"]').should('be.disabled')
})

it('can replace file by selecting another', () => {
cy.mount(<UploadForm />)

cy.get('input[type="file"]').selectFile({
contents: Cypress.Buffer.from('first'),
fileName: 'first.pdf',
mimeType: 'application/pdf',
}, { force: true })

cy.contains('first.pdf').should('exist')

cy.get('input[type="file"]').selectFile({
contents: Cypress.Buffer.from('second'),
fileName: 'second.pdf',
mimeType: 'application/pdf',
}, { force: true })

cy.contains('second.pdf').should('exist')
cy.contains('first.pdf').should('not.exist')
})

it('accepts PDF files', () => {
cy.mount(<UploadForm />)

cy.get('input[type="file"]').selectFile({
contents: Cypress.Buffer.from('%PDF-1.4'),
fileName: 'document.pdf',
mimeType: 'application/pdf',
}, { force: true })

cy.contains('document.pdf').should('exist')
})

it('accepts DOCX files', () => {
cy.mount(<UploadForm />)

cy.get('input[type="file"]').selectFile({
contents: Cypress.Buffer.from('PK'),
fileName: 'document.docx',
mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
}, { force: true })

cy.contains('document.docx').should('exist')
})

it('accepts TXT files', () => {
cy.mount(<UploadForm />)

cy.get('input[type="file"]').selectFile({
contents: Cypress.Buffer.from('Hello world'),
fileName: 'notes.txt',
mimeType: 'text/plain',
}, { force: true })

cy.contains('notes.txt').should('exist')
})

it('accepts image files', () => {
cy.mount(<UploadForm />)

cy.get('input[type="file"]').selectFile({
contents: Cypress.Buffer.from([0x89, 0x50, 0x4E, 0x47]),
fileName: 'photo.png',
mimeType: 'image/png',
}, { force: true })

cy.contains('photo.png').should('exist')
})
})

describe('<UploadForm /> - Mobile Layout', () => {

it('renders mobile-specific UI', () => {
cy.mount(<UploadForm mobile />)
cy.contains('Take a photo or upload a file').should('exist')
cy.contains('Take photo').should('exist')
cy.contains('10MB max').should('exist')
})

it('shows file info in mobile layout', () => {
cy.mount(<UploadForm mobile />)

cy.get('input[type="file"]').selectFile({
contents: Cypress.Buffer.from('mobile test'),
fileName: 'mobile-doc.pdf',
mimeType: 'application/pdf',
}, { force: true })

cy.contains('mobile-doc.pdf').should('exist')
cy.contains('Tap to replace').should('exist')
})

it('can remove file in mobile layout', () => {
cy.mount(<UploadForm mobile />)

cy.get('input[type="file"]').selectFile({
contents: Cypress.Buffer.from('test'),
fileName: 'mobile-remove.pdf',
mimeType: 'application/pdf',
}, { force: true })

cy.get('[aria-label="Remove file"]').click()
cy.contains('mobile-remove.pdf').should('not.exist')
})
})

describe('<UploadForm /> - Language Swap', () => {

it('swap button is disabled when source is auto-detect', () => {
cy.mount(<UploadForm />)
cy.get('[aria-label="Swap languages"]').should('be.disabled')
})

it('swap button is enabled when source is not auto-detect', () => {
cy.mount(<UploadForm />)

cy.contains('button', 'Detect language').click()
cy.get('[role="option"]').contains('English').click()

cy.get('[aria-label="Swap languages"]').should('not.be.disabled')
})

it('swaps source and target languages', () => {
cy.mount(<UploadForm />)

cy.contains('button', 'Detect language').click()
cy.get('[role="option"]').contains('French').click()

cy.contains('button', 'French').should('exist')
cy.contains('button', 'Spanish').should('exist')

cy.get('[aria-label="Swap languages"]').click()

cy.contains('button', 'Spanish').should('exist')
cy.contains('button', 'French').should('exist')
})
})

describe('<UploadForm /> - Submit Button State', () => {

it('submit button is disabled without file', () => {
cy.mount(<UploadForm />)
cy.get('button[type="submit"]').should('be.disabled')
})

it('submit button is enabled with file', () => {
cy.mount(<UploadForm />)

cy.get('input[type="file"]').selectFile({
contents: Cypress.Buffer.from('content'),
fileName: 'ready.pdf',
mimeType: 'application/pdf',
}, { force: true })

cy.get('button[type="submit"]').should('not.be.disabled')
cy.contains('Translate document').should('exist')
})

it('submit button becomes disabled after file removal', () => {
cy.mount(<UploadForm />)

cy.get('input[type="file"]').selectFile({
contents: Cypress.Buffer.from('content'),
fileName: 'temp.pdf',
mimeType: 'application/pdf',
}, { force: true })

cy.get('button[type="submit"]').should('not.be.disabled')

cy.get('[aria-label="Remove file"]').click()

cy.get('button[type="submit"]').should('be.disabled')
})
})
Loading