MediaWiki:Gadget-tidy-test.js

Definition från Wiktionary, den fria ordlistan.
Hoppa till navigering Hoppa till sök

OBS: Efter du har publicerat sidan kan du behöva tömma din webbläsares cache för att se ändringarna.

  • Firefox / Safari: Håll ned Skift och klicka på Uppdatera sidan eller tryck Ctrl-F5 eller Ctrl-R (⌘-R på Mac)
  • Google Chrome: Tryck Ctrl-Skift-R (⌘-Skift-R på Mac)
  • Internet Explorer / Edge: Håll ned Ctrl och klicka på Uppdatera eller tryck Ctrl-F5
  • Opera: Tryck Ctrl-F5.
// @ts-check

/**
 * @typedef {import("./tidy.js").MediaWiki} MediaWiki
 * @typedef {import("./tidy.js").JQuery} JQuery
 * @typedef {import("./tidy.js").JQueryStatic} JQueryStatic
 * @typedef {import("./tidy.js").TidyExports} TidyExports
 * @typedef {import("./tidy-on-save.js").Context} Context
 *
 * @typedef {{
 *  success: true;
 *  expected: unknown;
 *  actual: unknown;
 * }} OkTestResult
 * @typedef {{
 *  success: false;
 *  expected: unknown;
 *  actual: unknown;
 *  error: JQuery;
 * }} FailTestResult
 * @typedef {OkTestResult | FailTestResult} TestResult
 */

/** @type {MediaWiki} */
var mw = /** @type {*} */ (globalThis).mw;

/** @type {JQueryStatic} */
var $ = /** @type {*} */ (globalThis).$;

/** @type {TidyExports | undefined} */
var tidy;

if (mw.config.get("wgPageName") === "Wiktionary:Finesser/Tidy/Test") {
  mw.loader.using(["ext.gadget.tidy"]).then(function () {
    tidy = globalThis.tidy;
    var onlyPerf = location.search === "?perf";

    if (!onlyPerf) {
      mw.hook("wikipage.content").add(function (parent) {
        $(".tidy-tests-toolbar").remove();
        var toolbar = $("<div>", {
          class: "tidy-tests-toolbar",
          text: "Kör...",
        })
          .css(commonCss)
          .css({ padding: "1em", display: "inline-block" });
        parent.prepend(toolbar);

        setTimeout(function () {
          runTests(parent, toolbar);
        }, 20);
      });
    }

    mw.hook("wikipage.content").add(runPerformanceTests);

    mw.hook("wikipage.content").add(showErrorReporting);
  });
}

var commonCss = {
  border: "1px solid #009",
  marginBottom: "-1px",
  paddingLeft: "0.5em",
};
var okCss = {
  border: "1px solid #090",
  borderLeftWidth: "0.5em",
  background: "#efe",
  position: "static",
  zIndex: 0,
};
var failCss = {
  border: "0.5em double #900",
  background: "#fee",
  position: "relative",
  zIndex: 1,
};

/**
 * @param {JQuery} parent
 * @param {JQuery} toolbar
 */
function runTests(parent, toolbar) {
  var okCount = 0;
  var failCount = 0;

  parent.find("[data-test-case]").each(function (_i, testCase) {
    var $testCase = $(testCase);
    var res = runTestCase($testCase);

    $testCase.on("click", function () {
      console.log({
        expected: res.expected,
        actual: res.actual,
      });
    });

    if (res.success) {
      okCount++;
      $testCase.css(okCss).addClass("tidy-test-ok");
    } else {
      failCount++;
      $testCase.css(failCss).append(res.error);
    }
  });

  if (okCount === 0 && failCount === 0) {
    toolbar.remove();
    return;
  }

  var showSuccessful = true;
  var sessionStorageKey = "tidy-test:showSuccessful";
  var toggleButton = $("<button>", { text: "Dölj lyckade" }).on(
    "click",
    function () {
      showSuccessful = !showSuccessful;
      $(".tidy-test-ok").toggle();
      $(this).text(showSuccessful ? "Dölj lyckade" : "Visa lyckade");

      sessionStorage.setItem(sessionStorageKey, "" + showSuccessful);
    }
  );
  if (failCount && sessionStorage.getItem(sessionStorageKey) === "false") {
    toggleButton.trigger("click");
  }
  toolbar
    .text("")
    .css(failCount ? failCss : okCss)
    .append(
      okCount + "/" + (okCount + failCount) + " test lyckades",
      failCount ? [" ", toggleButton] : ""
    );
}

