diff --git a/e2e/path-delete.spec.ts b/e2e/path-delete.spec.ts new file mode 100644 index 0000000..06f6892 --- /dev/null +++ b/e2e/path-delete.spec.ts @@ -0,0 +1,86 @@ +import { test, expect } from '@playwright/test'; + +const EDITOR_LOCATOR = '.pf-v6-c-page'; + +test.describe('Path deletion', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + await expect(page.locator(EDITOR_LOCATOR)).toBeVisible({ timeout: 15000 }); + }); + + test('trash icon appears next to path items', async ({ page }) => { + // The default Pet Store API has paths - verify trash icons are present + const trashButton = page.getByRole('button', { name: /^Delete path / }); + await expect(trashButton.first()).toBeVisible(); + }); + + test('clicking trash icon opens confirmation dialog', async ({ page }) => { + // Click the trash icon on the first path + const trashButton = page.getByRole('button', { name: /^Delete path / }).first(); + await trashButton.click(); + + // Verify the confirmation modal appears + await expect(page.getByText('Delete Path')).toBeVisible(); + await expect(page.getByText('Are you sure you want to delete the path')).toBeVisible(); + }); + + test('cancel button closes confirmation dialog without deleting', async ({ page }) => { + // Count initial paths + const initialTrashButtons = page.getByRole('button', { name: /^Delete path / }); + const initialCount = await initialTrashButtons.count(); + + // Click trash on the first path + await initialTrashButtons.first().click(); + + // Verify dialog is open + await expect(page.getByText('Are you sure you want to delete the path')).toBeVisible(); + + // Click Cancel + await page.getByRole('button', { name: 'Cancel' }).click(); + + // Verify dialog is closed + await expect(page.getByText('Are you sure you want to delete the path')).not.toBeVisible(); + + // Verify path count is unchanged + await expect(page.getByRole('button', { name: /^Delete path / })).toHaveCount(initialCount); + }); + + test('create a path then delete it via trash icon', async ({ page }) => { + const testPath = '/test-delete-path'; + + // Click the add path button + await page.getByRole('button', { name: 'Add path' }).click(); + + // Fill in the path name and create it + await page.getByPlaceholder('/pets').fill(testPath); + await page.getByRole('button', { name: 'Create' }).click(); + + // Verify the new path appears in the navigation + await expect(page.getByText(testPath)).toBeVisible(); + + // Find and click the trash icon for our test path + const trashButton = page.getByRole('button', { name: `Delete path ${testPath}` }); + await expect(trashButton).toBeVisible(); + await trashButton.click(); + + // Verify the confirmation dialog shows the correct path name + await expect(page.getByText('Are you sure you want to delete the path')).toBeVisible(); + const modalBody = page.locator('.pf-v6-c-modal-box__body'); + await expect(modalBody).toContainText(testPath); + + // Click Delete to confirm + await page.getByRole('button', { name: 'Delete' }).click(); + + // Verify the dialog is closed + await expect(page.getByText('Are you sure you want to delete the path')).not.toBeVisible(); + + // Verify the path is gone from the navigation + await expect(page.getByRole('button', { name: `Delete path ${testPath}` })).not.toBeVisible(); + }); + + test('schemas section does not have trash icons', async ({ page }) => { + // Schemas should not have delete buttons (only paths do) + const schemaTrashButton = page.getByRole('button', { name: /^Delete schema / }); + await expect(schemaTrashButton).toHaveCount(0); + }); +}); diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..935812b --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,29 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + baseURL: 'http://localhost:5173', + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + ], + webServer: { + command: 'npm run test-app:dev', + url: 'http://localhost:5173', + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/src/components/editor/NavigationPanel.tsx b/src/components/editor/NavigationPanel.tsx index 800877b..076271a 100644 --- a/src/components/editor/NavigationPanel.tsx +++ b/src/components/editor/NavigationPanel.tsx @@ -2,13 +2,19 @@ * Navigation panel for the master section */ -import React, { useState, useMemo } from 'react'; +import React, { useState, useCallback, useMemo } from 'react'; import { Nav, NavList, NavItem, Divider, - SearchInput + SearchInput, + Modal, + ModalVariant, + ModalHeader, + ModalBody, + ModalFooter, + Button, } from '@patternfly/react-core'; import { useDocument } from '@hooks/useDocument'; import { useSelection } from '@hooks/useSelection'; @@ -41,6 +47,8 @@ export const NavigationPanel: React.FC = () => { const [isCreatePathModalOpen, setIsCreatePathModalOpen] = useState(false); const [isCreateSchemaModalOpen, setIsCreateSchemaModalOpen] = useState(false); const [filterText, setFilterText] = useState(''); + const [deleteConfirmPath, setDeleteConfirmPath] = useState(null); + const handleDeleteRequest = useCallback((path: string) => setDeleteConfirmPath(path), []); /** * Get list of paths from the document @@ -245,6 +253,7 @@ export const NavigationPanel: React.FC = () => { isItemActive={(path) => navigationObjectType === "pathItem" && navigationObject?.mapPropertyName() === path} nodePath="/paths" isTooltipEnabled={true} + onDeleteItem={handleDeleteRequest} /> @@ -277,6 +286,23 @@ export const NavigationPanel: React.FC = () => { onClose={() => setIsCreateSchemaModalOpen(false)} onConfirm={handleCreateSchema} /> + + {/* Delete Path Confirmation Modal */} + setDeleteConfirmPath(null)} + aria-labelledby="delete-path-modal-title" + > + + + Are you sure you want to delete the path {deleteConfirmPath}? + + + + + + ); }; diff --git a/src/components/editor/NavigationPanelSection.tsx b/src/components/editor/NavigationPanelSection.tsx index 630325a..b021610 100644 --- a/src/components/editor/NavigationPanelSection.tsx +++ b/src/components/editor/NavigationPanelSection.tsx @@ -12,7 +12,7 @@ import { MenuList, MenuItem, Tooltip, } from '@patternfly/react-core'; -import { PlusCircleIcon } from '@patternfly/react-icons'; +import { PlusCircleIcon, TrashIcon } from '@patternfly/react-icons'; import { ExpandablePanel } from '@components/common/ExpandablePanel'; import {PathLabel} from "@components/common/PathLabel.tsx"; @@ -78,6 +78,11 @@ export interface NavigationPanelSectionProps { * Optional node path for the section (for selection support) */ nodePath?: string; + + /** + * Optional handler for deleting an item (renders a trash icon when provided) + */ + onDeleteItem?: (itemName: string) => void; } /** @@ -95,6 +100,7 @@ export const NavigationPanelSection: React.FC = ({ isTooltipEnabled, isItemActive, nodePath, + onDeleteItem, }) => { // Manage expanded/collapsed state internally const [isExpanded, setIsExpanded] = useState(true); @@ -197,15 +203,33 @@ export const NavigationPanelSection: React.FC = ({ onClick={() => onItemClick(itemName)} style={hasContextMenu ? { backgroundColor: 'var(--pf-v6-global--BackgroundColor--200)' } : undefined} > - {isTooltipEnabled ? ( - handleContextMenu(e, itemName)} style={{width: "100%", overflowX: "hidden", textWrap: "nowrap"}}> - {itemName}}> - +
+ }> + + + + ) : ( + handleContextMenu(e, itemName)}>{itemName} + )} +
+ {onDeleteItem && ( + +