diff --git a/src/ace/commands.js b/src/ace/commands.js index e917fd847..dcb707a51 100644 --- a/src/ace/commands.js +++ b/src/ace/commands.js @@ -357,6 +357,14 @@ const commands = [ }, readOnly: true, }, + { + name: "run-tests", + description: "Run Tests", + exec() { + acode.exec("run-tests"); + }, + readOnly: true, + }, ]; export function setCommands(editor) { diff --git a/src/lib/commands.js b/src/lib/commands.js index 8e01e4583..162e1e494 100644 --- a/src/lib/commands.js +++ b/src/lib/commands.js @@ -20,6 +20,7 @@ import findFile from "palettes/findFile"; import browser from "plugins/browser"; import help from "settings/helpSettings"; import mainSettings from "settings/mainSettings"; +import { runAllTests } from "test/tester"; import { getColorRange } from "utils/color/regex"; import helpers from "utils/helpers"; import Url from "utils/Url"; @@ -34,6 +35,9 @@ import appSettings from "./settings"; import showFileInfo from "./showFileInfo"; export default { + async "run-tests"() { + await runAllTests(); + }, async "close-all-tabs"() { let save = false; const unsavedFiles = editorManager.files.filter( diff --git a/src/lib/keyBindings.js b/src/lib/keyBindings.js index b7ab35b9a..f09f4d600 100644 --- a/src/lib/keyBindings.js +++ b/src/lib/keyBindings.js @@ -697,4 +697,10 @@ export default { readOnly: true, action: "new-terminal", }, + "run-tests": { + description: "Run Tests", + key: "Ctrl-Shift-T", + readOnly: true, + action: "run-tests", + }, }; diff --git a/src/test/editor.tests.js b/src/test/editor.tests.js new file mode 100644 index 000000000..d48b07973 --- /dev/null +++ b/src/test/editor.tests.js @@ -0,0 +1,217 @@ +import { TestRunner } from "./tester"; + +export async function runAceEditorTests(writeOutput) { + const runner = new TestRunner("Ace Editor API Tests"); + + function createEditor() { + const container = document.createElement("div"); + container.style.width = "500px"; + container.style.height = "300px"; + container.style.backgroundColor = "#a02f2f"; + document.body.appendChild(container); + + const editor = ace.edit(container); + return { editor, container }; + } + + async function withEditor(test, fn) { + let editor, container; + + try { + ({ editor, container } = createEditor()); + test.assert(editor != null, "Editor instance should be created"); + await new Promise((resolve) => setTimeout(resolve, 100)); + await fn(editor); + await new Promise((resolve) => setTimeout(resolve, 200)); + } finally { + if (editor) editor.destroy(); + if (container) container.remove(); + } + } + + // Test 1: Ace is available + runner.test("Ace is loaded", async (test) => { + test.assert(typeof ace !== "undefined", "Ace should be available globally"); + test.assert( + typeof ace.edit === "function", + "ace.edit should be a function", + ); + }); + + // Test 2: Editor creation + runner.test("Editor creation", async (test) => { + const { editor, container } = createEditor(); + test.assert(editor != null, "Editor instance should be created"); + test.assert( + typeof editor.getSession === "function", + "Editor should expose getSession", + ); + editor.destroy(); + container.remove(); + }); + + // Test 3: Session access + runner.test("Session access", async (test) => { + await withEditor(test, async (editor) => { + const session = editor.getSession(); + test.assert(session != null, "Editor session should exist"); + test.assert( + typeof session.getValue === "function", + "Session should expose getValue", + ); + }); + }); + + // Test 4: Set and get value + runner.test("Set and get value", async (test) => { + await withEditor(test, async (editor) => { + const text = "Hello Ace Editor"; + editor.setValue(text, -1); + test.assertEqual(editor.getValue(), text); + }); + }); + + // Test 5: Cursor movement + runner.test("Cursor movement", async (test) => { + await withEditor(test, async (editor) => { + editor.setValue("line1\nline2\nline3", -1); + editor.moveCursorTo(1, 2); + + const pos = editor.getCursorPosition(); + test.assertEqual(pos.row, 1); + test.assertEqual(pos.column, 2); + }); + }); + + // Test 6: Selection API + runner.test("Selection handling", async (test) => { + await withEditor(test, async (editor) => { + editor.setValue("abc\ndef", -1); + editor.selectAll(); + test.assert(editor.getSelectedText().length > 0); + }); + }); + + // Test 7: Undo manager + runner.test("Undo manager works", async (test) => { + await withEditor(test, async (editor) => { + const session = editor.getSession(); + const undoManager = session.getUndoManager(); + + session.setValue("one"); + undoManager.reset(); + + editor.insert("\ntwo"); + editor.undo(); + + test.assertEqual(editor.getValue(), "one"); + }); + }); + + // Test 8: Mode setting + runner.test("Mode setting", async (test) => { + await withEditor(test, async (editor) => { + const session = editor.getSession(); + session.setMode("ace/mode/javascript"); + + const mode = session.getMode(); + test.assert(mode && mode.$id === "ace/mode/javascript"); + }); + }); + + // Test 9: Theme setting + runner.test("Theme setting", async (test) => { + await withEditor(test, async (editor) => { + editor.setTheme("ace/theme/monokai"); + test.assert(editor.getTheme().includes("monokai")); + }); + }); + + // Test 11: Line count + runner.test("Line count", async (test) => { + await withEditor(test, async (editor) => { + editor.setValue("a\nb\nc\nd", -1); + test.assertEqual(editor.session.getLength(), 4); + }); + }); + + // Test 12: Replace text + runner.test("Replace text", async (test) => { + await withEditor(test, async (editor) => { + editor.setValue("hello world", -1); + editor.find("world"); + editor.replace("ace"); + + test.assertEqual(editor.getValue(), "hello ace"); + }); + }); + + // Test 13: Search API + runner.test("Search API", async (test) => { + await withEditor(test, async (editor) => { + editor.setValue("foo bar foo", -1); + editor.find("foo"); + + const range = editor.getSelectionRange(); + test.assert(range.start.column === 0); + }); + }); + + // Test 14: Renderer availability + runner.test("Renderer exists", async (test) => { + await withEditor(test, async (editor) => { + const renderer = editor.renderer; + test.assert(renderer != null); + test.assert(typeof renderer.updateFull === "function"); + }); + }); + + // Test 15: Editor options + runner.test("Editor options", async (test) => { + await withEditor(test, async (editor) => { + editor.setOption("showPrintMargin", false); + test.assertEqual(editor.getOption("showPrintMargin"), false); + }); + }); + + // Test 16: Scroll API + runner.test("Scroll API", async (test) => { + await withEditor(test, async (editor) => { + editor.setValue(Array(100).fill("line").join("\n"), -1); + editor.scrollToLine(50, true, true, () => {}); + + const firstVisibleRow = editor.renderer.getFirstVisibleRow(); + test.assert(firstVisibleRow >= 0); + }); + }); + + // Test 17: Redo manager + runner.test("Redo manager works", async (test) => { + await withEditor(test, async (editor) => { + const session = editor.getSession(); + const undoManager = session.getUndoManager(); + + session.setValue("one"); + undoManager.reset(); + + session.insert({ row: 0, column: 3 }, "\ntwo"); + editor.undo(); + editor.redo(); + + test.assertEqual(editor.getValue(), "one\ntwo"); + }); + }); + + // Test 18: Focus and blur + runner.test("Focus and blur", async (test) => { + await withEditor(test, async (editor) => { + editor.focus(); + test.assert(editor.isFocused()); + + editor.blur(); + test.assert(!editor.isFocused()); + }); + }); + + return await runner.run(writeOutput); +} diff --git a/src/test/sanity.tests.js b/src/test/sanity.tests.js new file mode 100644 index 000000000..28bc84355 --- /dev/null +++ b/src/test/sanity.tests.js @@ -0,0 +1,69 @@ +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"; + test.assertEqual(result, "Hello World", "String concatenation should work"); + }); + + // Test 2: Number operations + runner.test("Basic arithmetic", (test) => { + const sum = 5 + 3; + test.assertEqual(sum, 8, "Addition should work correctly"); + }); + + // Test 3: Array operations + runner.test("Array operations", (test) => { + const arr = [1, 2, 3]; + test.assertEqual(arr.length, 3, "Array length should be correct"); + test.assert(arr.includes(2), "Array should include 2"); + }); + + // Test 4: Object operations + runner.test("Object operations", (test) => { + const obj = { name: "Test", value: 42 }; + test.assertEqual(obj.name, "Test", "Object property should be accessible"); + test.assertEqual(obj.value, 42, "Object value should be correct"); + }); + + // Test 5: Function execution + runner.test("Function execution", (test) => { + const add = (a, b) => a + b; + const result = add(10, 20); + test.assertEqual(result, 30, "Function should return correct value"); + }); + + // Test 6: Async function + runner.test("Async function handling", async (test) => { + const asyncFunc = async () => { + return new Promise((resolve) => { + setTimeout(() => resolve("done"), 10); + }); + }; + + const result = await asyncFunc(); + test.assertEqual(result, "done", "Async function should work correctly"); + }); + + // Test 7: Error handling + runner.test("Error handling", (test) => { + try { + throw new Error("Test error"); + } catch (e) { + test.assert(e instanceof Error, "Should catch Error instances"); + } + }); + + // Test 8: Conditional logic + runner.test("Conditional logic", (test) => { + const value = 10; + test.assert(value > 5, "Condition should be true"); + test.assert(!(value < 5), "Negation should work"); + }); + + // Run all tests + return await runner.run(writeOutput); +} diff --git a/src/test/tester.js b/src/test/tester.js new file mode 100644 index 000000000..a51206523 --- /dev/null +++ b/src/test/tester.js @@ -0,0 +1,229 @@ +import { runAceEditorTests } from "./editor.tests"; +import { runSanityTests } from "./sanity.tests"; + +export async function runAllTests() { + const terminal = acode.require("terminal"); + const local = await terminal.createLocal({ name: "TestCode" }); + function write(data) { + terminal.write(local.id, data); + } + + // 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"); + + try { + // Run unit tests + await runSanityTests(write); + await runAceEditorTests(write); + + write("\x1b[36m\x1b[1mTests completed!\x1b[0m\n"); + } catch (error) { + write(`\x1b[31mโš ๏ธ Test execution error: ${error.message}\x1b[0m\n`); + } +} + +// 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 delay(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +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"); + }; +} + +// Spinner frames +const SPINNER_FRAMES = ["โ ‹", "โ ™", "โ น", "โ ธ", "โ ผ", "โ ด", "โ ฆ", "โ ง", "โ ‡", "โ "]; + +class TestRunner { + constructor(name = "Test Suite") { + this.name = name; + this.tests = []; + this.passed = 0; + this.failed = 0; + this.results = []; + } + + /** + * Register a test + */ + test(testName, testFn) { + this.tests.push({ name: testName, fn: testFn }); + } + + /** + * Assertions + */ + assert(condition, message) { + if (!condition) { + throw new Error(message || "Assertion failed"); + } + } + + assertEqual(actual, expected, message) { + if (actual !== expected) { + throw new Error(message || `Expected ${expected}, got ${actual}`); + } + } + + async _runWithTimeout(fn, ctx, timeoutMs) { + return new Promise((resolve, reject) => { + let finished = false; + + const timer = setTimeout(() => { + if (finished) return; + finished = true; + reject(new Error(`Test timed out after ${timeoutMs}ms`)); + }, timeoutMs); + + Promise.resolve() + .then(() => fn(ctx)) + .then((result) => { + if (finished) return; + finished = true; + clearTimeout(timer); + resolve(result); + }) + .catch((err) => { + if (finished) return; + finished = true; + clearTimeout(timer); + reject(err); + }); + }); + } + + /** + * Run all tests + */ + async run(writeOutput) { + const line = (text = "", color = "") => { + writeOutput(`${color}${text}${COLORS.RESET}\n`); + }; + + // Header + line(); + line( + "โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—", + COLORS.CYAN + COLORS.BRIGHT, + ); + line( + `โ•‘ ๐Ÿงช ${this._padCenter(this.name, 35)} โ”‚`, + COLORS.CYAN + COLORS.BRIGHT, + ); + line( + "โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•", + COLORS.CYAN + COLORS.BRIGHT, + ); + line(); + + // Run tests with spinner + for (const test of this.tests) { + const stopSpinner = startSpinner(writeOutput, `Running ${test.name}...`); + + try { + await delay(200); + + await this._runWithTimeout(test.fn, this, 3000); + + stopSpinner(); + + this.passed++; + this.results.push({ name: test.name, status: "PASS", error: null }); + line(` ${COLORS.GREEN}โœ“${COLORS.RESET} ${test.name}`, COLORS.GREEN); + } catch (error) { + stopSpinner(); + + this.failed++; + this.results.push({ + name: test.name, + status: "FAIL", + 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, + ); + } + } + + // Summary + line(); + line("โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€", COLORS.GRAY); + + const total = this.tests.length; + const percentage = total ? ((this.passed / total) * 100).toFixed(1) : "0.0"; + + const statusColor = this.failed === 0 ? COLORS.GREEN : COLORS.YELLOW; + + line( + ` Tests: ${COLORS.BRIGHT}${total}${COLORS.RESET} | ` + + `${statusColor}Passed: ${this.passed}${COLORS.RESET} | ` + + `${COLORS.RED}Failed: ${this.failed}${COLORS.RESET}`, + statusColor, + ); + + 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 ( + " ".repeat(Math.floor(pad / 2)) + text + " ".repeat(Math.ceil(pad / 2)) + ); + } +} + +export { TestRunner };