import React from 'react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { act, cleanup, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import StudioUploadQueue from '../StudioUploadQueue' let pageMock = { props: {} } vi.mock('@inertiajs/react', () => ({ usePage: () => pageMock, })) vi.mock('../../../Layouts/StudioLayout', () => ({ default: ({ children }) =>
{children}
, })) function makeQueueProps(overrides = {}) { return { title: 'Upload Queue', description: 'Queue drafts', chunkSize: 5242880, chunkRequestTimeoutMs: 45000, contentTypes: [ { name: 'Photography', categories: [ { id: 10, name: 'Portraits', children: [] }, ], }, ], queue: { filters: { batch_id: 1, status: 'all', sort: 'newest' }, batches: [{ id: 1, name: 'Spring Set' }], current_batch: { id: 1, name: 'Spring Set', status: 'completed_with_errors', total_items: 2, ready_items: 1, processing_items: 0, needs_review_items: 1, failed_items: 0, updated_at: '2026-04-18T10:00:00Z', }, status_options: [ { value: 'all', label: 'All' }, { value: 'ready', label: 'Ready' }, { value: 'needs_review', label: 'Needs review' }, ], sort_options: [ { value: 'newest', label: 'Newest first' }, { value: 'filename', label: 'Filename' }, ], items: [ { id: 101, title: 'Ready draft', original_filename: 'ready.webp', status: 'ready', processing_stage: 'finalized', metadata_label: '100% complete', is_ready_to_publish: true, missing: [], error_message: null, updated_at: '2026-04-18T10:00:00Z', edit_url: '/studio/artworks/1/edit', actions: { can_edit: true, can_publish: true, can_delete: true, can_retry_processing: false, can_generate_ai: true, }, }, { id: 102, title: 'Needs review draft', original_filename: 'review.webp', status: 'needs_review', processing_stage: 'finalized', metadata_label: '75% complete', is_ready_to_publish: false, missing: ['Needs maturity review'], error_message: null, updated_at: '2026-04-18T11:00:00Z', edit_url: '/studio/artworks/2/edit', actions: { can_edit: true, can_publish: false, can_delete: true, can_retry_processing: false, can_generate_ai: true, }, }, ], ...overrides.queue, }, ...overrides, } } describe('StudioUploadQueue', () => { beforeEach(() => { pageMock = { props: makeQueueProps() } window.axios = { get: vi.fn().mockResolvedValue({ data: pageMock.props.queue }), post: vi.fn().mockResolvedValue({ data: { success: 1, failed: 0, errors: [] } }), } window.confirm = vi.fn(() => true) window.prompt = vi.fn(() => 'DELETE') }) afterEach(() => { cleanup() vi.useRealTimers() vi.restoreAllMocks() }) it('renders mixed queue states and item actions', () => { render() expect(screen.getByText('Ready draft')).not.toBeNull() expect(screen.getByText('Needs review draft')).not.toBeNull() expect(screen.getAllByText('Ready to publish')[0]).not.toBeNull() expect(screen.getByText('Needs maturity review')).not.toBeNull() expect(screen.getAllByRole('link', { name: /edit in studio/i })).toHaveLength(2) expect(screen.getAllByRole('button', { name: /^generate ai$/i })).toHaveLength(3) }) it('reloads the queue when filters change', async () => { const user = userEvent.setup() render() await user.selectOptions(screen.getByRole('combobox', { name: /filter/i }), 'ready') await waitFor(() => { expect(window.axios.get).toHaveBeenCalledWith('/api/studio/upload-queue', { params: expect.objectContaining({ batch_id: 1, status: 'ready', sort: 'newest', }), }) }) }) it('shows a publish confirmation summary before bulk publish', async () => { const user = userEvent.setup() render() const checkboxes = screen.getAllByRole('checkbox') const itemCheckboxes = checkboxes.slice(-2) await user.click(itemCheckboxes[0]) await user.click(itemCheckboxes[1]) await user.click(screen.getByRole('button', { name: /publish selected/i })) expect(window.confirm).toHaveBeenCalledWith([ 'Publish 1 ready draft(s)?', 'Selected: 2', 'Ready now: 1', 'Blocked and skipped: 1', 'Needs review: 1', 'Blocked drafts will not be published.', ].join('\n')) await waitFor(() => { expect(window.axios.post).toHaveBeenCalledWith('/api/studio/upload-queue/bulk', expect.objectContaining({ action: 'publish', item_ids: [101, 102], })) }) }) it('does not attempt bulk publish when no selected drafts are ready', async () => { const user = userEvent.setup() pageMock = { props: makeQueueProps({ queue: { items: [ { id: 202, title: 'Blocked draft', original_filename: 'blocked.webp', status: 'needs_metadata', processing_stage: 'finalized', metadata_label: '50% complete', is_ready_to_publish: false, missing: ['Missing title'], error_message: null, updated_at: '2026-04-18T10:00:00Z', edit_url: '/studio/artworks/3/edit', actions: { can_edit: true, can_publish: false, can_delete: true, can_retry_processing: false, can_generate_ai: true, }, }, ], }, }), } render() const checkboxes = screen.getAllByRole('checkbox') await user.click(checkboxes.at(-1)) await user.click(screen.getByRole('button', { name: /publish selected/i })) expect(window.confirm).not.toHaveBeenCalled() expect(window.axios.post).not.toHaveBeenCalledWith('/api/studio/upload-queue/bulk', expect.objectContaining({ action: 'publish', })) expect(screen.getByText('None of the selected drafts are ready to publish yet.')).not.toBeNull() }) it('shows the correct Studio links and publish readiness state per item', () => { render() const studioLinks = screen.getAllByRole('link', { name: /edit in studio/i }) expect(studioLinks).toHaveLength(2) expect(studioLinks[0].getAttribute('href')).toBe('/studio/artworks/1/edit') expect(studioLinks[1].getAttribute('href')).toBe('/studio/artworks/2/edit') expect(screen.getAllByRole('button', { name: /^publish$/i })).toHaveLength(1) }) it('bulk actions apply only to selected queue items', async () => { const user = userEvent.setup() render() const checkboxes = screen.getAllByRole('checkbox') const itemCheckboxes = checkboxes.slice(-2) await user.click(itemCheckboxes[0]) await user.click(screen.getAllByRole('button', { name: /^generate ai$/i })[0]) await waitFor(() => { expect(window.axios.post).toHaveBeenCalledWith('/api/studio/upload-queue/bulk', expect.objectContaining({ action: 'generate_ai', item_ids: [101], })) }) }) it('shows failed items clearly and lets creators retry them', async () => { const user = userEvent.setup() pageMock = { props: makeQueueProps({ queue: { current_batch: { id: 1, name: 'Spring Set', status: 'completed_with_errors', total_items: 1, ready_items: 0, processing_items: 0, needs_review_items: 0, failed_items: 1, updated_at: '2026-04-18T12:00:00Z', }, items: [ { id: 303, title: 'Broken draft', original_filename: 'broken.webp', status: 'failed', processing_stage: 'finalized', metadata_label: '25% complete', is_ready_to_publish: false, missing: ['Processing incomplete'], error_message: 'Derivative generation failed.', updated_at: '2026-04-18T12:00:00Z', edit_url: '/studio/artworks/4/edit', actions: { can_edit: true, can_publish: false, can_delete: true, can_retry_processing: true, can_generate_ai: true, }, }, ], }, }), } render() expect(screen.getByText('Derivative generation failed.')).not.toBeNull() expect(screen.getByText('Processing incomplete')).not.toBeNull() await user.click(screen.getByRole('button', { name: /retry/i })) await waitFor(() => { expect(window.axios.post).toHaveBeenCalledWith('/api/studio/upload-queue/items/303/retry') }) }) it('polls the queue while processing items are still running', async () => { vi.useFakeTimers() pageMock = { props: makeQueueProps({ queue: { current_batch: { id: 1, name: 'Spring Set', status: 'processing', total_items: 1, ready_items: 0, processing_items: 1, needs_review_items: 0, failed_items: 0, updated_at: '2026-04-18T12:15:00Z', }, items: [ { id: 404, title: 'Processing draft', original_filename: 'processing.webp', status: 'processing', processing_stage: 'maturity_check', metadata_label: '50% complete', is_ready_to_publish: false, missing: ['Maturity analysis pending'], error_message: null, updated_at: '2026-04-18T12:15:00Z', edit_url: '/studio/artworks/5/edit', actions: { can_edit: true, can_publish: false, can_delete: true, can_retry_processing: false, can_generate_ai: true, }, }, ], }, }), } render() expect(screen.getByText('Maturity analysis pending')).not.toBeNull() expect(screen.queryByRole('button', { name: /^publish$/i })).toBeNull() await act(async () => { vi.advanceTimersByTime(3000) await Promise.resolve() }) expect(window.axios.get).toHaveBeenCalledWith('/api/studio/upload-queue', { params: expect.objectContaining({ batch_id: 1, status: 'all', sort: 'newest', }), }) }) })