From 985f1b3bf3e49f0399c97470676be9f11dc1a2a9 Mon Sep 17 00:00:00 2001 From: Rohit Kushwaha Date: Wed, 24 Jun 2026 18:19:46 +0530 Subject: [PATCH 1/5] feat: improved testing --- src/test/ace.test.js | 88 ++-- src/test/fs.tests.js | 205 +++++++++ src/test/sanity.tests.js | 1 - src/test/test-definitions.js | 21 + src/test/tester.js | 861 +++++++++++++++++++++++++++++------ src/test/tester.tests.js | 104 +++++ 6 files changed, 1109 insertions(+), 171 deletions(-) create mode 100644 src/test/fs.tests.js create mode 100644 src/test/test-definitions.js create mode 100644 src/test/tester.tests.js diff --git a/src/test/ace.test.js b/src/test/ace.test.js index 36b99a3f0..0cd43d091 100644 --- a/src/test/ace.test.js +++ b/src/test/ace.test.js @@ -141,11 +141,9 @@ export async function runAceCompatibilityTests(writeOutput) { test.assert(typeof text === "string", "should return string"); }); - runner.test("editor.session exists", async (test) => { - const testFile = await createTestFile("test"); + runner.test("editor.session exists", (test) => { const editor = getEditor(); - test.assert(editor.session != null, "session should exist"); - testFile.remove(false); + test.assert("session" in editor, "session property should exist on editor"); }); runner.test("editor.setTheme()", (test) => { @@ -238,53 +236,65 @@ export async function runAceCompatibilityTests(writeOutput) { runner.test("session.getValue()", async (test) => { const testFile = await createTestFile("test content"); - const editor = getEditor(); - test.assert( - typeof editor.session.getValue === "function", - "getValue should exist", - ); - const value = editor.session.getValue(); - test.assert(typeof value === "string", "should return string"); - test.assertEqual(value, "test content"); - testFile.remove(false); + try { + const session = testFile.session; + test.assert( + typeof session.getValue === "function", + "getValue should exist", + ); + const value = session.getValue(); + test.assert(typeof value === "string", "should return string"); + test.assertEqual(value, "test content"); + } finally { + testFile.remove(false); + } }); runner.test("session.setValue()", async (test) => { const testFile = await createTestFile("original"); - const editor = getEditor(); - test.assert( - typeof editor.session.setValue === "function", - "setValue should exist", - ); - editor.session.setValue("modified"); - test.assertEqual(editor.session.getValue(), "modified"); - testFile.remove(false); + try { + const session = testFile.session; + test.assert( + typeof session.setValue === "function", + "setValue should exist", + ); + session.setValue("modified"); + test.assertEqual(testFile.session.getValue(), "modified"); + } finally { + testFile.remove(false); + } }); runner.test("session.getLength()", async (test) => { const testFile = await createTestFile("line1\nline2\nline3"); - const editor = getEditor(); - test.assert( - typeof editor.session.getLength === "function", - "getLength should exist", - ); - const len = editor.session.getLength(); - test.assert(typeof len === "number", "should return number"); - test.assertEqual(len, 3); - testFile.remove(false); + try { + const session = testFile.session; + test.assert( + typeof session.getLength === "function", + "getLength should exist", + ); + const len = session.getLength(); + test.assert(typeof len === "number", "should return number"); + test.assertEqual(len, 3); + } finally { + testFile.remove(false); + } }); runner.test("session.getLine()", async (test) => { const testFile = await createTestFile("first\nsecond\nthird"); - const editor = getEditor(); - test.assert( - typeof editor.session.getLine === "function", - "getLine should exist", - ); - test.assertEqual(editor.session.getLine(0), "first"); - test.assertEqual(editor.session.getLine(1), "second"); - test.assertEqual(editor.session.getLine(2), "third"); - testFile.remove(false); + try { + const session = testFile.session; + test.assert( + typeof session.getLine === "function", + "getLine should exist", + ); + test.assertEqual(session.getLine(0), "first"); + test.assertEqual(session.getLine(1), "second"); + test.assertEqual(session.getLine(2), "third"); + } finally { + testFile.remove(false); + } }); return await runner.run(writeOutput); diff --git a/src/test/fs.tests.js b/src/test/fs.tests.js new file mode 100644 index 000000000..fa853a258 --- /dev/null +++ b/src/test/fs.tests.js @@ -0,0 +1,205 @@ +import fsOperation from "fileSystem"; +import Url from "utils/Url"; +import { TestRunner } from "./tester"; + +export async function runFsTests(writeOutput) { + const runner = new TestRunner("Filesystem API Tests"); + + const testDir = window.CACHE_STORAGE || "file:///sdcard/AcodeCache"; + + runner.test("CACHE_STORAGE is defined", (test) => { + test.assert( + typeof window.CACHE_STORAGE === "string", + "CACHE_STORAGE should be a string path", + ); + }); + + runner.test("fsOperation returns a FileSystem object", (test) => { + const fs = fsOperation(testDir); + test.assert(fs !== null, "fsOperation should return filesystem handler"); + test.assert( + typeof fs.createFile === "function", + "createFile should be a function", + ); + test.assert(typeof fs.exists === "function", "exists should be a function"); + }); + + runner.test( + "createFile, exists, writeFile, readFile, delete", + async (test) => { + const fs = fsOperation(testDir); + const filename = `__fs_test_${Date.now()}__.txt`; + const fileUrl = Url.join(testDir, filename); + + try { + // 1. Create the file + const createdUrl = await fs.createFile(filename, "initial content"); + test.assertEqual( + createdUrl, + fileUrl, + "Created file URL should match expected path", + ); + + // 2. Check existence + const fileFs = fsOperation(createdUrl); + const exists = await fileFs.exists(); + test.assertEqual(exists, true, "Created file should exist"); + + // 3. Read content + const content = await fileFs.readFile("utf-8"); + test.assertEqual( + content, + "initial content", + "Read content should match initial content", + ); + + // 4. Write new content + await fileFs.writeFile("updated content"); + const updatedContent = await fileFs.readFile("utf-8"); + test.assertEqual( + updatedContent, + "updated content", + "Read content should match updated content", + ); + + // 5. Stat check + const stat = await fileFs.stat(); + test.assert(stat !== null, "Stat should not be null"); + test.assertEqual(stat.isFile, true, "Stat should show isFile true"); + test.assertEqual( + stat.isDirectory, + false, + "Stat should show isDirectory false", + ); + + // 6. Delete file + await fileFs.delete(); + const existsAfterDelete = await fileFs.exists(); + test.assertEqual( + existsAfterDelete, + false, + "File should not exist after deletion", + ); + } catch (error) { + // Cleanup if anything fails + try { + const fileFs = fsOperation(fileUrl); + if (await fileFs.exists()) { + await fileFs.delete(); + } + } catch (_) {} + throw error; + } + }, + ); + + runner.test("createDirectory, lsDir, delete directory", async (test) => { + const fs = fsOperation(testDir); + const dirname = `__fs_dir_test_${Date.now()}__`; + const dirUrl = Url.join(testDir, dirname); + + try { + // 1. Create directory + const createdDirUrl = await fs.createDirectory(dirname); + test.assertEqual( + createdDirUrl, + dirUrl, + "Created directory URL should match expected path", + ); + + const dirFs = fsOperation(createdDirUrl); + const exists = await dirFs.exists(); + test.assertEqual(exists, true, "Created directory should exist"); + + // 2. Stat check + const stat = await dirFs.stat(); + test.assertEqual( + stat.isDirectory, + true, + "Stat should show isDirectory true", + ); + test.assertEqual(stat.isFile, false, "Stat should show isFile false"); + + // 3. Create a file inside directory + const fileUrl = await dirFs.createFile("child.txt", "child content"); + + // 4. List directory contents + const list = await dirFs.lsDir(); + const child = list.find((item) => item.name === "child.txt"); + test.assert( + child !== undefined, + "lsDir should list the created child file", + ); + test.assertEqual(child.isFile, true, "child item should be a file"); + + // 5. Delete child file and directory recursively + const childFs = fsOperation(fileUrl); + await childFs.delete(); + await dirFs.delete(); + + const dirExistsAfterDelete = await dirFs.exists(); + test.assertEqual( + dirExistsAfterDelete, + false, + "Directory should not exist after deletion", + ); + } catch (error) { + // Cleanup if anything fails + try { + const dirFs = fsOperation(dirUrl); + if (await dirFs.exists()) { + await dirFs.delete(); + } + } catch (_) {} + throw error; + } + }); + + runner.test("read/write with explicit encodings", async (test) => { + const fs = fsOperation(testDir); + const utf8Filename = `__fs_utf8_test_${Date.now()}__.txt`; + const gbkFilename = `__fs_gbk_test_${Date.now()}__.txt`; + + const utf8FileUrl = Url.join(testDir, utf8Filename); + const gbkFileUrl = Url.join(testDir, gbkFilename); + + try { + // Write and read with UTF-8 + const utf8Url = await fs.createFile(utf8Filename, ""); + const utf8Fs = fsOperation(utf8Url); + await utf8Fs.writeFile("Hello 世界 (UTF-8)", "utf-8"); + const utf8Content = await utf8Fs.readFile("utf-8"); + test.assertEqual( + utf8Content, + "Hello 世界 (UTF-8)", + "UTF-8 read/write should match", + ); + + // Write and read with GBK (simplified Chinese characters like 世界) + const gbkUrl = await fs.createFile(gbkFilename, ""); + const gbkFs = fsOperation(gbkUrl); + await gbkFs.writeFile("Hello 世界 (GBK)", "gbk"); + const gbkContent = await gbkFs.readFile("gbk"); + test.assertEqual( + gbkContent, + "Hello 世界 (GBK)", + "GBK read/write should match", + ); + + // Cleanup + await utf8Fs.delete(); + await gbkFs.delete(); + } catch (error) { + // Cleanup on failure + try { + await fsOperation(utf8FileUrl).delete(); + } catch (_) {} + try { + await fsOperation(gbkFileUrl).delete(); + } catch (_) {} + throw error; + } + }); + + return await runner.run(writeOutput); +} diff --git a/src/test/sanity.tests.js b/src/test/sanity.tests.js index 28bc84355..6b5938475 100644 --- a/src/test/sanity.tests.js +++ b/src/test/sanity.tests.js @@ -2,7 +2,6 @@ import { TestRunner } from "./tester"; export async function runSanityTests(writeOutput) { const runner = new TestRunner("JS (WebView) Sanity Tests"); - // Test 1: String operations runner.test("String concatenation", (test) => { const result = "Hello" + " " + "World"; diff --git a/src/test/test-definitions.js b/src/test/test-definitions.js new file mode 100644 index 000000000..e7cf47421 --- /dev/null +++ b/src/test/test-definitions.js @@ -0,0 +1,21 @@ +import { runAceCompatibilityTests } from "./ace.test"; +import { runCodeMirrorTests } from "./editor.tests"; +import { runExecutorTests } from "./exec.tests"; +import { runFsTests } from "./fs.tests"; +import { runSanityTests } from "./sanity.tests"; +import { runTesterTests } from "./tester.tests"; +import { runUrlTests } from "./url.tests"; + +/** + * Register Acode test suites here. + * these are just functions that runs tests by creating a instance of TestRunner + */ +export const testDefinitions = [ + runTesterTests, + runSanityTests, + runExecutorTests, + runUrlTests, + runFsTests, + runCodeMirrorTests, + runAceCompatibilityTests, +]; diff --git a/src/test/tester.js b/src/test/tester.js index be3ba8ac6..726dda10f 100644 --- a/src/test/tester.js +++ b/src/test/tester.js @@ -1,92 +1,670 @@ -import { runAceCompatibilityTests } from "./ace.test"; -import { runCodeMirrorTests } from "./editor.tests"; -import { runExecutorTests } from "./exec.tests"; -import { runSanityTests } from "./sanity.tests"; -import { runUrlTests } from "./url.tests"; +import tag from "html-tag-js"; +import EditorFile from "lib/editorFile"; +import { testDefinitions } from "./test-definitions"; + +// Global Runner State +let currentRunnerState = { + status: "idle", // "idle", "running", "completed" + suites: [], + stats: { + total: 0, + passed: 0, + failed: 0, + skipped: 0, + successRate: "0.0", + }, + logs: "", +}; -export async function runAllTests() { - const terminal = acode.require("terminal"); - const local = await terminal.createLocal({ name: "TestCode" }); - function write(data) { - terminal.write(local.id, data); +let activeRunners = []; +let isRegistrationPass = false; + +// DOM references +let $pageBody = null; +let $statsTotal = null; +let $statsPassed = null; +let $statsFailed = null; +let $statsSkipped = null; +let $statsSuccessRate = null; +let $progressBar = null; +let $runBtn = null; +let $suiteList = null; + +const suiteElementsMap = new Map(); + +function delay(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +// Update the entire dashboard and components +function updateUI() { + if (!$pageBody) return; + + // 1. Update stats + if ($statsTotal) $statsTotal.textContent = currentRunnerState.stats.total; + if ($statsPassed) $statsPassed.textContent = currentRunnerState.stats.passed; + if ($statsFailed) $statsFailed.textContent = currentRunnerState.stats.failed; + if ($statsSkipped) + $statsSkipped.textContent = currentRunnerState.stats.skipped; + + const effectiveTotal = + currentRunnerState.stats.total - currentRunnerState.stats.skipped; + const rate = + effectiveTotal > 0 + ? ((currentRunnerState.stats.passed / effectiveTotal) * 100).toFixed(1) + : "0.0"; + if ($statsSuccessRate) $statsSuccessRate.textContent = rate + "%"; + + // 2. Update progress bar + if ($progressBar) { + const total = currentRunnerState.stats.total; + const completed = + currentRunnerState.stats.passed + + currentRunnerState.stats.failed + + currentRunnerState.stats.skipped; + const pct = total > 0 ? (completed / total) * 100 : 0; + $progressBar.style.width = pct + "%"; } - // Run tests at runtime - write("\x1b[36m\x1b[1m🚀 TestCode Plugin Loaded\x1b[0m\n"); - write("\x1b[36m\x1b[1mStarting test execution...\x1b[0m\n"); + // 3. Update Run button status + if ($runBtn) { + if (currentRunnerState.status === "running") { + $runBtn.classList.add("running"); + $runBtn.disabled = true; + $runBtn.innerHTML = ' Running...'; + } else { + $runBtn.classList.remove("running"); + $runBtn.disabled = false; + $runBtn.innerHTML = + ' Run Tests'; + } + } - try { - // Run unit tests - await runSanityTests(write); - await runCodeMirrorTests(write); - await runAceCompatibilityTests(write); - await runExecutorTests(write); - await runUrlTests(write); - - write("\x1b[36m\x1b[1mTests completed!\x1b[0m\n"); - } catch (error) { - write(`\x1b[31m⚠️ Test execution error: ${error.message}\x1b[0m\n`); + // 4. Render/Update each suite card + currentRunnerState.suites.forEach((suite) => { + updateSuiteElement(suite); + }); +} + +function getOrCreateSuiteElement(suite) { + if (suiteElementsMap.has(suite.name)) { + return suiteElementsMap.get(suite.name); + } + + // Create suite element components + let isCollapsed = false; + const $body =
; + + const toggleCollapse = () => { + isCollapsed = !isCollapsed; + if (isCollapsed) { + $body.classList.add("collapsed"); + } else { + $body.classList.remove("collapsed"); + } + }; + + const $header = ( +
+
+ + {suite.name} +
+
+ +
+
+ ); + + const $el = ( +
+ {$header} + {$body} +
+ ); + + if ($suiteList) { + $suiteList.append($el); } + + const suiteData = { + el: $el, + header: $header, + body: $body, + statusIcon: $header.querySelector(".suite-status-icon"), + badge: $header.querySelector(".suite-badge"), + }; + + suiteElementsMap.set(suite.name, suiteData); + return suiteData; } -// ANSI color codes for terminal output -const COLORS = { - RESET: "\x1b[0m", - BRIGHT: "\x1b[1m", - DIM: "\x1b[2m", - ITALIC: "\x1b[3m", - - // Foreground colors - RED: "\x1b[31m", - GREEN: "\x1b[32m", - YELLOW: "\x1b[33m", - BLUE: "\x1b[34m", - MAGENTA: "\x1b[35m", - CYAN: "\x1b[36m", - WHITE: "\x1b[37m", - GRAY: "\x1b[90m", - - // Background colors - BG_RED: "\x1b[41m", - BG_GREEN: "\x1b[42m", - BG_YELLOW: "\x1b[43m", - BG_BLUE: "\x1b[44m", -}; +function updateSuiteElement(suite) { + const { body, statusIcon, badge } = getOrCreateSuiteElement(suite); -function delay(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); + // Update status icon + statusIcon.className = "suite-status-icon"; + if (suite.status === "completed") { + if (suite.failed > 0) { + statusIcon.className += " fail icon cancel"; + } else { + statusIcon.className += " pass icon check_circle"; + } + } else if (suite.status === "running") { + statusIcon.className += " running icon loader"; + } else { + statusIcon.className += " pending icon help"; + } + + // Update badge + badge.className = "suite-badge"; + if (suite.failed > 0) { + badge.className += " fail"; + badge.textContent = `${suite.passed}/${suite.total} passed · ${suite.failed} failed`; + } else { + badge.className += " pass"; + badge.textContent = `${suite.passed}/${suite.total} passed`; + } + + // Render tests inside body + body.innerHTML = ""; + suite.tests.forEach((test) => { + let iconClass = "help"; + let iconStyle = { color: "var(--secondary-text-color)" }; + if (test.status === "PASS") { + iconClass = "check_circle"; + iconStyle = { color: "var(--active-color)" }; + } else if (test.status === "FAIL") { + iconClass = "cancel"; + iconStyle = { color: "var(--danger-color)" }; + } else if (test.status === "SKIP") { + iconClass = "warningreport_problem"; + iconStyle = { color: "var(--error-text-color)" }; + } else if (test.status === "running") { + iconClass = "loader"; + iconStyle = { color: "var(--active-color)" }; + } + + const $item = ( +
+
+
+ + {test.name} +
+ {test.time !== undefined && ( + {test.time}ms + )} +
+ {test.status === "FAIL" && test.error && ( +
{test.error}
+ )} + {test.status === "SKIP" && test.reason && ( +
+ {test.reason} +
+ )} +
+ ); + body.append($item); + }); } -function startSpinner(writeOutput, text) { - let index = 0; - let active = true; - - const timer = setInterval(() => { - if (!active) return; - const frame = SPINNER_FRAMES[index++ % SPINNER_FRAMES.length]; - // \r moves cursor to start, \x1b[K clears the line to the right - writeOutput(`\r ${COLORS.CYAN}${frame}${COLORS.RESET} ${text}`); - }, 80); - - return () => { - active = false; - clearInterval(timer); - // Clear the line so the "Success/Fail" message can take its place - writeOutput("\r\x1b[K"); +function createTestRunnerContent() { + const triggerRun = (e) => { + e.preventDefault(); + e.stopPropagation(); + if (currentRunnerState.status !== "running") { + runTestsInternal(); + } }; + + $runBtn = ( + + ); + + $statsTotal = 0; + $statsPassed = 0; + $statsFailed = 0; + $statsSkipped = 0; + $statsSuccessRate = 0.0%; + + $progressBar =
; + $suiteList =
; + + const style = ( + + ); + + $pageBody = ( +
+ {style} +
{$progressBar}
+
+
+
{$runBtn}
+
+
+ Success: + {$statsSuccessRate} +
+
+ Passed: + {$statsPassed} +
+
+ Failed: + {$statsFailed} +
+
+ Skipped: + {$statsSkipped} +
+
+ Total: + {$statsTotal} +
+
+
+ + {$suiteList} +
+
+ ); + + return $pageBody; } -// Spinner frames -const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; +function registerAllSuites() { + isRegistrationPass = true; + activeRunners = []; + currentRunnerState.status = "idle"; + currentRunnerState.suites = []; + currentRunnerState.stats = { + total: 0, + passed: 0, + failed: 0, + skipped: 0, + successRate: "0.0", + }; + suiteElementsMap.clear(); + if ($suiteList) $suiteList.innerHTML = ""; + + // Execute suite functions in registration mode to populate the tree view + for (const runTestSuite of testDefinitions) { + runTestSuite(null); + } + isRegistrationPass = false; +} + +async function runTestsInternal() { + currentRunnerState.status = "running"; + currentRunnerState.stats = { + total: currentRunnerState.stats.total, + passed: 0, + failed: 0, + skipped: 0, + successRate: "0.0", + }; + currentRunnerState.logs = ""; + + // Reset all suite and test states in UI to pending before we start + activeRunners.forEach((runner) => { + runner.suiteState.status = "pending"; + runner.suiteState.passed = 0; + runner.suiteState.failed = 0; + runner.suiteState.skipped = 0; + runner.suiteState.tests.forEach((t) => { + t.status = "pending"; + t.time = undefined; + t.error = undefined; + t.reason = undefined; + }); + }); + updateUI(); + + function writeOutput(data) { + currentRunnerState.logs += data; + console.log(data); + } + + writeOutput("🚀 Test Runner Started\n"); + writeOutput("Running Acode test suite...\n"); + + try { + for (const runner of activeRunners) { + await runner.executeSuite(writeOutput); + } + writeOutput("\n🎉 All test suites completed!\n"); + } catch (error) { + writeOutput(`\n⚠️ Test execution error: ${error.message}\n`); + if (error.stack) { + writeOutput(`${error.stack}\n`); + } + } finally { + currentRunnerState.status = "completed"; + updateUI(); + const testRunnerTab = editorManager.files.find( + (f) => f.id === "test-runner", + ); + if (testRunnerTab && editorManager.activeFile?.id !== "test-runner") { + testRunnerTab.makeActive(); + } + } +} + +export function openTestRunnerTab() { + const existingFile = editorManager.files.find((f) => f.id === "test-runner"); + if (existingFile) { + existingFile.makeActive(); + if (currentRunnerState.status !== "running") { + runTestsInternal(); + } + return; + } + + const content = createTestRunnerContent(); + + const testRunnerFile = new EditorFile("Test Runner", { + id: "test-runner", + render: true, + type: "page", + content: content, + tabIcon: "icon verified", + hideQuickTools: true, + }); + + const onFileRemoved = (removedFile) => { + if (removedFile.id === "test-runner") { + $pageBody = null; + $statsTotal = null; + $statsPassed = null; + $statsFailed = null; + $statsSkipped = null; + $statsSuccessRate = null; + $progressBar = null; + $runBtn = null; + $suiteList = null; + suiteElementsMap.clear(); + activeRunners = []; + + editorManager.off("remove-file", onFileRemoved); + } + }; + editorManager.on("remove-file", onFileRemoved); + + testRunnerFile.setCustomTitle(() => "Verification"); + testRunnerFile.makeActive(); + + // Load and register all test definitions immediately so they are shown in the UI + registerAllSuites(); + runTestsInternal(); +} + +export async function runAllTests() { + openTestRunnerTab(); +} class TestRunner { - constructor(name = "Test Suite") { + constructor(name = "Test Suite", register = true) { this.name = name; this.tests = []; this.passed = 0; this.failed = 0; this.results = []; this.skipped = 0; + + this.suiteState = { + name: this.name, + status: "pending", + tests: [], + passed: 0, + failed: 0, + skipped: 0, + total: 0, + }; + if (register) { + currentRunnerState.suites.push(this.suiteState); + activeRunners.push(this); + updateUI(); + } } /** @@ -94,6 +672,13 @@ class TestRunner { */ test(testName, testFn) { this.tests.push({ name: testName, fn: testFn }); + this.suiteState.tests.push({ + name: testName, + status: "pending", + }); + this.suiteState.total++; + currentRunnerState.stats.total++; + updateUI(); } /** @@ -143,45 +728,66 @@ class TestRunner { } /** - * Run all tests + * Run all tests in this suite */ async run(writeOutput) { - const line = (text = "", color = "") => { - writeOutput(`${color}${text}${COLORS.RESET}\n`); + if (isRegistrationPass) { + return this.results; + } + return await this.executeSuite(writeOutput); + } + + async executeSuite(writeOutput) { + const line = (text = "") => { + writeOutput(`${text}\n`); }; - // Header - line(); - line( - "╔════════════════════════════════════════════╗", - COLORS.CYAN + COLORS.BRIGHT, - ); - line( - `║ 🧪 ${this._padCenter(this.name, 35)} │`, - COLORS.CYAN + COLORS.BRIGHT, - ); - line( - "╚════════════════════════════════════════════╝", - COLORS.CYAN + COLORS.BRIGHT, - ); - line(); + this.passed = 0; + this.failed = 0; + this.skipped = 0; + this.results = []; - // Run tests with spinner - for (const test of this.tests) { - const stopSpinner = startSpinner(writeOutput, `Running ${test.name}...`); + this.suiteState.status = "running"; + this.suiteState.passed = 0; + this.suiteState.failed = 0; + this.suiteState.skipped = 0; + updateUI(); + line(`🧪 Running suite: ${this.name}`); + + // Run tests + for (const test of this.tests) { + const tState = this.suiteState.tests.find((t) => t.name === test.name); + if (tState) { + tState.status = "running"; + updateUI(); + } + const startTime = performance.now(); try { - await delay(200); - await this._runWithTimeout(test.fn, this, 10000); + await delay(50); - stopSpinner(); + await this._runWithTimeout(test.fn, this, 10000); + const duration = Math.max( + 0, + Math.round(performance.now() - startTime) - 50, + ); this.passed++; this.results.push({ name: test.name, status: "PASS" }); - line(` ${COLORS.GREEN}✓${COLORS.RESET} ${test.name}`, COLORS.GREEN); - } catch (error) { - stopSpinner(); + if (tState) { + tState.status = "PASS"; + tState.time = duration; + } + this.suiteState.passed++; + currentRunnerState.stats.passed++; + + line(` ✓ ${test.name} (${duration}ms)`); + } catch (error) { + const duration = Math.max( + 0, + Math.round(performance.now() - startTime) - 50, + ); if (error instanceof SkipTest) { this.skipped++; this.results.push({ @@ -190,14 +796,15 @@ class TestRunner { reason: error.message, }); - line( - ` ${COLORS.YELLOW}?${COLORS.RESET} ${test.name}`, - COLORS.YELLOW + COLORS.BRIGHT, - ); - line( - ` ${COLORS.DIM}└─ ${error.message}${COLORS.RESET}`, - COLORS.YELLOW + COLORS.DIM, - ); + if (tState) { + tState.status = "SKIP"; + tState.reason = error.message; + tState.time = duration; + } + this.suiteState.skipped++; + currentRunnerState.stats.skipped++; + + line(` ? ${test.name} - Skipped: ${error.message}`); } else { this.failed++; this.results.push({ @@ -206,21 +813,28 @@ class TestRunner { error: error.message, }); - line( - ` ${COLORS.RED}✗${COLORS.RESET} ${test.name}`, - COLORS.RED + COLORS.BRIGHT, - ); - line( - ` ${COLORS.DIM}└─ ${error.message}${COLORS.RESET}`, - COLORS.RED + COLORS.DIM, - ); + if (tState) { + tState.status = "FAIL"; + tState.error = error.message; + tState.time = duration; + } + this.suiteState.failed++; + currentRunnerState.stats.failed++; + + line(` ✗ ${test.name} - Failed: ${error.message}`); } } + const testRunnerTab = editorManager.files.find( + (f) => f.id === "test-runner", + ); + if (testRunnerTab && editorManager.activeFile?.id !== "test-runner") { + testRunnerTab.makeActive(); + } + updateUI(); } - // Summary - line(); - line("─────────────────────────────────────────────", COLORS.GRAY); + this.suiteState.status = "completed"; + updateUI(); const total = this.tests.length; const effectiveTotal = total - this.skipped; @@ -229,28 +843,13 @@ class TestRunner { ? ((this.passed / effectiveTotal) * 100).toFixed(1) : "0.0"; - const statusColor = this.failed === 0 ? COLORS.GREEN : COLORS.YELLOW; - line( - ` Tests: ${COLORS.BRIGHT}${total}${COLORS.RESET} | ` + - `${COLORS.GREEN}Passed: ${this.passed}${COLORS.RESET} | ` + - `${COLORS.YELLOW}Skipped: ${this.skipped}${COLORS.RESET} | ` + - `${COLORS.RED}Failed: ${this.failed}${COLORS.RESET}`, + `📋 Suite Summary: ${this.passed}/${total} passed (Success Rate: ${percentage}%)\n`, ); - line( - ` Success Rate: ${statusColor}${percentage}%${COLORS.RESET}`, - statusColor, - ); - line("─────────────────────────────────────────────", COLORS.GRAY); - line(); - return this.results; } - /** - * Center text helper - */ _padCenter(text, width) { const pad = Math.max(0, width - text.length); return ( @@ -266,4 +865,4 @@ class SkipTest extends Error { } } -export { TestRunner }; +export { SkipTest, TestRunner }; diff --git a/src/test/tester.tests.js b/src/test/tester.tests.js new file mode 100644 index 000000000..2feb36020 --- /dev/null +++ b/src/test/tester.tests.js @@ -0,0 +1,104 @@ +import { SkipTest, TestRunner } from "./tester"; + +// A tester to test the tester +export async function runTesterTests(writeOutput) { + const runner = new TestRunner("Test Runner Verification"); + + // Test 1: Assertions passing + runner.test("Assertion passing", (test) => { + test.assert(true, "True should pass"); + }); + + // Test 2: Assertions failing + runner.test("Assertion failing", (test) => { + try { + test.assert(false, "Should fail"); + throw new Error("Should have thrown assertion failure"); + } catch (e) { + test.assertEqual(e.message, "Should fail"); + } + }); + + // Test 3: Assert equal passing + runner.test("Assert equal passing", (test) => { + test.assertEqual(1 + 1, 2, "1 + 1 = 2"); + }); + + // Test 4: Assert equal failing + runner.test("Assert equal failing", (test) => { + try { + test.assertEqual(1 + 1, 3, "1 + 1 != 3"); + throw new Error("Should have thrown assertion failure"); + } catch (e) { + test.assertEqual(e.message, "1 + 1 != 3"); + } + }); + + // Test 5: Skip test handling + runner.test("Skip test handling", async (test) => { + try { + test.skip("Ignore this test"); + throw new Error("Should have skipped"); + } catch (e) { + test.assert(e instanceof SkipTest, "Should throw SkipTest instance"); + test.assertEqual(e.message, "Ignore this test"); + } + }); + + // Test 6: Async timeout verification + runner.test("Async timeout verification", async (test) => { + // Use register = false so this mock runner doesn't pollute Acode UI state + const dummyRunner = new TestRunner("Dummy Suite", false); + dummyRunner.test("Long running test", async () => { + return new Promise((resolve) => setTimeout(resolve, 500)); + }); + + // Run with a very short timeout of 50ms + try { + await dummyRunner._runWithTimeout( + dummyRunner.tests[0].fn, + dummyRunner, + 50, + ); + throw new Error("Test should have timed out"); + } catch (e) { + test.assert( + e.message.includes("timed out"), + "Should contain timed out message", + ); + } + }); + + // Test 7: Runner execution and statistics + runner.test("Runner execution and statistics", async (test) => { + // Use register = false so this mock runner doesn't pollute Acode UI state + const mockRunner = new TestRunner("Mock Suite", false); + + mockRunner.test("Passing test", (t) => { + t.assert(true); + }); + + mockRunner.test("Failing test", (t) => { + t.assert(false, "Fail intentional"); + }); + + mockRunner.test("Skipped test", (t) => { + t.skip("Skip intentional"); + }); + + // Run mock runner with a dummy logger + const results = await mockRunner.run(() => {}); + + test.assertEqual(results.length, 3); + test.assertEqual(results[0].status, "PASS"); + test.assertEqual(results[1].status, "FAIL"); + test.assertEqual(results[2].status, "SKIP"); + + test.assertEqual(mockRunner.passed, 1); + test.assertEqual(mockRunner.failed, 1); + test.assertEqual(mockRunner.skipped, 1); + }); + + // Run all verification tests + return await runner.run(writeOutput); +} From a5cfcd44747ab3b1f960061ca77920fb19fc9999 Mon Sep 17 00:00:00 2001 From: Rohit Kushwaha Date: Wed, 24 Jun 2026 18:21:47 +0530 Subject: [PATCH 2/5] removed meta tests --- src/test/test-definitions.js | 13 ++--- src/test/tester.tests.js | 104 ----------------------------------- 2 files changed, 6 insertions(+), 111 deletions(-) delete mode 100644 src/test/tester.tests.js diff --git a/src/test/test-definitions.js b/src/test/test-definitions.js index e7cf47421..178d30c62 100644 --- a/src/test/test-definitions.js +++ b/src/test/test-definitions.js @@ -11,11 +11,10 @@ import { runUrlTests } from "./url.tests"; * these are just functions that runs tests by creating a instance of TestRunner */ export const testDefinitions = [ - runTesterTests, - runSanityTests, - runExecutorTests, - runUrlTests, - runFsTests, - runCodeMirrorTests, - runAceCompatibilityTests, + runSanityTests, + runExecutorTests, + runUrlTests, + runFsTests, + runCodeMirrorTests, + runAceCompatibilityTests, ]; diff --git a/src/test/tester.tests.js b/src/test/tester.tests.js deleted file mode 100644 index 2feb36020..000000000 --- a/src/test/tester.tests.js +++ /dev/null @@ -1,104 +0,0 @@ -import { SkipTest, TestRunner } from "./tester"; - -// A tester to test the tester -export async function runTesterTests(writeOutput) { - const runner = new TestRunner("Test Runner Verification"); - - // Test 1: Assertions passing - runner.test("Assertion passing", (test) => { - test.assert(true, "True should pass"); - }); - - // Test 2: Assertions failing - runner.test("Assertion failing", (test) => { - try { - test.assert(false, "Should fail"); - throw new Error("Should have thrown assertion failure"); - } catch (e) { - test.assertEqual(e.message, "Should fail"); - } - }); - - // Test 3: Assert equal passing - runner.test("Assert equal passing", (test) => { - test.assertEqual(1 + 1, 2, "1 + 1 = 2"); - }); - - // Test 4: Assert equal failing - runner.test("Assert equal failing", (test) => { - try { - test.assertEqual(1 + 1, 3, "1 + 1 != 3"); - throw new Error("Should have thrown assertion failure"); - } catch (e) { - test.assertEqual(e.message, "1 + 1 != 3"); - } - }); - - // Test 5: Skip test handling - runner.test("Skip test handling", async (test) => { - try { - test.skip("Ignore this test"); - throw new Error("Should have skipped"); - } catch (e) { - test.assert(e instanceof SkipTest, "Should throw SkipTest instance"); - test.assertEqual(e.message, "Ignore this test"); - } - }); - - // Test 6: Async timeout verification - runner.test("Async timeout verification", async (test) => { - // Use register = false so this mock runner doesn't pollute Acode UI state - const dummyRunner = new TestRunner("Dummy Suite", false); - dummyRunner.test("Long running test", async () => { - return new Promise((resolve) => setTimeout(resolve, 500)); - }); - - // Run with a very short timeout of 50ms - try { - await dummyRunner._runWithTimeout( - dummyRunner.tests[0].fn, - dummyRunner, - 50, - ); - throw new Error("Test should have timed out"); - } catch (e) { - test.assert( - e.message.includes("timed out"), - "Should contain timed out message", - ); - } - }); - - // Test 7: Runner execution and statistics - runner.test("Runner execution and statistics", async (test) => { - // Use register = false so this mock runner doesn't pollute Acode UI state - const mockRunner = new TestRunner("Mock Suite", false); - - mockRunner.test("Passing test", (t) => { - t.assert(true); - }); - - mockRunner.test("Failing test", (t) => { - t.assert(false, "Fail intentional"); - }); - - mockRunner.test("Skipped test", (t) => { - t.skip("Skip intentional"); - }); - - // Run mock runner with a dummy logger - const results = await mockRunner.run(() => {}); - - test.assertEqual(results.length, 3); - test.assertEqual(results[0].status, "PASS"); - test.assertEqual(results[1].status, "FAIL"); - test.assertEqual(results[2].status, "SKIP"); - - test.assertEqual(mockRunner.passed, 1); - test.assertEqual(mockRunner.failed, 1); - test.assertEqual(mockRunner.skipped, 1); - }); - - // Run all verification tests - return await runner.run(writeOutput); -} From 74b60c15cc502a7f87430399ece60cbdef599b98 Mon Sep 17 00:00:00 2001 From: Rohit Kushwaha Date: Wed, 24 Jun 2026 18:22:55 +0530 Subject: [PATCH 3/5] format --- src/test/test-definitions.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/test-definitions.js b/src/test/test-definitions.js index 178d30c62..8d8f844d3 100644 --- a/src/test/test-definitions.js +++ b/src/test/test-definitions.js @@ -11,10 +11,10 @@ import { runUrlTests } from "./url.tests"; * these are just functions that runs tests by creating a instance of TestRunner */ export const testDefinitions = [ - runSanityTests, - runExecutorTests, - runUrlTests, - runFsTests, - runCodeMirrorTests, - runAceCompatibilityTests, + runSanityTests, + runExecutorTests, + runUrlTests, + runFsTests, + runCodeMirrorTests, + runAceCompatibilityTests, ]; From a20b128b12697964ff3f1ff8cf16122ae1fccc6e Mon Sep 17 00:00:00 2001 From: Rohit Kushwaha Date: Wed, 24 Jun 2026 18:23:47 +0530 Subject: [PATCH 4/5] fix --- src/test/test-definitions.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/test/test-definitions.js b/src/test/test-definitions.js index 8d8f844d3..f0de10576 100644 --- a/src/test/test-definitions.js +++ b/src/test/test-definitions.js @@ -3,7 +3,6 @@ import { runCodeMirrorTests } from "./editor.tests"; import { runExecutorTests } from "./exec.tests"; import { runFsTests } from "./fs.tests"; import { runSanityTests } from "./sanity.tests"; -import { runTesterTests } from "./tester.tests"; import { runUrlTests } from "./url.tests"; /** @@ -11,10 +10,10 @@ import { runUrlTests } from "./url.tests"; * these are just functions that runs tests by creating a instance of TestRunner */ export const testDefinitions = [ - runSanityTests, - runExecutorTests, - runUrlTests, - runFsTests, - runCodeMirrorTests, - runAceCompatibilityTests, + runSanityTests, + runExecutorTests, + runUrlTests, + runFsTests, + runCodeMirrorTests, + runAceCompatibilityTests, ]; From 1284afdbf987afaec1b21b3284bedb13aa485a25 Mon Sep 17 00:00:00 2001 From: Rohit Kushwaha Date: Wed, 24 Jun 2026 18:24:59 +0530 Subject: [PATCH 5/5] format --- src/test/test-definitions.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/test-definitions.js b/src/test/test-definitions.js index f0de10576..f29c7cead 100644 --- a/src/test/test-definitions.js +++ b/src/test/test-definitions.js @@ -10,10 +10,10 @@ import { runUrlTests } from "./url.tests"; * these are just functions that runs tests by creating a instance of TestRunner */ export const testDefinitions = [ - runSanityTests, - runExecutorTests, - runUrlTests, - runFsTests, - runCodeMirrorTests, - runAceCompatibilityTests, + runSanityTests, + runExecutorTests, + runUrlTests, + runFsTests, + runCodeMirrorTests, + runAceCompatibilityTests, ];