This is an automated email from the ASF dual-hosted git repository.

tbonelee pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git


The following commit(s) were added to refs/heads/master by this push:
     new a454d173e5 [ZEPPELIN-6547] Add collaboration-mode and editor-search 
e2e coverage for the new UI
a454d173e5 is described below

commit a454d173e52edfe413fc57341c8d5002ad6659c6
Author: YONGJAE LEE (이용재) <[email protected]>
AuthorDate: Wed Jul 22 10:46:37 2026 +0900

    [ZEPPELIN-6547] Add collaboration-mode and editor-search e2e coverage for 
the new UI
    
    ### What is this PR for?
    Adds two e2e scenarios missing from the Angular suite and cleans up the 
action bar's personalized-mode toggle found along the way.
    
    - **Collaborative edit sync**: edits in one viewer propagate to a second 
viewer of the same note (same-principal scope).
    - **Editor find widget (per-paragraph Monaco)**: open via shortcut, match 
count and highlights, next/previous navigation, replace-all. The notebook-wide 
search/replace menu is unimplemented and tracked separately by 
[ZEPPELIN-6442](https://issues.apache.org/jira/browse/ZEPPELIN-6442), so its 
scenarios are out of scope here.
    - **Accessibility**: `aria-label` on the two icon-only personalized-mode 
toggle buttons (the only `src/` change).
    - **Test cleanup**: replaces the always-skipped action-bar toggle test (its 
gate targeted a `ng-container[ngSwitch=...]` that never renders) with a real 
toggle round-trip test (auth mode; skipped for anonymous, where the button 
isn't rendered).
    
    Page objects follow `e2e/AGENTS.md` (EditorSearchPage, CollaborationPage; 
shared auth-skip helper in `e2e/utils.ts`).
    
    ### What type of PR is it?
    Improvement
    
    ### What is the Jira issue?
    https://issues.apache.org/jira/browse/ZEPPELIN-6547
    
    ### How should this be tested?
    Ran the collaboration and editor-search specs 10x per mode (anonymous + 
auth) across all browser projects with `--retries=0`, all green:
    https://github.com/voidmatcha/zeppelin/actions/runs/29684367065
    
    ### Questions:
    * Does the license files need to update? No
    * Is there breaking changes for older versions? No
    * Does this needs documentation? No
    
    Closes #5327 from voidmatcha/fix/new-ui-personalized-toggle-e2e.
    
    Signed-off-by: ChanHo Lee <[email protected]>
---
 .../e2e/models/collaboration-page.ts               | 62 ++++++++++++++
 .../e2e/models/editor-search-page.ts               | 81 ++++++++++++++++++
 .../e2e/models/notebook-action-bar-page.ts         | 14 ----
 .../action-bar/action-bar-functionality.spec.ts    | 19 +----
 .../collaboration/collaborative-mode.spec.ts       | 90 ++++++++++++++++++++
 .../tests/notebook/search/editor-search.spec.ts    | 98 ++++++++++++++++++++++
 zeppelin-web-angular/e2e/utils.ts                  |  8 ++
 .../notebook/action-bar/action-bar.component.html  |  2 +
 8 files changed, 342 insertions(+), 32 deletions(-)

diff --git a/zeppelin-web-angular/e2e/models/collaboration-page.ts 
b/zeppelin-web-angular/e2e/models/collaboration-page.ts
new file mode 100644
index 0000000000..6bab8540d6
--- /dev/null
+++ b/zeppelin-web-angular/e2e/models/collaboration-page.ts
@@ -0,0 +1,62 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { expect, Locator, Page } from '@playwright/test';
+import { waitForZeppelinReady } from '../utils';
+import { BasePage } from './base-page';
+
+export class CollaborationPage extends BasePage {
+  readonly paragraph: Locator;
+  readonly editor: Locator;
+  readonly editorText: Locator;
+  readonly switchToPersonalModeButton: Locator;
+  readonly switchToCollaborationModeButton: Locator;
+
+  constructor(page: Page) {
+    super(page);
+    // JUSTIFIED: CSS chains into Monaco's third-party DOM — it exposes no 
roles/test ids.
+    this.paragraph = page.locator('zeppelin-notebook-paragraph').first();
+    this.editor = this.paragraph.locator('.monaco-editor').first();
+    this.editorText = this.paragraph.locator('.view-lines').first();
+    this.switchToPersonalModeButton = page.getByRole('button', { name: 'Switch 
to personal mode' });
+    this.switchToCollaborationModeButton = page.getByRole('button', { name: 
'Switch to collaboration mode' });
+  }
+
+  async openNotebook(noteId: string): Promise<void> {
+    await this.page.goto(`/#/notebook/${noteId}`);
+    await waitForZeppelinReady(this.page);
+    await expect(this.paragraph).toBeVisible({ timeout: 15000 });
+  }
+
+  async getPrincipal(): Promise<string> {
+    const response = await this.page.request.get('/api/security/ticket', { 
failOnStatusCode: false });
+    if (!response.ok()) {
+      return '';
+    }
+    const json = (await response.json()) as { body?: { principal?: string } };
+    return json.body?.principal ?? '';
+  }
+
+  async confirmPersonalizedModeChange(): Promise<void> {
+    // Scope to this dialog and wait for it to close, so a back-to-back toggle 
can't race the animation or hit another modal.
+    const dialog = this.page.locator('.ant-modal-confirm', { hasText: 'Setting 
the result display' }).first();
+    await expect(dialog).toBeVisible({ timeout: 15000 });
+    await dialog.locator('button:has-text("OK")').click();
+    await expect(dialog).toBeHidden({ timeout: 15000 });
+  }
+
+  async typeInEditor(text: string): Promise<void> {
+    await this.editor.click();
+    // insertText avoids per-key events that can trigger Monaco autocomplete 
(see ZEPPELIN-6536).
+    await this.page.keyboard.insertText(text);
+  }
+}
diff --git a/zeppelin-web-angular/e2e/models/editor-search-page.ts 
b/zeppelin-web-angular/e2e/models/editor-search-page.ts
new file mode 100644
index 0000000000..d47ba32fc4
--- /dev/null
+++ b/zeppelin-web-angular/e2e/models/editor-search-page.ts
@@ -0,0 +1,81 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { expect, Locator, Page } from '@playwright/test';
+import { waitForZeppelinReady } from '../utils';
+import { BasePage } from './base-page';
+
+export class EditorSearchPage extends BasePage {
+  readonly editor: Locator;
+  readonly editorText: Locator;
+  readonly findWidget: Locator;
+  readonly findInput: Locator;
+  readonly replaceInput: Locator;
+  readonly matchesCount: Locator;
+  readonly matchHighlights: Locator;
+  readonly nextMatchButton: Locator;
+  readonly previousMatchButton: Locator;
+  readonly toggleReplaceButton: Locator;
+  readonly replaceAllButton: Locator;
+
+  constructor(page: Page) {
+    super(page);
+    // JUSTIFIED: Monaco's find-widget DOM exposes no roles/test ids; 
aria-label/title alternates used where available.
+    this.editor = page.locator('zeppelin-notebook-paragraph 
.monaco-editor').first();
+    this.editorText = this.editor.locator('.view-lines').first();
+    this.findWidget = this.editor.locator('.find-widget').first();
+    this.findInput = this.findWidget
+      .locator('.monaco-findInput .input, input[aria-label="Find"], 
textarea[aria-label="Find"]')
+      .first();
+    this.replaceInput = this.findWidget
+      .locator('.replace-input .input, input[aria-label="Replace"], 
textarea[aria-label="Replace"]')
+      .first();
+    this.matchesCount = this.findWidget.locator('.matchesCount').first();
+    // Monaco decorates every match with .findMatch and the active one with 
.currentFindMatch.
+    this.matchHighlights = this.editor.locator('.findMatch, 
.currentFindMatch');
+    this.nextMatchButton = this.findWidget.locator('.button.next, 
[title^="Next Match"]').first();
+    this.previousMatchButton = this.findWidget.locator('.button.previous, 
[title^="Previous Match"]').first();
+    this.toggleReplaceButton = this.findWidget.locator('.button.toggle, 
[title^="Toggle Replace"]').first();
+    this.replaceAllButton = this.findWidget.locator('.button.replace-all, 
[title^="Replace All"]').first();
+  }
+
+  async openNotebook(noteId: string): Promise<void> {
+    await this.page.goto(`/#/notebook/${noteId}`);
+    await waitForZeppelinReady(this.page);
+    await expect(this.editor).toBeVisible({ timeout: 15000 });
+  }
+
+  async setEditorContent(content: string): Promise<void> {
+    await this.editor.click();
+    // Key off the browser, not the host: Monaco follows the browser UA's 
keymap, and
+    // webkit emulates macOS (Meta) even on a Linux CI host.
+    const isWebkit = this.page.context().browser()?.browserType().name() === 
'webkit';
+    await this.page.keyboard.press(isWebkit ? 'Meta+A' : 'ControlOrMeta+A');
+    await this.page.keyboard.insertText(content);
+    await expect(this.editorText).toContainText(content.split('\n')[0], { 
timeout: 15000 });
+  }
+
+  async openFindWidget(): Promise<void> {
+    await this.editor.click();
+    // 'Home' anchors the find widget on the first match (cursor sits at end 
after seeding);
+    // keymap-independent, and single-line content means line start == 
document start.
+    await this.page.keyboard.press('Home');
+    // Control+S is Zeppelin's SearchInsideCode binding (shortcuts-map.ts), 
not a typo of Control+F.
+    await this.page.keyboard.press('Control+S');
+    await expect(this.findWidget).toBeVisible({ timeout: 15000 });
+  }
+
+  async searchFor(text: string): Promise<void> {
+    await this.findInput.fill(text);
+    await expect(this.matchesCount).toBeVisible({ timeout: 15000 });
+  }
+}
diff --git a/zeppelin-web-angular/e2e/models/notebook-action-bar-page.ts 
b/zeppelin-web-angular/e2e/models/notebook-action-bar-page.ts
index d73b268c2d..1ecc33bd2c 100644
--- a/zeppelin-web-angular/e2e/models/notebook-action-bar-page.ts
+++ b/zeppelin-web-angular/e2e/models/notebook-action-bar-page.ts
@@ -22,9 +22,6 @@ export class NotebookActionBarPage extends BasePage {
   readonly cloneButton: Locator;
   readonly exportButton: Locator;
   readonly reloadButton: Locator;
-  readonly collaborationModeToggle: Locator;
-  readonly personalModeButton: Locator;
-  readonly collaborationModeButton: Locator;
   readonly commitButton: Locator;
   readonly setRevisionButton: Locator;
   readonly compareRevisionsButton: Locator;
@@ -49,9 +46,6 @@ export class NotebookActionBarPage extends BasePage {
     this.cloneButton = page.locator('button[nzTooltipTitle="Clone this 
note"]');
     this.exportButton = page.locator('button[nzTooltipTitle="Export this 
note"]');
     this.reloadButton = page.locator('button[nzTooltipTitle="Reload from note 
file"]');
-    this.collaborationModeToggle = 
page.locator('ng-container[ngSwitch="note.config.personalizedMode"]');
-    this.personalModeButton = page.getByRole('button', { name: 'Personal' });
-    this.collaborationModeButton = page.getByRole('button', { name: 
'Collaboration' });
     this.commitButton = page.getByRole('button', { name: 'Commit' });
     this.setRevisionButton = page.getByRole('button', { name: 'Set as default 
revision' });
     this.compareRevisionsButton = page.getByRole('button', { name: 'Compare 
with current revision' });
@@ -83,14 +77,6 @@ export class NotebookActionBarPage extends BasePage {
     await this.clearOutputButton.click();
   }
 
-  async switchToPersonalMode(): Promise<void> {
-    await this.personalModeButton.click();
-  }
-
-  async switchToCollaborationMode(): Promise<void> {
-    await this.collaborationModeButton.click();
-  }
-
   async openRevisionDropdown(): Promise<void> {
     await this.revisionDropdown.click();
   }
diff --git 
a/zeppelin-web-angular/e2e/tests/notebook/action-bar/action-bar-functionality.spec.ts
 
b/zeppelin-web-angular/e2e/tests/notebook/action-bar/action-bar-functionality.spec.ts
index 358e77c65d..f8d158838b 100644
--- 
a/zeppelin-web-angular/e2e/tests/notebook/action-bar/action-bar-functionality.spec.ts
+++ 
b/zeppelin-web-angular/e2e/tests/notebook/action-bar/action-bar-functionality.spec.ts
@@ -140,24 +140,7 @@ test.describe('Notebook Action Bar Functionality', () => {
     await expect(actionBarPage.reloadButton).toBeEnabled();
   });
 
-  test('should handle collaboration mode toggle when available', async () => {
-    test.skip(
-      !(await actionBarPage.collaborationModeToggle.isVisible()),
-      'Collaboration mode not available in this environment'
-    );
-
-    const personalVisible = await actionBarPage.personalModeButton.isVisible();
-    const collaborationVisible = await 
actionBarPage.collaborationModeButton.isVisible();
-    expect(personalVisible || collaborationVisible).toBe(true);
-
-    if (personalVisible) {
-      await actionBarPage.switchToPersonalMode();
-      await expect(actionBarPage.collaborationModeButton).toBeVisible({ 
timeout: 5000 });
-    } else if (collaborationVisible) {
-      await actionBarPage.switchToCollaborationMode();
-      await expect(actionBarPage.personalModeButton).toBeVisible({ timeout: 
5000 });
-    }
-  });
+  // Toggle coverage lives in collaboration/collaborative-mode.spec.ts.
 
   test('should handle revision controls when supported', async () => {
     test.skip(!(await actionBarPage.commitButton.isVisible()), 'Revision 
controls not supported in this environment');
diff --git 
a/zeppelin-web-angular/e2e/tests/notebook/collaboration/collaborative-mode.spec.ts
 
b/zeppelin-web-angular/e2e/tests/notebook/collaboration/collaborative-mode.spec.ts
new file mode 100644
index 0000000000..7d463b7fb1
--- /dev/null
+++ 
b/zeppelin-web-angular/e2e/tests/notebook/collaboration/collaborative-mode.spec.ts
@@ -0,0 +1,90 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { expect, Page, test } from '@playwright/test';
+import { CollaborationPage } from 'e2e/models/collaboration-page';
+import {
+  addPageAnnotationBeforeEach,
+  createTestNotebook,
+  PAGES,
+  performLoginIfRequired,
+  skipWhenAuthenticationIsStillRequired,
+  waitForNotebookLinks,
+  waitForZeppelinReady
+} from '../../../utils';
+
+const prepareWorkspace = async (page: Page): Promise<void> => {
+  await page.goto('/#/');
+  await waitForZeppelinReady(page);
+  await performLoginIfRequired(page);
+  await skipWhenAuthenticationIsStillRequired(page);
+  await waitForNotebookLinks(page);
+};
+
+test.describe('Collaborative mode', () => {
+  addPageAnnotationBeforeEach(PAGES.WORKSPACE.NOTEBOOK);
+
+  // Both viewers share one principal (same storageState); cross-principal 
routing/permissions are out of scope here.
+  test('syncs paragraph editor changes between two notebook viewers', async ({ 
page, browser }) => {
+    const syncText = `collaborative_mode_text_${Date.now()}`;
+    const collaborationPage = new CollaborationPage(page);
+
+    await prepareWorkspace(page);
+
+    const { noteId } = await createTestNotebook(page);
+    await collaborationPage.openNotebook(noteId);
+
+    const collaboratorContext = await browser.newContext({ storageState: await 
page.context().storageState() });
+    const collaboratorPage = await collaboratorContext.newPage();
+    const collaboratorView = new CollaborationPage(collaboratorPage);
+
+    try {
+      await collaboratorPage.goto('/#/');
+      await waitForZeppelinReady(collaboratorPage);
+      await performLoginIfRequired(collaboratorPage);
+      await skipWhenAuthenticationIsStillRequired(collaboratorPage);
+      await collaboratorView.openNotebook(noteId);
+
+      await expect(collaborationPage.editor).toBeVisible({ timeout: 15000 });
+      await expect(collaboratorView.editor).toBeVisible({ timeout: 15000 });
+
+      await collaborationPage.typeInEditor(syncText);
+
+      await expect(collaborationPage.editorText).toContainText(syncText, { 
timeout: 15000 });
+      await expect(collaboratorView.editorText).toContainText(syncText, { 
timeout: 30000 });
+    } finally {
+      await collaboratorContext.close();
+    }
+  });
+
+  test('toggles between personal and collaboration mode from the action bar', 
async ({ page }) => {
+    const collaborationPage = new CollaborationPage(page);
+
+    await prepareWorkspace(page);
+
+    const principal = await collaborationPage.getPrincipal();
+    test.skip(!principal || principal === 'anonymous', 'The mode toggle is not 
rendered for anonymous principals');
+
+    const { noteId } = await createTestNotebook(page);
+    await collaborationPage.openNotebook(noteId);
+
+    await expect(collaborationPage.switchToPersonalModeButton).toBeVisible({ 
timeout: 15000 });
+
+    await collaborationPage.switchToPersonalModeButton.click();
+    await collaborationPage.confirmPersonalizedModeChange();
+    await 
expect(collaborationPage.switchToCollaborationModeButton).toBeVisible({ 
timeout: 15000 });
+
+    await collaborationPage.switchToCollaborationModeButton.click();
+    await collaborationPage.confirmPersonalizedModeChange();
+    await expect(collaborationPage.switchToPersonalModeButton).toBeVisible({ 
timeout: 15000 });
+  });
+});
diff --git 
a/zeppelin-web-angular/e2e/tests/notebook/search/editor-search.spec.ts 
b/zeppelin-web-angular/e2e/tests/notebook/search/editor-search.spec.ts
new file mode 100644
index 0000000000..a7420914f1
--- /dev/null
+++ b/zeppelin-web-angular/e2e/tests/notebook/search/editor-search.spec.ts
@@ -0,0 +1,98 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { expect, test } from '@playwright/test';
+import { EditorSearchPage } from 'e2e/models/editor-search-page';
+import {
+  addPageAnnotationBeforeEach,
+  createTestNotebook,
+  PAGES,
+  performLoginIfRequired,
+  skipWhenAuthenticationIsStillRequired,
+  waitForNotebookLinks,
+  waitForZeppelinReady
+} from '../../../utils';
+
+// Covers the per-paragraph Monaco find widget. The notebook-wide 
search/replace menu is
+// unimplemented and tracked by ZEPPELIN-6442.
+test.describe('Notebook editor search', () => {
+  addPageAnnotationBeforeEach(PAGES.WORKSPACE.NOTEBOOK);
+
+  let editorSearchPage: EditorSearchPage;
+
+  test.beforeEach(async ({ page }) => {
+    editorSearchPage = new EditorSearchPage(page);
+    await page.goto('/#/');
+    await waitForZeppelinReady(page);
+    await performLoginIfRequired(page);
+    await skipWhenAuthenticationIsStillRequired(page);
+    await waitForNotebookLinks(page);
+  });
+
+  test('shows match count and navigates next and previous matches', async ({ 
page }) => {
+    const { noteId } = await createTestNotebook(page);
+
+    await editorSearchPage.openNotebook(noteId);
+    await editorSearchPage.setEditorContent('alpha target beta target gamma 
target');
+    await editorSearchPage.openFindWidget();
+    await editorSearchPage.searchFor('target');
+
+    await expect(editorSearchPage.matchesCount).toContainText(/1 of 3/, { 
timeout: 15000 });
+
+    await editorSearchPage.nextMatchButton.click();
+    await expect(editorSearchPage.matchesCount).toContainText(/2 of 3/, { 
timeout: 15000 });
+
+    await editorSearchPage.previousMatchButton.click();
+    await expect(editorSearchPage.matchesCount).toContainText(/1 of 3/, { 
timeout: 15000 });
+  });
+
+  test('opens the find widget with the search shortcut', async ({ page }) => {
+    const { noteId } = await createTestNotebook(page);
+
+    await editorSearchPage.openNotebook(noteId);
+    await editorSearchPage.setEditorContent('find me in this line');
+    await editorSearchPage.openFindWidget();
+
+    await expect(editorSearchPage.findWidget).toBeVisible();
+  });
+
+  test('highlights every match in the editor', async ({ page }) => {
+    const { noteId } = await createTestNotebook(page);
+
+    await editorSearchPage.openNotebook(noteId);
+    await editorSearchPage.setEditorContent('alpha target beta target gamma 
target');
+    await editorSearchPage.openFindWidget();
+    await editorSearchPage.searchFor('target');
+
+    await expect(editorSearchPage.matchesCount).toContainText(/1 of 3/, { 
timeout: 15000 });
+    await expect(editorSearchPage.matchHighlights).toHaveCount(3);
+  });
+
+  test('replaces all matches in the editor search widget', async ({ page }) => 
{
+    const { noteId } = await createTestNotebook(page);
+
+    await editorSearchPage.openNotebook(noteId);
+    await editorSearchPage.setEditorContent('replace_target one replace_target 
two replace_target');
+    await editorSearchPage.openFindWidget();
+    await editorSearchPage.searchFor('replace_target');
+    await expect(editorSearchPage.matchesCount).toContainText(/1 of 3/, { 
timeout: 15000 });
+
+    await editorSearchPage.toggleReplaceButton.click();
+    await editorSearchPage.replaceInput.fill('replacement');
+    await editorSearchPage.replaceAllButton.click();
+
+    await expect(editorSearchPage.editorText).toContainText('replacement one 
replacement two replacement', {
+      timeout: 15000
+    });
+    await 
expect(editorSearchPage.editorText).not.toContainText('replace_target');
+  });
+});
diff --git a/zeppelin-web-angular/e2e/utils.ts 
b/zeppelin-web-angular/e2e/utils.ts
index 93aaf67f70..25e6d4304b 100644
--- a/zeppelin-web-angular/e2e/utils.ts
+++ b/zeppelin-web-angular/e2e/utils.ts
@@ -281,6 +281,14 @@ export const performLoginIfRequired = async (page: Page): 
Promise<boolean> => {
   return false;
 };
 
+export const skipWhenAuthenticationIsStillRequired = async (page: Page): 
Promise<void> => {
+  const loginStillVisible = await page
+    .locator('zeppelin-login')
+    .isVisible()
+    .catch(() => false);
+  test.skip(loginStillVisible, 'Authentication is enabled but no E2E test 
credentials are configured');
+};
+
 export const waitForZeppelinReady = async (page: Page, options: 
WaitForZeppelinReadyOptions = {}): Promise<void> => {
   try {
     // Enhanced wait for network idle with longer timeout for CI environments
diff --git 
a/zeppelin-web-angular/src/app/pages/workspace/notebook/action-bar/action-bar.component.html
 
b/zeppelin-web-angular/src/app/pages/workspace/notebook/action-bar/action-bar.component.html
index b829227fd9..f53f5292da 100644
--- 
a/zeppelin-web-angular/src/app/pages/workspace/notebook/action-bar/action-bar.component.html
+++ 
b/zeppelin-web-angular/src/app/pages/workspace/notebook/action-bar/action-bar.component.html
@@ -95,6 +95,7 @@
             <button
               nz-tooltip
               nzTooltipTitle="Switch to collaboration mode"
+              aria-label="Switch to collaboration mode"
               [disabled]="revisionView || !isOwner || isNoteParagraphRunning"
               nz-button
               nzType="primary"
@@ -107,6 +108,7 @@
             <button
               nz-tooltip
               nzTooltipTitle="Switch to personal mode"
+              aria-label="Switch to personal mode"
               nz-button
               [disabled]="revisionView || !isOwner || isNoteParagraphRunning"
               (click)="toggleNotePersonalizedMode()"

Reply via email to