/**
 * @param {JQuery} $testCase
 * @return {TestResult}
 */
function runTestCase($testCase) {
  var before = trimEnd($testCase.find("[data-test-before] pre").text());
  var after = trimEnd($testCase.find("[data-test-after] pre").text());
  var warnings = Array.from($testCase.find("[data-test-warnings] li"))
    .map(function (x) {
      return x.textContent || "";
    })
    .sort();
  var transformCats = $testCase
    .find("[data-test-transformation-category] li")
    .text()
    .trim()
    .split(", ")
    .filter(Boolean);
  var key = $testCase.find("[data-test-key]").text();

  if (
    before &&
    after &&
    // `warnings` is optional.
    // `transformCats` is optional.
    !key
  ) {
    return runTestCaseOnSave({
      before: before,
      after: after,
      warnings: warnings,
      transformCats: transformCats,
    });
  }

  if (before && after && key && !warnings.length && !transformCats.length) {
    return runTestCaseOnKeydown({
      before: before,
      after: after,
      key: key,
    });
  }

  return {
    success: false,
    expected: {
      before: before,
      after: after,
      warnings: warnings,
      transformCats: transformCats,
      key: key,
    },
    actual: undefined,
    error: $("<ul>").append(
      $("<li>", {
        text: 'För att testa "vid sparning" krävs före= och efter=.',
      }),
      $("<li>", {
        text: 'För att testa "medan man skriver" krävs före=, efter= och tangent=.',
      })
    ),
  };
}

/**
 * @param {{
 *  before: string;
 *  after: string;
 *  warnings: string[];
 *  transformCats: string[];
 * }} expected
 * @return {TestResult}
 */
function runTestCaseOnSave(expected) {
  if (!tidy) throw new Error("tidy invariant");

  /**
   * @type {{
   *  wikitext: string;
   *  warnings: string[];
   *  transformCats: string[];
   *  wikitextSecondRun: string;
   * }}
   */
  var actual;

  try {
    var context = tidy.processOnSave(expected.before, tidy.getData());
    var actualWikitext = trimEnd(context.unopaque(context.wikitext));

    var context2 = tidy.processOnSave(actualWikitext, tidy.getData());
    var actualWikitext2 = trimEnd(context2.unopaque(context2.wikitext));

    if (actualWikitext === actualWikitext2) {
      actualWikitext2 = "ingen ytterligare förändring";
    }

    actual = {
      wikitext: actualWikitext,
      warnings: Array.from(context.warnings).sort(),
      transformCats: context.transformCats.sort(),
      wikitextSecondRun: actualWikitext2,
    };

    if (!actual.transformCats.length) {
      actual.transformCats.push("ingen");
    }
  } catch (e) {
    actual = {
      wikitext: "ERROR",
      warnings: [String(e)],
      transformCats: ["ERROR"],
      wikitextSecondRun: "",
    };
  }

  if (actual.wikitext === expected.before) {
    actual.wikitext = "ingen förändring";
  }

  var wikitextOk, warningsOk, transformCatsOk, secondRunOk;
  wikitextOk = actual.wikitext === expected.after;
  warningsOk = actual.warnings.join() === expected.warnings.join();

  if (expected.transformCats.length) {
    transformCatsOk =
      actual.transformCats.join() === expected.transformCats.join();
  } else {
    transformCatsOk = true;
  }

  secondRunOk = actual.wikitextSecondRun === "ingen ytterligare förändring";

  if (wikitextOk && warningsOk && transformCatsOk && secondRunOk) {
    return {
      success: true,
      expected: expected,
      actual: actual,
    };
  }

  if (!actual.warnings.length) {
    actual.warnings = ["Inga"];
  }

  return {
    success: false,
    expected: expected,
    actual: actual,
    error: $("<div>").append(
      wikitextOk ? "✅" : "❌",
      " Faktiskt resultat",
      $("<pre>", {
        text: actual.wikitext,
      }),
      warningsOk ? "✅" : "❌",
      " Faktiska varningar",
      $("<ul>").append(
        actual.warnings.map(function (x) {
          return $("<li>", { text: x });
        })
      ),
      expected.transformCats.length === 0
        ? "➖"
        : transformCatsOk
        ? "✅"
        : "❌",
      " Faktiska transformationskategorier",
      $("<ul>").append($("<li>", { text: actual.transformCats.join(", ") })),
      secondRunOk ? "✅" : "❌",
      " Körning igen",
      $("<pre>", { text: actual.wikitextSecondRun })
    ),
  };
}

