diff --git a/.github/workflows/matlab-tests.yml b/.github/workflows/matlab-tests.yml index 135b641..fe50634 100644 --- a/.github/workflows/matlab-tests.yml +++ b/.github/workflows/matlab-tests.yml @@ -26,10 +26,11 @@ jobs: uses: matlab-actions/setup-matlab@v2 - name: Run tests - uses: matlab-actions/run-tests@v2 + uses: matlab-actions/run-command@v2 with: - source-folder: toolbox - code-coverage-cobertura: code-coverage/coverage.xml + command: | + openProject("uihtml-debugger.prj"); + runTests; - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 diff --git a/README.md b/README.md index 375a836..d73cfba 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,14 @@ The UIHTML Debugger is a non-intrusive toolkit designed to bridge the gap between MATLAB and JavaScript development. It provides two essential tools to streamline web-based UI development within MATLAB: 1. **Console Error Rerouter**: Forwards console.log, warn, and error messages directly to the MATLAB Command Window. -2. **UIHTML DevTools**: Injects Eruda—a full-featured mobile-style console—directly into your UI for deep inspection without leaving the MATLAB environment. +1. **UIHTML DevTools**: Injects Eruda—a full-featured mobile-style console—directly into your UI for deep inspection without leaving the MATLAB environment. + +--- + +## Requirements + +* **MATLAB R2023a** or later. +* **Base MATLAB** (no additional toolboxes required). --- diff --git a/resources/project/0qs9rsEL3yBSqUmUWZjEFIhMQPo/2rAGxcYfPv6icDMAq1Qlbz9L_I0d.xml b/resources/project/0qs9rsEL3yBSqUmUWZjEFIhMQPo/2rAGxcYfPv6icDMAq1Qlbz9L_I0d.xml new file mode 100644 index 0000000..8f602f1 --- /dev/null +++ b/resources/project/0qs9rsEL3yBSqUmUWZjEFIhMQPo/2rAGxcYfPv6icDMAq1Qlbz9L_I0d.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/0qs9rsEL3yBSqUmUWZjEFIhMQPo/2rAGxcYfPv6icDMAq1Qlbz9L_I0p.xml b/resources/project/0qs9rsEL3yBSqUmUWZjEFIhMQPo/2rAGxcYfPv6icDMAq1Qlbz9L_I0p.xml new file mode 100644 index 0000000..e55acbe --- /dev/null +++ b/resources/project/0qs9rsEL3yBSqUmUWZjEFIhMQPo/2rAGxcYfPv6icDMAq1Qlbz9L_I0p.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/0qs9rsEL3yBSqUmUWZjEFIhMQPo/SLdGz8tUgmpJ67GHyRyb0tvRNuUd.xml b/resources/project/0qs9rsEL3yBSqUmUWZjEFIhMQPo/SLdGz8tUgmpJ67GHyRyb0tvRNuUd.xml new file mode 100644 index 0000000..7688e41 --- /dev/null +++ b/resources/project/0qs9rsEL3yBSqUmUWZjEFIhMQPo/SLdGz8tUgmpJ67GHyRyb0tvRNuUd.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/0qs9rsEL3yBSqUmUWZjEFIhMQPo/SLdGz8tUgmpJ67GHyRyb0tvRNuUp.xml b/resources/project/0qs9rsEL3yBSqUmUWZjEFIhMQPo/SLdGz8tUgmpJ67GHyRyb0tvRNuUp.xml new file mode 100644 index 0000000..baafa8b --- /dev/null +++ b/resources/project/0qs9rsEL3yBSqUmUWZjEFIhMQPo/SLdGz8tUgmpJ67GHyRyb0tvRNuUp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/0qs9rsEL3yBSqUmUWZjEFIhMQPo/gY9EjcEXQx_03t0CxCXVb3wLivEd.xml b/resources/project/0qs9rsEL3yBSqUmUWZjEFIhMQPo/gY9EjcEXQx_03t0CxCXVb3wLivEd.xml new file mode 100644 index 0000000..79316c3 --- /dev/null +++ b/resources/project/0qs9rsEL3yBSqUmUWZjEFIhMQPo/gY9EjcEXQx_03t0CxCXVb3wLivEd.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/0qs9rsEL3yBSqUmUWZjEFIhMQPo/gY9EjcEXQx_03t0CxCXVb3wLivEp.xml b/resources/project/0qs9rsEL3yBSqUmUWZjEFIhMQPo/gY9EjcEXQx_03t0CxCXVb3wLivEp.xml new file mode 100644 index 0000000..b9195e0 --- /dev/null +++ b/resources/project/0qs9rsEL3yBSqUmUWZjEFIhMQPo/gY9EjcEXQx_03t0CxCXVb3wLivEp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/fjRQtWiSIy7hIlj-Kmk87M7s21k/0qs9rsEL3yBSqUmUWZjEFIhMQPod.xml b/resources/project/fjRQtWiSIy7hIlj-Kmk87M7s21k/0qs9rsEL3yBSqUmUWZjEFIhMQPod.xml new file mode 100644 index 0000000..fbd21c0 --- /dev/null +++ b/resources/project/fjRQtWiSIy7hIlj-Kmk87M7s21k/0qs9rsEL3yBSqUmUWZjEFIhMQPod.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/fjRQtWiSIy7hIlj-Kmk87M7s21k/0qs9rsEL3yBSqUmUWZjEFIhMQPop.xml b/resources/project/fjRQtWiSIy7hIlj-Kmk87M7s21k/0qs9rsEL3yBSqUmUWZjEFIhMQPop.xml new file mode 100644 index 0000000..274d91f --- /dev/null +++ b/resources/project/fjRQtWiSIy7hIlj-Kmk87M7s21k/0qs9rsEL3yBSqUmUWZjEFIhMQPop.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/fjRQtWiSIy7hIlj-Kmk87M7s21k/lJpff9HwDURjOood0D_4QIQyyZod.xml b/resources/project/fjRQtWiSIy7hIlj-Kmk87M7s21k/lJpff9HwDURjOood0D_4QIQyyZod.xml new file mode 100644 index 0000000..b706d30 --- /dev/null +++ b/resources/project/fjRQtWiSIy7hIlj-Kmk87M7s21k/lJpff9HwDURjOood0D_4QIQyyZod.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/fjRQtWiSIy7hIlj-Kmk87M7s21k/lJpff9HwDURjOood0D_4QIQyyZop.xml b/resources/project/fjRQtWiSIy7hIlj-Kmk87M7s21k/lJpff9HwDURjOood0D_4QIQyyZop.xml new file mode 100644 index 0000000..731c128 --- /dev/null +++ b/resources/project/fjRQtWiSIy7hIlj-Kmk87M7s21k/lJpff9HwDURjOood0D_4QIQyyZop.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/lJpff9HwDURjOood0D_4QIQyyZo/M2j0tNMQnrX0MAixgunqSH6O1cUd.xml b/resources/project/lJpff9HwDURjOood0D_4QIQyyZo/M2j0tNMQnrX0MAixgunqSH6O1cUd.xml new file mode 100644 index 0000000..79316c3 --- /dev/null +++ b/resources/project/lJpff9HwDURjOood0D_4QIQyyZo/M2j0tNMQnrX0MAixgunqSH6O1cUd.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/lJpff9HwDURjOood0D_4QIQyyZo/M2j0tNMQnrX0MAixgunqSH6O1cUp.xml b/resources/project/lJpff9HwDURjOood0D_4QIQyyZo/M2j0tNMQnrX0MAixgunqSH6O1cUp.xml new file mode 100644 index 0000000..7397d9b --- /dev/null +++ b/resources/project/lJpff9HwDURjOood0D_4QIQyyZo/M2j0tNMQnrX0MAixgunqSH6O1cUp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/lJpff9HwDURjOood0D_4QIQyyZo/O-y4x_Fk_elHFgVfW5CS2Gg4bVEd.xml b/resources/project/lJpff9HwDURjOood0D_4QIQyyZo/O-y4x_Fk_elHFgVfW5CS2Gg4bVEd.xml new file mode 100644 index 0000000..7688e41 --- /dev/null +++ b/resources/project/lJpff9HwDURjOood0D_4QIQyyZo/O-y4x_Fk_elHFgVfW5CS2Gg4bVEd.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/lJpff9HwDURjOood0D_4QIQyyZo/O-y4x_Fk_elHFgVfW5CS2Gg4bVEp.xml b/resources/project/lJpff9HwDURjOood0D_4QIQyyZo/O-y4x_Fk_elHFgVfW5CS2Gg4bVEp.xml new file mode 100644 index 0000000..d0c8018 --- /dev/null +++ b/resources/project/lJpff9HwDURjOood0D_4QIQyyZo/O-y4x_Fk_elHFgVfW5CS2Gg4bVEp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/lJpff9HwDURjOood0D_4QIQyyZo/PBlyKlej6TlbKRuFfxG9YYP9NA8d.xml b/resources/project/lJpff9HwDURjOood0D_4QIQyyZo/PBlyKlej6TlbKRuFfxG9YYP9NA8d.xml new file mode 100644 index 0000000..8f602f1 --- /dev/null +++ b/resources/project/lJpff9HwDURjOood0D_4QIQyyZo/PBlyKlej6TlbKRuFfxG9YYP9NA8d.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/lJpff9HwDURjOood0D_4QIQyyZo/PBlyKlej6TlbKRuFfxG9YYP9NA8p.xml b/resources/project/lJpff9HwDURjOood0D_4QIQyyZo/PBlyKlej6TlbKRuFfxG9YYP9NA8p.xml new file mode 100644 index 0000000..673600c --- /dev/null +++ b/resources/project/lJpff9HwDURjOood0D_4QIQyyZo/PBlyKlej6TlbKRuFfxG9YYP9NA8p.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/TCsOjEr3rMe6CtDnuQLgASe-kAEd.xml b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/TCsOjEr3rMe6CtDnuQLgASe-kAEd.xml new file mode 100644 index 0000000..99772b4 --- /dev/null +++ b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/TCsOjEr3rMe6CtDnuQLgASe-kAEd.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/TCsOjEr3rMe6CtDnuQLgASe-kAEp.xml b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/TCsOjEr3rMe6CtDnuQLgASe-kAEp.xml new file mode 100644 index 0000000..323751e --- /dev/null +++ b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/TCsOjEr3rMe6CtDnuQLgASe-kAEp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/root/amwE3LmoG--0pRWluRol6WgE4ZYd.xml b/resources/project/root/amwE3LmoG--0pRWluRol6WgE4ZYd.xml index cb17be0..7192f7b 100644 --- a/resources/project/root/amwE3LmoG--0pRWluRol6WgE4ZYd.xml +++ b/resources/project/root/amwE3LmoG--0pRWluRol6WgE4ZYd.xml @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/resources/project/root/xRfC_Iq2vKr7PkzRJo-T0mI2Drkd.xml b/resources/project/root/xRfC_Iq2vKr7PkzRJo-T0mI2Drkd.xml new file mode 100644 index 0000000..4ce2746 --- /dev/null +++ b/resources/project/root/xRfC_Iq2vKr7PkzRJo-T0mI2Drkd.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/root/xRfC_Iq2vKr7PkzRJo-T0mI2Drkp.xml b/resources/project/root/xRfC_Iq2vKr7PkzRJo-T0mI2Drkp.xml new file mode 100644 index 0000000..20870d6 --- /dev/null +++ b/resources/project/root/xRfC_Iq2vKr7PkzRJo-T0mI2Drkp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/xRfC_Iq2vKr7PkzRJo-T0mI2Drk/yBqJXaEP8Uc9sR9kNtkek_ebn80d.xml b/resources/project/xRfC_Iq2vKr7PkzRJo-T0mI2Drk/yBqJXaEP8Uc9sR9kNtkek_ebn80d.xml new file mode 100644 index 0000000..4356a6a --- /dev/null +++ b/resources/project/xRfC_Iq2vKr7PkzRJo-T0mI2Drk/yBqJXaEP8Uc9sR9kNtkek_ebn80d.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/xRfC_Iq2vKr7PkzRJo-T0mI2Drk/yBqJXaEP8Uc9sR9kNtkek_ebn80p.xml b/resources/project/xRfC_Iq2vKr7PkzRJo-T0mI2Drk/yBqJXaEP8Uc9sR9kNtkek_ebn80p.xml new file mode 100644 index 0000000..01cb34e --- /dev/null +++ b/resources/project/xRfC_Iq2vKr7PkzRJo-T0mI2Drk/yBqJXaEP8Uc9sR9kNtkek_ebn80p.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/runTests.m b/runTests.m new file mode 100644 index 0000000..8224f65 --- /dev/null +++ b/runTests.m @@ -0,0 +1,29 @@ +import matlab.unittest.TestRunner; +import matlab.unittest.TestSuite; +import matlab.unittest.plugins.CodeCoveragePlugin; +import matlab.unittest.plugins.codecoverage.CoberturaFormat; + +% Create suite +suite = TestSuite.fromFolder('tests', 'IncludingSubfolders', true); + +% Create runner +runner = TestRunner.withTextOutput; + +% Configure coverage +sourceFiles = { ... + fullfile(pwd, 'toolbox', 'ConsoleErrorRerouter.m'), ... + fullfile(pwd, 'toolbox', 'UIHTMLDevTools.m') ... +}; + +mkdir('code-coverage'); +coverageFile = fullfile(pwd, 'code-coverage', 'coverage.xml'); +plugin = CodeCoveragePlugin.forFile(sourceFiles, ... + 'Producing', CoberturaFormat(coverageFile)); +runner.addPlugin(plugin); + +% Run tests +results = runner.run(suite); + +% Output results for CI +display(results); +assertSuccess(results); diff --git a/tests/MockComponent.m b/tests/MockComponent.m index d57807b..f72ffaf 100644 --- a/tests/MockComponent.m +++ b/tests/MockComponent.m @@ -1,4 +1,4 @@ -classdef MockComponent < handle +classdef MockComponent < handle & dynamicprops properties HTMLSource end diff --git a/tests/tConsoleErrorRerouter.m b/tests/tConsoleErrorRerouter.m index 500a8c2..9455aa1 100644 --- a/tests/tConsoleErrorRerouter.m +++ b/tests/tConsoleErrorRerouter.m @@ -60,6 +60,13 @@ function testConstructorInvalidComponent(testCase) "ConsoleErrorRerouter:InvalidComponent"); end % function testConstructorInvalidComponent + function testConstructorNoSource(testCase) + testCase.Component.HTMLSource = ""; + rerouter = ConsoleErrorRerouter(testCase.Component); + testCase.addTeardown(@() delete(rerouter)); + testCase.verifyEqual(string(testCase.Component.HTMLSource), ""); + end % function testConstructorNoSource + function testUrlSourceMock(testCase) mockComp = MockComponent(); mockComp.HTMLSource = "http://example.com"; @@ -216,8 +223,115 @@ function testPayloadEdgeCases(testCase) 'HTMLEventData', struct('level', 'error', 'message', 'No stack')); rerouter.onHTMLEventReceived([], goodData); testCase.verifySubstring(rerouter.LastMessage, "No stack"); + + % Object payload (instead of struct) + mockPayload = struct('level', 'error', 'message', 'Object message'); + objData = struct('HTMLEventName', "ConsoleError", ... + 'HTMLEventData', mockPayload); + rerouter.onHTMLEventReceived([], objData); + testCase.verifySubstring(rerouter.LastMessage, "Object message"); + + % Missing level + badData = struct('HTMLEventName', "ConsoleError", ... + 'HTMLEventData', struct('message', 'No level')); + % We can't set LastMessage, but we know it should NOT be "No level" + % It currently is "Object message" from previous step + rerouter.onHTMLEventReceived([], badData); + testCase.verifyEqual(rerouter.LastMessage, "Object message"); + + % Other event name + otherData = struct('HTMLEventName', "OtherEvent", ... + 'HTMLEventData', struct('level', 'error', 'message', 'Ignored')); + rerouter.onHTMLEventReceived([], otherData); + testCase.verifyEqual(rerouter.LastMessage, "Object message"); end % function testPayloadEdgeCases + + function testListenerSetupFailure(testCase) + tempDir = tempname; + mkdir(tempDir); + testCase.addTeardown(@() rmdir(tempDir, 's')); + htmlFile = fullfile(tempDir, "test.html"); + writelines("", htmlFile); + + % Create a struct that doesn't support listeners but has valid HTMLSource + mock = struct('HTMLSource', htmlFile); + rerouter = ConsoleErrorRerouter(mock); + testCase.addTeardown(@() delete(rerouter)); + testCase.verifyClass(rerouter, "ConsoleErrorRerouter"); + end % function testListenerSetupFailure + + function testDeleteInvalidComponent(testCase) + rerouter = ConsoleErrorRerouter(testCase.Component); + testCase.Rerouter = rerouter; + delete(testCase.Figure); % Deletes component too + delete(rerouter); % Should not error in removeShim + testCase.Rerouter = []; + end % function testDeleteInvalidComponent + + function testOriginalSourceDeleted(testCase) + tempDir = tempname; + mkdir(tempDir); + testCase.addTeardown(@() rmdir(tempDir, 's')); + htmlFile = fullfile(tempDir, "temp.html"); + writelines("", htmlFile); + + mockComp = MockComponent(); + mockComp.HTMLSource = htmlFile; + rerouter = ConsoleErrorRerouter(mockComp); + + % Delete original source before rerouter is deleted + delete(htmlFile); + + % removeShim should handle this gracefully + delete(rerouter); + testCase.verifyEqual(string(mockComp.HTMLSource), string(htmlFile)); + end % function testOriginalSourceDeleted + function testCleanupWarnings(testCase) + if isunix + tempDir = tempname; + mkdir(tempDir); + testCase.addTeardown(@() rmdir(tempDir, 's')); + htmlFile = fullfile(tempDir, "test.html"); + writelines("", htmlFile); + + mockComp = MockComponent(); + mockComp.HTMLSource = htmlFile; + rerouter = ConsoleErrorRerouter(mockComp); + + % On Linux, to make delete fail, we make the directory read-only + tempFile = string(mockComp.HTMLSource); + fileattrib(tempDir, '-w'); + testCase.addTeardown(@() fileattrib(tempDir, '+w')); + + testCase.verifyWarning(@() delete(rerouter), "ConsoleErrorRerouter:FailedCleanup"); + end + end % function testCleanupWarnings + + function testTempWriteFailure(testCase) + if isunix + % On Unix, we can easily make a directory read-only + tempDir = tempname; + mkdir(tempDir); + testCase.addTeardown(@() rmdir(tempDir, 's')); + htmlFile = fullfile(tempDir, "test.html"); + writelines("", htmlFile); + + % Make dir read-only + fileattrib(tempDir, '-w'); + testCase.addTeardown(@() fileattrib(tempDir, '+w')); + + mockComp = MockComponent(); + mockComp.HTMLSource = htmlFile; + testCase.verifyError(@() ConsoleErrorRerouter(mockComp), ... + "ConsoleErrorRerouter:TempWriteFailure"); + else + % On Windows, it's harder to reliably trigger this via attributes, + % so we might skip or use a different trick if needed. + % For now, we'll assume unix coverage in CI is enough or skip. + end + end % function testTempWriteFailure + function testEmptyTargetDir(testCase) origDir = pwd; tempDir = tempname; diff --git a/tests/tUIHTMLDevTools.m b/tests/tUIHTMLDevTools.m index 6fba5ac..639aaae 100644 --- a/tests/tUIHTMLDevTools.m +++ b/tests/tUIHTMLDevTools.m @@ -74,7 +74,7 @@ function testEnabledToggle(testCase) testCase.verifyNotEqual(tempFile, string(origHtml)); testCase.verifyTrue(isfile(tempFile)); - % Set Enabled to same value + % Set Enabled to same value - should be a no-op devTools.Enabled = true; % Disable @@ -131,40 +131,109 @@ function testEmptyTargetDir(testCase) mkdir(tempDir); testCase.addTeardown(@() rmdir(tempDir, 's')); testCase.addTeardown(@() cd(origDir)); - + testFile = which(mfilename); testDir = fileparts(testFile); origHtml = fullfile(testDir, "html", "test_page.html"); copyfile(origHtml, fullfile(tempDir, "test.html")); - + cd(tempDir); mockComp = MockComponent(); mockComp.HTMLSource = "test.html"; - + devTools = UIHTMLDevTools(mockComp); testCase.addTeardown(@() delete(devTools)); - + testCase.verifyTrue(isfile("eruda.js")); testCase.verifySubstring(string(mockComp.HTMLSource), "_devtools_"); end % function testEmptyTargetDir + function testCleanupWarnings(testCase) + if isunix + tempDir = tempname; + mkdir(tempDir); + testCase.addTeardown(@() rmdir(tempDir, 's')); + htmlFile = fullfile(tempDir, "test.html"); + writelines("", htmlFile); + + mockComp = MockComponent(); + mockComp.HTMLSource = htmlFile; + devTools = UIHTMLDevTools(mockComp); + + % On Linux, make the directory read-only to force delete failure + fileattrib(tempDir, '-w'); + testCase.addTeardown(@() fileattrib(tempDir, '+w')); + + testCase.verifyWarning(@() delete(devTools), "uihtmlDevTools:FailedCleanup"); + end + end % function testCleanupWarnings + function testDeleteWithMissingFiles(testCase) devTools = UIHTMLDevTools(testCase.Component); - + % Delete files manually before object delete tempFile = string(testCase.Component.HTMLSource); - delete(tempFile); - + if isfile(tempFile) + delete(tempFile); + end + targetDir = fileparts(testCase.FixtureHtml); pErudaDest = fullfile(targetDir, "eruda.js"); if isfile(pErudaDest) delete(pErudaDest); end - - % Should not error/warning if they don't exist + + % Should not error or warn if files don't exist delete(devTools); end % function testDeleteWithMissingFiles + function testDeleteInvalidComponent(testCase) + devTools = UIHTMLDevTools(testCase.Component); + testCase.DevTools = devTools; + delete(testCase.Figure); % Deletes component too + delete(devTools); % Should not error in removeEruda + testCase.DevTools = []; + end % function testDeleteInvalidComponent + + function testOriginalSourceDeleted(testCase) + tempDir = tempname; + mkdir(tempDir); + testCase.addTeardown(@() rmdir(tempDir, 's')); + htmlFile = fullfile(tempDir, "temp.html"); + writelines("", htmlFile); + + mockComp = MockComponent(); + mockComp.HTMLSource = htmlFile; + devTools = UIHTMLDevTools(mockComp); + + % Delete original source before devTools is deleted + delete(htmlFile); + + % removeEruda should handle this gracefully + delete(devTools); + testCase.verifyEqual(string(mockComp.HTMLSource), string(htmlFile)); + end % function testOriginalSourceDeleted + + function testErudaCopyFailure(testCase) + if isunix + tempDir = tempname; + mkdir(tempDir); + testCase.addTeardown(@() rmdir(tempDir, 's')); + htmlFile = fullfile(tempDir, "test.html"); + writelines("", htmlFile); + + % Make directory read-only -- copyfile runs before writelines + % so ErudaCopyFailure is always the first error thrown + fileattrib(tempDir, '-w'); + testCase.addTeardown(@() fileattrib(tempDir, '+w')); + + mockComp = MockComponent(); + mockComp.HTMLSource = htmlFile; + testCase.verifyError(@() UIHTMLDevTools(mockComp), ... + "uihtmlDevTools:ErudaCopyFailure"); + end + end % function testErudaCopyFailure + function testUrlSourceMock(testCase) mockComp = MockComponent(); mockComp.HTMLSource = "http://example.com"; @@ -179,4 +248,4 @@ function testInvalidFileSourceMock(testCase) "uihtmlDevTools:InvalidHTMLSource"); end % function testInvalidFileSourceMock end % methods (Test) -end % classdef tUIHTMLDevTools +end % classdef tUIHTMLDevTools \ No newline at end of file diff --git a/toolbox/ConsoleErrorRerouter.m b/toolbox/ConsoleErrorRerouter.m index 85266bf..5310666 100644 --- a/toolbox/ConsoleErrorRerouter.m +++ b/toolbox/ConsoleErrorRerouter.m @@ -61,7 +61,7 @@ end % Handle shim delivery if HTMLSource is provided - if ~isempty(string(uihtmlComp.HTMLSource)) + if strlength(string(uihtmlComp.HTMLSource)) > 0 obj.injectShim(); end end % Constructor @@ -124,7 +124,7 @@ function injectShim(obj) writelines(newHtml,obj.TempHTMLPath); catch error("ConsoleErrorRerouter:TempWriteFailure", ... - "Filed to write temporary html file to:\n%s",obj.TempHTMLPath); + "Failed to write temporary html file to:\n%s",obj.TempHTMLPath); end % Update the component's HTMLSource with the temporary file path. @@ -136,21 +136,17 @@ function removeShim(obj) % removeShim Restores the original HTML and cleans up the temporary file. if isa(obj.HtmlComponent, "handle") && isvalid(obj.HtmlComponent) && ... strlength(obj.OriginalHTMLSource) > 0 - % Check if OriginalHTMLSource still exists (it might be a temp file of another tool) - if isfile(obj.OriginalHTMLSource) || startsWith(obj.OriginalHTMLSource, "http") - try - obj.HtmlComponent.HTMLSource = obj.OriginalHTMLSource; - catch - % Ignore restoration errors if the file was already deleted by another tool - end + try + obj.HtmlComponent.HTMLSource = obj.OriginalHTMLSource; + catch + % Ignore restoration errors end end % Delete temporary HTML file if ~isempty(obj.TempHTMLPath) && isfile(obj.TempHTMLPath) - try - delete(obj.TempHTMLPath); - catch + delete(obj.TempHTMLPath); + if isfile(obj.TempHTMLPath) warning("ConsoleErrorRerouter:FailedCleanup", ... "Failed to delete %s. Please check your file system", ... obj.TempHTMLPath) diff --git a/toolbox/UIHTMLDevTools.m b/toolbox/UIHTMLDevTools.m index 6a78109..99f0eff 100644 --- a/toolbox/UIHTMLDevTools.m +++ b/toolbox/UIHTMLDevTools.m @@ -134,21 +134,17 @@ function removeEruda(obj) % removeEruda Restores the original HTML and cleans up the temporary files. if isa(obj.HtmlComponent, "handle") && isvalid(obj.HtmlComponent) && ... strlength(obj.OriginalHTMLSource) > 0 - % Check if OriginalHTMLSource still exists (it might be a temp file of another tool) - if isfile(obj.OriginalHTMLSource) || startsWith(obj.OriginalHTMLSource, "http") - try - obj.HtmlComponent.HTMLSource = obj.OriginalHTMLSource; - catch - % Ignore restoration errors - end + try + obj.HtmlComponent.HTMLSource = obj.OriginalHTMLSource; + catch + % Ignore restoration errors end end % Delete temporary HTML file if strlength(obj.TempHTMLPath) > 0 && isfile(obj.TempHTMLPath) - try - delete(obj.TempHTMLPath); - catch + delete(obj.TempHTMLPath); + if isfile(obj.TempHTMLPath) warning("uihtmlDevTools:FailedCleanup", ... "Failed to delete %s. Please check your file system", ... obj.TempHTMLPath) @@ -163,9 +159,8 @@ function removeEruda(obj) end pErudaDest = fullfile(targetDir, "eruda.js"); if isfile(pErudaDest) - try - delete(pErudaDest); - catch + delete(pErudaDest); + if isfile(pErudaDest) warning("uihtmlDevTools:FailedCleanup", ... "Failed to delete %s. Please check your file system", ... pErudaDest)