TopCoder tests viewer

After submitting on the TopCoder web client, you can see the details of the tests.

// ==UserScript==
// @name         TopCoder tests viewer
// @name:zh-CN   TopCoder tests viewer
// @namespace    https://github.com/platelett/script
// @version      0.1
// @description  After submitting on the TopCoder web client, you can see the details of the tests.
// @description:zh-CN 在 TopCoder 网页端提交后可以看到测试点详情。
// @author       platelet
// @match        https://arena.topcoder.com/*
// @icon         https://www.google.com/s2/favicons?domain=topcoder.com
// @grant        none
// @license      MIT

// ==/UserScript==
var wsHook = {};

function loadWsHook(){
    /* wsHook.js
     * https://github.com/skepticfx/wshook
     * Reference: http://www.w3.org/TR/2011/WD-websockets-20110419/#websocket
     */

    (function () {
        // Mutable MessageEvent.
        // Subclasses MessageEvent and makes data, origin and other MessageEvent properites mutatble.
        function MutableMessageEvent (o) {
            this.bubbles = o.bubbles || false
            this.cancelBubble = o.cancelBubble || false
            this.cancelable = o.cancelable || false
            this.currentTarget = o.currentTarget || null
            this.data = o.data || null
            this.defaultPrevented = o.defaultPrevented || false
            this.eventPhase = o.eventPhase || 0
            this.lastEventId = o.lastEventId || ''
            this.origin = o.origin || ''
            this.path = o.path || new Array(0)
            this.ports = o.parts || new Array(0)
            this.returnValue = o.returnValue || true
            this.source = o.source || null
            this.srcElement = o.srcElement || null
            this.target = o.target || null
            this.timeStamp = o.timeStamp || null
            this.type = o.type || 'message'
            this.__proto__ = o.__proto__ || MessageEvent.__proto__
        }

        var before = wsHook.before = function (data, url, wsObject) {
            return data
        }
        var after = wsHook.after = function (e, url, wsObject) {
            return e
        }
        var modifyUrl = wsHook.modifyUrl = function(url) {
            return url
        }
        wsHook.resetHooks = function () {
            wsHook.before = before
            wsHook.after = after
            wsHook.modifyUrl = modifyUrl
        }

        var _WS = WebSocket
        WebSocket = function (url, protocols) {
            var WSObject
            url = wsHook.modifyUrl(url) || url
            this.url = url
            this.protocols = protocols
            if (!this.protocols) { WSObject = new _WS(url) } else { WSObject = new _WS(url, protocols) }

            var _send = WSObject.send
            WSObject.send = function (data) {
                arguments[0] = wsHook.before(data, WSObject.url, WSObject) || data
                _send.apply(this, arguments)
            }

            // Events needs to be proxied and bubbled down.
            WSObject._addEventListener = WSObject.addEventListener
            WSObject.addEventListener = function () {
                var eventThis = this
                // if eventName is 'message'
                if (arguments[0] === 'message') {
                    arguments[1] = (function (userFunc) {
                        return function instrumentAddEventListener () {
                            arguments[0] = wsHook.after(new MutableMessageEvent(arguments[0]), WSObject.url, WSObject)
                            if (arguments[0] === null) return
                            userFunc.apply(eventThis, arguments)
                        }
                    })(arguments[1])
                }
                return WSObject._addEventListener.apply(this, arguments)
            }

            Object.defineProperty(WSObject, 'onmessage', {
                'set': function () {
                    var eventThis = this
                    var userFunc = arguments[0]
                    var onMessageHandler = function () {
                        arguments[0] = wsHook.after(new MutableMessageEvent(arguments[0]), WSObject.url, WSObject)
                        if (arguments[0] === null) return
                        userFunc.apply(eventThis, arguments)
                    }
                    WSObject._addEventListener.apply(this, ['message', onMessageHandler, false])
                }
            })

            return WSObject
        }
    })()
}

var container
var timeLimit, memoryLimit, multiArgs
var numberOfTests, runningIndex, maxTime, maxMemory, totTime
var results, firstFail
var blocks = new Array()