/**
 * @param {{
 *  before: string;
 *  after: string;
 *  key: string;
 * }} expected
 * @return {TestResult}
 */
function runTestCaseOnKeydown(expected) {
  if (!tidy) throw new Error("tidy invariant");

  var CURSOR = "<markör>";

  var cursor = {
    before: expected.before.indexOf(CURSOR),
    after: expected.after.indexOf(CURSOR),
  };

  if (
    cursor.before === -1 ||
    (cursor.after === -1 && expected.after !== "vanlig inmatning")
  ) {
    return {
      success: false,
      expected: expected,
      actual: undefined,
      error: $("<div>", {
        text:
          "före= måste innehålla " +
          CURSOR +
          ". efter= måste innehålla " +
          CURSOR +
          ' eller vara "vanlig inmatning".',
      }),
    };
  }

  var actualInserted = tidy.processOnKeydown(
    expected.key,
    function () {
      return {
        text: expected.before.replace(CURSOR, ""),
        cursor: cursor.before,
      };
    },
    tidy.getData()
  );

  var actual =
    actualInserted === undefined
      ? "vanlig inmatning"
      : expected.before.slice(0, cursor.before) +
        actualInserted[0] +
        CURSOR +
        actualInserted[1] +
        expected.before.slice(cursor.before + CURSOR.length);

  if (actual === expected.after) {
    return { success: true, expected: expected, actual: actual };
  }

  return {
    success: false,
    expected: expected,
    actual: actual,
    error: $("<div>").append(
      "❌ Faktiskt resultat",
      $("<pre>", { text: actual })
    ),
  };
}

/**
 * @param {JQuery} parent
 * @param {"start"} [autoStart]
 */
