|
| 1 | +import { expect, Locator, Page } from '@playwright/test'; |
| 2 | +import { BasePage } from './base/BasePage'; |
| 3 | + |
| 4 | +type DetailsTab = 'summary' | 'edit' | 'logs' | 'exec'; |
| 5 | + |
| 6 | +export class WecsPage extends BasePage { |
| 7 | + readonly pageTitle: Locator; |
| 8 | + readonly noteBanner: Locator; |
| 9 | + readonly createWorkloadButton: Locator; |
| 10 | + readonly createOptionsDialog: Locator; |
| 11 | + readonly createOptionsTabs: Locator; |
| 12 | + readonly createOptionsCancelButton: Locator; |
| 13 | + readonly tilesViewButton: Locator; |
| 14 | + readonly listViewButton: Locator; |
| 15 | + readonly viewSkeleton: Locator; |
| 16 | + readonly listViewSkeleton: Locator; |
| 17 | + readonly reactFlowCanvas: Locator; |
| 18 | + readonly reactFlowNodes: Locator; |
| 19 | + readonly listViewContainer: Locator; |
| 20 | + readonly listViewItems: Locator; |
| 21 | + readonly listViewTableRows: Locator; |
| 22 | + readonly listViewSearchInput: Locator; |
| 23 | + readonly filterKindButton: Locator; |
| 24 | + readonly filterNamespaceButton: Locator; |
| 25 | + readonly filterLabelButton: Locator; |
| 26 | + readonly filterClearButton: Locator; |
| 27 | + readonly zoomControlsContainer: Locator; |
| 28 | + readonly zoomHideControlsButton: Locator; |
| 29 | + readonly zoomExpandAllButton: Locator; |
| 30 | + readonly zoomCollapseAllButton: Locator; |
| 31 | + readonly zoomFullscreenButton: Locator; |
| 32 | + readonly zoomEdgeSquareButton: Locator; |
| 33 | + readonly zoomEdgeCurvyButton: Locator; |
| 34 | + readonly contextMenu: Locator; |
| 35 | + readonly detailsPanel: Locator; |
| 36 | + readonly detailsPanelCloseButton: Locator; |
| 37 | + readonly summaryTab: Locator; |
| 38 | + readonly editTab: Locator; |
| 39 | + readonly logsTab: Locator; |
| 40 | + readonly execTab: Locator; |
| 41 | + readonly manifestEditor: Locator; |
| 42 | + readonly manifestFormatYamlButton: Locator; |
| 43 | + readonly manifestFormatJsonButton: Locator; |
| 44 | + readonly logsContainerDropdown: Locator; |
| 45 | + readonly logsPreviousButton: Locator; |
| 46 | + readonly logsDownloadButton: Locator; |
| 47 | + readonly logsTerminal: Locator; |
| 48 | + readonly execContainerDropdown: Locator; |
| 49 | + readonly execClearButton: Locator; |
| 50 | + readonly execMaximizeButton: Locator; |
| 51 | + readonly execTerminal: Locator; |
| 52 | + readonly snackbar: Locator; |
| 53 | + |
| 54 | + constructor(page: Page) { |
| 55 | + super(page); |
| 56 | + |
| 57 | + this.pageTitle = page.getByRole('heading', { name: /remote-?cluster treeview/i }).first(); |
| 58 | + this.noteBanner = page |
| 59 | + .locator( |
| 60 | + 'text=Note: Default, Kubernetes system, and OpenShift namespaces are filtered out from this view.' |
| 61 | + ) |
| 62 | + .first(); |
| 63 | + this.createWorkloadButton = page.getByRole('button', { name: /create workload/i }).first(); |
| 64 | + this.createOptionsDialog = page.getByRole('dialog', { name: /create/i }).first(); |
| 65 | + this.createOptionsTabs = this.createOptionsDialog.getByRole('tablist'); |
| 66 | + this.createOptionsCancelButton = page.getByRole('button', { name: /cancel|close/i }).first(); |
| 67 | + |
| 68 | + this.tilesViewButton = page |
| 69 | + .locator('button') |
| 70 | + .filter({ has: page.locator('i.fa-th, i.fa-solid.fa-th') }) |
| 71 | + .first(); |
| 72 | + this.listViewButton = page |
| 73 | + .locator('button') |
| 74 | + .filter({ has: page.locator('i.fa-th-list, i.fa-solid.fa-th-list') }) |
| 75 | + .first(); |
| 76 | + |
| 77 | + this.viewSkeleton = page |
| 78 | + .locator('[class*="UnifiedSkeleton"], [class*="unified-skeleton"]') |
| 79 | + .first(); |
| 80 | + this.listViewSkeleton = page.locator('[class*="ListViewSkeleton"]').first(); |
| 81 | + |
| 82 | + this.reactFlowCanvas = page.locator('.react-flow, [class*="react-flow"]').first(); |
| 83 | + this.reactFlowNodes = this.reactFlowCanvas.locator('.react-flow__node, [data-id]'); |
| 84 | + |
| 85 | + this.listViewContainer = page |
| 86 | + .locator('[class*="ListViewComponent"], [data-testid="list-view"]') |
| 87 | + .first(); |
| 88 | + this.listViewItems = this.listViewContainer.locator('[class*="list-item"], [role="row"]'); |
| 89 | + this.listViewTableRows = page.locator('table tbody tr'); |
| 90 | + this.listViewSearchInput = page.getByRole('textbox', { name: /quick search objects/i }).first(); |
| 91 | + this.filterKindButton = page.getByRole('button', { name: /kind/i }).first(); |
| 92 | + this.filterNamespaceButton = page.getByRole('button', { name: /namespace/i }).first(); |
| 93 | + this.filterLabelButton = page.getByRole('button', { name: /label/i }).first(); |
| 94 | + this.filterClearButton = page.getByRole('button', { name: /clear filters|reset/i }).first(); |
| 95 | + |
| 96 | + this.zoomControlsContainer = page.locator('text=/hide controls|show controls/i').first(); |
| 97 | + this.zoomHideControlsButton = page |
| 98 | + .getByRole('button', { name: /hide controls|show controls/i }) |
| 99 | + .first(); |
| 100 | + this.zoomExpandAllButton = page.getByRole('button', { name: /expand all/i }).first(); |
| 101 | + this.zoomCollapseAllButton = page.getByRole('button', { name: /collapse all/i }).first(); |
| 102 | + this.zoomFullscreenButton = page |
| 103 | + .getByRole('button', { name: /fullscreen|exit fullscreen/i }) |
| 104 | + .first(); |
| 105 | + this.zoomEdgeSquareButton = page.getByRole('button', { name: /square/i }).first(); |
| 106 | + this.zoomEdgeCurvyButton = page.getByRole('button', { name: /curvy/i }).first(); |
| 107 | + |
| 108 | + this.contextMenu = page.locator('[role="menu"]').first(); |
| 109 | + |
| 110 | + this.detailsPanel = page.locator('[data-testid="wecs-details-panel"]').first(); |
| 111 | + this.detailsPanelCloseButton = this.detailsPanel |
| 112 | + .getByRole('button', { name: /close/i }) |
| 113 | + .first(); |
| 114 | + this.summaryTab = this.detailsPanel.getByRole('tab', { name: /summary/i }).first(); |
| 115 | + this.editTab = this.detailsPanel.getByRole('tab', { name: /edit/i }).first(); |
| 116 | + this.logsTab = this.detailsPanel.getByRole('tab', { name: /logs/i }).first(); |
| 117 | + this.execTab = this.detailsPanel.getByRole('tab', { name: /exec/i }).first(); |
| 118 | + this.manifestEditor = this.detailsPanel.locator('.monaco-editor, textarea, pre').first(); |
| 119 | + this.manifestFormatYamlButton = this.detailsPanel |
| 120 | + .getByRole('button', { name: /yaml/i }) |
| 121 | + .first(); |
| 122 | + this.manifestFormatJsonButton = this.detailsPanel |
| 123 | + .getByRole('button', { name: /json/i }) |
| 124 | + .first(); |
| 125 | + |
| 126 | + this.logsContainerDropdown = this.detailsPanel.locator('.logs-container-dropdown').first(); |
| 127 | + this.logsPreviousButton = this.detailsPanel |
| 128 | + .getByRole('button', { name: /previous logs/i }) |
| 129 | + .first(); |
| 130 | + this.logsDownloadButton = this.detailsPanel |
| 131 | + .locator('button[title*="Download logs"], button[title*="download"]') |
| 132 | + .first(); |
| 133 | + this.logsTerminal = this.detailsPanel |
| 134 | + .locator('.xterm, .terminal, [data-testid="logs-terminal"]') |
| 135 | + .first(); |
| 136 | + |
| 137 | + this.execContainerDropdown = this.detailsPanel.locator('.container-dropdown').first(); |
| 138 | + this.execClearButton = this.detailsPanel |
| 139 | + .getByRole('button', { name: /clear terminal/i }) |
| 140 | + .first(); |
| 141 | + this.execMaximizeButton = this.detailsPanel |
| 142 | + .getByRole('button', { name: /maximize|minimize/i }) |
| 143 | + .first(); |
| 144 | + this.execTerminal = this.detailsPanel.locator('[data-terminal="exec"], .xterm').first(); |
| 145 | + |
| 146 | + this.snackbar = page.locator('.MuiSnackbar-root, .toast, [role="alert"]').first(); |
| 147 | + } |
| 148 | + |
| 149 | + async goto() { |
| 150 | + await super.goto('/wecs/treeview'); |
| 151 | + await this.waitForInitialLoad(); |
| 152 | + } |
| 153 | + |
| 154 | + async ensureOnWecsPage() { |
| 155 | + const url = this.page.url(); |
| 156 | + if (!/\/wecs\/treeview/.test(url)) { |
| 157 | + await this.page.goto(`${this.BASE_URL}/wecs/treeview`, { waitUntil: 'domcontentloaded' }); |
| 158 | + } |
| 159 | + await this.waitForInitialLoad(); |
| 160 | + } |
| 161 | + |
| 162 | + async waitForInitialLoad() { |
| 163 | + await this.page.waitForFunction( |
| 164 | + () => { |
| 165 | + const heading = Array.from(document.querySelectorAll('h1,h2,h3,h4')).some(el => |
| 166 | + /remote-?cluster treeview/i.test(el.textContent || '') |
| 167 | + ); |
| 168 | + if (!heading) return false; |
| 169 | + const hasSkeleton = document.querySelector('[class*="Skeleton"]'); |
| 170 | + const hasFlow = document.querySelector('.react-flow, [class*="react-flow"]'); |
| 171 | + const hasList = document.querySelector('[class*="ListViewComponent"]'); |
| 172 | + const bodyText = document.body?.innerText || ''; |
| 173 | + const hasEmpty = /No Workloads Found/i.test(bodyText); |
| 174 | + return Boolean(hasSkeleton || hasFlow || hasList || hasEmpty); |
| 175 | + }, |
| 176 | + { timeout: 30000 } |
| 177 | + ); |
| 178 | + } |
| 179 | + |
| 180 | + async waitForTilesView() { |
| 181 | + await this.page.waitForFunction( |
| 182 | + () => { |
| 183 | + const canvas = document.querySelector('.react-flow, [class*="react-flow"], canvas'); |
| 184 | + const emptyState = /No Workloads Found/i.test(document.body?.innerText || ''); |
| 185 | + const listView = document.querySelector('[class*="ListViewComponent"]'); |
| 186 | + return Boolean(canvas || emptyState) && !listView; |
| 187 | + }, |
| 188 | + { timeout: 20000 } |
| 189 | + ); |
| 190 | + } |
| 191 | + |
| 192 | + async waitForListView() { |
| 193 | + await this.page.waitForFunction( |
| 194 | + () => { |
| 195 | + const listView = document.querySelector('[class*="ListViewComponent"]'); |
| 196 | + const table = document.querySelector('table'); |
| 197 | + const empty = /No Workloads Found/i.test(document.body?.innerText || ''); |
| 198 | + return Boolean(listView || table || empty); |
| 199 | + }, |
| 200 | + { timeout: 20000 } |
| 201 | + ); |
| 202 | + } |
| 203 | + |
| 204 | + async switchToTilesView() { |
| 205 | + await this.tilesViewButton.waitFor({ state: 'visible', timeout: 10000 }); |
| 206 | + await this.tilesViewButton.click(); |
| 207 | + await this.waitForTilesView(); |
| 208 | + } |
| 209 | + |
| 210 | + async switchToListView() { |
| 211 | + await this.listViewButton.waitFor({ state: 'visible', timeout: 10000 }); |
| 212 | + await this.listViewButton.click(); |
| 213 | + await this.waitForListView(); |
| 214 | + } |
| 215 | + |
| 216 | + async openCreateOptions(option: string = 'yaml') { |
| 217 | + await this.createWorkloadButton.click(); |
| 218 | + await expect(this.createOptionsDialog).toBeVisible({ timeout: 5000 }); |
| 219 | + const tabLabelMap: Record<string, RegExp> = { |
| 220 | + yaml: /yaml/i, |
| 221 | + file: /file/i, |
| 222 | + github: /github/i, |
| 223 | + helm: /helm/i, |
| 224 | + artifactHub: /artifact hub/i, |
| 225 | + }; |
| 226 | + const tab = this.createOptionsDialog |
| 227 | + .getByRole('tab', { name: tabLabelMap[option] || /yaml/i }) |
| 228 | + .first(); |
| 229 | + await tab.click(); |
| 230 | + } |
| 231 | + |
| 232 | + async closeCreateOptions() { |
| 233 | + if (await this.createOptionsDialog.isVisible().catch(() => false)) { |
| 234 | + await this.page.keyboard.press('Escape').catch(() => {}); |
| 235 | + if (await this.createOptionsDialog.isVisible().catch(() => false)) { |
| 236 | + await this.createOptionsCancelButton.click().catch(() => {}); |
| 237 | + } |
| 238 | + await this.createOptionsDialog.waitFor({ state: 'hidden', timeout: 5000 }).catch(() => {}); |
| 239 | + } |
| 240 | + } |
| 241 | + |
| 242 | + async searchListView(query: string) { |
| 243 | + await this.listViewSearchInput.fill(''); |
| 244 | + await this.listViewSearchInput.fill(query); |
| 245 | + await this.page.waitForTimeout(400); |
| 246 | + } |
| 247 | + |
| 248 | + async selectFilter(filter: 'kind' | 'namespace' | 'label', value: string) { |
| 249 | + const target = |
| 250 | + filter === 'kind' |
| 251 | + ? this.filterKindButton |
| 252 | + : filter === 'namespace' |
| 253 | + ? this.filterNamespaceButton |
| 254 | + : this.filterLabelButton; |
| 255 | + await target.click(); |
| 256 | + const option = this.page.getByRole('menuitem', { name: new RegExp(value, 'i') }).first(); |
| 257 | + await option.click(); |
| 258 | + await this.page.waitForTimeout(300); |
| 259 | + } |
| 260 | + |
| 261 | + async clearFilters() { |
| 262 | + await this.filterClearButton.click().catch(() => {}); |
| 263 | + await this.page.waitForTimeout(300); |
| 264 | + } |
| 265 | + |
| 266 | + getNodeLocator(nodeName: string) { |
| 267 | + return this.reactFlowNodes.filter({ hasText: new RegExp(nodeName, 'i') }).first(); |
| 268 | + } |
| 269 | + |
| 270 | + async openNodeMenu(nodeName: string) { |
| 271 | + const node = this.getNodeLocator(nodeName); |
| 272 | + await node.waitFor({ state: 'visible', timeout: 10000 }); |
| 273 | + const menuButton = node.getByRole('button', { name: /more options/i }).first(); |
| 274 | + await menuButton.click(); |
| 275 | + await this.contextMenu.waitFor({ state: 'visible', timeout: 5000 }); |
| 276 | + } |
| 277 | + |
| 278 | + async selectContextMenuAction(action: 'details' | 'edit' | 'logs' | 'exec') { |
| 279 | + const labels: Record<typeof action, RegExp> = { |
| 280 | + details: /details/i, |
| 281 | + edit: /edit/i, |
| 282 | + logs: /logs/i, |
| 283 | + exec: /exec/i, |
| 284 | + }; |
| 285 | + const item = this.page.getByRole('menuitem', { name: labels[action] }).first(); |
| 286 | + await item.click(); |
| 287 | + } |
| 288 | + |
| 289 | + async selectNode(nodeName: string) { |
| 290 | + const node = this.getNodeLocator(nodeName); |
| 291 | + await node.click(); |
| 292 | + await this.page.waitForTimeout(300); |
| 293 | + } |
| 294 | + |
| 295 | + async waitForDetailsPanel() { |
| 296 | + await this.detailsPanel.waitFor({ state: 'visible', timeout: 10000 }); |
| 297 | + } |
| 298 | + |
| 299 | + async closeDetailsPanel() { |
| 300 | + if (await this.detailsPanel.isVisible().catch(() => false)) { |
| 301 | + await this.detailsPanelCloseButton.click().catch(() => {}); |
| 302 | + await this.detailsPanel.waitFor({ state: 'hidden', timeout: 5000 }).catch(() => {}); |
| 303 | + } |
| 304 | + } |
| 305 | + |
| 306 | + async openDetailsTab(tab: DetailsTab) { |
| 307 | + const tabMap: Record<DetailsTab, Locator> = { |
| 308 | + summary: this.summaryTab, |
| 309 | + edit: this.editTab, |
| 310 | + logs: this.logsTab, |
| 311 | + exec: this.execTab, |
| 312 | + }; |
| 313 | + await tabMap[tab].click(); |
| 314 | + await this.page.waitForTimeout(300); |
| 315 | + } |
| 316 | + |
| 317 | + async setManifestFormat(format: 'yaml' | 'json') { |
| 318 | + if (format === 'yaml') { |
| 319 | + await this.manifestFormatYamlButton.click(); |
| 320 | + } else { |
| 321 | + await this.manifestFormatJsonButton.click(); |
| 322 | + } |
| 323 | + await this.page.waitForTimeout(200); |
| 324 | + } |
| 325 | + |
| 326 | + async selectLogsContainer(containerName: string) { |
| 327 | + await this.logsContainerDropdown.click(); |
| 328 | + const option = this.page.getByRole('option', { name: new RegExp(containerName, 'i') }).first(); |
| 329 | + await option.click(); |
| 330 | + await this.page.waitForTimeout(200); |
| 331 | + } |
| 332 | + |
| 333 | + async togglePreviousLogs() { |
| 334 | + await this.logsPreviousButton.click(); |
| 335 | + await this.page.waitForTimeout(300); |
| 336 | + } |
| 337 | + |
| 338 | + async selectExecContainer(containerName: string) { |
| 339 | + await this.execContainerDropdown.click(); |
| 340 | + const option = this.page.getByRole('option', { name: new RegExp(containerName, 'i') }).first(); |
| 341 | + await option.click(); |
| 342 | + await this.page.waitForTimeout(200); |
| 343 | + } |
| 344 | + |
| 345 | + async clearExecTerminal() { |
| 346 | + await this.execClearButton.click(); |
| 347 | + await this.page.waitForTimeout(200); |
| 348 | + } |
| 349 | + |
| 350 | + async toggleExecMaximize() { |
| 351 | + await this.execMaximizeButton.click(); |
| 352 | + await this.page.waitForTimeout(300); |
| 353 | + } |
| 354 | + |
| 355 | + async waitForSnackbar(message?: RegExp) { |
| 356 | + await this.snackbar.waitFor({ state: 'visible', timeout: 10000 }); |
| 357 | + if (message) { |
| 358 | + await expect(this.snackbar).toHaveText(message); |
| 359 | + } |
| 360 | + } |
| 361 | +} |
0 commit comments