├── .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
--------------------------------------------------------------------------------