Skip to content

Commit cf991af

Browse files
committed
Add helper for navigating the quick picker
This has problems similar to the menu except instead of closing it gets re-created which interrupts the hover call and causes the test to fail. Now it will keep trying just like the menu.
1 parent 94f378c commit cf991af

File tree

1 file changed

+87
-34
lines changed

1 file changed

+87
-34
lines changed

test/e2e/models/CodeServer.ts

Lines changed: 87 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as cp from "child_process"
33
import { promises as fs } from "fs"
44
import * as path from "path"
55
import { Page } from "playwright"
6-
import { logError } from "../../../src/common/util"
6+
import { logError, plural } from "../../../src/common/util"
77
import { onLine } from "../../../src/node/util"
88
import { PASSWORD, workspaceDir } from "../../utils/constants"
99
import { idleTimer, tmpdir } from "../../utils/helpers"
@@ -13,14 +13,21 @@ interface CodeServerProcess {
1313
address: string
1414
}
1515

16-
class CancelToken {
16+
class Context {
1717
private _canceled = false
18+
private _done = false
1819
public canceled(): boolean {
1920
return this._canceled
2021
}
22+
public done(): void {
23+
this._done = true
24+
}
2125
public cancel(): void {
2226
this._canceled = true
2327
}
28+
public finish(): boolean {
29+
return this._done
30+
}
2431
}
2532

2633
/**
@@ -287,57 +294,103 @@ export class CodeServerPage {
287294
}
288295

289296
/**
290-
* Navigate through the specified set of menus. If it fails it will keep
291-
* trying.
297+
* Navigate through the items in the selector. `open` is a function that will
298+
* open the menu/popup containing the items through which to navigation.
292299
*/
293-
async navigateMenus(menus: string[]) {
294-
const navigate = async (cancelToken: CancelToken) => {
295-
const steps: Array<() => Promise<unknown>> = [() => this.page.waitForSelector(`${menuSelector}:focus-within`)]
296-
for (const menu of menus) {
300+
async navigateItems(items: string[], selector: string, open?: (selector: string) => void): Promise<void> {
301+
const logger = this.codeServer.logger.named(selector)
302+
303+
/**
304+
* If the selector loses focus or gets removed this will resolve with false,
305+
* signaling we need to try again.
306+
*/
307+
const openThenWaitClose = async (ctx: Context) => {
308+
if (open) {
309+
await open(selector)
310+
}
311+
this.codeServer.logger.debug(`watching ${selector}`)
312+
try {
313+
await this.page.waitForSelector(`${selector}:not(:focus-within)`)
314+
} catch (error) {
315+
if (!ctx.done()) {
316+
this.codeServer.logger.debug(`${selector} navigation: ${error.message || error}`)
317+
}
318+
}
319+
return false
320+
}
321+
322+
/**
323+
* This will step through each item, aborting and returning false if
324+
* canceled or if any navigation step has an error which signals we need to
325+
* try again.
326+
*/
327+
const navigate = async (ctx: Context) => {
328+
const steps: Array<{fn: () => Promise<unknown>, name: string}> = [{
329+
fn: () => this.page.waitForSelector(`${selector}:focus-within`),
330+
name: "focus",
331+
}]
332+
333+
for (const item of items) {
297334
// Normally these will wait for the item to be visible and then execute
298335
// the action. The problem is that if the menu closes these will still
299336
// be waiting and continue to execute once the menu is visible again,
300337
// potentially conflicting with the new set of navigations (for example
301338
// if the old promise clicks logout before the new one can). By
302339
// splitting them into two steps each we can cancel before running the
303340
// action.
304-
steps.push(() => this.page.hover(`text=${menu}`, { trial: true }))
305-
steps.push(() => this.page.hover(`text=${menu}`, { force: true }))
306-
steps.push(() => this.page.click(`text=${menu}`, { trial: true }))
307-
steps.push(() => this.page.click(`text=${menu}`, { force: true }))
341+
steps.push({fn: () => this.page.hover(`${selector} :text("${item}")`, { trial: true }), name: `${item}:hover:trial`})
342+
steps.push({fn: () => this.page.hover(`${selector} :text("${item}")`, { force: true }), name: `${item}:hover:force`})
343+
steps.push({fn: () => this.page.click(`${selector} :text("${item}")`, { trial: true }), name: `${item}:click:trial`})
344+
steps.push({fn: () => this.page.click(`${selector} :text("${item}")`, { force: true }), name: `${item}:click:force`})
308345
}
346+
309347
for (const step of steps) {
310-
await step()
311-
if (cancelToken.canceled()) {
312-
this.codeServer.logger.debug("menu navigation canceled")
348+
try {
349+
logger.debug(`navigation step: ${step.name}`)
350+
await step.fn()
351+
if (ctx.canceled()) {
352+
logger.debug("navigation canceled")
353+
return false
354+
}
355+
} catch (error) {
356+
logger.debug(`navigation: ${error.message || error}`)
313357
return false
314358
}
315359
}
316360
return true
317361
}
318362

319-
const menuSelector = '[aria-label="Application Menu"]'
320-
const open = async () => {
321-
await this.page.click(menuSelector)
322-
await this.page.waitForSelector(`${menuSelector}:not(:focus-within)`)
323-
return false
363+
// We are seeing the menu closing after opening if we open it too soon and
364+
// the picker getting recreated in the middle of trying to select an item.
365+
// To counter this we will keep trying to navigate through the items every
366+
// time we lose focus or there is an error.
367+
let attempts = 1
368+
let context = new Context()
369+
while (!(await Promise.race([openThenWaitClose(), navigate(context)]))) {
370+
++attempts
371+
logger.debug("closed, retrying (${attempt}/∞)")
372+
context.cancel()
373+
context = new Context()
324374
}
325375

326-
// TODO: Starting in 1.57 something closes the menu after opening it if we
327-
// open it too soon. To counter that we'll watch for when the menu loses
328-
// focus and when/if it does we'll try again.
329-
// I tried using the classic menu but it doesn't show up at all for some
330-
// reason. I also tried toggle but the menu disappears after toggling.
331-
let retryCount = 0
332-
let cancelToken = new CancelToken()
333-
while (!(await Promise.race([open(), navigate(cancelToken)]))) {
334-
this.codeServer.logger.debug("menu was closed, retrying")
335-
++retryCount
336-
cancelToken.cancel()
337-
cancelToken = new CancelToken()
338-
}
376+
context.finish()
377+
logger.debug(`navigation took ${attempts} ${plural(attempts, "attempt")}`)
378+
}
339379

340-
this.codeServer.logger.debug(`menu navigation retries: ${retryCount}`)
380+
/**
381+
* Navigate through a currently opened picker, retrying on failure.
382+
*/
383+
async navigatePicker(items: string[]): Promise<void> {
384+
await this.navigateItems(items, ".quick-input-widget")
385+
}
386+
387+
/**
388+
* Navigate through the menu, retrying on failure.
389+
*/
390+
async navigateMenus(menus: string[]): Promise<void> {
391+
await this.navigateItems(menus, '[aria-label="Application Menu"]', async (selector) => {
392+
await this.page.click(selector)
393+
})
341394
}
342395

343396
/**

0 commit comments

Comments
 (0)