Quellcode durchsuchen

Add stability feature Playwright coverage, fix Server selection start race

Stefan Stidl vor 2 Monaten
Ursprung
Commit
320b908864
3 geänderte Dateien mit 195 neuen und 4 gelöschten Zeilen
  1. 63 2
      stability.html
  2. 7 2
      tests/e2e/helpers/ui.js
  3. 125 0
      tests/e2e/stability.spec.js

+ 63 - 2
stability.html

@@ -63,13 +63,29 @@
           0 0 2em rgba(0, 0, 0, 0.1),
           inset 0 0 1em rgba(0, 0, 0, 0.1);
       }
+      #startBtn.disabled {
+        color: #a0a0a0;
+        border-color: #c0c0c0;
+        cursor: default;
+        opacity: 0.8;
+      }
+      #startBtn.disabled:hover {
+        box-shadow:
+          0 0 0 rgba(0, 0, 0, 0.1),
+          inset 0 0 0 rgba(0, 0, 0, 0.1);
+      }
       #startBtn:before {
         content: "Start";
       }
+      #startBtn.finding:before {
+        content: "Finding...";
+      }
       #startBtn.running {
         background-color: #ff3030;
         border-color: #ff6060;
         color: #ffffff;
+        cursor: pointer;
+        opacity: 1;
       }
       #startBtn.running:before {
         content: "Abort";
@@ -232,6 +248,10 @@
           color: #9090ff;
           border-color: #7070ff;
         }
+        #startBtn.disabled {
+          color: #707070;
+          border-color: #505050;
+        }
         #startBtn.running {
           background-color: #ff3030;
           border-color: #ff6060;
@@ -267,13 +287,13 @@
         <option value="180">3 Minutes</option>
         <option value="300">5 Minutes</option>
       </select>
-      <select id="targetSelect">
+      <select id="targetSelect" onchange="updateStartButtonState()">
         <option value="">Local Server</option>
         <option value="https://www.google.com/generate_204">Google</option>
         <option value="https://www.cloudflare.com/cdn-cgi/trace">Cloudflare</option>
         <option value="https://www.apple.com/library/test/success.html">Apple</option>
       </select>
-      <div id="startBtn" onclick="startStop()"></div>
+      <div id="startBtn" class="disabled finding" onclick="startStop()" aria-disabled="true" title="Finding best server..."></div>
       <div id="resetBtn" onclick="resetTest()">Reset</div>
     </div>
 
@@ -370,6 +390,8 @@
       var lastBeepTime = 0;
       var audioCtx = null;
       var selectedServer = null;
+      var localServerReady = false;
+      var serverDiscoveryPending = false;
 
       // Dark mode detection for canvas colors
       var isDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
@@ -449,10 +471,23 @@
 
       // Server initialization
       function initServers() {
+        selectedServer = null;
+        localServerReady = false;
+        serverDiscoveryPending = false;
+        I("server").innerHTML = "";
         if (SPEEDTEST_SERVERS.length === 0) {
           I("serverArea").style.display = "none";
+          localServerReady = true;
+          updateStartButtonState();
         } else {
           I("serverArea").style.display = "";
+          serverDiscoveryPending = true;
+          I("server").disabled = true;
+          var pendingOpt = document.createElement("option");
+          pendingOpt.value = "";
+          pendingOpt.textContent = "Finding best server...";
+          I("server").appendChild(pendingOpt);
+          updateStartButtonState();
           // ping servers to find best one
           var completed = 0;
           var best = null;
@@ -464,6 +499,7 @@
                 completed++;
                 if (completed === SPEEDTEST_SERVERS.length) {
                   // populate dropdown
+                  I("server").innerHTML = "";
                   for (var j = 0; j < SPEEDTEST_SERVERS.length; j++) {
                     if (SPEEDTEST_SERVERS[j].pingT <= 0) continue;
                     var opt = document.createElement("option");
@@ -473,6 +509,10 @@
                     I("server").appendChild(opt);
                   }
                   selectedServer = best;
+                  serverDiscoveryPending = false;
+                  localServerReady = selectedServer !== null || I("server").options.length > 0;
+                  I("server").disabled = !localServerReady;
+                  updateStartButtonState();
                 }
               });
             })(i);
