From 536fe99a506918c17a3680ce85ec4f98b7715c7b Mon Sep 17 00:00:00 2001 From: vocksel Date: Sun, 21 Apr 2024 20:49:15 -0700 Subject: [PATCH] Use Jest for running unit tests (#242) This PR ports us over to Jest! Closes #241 --- .gitignore | 4 +- dev.project.json | 5 +- justfile | 29 ++- selene.toml | 2 +- src/Common/mapRanges.spec.luau | 30 +-- src/Common/useDescendants.spec.luau | 171 +++++++++-------- src/Common/useEvent.spec.luau | 85 +++++---- src/Common/usePrevious.spec.luau | 104 +++++----- src/Common/useZoom.spec.luau | 179 +++++++++--------- .../filterComponentTreeNode.spec.luau | 127 +++++++------ src/Explorer/getTreeDescendants.spec.luau | 70 ++++--- src/Storybook/createStoryNodes.spec.luau | 96 +++++----- src/Storybook/isStoryModule.spec.luau | 38 ++-- src/Storybook/isStorybookModule.spec.luau | 66 ++++--- src/Testing/newFolder.spec.luau | 68 +++---- src/jest.config.luau | 4 + src/stories.spec.luau | 39 ++-- testez.d.luau | 24 --- testez.toml | 78 -------- tests.project.json | 18 +- tests/ClientAppSettings.json | 3 + tests/init.server.luau | 27 --- tests/run-tests.luau | 34 ++++ wally.toml | 3 +- 24 files changed, 636 insertions(+), 668 deletions(-) create mode 100644 src/jest.config.luau delete mode 100644 testez.d.luau delete mode 100644 testez.toml create mode 100644 tests/ClientAppSettings.json delete mode 100644 tests/init.server.luau create mode 100644 tests/run-tests.luau diff --git a/.gitignore b/.gitignore index 636458fd..90b27505 100644 --- a/.gitignore +++ b/.gitignore @@ -3,10 +3,8 @@ /*.rbxm* # Luau -sourcemap.json +sourcemap*.json -# Selene -/roblox.toml # Wally /DevPackages diff --git a/dev.project.json b/dev.project.json index 446d3be9..7469938f 100644 --- a/dev.project.json +++ b/dev.project.json @@ -1,12 +1,9 @@ { "name": "flipbook", "tree": { - "Packages": { - "$path": "Packages" - }, "Example": { "$path": "example" }, - "$path": "src" + "$path": "default.project.json" } } \ No newline at end of file diff --git a/justfile b/justfile index e8edec8c..3be1a978 100644 --- a/justfile +++ b/justfile @@ -15,7 +15,6 @@ tests_project := "tests.project.json" tmpdir := `mktemp -d` global_defs_path := tmpdir / "globalTypes.d.lua" -testez_defs_path := "testez.d.luau" sourcemap_path := tmpdir / "sourcemap.json" _lint-file-extensions: @@ -28,13 +27,27 @@ _lint-file-extensions: exit 1 fi +_get-client-settings: + #!/usr/bin/env bash + set -euxo pipefail + + os={{ os_family() }} + + if [[ "$os" == "macos" ]]; then + echo "/Applications/RobloxStudio.app/Contents/MacOS/ClientSettings" + elif [[ "$os" == "windows" ]]; then + robloxStudioPath=$(find "$LOCALAPPDATA/Roblox/Versions" -name "RobloxStudioBeta.exe") + dir=$(dirname $robloxStudioPath) + echo "$dir/ClientSettings" + fi + default: @just --list wally-install: wally install rojo sourcemap {{ tests_project }} -o {{ sourcemap_path }} - wally-package-types --sourcemap {{ sourcemap_path }} {{ packages_dir }} + wally-package-types --sourcemap {{ sourcemap_path }} {{ absolute_path(packages_dir) }} init: foreman install @@ -52,9 +65,18 @@ build-watch: npx -y chokidar-cli "{{ project_dir }}/**/*" --initial \ -c "just build" \ +set-flags: + #!/usr/bin/env bash + set -euxo pipefail + + clientSettings=$(just _get-client-settings) + mkdir -p "$clientSettings" + cp -R tests/ClientAppSettings.json "$clientSettings" + test: + just set-flags rojo build {{ tests_project }} -o test-place.rbxl - run-in-roblox --place test-place.rbxl --script tests/init.server.lua + run-in-roblox --place test-place.rbxl --script tests/run-tests.luau analyze: curl -s -o {{ global_defs_path }} \ @@ -64,7 +86,6 @@ analyze: luau-lsp analyze --sourcemap={{ sourcemap_path }} \ --defs={{ global_defs_path }} \ - --defs={{ testez_defs_path }} \ --settings="./.vscode/settings.json" \ --ignore=**/_Index/** \ {{ project_dir }} diff --git a/selene.toml b/selene.toml index 5bd218d4..54227d9c 100644 --- a/selene.toml +++ b/selene.toml @@ -1,4 +1,4 @@ -std = "roblox+testez" +std = "roblox" [lints] global_usage = "allow" diff --git a/src/Common/mapRanges.spec.luau b/src/Common/mapRanges.spec.luau index 06fa5828..1fb3b257 100644 --- a/src/Common/mapRanges.spec.luau +++ b/src/Common/mapRanges.spec.luau @@ -1,13 +1,17 @@ -return function() - local mapRanges = require(script.Parent.mapRanges) - - it("should return 1.5 if we remap 0.5 from a 0 -> 1 range to a 1 -> 2 range", function() - expect(mapRanges(0.5, 0, 1, 1, 2)).to.equal(1.5) - end) - - it("should error if max0 is the same as min0", function() - expect(function() - mapRanges(0.5, 1, 1, 2, 2) - end).to.throw() - end) -end +local flipbook = script:FindFirstAncestor("flipbook") + +local JestGlobals = require(flipbook.Packages.JestGlobals) +local mapRanges = require(script.Parent.mapRanges) + +local expect = JestGlobals.expect +local test = JestGlobals.test + +test("return 1.5 if we remap 0.5 from a 0 -> 1 range to a 1 -> 2 range", function() + expect(mapRanges(0.5, 0, 1, 1, 2)).toBe(1.5) +end) + +test("error if max0 is the same as min0", function() + expect(function() + mapRanges(0.5, 1, 1, 2, 2) + end).toThrow() +end) diff --git a/src/Common/useDescendants.spec.luau b/src/Common/useDescendants.spec.luau index 7f23e026..b763387f 100644 --- a/src/Common/useDescendants.spec.luau +++ b/src/Common/useDescendants.spec.luau @@ -1,107 +1,110 @@ local flipbook = script:FindFirstAncestor("flipbook") -return function() - local React = require(flipbook.Packages.React) - local ReactRoblox = require(flipbook.Packages.ReactRoblox) - local newFolder = require(flipbook.Testing.newFolder) - local useDescendants = require(script.Parent.useDescendants) - - local container = Instance.new("ScreenGui") - local root = ReactRoblox.createRoot(container) - - afterEach(function() - ReactRoblox.act(function() - root:unmount() - end) +local JestGlobals = require(flipbook.Packages.JestGlobals) +local React = require(flipbook.Packages.React) +local ReactRoblox = require(flipbook.Packages.ReactRoblox) +local newFolder = require(flipbook.Testing.newFolder) +local useDescendants = require(script.Parent.useDescendants) + +local afterEach = JestGlobals.afterEach +local expect = JestGlobals.expect +local test = JestGlobals.test + +local container = Instance.new("ScreenGui") +local root = ReactRoblox.createRoot(container) + +afterEach(function() + ReactRoblox.act(function() + root:unmount() end) - - it("should return an initial list of descendants that match the predicate", function() - local tree = newFolder({ - Match = Instance.new("Part"), - Foo = Instance.new("Part"), - }) - - local descendants - local function HookTester() - descendants = useDescendants(tree, function(descendant) - return descendant.Name == "Match" - end) - - return nil - end - - ReactRoblox.act(function() - root:render(React.createElement(HookTester)) +end) + +test("return an initial list of descendants that match the predicate", function() + local tree = newFolder({ + Match = Instance.new("Part"), + Foo = Instance.new("Part"), + }) + + local descendants + local function HookTester() + descendants = useDescendants(tree, function(descendant) + return descendant.Name == "Match" end) - expect(descendants).to.be.ok() - expect(#descendants).to.equal(1) - expect(descendants[1]).to.equal(tree:FindFirstChild("Match")) - end) - - it("should respond to changes in descendants that match the predicate", function() - local tree = newFolder({ - Match = Instance.new("Part"), - Foo = Instance.new("Part"), - }) + return nil + end - local descendants - local function HookTester() - descendants = useDescendants(tree, function(descendant) - return descendant.Name == "Match" - end) - - return nil - end + ReactRoblox.act(function() + root:render(React.createElement(HookTester)) + end) - ReactRoblox.act(function() - root:render(React.createElement(HookTester)) + expect(descendants).toBeDefined() + expect(#descendants).toBe(1) + expect(descendants[1]).toBe(tree:FindFirstChild("Match")) +end) + +test("respond to changes in descendants that match the predicate", function() + local tree = newFolder({ + Match = Instance.new("Part"), + Foo = Instance.new("Part"), + }) + + local descendants + local function HookTester() + descendants = useDescendants(tree, function(descendant) + return descendant.Name == "Match" end) - expect(descendants).to.be.ok() - expect(#descendants).to.equal(1) + return nil + end - local folder = newFolder({ - Match = Instance.new("Part"), - }) + ReactRoblox.act(function() + root:render(React.createElement(HookTester)) + end) - ReactRoblox.act(function() - folder.Parent = tree - end) + expect(descendants).toBeDefined() + expect(#descendants).toBe(1) - expect(#descendants).to.equal(2) - end) + local folder = newFolder({ + Match = Instance.new("Part"), + }) - it("should force an update when a matching descendant's name changes", function() - local descendants + ReactRoblox.act(function() + folder.Parent = tree + end) - local tree = newFolder({ - Match = Instance.new("Part"), - }) + expect(#descendants).toBe(2) +end) - local function HookTester() - descendants = useDescendants(tree, function(descendant) - return descendant:IsA("Part") - end) +test("force an update when a matching descendant's name changes", function() + local descendants - return nil - end + local tree = newFolder({ + Match = Instance.new("Part"), + }) - ReactRoblox.act(function() - root:render(React.createElement(HookTester)) + local function HookTester() + descendants = useDescendants(tree, function(descendant) + return descendant:IsA("Part") end) - expect(descendants).to.be.ok() - expect(#descendants).to.equal(1) + return nil + end - local prev = descendants - local match = tree:FindFirstChild("Match") + ReactRoblox.act(function() + root:render(React.createElement(HookTester)) + end) - ReactRoblox.act(function() - match.Name = "Changed" - end) + expect(descendants).toBeDefined() + expect(#descendants).toBe(1) - expect(descendants).never.to.equal(prev) - expect(descendants[1]).to.equal(match) + local prev = descendants + local match = tree:FindFirstChild("Match") + + ReactRoblox.act(function() + match.Name = "Changed" end) -end + + expect(descendants).never.toBe(prev) + expect(descendants[1]).toBe(match) +end) diff --git a/src/Common/useEvent.spec.luau b/src/Common/useEvent.spec.luau index 0a8bff23..6b3830a4 100644 --- a/src/Common/useEvent.spec.luau +++ b/src/Common/useEvent.spec.luau @@ -1,61 +1,64 @@ local flipbook = script:FindFirstAncestor("flipbook") -return function() - local React = require(flipbook.Packages.React) - local ReactRoblox = require(flipbook.Packages.ReactRoblox) - local useEvent = require(script.Parent.useEvent) +local JestGlobals = require(flipbook.Packages.JestGlobals) +local React = require(flipbook.Packages.React) +local ReactRoblox = require(flipbook.Packages.ReactRoblox) +local useEvent = require(script.Parent.useEvent) - local container = Instance.new("ScreenGui") - local root = ReactRoblox.createRoot(container) +local afterEach = JestGlobals.afterEach +local expect = JestGlobals.expect +local test = JestGlobals.test - local bindable = Instance.new("BindableEvent") - local wasFired = false +local container = Instance.new("ScreenGui") +local root = ReactRoblox.createRoot(container) - local function HookTester() - useEvent(bindable.Event, function() - wasFired = true - end) +local bindable = Instance.new("BindableEvent") +local wasFired = false - return nil - end - - afterEach(function() - wasFired = false - - ReactRoblox.act(function() - root:unmount() - end) +local function HookTester() + useEvent(bindable.Event, function() + wasFired = true end) - it("it should listen to the given event", function() - local element = React.createElement(HookTester) + return nil +end - ReactRoblox.act(function() - root:render(element) - end) +afterEach(function() + wasFired = false - expect(wasFired).to.equal(false) + ReactRoblox.act(function() + root:unmount() + end) +end) - bindable:Fire() +test("listen to the given event", function() + local element = React.createElement(HookTester) - expect(wasFired).to.equal(true) + ReactRoblox.act(function() + root:render(element) end) - it("should never fire when unmounted", function() - local element = React.createElement(HookTester) + expect(wasFired).toBe(false) - ReactRoblox.act(function() - root:render(element) - end) + bindable:Fire() - expect(wasFired).to.equal(false) + expect(wasFired).toBe(true) +end) - ReactRoblox.act(function() - root:unmount() - end) +test("never fire when unmounted", function() + local element = React.createElement(HookTester) - bindable:Fire() + ReactRoblox.act(function() + root:render(element) + end) - expect(wasFired).to.equal(false) + expect(wasFired).toBe(false) + + ReactRoblox.act(function() + root:unmount() end) -end + + bindable:Fire() + + expect(wasFired).toBe(false) +end) diff --git a/src/Common/usePrevious.spec.luau b/src/Common/usePrevious.spec.luau index 42e4ba02..ff42d865 100644 --- a/src/Common/usePrevious.spec.luau +++ b/src/Common/usePrevious.spec.luau @@ -1,64 +1,68 @@ local flipbook = script:FindFirstAncestor("flipbook") -return function() - local React = require(flipbook.Packages.React) - local ReactRoblox = require(flipbook.Packages.ReactRoblox) - local useEvent = require(flipbook.Common.useEvent) - local usePrevious = require(script.Parent.usePrevious) - - local container = Instance.new("ScreenGui") - local root = ReactRoblox.createRoot(container) - - local toggleState = Instance.new("BindableEvent") - - local function HookTester() - local state, setState = React.useState(false) - local prev = usePrevious(state) - - useEvent(toggleState.Event, function() - setState(not state) - end) - - return React.createElement("TextLabel", { - Text = tostring(prev), - }) - end - - afterEach(function() - ReactRoblox.act(function() - root:unmount() - end) +local JestGlobals = require(flipbook.Packages.JestGlobals) + +local React = require(flipbook.Packages.React) +local ReactRoblox = require(flipbook.Packages.ReactRoblox) +local useEvent = require(flipbook.Common.useEvent) +local usePrevious = require(script.Parent.usePrevious) + +local expect = JestGlobals.expect +local test = JestGlobals.test +local afterEach = JestGlobals.afterEach + +local container = Instance.new("ScreenGui") +local root = ReactRoblox.createRoot(container) + +local toggleState = Instance.new("BindableEvent") + +local function HookTester() + local state, setState = React.useState(false) + local prev = usePrevious(state) + + useEvent(toggleState.Event, function() + setState(not state) end) - it("should use the last value", function() - local element = React.createElement(HookTester) + return React.createElement("TextLabel", { + Text = tostring(prev), + }) +end - ReactRoblox.act(function() - root:render(element) - end) +afterEach(function() + ReactRoblox.act(function() + root:unmount() + end) +end) - local result = container:FindFirstChildWhichIsA("TextLabel") :: TextLabel +test("use the last value", function() + local element = React.createElement(HookTester) - expect(result.Text).to.equal("nil") + ReactRoblox.act(function() + root:render(element) + end) - ReactRoblox.act(function() - toggleState:Fire() - end) + local result = container:FindFirstChildWhichIsA("TextLabel") :: TextLabel - ReactRoblox.act(function() - task.wait() - end) + expect(result.Text).toBe("nil") - expect(result.Text).to.equal("false") + ReactRoblox.act(function() + toggleState:Fire() + end) - ReactRoblox.act(function() - toggleState:Fire() - end) + ReactRoblox.act(function() + task.wait() + end) - ReactRoblox.act(function() - task.wait() - end) + expect(result.Text).toBe("false") - expect(result.Text).to.equal("true") + ReactRoblox.act(function() + toggleState:Fire() end) -end + + ReactRoblox.act(function() + task.wait() + end) + + expect(result.Text).toBe("true") +end) diff --git a/src/Common/useZoom.spec.luau b/src/Common/useZoom.spec.luau index 8490f063..38b988be 100644 --- a/src/Common/useZoom.spec.luau +++ b/src/Common/useZoom.spec.luau @@ -1,117 +1,120 @@ local flipbook = script:FindFirstAncestor("flipbook") -return function() - local React = require(flipbook.Packages.React) - local ReactRoblox = require(flipbook.Packages.ReactRoblox) - local useEvent = require(flipbook.Common.useEvent) - local useZoom = require(script.Parent.useZoom) - - local container = Instance.new("ScreenGui") - local root = ReactRoblox.createRoot(container) - - local story = Instance.new("ModuleScript") - local zoomIn = Instance.new("BindableEvent") - local zoomOut = Instance.new("BindableEvent") - - local function HookTester(props: { story: ModuleScript }) - local zoom = useZoom(props.story) - - useEvent(zoomIn.Event, function() - zoom.zoomIn() - end) - - useEvent(zoomOut.Event, function() - zoom.zoomOut() - end) - - return React.createElement("TextLabel", { - Text = zoom.value, - }) - end - - afterEach(function() - ReactRoblox.act(function() - root:unmount() - end) - end) +local JestGlobals = require(flipbook.Packages.JestGlobals) +local React = require(flipbook.Packages.React) +local ReactRoblox = require(flipbook.Packages.ReactRoblox) +local useEvent = require(flipbook.Common.useEvent) +local useZoom = require(script.Parent.useZoom) + +local expect = JestGlobals.expect +local test = JestGlobals.test +local afterEach = JestGlobals.afterEach - it("should be set to 0 zoom by default", function() - local element = React.createElement(HookTester, { - story = story, - }) +local container = Instance.new("ScreenGui") +local root = ReactRoblox.createRoot(container) - ReactRoblox.act(function() - root:render(element) - end) +local story = Instance.new("ModuleScript") +local zoomIn = Instance.new("BindableEvent") +local zoomOut = Instance.new("BindableEvent") - local result = container:FindFirstChildWhichIsA("TextLabel") :: TextLabel +local function HookTester(props: { story: ModuleScript }) + local zoom = useZoom(props.story) - expect(tonumber(result.Text)).to.equal(0) + useEvent(zoomIn.Event, function() + zoom.zoomIn() end) - it("should zoom in", function() - local element = React.createElement(HookTester, { - story = story, - }) + useEvent(zoomOut.Event, function() + zoom.zoomOut() + end) + + return React.createElement("TextLabel", { + Text = zoom.value, + }) +end + +afterEach(function() + ReactRoblox.act(function() + root:unmount() + end) +end) + +test("be set to 0 zoom by default", function() + local element = React.createElement(HookTester, { + story = story, + }) + + ReactRoblox.act(function() + root:render(element) + end) - ReactRoblox.act(function() - root:render(element) - end) + local result = container:FindFirstChildWhichIsA("TextLabel") :: TextLabel - local result = container:FindFirstChildWhichIsA("TextLabel") :: TextLabel + expect(tonumber(result.Text)).toBe(0) +end) - ReactRoblox.act(function() - zoomIn:Fire() - end) +test("zoom in", function() + local element = React.createElement(HookTester, { + story = story, + }) - expect(tonumber(result.Text)).to.equal(0.25) + ReactRoblox.act(function() + root:render(element) end) - it("should zoom out", function() - local element = React.createElement(HookTester, { - story = story, - }) + local result = container:FindFirstChildWhichIsA("TextLabel") :: TextLabel - ReactRoblox.act(function() - root:render(element) - end) + ReactRoblox.act(function() + zoomIn:Fire() + end) - local result = container:FindFirstChildWhichIsA("TextLabel") :: TextLabel + expect(tonumber(result.Text)).toBe(0.25) +end) - ReactRoblox.act(function() - zoomOut:Fire() - end) +test("zoom out", function() + local element = React.createElement(HookTester, { + story = story, + }) - expect(tonumber(result.Text)).to.equal(-0.25) + ReactRoblox.act(function() + root:render(element) end) - it("should reset the zoom any time the story changes", function() - local element = React.createElement(HookTester, { - story = story, - }) + local result = container:FindFirstChildWhichIsA("TextLabel") :: TextLabel - ReactRoblox.act(function() - root:render(element) - end) + ReactRoblox.act(function() + zoomOut:Fire() + end) - local result = container:FindFirstChildWhichIsA("TextLabel") :: TextLabel + expect(tonumber(result.Text)).toBe(-0.25) +end) - ReactRoblox.act(function() - zoomIn:Fire() - end) +test("reset the zoom any time the story changes", function() + local element = React.createElement(HookTester, { + story = story, + }) - expect(tonumber(result.Text)).to.equal(0.25) + ReactRoblox.act(function() + root:render(element) + end) - element = React.createElement(HookTester, { - story = Instance.new("ModuleScript"), - }) + local result = container:FindFirstChildWhichIsA("TextLabel") :: TextLabel - ReactRoblox.act(function() - root:render(element) - end) + ReactRoblox.act(function() + zoomIn:Fire() + end) - result = container:FindFirstChildWhichIsA("TextLabel") :: TextLabel + expect(tonumber(result.Text)).toBe(0.25) - expect(tonumber(result.Text)).to.equal(0) + element = React.createElement(HookTester, { + story = Instance.new("ModuleScript"), + }) + + ReactRoblox.act(function() + root:render(element) end) -end + + result = container:FindFirstChildWhichIsA("TextLabel") :: TextLabel + + expect(tonumber(result.Text)).toBe(0) +end) diff --git a/src/Explorer/filterComponentTreeNode.spec.luau b/src/Explorer/filterComponentTreeNode.spec.luau index d8edd6e2..f4f86e03 100644 --- a/src/Explorer/filterComponentTreeNode.spec.luau +++ b/src/Explorer/filterComponentTreeNode.spec.luau @@ -1,77 +1,78 @@ local flipbook = script:FindFirstAncestor("flipbook") +local JestGlobals = require(flipbook.Packages.JestGlobals) +local filterComponentTreeNode = require(script.Parent.filterComponentTreeNode) local types = require(flipbook.Explorer.types) -return function() - local filterComponentTreeNode = require(script.Parent.filterComponentTreeNode) +local expect = JestGlobals.expect +local test = JestGlobals.test - it("should return true when the query does not match the story name", function() - local target: types.ComponentTreeNode = { - children = {}, - name = "test", - icon = "story", - } - local query = "other" +test("return true when the query does not match the story name", function() + local target: types.ComponentTreeNode = { + children = {}, + name = "test", + icon = "story", + } + local query = "other" - local result = filterComponentTreeNode(target, query) - expect(result).to.equal(true) - end) + local result = filterComponentTreeNode(target, query) + expect(result).toBe(true) +end) - it("should return false the query matches the story name", function() - local target: types.ComponentTreeNode = { - children = {}, - name = "test", - icon = "story", - } - local query = "tes" +test("return false the query matches the story name", function() + local target: types.ComponentTreeNode = { + children = {}, + name = "test", + icon = "story", + } + local query = "tes" - local result = filterComponentTreeNode(target, query) - expect(result).to.equal(false) - end) + local result = filterComponentTreeNode(target, query) + expect(result).toBe(false) +end) - it("should return true when the filter does not match any of node in tree", function() - local target: types.ComponentTreeNode = { - children = { - { - children = {}, - name = "test", - icon = "story", - }, - { - children = {}, - name = "folder", - icon = "folder", - }, +test("return true when the filter does not match any of node in tree", function() + local target: types.ComponentTreeNode = { + children = { + { + children = {}, + name = "test", + icon = "story", }, - name = "storybook", - icon = "storybook", - } - local query = "other" + { + children = {}, + name = "folder", + icon = "folder", + }, + }, + name = "storybook", + icon = "storybook", + } + local query = "other" - local result = filterComponentTreeNode(target, query) - expect(result).to.equal(true) - end) + local result = filterComponentTreeNode(target, query) + expect(result).toBe(true) +end) - it("should return false when a filter match at least one of nodes in tree", function() - local target: types.ComponentTreeNode = { - children = { - { - children = {}, - name = "test", - icon = "story", - }, - { - children = {}, - name = "folder", - icon = "folder", - }, +test("return false when a filter match at least one of nodes in tree", function() + local target: types.ComponentTreeNode = { + children = { + { + children = {}, + name = "test", + icon = "story", + }, + { + children = {}, + name = "folder", + icon = "folder", }, - name = "storybook", - icon = "storybook", - } - local query = "tes" + }, + name = "storybook", + icon = "storybook", + } + local query = "tes" - local result = filterComponentTreeNode(target, query) - expect(result).to.equal(false) - end) -end + local result = filterComponentTreeNode(target, query) + expect(result).toBe(false) +end) diff --git a/src/Explorer/getTreeDescendants.spec.luau b/src/Explorer/getTreeDescendants.spec.luau index 6cffec8c..ba714315 100644 --- a/src/Explorer/getTreeDescendants.spec.luau +++ b/src/Explorer/getTreeDescendants.spec.luau @@ -1,31 +1,39 @@ -return function() - local getTreeDescendants = require(script.Parent.getTreeDescendants) - - it("should return an empty table when the root has no children", function() - local root = { name = "root", children = {} } - - local result = getTreeDescendants(root) - expect(result).to.be.a("table") - expect(#result).to.equal(0) - end) - - it("should return a table with all descendants when the root has children", function() - local child1 = { name = "child1", children = {} } - local child2 = { name = "child2", children = {} } - local root = { name = "root", children = { child1, child2 } } - - local result = getTreeDescendants(root) - expect(result).to.be.a("table") - expect(#result).to.equal(2) - end) - - it("should return a table with all descendants when the tree has multiple levels", function() - local grandchild = { name = "grandchild", children = {} } - local child = { name = "child", children = { grandchild } } - local root = { name = "root", children = { child } } - - local result = getTreeDescendants(root) - expect(result).to.be.a("table") - expect(#result).to.equal(2) - end) -end +local flipbook = script:FindFirstAncestor("flipbook") + +local JestGlobals = require(flipbook.Packages.JestGlobals) +local getTreeDescendants = require(script.Parent.getTreeDescendants) + +local expect = JestGlobals.expect +local test = JestGlobals.test + +test("return an empty table when the root has no children", function() + local root = { name = "root", children = {} } + + local result = getTreeDescendants(root) + expect(result).toEqual({}) + expect(#result).toBe(0) +end) + +test("return a table with all descendants when the root has children", function() + local child1 = { name = "child1", children = {} } + local child2 = { name = "child2", children = {} } + local root = { name = "root", children = { child1, child2 } } + + local result = getTreeDescendants(root) + expect(result).toEqual({ + child1, + child2, + }) +end) + +test("return a table with all descendants when the tree has multiple levels", function() + local grandchild = { name = "grandchild", children = {} } + local child = { name = "child", children = { grandchild } } + local root = { name = "root", children = { child } } + + local result = getTreeDescendants(root) + expect(result).toEqual({ + child, + grandchild, + }) +end) diff --git a/src/Storybook/createStoryNodes.spec.luau b/src/Storybook/createStoryNodes.spec.luau index 279d4f71..2ac9818b 100644 --- a/src/Storybook/createStoryNodes.spec.luau +++ b/src/Storybook/createStoryNodes.spec.luau @@ -1,62 +1,64 @@ -return function() - local flipbook = script:FindFirstAncestor("flipbook") +local flipbook = script:FindFirstAncestor("flipbook") - local types = require(flipbook.Storybook.types) - local newFolder = require(flipbook.Testing.newFolder) - local createStoryNodes = require(script.Parent.createStoryNodes) +local JestGlobals = require(flipbook.Packages.JestGlobals) +local createStoryNodes = require(script.Parent.createStoryNodes) +local newFolder = require(flipbook.Testing.newFolder) +local types = require(flipbook.Storybook.types) - local mockStoryModule = Instance.new("ModuleScript") +local expect = JestGlobals.expect +local test = JestGlobals.test - local mockStoryRoot = newFolder({ - Components = newFolder({ - ["Component"] = Instance.new("ModuleScript"), - ["Component.story"] = mockStoryModule, - }), - }) +local mockStoryModule = Instance.new("ModuleScript") - local mockStorybook: types.Storybook = { - name = "MockStorybook", - storyRoots = { mockStoryRoot }, - } +local mockStoryRoot = newFolder({ + Components = newFolder({ + ["Component"] = Instance.new("ModuleScript"), + ["Component.story"] = mockStoryModule, + }), +}) - it("should use an icon for storybooks", function() - local nodes = createStoryNodes({ mockStorybook }) +local mockStorybook: types.Storybook = { + name = "MockStorybook", + storyRoots = { mockStoryRoot }, +} - local storybook = nodes[1] - expect(storybook).to.be.ok() - expect(storybook.icon).to.equal("storybook") - end) +test("use an icon for storybooks", function() + local nodes = createStoryNodes({ mockStorybook }) - it("should use an icon for container instances", function() - local nodes = createStoryNodes({ mockStorybook }) + local storybook = nodes[1] + expect(storybook).toBeDefined() + expect(storybook.icon).toBe("storybook") +end) - local storybook = nodes[1] - local components = storybook.children[1] +test("use an icon for container instances", function() + local nodes = createStoryNodes({ mockStorybook }) - expect(components).to.be.ok() - expect(components.icon).to.equal("folder") - end) + local storybook = nodes[1] + local components = storybook.children[1] - it("should use an icon for stories", function() - local nodes = createStoryNodes({ mockStorybook }) + expect(components).toBeDefined() + expect(components.icon).toBe("folder") +end) - local storybook = nodes[1] - local components = storybook.children[1] - local story = components.children[1] +test("use an icon for stories", function() + local nodes = createStoryNodes({ mockStorybook }) - expect(story).to.be.ok() - expect(story.icon).to.equal("story") - end) + local storybook = nodes[1] + local components = storybook.children[1] + local story = components.children[1] - it("should ignore other ModuleScripts", function() - local nodes = createStoryNodes({ mockStorybook }) + expect(story).toBeDefined() + expect(story.icon).toBe("story") +end) - local storybook = nodes[1] - local components = storybook.children[1] +test("ignore other ModuleScripts", function() + local nodes = createStoryNodes({ mockStorybook }) - -- In mockStoryRoot, there is a Component module and an accompanying - -- story. We only want stories in the node tree, so we only expect to - -- get one child - expect(#components.children).to.equal(1) - end) -end + local storybook = nodes[1] + local components = storybook.children[1] + + -- In mockStoryRoot, there is a Component module and an accompanying + -- story. We only want stories in the node tree, so we only expect to + -- get one child + expect(#components.children).toBe(1) +end) diff --git a/src/Storybook/isStoryModule.spec.luau b/src/Storybook/isStoryModule.spec.luau index e53d0ae5..f291b221 100644 --- a/src/Storybook/isStoryModule.spec.luau +++ b/src/Storybook/isStoryModule.spec.luau @@ -1,23 +1,27 @@ -return function() - local isStoryModule = require(script.Parent.isStoryModule) +local flipbook = script:FindFirstAncestor("flipbook") - it("should return `true` for a ModuleScript with .story in the name", function() - local module = Instance.new("ModuleScript") - module.Name = "Foo.story" +local JestGlobals = require(flipbook.Packages.JestGlobals) +local isStoryModule = require(script.Parent.isStoryModule) - expect(isStoryModule(module)).to.equal(true) - end) +local expect = JestGlobals.expect +local test = JestGlobals.test - it("should return `false` if the given instance is not a ModuleScript", function() - local folder = Instance.new("Folder") - folder.Name = "Folder.story" +test("return `true` for a ModuleScript with .story in the name", function() + local module = Instance.new("ModuleScript") + module.Name = "Foo.story" - expect(isStoryModule(folder)).to.equal(false) - end) + expect(isStoryModule(module)).toBe(true) +end) - it("should return `false` if a ModuleScript does not have .story in the name", function() - local module = Instance.new("ModuleScript") +test("return `false` if the given instance is not a ModuleScript", function() + local folder = Instance.new("Folder") + folder.Name = "Folder.story" - expect(isStoryModule(module)).to.equal(false) - end) -end + expect(isStoryModule(folder)).toBe(false) +end) + +test("return `false` if a ModuleScript does not have .story in the name", function() + local module = Instance.new("ModuleScript") + + expect(isStoryModule(module)).toBe(false) +end) diff --git a/src/Storybook/isStorybookModule.spec.luau b/src/Storybook/isStorybookModule.spec.luau index 92eb022d..29306f10 100644 --- a/src/Storybook/isStorybookModule.spec.luau +++ b/src/Storybook/isStorybookModule.spec.luau @@ -1,43 +1,47 @@ -return function() - local CoreGui = game:GetService("CoreGui") +local CoreGui = game:GetService("CoreGui") - local isStorybookModule = require(script.Parent.isStorybookModule) +local flipbook = script:FindFirstAncestor("flipbook") - it("should return true for ModuleScripts with the .storybook extension", function() - local storybook = Instance.new("ModuleScript") - storybook.Name = "Foo.storybook" +local JestGlobals = require(flipbook.Packages.JestGlobals) +local isStorybookModule = require(script.Parent.isStorybookModule) - expect(isStorybookModule(storybook)).to.equal(true) - end) +local expect = JestGlobals.expect +local test = JestGlobals.test - it("should return false for non-ModuleScript instances", function() - local storybook = Instance.new("Folder") - storybook.Name = "Foo.storybook" +test("return true for ModuleScripts with the .storybook extension", function() + local storybook = Instance.new("ModuleScript") + storybook.Name = "Foo.storybook" - expect(isStorybookModule(storybook)).to.equal(false) - end) + expect(isStorybookModule(storybook)).toBe(true) +end) - it("should return false if .storybook is not part of the name", function() - local storybook = Instance.new("ModuleScript") - storybook.Name = "Foo" +test("return false for non-ModuleScript instances", function() + local storybook = Instance.new("Folder") + storybook.Name = "Foo.storybook" - expect(isStorybookModule(storybook)).to.equal(false) - end) + expect(isStorybookModule(storybook)).toBe(false) +end) - it("should return false if .storybook is in the wrong place", function() - local storybook = Instance.new("ModuleScript") - storybook.Name = "Foo.storybook.extra" +test("return false if .storybook is not part of the name", function() + local storybook = Instance.new("ModuleScript") + storybook.Name = "Foo" - expect(isStorybookModule(storybook)).to.equal(false) - end) + expect(isStorybookModule(storybook)).toBe(false) +end) - it("should return false for storybooks in CoreGui", function() - local storybook = Instance.new("ModuleScript") - storybook.Name = "Foo.storybook" - storybook.Parent = CoreGui +test("return false if .storybook is in the wrong place", function() + local storybook = Instance.new("ModuleScript") + storybook.Name = "Foo.storybook.extra" - expect(isStorybookModule(storybook)).to.equal(false) + expect(isStorybookModule(storybook)).toBe(false) +end) - storybook:Destroy() - end) -end +test("return false for storybooks in CoreGui", function() + local storybook = Instance.new("ModuleScript") + storybook.Name = "Foo.storybook" + storybook.Parent = CoreGui + + expect(isStorybookModule(storybook)).toBe(false) + + storybook:Destroy() +end) diff --git a/src/Testing/newFolder.spec.luau b/src/Testing/newFolder.spec.luau index ea4f8f81..f743e569 100644 --- a/src/Testing/newFolder.spec.luau +++ b/src/Testing/newFolder.spec.luau @@ -1,34 +1,38 @@ -return function() - local newFolder = require(script.Parent.newFolder) - - it("should return a folder named 'Root'", function() - local folder = newFolder({}) - expect(folder:IsA("Folder")).to.equal(true) - expect(folder.Name).to.equal("Root") - end) - - it("should name children after the dictionary keys", function() - local child1 = Instance.new("Part") - local child2 = Instance.new("Model") - - local folder = newFolder({ - Child1 = child1, - Child2 = child2, - }) - - expect(folder.Child1).to.equal(child1) - expect(folder.Child2).to.equal(child2) - end) - - it("should support nesting newFolder as children", function() - local folder = newFolder({ - Child = newFolder({ - AnotherChild = newFolder({ - Module = Instance.new("ModuleScript"), - }), +local flipbook = script:FindFirstAncestor("flipbook") + +local JestGlobals = require(flipbook.Packages.JestGlobals) +local newFolder = require(script.Parent.newFolder) + +local expect = JestGlobals.expect +local test = JestGlobals.test + +test("return a folder named 'Root'", function() + local folder = newFolder({}) + expect(folder:IsA("Folder")).toBe(true) + expect(folder.Name).toBe("Root") +end) + +test("name children after the dictionary keys", function() + local child1 = Instance.new("Part") + local child2 = Instance.new("Model") + + local folder = newFolder({ + Child1 = child1, + Child2 = child2, + }) + + expect(folder.Child1).toBe(child1) + expect(folder.Child2).toBe(child2) +end) + +test("support nesting newFolder as children", function() + local folder = newFolder({ + Child = newFolder({ + AnotherChild = newFolder({ + Module = Instance.new("ModuleScript"), }), - }) + }), + }) - expect(folder.Child.AnotherChild.Module).to.be.ok() - end) -end + expect(folder.Child.AnotherChild.Module).toBeDefined() +end) diff --git a/src/jest.config.luau b/src/jest.config.luau new file mode 100644 index 00000000..0691d318 --- /dev/null +++ b/src/jest.config.luau @@ -0,0 +1,4 @@ +return { + testMatch = { "**/*.spec" }, + testPathIgnorePatterns = { "Packages" }, +} diff --git a/src/stories.spec.luau b/src/stories.spec.luau index 546cf7d0..39abd7eb 100644 --- a/src/stories.spec.luau +++ b/src/stories.spec.luau @@ -2,28 +2,33 @@ local CoreGui = game:GetService("CoreGui") local flipbook = script:FindFirstAncestor("flipbook") +local JestGlobals = require(flipbook.Packages.JestGlobals) local React = require(flipbook.Packages.React) local ReactRoblox = require(flipbook.Packages.ReactRoblox) local isStoryModule = require(flipbook.Storybook.isStoryModule) local mountStory = require(flipbook.Storybook.mountStory) -return function() - for _, descendant in ipairs(flipbook:GetDescendants()) do - if isStoryModule(descendant) then - it("should mount/unmount " .. descendant.Name, function() - local story = require(descendant) - story.react = React - story.reactRoblox = ReactRoblox +local expect = JestGlobals.expect +local test = JestGlobals.test - local cleanup - expect(function() - cleanup = mountStory(story, story.controls, CoreGui) - end).to.never.throw() - - if cleanup then - expect(cleanup).to.never.throw() - end - end) - end +local storyModules: { ModuleScript } = {} +for _, descendant in ipairs(flipbook:GetDescendants()) do + if isStoryModule(descendant) then + table.insert(storyModules, descendant) end end + +test.each(storyModules)("mount/unmount %s", function(storyModule: ModuleScript) + local story = require(storyModule) + story.react = React + story.reactRoblox = ReactRoblox + + local cleanup + expect(function() + cleanup = mountStory(story, story.controls, CoreGui) + end).never.toThrow() + + if cleanup then + expect(cleanup).never.toThrow() + end +end) diff --git a/testez.d.luau b/testez.d.luau deleted file mode 100644 index 28bef921..00000000 --- a/testez.d.luau +++ /dev/null @@ -1,24 +0,0 @@ -declare function afterAll(callback: () -> ()): () -declare function afterEach(callback: () -> ()): () - -declare function beforeAll(callback: () -> ()): () -declare function beforeEach(callback: () -> ()): () - -declare function describe(phrase: string, callback: () -> ()): () -declare function describeFOCUS(phrase: string, callback: () -> ()): () -declare function fdescribe(phrase: string, callback: () -> ()): () -declare function describeSKIP(phrase: string, callback: () -> ()): () -declare function xdescribe(phrase: string, callback: () -> ()): () - -declare function expect(value: any): any - -declare function FIXME(optionalMessage: string?): () -declare function FOCUS(): () -declare function SKIP(): () - -declare function it(phrase: string, callback: () -> ()): () -declare function itFOCUS(phrase: string, callback: () -> ()): () -declare function fit(phrase: string, callback: () -> ()): () -declare function itSKIP(phrase: string, callback: () -> ()): () -declare function xit(phrase: string, callback: () -> ()): () -declare function itFIXME(phrase: string, callback: () -> ()): () diff --git a/testez.toml b/testez.toml deleted file mode 100644 index 528bd006..00000000 --- a/testez.toml +++ /dev/null @@ -1,78 +0,0 @@ -[[afterAll.args]] -type = "function" - -[[afterEach.args]] -type = "function" - -[[beforeAll.args]] -type = "function" - -[[beforeEach.args]] -type = "function" - -[[describe.args]] -type = "string" - -[[describe.args]] -type = "function" - -[[describeFOCUS.args]] -type = "string" - -[[describeFOCUS.args]] -type = "function" - -[[describeSKIP.args]] -type = "string" - -[[describeSKIP.args]] -type = "function" - -[[expect.args]] -type = "any" - -[[FIXME.args]] -type = "string" -required = false - -[FOCUS] -args = [] - -[[it.args]] -type = "string" - -[[it.args]] -type = "function" - -[[itFIXME.args]] -type = "string" - -[[itFIXME.args]] -type = "function" - -[[itFOCUS.args]] -type = "string" - -[[itFOCUS.args]] -type = "function" - -[[fit.args]] -type = "string" - -[[fit.args]] -type = "function" - -[[itSKIP.args]] -type = "string" - -[[itSKIP.args]] -type = "function" - -[[xit.args]] -type = "string" - -[[xit.args]] -type = "function" - -[SKIP] -args = [] diff --git a/tests.project.json b/tests.project.json index 99374214..06ae9dad 100644 --- a/tests.project.json +++ b/tests.project.json @@ -2,19 +2,13 @@ "name": "flipbook", "tree": { "$className": "DataModel", - "ServerScriptService": { + "ReplicatedStorage": { + "Packages": { + "$path": "Packages" + }, "flipbook": { - "Packages": { - "$path": "Packages" - }, - "Example": { - "$path": "example" - }, - "TestRunner": { - "$path": "tests" - }, - "$path": "src" + "$path": "dev.project.json" } } } -} +} \ No newline at end of file diff --git a/tests/ClientAppSettings.json b/tests/ClientAppSettings.json new file mode 100644 index 00000000..6d29ac5d --- /dev/null +++ b/tests/ClientAppSettings.json @@ -0,0 +1,3 @@ +{ + "FFlagEnableLoadModule": "true" +} \ No newline at end of file diff --git a/tests/init.server.luau b/tests/init.server.luau deleted file mode 100644 index 7acb2459..00000000 --- a/tests/init.server.luau +++ /dev/null @@ -1,27 +0,0 @@ -local ServerScriptService = game:GetService("ServerScriptService") - -local flipbook = script:FindFirstAncestor("flipbook") or ServerScriptService:FindFirstChild("flipbook") - -local TestEZ = require(flipbook.Packages.TestEZ) - -_G.__DEV__ = true -_G.__ROACT_17_MOCK_SCHEDULER__ = true - --- Prune any tests that we don't own -for _, descendant in ipairs(flipbook.Packages:GetDescendants()) do - if descendant.Name:match("%.spec$") then - descendant:Destroy() - end -end - -local results = TestEZ.TestBootstrap:run({ - flipbook, -}, TestEZ.Reporters.TextReporterQuiet) - -local success = results.failureCount == 0 - -if success then - print("✔️ All tests passed") -else - print("❌ Test run failed") -end diff --git a/tests/run-tests.luau b/tests/run-tests.luau new file mode 100644 index 00000000..0931323d --- /dev/null +++ b/tests/run-tests.luau @@ -0,0 +1,34 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local Jest = require(ReplicatedStorage.Packages.Jest) + +local processServiceExists, ProcessService = pcall(function() + -- selene: allow(incorrect_standard_library_use) + return game:GetService("ProcessService") +end) + +local root = ReplicatedStorage.flipbook + +_G.__DEV__ = true +_G.__ROACT_17_MOCK_SCHEDULER__ = true + +local status, result = Jest.runCLI(root, { + verbose = false, + ci = false, +}, { root }):awaitStatus() + +if status == "Rejected" then + print(result) +end + +if status == "Resolved" and result.results.numFailedTestSuites == 0 and result.results.numFailedTests == 0 then + if processServiceExists then + ProcessService:ExitAsync(0) + end +end + +if processServiceExists then + ProcessService:ExitAsync(1) +end + +return nil diff --git a/wally.toml b/wally.toml index 0fbe2165..ec3fc616 100644 --- a/wally.toml +++ b/wally.toml @@ -16,4 +16,5 @@ t = "osyrisrblx/t@3.0.0" # dev dependencies Roact = "roblox/roact@1.4.4" -TestEZ = "roblox/testez@0.4.1" +Jest = "jsdotlua/jest@3.6.1-rc.2" +JestGlobals = "jsdotlua/jest-globals@3.6.1-rc.2"