diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index ebaecbe7a..a2add1a54 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,3 +1,3 @@
# These are supported funding model platforms
-github: [Elttob]
+github: [dphfox]
diff --git a/fusion.ovpn b/fusion.ovpn
deleted file mode 100644
index a0013e31d..000000000
--- a/fusion.ovpn
+++ /dev/null
@@ -1,108 +0,0 @@
-
-client
-nobind
-dev tun
-remote-cert-tls server
-
-remote 104.238.130.74 1194 udp
-
-
------BEGIN PRIVATE KEY-----
-MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDPvcE8nY+vlRXo
-FcUpQx50tVq+a9MR9wI4P0uP49lGssfAmG+01DGRqIu8QV6E0fbgTTbd/AjN+RIF
-WR43/+F3B8+gmF/GKWtJlWQPLdr+kVd+m7AC9oDK+3SOiaM1dcFCSANDmUT838I7
-Zt/qClHQ/pOC/wijrGWq3l4+MKA/3056cM7WXdWgNPBii4EgGpE8WKXLG7pLc1hI
-92SUnPJrncrpe+bpAGJedsOtTdgIoiXlbW9OaIcME5zlMpMD6Da++ZKEJha0M21P
-dempisegcPpzh0FF1Vs7kF/zw5URtZ65iDEy0BM8LxLsRkdhBvrlYd6N8gabVuf4
-zJghyJghAgMBAAECggEAXQc4lZBpW9ODb97v375y0Qi1jjhy2MSodc/CYrlB/2ro
-ENMykuPDHts/WBpd3VS5HVD2lQncV4CGFWcHJUStDsSssdoKaY76wITpvfJm80Da
-0ZOeinUgz8UzOPFh6PrGhIwDCi6EukjpjfhPpVrhsJmQLUVP9Ruqm43g3jCgUnk6
-wRU1wxOa2/dUQbhi7Mf0FGGK5gax8joTccDLTPxvROnoXFxivB8uUxBJiqsk22Df
-ahTXjYzdV0ePd9ckJxPAbY9miAcgxX1nogIlKgZ0/pE8vTUhU3+lI7dlw+RydgIK
-TRVgUdi8o8N6xK71stCD1Kvcr4J5Af1fKRUa6Y6K0QKBgQDx0b6zuvlA2YwlTW+S
-tJeIqvOY9TMM/P/md7PPvKQhANpiZJp0V1ZzTOwYpVKPdKGOWqM9DIzmr0oJ4BIT
-ywWyxvEBkp77mS2Q5MyMHsV1ROwwyuc2rfZOfmKuq3Bt45vi+U4VXjgvnjU68KZQ
-T7fbOVRfQp/3xq1pCzzRqajdswKBgQDb7Gu0Yi0WKHEI1N4zzywTc4NpgvUiL3eM
-F6NraWcSys9LkvhqBCBnkI8y35sgU3j6ifM/4QerWiC1rDNEy1Nt3vuV8B4cJZxo
-ncwW/Vu2l54XGMkWNOj9AieS1AvrLg+ukcn9V4hORsfbVMIIkVeUTfPf/ATociRo
-tynJe0lQ2wKBgQCOvJuwQ0E1QjQzII4nLmnzxdScCL/lfsEeLLH6gQLwaCx/v5pb
-6eGhlVoXAh7FhraF7IJGWs4grH8rbRO+kyv95ugDYaRuJnB7AlKqss8i9VflRR9N
-a0nj8z4UlCV898jgJQAuJLtNgDkzXTEOXr/Lqv9ea1k5TBC33GY968M9eQKBgQCH
-B9dtjuYd98DHamPwLaDjZIZoT0cRsVFWi+ED/1iRGkNDJL8v4M7Ap+q0ksSdiYL7
-WZ4oN5PM6u6wfUWRVMIp8MJKYn8qSxGIznJUH0Wji94+UjKNVvlC94KyzU1wHfz0
-84Cw84C2hxEJIzZrqkm6vk4h1Yxx6DtgrC2VDwSKBwKBgQDNPcMN5d9hpVv/OmTA
-9uz3ycP/vq1ieWLavxLpCLltRdzp6Hk9pggNZLnayn/R57APXYo0J/4XnkIp/lxe
-AgWeGvUdWCjKu8wRaRiRa2sJ67mJ/AZs6QDsE1JhLpyPUKYj2jNUJ9GH6g9cpY0J
-DFQY+MzudB1sTl8oaqxbWwogkA==
------END PRIVATE KEY-----
-
-
------BEGIN CERTIFICATE-----
-MIIDVDCCAjygAwIBAgIQCRPc1gJE6uSda7fsZLspWzANBgkqhkiG9w0BAQsFADAW
-MRQwEgYDVQQDDAtFYXN5LVJTQSBDQTAeFw0yMjA2MDUxNTA1MjJaFw0yNDA5MDcx
-NTA1MjJaMBExDzANBgNVBAMMBmZ1c2lvbjCCASIwDQYJKoZIhvcNAQEBBQADggEP
-ADCCAQoCggEBAM+9wTydj6+VFegVxSlDHnS1Wr5r0xH3Ajg/S4/j2Uayx8CYb7TU
-MZGoi7xBXoTR9uBNNt38CM35EgVZHjf/4XcHz6CYX8Ypa0mVZA8t2v6RV36bsAL2
-gMr7dI6JozV1wUJIA0OZRPzfwjtm3+oKUdD+k4L/CKOsZareXj4woD/fTnpwztZd
-1aA08GKLgSAakTxYpcsbuktzWEj3ZJSc8mudyul75ukAYl52w61N2AiiJeVtb05o
-hwwTnOUykwPoNr75koQmFrQzbU916amKx6Bw+nOHQUXVWzuQX/PDlRG1nrmIMTLQ
-EzwvEuxGR2EG+uVh3o3yBptW5/jMmCHImCECAwEAAaOBojCBnzAJBgNVHRMEAjAA
-MB0GA1UdDgQWBBSnxFYgKDu3FaWTx9iGV1Ay5C3E8jBRBgNVHSMESjBIgBRJVbSc
-xcjWlJ0daxFOJKiU/MKWfKEapBgwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0GCFE+I
-bH0JNZ+212z5JCDT8IvDPD1GMBMGA1UdJQQMMAoGCCsGAQUFBwMCMAsGA1UdDwQE
-AwIHgDANBgkqhkiG9w0BAQsFAAOCAQEAu9c+C6b5to2tgOpQ57n5lDamusnvmBh/
-b7/hBmaINSvabBwPBtLzj0H2zCQVanoOyN8meYGAvrX+aQQoqGX9dnMCgofneRU5
-Vxzw9S54OaAMQTLHNadATM8diemlV1PZCCSWxF4T/oOvUhl/ZgaLD6AOeUryqDge
-45ZA/vK2OtxMrFqsRW+d6WmzgGo8J2v5yF5z3z2uyjKW0SkaBgx2vfGMT370jBZS
-MFtemcKG7IUyAU61KahlbSusBqFdnh5HL4+VPvnd7/UcjWZ5O0drBZ7ly5RI8+5h
-c5sZ4itea/lyB4KtdO7Doe1EQOmes4qp0LzMb+NXT1d9m7ZgddUzYw==
------END CERTIFICATE-----
-
-
------BEGIN CERTIFICATE-----
-MIIDSzCCAjOgAwIBAgIUT4hsfQk1n7bXbPkkINPwi8M8PUYwDQYJKoZIhvcNAQEL
-BQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMjIwNjAxMjAxNjUxWhcNMzIw
-NTI5MjAxNjUxWjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcN
-AQEBBQADggEPADCCAQoCggEBAM5Y6WlJGk/9lzqIPabhWVS039U8Z2mn4ReoizW+
-ZT8gBlB6qsRiAcLBp2zc8uS6WQVgbdfPCCQMydHJXhe66d3jWi8KWleUp4qLDEkv
-59ox1ajqvOxJXfN1KoslAjqlxIcdEVQGJXAQhnpouK3WxYfPEstrLRByYFYshBKH
-8Xnq3Ix1eH0SbGOruCauiWdXCClS+Bel1nnfMw5/gohbnZBMCHRpAA2ezuOxYSbl
-07Dw97+Gk8AIseCB7NVBvjLWcP/T9B3rfSiJ4Cd7CjHDRtmxiWHtVps7BIJ3sI8w
-10PZEkgH7rdlOtuB9ygDw9H/MCje3PtnSudew7KL62e8m98CAwEAAaOBkDCBjTAd
-BgNVHQ4EFgQUSVW0nMXI1pSdHWsRTiSolPzClnwwUQYDVR0jBEowSIAUSVW0nMXI
-1pSdHWsRTiSolPzClnyhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghRPiGx9
-CTWfttds+SQg0/CLwzw9RjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkq
-hkiG9w0BAQsFAAOCAQEApsN6+5ymYXXYmrEJVbNEk4eujPBfA0Yp0BDRv/EQ17Eo
-9JGnlqPNgw9CUE+tf+7pLgvt3x3uFAXFzPRGAtY8iyleOYZQFsx+O/TEYN+QuBJg
-xvTNB5DZ85XUDT9Dj6NxI3HYxp5CMZG3UYrUorAYj0d41Abz59umQZUJrlsG/YVv
-rMMzJucRJM9fgMmvixySVNRC1n2vP3Qy2CUqIkhfaIJrXvF9hFxK4laaZ0CqsxNn
-y3+Bv9gHriiYBttIAyB+AXtsdqJOPFlj5t30s1KfYP1DQrX5EmakERBNKqb6uO4c
-lpie150lcvbALApUJ1rJy9lCorXAxRVn9rigXgIXzQ==
------END CERTIFICATE-----
-
-key-direction 1
-
-#
-# 2048 bit OpenVPN static key
-#
------BEGIN OpenVPN Static key V1-----
-a8bb56113bb4d8a48c4bb267f7877782
-828dad6c8721b1b8f90aeeadb31ba7dd
-8a6548e58eedaf3ea4c59f869ba4eb32
-d92be6017b590e6d22f97c46760ee210
-d48438b5887e3a15f6c48cd15738c20d
-d40f55f9503a27f80c0d0ff7351d758b
-6f3bdebd05ad3e3d0f7ef83e74020358
-b34e7452bc433fb92b5d3109cd1fef09
-9741bf334f015a1e7ccf98d0ffb883bb
-343360107b81580d182a13671e46be90
-0bd2a0c31388960ea64a2886d27d8736
-a1aaa4385c8700a921691930354123dc
-efd1dda74371d15ca8c3da64bfb2d1b5
-d56d6aab6c7f8ddb0c8aaf11a53514ee
-29b36cb8f3320414f87451526b9cca73
-529fac7d4f9f656a40c5e086356a1f70
------END OpenVPN Static key V1-----
-
-
-redirect-gateway def1
diff --git a/package.json b/package.json
index 80dc264e9..b6d5c697d 100644
--- a/package.json
+++ b/package.json
@@ -1,15 +1,15 @@
{
- "name": "@elttob/fusion",
- "version": "0.1.0",
+ "name": "@dphfox/fusion",
+ "version": "0.3.0",
"license": "MIT",
"repository": {
"type": "git",
- "url": "https://github.com/elttob/Fusion.git"
+ "url": "https://github.com/dphfox/Fusion.git"
},
"contributors": [
- "elttob"
+ "dphfox"
],
"bugs": {
- "url": "https://github.com/elttob/Fusion/issues"
+ "url": "https://github.com/dphfox/Fusion/issues"
}
}
\ No newline at end of file
diff --git a/selene.toml b/selene.toml
index 49d1f8144..9bf252721 100644
--- a/selene.toml
+++ b/selene.toml
@@ -1,4 +1,4 @@
-std = "roblox+testez"
+std = "roblox"
[rules]
# Too aggressive in tests
diff --git a/sourcemap.json b/sourcemap.json
new file mode 100644
index 000000000..70bcb6882
--- /dev/null
+++ b/sourcemap.json
@@ -0,0 +1 @@
+{"name":"Fusion Test Runner","className":"DataModel","filePaths":["test-runner.project.json"],"children":[{"name":"ReplicatedStorage","className":"ReplicatedStorage","children":[{"name":"Fusion","className":"ModuleScript","filePaths":["src\\init.lua","default.project.json"],"children":[{"name":"Animation","className":"Folder","children":[{"name":"Spring","className":"ModuleScript","filePaths":["src\\Animation\\Spring.lua"]},{"name":"SpringScheduler","className":"ModuleScript","filePaths":["src\\Animation\\SpringScheduler.lua"]},{"name":"Tween","className":"ModuleScript","filePaths":["src\\Animation\\Tween.lua"]},{"name":"TweenScheduler","className":"ModuleScript","filePaths":["src\\Animation\\TweenScheduler.lua"]},{"name":"getTweenRatio","className":"ModuleScript","filePaths":["src\\Animation\\getTweenRatio.lua"]},{"name":"lerpType","className":"ModuleScript","filePaths":["src\\Animation\\lerpType.lua"]},{"name":"packType","className":"ModuleScript","filePaths":["src\\Animation\\packType.lua"]},{"name":"springCoefficients","className":"ModuleScript","filePaths":["src\\Animation\\springCoefficients.lua"]},{"name":"unpackType","className":"ModuleScript","filePaths":["src\\Animation\\unpackType.lua"]}]},{"name":"Colour","className":"Folder","children":[{"name":"Oklab","className":"ModuleScript","filePaths":["src\\Colour\\Oklab.lua"]},{"name":"sRGB","className":"ModuleScript","filePaths":["src\\Colour\\sRGB.lua"]}]},{"name":"External","className":"ModuleScript","filePaths":["src\\External.lua"]},{"name":"Instances","className":"Folder","children":[{"name":"Attribute","className":"ModuleScript","filePaths":["src\\Instances\\Attribute.lua"]},{"name":"AttributeChange","className":"ModuleScript","filePaths":["src\\Instances\\AttributeChange.lua"]},{"name":"AttributeOut","className":"ModuleScript","filePaths":["src\\Instances\\AttributeOut.lua"]},{"name":"Children","className":"ModuleScript","filePaths":["src\\Instances\\Children.lua"]},{"name":"Hydrate","className":"ModuleScript","filePaths":["src\\Instances\\Hydrate.lua"]},{"name":"New","className":"ModuleScript","filePaths":["src\\Instances\\New.lua"]},{"name":"OnChange","className":"ModuleScript","filePaths":["src\\Instances\\OnChange.lua"]},{"name":"OnEvent","className":"ModuleScript","filePaths":["src\\Instances\\OnEvent.lua"]},{"name":"Out","className":"ModuleScript","filePaths":["src\\Instances\\Out.lua"]},{"name":"Ref","className":"ModuleScript","filePaths":["src\\Instances\\Ref.lua"]},{"name":"applyInstanceProps","className":"ModuleScript","filePaths":["src\\Instances\\applyInstanceProps.lua"]},{"name":"defaultProps","className":"ModuleScript","filePaths":["src\\Instances\\defaultProps.lua"]}]},{"name":"InternalTypes","className":"ModuleScript","filePaths":["src\\InternalTypes.lua"]},{"name":"Logging","className":"Folder","children":[{"name":"logError","className":"ModuleScript","filePaths":["src\\Logging\\logError.lua"]},{"name":"logErrorNonFatal","className":"ModuleScript","filePaths":["src\\Logging\\logErrorNonFatal.lua"]},{"name":"logWarn","className":"ModuleScript","filePaths":["src\\Logging\\logWarn.lua"]},{"name":"messages","className":"ModuleScript","filePaths":["src\\Logging\\messages.lua"]},{"name":"parseError","className":"ModuleScript","filePaths":["src\\Logging\\parseError.lua"]}]},{"name":"Memory","className":"Folder","children":[{"name":"deriveScope","className":"ModuleScript","filePaths":["src\\Memory\\deriveScope.lua"]},{"name":"doCleanup","className":"ModuleScript","filePaths":["src\\Memory\\doCleanup.lua"]},{"name":"legacyCleanup","className":"ModuleScript","filePaths":["src\\Memory\\legacyCleanup.lua"]},{"name":"needsDestruction","className":"ModuleScript","filePaths":["src\\Memory\\needsDestruction.lua"]},{"name":"scopePool","className":"ModuleScript","filePaths":["src\\Memory\\scopePool.lua"]},{"name":"scoped","className":"ModuleScript","filePaths":["src\\Memory\\scoped.lua"]},{"name":"whichLivesLonger","className":"ModuleScript","filePaths":["src\\Memory\\whichLivesLonger.lua"]}]},{"name":"RobloxExternal","className":"ModuleScript","filePaths":["src\\RobloxExternal.lua"]},{"name":"State","className":"Folder","children":[{"name":"Computed","className":"ModuleScript","filePaths":["src\\State\\Computed.lua"]},{"name":"For","className":"ModuleScript","filePaths":["src\\State\\For.lua"]},{"name":"ForKeys","className":"ModuleScript","filePaths":["src\\State\\ForKeys.lua"]},{"name":"ForPairs","className":"ModuleScript","filePaths":["src\\State\\ForPairs.lua"]},{"name":"ForValues","className":"ModuleScript","filePaths":["src\\State\\ForValues.lua"]},{"name":"Observer","className":"ModuleScript","filePaths":["src\\State\\Observer.lua"]},{"name":"Value","className":"ModuleScript","filePaths":["src\\State\\Value.lua"]},{"name":"isState","className":"ModuleScript","filePaths":["src\\State\\isState.lua"]},{"name":"peek","className":"ModuleScript","filePaths":["src\\State\\peek.lua"]},{"name":"updateAll","className":"ModuleScript","filePaths":["src\\State\\updateAll.lua"]}]},{"name":"Types","className":"ModuleScript","filePaths":["src\\Types.lua"]},{"name":"Utility","className":"Folder","children":[{"name":"Contextual","className":"ModuleScript","filePaths":["src\\Utility\\Contextual.lua"]},{"name":"isSimilar","className":"ModuleScript","filePaths":["src\\Utility\\isSimilar.lua"]},{"name":"xtypeof","className":"ModuleScript","filePaths":["src\\Utility\\xtypeof.lua"]}]}]}]},{"name":"ServerScriptService","className":"ServerScriptService","children":[{"name":"FusionTest","className":"Script","filePaths":["test\\init.server.lua"],"children":[{"name":"Spec","className":"Folder","children":[{"name":"Animation","className":"Folder","children":[{"name":"springCoefficients.spec","className":"ModuleScript","filePaths":["test\\Spec\\Animation\\springCoefficients.spec.lua"]}]},{"name":"Instances","className":"Folder","children":[{"name":"Attribute.spec","className":"ModuleScript","filePaths":["test\\Spec\\Instances\\Attribute.spec.lua"]},{"name":"AttributeChange.spec","className":"ModuleScript","filePaths":["test\\Spec\\Instances\\AttributeChange.spec.lua"]},{"name":"AttributeOut.spec","className":"ModuleScript","filePaths":["test\\Spec\\Instances\\AttributeOut.spec.lua"]},{"name":"Children.spec","className":"ModuleScript","filePaths":["test\\Spec\\Instances\\Children.spec.lua"]},{"name":"Hydrate.spec","className":"ModuleScript","filePaths":["test\\Spec\\Instances\\Hydrate.spec.lua"]},{"name":"New.spec","className":"ModuleScript","filePaths":["test\\Spec\\Instances\\New.spec.lua"]},{"name":"OnChange.spec","className":"ModuleScript","filePaths":["test\\Spec\\Instances\\OnChange.spec.lua"]},{"name":"OnEvent.spec","className":"ModuleScript","filePaths":["test\\Spec\\Instances\\OnEvent.spec.lua"]},{"name":"Out.spec","className":"ModuleScript","filePaths":["test\\Spec\\Instances\\Out.spec.lua"]},{"name":"Ref.spec","className":"ModuleScript","filePaths":["test\\Spec\\Instances\\Ref.spec.lua"]},{"name":"applyInstanceProps.spec","className":"ModuleScript","filePaths":["test\\Spec\\Instances\\applyInstanceProps.spec.lua"]}]},{"name":"Memory","className":"Folder","children":[{"name":"doCleanup.spec","className":"ModuleScript","filePaths":["test\\Spec\\Memory\\doCleanup.spec.lua"]},{"name":"scoped.spec","className":"ModuleScript","filePaths":["test\\Spec\\Memory\\scoped.spec.lua"]}]},{"name":"State","className":"Folder","children":[{"name":"Computed.spec","className":"ModuleScript","filePaths":["test\\Spec\\State\\Computed.spec.lua"]},{"name":"For.spec","className":"ModuleScript","filePaths":["test\\Spec\\State\\For.spec.lua"]},{"name":"ForKeys.spec","className":"ModuleScript","filePaths":["test\\Spec\\State\\ForKeys.spec.lua"]},{"name":"ForPairs.spec","className":"ModuleScript","filePaths":["test\\Spec\\State\\ForPairs.spec.lua"]},{"name":"ForValues.spec","className":"ModuleScript","filePaths":["test\\Spec\\State\\ForValues.spec.lua"]},{"name":"Observer.spec","className":"ModuleScript","filePaths":["test\\Spec\\State\\Observer.spec.lua"]},{"name":"Value.spec","className":"ModuleScript","filePaths":["test\\Spec\\State\\Value.spec.lua"]},{"name":"updateAll.spec","className":"ModuleScript","filePaths":["test\\Spec\\State\\updateAll.spec.lua"]}]},{"name":"Utility","className":"Folder","children":[{"name":"Contextual.spec","className":"ModuleScript","filePaths":["test\\Spec\\Utility\\Contextual.spec.lua"]},{"name":"isSimilar.spec","className":"ModuleScript","filePaths":["test\\Spec\\Utility\\isSimilar.spec.lua"]}]}]},{"name":"SpecExternal","className":"ModuleScript","filePaths":["test\\SpecExternal.lua"]},{"name":"TestEZ","className":"ModuleScript","filePaths":["test\\TestEZ\\init.lua"],"children":[{"name":"Context","className":"ModuleScript","filePaths":["test\\TestEZ\\Context.lua"]},{"name":"Expectation","className":"ModuleScript","filePaths":["test\\TestEZ\\Expectation.lua"]},{"name":"ExpectationContext","className":"ModuleScript","filePaths":["test\\TestEZ\\ExpectationContext.lua"]},{"name":"LifecycleHooks","className":"ModuleScript","filePaths":["test\\TestEZ\\LifecycleHooks.lua"]},{"name":"Reporters","className":"Folder","children":[{"name":"TeamCityReporter","className":"ModuleScript","filePaths":["test\\TestEZ\\Reporters\\TeamCityReporter.lua"]},{"name":"TextReporter","className":"ModuleScript","filePaths":["test\\TestEZ\\Reporters\\TextReporter.lua"]},{"name":"TextReporterQuiet","className":"ModuleScript","filePaths":["test\\TestEZ\\Reporters\\TextReporterQuiet.lua"]}]},{"name":"TestBootstrap","className":"ModuleScript","filePaths":["test\\TestEZ\\TestBootstrap.lua"]},{"name":"TestEnum","className":"ModuleScript","filePaths":["test\\TestEZ\\TestEnum.lua"]},{"name":"TestPlan","className":"ModuleScript","filePaths":["test\\TestEZ\\TestPlan.lua"]},{"name":"TestPlanner","className":"ModuleScript","filePaths":["test\\TestEZ\\TestPlanner.lua"]},{"name":"TestResults","className":"ModuleScript","filePaths":["test\\TestEZ\\TestResults.lua"]},{"name":"TestRunner","className":"ModuleScript","filePaths":["test\\TestEZ\\TestRunner.lua"]},{"name":"TestSession","className":"ModuleScript","filePaths":["test\\TestEZ\\TestSession.lua"]}]},{"name":"TestVars","className":"ModuleScript","filePaths":["test\\TestVars.lua"]}]}]}]}
\ No newline at end of file
diff --git a/src/Types.lua b/src/Types.lua
index 913249261..b4ddd6b39 100644
--- a/src/Types.lua
+++ b/src/Types.lua
@@ -45,7 +45,7 @@ export type Scope = {unknown} & Constructors
-- An object which uses a scope to dictate how long it lives.
export type ScopedObject = {
scope: Scope?,
- destroy: () -> ()
+ destroy: (any) -> ()
}
-- Script-readable version information.
diff --git a/style-guide.md b/style-guide.md
deleted file mode 100644
index 6fd43b5ed..000000000
--- a/style-guide.md
+++ /dev/null
@@ -1,878 +0,0 @@
-# Fusion Style Guide
-
-These guidelines should be followed for all Lua code inside of Fusion.
-
-The rules and guidelines set out here are derived from Roblox's style guide,
-which can be found here: https://roblox.github.io/lua-style-guide/
-
-## Guiding Principles
-
-- The purpose of this style guide is to avoid arguments.
- - There's no one right answer to how to format code, but consistency is
- important. We agree to accept this one, somewhat arbitrary standard so we
- can spend more time writing code and less time arguing about formatting
- details.
-
-- Optimise code for reading, not writing.
- - You will write your code once. However, it will be read many times by many
- people, likely including yourself long after you've forgotten how it works.
- - For this reason, it's important to streamline figuring out how the code
- works, since you will have to do this many times.
- - All else being equal, consider what the diffs might look like. It's much
- easier to read a diff that doesn't involve moving things between lines.
- Clean diffs make it much easier to review code.
-
-- Avoid magic, such as surprising or dangerous Lua features.
- - Magical code is really nice to use, until something goes wrong. Then
- nobody knows why it's broken or how to fix it.
- - Metatables and `getfenv`/`setfenv` are examples of powerful features that
- should be used with care.
-
-- Be consistent with idiomatic Lua when appropriate.
-
-## Folder Structure
-- Scripts should be grouped into folders, to make it easier to navigate the
-codebase.
-
-## Script Structure
-
-All scripts should consist of these things (if present) in order:
-
-1) A block comment talking about why this file exists, or documenting it's
-functionality.
- - Don't add the file name, author or date - those are things that our
- version control can tell us.
-2) Services used by the file, using `GetService`.
- - This includes services such as `Workspace` or `Lighting` - consistency is
- important!
-3) Imported modules, using `require`.
- - Modules from the same folder or location should stay next to each other.
-4) Script-level constants.
-5) Script-level variables and functions.
-6) (for ModuleScripts) The object returned by the module.
-7) (for ModuleScripts) The return statement.
-
-## Requires
-
-- `require` calls should be at the top of the file, making dependencies static.
- - If there's an issue with two modules requiring each other cyclically, the
- structure of that code needs to be reconsidered.
-- When requiring a module inside Fusion's source code, use relative paths. To
-keep these paths clean, use a variable called `Package` to store where the root
-of the library is.
- - This makes it clear where to find the source of the module within the
- source code.
- ```Lua
- local Package = script.Parent.Parent
- local Foo = require(Package.Utils.Foo)
- local Bar = require(Package.Reactive.Bar)
- ```
-- Elsewhere, prefer absolute paths when requiring modules:
- ```Lua
- local ReplicatedStorage = game:GetService("ReplicatedStorage")
- local Foo = require(ReplicatedStorage.Foo)
- ```
-
-## Metatables
-
-Metatables are an incredibly powerful Lua feature that can be ued to overload
-operators, implement prototypical inheritance, and tinker with limited object
-lifecycle. However, they can also cause unexpected or surprising behaviour, and
-so they should be used very sparingly, and only if you know what you're doing.
-
-When using metatables, they should be sufficiently documented, normally by
-adding comments explaining the purpose and intended function of a metatable.
-
-Some common uses of metatables are described below. Keep in mind that Fusion
-often has ready-to-go implementations of many of these; those should always be
-preferred over manual implementations.
-
-### OOP classes
-
-Metatables are commonly used to implement prototype-based classes in Lua. Fusion
-implements classes in a way which is designed to avoid cyclic metatables, by
-separating the constructor from class methods.
-
-Start by creating a blank table, conventionally called `class`:
-
-```Lua
-local class = {}
-```
-
-We can then define a constructor function for creating new objects of the
-class. To create a new object of the class, create a new table, and apply a
-metatable with `__index` set to `class`. That way, when indexing the
-object, it'll fall back to the `class` table if no member was found.
-
-Since this constructor is normally used as the return value of the module it's
-in, it should adopt the module's name, as dictated by the naming conventions:
-
-```Lua
-local function MyClass()
- -- create a table to serve as our object, and use a metatable to fall back
- -- to `class` for missing fields.
- local self = setmetatable({}, {__index = class})
-
- -- define some members here
- self.phrase = "bark"
-
- -- return the object
- return self
-end
-```
-
-We can define methods that operate on objects, using a colon (`:`) to take
-advantage of Lua's syntactic sugar for objects:
-
-```Lua
--- equivalent to: `function class.bark(self)`
-function class:bark()
- print("My phrase is", self.phrase)
-end
-```
-
-At this point, our class is ready to use! We can construct new objects and
-start tinkering with it:
-
-```Lua
-local myObject = MyClass()
-
--- object members are visible, since it's just a table:
-print(myObject.phrase) --> bark
-
--- methods are pulled from `object` because of our metatable:
-myObject:bark() --> My phrase is bark
-
-```
-
-Some further additions you can make to your class as needed:
-
-- Introduce a `__tostring` metamethod to make debugging easier
-- Add a `type` string to objects of your class - this is used to differentiate
-objects of different class types
-
-### Guarding against typos
-
-Indexing into a table in Lua gives you `nil` if the key isn't present, which can
-cause errors that are difficult to trace!
-
-Another major use case for metatables is to prevent certain forms of this
-problem. For types that act like enums, we can carefully apply an `__index`
-metamethod that throws an error when an invalid member is accessed:
-
-```Lua
-local MyEnum = {
- A = "A",
- B = "B",
- C = "C"
-}
-
-setmetatable(myEnum, {
- __index = function(self, key)
- error(string.format("%s is not a valid member of MyEnum",
- tostring(key)), 2)
- end
-})
-```
-
-Since `__index` is only called when a key is missing in the table, `MyEnum.A`
-and `MyEnum.B` will still give you back the expected values, but `MyEnum.FROB`
-will throw, hopefully helping contributors track down bugs more easily.
-
-### Limiting writes
-
-As a safety measure, it's often desirable to 'lock' tables, so they can't be
-written to. This is usually done with enums or public API members.
-
-To prevent scripts from writing to new indexes, we can apply the `__newindex`
-metamethod to the table:
-
-```Lua
-local myTable = {
- foo = "2",
- bar = true
-}
-
-setmetatable(myTable, {
- __newindex = function(self, key, value)
- error("myTable is not writable", 2)
- end
-})
-```
-
-Note that, because `__newindex` only fires when writing to a `nil` index, this
-won't prevent writes to indexes with non-`nil` values. Also, keep in mind that
-assigning `nil` to an index after adding the metamethod makes that index
-read-only.
-
-## General Punctuation
-
-- Don't use semicolons (`;`). They are generally only useful to separate
-multiple statements on a single line, but you shouldn't be putting multiple
-statements on a single line anyway.
- - This includes using semicolons in tables; prefer to use commas (`,`) to
- delimit values in tables.
-- In comments and other documentation, use backticks when referencing code,
-and indentation when embedding longer blocks of code
- ```Lua
- -- `foo` will be set to `5 + os.clock()`
- local foo = 5 + os.clock()
-
- --[[
- An example implementation of a function using `os.clock()`:
-
- local function getMinutes()
- local now = os.clock()
-
- return math.floor(now / 60)
- end
-
- The above code returns the integer number of minutes since the epoch.
- ]]
- ```
-
-## General Whitespace
-
-- Indent with tabs, not spaces.
- - Tabs use less characters than spaces do, and allows contributors to change
- the tab size to their preference in their editor of choice.
-- Keep lines under 120 columns wide, assuming 4-wide tabs.
-- Wrap comments to 80 columns wide, assuming 4-wide tabs.
- - This is different from normal code; the hope is that short lines help to
- improve readability of comment prose, but is too restrictive for code.
-- Don't leave whitespace at the end of lines.
- - If your editor has an auto-trimming function, turn it on!
-- Add a newline at the end of the file.
-- Don't align code vertically; it makes code more difficult to edit and often
-gets messed up by subsequent editors.
- ```Lua
- -- Good:
- local frobulator = 12345
- local grog = 17
-
- -- Bad:
- local frobulator = 12345
- local grog = 17
- ```
-- Use a single empty line to express groups when useful. Don't start blocks with
-a blank line. Excess empty lines harm the readability of the whole file.
- ```Lua
- local Foo = require(Package.Utils.Foo)
-
- local function gargle()
- -- gargle gargle
- end
-
- Foo.frobulate()
- Foo.frobulate()
-
- Foo.munge()
- ```
-- Use one statement per line. Put function bodies on new lines.
- - This is especially true for functions that return multiple values. It's
- much easier to spot mistakes (and harder to make the mistake in the first
- place) if the function isn't on one line.
- - This is also true for `if` blocks, even if their body is just a return
- statement.
- - This also helps code diff better.
- ```Lua
- -- Good:
- table.sort(stuff, function(a, b)
- local sum = a + b
- return math.abs(sum) > 2
- end)
-
- -- Bad:
- table.sort(stuff, function(a, b) local sum = a + b return math.abs(sum) > 2 end)
- ```
-- Put a space between operators, except when clarifying precedence.
- ```Lua
- -- Good:
- print(5 + 5 * 6^2)
-
- -- Bad:
- print(5+5* 6 ^2)
- ```
-- Put a space after commas in tables and function calls.
- ```Lua
- -- Good:
- local friends = {"bob", "amy", "joe"}
- foo(5, 6, 7)
-
- -- Bad:
- local friends = {"bob","amy" ,"joe"}
- foo(5,6 ,7)
- ```
-- When creating blocks, inline any opening syntax elements.
- ```Lua
- -- Good:
- local foo = {
- bar = 2,
- }
-
- if foo then
- -- do something
- end
-
- -- Bad:
- local foo =
- {
- bar = 2,
- }
-
- if foo
- then
- -- do something
- end
- ```
-- Avoid putting curly braces for tables on their own line. Doing so harms
-readability, since it forces the reader to move to another line in an awkward
-spot in the statement.
- ```Lua
- -- Good:
- local foo = {
- bar = {
- baz = "baz",
- },
- }
-
- frob({
- x = 1,
- })
-
- -- Bad:
- local foo =
- {
- bar =
-
- {
- baz = "baz",
- },
- }
-
- frob(
- {
- x = 1,
- })
-
- -- Exception:
- -- In function calls with large inline tables or functions, sometimes it's
- -- more clear to put braces and functions on new lines:
- foo(
- {
- type = "foo",
- },
- function(something)
- print("Hello," something)
- end
- )
-
- -- As opposed to:
- foo({
- type = "foo",
- }, function(something) -- How do we indent this line?
- print("Hello,", something)
- end)
- ```
-### Newlines in Long Expressions
-- First, try and break up the expression so that no one part is long enough to
-need newlines. This isn't always the right answer, as keeping an expression
-together is sometimes more readable than trying to parse how several small
-expressions relate, but it's worth pausing to consider which case you're in.
-
-- It is often worth breaking up tables and arrays with more than two or three
-keys, or with nested sub-tables, even if it doesn't exceed the line length
-limit. Shorter, simpler tables can stay on one line though.
-
-- Prefer adding the extra trailing comma to the elements within a multiline
-table or array. This makes it easier to add new items or rearrange existing
-items.
-
-- Break dictionary-like tables with more than a couple keys onto multiple lines.
- ```Lua
- -- Good:
- local foo = {type = "foo"}
-
- local bar = {
- type = "bar",
- phrase = "hooray",
- }
-
- -- It's also okay to use multiple lines for a single field
- local baz = {
- type = "baz",
- }
-
- -- Bad:
- local stuff = {hello = "world", hola = "mundo", howdy = "y'all", sup = "homies"}
- ```
-- Break list-like tables onto multiple lines however it makes sense.
- - Make sure to follow the line length limit!
- ```Lua
- local libs = {"fusion", "luau", "class", "maid", "event"}
-
- -- You can break these onto multiple lines, which makes diffs cleaner:
- local libs = {
- "fusion",
- "luau",
- "class",
- "maid",
- "event",
- }
-
- -- We can also group them, if grouping has useful information:
- local libs = {
- "fusion", "luau",
-
- "class", "maid", "event"
- }
- ```
-- For long argument lists or longer, nested tables, prefer to expand all the
-subtables. This makes for the cleanest diffs as further changes are made.
- ```Lua
- local aTable = {
- {
- aLongKey = aLongValue,
- anotherLongKey = anotherLongValue,
- },
- {
- aLongKey = anotherLongValue,
- anotherLongKey = aLongValue,
- },
- }
-
- doSomething(
- {
- aLongKey = aLongValue,
- anotherLongKey = anotherLongValue,
- },
- {
- aLongKey = anotherLongValue,
- anotherLongKey = aLongValue,
- }
- )
- ```
-- For long expressions try and add newlines between logical subunits. If you're
-adding up lots of terms, place each term on its own line. If you have
-parenthesized subexpressions, put each subexpression on a newline.
-
- - Place the operator at the beginning of the new line. This makes it clearer
- at a glance that this is a continuation of the previous line.
-
- - If you have to need to add newlines within a parenthesized subexpression,
- reconsider if you can't use temporary variables. If you still can't, add a
- new level of indentation for the parts of the statement inside the open
- parentheses much like you would with nested tables.
-
- - Don't put extra parentheses around the whole expression. This is necessary
- in Python, but Lua doesn't need anything special to indicate multiline
- expressions.
-
-- For long conditions in `if` statements, put the condition in its own indented
-section and place the `then` on its own line to separate the condition from the
-body of the `if` block. Break up the condition as any other long expression.
- ```Lua
- -- Good:
- if
- someReallyLongCondition
- and someOtherReallyLongCondition
- and somethingElse
- then
- doSomething()
- doSomethingElse()
- end
-
- -- Bad:
- if someReallyLongCondition and someOtherReallyLongCondition
- and somethingElse then
- doSomething()
- doSomethingElse()
- end
-
- if someReallyLongCondition and someOtherReallyLongCondition
- and somethingElse then
- doSomething()
- doSomethingElse()
- end
-
- if someReallyLongCondition and someOtherReallyLongCondition
- and somethingElse then
- doSomething()
- doSomethingElse()
- end
- ```
-
-## Blocks
-
-- Don't use parentheses around the conditions in `if`, `while`, or `repeat`
-blocks. They're not necessary in Lua!
- ```Lua
- if condition then
- -- ...
- end
-
- while condition do
- -- ...
- end
-
- repeat
- -- ...
- until condition
- ```
-- Use `do` blocks if limiting the scope of a variable is useful.
- ```Lua
- local getId
- do
- local lastId = 0
- getId = function()
- lastId += 1
- return lastId
- end
- end
- ```
-
-## Literals
-
-- Use double quotes when declaring single-line string literals.
- - Using single quotes means we have to escape apostrophes, which are often
- useful in English words.
- - Empty strings are easier to identify with double quotes, because in some
- fonts two single quotes might look like a single double quote.
-- Use brackets (`[[` and `]]`) when declaring multi-line string literals.
- - Don't indent multi-line string literals, as the tab characters will be
- interpreted as part of the string!
- ```Lua
- print("Here's a message")
-
- print([[
- This is a longer message, which is designed to span multiple lines to
- demonstrate how multi-line strings work.
- ]])
- ```
-
-## Tables
-
-- Avoid tables with bost list-like and dictionary-like keys.
- - Iterating over these mixed tables is troublesome.
- - If you do end up using mixed tables, make sure to 'unmix' the array part
- from the dictionary part as soon as possible to minimise possible bugs.
-- Iterate over list-like tables with `ipairs` and dictionary-like tables with
-`pairs`.
- - This helps clarify what kind of table we're expecting in a given block of
- code!
-
-## Functions
-
-- Keep the number of arguments to a given function small, preferably 1 or 2.
-- Similarly, keep the number or returned values from a function small.
-- When calling a function, you may only omit parentheses when passing in a
-dictionary-like table.
- - Omitting parentheses when passing in an array-like table can cause some
- confusion, as the curly braces may look like parentheses at a glance, hiding
- that a table is being passed.
- - This isn't a problem with dictionary-like tables. In fact, this is a
- common pattern in Fusion, where constructors for primitives are often
- called this way.
- ```Lua
- -- Good:
- local x = doSomething("home")
- local y = doSomething({1, 2, 3, 4, 5})
- local z = doSomething({foo = 2, bar = "frob"})
-
- -- omitting parentheses is only OK for dictionary-like tables
- local w = doSomething {
- foo = 2,
- bar = "frob"
- }
-
- -- Bad:
- local x = doSomething "home"
- local y = doSomething {1, 2, 3, 4, 5}
- ```
-- Declare named functions using function-prefix syntax. Non-member functions
-should always be local.
- - An exception can be made for late-initializing functions, for example in
- conditionals.
- ```Lua
- -- Good:
- local function add(a, b)
- return a + b
- end
-
- -- Bad:
- function add(a, b)
- return a + b
- end
-
- local add = function(a, b)
- return a + b
- end
-
- -- Exception:
- local doSomething
-
- if CONDITION then
- function doSomething()
- -- Version of doSomething with CONDITION enabled
- end
- else
- function doSomething()
- -- Version of doSomething with CONDITION disabled
- end
- end
- ```
-- When declaring a function inside a table, use function-prefix syntax. Use
-a dot (`.`) or colon (`:`) to denote intended calling convention.
- ```Lua
- -- Good:
- -- This function should be called as Frobulator.new()
- function Frobulator.new()
- return {}
- end
-
- -- This function should be called as Frobulator:frob()
- function Frobulator:frob()
- print("Frobbing", self)
- end
-
- -- Bad:
- function Frobulator.garb(self)
- print("Frobbing", self)
- end
-
- Frobulator.jarp = function()
- return {}
- end
- ```
-
-## Comments
-
-- Wrap comments to 80 columns wide.
- - It's easier to read comments with shorter lines, but fitting code into 80
- columns can be challenging.
-- Use single line comments for inline notes.
- - If the comment spans multiple lines, use multiple single line comments.
- ```Lua
- -- This condition is really important because the world would blow up if it
- -- were missing.
- if not foo then
- stopWorldFromBlowingUp()
- end
- ```
-- Use block comments for documenting items:
- - Use a block comment at the top of files to describe their purpose.
- - Use a block comment before functions or objects to describe their intent.
- ```Lua
- --[[
- Shuts off the cosmic moon ray immediately.
-
- Should only be called within 15 minutes of midnight Mountain Standard
- Time, or the cosmic moon ray may be damaged.
- ]]
- local function stopCosmicMoonRay()
- end
- ```
-- Comments should focus on *why* code is written a certain way, instead of
-*what* the code is doing.
- ```Lua
- -- Good:
- -- Without this condition, the aircraft hangar would fill up with water.
- if waterLevelTooHigh() then
- drainHangar()
- end
-
- -- Bad:
- -- Check if the water level is too high.
- if waterLevelTooHigh() then
- -- Drain the hangar
- drainHangar()
- end
- ```
-- No section comments.
- - Comments that only exist to break up a large file are a code smell; you
- probably need to find some way to make your file smaller instead of working
- around that problem with section comments.
- - Comments that only exist to demark already obvious groupings of code
- (e.g. `--- VARIABLES ---`) and overly stylized comments can actually make
- the code harder to read, not easier.
- - Additionally, when writing section headers, you (and anyone else editing
- the file later) have to be thorough to avoid confusing the reader with
- questions of where sections end.
- - Some examples of other ways of breaking up files:
- - Move inner classes and static functions into their own files, which
- aren't included in the public API. This also makes testing those classes
- and functions easier.
- - Check if there are any existing libraries that can simplify your code.
- If you're writing something and think that you could make part of this
- into a library, there's a good chance someone already has.
- - If you can't break the file up, and still feel like you need section
- headings, consider these alternatives:
- - If you want to put a section header on a group of functions, put that
- information in a block comment attached to the first function in that
- section. You should still make sure the comment is about the function
- its attached to, but it can also include information about the section
- as a whole. Try and write the comment in a way that makes it clear
- what's included in the section.
- ```Lua
- --[[
- All of the readX functions return the next token from the string
- passed in to the Reader or returns nil if the next token doesn't
- match the type the function is trying to read.
-
- local test = "123 ABC"
- i = reader:readInt()
- print(i, ",", test.remaining) -- 123 , ABC
-
- readInt reads an integer, positive or negative.
- ]]
- function Reader:readInt() -- ...
-
- -- readFloat reads a floating point number, but does not accept
- -- scientific notation
- function Reader:readFloat() -- ...
- ```
- - The same can be done for a group of variables in some cases. All the
- same caveats apply though, and you have to consider whether one block
- comment or a normal comment on each variable (or even using just
- whitespace to separate groups) would be more readable.
- - General organization of your code can aid readibility while making
- logical sections more obvious as well. Module level variables and
- functions can appear in any order, so you can sometimes put a group of
- variables above a group of functions to make a section.
-
-## Naming
-
-- Spell out words fully! Abbreviations generally make code easier to write, but
-harder to read.
- - Make sure that names don't get too long, however! Extremely long names can
- also be detrimental to code readability.
-- Avoid single-letter names; names should be adequately descriptive.
- - An exceptions to this rule are coordinates, e.g. `x`, `y` and `z`
- - In code working with multiple coordinate spaces (e.g. object and world
- space), or a combination of Offset and Scale (for UDims), this is less
- acceptable. Prefer prefixed names in those cases, for example `objectX`
- and `worldY`.
- - Another exception is generics in typed Luau, e.g. `type Foo = () -> T`
- ```Lua
- -- Good:
- local function isFrob(value)
- return tostring(value) == "frob"
- end
-
- for index, value in pairs(garb) do
- print(index, "=", value)
- end
-
- -- Bad:
- local function isFrob(x)
- return tostring(x) == "frob"
- end
-
- for i, v in pairs(garb) do
- print(i, "=", v)
- end
- ```
-- Use `PascalCase` for all Roblox APIs.
- - `camelCase` APIs are mostly deprecated, but still work for now.
-- Use `PascalCase` for enum-like objects.
-- Use `PascalCase` for named Luau type definitions.
-- Use `PascalCase` for functions that construct class objects - this is in line
-with how classes are conventionally named:
- ```Lua
- -- Good:
- local foo = State(5)
- local bar = Maid()
-
- -- Bad:
- local foo = state(5)
- local bar = maid()
- ```
-- Use `camelCase` for variables, member values and functions.
-- Use `LOUD_SNAKE_CASE` for constants.
-- For acronyms within names, don't capitalise the whole thing. For example,
-`aJsonVariable` or `MakeHttpCall`.
- - The exception to this is when the abbreviation represents a set. For
- example, in `myRGBValue` or `GetXYZ`. In those cases, `RGB` should be
- treated as an abbreviation of 'Red Green Blue' and not as an acronym.
-- If a member of a class is private, prefix it with one underscore, for example
-`_foob`.
- - Lua does not have visibility rules, but using underscores helps make
- private access stand out.
-- A file's name should match the name of whatever it exports.
- - If your module exports a single function named `doSomething`, the file
- should be named `doSomething.lua`.
-
-## Yielding
-
-Don't call yielding functions on the main thread. Wrap them in `coroutine.wrap`
-or `delay`, and consider exposing a Promise or Promise-like async interface for
-your own functions.
-
-Unintended yielding can cause hard-to-track data races. Simple code
-involving callbacks can cause confusing bugs if the input callback yields:
-```Lua
-local value = 0
-
-local function doSomething(callback)
- local newValue = value + 1
- callback(newValue)
- value = newValue
-end
-```
-
-Similarly, if a callback is not allowed to yield, your code should check that it
-doesn't yield, so the error can be caught early on. Fusion provides utility
-functions to assert a callback doesn't yield while it's running.
-
-## Error Handling
-
-When writing functions that are expected to fail sometimes, return
-`success, result`, use a `Result` type, or use an async primitive that encodes
-failure, like `Promise`.
-
-Avoid throwing errors unless your code encounters something that might be a bug -
-errors are not encoded into a function's contract explicitly, so your caller
-isn't forced to consider whether an error will happen, and how any errors
-should be dealt with.
-```Lua
--- Good:
--- type checking should throw an error, since incorrect types is likely a bug
-assert(typeof(number) == "number", "Must pass number to function")
-if foo < 0 then
- error("foo must not be negative")
-end
-
--- Bad:
--- a player running out of money is not typically a bug, so this would be better
--- implemented using `success, result` or similar
-if numCoins < itemPrice then
- error("Player doesn't have enough coins for transaction")
-end
-```
-
-When calling functions that communicate failure by throwing, wrap calls in
-`pcall` and make it clear via comment what kinds of errors you're expecting to
-handle.
-
-## Logging
-
-Except for debugging, any code in Fusion that throws an error, emits a warning
-or prints a message should do so using Fusion's logging utilities. These logging
-utilities add extra information to the message, so the user of the library can
-easily find more information about where they're coming from.
-
-These logging utilities work with message IDs rather than plain text; if you
-need to log a new kind of message, add it to the list of messages under a new
-message ID.
-
-Generally, you should avoid reusing message IDs that are used in other areas of
-the library. Message IDs should be limited to a small area, so the documentation
-for them can provide more specific details about what's going on, which helps
-debugging efforts.
-
-When adding a new message to the list of messages, make sure to document the new
-message in Fusion's 'Errors & Messages' section of the API Reference.
-
-## General Roblox Best Practices
-- All services should be referenced using `GetService` at the top of the file.
-- When importing a module, use the name of the module for its variable name.
\ No newline at end of file
diff --git a/test-runner.project.json b/test-runner.project.json
index 7c51d64ad..8bab1e91d 100644
--- a/test-runner.project.json
+++ b/test-runner.project.json
@@ -6,19 +6,15 @@
"ReplicatedStorage": {
"$className": "ReplicatedStorage",
"Fusion": {
- "$path": "src"
- },
- "FusionTest": {
- "$path": "test"
+ "$path": "default.project.json"
}
},
- "StarterPlayer": {
- "$className": "StarterPlayer",
- "StarterPlayerScripts": {
- "$className": "StarterPlayerScripts",
- "$path": "test-runner"
+ "ServerScriptService": {
+ "$className": "ServerScriptService",
+ "FusionTest": {
+ "$path": "test"
}
- }
+ }
}
}
\ No newline at end of file
diff --git a/test-runner/Run.client.lua b/test-runner/Run.client.lua
deleted file mode 100644
index a46b43601..000000000
--- a/test-runner/Run.client.lua
+++ /dev/null
@@ -1,21 +0,0 @@
-local ReplicatedStorage = game:GetService("ReplicatedStorage")
-local StarterPlayerScripts = game:GetService("StarterPlayer").StarterPlayerScripts
-
-local TestEZ = require(StarterPlayerScripts.TestEZ)
-
-local RUN_TESTS = true
-
--- run unit tests
-if RUN_TESTS then
- print("Running unit tests...")
- local External = require(ReplicatedStorage.Fusion.External)
- External.unitTestSilenceNonFatal = true
- local data = TestEZ.TestBootstrap:run({
- ReplicatedStorage.FusionTest
- })
- External.unitTestSilenceNonFatal = false
-
- if data.failureCount > 0 then
- return
- end
-end
\ No newline at end of file
diff --git a/test/Animation/Tween.spec.lua b/test/Animation/Tween.spec.lua
deleted file mode 100644
index a511f4baa..000000000
--- a/test/Animation/Tween.spec.lua
+++ /dev/null
@@ -1,56 +0,0 @@
-local Package = game:GetService("ReplicatedStorage").Fusion
-local Value = require(Package.State.Value)
-local Tween = require(Package.Animation.Tween)
-local New = require(Package.Instances.New)
-
-return function()
- -- it("should construct a Tween object with default TweenInfo", function()
- -- local followerState = Value(1)
- -- local tween = Tween(followerState)
-
- -- expect(tween).to.be.a("table")
- -- expect(tween.type).to.equal("State")
- -- expect(tween.kind).to.equal("Tween")
- -- end)
-
- -- it("should not construct a Tween object with invalid TweenInfo", function()
- -- local followerState = Value(1)
-
- -- local incorrectTweenInfo = 5
- -- expect(function() Tween(followerState, incorrectTweenInfo) end).to.throw()
-
- -- local incorrectTweenInfoState = Value(5)
- -- expect(function() Tween(followerState, incorrectTweenInfoState) end).to.throw()
- -- end)
-
- -- it("should construct a Tween object with valid TweenInfo", function()
- -- local followerState = Value(1)
-
- -- local tweenInfo = TweenInfo.new()
- -- local normalInfotween = Tween(followerState, tweenInfo)
- -- expect(normalInfotween).to.be.a("table")
- -- expect(normalInfotween.type).to.equal("State")
- -- expect(normalInfotween.kind).to.equal("Tween")
-
- -- local stateTweenInfo = Value(TweenInfo.new())
- -- local stateTween = Tween(followerState, stateTweenInfo)
- -- expect(stateTween).to.be.a("table")
- -- expect(stateTween.type).to.equal("State")
- -- expect(stateTween.kind).to.equal("Tween")
- -- end)
-
- -- it("should update when it's watched state updates", function()
- -- local followerState = Value(UDim2.fromScale(1, 1))
-
- -- local tween = Tween(followerState, TweenInfo.new(0.1))
- -- local testInstance = New(scope, "Frame") { Size = tween }
-
- -- followerState:set(UDim2.fromScale(0.5, 0.5))
-
- -- -- wait for the tween to finish
- -- task.wait(0.5)
-
- -- expect(testInstance.Size.X.Scale).to.equal(0.5)
- -- expect(testInstance.Size.Y.Scale).to.equal(0.5)
- -- end)
-end
\ No newline at end of file
diff --git a/test/Instances/Ref.spec.lua b/test/Instances/Ref.spec.lua
deleted file mode 100644
index 5208255b0..000000000
--- a/test/Instances/Ref.spec.lua
+++ /dev/null
@@ -1,20 +0,0 @@
-local Package = game:GetService("ReplicatedStorage").Fusion
-local New = require(Package.Instances.New)
-local Ref = require(Package.Instances.Ref)
-local Value = require(Package.State.Value)
-local peek = require(Package.State.peek)
-local doCleanup = require(Package.Memory.doCleanup)
-
-return function()
- it("should set State objects passed as [Ref]", function()
- local scope = {}
- local refValue = Value(scope, nil)
-
- local child = New(scope, "Folder") {
- [Ref] = refValue
- }
-
- expect(peek(refValue)).to.equal(child)
- doCleanup(scope)
- end)
-end
diff --git a/test/Memory/scoped.spec.lua b/test/Memory/scoped.spec.lua
deleted file mode 100644
index 8b7a5472f..000000000
--- a/test/Memory/scoped.spec.lua
+++ /dev/null
@@ -1,72 +0,0 @@
-local Package = game:GetService("ReplicatedStorage").Fusion
-local scoped = require(Package.Memory.scoped)
-
-return function()
- -- it("should accept zero arguments", function()
- -- local merged = merge()
-
- -- expect(merged).to.be.a("table")
- -- expect(#merged).to.equal(0)
- -- end)
-
- -- it("should clone single arguments", function()
- -- local original = {foo = "FOO", bar = "BAR", baz = "BAZ"}
- -- local merged = merge(original)
-
- -- expect(merged).to.be.a("table")
- -- expect(merged).to.never.equal(original)
- -- for key, value in original do
- -- expect(merged[key]).to.equal(value)
- -- end
- -- end)
-
- -- it("should merge two arguments", function()
- -- local originalA = {foo = "FOO", bar = "BAR", baz = "BAZ"}
- -- local originalB = {frob = "FROB", garb = "GARB", grok = "GROK"}
- -- local merged = merge(originalA, originalB)
-
- -- expect(merged).to.be.a("table")
- -- for _, original in {originalA, originalB} do
- -- expect(merged).to.never.equal(original)
- -- for key, value in original do
- -- expect(merged[key]).to.equal(value)
- -- end
- -- for key, value in original do
- -- expect(merged[key]).to.equal(value)
- -- end
- -- for key, value in original do
- -- expect(merged[key]).to.equal(value)
- -- end
- -- end
- -- end)
-
- -- it("should merge three arguments", function()
- -- local originalA = {foo = "FOO", bar = "BAR", baz = "BAZ"}
- -- local originalB = {frob = "FROB", garb = "GARB", grok = "GROK"}
- -- local originalC = {grep = "GREP", bork = "BORK", grum = "GRUM"}
- -- local merged = merge(originalA, originalB, originalC)
-
- -- expect(merged).to.be.a("table")
- -- for _, original in {originalA, originalB, originalC} do
- -- expect(merged).to.never.equal(original)
- -- for key, value in original do
- -- expect(merged[key]).to.equal(value)
- -- end
- -- for key, value in original do
- -- expect(merged[key]).to.equal(value)
- -- end
- -- for key, value in original do
- -- expect(merged[key]).to.equal(value)
- -- end
- -- end
- -- end)
-
- -- it("should error on collision", function()
- -- expect(function()
- -- local originalA = {foo = "FOO", bar = "BAR", baz = "BAZ"}
- -- local originalB = {frob = "FROB", garb = "GARB", grok = "GROK"}
- -- local originalC = {grep = "GREP", grok = "GROK", grum = "GRUM"}
- -- merge(originalA, originalB, originalC)
- -- end).to.throw("mergeConflict")
- -- end)
-end
\ No newline at end of file
diff --git a/test/Animation/springCoefficients.spec.lua b/test/Spec/Animation/springCoefficients.spec.lua
similarity index 81%
rename from test/Animation/springCoefficients.spec.lua
rename to test/Spec/Animation/springCoefficients.spec.lua
index 107c8f622..5db45f27c 100644
--- a/test/Animation/springCoefficients.spec.lua
+++ b/test/Spec/Animation/springCoefficients.spec.lua
@@ -1,8 +1,18 @@
-local Package = game:GetService("ReplicatedStorage").Fusion
-local springCoefficients = require(Package.Animation.springCoefficients)
+--!strict
+--!nolint LocalUnused
+local task = nil -- Disable usage of Roblox's task scheduler
+
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local Fusion = ReplicatedStorage.Fusion
+
+local springCoefficients = require(Fusion.Animation.springCoefficients)
return function()
+ local it = getfenv().it
+
it("should return the identity matrix for zero time", function()
+ local expect = getfenv().expect
+
local posPos, posVel, velPos, velVel = springCoefficients(0, 5.1, 3.4)
expect(posPos).to.equal(1)
@@ -12,6 +22,8 @@ return function()
end)
it("should return the identity matrix for zero speed", function()
+ local expect = getfenv().expect
+
local posPos, posVel, velPos, velVel = springCoefficients(5.1, 3.4, 0)
expect(posPos).to.equal(1)
@@ -28,6 +40,8 @@ return function()
local ERROR_MARGIN = 0.00001
it("should return reasonable underdamped values", function()
+ local expect = getfenv().expect
+
local posPos, posVel, velPos, velVel = springCoefficients(3.6, 0.2, 6.3)
expect(posPos).to.be.near(-0.010932478209024278, ERROR_MARGIN)
@@ -37,6 +51,8 @@ return function()
end)
it("should return reasonable critically damped values", function()
+ local expect = getfenv().expect
+
local posPos, posVel, velPos, velVel = springCoefficients(0.24, 1, 3.6)
expect(posPos).to.be.near(0.7856253267423104, ERROR_MARGIN)
@@ -46,6 +62,8 @@ return function()
end)
it("should return reasonable overdamped values", function()
+ local expect = getfenv().expect
+
local posPos, posVel, velPos, velVel = springCoefficients(1.74, 8.4, 7.2)
expect(posPos).to.be.near(0.4748290157123269, ERROR_MARGIN)
diff --git a/test/Instances/Attribute.spec.lua b/test/Spec/Instances/Attribute.spec.lua
similarity index 59%
rename from test/Instances/Attribute.spec.lua
rename to test/Spec/Instances/Attribute.spec.lua
index c9f3c4bfe..ab4a13ac7 100644
--- a/test/Instances/Attribute.spec.lua
+++ b/test/Spec/Instances/Attribute.spec.lua
@@ -1,12 +1,21 @@
-local Package = game:GetService("ReplicatedStorage").Fusion
+--!strict
+--!nolint LocalUnused
+local task = nil -- Disable usage of Roblox's task scheduler
-local New = require(Package.Instances.New)
-local Attribute = require(Package.Instances.Attribute)
-local Value = require(Package.State.Value)
-local doCleanup = require(Package.Memory.doCleanup)
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local Fusion = ReplicatedStorage.Fusion
+
+local New = require(Fusion.Instances.New)
+local Attribute = require(Fusion.Instances.Attribute)
+local Value = require(Fusion.State.Value)
+local doCleanup = require(Fusion.Memory.doCleanup)
return function()
+ local it = getfenv().it
+
it("creates attributes (constant)", function()
+ local expect = getfenv().expect
+
local scope = {}
local child = New(scope, "Folder") {
[Attribute "Foo"] = "Bar"
@@ -16,6 +25,8 @@ return function()
end)
it("creates attributes (state)", function()
+ local expect = getfenv().expect
+
local scope = {}
local attributeValue = Value(scope, "Bar")
local child = New(scope, "Folder") {
@@ -25,6 +36,8 @@ return function()
end)
it("updates attributes when state objects are updated", function()
+ local expect = getfenv().expect
+
local scope = {}
local attributeValue = Value(scope, "Bar")
local child = New(scope, "Folder") {
@@ -32,20 +45,7 @@ return function()
}
expect(child:GetAttribute("Foo")).to.equal("Bar")
attributeValue:set("Baz")
- task.wait()
expect(child:GetAttribute("Foo")).to.equal("Baz")
doCleanup(scope)
end)
-
- it("defers attribute changes", function()
- local scope = {}
- local value = Value(scope, "Bar")
- local child = New(scope, "Folder") {
- [Attribute "Foo"] = value
- }
- value:set("Baz")
- expect(child:GetAttribute("Foo")).to.equal("Bar")
- task.wait()
- expect(child:GetAttribute("Foo")).to.equal("Baz")
- end)
end
diff --git a/test/Instances/AttributeChange.spec.lua b/test/Spec/Instances/AttributeChange.spec.lua
similarity index 61%
rename from test/Instances/AttributeChange.spec.lua
rename to test/Spec/Instances/AttributeChange.spec.lua
index 06a82e49e..3dedcb84d 100644
--- a/test/Instances/AttributeChange.spec.lua
+++ b/test/Spec/Instances/AttributeChange.spec.lua
@@ -1,11 +1,21 @@
-local Package = game:GetService("ReplicatedStorage").Fusion
-local New = require(Package.Instances.New)
-local Attribute = require(Package.Instances.Attribute)
-local AttributeChange = require(Package.Instances.AttributeChange)
-local doCleanup = require(Package.Memory.doCleanup)
+--!strict
+--!nolint LocalUnused
+local task = nil -- Disable usage of Roblox's task scheduler
+
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local Fusion = ReplicatedStorage.Fusion
+
+local New = require(Fusion.Instances.New)
+local Attribute = require(Fusion.Instances.Attribute)
+local AttributeChange = require(Fusion.Instances.AttributeChange)
+local doCleanup = require(Fusion.Memory.doCleanup)
return function()
+ local it = getfenv().it
+
it("should connect attribute change handlers", function()
+ local expect = getfenv().expect
+
local scope = {}
local changeCount = 0
local child = New(scope, "Folder") {
@@ -16,12 +26,13 @@ return function()
}
child:SetAttribute("Foo", "Baz")
- task.wait()
expect(changeCount).never.to.equal(0)
doCleanup(scope)
end)
it("should pass the updated value as an argument", function()
+ local expect = getfenv().expect
+
local scope = {}
local updatedValue = ""
local child = New(scope, "Folder") {
@@ -31,15 +42,16 @@ return function()
}
child:SetAttribute("Foo", "Baz")
- task.wait()
expect(updatedValue).to.equal("Baz")
doCleanup(scope)
end)
it("should error when given an invalid handler", function()
+ local expect = getfenv().expect
+
expect(function()
local scope = {}
- local child = New(scope, "Folder") {
+ New(scope, "Folder") {
[AttributeChange "Foo"] = 0
}
doCleanup(scope)
diff --git a/test/Instances/AttributeOut.spec.lua b/test/Spec/Instances/AttributeOut.spec.lua
similarity index 66%
rename from test/Instances/AttributeOut.spec.lua
rename to test/Spec/Instances/AttributeOut.spec.lua
index 73cc1ea7a..8928aaa35 100644
--- a/test/Instances/AttributeOut.spec.lua
+++ b/test/Spec/Instances/AttributeOut.spec.lua
@@ -1,13 +1,23 @@
-local Package = game:GetService("ReplicatedStorage").Fusion
-local New = require(Package.Instances.New)
-local Attribute = require(Package.Instances.Attribute)
-local AttributeOut = require(Package.Instances.AttributeOut)
-local Value = require(Package.State.Value)
-local peek = require(Package.State.peek)
-local doCleanup = require(Package.Memory.doCleanup)
+--!strict
+--!nolint LocalUnused
+local task = nil -- Disable usage of Roblox's task scheduler
+
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local Fusion = ReplicatedStorage.Fusion
+
+local New = require(Fusion.Instances.New)
+local Attribute = require(Fusion.Instances.Attribute)
+local AttributeOut = require(Fusion.Instances.AttributeOut)
+local Value = require(Fusion.State.Value)
+local peek = require(Fusion.State.peek)
+local doCleanup = require(Fusion.Memory.doCleanup)
return function()
+ local it = getfenv().it
+
it("should update when attributes are changed externally", function()
+ local expect = getfenv().expect
+
local scope = {}
local attributeValue = Value(scope, nil)
local child = New(scope, "Folder") {
@@ -16,27 +26,29 @@ return function()
expect(peek(attributeValue)).to.equal(nil)
child:SetAttribute("Foo", "Bar")
- task.wait()
expect(peek(attributeValue)).to.equal("Bar")
doCleanup(scope)
end)
it("should update when state objects linked update", function()
+ local expect = getfenv().expect
+
local scope = {}
local attributeValue = Value(scope, "Foo")
local attributeOutValue = Value(scope, nil)
- local child = New(scope, "Folder") {
+ New(scope, "Folder") {
[Attribute "Foo"] = attributeValue,
[AttributeOut "Foo"] = attributeOutValue
}
expect(peek(attributeOutValue)).to.equal("Foo")
attributeValue:set("Bar")
- task.wait()
expect(peek(attributeOutValue)).to.equal("Bar")
doCleanup(scope)
end)
it("should work with two-way connections", function()
+ local expect = getfenv().expect
+
local scope = {}
local attributeValue = Value(scope, "Bar")
local child = New(scope, "Folder") {
@@ -46,10 +58,8 @@ return function()
expect(peek(attributeValue)).to.equal("Bar")
attributeValue:set("Baz")
- task.wait()
expect(child:GetAttribute("Foo")).to.equal("Baz")
child:SetAttribute("Foo", "Biff")
- task.wait()
expect(peek(attributeValue)).to.equal("Biff")
doCleanup(scope)
end)
diff --git a/test/Instances/Children.spec.lua b/test/Spec/Instances/Children.spec.lua
similarity index 82%
rename from test/Instances/Children.spec.lua
rename to test/Spec/Instances/Children.spec.lua
index 2396b6b0d..f7765aecd 100644
--- a/test/Instances/Children.spec.lua
+++ b/test/Spec/Instances/Children.spec.lua
@@ -1,11 +1,21 @@
-local Package = game:GetService("ReplicatedStorage").Fusion
-local New = require(Package.Instances.New)
-local Children = require(Package.Instances.Children)
-local Value = require(Package.State.Value)
-local doCleanup = require(Package.Memory.doCleanup)
+--!strict
+--!nolint LocalUnused
+local task = nil -- Disable usage of Roblox's task scheduler
+
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local Fusion = ReplicatedStorage.Fusion
+
+local New = require(Fusion.Instances.New)
+local Children = require(Fusion.Instances.Children)
+local Value = require(Fusion.State.Value)
+local doCleanup = require(Fusion.Memory.doCleanup)
return function()
+ local it = getfenv().it
+
it("should assign single children to instances", function()
+ local expect = getfenv().expect
+
local scope = {}
local ins = New(scope, "Folder") {
Name = "Bob",
@@ -20,6 +30,8 @@ return function()
end)
it("should assign multiple children to instances", function()
+ local expect = getfenv().expect
+
local scope = {}
local ins = New(scope, "Folder") {
Name = "Bob",
@@ -44,6 +56,8 @@ return function()
end)
it("should flatten children to be assigned", function()
+ local expect = getfenv().expect
+
local scope = {}
local ins = New(scope, "Folder") {
Name = "Bob",
@@ -51,11 +65,11 @@ return function()
[Children] = {
New(scope, "Folder") {
Name = "Fred"
- },
+ } :: any,
{
New(scope, "Folder") {
Name = "George"
- },
+ } :: any,
{
New(scope, "Folder") {
Name = "Harry"
@@ -72,6 +86,8 @@ return function()
end)
it("should bind State objects passed as children", function()
+ local expect = getfenv().expect
+
local scope = {}
local child1 = New(scope, "Folder") {}
local child2 = New(scope, "Folder") {}
@@ -89,14 +105,12 @@ return function()
expect(child1.Parent).to.equal(parent)
children:set({child2, child3})
- task.wait()
expect(child1.Parent).to.equal(nil)
expect(child2.Parent).to.equal(parent)
expect(child3.Parent).to.equal(parent)
children:set({child1, child2, child3, child4})
- task.wait()
expect(child1.Parent).to.equal(parent)
expect(child2.Parent).to.equal(parent)
@@ -106,6 +120,8 @@ return function()
end)
it("should defer updates to State children", function()
+ local expect = getfenv().expect
+
local scope = {}
local child1 = New(scope, "Folder") {}
local child2 = New(scope, "Folder") {}
@@ -125,14 +141,14 @@ return function()
expect(child1.Parent).to.equal(parent)
expect(child2.Parent).to.equal(nil)
- task.wait()
-
expect(child1.Parent).to.equal(nil)
expect(child2.Parent).to.equal(parent)
doCleanup(scope)
end)
it("should recursively bind State children", function()
+ local expect = getfenv().expect
+
local scope = {}
local child1 = New(scope, "Folder") {}
local child2 = New(scope, "Folder") {}
@@ -140,10 +156,10 @@ return function()
local child4 = New(scope, "Folder") {}
local children = Value(scope, {
- child1,
+ child1 :: any,
Value(scope, child2),
Value(scope, {
- child3,
+ child3 :: any,
Value(scope, Value(scope, child4))
})
})
@@ -162,10 +178,12 @@ return function()
end)
it("should allow for State children to be nil", function()
+ local expect = getfenv().expect
+
local scope = {}
local child = New(scope, "Folder") {}
- local children = Value(scope, nil)
+ local children = Value(scope, nil :: Instance?)
local parent = New(scope, "Folder") {
[Children] = {
@@ -176,12 +194,10 @@ return function()
expect(child.Parent).to.equal(nil)
children:set(child)
- task.wait()
expect(child.Parent).to.equal(parent)
children:set(nil)
- task.wait()
expect(child.Parent).to.equal(nil)
doCleanup(scope)
diff --git a/test/Instances/Hydrate.spec.lua b/test/Spec/Instances/Hydrate.spec.lua
similarity index 52%
rename from test/Instances/Hydrate.spec.lua
rename to test/Spec/Instances/Hydrate.spec.lua
index 66a5adfe0..3112e9795 100644
--- a/test/Instances/Hydrate.spec.lua
+++ b/test/Spec/Instances/Hydrate.spec.lua
@@ -1,9 +1,19 @@
-local Package = game:GetService("ReplicatedStorage").Fusion
-local Hydrate = require(Package.Instances.Hydrate)
-local doCleanup = require(Package.Memory.doCleanup)
+--!strict
+--!nolint LocalUnused
+local task = nil -- Disable usage of Roblox's task scheduler
+
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local Fusion = ReplicatedStorage.Fusion
+
+local Hydrate = require(Fusion.Instances.Hydrate)
+local doCleanup = require(Fusion.Memory.doCleanup)
return function()
+ local it = getfenv().it
+
it("should return the instance it was passed", function()
+ local expect = getfenv().expect
+
local scope = {}
local ins = Instance.new("Folder")
expect(Hydrate(scope, ins) {}).to.equal(ins)
@@ -11,6 +21,8 @@ return function()
end)
it("should apply properties to the instance", function()
+ local expect = getfenv().expect
+
local scope = {}
local ins = Instance.new("Folder")
Hydrate(scope, ins) {
diff --git a/test/Instances/New.spec.lua b/test/Spec/Instances/New.spec.lua
similarity index 56%
rename from test/Instances/New.spec.lua
rename to test/Spec/Instances/New.spec.lua
index 43ef6098d..b5754010f 100644
--- a/test/Instances/New.spec.lua
+++ b/test/Spec/Instances/New.spec.lua
@@ -1,10 +1,20 @@
-local Package = game:GetService("ReplicatedStorage").Fusion
-local New = require(Package.Instances.New)
-local defaultProps = require(Package.Instances.defaultProps)
-local doCleanup = require(Package.Memory.doCleanup)
+--!strict
+--!nolint LocalUnused
+local task = nil -- Disable usage of Roblox's task scheduler
+
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local Fusion = ReplicatedStorage.Fusion
+
+local New = require(Fusion.Instances.New)
+local defaultProps = require(Fusion.Instances.defaultProps)
+local doCleanup = require(Fusion.Memory.doCleanup)
return function()
+ local it = getfenv().it
+
it("should create a new instance", function()
+ local expect = getfenv().expect
+
local scope = {}
local ins = New (scope, "Frame") {}
expect(typeof(ins) == "Instance").to.be.ok()
@@ -12,6 +22,8 @@ return function()
end)
it("should throw for non-existent class types", function()
+ local expect = getfenv().expect
+
expect(function()
local scope = {}
New (scope, "This is not a valid class type") {}
@@ -20,11 +32,13 @@ return function()
end)
it("should apply 'sensible default' properties", function()
+ local expect = getfenv().expect
+
for className, defaults in pairs(defaultProps) do
local scope = {}
local ins = New (scope, className) {}
for propName, propValue in pairs(defaults) do
- expect(ins[propName]).to.equal(propValue)
+ expect((ins :: any)[propName]).to.equal(propValue)
end
doCleanup(scope)
end
diff --git a/test/Instances/OnChange.spec.lua b/test/Spec/Instances/OnChange.spec.lua
similarity index 71%
rename from test/Instances/OnChange.spec.lua
rename to test/Spec/Instances/OnChange.spec.lua
index b905951f7..634e4798e 100644
--- a/test/Instances/OnChange.spec.lua
+++ b/test/Spec/Instances/OnChange.spec.lua
@@ -1,10 +1,20 @@
-local Package = game:GetService("ReplicatedStorage").Fusion
-local New = require(Package.Instances.New)
-local OnChange = require(Package.Instances.OnChange)
-local doCleanup = require(Package.Memory.doCleanup)
+--!strict
+--!nolint LocalUnused
+local task = nil -- Disable usage of Roblox's task scheduler
+
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local Fusion = ReplicatedStorage.Fusion
+
+local New = require(Fusion.Instances.New)
+local OnChange = require(Fusion.Instances.OnChange)
+local doCleanup = require(Fusion.Memory.doCleanup)
return function()
+ local it = getfenv().it
+
it("should connect property change handlers", function()
+ local expect = getfenv().expect
+
local scope = {}
local fires = 0
local ins = New(scope, "Folder") {
@@ -16,12 +26,13 @@ return function()
}
ins.Name = "Bar"
- task.wait()
expect(fires).never.to.equal(0)
doCleanup(scope)
end)
it("should pass the new value to the handler", function()
+ local expect = getfenv().expect
+
local scope = {}
local arg = nil
local ins = New(scope, "Folder") {
@@ -33,12 +44,13 @@ return function()
}
ins.Name = "Bar"
- task.wait()
expect(arg).to.equal("Bar")
doCleanup(scope)
end)
it("should throw when connecting to non-existent property changes", function()
+ local expect = getfenv().expect
+
local scope = {}
expect(function()
New(scope, "Folder") {
@@ -51,6 +63,8 @@ return function()
end)
it("shouldn't fire property changes during initialisation", function()
+ local expect = getfenv().expect
+
local scope = {}
local fires = 0
local ins = New(scope, "Folder") {
@@ -68,7 +82,6 @@ return function()
local totalFires = fires
ins:Destroy()
- task.wait()
expect(totalFires).to.equal(0)
doCleanup(scope)
end)
diff --git a/test/Instances/OnEvent.spec.lua b/test/Spec/Instances/OnEvent.spec.lua
similarity index 70%
rename from test/Instances/OnEvent.spec.lua
rename to test/Spec/Instances/OnEvent.spec.lua
index 43409057e..4bff20193 100644
--- a/test/Instances/OnEvent.spec.lua
+++ b/test/Spec/Instances/OnEvent.spec.lua
@@ -1,11 +1,21 @@
-local Package = game:GetService("ReplicatedStorage").Fusion
-local New = require(Package.Instances.New)
-local Children = require(Package.Instances.Children)
-local OnEvent = require(Package.Instances.OnEvent)
-local doCleanup = require(Package.Memory.doCleanup)
+--!strict
+--!nolint LocalUnused
+local task = nil -- Disable usage of Roblox's task scheduler
+
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local Fusion = ReplicatedStorage.Fusion
+
+local New = require(Fusion.Instances.New)
+local Children = require(Fusion.Instances.Children)
+local OnEvent = require(Fusion.Instances.OnEvent)
+local doCleanup = require(Fusion.Memory.doCleanup)
return function()
+ local it = getfenv().it
+
it("should connect event handlers", function()
+ local expect = getfenv().expect
+
local scope = {}
local fires = 0
local ins = New(scope, "Folder") {
@@ -19,13 +29,13 @@ return function()
ins.Parent = game
ins:Destroy()
- task.wait()
-
expect(fires).never.to.equal(0)
doCleanup(scope)
end)
it("should throw for non-existent events", function()
+ local expect = getfenv().expect
+
expect(function()
local scope = {}
New(scope, "Folder") {
@@ -38,6 +48,8 @@ return function()
end)
it("should throw for non-event event handlers", function()
+ local expect = getfenv().expect
+
expect(function()
local scope = {}
New(scope, "Folder") {
@@ -50,6 +62,8 @@ return function()
end)
it("shouldn't fire events during initialisation", function()
+ local expect = getfenv().expect
+
local scope = {}
local fires = 0
local ins = New(scope, "Folder") {
@@ -76,8 +90,6 @@ return function()
local totalFires = fires
ins:Destroy()
- task.wait()
-
expect(totalFires).to.equal(0)
doCleanup(scope)
end)
diff --git a/test/Instances/Out.spec.lua b/test/Spec/Instances/Out.spec.lua
similarity index 66%
rename from test/Instances/Out.spec.lua
rename to test/Spec/Instances/Out.spec.lua
index 8b311ff1d..8b2692f58 100644
--- a/test/Instances/Out.spec.lua
+++ b/test/Spec/Instances/Out.spec.lua
@@ -1,12 +1,22 @@
-local Package = game:GetService("ReplicatedStorage").Fusion
-local New = require(Package.Instances.New)
-local Out = require(Package.Instances.Out)
-local Value = require(Package.State.Value)
-local peek = require(Package.State.peek)
-local doCleanup = require(Package.Memory.doCleanup)
+--!strict
+--!nolint LocalUnused
+local task = nil -- Disable usage of Roblox's task scheduler
+
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local Fusion = ReplicatedStorage.Fusion
+
+local New = require(Fusion.Instances.New)
+local Out = require(Fusion.Instances.Out)
+local Value = require(Fusion.State.Value)
+local peek = require(Fusion.State.peek)
+local doCleanup = require(Fusion.Memory.doCleanup)
return function()
+ local it = getfenv().it
+
it("should reflect external property changes", function()
+ local expect = getfenv().expect
+
local scope = {}
local outValue = Value(scope, nil)
@@ -16,29 +26,31 @@ return function()
expect(peek(outValue)).to.equal("Folder")
child.Name = "Mary"
- task.wait()
expect(peek(outValue)).to.equal("Mary")
doCleanup(scope)
end)
it("should reflect property changes from bound state", function()
+ local expect = getfenv().expect
+
local scope = {}
local outValue = Value(scope, nil)
local inValue = Value(scope, "Gabriel")
- local child = New(scope, "Folder") {
+ New(scope, "Folder") {
Name = inValue,
[Out "Name"] = outValue
}
expect(peek(outValue)).to.equal("Gabriel")
inValue:set("Joseph")
- task.wait()
expect(peek(outValue)).to.equal("Joseph")
doCleanup(scope)
end)
it("should support two-way data binding", function()
+ local expect = getfenv().expect
+
local scope = {}
local twoWayValue = Value(scope, "Gabriel")
@@ -49,11 +61,9 @@ return function()
expect(peek(twoWayValue)).to.equal("Gabriel")
twoWayValue:set("Joseph")
- task.wait()
expect(child.Name).to.equal("Joseph")
child.Name = "Elias"
- task.wait()
expect(peek(twoWayValue)).to.equal("Elias")
doCleanup(scope)
end)
diff --git a/test/Spec/Instances/Ref.spec.lua b/test/Spec/Instances/Ref.spec.lua
new file mode 100644
index 000000000..ae0319eaa
--- /dev/null
+++ b/test/Spec/Instances/Ref.spec.lua
@@ -0,0 +1,30 @@
+--!strict
+--!nolint LocalUnused
+local task = nil -- Disable usage of Roblox's task scheduler
+
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local Fusion = ReplicatedStorage.Fusion
+
+local New = require(Fusion.Instances.New)
+local Ref = require(Fusion.Instances.Ref)
+local Value = require(Fusion.State.Value)
+local peek = require(Fusion.State.peek)
+local doCleanup = require(Fusion.Memory.doCleanup)
+
+return function()
+ local it = getfenv().it
+
+ it("should set State objects passed as [Ref]", function()
+ local expect = getfenv().expect
+
+ local scope = {}
+ local refValue = Value(scope, nil)
+
+ local child = New(scope, "Folder") {
+ [Ref] = refValue
+ }
+
+ expect(peek(refValue)).to.equal(child)
+ doCleanup(scope)
+ end)
+end
diff --git a/test/Instances/applyInstanceProps.spec.lua b/test/Spec/Instances/applyInstanceProps.spec.lua
similarity index 76%
rename from test/Instances/applyInstanceProps.spec.lua
rename to test/Spec/Instances/applyInstanceProps.spec.lua
index c977b4d5c..4ecd6d5e6 100644
--- a/test/Instances/applyInstanceProps.spec.lua
+++ b/test/Spec/Instances/applyInstanceProps.spec.lua
@@ -1,10 +1,20 @@
-local Package = game:GetService("ReplicatedStorage").Fusion
-local applyInstanceProps = require(Package.Instances.applyInstanceProps)
-local Value = require(Package.State.Value)
-local doCleanup = require(Package.Memory.doCleanup)
+--!strict
+--!nolint LocalUnused
+local task = nil -- Disable usage of Roblox's task scheduler
+
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local Fusion = ReplicatedStorage.Fusion
+
+local applyInstanceProps = require(Fusion.Instances.applyInstanceProps)
+local Value = require(Fusion.State.Value)
+local doCleanup = require(Fusion.Memory.doCleanup)
return function()
+ local it = getfenv().it
+
it("should assign properties (constant)", function()
+ local expect = getfenv().expect
+
local scope = {}
local instance = Instance.new("Folder")
table.insert(scope, instance)
@@ -18,6 +28,8 @@ return function()
end)
it("should assign properties (state)", function()
+ local expect = getfenv().expect
+
local scope = {}
local value = Value(scope, "Bob")
local instance = Instance.new("Folder")
@@ -30,13 +42,13 @@ return function()
expect(instance.Name).to.equal("Bob")
value:set("Maya")
- task.wait() -- property changes are deferred
-
expect(instance.Name).to.equal("Maya")
doCleanup(scope)
end)
it("should assign Parent (constant)", function()
+ local expect = getfenv().expect
+
local scope = {}
local parent = Instance.new("Folder")
table.insert(scope, parent)
@@ -52,6 +64,8 @@ return function()
end)
it("should assign Parent (state)", function()
+ local expect = getfenv().expect
+
local scope = {}
local parent1 = Instance.new("Folder")
table.insert(scope, parent1)
@@ -68,13 +82,13 @@ return function()
expect(instance.Parent).to.equal(parent1)
value:set(parent2)
- task.wait() -- property changes are deferred
-
expect(instance.Parent).to.equal(parent2)
doCleanup(scope)
end)
it("should throw for non-existent properties (constant)", function()
+ local expect = getfenv().expect
+
expect(function()
local scope = {}
local instance = Instance.new("Folder")
@@ -89,6 +103,8 @@ return function()
end)
it("should throw for non-existent properties (state)", function()
+ local expect = getfenv().expect
+
expect(function()
local scope = {}
local value = Value(scope, true)
@@ -104,6 +120,8 @@ return function()
end)
it("should throw for invalid property types (constant)", function()
+ local expect = getfenv().expect
+
expect(function()
local scope = {}
local instance = Instance.new("Folder")
@@ -118,6 +136,8 @@ return function()
end)
it("should throw for invalid property types (state)", function()
+ local expect = getfenv().expect
+
expect(function()
local scope = {}
local value = Value(scope, Vector3.new())
@@ -133,6 +153,8 @@ return function()
end)
it("should throw for invalid Parent types (constant)", function()
+ local expect = getfenv().expect
+
expect(function()
local scope = {}
local instance = Instance.new("Folder")
@@ -147,6 +169,8 @@ return function()
end)
it("should throw for invalid Parent types (state)", function()
+ local expect = getfenv().expect
+
expect(function()
local scope = {}
local value = Value(scope, Vector3.new())
@@ -162,6 +186,8 @@ return function()
end)
it("should throw for unrecognised keys in the property table", function()
+ local expect = getfenv().expect
+
expect(function()
local scope = {}
local instance = Instance.new("Folder")
@@ -174,44 +200,4 @@ return function()
doCleanup(scope)
end).to.throw("unrecognisedPropertyKey")
end)
-
- it("should defer property changes", function()
- local scope = {}
- local value = Value(scope, "Bob")
- local instance = Instance.new("Folder")
- table.insert(scope, instance)
- applyInstanceProps(
- scope,
- { Name = value },
- instance
- )
- value:set("Maya")
-
- expect(instance.Name).to.equal("Bob")
- task.wait()
- expect(instance.Name).to.equal("Maya")
- doCleanup(scope)
- end)
-
- it("should defer Parent changes", function()
- local scope = {}
- local parent1 = Instance.new("Folder")
- table.insert(scope, parent1)
- local parent2 = Instance.new("Folder")
- table.insert(scope, parent2)
- local value = Value(scope, parent1)
- local instance = Instance.new("Folder")
- table.insert(scope, instance)
- applyInstanceProps(
- scope,
- { Parent = value },
- instance
- )
- value:set(parent2)
-
- expect(instance.Parent).to.equal(parent1)
- task.wait()
- expect(instance.Parent).to.equal(parent2)
- doCleanup(scope)
- end)
end
diff --git a/test/Memory/doCleanup.spec.lua b/test/Spec/Memory/doCleanup.spec.lua
similarity index 74%
rename from test/Memory/doCleanup.spec.lua
rename to test/Spec/Memory/doCleanup.spec.lua
index 7f40dcba4..76a425dda 100644
--- a/test/Memory/doCleanup.spec.lua
+++ b/test/Spec/Memory/doCleanup.spec.lua
@@ -1,9 +1,19 @@
-local Package = game:GetService("ReplicatedStorage").Fusion
-local New = require(Package.Instances.New)
-local doCleanup = require(Package.Memory.doCleanup)
+--!strict
+--!nolint LocalUnused
+local task = nil -- Disable usage of Roblox's task scheduler
+
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local Fusion = ReplicatedStorage.Fusion
+
+local New = require(Fusion.Instances.New)
+local doCleanup = require(Fusion.Memory.doCleanup)
return function()
+ local it = getfenv().it
+
it("should destroy instances", function()
+ local expect = getfenv().expect
+
local instance = New({}, "Folder") {}
-- one of the only reliable ways to test for proper destruction
local conn = instance.AncestryChanged:Connect(function() end)
@@ -14,6 +24,8 @@ return function()
end)
it("should disconnect connections", function()
+ local expect = getfenv().expect
+
local instance = New({}, "Folder") {}
local conn = instance.AncestryChanged:Connect(function() end)
@@ -23,6 +35,8 @@ return function()
end)
it("should invoke callbacks", function()
+ local expect = getfenv().expect
+
local didRun = false
doCleanup(function()
@@ -33,6 +47,8 @@ return function()
end)
it("should invoke :destroy() methods", function()
+ local expect = getfenv().expect
+
local didRun = false
doCleanup({
@@ -45,6 +61,8 @@ return function()
end)
it("should invoke :Destroy() methods", function()
+ local expect = getfenv().expect
+
local didRun = false
doCleanup({
@@ -57,6 +75,8 @@ return function()
end)
it("should clean up contents of arrays", function()
+ local expect = getfenv().expect
+
local numRuns = 0
local function doRun()
@@ -74,18 +94,22 @@ return function()
end)
it("should clean up contents of nested arrays", function()
+ local expect = getfenv().expect
+
local numRuns = 0
local function doRun()
numRuns += 1
end
- doCleanup({{doRun, {doRun, {doRun}}}})
+ doCleanup({{doRun :: any, {doRun :: any, {doRun}}}})
expect(numRuns).to.equal(3)
end)
it("should clean up contents of arrays in reverse order", function()
+ local expect = getfenv().expect
+
local runs = {}
local tasks = {}
@@ -110,6 +134,8 @@ return function()
end)
it("should clean up variadic arguments", function()
+ local expect = getfenv().expect
+
local numRuns = 0
local function doRun()
diff --git a/test/Spec/Memory/scoped.spec.lua b/test/Spec/Memory/scoped.spec.lua
new file mode 100644
index 000000000..617ed86ff
--- /dev/null
+++ b/test/Spec/Memory/scoped.spec.lua
@@ -0,0 +1,90 @@
+--!strict
+--!nolint LocalUnused
+local task = nil -- Disable usage of Roblox's task scheduler
+
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local Fusion = ReplicatedStorage.Fusion
+
+local scoped = require(Fusion.Memory.scoped)
+
+return function()
+ local it = getfenv().it
+
+ it("should accept zero arguments", function()
+ local expect = getfenv().expect
+
+ local scope = scoped()
+
+ expect(scope).to.be.a("table")
+ expect(#scope).to.equal(0)
+ end)
+
+ it("should accept single arguments", function()
+ local expect = getfenv().expect
+
+ local original = {foo = "FOO", bar = "BAR", baz = "BAZ"}
+ local scope = scoped(original)
+
+ expect(scope).to.be.a("table")
+ expect(scope).to.never.equal(original)
+ for key, value in original do
+ expect((scope :: any)[key]).to.equal(value)
+ end
+ end)
+
+ it("should merge two arguments", function()
+ local expect = getfenv().expect
+
+ local originalA = {foo = "FOO", bar = "BAR", baz = "BAZ"}
+ local originalB = {frob = "FROB", garb = "GARB", grok = "GROK"}
+ local scope = scoped(originalA, originalB)
+
+ expect(scope).to.be.a("table")
+ for _, original in {originalA :: any, originalB} do
+ expect(scope).to.never.equal(original)
+ for key, value in original do
+ expect((scope :: any)[key]).to.equal(value)
+ end
+ for key, value in original do
+ expect((scope :: any)[key]).to.equal(value)
+ end
+ for key, value in original do
+ expect((scope :: any)[key]).to.equal(value)
+ end
+ end
+ end)
+
+ it("should merge three arguments", function()
+ local expect = getfenv().expect
+
+ local originalA = {foo = "FOO", bar = "BAR", baz = "BAZ"}
+ local originalB = {frob = "FROB", garb = "GARB", grok = "GROK"}
+ local originalC = {grep = "GREP", bork = "BORK", grum = "GRUM"}
+ local scope = scoped(originalA, originalB, originalC)
+
+ expect(scope).to.be.a("table")
+ for _, original in {originalA :: any, originalB, originalC} do
+ expect(scope).to.never.equal(original)
+ for key, value in original do
+ expect((scope :: any)[key]).to.equal(value)
+ end
+ for key, value in original do
+ expect((scope :: any)[key]).to.equal(value)
+ end
+ for key, value in original do
+ expect((scope :: any)[key]).to.equal(value)
+ end
+ end
+ end)
+
+ it("should error on collision", function()
+ local expect = getfenv().expect
+
+ expect(function()
+ local originalA = {foo = "FOO", bar = "BAR", baz = "BAZ"}
+ local originalB = {frob = "FROB", garb = "GARB", grok = "GROK"}
+ local originalC = {grep = "GREP", grok = "GROK", grum = "GRUM"}
+ scoped(originalA, originalB, originalC)
+ end).to.throw("mergeConflict")
+ end)
+end
\ No newline at end of file
diff --git a/test/State/Computed.spec.lua b/test/Spec/State/Computed.spec.lua
similarity index 78%
rename from test/State/Computed.spec.lua
rename to test/Spec/State/Computed.spec.lua
index 783f525b8..75ac1862a 100644
--- a/test/State/Computed.spec.lua
+++ b/test/Spec/State/Computed.spec.lua
@@ -1,15 +1,25 @@
-local Package = game:GetService("ReplicatedStorage").Fusion
-local Computed = require(Package.State.Computed)
-local Value = require(Package.State.Value)
-local peek = require(Package.State.peek)
-local doCleanup = require(Package.Memory.doCleanup)
+--!strict
+--!nolint LocalUnused
+local task = nil -- Disable usage of Roblox's task scheduler
+
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local Fusion = ReplicatedStorage.Fusion
+
+local Computed = require(Fusion.State.Computed)
+local Value = require(Fusion.State.Value)
+local peek = require(Fusion.State.peek)
+local doCleanup = require(Fusion.Memory.doCleanup)
return function()
+ local it = getfenv().it
+
it("constructs in scopes", function()
+ local expect = getfenv().expect
+
local scope = {}
local computed = Computed(scope, function()
-- intentionally blank
- end)
+ end :: any)
expect(computed).to.be.a("table")
expect(computed.type).to.equal("State")
@@ -20,16 +30,20 @@ return function()
end)
it("is destroyable", function()
+ local expect = getfenv().expect
+
local scope = {}
local computed = Computed(scope, function()
-- intentionally blank
- end)
+ end :: any)
expect(function()
computed:destroy()
end).to.never.throw()
end)
it("computes with constants", function()
+ local expect = getfenv().expect
+
local scope = {}
local computed = Computed(scope, function(use)
return use(5)
@@ -39,8 +53,10 @@ return function()
end)
it("computes with state objects", function()
+ local expect = getfenv().expect
+
local scope = {}
- local dependency = Value(scope, 5)
+ local dependency = Value(scope, 5 :: number | string)
local computed = Computed(scope, function(use)
return use(dependency)
end)
@@ -51,6 +67,8 @@ return function()
end)
it("preserves value on error", function()
+ local expect = getfenv().expect
+
local scope = {}
local dependency = Value(scope, 5)
local computed = Computed(scope, function(use)
@@ -66,19 +84,23 @@ return function()
end)
it("doesn't destroy inner scope on creation", function()
+ local expect = getfenv().expect
+
local scope = {}
local destructed = false
local _ = Computed(scope, function(innerScope)
- table.insert(innerScope, function()
+ table.insert(innerScope :: any, function()
destructed = true
end)
- end)
+ end :: any)
expect(destructed).to.equal(false)
doCleanup(scope)
end)
it("destroys inner scope on update", function()
+ local expect = getfenv().expect
+
local scope = {}
local destructed = {}
local dependency = Value(scope, 1)
@@ -100,6 +122,8 @@ return function()
end)
it("destroys errored values and preserves the last non-error value", function()
+ local expect = getfenv().expect
+
local scope = {}
local numDestructions = {}
local dependency = Value(scope, 1)
@@ -125,13 +149,15 @@ return function()
end)
it("destroys inner scope on destroy", function()
+ local expect = getfenv().expect
+
local scope = {}
local destructed = false
local _ = Computed(scope, function(use, innerScope)
table.insert(innerScope, function()
destructed = true
end)
- end)
+ end :: any)
doCleanup(scope)
expect(destructed).to.equal(true)
end)
diff --git a/test/State/For.spec.lua b/test/Spec/State/For.spec.lua
similarity index 83%
rename from test/State/For.spec.lua
rename to test/Spec/State/For.spec.lua
index 9ac4821c8..a12a524f2 100644
--- a/test/State/For.spec.lua
+++ b/test/Spec/State/For.spec.lua
@@ -1,16 +1,26 @@
-local Package = game:GetService("ReplicatedStorage").Fusion
-local For = require(Package.State.For)
-local Value = require(Package.State.Value)
-local Computed = require(Package.State.Computed)
-local peek = require(Package.State.peek)
-local doCleanup = require(Package.Memory.doCleanup)
+--!strict
+--!nolint LocalUnused
+local task = nil -- Disable usage of Roblox's task scheduler
+
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local Fusion = ReplicatedStorage.Fusion
+
+local For = require(Fusion.State.For)
+local Value = require(Fusion.State.Value)
+local Computed = require(Fusion.State.Computed)
+local peek = require(Fusion.State.peek)
+local doCleanup = require(Fusion.Memory.doCleanup)
return function()
+ local it = getfenv().it
+
it("constructs in scopes", function()
+ local expect = getfenv().expect
+
local scope = {}
local forObject = For(scope, {}, function()
-- intentionally blank
- end)
+ end :: any)
expect(forObject).to.be.a("table")
expect(forObject.type).to.equal("State")
@@ -21,10 +31,12 @@ return function()
end)
it("is destroyable", function()
+ local expect = getfenv().expect
+
local scope = {}
local forObject = For(scope, {}, function()
-- intentionally blank
- end)
+ end :: any)
expect(function()
forObject:destroy()
@@ -32,6 +44,8 @@ return function()
end)
it("processes pairs for constant tables", function()
+ local expect = getfenv().expect
+
local scope = {}
local data = {foo = 1, bar = 2}
local seen = {}
@@ -41,7 +55,7 @@ return function()
local k, v = peek(inputPair).key, peek(inputPair).value
seen[k] = v
return Computed(scope, function(use)
- return {key = string.upper(use(inputPair).key), value = use(inputPair).value * 10}
+ return {key = string.upper(use(inputPair).key), value = (use(inputPair) :: any).value * 10}
end)
end)
expect(numCalls).to.equal(2)
@@ -55,13 +69,15 @@ return function()
end)
it("processes pairs for state tables", function()
+ local expect = getfenv().expect
+
local scope = {}
- local data = Value(scope, {foo = 1, bar = 2})
+ local data = Value(scope, {foo = 1, bar = 2} :: {[string]: number})
local numCalls = 0
local forObject = For(scope, data, function(scope, inputPair)
numCalls += 1
return Computed(scope, function(use)
- return {key = string.upper(use(inputPair).key), value = use(inputPair).value * 10}
+ return {key = string.upper(use(inputPair).key), value = (use(inputPair) :: any).value * 10}
end)
end)
expect(numCalls).to.equal(2)
@@ -102,6 +118,8 @@ return function()
end)
it("omits pairs that error", function()
+ local expect = getfenv().expect
+
local scope = {}
local data = {first = 1, second = 2, third = 3}
local forObject = For(scope, data, function(scope, inputPair)
@@ -115,6 +133,8 @@ return function()
end)
it("omits pairs when their value is nil", function()
+ local expect = getfenv().expect
+
local scope = {}
local data = {first = 1, second = 2, third = 3}
local omitThird = Value(scope, false)
@@ -128,7 +148,7 @@ return function()
return use(inputPair)
end
end)
- end)
+ end :: any)
expect(peek(forObject).first).to.equal(1)
expect(peek(forObject).second).to.equal(nil)
expect(peek(forObject).third).to.equal(3)
@@ -144,6 +164,8 @@ return function()
end)
it("allows values to roam when their key is nil", function()
+ local expect = getfenv().expect
+
local scope = {}
local data = Value(scope, {"first", "second", "third"})
local numCalls = 0
diff --git a/test/State/ForKeys.spec.lua b/test/Spec/State/ForKeys.spec.lua
similarity index 85%
rename from test/State/ForKeys.spec.lua
rename to test/Spec/State/ForKeys.spec.lua
index d597bd0ca..d49748658 100644
--- a/test/State/ForKeys.spec.lua
+++ b/test/Spec/State/ForKeys.spec.lua
@@ -1,15 +1,25 @@
-local Package = game:GetService("ReplicatedStorage").Fusion
-local ForKeys = require(Package.State.ForKeys)
-local Value = require(Package.State.Value)
-local peek = require(Package.State.peek)
-local doCleanup = require(Package.Memory.doCleanup)
+--!strict
+--!nolint LocalUnused
+local task = nil -- Disable usage of Roblox's task scheduler
+
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local Fusion = ReplicatedStorage.Fusion
+
+local ForKeys = require(Fusion.State.ForKeys)
+local Value = require(Fusion.State.Value)
+local peek = require(Fusion.State.peek)
+local doCleanup = require(Fusion.Memory.doCleanup)
return function()
+ local it = getfenv().it
+
it("constructs in scopes", function()
+ local expect = getfenv().expect
+
local scope = {}
local forObject = ForKeys(scope, {}, function()
-- intentionally blank
- end)
+ end :: any)
expect(forObject).to.be.a("table")
expect(forObject.type).to.equal("State")
@@ -20,16 +30,20 @@ return function()
end)
it("is destroyable", function()
+ local expect = getfenv().expect
+
local scope = {}
local forObject = ForKeys(scope, {}, function()
-- intentionally blank
- end)
+ end :: any)
expect(function()
forObject:destroy()
end).to.never.throw()
end)
it("iterates on constants", function()
+ local expect = getfenv().expect
+
local scope = {}
local data = {foo = 1, bar = 2}
local forObject = ForKeys(scope, data, function(_, _, key)
@@ -42,8 +56,10 @@ return function()
end)
it("iterates on state objects", function()
+ local expect = getfenv().expect
+
local scope = {}
- local data = Value(scope, {foo = 1, bar = 2})
+ local data = Value(scope, {foo = 1, bar = 2} :: {[string]: number})
local forObject = ForKeys(scope, data, function(_, _, key)
return key:upper()
end)
@@ -59,10 +75,12 @@ return function()
end)
it("computes with constants", function()
+ local expect = getfenv().expect
+
local scope = {}
local data = {foo = 1, bar = 2}
local forObject = ForKeys(scope, data, function(use, _, key)
- return key .. use("baz")
+ return key :: any .. use("baz")
end)
expect(peek(forObject).foobaz).to.equal(1)
expect(peek(forObject).barbaz).to.equal(2)
@@ -70,11 +88,13 @@ return function()
end)
it("computes with state objects", function()
+ local expect = getfenv().expect
+
local scope = {}
local data = {foo = 1, bar = 2}
local suffix = Value(scope, "first")
local forObject = ForKeys(scope, data, function(use, _, key)
- return key .. use(suffix)
+ return key :: any .. use(suffix)
end)
expect(peek(forObject).foofirst).to.equal(1)
expect(peek(forObject).barfirst).to.equal(2)
@@ -87,12 +107,14 @@ return function()
end)
it("destroys and omits keys that error during processing", function()
+ local expect = getfenv().expect
+
local scope = {}
local data = {foo = 1, bar = 2, baz = 3}
local suffix = Value(scope, "first")
local destroyed = {}
local forObject = ForKeys(scope, data, function(use, innerScope, key)
- local generated = key .. use(suffix)
+ local generated = key :: any .. use(suffix)
table.insert(innerScope, function()
destroyed[generated] = true
end)
@@ -131,12 +153,14 @@ return function()
end)
it("omits keys that return nil", function()
+ local expect = getfenv().expect
+
local scope = {}
local data = {foo = 1, bar = 2, baz = 3}
local omitThird = Value(scope, false)
local forObject = ForKeys(scope, data, function(use, _, key)
if key == "bar" then
- return nil
+ return nil :: any
end
if use(omitThird) then
if key == "baz" then
@@ -160,6 +184,8 @@ return function()
end)
it("doesn't destroy inner scope on creation", function()
+ local expect = getfenv().expect
+
local scope = {}
local destructed = {}
local data = Value(scope, {foo = 1, bar = 2})
@@ -179,9 +205,11 @@ return function()
end)
it("destroys inner scope on update", function()
+ local expect = getfenv().expect
+
local scope = {}
local destructed = {}
- local data = Value(scope, {foo = 1, bar = 2})
+ local data = Value(scope, {foo = 1, bar = 2} :: {[string]: number})
local _ = ForKeys(scope, data, function(_, innerScope, key)
table.insert(innerScope, function()
destructed[key] = true
@@ -198,6 +226,8 @@ return function()
end)
it("destroys inner scope on destroy", function()
+ local expect = getfenv().expect
+
local scope = {}
local destructed = {}
local data = Value(scope, {foo = 1, bar = 2})
@@ -215,6 +245,8 @@ return function()
end)
it("doesn't recompute when values change", function()
+ local expect = getfenv().expect
+
local scope = {}
local data = Value(scope, {foo = 1, bar = 2})
local computations = 0
diff --git a/test/State/ForPairs.spec.lua b/test/Spec/State/ForPairs.spec.lua
similarity index 84%
rename from test/State/ForPairs.spec.lua
rename to test/Spec/State/ForPairs.spec.lua
index 68e38c29f..afa002f14 100644
--- a/test/State/ForPairs.spec.lua
+++ b/test/Spec/State/ForPairs.spec.lua
@@ -1,17 +1,25 @@
-local RunService = game:GetService("RunService")
+--!strict
+--!nolint LocalUnused
+local task = nil -- Disable usage of Roblox's task scheduler
-local Package = game:GetService("ReplicatedStorage").Fusion
-local ForPairs = require(Package.State.ForPairs)
-local Value = require(Package.State.Value)
-local peek = require(Package.State.peek)
-local doCleanup = require(Package.Memory.doCleanup)
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local Fusion = ReplicatedStorage.Fusion
+
+local ForPairs = require(Fusion.State.ForPairs)
+local Value = require(Fusion.State.Value)
+local peek = require(Fusion.State.peek)
+local doCleanup = require(Fusion.Memory.doCleanup)
return function()
+ local it = getfenv().it
+
it("constructs in scopes", function()
+ local expect = getfenv().expect
+
local scope = {}
local forObject = ForPairs(scope, {}, function()
-- intentionally blank
- end)
+ end :: any)
expect(forObject).to.be.a("table")
expect(forObject.type).to.equal("State")
@@ -22,16 +30,20 @@ return function()
end)
it("is destroyable", function()
+ local expect = getfenv().expect
+
local scope = {}
local forObject = ForPairs(scope, {}, function()
-- intentionally blank
- end)
+ end :: any)
expect(function()
forObject:destroy()
end).to.never.throw()
end)
it("iterates on constants", function()
+ local expect = getfenv().expect
+
local scope = {}
local data = {foo = "oof", bar = "rab"}
local forObject = ForPairs(scope, data, function(_, _, key, value)
@@ -44,8 +56,10 @@ return function()
end)
it("iterates on state objects", function()
+ local expect = getfenv().expect
+
local scope = {}
- local data = Value(scope, {foo = "oof", bar = "rab"})
+ local data = Value(scope, {foo = "oof", bar = "rab"} :: {[string]: string})
local forObject = ForPairs(scope, data, function(_, _, key, value)
return value, key
end)
@@ -61,10 +75,12 @@ return function()
end)
it("computes with constants", function()
+ local expect = getfenv().expect
+
local scope = {}
local data = {foo = "oof", bar = "rab"}
local forObject = ForPairs(scope, data, function(use, _, key, value)
- return value .. use("baz"), key .. use("baz")
+ return value :: any .. use("baz"), key :: any .. use("baz")
end)
expect(peek(forObject).oofbaz).to.equal("foobaz")
expect(peek(forObject).rabbaz).to.equal("barbaz")
@@ -72,11 +88,13 @@ return function()
end)
it("computes with state objects", function()
+ local expect = getfenv().expect
+
local scope = {}
local data = {foo = "oof", bar = "rab"}
local suffix = Value(scope, "first")
local forObject = ForPairs(scope, data, function(use, _, key, value)
- return value .. use(suffix), key .. use(suffix)
+ return value :: any .. use(suffix), key :: any .. use(suffix)
end)
expect(peek(forObject).ooffirst).to.equal("foofirst")
expect(peek(forObject).rabfirst).to.equal("barfirst")
@@ -89,13 +107,15 @@ return function()
end)
it("destroys and omits pair that error during processing", function()
+ local expect = getfenv().expect
+
local scope = {}
local data = {foo = "oof", bar = "rab", baz = "zab"}
local suffix = Value(scope, "first")
local destroyed = {}
local forObject = ForPairs(scope, data, function(use, innerScope, key, value)
- local generatedKey = value .. use(suffix)
- local generatedValue = key .. use(suffix)
+ local generatedKey = value :: any .. use(suffix)
+ local generatedValue = key :: any .. use(suffix)
table.insert(innerScope, function()
destroyed[generatedKey] = true
end)
@@ -134,12 +154,14 @@ return function()
end)
it("omits values that return nil", function()
+ local expect = getfenv().expect
+
local scope = {}
local data = {foo = "oof", bar = "rab", baz = "zab"}
local omitThird = Value(scope, false)
local forObject = ForPairs(scope, data, function(use, _, key, value)
if key == "bar" then
- return nil
+ return nil :: any, nil :: any
end
if use(omitThird) then
if key == "baz" then
@@ -163,6 +185,8 @@ return function()
end)
it("doesn't destroy inner scope on creation", function()
+ local expect = getfenv().expect
+
local scope = {}
local destructed = {}
local data = Value(scope, {foo = "oof", bar = "rab", baz = "zab"})
@@ -182,9 +206,11 @@ return function()
end)
it("destroys inner scope on update", function()
+ local expect = getfenv().expect
+
local scope = {}
local destructed = {}
- local data = Value(scope, {foo = "oof", bar = "rab"})
+ local data = Value(scope, {foo = "oof", bar = "rab"} :: {[string]: string})
local _ = ForPairs(scope, data, function(_, innerScope, key, value)
table.insert(innerScope, function()
destructed[key] = true
@@ -201,6 +227,8 @@ return function()
end)
it("destroys inner scope on destroy", function()
+ local expect = getfenv().expect
+
local scope = {}
local destructed = {}
local data = Value(scope, {foo = "oof", bar = "rab"})
diff --git a/test/State/ForValues.spec.lua b/test/Spec/State/ForValues.spec.lua
similarity index 88%
rename from test/State/ForValues.spec.lua
rename to test/Spec/State/ForValues.spec.lua
index f7ffd43f5..240e4ee10 100644
--- a/test/State/ForValues.spec.lua
+++ b/test/Spec/State/ForValues.spec.lua
@@ -1,17 +1,25 @@
-local RunService = game:GetService("RunService")
+--!strict
+--!nolint LocalUnused
+local task = nil -- Disable usage of Roblox's task scheduler
-local Package = game:GetService("ReplicatedStorage").Fusion
-local ForValues = require(Package.State.ForValues)
-local Value = require(Package.State.Value)
-local peek = require(Package.State.peek)
-local doCleanup = require(Package.Memory.doCleanup)
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local Fusion = ReplicatedStorage.Fusion
+
+local ForValues = require(Fusion.State.ForValues)
+local Value = require(Fusion.State.Value)
+local peek = require(Fusion.State.peek)
+local doCleanup = require(Fusion.Memory.doCleanup)
return function()
+ local it = getfenv().it
+
it("constructs in scopes", function()
+ local expect = getfenv().expect
+
local scope = {}
local forObject = ForValues(scope, {}, function()
-- intentionally blank
- end)
+ end :: any)
expect(forObject).to.be.a("table")
expect(forObject.type).to.equal("State")
@@ -22,16 +30,20 @@ return function()
end)
it("is destroyable", function()
+ local expect = getfenv().expect
+
local scope = {}
local forObject = ForValues(scope, {}, function()
-- intentionally blank
- end)
+ end :: any)
expect(function()
forObject:destroy()
end).to.never.throw()
end)
it("iterates on constants", function()
+ local expect = getfenv().expect
+
local scope = {}
local data = {"foo", "bar"}
local forObject = ForValues(scope, data, function(_, _, value)
@@ -44,6 +56,8 @@ return function()
end)
it("iterates on state objects", function()
+ local expect = getfenv().expect
+
local scope = {}
local data = Value(scope, {"foo", "bar"})
local forObject = ForValues(scope, data, function(_, _, value)
@@ -61,6 +75,8 @@ return function()
end)
it("computes with constants", function()
+ local expect = getfenv().expect
+
local scope = {}
local data = {"foo", "bar"}
local forObject = ForValues(scope, data, function(use, _, value)
@@ -72,6 +88,8 @@ return function()
end)
it("computes with state objects", function()
+ local expect = getfenv().expect
+
local scope = {}
local data = {"foo", "bar"}
local suffix = Value(scope, "first")
@@ -87,6 +105,8 @@ return function()
end)
it("destroys and omits values that error during processing", function()
+ local expect = getfenv().expect
+
local scope = {}
local data = {"foo", "bar", "baz"}
local suffix = Value(scope, "first")
@@ -131,12 +151,14 @@ return function()
end)
it("omits values that return nil", function()
+ local expect = getfenv().expect
+
local scope = {}
local data = {"foo", "bar", "baz"}
local omitThird = Value(scope, false)
local forObject = ForValues(scope, data, function(use, _, value)
if value == "bar" then
- return nil
+ return nil :: any
end
if use(omitThird) then
if value == "baz" then
@@ -160,6 +182,8 @@ return function()
end)
it("doesn't destroy inner scope on creation", function()
+ local expect = getfenv().expect
+
local scope = {}
local destructed = {}
local data = Value(scope, {"foo", "bar"})
@@ -179,6 +203,8 @@ return function()
end)
it("destroys inner scope on update", function()
+ local expect = getfenv().expect
+
local scope = {}
local destructed = {}
local data = Value(scope, {"foo", "bar"})
@@ -198,6 +224,8 @@ return function()
end)
it("destroys inner scope on destroy", function()
+ local expect = getfenv().expect
+
local scope = {}
local destructed = {}
local data = Value(scope, {"foo", "bar"})
@@ -215,10 +243,12 @@ return function()
end)
it("doesn't recompute when values roam between keys", function()
+ local expect = getfenv().expect
+
local scope = {}
local data = Value(scope, {"foo", "bar"})
local computations = 0
- local forObject = ForValues(scope, data, function(_, _, value)
+ ForValues(scope, data, function(_, _, value)
computations += 1
return string.upper(value)
end)
@@ -235,10 +265,12 @@ return function()
end)
it("does not reuse values for duplicated items", function()
+ local expect = getfenv().expect
+
local scope = {}
local data = Value(scope, {"foo", "foo", "foo"})
local computations = 0
- local forObject = ForValues(scope, data, function(_, _, value)
+ ForValues(scope, data, function(_, _, value)
computations += 1
return string.upper(value)
end)
diff --git a/test/State/Observer.spec.lua b/test/Spec/State/Observer.spec.lua
similarity index 69%
rename from test/State/Observer.spec.lua
rename to test/Spec/State/Observer.spec.lua
index f1eaa498d..f9f02e0e8 100644
--- a/test/State/Observer.spec.lua
+++ b/test/Spec/State/Observer.spec.lua
@@ -1,11 +1,20 @@
-local Package = game:GetService("ReplicatedStorage").Fusion
-local Observer = require(Package.State.Observer)
-local Value = require(Package.State.Value)
-local peek = require(Package.State.peek)
-local doCleanup = require(Package.Memory.doCleanup)
+--!strict
+--!nolint LocalUnused
+local task = nil -- Disable usage of Roblox's task scheduler
+
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local Fusion = ReplicatedStorage.Fusion
+
+local Observer = require(Fusion.State.Observer)
+local Value = require(Fusion.State.Value)
+local doCleanup = require(Fusion.Memory.doCleanup)
return function()
+ local it = getfenv().it
+
it("constructs in scopes", function()
+ local expect = getfenv().expect
+
local scope = {}
local dependency = Value(scope, 5)
local observer = Observer(scope, dependency)
@@ -18,6 +27,8 @@ return function()
end)
it("is destroyable", function()
+ local expect = getfenv().expect
+
local scope = {}
local dependency = Value(scope, 5)
local observer = Observer(scope, dependency)
@@ -27,6 +38,8 @@ return function()
end)
it("fires once after change", function()
+ local expect = getfenv().expect
+
local scope = {}
local dependency = Value(scope, 5)
local observer = Observer(scope, dependency)
@@ -43,23 +56,32 @@ return function()
end)
it("fires asynchronously", function()
+ local expect = getfenv().expect
+
local scope = {}
local dependency = Value(scope, 5)
local observer = Observer(scope, dependency)
- local numFires = 0
- local disconnect = observer:onChange(function()
- task.wait(1)
- numFires += 1
- end)
- dependency:set(15)
- disconnect()
- expect(numFires).to.equal(0)
+ local firesPerThread = {} :: {[thread]: number}
+
+ for i=1, 5 do
+ observer:onChange(function()
+ local thread = coroutine.running()
+ firesPerThread[thread] = (firesPerThread[thread] or 0) + 1
+ end)
+ end
+ dependency:set(10)
+
+ for _, numFires in firesPerThread do
+ expect(firesPerThread).to.equal(0)
+ end
doCleanup(scope)
end)
it("fires onBind at bind time", function()
+ local expect = getfenv().expect
+
local scope = {}
local dependency = Value(scope, 5)
local observer = Observer(scope, dependency)
@@ -75,6 +97,8 @@ return function()
end)
it("disconnects manually", function()
+ local expect = getfenv().expect
+
local scope = {}
local dependency = Value(scope, 5)
local observer = Observer(scope, dependency)
@@ -92,6 +116,8 @@ return function()
end)
it("disconnects on destroy", function()
+ local expect = getfenv().expect
+
local scope = {}
local dependency = Value(scope, 5)
diff --git a/test/State/Value.spec.lua b/test/Spec/State/Value.spec.lua
similarity index 60%
rename from test/State/Value.spec.lua
rename to test/Spec/State/Value.spec.lua
index 872a19371..5c24f8a14 100644
--- a/test/State/Value.spec.lua
+++ b/test/Spec/State/Value.spec.lua
@@ -1,10 +1,20 @@
-local Package = game:GetService("ReplicatedStorage").Fusion
-local Value = require(Package.State.Value)
-local peek = require(Package.State.peek)
-local doCleanup = require(Package.Memory.doCleanup)
+--!strict
+--!nolint LocalUnused
+local task = nil -- Disable usage of Roblox's task scheduler
+
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local Fusion = ReplicatedStorage.Fusion
+
+local Value = require(Fusion.State.Value)
+local peek = require(Fusion.State.peek)
+local doCleanup = require(Fusion.Memory.doCleanup)
return function()
+ local it = getfenv().it
+
it("constructs in scopes", function()
+ local expect = getfenv().expect
+
local scope = {}
local value = Value(scope, nil)
@@ -17,6 +27,8 @@ return function()
end)
it("is destroyable", function()
+ local expect = getfenv().expect
+
local value = Value({}, nil)
expect(value.destroy).to.be.a("function")
expect(function()
@@ -25,6 +37,8 @@ return function()
end)
it("accepts a default value", function()
+ local expect = getfenv().expect
+
local scope = {}
local value = Value(scope, 5)
expect(peek(value)).to.equal(5)
@@ -32,8 +46,10 @@ return function()
end)
it("is settable", function()
+ local expect = getfenv().expect
+
local scope = {}
- local value = Value(scope, 0)
+ local value = Value(scope, 0 :: string | number)
expect(peek(value)).to.equal(0)
value:set(10)
diff --git a/test/State/updateAll.spec.lua b/test/Spec/State/updateAll.spec.lua
similarity index 88%
rename from test/State/updateAll.spec.lua
rename to test/Spec/State/updateAll.spec.lua
index f01f7ba81..fc48c22be 100644
--- a/test/State/updateAll.spec.lua
+++ b/test/Spec/State/updateAll.spec.lua
@@ -1,11 +1,17 @@
-local Package = game:GetService("ReplicatedStorage").Fusion
-local updateAll = require(Package.State.updateAll)
+--!strict
+--!nolint LocalUnused
+local task = nil -- Disable usage of Roblox's task scheduler
+
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local Fusion = ReplicatedStorage.Fusion
+
+local updateAll = require(Fusion.State.updateAll)
local function edge(from, to)
return { from = from, to = to }
end
-local function buildReactiveGraph(ancestorsToDescendants, handler)
+local function buildReactiveGraph(ancestorsToDescendants, handler: (any) -> boolean): any
local objects = {}
local function getObject(named)
@@ -41,12 +47,16 @@ local function buildReactiveGraph(ancestorsToDescendants, handler)
end
return function()
+ local it = getfenv().it
+
it("should update transitive dependencies", function()
+ local expect = getfenv().expect
+
local objects = buildReactiveGraph({
edge("A", "B"),
edge("B", "C"),
edge("C", "D"),
- }, function(self)
+ }, function(self: any)
self.updates += 1
return true
end)
@@ -60,10 +70,12 @@ return function()
end)
it("should only update objects once", function()
+ local expect = getfenv().expect
+
local objects = buildReactiveGraph({
edge("A", "B"), edge("A", "C"),
edge("B", "D"), edge("C", "D"),
- }, function(self)
+ }, function(self: any)
self.updates += 1
return true
end)
@@ -77,6 +89,8 @@ return function()
end)
it("should not update destroyed objects", function()
+ local expect = getfenv().expect
+
local objects = buildReactiveGraph({
edge("A", "B"),
edge("B", "C"),
@@ -102,6 +116,8 @@ return function()
end)
it("should not update unchanged subgraphs", function()
+ local expect = getfenv().expect
+
local objects = buildReactiveGraph({
edge("A", "B"),
edge("B", "C"),
@@ -120,6 +136,8 @@ return function()
end)
it("should update state objects in subgraphs of unchanged state objects", function()
+ local expect = getfenv().expect
+
local objects = buildReactiveGraph({
edge("A", "B"), edge("A", "D"),
edge("B", "C"),
@@ -146,6 +164,8 @@ return function()
end)
it("should update complicated graphs correctly", function()
+ local expect = getfenv().expect
+
local objects = buildReactiveGraph({
edge("A", "B"), edge("A", "F"), edge("A", "I"),
edge("B", "C"), edge("B", "D"), edge("B", "E"),
diff --git a/test/Utility/Contextual.spec.lua b/test/Spec/Utility/Contextual.spec.lua
similarity index 74%
rename from test/Utility/Contextual.spec.lua
rename to test/Spec/Utility/Contextual.spec.lua
index 859999b38..5dff28a27 100644
--- a/test/Utility/Contextual.spec.lua
+++ b/test/Spec/Utility/Contextual.spec.lua
@@ -1,21 +1,35 @@
-local Package = game:GetService("ReplicatedStorage").Fusion
-local Contextual = require(Package.Utility.Contextual)
+--!strict
+--!nolint LocalUnused
+local task = nil -- Disable usage of Roblox's task scheduler
+
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local Fusion = ReplicatedStorage.Fusion
+
+local Contextual = require(Fusion.Utility.Contextual)
return function()
+ local it = getfenv().it
+
it("should construct a Contextual object", function()
- local ctx = Contextual()
+ local expect = getfenv().expect
+
+ local ctx = Contextual(nil)
expect(ctx).to.be.a("table")
expect(ctx.type).to.equal("Contextual")
end)
it("should provide its default value", function()
+ local expect = getfenv().expect
+
local ctx = Contextual("foo")
expect(ctx:now()).to.equal("foo")
end)
it("should correctly scope temporary values", function()
+ local expect = getfenv().expect
+
local ctx = Contextual("foo")
expect(ctx:now()).to.equal("foo")
@@ -25,15 +39,19 @@ return function()
ctx:is("baz"):during(function()
expect(ctx:now()).to.equal("baz")
+ return nil
end)
expect(ctx:now()).to.equal("bar")
+ return nil
end)
expect(ctx:now()).to.equal("foo")
end)
it("should allow for argument passing", function()
+ local expect = getfenv().expect
+
local ctx = Contextual("foo")
local function test(a, b, c, d)
@@ -41,12 +59,15 @@ return function()
expect(b).to.equal("b")
expect(c).to.equal("c")
expect(d).to.equal("d")
+ return nil
end
ctx:is("bar"):during(test, "a", "b", "c", "d")
end)
it("should not interfere across coroutines", function()
+ local expect = getfenv().expect
+
local ctx = Contextual("foo")
local coro1 = coroutine.create(function()
@@ -54,6 +75,7 @@ return function()
expect(ctx:now()).to.equal("bar")
coroutine.yield()
expect(ctx:now()).to.equal("bar")
+ return nil
end)
end)
@@ -62,6 +84,7 @@ return function()
expect(ctx:now()).to.equal("baz")
coroutine.yield()
expect(ctx:now()).to.equal("baz")
+ return nil
end)
end)
diff --git a/test/Utility/isSimilar.spec.lua b/test/Spec/Utility/isSimilar.spec.lua
similarity index 71%
rename from test/Utility/isSimilar.spec.lua
rename to test/Spec/Utility/isSimilar.spec.lua
index 5d071140b..e7dbbcf52 100644
--- a/test/Utility/isSimilar.spec.lua
+++ b/test/Spec/Utility/isSimilar.spec.lua
@@ -1,14 +1,26 @@
-local Package = game:GetService("ReplicatedStorage").Fusion
-local isSimilar = require(Package.Utility.isSimilar)
+--!strict
+--!nolint LocalUnused
+local task = nil -- Disable usage of Roblox's task scheduler
+
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local Fusion = ReplicatedStorage.Fusion
+
+local isSimilar = require(Fusion.Utility.isSimilar)
return function()
+ local it = getfenv().it
+
it("should return similar for identical values", function()
+ local expect = getfenv().expect
+
local value = 123
expect(isSimilar(value, value)).to.equal(true)
end)
it("should return non-similar for different values", function()
+ local expect = getfenv().expect
+
local value1 = 123
local value2 = 321
@@ -16,6 +28,8 @@ return function()
end)
it("should return similar for any NaN values", function()
+ local expect = getfenv().expect
+
local nan1 = 0 / 0
local nan2 = math.huge / math.huge
@@ -24,6 +38,8 @@ return function()
end)
it("should return non-similar for any tables", function()
+ local expect = getfenv().expect
+
local initialTable = { foo = 123, bar = "hello" }
local similarTable = { foo = 123, bar = "hello" }
local differentTable = { foo = 321, bar = "world" }
diff --git a/test/SpecExternal.lua b/test/SpecExternal.lua
new file mode 100644
index 000000000..59146e073
--- /dev/null
+++ b/test/SpecExternal.lua
@@ -0,0 +1,77 @@
+--!strict
+
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local Fusion = ReplicatedStorage.Fusion
+
+local External = require(Fusion.External)
+
+local SpecExternal = {}
+
+local queue = {} :: {thread}
+
+--[[
+ Sends an immediate task to the external scheduler. Throws if none is set.
+]]
+function SpecExternal.doTaskImmediate(
+ resume: () -> ()
+)
+ print("Scheduling task to run now...", debug.traceback())
+ table.insert(queue, 1, coroutine.running())
+ table.insert(queue, 1, coroutine.create(resume))
+ coroutine.yield()
+end
+
+--[[
+ Sends a deferred task to the external scheduler. Throws if none is set.
+]]
+function SpecExternal.doTaskDeferred(
+ resume: () -> ()
+)
+ print("Scheduling task to run later...")
+ table.insert(queue, coroutine.create(resume))
+end
+
+local doUpdateSteps = false
+
+--[[
+ Binds Fusion's update step to RunService step events.
+]]
+function SpecExternal.startScheduler()
+ doUpdateSteps = true
+end
+
+--[[
+ Unbinds Fusion's update step from RunService step events.
+]]
+function SpecExternal.stopScheduler()
+ doUpdateSteps = false
+end
+
+--[[
+ Unbinds Fusion's update step from RunService step events.
+]]
+function SpecExternal.step(
+ currentTime: number
+)
+ print("Doing step")
+ if doUpdateSteps then
+ print("Update with time", currentTime)
+ External.performUpdateStep(currentTime)
+ end
+
+ print("Draining queue...")
+ while true do
+ local nextTask = table.remove(queue, 1)
+ if nextTask == nil then
+ print("Ran out of tasks.")
+ break
+ end
+ print("Resuming a task...")
+ local ok, result: string = coroutine.resume(nextTask)
+ if not ok then
+ warn("Error in spec scheduler: " .. result)
+ end
+ end
+end
+
+return SpecExternal
\ No newline at end of file
diff --git a/test-runner/TestEZ/Context.lua b/test/TestEZ/Context.lua
similarity index 100%
rename from test-runner/TestEZ/Context.lua
rename to test/TestEZ/Context.lua
diff --git a/test-runner/TestEZ/Expectation.lua b/test/TestEZ/Expectation.lua
similarity index 100%
rename from test-runner/TestEZ/Expectation.lua
rename to test/TestEZ/Expectation.lua
diff --git a/test-runner/TestEZ/ExpectationContext.lua b/test/TestEZ/ExpectationContext.lua
similarity index 100%
rename from test-runner/TestEZ/ExpectationContext.lua
rename to test/TestEZ/ExpectationContext.lua
diff --git a/test-runner/TestEZ/LifecycleHooks.lua b/test/TestEZ/LifecycleHooks.lua
similarity index 100%
rename from test-runner/TestEZ/LifecycleHooks.lua
rename to test/TestEZ/LifecycleHooks.lua
diff --git a/test-runner/TestEZ/Reporters/TeamCityReporter.lua b/test/TestEZ/Reporters/TeamCityReporter.lua
similarity index 100%
rename from test-runner/TestEZ/Reporters/TeamCityReporter.lua
rename to test/TestEZ/Reporters/TeamCityReporter.lua
diff --git a/test-runner/TestEZ/Reporters/TextReporter.lua b/test/TestEZ/Reporters/TextReporter.lua
similarity index 100%
rename from test-runner/TestEZ/Reporters/TextReporter.lua
rename to test/TestEZ/Reporters/TextReporter.lua
diff --git a/test-runner/TestEZ/Reporters/TextReporterQuiet.lua b/test/TestEZ/Reporters/TextReporterQuiet.lua
similarity index 100%
rename from test-runner/TestEZ/Reporters/TextReporterQuiet.lua
rename to test/TestEZ/Reporters/TextReporterQuiet.lua
diff --git a/test-runner/TestEZ/TestBootstrap.lua b/test/TestEZ/TestBootstrap.lua
similarity index 100%
rename from test-runner/TestEZ/TestBootstrap.lua
rename to test/TestEZ/TestBootstrap.lua
diff --git a/test-runner/TestEZ/TestEnum.lua b/test/TestEZ/TestEnum.lua
similarity index 100%
rename from test-runner/TestEZ/TestEnum.lua
rename to test/TestEZ/TestEnum.lua
diff --git a/test-runner/TestEZ/TestPlan.lua b/test/TestEZ/TestPlan.lua
similarity index 100%
rename from test-runner/TestEZ/TestPlan.lua
rename to test/TestEZ/TestPlan.lua
diff --git a/test-runner/TestEZ/TestPlanner.lua b/test/TestEZ/TestPlanner.lua
similarity index 100%
rename from test-runner/TestEZ/TestPlanner.lua
rename to test/TestEZ/TestPlanner.lua
diff --git a/test-runner/TestEZ/TestResults.lua b/test/TestEZ/TestResults.lua
similarity index 100%
rename from test-runner/TestEZ/TestResults.lua
rename to test/TestEZ/TestResults.lua
diff --git a/test-runner/TestEZ/TestRunner.lua b/test/TestEZ/TestRunner.lua
similarity index 100%
rename from test-runner/TestEZ/TestRunner.lua
rename to test/TestEZ/TestRunner.lua
diff --git a/test-runner/TestEZ/TestSession.lua b/test/TestEZ/TestSession.lua
similarity index 100%
rename from test-runner/TestEZ/TestSession.lua
rename to test/TestEZ/TestSession.lua
diff --git a/test-runner/TestEZ/init.lua b/test/TestEZ/init.lua
similarity index 100%
rename from test-runner/TestEZ/init.lua
rename to test/TestEZ/init.lua
diff --git a/test/TestVars.lua b/test/TestVars.lua
new file mode 100644
index 000000000..427a56baa
--- /dev/null
+++ b/test/TestVars.lua
@@ -0,0 +1,5 @@
+--!strict
+
+return {
+ runTests = true
+}
\ No newline at end of file
diff --git a/test/init.server.lua b/test/init.server.lua
new file mode 100644
index 000000000..9affd568e
--- /dev/null
+++ b/test/init.server.lua
@@ -0,0 +1,29 @@
+--!strict
+
+local TestEZ = require(script.TestEZ)
+local TestVars = require(script.TestVars)
+
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local Fusion = ReplicatedStorage.Fusion
+
+local External = require(Fusion.External)
+local SpecExternal = require(script.SpecExternal)
+
+-- run unit tests
+if TestVars.runTests then
+ print("Running unit tests...")
+ -- Suppress "unknown require path" error here.
+ External.unitTestSilenceNonFatal = true
+ External.setExternalScheduler(SpecExternal)
+
+ local data
+ SpecExternal.doTaskDeferred(function()
+ data = TestEZ.TestBootstrap:run({script.Spec})
+ end)
+ SpecExternal.step(0)
+
+ External.unitTestSilenceNonFatal = false
+ if data == nil or data.failureCount > 0 then
+ return
+ end
+end
\ No newline at end of file
diff --git a/test/init.spec.lua b/test/init.spec.lua
deleted file mode 100644
index e155a238c..000000000
--- a/test/init.spec.lua
+++ /dev/null
@@ -1,65 +0,0 @@
-local Package = game:GetService("ReplicatedStorage").Fusion
-local Fusion = require(Package)
-
-return function()
- it("should load with the correct public APIs", function()
- expect(Fusion).to.be.a("table")
-
- local api = {
- version = "table",
- Contextual = "function",
-
- cleanup = "function",
- doCleanup = "function",
- scoped = "function",
- deriveScope = "function",
-
- peek = "function",
- Value = "function",
- Computed = "function",
- ForPairs = "function",
- ForKeys = "function",
- ForValues = "function",
- Observer = "function",
-
- Tween = "function",
- Spring = "function",
-
- New = "function",
- Hydrate = "function",
-
- Ref = "table",
- Out = "function",
- Children = "table",
- OnEvent = "function",
- OnChange = "function",
- Attribute = "function",
- AttributeChange = "function",
- AttributeOut = "function"
- }
-
- for apiName, apiType in pairs(api) do
- local realValue = rawget(Fusion, apiName)
- local realType = typeof(realValue)
-
- if realType ~= apiType then
- error("API member '" .. apiName .. "' expected type '" .. apiType .. "' but got '" .. realType .. "'")
- end
- end
-
- for realName, realValue in pairs(Fusion) do
- local realType = typeof(realValue)
- local apiType = api[realName] or "nil"
-
- if realType ~= apiType then
- error("API member '" .. realName .. "' expected type '" .. apiType .. "' but got '" .. realType .. "'")
- end
- end
- end)
-
- it("should not error when accessing non-existent APIs", function()
- expect(function()
- local foo = Fusion["thisIsNotARealAPI" :: any]
- end).never.to.throw()
- end)
-end
\ No newline at end of file