Skip to content

Commit bb43635

Browse files
authored
Fix image upload modal cancel loop (#1755)
* 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
1 parent e155878 commit bb43635

File tree

6 files changed

+74
-71
lines changed

6 files changed

+74
-71
lines changed

app/test/e2e/image-upload.e2e.ts

Lines changed: 56 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import { expect, test } from '@playwright/test'
1010

1111
import { MiB } from '@oxide/util'
1212

13-
import { expectNotVisible, expectRowVisible, expectVisible } from './utils'
13+
import { expectNotVisible, expectRowVisible, expectVisible, sleep } from './utils'
1414

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

52+
async function fillForm(page: Page, name: string) {
53+
await page.goto('/projects/mock-project/images-new')
54+
55+
await page.fill('role=textbox[name="Name"]', name)
56+
await page.fill('role=textbox[name="Description"]', 'image description')
57+
await page.fill('role=textbox[name="OS"]', 'Ubuntu')
58+
await page.fill('role=textbox[name="Version"]', 'Dapper Drake')
59+
await chooseFile(page)
60+
}
61+
5262
test.describe('Image upload', () => {
5363
test('happy path', async ({ page }) => {
5464
await page.goto('/projects/mock-project/images')
@@ -61,11 +71,7 @@ test.describe('Image upload', () => {
6171

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

64-
await page.fill('role=textbox[name="Name"]', 'new-image')
65-
await page.fill('role=textbox[name="Description"]', 'image description')
66-
await page.fill('role=textbox[name="OS"]', 'Ubuntu')
67-
await page.fill('role=textbox[name="Version"]', 'Dapper Drake')
68-
await chooseFile(page)
74+
await fillForm(page, 'new-image')
6975

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

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

8389
test('with name taken', async ({ page }) => {
84-
await page.goto('/projects/mock-project/images-new')
85-
86-
await page.fill('role=textbox[name="Name"]', 'image-1')
87-
await page.fill('role=textbox[name="Description"]', 'image description')
88-
await page.fill('role=textbox[name="OS"]', 'Ubuntu')
89-
await page.fill('role=textbox[name="Version"]', 'Dapper Drake')
90-
await chooseFile(page)
90+
await fillForm(page, 'image-1')
9191

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

129129
test('cancel', async ({ page }) => {
130-
await page.goto('/projects/mock-project/images-new')
131-
132-
await page.fill('role=textbox[name="Name"]', 'new-image')
133-
await page.fill('role=textbox[name="Description"]', 'image description')
134-
await page.fill('role=textbox[name="OS"]', 'Ubuntu')
135-
await page.fill('role=textbox[name="Version"]', 'Dapper Drake')
136-
await chooseFile(page)
130+
await fillForm(page, 'new-image')
137131

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

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

144+
const progressModal = page.getByRole('dialog', { name: 'Image upload progress' })
145+
150146
page.on('dialog', (dialog) => dialog.accept()) // click yes on the are you sure prompt
151-
await page
152-
.getByRole('dialog', { name: 'Image upload progress' })
153-
.getByRole('button', { name: 'Cancel' })
154-
.click()
147+
await progressModal.getByRole('button', { name: 'Cancel' }).click()
155148

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

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

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

174-
await page.fill('role=textbox[name="Name"]', 'new-image')
175-
await page.fill('role=textbox[name="Description"]', 'image description')
176-
await page.fill('role=textbox[name="OS"]', 'Ubuntu')
177-
await page.fill('role=textbox[name="Version"]', 'Dapper Drake')
178-
await chooseFile(page)
168+
const progressModal = page.getByRole('dialog', { name: 'Image upload progress' })
169+
170+
await page.getByRole('button', { name: 'Upload image' }).click()
171+
await expect(progressModal).toBeVisible()
172+
173+
let confirmCount = 0
174+
175+
page.on('dialog', (dialog) => {
176+
confirmCount += 1
177+
dialog.dismiss()
178+
}) // click cancel on the are you sure prompt
179+
180+
await progressModal.getByRole('button', { name: 'Cancel' }).click()
181+
182+
// still visible because we canceled the cancel!
183+
await expect(progressModal).toBeVisible()
184+
expect(confirmCount).toEqual(1)
185+
186+
// now let's try canceling by clicking out on the background over the side modal
187+
await page.getByLabel('4096').click()
188+
189+
await sleep(100)
190+
191+
// without the onFocusOutside fix this is a higher number
192+
expect(confirmCount).toEqual(2)
193+
})
194+
195+
test('Image upload cancel and retry', async ({ page }) => {
196+
await fillForm(page, 'new-image')
179197

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

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

199217
// form's back
200-
await expectVisible(page, [
201-
'role=textbox[name="Name"]',
202-
// need to wait for submit button to come back because it's in a loading
203-
// state while the cleanup runs
204-
'role=button[name="Upload image"]',
205-
])
218+
await expect(page.getByRole('textbox', { name: 'Name' })).toBeVisible()
219+
// need to wait for submit button to come back because it's in a loading
220+
// state while the cleanup runs
221+
await expect(page.getByRole('button', { name: 'Upload image' })).toBeVisible()
206222

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

219235
for (const { imageName, stepText } of failureCases) {
220236
test(`failure ${imageName}`, async ({ page }) => {
221-
await page.goto('/projects/mock-project/images-new')
222-
223-
// note special image name
224-
await page.fill('role=textbox[name="Name"]', imageName)
225-
226-
await page.fill('role=textbox[name="Description"]', 'image description')
227-
await page.fill('role=textbox[name="OS"]', 'Ubuntu')
228-
await page.fill('role=textbox[name="Version"]', 'Dapper Drake')
229-
await chooseFile(page)
237+
await fillForm(page, imageName)
230238

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

app/test/e2e/networking.e2e.ts

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8-
import { test } from '@playwright/test'
8+
import { expect, test } from '@playwright/test'
99

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

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

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

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

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

5557
// VPC detail, subnets tab
5658
await expectVisible(page, [
57-
'role=heading[name*="new-vpc"]',
59+
'role=heading[name*="mock-vpc"]',
5860
'role=tab[name="Subnets"]',
5961
// 'role=tab[name="System Routes"]',
6062
// 'role=tab[name="Routers"]',
@@ -92,22 +94,6 @@ test('Click through networking', async ({ page }) => {
9294
await expectNotVisible(page, ['role=cell[name="new-subnet"]'])
9395
await expectVisible(page, ['role=cell[name="edited-subnet"]'])
9496

95-
// // System routes
96-
// await page.click('role=tab[name="System Routes"]')
97-
// await expectVisible(page, ['role=cell[name="system"]'])
98-
99-
// // Routers
100-
// await page.click('role=tab[name="Routers"]')
101-
// await expectVisible(page, ['role=cell[name="system"] >> nth=0'])
102-
// await page.click('role=button[name="New router"]')
103-
// await expectVisible(page, [
104-
// 'role=heading[name="Create VPC Router"]',
105-
// 'role=button[name="Create VPC Router"]',
106-
// ])
107-
// await page.fill('role=textbox[name="Name"]', 'new-router')
108-
// await page.click('role=button[name="Create VPC Router"]')
109-
// await expectVisible(page, ['role=cell[name="new-router"]', 'role=cell[name="custom"]'])
110-
11197
// Firewall rules
11298
await page.click('role=tab[name="Firewall Rules"]')
11399
await expectVisible(page, [

app/test/e2e/utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,5 @@ export async function expectObscured(locator: Locator) {
140140
async () => await locator.click({ trial: true, timeout: 50 })
141141
).rejects.toThrow(/locator.click: Timeout 50ms exceeded/)
142142
}
143+
144+
export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

libs/ui/lib/modal/Modal.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ export function Modal({ children, onDismiss, title, isOpen }: ModalProps) {
6666
style={{
6767
transform: y.to((value) => `translate3d(-50%, ${-50 + value}%, 0px)`),
6868
}}
69+
// Prevents cancel loop on clicking on background over side
70+
// modal to get out of image upload modal. Canceling out of
71+
// confirm dialog returns focus to the dismissable layer,
72+
// which triggers onDismiss again. And again.
73+
// https://github.com/oxidecomputer/console/issues/1745
74+
onFocusOutside={(e) => e.preventDefault()}
6975
>
7076
{title && (
7177
<Dialog.Title asChild>

playwright.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const config: PlaywrightTestConfig = {
2020
retries: process.env.CI ? 2 : 0,
2121
/* Opt out of parallel tests on CI. */
2222
workers: process.env.CI ? 1 : undefined,
23-
timeout: 60000,
23+
timeout: 2 * 60 * 1000, // 2 minutes, surely overkill
2424
fullyParallel: true,
2525
use: {
2626
trace: 'retain-on-failure',

tools/debug-ci-e2e-fail.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ set -e
99
set -o pipefail
1010

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

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

0 commit comments

Comments
 (0)