Skip to content

Commit

Permalink
Fix image upload modal cancel loop (#1755)
Browse files Browse the repository at this point in the history
* minimal repro. lmfao

* fix the most pressing part of the problem

* Revert "minimal repro. lmfao"

e9c51c2

* using e2e tests for evil again

* increase per-test timeout

* fix debug-ci-e2e-fail by filtering for CI workflow runs

* try giving the file upload slightly more time

* split click through networking test in two so safari might not be such a BABY about it
  • Loading branch information
david-crespo authored Sep 7, 2023
1 parent e155878 commit bb43635
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 71 deletions.
104 changes: 56 additions & 48 deletions app/test/e2e/image-upload.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import { expect, test } from '@playwright/test'

import { MiB } from '@oxide/util'

import { expectNotVisible, expectRowVisible, expectVisible } from './utils'
import { expectNotVisible, expectRowVisible, expectVisible, sleep } from './utils'

async function chooseFile(page: Page, size = 10 * MiB) {
async function chooseFile(page: Page, size = 15 * MiB) {
const fileChooserPromise = page.waitForEvent('filechooser')
await page.getByText('Image file', { exact: true }).click()
const fileChooser = await fileChooserPromise
Expand Down Expand Up @@ -49,6 +49,16 @@ async function expectUploadProcess(page: Page) {
await done.click()
}

async function fillForm(page: Page, name: string) {
await page.goto('/projects/mock-project/images-new')

await page.fill('role=textbox[name="Name"]', name)
await page.fill('role=textbox[name="Description"]', 'image description')
await page.fill('role=textbox[name="OS"]', 'Ubuntu')
await page.fill('role=textbox[name="Version"]', 'Dapper Drake')
await chooseFile(page)
}

test.describe('Image upload', () => {
test('happy path', async ({ page }) => {
await page.goto('/projects/mock-project/images')
Expand All @@ -61,11 +71,7 @@ test.describe('Image upload', () => {

await expectVisible(page, ['role=dialog[name="Upload image"]'])

await page.fill('role=textbox[name="Name"]', 'new-image')
await page.fill('role=textbox[name="Description"]', 'image description')
await page.fill('role=textbox[name="OS"]', 'Ubuntu')
await page.fill('role=textbox[name="Version"]', 'Dapper Drake')
await chooseFile(page)
await fillForm(page, 'new-image')

await page.click('role=button[name="Upload image"]')

Expand All @@ -81,13 +87,7 @@ test.describe('Image upload', () => {
})

test('with name taken', async ({ page }) => {
await page.goto('/projects/mock-project/images-new')

await page.fill('role=textbox[name="Name"]', 'image-1')
await page.fill('role=textbox[name="Description"]', 'image description')
await page.fill('role=textbox[name="OS"]', 'Ubuntu')
await page.fill('role=textbox[name="Version"]', 'Dapper Drake')
await chooseFile(page)
await fillForm(page, 'image-1')

await expectNotVisible(page, ['text="Image name already exists"'])
await page.click('role=button[name="Upload image"]')
Expand Down Expand Up @@ -127,13 +127,7 @@ test.describe('Image upload', () => {
})

test('cancel', async ({ page }) => {
await page.goto('/projects/mock-project/images-new')

await page.fill('role=textbox[name="Name"]', 'new-image')
await page.fill('role=textbox[name="Description"]', 'image description')
await page.fill('role=textbox[name="OS"]', 'Ubuntu')
await page.fill('role=textbox[name="Version"]', 'Dapper Drake')
await chooseFile(page)
await fillForm(page, 'new-image')

await page.click('role=button[name="Upload image"]')

Expand All @@ -147,14 +141,13 @@ test.describe('Image upload', () => {
// form is disabled and semi-hidden
// await expectNotVisible(page, ['role=textbox[name="Name"]'])

const progressModal = page.getByRole('dialog', { name: 'Image upload progress' })

page.on('dialog', (dialog) => dialog.accept()) // click yes on the are you sure prompt
await page
.getByRole('dialog', { name: 'Image upload progress' })
.getByRole('button', { name: 'Cancel' })
.click()
await progressModal.getByRole('button', { name: 'Cancel' }).click()

// modal has closed
await expectNotVisible(page, ['role=dialog[name="Image upload progress"]'])
await expect(progressModal).toBeHidden()

// form's back
await expectVisible(page, ['role=textbox[name="Name"]'])
Expand All @@ -168,14 +161,39 @@ test.describe('Image upload', () => {
// await expectNotVisible(page, ['role=cell[name=tmp]'])
})

test('Image upload cancel and retry', async ({ page }) => {
await page.goto('/projects/mock-project/images-new')
// testing the onFocusOutside fix
test('cancel canceling', async ({ page }) => {
await fillForm(page, 'new-image')

await page.fill('role=textbox[name="Name"]', 'new-image')
await page.fill('role=textbox[name="Description"]', 'image description')
await page.fill('role=textbox[name="OS"]', 'Ubuntu')
await page.fill('role=textbox[name="Version"]', 'Dapper Drake')
await chooseFile(page)
const progressModal = page.getByRole('dialog', { name: 'Image upload progress' })

await page.getByRole('button', { name: 'Upload image' }).click()
await expect(progressModal).toBeVisible()

let confirmCount = 0

page.on('dialog', (dialog) => {
confirmCount += 1
dialog.dismiss()
}) // click cancel on the are you sure prompt

await progressModal.getByRole('button', { name: 'Cancel' }).click()

// still visible because we canceled the cancel!
await expect(progressModal).toBeVisible()
expect(confirmCount).toEqual(1)

// now let's try canceling by clicking out on the background over the side modal
await page.getByLabel('4096').click()

await sleep(100)

// without the onFocusOutside fix this is a higher number
expect(confirmCount).toEqual(2)
})

test('Image upload cancel and retry', async ({ page }) => {
await fillForm(page, 'new-image')

await page.click('role=button[name="Upload image"]')

Expand All @@ -197,12 +215,10 @@ test.describe('Image upload', () => {
await expect(progressModal).toBeHidden()

// form's back
await expectVisible(page, [
'role=textbox[name="Name"]',
// need to wait for submit button to come back because it's in a loading
// state while the cleanup runs
'role=button[name="Upload image"]',
])
await expect(page.getByRole('textbox', { name: 'Name' })).toBeVisible()
// need to wait for submit button to come back because it's in a loading
// state while the cleanup runs
await expect(page.getByRole('button', { name: 'Upload image' })).toBeVisible()

// resubmit and it should work fine
await page.getByRole('button', { name: 'Upload image' }).click()
Expand All @@ -218,15 +234,7 @@ test.describe('Image upload', () => {

for (const { imageName, stepText } of failureCases) {
test(`failure ${imageName}`, async ({ page }) => {
await page.goto('/projects/mock-project/images-new')

// note special image name
await page.fill('role=textbox[name="Name"]', imageName)

await page.fill('role=textbox[name="Description"]', 'image description')
await page.fill('role=textbox[name="OS"]', 'Ubuntu')
await page.fill('role=textbox[name="Version"]', 'Dapper Drake')
await chooseFile(page)
await fillForm(page, imageName)

await page.click('role=button[name="Upload image"]')

Expand Down
28 changes: 7 additions & 21 deletions app/test/e2e/networking.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
*
* Copyright Oxide Computer Company
*/
import { test } from '@playwright/test'
import { expect, test } from '@playwright/test'

import { expectNotVisible, expectVisible } from './utils'

test('Click through networking', async ({ page }) => {
test('Create and edit VPC', async ({ page }) => {
await page.goto('/projects/mock-project')

await page.click('role=link[name*="Networking"]')
Expand Down Expand Up @@ -48,13 +48,15 @@ test('Click through networking', async ({ page }) => {
// Close toast, it holds up the test for some reason
await page.click('role=button[name="Dismiss notification"]')

await expectVisible(page, ['role=link[name="new-vpc"]'])
await expect(page.getByRole('link', { name: 'new-vpc' })).toBeVisible()
})

await page.click('role=link[name="new-vpc"]')
test('Create and edit subnet', async ({ page }) => {
await page.goto('/projects/mock-project/vpcs/mock-vpc')

// VPC detail, subnets tab
await expectVisible(page, [
'role=heading[name*="new-vpc"]',
'role=heading[name*="mock-vpc"]',
'role=tab[name="Subnets"]',
// 'role=tab[name="System Routes"]',
// 'role=tab[name="Routers"]',
Expand Down Expand Up @@ -92,22 +94,6 @@ test('Click through networking', async ({ page }) => {
await expectNotVisible(page, ['role=cell[name="new-subnet"]'])
await expectVisible(page, ['role=cell[name="edited-subnet"]'])

// // System routes
// await page.click('role=tab[name="System Routes"]')
// await expectVisible(page, ['role=cell[name="system"]'])

// // Routers
// await page.click('role=tab[name="Routers"]')
// await expectVisible(page, ['role=cell[name="system"] >> nth=0'])
// await page.click('role=button[name="New router"]')
// await expectVisible(page, [
// 'role=heading[name="Create VPC Router"]',
// 'role=button[name="Create VPC Router"]',
// ])
// await page.fill('role=textbox[name="Name"]', 'new-router')
// await page.click('role=button[name="Create VPC Router"]')
// await expectVisible(page, ['role=cell[name="new-router"]', 'role=cell[name="custom"]'])

// Firewall rules
await page.click('role=tab[name="Firewall Rules"]')
await expectVisible(page, [
Expand Down
2 changes: 2 additions & 0 deletions app/test/e2e/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,5 @@ export async function expectObscured(locator: Locator) {
async () => await locator.click({ trial: true, timeout: 50 })
).rejects.toThrow(/locator.click: Timeout 50ms exceeded/)
}

export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
6 changes: 6 additions & 0 deletions libs/ui/lib/modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ export function Modal({ children, onDismiss, title, isOpen }: ModalProps) {
style={{
transform: y.to((value) => `translate3d(-50%, ${-50 + value}%, 0px)`),
}}
// Prevents cancel loop on clicking on background over side
// modal to get out of image upload modal. Canceling out of
// confirm dialog returns focus to the dismissable layer,
// which triggers onDismiss again. And again.
// https://github.com/oxidecomputer/console/issues/1745
onFocusOutside={(e) => e.preventDefault()}
>
{title && (
<Dialog.Title asChild>
Expand Down
2 changes: 1 addition & 1 deletion playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const config: PlaywrightTestConfig = {
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
timeout: 60000,
timeout: 2 * 60 * 1000, // 2 minutes, surely overkill
fullyParallel: true,
use: {
trace: 'retain-on-failure',
Expand Down
3 changes: 2 additions & 1 deletion tools/debug-ci-e2e-fail.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ set -e
set -o pipefail

# Get the ID of the last github actions run if there was one
RUN_ID=$(gh run list -b $(git rev-parse --abbrev-ref HEAD) -L 1 --json databaseId --jq '.[0].databaseId')
BRANCH=$(git rev-parse --abbrev-ref HEAD)
RUN_ID=$(gh run list -b "$BRANCH" -w CI -L 1 --json databaseId --jq '.[0].databaseId')

if [ -z "$RUN_ID" ]; then
echo "No action runs found for this branch"
Expand Down

0 comments on commit bb43635

Please sign in to comment.