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..f29c7cead --- /dev/null +++ b/src/test/test-definitions.js @@ -0,0 +1,19 @@ +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 { 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 = [ + 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 = ( +{test.error}
+ )}
+ {test.status === "SKIP" && test.reason && (
+