const tests = [];

/**
 * Describe a test.
 */
function test(description, run) {
    tests.push({
        description,
        run
    });
}

/**
 * Assert that a value is true.
 */
function assertTrue(assertion, message) {
    if (!assertion) throw message || 'Assertion failed';
}

/**
 * Assert that two values are equal.
 */
function assertEqual(expected, actual) {
    const error = `Expected ${expected} but got ${actual}`;
    if (Array.isArray(expected) && Array.isArray(actual)) {
        if (expected.length !== actual.length) throw error;
        for (const index in expected) {
            assertEqual(expected[index], actual[index]);
        }
    }
    else {
        if (actual !== expected) throw error;
    }
}

/**
 * Run a test and return the results.
 */
function runTest({ description, run }) {
    try {
        run();
    }
    catch (error) {
        return {
            description,
            passed: false,
            error
        };
    }

    return {
        description,
        passed: true
    };
}

/**
 * Run all described tests and return the results.
 */
function runTests() {
    return tests.map(runTest);
}

/**
 * Run all describe tests and display the results in a table, which is placed
 * into the supplied DOM element.
 */
function displayResults(parent) {
    function createCell(content) {
        const cell = document.createElement('td');
        if (content !== undefined) cell.innerText = content;
        return cell;
    }
    const results = runTests();
    const table = document.createElement('table');
    const header = document.createElement('tr');
    header.appendChild(createCell('Test name'));
    header.appendChild(createCell('Passed'));
    header.appendChild(createCell('Error message'));
    table.appendChild(header);

    for (const { description, passed, error } of results) {
        const row = document.createElement('tr');
        row.appendChild(createCell(description));
        row.appendChild(createCell(passed));
        row.appendChild(createCell(error));
        table.appendChild(row);
    }

    parent.appendChild(table);
}
@@ -682,24 +682,311 @@
   %7D%0D%0A    %7D%0D%0A
+    else if (typeof expected == 'object' && typeof actual == 'object') %7B%0D%0A        for (const key in expected) %7B%0D%0A            assertEqual(expected%5Bkey%5D, actual%5Bkey%5D);%0D%0A        %7D%0D%0A        for (const key in actual) %7B%0D%0A            assertEqual(expected%5Bkey%5D, actual%5Bkey%5D);%0D%0A        %7D%0D%0A    %7D%0D%0A
     else %7B%0D%0A
@@ -1650,24 +1650,220 @@
 (parent) %7B%0D%0A
+    function createRow(...elements) %7B%0D%0A        const row = document.createElement('tr');%0D%0A        elements.forEach(element =%3E row.appendChild(createCell(element)));%0D%0A        return row;%0D%0A    %7D%0D%0A%0D%0A
     function
@@ -2025,24 +2025,26 @@
 ll;%0D%0A    %7D%0D%0A
+%0D%0A
     const re
@@ -2112,24 +2112,26 @@
 ('table');%0D%0A
+%0D%0A
     const he
@@ -2141,73 +2141,28 @@
 r = 
-document.createElement('tr');%0D%0A    header.appendChild(createCell(
+createRow(%0D%0A        
 'Tes
@@ -2172,64 +2172,67 @@
 ame'
-));
+,
 %0D%0A    
-header.appendChild(createCell('Passed')
+    'Passed',%0D%0A        'Error message'%0D%0A    
 );%0D%0A    
 head
@@ -2227,22 +2227,21 @@
 );%0D%0A    
-header
+table
 .appendC
@@ -2249,71 +2249,196 @@
 ild(
-createCell('Error message'));%0D%0A    table.appendChild(header);%0D%0A
+header);%0D%0A%0D%0A    const summary = createRow(%0D%0A        'Total tests',%0D%0A        '0/0',%0D%0A        ''%0D%0A    );%0D%0A    table.appendChild(summary);%0D%0A%0D%0A    let passedTests = 0;%0D%0A    let totalTests = 0;
 %0D%0A  
@@ -2520,74 +2520,32 @@
 w = 
-document.createElement('tr');%0D%0A        row.appendChild(createCell(
+createRow(%0D%0A            
 desc
@@ -2551,19 +2551,17 @@
 cription
-));
+,
 %0D%0A      
@@ -2566,44 +2566,19 @@
     
-row.appendChild(createCell(
+    
 passed
-));
+,
 %0D%0A  
@@ -2587,41 +2587,27 @@
     
-row.appendChild(createCell(error)
+    error%0D%0A        
 );%0D%0A
@@ -2639,25 +2639,156 @@
 (row);%0D%0A    
-%7D
+    if (passed) passedTests++;%0D%0A        totalTests++;%0D%0A    %7D%0D%0A%0D%0A    summary.childNodes%5B1%5D.innerText = %60$%7BpassedTests%7D/$%7BtotalTests%7D%60
 %0D%0A%0D%0A    pare

Add test summary