Skip to content

Commit

Permalink
Fix spurious gui e2e dropdown test failures (#9697)
Browse files Browse the repository at this point in the history
In GUI E2E `widgets.spec.ts`, be more specific about which tested dropdowns should be visible. Since the tested node actually receives two dropdowns, the test could fail by detecting both and expecting only one. Since we already have a very specific selector for the dropdown we are testing, use it instead of fresh global locator.
  • Loading branch information
Frizi authored Apr 15, 2024
1 parent 59a9c8e commit 9d2d903
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 73 deletions.
4 changes: 1 addition & 3 deletions app/gui2/e2e/locate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,7 @@ export function graphNode(page: Page | Locator): Node {
return page.locator('.GraphNode') as Node
}
export function graphNodeByBinding(page: Locator | Page, binding: string): Node {
return graphNode(page).filter({
has: page.locator('.binding').and(page.getByText(binding, { exact: true })),
}) as Node
return graphNode(page).filter({ has: page.locator('.binding', { hasText: binding }) }) as Node
}
export function graphNodeIcon(node: Node) {
return node.locator('.nodeCategoryIcon')
Expand Down
146 changes: 78 additions & 68 deletions app/gui2/e2e/widgets.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@ class DropDownLocator {
readonly selectedItems: Locator

constructor(ancestor: Locator) {
this.rootWidget = ancestor.locator('.WidgetSelection')
this.dropDown = ancestor.locator('.DropdownWidget')
this.rootWidget = ancestor.locator('.WidgetSelection').first()
const page = ancestor.page()
// There can be only one open dropdown at a time on a page. We have to filter out the ones that
// still have leaving animation running.
this.dropDown = page.locator('.DropdownWidget:not([data-transitioning])')
this.items = this.dropDown.locator('.item')
this.selectedItems = this.dropDown.locator('.item.selected')
}

async expectVisibleWithOptions(options: string[]): Promise<void> {
await this.expectVisible()
const page = this.dropDown.page()
await expect(this.items.first()).toBeVisible()
for (const option of options) {
Expand All @@ -28,16 +32,21 @@ class DropDownLocator {
await expect(this.items).toHaveCount(options.length)
}

async expectVisible(): Promise<void> {
await expect(this.dropDown).toBeVisible()
}

async expectNotVisible(): Promise<void> {
await expect(this.dropDown).not.toBeVisible()
}

async clickOption(option: string): Promise<void> {
const page = this.dropDown.page()
await this.items.filter({ has: page.getByText(option) }).click()
}

async openWithArrow(): Promise<void> {
await this.rootWidget.hover()
await expect(this.rootWidget.locator('.arrow')).toBeVisible()
await this.rootWidget.locator('.arrow').click({ force: true })
await expect(this.dropDown).toBeVisible()
async clickWidget(): Promise<void> {
await this.rootWidget.click()
}
}

Expand Down Expand Up @@ -71,12 +80,13 @@ test('Multi-selection widget', async ({ page }) => {

// Click the argument to open the dropdown.
const node = locate.graphNodeByBinding(page, 'selected')
const argumentNames = node.locator('.WidgetArgumentName')
await expect(argumentNames).toHaveCount(1)
await argumentNames.first().click()
const topLevelArgs = node.locator('.WidgetTopLevelArgument')
await expect(topLevelArgs).toHaveCount(1)
const columnsArg = topLevelArgs.filter({ has: page.getByText('columns') })

// Get the dropdown and corresponding vector; they both have 0 items.
const dropDown = new DropDownLocator(node)
const dropDown = new DropDownLocator(columnsArg)
await dropDown.clickWidget()
await dropDown.expectVisibleWithOptions(['Column A', 'Column B'])
await expect(dropDown.rootWidget).toHaveClass(/multiSelect/)
const vector = node.locator('.WidgetVector')
Expand All @@ -94,7 +104,7 @@ test('Multi-selection widget', async ({ page }) => {

// Add-item button opens dropdown, after closing with escape.
await page.keyboard.press('Escape')
await expect(page.locator('.DropdownWidget')).not.toBeVisible()
await dropDown.expectNotVisible()
await vector.locator('.add-item').click()
await expect(dropDown.items).toHaveCount(2)
await expect(dropDown.selectedItems).toHaveCount(1)
Expand Down Expand Up @@ -141,19 +151,19 @@ test('Selection widgets in Data.read node', async ({ page }) => {

// Check initially visible arguments
const node = await dataReadNodeWithMethodCallInfo(page)
const dropDown = new DropDownLocator(node)
const argumentNames = node.locator('.WidgetArgumentName')
await expect(argumentNames).toHaveCount(1)
const topLevelArgs = node.locator('.WidgetTopLevelArgument')
await expect(topLevelArgs).toHaveCount(1)

// Check arguments after selecting node
await node.click()
await expect(argumentNames).toHaveCount(3)
await expect(topLevelArgs).toHaveCount(3)

// Set value on `on_problems` (static drop-down)
const onProblemsArg = argumentNames.filter({ has: page.getByText('on_problems') })
const onProblemsArg = topLevelArgs.filter({ has: page.getByText('on_problems') })
await onProblemsArg.click()
await dropDown.expectVisibleWithOptions(['Ignore', 'Report_Warning', 'Report_Error'])
await dropDown.clickOption('Report_Error')
const onProblemsDropdown = new DropDownLocator(onProblemsArg)
await onProblemsDropdown.expectVisibleWithOptions(['Ignore', 'Report_Warning', 'Report_Error'])
await onProblemsDropdown.clickOption('Report_Error')
await expect(onProblemsArg.locator('.WidgetToken')).toContainText([
'Problem_Behavior',
'.',
Expand All @@ -170,20 +180,20 @@ test('Selection widgets in Data.read node', async ({ page }) => {
notAppliedArguments: [0, 1],
})
await page.getByText('Report_Error').click()
await dropDown.expectVisibleWithOptions(['Ignore', 'Report_Warning', 'Report_Error'])
await dropDown.clickOption('Report_Warning')
await onProblemsDropdown.expectVisibleWithOptions(['Ignore', 'Report_Warning', 'Report_Error'])
await onProblemsDropdown.clickOption('Report_Warning')
await expect(onProblemsArg.locator('.WidgetToken')).toContainText([
'Problem_Behavior',
'.',
'Report_Warning',
])

// Set value on `path` (dynamic config)
const pathArg = argumentNames.filter({ has: page.getByText('path') })
const pathArg = topLevelArgs.filter({ has: page.getByText('path') })
await pathArg.click()
await expect(page.locator('.DropdownWidget')).toBeVisible()
await dropDown.expectVisibleWithOptions(['Choose file…', 'File 1', 'File 2'])
await dropDown.clickOption('File 2')
const pathDropdown = new DropDownLocator(pathArg)
await pathDropdown.expectVisibleWithOptions(['Choose file…', 'File 1', 'File 2'])
await pathDropdown.clickOption('File 2')
await expect(pathArg.locator('.WidgetText > input')).toHaveValue('File 2')

// Change value on `path` (dynamic config)
Expand All @@ -196,66 +206,66 @@ test('Selection widgets in Data.read node', async ({ page }) => {
notAppliedArguments: [1],
})
await page.getByText('path').click()
await dropDown.expectVisibleWithOptions(['Choose file…', 'File 1', 'File 2'])
await dropDown.clickOption('File 1')
await pathDropdown.expectVisibleWithOptions(['Choose file…', 'File 1', 'File 2'])
await pathDropdown.clickOption('File 1')
await expect(pathArg.locator('.WidgetText > input')).toHaveValue('File 1')
})

test('Selection widget with text widget as input', async ({ page }) => {
await actions.goToGraph(page)

const node = await dataReadNodeWithMethodCallInfo(page)
const dropDown = new DropDownLocator(node)
const argumentNames = node.locator('.WidgetArgumentName')
const pathArg = argumentNames.filter({ has: page.getByText('path') })
const topLevelArgs = node.locator('.WidgetTopLevelArgument')
const pathArg = topLevelArgs.filter({ has: page.getByText('path') })
const pathDropdown = new DropDownLocator(pathArg)
const pathArgInput = pathArg.locator('.WidgetText > input')
await pathArg.click()
await expect(page.locator('.DropdownWidget')).toBeVisible()
await dropDown.clickOption('File 2')
await pathDropdown.expectVisible()
await pathDropdown.clickOption('File 2')
await expect(pathArgInput).toHaveValue('File 2')

// Editing text input shows and filters drop down
await pathArgInput.click()
await dropDown.expectVisibleWithOptions(['Choose file…', 'File 1', 'File 2'])
await pathDropdown.expectVisibleWithOptions(['Choose file…', 'File 1', 'File 2'])
await page.keyboard.insertText('File 1')
await dropDown.expectVisibleWithOptions(['File 1'])
await pathDropdown.expectVisibleWithOptions(['File 1'])
// Clearing input should show all text literal options
await pathArgInput.clear()
await dropDown.expectVisibleWithOptions(['File 1', 'File 2'])
await pathDropdown.expectVisibleWithOptions(['File 1', 'File 2'])

// Esc should cancel editing and close drop down
await page.keyboard.press('Escape')
await expect(pathArgInput).not.toBeFocused()
await expect(pathArgInput).toHaveValue('File 2')
await expect(dropDown.dropDown).not.toBeVisible()
await expect(pathDropdown.dropDown).not.toBeVisible()

// Choosing entry should finish editing
await pathArgInput.click()
await dropDown.expectVisibleWithOptions(['Choose file…', 'File 1', 'File 2'])
await pathDropdown.expectVisibleWithOptions(['Choose file…', 'File 1', 'File 2'])
await page.keyboard.insertText('File')
await dropDown.expectVisibleWithOptions(['File 1', 'File 2'])
await dropDown.clickOption('File 1')
await pathDropdown.expectVisibleWithOptions(['File 1', 'File 2'])
await pathDropdown.clickOption('File 1')
await expect(pathArgInput).not.toBeFocused()
await expect(pathArgInput).toHaveValue('File 1')
await expect(dropDown.dropDown).not.toBeVisible()
await expect(pathDropdown.dropDown).not.toBeVisible()

// Clicking-off and pressing enter should accept text as-is
await pathArgInput.click()
await dropDown.expectVisibleWithOptions(['Choose file…', 'File 1', 'File 2'])
await pathDropdown.expectVisibleWithOptions(['Choose file…', 'File 1', 'File 2'])
await page.keyboard.insertText('File')
await page.keyboard.press('Enter')
await expect(pathArgInput).not.toBeFocused()
await expect(pathArgInput).toHaveValue('File')
await expect(dropDown.dropDown).not.toBeVisible()
await expect(pathDropdown.dropDown).not.toBeVisible()

await pathArgInput.click()
await dropDown.expectVisibleWithOptions(['Choose file…', 'File 1', 'File 2'])
await pathDropdown.expectVisibleWithOptions(['Choose file…', 'File 1', 'File 2'])
await page.keyboard.insertText('Foo')
await expect(pathArgInput).toHaveValue('Foo')
await page.mouse.click(200, 200)
await expect(pathArgInput).not.toBeFocused()
await expect(pathArgInput).toHaveValue('Foo')
await expect(dropDown.dropDown).not.toBeVisible()
await expect(pathDropdown.dropDown).not.toBeVisible()
})

test('File Browser widget', async ({ page }) => {
Expand All @@ -270,14 +280,13 @@ test('File Browser widget', async ({ page }) => {
})
// Wait for arguments to load.
const node = locate.graphNodeByBinding(page, 'data')
const dropDown = new DropDownLocator(node)
const argumentNames = node.locator('.WidgetArgumentName')
await expect(argumentNames).toHaveCount(1)
const pathArg = argumentNames.filter({ has: page.getByText('path') })

const topLevelArgs = node.locator('.WidgetTopLevelArgument')
await expect(topLevelArgs).toHaveCount(1)
const pathArg = topLevelArgs.filter({ has: page.getByText('path') })
const pathDropdown = new DropDownLocator(pathArg)
await pathArg.click()
await dropDown.expectVisibleWithOptions(['Choose file…', 'File 1', 'File 2'])
await dropDown.clickOption('Choose file…')
await pathDropdown.expectVisibleWithOptions(['Choose file…', 'File 1', 'File 2'])
await pathDropdown.clickOption('Choose file…')
await expect(pathArg.locator('.WidgetText > input')).toHaveValue('/path/to/some/mock/file')
})

Expand All @@ -294,16 +303,16 @@ test('Managing aggregates in `aggregate` node', async ({ page }) => {

// Check initially visible arguments
const node = locate.graphNodeByBinding(page, 'aggregated')
const dropDown = new DropDownLocator(node)
const argumentNames = node.locator('.WidgetArgumentName')
await expect(argumentNames).toHaveCount(1)
const topLevelArgs = node.locator('.WidgetTopLevelArgument')
await expect(topLevelArgs).toHaveCount(1)

// Check arguments after selecting node
await node.click()
await expect(argumentNames).toHaveCount(3)
await expect(topLevelArgs).toHaveCount(3)

// Add first aggregate
const columnsArg = argumentNames.filter({ has: page.getByText('columns') })
const columnsArg = topLevelArgs.filter({ has: page.getByText('columns') })

await columnsArg.locator('.add-item').click()
await expect(columnsArg.locator('.WidgetToken')).toContainText([
'Aggregate_Column',
Expand All @@ -327,10 +336,10 @@ test('Managing aggregates in `aggregate` node', async ({ page }) => {
)

// Change aggregation type
const firstItem = columnsArg.locator('.item > .WidgetPort > .WidgetSelection')
await firstItem.click()
await dropDown.expectVisibleWithOptions(['Group_By', 'Count', 'Count_Distinct'])
await dropDown.clickOption('Count_Distinct')
const columnsDropdown = new DropDownLocator(columnsArg)
await columnsDropdown.clickWidget()
await columnsDropdown.expectVisibleWithOptions(['Group_By', 'Count', 'Count_Distinct'])
await columnsDropdown.clickOption('Count_Distinct')
await expect(columnsArg.locator('.WidgetToken')).toContainText([
'Aggregate_Column',
'.',
Expand All @@ -353,10 +362,11 @@ test('Managing aggregates in `aggregate` node', async ({ page }) => {
)

// Set column
const columnArg = firstItem.locator('.WidgetSelection').first()
await columnArg.click()
await dropDown.expectVisibleWithOptions(['column 1', 'column 2'])
await dropDown.clickOption('column 1')
const firstItem = columnsArg.locator('.item > .WidgetPort > .WidgetSelection').nth(0)
const firstItemDropdown = new DropDownLocator(firstItem)
await firstItemDropdown.clickWidget()
await firstItemDropdown.expectVisibleWithOptions(['column 1', 'column 2'])
await firstItemDropdown.clickOption('column 1')
await expect(columnsArg.locator('.WidgetToken')).toContainText([
'Aggregate_Column',
'.',
Expand Down Expand Up @@ -392,10 +402,10 @@ test('Managing aggregates in `aggregate` node', async ({ page }) => {

// Set new aggregate's column
const secondItem = columnsArg.locator('.item > .WidgetPort > .WidgetSelection').nth(1)
const secondColumnArg = secondItem.locator('.WidgetSelection').first()
await secondColumnArg.click()
await dropDown.expectVisibleWithOptions(['column 1', 'column 2'])
await dropDown.clickOption('column 2')
const secondItemDropdown = new DropDownLocator(secondItem)
await secondItemDropdown.clickWidget()
await secondItemDropdown.expectVisibleWithOptions(['column 1', 'column 2'])
await secondItemDropdown.clickOption('column 2')
await expect(secondItem.locator('.WidgetToken')).toContainText([
'Aggregate_Column',
'.',
Expand Down
15 changes: 13 additions & 2 deletions app/gui2/src/components/SizeTransition.vue
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,22 @@ function runAnimation(e: HTMLElement, done: Done, isEnter: boolean) {
end.marginLeft = isEnter ? current.marginLeft : negativeGap
}
const animation = e.animate([start, end], { duration: props.duration })
animation.addEventListener('finish', () => done(false))
animation.addEventListener('cancel', () => done(true))
animation.addEventListener('finish', () => {
cleanup(e)
done(false)
})
animation.addEventListener('cancel', () => {
cleanup(e)
done(true)
})
e.dataset['transitioning'] = ''
animation.play()
animationsMap.set(e, animation)
}
function cleanup(e: HTMLElement) {
delete e.dataset['transitioning']
}
</script>

<template>
Expand Down

0 comments on commit 9d2d903

Please sign in to comment.