├── .gitignore ├── .github └── FUNDING.yml ├── .gitmodules ├── default.project.json ├── place.project.json ├── LICENSE ├── README.md ├── src ├── UI │ ├── Button │ │ ├── RunTestsButton.lua │ │ ├── RunFailedTestsButton.lua │ │ └── RunSelectedTestsButton.lua │ ├── Window │ │ ├── OutputWindow.lua │ │ ├── TestListWindow.lua │ │ ├── OutputView.lua │ │ └── TestListView.lua │ ├── List │ │ ├── OutputTextEntry.lua │ │ └── TestListFrame.lua │ ├── Bar │ │ ├── ButtonSideBar.lua │ │ └── TestProgressBar.lua │ └── TestStateIcon.lua └── init.server.lua └── test └── UI ├── Window ├── TestListViewTests.spec.lua └── OutputViewTests.spec.lua ├── Bar └── TestProgressBarTests.spec.lua └── TestStateIconTests.spec.lua /.gitignore: -------------------------------------------------------------------------------- 1 | sourcemap.json -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: thenexusavenger -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "module/Nexus-Unit-Testing"] 2 | path = module/Nexus-Unit-Testing 3 | url = https://github.com/TheNexusAvenger/Nexus-Unit-Testing.git 4 | [submodule "module/Nexus-Plugin-Components"] 5 | path = module/Nexus-Plugin-Components 6 | url = https://github.com/TheNexusAvenger/Nexus-Plugin-Components.git 7 | -------------------------------------------------------------------------------- /default.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NexusUnitTestingPlugin", 3 | "tree": { 4 | "$path": "src", 5 | "NexusUnitTestingModule": { 6 | "$path": "module/Nexus-Unit-Testing/" 7 | }, 8 | "NexusPluginComponents": { 9 | "$path": "module/Nexus-Plugin-Components/" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /place.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Nexus-Unit-Testing-Plugin", 3 | "tree": { 4 | "$className": "DataModel", 5 | "ReplicatedStorage": { 6 | "$className": "ReplicatedStorage", 7 | "NexusUnitTestingPlugin": { 8 | "$path": "." 9 | }, 10 | "NexusUnitTestingPluginTests": { 11 | "$path": "test" 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 TheNexusAvenger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Nexus Unit Testing Plugin has been replaced with [Avant Plugin](https://github.com/Avant-Rbx/Avant-Plugin).** 2 | 3 | # Nexus Unit Testing Plugin 4 | Nexus Unit Testing Plugin provides a user interface 5 | for running tests with either [Nexus Unit Testing](https://github.com/thenexusavenger/nexus-unit-testing) 6 | or [TestEZ](https://github.com/Roblox/testez). Included 7 | is a viewer for individual tests and an output viewer that 8 | is isolated for each unit test. 9 | 10 | ## Installing 11 | ### itch.io 12 | The plugin is distrbuted as a file on itch.io as a method 13 | to be able to fund the project. Consider supporting. 14 |
https://thenexusavenger.itch.io/nexus-unit-testing-plugin 15 | 16 | ### Roblox Plugin Marketplace 17 | The plugin can be found on the Plugin Marketplace. 18 |
https://www.roblox.com/library/11039022908/Nexus-Unit-Testing-Plugin 19 | 20 | ### Rojo 21 | This repository can be synced into Roblox Studio using 22 | [Rojo](https://github.com/rojo-rbx/rojo). 23 | Look at the respective repository for how to use them. 24 | 25 | ## License 26 | Nexus Unit Testing Plugin is available under the terms of the MIT 27 | License. See [LICENSE](LICENSE) for details. 28 | -------------------------------------------------------------------------------- /src/UI/Button/RunTestsButton.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | TheNexusAvenger 3 | 4 | Button for running tests. 5 | --]] 6 | --!strict 7 | 8 | local BUTTON_ICONS = "https://www.roblox.com/asset/?id=4734758315" 9 | 10 | local NexusUnitTestingPlugin = script.Parent.Parent.Parent 11 | local PluginInstance = require(NexusUnitTestingPlugin:WaitForChild("NexusPluginComponents"):WaitForChild("Base"):WaitForChild("PluginInstance")) 12 | 13 | local RunTestsButton = PluginInstance:Extend() 14 | RunTestsButton:SetClassName("RunTestsButton") 15 | 16 | export type RunTestsButton = { 17 | new: () -> (RunTestsButton), 18 | Extend: (self: RunTestsButton) -> (RunTestsButton), 19 | } & PluginInstance.PluginInstance & ImageButton 20 | 21 | 22 | --[[ 23 | Creates a Run Tests Button object. 24 | --]] 25 | function RunTestsButton:__new(): () 26 | PluginInstance.__new(self, "ImageButton") 27 | 28 | --Set the defaults. 29 | self.Image = BUTTON_ICONS 30 | self.ImageRectSize = Vector2.new(512, 512) 31 | self.BackgroundColor3 = Enum.StudioStyleGuideColor.MainBackground 32 | self.ImageColor3 = Color3.fromRGB(0, 170, 0) 33 | self.BorderSizePixel = 0 34 | end 35 | 36 | 37 | 38 | return (RunTestsButton :: any) :: RunTestsButton -------------------------------------------------------------------------------- /src/UI/Window/OutputWindow.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | TheNexusAvenger 3 | 4 | Window for the displaying the output of a test. 5 | --]] 6 | --!strict 7 | 8 | local NexusUnitTestingPlugin = script.Parent.Parent.Parent 9 | local OutputView = require(NexusUnitTestingPlugin:WaitForChild("UI"):WaitForChild("Window"):WaitForChild("OutputView")) 10 | local PluginInstance = require(NexusUnitTestingPlugin:WaitForChild("NexusPluginComponents"):WaitForChild("Base"):WaitForChild("PluginInstance")) 11 | local UnitTest = require(NexusUnitTestingPlugin:WaitForChild("NexusUnitTestingModule"):WaitForChild("UnitTest"):WaitForChild("UnitTest")) 12 | 13 | local OutputWindow = PluginInstance:Extend() 14 | OutputWindow:SetClassName("OutputWindow") 15 | 16 | export type OutputWindow = { 17 | new: (Plugin: Plugin) -> (OutputWindow), 18 | Extend: (self: OutputWindow) -> (OutputWindow), 19 | 20 | SetTest: (self: OutputWindow, Test: UnitTest.UnitTest) -> (), 21 | } & PluginInstance.PluginInstance & DockWidgetPluginGui 22 | 23 | 24 | 25 | --[[ 26 | Creates a Output Window object. 27 | --]] 28 | function OutputWindow:__new(Plugin: Plugin) 29 | PluginInstance.__new(self, Plugin:CreateDockWidgetPluginGui("Test Output", DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Bottom, false, false, 300, 200, 200, 100))) 30 | self.Title = "Test Output" 31 | self.Name = "Test Output" 32 | 33 | --Add the output view. 34 | local Output = OutputView.new() 35 | Output.Size = UDim2.new(1, 0, 1, 0) 36 | Output.Parent = self 37 | self:DisableChangeReplication("OutputView") 38 | self.OutputView = Output 39 | end 40 | 41 | --[[ 42 | Sets the test to display. 43 | --]] 44 | function OutputWindow:SetTest(Test: UnitTest.UnitTest): () 45 | self.Enabled = true 46 | self.OutputView:SetTest(Test) 47 | end 48 | 49 | 50 | 51 | return OutputWindow -------------------------------------------------------------------------------- /src/UI/Button/RunFailedTestsButton.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | TheNexusAvenger 3 | 4 | Button for re-running failed tests. 5 | --]] 6 | --!strict 7 | 8 | local BUTTON_ICONS = "https://www.roblox.com/asset/?id=4734758315" 9 | 10 | local NexusUnitTestingPlugin = script.Parent.Parent.Parent 11 | local NexusPluginComponents = require(NexusUnitTestingPlugin:WaitForChild("NexusPluginComponents")) 12 | local PluginInstance = require(NexusUnitTestingPlugin:WaitForChild("NexusPluginComponents"):WaitForChild("Base"):WaitForChild("PluginInstance")) 13 | 14 | local RunFailedTestsButton = PluginInstance:Extend() 15 | RunFailedTestsButton:SetClassName("RunFailedTestsButton") 16 | 17 | export type RunFailedTestsButton = { 18 | new: () -> (RunFailedTestsButton), 19 | Extend: (self: RunFailedTestsButton) -> (RunFailedTestsButton), 20 | } & PluginInstance.PluginInstance & ImageButton 21 | 22 | 23 | 24 | --[[ 25 | Creates a Run Failed Tests Button object. 26 | --]] 27 | function RunFailedTestsButton:__new(): () 28 | PluginInstance.__new(self, "ImageButton") 29 | 30 | --Create the failed tests icon. 31 | local Icon = NexusPluginComponents.new("ImageLabel") 32 | Icon.Size = UDim2.new(0.5, 0, 0.5, 0) 33 | Icon.Position = UDim2.new(0.5, 0, 0.5, 0) 34 | Icon.BackgroundTransparency = 1 35 | Icon.Image = BUTTON_ICONS 36 | Icon.ImageRectSize = Vector2.new(512, 512) 37 | Icon.ImageRectOffset = Vector2.new(512, 0) 38 | Icon.ImageColor3 = Color3.fromRGB(200, 0, 0) 39 | Icon.Parent = self 40 | 41 | --Set the defaults. 42 | self.Image = BUTTON_ICONS 43 | self.ImageRectSize = Vector2.new(512, 512) 44 | self.BackgroundColor3 = Enum.StudioStyleGuideColor.MainBackground 45 | self.ImageColor3 = Color3.fromRGB(0, 170, 0) 46 | self.BorderSizePixel = 0 47 | end 48 | 49 | 50 | 51 | return (RunFailedTestsButton :: any) :: RunFailedTestsButton -------------------------------------------------------------------------------- /src/UI/Button/RunSelectedTestsButton.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | TheNexusAvenger 3 | 4 | Button for re-running selected tests. 5 | --]] 6 | --!strict 7 | 8 | local BUTTON_ICONS = "https://www.roblox.com/asset/?id=4734758315" 9 | 10 | local NexusUnitTestingPlugin = script.Parent.Parent.Parent 11 | local NexusPluginComponents = require(NexusUnitTestingPlugin:WaitForChild("NexusPluginComponents")) 12 | local PluginInstance = require(NexusUnitTestingPlugin:WaitForChild("NexusPluginComponents"):WaitForChild("Base"):WaitForChild("PluginInstance")) 13 | 14 | local RunSelectedTestsButton = PluginInstance:Extend() 15 | RunSelectedTestsButton:SetClassName("RunSelectedTestsButton") 16 | 17 | export type RunSelectedTestsButton = { 18 | new: () -> (RunSelectedTestsButton), 19 | Extend: (self: RunSelectedTestsButton) -> (RunSelectedTestsButton), 20 | } & PluginInstance.PluginInstance & ImageButton 21 | 22 | 23 | 24 | --[[ 25 | Creates a Run Selected Tests Button object. 26 | --]] 27 | function RunSelectedTestsButton:__new(): () 28 | PluginInstance.__new(self, "ImageButton") 29 | 30 | --Create the selected tests icon. 31 | local Icon = NexusPluginComponents.new("ImageLabel") 32 | Icon.Size = UDim2.new(0.5, 0, 0.5, 0) 33 | Icon.Position = UDim2.new(0.5, 0, 0.5, 0) 34 | Icon.BackgroundTransparency = 1 35 | Icon.Image = BUTTON_ICONS 36 | Icon.ImageRectSize = Vector2.new(512, 512) 37 | Icon.ImageRectOffset = Vector2.new(0, 512) 38 | Icon.ImageColor3 = Color3.fromRGB(0, 170, 255) 39 | Icon.Parent = self 40 | 41 | --Set the defaults. 42 | self.Image = BUTTON_ICONS 43 | self.ImageRectSize = Vector2.new(512, 512) 44 | self.BackgroundColor3 = Enum.StudioStyleGuideColor.MainBackground 45 | self.ImageColor3 = Color3.fromRGB(0, 170, 0) 46 | self.BorderSizePixel = 0 47 | end 48 | 49 | 50 | 51 | return (RunSelectedTestsButton :: any) :: RunSelectedTestsButton -------------------------------------------------------------------------------- /src/UI/Window/TestListWindow.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | TheNexusAvenger 3 | 4 | Window for the list of tests and actions. 5 | --]] 6 | --!strict 7 | 8 | local NexusUnitTestingPlugin = script.Parent.Parent.Parent 9 | local OutputWindow = require(NexusUnitTestingPlugin:WaitForChild("UI"):WaitForChild("Window"):WaitForChild("OutputWindow")) 10 | local TestListView = require(NexusUnitTestingPlugin:WaitForChild("UI"):WaitForChild("Window"):WaitForChild("TestListView")) 11 | local PluginInstance = require(NexusUnitTestingPlugin:WaitForChild("NexusPluginComponents"):WaitForChild("Base"):WaitForChild("PluginInstance")) 12 | 13 | local TestListWindow = PluginInstance:Extend() 14 | TestListWindow:SetClassName("TestListWindow") 15 | 16 | export type TestListWindow = { 17 | new: (Plugin: Plugin, OutputWindow: OutputWindow.OutputWindow) -> (TestListWindow), 18 | Extend: (self: TestListWindow) -> (TestListWindow), 19 | } & PluginInstance.PluginInstance & Frame 20 | 21 | 22 | 23 | --[[ 24 | Creates a Test List Window object. 25 | --]] 26 | function TestListWindow:__new(Plugin: Plugin, OutputWindow: OutputWindow.OutputWindow): () 27 | PluginInstance.__new(self, Plugin:CreateDockWidgetPluginGui("Unit Tests", DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Bottom, false, false, 300, 200, 200, 100))) 28 | self.Title = "Unit Tests" 29 | self.Name = "Unit Tests" 30 | 31 | --Add the test list frame. 32 | local ListView = TestListView.new() 33 | ListView.Size = UDim2.new(1, 0, 1, 0) 34 | ListView.Parent = self:GetWrappedInstance() 35 | self:DisableChangeReplication("TestListView") 36 | self.TestListView = ListView 37 | 38 | --Connect setting the output. 39 | ListView.TestOutputOpened:Connect(function(Test, DontForceEnabled) 40 | if DontForceEnabled ~= true or OutputWindow.Enabled then 41 | OutputWindow:SetTest(Test) 42 | end 43 | end) 44 | end 45 | 46 | 47 | 48 | return (TestListWindow :: any) :: TestListWindow -------------------------------------------------------------------------------- /src/init.server.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | TheNexusAvenger 3 | 4 | Runs the Nexus Unit Testing Plugin. 5 | --]] 6 | --!strict 7 | 8 | local PluginToggleButton = require(script:WaitForChild("NexusPluginComponents"):WaitForChild("Input"):WaitForChild("Custom"):WaitForChild("PluginToggleButton")) 9 | local TestListWindow = require(script:WaitForChild("UI"):WaitForChild("Window"):WaitForChild("TestListWindow")) 10 | local OutputWindow = require(script:WaitForChild("UI"):WaitForChild("Window"):WaitForChild("OutputWindow")) 11 | 12 | 13 | 14 | --Create the window. 15 | local OutputView = OutputWindow.new(plugin) 16 | local TestsLists = TestListWindow.new(plugin, OutputView) 17 | 18 | --Create the button. 19 | local NexusWidgetsToolbar = plugin:CreateToolbar("Nexus Widgets") 20 | local NexusUnitTestsPlugin = NexusWidgetsToolbar:CreateButton("Unit Tests", "Opens the Nexus Unit Testing window", "https://www.roblox.com/asset/?id=4734891702") 21 | local NexusUnitTestingButton = PluginToggleButton.new(NexusUnitTestsPlugin, TestsLists) 22 | NexusUnitTestingButton.ClickableWhenViewportHidden = true 23 | 24 | --Create the actions. 25 | plugin:CreatePluginAction("NexusUnitTesting_RunAllTests", "Run Unit Tests", "Runs all the unit tests in the game.\nPart of Nexus Unit Testing.", "https://www.roblox.com/asset/?id=4734926678").Triggered:Connect(function() 26 | TestsLists.Enabled = true 27 | TestsLists.TestListView:RunAllTests() 28 | end) 29 | plugin:CreatePluginAction("NexusUnitTesting_RunFailedTests", "Run Failed Unit Tests", "Runs the failed unit tests from the last run.\nPart of Nexus Unit Testing.", "https://www.roblox.com/asset/?id=4734926820").Triggered:Connect(function() 30 | TestsLists.Enabled = true 31 | TestsLists.TestListView:RunFailedTests() 32 | end) 33 | plugin:CreatePluginAction("NexusUnitTesting_RunSelectedTests", "Run Selected Unit Tests", "Runs the selected unit tests from the last run.\nPart of Nexus Unit Testing.", "https://www.roblox.com/asset/?id=4734926979").Triggered:Connect(function() 34 | TestsLists.Enabled = true 35 | TestsLists.TestListView:RunSelectedTests() 36 | end) -------------------------------------------------------------------------------- /test/UI/Window/TestListViewTests.spec.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | TheNexusAvenger 3 | 4 | Tests the TestListView class. 5 | --]] 6 | --!strict 7 | 8 | local NexusUnitTestingPlugin = game:GetService("ReplicatedStorage"):WaitForChild("NexusUnitTestingPlugin") 9 | local TestListView = require(NexusUnitTestingPlugin:WaitForChild("UI"):WaitForChild("Window"):WaitForChild("TestListView")) 10 | local ModuleUnitTest = require(NexusUnitTestingPlugin:WaitForChild("NexusUnitTestingModule"):WaitForChild("Runtime"):WaitForChild("ModuleUnitTest")) 11 | 12 | return function() 13 | local TestTestListView = nil 14 | beforeEach(function() 15 | TestTestListView = TestListView.new() 16 | end) 17 | afterEach(function() 18 | TestTestListView:Destroy() 19 | end) 20 | 21 | describe("A test list view", function() 22 | it("should accept new tests.", function() 23 | --Create 3 tests. 24 | local ModuleScript1, ModuleScript2 = Instance.new("ModuleScript"), Instance.new("ModuleScript") 25 | ModuleScript1.Name = "ModuleScript1" 26 | ModuleScript2.Name = "ModuleScript2" 27 | local ModuleUnitTest1, ModuleUnitTest2, ModuleUnitTest3 = ModuleUnitTest.new(ModuleScript1), ModuleUnitTest.new(ModuleScript2), ModuleUnitTest.new(ModuleScript1) 28 | 29 | --Register a test and assert it was added. 30 | TestTestListView:RegisterTest(ModuleUnitTest1) 31 | expect(#TestTestListView.Tests.Children).to.equal(1) 32 | expect(TestTestListView.ModuleScriptsToEntry[ModuleScript1]).to.equal(TestTestListView.Tests.Children[1]) 33 | 34 | --Register a new test and assert it was added. 35 | TestTestListView:RegisterTest(ModuleUnitTest2) 36 | expect(#TestTestListView.Tests.Children).to.equal(2) 37 | expect(TestTestListView.ModuleScriptsToEntry[ModuleScript2]).to.equal(TestTestListView.Tests.Children[2]) 38 | 39 | --Register a rerun test and assert it was added. 40 | TestTestListView:RegisterTest(ModuleUnitTest3) 41 | expect(#TestTestListView.Tests.Children).to.equal(2) 42 | expect(TestTestListView.ModuleScriptsToEntry[ModuleScript1]).to.equal(TestTestListView.Tests.Children[1]) 43 | end) 44 | end) 45 | end -------------------------------------------------------------------------------- /src/UI/List/OutputTextEntry.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | TheNexusAvenger 3 | 4 | Entry for the output. 5 | --]] 6 | --!strict 7 | 8 | local TEXT_MARGIN_PIXELS = 3 9 | local ENUMS_TO_COLORS = { 10 | [Enum.MessageType.MessageOutput] = Enum.StudioStyleGuideColor.MainText, 11 | [Enum.MessageType.MessageWarning] = Enum.StudioStyleGuideColor.WarningText, 12 | [Enum.MessageType.MessageError] = Enum.StudioStyleGuideColor.ErrorText, 13 | [Enum.MessageType.MessageInfo] = Enum.StudioStyleGuideColor.InfoText, 14 | } 15 | 16 | 17 | 18 | local NexusUnitTestingPlugin = script.Parent.Parent.Parent 19 | local PluginInstance = require(NexusUnitTestingPlugin:WaitForChild("NexusPluginComponents"):WaitForChild("Base"):WaitForChild("PluginInstance")) 20 | 21 | local OutputTextEntry = PluginInstance:Extend() 22 | OutputTextEntry:SetClassName("OutputView") 23 | 24 | export type OutputTextEntry = { 25 | new: () -> (OutputTextEntry), 26 | Extend: (self: OutputTextEntry) -> (OutputTextEntry), 27 | 28 | Update: (self: OutputTextEntry, Data: {Message: string, Type: Enum.MessageType}?) -> (), 29 | } & PluginInstance.PluginInstance & Frame 30 | 31 | 32 | 33 | --[[ 34 | Creates the text entry. 35 | --]] 36 | function OutputTextEntry:__new(): () 37 | PluginInstance.__new(self, "Frame") 38 | 39 | --Create the text. 40 | local TextLabel = PluginInstance.new("TextLabel") 41 | TextLabel.Size = UDim2.new(1, -(2 * TEXT_MARGIN_PIXELS), 1, 0) 42 | TextLabel.Position = UDim2.new(0, TEXT_MARGIN_PIXELS, 0, 0) 43 | TextLabel.Parent = self 44 | self:DisableChangeReplication("TextLabel") 45 | self.TextLabel = TextLabel 46 | end 47 | 48 | --[[ 49 | Updates the text. 50 | --]] 51 | function OutputTextEntry:Update(Data: {Message: string, Type: Enum.MessageType}?): () 52 | if Data then 53 | self.TextLabel.Text = Data.Message 54 | if Data.Type then 55 | self.TextLabel.TextColor3 = ENUMS_TO_COLORS[Data.Type] 56 | self.TextLabel.Font = Enum.Font.SourceSans 57 | else 58 | self.TextLabel.TextColor3 = Enum.StudioStyleGuideColor.MainText 59 | self.TextLabel.Font = Enum.Font.SourceSansItalic 60 | end 61 | else 62 | self.TextLabel.Text = "" 63 | end 64 | end 65 | 66 | 67 | 68 | return (OutputTextEntry :: any) :: OutputTextEntry -------------------------------------------------------------------------------- /src/UI/Bar/ButtonSideBar.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | TheNexusAvenger 3 | 4 | Frame containing the buttons. 5 | --]] 6 | --!strict 7 | 8 | local NexusUnitTestingPlugin = script.Parent.Parent.Parent 9 | local RunTestsButton = require(NexusUnitTestingPlugin:WaitForChild("UI"):WaitForChild("Button"):WaitForChild("RunTestsButton")) 10 | local RunFailedTestsButton = require(NexusUnitTestingPlugin:WaitForChild("UI"):WaitForChild("Button"):WaitForChild("RunFailedTestsButton")) 11 | local RunSelectedTestsButton = require(NexusUnitTestingPlugin:WaitForChild("UI"):WaitForChild("Button"):WaitForChild("RunSelectedTestsButton")) 12 | local PluginInstance = require(NexusUnitTestingPlugin:WaitForChild("NexusPluginComponents"):WaitForChild("Base"):WaitForChild("PluginInstance")) 13 | 14 | local ButtonSideBar = PluginInstance:Extend() 15 | ButtonSideBar:SetClassName("ButtonSideBar") 16 | 17 | export type ButtonSideBar = { 18 | new: () -> (ButtonSideBar), 19 | Extend: (self: ButtonSideBar) -> (ButtonSideBar), 20 | 21 | RunTestsButton: RunTestsButton.RunTestsButton, 22 | RunFailedTestsButton: RunFailedTestsButton.RunFailedTestsButton, 23 | RunSelectedTestsButton: RunSelectedTestsButton.RunSelectedTestsButton, 24 | } & PluginInstance.PluginInstance & Frame 25 | 26 | 27 | 28 | --[[ 29 | Creates a Button Side Bar object. 30 | --]] 31 | function ButtonSideBar:__new(): () 32 | PluginInstance.__new(self, "Frame") 33 | 34 | --Create the buttons. 35 | local RunTests = RunTestsButton.new() 36 | RunTests.Size = UDim2.new(0, 14, 0, 14) 37 | RunTests.Position = UDim2.new(0, 7, 0, 7) 38 | RunTests.Parent = self 39 | self:DisableChangeReplication("RunTestsButton") 40 | self.RunTestsButton = RunTests 41 | 42 | local RunFailedTests = RunFailedTestsButton.new() 43 | RunFailedTests.Size = UDim2.new(0, 14, 0, 14) 44 | RunFailedTests.Position = UDim2.new(0, 7, 0, 32) 45 | RunFailedTests.Parent = self 46 | self:DisableChangeReplication("RunFailedTestsButton") 47 | self.RunFailedTestsButton = RunFailedTests 48 | 49 | local RunSelectedTests = RunSelectedTestsButton.new() 50 | RunSelectedTests.Size = UDim2.new(0, 14, 0, 14) 51 | RunSelectedTests.Position = UDim2.new(0, 7, 0, 57) 52 | RunSelectedTests.Parent = self 53 | self:DisableChangeReplication("RunSelectedTestsButton") 54 | self.RunSelectedTestsButton = RunSelectedTests 55 | 56 | --Set the defaults. 57 | self.BorderSizePixel = 1 58 | self.Size = UDim2.new(0, 28, 1, 0) 59 | end 60 | 61 | 62 | 63 | return (ButtonSideBar :: any) :: ButtonSideBar -------------------------------------------------------------------------------- /src/UI/List/TestListFrame.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | TheNexusAvenger 3 | 4 | Frame for showing and selecting tests. 5 | --]] 6 | --!strict 7 | 8 | local NexusUnitTestingPlugin = script.Parent.Parent.Parent 9 | local TestStateIcon = require(NexusUnitTestingPlugin:WaitForChild("UI"):WaitForChild("TestStateIcon")) 10 | local NexusPluginComponents = require(NexusUnitTestingPlugin:WaitForChild("NexusPluginComponents")) 11 | local CollapsableListFrame = require(NexusUnitTestingPlugin:WaitForChild("NexusPluginComponents"):WaitForChild("Input"):WaitForChild("Custom"):WaitForChild("CollapsableListFrame")) 12 | 13 | local TestListFrame = CollapsableListFrame:Extend() 14 | TestListFrame:SetClassName("TestListFrame") 15 | 16 | export type TestListFrame = { 17 | new: () -> (TestListFrame), 18 | Extend: (self: TestListFrame) -> (TestListFrame), 19 | 20 | Update: (self: TestListFrame, Data: any) -> (), 21 | } & CollapsableListFrame.CollapsableListFrame 22 | 23 | 24 | 25 | --[[ 26 | Creates a Test List Frame object. 27 | --]] 28 | function TestListFrame:__new(): () 29 | CollapsableListFrame.__new(self) 30 | self:DisableChangeReplication("TestListView") 31 | 32 | --Create the icon. 33 | local Icon = TestStateIcon.new() 34 | Icon.Size = UDim2.new(0, 16, 0, 16) 35 | Icon.Position = UDim2.new(0, 2, 0, 2) 36 | Icon.Parent = self.AdornFrame 37 | self:DisableChangeReplication("Icon") 38 | self.Icon = Icon 39 | 40 | --Create the text for the test name and time. 41 | local TestNameLabel = NexusPluginComponents.new("TextLabel") 42 | TestNameLabel.Size = UDim2.new(1, -24, 0, 16) 43 | TestNameLabel.Position = UDim2.new(0, 22, 0, 1) 44 | TestNameLabel.Parent = self.AdornFrame 45 | self:DisableChangeReplication("TestNameLabel") 46 | self.TestNameLabel = TestNameLabel 47 | 48 | local TestTimeLabel = NexusPluginComponents.new("TextLabel") 49 | TestTimeLabel.Size = UDim2.new(1, -24, 0, 16) 50 | TestTimeLabel.TextColor3 = "SubText" 51 | TestTimeLabel.Text = "" 52 | TestTimeLabel.Parent = self.AdornFrame 53 | self:DisableChangeReplication("TestTimeLabel") 54 | self.TestTimeLabel = TestTimeLabel 55 | 56 | --Set the defaults. 57 | self.Size = UDim2.new(1, 0, 0, 20) 58 | end 59 | 60 | --[[ 61 | Updates the value of the list frame. 62 | --]] 63 | function TestListFrame:Update(Data: any): () 64 | CollapsableListFrame.Update(self, Data) 65 | 66 | --Update the test display. 67 | local Test = Data and Data.Test 68 | if not Test then return end 69 | self.Icon.TestState = Test.CombinedState 70 | self.Icon.HasOutput = Data.HasOutput 71 | self.TestNameLabel.Text = Test.Name 72 | self.TestTimeLabel.Text = Data.Duration and string.format("%.3f", Data.Duration).." seconds" or "" 73 | self.TestTimeLabel.Position = UDim2.new(0, Data.DurationPosition, 0, 1) 74 | 75 | --Update if the frame is selected. 76 | if not self.TestListView or not Test.FullName then return end 77 | self.TestListView.SelectedTestsNames[Test.FullName] = Data.Selected and true or nil 78 | end 79 | 80 | 81 | 82 | return (TestListFrame :: any) :: TestListFrame -------------------------------------------------------------------------------- /src/UI/TestStateIcon.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | TheNexusAvenger 3 | 4 | Icon for showing the state of a test. 5 | --]] 6 | --!strict 7 | 8 | local NexusUnitTestingPlugin = script.Parent.Parent 9 | local NexusUnitTesting = require(NexusUnitTestingPlugin:WaitForChild("NexusUnitTestingModule")) 10 | local PluginInstance = require(NexusUnitTestingPlugin:WaitForChild("NexusPluginComponents"):WaitForChild("Base"):WaitForChild("PluginInstance")) 11 | 12 | local TEST_ICON_SPRITESHEET = "https://www.roblox.com/asset/?id=4595118527" 13 | local ICON_SIZE = Vector2.new(256, 256) 14 | 15 | local ICON_POSITIONS = { 16 | [NexusUnitTesting.TestState.NotRun] = Vector2.new(0, 0), 17 | [NexusUnitTesting.TestState.InProgress] = Vector2.new(256, 0), 18 | [NexusUnitTesting.TestState.Passed] = Vector2.new(512, 0), 19 | [NexusUnitTesting.TestState.Failed] = Vector2.new(768, 0), 20 | [NexusUnitTesting.TestState.Skipped] = Vector2.new(0, 256), 21 | } 22 | local ICON_COLORS = { 23 | [NexusUnitTesting.TestState.NotRun] = Color3.fromRGB(0, 170, 255), 24 | [NexusUnitTesting.TestState.InProgress] = Color3.fromRGB(255, 150, 0), 25 | [NexusUnitTesting.TestState.Passed] = Color3.fromRGB(0, 200, 0), 26 | [NexusUnitTesting.TestState.Failed] = Color3.fromRGB(200, 0, 0), 27 | [NexusUnitTesting.TestState.Skipped] = Color3.fromRGB(220, 220, 0), 28 | } 29 | 30 | local TestStateIcon = PluginInstance:Extend() 31 | TestStateIcon:SetClassName("TestStateIcon") 32 | 33 | export type TestStateIcon = { 34 | new: () -> (TestStateIcon), 35 | Extend: (self: TestStateIcon) -> (TestStateIcon), 36 | 37 | TestState: string, 38 | HasOutput: boolean, 39 | } & PluginInstance.PluginInstance & ImageLabel 40 | 41 | 42 | 43 | --[[ 44 | Creates the Test State Icon. 45 | --]] 46 | function TestStateIcon:__new(): () 47 | PluginInstance.__new(self, "ImageLabel") 48 | 49 | --Set up changing the test state. 50 | self:DisableChangeReplication("TestState") 51 | self:GetPropertyChangedSignal("TestState"):Connect(function() 52 | self.ImageColor3 = ICON_COLORS[self.TestState] 53 | self.ImageRectOffset = ICON_POSITIONS[self.TestState] 54 | end) 55 | 56 | --Add an indicator for if there is any output. 57 | local OutputIndicator = PluginInstance.new("Frame") 58 | OutputIndicator.BackgroundColor3 = Color3.new(0, 170/255, 255/255) 59 | OutputIndicator.Size = UDim2.new(0.5, 0, 0.5, 0) 60 | OutputIndicator.Position = UDim2.new(0.5, 0, 0.5, 0) 61 | OutputIndicator.Parent = self 62 | 63 | local UICorner = PluginInstance.new("UICorner") 64 | UICorner.CornerRadius = UDim.new(0.5, 0) 65 | UICorner.Parent = OutputIndicator 66 | self:DisableChangeReplication("OutputIndicator") 67 | self.OutputIndicator = OutputIndicator 68 | 69 | --Set up showing and hiding the indicator. 70 | self:DisableChangeReplication("HasOutput") 71 | self:GetPropertyChangedSignal("HasOutput"):Connect(function() 72 | OutputIndicator.Visible = self.HasOutput 73 | end) 74 | 75 | --Set the defaults. 76 | self.BackgroundTransparency = 1 77 | self.Image = TEST_ICON_SPRITESHEET 78 | self.ImageRectSize = ICON_SIZE 79 | self.TestState = NexusUnitTesting.TestState.NotRun 80 | self.HasOutput = false 81 | end 82 | 83 | 84 | 85 | return (TestStateIcon :: any) :: TestStateIcon -------------------------------------------------------------------------------- /test/UI/Bar/TestProgressBarTests.spec.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | TheNexusAvenger 3 | 4 | Tests the TestProgressBar class. 5 | --]] 6 | --!strict 7 | 8 | local NexusUnitTestingPlugin = game:GetService("ReplicatedStorage"):WaitForChild("NexusUnitTestingPlugin") 9 | local TestProgressBar = require(NexusUnitTestingPlugin:WaitForChild("UI"):WaitForChild("Bar"):WaitForChild("TestProgressBar")) 10 | local UnitTestClass = require(NexusUnitTestingPlugin:WaitForChild("NexusUnitTestingModule"):WaitForChild("UnitTest"):WaitForChild("UnitTest")) 11 | 12 | return function() 13 | local TestTestProgressBar = nil 14 | beforeEach(function() 15 | TestTestProgressBar = TestProgressBar.new() 16 | end) 17 | afterEach(function() 18 | TestTestProgressBar:Destroy() 19 | end) 20 | 21 | describe("A test progress bar", function() 22 | it("should update with new tests.", function() 23 | --Modify the progress bar and assert it is set up correctly. 24 | TestTestProgressBar.TimeText = "[Mock time]" 25 | local Icon = TestTestProgressBar.Icon 26 | expect(Icon.ImageRectOffset).to.equal(Vector2.new(0, 0)) 27 | 28 | --Add a series of tests and assert it is correct. 29 | local Test1 = UnitTestClass.new("Test 1") 30 | Test1.State = "PASSED" :: any 31 | local Test2 = UnitTestClass.new("Test 2") 32 | Test2.State = "PASSED" :: any 33 | local Test3 = UnitTestClass.new("Test 3") 34 | Test3.State = "INPROGRESS" :: any 35 | Test1:RegisterUnitTest(Test2) 36 | Test1:RegisterUnitTest(Test3) 37 | TestTestProgressBar:AddUnitTest(Test1) 38 | expect(TestTestProgressBar.TotalsTextLabel.Text).to.equal("2 passed, 0 failed, 0 skipped (3 total) [Mock time]") 39 | expect(Icon.ImageRectOffset).to.equal(Vector2.new(256, 0)) 40 | 41 | --Change a test and assert it changed. 42 | Test3.State = "FAILED" :: any 43 | expect(TestTestProgressBar.TotalsTextLabel.Text).to.equal("2 passed, 1 failed, 0 skipped (3 total) [Mock time]") 44 | expect(Icon.ImageRectOffset).to.equal(Vector2.new(768, 0)) 45 | 46 | --Add another test as a subtest and assert it changed. 47 | local Test4 = UnitTestClass.new("Test 4") 48 | Test4.State = "SKIPPED" :: any 49 | Test3:RegisterUnitTest(Test4) 50 | expect(TestTestProgressBar.TotalsTextLabel.Text).to.equal("2 passed, 1 failed, 1 skipped (4 total) [Mock time]") 51 | expect(Icon.ImageRectOffset).to.equal(Vector2.new(768, 0)) 52 | 53 | --Remove a test and assert it changed. 54 | TestTestProgressBar:RemoveUnitTest(Test3) 55 | expect(TestTestProgressBar.TotalsTextLabel.Text).to.equal("2 passed, 0 failed, 0 skipped (2 total) [Mock time]") 56 | expect(Icon.ImageRectOffset).to.equal(Vector2.new(512, 0)) 57 | end) 58 | 59 | it("should set the time.", function() 60 | TestTestProgressBar:SetTime(10, 20, 30) 61 | expect(TestTestProgressBar.TimeText).to.equal("[Started at 10:20:30]") 62 | TestTestProgressBar:SetTime(1, 2, 3) 63 | expect(TestTestProgressBar.TimeText).to.equal("[Started at 1:02:03]") 64 | TestTestProgressBar:SetTime(0, 0, 0) 65 | expect(TestTestProgressBar.TimeText).to.equal("[Started at 0:00:00]") 66 | end) 67 | end) 68 | end -------------------------------------------------------------------------------- /test/UI/TestStateIconTests.spec.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | TheNexusAvenger 3 | 4 | Tests the TestStateIcon class. 5 | --]] 6 | --!strict 7 | 8 | local NexusUnitTestingPlugin = game:GetService("ReplicatedStorage"):WaitForChild("NexusUnitTestingPlugin") 9 | local TestStateIcon = require(NexusUnitTestingPlugin:WaitForChild("UI"):WaitForChild("TestStateIcon")) 10 | 11 | return function() 12 | local TestTestStateIcon = nil 13 | beforeEach(function() 14 | TestTestStateIcon = TestStateIcon.new() 15 | end) 16 | afterEach(function() 17 | TestTestStateIcon:Destroy() 18 | end) 19 | 20 | describe("A test state icon", function() 21 | it("should update based on the test state.", function() 22 | --Assert the icon is correct. 23 | expect(TestTestStateIcon.TestState).to.equal("NOTRUN") 24 | expect(TestTestStateIcon.Image).to.equal("https://www.roblox.com/asset/?id=4595118527") 25 | expect(TestTestStateIcon.ImageColor3).to.equal(Color3.fromRGB(0, 170, 255)) 26 | expect(TestTestStateIcon.ImageRectSize).to.equal(Vector2.new(256, 256)) 27 | expect(TestTestStateIcon.ImageRectOffset).to.equal(Vector2.new(0, 0)) 28 | 29 | --Set the test as in progress and assert it is correct. 30 | TestTestStateIcon.TestState = "INPROGRESS" 31 | expect(TestTestStateIcon.Image).to.equal("https://www.roblox.com/asset/?id=4595118527") 32 | expect(TestTestStateIcon.ImageColor3).to.equal(Color3.fromRGB(255, 150, 0)) 33 | expect(TestTestStateIcon.ImageRectSize).to.equal(Vector2.new(256, 256)) 34 | expect(TestTestStateIcon.ImageRectOffset).to.equal(Vector2.new(256, 0)) 35 | 36 | --Set the test as passed and assert it is correct. 37 | TestTestStateIcon.TestState = "PASSED" 38 | expect(TestTestStateIcon.Image).to.equal("https://www.roblox.com/asset/?id=4595118527") 39 | expect(TestTestStateIcon.ImageColor3).to.equal(Color3.fromRGB(0, 200, 0)) 40 | expect(TestTestStateIcon.ImageRectSize).to.equal(Vector2.new(256, 256)) 41 | expect(TestTestStateIcon.ImageRectOffset).to.equal(Vector2.new(512, 0)) 42 | 43 | --Set the test as failed and assert it is correct. 44 | TestTestStateIcon.TestState = "FAILED" 45 | expect(TestTestStateIcon.Image).to.equal("https://www.roblox.com/asset/?id=4595118527") 46 | expect(TestTestStateIcon.ImageColor3).to.equal(Color3.fromRGB(200, 0, 0)) 47 | expect(TestTestStateIcon.ImageRectSize).to.equal(Vector2.new(256, 256)) 48 | expect(TestTestStateIcon.ImageRectOffset).to.equal(Vector2.new(768, 0)) 49 | 50 | --Set the test as failed and skipped it is correct. 51 | TestTestStateIcon.TestState = "SKIPPED" 52 | expect(TestTestStateIcon.Image).to.equal("https://www.roblox.com/asset/?id=4595118527") 53 | expect(TestTestStateIcon.ImageColor3).to.equal(Color3.fromRGB(220, 220, 0)) 54 | expect(TestTestStateIcon.ImageRectSize).to.equal(Vector2.new(256, 256)) 55 | expect(TestTestStateIcon.ImageRectOffset).to.equal(Vector2.new(0, 256)) 56 | end) 57 | 58 | it("should update the output indicator", function() 59 | --Assert the indicator isn't visible by default. 60 | expect(TestTestStateIcon.OutputIndicator.Visible).to.equal(false) 61 | 62 | --Set that there is output and assert the indicator is visible. 63 | TestTestStateIcon.HasOutput = true 64 | expect(TestTestStateIcon.OutputIndicator.Visible).to.equal(true) 65 | 66 | --Set that there is not output and assert the indicator is not visible. 67 | TestTestStateIcon.HasOutput = false 68 | expect(TestTestStateIcon.OutputIndicator.Visible).to.equal(false) 69 | end) 70 | end) 71 | end -------------------------------------------------------------------------------- /src/UI/Window/OutputView.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | TheNexusAvenger 3 | 4 | Frame for viewing the output of a test. 5 | --]] 6 | --!strict 7 | 8 | local TEXT_MARGIN_PIXELS = 3 9 | local LINE_HEIGHT_PIXELS = 17 10 | 11 | 12 | 13 | local TextService = game:GetService("TextService") 14 | 15 | local NexusUnitTestingPlugin = script.Parent.Parent.Parent 16 | local NexusPluginComponents = require(NexusUnitTestingPlugin:WaitForChild("NexusPluginComponents")) 17 | local PluginInstance = require(NexusUnitTestingPlugin:WaitForChild("NexusPluginComponents"):WaitForChild("Base"):WaitForChild("PluginInstance")) 18 | local OutputTextEntry = require(NexusUnitTestingPlugin:WaitForChild("UI"):WaitForChild("List"):WaitForChild("OutputTextEntry")) 19 | local UnitTest = require(NexusUnitTestingPlugin:WaitForChild("NexusUnitTestingModule"):WaitForChild("UnitTest"):WaitForChild("UnitTest")) 20 | 21 | local OutputView = PluginInstance:Extend() 22 | OutputView:SetClassName("OutputView") 23 | 24 | export type OutputView = { 25 | new: () -> (OutputView), 26 | Extend: (self: OutputView) -> (OutputView), 27 | 28 | AddOutput: (self: OutputView, String: string, Type: Enum.MessageType) -> (), 29 | SetTest: (self: OutputView, Test: UnitTest.UnitTest) -> (), 30 | } & PluginInstance.PluginInstance & Frame 31 | 32 | 33 | 34 | --[[ 35 | Creates a Output View frame object. 36 | --]] 37 | function OutputView:__new(): () 38 | PluginInstance.__new(self, "Frame") 39 | 40 | --Store the output data. 41 | self:DisableChangeReplication("OutputLines") 42 | self.OutputLines = {} 43 | self:DisableChangeReplication("TestEvents") 44 | self.TestEvents = {} 45 | 46 | --Create the top bar. 47 | local TopBar = NexusPluginComponents.new("Frame") 48 | TopBar.BorderSizePixel = 1 49 | TopBar.Size = UDim2.new(1, 0, 0, 21) 50 | TopBar.Parent = self 51 | 52 | local TopBarLabel = NexusPluginComponents.new("TextLabel") 53 | TopBarLabel.Size = UDim2.new(1, -4, 0, 16) 54 | TopBarLabel.Position = UDim2.new(0, 2, 0, 2) 55 | TopBarLabel.Text = "" 56 | TopBarLabel.Font = Enum.Font.SourceSansBold 57 | TopBarLabel.Parent = TopBar 58 | self:DisableChangeReplication("TopBarLabel") 59 | self.TopBarLabel = TopBarLabel 60 | 61 | local TopBarFullNameLabel = NexusPluginComponents.new("TextLabel") 62 | TopBarFullNameLabel.Size = UDim2.new(1, -4, 0, 16) 63 | TopBarFullNameLabel.Position = UDim2.new(0, 2, 0, 2) 64 | TopBarFullNameLabel.Text = "" 65 | TopBarFullNameLabel.Font = Enum.Font.SourceSansItalic 66 | TopBarFullNameLabel.Parent = TopBar 67 | self:DisableChangeReplication("TopBarFullNameLabel") 68 | self.TopBarFullNameLabel = TopBarFullNameLabel 69 | 70 | --Create the scrolling frame. 71 | local ScrollingFrame = NexusPluginComponents.new("ScrollingFrame") 72 | ScrollingFrame.Size = UDim2.new(1, 0, 1, -22) 73 | ScrollingFrame.Position = UDim2.new(0, 0, 0, 22) 74 | ScrollingFrame.BackgroundTransparency = 1 75 | ScrollingFrame.Parent = self 76 | self:DisableChangeReplication("ScrollingFrame") 77 | self.ScrollingFrame = ScrollingFrame 78 | 79 | local ElementList = NexusPluginComponents.new("ElementList", OutputTextEntry) 80 | ElementList.EntryHeight = LINE_HEIGHT_PIXELS 81 | ElementList:ConnectScrollingFrame(ScrollingFrame) 82 | self:DisableChangeReplication("ElementList") 83 | self.ElementList = ElementList 84 | 85 | self:DisableChangeReplication("MaxLineWidth") 86 | self:GetPropertyChangedSignal("MaxLineWidth"):Connect(function() 87 | ElementList.CurrentWidth = math.max(100, self.MaxLineWidth) 88 | end) 89 | self.MaxLineWidth = 0 90 | 91 | --Set the defaults. 92 | self.Size = UDim2.new(1, 0, 1, 0) 93 | self.ElementList:SetEntries({{Message="No Test Selected"}}) 94 | end 95 | 96 | --[[ 97 | Updates the displayed output. 98 | --]] 99 | function OutputView:UpdateDisplayedOutput(): () 100 | if #self.OutputLines == 0 then 101 | self.ElementList:SetEntries({{Message = "No Output"}}) 102 | else 103 | self.ElementList:SetEntries(self.OutputLines) 104 | end 105 | end 106 | 107 | --[[ 108 | Processes a new output entry. 109 | --]] 110 | function OutputView:ProcessOutput(String: string, Type: Enum.MessageType): () 111 | --If the string has multiple lines, split the string and add them. 112 | if string.find(String,"\n") then 113 | for _,SubString in string.split(String,"\n") do 114 | self:AddOutput(SubString, Type) 115 | end 116 | return 117 | end 118 | 119 | --Add the string. 120 | table.insert(self.OutputLines, {Message = String, Type = Type}) 121 | 122 | --Update the max size. 123 | local StringWidth = TextService:GetTextSize(String, 14, Enum.Font.SourceSans, Vector2.new(2000, LINE_HEIGHT_PIXELS)).X + (TEXT_MARGIN_PIXELS * 2) 124 | if StringWidth > self.MaxLineWidth then 125 | self.MaxLineWidth = StringWidth 126 | end 127 | end 128 | 129 | --[[ 130 | Adds a line to display in the output. 131 | --]] 132 | function OutputView:AddOutput(String: string, Type: Enum.MessageType) 133 | self:ProcessOutput(String, Type) 134 | self:UpdateDisplayedOutput() 135 | end 136 | 137 | --[[ 138 | Sets the test to use for the output. 139 | --]] 140 | function OutputView:SetTest(Test: UnitTest.UnitTest): () 141 | --Set the top bar name. 142 | self.TopBarLabel.Text = Test.Name 143 | if Test.FullName then 144 | self.TopBarFullNameLabel.Text = "("..Test.FullName..")" 145 | self.TopBarFullNameLabel.Position = UDim2.new(0, TextService:GetTextSize(Test.Name, 14, Enum.Font.SourceSansBold, Vector2.new(2000, 16)).X + 4, 0, 2) 146 | else 147 | self.TopBarFullNameLabel.Text = "" 148 | end 149 | 150 | --Clear the output. 151 | self.OutputLines = {} 152 | self.MaxLineWidth = 0 153 | 154 | --Disconnect the existing events. 155 | for _,Event in self.TestEvents do 156 | Event:Disconnect() 157 | end 158 | self.TestEvents = {} 159 | 160 | --Add the existing output. 161 | for _, Output in Test.Output :: {{Message: string, Type: Enum.MessageType}} do 162 | self:ProcessOutput(Output.Message, Output.Type) 163 | end 164 | self:UpdateDisplayedOutput() 165 | 166 | --Connect the events. 167 | table.insert(self.TestEvents,Test.MessageOutputted:Connect(function(Message,Type) 168 | self:AddOutput(Message,Type) 169 | end)) 170 | end 171 | 172 | 173 | 174 | return (OutputView :: any) :: OutputView -------------------------------------------------------------------------------- /src/UI/Bar/TestProgressBar.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | TheNexusAvenger 3 | 4 | Frame containing the progress of the tests. 5 | --]] 6 | --!strict 7 | 8 | local NexusUnitTestingPlugin = script.Parent.Parent.Parent 9 | local TestStateIcon = require(NexusUnitTestingPlugin:WaitForChild("UI"):WaitForChild("TestStateIcon")) 10 | local NexusPluginComponents = require(NexusUnitTestingPlugin:WaitForChild("NexusPluginComponents")) 11 | local PluginInstance = require(NexusUnitTestingPlugin:WaitForChild("NexusPluginComponents"):WaitForChild("Base"):WaitForChild("PluginInstance")) 12 | local NexusUnitTesting = require(NexusUnitTestingPlugin:WaitForChild("NexusUnitTestingModule")) 13 | local UnitTest = require(NexusUnitTestingPlugin:WaitForChild("NexusUnitTestingModule"):WaitForChild("UnitTest"):WaitForChild("UnitTest")) 14 | 15 | local TestProgressBar = PluginInstance:Extend() 16 | TestProgressBar:SetClassName("TestProgressBar") 17 | 18 | export type TestProgressBar = { 19 | new: () -> (TestProgressBar), 20 | Extend: (self: TestProgressBar) -> (TestProgressBar), 21 | 22 | SetTime: (self: TestProgressBar, Hours: number?, Minutes: number?, Seconds: number?) -> (), 23 | AddUnitTest: (self: TestProgressBar, UnitTest: UnitTest.UnitTest, DontUpdateBar: boolean?) -> (), 24 | RemoveUnitTest: (Uself: TestProgressBar, nitTest: UnitTest.UnitTest, DontUpdateBar: boolean?) -> (), 25 | } & PluginInstance.PluginInstance & Frame 26 | 27 | 28 | 29 | --[[ 30 | Creates a Test Progress Bar object. 31 | --]] 32 | function TestProgressBar:__new(): () 33 | PluginInstance.__new(self, "Frame") 34 | 35 | --Store the tests. 36 | self:DisableChangeReplication("TimeText") 37 | self:DisableChangeReplication("Tests") 38 | self:DisableChangeReplication("TestEvents") 39 | self.TimeText = "" 40 | self.Tests = {} 41 | self.TestEvents = {} 42 | 43 | --Create the icon. 44 | local Icon = TestStateIcon.new() 45 | Icon.Size = UDim2.new(0, 16, 0, 16) 46 | Icon.Position = UDim2.new(0, 6, 0, 6) 47 | Icon.Parent = self:GetWrappedInstance() 48 | self:DisableChangeReplication("Icon") 49 | self.Icon = Icon 50 | 51 | --Create the bars. 52 | local BarBackground = NexusPluginComponents.new("Frame") 53 | BarBackground.Size = UDim2.new(1, -32, 0, 6) 54 | BarBackground.Position = UDim2.new(0, 30, 0, 2) 55 | BarBackground.BorderSizePixel = 1 56 | BarBackground.Parent = self 57 | 58 | local PassedBar = NexusPluginComponents.new("Frame") 59 | PassedBar.Size = UDim2.new(0, 0, 0, 0) 60 | PassedBar.BorderSizePixel = 0 61 | PassedBar.BackgroundColor3 = Color3.new(0, 200/255, 0) 62 | PassedBar.ClipsDescendants = true 63 | PassedBar.Parent = BarBackground 64 | self:DisableChangeReplication("PassedBar") 65 | self.PassedBar = PassedBar 66 | 67 | local FailedBar = NexusPluginComponents.new("Frame") 68 | FailedBar.Size = UDim2.new(0, 0, 0, 0) 69 | FailedBar.BorderSizePixel = 0 70 | FailedBar.BackgroundColor3 = Color3.new(200/255, 0, 0) 71 | FailedBar.ClipsDescendants = true 72 | FailedBar.Parent = BarBackground 73 | self:DisableChangeReplication("FailedBar") 74 | self.FailedBar = FailedBar 75 | 76 | local SkippedBar = NexusPluginComponents.new("Frame") 77 | SkippedBar.Size = UDim2.new(0, 0, 0, 0) 78 | SkippedBar.BorderSizePixel = 0 79 | SkippedBar.BackgroundColor3 = Color3.new(200/255, 200/255 ,0) 80 | SkippedBar.ClipsDescendants = true 81 | SkippedBar.Parent = BarBackground 82 | self:DisableChangeReplication("SkippedBar") 83 | self.SkippedBar = SkippedBar 84 | 85 | local TotalsTextLabel = NexusPluginComponents.new("TextLabel") 86 | TotalsTextLabel.Size = UDim2.new(1, -32, 0, 16) 87 | TotalsTextLabel.Position = UDim2.new(0, 30, 0, 10) 88 | TotalsTextLabel.Text = "Not run" 89 | TotalsTextLabel.Parent = self 90 | self:DisableChangeReplication("TotalsTextLabel") 91 | self.TotalsTextLabel = TotalsTextLabel 92 | 93 | --Set the defaults. 94 | self.BorderSizePixel = 1 95 | self.Size = UDim2.new(1, 0, 0, 28) 96 | end 97 | 98 | --[[ 99 | Updates the progress bar. 100 | --]] 101 | function TestProgressBar:UpdateProgressBar(): () 102 | --Determinee the amount of each type. 103 | local TotalTests, InProgressTests, PassedTests, FailedTests, SkippedTests = 0, 0, 0, 0, 0 104 | for _, Test in self.Tests do 105 | TotalTests = TotalTests + 1 106 | 107 | if Test.State == NexusUnitTesting.TestState.Passed then 108 | PassedTests = PassedTests + 1 109 | elseif Test.State == NexusUnitTesting.TestState.InProgress then 110 | InProgressTests = InProgressTests + 1 111 | elseif Test.State == NexusUnitTesting.TestState.Failed then 112 | FailedTests = FailedTests + 1 113 | elseif Test.State == NexusUnitTesting.TestState.Skipped then 114 | SkippedTests = SkippedTests + 1 115 | end 116 | end 117 | 118 | --Update the icon. 119 | if InProgressTests > 0 then 120 | self.Icon.TestState = NexusUnitTesting.TestState.InProgress 121 | elseif FailedTests > 0 then 122 | self.Icon.TestState = NexusUnitTesting.TestState.Failed 123 | elseif PassedTests > 0 then 124 | self.Icon.TestState = NexusUnitTesting.TestState.Passed 125 | elseif SkippedTests > 0 then 126 | self.Icon.TestState = NexusUnitTesting.TestState.Skipped 127 | else 128 | self.Icon.TestState = NexusUnitTesting.TestState.NotRun 129 | end 130 | 131 | --Update the text. 132 | self.TotalsTextLabel.Text = tostring(PassedTests).." passed, "..tostring(FailedTests).." failed, "..tostring(SkippedTests).." skipped ("..tostring(TotalTests).." total) "..self.TimeText 133 | 134 | --Update the sizes. 135 | if TotalTests == 0 then TotalTests = 1 end 136 | self.PassedBar.Size = UDim2.new(PassedTests / TotalTests, 0, 1, 0) 137 | self.FailedBar.Size = UDim2.new(FailedTests / TotalTests, 0, 1, 0) 138 | self.SkippedBar.Size = UDim2.new(SkippedTests / TotalTests, 0, 1, 0) 139 | self.FailedBar.Position = UDim2.new(PassedTests / TotalTests, 0, 0, 0) 140 | self.SkippedBar.Position = UDim2.new((PassedTests + FailedTests) / TotalTests, 0, 0, 0) 141 | end 142 | 143 | --[[ 144 | Updates the time text of the test. 145 | Does not update the text automatically. 146 | --]] 147 | function TestProgressBar:SetTime(Hours: number?, Minutes: number?, Seconds: number?): () 148 | --Set the time if it isn't set. 149 | if not Hours and not Minutes and not Seconds then 150 | local CurrentTime = os.date("*t", tick()) :: any 151 | Hours, Minutes, Seconds = CurrentTime.hour, CurrentTime.min, CurrentTime.sec 152 | elseif Hours and not Minutes and not Seconds then 153 | local CurrentTime = os.date("*t", Hours) :: any 154 | Hours, Minutes, Seconds = CurrentTime.hour, CurrentTime.min, CurrentTime.sec 155 | end 156 | 157 | --Format and set the time. 158 | self.TimeText = string.format("[Started at %d:%02d:%02d]", Hours :: number, Minutes :: number, Seconds :: number) 159 | end 160 | 161 | --[[ 162 | Adds a unit test to track. 163 | --]] 164 | function TestProgressBar:AddUnitTest(UnitTest: UnitTest.UnitTest, DontUpdateBar: boolean?): () 165 | --Store the test. 166 | table.insert(self.Tests, UnitTest) 167 | 168 | --Connect the events. 169 | local Events = {} 170 | self.TestEvents[UnitTest] = Events 171 | table.insert(Events, UnitTest:GetPropertyChangedSignal("State"):Connect(function() 172 | self:UpdateProgressBar() 173 | end)) 174 | table.insert(Events, UnitTest.TestAdded:Connect(function(SubUnitTest) 175 | self:AddUnitTest(SubUnitTest) 176 | end)) 177 | 178 | --Add the subtests. 179 | for _,SubUnitTest in UnitTest.SubTests :: {UnitTest.UnitTest} do 180 | self:AddUnitTest(SubUnitTest,true) 181 | end 182 | 183 | --Update the bar. 184 | if DontUpdateBar ~= true then 185 | self:UpdateProgressBar() 186 | end 187 | end 188 | 189 | --[[ 190 | Removes a unit test to track. 191 | --]] 192 | function TestProgressBar:RemoveUnitTest(UnitTest: UnitTest.UnitTest, DontUpdateBar: boolean?): () 193 | --Remove the unit test. 194 | for i,Test in self.Tests do 195 | if Test == UnitTest then 196 | table.remove(self.Tests, i) 197 | break 198 | end 199 | end 200 | 201 | --Disconnect the events. 202 | for _,Connection in self.TestEvents[UnitTest] or {} do 203 | Connection:Disconnect() 204 | end 205 | 206 | --Remove the subtests. 207 | for _,SubUnitTest in UnitTest.SubTests :: {UnitTest.UnitTest} do 208 | self:RemoveUnitTest(SubUnitTest, true) 209 | end 210 | 211 | --Update the bar. 212 | if DontUpdateBar ~= true then 213 | self:UpdateProgressBar() 214 | end 215 | end 216 | 217 | 218 | 219 | return (TestProgressBar :: any) :: TestProgressBar -------------------------------------------------------------------------------- /test/UI/Window/OutputViewTests.spec.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | TheNexusAvenger 3 | 4 | Tests the OutputView class. 5 | --]] 6 | --!strict 7 | --$NexusUnitTestExtensions 8 | 9 | local NexusUnitTestingPlugin = game:GetService("ReplicatedStorage"):WaitForChild("NexusUnitTestingPlugin") 10 | local OutputView = require(NexusUnitTestingPlugin:WaitForChild("UI"):WaitForChild("Window"):WaitForChild("OutputView")) 11 | local UnitTestClass = require(NexusUnitTestingPlugin:WaitForChild("NexusUnitTestingModule"):WaitForChild("UnitTest"):WaitForChild("UnitTest")) 12 | 13 | return function() 14 | local TestOutputView = nil 15 | beforeEach(function() 16 | TestOutputView = OutputView.new() 17 | TestOutputView.Size = UDim2.new(0, 200, 0, 50 + 22) 18 | 19 | local Parent = Instance.new("ScreenGui") 20 | Parent.Parent = game:GetService("Lighting") 21 | TestOutputView.Parent = Parent 22 | task.wait() 23 | wait() 24 | end) 25 | afterEach(function() 26 | TestOutputView.Parent:Destroy() 27 | TestOutputView:Destroy() 28 | end) 29 | 30 | local function GetLabels() 31 | local Entries = {} 32 | for _, Entry in TestOutputView.ElementList.FrameEntries do 33 | table.insert(Entries, Entry:GetChildren()[1]) 34 | end 35 | return Entries 36 | end 37 | 38 | describe("An output view", function() 39 | it("should update the displayed output.", function() 40 | --Set the output as having no lines and assert the text is correct. 41 | TestOutputView.OutputLines = {} 42 | TestOutputView.MaxLineWidth = 50 43 | TestOutputView:UpdateDisplayedOutput() 44 | local OutputLabels = GetLabels() 45 | expect(OutputLabels[1].Text).to.equal("No Output") 46 | expect(OutputLabels[1].Font).to.equal(Enum.Font.SourceSansItalic) 47 | expect(OutputLabels[2].Text).to.equal("") 48 | expect(OutputLabels[3].Text).to.equal("") 49 | 50 | --Set the output as having not enough lines and assert the text is correct. 51 | TestOutputView.OutputLines = {{Message = "String 1", Type = Enum.MessageType.MessageOutput}, {Message = "String 2", Type = Enum.MessageType.MessageWarning}} 52 | TestOutputView:UpdateDisplayedOutput() 53 | expect(OutputLabels[1].Text).to.equal("String 1") 54 | expect(OutputLabels[1].Font).to.equal(Enum.Font.SourceSans) 55 | expect(OutputLabels[2].Text).to.equal("String 2") 56 | expect(OutputLabels[3].Text).to.equal("") 57 | expect(OutputLabels[1].TextColor3.ColorEnum).to.equal(Enum.StudioStyleGuideColor.MainText) 58 | expect(OutputLabels[2].TextColor3.ColorEnum).to.equal(Enum.StudioStyleGuideColor.WarningText) 59 | 60 | --Set the output as having more than enough lines and assert the text is correct. 61 | TestOutputView.OutputLines = {{Message = "String 1", Type = Enum.MessageType.MessageOutput}, {Message = "String 2", Type = Enum.MessageType.MessageWarning}, {Message = "String 3", Type = Enum.MessageType.MessageError}, {Message = "String 4", Type = Enum.MessageType.MessageInfo}, {Message = "String 5", Type = Enum.MessageType.MessageInfo}} 62 | TestOutputView:UpdateDisplayedOutput() 63 | expect(OutputLabels[1].Text).to.equal("String 1") 64 | expect(OutputLabels[1].Font).to.equal(Enum.Font.SourceSans) 65 | expect(OutputLabels[2].Text).to.equal("String 2") 66 | expect(OutputLabels[3].Text).to.equal("String 3") 67 | expect(OutputLabels[1].TextColor3.ColorEnum).to.equal(Enum.StudioStyleGuideColor.MainText) 68 | expect(OutputLabels[2].TextColor3.ColorEnum).to.equal(Enum.StudioStyleGuideColor.WarningText) 69 | expect(OutputLabels[3].TextColor3.ColorEnum).to.equal(Enum.StudioStyleGuideColor.ErrorText) 70 | 71 | --Set the canvas position as nearly scrolled and assert that the correct lines show. 72 | TestOutputView.ScrollingFrame.CanvasPosition = Vector2.new(0, 10) 73 | TestOutputView.OutputLines = {{Message = "String 1", Type = Enum.MessageType.MessageOutput}, {Message = "String 2", Type = Enum.MessageType.MessageWarning}, {Message = "String 3", Type = Enum.MessageType.MessageError}, {Message = "String 4", Type = Enum.MessageType.MessageInfo}, {Message = "String 5", Type = Enum.MessageType.MessageInfo}} 74 | TestOutputView:UpdateDisplayedOutput() 75 | expect(OutputLabels[1].Text).to.equal("String 1") 76 | expect(OutputLabels[1].Font).to.equal(Enum.Font.SourceSans) 77 | expect(OutputLabels[2].Text).to.equal("String 2") 78 | expect(OutputLabels[3].Text).to.equal("String 3") 79 | expect(OutputLabels[1].TextColor3.ColorEnum).to.equal(Enum.StudioStyleGuideColor.MainText) 80 | expect(OutputLabels[2].TextColor3.ColorEnum).to.equal(Enum.StudioStyleGuideColor.WarningText) 81 | expect(OutputLabels[3].TextColor3.ColorEnum).to.equal(Enum.StudioStyleGuideColor.ErrorText) 82 | 83 | --Set the canvas position as scrolled and assert that the correct lines show. 84 | TestOutputView.ScrollingFrame.CanvasPosition = Vector2.new(0, 20) 85 | TestOutputView.OutputLines = {{Message = "String 1", Type = Enum.MessageType.MessageOutput}, {Message = "String 2", Type = Enum.MessageType.MessageWarning}, {Message = "String 3", Type = Enum.MessageType.MessageError}, {Message = "String 4", Type = Enum.MessageType.MessageInfo}, {Message = "String 5", Type = Enum.MessageType.MessageInfo}} 86 | TestOutputView:UpdateDisplayedOutput() 87 | expect(OutputLabels[1].Text).to.equal("String 2") 88 | expect(OutputLabels[1].Font).to.equal(Enum.Font.SourceSans) 89 | expect(OutputLabels[2].Text).to.equal("String 3") 90 | expect(OutputLabels[3].Text).to.equal("String 4") 91 | expect(OutputLabels[1].TextColor3.ColorEnum).to.equal(Enum.StudioStyleGuideColor.WarningText) 92 | expect(OutputLabels[2].TextColor3.ColorEnum).to.equal(Enum.StudioStyleGuideColor.ErrorText) 93 | expect(OutputLabels[3].TextColor3.ColorEnum).to.equal(Enum.StudioStyleGuideColor.InfoText) 94 | 95 | --Set the canvas position to the bottom and assert that the correct lines show. 96 | TestOutputView.ScrollingFrame.CanvasPosition = Vector2.new(0, (17 * 5) - 50) 97 | TestOutputView.OutputLines = {{Message = "String 1", Type = Enum.MessageType.MessageOutput}, {Message = "String 2", Type = Enum.MessageType.MessageWarning}, {Message = "String 3", Type = Enum.MessageType.MessageError}, {Message = "String 4", Type = Enum.MessageType.MessageInfo}, {Message = "String 5", Type = Enum.MessageType.MessageInfo}} 98 | TestOutputView:UpdateDisplayedOutput() 99 | expect(OutputLabels[1].Text).to.equal("String 3") 100 | expect(OutputLabels[1].Font).to.equal(Enum.Font.SourceSans) 101 | expect(OutputLabels[2].Text).to.equal("String 4") 102 | expect(OutputLabels[3].Text).to.equal("String 5") 103 | expect(OutputLabels[1].TextColor3.ColorEnum).to.equal(Enum.StudioStyleGuideColor.ErrorText) 104 | expect(OutputLabels[2].TextColor3.ColorEnum).to.equal(Enum.StudioStyleGuideColor.InfoText) 105 | expect(OutputLabels[3].TextColor3.ColorEnum).to.equal(Enum.StudioStyleGuideColor.InfoText) 106 | end) 107 | 108 | it("should add output entries.", function() 109 | --Add an empty string and assert the output is correct. 110 | TestOutputView:AddOutput("", Enum.MessageType.MessageOutput) 111 | local OutputLabels = GetLabels() 112 | expect(TestOutputView.OutputLines).to.deepEqual({{Message = "", Type = Enum.MessageType.MessageOutput}}) 113 | expect(OutputLabels[1].Text).to.equal("") 114 | expect(OutputLabels[2].Text).to.equal("") 115 | expect(OutputLabels[3].Text).to.equal("") 116 | expect(OutputLabels[1].TextColor3.ColorEnum).to.equal(Enum.StudioStyleGuideColor.MainText) 117 | 118 | --Add a string and assert the output is correct. 119 | TestOutputView:AddOutput("String 1", Enum.MessageType.MessageWarning) 120 | expect(TestOutputView.OutputLines).to.deepEqual({{Message = "" ,Type = Enum.MessageType.MessageOutput}, {Message = "String 1", Type = Enum.MessageType.MessageWarning}}) 121 | expect(OutputLabels[1].Text).to.equal("") 122 | expect(OutputLabels[2].Text).to.equal("String 1") 123 | expect(OutputLabels[3].Text).to.equal("") 124 | expect(OutputLabels[1].TextColor3.ColorEnum).to.equal(Enum.StudioStyleGuideColor.MainText) 125 | expect(OutputLabels[2].TextColor3.ColorEnum).to.equal(Enum.StudioStyleGuideColor.WarningText) 126 | 127 | --Add a string with a new line and assert the output is correct. 128 | TestOutputView:AddOutput("String 2\nString 3\n", Enum.MessageType.MessageOutput) 129 | expect(TestOutputView.OutputLines).to.deepEqual({{Message = "", Type = Enum.MessageType.MessageOutput}, {Message = "String 1", Type = Enum.MessageType.MessageWarning}, {Message = "String 2", Type = Enum.MessageType.MessageOutput}, {Message = "String 3", Type = Enum.MessageType.MessageOutput}, {Message = "", Type = Enum.MessageType.MessageOutput}}) 130 | expect(OutputLabels[1].Text).to.equal("") 131 | expect(OutputLabels[2].Text).to.equal("String 1") 132 | expect(OutputLabels[3].Text).to.equal("String 2") 133 | expect(OutputLabels[1].TextColor3.ColorEnum).to.equal(Enum.StudioStyleGuideColor.MainText) 134 | expect(OutputLabels[2].TextColor3.ColorEnum).to.equal(Enum.StudioStyleGuideColor.WarningText) 135 | expect(OutputLabels[3].TextColor3.ColorEnum).to.equal(Enum.StudioStyleGuideColor.MainText) 136 | end) 137 | 138 | it("should update from set tests.", function() 139 | --Create 3 tests. 140 | local Test1, Test2, Test3 = UnitTestClass.new("Test 1"), UnitTestClass.new("Test 2"), UnitTestClass.new("Test 3") 141 | Test2.FullName = "Test N > Test 2" 142 | Test2:OutputMessage(Enum.MessageType.MessageOutput, "String 4") 143 | Test2:OutputMessage(Enum.MessageType.MessageWarning, "String 5") 144 | local OutputLabels = GetLabels() 145 | 146 | --Set a test, add messages, and assert the messages are displayed. 147 | TestOutputView:SetTest(Test1) 148 | Test1:OutputMessage(Enum.MessageType.MessageError, "String 1") 149 | Test1:OutputMessage(Enum.MessageType.MessageInfo, "String 2\nString 3") 150 | expect(TestOutputView.OutputLines).to.deepEqual({{Message = "String 1", Type = Enum.MessageType.MessageError}, {Message = "String 2", Type = Enum.MessageType.MessageInfo}, {Message = "String 3", Type = Enum.MessageType.MessageInfo}}) 151 | expect(OutputLabels[1].Text).to.equal("String 1") 152 | expect(OutputLabels[2].Text).to.equal("String 2") 153 | expect(OutputLabels[3].Text).to.equal("String 3") 154 | expect(OutputLabels[1].TextColor3.ColorEnum).to.equal(Enum.StudioStyleGuideColor.ErrorText) 155 | expect(OutputLabels[2].TextColor3.ColorEnum).to.equal(Enum.StudioStyleGuideColor.InfoText) 156 | expect(OutputLabels[3].TextColor3.ColorEnum).to.equal(Enum.StudioStyleGuideColor.InfoText) 157 | expect(TestOutputView.TopBarLabel.Text).to.equal("Test 1") 158 | expect(TestOutputView.TopBarFullNameLabel.Text).to.equal("") 159 | 160 | --Set a test with an existting output and assert the messages are displayed. 161 | TestOutputView:SetTest(Test2) 162 | expect(TestOutputView.OutputLines).to.deepEqual({{Message = "String 4", Type = Enum.MessageType.MessageOutput}, {Message = "String 5", Type = Enum.MessageType.MessageWarning}}) 163 | expect(OutputLabels[1].Text).to.equal("String 4") 164 | expect(OutputLabels[2].Text).to.equal("String 5") 165 | expect(OutputLabels[3].Text).to.equal("") 166 | expect(OutputLabels[1].TextColor3.ColorEnum).to.equal(Enum.StudioStyleGuideColor.MainText) 167 | expect(OutputLabels[2].TextColor3.ColorEnum).to.equal(Enum.StudioStyleGuideColor.WarningText) 168 | expect(TestOutputView.TopBarLabel.Text).to.equal("Test 2") 169 | expect(TestOutputView.TopBarFullNameLabel.Text).to.equal("(Test N > Test 2)") 170 | 171 | --Send an output for a previous test and assert it wasn't added. 172 | Test1:OutputMessage(Enum.MessageType.MessageError, "Fail") 173 | expect(TestOutputView.OutputLines).to.deepEqual({{Message = "String 4", Type = Enum.MessageType.MessageOutput}, {Message = "String 5", Type = Enum.MessageType.MessageWarning}}) 174 | expect(OutputLabels[1].Text).to.equal("String 4") 175 | expect(OutputLabels[2].Text).to.equal("String 5") 176 | expect(OutputLabels[3].Text).to.equal("") 177 | expect(OutputLabels[1].TextColor3.ColorEnum).to.equal(Enum.StudioStyleGuideColor.MainText) 178 | expect(OutputLabels[2].TextColor3.ColorEnum).to.equal(Enum.StudioStyleGuideColor.WarningText) 179 | 180 | --Set a test with no existing output and assert the messages are displayed. 181 | TestOutputView:SetTest(Test3) 182 | expect(TestOutputView.OutputLines).to.deepEqual({}) 183 | expect(OutputLabels[1].Text).to.equal("No Output") 184 | expect(OutputLabels[1].Font).to.equal(Enum.Font.SourceSansItalic) 185 | expect(OutputLabels[2].Text).to.equal("") 186 | expect(OutputLabels[3].Text).to.equal("") 187 | expect(TestOutputView.TopBarLabel.Text).to.equal("Test 3") 188 | expect(TestOutputView.TopBarFullNameLabel.Text).to.equal("") 189 | end) 190 | end) 191 | end -------------------------------------------------------------------------------- /src/UI/Window/TestListView.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | TheNexusAvenger 3 | 4 | Frame for the list of tests and actions. 5 | --]] 6 | --!strict 7 | 8 | local SERVICES_WITH_TESTS = { 9 | game:GetService("Workspace"), 10 | game:GetService("Lighting"), 11 | game:GetService("ReplicatedFirst"), 12 | game:GetService("ReplicatedStorage"), 13 | game:GetService("ServerScriptService"), 14 | game:GetService("ServerStorage"), 15 | game:GetService("StarterGui"), 16 | game:GetService("StarterPack"), 17 | game:GetService("StarterPlayer"), 18 | game:GetService("Teams"), 19 | game:GetService("SoundService"), 20 | game:GetService("Chat"), 21 | game:GetService("LocalizationService"), 22 | game:GetService("TestService"), 23 | } 24 | 25 | 26 | 27 | local TextService = game:GetService("TextService") 28 | 29 | local NexusUnitTestingPlugin = script.Parent.Parent.Parent 30 | local ButtonSideBar = require(NexusUnitTestingPlugin:WaitForChild("UI"):WaitForChild("Bar"):WaitForChild("ButtonSideBar")) 31 | local TestProgressBar = require(NexusUnitTestingPlugin:WaitForChild("UI"):WaitForChild("Bar"):WaitForChild("TestProgressBar")) 32 | local TestListFrame = require(NexusUnitTestingPlugin:WaitForChild("UI"):WaitForChild("List"):WaitForChild("TestListFrame")) 33 | local NexusUnitTesting = require(NexusUnitTestingPlugin:WaitForChild("NexusUnitTestingModule")) 34 | local TestFinder = require(NexusUnitTestingPlugin:WaitForChild("NexusUnitTestingModule"):WaitForChild("Runtime"):WaitForChild("TestFinder")) 35 | local UnitTest = require(NexusUnitTestingPlugin:WaitForChild("NexusUnitTestingModule"):WaitForChild("UnitTest"):WaitForChild("UnitTest")) 36 | local ModuleUnitTest = require(NexusUnitTestingPlugin:WaitForChild("NexusUnitTestingModule"):WaitForChild("Runtime"):WaitForChild("ModuleUnitTest")) 37 | local NexusEvent = require(NexusUnitTestingPlugin:WaitForChild("NexusPluginComponents"):WaitForChild("NexusInstance"):WaitForChild("Event"):WaitForChild("NexusEvent")) 38 | local NexusPluginComponents = require(NexusUnitTestingPlugin:WaitForChild("NexusPluginComponents")) 39 | local PluginInstance = require(NexusUnitTestingPlugin:WaitForChild("NexusPluginComponents"):WaitForChild("Base"):WaitForChild("PluginInstance")) 40 | 41 | local TestListView = PluginInstance:Extend() 42 | TestListView:SetClassName("TestListView") 43 | 44 | export type TestListView = { 45 | new: () -> (TestListView), 46 | Extend: (self: TestListView) -> (TestListView), 47 | 48 | TestOutputOpened: NexusEvent.NexusEvent, 49 | } & PluginInstance.PluginInstance & Frame 50 | 51 | 52 | 53 | --[[ 54 | Creates a Test List Frame object. 55 | --]] 56 | function TestListView:__new(): () 57 | PluginInstance.__new(self, "Frame") 58 | 59 | --Create the state container for the tests. 60 | self:DisableChangeReplication("Tests") 61 | self.Tests = NexusPluginComponents.new("SelectionList") 62 | self:DisableChangeReplication("ModuleScriptsToEntry") 63 | self.ModuleScriptsToEntry = {} 64 | self:DisableChangeReplication("TestEvents") 65 | self.TestEvents = {} 66 | self:DisableChangeReplication("SelectedTestsNames") 67 | self.SelectedTestsNames = {} 68 | self:DisableChangeReplication("CurrentOutputTest") 69 | 70 | --Create the events. 71 | self:DisableChangeReplication("TestOutputOpened") 72 | self.TestOutputOpened = NexusEvent.new() 73 | 74 | --Create the bars. 75 | local SideBar = ButtonSideBar.new() 76 | SideBar.Parent = self:GetWrappedInstance() 77 | self:DisableChangeReplication("ButtonSideBar") 78 | self.ButtonSideBar = SideBar 79 | 80 | local BottomBar = TestProgressBar.new() 81 | BottomBar.Size = UDim2.new(1, -29, 0, 28) 82 | BottomBar.Position = UDim2.new(0, 29, 1, -28) 83 | BottomBar.Parent = self:GetWrappedInstance() 84 | self:DisableChangeReplication("TestProgressBar") 85 | self.TestProgressBar = BottomBar 86 | 87 | --Create the text for no tests. 88 | local NoTestsLabel = NexusPluginComponents.new("TextLabel") 89 | NoTestsLabel.Size = UDim2.new(1, -32, 0, 16) 90 | NoTestsLabel.Position = UDim2.new(0, 31, 0, 2) 91 | NoTestsLabel.Visible = false 92 | NoTestsLabel.Font = Enum.Font.SourceSansItalic 93 | NoTestsLabel.Text = "No tests found. Make sure test ModuleScripts end in .spec or .nexusspec" 94 | NoTestsLabel.Parent = self 95 | self:DisableChangeReplication("NoTestsLabel") 96 | self.NoTestsLabel = NoTestsLabel 97 | 98 | --Creating the scrolling frame. 99 | local ScrollingFrame = NexusPluginComponents.new("ScrollingFrame") 100 | ScrollingFrame.Size = UDim2.new(1, -29, 1, -29) 101 | ScrollingFrame.Position = UDim2.new(0, 29, 0, 0) 102 | ScrollingFrame.BackgroundTransparency = 1 103 | ScrollingFrame.Parent = self 104 | self:DisableChangeReplication("ScrollingFrame") 105 | self.ScrollingFrame = ScrollingFrame 106 | 107 | local ElementList = NexusPluginComponents.new("ElementList", function() 108 | --Create the frame. 109 | local Frame = TestListFrame.new() 110 | Frame.SelectionList = self.Tests 111 | Frame.TestListView = self 112 | 113 | --Connect the double click. 114 | Frame.DoubleClicked:Connect(function() 115 | local Entry = Frame.SelectionListEntry 116 | if not Entry or not Entry.Test then return end 117 | self.TestOutputOpened:Fire(Entry.Test) 118 | self.CurrentOutputTest = Entry.Test.FullName 119 | end) 120 | 121 | --Return the frame. 122 | return Frame 123 | end) 124 | ElementList.EntryHeight = 20 125 | ElementList:ConnectScrollingFrame(ScrollingFrame) 126 | self:DisableChangeReplication("ElementList") 127 | self.ElementList = ElementList 128 | 129 | --Connect the events. 130 | local DB = true 131 | SideBar.RunTestsButton.MouseButton1Down:Connect(function() 132 | if DB then 133 | DB = false 134 | delay(0.1, function() DB = true end) 135 | self:RunAllTests() 136 | end 137 | end) 138 | SideBar.RunFailedTestsButton.MouseButton1Down:Connect(function() 139 | if DB then 140 | DB = false 141 | delay(0.1, function() DB = true end) 142 | self:RunFailedTests() 143 | end 144 | end) 145 | SideBar.RunSelectedTestsButton.MouseButton1Down:Connect(function() 146 | if DB then 147 | DB = false 148 | delay(0.1, function() DB = true end) 149 | self:RunSelectedTests() 150 | end 151 | end) 152 | 153 | --Set the defaults. 154 | self.Size = UDim2.new(1, 0, 1, 0) 155 | end 156 | 157 | --[[ 158 | Updates the view of tests. 159 | --]] 160 | function TestListView:TestsUpdated(): () 161 | --Set the element list. 162 | local Tests = self.Tests:GetDescendants() 163 | self.ElementList:SetEntries(Tests) 164 | 165 | --Update the max width. 166 | local MaxWidth = 100 167 | for _, Entry in Tests do 168 | MaxWidth = math.max(MaxWidth, (20 * (Entry.Indent - 1)) + Entry.EntryWidth) 169 | end 170 | self.ElementList.CurrentWidth = MaxWidth 171 | end 172 | 173 | --[[ 174 | Runs all of the detected tests. 175 | --]] 176 | function TestListView:RunAllTests(): () 177 | --Find the tests to run. 178 | local Tests = {} 179 | local Modules = {} 180 | for _, Service in SERVICES_WITH_TESTS do 181 | for _, Test in TestFinder.GetTests(Service) do 182 | table.insert(Tests, Test) 183 | Modules[Test.ModuleScript] = true 184 | end 185 | end 186 | 187 | --Remove the non-existent tests. 188 | local EntriesToRemove = {} 189 | for _, Entry in self.Tests.Children do 190 | local Test = Entry.Test 191 | if not Modules[Test.ModuleScript] then 192 | table.insert(EntriesToRemove, Entry) 193 | end 194 | end 195 | self:RemvoeEntries(EntriesToRemove) 196 | 197 | --Run the tests. 198 | self:RunTests(Tests) 199 | end 200 | 201 | --[[ 202 | Reruns the failed test. Runs all of the 203 | tests if no test run was done. 204 | --]] 205 | function TestListView:RunFailedTests() 206 | --Run all the tests if nothing was run. 207 | if #self.Tests.Children == 0 then 208 | self:RunAllTests() 209 | return 210 | end 211 | 212 | --[[ 213 | Return if a test contains a failed test. 214 | --]] 215 | local function ContainsFailedTest(Test: UnitTest.UnitTest): boolean 216 | --Return true if the test failed. 217 | if Test.State == "FAILED" or Test.CombinedState == "FAILED" then 218 | return true 219 | end 220 | 221 | --Return if a subtest has a failure. 222 | for _, SubTest in Test.SubTests :: {UnitTest.UnitTest} do 223 | if ContainsFailedTest(SubTest :: any) then 224 | return true 225 | end 226 | end 227 | 228 | --Return false (no failure). 229 | return false 230 | end 231 | 232 | --Determine the ModuleScripts to rerun. 233 | local TestsToRerun = {} 234 | local EntriesToRemove = {} 235 | for _, Entry in self.Tests.Children do 236 | local Test = Entry.Test 237 | if ContainsFailedTest(Test) then 238 | local ModuleScript = Test.ModuleScript 239 | if ModuleScript:IsDescendantOf(game) then 240 | table.insert(TestsToRerun, ModuleUnitTest.new(ModuleScript)) 241 | else 242 | table.insert(EntriesToRemove, Entry) 243 | end 244 | end 245 | end 246 | 247 | --Remove the non-existent tests. 248 | self:RemvoeEntries(EntriesToRemove) 249 | 250 | --Run the tests. 251 | self:RunTests(TestsToRerun) 252 | end 253 | 254 | --[[ 255 | Reruns the selected test. Runs all of the 256 | tests if tests were selected. 257 | --]] 258 | function TestListView:RunSelectedTests(): () 259 | --[[ 260 | Returns if an entry is selected or a child is. 261 | --]] 262 | local function EntryIsSelected(Entry: TestListFrame.TestListFrame): boolean 263 | --If the label is selected, return true. 264 | if Entry.Selected then 265 | return true 266 | end 267 | 268 | --Return true if a subtest is selected. 269 | for _, SubEntry in Entry.Children do 270 | if EntryIsSelected(SubEntry) then 271 | return true 272 | end 273 | end 274 | 275 | --Return false (list frame and children aren't selected). 276 | return false 277 | end 278 | 279 | --Determine the tests to rerun and the frames to remove. 280 | local EntriesToRemove = {} 281 | local TestsToRerun = {} 282 | for _, Entry in self.Tests.Children do 283 | --Add the test to be removed if the module was removed. 284 | if EntryIsSelected(Entry) then 285 | local ModuleScript = Entry.Test.ModuleScript 286 | if ModuleScript:IsDescendantOf(game) then 287 | table.insert(TestsToRerun, ModuleUnitTest.new(ModuleScript)) 288 | else 289 | table.insert(EntriesToRemove, Entry) 290 | end 291 | end 292 | end 293 | 294 | --Rerun all tests if none are selected. 295 | if #TestsToRerun == 0 then 296 | self:RunAllTests() 297 | return 298 | end 299 | 300 | --Remove the non-existent tests. 301 | self:RemvoeEntries(EntriesToRemove) 302 | 303 | --Run the tests. 304 | self:RunTests(TestsToRerun) 305 | end 306 | 307 | --[[ 308 | Removes a list of test entries. 309 | --]] 310 | function TestListView:RemvoeEntries(Entries: {TestListFrame.TestListFrame}): () 311 | for _, Entry in Entries do 312 | self.TestProgressBar:RemoveUnitTest(Entry.Test, true) 313 | self.Tests:RemoveChild(Entry) 314 | self.ModuleScriptsToEntry[Entry.Test.ModuleScript] = nil 315 | for _, Event in self.TestEvents[Entry.Test] do 316 | Event:Disconnect() 317 | end 318 | self.TestEvents[Entry.Test] = nil 319 | end 320 | end 321 | 322 | --[[ 323 | Connects the events of a test. 324 | --]] 325 | function TestListView:ConnectTest(Test: UnitTest.UnitTest, Entry: TestListFrame.TestListFrame, RootTest: UnitTest.UnitTest, BaseFullName: string?): () 326 | BaseFullName = BaseFullName or "" 327 | 328 | --Set up the event storage. 329 | if not self.TestEvents[RootTest] then 330 | self.TestEvents[RootTest] = {} 331 | end 332 | local Events = self.TestEvents[RootTest] 333 | 334 | --Set the full name. 335 | local FullName = (BaseFullName :: string)..Test.Name 336 | Test.FullName = FullName 337 | Entry.DurationPosition = 22 + 4 + TextService:GetTextSize(Test.Name, 14, Enum.Font.SourceSans, Vector2.new(2000, 16)).X 338 | Entry.EntryWidth = Entry.DurationPosition 339 | 340 | --Select the list frame if it was selected before. 341 | if self.SelectedTestsNames[FullName] then 342 | Entry.Selected = true 343 | end 344 | 345 | --Connect changing the test state. 346 | local TestStartTime = 0 347 | table.insert(Events, Test:GetPropertyChangedSignal("CombinedState"):Connect(function() 348 | if Test.CombinedState == NexusUnitTesting.TestState.InProgress then 349 | TestStartTime = tick() 350 | elseif Test.CombinedState ~= NexusUnitTesting.TestState.NotRun then 351 | if TestStartTime ~= 0 then 352 | Entry.Duration = tick() - TestStartTime 353 | Entry.EntryWidth = Entry.EntryWidth + TextService:GetTextSize(string.format("%.3f", Entry.Duration).." seconds" or "", 14, Enum.Font.SourceSans, Vector2.new(2000, 16)).X + 4 354 | end 355 | end 356 | self:TestsUpdated() 357 | end)) 358 | 359 | --Connect the test outputting. 360 | if #Test.Output > 0 then 361 | Entry.HasOutput = true 362 | else 363 | Entry.HasOutput = false 364 | local MessageOutputtedEvent 365 | MessageOutputtedEvent = Test.MessageOutputted:Connect(function() 366 | Entry.HasOutput = true 367 | if MessageOutputtedEvent then 368 | MessageOutputtedEvent:Disconnect() 369 | MessageOutputtedEvent = nil :: any 370 | end 371 | end) 372 | table.insert(Events, MessageOutputtedEvent) 373 | self:TestsUpdated() 374 | end 375 | 376 | --Connect tests being added. 377 | Test.TestAdded:Connect(function(NewTest) 378 | local NewEntry = Entry:CreateChild() 379 | NewEntry.Test = NewTest 380 | self:ConnectTest(NewTest, NewEntry, RootTest, FullName.." > ") 381 | end) 382 | 383 | --Add the existing subtests. 384 | for _, NewTest in Test.SubTests :: {UnitTest.UnitTest} do 385 | local NewEntry = Entry:CreateChild() 386 | NewEntry.Test = NewTest 387 | self:ConnectTest(NewTest, NewEntry, RootTest, FullName.." > ") 388 | end 389 | 390 | --Open the output window if the test name matches (test is rerunning). 391 | if self.CurrentOutputTest == FullName then 392 | self.TestOutputOpened:Fire(Test, true) 393 | end 394 | end 395 | 396 | --[[ 397 | Runs a list of tests. 398 | --]] 399 | function TestListView:RunTests(Tests: {UnitTest.UnitTest}): () 400 | --Set the test time. 401 | self.TestProgressBar:SetTime() 402 | 403 | --Sort the tests. 404 | table.sort(Tests, function(TestA, TestB) 405 | return (TestA.Name :: string) < (TestB.Name :: string) 406 | end) 407 | 408 | --Register the tests. 409 | for _, Test in Tests do 410 | self:RegisterTest(Test) 411 | end 412 | 413 | --Run the tests. 414 | for _, Test in Tests do 415 | Test:RunTest() 416 | end 417 | 418 | --Update the bar if there is no tests. 419 | if #Tests == 0 then 420 | self.TestProgressBar:UpdateProgressBar() 421 | self:TestsUpdated() 422 | end 423 | self.NoTestsLabel.Visible = (#self.Tests.Children == 0) 424 | end 425 | 426 | --[[ 427 | Registers a ModuleScript unit test. 428 | --]] 429 | function TestListView:RegisterTest(ModuleScriptTest: ModuleUnitTest.ModuleUnitTest): () 430 | --Remove the existing entry if it exists. 431 | if self.ModuleScriptsToEntry[ModuleScriptTest.ModuleScript] then 432 | local Entry = self.ModuleScriptsToEntry[ModuleScriptTest.ModuleScript] 433 | self.Tests:RemoveChild(Entry) 434 | self.TestProgressBar:RemoveUnitTest(Entry.Test, true) 435 | end 436 | 437 | --Create the child entry. 438 | local Entry = self.Tests:CreateChild() 439 | Entry.Test = ModuleScriptTest 440 | self.ModuleScriptsToEntry[ModuleScriptTest.ModuleScript] = Entry 441 | self.TestProgressBar:AddUnitTest(ModuleScriptTest) 442 | 443 | --Sort the entries. 444 | table.sort(self.Tests.Children, function(EntryA, entryB) 445 | return EntryA.Test.Name < entryB.Test.Name 446 | end) 447 | 448 | --Connect the child. 449 | self:ConnectTest(ModuleScriptTest, Entry, ModuleScriptTest) 450 | self:TestsUpdated() 451 | end 452 | 453 | 454 | 455 | return (TestListView :: TestListView) :: any --------------------------------------------------------------------------------