AtCoder Easy Test

Make testing sample cases easy

As of 2020-11-13. See the latest version.

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