Wire admin studio SSR and search infrastructure
This commit is contained in:
362
resources/js/Pages/Studio/__tests__/StudioUploadQueue.test.jsx
Normal file
362
resources/js/Pages/Studio/__tests__/StudioUploadQueue.test.jsx
Normal file
@@ -0,0 +1,362 @@
|
||||
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',
|
||||
}),
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user