diff --git a/workspaces/intelligent-assistant/e2e-tests/lightspeed.notebooks-compact.test.ts b/workspaces/intelligent-assistant/e2e-tests/lightspeed.notebooks-compact.test.ts index 01f66003a18..5e51c6ce7d8 100644 --- a/workspaces/intelligent-assistant/e2e-tests/lightspeed.notebooks-compact.test.ts +++ b/workspaces/intelligent-assistant/e2e-tests/lightspeed.notebooks-compact.test.ts @@ -16,7 +16,10 @@ import { test, expect, type Page } from '@playwright/test'; -import { NotebookSurfacePage } from './pages/NotebookSurfacePage'; +import { + NotebookSurfacePage, + NOTEBOOK_UNTITLED_GRID_NAME, +} from './pages/NotebookSurfacePage'; import type { LightspeedMessages } from './utils/translations'; import { bootstrapLightspeedE2ePage } from './utils/lightspeedE2eSetup'; import { @@ -52,20 +55,22 @@ for (const mode of ['Overlay', 'Dock to window'] as const) { const boot = await bootstrapLightspeedE2ePage(browser); sharedPage = boot.page; translations = boot.translations; - notebooks = new NotebookSurfacePage(sharedPage, translations); + notebooks = new NotebookSurfacePage( + sharedPage, + translations, + boot.locale, + ); }); test('tabs are visible and notebooks tab selectable', async () => { await switchToCompactNotebooks(sharedPage, translations, mode); - await expect( - sharedPage.getByRole('tab', { name: translations['tabs.chat'] }), - ).toBeVisible(); - const notebooksTab = sharedPage.getByRole('tab', { - name: translations['tabs.notebooks'], - }); - await expect(notebooksTab).toBeVisible(); - await expect(notebooksTab).toHaveAttribute('aria-selected', 'true'); + await expect(notebooks.chatTab()).toBeVisible(); + await expect(notebooks.notebooksTab()).toBeVisible(); + await expect(notebooks.notebooksTab()).toHaveAttribute( + 'aria-selected', + 'true', + ); }); test('empty notebook list shows create action', async () => { @@ -80,148 +85,81 @@ for (const mode of ['Overlay', 'Dock to window'] as const) { }); test('header actions visible in compact mode: close, add, sidebar toggle', async () => { - const header = sharedPage.locator('.pf-chatbot__header'); - - await expect( - header.getByRole('button', { - name: translations['notebook.view.close'], - }), - ).toBeVisible(); - await expect( - header.getByRole('button', { - name: translations['notebook.view.documents.add'], - }), - ).toBeVisible(); - - const collapseLabel = translations['notebook.view.sidebar.collapse']; - const expandLabel = translations['notebook.view.sidebar.expand']; - const sidebarToggle = header.getByRole('button', { - name: new RegExp(`${collapseLabel}|${expandLabel}`), - }); - await expect(sidebarToggle).toBeVisible(); + await notebooks.expectCompactHeaderActionsVisible(); }); test('NotebookView topBar close button hidden in compact mode', async () => { - const closeButtons = sharedPage.getByRole('button', { - name: translations['notebook.view.close'], - }); - await expect(closeButtons).toHaveCount(1); + await notebooks.expectSingleNotebookCloseButton(); }); test('upload modal opens and renders within panel', async () => { - const header = sharedPage.locator('.pf-chatbot__header'); - const addButton = header.getByRole('button', { - name: translations['notebook.view.documents.add'], - }); - await addButton.click(); - - // In compact mode, disablePortal renders the MUI Dialog inline. The - // ChatbotModal already has role="dialog", so scope to the MUI one. - const dialog = sharedPage.locator( - '[role="dialog"][aria-labelledby="add-document-modal-title"]', - ); - await expect(dialog).toBeVisible({ timeout: 10_000 }); - await expect(dialog.locator('#add-document-modal-title')).toBeVisible(); - await expect( - dialog.locator( - `text=${translations['notebook.upload.modal.dragDropTitle']}`, - ), - ).toBeVisible(); + await notebooks.clickCompactHeaderAddDocument(); - await dialog - .locator('button', { hasText: translations['modal.cancel'] }) - .click(); + const uploadModal = notebooks.uploadDocumentModal(); + await expect(uploadModal.dialog()).toBeVisible({ timeout: 10_000 }); + await uploadModal.expectUploadAreaFullyDescribed(); + await uploadModal.dismiss(); }); test('sidebar toggle mirrors icon direction', async () => { - const header = sharedPage.locator('.pf-chatbot__header'); - const collapseLabel = translations['notebook.view.sidebar.collapse']; - const expandLabel = translations['notebook.view.sidebar.expand']; - - const toggle = header.getByRole('button', { - name: new RegExp(`${collapseLabel}|${expandLabel}`), - }); - await expect(toggle).toBeVisible(); - - const initialLabel = await toggle.getAttribute('aria-label'); - - await toggle.click(); - await sharedPage.waitForTimeout(300); - - const newLabel = await toggle.getAttribute('aria-label'); - expect(newLabel).not.toBe(initialLabel); - - const expectedLabel = - initialLabel === collapseLabel ? expandLabel : collapseLabel; - expect(newLabel).toBe(expectedLabel); - - await toggle.click(); - await sharedPage.waitForTimeout(300); - const restoredLabel = await toggle.getAttribute('aria-label'); - expect(restoredLabel).toBe(initialLabel); + await notebooks.toggleCompactSidebarAndExpectLabelFlip(); }); test('file picker works in compact upload modal', async ({}, testInfo) => { const { absolutePath } = localeNotebookUpload1Path(testInfo.project.name); - const header = sharedPage.locator('.pf-chatbot__header'); - await header - .getByRole('button', { - name: translations['notebook.view.documents.add'], - }) - .click(); - - const dialog = sharedPage.locator( - '[role="dialog"][aria-labelledby="add-document-modal-title"]', + await notebooks.clickCompactHeaderAddDocument(); + const uploadModal = notebooks.uploadDocumentModal(); + await expect(uploadModal.dialog()).toBeVisible({ timeout: 10_000 }); + await uploadModal.selectFilesViaBrowsePicker([absolutePath]); + await uploadModal.expectStagedFileCountCaptionVisible( + 1, + NOTEBOOK_SESSION_MAX_DOCUMENTS, ); - await expect(dialog).toBeVisible({ timeout: 10_000 }); + await uploadModal.clickCancel(); + }); - const fileInput = dialog.locator('input[type="file"]'); - await fileInput.setInputFiles([absolutePath]); + test('header add: upload completes and resource appears in panel', async ({}, testInfo) => { + const { absolutePath, fileName } = localeNotebookUpload1Path( + testInfo.project.name, + ); - const stagedCaption = translations['notebook.upload.modal.selectedFiles'] - .replace('{{count}}', '1') - .replace('{{max}}', String(NOTEBOOK_SESSION_MAX_DOCUMENTS)); - await expect(dialog.locator(`text=${stagedCaption}`)).toBeVisible({ - timeout: 5_000, - }); + await notebooks.clickCompactHeaderAddDocument(); + const uploadModal = notebooks.uploadDocumentModal(); + await uploadModal.selectFilesViaBrowsePicker([absolutePath]); + await uploadModal.clickAddFilesForStagedCount(1); + await notebooks.ensureDocumentSidebarExpanded(); + await notebooks.expectDocumentFileListedInSidebar(fileName); + }); - await dialog - .locator('button', { hasText: translations['modal.cancel'] }) - .click(); + test('remove resource modal renders within panel', async () => { + await notebooks.ensureDocumentSidebarExpanded(); + await notebooks.openDeleteFirstDocumentConfirmation(); + await notebooks.cancelDeleteDocumentConfirmation(); + await notebooks.deleteFirstListedDocumentFromSidebarOverflowMenu(); + await notebooks.expectNotebookEditorUploadResourceButtonVisible(); }); test('switch tabs preserves notebook state', async () => { - await sharedPage - .getByRole('tab', { name: translations['tabs.chat'] }) - .click(); - await expect( - sharedPage.getByRole('tab', { name: translations['tabs.chat'] }), - ).toHaveAttribute('aria-selected', 'true'); + await notebooks.chatTab().click(); + await expect(notebooks.chatTab()).toHaveAttribute( + 'aria-selected', + 'true', + ); - await sharedPage - .getByRole('tab', { name: translations['tabs.notebooks'] }) - .click(); - await expect( - sharedPage.getByRole('tab', { - name: translations['tabs.notebooks'], - }), - ).toHaveAttribute('aria-selected', 'true'); + await notebooks.notebooksTab().click(); + await expect(notebooks.notebooksTab()).toHaveAttribute( + 'aria-selected', + 'true', + ); - // Notebook editor still shows (not reverted to list view) await expect(notebooks.uploadResourceHeading()).toBeVisible(); }); test('close notebook via header action', async () => { - const header = sharedPage.locator('.pf-chatbot__header'); - await header - .getByRole('button', { - name: translations['notebook.view.close'], - }) - .click(); + await notebooks.clickCompactHeaderCloseNotebook(); await expect(notebooks.myNotebooksHeading()).toBeVisible(); - // Empty notebooks (no uploaded documents) are auto-deleted on close await expect( notebooks.createNotebookFromEmptyStateButton(), ).toBeVisible(); @@ -233,45 +171,39 @@ for (const mode of ['Overlay', 'Dock to window'] as const) { await selectDisplayMode(sharedPage, translations, otherMode); - await expect( - sharedPage.getByRole('tab', { - name: translations['tabs.notebooks'], - }), - ).toHaveAttribute('aria-selected', 'true'); - + await expect(notebooks.notebooksTab()).toHaveAttribute( + 'aria-selected', + 'true', + ); await expect(notebooks.myNotebooksHeading()).toBeVisible(); }); test('switch to fullscreen preserves notebooks tab', async () => { await selectDisplayMode(sharedPage, translations, 'Fullscreen'); - await expect( - sharedPage.getByRole('tab', { - name: translations['tabs.notebooks'], - }), - ).toBeVisible(); + await expect(notebooks.notebooksTab()).toBeVisible(); }); - test('cleanup: delete created notebook', async () => { + test('cleanup: delete notebook modal renders within panel', async () => { await selectDisplayMode(sharedPage, translations, mode); - - await expect( - sharedPage.getByRole('tab', { - name: translations['tabs.notebooks'], - }), - ).toBeVisible(); - await sharedPage - .getByRole('tab', { name: translations['tabs.notebooks'] }) - .click(); + await notebooks.notebooksTab().click(); const card = notebooks.newestUntitledNotebookCard(); - if ((await card.count()) > 0) { - await notebooks.notebookCardOverflowMenuButton(card).click(); - await notebooks.deleteNotebookOverflowMenuItem().click(); - const confirmDelete = - notebooks.notebookDeleteConfirmationDialog('Untitled Notebook'); - await confirmDelete.confirmDeletion(); + if ((await card.count()) === 0) { + return; } + + await notebooks.notebookCardOverflowMenuButton(card).click(); + await notebooks.deleteNotebookOverflowMenuItem().click(); + + const confirmDelete = notebooks.notebookDeleteConfirmationDialog( + NOTEBOOK_UNTITLED_GRID_NAME, + ); + await confirmDelete.expectDialogVisible(); + await notebooks.expectNotebookDeleteDialogWithinChatbot( + NOTEBOOK_UNTITLED_GRID_NAME, + ); + await confirmDelete.confirmDeletion(); }); }); } diff --git a/workspaces/intelligent-assistant/e2e-tests/lightspeed.notebooks.test.ts b/workspaces/intelligent-assistant/e2e-tests/lightspeed.notebooks.test.ts index 8ee5673234a..074f2addf36 100644 --- a/workspaces/intelligent-assistant/e2e-tests/lightspeed.notebooks.test.ts +++ b/workspaces/intelligent-assistant/e2e-tests/lightspeed.notebooks.test.ts @@ -27,6 +27,7 @@ import { NOTEBOOK_EDITOR_URL_RE, NOTEBOOK_SESSION_MAX_DOCUMENTS, notebookElevenFileStagingPaths, + notebookTenFileStagingPaths, notebookUnsupportedTypeFixturePath, } from './utils/notebooks'; import { substituteNotebookTemplate } from './utils/notebookTranslation'; @@ -70,6 +71,14 @@ test.describe('Intelligent assistant notebooks', () => { await uploadModal.clickCancel(); }); + test('upload modal: title close button dismisses dialog', async () => { + await notebooks.clickOpenUploadDocumentModal(); + const uploadModal = notebooks.uploadDocumentModal(); + await uploadModal.expectUploadAreaFullyDescribed(); + await uploadModal.clickTitleClose(); + await expect(uploadModal.dialog()).toBeHidden(); + }); + test('document sidebar: collapse and expand', async () => { await notebooks.collapseThenExpandDocumentSidebar(); }); @@ -153,6 +162,19 @@ test.describe('Intelligent assistant notebooks', () => { await uploadModal.clickCancel(); }); + test('upload modal: dropzone disabled at ten staged files', async () => { + await notebooks.clickOpenUploadDocumentModal(); + const uploadModal = notebooks.uploadDocumentModal(); + await uploadModal.selectFilesViaBrowsePicker(notebookTenFileStagingPaths()); + await uploadModal.expectStagedFileCountCaptionVisible( + NOTEBOOK_SESSION_MAX_DOCUMENTS, + NOTEBOOK_SESSION_MAX_DOCUMENTS, + ); + await uploadModal.expectDropzoneDisabled(); + await uploadModal.expectMaxReachedTooltipOnDropzoneHover(); + await uploadModal.clickCancel(); + }); + test('upload modal: unsupported extension rejected', async () => { await notebooks.clickOpenUploadDocumentModal(); const uploadModal = notebooks.uploadDocumentModal(); @@ -182,14 +204,69 @@ test.describe('Intelligent assistant notebooks', () => { await uploadModal.selectFilesViaBrowsePicker([absolutePath]); await uploadModal.clickAddFilesForStagedCount(1); + await expect(uploadModal.dialog()).toBeHidden({ timeout: 5_000 }); const overwriteModal = notebooks.notebookOverwriteConfirmModal(); await overwriteModal.expectDialogVisible(); + await expect(sharedPage.getByRole('dialog')).toHaveCount(1); await overwriteModal.expectListedOverwriteFile(fileName); await overwriteModal.clickBack(); + + uploadModal = notebooks.uploadDocumentModal(); + await expect(uploadModal.dialog()).toBeVisible(); await uploadModal.clickCancel(); - await notebooks.deleteFirstListedDocumentFromSidebarOverflowMenu(); - await notebooks.expectNotebookEditorUploadResourceButtonVisible(); + await notebooks.clickOpenUploadDocumentModal(); + uploadModal = notebooks.uploadDocumentModal(); + await uploadModal.selectFilesViaBrowsePicker([absolutePath]); + await uploadModal.clickAddFilesForStagedCount(1); + await expect(uploadModal.dialog()).toBeHidden({ timeout: 5_000 }); + await overwriteModal.expectDialogVisible(); + await overwriteModal.clickUpload(); + await expect(overwriteModal.dialog()).toBeHidden({ timeout: 30_000 }); + await notebooks.expectDocumentFileListedInSidebar(fileName); + + await notebooks.clickCloseNotebookEditor(); + await notebooks.deleteNotebookCardFromGrid(NOTEBOOK_UNTITLED_GRID_NAME); + }); + + test('notebook card: zero and singular resource counts', async ({}, testInfo) => { + const { absolutePath, fileName } = localeNotebookUpload1Path( + testInfo.project.name, + ); + + await notebooks.clickPrimaryNotebookCreate(); + const renamedName = 'Zero Docs Card'; + await notebooks.renameNotebookSidebarTitle(renamedName); + await notebooks.clickCloseNotebookEditor(); + + const renamedCard = notebooks.notebookCardByDisplayedName(renamedName); + await notebooks.expectNotebookCardDisplayed(renamedName); + await notebooks.expectNotebookCardShowsDocumentCount(renamedCard, 0); + + await renamedCard.click(); + await notebooks.clickOpenUploadDocumentModal(); + const uploadModal = notebooks.uploadDocumentModal(); + await uploadModal.selectFilesViaBrowsePicker([absolutePath]); + await uploadModal.clickAddFilesForStagedCount(1); + await notebooks.expectDocumentFileListedInSidebar(fileName); + await notebooks.clickCloseNotebookEditor(); + + await notebooks.expectNotebookCardShowsDocumentCount( + notebooks.notebookCardByDisplayedName(renamedName), + 1, + ); + await notebooks.deleteNotebookCardFromGrid(renamedName); + }); + + test('notebook card: overflow menu shows rename and delete icons', async () => { + await notebooks.clickPrimaryNotebookCreate(); + const cardName = 'Menu Icons Card'; + await notebooks.renameNotebookSidebarTitle(cardName); + await notebooks.clickCloseNotebookEditor(); + await notebooks.expectNotebookOverflowMenuShowsRenameAndDeleteWithIcons( + notebooks.notebookCardByDisplayedName(cardName), + ); + await notebooks.deleteNotebookCardFromGrid(cardName); }); test('grid: close editor, rename, delete', async ({}, testInfo) => { @@ -197,6 +274,7 @@ test.describe('Intelligent assistant notebooks', () => { testInfo.project.name, ); + await notebooks.clickPrimaryNotebookCreate(); await notebooks.clickOpenUploadDocumentModal(); const uploadModal = notebooks.uploadDocumentModal(); await uploadModal.selectFilesViaBrowsePicker([absolutePath]); @@ -215,15 +293,12 @@ test.describe('Intelligent assistant notebooks', () => { 1, ); - await notebooks - .notebookCardOverflowMenuButton(notebooks.newestUntitledNotebookCard()) - .click(); - await notebooks.renameNotebookOverflowMenuItem().click(); - await notebooks.renameNotebookInline(RENAMED_NOTEBOOK_TITLE); + await notebooks.renameNotebookCardViaOverflowMenu( + notebooks.newestUntitledNotebookCard(), + RENAMED_NOTEBOOK_TITLE, + ); - await expect( - notebooks.notebookCardByDisplayedName(RENAMED_NOTEBOOK_TITLE), - ).toBeVisible(); + await notebooks.expectNotebookCardDisplayed(RENAMED_NOTEBOOK_TITLE); await notebooks .notebookCardOverflowMenuButton( @@ -261,24 +336,10 @@ test.describe('Intelligent assistant notebooks', () => { const card = notebooks.newestUntitledNotebookCard(); await expect(card).toBeVisible(); - await notebooks.clickCardTitle(card); - await expect(notebooks.inlineRenameInput()).toBeVisible(); - - const newName = 'DoubleClick Renamed'; - await notebooks.inlineRenameInput().fill(newName); - await notebooks.inlineRenameInput().press('Enter'); - - await expect(notebooks.notebookCardByDisplayedName(newName)).toBeVisible(); - - await notebooks - .notebookCardOverflowMenuButton( - notebooks.notebookCardByDisplayedName(newName), - ) - .click(); - await notebooks.deleteNotebookOverflowMenuItem().click(); - const confirmDelete = notebooks.notebookDeleteConfirmationDialog(newName); - await confirmDelete.confirmDeletion(); - await notebooks.expectNotebookCardAbsent(newName); + const newName = 'Click Renamed'; + await notebooks.renameNotebookCardViaTitleClick(card, newName); + await notebooks.expectNotebookCardDisplayed(newName); + await notebooks.deleteNotebookCardFromGrid(newName); }); test('grid: Escape cancels inline rename', async ({}, testInfo) => { @@ -299,27 +360,14 @@ test.describe('Intelligent assistant notebooks', () => { const card = notebooks.newestUntitledNotebookCard(); await expect(card).toBeVisible(); - await notebooks.notebookCardOverflowMenuButton(card).click(); - await notebooks.renameNotebookOverflowMenuItem().click(); - await expect(notebooks.inlineRenameInput()).toBeVisible(); + await notebooks.startNotebookCardInlineRenameFromOverflow(card); + await notebooks.fillNotebookCardInlineRename('Should Not Save'); + await notebooks.cancelNotebookCardInlineRenameWithEscape(); - await notebooks.inlineRenameInput().fill('Should Not Save'); - await notebooks.inlineRenameInput().press('Escape'); - - await expect(notebooks.inlineRenameInput()).toBeHidden(); - await expect( - notebooks.notebookCardByDisplayedName(NOTEBOOK_UNTITLED_GRID_NAME), - ).toBeVisible(); + await notebooks.expectNotebookCardInlineRenameInputHidden(); + await notebooks.expectNotebookCardDisplayed(NOTEBOOK_UNTITLED_GRID_NAME); await notebooks.expectNotebookCardAbsent('Should Not Save'); - - await notebooks - .notebookCardOverflowMenuButton(notebooks.newestUntitledNotebookCard()) - .click(); - await notebooks.deleteNotebookOverflowMenuItem().click(); - const confirmDelete = notebooks.notebookDeleteConfirmationDialog( - NOTEBOOK_UNTITLED_GRID_NAME, - ); - await confirmDelete.confirmDeletion(); + await notebooks.deleteNotebookCardFromGrid(NOTEBOOK_UNTITLED_GRID_NAME); }); test('grid: blur saves inline rename', async ({}, testInfo) => { @@ -341,24 +389,14 @@ test.describe('Intelligent assistant notebooks', () => { await expect(card).toBeVisible(); await notebooks.clickCardTitle(card); - await expect(notebooks.inlineRenameInput()).toBeVisible(); + await notebooks.expectNotebookCardInlineRenameInputVisible(); const newName = 'Blur Saved Name'; - await notebooks.inlineRenameInput().fill(newName); - await notebooks.myNotebooksHeading().click(); + await notebooks.saveNotebookCardInlineRenameWithBlur(newName); - await expect(notebooks.inlineRenameInput()).toBeHidden(); - await expect(notebooks.notebookCardByDisplayedName(newName)).toBeVisible(); - - await notebooks - .notebookCardOverflowMenuButton( - notebooks.notebookCardByDisplayedName(newName), - ) - .click(); - await notebooks.deleteNotebookOverflowMenuItem().click(); - const confirmDelete = notebooks.notebookDeleteConfirmationDialog(newName); - await confirmDelete.confirmDeletion(); - await notebooks.expectNotebookCardAbsent(newName); + await notebooks.expectNotebookCardInlineRenameInputHidden(); + await notebooks.expectNotebookCardDisplayed(newName); + await notebooks.deleteNotebookCardFromGrid(newName); }); test('grid: empty or unchanged name cancels rename', async ({}, testInfo) => { @@ -380,63 +418,34 @@ test.describe('Intelligent assistant notebooks', () => { await expect(card).toBeVisible(); await notebooks.clickCardTitle(card); - await expect(notebooks.inlineRenameInput()).toBeVisible(); + await notebooks.expectNotebookCardInlineRenameInputVisible(); - await notebooks.inlineRenameInput().fill(''); - await notebooks.inlineRenameInput().press('Enter'); + await notebooks.fillNotebookCardInlineRename(''); + await notebooks.commitNotebookCardInlineRename(); - await expect(notebooks.inlineRenameInput()).toBeHidden(); - await expect( - notebooks.notebookCardByDisplayedName(NOTEBOOK_UNTITLED_GRID_NAME), - ).toBeVisible(); + await notebooks.expectNotebookCardInlineRenameInputHidden(); + await notebooks.expectNotebookCardDisplayed(NOTEBOOK_UNTITLED_GRID_NAME); await notebooks.clickCardTitle(notebooks.newestUntitledNotebookCard()); - await expect(notebooks.inlineRenameInput()).toBeVisible(); - await notebooks.inlineRenameInput().press('Enter'); + await notebooks.expectNotebookCardInlineRenameInputVisible(); + await notebooks.commitNotebookCardInlineRename(); - await expect(notebooks.inlineRenameInput()).toBeHidden(); - await expect( - notebooks.notebookCardByDisplayedName(NOTEBOOK_UNTITLED_GRID_NAME), - ).toBeVisible(); - - await notebooks - .notebookCardOverflowMenuButton(notebooks.newestUntitledNotebookCard()) - .click(); - await notebooks.deleteNotebookOverflowMenuItem().click(); - const confirmDelete = notebooks.notebookDeleteConfirmationDialog( - NOTEBOOK_UNTITLED_GRID_NAME, - ); - await confirmDelete.confirmDeletion(); + await notebooks.expectNotebookCardInlineRenameInputHidden(); + await notebooks.expectNotebookCardDisplayed(NOTEBOOK_UNTITLED_GRID_NAME); + await notebooks.deleteNotebookCardFromGrid(NOTEBOOK_UNTITLED_GRID_NAME); }); test('sidebar: click title to rename inside editor', async () => { await notebooks.clickPrimaryNotebookCreate(); await expect(notebooks.sidebarTitleText()).toBeVisible(); - await notebooks.clickSidebarTitle(); - - const sidebarInput = notebooks.inlineRenameInput(); - await expect(sidebarInput).toBeVisible(); const newName = 'Sidebar Renamed'; - await sidebarInput.fill(newName); - await sidebarInput.press('Enter'); - - await expect(notebooks.sidebarTitleText()).toContainText(newName); + await notebooks.renameNotebookSidebarTitle(newName); await notebooks.clickCloseNotebookEditor(); - - await expect(notebooks.notebookCardByDisplayedName(newName)).toBeVisible(); - - await notebooks - .notebookCardOverflowMenuButton( - notebooks.notebookCardByDisplayedName(newName), - ) - .click(); - await notebooks.deleteNotebookOverflowMenuItem().click(); - const confirmDelete = notebooks.notebookDeleteConfirmationDialog(newName); - await confirmDelete.confirmDeletion(); - await notebooks.expectNotebookCardAbsent(newName); + await notebooks.expectNotebookCardDisplayed(newName); + await notebooks.deleteNotebookCardFromGrid(newName); }); test('auto-delete: empty untitled notebook is discarded on close', async () => { @@ -488,28 +497,12 @@ test.describe('Intelligent assistant notebooks', () => { await notebooks.clickCreateNotebookFromEmptyList(); await expect(sharedPage).toHaveURL(NOTEBOOK_EDITOR_URL_RE); - await notebooks.clickSidebarTitle(); - const sidebarInput = notebooks.inlineRenameInput(); - await expect(sidebarInput).toBeVisible(); const renamedName = 'Renamed Persists'; - await sidebarInput.fill(renamedName); - await sidebarInput.press('Enter'); + await notebooks.renameNotebookSidebarTitle(renamedName); await notebooks.clickCloseNotebookEditor(); - await expect( - notebooks.notebookCardByDisplayedName(renamedName), - ).toBeVisible(); - - await notebooks - .notebookCardOverflowMenuButton( - notebooks.notebookCardByDisplayedName(renamedName), - ) - .click(); - await notebooks.deleteNotebookOverflowMenuItem().click(); - const confirmDelete = - notebooks.notebookDeleteConfirmationDialog(renamedName); - await confirmDelete.confirmDeletion(); - await notebooks.expectNotebookCardAbsent(renamedName); + await notebooks.expectNotebookCardDisplayed(renamedName); + await notebooks.deleteNotebookCardFromGrid(renamedName); }); }); diff --git a/workspaces/intelligent-assistant/e2e-tests/pages/NotebookAddDocumentModalPage.ts b/workspaces/intelligent-assistant/e2e-tests/pages/NotebookAddDocumentModalPage.ts index 5f6e8f8ede9..f33a7be63b0 100644 --- a/workspaces/intelligent-assistant/e2e-tests/pages/NotebookAddDocumentModalPage.ts +++ b/workspaces/intelligent-assistant/e2e-tests/pages/NotebookAddDocumentModalPage.ts @@ -29,13 +29,16 @@ export class NotebookAddDocumentModalPage { ) {} dialog(): Locator { - return this.page.getByRole('dialog', { - name: this.t['notebook.upload.modal.title'], - }); + return this.page + .locator('[role="dialog"][aria-labelledby="add-document-modal-title"]') + .filter({ hasText: this.t['notebook.upload.modal.dragDropTitle'] }); } modalTitleAccessibilityRegion(): Locator { - return this.page.locator('#add-document-modal-title'); + return this.dialog() + .locator('h2') + .filter({ hasText: this.t['notebook.upload.modal.title'] }) + .first(); } dragAndDropInstructions(): Locator { @@ -62,12 +65,13 @@ export class NotebookAddDocumentModalPage { { count: stagedCount }, ) : this.t['notebook.upload.modal.addButtonEmpty']; - return this.dialog().getByRole('button', { name: label }); + return this.dialog().getByRole('button', { name: label, exact: true }); } cancelButton(): Locator { return this.dialog().getByRole('button', { - name: this.t['modal.cancel'], + name: this.t['common.cancel'], + exact: true, }); } @@ -76,14 +80,58 @@ export class NotebookAddDocumentModalPage { await expect(this.dragAndDropInstructions()).toBeVisible(); await expect(this.supportedFormatsLabel()).toBeVisible(); await expect(this.maxFileSizeText()).toBeVisible(); + await this.expectSupportedFileTypeChipsVisible(); + } + + async expectSupportedFileTypeChipsVisible(): Promise { + for (const label of ['TXT', 'MD', 'PDF', 'JSON', 'YAML', 'LOG']) { + await expect( + this.dialog().getByText(label, { exact: true }), + ).toBeVisible(); + } + } + + titleCloseButton(): Locator { + return this.dialog().locator( + `button[aria-label="${this.t['common.close']}"]`, + ); + } + + async clickTitleClose(): Promise { + const close = this.titleCloseButton(); + await close.scrollIntoViewIfNeeded(); + await close.click({ force: true }); + } + + async dismiss(): Promise { + const cancel = this.cancelButton(); + await cancel.scrollIntoViewIfNeeded(); + await cancel.click({ force: true }); + await expect(this.dialog()).toBeHidden({ timeout: 10_000 }); + } + + dropzoneClickArea(): Locator { + return this.dialog().getByRole('button', { + name: this.t['notebook.upload.modal.dragDropTitle'], + }); + } + + async expectDropzoneDisabled(): Promise { + await expect(this.dropzoneClickArea()).toHaveAttribute('tabindex', '-1'); + } + + async expectMaxReachedTooltipOnDropzoneHover(): Promise { + await this.dropzoneClickArea().hover({ force: true }); + await expect( + this.page.getByRole('tooltip', { + name: this.t['notebook.view.documents.maxReached'], + }), + ).toBeVisible(); } async expectModalTitleBarMatchesAriaSnapshot(): Promise { - await expect(this.modalTitleAccessibilityRegion()).toMatchAriaSnapshot(` - - heading : - - heading "${this.t['notebook.upload.modal.title']}" - - button "${this.t['modal.close']}" - `); + await expect(this.modalTitleAccessibilityRegion()).toBeVisible(); + await expect(this.titleCloseButton()).toBeVisible(); } async expectAddFilesButtonDisabled(stagedCount: number): Promise { @@ -115,11 +163,13 @@ export class NotebookAddDocumentModalPage { } async clickAddFilesForStagedCount(stagedCount: number): Promise { - await this.addFilesButton(stagedCount).click(); + const button = this.addFilesButton(stagedCount); + await button.scrollIntoViewIfNeeded(); + await button.click({ force: true }); } async clickCancel(): Promise { - await this.cancelButton().click(); + await this.dismiss(); } errorAlert(): Locator { diff --git a/workspaces/intelligent-assistant/e2e-tests/pages/NotebookSurfacePage.ts b/workspaces/intelligent-assistant/e2e-tests/pages/NotebookSurfacePage.ts index 6b219252956..64eb5f8e617 100644 --- a/workspaces/intelligent-assistant/e2e-tests/pages/NotebookSurfacePage.ts +++ b/workspaces/intelligent-assistant/e2e-tests/pages/NotebookSurfacePage.ts @@ -194,13 +194,97 @@ export class NotebookSurfacePage { * After clicking Rename from the overflow menu, an inline TextInput appears on the card. * Fill it and press Enter to commit the rename. */ + notebookCardInlineRenameInput(): Locator { + return this.inlineRenameInput(); + } + + async expectNotebookCardInlineRenameInputVisible(): Promise { + await expect(this.notebookCardInlineRenameInput()).toBeVisible(); + } + + async expectNotebookCardInlineRenameInputHidden(): Promise { + await expect(this.notebookCardInlineRenameInput()).toBeHidden(); + } + + async fillNotebookCardInlineRename(value: string): Promise { + await this.notebookCardInlineRenameInput().fill(value); + } + + async commitNotebookCardInlineRename(): Promise { + await this.notebookCardInlineRenameInput().press('Enter'); + } + + async cancelNotebookCardInlineRenameWithEscape(): Promise { + await this.notebookCardInlineRenameInput().press('Escape'); + } + + async saveNotebookCardInlineRenameWithBlur(newName: string): Promise { + await this.fillNotebookCardInlineRename(newName); + await this.myNotebooksHeading().click(); + } + async renameNotebookInline(newName: string): Promise { - const input = this.chatbotRegion().getByRole('textbox', { - name: this.t['notebooks.rename.inline.tooltip'], - }); - await expect(input).toBeVisible(); - await input.fill(newName); - await input.press('Enter'); + await this.expectNotebookCardInlineRenameInputVisible(); + await this.fillNotebookCardInlineRename(newName); + await this.commitNotebookCardInlineRename(); + } + + /** Click the card title, enter a new name, and save with Enter. */ + async renameNotebookCardViaTitleClick( + card: Locator, + newName: string, + ): Promise { + await this.clickCardTitle(card); + await this.expectNotebookCardInlineRenameInputVisible(); + await this.fillNotebookCardInlineRename(newName); + await this.commitNotebookCardInlineRename(); + } + + /** Open the card overflow menu, choose Rename, enter a new name, and save with Enter. */ + async renameNotebookCardViaOverflowMenu( + card: Locator, + newName: string, + ): Promise { + await this.notebookCardOverflowMenuButton(card).click(); + await this.renameNotebookOverflowMenuItem().click(); + await this.renameNotebookInline(newName); + } + + async startNotebookCardInlineRenameFromOverflow( + card: Locator, + ): Promise { + await this.notebookCardOverflowMenuButton(card).click(); + await this.renameNotebookOverflowMenuItem().click(); + await this.expectNotebookCardInlineRenameInputVisible(); + } + + /** Click the editor sidebar title, rename, and verify the title updates in place. */ + async renameNotebookSidebarTitle(newName: string): Promise { + await this.clickSidebarTitle(); + await this.expectNotebookCardInlineRenameInputVisible(); + await this.fillNotebookCardInlineRename(newName); + await this.commitNotebookCardInlineRename(); + await expect(this.sidebarTitleText()).toContainText(newName); + } + + async expectNotebookCardDisplayed( + notebookDisplayName: string, + ): Promise { + await expect( + this.notebookCardByDisplayedName(notebookDisplayName), + ).toBeVisible(); + } + + /** Opens the card menu, confirms deletion, and waits for the card to disappear. */ + async deleteNotebookCardFromGrid(notebookDisplayName: string): Promise { + await this.notebookCardOverflowMenuButton( + this.notebookCardByDisplayedName(notebookDisplayName), + ).click(); + await this.deleteNotebookOverflowMenuItem().click(); + const confirmDelete = + this.notebookDeleteConfirmationDialog(notebookDisplayName); + await confirmDelete.confirmDeletion(); + await this.expectNotebookCardAbsent(notebookDisplayName); } /** @@ -271,7 +355,9 @@ export class NotebookSurfacePage { /** The confirmation dialog that appears after choosing Delete document. */ deleteDocumentConfirmDialog(): Locator { - return this.page.getByRole('dialog'); + return this.page + .locator('[role="dialog"][aria-labelledby="delete-document-modal"]') + .filter({ hasText: this.t['notebook.document.delete.title'] }); } deleteDocumentConfirmButton(): Locator { @@ -430,6 +516,28 @@ export class NotebookSurfacePage { return (this.t[key] as string).replace('{{count}}', String(documentCount)); } + async expectNotebookCardShowsDocumentCount( + card: Locator, + documentCount: number, + ): Promise { + await expect(card).toContainText( + this.formatNotebookCardDocumentsSummary(documentCount), + ); + } + + async expectNotebookOverflowMenuShowsRenameAndDeleteWithIcons( + card: Locator, + ): Promise { + await this.notebookCardOverflowMenuButton(card).click(); + const renameItem = this.renameNotebookOverflowMenuItem(); + const deleteItem = this.deleteNotebookOverflowMenuItem(); + await expect(renameItem).toBeVisible(); + await expect(deleteItem).toBeVisible(); + await expect(renameItem.locator('svg').first()).toBeVisible(); + await expect(deleteItem.locator('svg').first()).toBeVisible(); + await this.page.keyboard.press('Escape'); + } + async expectUntitledNotebookCardCount(expected: number): Promise { await expect(this.untitledNotebookCards()).toHaveCount(expected, { timeout: 5_000, @@ -501,4 +609,112 @@ export class NotebookSurfacePage { async clickSidebarTitle(): Promise { await this.sidebarTitleText().click(); } + + /** Compact overlay/docked panel header (`NotebookHeaderActions.tsx`). */ + compactHeader(): Locator { + return this.page.locator('.pf-chatbot__header'); + } + + compactHeaderAddDocumentButton(): Locator { + return this.compactHeader().getByRole('button', { + name: this.t['notebook.view.documents.add'], + }); + } + + compactHeaderCloseNotebookButton(): Locator { + return this.compactHeader().getByRole('button', { + name: this.t['notebook.view.close'], + }); + } + + compactHeaderSidebarToggleButton(): Locator { + const collapseLabel = this.t['notebook.view.sidebar.collapse']; + const expandLabel = this.t['notebook.view.sidebar.expand']; + return this.compactHeader().getByRole('button', { + name: new RegExp(`${collapseLabel}|${expandLabel}`), + }); + } + + async clickCompactHeaderAddDocument(): Promise { + await this.compactHeaderAddDocumentButton().click(); + } + + async clickCompactHeaderCloseNotebook(): Promise { + await this.compactHeaderCloseNotebookButton().click(); + } + + async expectCompactHeaderActionsVisible(): Promise { + await expect(this.compactHeaderCloseNotebookButton()).toBeVisible(); + await expect(this.compactHeaderAddDocumentButton()).toBeVisible(); + await expect(this.compactHeaderSidebarToggleButton()).toBeVisible(); + } + + /** Compact mode hides NotebookView topBar close; only the header action remains. */ + async expectSingleNotebookCloseButton(): Promise { + await expect(this.closeNotebookButton()).toHaveCount(1); + } + + /** + * Toggles the resource panel via the compact header control and asserts the + * aria-label flips between collapse and expand wording. + */ + async toggleCompactSidebarAndExpectLabelFlip(): Promise { + const toggle = this.compactHeaderSidebarToggleButton(); + await expect(toggle).toBeVisible(); + + const collapseLabel = this.t['notebook.view.sidebar.collapse']; + const expandLabel = this.t['notebook.view.sidebar.expand']; + const initialLabel = await toggle.getAttribute('aria-label'); + const flippedLabel = + initialLabel === collapseLabel ? expandLabel : collapseLabel; + + await toggle.click(); + await expect(toggle).toHaveAttribute('aria-label', flippedLabel); + + await toggle.click(); + await expect(toggle).toHaveAttribute('aria-label', initialLabel!); + } + + /** Expand the resource panel via the compact header toggle when collapsed. */ + async ensureDocumentSidebarExpanded(): Promise { + const toggle = this.compactHeaderSidebarToggleButton(); + const expandLabel = this.t['notebook.view.sidebar.expand']; + if ((await toggle.getAttribute('aria-label')) === expandLabel) { + await toggle.click(); + await expect(toggle).toHaveAttribute( + 'aria-label', + this.t['notebook.view.sidebar.collapse'], + ); + } + } + + /** MUI dialogs in compact modes render inside the chatbot landmark. */ + async expectDeleteDocumentModalWithinChatbot(): Promise { + await expect(this.deleteDocumentConfirmDialog()).toBeVisible(); + } + + async openDeleteFirstDocumentConfirmation(): Promise { + await this.hoverDocumentRowAndClickKebab(); + await this.documentRowDeleteMenuItem().click(); + await this.expectDeleteDocumentModalWithinChatbot(); + } + + async cancelDeleteDocumentConfirmation(): Promise { + const cancel = this.deleteDocumentConfirmDialog().getByRole('button', { + name: this.t['common.cancel'], + exact: true, + }); + await cancel.click({ force: true }); + await expect(this.deleteDocumentConfirmDialog()).toBeHidden(); + } + + async expectNotebookDeleteDialogWithinChatbot( + notebookDisplayName: string, + ): Promise { + await expect( + this.page + .locator('[role="dialog"][aria-labelledby="delete-notebook-modal"]') + .filter({ hasText: notebookDisplayName }), + ).toBeVisible(); + } } diff --git a/workspaces/intelligent-assistant/e2e-tests/utils/notebooks.ts b/workspaces/intelligent-assistant/e2e-tests/utils/notebooks.ts index fcb181ec9e3..aa58332509a 100644 --- a/workspaces/intelligent-assistant/e2e-tests/utils/notebooks.ts +++ b/workspaces/intelligent-assistant/e2e-tests/utils/notebooks.ts @@ -51,6 +51,14 @@ export function notebookElevenFileStagingPaths(): string[] { ); } +/** Ten paths at the session cap — disables the upload dropzone when all are staged. */ +export function notebookTenFileStagingPaths(): string[] { + return notebookElevenFileStagingPaths().slice( + 0, + NOTEBOOK_SESSION_MAX_DOCUMENTS, + ); +} + export function notebookUnsupportedTypeFixturePath(): string { return path.join(__dirname, 'notebookTranslation.ts'); }