From 4f6752d1144c4961b81cdcdf8d28bfd3021bfb05 Mon Sep 17 00:00:00 2001 From: Daniel Ogirev Date: Sat, 10 Oct 2026 20:12:52 +0300 Subject: [PATCH 1/2] fix(ui): reset environment editor when opening modal --- .../app/components/EnvironmentsModal.vue | 4 ++ tests/ui/environments-modal.spec.ts | 49 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/orchestrator/app/components/EnvironmentsModal.vue b/orchestrator/app/components/EnvironmentsModal.vue index ee6a3f8b..35049e11 100644 --- a/orchestrator/app/components/EnvironmentsModal.vue +++ b/orchestrator/app/components/EnvironmentsModal.vue @@ -29,6 +29,10 @@ function cancelEdit() { editingId.value = null; } +watch(open, (isOpen) => { + if (isOpen) cancelEdit(); +}); + async function handleSave(data: Partial) { if (editingId.value) { await updateEnvironment(editingId.value, data); diff --git a/tests/ui/environments-modal.spec.ts b/tests/ui/environments-modal.spec.ts index ecdff8db..7c4a6937 100644 --- a/tests/ui/environments-modal.spec.ts +++ b/tests/ui/environments-modal.spec.ts @@ -16,6 +16,55 @@ test.describe('Environments Modal', () => { await expect(page.locator('[role="dialog"]')).toBeHidden({ timeout: 10_000 }); }); + for (const closeMethod of ['Close', 'Escape', 'outside click'] as const) { + test(`reopening shows the list after dismissing an editor with ${closeMethod}`, async ({ page, request }) => { + const api = new ApiClient(request); + const envName = `UIEnvReopen-${closeMethod}-${Date.now()}`; + const { body: created } = await api.createEnvironment({ name: envName }); + + try { + await goToDashboard(page); + await openEnvironmentsModal(page); + const dialog = page.getByRole('dialog'); + const nameInput = dialog.locator('input[placeholder="My environment"]'); + const envRow = dialog.locator('.rounded-lg').filter({ hasText: envName }); + + for (const mode of ['edit', 'create', 'view'] as const) { + await test.step(`dismiss and reopen from ${mode} mode`, async () => { + if (mode === 'edit') { + await envRow.getByRole('button', { name: 'Edit', exact: true }).click(); + await expect(nameInput).toHaveValue(envName); + } else if (mode === 'create') { + await dialog.getByRole('button', { name: 'New', exact: true }).click(); + await expect(nameInput).toHaveValue(''); + } else { + await dialog.getByRole('button', { name: 'View', exact: true }).first().click(); + await expect(nameInput).toBeDisabled(); + } + + if (mode !== 'view') await nameInput.fill('Unsaved draft'); + + if (closeMethod === 'Close') { + await dialog.getByRole('button', { name: 'Close', exact: true }).first().click(); + } else if (closeMethod === 'Escape') { + await page.keyboard.press('Escape'); + } else { + await page.mouse.click(10, 10); + } + await expect(dialog).toBeHidden(); + + await openEnvironmentsModal(page); + await expect(dialog.getByRole('button', { name: 'New', exact: true })).toBeVisible(); + await expect(envRow).toBeVisible(); + await expect(nameInput).toBeHidden(); + }); + } + } finally { + await api.deleteEnvironment(created.id); + } + }); + } + test('shows pre-created environments', async ({ page, request }) => { // Create an environment via API first const api = new ApiClient(request); From d038d477cb4d9924e7a6f5f5ddcfb5dfefcbe451 Mon Sep 17 00:00:00 2001 From: Daniel Ogirev Date: Sat, 10 Oct 2026 20:19:28 +0300 Subject: [PATCH 2/2] fix(ui): reset remaining list modals when opening --- .../app/components/ListEditorModal.vue | 4 + orchestrator/app/components/UsersModal.vue | 5 +- tests/ui/list-editor-modal-reopen.spec.ts | 98 +++++++++++++++++++ tests/ui/users-modal.spec.ts | 30 ++++++ 4 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 tests/ui/list-editor-modal-reopen.spec.ts diff --git a/orchestrator/app/components/ListEditorModal.vue b/orchestrator/app/components/ListEditorModal.vue index ad213b67..4b904782 100644 --- a/orchestrator/app/components/ListEditorModal.vue +++ b/orchestrator/app/components/ListEditorModal.vue @@ -67,6 +67,10 @@ function cancelEdit() { viewing.value = null; } +watch(open, (isOpen) => { + if (isOpen) cancelEdit(); +}); + async function handleSave() { if (!editForm.name.trim() || !editForm.content.trim()) return; const body = { name: editForm.name, content: editForm.content }; diff --git a/orchestrator/app/components/UsersModal.vue b/orchestrator/app/components/UsersModal.vue index ceb4b735..24c28984 100644 --- a/orchestrator/app/components/UsersModal.vue +++ b/orchestrator/app/components/UsersModal.vue @@ -43,7 +43,10 @@ async function refresh() { } watch(open, (v) => { - if (v) refresh(); + if (v) { + cancelCreate(); + refresh(); + } }); function startCreate() { diff --git a/tests/ui/list-editor-modal-reopen.spec.ts b/tests/ui/list-editor-modal-reopen.spec.ts new file mode 100644 index 00000000..857c875b --- /dev/null +++ b/tests/ui/list-editor-modal-reopen.spec.ts @@ -0,0 +1,98 @@ +import { test, expect } from '@playwright/test'; +import { goToDashboard } from '../helpers/ui-helpers'; +import { ApiClient } from '../helpers/api-client'; + +const resources = [ + { + title: 'Capabilities', + namePlaceholder: 'Capability name', + initialContent: '', + content: '---\ndescription: Modal reopen test\n---\n\nOriginal content.', + create: 'createCapability', + remove: 'deleteCapability', + }, + { + title: 'Instructions', + namePlaceholder: 'Entry name', + initialContent: '', + content: '# Original\n\nOriginal content.', + create: 'createInstruction', + remove: 'deleteInstruction', + }, + { + title: 'Init Scripts', + namePlaceholder: 'Script name', + initialContent: '#!/bin/bash\n', + content: '#!/bin/bash\necho original', + create: 'createInitScript', + remove: 'deleteInitScript', + }, +] as const; + +for (const resource of resources) { + test.describe(`${resource.title} modal reopening`, () => { + for (const closeMethod of ['Close', 'Escape', 'outside click'] as const) { + test(`reopening shows the list after dismissing an editor with ${closeMethod}`, async ({ page, request }) => { + const api = new ApiClient(request); + const name = `UIReopen-${resource.title}-${closeMethod}-${Date.now()}`; + const { body: created } = await api[resource.create]({ name, content: resource.content }); + + try { + await goToDashboard(page); + const openButton = page.getByRole('button', { name: resource.title, exact: true }); + await openButton.click(); + const dialog = page.getByRole('dialog'); + const nameInput = dialog.getByPlaceholder(resource.namePlaceholder, { exact: true }); + const contentInput = dialog.locator('textarea'); + const itemRow = dialog.locator('.rounded-lg').filter({ hasText: name }); + + for (const mode of ['edit', 'create', 'view'] as const) { + await test.step(`dismiss and reopen from ${mode} mode`, async () => { + if (mode === 'edit') { + await itemRow.getByRole('button', { name: 'Edit', exact: true }).click(); + await expect(nameInput).toHaveValue(name); + await expect(contentInput).toHaveValue(resource.content); + } else if (mode === 'create') { + await dialog.getByRole('button', { name: 'New', exact: true }).click(); + await expect(nameInput).toHaveValue(''); + await expect(contentInput).toHaveValue(resource.initialContent); + } else { + await dialog.getByRole('button', { name: 'View', exact: true }).first().click(); + await expect(nameInput).toBeDisabled(); + await expect(contentInput).toBeDisabled(); + } + + if (mode !== 'view') { + await nameInput.fill('Unsaved draft'); + await contentInput.fill('Unsaved content'); + } + + if (closeMethod === 'Close') { + await dialog.getByRole('button', { name: 'Close', exact: true }).first().click(); + } else if (closeMethod === 'Escape') { + await page.keyboard.press('Escape'); + } else { + await page.mouse.click(10, 10); + } + await expect(dialog).toBeHidden(); + + await openButton.click(); + await expect(dialog.getByRole('button', { name: 'New', exact: true })).toBeVisible(); + await expect(itemRow).toBeVisible(); + await expect(nameInput).toBeHidden(); + await expect(contentInput).toBeHidden(); + }); + } + + await dialog.getByRole('button', { name: 'New', exact: true }).click(); + await expect(nameInput).toBeEnabled(); + await expect(nameInput).toHaveValue(''); + await expect(contentInput).toBeEnabled(); + await expect(contentInput).toHaveValue(resource.initialContent); + } finally { + await api[resource.remove](created.id); + } + }); + } + }); +} diff --git a/tests/ui/users-modal.spec.ts b/tests/ui/users-modal.spec.ts index 5cfb38c6..652e6fb7 100644 --- a/tests/ui/users-modal.spec.ts +++ b/tests/ui/users-modal.spec.ts @@ -1,10 +1,40 @@ import { test, expect } from '@playwright/test'; import { createTestUser, deleteTestUser, type CreatedUser } from '../helpers/test-users'; +import { goToDashboard, selectSidebarTab } from '../helpers/ui-helpers'; /** * Admin-only Users modal. Runs with the global admin storage state (default). */ test.describe('Users modal (admin)', () => { + for (const closeMethod of ['Close', 'Escape', 'outside click'] as const) { + test(`reopening shows the list after dismissing the new user form with ${closeMethod}`, async ({ page }) => { + await goToDashboard(page); + await selectSidebarTab(page, 'System'); + const openButton = page.getByRole('button', { name: 'Users', exact: true }); + await openButton.click(); + const dialog = page.getByRole('dialog'); + await dialog.getByRole('button', { name: 'New', exact: true }).click(); + await dialog.locator('input[type="email"]').fill('unsaved@example.test'); + + if (closeMethod === 'Close') { + await dialog.getByRole('button', { name: 'Close', exact: true }).click(); + } else if (closeMethod === 'Escape') { + await page.keyboard.press('Escape'); + } else { + await page.mouse.click(10, 10); + } + await expect(dialog).toBeHidden(); + + await openButton.click(); + await expect(dialog.getByRole('button', { name: 'New', exact: true })).toBeVisible(); + await expect(dialog.locator('[data-user-row]').first()).toBeVisible(); + await expect(dialog.locator('input[type="email"]')).toBeHidden(); + + await dialog.getByRole('button', { name: 'New', exact: true }).click(); + await expect(dialog.locator('input[type="email"]')).toHaveValue(''); + }); + } + test('admin opens Users modal from the System tab', async ({ page }) => { await page.goto('/'); await page.waitForSelector('h1:has-text("Agentor")', { timeout: 15_000 });