6 Commits bd8501ad13 ... be1e0a0daa

Auteur SHA1 Bericht Datum
  Copilot be1e0a0daa Fix mixed-content server selection (#855) 6 dagen geleden
  Copilot 449e9962ed Keep the modern mobile gauge row in view when a test starts (#854) 1 week geleden
  dependabot[bot] 678d8f6bf3 chore(deps-dev): bump js-yaml from 4.3.1 to 4.3.2 (#853) 1 week geleden
  Stefan Stidl 0490f3e935 chore(release): bump version to 6.3.0 1 week geleden
  Copilot fa2e28626a Prevent stability ping values from freezing on long tests (#851) 1 week geleden
  marcel1702 0603db7b37 Don't force the IE11 upload workaround on Safari (#847) 1 week geleden

+ 2 - 2
doc.md

@@ -1,7 +1,7 @@
 # LibreSpeed
 # LibreSpeed
 
 
 > by Federico Dossena
 > by Federico Dossena
-> Version 6.2.1
+> Version 6.3.0
 > [https://github.com/librespeed/speedtest/](https://github.com/librespeed/speedtest/)
 > [https://github.com/librespeed/speedtest/](https://github.com/librespeed/speedtest/)
 
 
 ## Introduction
 ## Introduction
@@ -732,7 +732,7 @@ To keep track of the amount of transferred data, the XHR Level 2 `upload.onprogr
 
 
 This test has a couple of complications:
 This test has a couple of complications:
 
 
-* Some browsers don't have a working `upload.onprogress` event. For this, we use a small blobs instead of a large one and we keep track of progress using the `onload` event. This is referred to as IE11 Workaround (but the same bug was also found in some versions of Edge and Safari)
+* Some browsers don't have a working `upload.onprogress` event. For this, we use a small blobs instead of a large one and we keep track of progress using the `onload` event. This is referred to as IE11 Workaround. Browsers that expose no usable `xhr.upload` object, such as IE11, select it automatically through feature detection. Some versions of Edge and the PlayStation 4 browser do have an `xhr.upload` object whose events never fire, which feature detection cannot see, so those two are still matched by user agent. Safari is __not__ affected and uses the regular, more accurate upload test
 * When `mpot` is set to `true`, an empty request must first be sent in order to load the CORS headers before the test can start
 * When `mpot` is set to `true`, an empty request must first be sent in order to load the CORS headers before the test can start
 
 
 See the code for more implementation details.
 See the code for more implementation details.

+ 42 - 0
frontend/javascript/index.js

@@ -16,6 +16,8 @@ const testState = {
   state: INITIALIZING,
   state: INITIALIZING,
   speedtest: null,
   speedtest: null,
   servers: [],
   servers: [],
+  initialGaugeScrollPending: false,
+  initialGaugeScrollScheduled: false,
   selectedServerDirty: false,
   selectedServerDirty: false,
   testData: null,
   testData: null,
   testDataDirty: false,
   testDataDirty: false,
@@ -81,6 +83,7 @@ function startButtonClickHandler() {
     case READY:
     case READY:
     case FINISHED:
     case FINISHED:
       testState.speedtest.start();
       testState.speedtest.start();
+      testState.initialGaugeScrollPending = true;
       testState.state = RUNNING;
       testState.state = RUNNING;
       return;
       return;
     case RUNNING:
     case RUNNING:
@@ -92,6 +95,30 @@ function startButtonClickHandler() {
   }
   }
 }
 }
 
 
+/**
+ * Scroll the initial download gauge into view on narrow viewports when starting a test
+ */
+function scrollInitialDownloadGaugeIntoView() {
+  if (!window.matchMedia("(max-width: 800px)").matches) {
+    return;
+  }
+
+  const downloadGauge = document.querySelector("#download-gauge");
+  if (!downloadGauge) {
+    return;
+  }
+
+  const { top, bottom } = downloadGauge.getBoundingClientRect();
+  if (top >= 0 && bottom <= window.innerHeight) {
+    return;
+  }
+
+  downloadGauge.scrollIntoView({
+    block: "center",
+    inline: "nearest",
+  });
+}
+
 /**
 /**
  * Event listener for clicks on the "Copy link" button in the modal
  * Event listener for clicks on the "Copy link" button in the modal
  */
  */
@@ -349,6 +376,21 @@ function startRenderingLoop() {
       )
       )
     );
     );
 
 
+    if (
+      testState.state === RUNNING &&
+      testState.initialGaugeScrollPending &&
+      !testState.initialGaugeScrollScheduled
+    ) {
+      testState.initialGaugeScrollScheduled = true;
+      requestAnimationFrame(() => {
+        if (testState.state === RUNNING) {
+          scrollInitialDownloadGaugeIntoView();
+        }
+        testState.initialGaugeScrollPending = false;
+        testState.initialGaugeScrollScheduled = false;
+      });
+    }
+
     // Show ping and jitter if data is available
     // Show ping and jitter if data is available
     pingAndJitter.forEach((e) =>
     pingAndJitter.forEach((e) =>
       e.classList.toggle(
       e.classList.toggle(

+ 5 - 5
package-lock.json

@@ -1,12 +1,12 @@
 {
 {
   "name": "librespeed-speedtest",
   "name": "librespeed-speedtest",
-  "version": "6.2.1",
+  "version": "6.3.0",
   "lockfileVersion": 3,
   "lockfileVersion": 3,
   "requires": true,
   "requires": true,
   "packages": {
   "packages": {
     "": {
     "": {
       "name": "librespeed-speedtest",
       "name": "librespeed-speedtest",
-      "version": "6.2.1",
+      "version": "6.3.0",
       "license": "LGPL-3.0-or-later",
       "license": "LGPL-3.0-or-later",
       "devDependencies": {
       "devDependencies": {
         "@playwright/test": "^1.55.0",
         "@playwright/test": "^1.55.0",
@@ -811,9 +811,9 @@
       "license": "ISC"
       "license": "ISC"
     },
     },
     "node_modules/js-yaml": {
     "node_modules/js-yaml": {
-      "version": "4.3.1",
-      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
-      "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
+      "version": "4.3.2",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
+      "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
       "dev": true,
       "dev": true,
       "funding": [
       "funding": [
         {
         {

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "librespeed-speedtest",
   "name": "librespeed-speedtest",
-  "version": "6.2.1",
+  "version": "6.3.0",
   "description": "LibreSpeed - A Free and Open Source speed test that you can host on your server(s)",
   "description": "LibreSpeed - A Free and Open Source speed test that you can host on your server(s)",
   "main": "speedtest.js",
   "main": "speedtest.js",
   "scripts": {
   "scripts": {

+ 6 - 2
speedtest.js

@@ -49,7 +49,7 @@ function Speedtest() {
   this._settings = {}; //settings for the speed test worker
   this._settings = {}; //settings for the speed test worker
   this._state = 0; //0=adding settings, 1=adding servers, 2=server selection done, 3=test running, 4=done
   this._state = 0; //0=adding settings, 1=adding servers, 2=server selection done, 3=test running, 4=done
   console.log(
   console.log(
-    "LibreSpeed by Federico Dossena v6.2.1 - https://github.com/librespeed/speedtest"
+    "LibreSpeed by Federico Dossena v6.3.0 - https://github.com/librespeed/speedtest"
   );
   );
 }
 }
 
 
@@ -239,7 +239,11 @@ Speedtest.prototype = {
       const checkServer = function(server, done) {
       const checkServer = function(server, done) {
         let i = 0;
         let i = 0;
         server.pingT = -1;
         server.pingT = -1;
-        if (server.server.indexOf(location.protocol) == -1) done();
+        if (
+          location.protocol === "https:" &&
+          server.server.substring(0, 7).toLowerCase() === "http://"
+        )
+          done();
         else {
         else {
           const nextPing = function() {
           const nextPing = function() {
             if (i++ == PINGS) {
             if (i++ == PINGS) {

+ 8 - 8
speedtest_worker.js

@@ -146,22 +146,22 @@ this.addEventListener("message", function(e) {
 				}
 				}
 			}
 			}
 			if (/Edge.(\d+\.\d+)/i.test(ua)) {
 			if (/Edge.(\d+\.\d+)/i.test(ua)) {
-				//Edge 15 introduced a bug that causes onprogress events to not get fired, we have to use the "small chunks" workaround that reduces accuracy
-				settings.forceIE11Workaround = true;
+				if (typeof s.forceIE11Workaround === "undefined") {
+					//Edge 15 introduced a bug that causes onprogress events to not get fired, we have to use the "small chunks" workaround that reduces accuracy
+					settings.forceIE11Workaround = true;
+				}
 			}
 			}
 			if (/PlayStation 4.(\d+\.\d+)/i.test(ua)) {
 			if (/PlayStation 4.(\d+\.\d+)/i.test(ua)) {
-				//PS4 browser has the same bug as IE11/Edge
-				settings.forceIE11Workaround = true;
+				if (typeof s.forceIE11Workaround === "undefined") {
+					//PS4 browser has the same bug as IE11/Edge
+					settings.forceIE11Workaround = true;
+				}
 			}
 			}
 			if (/Chrome.(\d+)/i.test(ua) && /Android|iPhone|iPad|iPod|Windows Phone/i.test(ua)) {
 			if (/Chrome.(\d+)/i.test(ua) && /Android|iPhone|iPad|iPod|Windows Phone/i.test(ua)) {
 				//cheap af
 				//cheap af
 				//Chrome mobile introduced a limitation somewhere around version 65, we have to limit XHR upload size to 4 megabytes
 				//Chrome mobile introduced a limitation somewhere around version 65, we have to limit XHR upload size to 4 megabytes
 				settings.xhr_ul_blob_megabytes = 4;
 				settings.xhr_ul_blob_megabytes = 4;
 			}
 			}
-			if (/^((?!chrome|android|crios|fxios).)*safari/i.test(ua)) {
-				//Safari also needs the IE11 workaround but only for the MPOT version
-				settings.forceIE11Workaround = true;
-			}
 			//telemetry_level has to be parsed and not just copied
 			//telemetry_level has to be parsed and not just copied
 			if (typeof s.telemetry_level !== "undefined") settings.telemetry_level = s.telemetry_level === "basic" ? 1 : s.telemetry_level === "full" ? 2 : s.telemetry_level === "debug" ? 3 : 0; // telemetry level
 			if (typeof s.telemetry_level !== "undefined") settings.telemetry_level = s.telemetry_level === "basic" ? 1 : s.telemetry_level === "full" ? 2 : s.telemetry_level === "debug" ? 3 : 0; // telemetry level
 			//transform test_order to uppercase, just in case
 			//transform test_order to uppercase, just in case

+ 4 - 0
stability_worker.js

@@ -185,6 +185,10 @@ function doPing() {
         if (d > 0 && d < instspd) instspd = d;
         if (d > 0 && d < instspd) instspd = d;
       } catch (e) {
       } catch (e) {
         // Performance API not available, use estimate
         // Performance API not available, use estimate
+      } finally {
+        try {
+          performance.clearResourceTimings();
+        } catch (e) {}
       }
       }
     }
     }
 
 

+ 99 - 0
tests/e2e/mobile-gauge-visibility.spec.js

@@ -0,0 +1,99 @@
+const { test, expect } = require("@playwright/test");
+const { modernStartButton } = require("./helpers/ui");
+
+test.use({ viewport: { width: 390, height: 560 } });
+
+test.describe("Mobile gauge visibility", () => {
+  test("keeps the active gauge visible after pressing Start", async ({ page }) => {
+    await page.route("**/speedtest.js", async (route) => {
+      await route.fulfill({
+        contentType: "text/javascript; charset=utf-8",
+        body: `
+          class Speedtest {
+            constructor() {
+              this.selectedServer = null;
+              this.onupdate = null;
+              this.onend = null;
+            }
+
+            start() {
+              this.onupdate?.({
+                dlProgress: 0.1,
+                ulProgress: 0,
+                dlStatus: "123.45",
+                ulStatus: "0",
+                pingStatus: "10.5",
+                jitterStatus: "0.9",
+                testState: 1
+              });
+            }
+
+            abort() {
+              this.onend?.(true);
+            }
+
+            setParameter() {}
+            addTestPoints() {}
+            selectServer(callback) {
+              callback(this.selectedServer);
+            }
+            setSelectedServer(server) {
+              this.selectedServer = server;
+            }
+            getSelectedServer() {
+              return this.selectedServer;
+            }
+          }
+
+          window.Speedtest = Speedtest;
+        `,
+      });
+    });
+
+    await page.route("**/settings.json", async (route) => {
+      await route.fulfill({
+        contentType: "application/json; charset=utf-8",
+        body: JSON.stringify({ telemetry_level: "off" }),
+      });
+    });
+
+    await page.route("**/server-list.json", async (route) => {
+      await route.fulfill({
+        contentType: "application/json; charset=utf-8",
+        body: JSON.stringify([
+          {
+            name: "Test Server",
+            server: "http://127.0.0.1:18184/backend/",
+          },
+        ]),
+      });
+    });
+
+    await page.goto("http://127.0.0.1:18184/index-modern.html");
+
+    const startButton = modernStartButton(page);
+    const downloadGauge = page.locator("#download-gauge");
+
+    await expect(startButton).toHaveText("Let's start", { timeout: 10_000 });
+
+    await page.evaluate(() => window.scrollTo(0, 0));
+
+    const gaugeStartsBelowFold = await downloadGauge.evaluate((element) => {
+      const { top, bottom } = element.getBoundingClientRect();
+      return bottom > window.innerHeight || top < 0;
+    });
+    expect(gaugeStartsBelowFold).toBe(true);
+
+    await startButton.click();
+    await expect(startButton).toHaveText("Abort");
+
+    await expect
+      .poll(() =>
+        downloadGauge.evaluate((element) => {
+          const { top, bottom } = element.getBoundingClientRect();
+          return top >= 0 && bottom <= window.innerHeight;
+        })
+      )
+      .toBe(true);
+  });
+});

+ 113 - 0
tests/e2e/server-selection.spec.js

@@ -0,0 +1,113 @@
+const { test, expect } = require('@playwright/test');
+const fs = require('node:fs');
+const path = require('node:path');
+const vm = require('node:vm');
+
+const speedtestSource = fs.readFileSync(
+  path.resolve(__dirname, '../../speedtest.js'),
+  'utf8'
+);
+
+function createHarness(protocol, responders = {}) {
+  const requests = [];
+
+  function FakeXMLHttpRequest() {
+    this.responseText = '';
+    this.timeout = 0;
+  }
+
+  FakeXMLHttpRequest.prototype.open = function open(method, url) {
+    this._method = method;
+    this._url = url;
+  };
+
+  FakeXMLHttpRequest.prototype.send = function send() {
+    requests.push(this._url);
+    const baseUrl = this._url.replace(/[?&]cors=true$/, '');
+    const outcome = responders[baseUrl] || 'error';
+
+    if (outcome === 'success') {
+      if (typeof this.onload === 'function') {
+        this.onload();
+      }
+      return;
+    }
+
+    if (typeof this.onerror === 'function') {
+      this.onerror(new Error(`Request failed for ${baseUrl}`));
+    }
+  };
+
+  const context = {
+    console: { log() {} },
+    Date,
+    location: { protocol },
+    navigator: { userAgent: 'Playwright' },
+    performance: { getEntriesByName: () => [] },
+    XMLHttpRequest: FakeXMLHttpRequest
+  };
+
+  vm.createContext(context);
+  vm.runInContext(speedtestSource, context, { filename: 'speedtest.js' });
+
+  return { Speedtest: context.Speedtest, requests };
+}
+
+function createServer(server) {
+  return {
+    name: server,
+    server,
+    dlURL: 'garbage.php',
+    ulURL: 'empty.php',
+    pingURL: 'empty.php',
+    getIpURL: 'getIP.php'
+  };
+}
+
+function selectServer(speedtest) {
+  return new Promise((resolve) => {
+    speedtest.selectServer(resolve);
+  });
+}
+
+test.describe('Speedtest server selection protocol handling', () => {
+  test('allows HTTPS backends to be pinged from an HTTP frontend', async () => {
+    const secureServer = 'https://secure.example/';
+    const { Speedtest, requests } = createHarness('http:', {
+      [`${secureServer}empty.php`]: 'success'
+    });
+
+    const speedtest = new Speedtest();
+    speedtest.addTestPoint(createServer(secureServer));
+
+    const selectedServer = await selectServer(speedtest);
+
+    expect(selectedServer.server).toBe(secureServer);
+    expect(
+      requests.some((url) => url.startsWith(`${secureServer}empty.php`))
+    ).toBeTruthy();
+  });
+
+  test('still skips insecure HTTP backends from an HTTPS frontend', async () => {
+    const insecureServer = 'http://insecure.example/';
+    const secureServer = 'https://secure.example/';
+    const { Speedtest, requests } = createHarness('https:', {
+      [`${insecureServer}empty.php`]: 'success',
+      [`${secureServer}empty.php`]: 'success'
+    });
+
+    const speedtest = new Speedtest();
+    speedtest.addTestPoint(createServer(insecureServer));
+    speedtest.addTestPoint(createServer(secureServer));
+
+    const selectedServer = await selectServer(speedtest);
+
+    expect(selectedServer.server).toBe(secureServer);
+    expect(
+      requests.some((url) => url.startsWith(`${insecureServer}empty.php`))
+    ).toBeFalsy();
+    expect(
+      requests.some((url) => url.startsWith(`${secureServer}empty.php`))
+    ).toBeTruthy();
+  });
+});

+ 39 - 0
tests/e2e/stability.spec.js

@@ -1,8 +1,11 @@
 const fs = require("node:fs");
 const fs = require("node:fs");
+const path = require("node:path");
 const { test, expect } = require("@playwright/test");
 const { test, expect } = require("@playwright/test");
 const { baseUrls } = require("./helpers/env");
 const { baseUrls } = require("./helpers/env");
 const { stabilityStartButton } = require("./helpers/ui");
 const { stabilityStartButton } = require("./helpers/ui");
 
 
+const workerSource = fs.readFileSync(path.join(__dirname, "..", "..", "stability_worker.js"), "utf8");
+
 async function setShortDuration(page) {
 async function setShortDuration(page) {
   await page.evaluate(() => {
   await page.evaluate(() => {
     const select = document.querySelector("#durationSelect");
     const select = document.querySelector("#durationSelect");
@@ -122,4 +125,40 @@ test.describe("Stability test", () => {
     await expect(page.locator("#serverArea")).toBeVisible({ timeout: 10_000 });
     await expect(page.locator("#serverArea")).toBeVisible({ timeout: 10_000 });
     await expect(page.locator("#server option")).toContainText("Local dual backend", { timeout: 10_000 });
     await expect(page.locator("#server option")).toContainText("Local dual backend", { timeout: 10_000 });
   });
   });
+
+  test("clears resource timings after measuring a ping", async ({ page }) => {
+    await page.goto(`${baseUrls.standalone}/stability.html`);
+    await page.route(`${baseUrls.backend}/empty.php?cors=true&r=*`, route => route.fulfill({ status: 200, headers: { "Access-Control-Allow-Origin": "*" }, body: "" }));
+
+    await expect(
+      page.evaluate(
+        async ({ source, url }) => {
+          const instrumentedSource = `
+          const clearResourceTimings = performance.clearResourceTimings.bind(performance);
+          performance.clearResourceTimings = () => {
+            postMessage("resource timings cleared");
+            clearResourceTimings();
+          };
+          ${source}
+        `;
+          const worker = new Worker(URL.createObjectURL(new Blob([instrumentedSource], { type: "text/javascript" })));
+          try {
+            await new Promise((resolve, reject) => {
+              const timeout = setTimeout(() => reject(new Error("Resource timings were not cleared")), 10_000);
+              worker.onmessage = event => {
+                if (event.data === "resource timings cleared") {
+                  clearTimeout(timeout);
+                  resolve();
+                }
+              };
+              worker.postMessage(`start ${JSON.stringify({ url_ping: url, duration: 1, mpot: true })}`);
+            });
+          } finally {
+            worker.terminate();
+          }
+        },
+        { source: workerSource, url: `${baseUrls.backend}/empty.php` }
+      )
+    ).resolves.toBeUndefined();
+  });
 });
 });

+ 91 - 0
tests/e2e/worker-browser-quirks.spec.js

@@ -0,0 +1,91 @@
+const fs = require("node:fs");
+const path = require("node:path");
+const { test, expect } = require("@playwright/test");
+
+const workerSource = fs.readFileSync(path.join(__dirname, "..", "..", "speedtest_worker.js"), "utf8");
+
+const userAgents = {
+  safariMac:
+    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15",
+  safariIos:
+    "Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1",
+  chromeIos:
+    "Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/123.0.6312.52 Mobile/15E148 Safari/604.1",
+  firefoxIos:
+    "Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/124.0 Mobile/15E148 Safari/605.1.15",
+  chromeAndroid:
+    "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Mobile Safari/537.36",
+  edge16:
+    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/16.16299",
+  playstation4: "Mozilla/5.0 (PlayStation 4 5.55) AppleWebKit/601.2 (KHTML, like Gecko)"
+};
+
+/*
+  Loads speedtest_worker.js into a page that reports the given user agent, feeds it a "start"
+  command and reports back the settings the worker resolved. The "_" test step is a no-op delay,
+  so the worker settles its settings without performing any network I/O.
+*/
+async function resolveSettings(browser, userAgent, customSettings) {
+  const context = await browser.newContext({ userAgent });
+  try {
+    const page = await context.newPage();
+    await page.goto("about:blank");
+    await page.addScriptTag({ content: workerSource });
+    return await page.evaluate(custom => {
+      window.dispatchEvent(new MessageEvent("message", { data: "start " + JSON.stringify(custom) }));
+      return {
+        userAgent: navigator.userAgent,
+        forceIE11Workaround: settings.forceIE11Workaround,
+        xhr_ul_blob_megabytes: settings.xhr_ul_blob_megabytes
+      };
+    }, Object.assign({ test_order: "_" }, customSettings));
+  } finally {
+    await context.close();
+  }
+}
+
+test.describe("speedtest_worker browser quirks", () => {
+  test("Safari uses the accurate upload test instead of the IE11 workaround", async ({ browser }) => {
+    for (const ua of [userAgents.safariMac, userAgents.safariIos]) {
+      const resolved = await resolveSettings(browser, ua);
+      expect(resolved.userAgent, "context user agent should be applied").toBe(ua);
+      expect(resolved.forceIE11Workaround, `${ua} should not force the IE11 workaround`).toBe(false);
+      expect(resolved.xhr_ul_blob_megabytes).toBe(20);
+    }
+  });
+
+  test("Safari, Chrome and Firefox on iOS resolve to the same upload path", async ({ browser }) => {
+    const safari = await resolveSettings(browser, userAgents.safariIos);
+    const chrome = await resolveSettings(browser, userAgents.chromeIos);
+    const firefox = await resolveSettings(browser, userAgents.firefoxIos);
+    expect(safari.forceIE11Workaround).toBe(chrome.forceIE11Workaround);
+    expect(safari.forceIE11Workaround).toBe(firefox.forceIE11Workaround);
+  });
+
+  test("Safari in MPOT mode also uses the accurate upload test", async ({ browser }) => {
+    const resolved = await resolveSettings(browser, userAgents.safariMac, { mpot: true });
+    expect(resolved.forceIE11Workaround).toBe(false);
+  });
+
+  test("Chrome mobile still caps the upload blob at 4 megabytes", async ({ browser }) => {
+    const resolved = await resolveSettings(browser, userAgents.chromeAndroid);
+    expect(resolved.xhr_ul_blob_megabytes).toBe(4);
+    expect(resolved.forceIE11Workaround).toBe(false);
+  });
+
+  test("Edge and the PlayStation 4 browser keep the IE11 workaround by default", async ({ browser }) => {
+    for (const ua of [userAgents.edge16, userAgents.playstation4]) {
+      const resolved = await resolveSettings(browser, ua);
+      expect(resolved.forceIE11Workaround, `${ua} should keep the IE11 workaround`).toBe(true);
+    }
+  });
+
+  test("an explicitly passed forceIE11Workaround is not overwritten by the quirks", async ({ browser }) => {
+    for (const ua of [userAgents.edge16, userAgents.playstation4]) {
+      const off = await resolveSettings(browser, ua, { forceIE11Workaround: false });
+      expect(off.forceIE11Workaround, `${ua} should honour an explicit false`).toBe(false);
+    }
+    const on = await resolveSettings(browser, userAgents.safariMac, { forceIE11Workaround: true });
+    expect(on.forceIE11Workaround, "an explicit true should still force the workaround").toBe(true);
+  });
+});