@@ -500,7 +540,24 @@
       }
 
       function onServerChange(idx) {
+        if (idx === "") return;
         selectedServer = SPEEDTEST_SERVERS[idx];
+        updateStartButtonState();
+      }
+
+      function canStartTest() {
+        return !!I("targetSelect").value || localServerReady;
+      }
+
+      function updateStartButtonState() {
+        if (running) return;
+
+        var startBtn = I("startBtn");
+        var canStart = canStartTest();
+        startBtn.className = canStart ? "" : serverDiscoveryPending ? "disabled finding" : "disabled";
+        startBtn.setAttribute("aria-disabled", canStart ? "false" : "true");
+        startBtn.title = canStart ? "" : serverDiscoveryPending ? "Finding best server..." : "No reachable local server found";
+        I("server").disabled = !localServerReady || serverDiscoveryPending;
       }
 
       // Start/Stop
@@ -510,11 +567,14 @@
           if (worker) worker.postMessage("abort");
           stopTest();
         } else {
+          if (!canStartTest()) return;
           startTest();
         }
       }
 
       function startTest() {
+        if (!canStartTest()) return;
+
         allPingData = [];
         latestData = null;
         resetUI();
@@ -572,6 +632,7 @@
         I("durationSelect").disabled = false;
         I("targetSelect").disabled = false;
         I("server").disabled = false;
+        updateStartButtonState();
         if (updater) {
           clearInterval(updater);
           updater = null;

+ 7 - 2
tests/e2e/helpers/ui.js

@@ -1,12 +1,17 @@
 function modernStartButton(page) {
-  return page.locator('#start-button');
+  return page.locator("#start-button");
 }
 
 function classicStartButton(page) {
-  return page.locator('#startStopBtn');
+  return page.locator("#startStopBtn");
+}
+
+function stabilityStartButton(page) {
+  return page.locator("#startBtn");
 }
 
 module.exports = {
   modernStartButton,
   classicStartButton,
+  stabilityStartButton
 };

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

@@ -0,0 +1,125 @@
+const fs = require("node:fs");
+const { test, expect } = require("@playwright/test");
+const { baseUrls } = require("./helpers/env");
+const { stabilityStartButton } = require("./helpers/ui");
+
+async function setShortDuration(page) {
+  await page.evaluate(() => {
+    const select = document.querySelector("#durationSelect");
+    const option = document.createElement("option");
+    option.value = "1";
+    option.textContent = "1 Second";
+    select.insertBefore(option, select.firstChild);
+    select.value = "1";
+    select.dispatchEvent(new Event("change", { bubbles: true }));
+  });
+}
+
+async function setAlertThreshold(page, value) {
+  await page.evaluate(threshold => {
+    const input = document.querySelector("#alertThreshold");
+    input.value = String(threshold);
+    input.dispatchEvent(new Event("input", { bubbles: true }));
+  }, value);
+}
+
+async function waitForSamples(page) {
+  await expect.poll(() => page.evaluate(() => window.allPingData.length), { timeout: 10_000 }).toBeGreaterThan(0);
+}
+
+async function waitForLocalServer(page, serverName) {
+  await expect(page.locator("#serverArea")).toBeVisible({ timeout: 10_000 });
+  await expect(page.locator("#server option")).toContainText(serverName, { timeout: 10_000 });
+}
+
+test.describe("Stability test", () => {
+  test("keeps the local start control disabled until server discovery completes", async ({ page }) => {
+    let releaseServerProbe;
+    const serverProbe = new Promise(resolve => {
+      releaseServerProbe = resolve;
+    });
+
+    await page.route(/\/backend\/empty\.php\?cors=true/, async route => {
+      await serverProbe;
+      await route.fulfill({ status: 200, body: "" });
+    });
+
+    await page.goto(`${baseUrls.standalone}/stability.html`);
+
+    await expect(stabilityStartButton(page)).toHaveClass(/disabled/);
+    await expect(stabilityStartButton(page)).toHaveClass(/finding/);
+    await expect(stabilityStartButton(page)).toHaveAttribute("aria-disabled", "true");
+    await expect(stabilityStartButton(page)).toHaveAttribute("title", "Finding best server...");
+    await expect(page.locator("#server")).toBeDisabled();
+
+    releaseServerProbe();
+
+    await expect(page.locator("#server option")).toContainText("local", { timeout: 10_000 });
+    await expect(stabilityStartButton(page)).not.toHaveClass(/disabled/);
+    await expect(stabilityStartButton(page)).toHaveAttribute("aria-disabled", "false");
+    await expect(stabilityStartButton(page)).toHaveAttribute("title", "");
+    await expect(page.locator("#server")).toBeEnabled();
+  });
+
+  test("runs a short local measurement and exports CSV data", async ({ page }) => {
+    await page.goto(`${baseUrls.standalone}/stability.html`);
+
+    await expect(page).toHaveTitle("LibreSpeed - Stability Test");
+    await waitForLocalServer(page, "local");
+
+    await setShortDuration(page);
+    await stabilityStartButton(page).click();
+
+    await expect(stabilityStartButton(page)).toHaveClass(/running/);
+    await expect(page.locator("#durationSelect")).toBeDisabled();
+    await expect(page.locator("#targetSelect")).toBeDisabled();
+    await expect(page.locator("#server")).toBeDisabled();
+
+    await waitForSamples(page);
+    await expect(page.locator("#statAvg")).not.toHaveText("", { timeout: 5_000 });
+    await expect(page.locator("#rating")).not.toHaveText("--", { timeout: 5_000 });
+
+    await expect(stabilityStartButton(page)).not.toHaveClass(/running/, { timeout: 10_000 });
+    await expect(page.locator("#durationSelect")).toBeEnabled();
+
+    const [download] = await Promise.all([page.waitForEvent("download"), page.locator("#downloadCsvBtn").click()]);
+    expect(download.suggestedFilename()).toMatch(/^stability_test_.*\.csv$/);
+
+    const csvPath = await download.path();
+    const csv = fs.readFileSync(csvPath, "utf8");
+    expect(csv).toContain("elapsed_s,ping_ms,failed\n");
+    expect(csv.trim().split("\n").length).toBeGreaterThan(1);
+  });
+
+  test("supports threshold display, abort, and reset controls", async ({ page }) => {
+    await page.goto(`${baseUrls.standalone}/stability.html`);
+    await waitForLocalServer(page, "local");
+
+    await setAlertThreshold(page, 40);
+    await expect(page.locator("#thresholdValue")).toHaveText("40 ms");
+
+    await stabilityStartButton(page).click();
+    await expect(stabilityStartButton(page)).toHaveClass(/running/);
+    await waitForSamples(page);
+
+    await stabilityStartButton(page).click();
+    await expect(stabilityStartButton(page)).not.toHaveClass(/running/);
+    await expect(page.locator("#durationSelect")).toBeEnabled();
+    await expect(page.locator("#targetSelect")).toBeEnabled();
+
+    await page.waitForTimeout(700);
+    await page.locator("#resetBtn").click();
+
+    await expect(page.locator("#rating")).toHaveText("--");
+    await expect(page.locator("#statAvg")).toHaveText("");
+    await expect.poll(() => page.evaluate(() => window.allPingData.length)).toBe(0);
+    await expect.poll(() => page.evaluate(() => window.latestData)).toBeNull();
+  });
+
+  test("loads the configured dual-mode server list", async ({ page }) => {
+    await page.goto(`${baseUrls.dual}/stability.html`);
+
+    await expect(page.locator("#serverArea")).toBeVisible({ timeout: 10_000 });
+    await expect(page.locator("#server option")).toContainText("Local dual backend", { timeout: 10_000 });
+  });
+});