* Navigate through the items in the selector. `open` is a function that will * open the menu/popup containing the items through which to navigation.
(items: string[], selector: string, open?: (selector: string) => void)
| 448 | * open the menu/popup containing the items through which to navigation. |
| 449 | */ |
| 450 | async navigateItems(items: string[], selector: string, open?: (selector: string) => void): Promise<void> { |
| 451 | const logger = this.codeServer.logger.named(selector) |
| 452 | |
| 453 | /** |
| 454 | * If the selector loses focus or gets removed this will resolve with false, |
| 455 | * signaling we need to try again. |
| 456 | */ |
| 457 | const openThenWaitClose = async (ctx: Context) => { |
| 458 | if (open) { |
| 459 | await open(selector) |
| 460 | } |
| 461 | this.codeServer.logger.debug(`watching ${selector}`) |
| 462 | try { |
| 463 | await this.page.waitForSelector(`${selector}:not(:focus-within)`) |
| 464 | } catch (error) { |
| 465 | if (!ctx.finished()) { |
| 466 | this.codeServer.logger.debug(`${selector} navigation: ${(error as any).message || error}`) |
| 467 | } |
| 468 | } |
| 469 | return false |
| 470 | } |
| 471 | |
| 472 | /** |
| 473 | * This will step through each item, aborting and returning false if |
| 474 | * canceled or if any navigation step has an error which signals we need to |
| 475 | * try again. |
| 476 | */ |
| 477 | const navigate = async (ctx: Context) => { |
| 478 | const steps: Array<{ fn: () => Promise<unknown>; name: string }> = [ |
| 479 | { |
| 480 | fn: () => this.page.waitForSelector(`${selector}:focus-within`), |
| 481 | name: "focus", |
| 482 | }, |
| 483 | ] |
| 484 | |
| 485 | for (const item of items) { |
| 486 | // Normally these will wait for the item to be visible and then execute |
| 487 | // the action. The problem is that if the menu closes these will still |
| 488 | // be waiting and continue to execute once the menu is visible again, |
| 489 | // potentially conflicting with the new set of navigations (for example |
| 490 | // if the old promise clicks logout before the new one can). By |
| 491 | // splitting them into two steps each we can cancel before running the |
| 492 | // action. |
| 493 | steps.push({ |
| 494 | fn: () => this.page.hover(`${selector} :text-is("${item}")`, { trial: true }), |
| 495 | name: `${item}:hover:trial`, |
| 496 | }) |
| 497 | steps.push({ |
| 498 | fn: () => this.page.hover(`${selector} :text-is("${item}")`, { force: true }), |
| 499 | name: `${item}:hover:force`, |
| 500 | }) |
| 501 | steps.push({ |
| 502 | fn: () => this.page.click(`${selector} :text-is("${item}")`, { trial: true }), |
| 503 | name: `${item}:click:trial`, |
| 504 | }) |
| 505 | steps.push({ |
| 506 | fn: () => this.page.click(`${selector} :text-is("${item}")`, { force: true }), |
| 507 | name: `${item}:click:force`, |
no test coverage detected