| /* |
| * 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 { Locator, Page } from '@playwright/test'; |
| import { BasePage } from './base-page'; |
| |
| export class HeaderPage extends BasePage { |
| readonly header: Locator; |
| readonly brandLogo: Locator; |
| readonly brandLink: Locator; |
| readonly notebookMenuItem: Locator; |
| readonly notebookDropdownTrigger: Locator; |
| readonly notebookDropdown: Locator; |
| readonly jobMenuItem: Locator; |
| readonly userDropdownTrigger: Locator; |
| readonly userBadge: Locator; |
| readonly searchInput: Locator; |
| readonly themeToggleButton: Locator; |
| |
| readonly userMenuItems: { |
| aboutZeppelin: Locator; |
| interpreter: Locator; |
| notebookRepos: Locator; |
| credential: Locator; |
| configuration: Locator; |
| logout: Locator; |
| switchToClassicUI: Locator; |
| }; |
| |
| constructor(page: Page) { |
| super(page); |
| this.header = page.locator('.header'); |
| this.brandLogo = page.locator('.header .brand .logo'); |
| this.brandLink = page.locator('.header .brand'); |
| this.notebookMenuItem = page.locator('[nz-menu-item]').filter({ hasText: 'Notebook' }); |
| this.notebookDropdownTrigger = page.locator('.node-list-trigger'); |
| this.notebookDropdown = page.locator('zeppelin-node-list.ant-dropdown-menu'); |
| // A global getByRole('link', { name: 'Job' }) matches by substring. |
| // It also picks up note-list links (e.g. a note named "...Job..."), which is a strict-mode violation. |
| // Scoping to the menu item and matching exactly selects the single header entry. |
| this.jobMenuItem = page.locator('[nz-menu-item]').getByRole('link', { name: 'Job', exact: true }); |
| this.userDropdownTrigger = page.locator('.header .user .status'); |
| this.userBadge = page.locator('.header .user nz-badge'); |
| this.searchInput = page.locator('.header .search input[type="text"]'); |
| this.themeToggleButton = page.locator('zeppelin-theme-toggle button'); |
| |
| // Same substring and strict-mode risk as jobMenuItem above. |
| // Scope these links to the dropdown overlay and match exactly. |
| const userMenu = page.locator('.zeppelin-user-menu'); |
| this.userMenuItems = { |
| aboutZeppelin: userMenu.getByText('About Zeppelin', { exact: true }), |
| interpreter: userMenu.getByRole('link', { name: 'Interpreter', exact: true }), |
| notebookRepos: userMenu.getByRole('link', { name: 'Notebook Repos', exact: true }), |
| credential: userMenu.getByRole('link', { name: 'Credential', exact: true }), |
| configuration: userMenu.getByRole('link', { name: 'Configuration', exact: true }), |
| logout: userMenu.getByText('Logout', { exact: true }), |
| switchToClassicUI: userMenu.getByRole('link', { name: 'Switch to Classic UI', exact: true }) |
| }; |
| } |
| |
| async clickBrandLogo(): Promise<void> { |
| await this.brandLink.waitFor({ state: 'visible', timeout: 10000 }); |
| await this.brandLink.click(); |
| } |
| |
| async clickNotebookMenu(): Promise<void> { |
| await this.notebookDropdownTrigger.waitFor({ state: 'visible', timeout: 10000 }); |
| await this.notebookDropdownTrigger.click(); |
| } |
| |
| async clickJobMenu(): Promise<void> { |
| await this.jobMenuItem.waitFor({ state: 'visible', timeout: 10000 }); |
| await this.jobMenuItem.click(); |
| } |
| |
| async clickUserDropdown(): Promise<void> { |
| await this.userDropdownTrigger.waitFor({ state: 'visible', timeout: 10000 }); |
| await this.userDropdownTrigger.click(); |
| } |
| |
| async clickAboutZeppelin(): Promise<void> { |
| await this.userMenuItems.aboutZeppelin.click(); |
| } |
| |
| async clickInterpreter(): Promise<void> { |
| await this.userMenuItems.interpreter.click(); |
| } |
| |
| async clickNotebookRepos(): Promise<void> { |
| await this.userMenuItems.notebookRepos.click(); |
| } |
| |
| async clickCredential(): Promise<void> { |
| await this.userMenuItems.credential.click(); |
| } |
| |
| async clickConfiguration(): Promise<void> { |
| await this.userMenuItems.configuration.click(); |
| } |
| |
| /** The <li nz-menu-item> row for a user-menu entry, scoped to this dropdown's overlay. */ |
| getUserMenuItemRow(label: string): Locator { |
| return this.page.locator('.zeppelin-user-menu .ant-dropdown-menu-item').filter({ hasText: label }); |
| } |
| |
| /** |
| * Click the empty padding area near the right edge of a user-menu row, away from the |
| * link text. This is the dead zone that only navigates when the inner <a> fills the |
| * whole row; clicking here regresses to "dropdown closes, no navigation" if the |
| * full-row click fix is missing. |
| */ |
| async clickUserMenuItemEdge(label: string): Promise<void> { |
| const row = this.getUserMenuItemRow(label); |
| await row.waitFor({ state: 'visible', timeout: 10000 }); |
| const box = await row.boundingBox(); |
| if (!box) { |
| throw new Error(`User menu item "${label}" has no bounding box`); |
| } |
| await row.click({ position: { x: box.width - 8, y: box.height / 2 } }); |
| } |
| |
| async getUsernameText(): Promise<string> { |
| return (await this.userBadge.textContent()) || ''; |
| } |
| |
| async searchNote(query: string): Promise<void> { |
| await this.searchInput.waitFor({ state: 'visible', timeout: 10000 }); |
| await this.searchInput.fill(query); |
| await this.page.keyboard.press('Enter'); |
| } |
| } |