AtCoder Easy Test

Make testing sample cases easy

스크립트 설치?
개발자의 추천 스크립트

ac-predictor는 어떤가요?

스크립트 설치
질문, 리뷰하거나, 이 스크립트를 신고하세요.
// ==UserScript==
// @name         AtCoder Easy Test
// @namespace    http://atcoder.jp/
// @version      1.8.2
// @description  Make testing sample cases easy
// @author       magurofly
// @match        https://atcoder.jp/contests/*/tasks/*
// @grant        unsafeWindow
// ==/UserScript==

// This script uses variables from page below:
// * `$`
// * `getSourceCode`
// * `csrfToken`

// This scripts consists of three modules:
// * bottom menu
// * code runner
// * view

// This scripts may load scripts below to run code:
// * https://cdn.jsdelivr.net/gh/pythonpad/brython-runner/lib/brython-runner.bundle.js

(function script() {

const VERSION = "1.8.2";

if (typeof unsafeWindow !== "undefined") {
    console.log(unsafeWindow);
    unsafeWindow.eval(`(${script})();`);
    console.log("Script run in unsafeWindow");
    return;
}
if (typeof window === "undefined") {
    this.window = this.unsafeWindow;
}
const $ = window.$;
const getSourceCode = window.getSourceCode;
const csrfToken = window.csrfToken;

const $id = document.getElementById.bind(document);
const $select = document.querySelector.bind(document);
const $selectAll = document.querySelectorAll.bind(document);
const $create = (tagName, attrs = {}, children = []) => {
    const e = document.createElement(tagName);
    for (const name in attrs) e.setAttribute(name, attrs[name]);
    for (const child of children) e.appendChild(child);
    return e;
};

// -- code saver --
const codeSaver = {
    LIMIT: 10,
    get() {
        let data = localStorage.AtCoderEasyTest$lastCode;
        try {
            if (typeof data == "string") {
                data = JSON.parse(data);
            } else {
                data = [];
            }
        } catch(e) {
            data = [{
                path: localStorage.AtCoderEasyTest$lastPage,
                code: data,
            }];
        }
        return data;
    },
    set(data) {
        localStorage.AtCoderEasyTest$lastCode = JSON.stringify(data);
    },
    // @param code to save
    save(code) {
        let data = this.get();
        const idx = data.findIndex(({path}) => path == location.pathname);
        if (idx != -1) data.splice(idx, idx + 1);
        data.push({
            path: location.pathname,
            code,
        });
        while (data.length > this.LIMIT) data.shift();
        this.set(data);
    },
    // @return promise(code)
    restore() {
        const data = this.get();
        const idx = data.findIndex(({path}) => path == location.pathname);
        if (idx == -1 || !(data[idx] instanceof Object)) return Promise.reject(`no saved code found for ${location.pathname}`);
        return Promise.resolve(data[idx].code);
    },
};

// -- code runner --
const codeRunner = (function() {
    'use strict';

    function buildParams(data) {
        return Object.entries(data).map(([key, value]) => encodeURIComponent(key) + "=" + encodeURIComponent(value)).join("&");
    }

    function sleep(ms) {
        return new Promise(done => setTimeout(done, ms));
    }

    class CodeRunner {
        constructor(label, site) {
            this.label = `${label} [${site}]`;
        }

        async test(sourceCode, input, supposedOutput, options) {
            const result = await this.run(sourceCode, input);
            if (result.status != "OK" || typeof supposedOutput !== "string") return result;
            let output = result.stdout || "";

            if (options.trim) {
                supposedOutput = supposedOutput.trim();
                output = output.trim();
            }

            let equals = (x, y) => x === y;

            if ("allowableError" in options) {
                const floatPattern = /^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$/;
                const superEquals = equals;
                equals = (x, y) => {
                    if (floatPattern.test(x) && floatPattern.test(y)) return Math.abs(parseFloat(x) - parseFloat(y)) <= options.allowableError;
                    return superEquals(x, y);
                }
            }

            if (options.split) {
                const superEquals = equals;
                equals = (x, y) => {
                    x = x.split(/\s+/);
                    y = y.split(/\s+/);
                    if (x.length != y.length) return false;
                    const len = x.length;
                    for (let i = 0; i < len; i++) {
                        if (!superEquals(x[i], y[i])) return false;
                    }
                    return true;
                }
            }

            result.status = equals(output, supposedOutput) ? "AC" : "WA";

            return result;
        }
    }

    class CustomRunner extends CodeRunner {
        constructor(label, run) {
            super(label, "Browser");
            this.run = run;
        }
    }

    class WandboxRunner extends CodeRunner {
        constructor(name, label, options = {}) {
            super(label, "Wandbox");
            this.name = name;
            this.options = options;
        }

        run(sourceCode, input) {
            let options = this.options;
            if (typeof options == "function") options = options(sourceCode, input);
            return this.request(Object.assign(JSON.stringify({
                compiler: this.name,
                code: sourceCode,
                stdin: input,
            }), this.options));
        }

        async request(body) {
            const startTime = Date.now();
            let res;
            try {
                res = await fetch("https://wandbox.org/api/compile.json", {
                    method: "POST",
                    mode: "cors",
                    headers: {
                        "Content-Type": "application/json",
                    },
                    body,
                }).then(r => r.json());
            } catch (error) {
                console.error(error);
                return {
                    status: "IE",
                    stderr: error,
                };
            }
            const endTime = Date.now();

            const result = {
                status: "OK",
                exitCode: res.status,
                execTime: endTime - startTime,
                stdout: res.program_output,
                stderr: res.program_error,
            };
            if (res.status != 0) {
                if (res.signal) {
                    result.exitCode += " (" + res.signal + ")";
                }
                result.stdout = (res.compiler_output || "") + (result.stdout || "");
                result.stderr = (res.compiler_error || "") + (result.stderr || "");
                if (res.compiler_output || res.compiler_error) {
                    result.status = "CE";
                } else {
                    result.status = "RE";
                }
            }

            return result;
        }
    }

    class PaizaIORunner extends CodeRunner {
        constructor(name, label) {
            super(label, "PaizaIO");
            this.name = name;
        }

        async run(sourceCode, input) {
            let id, status, error;
            try {
                const res = await fetch("https://api.paiza.io/runners/create?" + buildParams({
                    source_code: sourceCode,
                    language: this.name,
                    input,
                    longpoll: true,
                    longpoll_timeout: 10,
                    api_key: "guest",
                }), {
                    method: "POST",
                    mode: "cors",
                }).then(r => r.json());
                id = res.id;
                status = res.status;
                error = res.error;
            } catch (error) {
                return {
                    status: "IE",
                    stderr: error,
                };
            }

            while (status == "running") {
                const res = await (await fetch("https://api.paiza.io/runners/get_status?" + buildParams({
                    id,
                    api_key: "guest",
                }), {
                    mode: "cors",
                })).json();
                status = res.status;
                error = res.error;
            }

            const res = await fetch("https://api.paiza.io/runners/get_details?" + buildParams({
                id,
                api_key: "guest",
            }), {
                mode: "cors",
            }).then(r => r.json());

            const result = {
                exitCode: res.exit_code,
                execTime: +res.time * 1e3,
                memory: +res.memory * 1e-3,
            };

            if (res.build_result == "failure") {
                result.status = "CE";
                result.exitCode = res.build_exit_code;
                result.stdout = res.build_stdout;
                result.stderr = res.build_stderr;
            } else {
                result.status = (res.result == "timeout") ? "TLE" : (res.result == "failure") ? "RE" : "OK";
                result.exitCode = res.exit_code;
                result.stdout = res.stdout;
                result.stderr = res.stderr;
            }

            return result;
        }
    }

    class WandboxCppRunner extends WandboxRunner {
        async run(sourceCode, input) {
          const ACLBase = "https://cdn.jsdelivr.net/gh/atcoder/ac-library/";
          const files = new Map();
          const includeHeader = async source => {
            const pattern = /^#\s*include\s*[<"]atcoder\/([^>"]+)[>"]/gm;
            const loaded = [];
            let match;
            while (match = pattern.exec(source)) {
              const file = "atcoder/" + match[1];
              if (files.has(file)) continue;
              files.set(file, null);
              loaded.push([file, fetch(ACLBase + file, { mode: "cors", cache: "force-cache", }).then(r => r.text())]);
            }
            const included = await Promise.all(loaded.map(async ([file, r]) => {
              const source = await r;
              files.set(file, source);
              return source;
            }));
            for (const source of included) {
              await includeHeader(source);
            }
          };
          await includeHeader(sourceCode);
          const codes = [];
          for (const [file, code] of files) {
            codes.push({ file, code, });
          }
          let options = this.options;
          if (typeof options == "function") options = options(sourceCode, input);
          return await this.request(JSON.stringify(Object.assign({
              compiler: this.name,
              code: sourceCode,
              stdin: input,
              codes,
              "compiler-option-raw": "-I.",
          }, options)));
        }
    }

    let waitAtCoderCustomTest = Promise.resolve();
    const AtCoderCustomTestBase = location.href.replace(/\/tasks\/.+$/, "/custom_test");
    const AtCoderCustomTestResultAPI = AtCoderCustomTestBase + "/json?reload=true";
    const AtCoderCustomTestSubmitAPI = AtCoderCustomTestBase + "/submit/json";
    class AtCoderRunner extends CodeRunner {
        constructor(languageId, label) {
            super(label, "AtCoder");
            this.languageId = languageId;
        }

        async run(sourceCode, input) {
            const promise = this.submit(sourceCode, input);
            waitAtCoderCustomTest = promise;
            return await promise;
        }

        async submit(sourceCode, input) {
            try {
                await waitAtCoderCustomTest;
            } catch (error) {
                console.error(error);
            }

            const error = await fetch(AtCoderCustomTestSubmitAPI, {
                method: "POST",
                credentials: "include",
                headers: {
                    "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
                },
                body: buildParams({
                    "data.LanguageId": this.languageId,
                    sourceCode,
                    input,
                    csrf_token: csrfToken,
                }),
            }).then(r => r.text());

            if (error) {
                throw new Error(error)
            }

            await sleep(100);

            for (;;) {
                const data = await fetch(AtCoderCustomTestResultAPI, {
                    method: "GET",
                    credentials: "include",
                }).then(r => r.json());

                if (!("Result" in data)) continue;
                const result = data.Result;

                if ("Interval" in data) {
                    await sleep(data.Interval);
                    continue;
                }

                return {
                    status: (result.ExitCode == 0) ? "OK" : (result.TimeConsumption == -1) ? "CE" : "RE",
                    exitCode: result.ExitCode,
                    execTime: result.TimeConsumption,
                    memory: result.MemoryConsumption,
                    stdout: data.Stdout,
                    stderr: data.Stderr,
                };
            }
        }
    }

    let brythonRunnerLoaded = false;
    const brythonRunner = new CustomRunner("Brython", async (sourceCode, input) => {
        if (!brythonRunnerLoaded) {
            await new Promise((resolve) => {
                const script = $create("script");
                script.src = "https://cdn.jsdelivr.net/gh/pythonpad/brython-runner/lib/brython-runner.bundle.js";
                script.onload = () => {
                    brythonRunnerLoaded = true;
                    resolve();
                };
                document.head.appendChild(script);
            });
        }

        let stdout = "";
        let stderr = "";
        let stdinOffset = 0;
        const runner = new BrythonRunner({
            stdout: { write(content) { stdout += content; }, flush() {} },
            stderr: { write(content) { stderr += content; }, flush() {} },
            stdin: { async readline() {
                let index = input.indexOf("\n", stdinOffset) + 1;
                if (index == 0) index = input.length;
                const text = input.slice(stdinOffset, index);
                stdinOffset = index;
                return text;
            } },
        });

        const timeStart = Date.now();
        await runner.runCode(sourceCode);
        const timeEnd = Date.now();

        return {
            status: "OK",
            exitCode: 0,
            execTime: (timeEnd - timeStart),
            stdout,
            stderr,
        };
    });

    const runners = {
        4001: [new WandboxRunner("gcc-10.1.0-c", "C (GCC 10.1.0)")],
        4002: [new PaizaIORunner("c", "C (C17 / Clang 10.0.0)", )],
        4003: [new WandboxCppRunner("gcc-10.1.0", "C++ (GCC 10.1.0)", {options: "warning,boost-1.73.0-gcc-9.2.0,gnu++17"})],
        4004: [new WandboxCppRunner("clang-10.0.0", "C++ (Clang 10.0.0)", {options: "warning,boost-nothing-clang-10.0.0,c++17"})],
        4006: [
            new PaizaIORunner("python3", "Python (3.8.2)"),
            brythonRunner,
        ],
        4007: [new PaizaIORunner("bash", "Bash (5.0.17)")],
        4010: [new WandboxRunner("csharp", "C# (.NET Core 6.0.100-alpha.1.20562.2)")],
        4011: [new WandboxRunner("mono-head", "C# (Mono-mcs 5.19.0.0)")],
        4013: [new PaizaIORunner("clojure", "Clojure (1.10.1-1)")],
        4017: [new PaizaIORunner("d", "D (LDC 1.23.0)")],
        4020: [new PaizaIORunner("erlang", "Erlang (10.6.4)")],
        4021: [new PaizaIORunner("elixir", "Elixir (1.10.4)")],
        4022: [new PaizaIORunner("fsharp", "F# (Interactive 4.0)")],
        4023: [new PaizaIORunner("fsharp", "F# (Interactive 4.0)")],
        4026: [new WandboxRunner("go-1.14.1", "Go (1.14.1)")],
        4027: [new WandboxRunner("ghc-head", "Haskell (GHC 8.7.20181121)")],
        4030: [new PaizaIORunner("javascript", "JavaScript (Node.js 12.18.3)")],
        4032: [new PaizaIORunner("kotlin", "Kotlin (1.4.0)")],
        4033: [new WandboxRunner("lua-5.3.4", "Lua (Lua 5.3.4)")],
        4034: [new WandboxRunner("luajit-head", "Lua (LuaJIT 2.1.0-beta3)")],
        4036: [new WandboxRunner("nim-1.0.6", "Nim (1.0.6)")],
        4037: [new PaizaIORunner("objective-c", "Objective-C (Clang 10.0.0)")],
        4039: [new WandboxRunner("ocaml-head", "OCaml (4.13.0+dev0-2020-10-19)")],
        4041: [new WandboxRunner("fpc-3.0.2", "Pascal (FPC 3.0.2)")],
        4042: [new PaizaIORunner("perl", "Perl (5.30.0)")],
        4044: [
            new PaizaIORunner("php", "PHP (7.4.10)"),
            new WandboxRunner("php-7.3.3", "PHP (7.3.3)"),
        ],
        4046: [new WandboxRunner("pypy-head", "PyPy2 (7.3.4-alpha0)")],
        4047: [new WandboxRunner("pypy-7.2.0-3", "PyPy3 (7.2.0)")],
        4049: [
            new PaizaIORunner("ruby", "Ruby (2.7.1)"),
            new WandboxRunner("ruby-head", "Ruby (HEAD 3.0.0dev)"),
            new WandboxRunner("ruby-2.7.0-preview1", "Ruby (2.7.0-preview1)"),
        ],
        4050: [
            new AtCoderRunner(4050, "Rust (1.42.0)"),
            new WandboxRunner("rust-head", "Rust (1.37.0-dev)"),
            new PaizaIORunner("rust", "Rust (1.43.0)"),
        ],
        4051: [new PaizaIORunner("scala", "Scala (2.13.3)")],
        4053: [new PaizaIORunner("scheme", "Scheme (Gauche 0.9.6)")],
        4055: [new PaizaIORunner("swift", "Swift (5.2.5)")],
        4056: [new CustomRunner("Text",
            async (sourceCode, input) => {
                return {
                    status: "OK",
                    exitCode: 0,
                    stdout: sourceCode,
                };
            }
        )],
        4058: [new PaizaIORunner("vb", "Visual Basic (.NET Core 4.0.1)")],
        4061: [new PaizaIORunner("cobol", "COBOL - Free (OpenCOBOL 2.2.0)")],
        4101: [new WandboxCppRunner("gcc-9.2.0", "C++ (GCC 9.2.0)")],
        4102: [new WandboxCppRunner("clang-10.0.0", "C++ (Clang 10.0.0)")],
    };

    for (const e of $selectAll("#select-lang option[value]")) {
        const languageId = e.value;
        if (!(languageId in runners)) runners[languageId] = [];
        if (runners[languageId].some(runner => runner instanceof AtCoderRunner)) continue;
        runners[languageId].push(new AtCoderRunner(languageId, e.textContent));
    }

    console.info("codeRunner OK");

    return {
        run(languageId, index, sourceCode, input, supposedOutput = null, options = { trim: true, split: true, }) {
            if (!(languageId in runners)) return Promise.reject("language not supported");

            // save last code
            codeSaver.save(sourceCode);

            // run
            return runners[languageId][index].test(sourceCode, input, supposedOutput, options);
        },

        getEnvironment(languageId) {
            if (!(languageId in runners)) return Promise.reject("language not supported");
            return Promise.resolve(runners[languageId].map(runner => runner.label));
        },
    };
})();


// -- bottom menu --
const bottomMenu = (function () {
    'use strict';

    const tabs = new Set();

    const bottomMenuKey = $(`<button id="bottom-menu-key" type="button" class="navbar-toggle collapsed glyphicon glyphicon-menu-down" data-toggle="collapse" data-target="#bottom-menu">`);
    const bottomMenuTabs = $(`<ul id="bottom-menu-tabs" class="nav nav-tabs">`);
    const bottomMenuContents = $(`<div id="bottom-menu-contents" class="tab-content">`);

    $(() => {
        const style = $create("style");
        style.textContent = `

#bottom-menu-wrapper {
    background: transparent;
    border: none;
    pointer-events: none;
    padding: 0;
}

#bottom-menu-wrapper>.container {
    position: absolute;
    bottom: 0;
    width: 100%;
    padding: 0;
}

#bottom-menu-wrapper>.container>.navbar-header {
    float: none;
}

#bottom-menu-key {
    display: block;
    float: none;
    margin: 0 auto;
    padding: 10px 3em;
    border-radius: 5px 5px 0 0;
    background: #000;
    opacity: 0.5;
    color: #FFF;
    cursor: pointer;
    pointer-events: auto;
    text-align: center;
}

@media screen and (max-width: 767px) {
    #bottom-menu-key {
        opacity: 0.25;
    }
}

#bottom-menu-key.collapsed:before {
    content: "\\e260";
}

#bottom-menu-tabs {
    padding: 3px 0 0 10px;
    cursor: n-resize;
}

#bottom-menu-tabs a {
    pointer-events: auto;
}

#bottom-menu {
    pointer-events: auto;
    background: rgba(0, 0, 0, 0.8);
    color: #fff;
    max-height: unset;
}

#bottom-menu.collapse:not(.in) {
    display: none !important;
}

#bottom-menu-tabs>li>a {
    background: rgba(150, 150, 150, 0.5);
    color: #000;
    border: solid 1px #ccc;
    filter: brightness(0.75);
}

#bottom-menu-tabs>li>a:hover {
    background: rgba(150, 150, 150, 0.5);
    border: solid 1px #ccc;
    color: #111;
    filter: brightness(0.9);
}

#bottom-menu-tabs>li.active>a {
    background: #eee;
    border: solid 1px #ccc;
    color: #333;
    filter: none;
}

.bottom-menu-btn-close {
    font-size: 8pt;
    vertical-align: baseline;
    padding: 0 0 0 6px;
    margin-right: -6px;
}

#bottom-menu-contents {
    padding: 5px 15px;
    max-height: 50vh;
    overflow-y: auto;
}

#bottom-menu-contents .panel {
    color: #333;
}

`;
        document.head.appendChild(style);
        const bottomMenu = $(`<div id="bottom-menu" class="collapse navbar-collapse">`).append(bottomMenuTabs, bottomMenuContents);
        $(`<div id="bottom-menu-wrapper" class="navbar navbar-default navbar-fixed-bottom">`)
        .append($(`<div class="container">`)
            .append(
                $(`<div class="navbar-header">`).append(bottomMenuKey),
                bottomMenu))
        .appendTo("#main-div");

        let resizeStart = null;
        bottomMenuTabs.on({
            mousedown({target, pageY}) {
                if (target.id != "bottom-menu-tabs") return;
                resizeStart = {y: pageY, height: bottomMenuContents.height()};
            },
            mousemove(e) {
                if (!resizeStart) return;
                e.preventDefault();
                bottomMenuContents.height(resizeStart.height - (e.pageY - resizeStart.y));
            },
        });
        document.addEventListener("mouseup", () => { resizeStart = null; });
        document.addEventListener("mouseleave", () => { resizeStart = null; });
    });

    const menuController = {
        addTab(tabId, tabLabel, paneContent, options = {}) {
            console.log("addTab: %s (%s)", tabLabel, tabId, paneContent);
            const tab = $(`<a id="bottom-menu-tab-${tabId}" href="#" data-target="#bottom-menu-pane-${tabId}" data-toggle="tab">`)
            .click(e => {
                e.preventDefault();
                tab.tab("show");
            })
            .append(tabLabel);
            const tabLi = $(`<li>`).append(tab).appendTo(bottomMenuTabs);
            const pane = $(`<div class="tab-pane" id="bottom-menu-pane-${tabId}">`).append(paneContent).appendTo(bottomMenuContents);
            console.dirxml(bottomMenuContents);
            const controller = {
                close() {
                    tabLi.remove();
                    pane.remove();
                    tabs.delete(tab);
                    if (tabLi.hasClass("active") && tabs.size > 0) {
                        tabs.values().next().value.tab("show");
                    }
                },

                show() {
                    menuController.show();
                    tab.tab("show");
                },

                set color(color) {
                    tab.css("background-color", color);
                },
            };
            tabs.add(tab);
            if (options.closeButton) tab.append($(`<a class="bottom-menu-btn-close btn btn-link glyphicon glyphicon-remove">`).click(() => controller.close()));
            if (options.active || tabs.size == 1) pane.ready(() => tab.tab("show"));
            return controller;
        },

        show() {
            if (bottomMenuKey.hasClass("collapsed")) bottomMenuKey.click();
        },

        toggle() {
            bottomMenuKey.click();
        },
    };

    console.info("bottomMenu OK");

    return menuController;
})();

$(() => {
    // returns [{input, output, anchor}]
    function getTestCases() {
        const selectors = [
            ["#task-statement p+pre.literal-block", ".section"], // utpc2011_1
            ["#task-statement pre.source-code-for-copy", ".part"],
            ["#task-statement .lang>*:nth-child(1) .div-btn-copy+pre", ".part"],
            ["#task-statement .div-btn-copy+pre", ".part"],
            ["#task-statement>.part pre.linenums", ".part"], // abc003_4
            ["#task-statement>.part:not(.io-style)>h3+section>pre", ".part"],
            ["#task-statement pre", ".part"],
        ];

        for (const [selector, closestSelector] of selectors) {
            const e = [... $selectAll(selector)].filter(e => {
                if ($(e).closest(".io-style").length) return false;
                return true;
            });
            if (e.length == 0) continue;
            const testcases = [];
            for (let i = 0; i < e.length; i += 2) {
                const container = e[i].closest(closestSelector) || e[i].parentElement;
                testcases.push({
                    input: (e[i]||{}).textContent,
                    output: (e[i+1]||{}).textContent,
                    anchor: container.querySelector("h3"),
                });
            }
            return testcases;
        }

        return [];
    }

    async function runTest(title, input, output = null) {
        const uid = Date.now().toString();
        title = title ? "Result " + title : "Result";
        const content = $create("div", { class: "container" });
        content.innerHTML = `
<div class="row">
    <div class="col-xs-12 ${(output == null) ? "" : "col-sm-6"}"><div class="form-group">
        <label class="control-label col-xs-12" for="atcoder-easy-test-${uid}-stdin">Standard Input</label>
        <div class="col-xs-12">
            <textarea id="atcoder-easy-test-${uid}-stdin" class="form-control" rows="3" readonly></textarea>
        </div>
    </div></div>${(output == null) ? "" : `
    <div class="col-xs-12 col-sm-6"><div class="form-group">
        <label class="control-label col-xs-12" for="atcoder-easy-test-${uid}-expected">Expected Output</label>
        <div class="col-xs-12">
            <textarea id="atcoder-easy-test-${uid}-expected" class="form-control" rows="3" readonly></textarea>
        </div>
    </div></div>
`}
</div>
<div class="row"><div class="col-sm-6 col-sm-offset-3">
        <div class="panel panel-default"><table class="table table-condensed">
            <tr>
                <th class="text-center">Exit Code</th>
                <th class="text-center">Exec Time</th>
                <th class="text-center">Memory</th>
            </tr>
            <tr>
                <td id="atcoder-easy-test-${uid}-exit-code" class="text-center"></td>
                <td id="atcoder-easy-test-${uid}-exec-time" class="text-center"></td>
                <td id="atcoder-easy-test-${uid}-memory" class="text-center"></td>
            </tr>
        </table></div>
</div></div>
<div class="row">
    <div class="col-xs-12 col-md-6"><div class="form-group">
        <label class="control-label col-xs-12" for="atcoder-easy-test-${uid}-stdout">Standard Output</label>
        <div class="col-xs-12">
            <textarea id="atcoder-easy-test-${uid}-stdout" class="form-control" rows="5" readonly></textarea>
        </div>
    </div></div>
    <div class="col-xs-12 col-md-6"><div class="form-group">
        <label class="control-label col-xs-12" for="atcoder-easy-test-${uid}-stderr">Standard Error</label>
        <div class="col-xs-12">
            <textarea id="atcoder-easy-test-${uid}-stderr" class="form-control" rows="5" readonly></textarea>
        </div>
    </div></div>
</div>
`;
        const tab = bottomMenu.addTab("easy-test-result-" + uid, title, content, { active: true, closeButton: true });
        $id(`atcoder-easy-test-${uid}-stdin`).value = input;
        if (output != null) $id(`atcoder-easy-test-${uid}-expected`).value = output;

        const options = { trim: true, split: true, };
        if ($id("atcoder-easy-test-allowable-error-check").checked) {
            options.allowableError = parseFloat($id("atcoder-easy-test-allowable-error").value);
        }

        const result = await codeRunner.run($select("#select-lang>select").value, +$id("atcoder-easy-test-language").value, window.getSourceCode(), input, output, options);

        if (result.status == "AC") {
            tab.color = "#dff0d8";
            $id(`atcoder-easy-test-${uid}-stdout`).style.backgroundColor = "#dff0d8";
        } else if (result.status != "OK") {
            tab.color = "#fcf8e3";
            if (result.status == "WA") $id(`atcoder-easy-test-${uid}-stdout`).style.backgroundColor = "#fcf8e3";
        }

        const eExitCode = $id(`atcoder-easy-test-${uid}-exit-code`);
        eExitCode.textContent = result.exitCode;
        eExitCode.classList.toggle("bg-success", result.exitCode == 0);
        eExitCode.classList.toggle("bg-danger", result.exitCode != 0);
        if ("execTime" in result) $id(`atcoder-easy-test-${uid}-exec-time`).textContent = result.execTime + " ms";
        if ("memory" in result) $id(`atcoder-easy-test-${uid}-memory`).textContent = result.memory + " KB";
        $id(`atcoder-easy-test-${uid}-stdout`).value = result.stdout || "";
        $id(`atcoder-easy-test-${uid}-stderr`).value = result.stderr || "";

        result.uid = uid;
        result.tab = tab;
        return result;
    }

    console.log("bottomMenu", bottomMenu);

    bottomMenu.addTab("easy-test", "Easy Test", $(`<form id="atcoder-easy-test-container" class="form-horizontal">`)
                      .html(`
<small style="position: absolute; display: block; bottom: 0; right: 0; padding: 1% 4%; width: 95%; text-align: right;">AtCoder Easy Test v${VERSION}</small>
<div class="row">
    <div class="col-xs-12 col-lg-8">
        <div class="form-group">
            <label class="control-label col-sm-2">Test Environment</label>
            <div class="col-sm-10">
                <select class="form-control" id="atcoder-easy-test-language"></select>
            </div>
        </div>
        <div class="form-group">
            <label class="control-label col-sm-2" for="atcoder-easy-test-input">Standard Input</label>
            <div class="col-sm-10">
                <textarea id="atcoder-easy-test-input" name="input" class="form-control" rows="3"></textarea>
            </div>
        </div>
    </div>
    <div class="col-xs-12 col-lg-4">
        <details close>
            <summary>Expected Output</summary>
            <div class="form-group">
                <label class="control-label col-sm-2" for="atcoder-easy-test-allowable-error-check">Allowable Error</label>
                <div class="col-sm-10">
                    <div class="input-group">
                        <span class="input-group-addon">
                            <input id="atcoder-easy-test-allowable-error-check" type="checkbox" checked>
                        </span>
                        <input id="atcoder-easy-test-allowable-error" type="text" class="form-control" value="1e-6">
                    </div>
                </div>
            </div>
            <div class="form-group">
                <label class="control-label col-sm-2" for="atcoder-easy-test-output">Expected Output</label>
                <div class="col-sm-10">
                    <textarea id="atcoder-easy-test-output" name="output" class="form-control" rows="3"></textarea>
                </div>
            </div>
        </details>
    </div>
    <div class="col-xs-12">
        <div class="col-xs-11 col-xs-offset=1">
            <div class="form-group">
                 <a id="atcoder-easy-test-run" class="btn btn-primary">Run</a>
            </div>
        </div>
    </div>
</div>
<style>
#atcoder-easy-test-language {
    border: none;
    background: transparent;
    font: inherit;
    color: #fff;
}
#atcoder-easy-test-language option {
    border: none;
    color: #333;
    font: inherit;
}
</style>
`).ready(() => {
    $id("atcoder-easy-test-run").addEventListener("click", () => {
        const title = "";
        const input = $id("atcoder-easy-test-input").value;
        const output = $id("atcoder-easy-test-output").value;
        runTest(title, input, output || null);
    });
    $("#select-lang>select").change(() => setLanguage()); //NOTE: This event is only for jQuery; do not replce with Vanilla
    $id("atcoder-easy-test-allowable-error").disabled = this.checked;
    $id("atcoder-easy-test-allowable-error-check").addEventListener("change", e => { $id("atcoder-easy-test-allowable-error").disabled = !e.target.checked; });

    async function setLanguage() {
        const languageId = $select("#select-lang>select").value;
        const eTestLanguage = $id("atcoder-easy-test-language");
        while (eTestLanguage.firstChild) eTestLanguage.removeChild(eTestLanguage.firstChild);
        try {
            const labels = await codeRunner.getEnvironment(languageId);
            console.log(`language: ${labels[0]} (${languageId})`);
            labels.forEach((label, index) => {
                const option = $create("option", {value: index});
                option.textContent = label;
                eTestLanguage.appendChild(option);
            });
            $id("atcoder-easy-test-run").classList.remove("disabled");
            $id("atcoder-easy-test-btn-test-all").disabled = false;
        } catch (error) {
            console.log(`language: ? (${languageId})`);
            const option = $create("option", { "class": "fg-danger" });
            option.textContent = error;
            eTestLanguage.appendChild(option);
            $id("atcoder-easy-test-run").classList.add("disabled");
            $id("atcoder-easy-test-btn-test-all").disabled = true;
        }
    }

    setLanguage();
}), { active: true });

    try {
        const testfuncs = [];
        const runButtons = [];

        const testcases = getTestCases();
        for (const {input, output, anchor} of testcases) {
            const testfunc = async () => {
                const title = anchor.childNodes[0].data;
                const result = await runTest(title, input, output);
                if (result.status == "OK" || result.status == "AC") {
                    $id(`atcoder-easy-test-${result.uid}-stdout`).classList.add("bg-success");
                }
                return result;
            };
            testfuncs.push(testfunc);

            const runButton = $(`<a class="btn btn-primary btn-sm" style="vertical-align: top; margin-left: 0.5em">`)
            .text("Run")
            .click(async () => {
                await testfunc();
                if ($id("bottom-menu-key").classList.contains("collapsed")) $id("bottom-menu-key").click();
            });
            anchor.appendChild(runButton[0]);
            runButtons.push(runButton);
        }

        const restoreLastPlayButton = $(`<a id="atcoder-easy-test-restore-last-play" class="btn btn-danger btn-sm">`)
        .text("Restore Last Play")
        .click(async () => {
            try {
                const lastCode = await codeSaver.restore();
                if (confirm("Your current code will be replaced. Are you sure?")) {
                    $('.plain-textarea').val(lastCode);
                    $('.editor').data('editor').doc.setValue(lastCode);
                }
            } catch (reason) {
                alert(reason);
                return;
            }
        })
        .appendTo(".editor-buttons");

        const fnTestAll = async () => {
            const statuses = testfuncs.map(_ => $(`<div class="label label-default" style="margin: 3px">`).text("WJ..."));
            const progress = $(`<div class="progress-bar">`).text(`0 / ${testfuncs.length}`);
            let finished = 0;
            const closeButton = $(`<button type="button" class="close" data-dismiss="alert" aria-label="close">`)
            .append($(`<span aria-hidden="true">`).text("\xd7"));
            const resultAlert = $(`<div class="alert alert-dismissible">`)
            .append(closeButton)
            .append($(`<div class="progress">`).append(progress))
            .append(...statuses)
            .prependTo(testAllResultRow);
            const results = await Promise.all(testfuncs.map(async (testfunc, i) => {
                const result = await testfunc();
                finished++;
                progress.text(`${finished} / ${statuses.length}`).css("width", `${finished/statuses.length*100}%`);
                statuses[i].toggleClass("label-success", result.status == "AC").toggleClass("label-warning", result.status != "AC").text(result.status).click(() => result.tab.show()).css("cursor", "pointer");
                return result;
            }));
            if (results.every(({status}) => status == "AC")) {
                resultAlert.addClass("alert-success");
            } else {
                resultAlert.addClass("alert-warning");
            }
            closeButton.click(() => {
                for (const {tab} of results) {
                    tab.close();
                }
            });
            return results;
        };

        const testAllResultRow = $(`<div class="row">`);
        const testAndSubmitButton = $(`<a id="atcoder-easy-test-btn-test-and-submit" class="btn btn-info btn" style="margin-left: 1rem" title="Ctrl+Enter" data-toggle="tooltip">`)
        .text("Test & Submit")
        .click(async () => {
            if (testAndSubmitButton.hasClass("disabled")) throw new Error("Button is disabled");
            testAndSubmitButton.addClass("disabled");
            try {
                const results = await fnTestAll();
                if (results.every(({status}) => status == "AC")) {
                    // submit
                    $("#submit").click();
                } else {
                    // failed to submit
                }
            } catch(e) {
                throw e;
            } finally {
                testAndSubmitButton.removeClass("disabled");
            }
        });
        const testAllButton = $(`<a id="atcoder-easy-test-btn-test-all" class="btn btn-default btn-sm" style="margin-left: 1rem" title="Alt+Enter" data-toggle="tooltip">`)
        .text("Test All Samples")
        .click(async () => {
            if (testAllButton.attr("disabled")) throw new Error("Button is disabled");
            await fnTestAll();
        });
        $("#submit").after(testAllButton).after(testAndSubmitButton).closest("form").append(testAllResultRow);
        document.addEventListener("keydown", e => {
            if (e.altKey) {
                switch (e.key) {
                    case "Enter":
                        testAllButton.click();
                        break;
                    case "Escape":
                        bottomMenu.toggle();
                        break;
                }
            }
            if (e.ctrlKey) {
                switch (e.key) {
                    case "Enter":
                        testAndSubmitButton.click();
                        break;
                }
            }
        });
    } catch (e) {
        console.error(e);
    }

    document.addEventListener("keydown", e => {
        if (e.altKey) {
            switch (e.key) {
                case "Escape":
                    bottomMenu.toggle();
                    break;
            }
        }
    });

    console.info("view OK");
});

})();