feat(frontend): streamline task creation and preview workflows
- remove prompt and preview generation from task creation - create tasks as inactive and route directly to edit page - add generated message history UX to edit task - update entity/task views and related test coverage
This commit is contained in:
@@ -229,6 +229,7 @@ TASK DETAILS
|
||||
|
||||
INSTRUCTIONS
|
||||
- Write exactly one email message.
|
||||
- The message must be written by Milton Fiscal (Director of Ceremonial Logistics) as the sender persona.
|
||||
- Use an extremely formal corporate tone.
|
||||
- Keep the content casual, mundane, and slightly nonsensical.
|
||||
- Reflect the entity personality and task prompt faithfully.
|
||||
|
||||
@@ -39,11 +39,6 @@ const mockEntity = {
|
||||
|
||||
describe('CreateTaskPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(tasksApi.getEmailLookbackLabel).mockReturnValue('Last week')
|
||||
vi.mocked(tasksApi.buildTaskPreviewPrompt).mockImplementation(
|
||||
(entity, task) =>
|
||||
`PROMPT FOR ${entity.name}: ${task.name} | ${task.prompt} | ${task.scheduleCron} | ${task.emailLookback}`
|
||||
)
|
||||
vi.mocked(entitiesApi.getEntity).mockResolvedValue(mockEntity)
|
||||
mockNavigate.mockClear()
|
||||
})
|
||||
@@ -54,25 +49,36 @@ describe('CreateTaskPage', () => {
|
||||
await screen.findByRole('link', { name: /back to entity a/i })
|
||||
|
||||
expect(screen.getByLabelText(/task name/i)).toBeInTheDocument()
|
||||
expect(screen.getByLabelText(/task prompt/i)).toBeInTheDocument()
|
||||
expect(screen.queryByLabelText(/task prompt/i)).not.toBeInTheDocument()
|
||||
expect(screen.getByLabelText(/^Email Period$/i)).toBeInTheDocument()
|
||||
expect(screen.getByLabelText(/^Minute$/i)).toBeInTheDocument()
|
||||
expect(screen.getByLabelText(/^Hour$/i)).toBeInTheDocument()
|
||||
expect(screen.getByLabelText(/^Day of Month$/i)).toBeInTheDocument()
|
||||
expect(screen.getByLabelText(/^Month$/i)).toBeInTheDocument()
|
||||
expect(screen.getByLabelText(/^Day of Week$/i)).toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: /generate test message/i })).not.toBeInTheDocument()
|
||||
expect(screen.queryByText(/Final Prompt/i)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should_generatePreviewAndCreateTask_when_formSubmitted', async () => {
|
||||
vi.mocked(tasksApi.generateTaskPreview).mockResolvedValue('SUBJECT: Preview\nBODY:\nFormal nonsense')
|
||||
it('should_createInactiveTaskAndNavigateToEditTask_when_formSubmitted', async () => {
|
||||
vi.mocked(tasksApi.createTask).mockResolvedValue({
|
||||
id: 'task-2',
|
||||
entityId: 'entity-1',
|
||||
name: 'Morning Blast',
|
||||
prompt: 'Talk about coffee',
|
||||
prompt: '',
|
||||
scheduleCron: '0 8 * * 1-5',
|
||||
emailLookback: 'last_week',
|
||||
active: true,
|
||||
active: false,
|
||||
createdAt: '2026-03-26T10:00:00Z',
|
||||
})
|
||||
vi.mocked(tasksApi.inactivateTask).mockResolvedValue({
|
||||
id: 'task-2',
|
||||
entityId: 'entity-1',
|
||||
name: 'Morning Blast',
|
||||
prompt: '',
|
||||
scheduleCron: '0 8 * * 1-5',
|
||||
emailLookback: 'last_week',
|
||||
active: false,
|
||||
createdAt: '2026-03-26T10:00:00Z',
|
||||
})
|
||||
|
||||
@@ -97,45 +103,10 @@ describe('CreateTaskPage', () => {
|
||||
expect(screen.getByRole('button', { name: /Monthly/i })).toBeInTheDocument()
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/task name/i), { target: { value: 'Morning Blast' } })
|
||||
fireEvent.change(screen.getByLabelText(/task prompt/i), { target: { value: 'Talk about coffee' } })
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Weekdays/i }))
|
||||
fireEvent.change(screen.getByLabelText(/^Hour$/i), { target: { value: '8' } })
|
||||
|
||||
expect(screen.getByText(/Final Prompt/i)).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByText('PROMPT FOR Entity A: Morning Blast | Talk about coffee | 0 8 * * 1-5 | last_week')
|
||||
).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /generate test message/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(tasksApi.generateTaskPreview).toHaveBeenCalled()
|
||||
expect(vi.mocked(tasksApi.buildTaskPreviewPrompt)).toHaveBeenCalledWith(
|
||||
mockEntity,
|
||||
expect.objectContaining({
|
||||
entityId: 'entity-1',
|
||||
name: 'Morning Blast',
|
||||
prompt: 'Talk about coffee',
|
||||
scheduleCron: '0 8 * * 1-5',
|
||||
emailLookback: 'last_week',
|
||||
})
|
||||
)
|
||||
expect(vi.mocked(tasksApi.generateTaskPreview).mock.calls[0][0]).toEqual(
|
||||
expect.objectContaining({
|
||||
entity: mockEntity,
|
||||
task: expect.objectContaining({
|
||||
entityId: 'entity-1',
|
||||
name: 'Morning Blast',
|
||||
prompt: 'Talk about coffee',
|
||||
scheduleCron: '0 8 * * 1-5',
|
||||
emailLookback: 'last_week',
|
||||
}),
|
||||
})
|
||||
)
|
||||
expect(screen.getByText(/Formal nonsense/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /create task/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -144,32 +115,13 @@ describe('CreateTaskPage', () => {
|
||||
expect.objectContaining({
|
||||
entityId: 'entity-1',
|
||||
name: 'Morning Blast',
|
||||
prompt: 'Talk about coffee',
|
||||
prompt: '',
|
||||
scheduleCron: '0 8 * * 1-5',
|
||||
emailLookback: 'last_week',
|
||||
})
|
||||
)
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/entities/entity-1')
|
||||
})
|
||||
})
|
||||
|
||||
it('should_showReadablePreviewError_when_generationFails', async () => {
|
||||
vi.mocked(tasksApi.generateTaskPreview).mockRejectedValue(
|
||||
new Error('Unable to generate a test message from the local model. Connection refused')
|
||||
)
|
||||
|
||||
render(<CreateTaskPage />, { wrapper })
|
||||
await screen.findByRole('link', { name: /back to entity a/i })
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/task name/i), { target: { value: 'Morning Blast' } })
|
||||
fireEvent.change(screen.getByLabelText(/task prompt/i), { target: { value: 'Talk about coffee' } })
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /generate test message/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(/Unable to generate a test message from the local model. Connection refused/i)
|
||||
).toBeInTheDocument()
|
||||
expect(tasksApi.inactivateTask).toHaveBeenCalledWith('task-2')
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/entities/entity-1/tasks/task-2')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -186,6 +186,59 @@ describe('EditTaskPage', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should_renderGeneratedMessagePanels_when_pageLoads', async () => {
|
||||
renderPage()
|
||||
|
||||
await screen.findByRole('link', { name: /back to entity a/i })
|
||||
|
||||
expect(screen.getByText(/Final Prompt/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/^Generated Message$/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/^Generated Message History$/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/Generate a message and it will appear here./i)).toBeInTheDocument()
|
||||
expect(screen.getByRole('list', { name: /generated message history/i })).toBeInTheDocument()
|
||||
expect(screen.getByText(/No generated messages yet./i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should_showAndManageGeneratedMessageHistory_when_multipleMessagesGenerated', async () => {
|
||||
vi.mocked(tasksApi.generateTaskPreview)
|
||||
.mockResolvedValueOnce('SUBJECT: First\nBODY:\nFirst output')
|
||||
.mockResolvedValueOnce('SUBJECT: Second\nBODY:\nSecond output')
|
||||
|
||||
renderPage()
|
||||
await screen.findByRole('link', { name: /back to entity a/i })
|
||||
|
||||
const generateButton = screen.getByRole('button', { name: /generate test message/i })
|
||||
|
||||
fireEvent.click(generateButton)
|
||||
await screen.findByText(/First output/i)
|
||||
|
||||
fireEvent.click(generateButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Second output/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const history = screen.getByRole('list', { name: /generated message history/i })
|
||||
expect(history).toBeInTheDocument()
|
||||
|
||||
const firstMessageHistoryItem = screen.getByRole('button', { name: /^message #1$/i })
|
||||
const secondMessageHistoryItem = screen.getByRole('button', { name: /^message #2$/i })
|
||||
|
||||
fireEvent.click(firstMessageHistoryItem)
|
||||
expect(screen.getByText(/First output/i)).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: /delete message #1/i,
|
||||
})
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(firstMessageHistoryItem).not.toBeInTheDocument()
|
||||
expect(secondMessageHistoryItem).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should_renderActivateButton_when_taskIsInactive', async () => {
|
||||
renderPage({ task: { ...mockTask, active: false } })
|
||||
|
||||
|
||||
@@ -41,6 +41,37 @@ describe('EntitiesPage', () => {
|
||||
fireEvent.click(addButton)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
expect(screen.queryByLabelText(/default email context window/i)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should_submitDefaultContextWindow_when_createEntitySubmitted', async () => {
|
||||
vi.mocked(entitiesApi.getEntities).mockResolvedValue([])
|
||||
vi.mocked(entitiesApi.createEntity).mockResolvedValue(mockEntity)
|
||||
|
||||
render(<EntitiesPage />, { wrapper })
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /add|create|new/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('dialog', { name: /create entity/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/entity name/i), { target: { value: 'Test Entity' } })
|
||||
fireEvent.change(screen.getByLabelText(/sender email/i), { target: { value: 'test@condado.com' } })
|
||||
fireEvent.change(screen.getByLabelText(/job title/i), { target: { value: 'Tester' } })
|
||||
fireEvent.change(screen.getByLabelText(/personality notes/i), { target: { value: 'Formal' } })
|
||||
fireEvent.click(screen.getByRole('button', { name: /create/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(entitiesApi.createEntity).toHaveBeenCalled()
|
||||
expect(vi.mocked(entitiesApi.createEntity).mock.calls[0]?.[0]).toEqual({
|
||||
name: 'Test Entity',
|
||||
email: 'test@condado.com',
|
||||
jobTitle: 'Tester',
|
||||
personality: 'Formal',
|
||||
contextWindowDays: 3,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import { render, screen, waitFor, fireEvent } from '@testing-library/react'
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
@@ -20,6 +20,84 @@ const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
)
|
||||
|
||||
describe('EntityDetailPage', () => {
|
||||
it('should_updateEntity_when_editSubmittedFromDetailPage', async () => {
|
||||
vi.mocked(tasksApi.getEmailLookbackLabel).mockReturnValue('Last week')
|
||||
|
||||
vi.mocked(entitiesApi.getEntity)
|
||||
.mockResolvedValueOnce({
|
||||
id: 'entity-1',
|
||||
name: 'Entity A',
|
||||
email: 'a@a.com',
|
||||
jobTitle: 'Ops',
|
||||
personality: 'Formal',
|
||||
scheduleCron: '0 9 * * 1',
|
||||
contextWindowDays: 3,
|
||||
active: true,
|
||||
createdAt: '',
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
id: 'entity-1',
|
||||
name: 'Entity A Updated',
|
||||
email: 'updated@a.com',
|
||||
jobTitle: 'Operations Lead',
|
||||
personality: 'Still formal',
|
||||
scheduleCron: '0 9 * * 1',
|
||||
contextWindowDays: 5,
|
||||
active: true,
|
||||
createdAt: '',
|
||||
})
|
||||
|
||||
vi.mocked(entitiesApi.updateEntity).mockResolvedValue({
|
||||
id: 'entity-1',
|
||||
name: 'Entity A Updated',
|
||||
email: 'updated@a.com',
|
||||
jobTitle: 'Operations Lead',
|
||||
personality: 'Still formal',
|
||||
scheduleCron: '0 9 * * 1',
|
||||
contextWindowDays: 5,
|
||||
active: true,
|
||||
createdAt: '',
|
||||
})
|
||||
vi.mocked(tasksApi.getTasksByEntity).mockResolvedValue([])
|
||||
|
||||
render(<EntityDetailPage />, { wrapper })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Entity A/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /edit entity/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('dialog', { name: /edit entity/i })).toBeInTheDocument()
|
||||
expect(screen.getByLabelText(/entity name/i)).toHaveValue('Entity A')
|
||||
expect(screen.getByLabelText(/sender email/i)).toHaveValue('a@a.com')
|
||||
expect(screen.getByLabelText(/job title/i)).toHaveValue('Ops')
|
||||
expect(screen.getByLabelText(/personality notes/i)).toHaveValue('Formal')
|
||||
expect(screen.queryByLabelText(/default email context window/i)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/entity name/i), { target: { value: 'Entity A Updated' } })
|
||||
fireEvent.change(screen.getByLabelText(/sender email/i), { target: { value: 'updated@a.com' } })
|
||||
fireEvent.change(screen.getByLabelText(/job title/i), { target: { value: 'Operations Lead' } })
|
||||
fireEvent.change(screen.getByLabelText(/personality notes/i), { target: { value: 'Still formal' } })
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /^save$/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(entitiesApi.updateEntity).toHaveBeenCalledWith('entity-1', {
|
||||
name: 'Entity A Updated',
|
||||
email: 'updated@a.com',
|
||||
jobTitle: 'Operations Lead',
|
||||
personality: 'Still formal',
|
||||
contextWindowDays: 3,
|
||||
})
|
||||
expect(screen.queryByRole('dialog', { name: /edit entity/i })).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('heading', { name: /Entity A Updated/i })).toBeInTheDocument()
|
||||
expect(screen.getByText(/Operations Lead - updated@a.com/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should_renderEntityAndTasks_when_pageLoads', async () => {
|
||||
vi.mocked(tasksApi.getEmailLookbackLabel).mockReturnValue('Last week')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user