AtCoder Easy Test

Make testing sample cases easy

נכון ליום 13-11-2020. ראה הגרסה האחרונה.

  1. // ==UserScript==
  2. // @name AtCoder Easy Test
  3. // @namespace http://atcoder.jp/
  4. // @version 0.1.6
  5. // @description Make testing sample cases easy
  6. // @author magurofly
  7. // @match https://atcoder.jp/contests/*/tasks/*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. // This script uses variables from page below:
  12. // * `$`
  13. // * `getSourceCode`
  14. // * `csrfToken`
  15.  
  16. // This scripts consists of three modules:
  17. // * bottom menu
  18. // * code runner
  19. // * view
  20.  
  21. // -- bottom menu --
  22. if (!window.bottomMenu) { var bottomMenu = (function () {
  23. 'use strict';
  24.  
  25. const tabs = new Set();
  26.  
  27. $(() => {
  28. $(`<style>`)
  29. .text(`
  30.  
  31. #bottom-menu-wrapper {
  32. background: transparent;
  33. border: none;
  34. pointer-events: none;
  35. padding: 0;
  36. }
  37.  
  38. #bottom-menu-wrapper>.container {
  39. position: absolute;
  40. bottom: 0;
  41. width: 100%;
  42. padding: 0;
  43. }
  44.  
  45. #bottom-menu-wrapper>.container>.navbar-header {
  46. float: none;
  47. }
  48.  
  49. #bottom-menu-key {
  50. display: block;
  51. float: none;
  52. margin: 0 auto;
  53. padding: 10px 3em;
  54. border-radius: 5px 5px 0 0;
  55. background: #000;
  56. opacity: 0.85;
  57. color: #FFF;
  58. cursor: pointer;
  59. pointer-events: auto;
  60. text-align: center;
  61. }
  62.  
  63. #bottom-menu-key.collapsed:before {
  64. content: "\\e260";
  65. }
  66.  
  67. #bottom-menu-tabs {
  68. padding: 3px 0 0 10px;
  69. }
  70.  
  71. #bottom-menu-tabs a {
  72. pointer-events: auto;
  73. }
  74.  
  75. #bottom-menu {
  76. pointer-events: auto;
  77. background: rgba(0, 0, 0, 0.8);
  78. color: #fff;
  79. max-height: unset;
  80. }
  81.  
  82. #bottom-menu.collapse:not(.in) {
  83. display: none !important;
  84. }
  85.  
  86. #bottom-menu-tabs>li>a {
  87. background: rgba(100, 100, 100, 0.5);
  88. border: solid 1px #ccc;
  89. color: #fff;
  90. }
  91.  
  92. #bottom-menu-tabs>li>a:hover {
  93. background: rgba(150, 150, 150, 0.5);
  94. border: solid 1px #ccc;
  95. color: #333;
  96. }
  97.  
  98. #bottom-menu-tabs>li.active>a {
  99. background: #eee;
  100. border: solid 1px #ccc;
  101. color: #333;
  102. }
  103.  
  104. .bottom-menu-btn-close {
  105. font-size: 8pt;
  106. vertical-align: baseline;
  107. padding: 0 0 0 6px;
  108. margin-right: -6px;
  109. }
  110.  
  111. #bottom-menu-contents {
  112. padding: 5px 15px;
  113. max-height: 50vh;
  114. overflow-y: auto;
  115. }
  116.  
  117. #bottom-menu-contents .panel {
  118. color: #333;
  119. }
  120.  
  121.  
  122.  
  123. #atcoder-easy-test-language {
  124. border: none;
  125. background: transparent;
  126. font: inherit;
  127. color: #fff;
  128. }
  129.  
  130. `)
  131. .appendTo("head");
  132. $(`<div id="bottom-menu-wrapper" class="navbar navbar-default navbar-fixed-bottom">`)
  133. .html(`
  134. <div class="container">
  135. <div class="navbar-header">
  136. <button id="bottom-menu-key" type="button" class="navbar-toggle collapsed glyphicon glyphicon-menu-down" data-toggle="collapse" data-target="#bottom-menu">
  137. </button>
  138. </div>
  139. <div id="bottom-menu" class="collapse navbar-collapse">
  140. <ul id="bottom-menu-tabs" class="nav nav-tabs">
  141. </ul>
  142. <div id="bottom-menu-contents" class="tab-content">
  143. </div>
  144. </div>
  145. </div>
  146. `)
  147. .appendTo("#main-div");
  148. });
  149.  
  150. const menuController = {
  151. addTab(tabId, tabLabel, paneContent, options = {}) {
  152. const tab = $(`<a id="bottom-menu-tab-${tabId}" href="#" data-target="#bottom-menu-pane-${tabId}" data-toggle="tab">`)
  153. .click(e => {
  154. e.preventDefault();
  155. tab.tab("show");
  156. })
  157. .append(tabLabel);
  158. const tabLi = $(`<li>`).append(tab).appendTo("#bottom-menu-tabs");
  159. const pane = $(`<div class="tab-pane" id="bottom-menu-pane-${tabId}">`).append(paneContent).appendTo("#bottom-menu-contents");
  160. const controller = {
  161. close() {
  162. tabLi.remove();
  163. pane.remove();
  164. tabs.delete(tab);
  165. if (tabLi.hasClass("active") && tabs.size > 0) {
  166. tabs.values().next().value.tab("show");
  167. }
  168. },
  169.  
  170. show() {
  171. menuController.show();
  172. tab.tab("show");
  173. }
  174. };
  175. tabs.add(tab);
  176. if (options.closeButton) tab.append($(`<a class="bottom-menu-btn-close btn btn-link glyphicon glyphicon-remove">`).click(() => controller.close()));
  177. if (options.active || tabs.size == 1) tab.tab("show");
  178. return controller;
  179. },
  180.  
  181. show() {
  182. if ($("#bottom-menu-key").hasClass("collapsed")) $("#bottom-menu-key").click();
  183. },
  184. };
  185.  
  186. return menuController;
  187. })(); }
  188.  
  189. // -- code runner --
  190. var codeRunner = (function() {
  191. 'use strict';
  192.  
  193. function buildParams(data) {
  194. return Object.entries(data).map(([key, value]) =>
  195. encodeURIComponent(key) + "=" + encodeURIComponent(value)).join("&");
  196. }
  197.  
  198. function sleep(ms) {
  199. return new Promise(done => setTimeout(done, ms));
  200. }
  201.  
  202. class WandboxRunner {
  203. constructor(name, label, options = {}) {
  204. this.name = name;
  205. this.label = label + " [Wandbox]";
  206. }
  207.  
  208. run(sourceCode, input) {
  209. return this.request(Object.assign(JSON.stringify({
  210. compiler: this.name,
  211. code: sourceCode,
  212. stdin: input,
  213. }), this.options));
  214. }
  215.  
  216. async request(body) {
  217. const startTime = Date.now();
  218. let res;
  219. try {
  220. res = await fetch("https://wandbox.org/api/compile.json", {
  221. method: "POST",
  222. mode: "cors",
  223. headers: {
  224. "Content-Type": "application/json",
  225. },
  226. body,
  227. }).then(r => r.json());
  228. } catch (error) {
  229. return {
  230. status: "IE",
  231. stderr: error,
  232. };
  233. }
  234. const endTime = Date.now();
  235.  
  236. const result = {
  237. status: "OK",
  238. exitCode: res.status,
  239. execTime: endTime - startTime,
  240. stdout: res.program_output,
  241. stderr: res.program_error,
  242. };
  243. if (res.status != 0) {
  244. if (res.signal) {
  245. result.exitCode += " (" + res.signal + ")";
  246. }
  247. result.stdout = (res.compiler_output || "") + (result.stdout || "");
  248. result.stderr = (res.compiler_error || "") + (result.stderr || "");
  249. if (res.compiler_output || res.compiler_error) {
  250. result.status = "CE";
  251. } else {
  252. result.status = "RE";
  253. }
  254. }
  255.  
  256. return result;
  257. }
  258. }
  259.  
  260. class PaizaIORunner {
  261. constructor(name, label) {
  262. this.name = name;
  263. this.label = label + " [PaizaIO]";
  264. }
  265.  
  266. async run(sourceCode, input) {
  267. let id, status, error;
  268. try {
  269. const res = await fetch("https://api.paiza.io/runners/create?" + buildParams({
  270. source_code: sourceCode,
  271. language: this.name,
  272. input,
  273. longpoll: true,
  274. longpoll_timeout: 10,
  275. api_key: "guest",
  276. }), {
  277. method: "POST",
  278. mode: "cors",
  279. }).then(r => r.json());
  280. id = res.id;
  281. status = res.status;
  282. error = res.error;
  283. } catch (error) {
  284. return {
  285. status: "IE",
  286. stderr: error,
  287. };
  288. }
  289.  
  290. while (status == "running") {
  291. const res = await (await fetch("https://api.paiza.io/runners/get_status?" + buildParams({
  292. id,
  293. api_key: "guest",
  294. }), {
  295. mode: "cors",
  296. })).json();
  297. status = res.status;
  298. error = res.error;
  299. }
  300.  
  301. const res = await fetch("https://api.paiza.io/runners/get_details?" + buildParams({
  302. id,
  303. api_key: "guest",
  304. }), {
  305. mode: "cors",
  306. }).then(r => r.json());
  307.  
  308. const result = {
  309. exitCode: res.exit_code,
  310. execTime: +res.time * 1e3,
  311. memory: +res.memory * 1e-3,
  312. };
  313.  
  314. if (res.build_result == "failure") {
  315. result.status = "CE";
  316. result.exitCode = res.build_exit_code;
  317. result.stdout = res.build_stdout;
  318. result.stderr = res.build_stderr;
  319. } else {
  320. result.status = (res.result == "timeout") ? "TLE" : (res.result == "failure") ? "RE" : "OK";
  321. result.exitCode = res.exit_code;
  322. result.stdout = res.stdout;
  323. result.stderr = res.stderr;
  324. }
  325.  
  326. return result;
  327. }
  328. }
  329.  
  330. let waitAtCoderCustomTest = Promise.resolve();
  331. const AtCoderCustomTestBase = location.href.replace(/\/tasks\/.+$/, "/custom_test");
  332. const AtCoderCustomTestResultAPI = AtCoderCustomTestBase + "/json?reload=true";
  333. const AtCoderCustomTestSubmitAPI = AtCoderCustomTestBase + "/submit/json";
  334. class AtCoderRunner {
  335. constructor(languageId, label) {
  336. this.languageId = languageId;
  337. this.label = label + " [AtCoder]";
  338. }
  339.  
  340. async run(sourceCode, input) {
  341. const promise = this.submit(sourceCode, input);
  342. waitAtCoderCustomTest = promise;
  343. return await promise;
  344. }
  345.  
  346. async submit(sourceCode, input) {
  347. try {
  348. await waitAtCoderCustomTest;
  349. } catch (error) {
  350. console.error(error);
  351. }
  352.  
  353. const error = await fetch(AtCoderCustomTestSubmitAPI, {
  354. method: "POST",
  355. credentials: "include",
  356. headers: {
  357. "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
  358. },
  359. body: buildParams({
  360. "data.LanguageId": this.languageId,
  361. sourceCode,
  362. input,
  363. csrf_token: csrfToken,
  364. }),
  365. }).then(r => r.text());
  366.  
  367. if (error) {
  368. throw new Error(error)
  369. }
  370.  
  371. await sleep(100);
  372.  
  373. for (;;) {
  374. const data = await fetch(AtCoderCustomTestResultAPI, {
  375. method: "GET",
  376. credentials: "include",
  377. }).then(r => r.json());
  378.  
  379. if (!("Result" in data)) continue;
  380. const result = data.Result;
  381.  
  382. if ("Interval" in data) {
  383. await sleep(data.Interval);
  384. continue;
  385. }
  386.  
  387. return {
  388. status: (result.ExitCode == 0) ? "OK" : (result.TimeConsumption == -1) ? "CE" : "RE",
  389. exitCode: result.ExitCode,
  390. execTime: result.TimeConsumption,
  391. memory: result.MemoryConsumption,
  392. stdout: data.Stdout,
  393. stderr: data.Stderr,
  394. };
  395. }
  396. }
  397. }
  398.  
  399. const runners = {
  400. 4001: new WandboxRunner("gcc-9.2.0-c", "C (GCC 9.2.0)"),
  401. 4002: new PaizaIORunner("c", "C (C17 / Clang 10.0.0)"),
  402. 4003: new WandboxRunner("gcc-9.2.0", "C++ (GCC 9.2.0)"),
  403. 4004: new WandboxRunner("clang-10.0.0", "C++ (Clang 10.0.0)"),
  404. 4006: new PaizaIORunner("python3", "Python (3.8.2)"),
  405. 4007: new PaizaIORunner("bash", "Bash (5.0.17)"),
  406. 4010: new WandboxRunner("csharp", "C# (.NET Core 6.0.100-alpha.1.20562.2)"),
  407. 4011: new WandboxRunner("mono-head", "C# (Mono-mcs 5.19.0.0)"),
  408. 4012: new PaizaIORunner("csharp", "C# (Mono-mcs 6.8.0.105)"),
  409. 4013: new PaizaIORunner("clojure", "Clojure (1.10.1-1)"),
  410. 4015: new PaizaIORunner("d", "D (LDC 1.23.0)"),
  411. 4016: new PaizaIORunner("d", "D (LDC 1.23.0)"),
  412. 4017: new PaizaIORunner("d", "D (LDC 1.23.0)"),
  413. 4020: new PaizaIORunner("erlang", "Erlang (10.6.4)"),
  414. 4021: new PaizaIORunner("elixir", "Elixir (1.10.4)"),
  415. 4022: new PaizaIORunner("fsharp", "F# (Interactive 4.0)"),
  416. 4023: new PaizaIORunner("fsharp", "F# (Interactive 4.0)"),
  417. 4026: new WandboxRunner("go-1.14.1", "Go (1.14.1)"),
  418. 4027: new WandboxRunner("ghc-head", "Haskell (GHC 8.7.20181121)"),
  419. 4030: new PaizaIORunner("javascript", "JavaScript (Node.js 12.18.3)"),
  420. 4032: new PaizaIORunner("kotlin", "Kotlin (1.4.0)"),
  421. 4033: new WandboxRunner("lua-5.3.4", "Lua (Lua 5.3.4)"),
  422. 4034: new WandboxRunner("luajit-head", "Lua (LuaJIT 2.1.0-beta3)"),
  423. 4035: new PaizaIORunner("bash", "Bash (5.0.17)"),
  424. 4036: new WandboxRunner("nim-1.0.6", "Nim (1.0.6)"),
  425. 4037: new PaizaIORunner("objective-c", "Objective-C (Clang 10.0.0)"),
  426. 4036: new WandboxRunner("ocaml-head", "OCaml (4.13.0+dev0-2020-10-19)"),
  427. 4041: new WandboxRunner("fpc-3.0.2", "Pascal (FPC 3.0.2)"),
  428. 4042: new PaizaIORunner("perl", "Perl (5.30.0)"),
  429. 4043: new PaizaIORunner("perl", "Perl (5.30.0)"),
  430. 4044: new PaizaIORunner("php", "PHP (7.4.10)"),
  431. 4046: new WandboxRunner("pypy-head", "PyPy2 (7.3.4-alpha0)"),
  432. 4047: new WandboxRunner("pypy-7.2.0-3", "PyPy3 (7.2.0)"),
  433. 4049: new PaizaIORunner("ruby", "Ruby (2.7.1)"),
  434. 4050: new WandboxRunner("rust-head", "Rust (1.37.0-dev)"),
  435. 4051: new PaizaIORunner("scala", "Scala (2.13.3)"),
  436. 4053: new PaizaIORunner("scheme", "Scheme (Gauche 0.9.6)"),
  437. 4055: new PaizaIORunner("swift", "Swift (5.2.5)"),
  438. 4056: {
  439. label: "Text (JavaScript)",
  440. name: "text",
  441. async run(sourceCode, input) {
  442. return {
  443. status: "OK",
  444. exitCode: 0,
  445. stdout: sourceCode,
  446. };
  447. },
  448. },
  449. 4057: new WandboxRunner("typescript-3.8.3", "TypeScript (3.8.3)", {
  450. "compiler-option-raw": `--types=node --moduleResolution=node --lib=esnext`,
  451. "runtime-option-raw": `--types=node --moduleResolution=node --lib=esnext`,
  452. }),
  453. 4058: new PaizaIORunner("vb", "Visual Basic (.NET Core 4.0.1)"),
  454. 4060: new PaizaIORunner("cobol", "COBOL - Free (OpenCOBOL 2.2.0)"),
  455. 4061: new PaizaIORunner("cobol", "COBOL - Free (OpenCOBOL 2.2.0)"),
  456. 4067: new WandboxRunner("vim-head", "Vim (8.2.1975)"),
  457. };
  458.  
  459. $("#select-lang option[value]").each((_, e) => {
  460. const elem = $(e);
  461. const languageId = elem.val();
  462. if (languageId in runners) return;
  463. runners[languageId] = new AtCoderRunner(languageId, elem.text());
  464. });
  465.  
  466. return {
  467. run(languageId, sourceCode, input) {
  468. if (!(languageId in runners)) {
  469. return Promise.reject("language not supported");
  470. }
  471. return runners[languageId].run(sourceCode, input);
  472. },
  473.  
  474. getEnvironment(languageId) {
  475. if (!(languageId in runners)) {
  476. return Promise.reject("language not supported");
  477. }
  478. return Promise.resolve(runners[languageId].label);
  479. },
  480. };
  481. })();
  482.  
  483. (function () {
  484. function setLanguage() {
  485. const languageId = $("#select-lang>select").val();
  486. codeRunner.getEnvironment(languageId).then(label => {
  487. $("#atcoder-easy-test-language").css("color", "#fff").val(label);
  488. $("#atcoder-easy-test-run").removeClass("disabled");
  489. $("#atcoder-easy-test-btn-test-all").attr("disabled", false);
  490. }, error => {
  491. $("#atcoder-easy-test-language").css("color", "#f55").val(error);
  492. $("#atcoder-easy-test-run").addClass("disabled");
  493. $("#atcoder-easy-test-btn-test-all").attr("disabled", true);
  494. });
  495. }
  496. setLanguage();
  497.  
  498. async function runTest(input, title = "") {
  499. const uid = Date.now().toString();
  500. title = title ? "Result " + title : "Result";
  501. const content = $(`<div class="container">`)
  502. .html(`
  503. <div class="row"><div class="form-group">
  504. <label class="control-label col-sm-2" for="atcoder-easy-test-${uid}-stdin">Standard Input</label>
  505. <div class="col-sm-8">
  506. <textarea id="atcoder-easy-test-${uid}-stdin" class="form-control" rows="5" readonly></textarea>
  507. </div>
  508. </div></div>
  509. <div class="row"><div class="col-sm-4 col-sm-offset-4">
  510. <div class="panel panel-default"><table class="table table-bordered">
  511. <tr id="atcoder-easy-test-${uid}-row-exit-code">
  512. <th class="text-center">Exit Code</th>
  513. <td id="atcoder-easy-test-${uid}-exit-code" class="text-right"></td>
  514. </tr>
  515. <tr id="atcoder-easy-test-${uid}-row-exec-time">
  516. <th class="text-center">Exec Time</th>
  517. <td id="atcoder-easy-test-${uid}-exec-time" class="text-right"></td>
  518. </tr>
  519. <tr id="atcoder-easy-test-${uid}-row-memory">
  520. <th class="text-center">Memory</th>
  521. <td id="atcoder-easy-test-${uid}-memory" class="text-right"></td>
  522. </tr>
  523. </table></div>
  524. </div></div>
  525. <div class="row"><div class="form-group">
  526. <label class="control-label col-sm-2" for="atcoder-easy-test-${uid}-stdout">Standard Output</label>
  527. <div class="col-sm-8">
  528. <textarea id="atcoder-easy-test-${uid}-stdout" class="form-control" rows="5" readonly></textarea>
  529. </div>
  530. </div></div>
  531. <div class="row"><div class="form-group">
  532. <label class="control-label col-sm-2" for="atcoder-easy-test-${uid}-stderr">Standard Error</label>
  533. <div class="col-sm-8">
  534. <textarea id="atcoder-easy-test-${uid}-stderr" class="form-control" rows="5" readonly></textarea>
  535. </div>
  536. </div></div>
  537. `);
  538. const tab = bottomMenu.addTab("easy-test-result-" + uid, title, content, { active: true, closeButton: true });
  539. $(`#atcoder-easy-test-${uid}-stdin`).val(input);
  540.  
  541. const result = await codeRunner.run($("#select-lang>select").val(), getSourceCode(), input);
  542.  
  543. $(`#atcoder-easy-test-${uid}-row-exit-code`).toggleClass("bg-danger", result.exitCode != 0).toggleClass("bg-success", result.exitCode == 0);
  544. $(`#atcoder-easy-test-${uid}-exit-code`).text(result.exitCode);
  545. if ("execTime" in result) $(`#atcoder-easy-test-${uid}-exec-time`).text(result.execTime + " ms");
  546. if ("memory" in result) $(`#atcoder-easy-test-${uid}-memory`).text(result.memory + " KB");
  547. $(`#atcoder-easy-test-${uid}-stdout`).val(result.stdout);
  548. $(`#atcoder-easy-test-${uid}-stderr`).val(result.stderr);
  549.  
  550. result.uid = uid;
  551. result.tab = tab;
  552. return result;
  553. }
  554.  
  555. bottomMenu.addTab("easy-test", "Easy Test", $(`<form id="atcoder-easy-test-container" class="form-horizontal">`)
  556. .html(`
  557. <div class="row">
  558. <div class="col-12 col-md-10">
  559. <div class="form-group">
  560. <label class="control-label col-sm-2">Test Environment</label>
  561. <div class="col-sm-8">
  562. <input id="atcoder-easy-test-language" class="form-control" readonly>
  563. </div>
  564. </div>
  565. </div>
  566. </div>
  567. <div class="row">
  568. <div class="col-12 col-md-10">
  569. <div class="form-group">
  570. <label class="control-label col-sm-2" for="atcoder-easy-test-input">Standard Input</label>
  571. <div class="col-sm-8">
  572. <textarea id="atcoder-easy-test-input" name="input" class="form-control" rows="5"></textarea>
  573. </div>
  574. </div>
  575. </div>
  576. <div class="col-12 col-md-4">
  577. <label class="control-label col-sm-2"></label>
  578. <div class="form-group">
  579. <div class="col-sm-8">
  580. <a id="atcoder-easy-test-run" class="btn btn-primary">Run</a>
  581. </div>
  582. </div>
  583. </div>
  584. </div>
  585. `), { active: true });
  586. $("#atcoder-easy-test-run").click(() => runTest($("#atcoder-easy-test-input").val()));
  587. $("#select-lang>select").on("change", () => setLanguage());
  588.  
  589. const testfuncs = [];
  590.  
  591. const testcases = $(".lang>span:nth-child(1) .div-btn-copy+pre[id]").toArray();
  592. for (let i = 0; i < testcases.length; i += 2) {
  593. const input = $(testcases[i]), output = $(testcases[i+1]);
  594. const testfunc = async () => {
  595. const title = input.closest(".part").find("h3")[0].childNodes[0].data;
  596. const result = await runTest(input.text(), title);
  597. if (result.status == "OK") {
  598. if (result.stdout.trim() == output.text().trim()) {
  599. $(`#atcoder-easy-test-${result.uid}-stdout`).addClass("bg-success");
  600. result.status = "AC";
  601. } else {
  602. result.status = "WA";
  603. }
  604. }
  605. return result;
  606. };
  607. testfuncs.push(testfunc);
  608.  
  609. const runButton = $(`<a class="btn btn-primary btn-sm" style="vertical-align: top; margin-left: 0.5em">`)
  610. .text("Run")
  611. .click(async () => {
  612. await testfunc();
  613. if ($("#bottom-menu-key").hasClass("collapsed")) $("#bottom-menu-key").click();
  614. });
  615. input.closest(".part").find(".btn-copy").eq(0).after(runButton);
  616. }
  617.  
  618. const testAllResultRow = $(`<div class="row">`);
  619. const testAllButton = $(`<a id="atcoder-easy-test-btn-test-all" class="btn btn-default btn-sm" style="margin-left: 5px">`)
  620. .text("Test All Samples")
  621. .click(async () => {
  622. if (testAllButton.attr("disabled")) throw new Error("Button is disabled");
  623. const statuses = testfuncs.map(_ => $(`<div class="label label-default" style="margin: 3px">`).text("WJ..."));
  624. const progress = $(`<div class="progress-bar">`).text(`0 / ${testfuncs.length}`);
  625. let finished = 0;
  626. const closeButton = $(`<button type="button" class="close" data-dismiss="alert" aria-label="close">`)
  627. .append($(`<span aria-hidden="true">`).text("\xd7"));
  628. const resultAlert = $(`<div class="alert alert-dismissible">`)
  629. .append(closeButton)
  630. .append($(`<div class="progress">`).append(progress))
  631. .append(...statuses)
  632. .appendTo(testAllResultRow);
  633. const results = await Promise.all(testfuncs.map(async (testfunc, i) => {
  634. const result = await testfunc();
  635. finished++;
  636. progress.text(`${finished} / ${statuses.length}`).css("width", `${finished/statuses.length*100}%`);
  637. statuses[i].toggleClass("label-success", result.status == "AC").toggleClass("label-warning", result.status != "AC").text(result.status).click(() => result.tab.show()).css("cursor", "pointer");
  638. return result;
  639. }));
  640. if (results.every(({status}) => status == "AC")) {
  641. resultAlert.addClass("alert-success");
  642. } else {
  643. resultAlert.addClass("alert-warning");
  644. }
  645. closeButton.click(() => {
  646. for (const {tab} of results) {
  647. tab.close();
  648. }
  649. });
  650. });
  651. $("#submit").after(testAllButton).closest("form").append(testAllResultRow);
  652. })();