diff --git a/e2e-tests/fixtures/Workspace.ts b/e2e-tests/fixtures/Workspace.ts index 400bb11332..f9e7f9644e 100644 --- a/e2e-tests/fixtures/Workspace.ts +++ b/e2e-tests/fixtures/Workspace.ts @@ -49,8 +49,11 @@ export class Workspace { await this.searchInput.clear(); } - async clickFile(name: string): Promise { - await this.workspaceFileGrid.getByRole('row', { name }).click(); + async clickFile(name: string, options?: { force?: boolean }): Promise { + // `force: true` is for callers that expect the click to immediately summon a modal + // (e.g. unsaved-changes confirmation) — Playwright's hit-test would otherwise see the + // modal backdrop and retry until timeout, even though the original click registered. + await this.workspaceFileGrid.getByRole('row', { name }).click(options); } async createFolder(folderPath?: string): Promise { @@ -222,12 +225,15 @@ export class Workspace { /** * Open the right-side metadata panel by clicking the metadata tab icon. - * If the panel is already open on the metadata tab, this is a no-op. + * If the panel is already open on the metadata tab, this is a no-op — clicking again + * would toggle the panel closed. */ async openMetadataPanel(): Promise { - // Click the Metadata tab button in the right icon rail - await this.metadataTabButton.click(); - // Wait for the metadata panel content to appear + // The icon rail button sets data-active="true" iff metadata tab is active AND panel is open. + const isAlreadyOpen = (await this.metadataTabButton.getAttribute('data-active')) === 'true'; + if (!isAlreadyOpen) { + await this.metadataTabButton.click(); + } await this.page.getByText('User metadata', { exact: true }).waitFor({ state: 'visible', timeout: 5000 }); } @@ -279,7 +285,7 @@ export class Workspace { this.metadataCancelButton = page.locator('.user-metadata-editor + div').getByRole('button', { name: 'Cancel' }); this.metadataPanel = page.locator('.user-metadata-editor').first(); this.metadataSaveButton = page.locator('.user-metadata-editor + div').getByRole('button', { name: 'Save' }); - this.metadataTabButton = page.getByRole('button', { name: 'Metadata' }); + this.metadataTabButton = page.getByRole('button', { exact: true, name: 'Metadata' }); this.navButtonSequences = page.locator('.nav-button:has-text("Sequences")'); this.navButtonSequencesMenu = this.navButtonSequences.getByRole('menu'); this.page = page; diff --git a/e2e-tests/tests/workspace.test.ts b/e2e-tests/tests/workspace.test.ts index b1cfb9ebbf..bd3dc33039 100644 --- a/e2e-tests/tests/workspace.test.ts +++ b/e2e-tests/tests/workspace.test.ts @@ -83,6 +83,14 @@ test.describe.serial('Workspace', () => { await workspace.pageLoadingLocatorWithData.waitFor({ state: 'detached' }); }); + test('Right icon rail shows sequence tabs when workspace loads with no file selected', async () => { + // With no file in the URL the page defaults to a SequenceEditor, so the right + // icon rail should expose Selected Command and Command Dictionary tabs alongside Metadata. + await expect(setup.page.getByRole('button', { exact: true, name: 'Metadata' })).toBeVisible(); + await expect(setup.page.getByRole('button', { exact: true, name: 'Selected Command' })).toBeVisible(); + await expect(setup.page.getByRole('button', { exact: true, name: 'Command Dictionary' })).toBeVisible(); + }); + test('Workspace header menu should be accessible', async () => { await expect(workspace.workspaceContextMenuButton).toBeVisible(); await workspace.openWorkspaceContextMenu(); @@ -279,7 +287,7 @@ test.describe.serial('Workspace', () => { // Try to navigate to second file await workspace.searchForFileAndWait(file2); - await workspace.clickFile(file2); + await workspace.clickFile(file2, { force: true }); // Should show confirmation modal const modal = setup.page.locator('#modal-container'); diff --git a/src/routes/workspaces/[workspaceId]/+page.svelte b/src/routes/workspaces/[workspaceId]/+page.svelte index 7d78bbe878..ba3fabaf53 100644 --- a/src/routes/workspaces/[workspaceId]/+page.svelte +++ b/src/routes/workspaces/[workspaceId]/+page.svelte @@ -142,6 +142,8 @@ const resizableHandleClass = 'w-[3px] hover:after:bg-neutral-300 hover:after:transition-all hover:after:delay-[400ms] data-[active]:after:bg-neutral-300 data-[active]:after:transition-all'; + let activeFileIsSequence: boolean = false; + let actionDetailIsDirty: boolean = false; let availableActionsForActiveFile: ActionParameterPair[] = []; let panelsReady: boolean = false; let allActionsForWorkspace: ActionDefinition[] = []; @@ -180,7 +182,6 @@ let workspaceTree: WorkspaceTreeNode | null = null; let workspaceTreeMap: WorkspaceTreeMap = {}; let workspaceFileList: WorkspaceTreeNodeWithFullPath[] = []; - let actionDetailIsDirty: boolean = false; if (initialActionRunIdParam) { const runId = parseInt(initialActionRunIdParam, 10); @@ -277,13 +278,14 @@ $: activeFileMetadata = ($activeDocumentPath && workspaceTreeMap[$activeDocumentPath]?.metadata) || null; $: activeFileIsSequence = - $activeDocumentPath !== null && - $activeDocument.type !== null && - $activeDocument.type === WorkspaceContentType.Sequence; + $activeDocumentPath === null || + ($activeDocument.type !== null && $activeDocument.type === WorkspaceContentType.Sequence); $: commandInfoMapper = $sequenceAdaptation.input.commandInfoMapper; $: isFileReadOnly = activeFileMetadata?.readOnly ?? false; - // Switch right panel tab when file type changes + // Auto-switch the right-panel tab only when the editor crosses between sequence mode and + // non-sequence mode. Switching between two sequence files (or between blank and a sequence + // file) preserves whatever tab the user last chose. let previousActiveFileIsSequence: boolean = activeFileIsSequence; $: if (activeFileIsSequence !== previousActiveFileIsSequence) { previousActiveFileIsSequence = activeFileIsSequence; @@ -1410,9 +1412,6 @@ {:else} {@const isTextOrEmpty = $activeDocumentPath === null || isTextFile(workspaceTreeMap[$activeDocumentPath]?.type)} - {@const isSequenceFile = - $activeDocumentPath === null || - ($activeDocument.type !== null && $activeDocument.type === WorkspaceContentType.Sequence)}
{#if showLoadingSpinner && isTextOrEmpty}
{/if} - {#if isTextOrEmpty && isSequenceFile} + {#if isTextOrEmpty && activeFileIsSequence}