diff --git a/framework/src/components/rooibos/RooibosResultRow.brs b/framework/src/components/rooibos/RooibosResultRow.brs
new file mode 100644
index 00000000..4377c2e8
--- /dev/null
+++ b/framework/src/components/rooibos/RooibosResultRow.brs
@@ -0,0 +1,73 @@
+function init() as void
+ m.bar = m.top.findNode("bar")
+ m.nameLabel = m.top.findNode("nameLabel")
+ m.countLabel = m.top.findNode("countLabel")
+
+ m.colors = {
+ passBar: "#3ddc97"
+ failBar: "#ff5470"
+ passName: "#cfcfdc"
+ failName: "#ffdcdc"
+ focusName: "#6c63ff"
+ countText: "#ff5470"
+ }
+ m.countLabel.color = m.colors.countText
+
+ m.passed = true
+ m.failedCount = 0
+ m.totalCount = 0
+end function
+
+function onItemContentChanged() as void
+ content = m.top.itemContent
+ if content = invalid then return
+
+ if content.passed = invalid then
+ m.passed = true
+ else
+ m.passed = content.passed
+ end if
+ m.failedCount = pickInt(content.failedCount, 0)
+ m.totalCount = pickInt(content.totalCount, 0)
+
+ name = ""
+ if content.name <> invalid then name = content.name
+ m.nameLabel.text = name
+
+ if m.passed then
+ m.bar.color = m.colors.passBar
+ else
+ m.bar.color = m.colors.failBar
+ end if
+
+ applyDisplay()
+end function
+
+function onFocusChanged() as void
+ applyDisplay()
+end function
+
+function applyDisplay() as void
+ focused = m.top.itemHasFocus
+ m.nameLabel.color = nameColorFor(focused, m.passed)
+ m.countLabel.text = countTextFor(focused, m.passed, m.failedCount, m.totalCount)
+end function
+
+function nameColorFor(focused as boolean, passed as boolean) as string
+ if focused then return m.colors.focusName
+ if passed then return m.colors.passName
+ return m.colors.failName
+end function
+
+function countTextFor(focused as boolean, passed as boolean, failedCount as integer, totalCount as integer) as string
+ if not focused or passed or totalCount <= 0 or failedCount <= 0 then return ""
+ if failedCount = totalCount then
+ return "all " + totalCount.toStr().trim() + " failed"
+ end if
+ return failedCount.toStr().trim() + " failed"
+end function
+
+function pickInt(value as dynamic, fallback as integer) as integer
+ if value = invalid then return fallback
+ return value
+end function
diff --git a/framework/src/components/rooibos/RooibosResultRow.xml b/framework/src/components/rooibos/RooibosResultRow.xml
new file mode 100644
index 00000000..2896f457
--- /dev/null
+++ b/framework/src/components/rooibos/RooibosResultRow.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/framework/src/components/rooibos/RooibosScrollableResults.brs b/framework/src/components/rooibos/RooibosScrollableResults.brs
new file mode 100644
index 00000000..e3688dd6
--- /dev/null
+++ b/framework/src/components/rooibos/RooibosScrollableResults.brs
@@ -0,0 +1,36 @@
+function init() as void
+ m.rootContent = m.top.findNode("rootContent")
+ m.top.itemComponentName = "RooibosResultRow"
+ m.top.drawFocusFeedback = false
+end function
+
+' Append a result line. args is an associative array with:
+' name as string — display text
+' passed as boolean — true for OK rows, false for failed/error rows
+' failedCount as integer — failures within the suite (failed rows only)
+' totalCount as integer — total tests run within the suite
+function appendLine(args as object) as void
+ if args = invalid or args.name = invalid then return
+
+ item = m.rootContent.createChild("ContentNode")
+ item.addFields({
+ name: ""
+ passed: true
+ failedCount: 0
+ totalCount: 0
+ })
+
+ item.name = args.name
+ if args.passed <> invalid then item.passed = args.passed
+ if args.failedCount <> invalid then item.failedCount = args.failedCount
+ if args.totalCount <> invalid then item.totalCount = args.totalCount
+
+ ' Auto-scroll to the new row
+ m.top.jumpToItem = m.rootContent.getChildCount() - 1
+end function
+
+function clear() as void
+ while m.rootContent.getChildCount() > 0
+ m.rootContent.removeChildrenIndex(m.rootContent.getChildCount(), 0)
+ end while
+end function
diff --git a/framework/src/components/rooibos/RooibosScrollableResults.xml b/framework/src/components/rooibos/RooibosScrollableResults.xml
new file mode 100644
index 00000000..0414e87a
--- /dev/null
+++ b/framework/src/components/rooibos/RooibosScrollableResults.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/framework/src/images/rooibos/failure.png b/framework/src/images/rooibos/failure.png
new file mode 100644
index 00000000..9e58f350
Binary files /dev/null and b/framework/src/images/rooibos/failure.png differ
diff --git a/framework/src/images/rooibos/loading.png b/framework/src/images/rooibos/loading.png
new file mode 100644
index 00000000..50c52792
Binary files /dev/null and b/framework/src/images/rooibos/loading.png differ
diff --git a/framework/src/images/rooibos/success.png b/framework/src/images/rooibos/success.png
new file mode 100644
index 00000000..5c3bccfe
Binary files /dev/null and b/framework/src/images/rooibos/success.png differ
diff --git a/framework/src/images/rooibos/your-company-logo.png b/framework/src/images/rooibos/your-company-logo.png
new file mode 100644
index 00000000..d0b848a7
Binary files /dev/null and b/framework/src/images/rooibos/your-company-logo.png differ
diff --git a/framework/src/source/rooibos/TestRunner.bs b/framework/src/source/rooibos/TestRunner.bs
index 747445aa..3629f64e 100644
--- a/framework/src/source/rooibos/TestRunner.bs
+++ b/framework/src/source/rooibos/TestRunner.bs
@@ -5,6 +5,7 @@ import "pkg:/source/rooibos/Coverage.bs"
import "pkg:/source/rooibos/RuntimeConfig.bs"
import "pkg:/source/rooibos/Stats.bs"
import "pkg:/source/rooibos/Test.bs"
+import "pkg:/source/rooibos/TestRunnerUIReporter.bs"
namespace rooibos
' @ignore
@@ -14,6 +15,7 @@ namespace rooibos
public nodeContext = invalid
public config = invalid
public testSuites = []
+ public uiReporter = invalid
private runtimeConfig = invalid
private stats = invalid
private top = invalid
@@ -37,102 +39,283 @@ namespace rooibos
' Executes all tests for a project, as per the config
public function run()
+ m.notifyReporters("onBegin", { runner: m })
- for each reporter in m.testReporters
- if rooibos.common.isFunction(reporter.onBegin) then
- reporter.onBegin({ runner: m })
- end if
- end for
+ timer = createObject("roTimespan")
+ timer.mark()
- rooibosTimer = createObject("roTimespan")
- rooibosTimer.mark()
suiteNames = m.runtimeConfig.getAllTestSuitesNames()
- isFailed = false
- failedText = ""
- i = 0
- numSuites = suiteNames.count()
- testSuite = invalid
+ m.attachUIReporter(m.precomputeTotalTests(suiteNames))
+
+ ctx = {
+ isFailed: false
+ passedSuites: 0
+ failedSuites: 0
+ numSuites: suiteNames.count()
+ index: 0
+ lastSuite: invalid
+ ui: invalid ' resolved on first suite
+ }
+
for each name in suiteNames
- i++
- suiteClass = m.runtimeConfig.getTestSuiteClassWithName(name)
- testSuite = invalid
- if suiteClass <> invalid then
- testSuite = suiteClass()
- testSuite.testRunner = m
- testSuite.testReporters = m.testReporters
- testSuite.global = m.nodeContext.global
- testSuite.context = m.nodeContext
- testSuite.top = m.nodeContext.top
- testSuite.scene = m.nodeContext.global.testsScene
- testSuite.catchCrashes = m.config.catchCrashes
- testSuite.throwOnFailedAssertion = m.config.throwOnFailedAssertion
- testSuite.scene.testText = `Running Suite ${i} of ${numSuites}: ${name}`
- m.runTestSuite(testSuite)
- if m.stats.hasFailures = true then
- if not isFailed then
- isFailed = true
- testSuite.scene.statusColor = "#DA3633"
- end if
- if m.config.failFast = true then
- exit for
- end if
- end if
+ ctx.index++
+ m.executeSuite(name, ctx)
+ if ctx.isFailed and m.config.failFast = true then
+ exit for
+ end if
+ end for
- if testSuite.stats.hasFailures then
- failedText = name + chr(10) + failedText
- testSuite.scene.failedText = "Failed Suites: " + chr(10) + failedText
+ m.finalizeRun(ctx, timer)
+ m.dispatchOnEnd()
+ m.publishRooibosResult()
+ m.maybeRunCoverage()
+ m.printFinalStatus(ctx.isFailed)
+
+ if m.config.sendHomeOnFinish <> false then
+ m.sendHomeKeyPress()
+ end if
+ end function
+
+ '----------------------------------------------------------------
+ ' run() helpers
+ '----------------------------------------------------------------
+
+ private function precomputeTotalTests(suiteNames as object) as integer
+ total = 0
+ for each name in suiteNames
+ cls = m.runtimeConfig.getTestSuiteClassWithName(name)
+ if cls <> invalid then
+ suite = cls()
+ if suite.groupsData <> invalid then
+ for each gd in suite.groupsData
+ if gd.testCases <> invalid then total += gd.testCases.count()
+ end for
end if
- else
- rooibos.common.logError(`Could not create test for suite : ${name}`)
- failedText = "COULD NOT CREATE suite " + name + chr(10) + failedText
- testSuite.scene.failedText = "Failed Suites: " + chr(10) + failedText
end if
end for
+ return total
+ end function
+
+ private function attachUIReporter(totalTests as integer) as void
+ m.uiReporter = new rooibos.TestRunnerUIReporter(m, totalTests)
+ m.testReporters.push(m.uiReporter)
+ end function
+
+ private function executeSuite(name as string, ctx as object) as void
+ cls = m.runtimeConfig.getTestSuiteClassWithName(name)
+ if cls = invalid then
+ rooibos.common.logError(`Could not create test for suite : ${name}`)
+ if ctx.ui <> invalid and ctx.ui.resultsNode <> invalid then
+ ctx.ui.resultsNode.callFunc("appendLine", { name: name + " (could not create)", passed: false })
+ end if
+ return
+ end if
+
+ testSuite = m.buildSuite(cls)
+ ctx.lastSuite = testSuite
+
+ if ctx.ui = invalid then
+ ctx.ui = m.resolveUINodes(testSuite.scene)
+ m.uiReporter.bindNodes(testSuite.scene)
+ end if
+
+ m.uiReporter.setCurrentSuite(name)
+
+ startedBefore = m.uiReporter.testsStarted
+ m.runTestSuite(testSuite)
+
+ ' Node/async test suites fire per-test callbacks on the child node, not on
+ ' our reporter list, so reconcile against the suite's final stats.
+ if m.uiReporter.testsStarted = startedBefore and testSuite.stats.ranCount > 0 then
+ m.uiReporter.recordSuiteStats(testSuite.stats)
+ end if
+
+ m.appendSuiteResultLine(ctx, name, testSuite)
+ m.updateInProgressSummary(ctx)
+
+ if m.stats.hasFailures = true then
+ ctx.isFailed = true
+ end if
+ end function
+
+ private function buildSuite(suiteClass as function) as rooibos.BaseTestSuite
+ testSuite = suiteClass()
+ testSuite.testRunner = m
+ testSuite.testReporters = m.testReporters
+ testSuite.global = m.nodeContext.global
+ testSuite.context = m.nodeContext
+ testSuite.top = m.nodeContext.top
+ testSuite.scene = m.nodeContext.global.testsScene
+ testSuite.catchCrashes = m.config.catchCrashes
+ testSuite.throwOnFailedAssertion = m.config.throwOnFailedAssertion
+ return testSuite
+ end function
+
+ private function resolveUINodes(scene as object) as object
+ return {
+ statusNode: scene.findNode("statusLabel")
+ resultsNode: scene.findNode("resultsLabel")
+ summaryNode: scene.findNode("summaryLabel")
+ progressNode: scene.findNode("progressFill")
+ }
+ end function
+
+ private function appendSuiteResultLine(ctx as object, name as string, testSuite as rooibos.BaseTestSuite) as void
+ if ctx.ui = invalid or ctx.ui.resultsNode = invalid then return
+
+ if testSuite.stats.hasFailures then
+ ctx.failedSuites++
+ ctx.ui.resultsNode.callFunc("appendLine", {
+ name: name
+ passed: false
+ failedCount: testSuite.stats.failedCount + testSuite.stats.crashedCount
+ totalCount: testSuite.stats.ranCount
+ })
+ else
+ ctx.passedSuites++
+ ctx.ui.resultsNode.callFunc("appendLine", {
+ name: name
+ passed: true
+ failedCount: 0
+ totalCount: testSuite.stats.ranCount
+ })
+ end if
+ end function
+
+ private function updateInProgressSummary(ctx as object) as void
+ if ctx.ui = invalid or ctx.ui.summaryNode = invalid then return
+ ctx.ui.summaryNode.text = "Passed: " + ctx.passedSuites.toStr().trim() + " | Failed: " + ctx.failedSuites.toStr().trim() + " | " + ctx.index.toStr().trim() + "/" + ctx.numSuites.toStr().trim() + " suites"
+ end function
+
+ private function finalizeRun(ctx as object, timer as object) as void
+ if ctx.lastSuite = invalid then
+ m.handleNoSuitesFound()
+ return
+ end if
+
+ if ctx.ui = invalid then return
+
+ elapsed = (timer.totalMilliseconds() / 1000).toStr()
+ if ctx.isFailed then
+ m.renderFailureFinalState(ctx, elapsed)
+ else
+ m.renderSuccessFinalState(ctx, elapsed)
+ end if
+ m.giveFocusToResults(ctx)
+ m.stats.time = timer.totalMilliseconds()
+ end function
+
+ private function renderSuccessFinalState(ctx as object, elapsed as string) as void
+ if ctx.ui.statusNode <> invalid then ctx.ui.statusNode.text = "All suites passed!"
+ m.swapResultImage(ctx.lastSuite.scene, "pkg:/images/rooibos/success.png")
+ if ctx.ui.progressNode <> invalid then ctx.ui.progressNode.width = 500.0
+ if ctx.ui.summaryNode <> invalid then
+ ctx.ui.summaryNode.text = "Passed: " + ctx.passedSuites.toStr().trim() + " | Failed: 0 | " + ctx.numSuites.toStr().trim() + " suites in " + elapsed + "s"
+ end if
+ end function
+
+ private function renderFailureFinalState(ctx as object, elapsed as string) as void
+ if ctx.ui.statusNode <> invalid then
+ ctx.ui.statusNode.text = (m.stats.failedCount + m.stats.crashedCount).toStr().trim() + " tests failed across " + ctx.failedSuites.toStr().trim() + " of " + ctx.numSuites.toStr().trim() + " suites"
+ end if
+ m.swapResultImage(ctx.lastSuite.scene, "pkg:/images/rooibos/failure.png")
+ if ctx.ui.progressNode <> invalid then ctx.ui.progressNode.width = 500.0
+ if ctx.ui.summaryNode <> invalid then
+ ctx.ui.summaryNode.text = "Passed: " + ctx.passedSuites.toStr().trim() + " | Failed: " + ctx.failedSuites.toStr().trim() + " | " + ctx.numSuites.toStr().trim() + " suites in " + elapsed + "s"
+ end if
+ end function
- if not isFailed and testSuite <> invalid then
- testSuite.scene.statusColor = "#238636"
+ private function swapResultImage(scene as object, uri as string) as void
+ if scene = invalid then return
+ spinner = scene.findNode("resultSpinner")
+ if spinner <> invalid then
+ spinner.control = "stop"
+ spinner.scale = [0, 0]
+ end if
+ poster = scene.findNode("resultPoster")
+ if poster <> invalid then
+ poster.uri = uri
+ poster.scale = [1, 1]
end if
+ end function
- if testSuite = invalid then
- m.nodeContext.global.testsScene.failedText = "No tests were found"
+ private function giveFocusToResults(ctx as object) as void
+ if ctx.ui <> invalid and ctx.ui.resultsNode <> invalid then
+ ctx.ui.resultsNode.setFocus(true)
end if
+ end function
- m.stats.time = rooibosTimer.totalMilliseconds()
+ private function handleNoSuitesFound() as void
+ results = m.nodeContext.global.testsScene.findNode("resultsLabel")
+ if results <> invalid then
+ results.callFunc("appendLine", { name: "No tests were found", passed: false })
+ end if
+ end function
+ private function notifyReporters(method as string, event as object) as void
for each reporter in m.testReporters
- if rooibos.common.isFunction(reporter.onEnd) then
- reporter.onEnd({ stats: m.stats })
+ if rooibos.common.isFunction(reporter[method]) then
+ reporter[method](event)
end if
end for
+ end function
+
+ private function dispatchOnEnd() as void
+ m.notifyReporters("onEnd", { stats: m.stats })
+ end function
- rooibosResult = {
+ private function publishRooibosResult() as void
+ m.nodeContext.global.testsScene.rooibosTestResult = {
stats: m.stats
testSuites: m.testSuites
}
- m.nodeContext.global.testsScene.rooibosTestResult = rooibosResult
+ end function
+ private function maybeRunCoverage() as void
if m.config.isRecordingCodeCoverage then
rooibos.Coverage.reportCodeCoverage()
-
if m.config.printLcov = true then
rooibos.Coverage.printLCovInfo()
end if
else
rooibos.common.logDebug("rooibos.Coverage.reportCodeCoverage is not a function")
end if
+ end function
- ' Final results to be logged after all test reporters have finished
+ private function printFinalStatus(isFailed as boolean) as void
resultStatus = "PASS"
if isFailed then resultStatus = "FAIL"
print "[Rooibos Result]: " + resultStatus
print "[Rooibos Shutdown]"
-
- if m.config.sendHomeOnFinish <> false then
- m.sendHomeKeyPress()
- end if
end function
public function runInNodeMode(nodeTestName as string) as void
+ ' Push a reporter that updates the test node's rooibosTestsRunSoFar field
+ ' so the main thread can observe per-test progress for real-time UI updates
+ nodeProgressReporter = {
+ topNode: m.top
+ count: 0
+ passedCount: 0
+ failedCount: 0
+ onTestBegin: function(_args as object)
+ m.count++
+ if m.topNode <> invalid then m.topNode.rooibosTestsRunSoFar = m.count
+ end function
+ onTestComplete: function(args as object)
+ if args <> invalid and args.test <> invalid and args.test.result <> invalid then
+ result = args.test.result
+ if result.isCrash or result.isFail then
+ m.failedCount++
+ if m.topNode <> invalid then m.topNode.rooibosTestsFailed = m.failedCount
+ else if not result.isSkipped then
+ m.passedCount++
+ if m.topNode <> invalid then m.topNode.rooibosTestsPassed = m.passedCount
+ end if
+ end if
+ end function
+ }
+ m.testReporters.push(nodeProgressReporter)
+
suiteClass = m.runtimeConfig.getTestSuiteClassWithName(nodeTestName)
testSuite = invalid
@@ -224,8 +407,20 @@ namespace rooibos
rooibos.common.logDebug(`+++++RUNNING NODE TEST${chr(10)}node type is ${testSuite.generatedNodeName}`)
nodeCreator = CreateObject("roSGNode", "RooibosNodeCreator")
- node = nodeCreator@.CreateNode(testSuite.generatedNodeName, m.testScene)
+ if m.testNodeContainer = invalid
+ m.testNodeContainer = m.testScene.createChild("Group")
+ m.testNodeContainer.visible = false
+ end if
+ node = m.testNodeContainer.createChild(testSuite.generatedNodeName)
port = CreateObject("roMessagePort")
+ ' Per-test progress fields — child node writes to these from render thread,
+ ' main thread observes for real-time UI updates during node-test suite execution
+ node.addField("rooibosTestsRunSoFar", "integer", false)
+ node.addField("rooibosTestsPassed", "integer", false)
+ node.addField("rooibosTestsFailed", "integer", false)
+ node.observeField("rooibosTestsRunSoFar", port)
+ node.observeField("rooibosTestsPassed", port)
+ node.observeField("rooibosTestsFailed", port)
node.observeField("rooibosTestResult", port)
' Trigger test via observer so that thread ownership
' can be transferred to the render thread
@@ -237,7 +432,12 @@ namespace rooibos
while true
event = wait(0, port)
if type(event) = "roSGNodeEvent" then
- exit while
+ field = event.getField()
+ if field = "rooibosTestResult" then
+ exit while
+ else if m.uiReporter <> invalid then
+ m.uiReporter.applyNodeProgressEvent(field)
+ end if
end if
end while
nodeResults = event.getData()
@@ -254,7 +454,7 @@ namespace rooibos
else
rooibos.common.logError(`The node test ${testSuite.name} did not indicate test completion. Did you call m.done() in your test? Did you correctly configure your node test? Please refer to : https://github.com/rokucommunity/rooibos/blob/master/docs/index.md#testing-scenegraph-nodes`)
end if
- m.testScene.removeChild(node)
+ m.testNodeContainer.removeChild(node)
return
else
diff --git a/framework/src/source/rooibos/TestRunnerUIReporter.bs b/framework/src/source/rooibos/TestRunnerUIReporter.bs
new file mode 100644
index 00000000..3ccebe27
--- /dev/null
+++ b/framework/src/source/rooibos/TestRunnerUIReporter.bs
@@ -0,0 +1,102 @@
+import "pkg:/source/rooibos/BaseTestReporter.bs"
+
+namespace rooibos
+ ' @ignore
+ ' Reporter that mirrors live test progress into the on-device test runner UI.
+ ' Owns references to status/count/progress nodes and the running totals.
+ class TestRunnerUIReporter extends rooibos.BaseTestReporter
+
+ public totalTests = 0
+ public testsStarted = 0
+ public testsCompleted = 0
+ public testsPassed = 0
+ public testsFailed = 0
+ public currentSuiteName = ""
+
+ public statusNode = invalid
+ public statusCountsNode = invalid
+ public progressNode = invalid
+
+ private progressBarWidth = 500.0
+
+ function new(runner as dynamic, totalTests as integer)
+ super(runner)
+ m.totalTests = totalTests
+ end function
+
+ ' Resolve the scene-graph nodes this reporter writes to.
+ ' Called once UI nodes are attached to the scene.
+ public function bindNodes(scene as object) as void
+ if scene = invalid then return
+ m.statusNode = scene.findNode("statusLabel")
+ m.statusCountsNode = scene.findNode("statusCounts")
+ m.progressNode = scene.findNode("progressFill")
+ end function
+
+ public function setCurrentSuite(name as string) as void
+ m.currentSuiteName = name
+ m.updateStatus()
+ end function
+
+ ' Compensation path for node/async test suites whose per-test callbacks fire
+ ' on the child node's reporter list rather than ours.
+ public function recordSuiteStats(stats as object) as void
+ if stats = invalid then return
+ m.testsStarted += stats.ranCount
+ m.testsCompleted += stats.ranCount
+ m.testsPassed += stats.passedCount
+ m.testsFailed += stats.failedCount + stats.crashedCount
+ m.updateStatus()
+ m.updateProgress()
+ end function
+
+ ' Cross-thread bridge: a node-test child writes to a field on the render
+ ' thread; the main thread's wait loop forwards the field name here so the
+ ' UI updates in real time during node-test suite execution.
+ public function applyNodeProgressEvent(field as string) as void
+ if field = "rooibosTestsRunSoFar" then
+ m.testsStarted++
+ m.testsCompleted++
+ m.updateProgress()
+ else if field = "rooibosTestsPassed" then
+ m.testsPassed++
+ else if field = "rooibosTestsFailed" then
+ m.testsFailed++
+ end if
+ m.updateStatus()
+ end function
+
+ override function onTestBegin(_event as rooibos.TestReporterOnTestBeginEvent)
+ m.testsStarted++
+ m.updateStatus()
+ end function
+
+ override function onTestComplete(event as rooibos.TestReporterOnTestCompleteEvent)
+ m.testsCompleted++
+ if event <> invalid and event.test <> invalid and event.test.result <> invalid then
+ result = event.test.result
+ if result.isCrash or result.isFail then
+ m.testsFailed++
+ else if not result.isSkipped then
+ m.testsPassed++
+ end if
+ end if
+ m.updateStatus()
+ m.updateProgress()
+ end function
+
+ private function updateStatus() as void
+ if m.statusNode <> invalid then m.statusNode.text = m.currentSuiteName
+ if m.statusCountsNode <> invalid then
+ m.statusCountsNode.text = m.testsStarted.toStr().trim() + "/" + m.totalTests.toStr().trim() + " · ✓ " + m.testsPassed.toStr().trim() + " · ✗ " + m.testsFailed.toStr().trim()
+ end if
+ end function
+
+ private function updateProgress() as void
+ if m.progressNode <> invalid and m.totalTests > 0 then
+ m.progressNode.width = m.progressBarWidth * m.testsCompleted / m.totalTests
+ end if
+ end function
+
+ end class
+end namespace
diff --git a/src/lib/rooibos/FileFactory.ts b/src/lib/rooibos/FileFactory.ts
index cb585146..d6a559e0 100644
--- a/src/lib/rooibos/FileFactory.ts
+++ b/src/lib/rooibos/FileFactory.ts
@@ -76,22 +76,72 @@ export class FileFactory {
scriptImports.push(``);
}
+ // Node-test components run offscreen and don't need the visual UI.
+ // Emitting it here would also create duplicate scene-graph IDs
+ // (e.g. multiple `resultImage` BusySpinners), which corrupts the
+ // SceneGraph state and crashes the runtime at end of run.
+ if (suite) {
+ return `
+
+ ${scriptImports.join('\n')}
+
+
+
+
+
+ `;
+ }
+
let contents = `
${scriptImports.join('\n')}
-
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -150,4 +200,20 @@ export class FileFactory {
console.error(`Error adding framework file: ${path} : ${error.message}`);
}
}
+
+ public copyImageAssets(program: Program) {
+ let imageFiles = fastGlob.sync(['images/**/*.{png,jpg,jpeg,gif}'], {
+ cwd: this.frameworkSourcePath,
+ absolute: false,
+ onlyFiles: true
+ });
+ for (let imgPath of imageFiles) {
+ let srcPath = path.resolve(this.frameworkSourcePath, imgPath);
+ let destPath = path.join(
+ program.options.stagingFolderPath ?? program.options.stagingDir ?? program.options.sourceRoot,
+ imgPath
+ );
+ fse.copySync(srcPath, destPath);
+ }
+ }
}
diff --git a/src/plugin.ts b/src/plugin.ts
index fdabb837..09ac44b7 100644
--- a/src/plugin.ts
+++ b/src/plugin.ts
@@ -161,6 +161,7 @@ export class RooibosPlugin implements CompilerPlugin {
afterProgramTranspile(program: Program, entries: TranspileObj[], editor: AstEditor) {
this.session.addLaunchHookFileIfNotPresent();
this.codeCoverageProcessor.generateMetadata(this.config.isRecordingCodeCoverage, program);
+ this.fileFactory.copyImageAssets(program);
}
beforeFileTranspile(event: BeforeFileTranspileEvent) {