diff --git a/.cspell.json b/.cspell.json index d09743f357..3716e48fea 100644 --- a/.cspell.json +++ b/.cspell.json @@ -483,7 +483,11 @@ "websockets", "swgs", "memlab", - "devmode" + "devmode", + "blockquote", + "blockquotes", + "Blockquote", + "Blockquotes" ], "dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US"], "ignorePaths": [ diff --git a/e2e/helper/notebookUtils.js b/e2e/helper/notebookUtils.js index ae4e6bd39e..75571f8966 100644 --- a/e2e/helper/notebookUtils.js +++ b/e2e/helper/notebookUtils.js @@ -34,7 +34,6 @@ async function enterTextEntry(page, text) { await page.locator(NOTEBOOK_DROP_AREA).click(); // enter text - await page.getByLabel('Notebook Entry Display').last().click(); await page.getByLabel('Notebook Entry Input').last().fill(text); await commitEntry(page); } @@ -53,6 +52,7 @@ async function dragAndDropEmbed(page, notebookObject) { await page.click('button[title="Show selected item in tree"]'); // Drag and drop the SWG into the notebook await page.dragAndDrop(`text=${swg.name}`, NOTEBOOK_DROP_AREA); + await commitEntry(page); } /** diff --git a/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js b/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js index fe68487377..81e858a88d 100644 --- a/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js @@ -279,7 +279,7 @@ test.describe('Notebook entry tests', () => { // Click .c-notebook__drag-area await page.locator('.c-notebook__drag-area').click(); - await expect(page.getByLabel('Notebook Entry Display')).toBeVisible(); + await expect(page.getByLabel('Notebook Entry Input')).toBeVisible(); await expect(page.getByLabel('Notebook Entry', { exact: true })).toHaveClass(/is-selected/); }); test('When an object is dropped into a notebook, a new entry is created and it should be focused @unstable', async ({ @@ -514,10 +514,23 @@ test.describe('Notebook entry tests', () => { const childItem = page.locator('li:has-text("List Item 2") ol li:has-text("Order 2")'); await expect(childItem).toBeVisible(); - // Blocks - const blockTest = '```javascript\nconst foo = "bar";\nconst bar = "foo";\n```'; - await nbUtils.enterTextEntry(page, blockTest); + // Code Blocks + const codeblockTest = '```javascript\nconst foo = "bar";\nconst bar = "foo";\n```'; + await nbUtils.enterTextEntry(page, codeblockTest); const codeBlock = page.locator('code.language-javascript:has-text("const foo = \\"bar\\";")'); await expect(codeBlock).toBeVisible(); + + // Blockquotes + const blockquoteTest = + 'This is a quote by Mark Twain:\n> "The man with a new idea is a crank\n>until the idea succeeds."'; + await nbUtils.enterTextEntry(page, blockquoteTest); + const firstLineOfBlockquoteText = page.locator( + 'blockquote:has-text("The man with a new idea is a crank")' + ); + await expect(firstLineOfBlockquoteText).toBeVisible(); + const secondLineOfBlockquoteText = page.locator( + 'blockquote:has-text("until the idea succeeds")' + ); + await expect(secondLineOfBlockquoteText).toBeVisible(); }); }); diff --git a/e2e/tests/functional/plugins/notebook/notebookSnapshots.e2e.spec.js b/e2e/tests/functional/plugins/notebook/notebookSnapshots.e2e.spec.js index e4b80b62ee..f3fa07f58b 100644 --- a/e2e/tests/functional/plugins/notebook/notebookSnapshots.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/notebookSnapshots.e2e.spec.js @@ -188,12 +188,11 @@ test.describe('Snapshot image tests', () => { }, fileData); await page.dispatchEvent('.c-notebook__drag-area', 'drop', { dataTransfer: dropTransfer }); - + await page.locator('.c-ne__save-button > button').click(); // be sure that entry was created await expect(page.getByText('favicon-96x96.png')).toBeVisible(); await page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).click(); - // expect large image to be displayed await expect(page.getByRole('dialog').getByText('favicon-96x96.png')).toBeVisible(); @@ -215,3 +214,59 @@ test.describe('Snapshot image tests', () => { expect(await page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).count()).toBe(1); }); }); + +test.describe('Snapshot image failure tests', () => { + test.use({ failOnConsoleError: false }); + test.beforeEach(async ({ page }) => { + //Navigate to baseURL + await page.goto('./', { waitUntil: 'domcontentloaded' }); + + // Create Notebook + await createDomainObjectWithDefaults(page, { + type: NOTEBOOK_NAME + }); + }); + + test('Get an error notification when dropping unknown file onto notebook entry', async ({ + page + }) => { + // fill Uint8Array array with some garbage data + const garbageData = new Uint8Array(100); + const fileData = Array.from(garbageData); + + const dropTransfer = await page.evaluateHandle((data) => { + const dataTransfer = new DataTransfer(); + const file = new File([new Uint8Array(data)], 'someGarbage.foo', { type: 'unknown/garbage' }); + dataTransfer.items.add(file); + return dataTransfer; + }, fileData); + + await page.dispatchEvent('.c-notebook__drag-area', 'drop', { dataTransfer: dropTransfer }); + + // should have gotten a notification from OpenMCT that we couldn't add it + await expect(page.getByText('Unknown object(s) dropped and cannot embed')).toBeVisible(); + }); + + test('Get an error notification when dropping big files onto notebook entry', async ({ + page + }) => { + const garbageSize = 15 * 1024 * 1024; // 15 megabytes + + await page.addScriptTag({ + // make the garbage client side + content: `window.bigGarbageData = new Uint8Array(${garbageSize})` + }); + + const bigDropTransfer = await page.evaluateHandle(() => { + const dataTransfer = new DataTransfer(); + const file = new File([window.bigGarbageData], 'bigBoy.png', { type: 'image/png' }); + dataTransfer.items.add(file); + return dataTransfer; + }); + + await page.dispatchEvent('.c-notebook__drag-area', 'drop', { dataTransfer: bigDropTransfer }); + + // should have gotten a notification from OpenMCT that we couldn't add it as it's too big + await expect(page.getByText('unable to embed')).toBeVisible(); + }); +}); diff --git a/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js b/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js index 89f46c7d3c..b4c9270b00 100644 --- a/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js @@ -150,7 +150,6 @@ test.describe('Tagging in Notebooks @addInit', () => { await createNotebookEntryAndTags(page); await page.locator('text=To start a new entry, click here or drag and drop any object').click(); - await page.getByLabel('Notebook Entry Display').last().click(); await page.getByLabel('Notebook Entry Input').fill(`An entry without tags`); await page.locator('.c-ne__save-button > button').click(); diff --git a/e2e/tests/performance/contract/notebook.contract.perf.spec.js b/e2e/tests/performance/contract/notebook.contract.perf.spec.js index a2db7a0f3c..a2d811256e 100644 --- a/e2e/tests/performance/contract/notebook.contract.perf.spec.js +++ b/e2e/tests/performance/contract/notebook.contract.perf.spec.js @@ -131,7 +131,6 @@ test.describe('Performance tests', () => { await page.evaluate(() => window.performance.mark('new-notebook-entry-created')); // Enter Notebook Entry text - await page.getByLabel('Notebook Entry').last().click(); await page.getByLabel('Notebook Entry Input').last().fill('New Entry'); await page.locator('.c-ne__save-button').click(); await page.evaluate(() => window.performance.mark('new-notebook-entry-filled')); diff --git a/src/plugins/notebook/components/NotebookComponent.vue b/src/plugins/notebook/components/NotebookComponent.vue index 1593988583..4b08974866 100644 --- a/src/plugins/notebook/components/NotebookComponent.vue +++ b/src/plugins/notebook/components/NotebookComponent.vue @@ -625,21 +625,35 @@ export default { dropEvent.preventDefault(); dropEvent.stopImmediatePropagation(); - const localImageDropped = dropEvent.dataTransfer.files?.[0]?.type.includes('image'); - const imageUrl = dropEvent.dataTransfer.getData('URL'); + const dataTransferFiles = Array.from(dropEvent.dataTransfer.files); + const localImageDropped = dataTransferFiles.some((file) => file.type.includes('image')); const snapshotId = dropEvent.dataTransfer.getData('openmct/snapshot/id'); + const domainObjectData = dropEvent.dataTransfer.getData('openmct/domain-object-path'); + const imageUrl = dropEvent.dataTransfer.getData('URL'); if (localImageDropped) { - // local image dropped from disk (file) - const imageData = dropEvent.dataTransfer.files[0]; - const imageEmbed = await createNewImageEmbed(imageData, this.openmct, imageData?.name); - this.newEntry(imageEmbed); + // local image(s) dropped from disk (file) + const embeds = []; + await Promise.all( + dataTransferFiles.map(async (file) => { + if (file.type.includes('image')) { + const imageData = file; + const imageEmbed = await createNewImageEmbed( + imageData, + this.openmct, + imageData?.name + ); + embeds.push(imageEmbed); + } + }) + ); + this.newEntry(embeds); } else if (imageUrl) { // remote image dropped (URL) try { const response = await fetch(imageUrl); const imageData = await response.blob(); const imageEmbed = await createNewImageEmbed(imageData, this.openmct); - this.newEntry(imageEmbed); + this.newEntry([imageEmbed]); } catch (error) { this.openmct.notifications.alert(`Unable to add image: ${error.message} `); console.error(`Problem embedding remote image`, error); @@ -647,7 +661,7 @@ export default { } else if (snapshotId.length) { // snapshot object const snapshot = this.snapshotContainer.getSnapshot(snapshotId); - this.newEntry(snapshot.embedObject); + this.newEntry([snapshot.embedObject]); this.snapshotContainer.removeSnapshot(snapshotId); const namespace = this.domainObject.identifier.namespace; @@ -656,10 +670,9 @@ export default { namespace ); saveNotebookImageDomainObject(this.openmct, notebookImageDomainObject); - } else { + } else if (domainObjectData) { // plain domain object - const data = dropEvent.dataTransfer.getData('openmct/domain-object-path'); - const objectPath = JSON.parse(data); + const objectPath = JSON.parse(domainObjectData); const bounds = this.openmct.time.bounds(); const snapshotMeta = { bounds, @@ -668,8 +681,15 @@ export default { openmct: this.openmct }; const embed = await createNewEmbed(snapshotMeta); - - this.newEntry(embed); + this.newEntry([embed]); + } else { + this.openmct.notifications.error( + `Unknown object(s) dropped and cannot embed. Try again with an image or domain object.` + ); + console.warn( + `Unknown object(s) dropped and cannot embed. Try again with an image or domain object.` + ); + return; } }, focusOnEntryId() { @@ -838,12 +858,12 @@ export default { getSelectedSectionId() { return this.selectedSection?.id; }, - async newEntry(embed, event) { + async newEntry(embeds, event) { this.startTransaction(); this.resetSearch(); const notebookStorage = this.createNotebookStorageObject(); this.updateDefaultNotebook(notebookStorage); - const id = await addNotebookEntry(this.openmct, this.domainObject, notebookStorage, embed); + const id = await addNotebookEntry(this.openmct, this.domainObject, notebookStorage, embeds); const element = this.$refs.notebookEntries.querySelector(`#${id}`); const entryAnnotations = this.notebookAnnotations[id] ?? {}; @@ -861,6 +881,11 @@ export default { this.filterAndSortEntries(); this.focusEntryId = id; this.selectedEntryId = id; + + // put entry into edit mode + this.$nextTick(() => { + element.dispatchEvent(new Event('click')); + }); }, orientationChange() { this.formatSidebar(); diff --git a/src/plugins/notebook/components/NotebookEntry.vue b/src/plugins/notebook/components/NotebookEntry.vue index 99c93498e7..0f9a8c0c3b 100644 --- a/src/plugins/notebook/components/NotebookEntry.vue +++ b/src/plugins/notebook/components/NotebookEntry.vue @@ -31,6 +31,7 @@ @drop.capture="cancelEditMode" @drop.prevent="dropOnEntry" @click="selectAndEmitEntry($event, entry)" + @paste="addImageFromPaste" >