Files
SkinbaseNova/resources/js/Pages/Studio/__tests__/StudioUploadQueue.test.jsx

362 lines
11 KiB
JavaScript

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 }) => <div>{children}</div>,
}))
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(<StudioUploadQueue />)
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(<StudioUploadQueue />)
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(<StudioUploadQueue />)
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(<StudioUploadQueue />)
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(<StudioUploadQueue />)
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(<StudioUploadQueue />)
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(<StudioUploadQueue />)
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(<StudioUploadQueue />)
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',
}),
})
})
})