function runPerformanceTests(parent, autoStart) {
  /** @type {TidyExports} */
  var tidy = /** @type {*} */ (globalThis).tidy;
  var data = tidy.getData();
  /** Class for elements added by the perf test. Will be removed if test is restarted. */
  var addedClass = "tidy-perf-test-added";

  parent.find("." + addedClass).remove();

  /** @type {number} */
  var initTime = 0;

  var startButton = $("<button>", { text: "Starta" }).on("click", start);
  var perfOnlyLink = $("<a>", { text: "Endast prestandatest" }).prop({
    href: location.pathname + "?perf" + location.hash,
  });
  var toolbar = $("<p>")
    .addClass(addedClass)
    .append(startButton, " ", perfOnlyLink);
  parent.find("[data-test-performance]").prepend(toolbar);

  if (autoStart === "start") {
    start();
  }

  function start() {
    startButton.prop({ disabled: true });
    var links = Array.from(parent.find("[data-test-performance] li a"));
    getPages(links).then(function (pages) {
      initTime = performance.now();
      process(pages, 0, 0);
    });
  }

  /**
   * @typedef {{
   *  output: JQuery;
   *  wikitext?: string;
   *  processCount: number;
   *  processTime: number;
   *  maxProcessTime: number;
   * }} PerfPage
   *
   * @param {HTMLElement[]} links
   * @returns {Promise<PerfPage[]>}
   */
  function getPages(links) {
    return Promise.all(
      links.map(function (link) {
        return fetch(
          "https://sv.wiktionary.org/w/rest.php/v1/page/" +
            encodeURIComponent(link.title)
        )
          .then(function (response) {
            return response.json();
          })
          .then(function (json) {
            return json.source;
          })
          .catch(function () {})
          .then(function (source) {
            var output = $("<div>").addClass(addedClass);
            $(link).parent().append(output);
            return {
              output: output,
              wikitext: source,
              processCount: 0,
              processTime: 0,
              maxProcessTime: 0,
            };
          });
      })
    );
  }

  /**
   * @param {PerfPage[]} pages
   * @param {number} index
   * @param {number} count
   */
  function process(pages, index, count) {
    var maxDelay = 40;
    var totalCount = 500;

    var now = Date.now();
    for (; index < pages.length; index++, count = 0) {
      var page = pages[index];
      if (!page.wikitext) {
        page.output.text("Kunde inte ladda sidan");
        continue;
      }

      for (; count < totalCount; count++) {
        if (now + maxDelay < Date.now()) {
          // Allow the browser to regain control.
          setTimeout(function () {
            process(pages, index, count);
          });
          return;
        }

        var exactNow = performance.now();
        var context = tidy.processOnSave(page.wikitext, data);
        var diff = performance.now() - exactNow;
        page.processCount++;
        page.processTime += diff;
        page.maxProcessTime = Math.max(page.maxProcessTime, diff);
        page.output.text(
          [
            page.processCount + " körningar",
            "max " + round(page.maxProcessTime) + " ms",
            "genomsnitt " + round(page.processTime / page.processCount) + " ms",
            context.warnings.size && "obs: varningar",
          ]
            .filter(Boolean)
            .join(" - ")
        );
      }
    }

    // The whole performance test is complete now.
    toolbar.text("").append(
      $("<button>", { text: "Start om" }).on("click", function () {
        parent.find("." + addedClass).remove();
        runPerformanceTests(parent, "start");
      }),
      " ",
      perfOnlyLink,
      " Testkörning: " + round((performance.now() - initTime) / 1000) + " sek"
    );
  }
}

/** @param {JQuery} parent */
function showErrorReporting(parent) {
  var opts = mw.user.options.get();

  var enabledGadgets = Object.entries(opts)
    .filter(function (x) {
      return x[0].startsWith("gadget-") && normalizeOpt(x[1]) === "ja";
    })
    .map(function (x) {
      return x[0].replace("gadget-", "");
    })
    .join(", ");

  var data = [
    { name: "Tema", val: opts.skin },
    {
      name: "Liveförhandsgranskning",
      long: "Förhandsgranskning utan att uppdatera sidan",
      val: normalizeOpt(opts.uselivepreview),
    },
    { name: "Syntaxmarkering", val: normalizeOpt(opts.usecodemirror) },
    {
      name: "Nytt wikitextläge",
      val: normalizeOpt(opts["visualeditor-newwikitext"]),
    },
    { long: "Finesser", val: enabledGadgets },
    { long: "Webbläsare", val: navigator.userAgent },
  ];

  var copyable = data
    .map(function (x) {
      return (x.name ? x.name + ":" : "") + x.val;
    })
    .join(", ");

  var copyButton = $("<button>", { text: "Kopiera" }).on("click", function () {
    navigator.clipboard.writeText(copyable);
    copyButton.text("Kopierat");
    setTimeout(function () {
      copyButton.text("Kopiera");
    }, 1000);
  });

  parent
    .find("#error-reporting")
    .text("")
    .css(commonCss)
    .css({ padding: "1em", display: "inline-block" })
    .append(
      $("<div>")
        .append(
          "Skicka också med följande information om din webbläsare och dina inställningar vid felrapportering ",
          copyButton,
          $("<ul>")
            .css({ fontSize: "0.8em" })
            .append(
              data.map(function (x) {
                return $("<li>", { text: (x.long || x.name) + ": " + x.val });
              })
            )
        )
        .prop({ title: copyable })
    );

  /** @param {*} value */
  function normalizeOpt(value) {
    return [0, 1, "0", "1"].includes(value) ? (+value ? "ja" : "nej") : value;
  }
}

/** @param {number} num */
function round(num) {
  return Math.round(100 * num) / 100;
}

/**
 * Trim final newlines.
 * @param {string} str
 */
function trimEnd(str) {
  return str.replace(/\n+$/, "");
}