fix: (regression 3.0.1) admin/options: couldn't add a "blocked ip" entry (and other problems) with firefox
Massimo Melina committed
Jan 24, 2026 at 20:03 UTC
fbf5a82ffc36743f92af5f092cf1eb204af57e40
6 files changed
+30
-11
e2e/common.ts
+5
-1
@@ -1,4 +1,4 @@
1
-import { test } from '@playwright/test'
1
+import { Page, test } from '@playwright/test'
2
import fs from 'fs'
3
4
export const username = 'rejetto'
@@ -18,4 +18,8 @@ export function clearUploads() {
18
export function resetTimestamp() {
19
fs.utimesSync('tests', t, t);
20
fs.utimesSync('tests/alfa.txt', t, t);
21
+}
22
+
23
+export function forwardConsole(page: Page) {
24
+ page.on('console', msg => console.log(msg.type(), msg.text()));
25
}
\ No newline at end of file
e2e/frontend.spec.ts
+11
@@ -253,6 +253,17 @@ test('admin1', async ({ page }) => {
253
await expect(page.getByText('Expire', { exact: true })).toBeVisible() // wait for layout of 'block' table
254
await page.mouse.click(1, 1) // avoid focus inconsistencies
255
await screenshot(page)
256
+ { // regression test on dialog navigation not properly working
257
+ const blockTable = page.getByRole('grid').filter({ has: page.getByText('Blocked IP', { exact: true }) }).first()
258
+ await blockTable.getByRole('button', { name: 'Add' }).click()
259
+ const addDialog = page.getByRole('dialog').filter({ hasText: 'Add' })
260
+ await addDialog.getByRole('textbox', { name: 'Blocked IP' }).fill('1.2.3.4')
261
+ await addDialog.getByRole('button').last().click()
262
+ await expect(addDialog).not.toBeVisible()
263
+ await expect(page.getByRole('heading', { name: 'Options', exact: true })).toBeVisible() // still on the same page
264
+ await expect(blockTable.getByText('1.2.3.4')).toBeVisible()
265
+ await page.getByRole('button', { name: 'Reload' }).click() // cancel
266
+ }
267
268
await clickMenu('Logs')
269
await dataTableLoading()
e2e/serial.spec.ts
+1
@@ -36,6 +36,7 @@ test('upload1', async ({ page, context, browserName }) => {
36
await page.getByRole('button', { name: 'Edit' }).click();
37
await page.getByRole('textbox').fill(uploadName);
38
await page.getByRole('button', { name: 'Continue' }).click();
39
+ await expect(page.getByText(uploadName)).toBeVisible() // rename was effective
40
await page.getByRole('button', { name: 'Send 1 file' }).click();
41
await wait(1500)
42
await pageAdmin.getByRole('cell', { name: uploadName }).click();
frontend/src/login.ts
+1
-3
@@ -134,7 +134,7 @@ export async function loginDialog(closable=true, reloadAfter=true) {
134
const res = await login(u, password, {
135
[ALLOW_SESSION_IP_CHANGE]: ipRef.current?.checked,
136
...rest
137
- })
137
+ }).finally(() => going = false)
138
await close(true)
139
toast(t`Logged in`, 'success')
140
if (res?.redirect)
@@ -145,8 +145,6 @@ export async function loginDialog(closable=true, reloadAfter=true) {
145
} catch (err: any) {
146
await alertDialog(err)
147
usrRef.current?.focus()
148
- } finally {
149
- going = false
148
}
149
}
150
playwright.config.ts
+2
-2
@@ -13,7 +13,7 @@ import { defineConfig, devices } from '@playwright/test';
13
*/
14
export default defineConfig({
15
testDir: './e2e',
16
- timeout: 25_000,
16
+ timeout: 30_000,
17
fullyParallel: true, // Run tests in files in parallel
18
forbidOnly: !!process.env.CI, // Fail the build on CI if you accidentally left test.only in the source code.
19
retries: process.env.CI ? 2 : 0, // Retry on CI only
@@ -94,7 +94,7 @@ export default defineConfig({
94
},
95
/* Run your local dev server before starting the tests */
96
webServer: [{
97
- command: 'npm run server-for-test',
97
+ command: 'npm run server-for-test', // you can run server-for-test-dev instead, but then you need frontend and admin to be running too
98
url: 'http://127.0.0.1:81',
99
reuseExistingServer: !process.env.CI,
100
}, { // launch a second server for tests with an empty/default config
shared/dialogs.ts
+10
-5
@@ -77,13 +77,18 @@ let waitClosing = Promise.resolve()
77
let ignorePopState = false
78
async function back() {
79
ignorePopState = true
80
- let was = history.state
80
+ const was = history.state
81
return waitClosing = waitClosing.then(() => new Promise<void>(async res => {
82
- const started = Date.now()
82
+ const timeout = Date.now() + 1000
83
+ let lastBack = 0
84
while (was === history.state) { // history.back seems to not always be effective, so we loop for it
84
- if (Date.now() - started > 1000) break // emergency brake
85
- history.back()
86
- await wait(10)
85
+ const now = Date.now()
86
+ if (now > timeout) break // emergency brake
87
+ if (now - lastBack > 500) { // after this long time we try again
88
+ history.back()
89
+ lastBack = now
90
+ }
91
+ await wait(10) // we wait shorter and loop faster so to exit/resolve asap
92
}
93
res()
94
}))