From 2999a5135e8df553c22c4f28136a7c4dfb1f18db Mon Sep 17 00:00:00 2001
From: Scott Bell <scott@traclabs.com>
Date: Tue, 5 Jul 2022 17:45:39 +0200
Subject: [PATCH] 5361 Tags not persisting when several notebook entries are
 created at once (#5428)

* add end to end test to catch multiple entry errors

* click expansion triangle instead

* fix race condition between annotation creation and mutation

* make sure notebook tags run in e2e

* address PR comments
---
 e2e/tests/plugins/notebook/tags.e2e.spec.js   | 107 ++++++++++++++----
 package.json                                  |   2 +-
 src/api/annotation/AnnotationAPI.js           |  16 +--
 .../notebook/components/NotebookEntry.vue     |   1 +
 4 files changed, 94 insertions(+), 32 deletions(-)

diff --git a/e2e/tests/plugins/notebook/tags.e2e.spec.js b/e2e/tests/plugins/notebook/tags.e2e.spec.js
index 422820fef6..c7f145c2ee 100644
--- a/e2e/tests/plugins/notebook/tags.e2e.spec.js
+++ b/e2e/tests/plugins/notebook/tags.e2e.spec.js
@@ -29,46 +29,57 @@ const { test } = require('../../../fixtures');
 
 /**
   * Creates a notebook object and adds an entry.
-  * @param {import('@playwright/test').Page} page
+  * @param {import('@playwright/test').Page} - page to load
+  * @param {number} [iterations = 1] - the number of entries to create
   */
-async function createNotebookAndEntry(page) {
+async function createNotebookAndEntry(page, iterations = 1) {
     //Go to baseURL
     await page.goto('/', { waitUntil: 'networkidle' });
 
     // Click button:has-text("Create")
     await page.locator('button:has-text("Create")').click();
-    // Click li:has-text("Notebook")
-    await page.locator('li:has-text("Notebook")').click();
+    await page.locator('[title="Create and save timestamped notes with embedded object snapshots."]').click();
     // Click button:has-text("OK")
     await Promise.all([
         page.waitForNavigation(),
+        page.locator('[name="mctForm"] >> text=My Items').click(),
         page.locator('button:has-text("OK")').click()
     ]);
 
-    // Click text=To start a new entry, click here or drag and drop any object
-    await page.locator('text=To start a new entry, click here or drag and drop any object').click();
+    for (let iteration = 0; iteration < iterations; iteration++) {
+        // Click text=To start a new entry, click here or drag and drop any object
+        await page.locator('text=To start a new entry, click here or drag and drop any object').click();
+        const entryLocator = `[aria-label="Notebook Entry Input"] >> nth = ${iteration}`;
+        await page.locator(entryLocator).click();
+        await page.locator(entryLocator).fill(`Entry ${iteration}`);
+    }
+
 }
 
 /**
   * Creates a notebook object, adds an entry, and adds a tag.
   * @param {import('@playwright/test').Page} page
+  * @param {number} [iterations = 1] - the number of entries (and tags) to create
   */
-async function createNotebookEntryAndTags(page) {
-    await createNotebookAndEntry(page);
-    // Click text=To start a new entry, click here or drag and drop any object
-    await page.locator('button:has-text("Add Tag")').click();
+async function createNotebookEntryAndTags(page, iterations = 1) {
+    await createNotebookAndEntry(page, iterations);
 
-    // Click [placeholder="Type to select tag"]
-    await page.locator('[placeholder="Type to select tag"]').click();
-    // Click text=Driving
-    await page.locator('text=Driving').click();
+    for (let iteration = 0; iteration < iterations; iteration++) {
+        // Click text=To start a new entry, click here or drag and drop any object
+        await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
 
-    // Click button:has-text("Add Tag")
-    await page.locator('button:has-text("Add Tag")').click();
-    // Click [placeholder="Type to select tag"]
-    await page.locator('[placeholder="Type to select tag"]').click();
-    // Click text=Science
-    await page.locator('text=Science').click();
+        // Click [placeholder="Type to select tag"]
+        await page.locator('[placeholder="Type to select tag"]').click();
+        // Click text=Driving
+        await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
+
+        // Click button:has-text("Add Tag")
+        await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
+        // Click [placeholder="Type to select tag"]
+        await page.locator('[placeholder="Type to select tag"]').click();
+        // Click text=Science
+        await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
+    }
 }
 
 test.describe('Tagging in Notebooks', () => {
@@ -137,10 +148,58 @@ test.describe('Tagging in Notebooks', () => {
         await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
     });
     test('Tags persist across reload', async ({ page }) => {
-        await createNotebookEntryAndTags(page);
         //Go to baseURL
-        await page.reload();
-        await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
-        await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Driving");
+        await page.goto('/', { waitUntil: 'networkidle' });
+
+        // Create a clock object we can navigate to
+        await page.click('button:has-text("Create")');
+
+        // Click Clock
+        await page.click('text=Clock');
+        // Click button:has-text("OK")
+        await Promise.all([
+            page.waitForNavigation(),
+            page.locator('[name="mctForm"] >> text=My Items').click(),
+            page.locator('button:has-text("OK")').click()
+        ]);
+
+        await page.click('.c-disclosure-triangle');
+
+        const ITERATIONS = 4;
+        await createNotebookEntryAndTags(page, ITERATIONS);
+
+        for (let iteration = 0; iteration < ITERATIONS; iteration++) {
+            const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
+            await expect(page.locator(entryLocator)).toContainText("Science");
+            await expect(page.locator(entryLocator)).toContainText("Driving");
+        }
+
+        // Click Unnamed Clock
+        await page.click('text="Unnamed Clock"');
+
+        // Click Unnamed Notebook
+        await page.click('text="Unnamed Notebook"');
+
+        for (let iteration = 0; iteration < ITERATIONS; iteration++) {
+            const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
+            await expect(page.locator(entryLocator)).toContainText("Science");
+            await expect(page.locator(entryLocator)).toContainText("Driving");
+        }
+
+        //Reload Page
+        await Promise.all([
+            page.reload(),
+            page.waitForLoadState('networkidle')
+        ]);
+
+        // Click Unnamed Notebook
+        await page.click('text="Unnamed Notebook"');
+
+        for (let iteration = 0; iteration < ITERATIONS; iteration++) {
+            const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
+            await expect(page.locator(entryLocator)).toContainText("Science");
+            await expect(page.locator(entryLocator)).toContainText("Driving");
+        }
+
     });
 });
diff --git a/package.json b/package.json
index fb1094f23c..4439b37a68 100644
--- a/package.json
+++ b/package.json
@@ -89,7 +89,7 @@
     "test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
     "test:firefox": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless",
     "test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
-    "test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke branding default condition timeConductor clock exampleImagery persistence performance grandsearch notebook/tags",
+    "test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke branding default condition timeConductor clock exampleImagery persistence performance grandsearch tags",
     "test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
     "test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome --grep @snapshot --update-snapshots",
     "test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js",
diff --git a/src/api/annotation/AnnotationAPI.js b/src/api/annotation/AnnotationAPI.js
index a856114957..a197620389 100644
--- a/src/api/annotation/AnnotationAPI.js
+++ b/src/api/annotation/AnnotationAPI.js
@@ -172,17 +172,19 @@ export default class AnnotationAPI extends EventEmitter {
                 name: contentText,
                 domainObject: targetDomainObject,
                 annotationType,
-                tags: [],
+                tags: [tag],
                 contentText,
                 targets
             };
-            existingAnnotation = await this.create(annotationCreationArguments);
+            const newAnnotation = await this.create(annotationCreationArguments);
+
+            return newAnnotation;
+        } else {
+            const tagArray = [tag, ...existingAnnotation.tags];
+            this.openmct.objects.mutate(existingAnnotation, 'tags', tagArray);
+
+            return existingAnnotation;
         }
-
-        const tagArray = [tag, ...existingAnnotation.tags];
-        this.openmct.objects.mutate(existingAnnotation, 'tags', tagArray);
-
-        return existingAnnotation;
     }
 
     removeAnnotationTag(existingAnnotation, tagToRemove) {
diff --git a/src/plugins/notebook/components/NotebookEntry.vue b/src/plugins/notebook/components/NotebookEntry.vue
index 85716f6e83..f90aecb030 100644
--- a/src/plugins/notebook/components/NotebookEntry.vue
+++ b/src/plugins/notebook/components/NotebookEntry.vue
@@ -59,6 +59,7 @@
                 <div
                     :id="entry.id"
                     class="c-ne__text c-ne__input"
+                    aria-label="Notebook Entry Input"
                     tabindex="0"
                     contenteditable="true"
                     @focus="editingEntry()"