function append(parent, html) {
    var element = document.createElement("div")
    element.innerHTML = html
    return parent.appendChild(element.children[0])
}
function initialize() {
    maxTime = maxMemory = totTime = 0, runningIndex = 1
    results = new Array(numberOfTests)
    firstFail = 0, blocks.length = 0
    var temp = document.querySelectorAll("#top-content > div:nth-child(1) > div > div:nth-child(11) > div")
    timeLimit = parseFloat(temp[1].textContent) * (temp[0].textContent == "Time limit (s):" ? 1000 : 1)
    temp = document.querySelectorAll("#top-content > div:nth-child(1) > div > div:nth-child(12) > div")
    memoryLimit = parseFloat(temp[1].textContent) * (temp[0].textContent == "Memory limit (MB):" ? 1024 : 1)
    multiArgs = document.querySelector("#top-content > div:nth-child(1) > div > div:nth-child(6) > div.textWhite.ng-binding").textContent.indexOf(',') != -1
    console.log(timeLimit, memoryLimit, document.querySelector("#top-content > div:nth-child(1) > div > div:nth-child(6) > div.textWhite.ng-binding").textContent)
}
function loadContainer() {
    if (container) document.body.removeChild(container)
    container = append(document.body, `
<div style="background: white; padding-top: 50px; padding-left: 50px; font-size: 48px; font-family: verdana, arial, sans-serif; color: black;">
    <span style="color: #808080; line-height: 50px;" id="verdict">Running on test 1</span>
    (${numberOfTests} test cases)
    <br><br>
    <table style="line-height: 50px;" id="overview"><tbody>
        <tr><td>Total Time</td><td>Max Time</td><td>Max Memory</td></tr>
        <tr><td id="totTime">0 ms</td><td id="maxTime">0 ms</td><td id="memory">0 KB</td></tr>
    </tbody></table>
</div>
    `)
}
function memoryString(value) {
    return value < 1024 ? `${value} KB` : `${(value / 1024).toFixed(2)} MB`
}
function printData(ctr, name, data) {
    var index = blocks.length
    blocks.push(data)
    var button = append(ctr, `
<div>
    <span style="font-size: 24px; vertical-align: middle; line-height: 50px;">${name}</span>
    <span style="font-weight: bold; color: navy; cursor: pointer; user-select: none !important; font-family: 'Lato', 'Helvetica Neue', arial, sans-serif; vertical-align: middle;">[Copy full data]</span>
    <br>
    <div class="codeblock">${data.length <= 80 ? data : data.slice(0, 77) + "..."}</div>
</div>
    `).children[1]
    button.onclick = () => navigator.clipboard.writeText(blocks[index]).then(() => {
        button.innerHTML = "[Copied]"
        setTimeout(() => button.innerHTML = "[Copy full data]", 500)
    }, () => alert("Copy failed!"));
}
function dataString(data) {
    var args = JSON.stringify(data).split("")
    var inq = false
    for (let i in args) {
        if (args[i] == '"') inq = !inq
        if (inq) continue
        if (args[i] == '[') args[i] = '{'
        if (args[i] == ']') args[i] = '}'
    }
    return args.join("")
}
function getVerdict(result) {
    if (result.succeeded) return "Accepted"
    if ("checkAnswerResponse" in result) return "Wrong answer"
    if (result.message.startsWith("The code execution time exceeded"))
        return "Time limit exceeded"
    if (result.message.startsWith("sandbox did not finish on time"))
        return "Time limit exceeded"
    if (result.message.startsWith("abnormal termination"))
        return "Runtime error"
    if (result.message.startsWith("segmentation fault"))
        return "Runtime error"
    if (result.message.startsWith("caught signal"))
        return "Runtime error"
    return "Unknown error (possibly MLE)"
}
function printResult(result) {
    var verdict = getVerdict(result)
    var ctr = append(container, `
<span style="font-size: 18px">
    <br>
    <span style="font-size: 30px; color: #3B5998">#Test: ${result.testCaseIndex}</span>
    <br>
    <span style="font-size: 24px; color: ${verdict == "Accepted" ? "#00AA00" : "#0000AA"};">${verdict}</span>
    <br>
    <strong>Time</strong>: ${result.execTime} ms, <strong>Memory</strong>: ${memoryString(result.maxMemoryUsed)}
    <br>
</span>
    `)
    printData(ctr, "Input", dataString(multiArgs ? result.args.slice(1, -1) : result.args))
    if ("returnValue" in result) printData(ctr, "Output", dataString(result.returnValue))
    if (verdict != "Accepted") printData(ctr, "Answer", dataString(result.expectedValue))
    if ("checkAnswerResponse" in result) printData(ctr, "Message", result.checkAnswerResponse)
    else if ("message" in result) printData(ctr, "Message", result.message)
}
function printResults() {
    var state = document.querySelector("#verdict")
    state.style["font-weight"] = "bold"
    if (!firstFail) {
        state.innerHTML = "Accepted", state.style.color = "#00AA00"
        for (const a of results) if(a) printResult(a)
    } else {
        state.innerHTML = `${getVerdict(results[firstFail - 1])} on test ${firstFail}`
        state.style.color = "#0000AA"
        for (const a of results) if (a && !a.succeeded) printResult(a)
        for (const a of results) if (a && a.succeeded) printResult(a)
    }
    container.scrollIntoView();
}
function addResult(result) {
    console.log(result)
    results[result.testCaseIndex++] = result
    if (getVerdict(result) == "Time limit exceeded") result.execTime = timeLimit;
    maxTime = Math.max(maxTime, result.execTime)
    maxMemory = Math.max(maxMemory, result.maxMemoryUsed)
    totTime += result.execTime
    document.querySelector("#maxTime").innerHTML = `${maxTime} ms`
    document.querySelector("#totTime").innerHTML = `${totTime} ms`
    document.querySelector("#memory").innerHTML = memoryString(maxMemory)

    if (firstFail) return
    if (!result.succeeded) {
        firstFail = result.testCaseIndex
        setTimeout(printResults, 200)
    }
    if (++runningIndex > numberOfTests) printResults()
    else document.querySelector("#verdict").innerHTML = `Running on test ${runningIndex}`
}
function handleArenaEvent(event) {
    if (event.name == "PracticeSystemTestResponse") {
        numberOfTests = Object.values(event.args[0].testCaseCountByComponentId)[0]
        initialize(), loadContainer(), container.scrollIntoView()
    }
    if (event.name == "PracticeSystemTestResultResponse")
        addResult(event.args[0].resultData)
}
(function() {
    onload = () => append(document.head, `
<style type="text/css">
.codeblock {
    height: 35px;
    width: 1000px;
    background: #EEEEEE;
    padding-top: 5px;
    padding-left: 10px;
    font-size: 24px;
    font-family: monospace;
    color: black;
}
#overview {
    width: 1000px;
    min-height: 140px;
    text-align: center;
    border-collapse: collapse;
}
#overview, #overview td {
    border: 5px solid;
}
</style>
    `)
    loadWsHook()
    wsHook.after = function(messageEvent, url, wsObject) {
        if (messageEvent.data.startsWith("5:::")) {
            handleArenaEvent(JSON.parse(messageEvent.data.slice(4)))
        }
        return messageEvent
    }
})();