1
0

stability.spec.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. const fs = require("node:fs");
  2. const path = require("node:path");
  3. const { test, expect } = require("@playwright/test");
  4. const { baseUrls } = require("./helpers/env");
  5. const { stabilityStartButton } = require("./helpers/ui");
  6. const workerSource = fs.readFileSync(path.join(__dirname, "..", "..", "stability_worker.js"), "utf8");
  7. async function setShortDuration(page) {
  8. await page.evaluate(() => {
  9. const select = document.querySelector("#durationSelect");
  10. const option = document.createElement("option");
  11. option.value = "1";
  12. option.textContent = "1 Second";
  13. select.insertBefore(option, select.firstChild);
  14. select.value = "1";
  15. select.dispatchEvent(new Event("change", { bubbles: true }));
  16. });
  17. }
  18. async function setAlertThreshold(page, value) {
  19. await page.evaluate(threshold => {
  20. const input = document.querySelector("#alertThreshold");
  21. input.value = String(threshold);
  22. input.dispatchEvent(new Event("input", { bubbles: true }));
  23. }, value);
  24. }
  25. async function waitForSamples(page) {
  26. await expect.poll(() => page.evaluate(() => window.allPingData.length), { timeout: 10_000 }).toBeGreaterThan(0);
  27. }
  28. async function waitForLocalServer(page, serverName) {
  29. await expect(page.locator("#serverArea")).toBeVisible({ timeout: 10_000 });
  30. await expect(page.locator("#server option")).toContainText(serverName, { timeout: 10_000 });
  31. }
  32. test.describe("Stability test", () => {
  33. test("keeps the local start control disabled until server discovery completes", async ({ page }) => {
  34. let releaseServerProbe;
  35. const serverProbe = new Promise(resolve => {
  36. releaseServerProbe = resolve;
  37. });
  38. await page.route(/\/backend\/empty\.php\?cors=true/, async route => {
  39. await serverProbe;
  40. await route.fulfill({ status: 200, body: "" });
  41. });
  42. await page.goto(`${baseUrls.standalone}/stability.html`);
  43. await expect(stabilityStartButton(page)).toHaveClass(/disabled/);
  44. await expect(stabilityStartButton(page)).toHaveClass(/finding/);
  45. await expect(stabilityStartButton(page)).toHaveAttribute("aria-disabled", "true");
  46. await expect(stabilityStartButton(page)).toHaveAttribute("title", "Finding best server...");
  47. await expect(page.locator("#server")).toBeDisabled();
  48. releaseServerProbe();
  49. await expect(page.locator("#server option")).toContainText("local", { timeout: 10_000 });
  50. await expect(stabilityStartButton(page)).not.toHaveClass(/disabled/);
  51. await expect(stabilityStartButton(page)).toHaveAttribute("aria-disabled", "false");
  52. await expect(stabilityStartButton(page)).toHaveAttribute("title", "");
  53. await expect(page.locator("#server")).toBeEnabled();
  54. });
  55. test("runs a short local measurement and exports CSV data", async ({ page }) => {
  56. await page.goto(`${baseUrls.standalone}/stability.html`);
  57. await expect(page).toHaveTitle("LibreSpeed - Stability Test");
  58. await waitForLocalServer(page, "local");
  59. await setShortDuration(page);
  60. await stabilityStartButton(page).click();
  61. await expect(stabilityStartButton(page)).toHaveClass(/running/);
  62. await expect(page.locator("#durationSelect")).toBeDisabled();
  63. await expect(page.locator("#targetSelect")).toBeDisabled();
  64. await expect(page.locator("#server")).toBeDisabled();
  65. await waitForSamples(page);
  66. await expect(page.locator("#statAvg")).not.toHaveText("", { timeout: 5_000 });
  67. await expect(page.locator("#rating")).not.toHaveText("--", { timeout: 5_000 });
  68. await expect(stabilityStartButton(page)).not.toHaveClass(/running/, { timeout: 10_000 });
  69. await expect(page.locator("#durationSelect")).toBeEnabled();
  70. const [download] = await Promise.all([page.waitForEvent("download"), page.locator("#downloadCsvBtn").click()]);
  71. expect(download.suggestedFilename()).toMatch(/^stability_test_.*\.csv$/);
  72. const csvPath = await download.path();
  73. const csv = fs.readFileSync(csvPath, "utf8");
  74. expect(csv).toContain("elapsed_s,ping_ms,failed\n");
  75. expect(csv.trim().split("\n").length).toBeGreaterThan(1);
  76. });
  77. test("supports threshold display, abort, and reset controls", async ({ page }) => {
  78. await page.goto(`${baseUrls.standalone}/stability.html`);
  79. await waitForLocalServer(page, "local");
  80. await setAlertThreshold(page, 40);
  81. await expect(page.locator("#thresholdValue")).toHaveText("40 ms");
  82. await stabilityStartButton(page).click();
  83. await expect(stabilityStartButton(page)).toHaveClass(/running/);
  84. await waitForSamples(page);
  85. await stabilityStartButton(page).click();
  86. await expect(stabilityStartButton(page)).not.toHaveClass(/running/);
  87. await expect(page.locator("#durationSelect")).toBeEnabled();
  88. await expect(page.locator("#targetSelect")).toBeEnabled();
  89. await page.waitForTimeout(700);
  90. await page.locator("#resetBtn").click();
  91. await expect(page.locator("#rating")).toHaveText("--");
  92. await expect(page.locator("#statAvg")).toHaveText("");
  93. await expect.poll(() => page.evaluate(() => window.allPingData.length)).toBe(0);
  94. await expect.poll(() => page.evaluate(() => window.latestData)).toBeNull();
  95. });
  96. test("loads the configured dual-mode server list", async ({ page }) => {
  97. await page.goto(`${baseUrls.dual}/stability.html`);
  98. await expect(page.locator("#serverArea")).toBeVisible({ timeout: 10_000 });
  99. await expect(page.locator("#server option")).toContainText("Local dual backend", { timeout: 10_000 });
  100. });
  101. test("clears resource timings after measuring a ping", async ({ page }) => {
  102. await page.goto(`${baseUrls.standalone}/stability.html`);
  103. await page.route(`${baseUrls.backend}/empty.php?cors=true&r=*`, route => route.fulfill({ status: 200, headers: { "Access-Control-Allow-Origin": "*" }, body: "" }));
  104. await expect(
  105. page.evaluate(
  106. async ({ source, url }) => {
  107. const instrumentedSource = `
  108. const clearResourceTimings = performance.clearResourceTimings.bind(performance);
  109. performance.clearResourceTimings = () => {
  110. postMessage("resource timings cleared");
  111. clearResourceTimings();
  112. };
  113. ${source}
  114. `;
  115. const worker = new Worker(URL.createObjectURL(new Blob([instrumentedSource], { type: "text/javascript" })));
  116. try {
  117. await new Promise((resolve, reject) => {
  118. const timeout = setTimeout(() => reject(new Error("Resource timings were not cleared")), 10_000);
  119. worker.onmessage = event => {
  120. if (event.data === "resource timings cleared") {
  121. clearTimeout(timeout);
  122. resolve();
  123. }
  124. };
  125. worker.postMessage(`start ${JSON.stringify({ url_ping: url, duration: 1, mpot: true })}`);
  126. });
  127. } finally {
  128. worker.terminate();
  129. }
  130. },
  131. { source: workerSource, url: `${baseUrls.backend}/empty.php` }
  132. )
  133. ).resolves.toBeUndefined();
  134. });
  135. });