Skip to content

Commit 5e9847b

Browse files
authored
[ZEPPELIN-6358] Add E2E test coverage for notebook components
### What is this PR for? This is the final PR in the series derived from #5101. Notebook features had zero E2E coverage. This adds 20 spec files (~3500 lines). **Notebook core** - `notebook-container` — structure, action bar presence, sidebar width constraints, paragraph grid layout, extension area - `action-bar-functionality` — run all, code/output toggle, clear output, clone/export/reload, collaboration mode, revision controls, scheduler, settings group - `notebook-keyboard-shortcuts` — full ShortcutsMap coverage (Monaco editor; serial because Monaco holds focus state between tests — isolating via `beforeEach` wasn't viable) - `sidebar-functionality` — TOC panel, file tree panel, open/close state transitions - `paragraph-functionality` — edit mode, run/cancel, dynamic forms, footer DOM presence **Share features** - `folder-rename` — hover context menu, rename modal, validation, delete confirmation, folder merge on name collision - `note-rename` — inline title editing, enter/blur/escape flows, empty name rejection, special characters - `note-toc` — panel open/close, empty state message, toggle button attributes, repeated toggle #### Pulled in test failure fixes from #5180 - Cleaned up `about-zeppelin-modal` and `note-create-modal` specs and models - Added missing aria attributes and `data-testid` selectors to `action-bar.component.html` - Bumped `flatted` 3.3.3 → 3.4.1 (npm audit) ### What type of PR is it? Improvement Feature Documentation ### Todos ### What is the Jira issue? ZEPPELIN-6358 ### How should this be tested? ### Screenshots (if appropriate) ### Questions: * Does the license files need to update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Closes #5181 from dididy/e2e/notebook-final. Signed-off-by: Jongyoul Lee <jongyoul@gmail.com>
1 parent 076676a commit 5e9847b

24 files changed

