From bbb7a571fa4a580c79cf3174c330813d091f3bad Mon Sep 17 00:00:00 2001 From: Daniel P H Fox Date: Mon, 15 Apr 2024 16:52:04 +0100 Subject: [PATCH] Upgrade unit testing infrastructure --- .github/FUNDING.yml | 2 +- fusion.ovpn | 108 --- package.json | 10 +- selene.toml | 2 +- sourcemap.json | 1 + src/Types.lua | 2 +- style-guide.md | 878 ------------------ test-runner.project.json | 16 +- test-runner/Run.client.lua | 21 - test/Animation/Tween.spec.lua | 56 -- test/Instances/Ref.spec.lua | 20 - test/Memory/scoped.spec.lua | 72 -- .../Animation/springCoefficients.spec.lua | 22 +- test/{ => Spec}/Instances/Attribute.spec.lua | 36 +- .../Instances/AttributeChange.spec.lua | 28 +- .../Instances/AttributeOut.spec.lua | 34 +- test/{ => Spec}/Instances/Children.spec.lua | 48 +- test/{ => Spec}/Instances/Hydrate.spec.lua | 18 +- test/{ => Spec}/Instances/New.spec.lua | 24 +- test/{ => Spec}/Instances/OnChange.spec.lua | 27 +- test/{ => Spec}/Instances/OnEvent.spec.lua | 30 +- test/{ => Spec}/Instances/Out.spec.lua | 32 +- test/Spec/Instances/Ref.spec.lua | 30 + .../Instances/applyInstanceProps.spec.lua | 82 +- test/{ => Spec}/Memory/doCleanup.spec.lua | 34 +- test/Spec/Memory/scoped.spec.lua | 90 ++ test/{ => Spec}/State/Computed.spec.lua | 48 +- test/{ => Spec}/State/For.spec.lua | 46 +- test/{ => Spec}/State/ForKeys.spec.lua | 58 +- test/{ => Spec}/State/ForPairs.spec.lua | 58 +- test/{ => Spec}/State/ForValues.spec.lua | 54 +- test/{ => Spec}/State/Observer.spec.lua | 52 +- test/{ => Spec}/State/Value.spec.lua | 26 +- test/{ => Spec}/State/updateAll.spec.lua | 30 +- test/{ => Spec}/Utility/Contextual.spec.lua | 29 +- test/{ => Spec}/Utility/isSimilar.spec.lua | 20 +- test/SpecExternal.lua | 77 ++ {test-runner => test}/TestEZ/Context.lua | 0 {test-runner => test}/TestEZ/Expectation.lua | 0 .../TestEZ/ExpectationContext.lua | 0 .../TestEZ/LifecycleHooks.lua | 0 .../TestEZ/Reporters/TeamCityReporter.lua | 0 .../TestEZ/Reporters/TextReporter.lua | 0 .../TestEZ/Reporters/TextReporterQuiet.lua | 0 .../TestEZ/TestBootstrap.lua | 0 {test-runner => test}/TestEZ/TestEnum.lua | 0 {test-runner => test}/TestEZ/TestPlan.lua | 0 {test-runner => test}/TestEZ/TestPlanner.lua | 0 {test-runner => test}/TestEZ/TestResults.lua | 0 {test-runner => test}/TestEZ/TestRunner.lua | 0 {test-runner => test}/TestEZ/TestSession.lua | 0 {test-runner => test}/TestEZ/init.lua | 0 test/TestVars.lua | 5 + test/init.server.lua | 29 + test/init.spec.lua | 65 -- 55 files changed, 849 insertions(+), 1471 deletions(-) delete mode 100644 fusion.ovpn create mode 100644 sourcemap.json delete mode 100644 style-guide.md delete mode 100644 test-runner/Run.client.lua delete mode 100644 test/Animation/Tween.spec.lua delete mode 100644 test/Instances/Ref.spec.lua delete mode 100644 test/Memory/scoped.spec.lua rename test/{ => Spec}/Animation/springCoefficients.spec.lua (81%) rename test/{ => Spec}/Instances/Attribute.spec.lua (59%) rename test/{ => Spec}/Instances/AttributeChange.spec.lua (61%) rename test/{ => Spec}/Instances/AttributeOut.spec.lua (66%) rename test/{ => Spec}/Instances/Children.spec.lua (82%) rename test/{ => Spec}/Instances/Hydrate.spec.lua (52%) rename test/{ => Spec}/Instances/New.spec.lua (56%) rename test/{ => Spec}/Instances/OnChange.spec.lua (71%) rename test/{ => Spec}/Instances/OnEvent.spec.lua (70%) rename test/{ => Spec}/Instances/Out.spec.lua (66%) create mode 100644 test/Spec/Instances/Ref.spec.lua rename test/{ => Spec}/Instances/applyInstanceProps.spec.lua (76%) rename test/{ => Spec}/Memory/doCleanup.spec.lua (74%) create mode 100644 test/Spec/Memory/scoped.spec.lua rename test/{ => Spec}/State/Computed.spec.lua (78%) rename test/{ => Spec}/State/For.spec.lua (83%) rename test/{ => Spec}/State/ForKeys.spec.lua (85%) rename test/{ => Spec}/State/ForPairs.spec.lua (84%) rename test/{ => Spec}/State/ForValues.spec.lua (88%) rename test/{ => Spec}/State/Observer.spec.lua (69%) rename test/{ => Spec}/State/Value.spec.lua (60%) rename test/{ => Spec}/State/updateAll.spec.lua (88%) rename test/{ => Spec}/Utility/Contextual.spec.lua (74%) rename test/{ => Spec}/Utility/isSimilar.spec.lua (71%) create mode 100644 test/SpecExternal.lua rename {test-runner => test}/TestEZ/Context.lua (100%) rename {test-runner => test}/TestEZ/Expectation.lua (100%) rename {test-runner => test}/TestEZ/ExpectationContext.lua (100%) rename {test-runner => test}/TestEZ/LifecycleHooks.lua (100%) rename {test-runner => test}/TestEZ/Reporters/TeamCityReporter.lua (100%) rename {test-runner => test}/TestEZ/Reporters/TextReporter.lua (100%) rename {test-runner => test}/TestEZ/Reporters/TextReporterQuiet.lua (100%) rename {test-runner => test}/TestEZ/TestBootstrap.lua (100%) rename {test-runner => test}/TestEZ/TestEnum.lua (100%) rename {test-runner => test}/TestEZ/TestPlan.lua (100%) rename {test-runner => test}/TestEZ/TestPlanner.lua (100%) rename {test-runner => test}/TestEZ/TestResults.lua (100%) rename {test-runner => test}/TestEZ/TestRunner.lua (100%) rename {test-runner => test}/TestEZ/TestSession.lua (100%) rename {test-runner => test}/TestEZ/init.lua (100%) create mode 100644 test/TestVars.lua create mode 100644 test/init.server.lua delete mode 100644 test/init.spec.lua 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