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()"