Lines changed: 3452 additions & 182 deletions
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
* http://www.apache.org/licenses/LICENSE-2.0
6+
* Unless required by applicable law or agreed to in writing, software
7+
* distributed under the License is distributed on an "AS IS" BASIS,
8+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9+
* See the License for the specific language governing permissions and
10+
* limitations under the License.
11+
*/
12+
13+
import { expect, Locator, Page } from '@playwright/test';
14+
import { BasePage } from './base-page';
15+
16+
export class FolderRenamePage extends BasePage {
17+
readonly folderList: Locator;
18+
readonly renameModal: Locator;
19+
readonly renameInput: Locator;
20+
readonly confirmButton: Locator;
21+
readonly cancelButton: Locator;
22+
readonly deleteConfirmation: Locator;
23+
24+
constructor(page: Page) {
25+
super(page);
26+
this.folderList = page.locator('zeppelin-node-list');
27+
this.renameModal = page.locator('.ant-modal');
28+
this.renameInput = page.locator('input[placeholder="Insert New Name"]');
29+
this.confirmButton = page.getByRole('button', { name: 'Rename' });
30+
this.cancelButton = page.locator('.ant-modal-close-x'); // Modal close button
31+
this.deleteConfirmation = page.locator('.ant-popover').filter({ hasText: 'This folder will be moved to trash.' });
32+
}
33+
34+
private getFolderNode(folderName: string): Locator {
35+
return this.page
36+
.locator('.folder')
37+
.filter({
38+
has: this.page.locator('a.name', {
39+
hasText: new RegExp(`^\\s*${folderName}\\s*$`, 'i')
40+
})
41+
})
42+
.first();
43+
}
44+
45+
async hoverOverFolder(folderName: string): Promise<void> {
46+
await this.page.waitForSelector('zeppelin-node-list', { state: 'visible' });
47+
const folderNode = this.getFolderNode(folderName);
48+
// Hover a.name (not .folder) — CSS :hover on .operation is triggered by the text link, same as clickRenameMenuItem()
49+
const nameLink = folderNode.locator('a.name');
50+
await nameLink.scrollIntoViewIfNeeded();
51+
await nameLink.hover({ force: true }); // JUSTIFIED: .operation buttons are CSS-:hover-revealed; force required to trigger the hover event on the text link that activates the context menu
52+
}
53+
54+
async clickDeleteIcon(folderName: string): Promise<void> {
55+
await this.hoverOverFolder(folderName);
56+
const folderNode = this.getFolderNode(folderName);
57+
const deleteIcon = folderNode.locator(
58+
'.operation a[nztooltiptitle*="Move folder to Trash"], .operation a[nztooltiptitle*="Trash"]'
59+
);
60+
await expect(deleteIcon).toBeVisible({ timeout: 5000 });
61+
await deleteIcon.click({ force: true }); // JUSTIFIED: icon is only actionable after CSS-:hover; force bypasses the actionability check that fails before hover state propagates
62+
}
63+
64+
async clickRenameMenuItem(folderName: string): Promise<void> {
65+
const folderNode = this.getFolderNode(folderName);
66+
const nameLink = folderNode.locator('a.name');
67+
68+
await nameLink.scrollIntoViewIfNeeded();
69+
await nameLink.hover({ force: true }); // JUSTIFIED: .operation buttons are CSS-:hover-revealed; force required to trigger the hover event on the text link that activates the context menu
70+
71+
const renameIcon = folderNode.locator('.operation a[nztooltiptitle="Rename folder"]');
72+
73+
await expect(renameIcon).toBeVisible({ timeout: 3000 });
74+
await renameIcon.click({ force: true }); // JUSTIFIED: icon is only actionable after CSS-:hover; force bypasses the actionability check that fails before hover state propagates
75+
76+
await this.renameModal.waitFor({ state: 'visible', timeout: 3000 });
77+
}
78+
79+
async enterNewName(name: string): Promise<void> {
80+
await this.renameInput.fill(name);
81+
}
82+
83+
async clearNewName(): Promise<void> {
84+
await this.renameInput.clear();
85+
await expect(this.renameInput).toHaveValue('');
86+
}
87+
88+
async clickConfirm(): Promise<void> {
89+
// Wait for button to be enabled before clicking
90+
await expect(this.confirmButton).toBeEnabled({ timeout: 5000 });
91+
await this.confirmButton.click();
92+
93+
// Wait for modal to close; if it stays open validation errors prevented submission (caller re-checks)
94+
await this.renameModal.waitFor({ state: 'detached', timeout: 3000 }).catch(() => {}); // JUSTIFIED: modal stays open on validation failure; caller asserts final state
95+
}
96+
97+
async clickCancel(): Promise<void> {
98+
await this.cancelButton.click();
99+
}
100+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
* http://www.apache.org/licenses/LICENSE-2.0
6+
* Unless required by applicable law or agreed to in writing, software
7+
* distributed under the License is distributed on an "AS IS" BASIS,
8+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9+
* See the License for the specific language governing permissions and
10+
* limitations under the License.
11+
*/
12+
13+
import { expect } from '@playwright/test';
14+
import { FolderRenamePage } from './folder-rename-page';
15+
16+
export class FolderRenamePageUtil {
17+
private folderRenamePage: FolderRenamePage;
18+
19+
constructor(folderRenamePage: FolderRenamePage) {
20+
this.folderRenamePage = folderRenamePage;
21+
}
22+
23+
async openContextMenuOnHoverAndVerifyOptions(folderName: string): Promise<void> {
24+
await this.folderRenamePage.hoverOverFolder(folderName);
25+
const folderNode = this.getFolderNode(folderName);
26+
await expect(folderNode.locator('.folder .operation a[nz-tooltip][nztooltiptitle="Rename folder"]')).toHaveCount(1);
27+
await expect(folderNode.locator('.folder .operation a[nztooltiptitle*="Move folder to Trash"]')).toBeVisible();
28+
}
29+
30+
private getFolderNode(folderName: string) {
31+
return this.folderRenamePage.page
32+
.locator('.node')
33+
.filter({
34+
has: this.folderRenamePage.page.locator('.folder .name', { hasText: folderName })
35+
})
36+
.first();
37+
}
38+
}

zeppelin-web-angular/e2e/models/header-page.util.ts

Lines changed: 0 additions & 109 deletions
This file was deleted.

zeppelin-web-angular/e2e/models/note-create-modal.util.ts

Lines changed: 0 additions & 40 deletions
This file was deleted.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
* http://www.apache.org/licenses/LICENSE-2.0
6+
* Unless required by applicable law or agreed to in writing, software
7+
* distributed under the License is distributed on an "AS IS" BASIS,
8+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9+
* See the License for the specific language governing permissions and
10+
* limitations under the License.
11+
*/
12+
13+
import { Locator, Page } from '@playwright/test';
14+
import { BasePage } from './base-page';
15+
16+
export class NoteRenamePage extends BasePage {
17+
readonly noteTitle: Locator;
18+
readonly noteTitleInput: Locator;
19+
20+
constructor(page: Page) {
21+
super(page);
22+
// Note title in elastic input component inside the heading element
23+
this.noteTitle = page.getByRole('heading').locator('p');
24+
this.noteTitleInput = page.getByRole('heading').locator('input');
25+
}
26+
27+
async clickTitle(): Promise<void> {
28+
await this.noteTitle.click({ timeout: 15000 });
29+
await this.noteTitleInput.waitFor({ state: 'visible', timeout: 5000 });
30+
}
31+
32+
async enterTitle(title: string): Promise<void> {
33+
await this.ensureEditMode();
34+
await this.noteTitleInput.fill(title, { timeout: 15000 });
35+
}
36+
37+
async clearTitle(): Promise<void> {
38+
await this.ensureEditMode();
39+
await this.noteTitleInput.clear();
40+
}
41+
42+
async pressEnter(): Promise<void> {
43+
await this.noteTitleInput.waitFor({ state: 'visible', timeout: 5000 });
44+
await this.noteTitleInput.press('Enter');
45+
await this.noteTitleInput.waitFor({ state: 'hidden', timeout: 5000 });
46+
}
47+
48+
async pressEscape(): Promise<void> {
49+
await this.noteTitleInput.waitFor({ state: 'visible', timeout: 5000 });
50+
await this.noteTitleInput.press('Escape');
51+
await this.noteTitleInput.waitFor({ state: 'hidden', timeout: 5000 });
52+
}
53+
54+
async blur(): Promise<void> {
55+
await this.noteTitleInput.blur();
56+
}
57+
58+
async getTitle(): Promise<string> {
59+
return this.getElementText(this.noteTitle);
60+
}
61+
62+
private async ensureEditMode(): Promise<void> {
63+
if (!(await this.noteTitleInput.isVisible())) {
64+
await this.clickTitle();
65+
}
66+
await this.noteTitleInput.waitFor({ state: 'visible' });
67+
}
68+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
* http://www.apache.org/licenses/LICENSE-2.0
6+
* Unless required by applicable law or agreed to in writing, software
7+
* distributed under the License is distributed on an "AS IS" BASIS,
8+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9+
* See the License for the specific language governing permissions and
10+
* limitations under the License.
11+
*/
12+
13+
import { expect } from '@playwright/test';
14+
import { NoteRenamePage } from './note-rename-page';
15+
16+
export class NoteRenamePageUtil {
17+
private noteRenamePage: NoteRenamePage;
18+
19+
constructor(noteRenamePage: NoteRenamePage) {
20+
this.noteRenamePage = noteRenamePage;
21+
}
22+
23+
async verifyTitleText(expectedTitle: string): Promise<void> {
24+
await expect(this.noteRenamePage.noteTitle).toContainText(expectedTitle);
25+
}
26+
27+
async verifyTitleCanBeChanged(newTitle: string): Promise<void> {
28+
await this.noteRenamePage.clickTitle();
29+
await this.noteRenamePage.clearTitle();
30+
await this.noteRenamePage.enterTitle(newTitle);
31+
await this.noteRenamePage.pressEnter();
32+
await this.verifyTitleText(newTitle);
33+
}
34+
}

0 commit comments

Comments
 (0)