diff --git a/CHANGELOG.md b/CHANGELOG.md index c714fe6e..a9c54ec0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,6 +94,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Spaces: allow empty Spaces (no last-node warning/auto-close), add pane context menu action to create an empty Space, and allow archiving a Space without saving its history. (#171) ### 🐞 Fixed +- Task: keep priority menus above the Task Creator and Task Editor dialog backdrops. (#313) - Notifications: clicking an Agent notification from another Project now lands on the corresponding Agent instead of only switching Projects. (#310) - Agent: derive directly launched Agent titles from their first bound session and keep short-turn completion notifications aligned with the resolved title. (#309) - Sidebar: distinguish expandable Spaces from empty Spaces in the collapsed rail, keeping disclosure arrows only for Spaces with agents and using a subtle leaf marker for empty Spaces. (#308) diff --git a/src/contexts/workspace/presentation/renderer/components/workspaceCanvas/windows/TaskCreatorWindow.tsx b/src/contexts/workspace/presentation/renderer/components/workspaceCanvas/windows/TaskCreatorWindow.tsx index 4a6431d8..91849f1e 100644 --- a/src/contexts/workspace/presentation/renderer/components/workspaceCanvas/windows/TaskCreatorWindow.tsx +++ b/src/contexts/workspace/presentation/renderer/components/workspaceCanvas/windows/TaskCreatorWindow.tsx @@ -194,6 +194,7 @@ export function TaskCreatorWindow({ ({ diff --git a/src/contexts/workspace/presentation/renderer/components/workspaceCanvas/windows/TaskEditorWindow.tsx b/src/contexts/workspace/presentation/renderer/components/workspaceCanvas/windows/TaskEditorWindow.tsx index dfaf2631..59ceac81 100644 --- a/src/contexts/workspace/presentation/renderer/components/workspaceCanvas/windows/TaskEditorWindow.tsx +++ b/src/contexts/workspace/presentation/renderer/components/workspaceCanvas/windows/TaskEditorWindow.tsx @@ -186,6 +186,7 @@ export function TaskEditorWindow({ ({ diff --git a/tests/e2e/workspace-canvas.tasks.edit-delete.spec.ts b/tests/e2e/workspace-canvas.tasks.edit-delete.spec.ts index 5c4fc24f..b2f38ad5 100644 --- a/tests/e2e/workspace-canvas.tasks.edit-delete.spec.ts +++ b/tests/e2e/workspace-canvas.tasks.edit-delete.spec.ts @@ -7,10 +7,19 @@ import { } from './workspace-canvas.helpers' test.describe('Workspace Canvas - Tasks (Edit & Delete)', () => { - test('supports task edit, resize, and delete confirmation', async () => { + test('supports task edit, resize, and delete confirmation', async ({ + browserName: _browserName, + }, testInfo) => { const { electronApp, window } = await launchApp() try { + const initialViewport = await window.evaluate(() => ({ + width: window.innerWidth, + height: window.innerHeight, + })) + + await window.setViewportSize({ width: 720, height: 480 }) + await clearAndSeedWorkspace(window, [ { id: 'task-edit-node', @@ -22,6 +31,7 @@ test.describe('Workspace Canvas - Tasks (Edit & Delete)', () => { task: { requirement: 'Initial requirement text', status: 'todo', + priority: 'medium', linkedAgentNodeId: null, lastRunAt: null, autoGeneratedTitle: false, @@ -54,10 +64,51 @@ test.describe('Workspace Canvas - Tasks (Edit & Delete)', () => { .locator('[data-testid="workspace-task-editor-requirement"]') .fill('Implement retry with capped exponential backoff and jitter') + const priorityMenu = window.locator('[data-testid="workspace-task-editor-priority-menu"]') + await window.locator('[data-testid="workspace-task-editor-priority-trigger"]').click() + await expect(priorityMenu).toBeVisible() + await expect(priorityMenu).toHaveClass(/cove-select__menu--within-dialog/) + + const editorBackdrop = window.locator('.cove-window-backdrop').filter({ has: editor }) + const menuZIndex = await priorityMenu.evaluate(element => + Number(window.getComputedStyle(element).zIndex), + ) + const backdropZIndex = await editorBackdrop.evaluate(element => + Number(window.getComputedStyle(element).zIndex), + ) + + expect(menuZIndex).toBe(175) + expect(backdropZIndex).toBe(160) + expect(menuZIndex).toBeGreaterThan(backdropZIndex) + + const priorityMenuBox = await priorityMenu.boundingBox() + const viewport = window.viewportSize() + if (!priorityMenuBox || !viewport) { + throw new Error('task priority menu or viewport bounding box unavailable') + } + + expect(priorityMenuBox.x).toBeGreaterThanOrEqual(0) + expect(priorityMenuBox.y).toBeGreaterThanOrEqual(0) + expect(priorityMenuBox.x + priorityMenuBox.width).toBeLessThanOrEqual(viewport.width) + expect(priorityMenuBox.y + priorityMenuBox.height).toBeLessThanOrEqual(viewport.height) + + const urgentOption = priorityMenu.locator('[data-cove-select-option-value="urgent"]') + await expect(urgentOption).toBeVisible() + + const screenshotPath = testInfo.outputPath('task-editor-priority-menu-layer.png') + await window.screenshot({ path: screenshotPath }) + await testInfo.attach('task-editor-priority-menu-layer', { + path: screenshotPath, + contentType: 'image/png', + }) + + await urgentOption.click() + await window.setViewportSize(initialViewport) await window.locator('[data-testid="workspace-task-edit-submit"]').click() await expect(taskNode.locator('[data-testid="task-node-inline-title-input"]')).toHaveCount(0) await expect(taskNode.locator('.task-node__header')).toContainText('Retry login workflow') + await expect(taskNode.locator('.task-node__priority')).toHaveText('URGENT') await expect(inlineRequirementInput).toHaveValue(/capped exponential/) await expect(taskNode.locator('.task-node__inline-hint')).toHaveCount(0) @@ -116,6 +167,7 @@ test.describe('Workspace Canvas - Tasks (Edit & Delete)', () => { title: string task?: { requirement?: string + priority?: string } }> }> @@ -129,6 +181,7 @@ test.describe('Workspace Canvas - Tasks (Edit & Delete)', () => { expect(resizedTask?.height ?? 0).toBeGreaterThan(280) expect(resizedTask?.title).toBe('Retry login workflow final') expect(resizedTask?.task?.requirement).toContain('capped exponential') + expect(resizedTask?.task?.priority).toBe('urgent') await taskNode.locator('[data-testid="task-node-close"]').click() diff --git a/tests/unit/contexts/coveSelect.spec.tsx b/tests/unit/contexts/coveSelect.spec.tsx index 8886c96d..0a2f8d3c 100644 --- a/tests/unit/contexts/coveSelect.spec.tsx +++ b/tests/unit/contexts/coveSelect.spec.tsx @@ -70,6 +70,14 @@ describe('CoveSelect', () => { expect(screen.queryByRole('option', { name: 'Light' })).not.toBeInTheDocument() }) + it('keeps a standalone select on the default popover layer', () => { + renderHarness() + + fireEvent.click(screen.getByTestId('theme-select-trigger')) + + expect(screen.getByRole('listbox')).not.toHaveClass('cove-select__menu--within-dialog') + }) + it('raises a portaled menu above its containing dialog', () => { render( {}}> diff --git a/tests/unit/contexts/taskPrioritySelectLayer.spec.tsx b/tests/unit/contexts/taskPrioritySelectLayer.spec.tsx new file mode 100644 index 00000000..cbeb9afb --- /dev/null +++ b/tests/unit/contexts/taskPrioritySelectLayer.spec.tsx @@ -0,0 +1,89 @@ +import React, { useState } from 'react' +import { fireEvent, render, screen } from '@testing-library/react' +import { describe, expect, it } from 'vitest' +import type { + TaskCreatorState, + TaskEditorState, +} from '../../../src/contexts/workspace/presentation/renderer/components/workspaceCanvas/types' +import { TaskCreatorWindow } from '../../../src/contexts/workspace/presentation/renderer/components/workspaceCanvas/windows/TaskCreatorWindow' +import { TaskEditorWindow } from '../../../src/contexts/workspace/presentation/renderer/components/workspaceCanvas/windows/TaskEditorWindow' + +function TaskEditorHarness(): React.JSX.Element { + const [taskEditor, setTaskEditor] = useState({ + nodeId: 'task-edit-node', + initialTitle: 'Initial Task Title', + initialAutoGeneratedTitle: false, + titleGeneratedInEditor: false, + title: 'Initial Task Title', + requirement: 'Initial requirement text', + priority: 'medium', + selectedTags: [], + autoGenerateTitle: false, + isGeneratingTitle: false, + isSaving: false, + error: null, + }) + + return ( + {}} + generateTaskEditorTitle={async () => {}} + saveTaskEdits={async () => {}} + /> + ) +} + +function TaskCreatorHarness(): React.JSX.Element { + const [taskCreator, setTaskCreator] = useState({ + anchor: { x: 120, y: 80 }, + title: '', + requirement: '', + priority: 'medium', + selectedTags: [], + autoGenerateTitle: false, + isGeneratingTitle: false, + isCreating: false, + error: null, + }) + + return ( + {}} + generateTaskTitle={async () => {}} + createTask={async () => {}} + /> + ) +} + +describe('task priority select dialog layer', () => { + it('raises the Task Editor priority menu above its modal backdrop', () => { + render() + + fireEvent.click(screen.getByTestId('workspace-task-editor-priority-trigger')) + + const menu = screen.getByTestId('workspace-task-editor-priority-menu') + expect(menu.parentElement).toBe(document.body) + expect(menu).toHaveClass('cove-select__menu--within-dialog') + }) + + it('raises the Task Creator priority menu above its modal backdrop', () => { + render() + + fireEvent.click(screen.getByTestId('workspace-task-advanced-toggle')) + fireEvent.click(screen.getByTestId('workspace-task-priority-trigger')) + + const menu = screen.getByTestId('workspace-task-priority-menu') + expect(menu.parentElement).toBe(document.body) + expect(menu).toHaveClass('cove-select__menu--within-dialog') + }) +})