diff --git a/.gitignore b/.gitignore index 0d2d1a09..ca31c7a1 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,7 @@ roblox.toml .vscode/launch.json .idea .vscode + +jest_runner_tmp +lcov.info +rbx-aged-tool.log diff --git a/CHANGELOG.md b/CHANGELOG.md index ebb5b35b..c881e2a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,29 @@ * fix warning when multiple configuration are found ([#8](https://github.com/jsdotlua/jest-lua/pull/8)) * fix error message when no tests are found ([#7](https://github.com/jsdotlua/jest-lua/pull/7)) +## 3.9.1 (2024-08-02) +* :bug: Fix a type analysis error in `JestRuntime` ([#403](https://github.com/Roblox/jest-roblox-internal/pull/403)) + +## 3.9.0 (2024-08-02) +* :sparkles: Support spying on Lua globals with `spyOn` ([#397](https://github.com/Roblox/jest-roblox-internal/pull/397)) +* :bug: Expose safe APIs to read and write Roblox Instance properties in the `RobloxInstance` library for `PrettyFormat` to serialize Instances safely ([#398](https://github.com/Roblox/jest-roblox-internal/pull/398)) +* :hammer_and_wrench: Clean up package manifests, READMEs and documentation ([#400](https://github.com/Roblox/jest-roblox-internal/pull/400) [#402](https://github.com/Roblox/jest-roblox-internal/pull/402)) + +## 3.8.1 (2024-06-18) +* :bug: Fix mismatched test paths between reporter and runner ([#396](https://github.com/Roblox/jest-roblox-internal/pull/395)) + +## 3.8.0 (2024-05-20) +* :sparkles: Mock task.wait ([#388](https://github.com/Roblox/jest-roblox-internal/pull/373)) + +## 3.7.0 (2024-04-10) +* :sparkles: Resolve DOM paths to FS paths if possible in `JestRunner` ([#373](https://github.com/Roblox/jest-roblox-internal/pull/373)) + +## 3.6.2 (2024-03-21) +* :sparkles: Added jest.spyOn ([#382](https://github.com/Roblox/jest-roblox-internal/pull/382)) +* :bug: Fixed jest.mock type ([#385](https://github.com/Roblox/jest-roblox-internal/pull/385)) + ## 3.6.1 (2024-01-16) -* Re-release of 3.6.0 with widened promise dependency that includes older versions for maximum flexibility ([#378](https://github.com/Roblox/jest-roblox-internal/pull/378)) +* :hammer_and_wrench: Re-release of 3.6.0 with widened promise dependency that includes older versions for maximum flexibility ([#378](https://github.com/Roblox/jest-roblox-internal/pull/378)) ## 3.6.0 (2024-01-09) * :hammer_and_wrench: Upgrade promise dependency, but keep constraint wide so that all future 3.x versions are valid ([#374](https://github.com/Roblox/jest-roblox-internal/pull/374)) diff --git a/bin/ci.sh b/bin/ci.sh index e86fa9c7..bf839b54 100755 --- a/bin/ci.sh +++ b/bin/ci.sh @@ -14,4 +14,10 @@ robloxdev-cli run --load.model jest.project.json \ EnableSignalBehavior=true DebugForceDeferredSignalBehavior=true MaxDeferReentrancyDepth=15 \ --lua.globals=UPDATESNAPSHOT=false --lua.globals=CI=false --load.asRobloxScript --fs.readwrite="$(pwd)" +echo "Running low privilege tests" +robloxdev-cli run --load.model jest.project.json \ + --run bin/spec.lua --testService.errorExitCode=1 \ + --fastFlags.overrides EnableLoadModule=false --load.asRobloxScript + +# Uncomment this to update snapshots # robloxdev-cli run --load.model jest.project.json --run bin/spec.lua --testService.errorExitCode=1 --fastFlags.allOnLuau --fastFlags.overrides EnableLoadModule=true DebugDisableOptimizedBytecode=true --lua.globals=UPDATESNAPSHOT=true --lua.globals=CI=true --load.asRobloxScript --fs.readwrite="$(pwd)" diff --git a/docs/docs/CLI.md b/docs/docs/CLI.md index a5b3713a..5d24160a 100644 --- a/docs/docs/CLI.md +++ b/docs/docs/CLI.md @@ -2,7 +2,9 @@ id: cli title: runCLI Options --- -

Jest

Deviation +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/cli) + +![Deviation](/img/deviation.svg) The `Jest` packages exports `runCLI`, which is the main entrypoint to run Jest Lua tests. In your entrypoint script, import `runCLI` from the `Jest` package. A basic entrypoint script can look like the following: @@ -49,47 +51,47 @@ import TOCInline from "@theme/TOCInline"; ## Reference ### `ci` \[boolean] -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/cli#--ci) ![Aligned](/img/aligned.svg) When this option is provided, Jest Lua will assume it is running in a CI environment. This changes the behavior when a new snapshot is encountered. Instead of the regular behavior of storing a new snapshot automatically, it will fail the test and require Jest Lua to be run with `updateSnapshot`. ### `clearMocks` \[boolean] -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/cli#--clearmocks) ![Aligned](/img/aligned.svg) Automatically clear mock calls, instances, contexts and results before every test. Equivalent to calling [`jest.clearAllMocks()`](jest-object#jestclearallmocks) before each test. This does not remove any mock implementation that may have been provided. ### `debug` \[boolean] -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/cli#--debug) ![Aligned](/img/aligned.svg) Print debugging info about your Jest config. ### `expand` \[boolean] -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/cli#--expand) ![Aligned](/img/aligned.svg) Use this flag to show full diffs and errors instead of a patch. ### `json` \[boolean] -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/cli#--json) ![Aligned](/img/aligned.svg) Prints the test results in JSON. This mode will send all other test output and user messages to stderr. ### `listTests` \[boolean] -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/cli#--listtests) ![Aligned](/img/aligned.svg) Lists all test files that Jest Lua will run given the arguments, and exits. ### `noStackTrace` \[boolean] -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/cli#--nostacktrace) ![Aligned](/img/aligned.svg) Disables stack trace in test results output. ### `passWithNoTests` \[boolean] -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/cli#--passwithnotests) ![Aligned](/img/aligned.svg) Allows the test suite to pass when no files are found. ### `resetMocks` \[boolean] -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/cli#--resetmocks) ![Aligned](/img/aligned.svg) Automatically reset mock state before every test. Equivalent to calling [`jest.resetAllMocks()`](jest-object#jestresetallmocks) before each test. This will lead to any mocks having their fake implementations removed but does not restore their initial implementation. @@ -98,17 +100,17 @@ Automatically reset mock state before every test. Equivalent to calling [`jest.r Automatically restore mock state and implementation before every test. Equivalent to calling [`jest.restoreAllMocks()`](JestObjectAPI.md#jestrestoreallmocks) before each test. This will lead to any mocks having their fake implementations removed and restores their initial implementation. --> ### `showConfig` \[boolean] -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/cli#--showconfig) ![Aligned](/img/aligned.svg) Print your Jest config and then exits. ### `testMatch` \[array<string>] -Jest API change +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/cli#--testmatch-glob1--globn) ![API Change](/img/apichange.svg) The glob patterns Jest uses to detect test files. Please refer to the [`testMatch` configuration](configuration#testmatch-arraystring) for details. ### `testNamePattern` \[regex] -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/cli#--testnamepatternregex) ![Aligned](/img/aligned.svg) Run only tests with a name that matches the regex. For example, suppose you want to run only tests related to authorization which will have names like "GET /api/posts with auth", then you can use `testNamePattern = "auth"`. @@ -119,26 +121,26 @@ The regex is matched against the full name, which is a combination of the test n ::: ### `testPathIgnorePatterns` \[array<regex>] -Jest API change +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/cli#--testpathignorepatternsregexarray) ![API Change](/img/apichange.svg) An array of regexp pattern strings that are tested against all tests paths before executing the test. Contrary to `testPathPattern`, it will only run those tests with a path that does not match with the provided regexp expressions. ### `testPathPattern` \[regex] -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/cli#--testpathpatternregex) ![Aligned](/img/aligned.svg) A regexp pattern string that is matched against all tests paths before executing the test. ### `testTimeout` \[number>] -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/cli#--testtimeoutnumber) ![Aligned](/img/aligned.svg) Default timeout of a test in milliseconds. Default value: 5000. ### `updateSnapshot` \[boolean] -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/cli#--updatesnapshot) ![Aligned](/img/aligned.svg) Use this flag to re-record every snapshot that fails during this test run. Can be used together with a test suite pattern or with `testNamePattern` to re-record snapshots. ### `verbose` \[boolean] -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/cli#--verbose) ![Aligned](/img/aligned.svg) -Display individual test results with the test suite hierarchy. \ No newline at end of file +Display individual test results with the test suite hierarchy. diff --git a/docs/docs/Configuration.md b/docs/docs/Configuration.md index 66019575..5a46f09c 100644 --- a/docs/docs/Configuration.md +++ b/docs/docs/Configuration.md @@ -2,11 +2,11 @@ id: configuration title: Configuring Jest --- -

Jest

+[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/configuration) The Jest Lua philosophy is to work great by default, but sometimes you just need more configuration power. -deviation +![Deviation](/img/deviation.svg) The configuration should be defined in a `jest.config.lua` file. @@ -37,14 +37,14 @@ import TOCInline from "@theme/TOCInline"; ## Reference ### `clearmocks` \[boolean] -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/configuration#clearmocks-boolean) ![Aligned](/img/aligned.svg) Default: `false` Automatically clear mock calls, instances, contexts and results before every test. Equivalent to calling [`jest.clearAllMocks()`](jest-object#jestclearallmocks) before each test. This does not remove any mock implementation that may have been provided. ### `displayName` \[string, table] -Jest API change +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/configuration#displayname-string-object) ![API Change](/img/apichange.svg) Default: `nil` @@ -68,7 +68,7 @@ return { ``` ### `projects` \[array<Instance>] -Jest API change +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/configuration#projects-arraystring--projectconfig) ![API Change](/img/apichange.svg) Default: `nil` @@ -89,14 +89,14 @@ When using multi-project runner, it's recommended to add a `displayName` for eac ::: ### `rootDir` \[Instance] -Jest API change +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/configuration#rootdir-string) ![API Change](/img/apichange.svg) Default: The root of the directory containing your Jest Lua [config file](#). @@ -115,7 +115,7 @@ Using `''` as a string token in any other path-based configuration sett ::: --> ### `roots` \[array<Instance>] -Jest API change +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/configuration#roots-arraystring) ![API Change](/img/apichange.svg) Default: `{}` @@ -124,14 +124,14 @@ A list of paths to directories that Jest Lua should use to search for files in. There are times where you only want Jest Lua to search in a single sub-directory (such as cases where you have a `src/` directory in your repo), but prevent it from accessing the rest of the repo. ### `setupFiles` \[array<ModuleScript>] -Jest API change +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/configuration#setupfiles-array) ![API Change](/img/apichange.svg) Default: `{}` A list of ModuleScripts that run some code to configure or set up the testing environment. Each setupFile will be run once per test file. Since every test runs in its own environment, these scripts will be executed in the testing environment before executing [`setupFilesAfterEnv`](#setupfilesafterenv-array) and before the test code itself. ### `setupFilesAfterEnv` \[array<ModuleScript>] -Jest API change +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/configuration#setupfilesafterenv-array) ![API Change](/img/apichange.svg) Default: `{}` @@ -162,20 +162,20 @@ return { ``` ### `slowTestThreshold` \[number] -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/configuration#slowtestthreshold-number) ![Aligned](/img/aligned.svg) Default: `5` The number of seconds after which a test is considered as slow and reported as such in the results. ### `snapshotFormat` \[table] -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/configuration#snapshotformat-object) ![Aligned](/img/aligned.svg) Default: `nil` Allows overriding specific snapshot formatting options documented in the [pretty-format readme](https://github.com/facebook/jest/blob/main/packages/pretty-format/README.md#usage-with-options), with the exceptions of `compareKeys` and `plugins`. -deviation +![Deviation](/img/deviation.svg) `pretty-format` also supports the formatting option `printInstanceDefaults` (default: `true`) which can be set to `false` to only print properties of a Roblox `Instance` that have been changed. @@ -204,7 +204,7 @@ TextLabel { ### `snapshotSerializers` \[array<serializer>] -Jest API change +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/configuration#snapshotserializers-arraystring) ![API Change](/img/apichange.svg) Default: `{}` @@ -263,7 +263,7 @@ To make a dependency explicit instead of implicit, you can call [`expect.addSnap More about serializers API can be found [here](https://github.com/facebook/jest/tree/main/packages/pretty-format/README.md#serialize). ### `testFailureExitCode` \[number] -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/configuration#testfailureexitcode-number) ![Aligned](/img/aligned.svg) Default: `1` @@ -276,7 +276,7 @@ This does not change the exit code in the case of Jest Lua errors (e.g. invalid ::: ### `testMatch` \[array<string>] -Jest Deviation +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/configuration#testmatch-arraystring) ![Deviation](/img/deviation.svg) Default: `{ "**/__tests__/**/*", "**/?(*.)+(spec|test)" }` @@ -294,7 +294,7 @@ Each glob pattern is applied in the order they are specified in the config. For ::: ### `testPathIgnorePatterns` \[array<string>] -Jest Deviation +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/configuration#testpathignorepatterns-arraystring) ![Deviation](/img/deviation.svg) Default: `{}` @@ -303,7 +303,7 @@ An array of regexp pattern strings that are matched against all test paths befor ### `testRegex` \[string | array<string>] -Jest Deviation +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/configuration#testregex-string--arraystring) ![Deviation](/img/deviation.svg) Default: `{}` @@ -316,15 +316,15 @@ The pattern or patterns Jest Lua uses to detect test files. See also [`testMatch ::: ### `testTimeout` \[number] -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/configuration#testtimeout-number) ![Aligned](/img/aligned.svg) Default: `5000` Default timeout of a test in milliseconds. ### `verbose` \[boolean] -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/configuration#verbose-boolean) ![Aligned](/img/aligned.svg) Default: `false` -Indicates whether each individual test should be reported during the run. All errors will also still be shown on the bottom after execution. Note that if there is only one test file being run it will default to `true`. \ No newline at end of file +Indicates whether each individual test should be reported during the run. All errors will also still be shown on the bottom after execution. Note that if there is only one test file being run it will default to `true`. diff --git a/docs/docs/ExpectAPI.md b/docs/docs/ExpectAPI.md index f37e9530..6961d3d5 100644 --- a/docs/docs/ExpectAPI.md +++ b/docs/docs/ExpectAPI.md @@ -2,11 +2,11 @@ id: expect title: Expect --- -

Jest

+[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect) When you're writing tests, you often need to check that values meet certain conditions. `expect` gives you access to a number of "matchers" that let you validate different things. -deviation +![Deviation](/img/deviation.svg) It must be imported explicitly from `JestGlobals`. ```lua @@ -14,7 +14,7 @@ local expect = require("@DevPackages/JestGlobals").expect ``` ### RegExp -Roblox only +![Roblox only](/img/roblox-only.svg) To use regular expressions in matchers that support it, you need to add [LuauRegExp](https://github.com/Roblox/luau-regexp) as a dependency in your `rotriever.toml` and require it in your code. ```yaml title="rotriever.toml" @@ -26,7 +26,7 @@ local RegExp = require("@Packages/RegExp") ``` ### Promise -Roblox only +![Roblox only](/img/roblox-only.svg) To use Promises in your tests, add [roblox-lua-promise](https://github.com/Roblox/roblox-lua-promise) as a dependency in your `rotriever.toml` ```yaml @@ -34,7 +34,7 @@ Promise = "github.com/evaera/roblox-lua-promise@3.3.0" ``` ### Error -Roblox only +![Roblox only](/img/roblox-only.svg) LuauPolyfill also provides an extensible `Error` class that can be used with throwing matchers. @@ -70,7 +70,8 @@ import TOCInline from "@theme/TOCInline"; ## Reference ### `expect(value)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#expectvalue) +![Aligned](/img/aligned.svg) The `expect` function is used every time you want to test a value. You will rarely call `expect` by itself. Instead, you will use `expect` along with a "matcher" function to assert something about value. @@ -87,7 +88,8 @@ In this case, `toBe` is the matcher function. There are a lot of different match The argument to `expect` should be the value that your code produces, and any argument to the matcher should be the correct value. If you mix them up, your tests will still work, but the error messages on failing tests will look strange. ### `expect.extend(matchers)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#expectextendmatchers) +![Aligned](/img/aligned.svg) You can use `expect.extend` to add your own matchers to Jest Lua. For example, let's say that you're testing a number utility library and you're frequently asserting that numbers appear within particular ranges of other numbers. You could abstract that into a `toBeWithinRange` matcher: @@ -127,7 +129,7 @@ end) ``` #### Custom Matchers API -API change +![API Change](/img/apichange.svg) Matchers should return a table with two keys. `pass` indicates whether there was a match or not, and `message` provides a function with no arguments that return an error message in case of failure. Thus, when `pass` is false, `message` should return the error message for when `expect(x).yourMatcher()` fails. And when `pass` is true, `message` should return the error message for when `expect(x).never.yourMatcher()` fails. @@ -205,7 +207,7 @@ Received: "apple" When an assertion fails, the error message should give as much signal as necessary to the user so they can resolve their issue quickly. You should craft a precise failure message to make sure users of your custom assertions have a good developer experience. #### Custom snapshot matchers -API change +![API Change](/img/apichange.svg) To use snapshot testing inside of your custom matcher you can import `JestSnapshot` and use it from within your matcher. @@ -235,7 +237,7 @@ end) ``` ### `expect.anything()` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#expectanything) ![Aligned](/img/aligned.svg) `expect.anything()` matches anything but `nil`. You can use it inside `toEqual` or `toBeCalledWith` instead of a literal value. For example, if you want to check that a mock function is called with a non-nil argument: @@ -248,7 +250,7 @@ end) ``` ### `expect.any(typename | prototype)` -Jest deviation +[![Jest](/img/jestjs.svg)](http://localhost:3000/expect#expectanytypename--prototype) ![Deviation](/img/deviation.svg) `expect.any(typename)` matches anything that has the given type. `expect.any(prototype)` matches anything that is an instance (or a derived instance) of the given prototype class. You can use it inside `toEqual` or `toBeCalledWith` instead of a literal value. For example: @@ -267,7 +269,7 @@ end) In addition to Lua prototype classes, it also supports Roblox types like [`DateTime`](https://developer.roblox.com/en-us/api-reference/datatype/DateTime), Luau types like `thread`, `RegExp` from the LuauRegExp library, and LuauPolyfill types like `Symbol`, `Set`, `Error` etc. ### `expect.nothing()` -Jest deviation +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#expectnothing) ![Deviation](/img/deviation.svg) `expect.nothing()` matches only `nil`. You can use it inside `toEqual`, `toMatchObject`, `toBeCalledWith`, or similar matchers instead of a literal value. For example, if you want to check that a value is left undefined in a table: @@ -283,7 +285,7 @@ end) ``` ### `expect.arrayContaining(array)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#expectarraycontainingarray) ![Aligned](/img/aligned.svg) `expect.arrayContaining(array)` matches a received array which contains all of the elements in the expected array. That is, the expected array is a **subset** of the received array. Therefore, it matches a received array which contains elements that are **not** in the expected array. @@ -321,7 +323,7 @@ end) ``` ### `expect.assertions(number)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#expectassertionsnumber) ![Aligned](/img/aligned.svg) `expect.assertions(number)` verifies that a certain number of assertions are called during a test. This is often useful when testing asynchronous code, in order to make sure that assertions in a callback actually got called. @@ -345,7 +347,7 @@ end) The `expect.assertions(2)` call ensures that both callbacks actually get called. ### `expect.hasAssertions()` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#expecthasassertions) ![Aligned](/img/aligned.svg) `expect.hasAssertions()` verifies that at least one assertion is called during a test. This is often useful when testing asynchronous code, in order to make sure that assertions in a callback actually got called. @@ -364,7 +366,7 @@ end) The `expect.hasAssertions()` call ensures that the `prepareState` callback actually gets called. ### `expect.never.arrayContaining(array)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#expectnotarraycontainingarray) ![Aligned](/img/aligned.svg) `expect.never.arrayContaining(array)` matches a received array which does not contain all of the elements in the expected array. That is, the expected array **is not a subset** of the received array. @@ -382,12 +384,12 @@ describe('never.arrayContaining', function() end) ``` -API change +![API Change](/img/apichange.svg) Also under the alias: `.arrayNotContaining(array)` ### `expect.never.objectContaining(table)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#expectnotobjectcontainingobject) ![Aligned](/img/aligned.svg) `expect.never.objectContaining(table)` matches any received table that does not recursively match the expected properties. That is, the expected table **is not a subset** of the received table. Therefore, it matches a received table which contains properties that are **not** in the expected table. @@ -403,12 +405,12 @@ describe('never.objectContaining', function() end) ``` -API change +![API Change](/img/apichange.svg) Also under the alias: `.objectNotContaining(table)` ### `expect.never.stringContaining(string)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#expectnotstringcontainingstring) ![Aligned](/img/aligned.svg) `expect.never.stringContaining(string)` matches the received value if it is not a string or if it is a string that does not contain the exact expected string. @@ -424,12 +426,12 @@ describe('never.stringContaining', function() end) ``` -API change +![API Change](/img/apichange.svg) Also under the alias: `.stringNotContaining(string)` ### `expect.never.stringMatching(string | regexp)` -Jest API change +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#expectnotstringmatchingstring--regexp) ![API Change](/img/apichange.svg) `expect.never.stringMatching(string | regexp)` matches the received value if it is not a string or if it is a string that does not match the expected [Lua string pattern](https://developer.roblox.com/en-us/articles/string-patterns-reference) or [regular expression](#regexp). @@ -445,12 +447,12 @@ describe('never.stringMatching', function() end) ``` -API change +![API Change](/img/apichange.svg) Also under the alias: `.stringNotMatching(string | regexp)` ### `expect.objectContaining(table)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#expectobjectcontainingobject) ![Aligned](/img/aligned.svg) `expect.objectContaining(table)` matches any received table that recursively matches the expected properties. That is, the expected table is a **subset** of the received table. Therefore, it matches a received table which contains properties that **are present** in the expected table. @@ -471,12 +473,12 @@ end) ``` ### `expect.stringContaining(string)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#expectstringcontainingstring) ![Aligned](/img/aligned.svg) `expect.stringContaining(string)` matches the received value if it is a string that contains the exact expected string. ### `expect.stringMatching(string | regexp)` -Jest API change +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#expectstringmatchingstring--regexp) ![API Change](/img/apichange.svg) `expect.stringMatching(string | regexp)` matches the received value if it is a string that matches the expected [Lua string pattern](https://developer.roblox.com/en-us/articles/string-patterns-reference) or [regular expression](#regexp). @@ -508,7 +510,7 @@ end) ``` ### `expect.addSnapshotSerializer(serializer)` -Jest API change +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#expectaddsnapshotserializerserializer) ![API Change](/img/apichange.svg) You can call `expect.addSnapshotSerializer` to add a module that formats application-specific data structures. @@ -523,7 +525,7 @@ expect.addSnapshotSerializer(serializer) See [configuring Jest Lua](configuration) for more information. ### `.never` -Jest API change +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#not) ![API Change](/img/apichange.svg) If you know how to test something, `.never` lets you test its opposite. For example, this code tests that the best La Croix flavor is not coconut: @@ -534,7 +536,7 @@ end) ``` ### `.resolves` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#resolves) ![Aligned](/img/aligned.svg) Use `resolves` to unwrap the value of a fulfilled [promise](#promise) so any other matcher can be chained. If the promise is rejected the assertion fails. @@ -554,7 +556,7 @@ Since you are still testing promises, the test is still asynchronous. Hence, you ::: ### `.rejects` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#rejects) ![Aligned](/img/aligned.svg) Use `.rejects` to unwrap the reason of a rejected [promise](#promise) so any other matcher can be chained. If the promise is fulfilled the assertion fails. @@ -574,7 +576,7 @@ Since you are still testing promises, the test is still asynchronous. Hence, you ::: ### `.toBe(value)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tobevalue) ![Aligned](/img/aligned.svg) Use `.toBe` to compare primitive values or to check referential identity of tables. It calls [Luau Polyfill's `Object.is`](https://github.com/Roblox/luau-polyfill/blob/main/src/Object/is.lua) to compare values, which mostly behaves like the `==` operator. @@ -605,7 +607,7 @@ Although the `.toBe` matcher **checks** referential identity, it **reports** a d - rewrite `expect(received).never.toBe(expected)` as `expect(received == expected).toBe(false)` ### `.toHaveBeenCalled()` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tohavebeencalled) ![Aligned](/img/aligned.svg) Also under the alias: `.toBeCalled()` @@ -636,7 +638,7 @@ end) ``` ### `.toHaveBeenCalledTimes(number)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tohavebeencalledtimesnumber) ![Aligned](/img/aligned.svg) Also under the alias: `.toBeCalledTimes(number)` @@ -653,7 +655,7 @@ end) ``` ### `.toHaveBeenCalledWith(arg1, arg2, ...)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tohavebeencalledwitharg1-arg2-) ![Aligned](/img/aligned.svg) Also under the alias: `.toBeCalledWith()` @@ -672,7 +674,7 @@ end) ``` ### `.toHaveBeenLastCalledWith(arg1, arg2, ...)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tohavebeenlastcalledwitharg1-arg2-) ![Aligned](/img/aligned.svg) Also under the alias: `.lastCalledWith(arg1, arg2, ...)` @@ -687,7 +689,7 @@ end) ``` ### `.toHaveBeenNthCalledWith(nthCall, arg1, arg2, ....)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tohavebeennthcalledwithnthcall-arg1-arg2-) ![Aligned](/img/aligned.svg) Also under the alias: `.nthCalledWith(nthCall, arg1, arg2, ...)` @@ -705,7 +707,7 @@ end) Note: the nth argument must be positive integer starting from 1. ### `.toHaveReturned()` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tohavereturned) ![Aligned](/img/aligned.svg) Also under the alias: `.toReturn()` @@ -722,7 +724,7 @@ end ``` ### `.toHaveReturnedTimes(number)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tohavereturnedtimesnumber) ![Aligned](/img/aligned.svg) Also under the alias: `.toReturnTimes(number)` @@ -742,7 +744,7 @@ end ``` ### `.toHaveReturnedWith(value)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tohavereturnedwithvalue) ![Aligned](/img/aligned.svg) Also under the alias: `.toReturnWith(value)` @@ -762,7 +764,7 @@ end) ``` ### `.toHaveLastReturnedWith(value)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tohavelastreturnedwithvalue) ![Aligned](/img/aligned.svg) Also under the alias: `.lastReturnedWith(value)` @@ -784,7 +786,7 @@ end) ``` ### `.toHaveNthReturnedWith(nthCall, value)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tohaventhreturnedwithnthcall-value) ![Aligned](/img/aligned.svg) Also under the alias: `.nthReturnedWith(nthCall, value)` @@ -809,7 +811,7 @@ end Note: the nth argument must be positive integer starting from 1. ### `.toHaveLength(number)` -Jest Deviation +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tohavelengthnumber) ![Deviation](/img/deviation.svg) Use `.toHaveLength` to check that an (array-like) table or string has a certain length. It calls the `#` operator and since `#` is only well defined for non-sparse array-like tables and strings it will return 0 for tables with key-value pairs. It checks the `.length` property of the table instead if it has one. @@ -822,7 +824,7 @@ expect('').never.toHaveLength(5) ``` ### `.toHaveProperty(keyPath, value?)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tohavepropertykeypath-value) ![Aligned](/img/aligned.svg) Use `.toHaveProperty` to check if property at provided reference `keyPath` exists for an object. For checking deeply nested properties in an object you may use dot notation or an array containing the `keyPath` for deep references. @@ -877,7 +879,7 @@ end) ``` ### `.toBeCloseTo(number, numDigits?)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tobeclosetonumber-numdigits) ![Aligned](/img/aligned.svg) Use `toBeCloseTo` to compare floating point numbers for approximate equality. @@ -902,7 +904,7 @@ end) ``` ### `.toBeDefined()` -Jest Deviation +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tobedefined) ![Deviation](/img/deviation.svg) Use `.toBeDefined` to check that a variable is not `nil`. For example, if you want to check that a function `fetchNewFlavorIdea()` returns _something_, you can write: @@ -917,7 +919,7 @@ end) ::: ### `.toBeFalsy()` -Jest Deviation +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tobefalsy) ![Deviation](/img/deviation.svg) Use `.toBeFalsy` when you don't care what a value is and you want to ensure a value is false in a boolean context. For example, let's say you have some application code that looks like: @@ -940,7 +942,7 @@ end) In Lua, there are two falsy values: `false` and `nil`. Everything else is truthy. ### `.toBeGreaterThan(number)` -Jest API change +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tobegreaterthannumber--bigint) ![API Change](/img/apichange.svg) Use `toBeGreaterThan` to compare `received > expected` for number values. For example, test that `ouncesPerCan()` returns a value of more than 10 ounces: @@ -951,7 +953,7 @@ end) ``` ### `.toBeGreaterThanOrEqual(number)` -Jest API change +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tobegreaterthanorequalnumber--bigint) ![API Change](/img/apichange.svg) Use `toBeGreaterThanOrEqual` to compare `received >= expected` for number values. For example, test that `ouncesPerCan()` returns a value of at least 12 ounces: @@ -962,7 +964,7 @@ end) ``` ### `.toBeLessThan(number)` -Jest API change +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tobelessthannumber--bigint) ![API Change](/img/apichange.svg) Use `toBeLessThan` to compare `received < expected` for number values. For example, test that `ouncesPerCan()` returns a value of less than 20 ounces: @@ -973,7 +975,7 @@ end) ``` ### `.toBeLessThanOrEqual(number)` -Jest API change +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tobelessthanorequalnumber--bigint) ![API Change](/img/apichange.svg) Use `toBeLessThanOrEqual` to compare `received <= expected` for number values. For example, test that `ouncesPerCan()` returns a value of at most 12 ounces: @@ -984,7 +986,7 @@ end) ``` ### `.toBeInstanceOf(prototype)` -Jest Deviation +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tobeinstanceofclass) ![Deviation](/img/deviation.svg) Use `.toBeInstanceOf(prototype)` to check that a value is an instance (or a derived instance) of a prototype class. This matcher uses the [`instanceof` method in LuauPolyfill](https://github.com/Roblox/luau-polyfill/blob/main/src/instanceof.lua) underneath. @@ -1023,7 +1025,7 @@ expect(C.new()).toBeInstanceOf(B) ``` ### `.toBeNil()` -Jest API change +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tobenull) ![API Change](/img/apichange.svg) Also under the alias: `.toBeNull()` @@ -1040,7 +1042,7 @@ end) ``` ### `.toBeTruthy()` -Jest Deviation +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tobetruthy) ![Deviation](/img/deviation.svg) Use `.toBeTruthy` when you don't care what a value is and you want to ensure a value is true in a boolean context. For example, let's say you have some application code that looks like: @@ -1063,7 +1065,7 @@ end) In Lua, there are two falsy values: `false` and `nil`. Everything else is truthy. ### `.toBeUndefined()` -Jest Deviation +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tobeundefined) ![Deviation](/img/deviation.svg) Use `.toBeUndefined()` to check that a variable is `nil`. @@ -1072,7 +1074,7 @@ Use `.toBeUndefined()` to check that a variable is `nil`. ::: ### `.toBeNan()` -Jest API change +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tobenan) ![API Change](/img/apichange.svg) Also under the alias: `.toBeNaN()` @@ -1086,7 +1088,7 @@ end) ``` ### `.toContain(item)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tocontainitem) ![Aligned](/img/aligned.svg) Use `.toContain` when you want to check that an item is in an array. For testing the items in the array, this uses `table.find`, which does a strict equality check. `.toContain` can also check whether a string is a substring of another string. This uses `string.find` with `plain = true` so magic characters are ignored. @@ -1099,7 +1101,7 @@ end) ``` ### `.toContainEqual(item)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tocontainequalitem) ![Aligned](/img/aligned.svg) Use `.toContainEqual` when you want to check that an item with a specific structure and values is contained in an array. For testing the items in the array, this matcher recursively checks the equality of all fields, rather than checking for table identity. @@ -1113,7 +1115,7 @@ end) ``` ### `.toEqual(value)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#toequalvalue) ![Aligned](/img/aligned.svg) Use `.toEqual` to compare recursively all properties of tables (also known as "deep" equality). It calls [Luau Polyfill's `Object.is`](https://github.com/Roblox/luau-polyfill/blob/main/src/Object/is.lua) to compare primitive values, which mostly behaves like the `==` operator. @@ -1149,7 +1151,7 @@ If differences between properties do not help you to understand why a test fails - rewrite `expect(received).never.toEqual(expected)` as `expect(received.equals(expected)).toBe(false)` ### `.toMatch(string | regexp)` -Jest API change +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tomatchregexp--string) ![API Change](/img/apichange.svg) Use `.toMatch` to check that a string matches a [Lua string pattern](https://developer.roblox.com/en-us/articles/string-patterns-reference). @@ -1174,7 +1176,7 @@ end) ``` ### `.toMatchInstance(table)` -Roblox only +![Roblox only](/img/roblox-only.svg) Use `.toMatchObject` to check that a Roblox Instance and its children matches all the properties defined in an expected table. @@ -1203,7 +1205,7 @@ end) ``` ### `.toMatchObject(table)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tomatchobjectobject) ![Aligned](/img/aligned.svg) Use `.toMatchObject` to check that a table matches a subset of the properties of an expected table. It will match received tables with properties that are **not** in the expected table. @@ -1250,7 +1252,7 @@ end) ``` ### `.toMatchSnapshot(propertyMatchers?, hint?)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tomatchsnapshotpropertymatchers-hint) ![Aligned](/img/aligned.svg) This ensures that a value matches the most recent snapshot. Check out [the Snapshot Testing guide](snapshot-testing) for more information. @@ -1259,7 +1261,7 @@ You can provide an optional `propertyMatchers` table argument, which has asymmet You can provide an optional `hint` string argument that is appended to the test name. Although Jest always appends a number at the end of a snapshot name, short descriptive hints might be more useful than numbers to differentiate **multiple** snapshots in a **single** `it` or `test` block. Jest sorts snapshots by name in the corresponding `.snap` file. ### `.toStrictEqual(value)` -Jest Deviation +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tostrictequalvalue) ![Deviation](/img/deviation.svg) Use `.toStrictEqual` to test that objects have the same types. @@ -1284,7 +1286,7 @@ end) ``` ### `.toThrow(error?)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tothrowerror) ![Aligned](/img/aligned.svg) Also under the alias: `.toThrowError(error?)` @@ -1307,7 +1309,7 @@ You can provide an optional argument to test that a specific error is thrown: - [regular expression](#regexp): error message **matches** the pattern - string: error message **includes** the substring -API change +![API Change](/img/apichange.svg) `.toThrow` can also handle custom Error objects provided by LuauPolyfill: @@ -1355,7 +1357,7 @@ end) ``` ### `.toThrowErrorMatchingSnapshot(hint?)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/expect#tothrowerrormatchingsnapshothint) ![Aligned](/img/aligned.svg) Use `.toThrowErrorMatchingSnapshot` to test that a function throws an error matching the most recent snapshot when it is called. diff --git a/docs/docs/GettingStarted.md b/docs/docs/GettingStarted.md index bee80b61..5dbdad8f 100644 --- a/docs/docs/GettingStarted.md +++ b/docs/docs/GettingStarted.md @@ -4,7 +4,7 @@ title: Getting Started slug: / --- -The Jest Lua API is similar to [the API used by JavaScript Jest.](https://jestjs.io/docs/27.x/api) +The Jest Roblox API is similar to [the API used by JavaScript Jest.](https://jest-archive-august-2023.netlify.app/docs/27.x/api) Jest Lua currently requires [`run-in-roblox`](https://github.com/rojo-rbx/run-in-roblox) to run from the command line. It can also be run directly inside of Roblox Studio. See issue [#2](https://github.com/jsdotlua/jest-lua/issues/2) for more. diff --git a/docs/docs/GlobalAPI.md b/docs/docs/GlobalAPI.md index a5faf6d7..60600238 100644 --- a/docs/docs/GlobalAPI.md +++ b/docs/docs/GlobalAPI.md @@ -2,9 +2,9 @@ id: api title: Globals --- -

Jest

+[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/api) -deviation +![Deviation](/img/deviation.svg) At the top of your test files, require `JestGlobals` from the `Packages` directory created by `rotriever`. @@ -30,7 +30,7 @@ import TOCInline from "@theme/TOCInline"; ## Reference ### `afterAll(fn, timeout)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/api#afterallfn-timeout) ![Aligned](/img/aligned.svg) Runs a function after all the tests in this file have completed. If the function returns a promise or is a generator, Jest Lua waits for that promise to resolve before continuing. @@ -71,7 +71,7 @@ If `afterAll` is inside a `describe` block, it runs at the end of the describe b If you want to run some cleanup after every test instead of after all tests, use `afterEach` instead. ### `afterEach(fn, timeout)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/api#aftereachfn-timeout) ![Aligned](/img/aligned.svg) Runs a function after each one of the tests in this file completes. If the function returns a promise or is a generator, Jest Lua waits for that promise to resolve before continuing. @@ -112,7 +112,7 @@ If `afterEach` is inside a `describe` block, it only runs after the tests that a If you want to run some cleanup just once, after all of the tests run, use `afterAll` instead. ### `beforeAll(fn, timeout)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/api#beforeallfn-timeout) ![Aligned](/img/aligned.svg) Runs a function before any of the tests in this file run. If the function returns a promise or is a generator, Jest Lua waits for that promise to resolve before running tests. @@ -150,7 +150,7 @@ If `beforeAll` is inside a `describe` block, it runs at the beginning of the des If you want to run something before every test instead of before any test runs, use `beforeEach` instead. ### `beforeEach(fn, timeout)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/api#beforeeachfn-timeout) ![Aligned](/img/aligned.svg) Runs a function before each of the tests in this file runs. If the function returns a promise or is a generator, Jest Lua waits for that promise to resolve before running the test. @@ -192,7 +192,7 @@ If `beforeEach` is inside a `describe` block, it runs for each test in the descr If you only need to run some setup code once, before any tests run, use `beforeAll` instead. ### `describe(name, fn)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/api#describename-fn) ![Aligned](/img/aligned.svg) `describe(name, fn)` creates a block that groups together several related tests. For example, if you have a `myBeverage` object that is supposed to be delicious but not sour, you could test it with: @@ -241,7 +241,7 @@ end) ``` ### `describe.each(table)(name, fn, timeout)` -Jest API change +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/api#describeeachtablename-fn-timeout) ![API Change](/img/apichange.svg) Use `describe.each` if you keep duplicating the same test suites with different data. `describe.each` allows you to write the test suite once and pass data in. @@ -300,7 +300,7 @@ end) ``` #### 2. `describe.each(...args)(name, fn, timeout)` -API change +![API Change](/img/apichange.svg) - `...args` - First argument is a string with headings separated by `|`, or a table with a single element containing that. @@ -334,7 +334,7 @@ end) ``` ### `describe.only(name, fn)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/api#describeonlyname-fn) ![Aligned](/img/aligned.svg) Also under the alias: `fdescribe(name, fn)` @@ -357,7 +357,7 @@ end) ``` ### `describe.only.each(table)(name, fn)` -Jest API change +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/api#describeonlyeachtablename-fn) ![API Change](/img/apichange.svg) Also under the aliases: `fdescribe.each(table)(name, fn)` and `` fdescribe.each`table`(name, fn) `` @@ -384,7 +384,7 @@ end) ``` #### `describe.only.each(...args)(name, fn)` -API change +![API Change](/img/apichange.svg) ```lua describe.only.each({'a | b | expected'}, @@ -405,7 +405,7 @@ end) ``` ### `describe.skip(name, fn)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/api#describeskipname-fn) ![Aligned](/img/aligned.svg) Also under the alias: `xdescribe(name, fn)` @@ -430,7 +430,7 @@ end) Using `describe.skip` is often a cleaner alternative to temporarily commenting out a chunk of tests. Beware that the `describe` block will still run. If you have some setup that also should be skipped, do it in a `beforeAll` or `beforeEach` block. ### `describe.skip.each(table)(name, fn)` -Jest API change +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/api#describeskipeachtablename-fn) ![API Change](/img/apichange.svg) Also under the aliases: `xdescribe.each(table)(name, fn)` and `xdescribe.each(...args)(name, fn)` @@ -457,7 +457,7 @@ end) ``` #### `describe.skip.each(...args)(name, fn)` -API change +![API Change](/img/apichange.svg) ```lua describe.skip.each({'a | b | expected'}, @@ -478,7 +478,7 @@ end) ``` ### `test(name, fn, timeout)` -Jest API change +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/api#testname-fn-timeout) ![API Change](/img/apichange.svg) Also under the alias: `it(name, fn, timeout)` @@ -512,7 +512,7 @@ end) Even though the call to `test` will return right away, the test doesn't complete until the promise resolves as well. ### `test.each(table)(name, fn, timeout)` -Jest API change +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/api#testeachtablename-fn-timeout) ![API Change](/img/apichange.svg) Also under the alias: `it.each(table)(name, fn)` and `it.each(...args)(name, fn)` @@ -551,7 +551,7 @@ end) ``` #### 2. `test.each(...args)(name, fn, timeout)` -API change +![API Change](/img/apichange.svg) - `...args` - First argument is a string with headings separated by `|`, or a table with a single element containing that. @@ -574,7 +574,7 @@ end) ``` ### `test.failing(name, fn, timeout)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/next/api#testfailingname-fn-timeout) ![Aligned](/img/aligned.svg) Also under the alias: `it.failing(name, fn, timeout)` @@ -603,7 +603,7 @@ end) ``` ### `test.only.failing(name, fn, timeout)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/next/api#testonlyfailingname-fn-timeout) ![Aligned](/img/aligned.svg) Also under the aliases: `it.only.failing(name, fn, timeout)` @@ -612,7 +612,7 @@ Also under the aliases: `it.only.failing(name, fn, timeout)` Use `test.only.failing` if you want to only run a specific failing test. ### `test.skip.failing(name, fn, timeout)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/next/api#testskipfailingname-fn-timeout) ![Aligned](/img/aligned.svg) Also under the aliases: `it.skip.failing(name, fn, timeout)` @@ -621,7 +621,7 @@ Also under the aliases: `it.skip.failing(name, fn, timeout)` Use `test.skip.failing` if you want to skip running a specific failing test. ### `test.only(name, fn, timeout)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/api#testonlyname-fn-timeout) ![Aligned](/img/aligned.svg) Also under the aliases: `it.only(name, fn, timeout)`, and `fit(name, fn, timeout)` @@ -646,7 +646,7 @@ Only the "it is raining" test will run in that test file, since it is run with ` Usually you wouldn't check code using `test.only` into source control - you would use it for debugging, and remove it once you have fixed the broken tests. ### `test.only.each(table)(name, fn)` -Jest API change +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/api#testonlyeachtablename-fn-1) ![API Change](/img/apichange.svg) Also under the aliases: `it.only.each(table)(name, fn)`, `fit.each(table)(name, fn)`, `` it.only.each`table`(name, fn) `` and `` fit.each`table`(name, fn) `` @@ -671,7 +671,7 @@ end) ``` #### `test.only.each(...args)(name, fn)` -API change +![API Change](/img/apichange.svg) ```lua test.only.each({'a | b | expected'}, @@ -689,7 +689,7 @@ end) ``` ### `test.skip(name, fn)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/api#testskipname-fn) ![Aligned](/img/aligned.svg) Also under the aliases: `it.skip(name, fn)`, `xit(name, fn)`, and `xtest(name, fn)` @@ -712,7 +712,7 @@ Only the "it is raining" test will run, since the other test is run with `test.s You could comment the test out, but it's often a bit nicer to use `test.skip` because it will maintain indentation and syntax highlighting. ### `test.skip.each(table)(name, fn)` -Jest API change +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/api#testskipeachtablename-fn) ![API Change](/img/apichange.svg) Also under the aliases: `it.skip.each(table)(name, fn)`, `xit.each(table)(name, fn)`, `xtest.each(table)(name, fn)`, `` it.skip.each`table`(name, fn) ``, `xit.each(..args)(name, fn) `` and `xtest.each(...args)(name, fn)` @@ -737,7 +737,7 @@ end) ``` #### `test.skip.each(...args)(name, fn)` -API change +![API Change](/img/apichange.svg) ```lua test.skip.each({'a | b | expected'}, @@ -755,7 +755,7 @@ end) ``` ### `test.todo(name)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/api#testtodoname) ![Aligned](/img/aligned.svg) Also under the alias: `it.todo(name)` diff --git a/docs/docs/GlobalMocks.md b/docs/docs/GlobalMocks.md new file mode 100644 index 00000000..d4e57198 --- /dev/null +++ b/docs/docs/GlobalMocks.md @@ -0,0 +1,109 @@ +--- +id: global-mocks +title: Global Mocks +--- + +Roblox only + +It can be desirable to track how an implementation interacts with Luau globals. +For example, you might want to test that a certain message is printed to the +console, or you might want to take control of the random number generator to get +a deterministic, predictable sequence of numbers. + +It isn't normally easy to mock these functions in the global Luau environment, +but Jest can replace their implementations for you, giving you a familiar +interface as if you're mocking any other regular function. + +:::warning + +Jest Roblox does not support mocking all globals - only a few are whitelisted. +If you try to mock a global which is not whitelisted, you will see an error +message that looks similar to this: + +``` +Jest does not yet support mocking the require global. +``` + +Most notably, Jest Roblox does not support mocking these globals: + +- `game:GetService()` and other Instance methods (an API for this is being investigated) +- the `require()` function (use [`jest.mock()`](jest-object) instead) +- task scheduling functions (use [Timer Mocks](timer-mocks) instead) + +::: + +## Mock a Global Function + +In the following example, we mock the global `print()` function so that our test +can see what's being printed. This is done as if you're spying on a table using +`jest.spyOn()`, except you use `jest.globalEnv` as the table. + +```lua title="limerick.lua" +return function() + print("There once was a print() in a test") + print("It would cause the maintainers unrest") + print("Printing all 'round the clock") + print("Beyond what they could mock") + print("Until globalEnv landed in Jest!") +end +``` + +```lua title="__tests__/limerick.spec.lua" +local jest = JestGlobals.jest + +test('mentions print() in the first line', function() + local limerick = require(Workspace.limerick) + + local mockPrint = jest.spyOn(jest.globalEnv, "print") + mockPrint.mockImplementationOnce(function(firstLine: string, ...: string) + expect(firstLine).toEqual(expect.stringContaining("print()")) + end) + + limerick() +end) +``` + +## Mocking Globals in Libraries + +You can mock functions in libraries like `math` by indexing into +`jest.globalEnv` with the name of the library. The following example shows how +to mock `math.random()` to return a predictable number. + +```lua title="diceRoll.lua" +return function() + return "You rolled a " .. math.random(1, 6) +end +``` + +```lua title="__tests__/diceRoll.spec.lua" +local jest = JestGlobals.jest + +test('correctly formats returned message', function() + local diceRoll = require(Workspace.diceRoll) + + local mockRandom = jest.spyOn(jest.globalEnv.math, "random") + mockRandom.mockImplementationOnce(function(_min: number?, _max: number?) + return 5 + end) + + expect(diceRoll()).toBe("You rolled a 5") +end) +``` + +## Use Original Implementation + +You can use the original (non-mocked) variant of the global function at any +time. Original implementations are available by indexing into `globalEnv` with +the name of the function you need access to. + +```lua +local mockRandom = jest.spyOn(jest.globalEnv.math, "random") +mockRandom.mockImplementation(function(_min: number?, _max: number?) + return 5 +end) + +-- will always be 5 +local mocked = math.random() +-- will be some random number between 0 and 1 +local unmocked = jest.globalEnv.math.random() +``` \ No newline at end of file diff --git a/docs/docs/JestBenchmarkAPI.md b/docs/docs/JestBenchmarkAPI.md index 65deefb2..d264bc27 100644 --- a/docs/docs/JestBenchmarkAPI.md +++ b/docs/docs/JestBenchmarkAPI.md @@ -2,7 +2,7 @@ id: jest-benchmark title: JestBenchmark --- -Roblox only +![Roblox only](/img/roblox-only.svg) Benchmarks are useful tools for gating performance in CI, optimizing code, and capturing performance gains. JestBenchmark aims to make it easier to write benchmarks in the Luau language. @@ -15,8 +15,7 @@ local CustomReporters = JestBenchmark.CustomReporters ``` ### benchmark - -Roblox only +![Roblox only](/img/roblox-only.svg) The `benchmark` function is a wrapper around `test` that provides automatic profiling for FPS and benchmark running time. Similar to `test`, it exposes `benchmark.only` and `benchmark.skip` to focus and skip tests, respectively. @@ -33,12 +32,12 @@ end) ``` ### Reporter -Roblox only +![Roblox only](/img/roblox-only.svg) The `Reporter` object collects and aggregates data generated during a benchmark. For example, you may have an FPS reporter that collects the delta time between each frame in a benchmark and calculates the average FPS over the benchmark. ### initializeReporter -Roblox only +![Roblox only](/img/roblox-only.svg) `initializeReporter` accepts a metric name and collector function as arguments and returns a Reporter object. The metric name is the label given to the data collected. The collector function accepts a list of values and reduces them to a single value. @@ -60,7 +59,7 @@ local averageReporter = initializeReporter("average", average) ``` ### Reporter.start() -Roblox only +![Roblox only](/img/roblox-only.svg) A reporting segment is initialized with `Reporter.start(sectionName: string)`. All values reported within the segment are collected as a group and reduced to a single value in `Reporter.finish`. The segment is labeled with the `sectionName` argument. Reporter segments can be nested or can run sequentially. All Reporter segments must be concluded by calling `Reporter.stop` @@ -85,27 +84,27 @@ local sectionNames, sectionValues = averageReporter.finish() ``` ### Reporter.stop() -Roblox only +![Roblox only](/img/roblox-only.svg) When `Reporter.stop` is called, the reporter section at the top of the stack is popped off, and a section of reported values are marked for collection at the end of benchmark. No collection is done during the benchmark runtime, since this could reduce performance. ### Reporter.report -Roblox only +![Roblox only](/img/roblox-only.svg) When `Reporter.report(value: T)` is called, a value is added to the report queue. The values passed to report are reduced when `reporter.finish` is called. ### Reporter.finish -Roblox only +![Roblox only](/img/roblox-only.svg) `Reporter.finish` should be called at the end of the benchmark runtime. It returns a list of section names and a list of section values generated according to the collectorFn. Values are returned in order of completion. ### Profiler -Roblox only +![Roblox only](/img/roblox-only.svg) The `Profiler` object controls a set of reporters and reports data generated during a benchmark. The Profiler is initialized with the `initializeProfiler` function. A profiling segment is started by calling `Profiler.start` and stopped by calling `Profiler.stop`. These segments can be called sequentially or can be nested. Results are generated by calling `Profiler.finish`. ### initializeProfiler -Roblox only +![Roblox only](/img/roblox-only.svg) `intializeProfiler` accepts a list of reporters and an outputFn as arguments and returns a Profiler object. @@ -123,7 +122,7 @@ local profiler = initializeProfiler(reporters, outputFn) ``` ### Profiler.start -Roblox only +![Roblox only](/img/roblox-only.svg) When `Profiler.start(sectionName: string)` is called, reporter.start is called for each reporter in the reporters list. Each Profiler section must be concluded with a `Profiler.stop()` call. @@ -134,17 +133,17 @@ Profiler.stop() ``` ### Profiler.stop -Roblox only +![Roblox only](/img/roblox-only.svg) When `Profiler.stop()` is called, reporter.stop is called for each reporter in the reporters list. Calling `Profiler.stop` without first calling `Profiler.start` will result in an error. ### Profiler.finish -Roblox only +![Roblox only](/img/roblox-only.svg) When `Profiler.finish` is called, reporter.finish is called for each reporter in the reporters list. The results of each finish call is then printed by the outputFn passed to the Profiler. ### CustomReporters -Roblox only +![Roblox only](/img/roblox-only.svg) By default, the `benchmark` function has two reporters attached: FPS and SectionTime. However, you may want to add custom reporters, perhaps to track Rodux action dispatches, time to interactive, or React re-renders. To enable this, the CustomReporters object exports `useCustomReporters`, which allows the user to add additional reporters to the Profiler. These reporters are passed in a key-value table as the second argument in the provided benchmark function. This should be used in combination with `useDefaultReporters`, which removes all custom reporters from the Profiler. @@ -172,7 +171,7 @@ end) ``` ### MetricLogger -Roblox only +![Roblox only](/img/roblox-only.svg) By default, benchmarks output directly to stdout. This may not be desirable in all cases. For example, you may want to output results to a BindableEvent or a file stream. The MetricLogger object exposes a `useCustomMetricLogger` function, which allows the user to override the default output function. This should be used in combination with `useDefaultMetricLogger`, which resets the output function to the default value diff --git a/docs/docs/JestObjectAPI.md b/docs/docs/JestObjectAPI.md index 9d1e1898..d433063c 100644 --- a/docs/docs/JestObjectAPI.md +++ b/docs/docs/JestObjectAPI.md @@ -2,11 +2,11 @@ id: jest-object title: The Jest Object --- -

Jest

+[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/jest-object) The methods in the `jest` object help create mocks and let you control Jest Lua's overall behavior. -deviation +![Deviation](/img/deviation.svg) It must be imported explicitly from `JestGlobals`. ```lua @@ -22,7 +22,7 @@ import TOCInline from "@theme/TOCInline"; ## Mock Modules ### `jest.mock(module, factory)` -Jest API change +[![Jest](/img/jestjs.svg)](https://jestjs.io/docs/jest-object#jestmockmodulename-factory-options) ![API Change](/img/apichange.svg) Mocks a module with an mocked version when it is being required. The second argument must be used to specify the value of the mocked module. ```lua title="mockedModule.lua" @@ -53,7 +53,7 @@ Modules that are mocked with `jest.mock` are mocked only for the file that calls Returns the `jest` object for chaining. ### `jest.unmock(module)` -Jest API change +[![Jest](/img/jestjs.svg)](https://jestjs.io/docs/jest-object#jestunmockmodulename) ![API Change](/img/apichange.svg) Indicates that the module system should never return a mocked version of the specified module from `require()` (e.g. that it should always return the real module). @@ -67,7 +67,7 @@ end) ``` ### `jest.requireActual(module)` -Jest API change +[![Jest](/img/jestjs.svg)](https://jestjs.io/docs/jest-object#jestrequireactualmodulename) ![API Change](/img/apichange.svg) Returns the actual module instead of a mock, bypassing all checks on whether the module should receive a mock implementation or not. @@ -79,7 +79,7 @@ end) ``` ### `jest.isolateModules(fn)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/jest-object#jestisolatemodulesfn) ![Aligned](/img/aligned.svg) `jest.isolateModules(fn)` creates a sandbox registry for the modules that are loaded inside the callback function. This is useful to isolate specific modules for every test so that local module state doesn't conflict between tests. @@ -93,7 +93,7 @@ local otherCopyOfMyModule = require(Workspace.MyModule) ``` ### `jest.resetModules()` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/jest-object#jestresetmodules) ![Aligned](/img/aligned.svg) Resets the module registry - the cache of all required modules. This is useful to isolate modules where local state might conflict between tests. @@ -129,7 +129,7 @@ Returns the `jest` object for chaining. ## Mock Functions ### `jest.fn(implementation)` -Jest Deviation +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/jest-object#jestfnimplementation) ![Deviation](/img/deviation.svg) Returns a new, unused [mock function](mock-function-api). Optionally takes a mock implementation. @@ -145,8 +145,47 @@ local returnsTrue = jest.fn(function() return true end) print(returnsTrue()) -- true ``` +### `jest.spyOn(object, methodName)` +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/28.x/jest-object#jestspyonobject-methodname) ![Aligned](/img/aligned.svg) + +Creates a mock function similar to `jest.fn` but also tracks calls to `object[methodName]`. Returns a Jest [mock function](MockFunctionAPI.md). + +_Note: By default, `jest.spyOn` also calls the **spied** method. This is different behavior from most other test libraries. If you want to overwrite the original function, you can use `jest.spyOn(object, methodName):mockImplementation(function() ... end)` or `object[methodName] = jest.fn(function ... end)`_ + +Example: + +```lua +local video = { + play = function() + return true + end +} + +return video +``` + +Example test: + +```lua +local video = require(Workspace.video); + +test("plays video", function() + local spy = jest.spyOn(video, "play") + local isPlaying = video.play() + + expect(spy).toHaveBeenCalled() + expect(isPlaying).toBe(true) + + spy:mockRestore() +end) +``` + +:::info +The `jest.spyOn(object, methodName, accessType?)` variant is not currently supported in jest-roblox. +::: + ### `jest.clearAllMocks()` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/jest-object#jestclearallmocks) ![Aligned](/img/aligned.svg) Clears the `mock.calls`, `mock.instances` and `mock.results` properties of all mocks. Equivalent to calling [`.mockClear()`](mock-function-api#mockfnmockclear) on every mocked function. @@ -155,14 +194,14 @@ This can be included in a `beforeEach()` block in your text fixture to clear out Returns the `jest` object for chaining. ### `jest.resetAllMocks()` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/jest-object#jestresetallmocks) ![Aligned](/img/aligned.svg) Resets the state of all mocks. Equivalent to calling [`.mockReset()`](mock-function-api#mockfnmockreset) on every mocked function. Returns the `jest` object for chaining. ### `mockFn.mockImplementation(fn)` -Jest Deviation +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/mock-function-api#mockfnmockimplementationfn) ![Deviation](/img/deviation.svg) Accepts a function that should be used as the implementation of the mock. The mock itself will still record all calls that go into and instances that come from itself – the only difference is that the implementation will also be executed when the mock is called. @@ -153,7 +153,7 @@ Mocks should be lightweight and easy to maintain and/or refactor, so users shoul ::: ### `mockFn.mockImplementationOnce(fn)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/mock-function-api#mockfnmockimplementationoncefn) ![Aligned](/img/aligned.svg) Accepts a function that will be used as an implementation of the mock for one call to the mocked function. Can be chained so that multiple function calls produce different results. @@ -180,7 +180,7 @@ print(myMockFn()) -- 'default ``` ### `mockFn.mockName(value)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/mock-function-api#mockfnmocknamevalue) ![Aligned](/img/aligned.svg) Accepts a string to use in test result output in place of "jest.fn()" to indicate which mock function is being referenced. @@ -202,12 +202,12 @@ Received number of calls: 0 ``` ### `mockFn.mockReturnThis()` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/mock-function-api#mockfnmockreturnthis) ![Aligned](/img/aligned.svg) Sets the implementation of `mockFn` to return itself whenenever the mock function is called. ### `mockFn.mockReturnValue(value)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/mock-function-api#mockfnmockreturnvaluevalue) ![Aligned](/img/aligned.svg) Accepts a value that will be returned whenever the mock function is called. @@ -220,7 +220,7 @@ mock() -- 43 ``` ### `mockFn.mockReturnValueOnce(value)` -Jest Aligned +[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/mock-function-api#mockfnmockreturnvalueoncevalue) ![Aligned](/img/aligned.svg) Accepts a value that will be returned for one call to the mock function. Can be chained so that successive calls to the mock function return different values. When there are no more `mockReturnValueOnce` values to use, calls will return a value specified by `mockReturnValue`. diff --git a/docs/docs/MockFunctions.md b/docs/docs/MockFunctions.md index eccdf191..ab1561f2 100644 --- a/docs/docs/MockFunctions.md +++ b/docs/docs/MockFunctions.md @@ -2,7 +2,7 @@ id: mock-functions title: Mock Functions --- -

Jest

+[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/mock-functions) Mock functions allow you to test the links between code by erasing the actual implementation of a function, capturing calls to the function (and the parameters passed in those calls), capturing instances of constructor functions when instantiated with `new`, and allowing test-time configuration of return values. @@ -193,4 +193,4 @@ expect(mockFunc.mock.calls[#mockFunc.mock.calls]).toEqual({ expect(mockFunc.mock.calls[#mockFunc.mock.calls][1]).toBe(42) ``` -For a complete list of matchers, check out the [reference docs](expect). \ No newline at end of file +For a complete list of matchers, check out the [reference docs](expect). diff --git a/docs/docs/SetupAndTeardown.md b/docs/docs/SetupAndTeardown.md index 7fac8a2f..a68c2bcb 100644 --- a/docs/docs/SetupAndTeardown.md +++ b/docs/docs/SetupAndTeardown.md @@ -2,7 +2,7 @@ id: setup-teardown title: Setup and Teardown --- -

Jest

+[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/setup-teardown) Often while writing tests you have some setup work that needs to happen before tests run, and you have some finishing work that needs to happen after tests run. Jest Lua provides helper functions to handle this. @@ -222,4 +222,4 @@ test('this test will not run', function() end) ``` -If you have a test that often fails when it's run as part of a larger suite, but doesn't fail when you run it alone, it's a good bet that something from a different test is interfering with this one. You can often fix this by clearing some shared state with `beforeEach`. If you're not sure whether some shared state is being modified, you can also try a `beforeEach` that logs data. \ No newline at end of file +If you have a test that often fails when it's run as part of a larger suite, but doesn't fail when you run it alone, it's a good bet that something from a different test is interfering with this one. You can often fix this by clearing some shared state with `beforeEach`. If you're not sure whether some shared state is being modified, you can also try a `beforeEach` that logs data. diff --git a/docs/docs/SnapshotTesting.md b/docs/docs/SnapshotTesting.md index 176e1af2..165712bf 100644 --- a/docs/docs/SnapshotTesting.md +++ b/docs/docs/SnapshotTesting.md @@ -2,7 +2,7 @@ id: snapshot-testing title: Snapshot Testing --- -

Jest

+[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/snapshot-testing) Snapshot tests are a very useful tool whenever you want to make sure your UI does not change unexpectedly. @@ -91,7 +91,7 @@ runCLI(Project, { You'll also need to pass the following flags to give `roblox-cli` the proper permissions to update snapshots: ``` ---load.asRobloxScript --fs.readwrite="$(pwd)" +--load.asRobloxScript --fs.readwrite="$(pwd)" ``` :::tip @@ -268,4 +268,4 @@ Although it is possible to write snapshot files manually, that is usually not ap ### Does code coverage work with snapshot testing? -Yes, as well as with any other test. \ No newline at end of file +Yes, as well as with any other test. diff --git a/docs/docs/TestingAsyncCode.md b/docs/docs/TestingAsyncCode.md index 5f02c29a..ed220f90 100644 --- a/docs/docs/TestingAsyncCode.md +++ b/docs/docs/TestingAsyncCode.md @@ -2,7 +2,7 @@ id: asynchronous title: Testing Asynchronous Code --- -

Jest

+[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/asynchronous) It's common in Lua for code to run asynchronously. When you have code that runs asynchronously, Jest Lua needs to know when the code it is testing has completed, before it can move on to another test. Jest Lua has several ways to handle this. @@ -75,4 +75,4 @@ If the `expect` statement fails, it throws an error and `done()` is not called. :::danger `done()` should not be mixed with Promises in your tests. -::: \ No newline at end of file +::: diff --git a/docs/docs/TimerMocks.md b/docs/docs/TimerMocks.md index c40c90af..2f1e8653 100644 --- a/docs/docs/TimerMocks.md +++ b/docs/docs/TimerMocks.md @@ -2,9 +2,9 @@ id: timer-mocks title: Timer Mocks --- -

Jest

+[![Jest](/img/jestjs.svg)](https://jest-archive-august-2023.netlify.app/docs/27.x/timer-mocks) -deviation +![Deviation](/img/deviation.svg) The Lua and Roblox native timer functions (i.e., `delay()`, `tick()`, `os.time()`, `os.clock()`) are less than ideal for a testing environment since they depend on real time to elapse. Jest Lua can swap out timers with functions that allow you to control the passage of time. [Great Scott!](https://www.youtube.com/watch?v=QZoJ2Pt27BY) @@ -113,7 +113,7 @@ end) ``` ## Advance Timers by Time -deviation +![Deviation](/img/deviation.svg) Another possibility is use `jest.advanceTimersByTime(secsToRun)`. When this API is called, all timers are advanced by `secsToRun` seconds. All pending "macro-tasks" that have been queued, and would be executed during this time frame, will be executed. Additionally, if those macro-tasks schedule new macro-tasks that would be executed within the same time frame, those will be executed until there are no more macro-tasks remaining in the queue that should be run within `secsToRun` seconds. @@ -153,7 +153,7 @@ end) Lastly, it may occasionally be useful in some tests to be able to clear all of the pending timers. For this, we have `jest.clearAllTimers()`. ## Setting Engine Frame Time -Roblox only +![Roblox only](/img/roblox-only.svg) By default, Jest Lua processes fake timers in continuous time. However, because the Roblox engine processes timers only once per frame, this may not accurately reflect engine behavior. diff --git a/docs/docs/UsingMatchers.md b/docs/docs/UsingMatchers.md index 4c22a33c..70f770f2 100644 --- a/docs/docs/UsingMatchers.md +++ b/docs/docs/UsingMatchers.md @@ -40,7 +40,7 @@ end) ``` ## Truthiness -Deviation +![Deviation](/img/deviation.svg) In tests, you sometimes need to distinguish between `nil`, and `false`, but you sometimes do not want to treat these differently. Jest Lua contains helpers that let you be explicit about what you want. @@ -101,7 +101,7 @@ end) ``` ## Strings -API change +![API Change](/img/apichange.svg) You can check strings against [Lua string patterns](https://developer.roblox.com/en-us/articles/string-patterns-reference) with `toMatch`: @@ -199,7 +199,7 @@ end) ``` ## Exceptions -API change +![API Change](/img/apichange.svg) If you want to test whether a particular function throws an error when it's called, use `toThrow`. diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 9bb89d49..9e97a07c 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -1,6 +1,6 @@ /** @type {import('@docusaurus/types').DocusaurusConfig} */ -const VERSION = '3.6.1-rc.2'; +const VERSION = '3.9.0'; module.exports = { title: 'Jest Lua', @@ -12,6 +12,7 @@ module.exports = { onBrokenLinks: 'throw', onBrokenMarkdownLinks: 'warn', favicon: 'img/favicon.svg', + staticDirectories: ['static'], themeConfig: { navbar: { title: `Jest Lua v${VERSION}`, diff --git a/docs/sidebars.js b/docs/sidebars.js index 3907795a..c421bc50 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -17,6 +17,7 @@ module.exports = { items: [ 'snapshot-testing', 'timer-mocks', + 'global-mocks', 'testez-migration', 'upgrading-to-jest3', ] diff --git a/src/diff-sequences/README.md b/src/diff-sequences/README.md index ce1c75fb..3b1f0cf8 100644 --- a/src/diff-sequences/README.md +++ b/src/diff-sequences/README.md @@ -1,10 +1,8 @@ # diff-sequences -Status: :heavy_check_mark: Ported +Upstream: https://github.com/facebook/jest/tree/v27.4.7/packages/diff-sequences -Source: https://github.com/facebook/jest/tree/v27.4.7/packages/diff-sequences - -Version: v27.4.7 +Compare items in two sequences to find a longest common subsequence. --- @@ -16,13 +14,3 @@ Version: v27.4.7 * Lua is 1 indexed so array indices are replaced with `index + 1`. * Uses of `NOT_YET_SET` are replaced with just a 0 since this is a JS-specific workaround. * Lua treats `0` as a true value so `nChange || baDeltaLength` needs to be written as `nChange ~= 0 and nChange or baDeltaLength`. - -### :x: Excluded -``` -perf -src/index.property.test.ts -``` - -### :package: [Dependencies](https://github.com/facebook/jest/blob/v27.4.7/packages/diff-sequences/package.json) -| Package | Version | Status | Notes | -| ------- | ------- | ------ | ----- | diff --git a/src/emittery/README.md b/src/emittery/README.md index 5705ab20..78547d83 100644 --- a/src/emittery/README.md +++ b/src/emittery/README.md @@ -1,19 +1,7 @@ # emittery -Status: :hammer: In Progress - -Source: https://github.com/sindresorhus/emittery/tree/v0.11.0 - -Version: v0.11.0 +Upstream: https://github.com/sindresorhus/emittery/tree/v0.11.0 --- ### :pencil2: Notes - -### :x: Excluded -``` -``` - -### :package: [Dependencies](https://github.com/sindresorhus/emittery/tree/v0.11.0/package.json) -| Package | Version | Status | Notes | -| ------- | ------- | ------ | ----- | diff --git a/src/expect/README.md b/src/expect/README.md index 9ba7cadf..806cbe91 100644 --- a/src/expect/README.md +++ b/src/expect/README.md @@ -1,10 +1,8 @@ # expect -Status: :hammer: In Progress +Upstream: https://github.com/facebook/jest/tree/v27.4.7/packages/expect -Source: https://github.com/facebook/jest/tree/v27.4.7/packages/expect - -Version: v27.4.7 +This package exports the `expect` function used in Jest. You can find its documentation in the [Jest documentation](https://roblox.github.io/jest-roblox-internal). --- @@ -41,15 +39,3 @@ Version: v27.4.7 * tables with a `message` key that has a string value * objects with a `__tostring` metamethod * :warning: Currently, the spyMatchers have undefined behavior when used with jest-mock and function calls with `nil` arguments, this should be fixed by ADO-1395 (the matchers may work incidentally but there are no guarantees) - -### :x: Excluded -``` -``` - -### :package: [Dependencies](https://github.com/facebook/jest/blob/v27.4.7/packages/expect/package.json) -| Package | Version | Status | Notes | -| -------------------- | ------- | ------------------------- | -------------------------------------------- | -| `@jest/types` | 27.4.2 | :heavy_check_mark: Ported | | -| `jest-get-type` | 27.4.0 | :heavy_check_mark: Ported | | -| `jest-matcher-utils` | 27.4.6 | :heavy_check_mark: Ported | | -| `jest-message-util` | 27.4.6 | :hammer: In Progress | Used for filtering stacktraces, low priority | diff --git a/src/jest-benchmark/README.md b/src/jest-benchmark/README.md index 798294e0..d57124c7 100644 --- a/src/jest-benchmark/README.md +++ b/src/jest-benchmark/README.md @@ -1,7 +1,5 @@ # jest-benchmark -Status: :hammer: In Progress +* No upstream. Roblox only.* -Source: no upstream, roblox-only - -Version: v0.0.1 +This package exports the benchmarking library used in Jest. You can find its documentation in the [Jest documentation](https://roblox.github.io/jest-roblox-internal). diff --git a/src/jest-benchmark/src/benchmark.lua b/src/jest-benchmark/src/benchmark.lua index 386b1f1b..879c312f 100644 --- a/src/jest-benchmark/src/benchmark.lua +++ b/src/jest-benchmark/src/benchmark.lua @@ -68,9 +68,6 @@ local function wrapBenchFnInProfiler(testName: Circus_TestName, benchFn: BenchFn Profiler.stop() Profiler.finish() - - -- Force gc step - task.wait() end end diff --git a/src/jest-circus/README.md b/src/jest-circus/README.md index a05c729b..9093fd91 100644 --- a/src/jest-circus/README.md +++ b/src/jest-circus/README.md @@ -1,40 +1,7 @@ # jest-circus -Status: :hammer: In Progress - -Source: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-circus - -Version: v27.4.7 +Upstream: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-circus --- ### :pencil2: Notes - -### :x: Excluded - - -### :package: [Dependencies](https://github.com/facebook/jest/blob/v27.4.7/packages/jest-circus/package.json) - -| Package | Version | Status | Notes | -| ------------------------------ | ------- | ------------------------- | ------------------------------------ | -| `@jest/environment` | ^27.4.6 | :heavy_check_mark: Ported | | -| `@jest/test-result` | ^27.4.6 | :heavy_check_mark: Ported | | -| `@jest/types` | ^27.4.2 | :heavy_check_mark: Ported | | -| `@types/node` | * | :x: Will not port | | -| `chalk` | ^4.0.0 | :heavy_check_mark: Ported | | -| `co` | ^4.6.0 | :x: Will not port | | -| `dedent` | ^0.7.0 | :x: Will not port | Using implementation from graphql-js | -| `expect` | ^27.4.6 | :heavy_check_mark: Ported | | -| `is-generator-fn` | ^2.0.0 | :x: Will not port | | -| `jest-each` | ^27.4.6 | :hammer: In Progress | | -| `jest-matcher-utils` | ^27.4.6 | :heavy_check_mark: Ported | | -| `jest-message-util` | ^27.4.6 | :heavy_check_mark: Ported | | -| `jest-runtime` | ^27.4.6 | | | -| `jest-snapshot` | ^27.4.6 | :heavy_check_mark: Ported | | -| `jest-util` | ^27.4.2 | :hammer: In Progress | | -| `pretty-format` | ^27.4.6 | :heavy_check_mark: Ported | | -| `slash` | ^3.0.0 | :x: Will not port | | -| `stack-utils` | ^2.0.3 | :x: Will not port | | -| `throat` | ^6.0.1 | :x: Will not port | | -| `jest-snapshot-serializer-raw` | 1.2.0 | :heavy_check_mark: Ported | | - diff --git a/src/jest-circus/src/circus/__tests__/afterAll.spec.lua b/src/jest-circus/src/circus/__tests__/afterAll.spec.lua index 66c5ebb8..35a58d03 100644 --- a/src/jest-circus/src/circus/__tests__/afterAll.spec.lua +++ b/src/jest-circus/src/circus/__tests__/afterAll.spec.lua @@ -12,6 +12,12 @@ local JestGlobals = require("@pkg/@jsdotlua/jest-globals") local expect = JestGlobals.expect local it = JestGlobals.it +-- ROBLOX deviation: these tests require loadmodule +local loadModuleEnabled = pcall((debug :: any).loadmodule, Instance.new("ModuleScript")) +if not loadModuleEnabled then + it = it.skip :: any +end + it("tests are not marked done until their parent afterAll runs", function() local stdout = runTest([[ describe("describe", function() diff --git a/src/jest-circus/src/circus/__tests__/baseTest.spec.lua b/src/jest-circus/src/circus/__tests__/baseTest.spec.lua index 6bf69c43..76a8e369 100644 --- a/src/jest-circus/src/circus/__tests__/baseTest.spec.lua +++ b/src/jest-circus/src/circus/__tests__/baseTest.spec.lua @@ -12,6 +12,12 @@ local JestGlobals = require("@pkg/@jsdotlua/jest-globals") local expect = JestGlobals.expect local test = JestGlobals.test +-- ROBLOX deviation: these tests require loadmodule +local loadModuleEnabled = pcall((debug :: any).loadmodule, Instance.new("ModuleScript")) +if not loadModuleEnabled then + test = test.skip :: any +end + test("simple test", function() local stdout = runTest([[ describe("describe", function() @@ -25,15 +31,15 @@ test("simple test", function() end) -- ROBLOX deviation START: see if we can make this work later -- test("function descriptors", function() -test.skip("function descriptors", function() - -- ROBLOX deviation END - local stdout = runTest([[ - describe(function describer() end, function() - test(class One {}, function() end); - end) - ]]).stdout - expect(stdout).toMatchSnapshot() -end) +-- test.skip("function descriptors", function() +-- -- ROBLOX deviation END +-- local stdout = runTest([[ +-- describe(function describer() end, function() +-- test(class One {}, function() end); +-- end) +-- ]]).stdout +-- expect(stdout).toMatchSnapshot() +-- end) test("failures", function() local stdout = runTest([[ describe("describe", function() diff --git a/src/jest-circus/src/circus/__tests__/formatNodeAssertErrors.roblox.spec.lua b/src/jest-circus/src/circus/__tests__/formatNodeAssertErrors.roblox.spec.lua index 452ea88f..8bac69c2 100644 --- a/src/jest-circus/src/circus/__tests__/formatNodeAssertErrors.roblox.spec.lua +++ b/src/jest-circus/src/circus/__tests__/formatNodeAssertErrors.roblox.spec.lua @@ -46,6 +46,7 @@ describe("formatNodeAssertErrors", function() parent = {} :: any, seenDone = false, } + assertionError.stack = pruneDeps(assertionError.stack) formatNodeAssertErrors(nil, { name = "test_done", test = test, diff --git a/src/jest-circus/src/circus/__tests__/hooks.spec.lua b/src/jest-circus/src/circus/__tests__/hooks.spec.lua index 34aceda9..72b209d1 100644 --- a/src/jest-circus/src/circus/__tests__/hooks.spec.lua +++ b/src/jest-circus/src/circus/__tests__/hooks.spec.lua @@ -12,6 +12,12 @@ local JestGlobals = require("@pkg/@jsdotlua/jest-globals") local expect = JestGlobals.expect local it = JestGlobals.it +-- ROBLOX deviation: these tests require loadmodule +local loadModuleEnabled = pcall((debug :: any).loadmodule, Instance.new("ModuleScript")) +if not loadModuleEnabled then + it = it.skip :: any +end + it("beforeEach is executed before each test in current/child describe blocks", function() local stdout = runTest([[ describe("describe", function() diff --git a/src/jest-circus/src/circus/formatNodeAssertErrors.lua b/src/jest-circus/src/circus/formatNodeAssertErrors.lua index 45b3ab5b..d4bec0be 100644 --- a/src/jest-circus/src/circus/formatNodeAssertErrors.lua +++ b/src/jest-circus/src/circus/formatNodeAssertErrors.lua @@ -16,6 +16,7 @@ local RobloxShared = require("@pkg/@jsdotlua/jest-roblox-shared") local escapePatternCharacters = RobloxShared.escapePatternCharacters local normalizePromiseError = RobloxShared.normalizePromiseError local Error = LuauPolyfill.Error +local cleanLoadStringStack = RobloxShared.cleanLoadStringStack -- ROBLOX deviation END type Record = { [K]: T } diff --git a/src/jest-config/README.md b/src/jest-config/README.md index e0ebd888..6cead5ac 100644 --- a/src/jest-config/README.md +++ b/src/jest-config/README.md @@ -1,10 +1,6 @@ # jest-config -Status: :hammer: In Progress - -Source: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-config - -Version: v27.4.7 +Upstream: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-config --- @@ -136,11 +132,3 @@ Version: v27.4.7 * `watchAll` * `watchman` * `watchPathIgnorePatterns` - -### :x: Excluded -``` -``` - -### :package: [Dependencies](https://github.com/facebook/jest/blob/v27.4.7/packages/jest-config/package.json) -| Package | Version | Status | Notes | -| ------- | ------- | ------ | ----- | diff --git a/src/jest-console/README.md b/src/jest-console/README.md index b6471e54..bee2067a 100644 --- a/src/jest-console/README.md +++ b/src/jest-console/README.md @@ -1,19 +1,7 @@ # jest-console -Status: :heavy_check_mark: Ported - -Source: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-console - -Version: v27.4.7 +Upstream: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-console --- ### :pencil2: Notes - -### :x: Excluded -``` -``` - -### :package: [Dependencies](https://github.com/facebook/jest/blob/v27.4.7/packages/jest-console/package.json) -| Package | Version | Status | Notes | -| ------- | ------- | ------ | ----- | diff --git a/src/jest-core/README.md b/src/jest-core/README.md index 4bb7cd74..f0cd0bf3 100644 --- a/src/jest-core/README.md +++ b/src/jest-core/README.md @@ -1,19 +1,7 @@ # jest-core -Status: :hammer: In Progress - -Source: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-core - -Version: v27.4.7 +Upstream: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-core --- ### :pencil2: Notes - -### :x: Excluded -``` -``` - -### :package: [Dependencies](https://github.com/facebook/jest/blob/v27.4.7/packages/jest-core/package.json) -| Package | Version | Status | Notes | -| ------- | ------- | ------ | ----- | diff --git a/src/jest-core/src/runJest.lua b/src/jest-core/src/runJest.lua index 49c12e12..cd8aba5a 100644 --- a/src/jest-core/src/runJest.lua +++ b/src/jest-core/src/runJest.lua @@ -11,6 +11,7 @@ local Array = LuauPolyfill.Array local Object = LuauPolyfill.Object local Set = LuauPolyfill.Set local console = LuauPolyfill.console +local Boolean = LuauPolyfill.Boolean type Array = LuauPolyfill.Array type Object = LuauPolyfill.Object type Promise = LuauPolyfill.Promise @@ -92,6 +93,10 @@ local process = nodeUtils.process local exit = nodeUtils.exit type NodeJS_WriteStream = RobloxShared.NodeJS_WriteStream local JSON = nodeUtils.JSON +local ensureDirectoryExists = RobloxShared.ensureDirectoryExists +local getDataModelService = RobloxShared.getDataModelService +local FileSystemService = getDataModelService("FileSystemService") +local CoreScriptSyncService = getDataModelService("CoreScriptSyncService") -- ROBLOX deviation END local function getTestPaths( @@ -181,16 +186,13 @@ local function processResults(runResults: AggregatedResult, options: ProcessResu -- ROBLOX deviation END if isJSON then - -- ROBLOX deviation START: no output to file support - -- if Boolean.toJSBoolean(outputFile) then - -- local cwd = tryRealpath(process:cwd()) - -- local filePath = path:resolve(cwd, outputFile) - -- fs:writeFileSync(filePath, JSON.stringify(formatTestResults(runResults))) - -- outputStream:write(("Test results written to: %s\n"):format(tostring(path:relative(cwd, filePath)))) - -- else - process.stdout:write(JSON.stringify(formatTestResults(runResults))) - -- end - -- ROBLOX deviation END + if _outputFile and FileSystemService and Boolean.toJSBoolean(_outputFile) then + ensureDirectoryExists(_outputFile) + FileSystemService:WriteFile(_outputFile, JSON.stringify(formatTestResults(runResults))) + process.stdout:write(("Test results written to: %s\n"):format(tostring(_outputFile))) + else + process.stdout:write(JSON.stringify(formatTestResults(runResults))) + end end if onComplete ~= nil then @@ -289,6 +291,10 @@ local function runJest(ref: { if globalConfig.listTests then local testsPaths = Array.from(Set.new(Array.map(allTests, function(test) + -- ROBLOX deviation: resolve to a FS path if CoreScriptSyncService is available + if CoreScriptSyncService then + return CoreScriptSyncService:GetScriptFilePath(test.script) + end return test.path end))) --[[ eslint-disable no-console ]] diff --git a/src/jest-diff/README.md b/src/jest-diff/README.md index bceb844d..c595850e 100644 --- a/src/jest-diff/README.md +++ b/src/jest-diff/README.md @@ -1,16 +1,24 @@ # jest-diff -Status: :hammer: In Progress +Upstream: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-diff -Source: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-diff +Display differences clearly so people can review changes confidently. -Version: v27.4.7 +The `diff` named export serializes **values**, compares them line-by-line, and returns a string which includes comparison lines. + +Two named exports compare **strings** character-by-character: + +- `diffStringsUnified` returns a string. +- `diffStringsRaw` returns an array of `Diff` objects. + +Three named exports compare **arrays of strings** line-by-line: + +- `diffLinesUnified` and `diffLinesUnified2` return a string. +- `diffLinesRaw` returns an array of `Diff` objects. --- ### :pencil2: Notes -* :x: Color formatting isn't supported. -* :hammer: Currently doesn't support any features that require `prettyFormat` plugins (e.g. React elements). * `CleanupSemantic.lua` is adapted from the Lua version of [`diff-match-patch`](https://github.com/google/diff-match-patch/blob/master/lua/diff_match_patch.lua) to resemble the upstream [`cleanupSemantics.ts`](https://github.com/facebook/jest/blob/v27.4.7/packages/jest-diff/src/cleanupSemantic.ts) instead of being a direct port of it. * Tests for it are added, which are not included in the upstream `jest-diff * Changes to tests: @@ -18,16 +26,3 @@ Version: v27.4.7 * Color formatting specific tests are omitted. * `changeColor` is assigned to a function that imitates `chalk.inverse` so we can test `diffStringsUnified`. * `Array[]`, `Object{}` are changed to `Table{}`. - -### :x: Excluded -``` -src/types.ts -``` - -### :package: [Dependencies](https://github.com/facebook/jest/blob/v27.4.7/packages/jest-diff/package.json) -| Package | Version | Status | Notes | -| -------------- | ------- | ------------------------- | ------------------------------------------------ | -| chalk | 4.0.0 | :heavy_check_mark: Ported | [Lua-Chalk](https://github.com/Roblox/lua-chalk) | -| diff-sequences | 27.4.0 | :heavy_check_mark: Ported | | -| jest-get-type | 27.4.0 | :heavy_check_mark: Ported | | -| pretty-format | 27.4.6 | :heavy_check_mark: Ported | Mostly complete, need plugins | diff --git a/src/jest-each/README.md b/src/jest-each/README.md index b6e058df..c8f7a61e 100644 --- a/src/jest-each/README.md +++ b/src/jest-each/README.md @@ -1,10 +1,8 @@ # jest-each -Status: :heavy_check_mark: Ported +Upstream: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-each -Source: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-each - -Version: v27.4.7 +A parameterized testing library for Jest. You can find its documentation in the [Jest documentation](https://roblox.github.io/jest-roblox-internal). --- @@ -34,14 +32,3 @@ Version: v27.4.7 ) ``` * TestEZ methods are supported (`*FOCUS`, `*SKIP`), however, these may be dropped at some point in favor of jest's callable objects(`it`, `it.only`, `it.skip`), which are supported too - -### :x: Excluded - -``` - -``` - -### :package: [Dependencies](https://github.com/facebook/jest/blob/v27.4.7/packages/jest-each/package.json) - -| Package | Version | Status | Notes | -| ------- | ------- | ------ | ----- | diff --git a/src/jest-environment-roblox/README.md b/src/jest-environment-roblox/README.md index 3b4f7fb0..a4a68ba9 100644 --- a/src/jest-environment-roblox/README.md +++ b/src/jest-environment-roblox/README.md @@ -1,6 +1,6 @@ # jest-environment-roblox -Status: :hammer: In Progress +*No upstream. Roblox only.* --- diff --git a/src/jest-environment/README.md b/src/jest-environment/README.md index 172b2643..2c5514f6 100644 --- a/src/jest-environment/README.md +++ b/src/jest-environment/README.md @@ -1,22 +1,7 @@ # jest-environment -Status: :hammer: In Progress - -Source: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-environment - -Version: v27.4.7 +Upstream: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-environment --- ### :pencil2: Notes - -### :x: Excluded - -``` - -``` - -### :package: [Dependencies](https://github.com/facebook/jest/blob/v27.4.7/packages/jest-environment/package.json) - -| Package | Version | Status | Notes | -| ------- | ------- | ------ | ----- | diff --git a/src/jest-environment/src/init.lua b/src/jest-environment/src/init.lua index 4a68c540..425cbb23 100644 --- a/src/jest-environment/src/init.lua +++ b/src/jest-environment/src/init.lua @@ -39,11 +39,13 @@ type Global_Global = typesModule.Global_Global local jestMockModule = require("@pkg/@jsdotlua/jest-mock") local JestMockFn = jestMockModule.fn local JestMockMocked = jestMockModule.mocked --- ROBLOX TODO: spyOn is not implemented --- local JestMockSpyOn = jestMockModule.spyOn +local JestMockSpyOn = jestMockModule.spyOn type ModuleMocker = jestMockModule.ModuleMocker +-- ROBLOX deviation: mocking globals +local jestMockGenvModule = require("@pkg/@jdsotlua/jest-mock-genv") + export type EnvironmentContext = { console: Console, -- docblockPragmas: Record>, @@ -112,7 +114,8 @@ export type Jest = { * of the specified module, including all of the specified module's * dependencies. ]] - deepUnmock: (moduleName: string) -> Jest, + -- ROBLOX deviation: using ModuleScript instead of string + deepUnmock: (moduleName: ModuleScript) -> Jest, --[[* * Disables automatic mocking in the module loader. * @@ -125,13 +128,15 @@ export type Jest = { * the top of the code block. Use this method if you want to explicitly avoid * this behavior. ]] - doMock: (moduleName: string, moduleFactory: (() -> any)?) -> Jest, + -- ROBLOX deviation: using ModuleScript instead of string + doMock: (moduleName: ModuleScript, moduleFactory: (() -> any)?) -> Jest, --[[* * Indicates that the module system should never return a mocked version * of the specified module from require() (e.g. that it should always return * the real module). ]] - dontMock: (moduleName: string) -> Jest, + -- ROBLOX deviation: using ModuleScript instead of string + dontMock: (moduleName: ModuleScript) -> Jest, --[[* * Enables automatic mocking in the module loader. ]] @@ -140,6 +145,13 @@ export type Jest = { * Creates a mock function. Optionally takes a mock implementation. ]] fn: typeof(JestMockFn), + -- ROBLOX deviation START: mocking globals + --[[* + * Represents the global environment and its libraries, for use with the + * `spyOn()` function. This can be used to spy on Lua globals. + ]] + globalEnv: jestMockGenvModule.GlobalEnv, + -- ROBLOX deviation END --[[* * Given the name of a module, use the automatic mocking system to generate a * mocked version of the module for you. @@ -149,7 +161,8 @@ export type Jest = { * * @deprecated Use `jest.createMockFromModule()` instead ]] - genMockFromModule: (moduleName: string) -> any, + -- ROBLOX deviation: using ModuleScript instead of string + genMockFromModule: (moduleName: ModuleScript) -> any, --[[* * Given the name of a module, use the automatic mocking system to generate a * mocked version of the module for you. @@ -157,7 +170,8 @@ export type Jest = { * This is useful when you want to create a manual mock that extends the * automatic mock's behavior. ]] - createMockFromModule: (moduleName: string) -> any, + -- ROBLOX deviation: using ModuleScript instead of string + createMockFromModule: (moduleName: ModuleScript) -> any, --[[* * Determines if the given function is a mocked function. ]] @@ -165,13 +179,15 @@ export type Jest = { --[[* * Mocks a module with an auto-mocked version when it is being required. ]] - mock: (moduleName: string, moduleFactory: (() -> any)?, options: { virtual: boolean? }?) -> Jest, + -- ROBLOX deviation: using ModuleScript instead of string + mock: (moduleName: ModuleScript, moduleFactory: (() -> any)?, options: { virtual: boolean? }?) -> Jest, --[[* * Mocks a module with the provided module factory when it is being imported. ]] -- ROBLOX TODO: add default generic. unstable_mockModule: ( - moduleName: string, + -- ROBLOX deviation: using ModuleScript instead of string + moduleName: ModuleScript, moduleFactory: () -> Promise | T, options: { virtual: boolean? }? ) -> Jest, @@ -194,12 +210,14 @@ export type Jest = { getRandom(); // Always returns 10 ``` ]] - requireActual: (moduleName: string) -> any, + -- ROBLOX deviation: using ModuleScript instead of string + requireActual: (moduleName: ModuleScript) -> any, --[[* * Returns a mock module instead of the actual module, bypassing all checks * on whether the module should be required normally or not. ]] - requireMock: (moduleName: string) -> any, + -- ROBLOX deviation: using ModuleScript instead of string + requireMock: (moduleName: ModuleScript) -> any, --[[* * Resets the state of all mocks. * Equivalent to calling .mockReset() on every mocked function. @@ -265,7 +283,8 @@ export type Jest = { * API's second argument is a module factory instead of the expected * exported module object. ]] - setMock: (moduleName: string, moduleExports: any) -> Jest, + -- ROBLOX deviation: using ModuleScript instead of string + setMock: (moduleName: ModuleScript, moduleExports: any) -> Jest, --[[* * Set the default timeout interval for tests and before/after hooks in * milliseconds. @@ -281,14 +300,14 @@ export type Jest = { * Note: By default, jest.spyOn also calls the spied method. This is * different behavior from most other test libraries. ]] - -- ROBLOX TODO: spyOn is not implemented - -- spyOn: typeof(JestMockSpyOn), + spyOn: typeof(JestMockSpyOn), --[[* * Indicates that the module system should never return a mocked version of * the specified module from require() (e.g. that it should always return the * real module). ]] - unmock: (moduleName: string) -> Jest, + -- ROBLOX deviation: using ModuleScript instead of string + unmock: (moduleName: ModuleScript) -> Jest, --[[* * Instructs Jest to use fake versions of the standard timer functions. ]] diff --git a/src/jest-fake-timers/README.md b/src/jest-fake-timers/README.md index c1fdb69c..4301ca17 100644 --- a/src/jest-fake-timers/README.md +++ b/src/jest-fake-timers/README.md @@ -1,10 +1,21 @@ # jest-fake-timers -Status: :hammer: In Progress - -Source: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-fake-timers - -Version: v27.4.7 +Upstream: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-fake-timers + +This package contains the fake timers implementation for Jest. It can be activated by calling `jest.useFakeTimers()`. You can find its documentation in the [Jest documentation](https://roblox.github.io/jest-roblox-internal). + +The following timers are mocked: +* `delay` +* `tick` +* `time` +* `os` + * `os.time` + * `os.clock` +* `task.delay` + * `task.delay` + * `task.cancel` + * `task.wait` +* `DateTime` --- @@ -13,20 +24,3 @@ Version: v27.4.7 Similar to how Jest fake timers work by mocking the native timer functions (i.e. `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval`) and `Date`, Jest Lua fake timers work by mocking the native timer functions in Roblox (i.e. `delay`, `tick`), the Roblox `DateTime` and the Lua native timer methods `os.time` and `os.clock`. Additionally, Jest Lua fake timers support a configurable engine frame time. By default, the engine frame time is 0 (i.e. continuous time), but if set, Jest Lua fake timers will be processed by multiples of frame time. If engine frame time is set, then timers will be processed in the first frame after they are triggered. - -### :x: Excluded -``` -src/legacyFakeTimers.ts -__tests__/legacyFakeTimers.test.ts -__tests__/__snapshots__/legacyFakeTimers.test.ts.snap -``` - -### :package: [Dependencies](https://github.com/facebook/jest/blob/v27.4.7/packages/jest-fake-timers/package.json) -| Package | Version | Status | Notes | -| -------------------- | ------- | ------------------------- | ----- | -| @jest/types | 27.4.2 | :heavy_check_mark: Ported | | -| @sinonjs/fake-timers | 8.0.1 | :x: Not needed | | -| @types/node | * | :x: Not needed | | -| jest-message-util | 27.4.6 | :x: Not needed | | -| jest-mock | 27.4.6 | :x: Not needed | | -| jest-util | 27.4.2 | :hammer: In Progress | | diff --git a/src/jest-fake-timers/src/__tests__/roblox.spec.lua b/src/jest-fake-timers/src/__tests__/roblox.spec.lua index 8b172327..fd103cab 100644 --- a/src/jest-fake-timers/src/__tests__/roblox.spec.lua +++ b/src/jest-fake-timers/src/__tests__/roblox.spec.lua @@ -867,18 +867,23 @@ describe("task", function() it("resets native timer APIs", function() local nativeTaskDelay = timers.taskOverride.delay.getMockImplementation() local nativeTaskCancel = timers.taskOverride.cancel.getMockImplementation() + local nativeTaskWait = timers.taskOverride.wait.getMockImplementation() timers:useFakeTimers() local fakeTaskDelay = timers.taskOverride.delay.getMockImplementation() local fakeTaskCancel = timers.taskOverride.cancel.getMockImplementation() + local fakeTaskWait = timers.taskOverride.wait.getMockImplementation() expect(fakeTaskDelay).never.toBe(nativeTaskDelay) expect(fakeTaskCancel).never.toBe(nativeTaskCancel) + expect(fakeTaskWait).never.toBe(nativeTaskWait) timers:useRealTimers() local realTaskDelay = timers.taskOverride.delay.getMockImplementation() local realTaskCancel = timers.taskOverride.cancel.getMockImplementation() + local realTaskWait = timers.taskOverride.wait.getMockImplementation() expect(realTaskDelay).toBe(nativeTaskDelay) expect(realTaskCancel).toBe(nativeTaskCancel) + expect(realTaskWait).toBe(nativeTaskWait) end) end) end) diff --git a/src/jest-fake-timers/src/__tests__/timers.roblox.spec.lua b/src/jest-fake-timers/src/__tests__/timers.roblox.spec.lua index d614b21d..0c3544f1 100644 --- a/src/jest-fake-timers/src/__tests__/timers.roblox.spec.lua +++ b/src/jest-fake-timers/src/__tests__/timers.roblox.spec.lua @@ -12,15 +12,16 @@ local test = JestGlobals.test local jest = JestGlobals.jest local FRAME_TIME = 15 -describe("timers", function() +describe("setTimeout", function() beforeEach(function() jest.useFakeTimers() end) + afterEach(function() jest.useRealTimers() end) - test("setTimeout - should not trigger", function() + test("should not trigger", function() local triggered = false setTimeout(function() triggered = true @@ -29,7 +30,7 @@ describe("timers", function() expect(triggered).toBe(false) end) - test("setTimeout - should trigger", function() + test("should trigger", function() local triggered = false setTimeout(function() triggered = true @@ -38,7 +39,28 @@ describe("timers", function() expect(triggered).toBe(true) end) - test("setInterval - should not trigger", function() + test("should trigger with a configurable frame time", function() + jest.useFakeTimers() + jest.setEngineFrameTime(FRAME_TIME) + local triggered = false + setTimeout(function() + triggered = true + end, 10) + jest.advanceTimersByTime(0) + expect(triggered).toBe(true) + end) +end) + +describe("setInterval", function() + beforeEach(function() + jest.useFakeTimers() + end) + + afterEach(function() + jest.useRealTimers() + end) + + test("should not trigger", function() local triggered = 0 setInterval(function() triggered += 1 @@ -47,7 +69,7 @@ describe("timers", function() expect(triggered).toBe(0) end) - test("setInterval - should trigger once", function() + test("should trigger once", function() local triggered = 0 setInterval(function() triggered += 1 @@ -56,7 +78,7 @@ describe("timers", function() expect(triggered).toBe(1) end) - test("setInterval - should trigger multiple times", function() + test("should trigger multiple times", function() local triggered = 0 setInterval(function() triggered += 1 @@ -82,8 +104,18 @@ describe("timers", function() jest.advanceTimersByTime(1) expect(triggered).toBe(3) end) +end) - test("task.delay - should trigger", function() +describe("task.delay", function() + beforeEach(function() + jest.useFakeTimers() + end) + + afterEach(function() + jest.useRealTimers() + end) + + test("should trigger", function() local triggered = false task.delay(2, function() triggered = true @@ -92,7 +124,7 @@ describe("timers", function() expect(triggered).toBe(true) end) - test("task.delay - should not trigger", function() + test("should not trigger", function() local triggered = false task.delay(2, function() triggered = true @@ -109,8 +141,18 @@ describe("timers", function() jest.advanceTimersByTime(10000) expect(triggered).toBe(true) end, 1000) +end) - test("task.cancel - timeout should be canceled and not trigger", function() +describe("task.cancel", function() + beforeEach(function() + jest.useFakeTimers() + end) + + afterEach(function() + jest.useRealTimers() + end) + + test("timeout should be canceled and not trigger", function() local triggered = false local timeout = task.delay(2, function() triggered = true @@ -120,7 +162,7 @@ describe("timers", function() expect(triggered).toBe(false) end) - test("task.cancel - one timeout should be canceled and not trigger", function() + test("one timeout should be canceled and not trigger", function() local triggered1 = false local triggered2 = false local timeout1 = task.delay(2, function() @@ -136,7 +178,7 @@ describe("timers", function() expect(triggered2).toBe(true) end) - test("task.cancel - cancel after delayed task runs", function() + test("cancel after delayed task runs", function() local triggered = false local timeout = task.delay(2, function() triggered = true @@ -147,15 +189,53 @@ describe("timers", function() end) end) -describe("timers with configurable frame time", function() - test("setTimeout - should trigger", function() +describe("task.wait", function() + beforeEach(function() jest.useFakeTimers() - jest.setEngineFrameTime(FRAME_TIME) - local triggered = false - setTimeout(function() - triggered = true - end, 10) - jest.advanceTimersByTime(0) - expect(triggered).toBe(true) + end) + + afterEach(function() + jest.useRealTimers() + end) + + test("should wait for the specified time", function() + local elapsed = 0 + coroutine.wrap(function() + elapsed = task.wait(2) + end)() + jest.advanceTimersByTime(2000) + expect(elapsed).toBe(2) + end) + + test("should not proceed before the specified time", function() + local elapsed = 0 + coroutine.wrap(function() + elapsed = task.wait(2) + end)() + jest.advanceTimersByTime(1999) + expect(elapsed).toBe(0) + end) + + test("should default to frame time if no time is specified", function() + local elapsed = 0 + coroutine.wrap(function() + elapsed = task.wait() + end)() + jest.advanceTimersByTime(1000 / 60) + expect(elapsed).toBeCloseTo(1 / 60, 0.001) + end) + + test("multiple waits should accumulate correctly", function() + local elapsed1, elapsed2 = 0, 0 + coroutine.wrap(function() + elapsed1 = task.wait(1) + elapsed2 = task.wait(2) + end)() + jest.advanceTimersByTime(1000) + expect(elapsed1).toBe(1) + expect(elapsed2).toBe(0) + jest.advanceTimersByTime(2000) + expect(elapsed1).toBe(1) + expect(elapsed2).toBe(2) end) end) diff --git a/src/jest-fake-timers/src/init.lua b/src/jest-fake-timers/src/init.lua index 49a1a618..6a175bf1 100644 --- a/src/jest-fake-timers/src/init.lua +++ b/src/jest-fake-timers/src/init.lua @@ -84,6 +84,7 @@ function FakeTimers.new(): FakeTimers local taskOverride = { delay = mock:fn(realTask.delay), cancel = mock:fn(realTask.cancel), + wait = mock:fn(realTask.wait), } setmetatable(taskOverride, { __index = realTask }) @@ -220,10 +221,15 @@ function FakeTimers:useRealTimers(): () self.osOverride.clock.mockImplementation(realOs.clock) self.taskOverride.delay.mockImplementation(realTask.delay) self.taskOverride.cancel.mockImplementation(realTask.cancel) + self.taskOverride.wait.mockImplementation(realTask.wait) self._fakingTime = false end end +local function fakeClock(self): number + return self._mockTimeMs / 1000 +end + local function fakeDelay(self, delayTime, callback, ...): Timeout -- Small hack to make sure 0 second recursive timers don't trigger twice in a single frame local delayTimeMs = (self._engineFrameTime / 1000) + delayTime * 1000 @@ -257,20 +263,33 @@ local function fakeCancel(self, timeout) end end +local function fakeWait(self, timeToWait: number?) + local running = coroutine.running() + local clock = fakeClock(self) + fakeDelay(self, timeToWait or 0, function() + task.spawn(running, fakeClock(self) - clock) + end) + return coroutine.yield() +end + function FakeTimers:useFakeTimers(): () if not self._fakingTime then self.delayOverride.mockImplementation(function(delayTime, callback) return fakeDelay(self, delayTime, callback) end) + self.tickOverride.mockImplementation(function() return self._mockSystemTime end) + self.timeOverride.mockImplementation(function() - return self._mockTimeMs / 1000 + return fakeClock(self) end) + self.dateTimeOverride.now.mockImplementation(function() return realDateTime.fromUnixTimestamp(self._mockSystemTime) end) + self.osOverride.time.mockImplementation(function(time_) if typeof(time_) == "table" then local unixTime = realDateTime.fromUniversalTime( @@ -285,9 +304,11 @@ function FakeTimers:useFakeTimers(): () end return self._mockSystemTime end) + self.osOverride.clock.mockImplementation(function() - return self._mockTimeMs / 1000 + return fakeClock(self) end) + self.taskOverride.delay.mockImplementation(function(delayTime, callback, ...) return fakeDelay(self, delayTime, callback, ...) end) @@ -296,6 +317,10 @@ function FakeTimers:useFakeTimers(): () fakeCancel(self, timeout) end) + self.taskOverride.wait.mockImplementation(function(timeToWait) + return fakeWait(self, timeToWait) + end) + self._fakingTime = true self:reset() end diff --git a/src/jest-get-type/README.md b/src/jest-get-type/README.md index 48570e4d..249a30fa 100644 --- a/src/jest-get-type/README.md +++ b/src/jest-get-type/README.md @@ -1,10 +1,14 @@ # jest-get-type -Status: :heavy_check_mark: Ported +Upstream: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-get-type -Source: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-get-type +A utility function to get the type of a value, including Luau and Roblox types. -Version: v27.4.7 +Types supported: + +* Lua Primitives - `nil`, `table`, `number`, `string`, `function`, `boolean`, `userdata`, `thread` +* [Luau Polyfill](https://github.com/Roblox/luau-polyfill) types - `symbol`, [`regexp`](https://github.com/Roblox/luau-regexp), `error`, `set` +* Roblox datatypes - `DateTime`, and other [`builtin`](https://developer.roblox.com/en-us/api-reference/data-types) types --- @@ -14,17 +18,3 @@ Version: v27.4.7 * Lua lacks the following primitives: `bigint`, `symbol`. * Lua lacks the following built-in types: `RegExp`, `Map`, `Set`, `Date`. * `JestGetType` deviates and exposes an `isRobloxBuiltin` method to check whether a value is a Roblox builtin type - -Types supported: - -* Lua Primitives - `nil`, `table`, `number`, `string`, `function`, `boolean`, `userdata`, `thread` -* [Luau Polyfill](https://github.com/Roblox/luau-polyfill) types - `symbol`, [`regexp`](https://github.com/Roblox/luau-regexp), `error`, `set` -* Roblox datatypes - `DateTime`, and other [`builtin`](https://developer.roblox.com/en-us/api-reference/data-types) types - -### :x: Excluded -``` -``` - -### :package: [Dependencies](https://github.com/facebook/jest/blob/v27.4.7/packages/jest-get-type/package.json) -| Package | Version | Status | Notes | -| ------- | ------- | ------ | ----- | diff --git a/src/jest-globals/README.md b/src/jest-globals/README.md index 4fd37580..422e0380 100644 --- a/src/jest-globals/README.md +++ b/src/jest-globals/README.md @@ -1,19 +1,9 @@ # jest-globals -Status: :hammer: In Progress +Upstream: https://github.com/jestjs/jest/tree/v27.4.7/packages/jest-globals -Source: - -Version: +This package exports all the "global" methods used by Jest. You can find its documentation in the [Jest documentation](https://roblox.github.io/jest-roblox-internal). --- ### :pencil2: Notes - -### :x: Excluded -``` -``` - -### :package: [Dependencies]() -| Package | Version | Status | Notes | -| ------- | ------- | ------ | ----- | diff --git a/src/jest-globals/src/__tests__/index.lua b/src/jest-globals/src/__tests__/index.lua index e20a65c7..76caeb25 100644 --- a/src/jest-globals/src/__tests__/index.lua +++ b/src/jest-globals/src/__tests__/index.lua @@ -26,7 +26,8 @@ return (function() require("../index") end).toThrowError( -- ROBLOX deviation START: aligned message to make sense for jest-roblox - "Do not import `JestGlobals` outside of the Jest test environment" + "Do not import `JestGlobals` outside of the Jest 3 test environment.\n" + .. "Tip: Jest 2 uses a different pattern - check your Jest version." -- ROBLOX deviation END ) end) diff --git a/src/jest-globals/src/index.lua b/src/jest-globals/src/index.lua index 8593ad2f..efebbe99 100644 --- a/src/jest-globals/src/index.lua +++ b/src/jest-globals/src/index.lua @@ -35,7 +35,8 @@ type JestGlobals = error(Error.new( -- ROBLOX deviation START: aligned message to make sense for jest-roblox - "Do not import `JestGlobals` outside of the Jest test environment" + "Do not import `JestGlobals` outside of the Jest 3 test environment.\n" + .. "Tip: Jest 2 uses a different pattern - check your Jest version." -- ROBLOX deviation END )) diff --git a/src/jest-jasmine2/README.md b/src/jest-jasmine2/README.md index 6b58ae69..85092a9a 100644 --- a/src/jest-jasmine2/README.md +++ b/src/jest-jasmine2/README.md @@ -1,10 +1,6 @@ # jest-jasmine2 -Status: :hammer: In Progress - -Source: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-jasmine2/src/jasmine - -Version: v27.4.7 +Upstream: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-jasmine2 --- @@ -13,12 +9,3 @@ Version: v27.4.7 * The tests for CallTracker and SpyStrategy are copied off of the upstream files but the `createSpy.ts` file doesn't actually have a direct upstream equivalent in Jasmine so we copy some tests from `SpySpec` instead, leaving out the majority of tests that aren't yet relevant * We expose `andAlso` in addition to the typical `and` for createSpy since `and` is a built in keyword for Lua so we can't cleanly chain fields (i.e. we can't get `var.and` to work so we allow for `var.andAlso`) * We use the [Roblox Lua Promise](https://github.com/evaera/roblox-lua-promise) library in this module in a number of places to more closely mirror the asynchronous tests in Jasmine - - -### :x: Excluded -``` -``` - -### :package: [Dependencies](https://github.com/facebook/jest/blob/v27.4.7/packages/jest-jasmine2/package.json) -| Package | Version | Status | Notes | -| ------- | ------- | ------ | ----- | diff --git a/src/jest-matcher-utils/README.md b/src/jest-matcher-utils/README.md index 88c22e4c..5e63a809 100644 --- a/src/jest-matcher-utils/README.md +++ b/src/jest-matcher-utils/README.md @@ -1,10 +1,8 @@ # jest-matcher-utils -Status: :heavy_check_mark: Ported +Upstream: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-matcher-utils -Source: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-matcher-utils - -Version: v27.4.7 +This package's exports are mainly used by `expect`'s `utils`. --- @@ -14,15 +12,3 @@ Version: v27.4.7 * Changed type annotations in upstream that were `unknown` to `any` because Luau doesn't have support for an `unknown` type * In many of the tests, there is differentiation between types such as `Array` and `Map`, however in Lua all of these are treated identically as `table`. The tests were translated to use the `table` type and some tests were left out if they became identical to other tests or were highly redundant. * Luau does not yet have functionality to use generics in function signatures and functions so those type annotations are left out - -### :x: Excluded -``` -``` - -### :package: [Dependencies](https://github.com/facebook/jest/blob/v27.4.7/packages/jest-matcher-utils/package.json) -| Package | Version | Status | Notes | -| ------------- | ------- | ------------------------- | ------------------------------------------------ | -| chalk | 4.0.0 | :heavy_check_mark: Ported | [Lua-Chalk](https://github.com/Roblox/lua-chalk) | -| jest-diff | 27.4.6 | :heavy_check_mark: Ported | | -| jest-get-type | 27.4.0 | :heavy_check_mark: Ported | | -| pretty-format | 27.4.6 | :heavy_check_mark: Ported | Mostly complete, need plugins | diff --git a/src/jest-matcher-utils/src/__tests__/index.spec.lua b/src/jest-matcher-utils/src/__tests__/index.spec.lua index 4a3bb201..4f5d17f3 100644 --- a/src/jest-matcher-utils/src/__tests__/index.spec.lua +++ b/src/jest-matcher-utils/src/__tests__/index.spec.lua @@ -439,7 +439,8 @@ end) describe("printDiffOrStringify", function() test("expected asymmetric matchers should be diffable", function() - jest.dontMock("jest-diff") + -- ROBLOX deviation: pass in ModuleScript instead of string + jest.dontMock(script.Parent.Parent.Parent.JestDiff) jest.resetModules() -- ROBLOX deviation START: fix incorrect 'require' call diff --git a/src/jest-message-util/README.md b/src/jest-message-util/README.md index 0bd68f2e..90449453 100644 --- a/src/jest-message-util/README.md +++ b/src/jest-message-util/README.md @@ -1,28 +1,7 @@ # jest-message-util -Status: :hammer: In Progress - -Source: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-message-util - -Version: v27.4.7 +Upstream: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-message-util --- ### :pencil2: Notes - -### :x: Excluded -``` -``` - -### :package: [Dependencies](https://github.com/facebook/jest/blob/v27.4.7/packages/jest-message-util/package.json) -| Package | Version | Status | Notes | -| ------------------ | ------- | ------------------------- | ------------------------------------------------ | -| @babel/code-frame | 7.0.0 | :x: Will not port | Babel is not needed | -| @jest/types | 27.4.2 | :heavy_check_mark: Ported | External typedefs not a priority | -| @types/stack-utils | 2.0.0 | :x: Will not port | External typedefs not a priority | -| chalk | 4.0.0 | :heavy_check_mark: Ported | [Lua-Chalk](https://github.com/Roblox/lua-chalk) | -| graceful-fs | 4.2.4 | :x: Will not port | No need to interact with the filesystem | -| micromatch | 4.0.4 | :x: Will not port | Deals with file paths | -| pretty-format | 27.4.6 | :heavy_check_mark: Ported | | -| slash | 3.0.0 | :x: Will not port | Deals with file paths | -| stack-utils | 2.0.3 | | | diff --git a/src/jest-message-util/src/init.lua b/src/jest-message-util/src/init.lua index c32a81d0..c1f7b5b0 100644 --- a/src/jest-message-util/src/init.lua +++ b/src/jest-message-util/src/init.lua @@ -27,7 +27,9 @@ local prettyFormat = require("@pkg/@jsdotlua/pretty-format").format type Path = Config_Path -- ROBLOX deviation START: additional dependencies -local normalizePromiseError = require("@pkg/@jsdotlua/jest-roblox-shared").normalizePromiseError +local RobloxShared = require("@pkg/@jsdotlua/jest-roblox-shared") +local normalizePromiseError = RobloxShared.normalizePromiseError +local cleanLoadStringStack = RobloxShared.cleanLoadStringStack -- ROBLOX deviation END -- ROBLOX deviation: forward declarations @@ -272,8 +274,8 @@ end -- ROBLOX deviation: config does not have StackTraceConfig type annotation local function formatPaths(config, relativeTestPath, line: string): string - -- ROBLOX deviation: we don't do any formatting of paths in Lua to align with upstream - return line + -- ROBLOX deviation: if loadstring is used, format the loadstring stacktrace to look like a path + return cleanLoadStringStack(line) end function getStackTraceLines(stack: string, options: StackTraceOptions): { string } diff --git a/src/jest-mock-genv/.npmignore b/src/jest-mock-genv/.npmignore new file mode 100644 index 00000000..afe7a485 --- /dev/null +++ b/src/jest-mock-genv/.npmignore @@ -0,0 +1,30 @@ +/.* +/scripts +/docs +/site + +/build +/roblox +/temp + +/*.json +/*.json5 +/*.yml +/*.toml +/*.md +/*.txt +/*.tgz + +**/*.d.lua +**/*.spec.lua +**/*.test.lua +**/tests +**/__tests__ +**/jest.config.lua + +**/*.rbxl +**/*.rbxlx +**/*.rbxl.lock +**/*.rbxlx.lock +**/*.rbxm +**/*.rbxmx diff --git a/src/jest-mock-genv/README.md b/src/jest-mock-genv/README.md new file mode 100644 index 00000000..6a427942 --- /dev/null +++ b/src/jest-mock-genv/README.md @@ -0,0 +1,37 @@ +# jest-mock-genv + +*No upstream. Roblox only.* + +This module houses the `GlobalMocker` class, the type definitions used for global +mocking utilities, and the `MOCKABLE_GLOBALS` constant which determines the global +environment members that are allowed to be mocked. + +## :pencil2: Notes + +- **Changing `MOCKABLE_GLOBALS` should be done with care.** + - By whitelisting a new global to be mocked, you may subtly affect any code + which uses that global, or allow users to do the same. + - Jest only generates mock functions for the globals that are + whitelisted, and doesn't generate anything for globals that are not + whitelisted. This can subtly change how a global appears to user code. + - Possible breakage will need to be investigated. + - Adding a new global to this list is not breaking, but removing a global + from this list *is* breaking. + - It is better to be selective than to be generous, because if the + whitelisting causes breakage, it's might be hard to undo. + - We also don't want to encourage bad practice, and mocking certain + globals could lead to unintended use cases which aren't idiomatic or + cause problems for ourselves later. + - Certain globals are not safe to mock right now, including task scheduling + functions and `require()`, because they already have customised + implementations in Jest that would be bypassed. + - This can probably be fixed down the line if there's a pressing need to + do it, but it would introduce more complexity. + - **Above all else, come talk to us first - we will help you 🙂** +- The `GlobalMocker` class does not implement any mocking capabilities itself; + instead, mock functions are stored in `GlobalMocker` by a `ModuleMocker`. +- Globals should *always* be mocked whenever a test is running, because the + test's sandbox environment redirects to these mock functions at all times - + even if the user has not used `spyOn`. + - This ensures that all modules can see mocked implementations, even if they + are required later than the call to `spyOn` which mocks the global. diff --git a/src/jest-mock-genv/package.json b/src/jest-mock-genv/package.json new file mode 100644 index 00000000..54f0fcf6 --- /dev/null +++ b/src/jest-mock-genv/package.json @@ -0,0 +1,21 @@ +{ + "name": "@jsdotlua/jest-mock-genv", + "version": "3.6.1-rc.2", + "repository": { + "type": "git", + "url": "https://github.com/jsdotlua/jest-lua.git", + "directory": "src/jest-mock" + }, + "license": "MIT", + "main": "src/init.lua", + "scripts": { + "prepare": "npmluau" + }, + "dependencies": { + "@jsdotlua/luau-polyfill": "^1.2.6" + }, + "devDependencies": { + "@jsdotlua/jest-globals": "workspace:^", + "npmluau": "^0.1.1" + } +} \ No newline at end of file diff --git a/src/jest-mock-genv/src/__tests__/index.spec.lua b/src/jest-mock-genv/src/__tests__/index.spec.lua new file mode 100644 index 00000000..bd08b115 --- /dev/null +++ b/src/jest-mock-genv/src/__tests__/index.spec.lua @@ -0,0 +1,104 @@ +--[[ + * Copyright (c) Roblox Corporation. All rights reserved. + * Licensed under the MIT License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +]] +--!strict +-- ROBLOX NOTE: no upstream + +local LuauPolyfill = require("@pkg/@jsdotlua/luau-polyfill") +type Object = LuauPolyfill.Object + +local exports = require("..") +local JestGlobals = require("@pkg/@jsdotlua/jest-globals") +local jest = JestGlobals.jest +local expect = JestGlobals.expect +local it = JestGlobals.it +local describe = JestGlobals.describe +local beforeEach = JestGlobals.beforeEach + +local globalMocker +beforeEach(function() + globalMocker = exports.GlobalMocker.new() +end) + +it("MOCKABLE_GLOBALS has correct structure", function() + expect(exports.MOCKABLE_GLOBALS).toEqual(expect.any("table")) + local function checkStructureOf(partOfTable: Object) + for name, value in partOfTable do + expect(name).toEqual(expect.any("string")) + if typeof(value) == "table" then + -- Empty table here allow users to index this library in + -- `globalEnv`, but don't let them do anything else. To ensure + -- errors are raised from use of *libraries* with no mocks, + -- rather than attempts to mock specific *functions*, disallow + -- empty tables from being present in this structure. + expect((next(value))).never.toBeNil() + checkStructureOf(value) + else + expect(value).toEqual(expect.any("function")) + end + end + end + checkStructureOf(exports.MOCKABLE_GLOBALS) +end) + +it("globalEnv is provided in the environment", function() + expect(jest.globalEnv).toEqual(expect.any("table")) + expect(globalMocker:isMockGlobalLibrary(jest.globalEnv)).toBe(true) +end) + +-- To show a full list of which MOCKABLE_GLOBALS aren't yet covered by +-- globalEnv, this test is structured as a set of dynamically generated it() +-- blocks. Each it() block is responsible for checking the existence of one +-- of the globalEnv members. The effect of this is to make clear in the unit +-- test output when certain mocks aren't present, labelled clearly with +-- their qualified name and expected type, even if multiple things are +-- missing at once. +describe("globalEnv implements all MOCKABLE_GLOBALS", function() + local function test(mockableGlobals: Object, globalPath: { string }) + for name, mockableGlobal in mockableGlobals do + local nestedPath = table.clone(globalPath) + table.insert(nestedPath, name) + local qualifiedName = table.concat(nestedPath, ".") + + if typeof(mockableGlobal) == "function" then + it(`unmocked function: {qualifiedName}`, function() + local target = jest.globalEnv :: Object + for _, pathPart in nestedPath do + target = target[pathPart] + end + expect(target).toEqual(expect.any("function")) + end) + elseif typeof(mockableGlobal) == "table" then + it(`mockable library: {qualifiedName}`, function() + local target = jest.globalEnv :: Object + for _, pathPart in nestedPath do + target = target[pathPart] + end + expect(globalMocker:isMockGlobalLibrary(target)).toBe(true) + end) + test(mockableGlobal, nestedPath) + end + end + end + test(exports.MOCKABLE_GLOBALS, {}) +end) + +it("globalEnv errors when indexing non-mocked globals", function() + expect(function() + local _ = (jest.globalEnv :: any).notreal + end).toThrow("Jest does not yet support mocking the notreal global") + expect(function() + local _ = (jest.globalEnv :: any).math.notreal + end).toThrow("Jest does not yet support mocking the math.notreal global") +end) diff --git a/src/jest-mock-genv/src/init.lua b/src/jest-mock-genv/src/init.lua new file mode 100644 index 00000000..fc041846 --- /dev/null +++ b/src/jest-mock-genv/src/init.lua @@ -0,0 +1,162 @@ +--!nonstrict +-- ROBLOX NOTE: no upstream +--[[ + * Copyright (c) Roblox Corporation. All rights reserved. + * Licensed under the MIT License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +]] + +local LuauPolyfill = require("@pkg/@jsdotlua/luau-polyfill") +type Object = LuauPolyfill.Object + +local exports = {} + +local GlobalMockerClass = {} + +export type GlobalAutomockFn = { + _isGlobalAutomockFn: true, + _maybeMock: any, + _maybeUnmocked: any, +} +export type GlobalAutomocks = { [string]: GlobalAutomockFn | GlobalAutomocks } +export type GlobalEnvLibrary = { + _isMockGlobalLibrary: true, + _automocksRef: GlobalAutomocks, +} +-- The GlobalEnv type should always look like the MOCKABLE_GLOBALS table; +-- users depend on GlobalEnv for autocomplete and type checking. +export type GlobalEnv = GlobalEnvLibrary & { + print: typeof(print), + warn: typeof(warn), + math: GlobalEnvLibrary & { + random: typeof(math.random), + }, +} +local MOCKABLE_GLOBALS = { + print = print, + warn = warn, + math = { + random = math.random, + }, +} + +export type GlobalMocker = { + isMockGlobalLibrary: (_self: GlobalMocker, object: any) -> boolean, + automocks: GlobalAutomocks, + envObject: GlobalEnv, + currentlyMocked: boolean, +} + +GlobalMockerClass.__index = GlobalMockerClass +function GlobalMockerClass.new(): GlobalMocker + local self = setmetatable({}, GlobalMockerClass) + + self.automocks = self:_createGlobalAutomocks() + self.envObject = self:_createGlobalEnv(self.automocks) + self.currentlyMocked = false + + return (self :: any) :: GlobalMocker +end + +function GlobalMockerClass:isMockGlobalLibrary(object: any): boolean + return typeof(object) == "table" and object._isMockGlobalLibrary == true +end + +function GlobalMockerClass:_createGlobalAutomocks(): GlobalAutomocks + local function implement(mockableGlobals: Object, into: GlobalAutomocks) + for name, mockableGlobal in mockableGlobals do + if typeof(mockableGlobal) == "function" then + into[name] = { + _isGlobalAutomockFn = true, + _maybeMock = nil, + _maybeUnmocked = nil, + } + elseif typeof(mockableGlobal) == "table" then + local subAutomocks = {} + implement(mockableGlobal, subAutomocks) + into[name] = subAutomocks + else + error("Unexpected mockable global type - this is an internal bug") + end + end + end + local automocks = {} + implement(MOCKABLE_GLOBALS, automocks) + return automocks +end + +function GlobalMockerClass:_createGlobalEnv(automocks: GlobalAutomocks): GlobalEnv + local function makeSentinelForLibrary(automocks: GlobalAutomocks, globalPath: { string }) + local library: GlobalEnvLibrary = { + _isMockGlobalLibrary = true, + _automocksRef = automocks, + } + + -- Allow users to access nested libraries like `math`. + for name, automock in automocks do + if typeof(automock) == "table" and not automock._isGlobalAutomockFn then + local libraryGlobalPath = table.clone(globalPath) + table.insert(libraryGlobalPath, name) + library[name] = makeSentinelForLibrary(automock, libraryGlobalPath) + end + end + + -- Users might want to mock functions that don't have an underlying + -- implementation in Jest. Detect that and throw an error here to inform + -- them Jest must explicitly support individual globals to be mocked. + setmetatable(library, { + __index = function(_, name: string) + -- name is actually `unknown` type; the type declaration is a + -- convenient lie so our code type checks without a fuss + if typeof(name) ~= "string" then + error(`Cannot index globalEnv with {name} (expected string)`) + end + + -- Give $$ names like $$typeof a free pass, because they're used + -- internally in some Jest/LuauPolyfill functions, and probably + -- aren't a user accidentally misusing `globalEnv`. + if string.sub(name, 1, 2) == "$$" then + return nil + end + + -- Unmocked functions aren't included in the actual object, so + -- simulate them being included here (where we can dynamically + -- fetch which function to actually return) + local automock = automocks[name] + if typeof(automock) == "table" and automock._isGlobalAutomockFn then + return automock._maybeUnmocked or error("globalEnv has not been initialised by Jest here") + end + + -- In theory, what we want to do is `table.concat` the global + -- path, but including `name` at the end. Instead of doing table + -- manipulation, just implement that with a plain loop. + local qualifiedName = "" + for _, parentName in globalPath do + qualifiedName ..= parentName .. "." + end + qualifiedName ..= name + error(`Jest does not yet support mocking the {qualifiedName} global.`) + end, + }) + + return table.freeze(library) + end + + -- This will match `GlobalEnv` in the end, but it's difficult to + -- statically type check that, so for code cleanliness, just cast to it. + return makeSentinelForLibrary(automocks, {}) :: any +end + +exports.GlobalMocker = GlobalMockerClass +exports.MOCKABLE_GLOBALS = MOCKABLE_GLOBALS + +return exports diff --git a/src/jest-mock/README.md b/src/jest-mock/README.md index 277bb013..09362d47 100644 --- a/src/jest-mock/README.md +++ b/src/jest-mock/README.md @@ -1,10 +1,8 @@ # jest-mock -Status: :hammer: In Progress +Upstream: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-mock -Source: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-mock - -Version: v27.4.7 +This package implements the various function and module mocking capabilities used by Jest. You can find its documentation in the [Jest documentation](https://roblox.github.io/jest-roblox-internal). --- @@ -16,13 +14,3 @@ Version: v27.4.7 * `clearAllMocks()` * `resetAllMocks()` * `restoreAllMocks()` - -### :x: Excluded -``` -``` - -### :package: [Dependencies](https://github.com/facebook/jest/blob/v27.4.7/packages/jest-mock/package.json) -| Package | Version | Status | Notes | -| ------------- | ------- | ------------------------- | ----- | -| `@jest/types` | 27.0.6 | :heavy_check_mark: Ported | | -| `@types/node` | * | :x: Will not port | | diff --git a/src/jest-mock/src/__tests__/index.spec.lua b/src/jest-mock/src/__tests__/index.spec.lua index 4be75edc..7b25b64a 100644 --- a/src/jest-mock/src/__tests__/index.spec.lua +++ b/src/jest-mock/src/__tests__/index.spec.lua @@ -7,20 +7,39 @@ -- * -- */ -local ModuleMocker = require("../init").ModuleMocker local JestGlobals = require("@pkg/@jsdotlua/jest-globals") local expect = JestGlobals.expect local describe = JestGlobals.describe local it = JestGlobals.it local beforeEach = JestGlobals.beforeEach +local jest = JestGlobals.jest -local moduleMocker -beforeEach(function() - moduleMocker = ModuleMocker.new() -end) +local LuauPolyfill = require("@pkg/@jsdotlua/luau-polyfill") +local Error = LuauPolyfill.Error + +local parentModule = require("../init") +local ModuleMocker = parentModule.ModuleMocker +local fn = parentModule.fn +local mocked = parentModule.mocked +local spyOn = parentModule.spyOn describe("moduleMocker", function() + local moduleMocker + beforeEach(function() + moduleMocker = ModuleMocker.new() + end) + + --[[ + ROBLOX deviation: skipped code: + original code lines 25 - 119 + ]] + describe("generateFromMetadata", function() + --[[ + ROBLOX deviation: skipped code: + original code lines 122 - 410 + ]] + describe("mocked functions", function() it("tracks calls to mocks", function() local fn = moduleMocker:fn() @@ -97,6 +116,12 @@ describe("moduleMocker", function() fn.mockClear() expect(fn.mock.calls).toEqual({}) + + fn("a", "b", "c") + + expect(fn.mock.calls).toEqual({ { "a", "b", "c" } }) + + expect(fn()).toEqual("abcd") end) it("supports clearing mocks", function() @@ -204,7 +229,7 @@ describe("moduleMocker", function() end) -- ROBLOX deviation: test is itSKIPped because we currently don't - -- implement this ability to inspect functionArity + -- preserve function arity for mocked functions it.skip("maintains function arity", function() local mockFunctionArity1 = moduleMocker:fn(function(x) return x @@ -216,82 +241,576 @@ describe("moduleMocker", function() expect(#mockFunctionArity1).toBe(1) expect(#mockFunctionArity2).toBe(2) end) + end) - -- ROBLOX deviation: tests commented out for now, not yet implemented spyOn - -- it('mocks the method in the passed object itself', function() - -- local parent = {func = function() return 'abcd' end} - -- local child = Object.create(parent) + it("mocks the method in the passed object itself", function() + local parent = { + func = function() + return "abcd" + end, + } + -- ROBLOX deviation: use metatables for prototype-like inheritance + local child = setmetatable({}, { __index = parent }) - -- moduleMocker.spyOn(child, 'func').mockReturnValue('efgh') + moduleMocker:spyOn(child, "func").mockReturnValue("efgh") - -- expect(child['func']).never.toBe(nil) - -- expect(child.func()).toEqual('efgh') - -- expect(parent.func()).toEqual('abcd') - -- end) + -- ROBLOX deviation: use rawget to access through metatable + expect(rawget(child :: any, "func")).never.toBeNil() + expect(child.func()).toEqual("efgh") + expect(parent.func()).toEqual("abcd") + end) - -- it('should delete previously inexistent methods when restoring', function() - -- local parent = {func = function() return 'abcd' end} - -- local child = Object.create(parent) + it("should delete previously inexistent methods when restoring", function() + local parent = { + func = function() + return "abcd" + end, + } + -- ROBLOX deviation: use metatables for prototype-like inheritance + local child = setmetatable({}, { __index = parent }) - -- moduleMocker.spyOn(child, 'func').mockReturnValue('efgh') + moduleMocker:spyOn(child, "func").mockReturnValue("efgh") - -- moduleMocker.restoreAllMocks() - -- expect(child.func()).toEqual('abcd') + moduleMocker:restoreAllMocks() + expect(child.func()).toEqual("abcd") - -- moduleMocker.spyOn(parent, 'func').mockReturnValue('jklm') + moduleMocker:spyOn(parent, "func").mockReturnValue("jklm") - -- expect(child.hasOwnProperty('func')).toBe(false) - -- expect(child.func()).toEqual('jklm') - -- end) + -- ROBLOX deviation: use rawget instead of hasOwnProperty + expect(rawget(child :: any, "func")).toBeNil() + expect(child.func()).toEqual("jklm") + end) - -- it('supports mock value returning undefined', function() - -- local obj = { - -- func = function() return 'some text' end - -- } + it("supports mock value returning nil", function() + local obj = { + func = function() + return "some text" + end, + } - -- moduleMocker.spyOn(obj, 'func').mockReturnValue(undefined) + moduleMocker:spyOn(obj, "func").mockReturnValue(nil) - -- expect(obj.func()).never.toEqual('some text') - -- end) + expect(obj.func()).never.toEqual("some text") + end) - -- it('supports mock value once returning undefined', function() - -- local obj = { - -- func = function() return 'some text' end, - -- } + it("supports mock value once returning nil", function() + local obj = { + func = function() + return "some text" + end, + } - -- moduleMocker.spyOn(obj, 'func').mockReturnValueOnce(undefined) + moduleMocker:spyOn(obj, "func").mockReturnValueOnce(nil) - -- expect(obj.func()).never.toEqual('some text') - -- end) + expect(obj.func()).never.toEqual("some text") + end) - it("mockReturnValueOnce mocks value just once", function() - local fake = moduleMocker:fn(function(a: number) - return a + 2 + it("mockReturnValueOnce mocks value just once", function() + local fake = moduleMocker:fn(function(a: number) + return a + 2 + end) + fake.mockReturnValueOnce(42) + expect(fake(2)).toEqual(42) + expect(fake(2)).toEqual(4) + end) + + --[[ + ROBLOX deviation: skipped code: + original code lines 647 - 692 + ]] + + describe("return values", function() + it("tracks return values", function() + local fn = moduleMocker:fn(function(x: number) + return x * 2 + end) + expect(fn.mock.results).toEqual({}) + fn(1) + fn(2) + expect(fn.mock.results).toEqual({ + { type = "return", value = 2 }, + { type = "return", value = 4 }, + } :: { any }) + end) + + it("tracks mocked return values", function() + local fn = moduleMocker:fn(function(x: number) + return x * 2 end) - fake.mockReturnValueOnce(42) - expect(fake(2)).toEqual(42) - expect(fake(2)).toEqual(4) + fn.mockReturnValueOnce("MOCKED!") + fn(1) + fn(2) + expect(fn.mock.results).toEqual({ + { type = "return", value = "MOCKED!" }, + { type = "return", value = 4 }, + } :: { any }) end) - it("mocks a function with return value of nil", function() - local fn = moduleMocker:fn(function() - return nil + it("supports resetting return values", function() + local fn = moduleMocker:fn(function(x: number) + return x * 2 end) - expect(fn()).toEqual(nil) - expect(fn.mock.calls).toEqual({ {} }) + expect(fn.mock.results).toEqual({}) + fn(1) + fn(2) + expect(fn.mock.results).toEqual({ + { type = "return", value = 2 }, + { type = "return", value = 4 }, + }) + fn.mockReset() + expect(fn.mock.results).toEqual({}) + end) + end) + + it("tracks thrown errors without interfering with other tracking", function() + local error_ = Error.new("ODD!") + local fn = moduleMocker:fn(function(x: number, y: number) + -- multiply params + local result = x * y + if result % 2 == 1 then + -- throw error if result is odd + error(error_) + else + return result + end + end) + expect(fn(2, 4)).toBe(8) -- Mock still throws the error even though it was internally + -- caught and recorded + expect(function() + fn(3, 5) + end).toThrow("ODD!") + expect(fn(6, 3)).toBe(18) -- All call args tracked + expect(fn.mock.calls).toEqual({ { 2, 4 }, { 3, 5 }, { 6, 3 } }) -- Results are tracked + expect(fn.mock.results).toEqual({ + { type = "return", value = 8 }, + { type = "throw", value = error_ }, + { type = "return", value = 18 }, + } :: { any }) + end) + + -- ROBLOX deviation: use `nil` instead of undefined for test + it("a call that throws nil is tracked properly", function() + local fn = moduleMocker:fn(function() + -- eslint-disable-next-line no-throw-literal + error(nil) + end) + pcall(function() + fn(2, 4) + end) + expect(fn.mock.calls).toEqual({ { 2, 4 } }) -- Results are tracked + expect(fn.mock.results).toEqual({ { type = "throw", value = nil } }) + end) + + it("results of recursive calls are tracked properly", function() + -- sums up all integers from 0 -> value, using recursion + local fn: any + -- ROBLOX deviation; separate declaration from assignment for + fn = moduleMocker:fn(function(value: number) + if value == 0 then + return 0 + else + return value + fn(value - 1) + end + end) + fn(4) -- All call args tracked + expect(fn.mock.calls).toEqual({ { 4 }, { 3 }, { 2 }, { 1 }, { 0 } }) -- Results are tracked + -- (in correct order of calls, rather than order of returns) + expect(fn.mock.results).toEqual({ + { type = "return", value = 10 }, + { type = "return", value = 6 }, + { type = "return", value = 3 }, + { type = "return", value = 1 }, + { type = "return", value = 0 }, + }) + end) + + it("test results of recursive calls from within the recursive call", function() + -- sums up all integers from 0 -> value, using recursion + local fn: any + -- ROBLOX deviation; separate declaration from assignment for + fn = moduleMocker:fn(function(value: number) + if value == 0 then + return 0 + else + local recursiveResult = fn(value - 1) + if value == 3 then + -- All recursive calls have been made at this point. + expect(fn.mock.calls).toEqual({ { 4 }, { 3 }, { 2 }, { 1 }, { 0 } }) -- But only the last 3 calls have returned at this point. + expect(fn.mock.results).toEqual({ + { type = "incomplete", value = nil }, + { type = "incomplete", value = nil }, + { type = "return", value = 3 }, + { type = "return", value = 1 }, + { type = "return", value = 0 }, + } :: { any }) + end + return value + recursiveResult + end + end) + fn(4) + end) + + it("call mockClear inside recursive mock", function() + -- sums up all integers from 0 -> value, using recursion + local fn: any + -- ROBLOX deviation; separate declaration from assignment for + fn = moduleMocker:fn(function(value: number) + if value == 3 then + fn:mockClear() + end + if value == 0 then + return 0 + else + return value + fn(value - 1) + end end) + fn(3) -- All call args (after the call that cleared the mock) are tracked + expect(fn.mock.calls).toEqual({ { 2 }, { 1 }, { 0 } }) -- Results (after the call that cleared the mock) are tracked + expect(fn.mock.results).toEqual({ + { type = "return", value = 3 }, + { type = "return", value = 1 }, + { type = "return", value = 0 }, + }) + end) + + describe("invocationCallOrder", function() + it("tracks invocationCallOrder made by mocks", function() + local fn1 = moduleMocker:fn() + expect(fn1.mock.invocationCallOrder).toEqual({}) + fn1(1, 2, 3) + expect(fn1.mock.invocationCallOrder[ + 1 --[[ ROBLOX adaptation: added 1 to array index ]] + ]).toBe(1) + fn1("a", "b", "c") + expect(fn1.mock.invocationCallOrder[ + 2 --[[ ROBLOX adaptation: added 1 to array index ]] + ]).toBe(2) + fn1(1, 2, 3) + expect(fn1.mock.invocationCallOrder[ + 3 --[[ ROBLOX adaptation: added 1 to array index ]] + ]).toBe(3) + local fn2 = moduleMocker:fn() + expect(fn2.mock.invocationCallOrder).toEqual({}) + fn2("d", "e", "f") + expect(fn2.mock.invocationCallOrder[ + 1 --[[ ROBLOX adaptation: added 1 to array index ]] + ]).toBe(4) + fn2(4, 5, 6) + expect(fn2.mock.invocationCallOrder[ + 2 --[[ ROBLOX adaptation: added 1 to array index ]] + ]).toBe(5) + end) + + it("supports clearing mock invocationCallOrder", function() + local fn = moduleMocker:fn() + expect(fn.mock.invocationCallOrder).toEqual({}) + fn(1, 2, 3) + expect(fn.mock.invocationCallOrder).toEqual({ 1 }) + fn.mockReturnValue("abcd") + fn.mockClear() + expect(fn.mock.invocationCallOrder).toEqual({}) + fn("a", "b", "c") + expect(fn.mock.invocationCallOrder).toEqual({ 2 }) + expect(fn()).toEqual("abcd") + end) + + it("supports clearing all mocks invocationCallOrder", function() + local fn1 = moduleMocker:fn() + fn1.mockImplementation(function() + return "abcd" + end) + fn1(1, 2, 3) + expect(fn1.mock.invocationCallOrder).toEqual({ 1 }) + local fn2 = moduleMocker:fn() + fn2.mockReturnValue("abcde") + fn2("a", "b", "c", "d") + expect(fn2.mock.invocationCallOrder).toEqual({ 2 }) + moduleMocker:clearAllMocks() + expect(fn1.mock.invocationCallOrder).toEqual({}) + expect(fn2.mock.invocationCallOrder).toEqual({}) + expect(fn1()).toEqual("abcd") + expect(fn2()).toEqual("abcde") + end) + + -- ROBLOX deviation START: skip non-applicable test + -- it("handles a property called `prototype`", function() + -- local mock = + -- moduleMocker:generateFromMetadata(moduleMocker:getMetadata({ prototype = 1 })) + -- expect(mock.prototype).toBe(1) + -- end) + -- ROBLOX deviation END + end) + end) + + describe("getMockImplementation", function() + it("should mock calls to a mock function", function() + local mockFn = moduleMocker:fn() + mockFn.mockImplementation(function() + return "Foo" + end) + expect(typeof(mockFn.getMockImplementation())).toBe("function") + expect(mockFn.getMockImplementation()()).toBe("Foo") end) end) + describe("mockImplementationOnce", function() + -- ROBLOX deviation START: disable Module constructor test + -- it("should mock constructor", function() + -- local mock1 = jest.fn() + -- local mock2 = jest.fn() + -- local Module = jest.fn(function() + -- return { someFn = mock1 } + -- end) + -- local function testFn() + -- local m = Module.new() + -- m:someFn() + -- end + -- Module:mockImplementationOnce(function() + -- return { someFn = mock2 } + -- end) + -- testFn() + -- expect(mock2).toHaveBeenCalled() + -- expect(mock1)["not"].toHaveBeenCalled() + -- testFn() + -- expect(mock1).toHaveBeenCalled() + -- end) + -- ROBLOX deviation END + + it("should mock single call to a mock function", function() + local mockFn = moduleMocker:fn() + mockFn + .mockImplementationOnce(function() + return "Foo" + end) + .mockImplementationOnce(function() + return "Bar" + end) + expect(mockFn()).toBe("Foo") + expect(mockFn()).toBe("Bar") + expect(mockFn()).toBeUndefined() + end) + + it("should fallback to default mock function when no specific mock is available", function() + local mockFn = moduleMocker:fn() + mockFn + .mockImplementationOnce(function() + return "Foo" + end) + .mockImplementationOnce(function() + return "Bar" + end) + .mockImplementation(function() + return "Default" + end) + expect(mockFn()).toBe("Foo") + expect(mockFn()).toBe("Bar") + expect(mockFn()).toBe("Default") + expect(mockFn()).toBe("Default") + end) + end) + + it("mockReturnValue does not override mockImplementationOnce", function() + local mockFn = jest.fn().mockReturnValue(1).mockImplementationOnce(function() + return 2 + end) + expect(mockFn()).toBe(2) + expect(mockFn()).toBe(1) + end) + + it("mockImplementation resets the mock", function() + local fn = jest.fn() + expect(fn()).toBeNil() + fn.mockReturnValue("returnValue") + fn.mockImplementation(function() + return "foo" + end) + expect(fn()).toBe("foo") + end) + + it("should recognize a mocked function", function() + local mockFn = moduleMocker:fn() + expect(moduleMocker:isMockFunction(function() end)).toBe(false) + expect(moduleMocker:isMockFunction(mockFn)).toBe(true) + end) + + it("default mockName is jest.fn()", function() + local fn = jest.fn() + expect(fn.getMockName()).toBe("jest.fn()") + end) + + it("mockName sets the mock name", function() + local fn = jest.fn() + fn.mockName("myMockFn") + expect(fn.getMockName()).toBe("myMockFn") + end) + + it("jest.fn should provide the correct lastCall", function() + local mock = jest.fn() + expect(mock.mock).never.toHaveProperty("lastCall") + mock("first") + mock("second") + mock("last", "call") + expect(mock).toHaveBeenLastCalledWith("last", "call") + expect(mock.mock.lastCall).toEqual({ "last", "call" }) + end) + + it("lastCall gets reset by mockReset", function() + local mock = jest.fn() + mock("first") + mock("last", "call") + expect(mock.mock.lastCall).toEqual({ "last", "call" }) + mock.mockReset() + expect(mock.mock).never.toHaveProperty("lastCall") + end) + + it("mockName gets reset by mockReset", function() + local fn = jest.fn() + expect(fn.getMockName()).toBe("jest.fn()") + fn.mockName("myMockFn") + expect(fn.getMockName()).toBe("myMockFn") + fn.mockReset() + expect(fn.getMockName()).toBe("jest.fn()") + end) + + it("mockName gets reset by mockRestore", function() + local fn = jest.fn() + expect(fn.getMockName()).toBe("jest.fn()") + fn.mockName("myMockFn") + expect(fn.getMockName()).toBe("myMockFn") + fn.mockRestore() + expect(fn.getMockName()).toBe("jest.fn()") + end) + + it("mockName is not reset by mockClear", function() + local fn = jest.fn(function() + return false + end) + fn.mockName("myMockFn") + expect(fn.getMockName()).toBe("myMockFn") + fn.mockClear() + expect(fn.getMockName()).toBe("myMockFn") + end) + + describe("spyOn", function() + it("should work", function() + local isOriginalCalled = false + local originalCallThis + local originalCallArguments: { any }? + local obj = { + -- ROBLOX deviation START: use ... for args + method = function(self, ...) + isOriginalCalled = true + originalCallThis = self + originalCallArguments = table.pack(...) + end, + -- ROBLOX deviation END + } + local spy = moduleMocker:spyOn(obj, "method") + local thisArg = { this = true } + local firstArg = { first = true } + local secondArg = { second = true } + obj.method(thisArg, firstArg, secondArg) + expect(isOriginalCalled).toBe(true) + expect(originalCallThis).toBe(thisArg) + assert(originalCallArguments, "luau narrow") + expect(#originalCallArguments).toBe(2) + expect(originalCallArguments[ + 1 --[[ ROBLOX adaptation: added 1 to array index ]] + ]).toBe(firstArg) + expect(originalCallArguments[ + 2 --[[ ROBLOX adaptation: added 1 to array index ]] + ]).toBe(secondArg) + expect(spy).toHaveBeenCalled() + isOriginalCalled = false + originalCallThis = nil + originalCallArguments = nil + spy.mockRestore() + obj.method(thisArg, firstArg, secondArg) + expect(isOriginalCalled).toBe(true) + expect(originalCallThis).toBe(thisArg) + assert(originalCallArguments, "luau narrow") + expect(#originalCallArguments).toBe(2) + expect(originalCallArguments[ + 1 --[[ ROBLOX adaptation: added 1 to array index ]] + ]).toBe(firstArg) + expect(originalCallArguments[ + 2 --[[ ROBLOX adaptation: added 1 to array index ]] + ]).toBe(secondArg) + expect(spy).never.toHaveBeenCalled() + end) + + it("should throw on invalid input", function() + expect(function() + moduleMocker:spyOn(nil, "method") + end).toThrow() + expect(function() + moduleMocker:spyOn({}, "method") + end).toThrow() + expect(function() + moduleMocker:spyOn({ method = 10 }, "method") + end).toThrow() + end) + + it("supports restoring all spies", function() + local methodOneCalls = 0 + local methodTwoCalls = 0 + local obj = { + methodOne = function(self) + methodOneCalls += 1 + end, + methodTwo = function(self) + methodTwoCalls += 1 + end, + } + local spy1 = moduleMocker:spyOn(obj, "methodOne") + local spy2 = moduleMocker:spyOn(obj, "methodTwo") -- First, we call with the spies: both spies and both original functions + -- should be called. + obj:methodOne() + obj:methodTwo() + expect(methodOneCalls).toBe(1) + expect(methodTwoCalls).toBe(1) + expect(#spy1.mock.calls).toBe(1) + expect(#spy2.mock.calls).toBe(1) + moduleMocker:restoreAllMocks() -- Then, after resetting all mocks, we call methods again. Only the real + -- methods should bump their count, not the spies. + obj:methodOne() + obj:methodTwo() + expect(methodOneCalls).toBe(2) + expect(methodTwoCalls).toBe(2) + expect(#spy1.mock.calls).toBe(1) + expect(#spy2.mock.calls).toBe(1) + end) + + --[[ + ROBLOX deviation: skipped code (getters & prototypes): + original code lines 1250 - 1300 + ]] + end) + --[[ + ROBLOX deviation: skipped code (spyOnProperty not supported): + original code lines 1098 - 1307 + ]] + + -- ROBLOX deviation START: add additional test for luau case + it("mocks a function with return value of nil", function() + local fn = moduleMocker:fn(function() + return nil + end) + expect(fn()).toEqual(nil) + expect(fn.mock.calls).toEqual({ {} }) + end) + -- ROBLOX deviation END end) describe("mocked", function() it("should return unmodified input", function() local subject = {} - expect(moduleMocker:mocked(subject)).toBe(subject) + expect(mocked(subject)).toBe(subject) end) end) ---[[ - ROBLOX deviation: skipped code: - original code lines 1462 - 1467 - ]] +it("`fn` and `spyOn` do not throw", function() + expect(function() + fn() + spyOn({ apple = function() end }, "apple") + end).never.toThrow() +end) diff --git a/src/jest-mock/src/__tests__/roblox.spec.lua b/src/jest-mock/src/__tests__/roblox.spec.lua index 231e24a5..01abaf8b 100644 --- a/src/jest-mock/src/__tests__/roblox.spec.lua +++ b/src/jest-mock/src/__tests__/roblox.spec.lua @@ -12,12 +12,19 @@ * See the License for the specific language governing permissions and * limitations under the License. ]] +--!strict -- ROBLOX NOTE: no upstream -local ModuleMocker = require("../init").ModuleMocker +local LuauPolyfill = require("@pkg/@jsdotlua/luau-polyfill") +type Object = LuauPolyfill.Object + +local exports = require("../init") +local ModuleMocker = exports.ModuleMocker local JestGlobals = require("@pkg/@jsdotlua/jest-globals") +local jest = JestGlobals.jest local expect = JestGlobals.expect local it = JestGlobals.it +local describe = JestGlobals.describe local beforeEach = JestGlobals.beforeEach local moduleMocker @@ -57,3 +64,71 @@ it("returns a function as the second return value", function() expect(mockFn()).toBe(true) expect(mock).toHaveLastReturnedWith(true) end) + +-- These tests are placed here rather than in JestMockGenv because they require +-- use of the ModuleMocker functions. +describe("global mocking & spying", function() + it("globalEnv can spy on top-level global functions", function() + local mockPrint = moduleMocker:spyOn(jest.globalEnv, "print") + mockPrint.mockReturnValueOnce("abcde") + + local print2 = print :: any -- satisfy the type checker + local returnValue = print2("This is an intentional print from a unit test") + local callsAfter = #mockPrint.mock.calls + + expect(callsAfter).toBe(1) + expect(returnValue).toBe("abcde") + + mockPrint.mockReset() + end) + + it("globalEnv can spy on nested global functions", function() + local mockRand = moduleMocker:spyOn(jest.globalEnv.math, "random") + mockRand.mockReturnValueOnce("abcde") + + local random2 = math.random :: any -- satisfy the type checker + local returnValue = random2() + local callsAfter = #mockRand.mock.calls + + expect(callsAfter).toBe(1) + expect(returnValue).toBe("abcde") + + mockRand.mockReset() + end) + + it("globalEnv unmocked functions bypass mock impls", function() + local mockRand = moduleMocker:spyOn(jest.globalEnv.math, "random") + mockRand.mockReturnValueOnce("abcde") + + local returnValue = jest.globalEnv.math.random() + local callsAfter = #mockRand.mock.calls + + expect(callsAfter).toBe(0) + expect(returnValue).never.toBe("abcde") + expect(returnValue).toEqual(expect.any("number")) + + mockRand.mockReset() + end) + + it("globalEnv mocks do not persist beyond restoration", function() + local mockRand = moduleMocker:spyOn(jest.globalEnv.math, "random") + mockRand.mockReturnValueOnce("abcde") + mockRand.mockRestore() + + local returnValue = math.random() + + expect(returnValue).never.toBe("abcde") + expect(returnValue).toEqual(expect.any("number")) + end) + + it("globalEnv can still be mocked after restoration", function() + local mockRand = moduleMocker:spyOn(jest.globalEnv.math, "random") + mockRand.mockReturnValueOnce("abcde") + mockRand.mockRestore() + mockRand.mockReturnValueOnce("vwxyz") + + local returnValue = math.random() + + expect(returnValue).toBe("vwxyz") + end) +end) diff --git a/src/jest-mock/src/init.lua b/src/jest-mock/src/init.lua index eb1e359a..6897c151 100644 --- a/src/jest-mock/src/init.lua +++ b/src/jest-mock/src/init.lua @@ -15,9 +15,18 @@ local LuauPolyfill = require("@pkg/@jsdotlua/luau-polyfill") local Array = LuauPolyfill.Array +local Boolean = LuauPolyfill.Boolean +local Error = LuauPolyfill.Error local Set = LuauPolyfill.Set local Symbol = LuauPolyfill.Symbol +-- ROBLOX deviation START: mocking globals +local JestMockGenv = require("@pkg/@jsdotlua/jest-mock-genv") +type GlobalMocker = JestMockGenv.GlobalMocker +type GlobalAutomocks = JestMockGenv.GlobalAutomocks +local GlobalMocker = JestMockGenv.GlobalMocker +-- ROBLOX deviation END + type Array = LuauPolyfill.Array type Object = LuauPolyfill.Object @@ -67,6 +76,8 @@ export type MaybeMocked = T original code lines 81 - 103 ]] +export type UnknownFunction = (...unknown) -> ...unknown +export type Mock = any -- ROBLOX TODO: Uncomment this type and use it once Luau has supported it -- ROBLOX TODO: Un in-line the MockInstance type declaration once we have "extends" syntax in Luau -- type Mock = { @@ -106,6 +117,9 @@ type MockFunctionConfig = { specificMockImpls: Array, } +-- ROBLOX deviation START: mocking globals +-- ROBLOX deviation END + export type ModuleMocker = { isMockFunction: (_self: ModuleMocker, fn: any) -> boolean, fn: (_self: ModuleMocker, implementation: ((Y...) -> T...)?) -> (MockFn, (...any) -> ...any), @@ -113,6 +127,11 @@ export type ModuleMocker = { resetAllMocks: (_self: ModuleMocker) -> (), restoreAllMocks: (_self: ModuleMocker) -> (), mocked: (_self: ModuleMocker, item: T, _deep: boolean?) -> MaybeMocked | MaybeMockedDeep, + spyOn: (_self: ModuleMocker, object: { [any]: any }, methodName: M, accessType: ("get" | "set")?) -> Mock, + -- ROBLOX deviation START: mocking globals + mockGlobals: (_self: ModuleMocker, globals: GlobalMocker, env: Object) -> (), + unmockGlobals: (_self: ModuleMocker, globals: GlobalMocker) -> (), + -- ROBLOX deviation END } ModuleMockerClass.__index = ModuleMockerClass @@ -263,7 +282,7 @@ function ModuleMockerClass:_makeComponent(metadata: any, restore) end if typeof(restore) == "function" then - mocker._spyState.add(restore) + mocker._spyState:add(restore) end mocker._mockState[f] = mocker._defaultMockState() @@ -387,7 +406,7 @@ end -- ROBLOX TODO: type return type as JestMock.Mock when Mock type is implemented properly type MockFn = any -- (...any) -> ...any -function ModuleMockerClass:fn(implementation: ((Y...) -> T...)?): (MockFn, (...any) -> ...any) +function ModuleMockerClass:fn(implementation: ((Y...) -> T...)?): (MockFn, (T...) -> Y...) local length = 0 local fn = self:_makeComponent({ length = length, type = "function" }) if implementation then @@ -403,6 +422,145 @@ function ModuleMockerClass:fn(implementation: ((Y...) -> T...)?): (M return fn, mockFn end +function ModuleMockerClass:spyOn(object: { [any]: any }, methodName: M, accessType: ("get" | "set")?): Mock + if Boolean.toJSBoolean(accessType) then + return self:_spyOnProperty(object, methodName, accessType) + end + -- ROBLOX deviation: function types cannot have fields in lua + if typeof(object) ~= "table" then + error(Error.new(("Cannot spyOn on a primitive value; %s given"):format(typeof(object)))) + end + + -- ROBLOX deviation START: mocking globals + if GlobalMocker:isMockGlobalLibrary(object) then + local automocks = object._automocksRef + -- note: indexing non-mockable functions in `globalEnv` will error, + -- making this index operation subtly, but expectedly, fallible. + local automockFn = automocks[methodName] + if typeof(automockFn) ~= "table" or not automockFn._isGlobalAutomockFn then + error( + Error.new( + ("Cannot spy the %s property because it is not a function; %s given instead"):format( + tostring(methodName), + typeof(automockFn) + ) + ) + ) + elseif automockFn._maybeMock == nil then + error(Error.new("globalEnv has not been initialised by Jest here")) + end + return automockFn._maybeMock + end + -- ROBLOX deviation END + local original = object[methodName] + + if not Boolean.toJSBoolean(self:isMockFunction(original)) then + if typeof(original) ~= "function" then + error( + Error.new( + ("Cannot spy the %s property because it is not a function; %s given instead"):format( + tostring(methodName), + typeof(original) + ) + ) + ) + end + local isMethodOwner = rawget(object, methodName) ~= nil + -- ROBLOX deviation START: ignore prototype and property descriptor logic + -- local descriptor = Object.getOwnPropertyDescriptor(object, methodName) + -- local proto = Object.getPrototypeOf(object) + -- while not Boolean.toJSBoolean(descriptor) and meta ~= nil do + -- descriptor = Object.getOwnPropertyDescriptor(proto, methodName) + -- proto = Object.getPrototypeOf(proto) + -- end + local mock: Mock + -- if Boolean.toJSBoolean(if Boolean.toJSBoolean(descriptor) then descriptor.get else descriptor) then + -- local originalGet = descriptor.get + -- mock = self:_makeComponent({ type = "function" }, function() + -- (descriptor :: any).get = originalGet + -- Object.defineProperty(object, methodName, descriptor :: any) + -- end) + -- descriptor.get = function(_self: any) + -- return mock + -- end + -- Object.defineProperty(object, methodName, descriptor) + -- else + -- ROBLOX deviation END + mock = self:_makeComponent({ type = "function" }, function() + if Boolean.toJSBoolean(isMethodOwner) then + object[methodName] = original + else + object[methodName] = nil + end + end) -- @ts-expect-error overriding original method with a Mock + object[methodName] = mock + -- end + mock.mockImplementation(function(...) + return original(...) + end) + end + return object[methodName] +end +function ModuleMockerClass:_spyOnProperty(obj: T, propertyName: M, accessType_: ("get" | "set")?): Mock<() -> T> + -- ROBLOX deviation: spyOnProperty not supported + + -- ROBLOX note: A version of this behavior _could_ be implemented using some + -- elaborate metatable shenanigans, but we should find a compelling need + -- before pursuing that route + error("spyOn with accessors is not currently supported") + -- local accessType: "get" | "set" = if accessType_ ~= nil then accessType_ else "get" + -- if typeof(obj) ~= "table" and typeof(obj) ~= "function" then + -- error(Error.new(("Cannot spyOn on a primitive value; %s given"):format(tostring(self:_typeOf(obj))))) + -- end + -- if not Boolean.toJSBoolean(obj) then + -- error(Error.new(("spyOn could not find an object to spy upon for %s"):format(tostring(propertyName)))) + -- end + -- if not Boolean.toJSBoolean(propertyName) then + -- error(Error.new("No property name supplied")) + -- end + -- local descriptor = Object.getOwnPropertyDescriptor(obj, propertyName) + -- local proto = Object.getPrototypeOf(obj) + -- while not Boolean.toJSBoolean(descriptor) and proto ~= nil do + -- descriptor = Object.getOwnPropertyDescriptor(proto, propertyName) + -- proto = Object.getPrototypeOf(proto) + -- end + -- if not Boolean.toJSBoolean(descriptor) then + -- error(Error.new(("%s property does not exist"):format(tostring(propertyName)))) + -- end + -- if not Boolean.toJSBoolean(descriptor.configurable) then + -- error(Error.new(("%s is not declared configurable"):format(tostring(propertyName)))) + -- end + -- if not Boolean.toJSBoolean(descriptor[tostring(accessType)]) then + -- error( + -- Error.new(("Property %s does not have access type %s"):format(tostring(propertyName), tostring(accessType))) + -- ) + -- end + -- local original = descriptor[tostring(accessType)] + -- if not Boolean.toJSBoolean(self:isMockFunction(original)) then + -- if typeof(original) ~= "function" then + -- error( + -- Error.new( + -- ("Cannot spy the %s property because it is not a function; %s given instead"):format( + -- tostring(propertyName), + -- tostring(self:_typeOf(original)) + -- ) + -- ) + -- ) + -- end + -- descriptor[tostring(accessType)] = self:_makeComponent({ type = "function" }, function() + -- -- @ts-expect-error: mock is assignable + -- (descriptor :: any)[tostring(accessType)] = original + -- Object.defineProperty(obj, propertyName, descriptor :: any) + -- end); + -- (descriptor[tostring(accessType)] :: Mock<() -> T>):mockImplementation(function(this: unknown) + -- -- @ts-expect-error + -- return original(self, table.unpack(arguments)) + -- end) + -- end + -- Object.defineProperty(obj, propertyName, descriptor) + -- return descriptor[tostring(accessType)] :: Mock<() -> T> +end + function ModuleMockerClass:clearAllMocks() self._mockState = {} end @@ -413,8 +571,8 @@ function ModuleMockerClass:resetAllMocks() end function ModuleMockerClass:restoreAllMocks() - for key, value in ipairs(self._spyState) do - key() + for _, value in self._spyState do + value() end self._spyState = Set.new() end @@ -431,6 +589,50 @@ function ModuleMockerClass:mocked(item: T, _deep: boolean?): MaybeMocked | return item :: any end +-- ROBLOX deviation START: mocking globals +function ModuleMockerClass:mockGlobals(globalMocker: GlobalMocker, env: Object) + assert(not globalMocker.currentlyMocked, "Attempt to mock globals while they're already mocked") + globalMocker.currentlyMocked = true + local function implement(automocks: GlobalAutomocks, env: Object) + for name, automock in automocks do + if automock._isGlobalAutomockFn then + local original = env[name] + local mock + local function mockOriginalImplementation() + mock.mockImplementation(function(...) + return original(...) + end) + end + mock = self:_makeComponent({ + type = "function", + }, mockOriginalImplementation) + mockOriginalImplementation() + automock._maybeUnmocked = original + automock._maybeMock = mock + else + implement(automock, env[name]) + end + end + end + implement(globalMocker.automocks, env) +end + +function ModuleMockerClass:unmockGlobals(globalMocker: GlobalMocker) + globalMocker.currentlyMocked = false + local function unimplement(automocks: GlobalAutomocks) + for name, automock in automocks do + if automock._isGlobalAutomockFn then + automock._maybeUnmocked = nil + automock._maybeMock = nil + else + unimplement(automock) + end + end + end + unimplement(globalMocker.automocks) +end +-- ROBLOX deviation END + exports.ModuleMocker = ModuleMockerClass local JestMock = ModuleMockerClass.new() @@ -438,10 +640,13 @@ local fn = function(implementation: ((Y...) -> T...)?) return JestMock:fn(implementation) end exports.fn = fn --- ROBLOX TODO: spyOn is not implemented --- local spyOn = JestMock.spyOn --- exports.spyOn = spyOn -local mocked = JestMock.mocked +local spyOn = function(object: { [any]: any }, methodName: M, accessType: ("get" | "set")?): Mock + return JestMock:spyOn(object, methodName, accessType) +end +exports.spyOn = spyOn +local mocked = function(item: T, _deep: boolean?): MaybeMocked | MaybeMockedDeep + return JestMock:mocked(item, _deep) +end exports.mocked = mocked return exports diff --git a/src/jest-reporters/README.md b/src/jest-reporters/README.md index 23429986..09b06859 100644 --- a/src/jest-reporters/README.md +++ b/src/jest-reporters/README.md @@ -1,19 +1,7 @@ # jest-reporters -Status: :heavy_check_mark: Ported - -Source: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-reporters - -Version: v27.4.7 +Upstream: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-reporters --- ### :pencil2: Notes - -### :x: Excluded -``` -``` - -### :package: [Dependencies](https://github.com/facebook/jest/blob/v27.4.7/packages/jest-reporters/package.json) -| Package | Version | Status | Notes | -| ------------------ | ------- | ------------------------- | ------------------------------------------------ | diff --git a/src/jest-roblox-shared/README.md b/src/jest-roblox-shared/README.md index 014a15f7..61ea0899 100644 --- a/src/jest-roblox-shared/README.md +++ b/src/jest-roblox-shared/README.md @@ -1,20 +1,7 @@ # roblox-shared -Status: :hammer: In Progress - -Source: N/A - -Version: +This module primarily contains shared util code moved out from the internals of other Jest modules to avoid cycles. --- ### :pencil2: Notes -* This module contains shared util code moved out from the internals of other modules to avoid cycles. - -### :x: Excluded -``` -``` - -### :package: [Dependencies]() -| Package | Version | Status | Notes | -| ------- | ------- | ------ | ----- | diff --git a/src/jest-roblox-shared/src/RobloxInstance.lua b/src/jest-roblox-shared/src/RobloxInstance.lua index a2f45b89..725f90c7 100644 --- a/src/jest-roblox-shared/src/RobloxInstance.lua +++ b/src/jest-roblox-shared/src/RobloxInstance.lua @@ -27,55 +27,67 @@ local isObjectWithKeys = CurrentModuleExpect.isObjectWithKeys local hasPropertyInObject = CurrentModuleExpect.hasPropertyInObject local isAsymmetric = CurrentModuleExpect.isAsymmetric -local _cachedPropertyValues = {} +local exports = {} -local function tryPropertyName(instance, propertyName) +-- Unsafe because no checks are performed that this property is readable. +local function readPropUnsafe(instance: Instance, propertyName: string): unknown return instance[propertyName] end -local function getRobloxProperties(class: string): { string } - local instanceClass = RobloxApi[class] - local t = {} - while instanceClass do - for _, property in ipairs(instanceClass.Properties) do - table.insert(t, property) - end - instanceClass = RobloxApi[instanceClass.Superclass] - end - table.sort(t) - return t +-- Unsafe because no checks are performed that this property is writable. +local function writePropUnsafe(instance: Instance, propertyName: string, value: unknown): () + instance[propertyName] = value end -local function getRobloxDefaults(className: string): ({ [string]: any }, { string }) - local propertiesList = getRobloxProperties(className) +function exports.readProp(instance: Instance, propertyName: string) + return pcall(readPropUnsafe, instance, propertyName) +end - local classCache = _cachedPropertyValues[className] - if classCache then - return classCache, propertiesList - else - classCache = {} - _cachedPropertyValues[className] = classCache - end +function exports.writeProp(instance: Instance, propertyName: string, value: unknown) + return pcall(writePropUnsafe, instance, propertyName, value) +end - local created = Instance.new(className) +function exports.listProps(instance: Instance): { [string]: unknown } + local props = {} + local inheritFrom = RobloxApi[instance.ClassName] + while inheritFrom ~= nil do + for _, unsafeProp in ipairs(inheritFrom.Properties) do + local ok, propValue = exports.readProp(instance, unsafeProp) + if ok then + props[unsafeProp] = if propValue == nil then Object.None else propValue + end + end + inheritFrom = RobloxApi[inheritFrom.Superclass] + end + return props +end - for _, propertyName in ipairs(propertiesList) do - local ok, defaultValue = pcall(tryPropertyName, created, propertyName) +do + -- Hidden from outside code. + local cachedDefaults = {} + function exports.listDefaultProps(className: string): { [string]: unknown } + local cached = cachedDefaults[className] + if cached ~= nil then + return cached + end - if ok then - classCache[propertyName] = defaultValue + local ok, instance = pcall(Instance.new, className) + if not ok then + error("Class type is abstract or not creatable - cannot list defaults") end - end + local defaults = exports.listProps(instance) + instance:Destroy() - created:Destroy() - return classCache, propertiesList + cachedDefaults[className] = defaults + return defaults + end end -- given an Instance and a property-value table subset -- returns true if all property-values in the subset table exist in the Instance -- and returns false otherwise -- returns nil for undefined behavior -local function instanceSubsetEquality(instance: any, subset: any): boolean | nil +function exports.instanceSubsetEquality(instance: any, subset: any): boolean | nil local function subsetEqualityWithContext(seenReferences) return function(localInstance, localSubset) seenReferences = seenReferences or {} @@ -84,12 +96,6 @@ local function instanceSubsetEquality(instance: any, subset: any): boolean | nil return nil end - local instanceProperties = getRobloxProperties(localInstance.ClassName) - local instanceChildren = {} - for _, v in ipairs(localInstance:getChildren()) do - instanceChildren[v.Name] = true - end - return Array.every(Object.keys(localSubset), function(prop) local subsetVal = localSubset[prop] if isObjectWithKeys(subsetVal) then @@ -99,9 +105,8 @@ local function instanceSubsetEquality(instance: any, subset: any): boolean | nil end seenReferences[subsetVal] = true end - local result = localInstance ~= nil - and (table.find(instanceProperties, prop) ~= nil or instanceChildren[prop] ~= nil) - and equals(localInstance[prop], subsetVal, { subsetEqualityWithContext(seenReferences) }) + local ok, value = exports.readProp(localInstance, prop) + local result = ok and equals(value, subsetVal, { subsetEqualityWithContext(seenReferences) }) seenReferences[subsetVal] = nil return result @@ -118,8 +123,12 @@ local function instanceSubsetEquality(instance: any, subset: any): boolean | nil end -- InstanceSubset object behaves like an Instance when serialized by pretty-format + local InstanceSubset = {} +exports.InstanceSubset = InstanceSubset + InstanceSubset.__index = InstanceSubset + function InstanceSubset.new(className, subset) table.sort(subset) local self = { @@ -131,14 +140,14 @@ function InstanceSubset.new(className, subset) return self end --- given an Instance and a property-value table subset, returns --- an InstanceSubset object representing the subset of Instance with values in the subset table --- and a InstanceSubset object representing the subset table -local function getInstanceSubset(instance: any, subset: any, seenReferences_: any?): (any, any) +-- given an Instance and an expected property-value table subset, returns +-- an InstanceSubset object representing the found subset of Instance with values in the subset table +-- and a InstanceSubset object representing the expected subset table +function exports.getInstanceSubset(instance: any, subset: any, seenReferences_: any?): (any, any) local seenReferences = seenReferences_ or {} - local trimmed: any = {} - seenReferences[instance] = trimmed + local foundSubset: any = {} + seenReferences[instance] = foundSubset -- return non-table primitives if equals(instance, subset) then @@ -148,29 +157,23 @@ local function getInstanceSubset(instance: any, subset: any, seenReferences_: an end -- collect non-table primitive values - local newSubset = {} + local expectedSubset = {} for k, v in pairs(subset) do if typeof(v) ~= "table" then - newSubset[k] = v + expectedSubset[k] = v end end - local propsAndChildren = getRobloxProperties(instance.ClassName) - for _, v in ipairs(instance:getChildren()) do - table.insert(propsAndChildren, v.Name) - end - - for i, prop in - ipairs(Array.filter(propsAndChildren, function(prop) - return hasPropertyInObject(subset, prop) - end)) - do - if seenReferences[instance[prop]] ~= nil then + for name, subsetPropOrChild in pairs(subset) do + local ok, realPropOrChild = exports.readProp(instance, name) + if not ok then + continue + elseif seenReferences[realPropOrChild] ~= nil then error("Circular reference passed into .toMatchInstance(subset)") else - local nestedSubset - trimmed[prop], nestedSubset = getInstanceSubset(instance[prop], subset[prop], seenReferences) - newSubset[prop] = nestedSubset + expectedSubset[name] = {} + foundSubset[name], expectedSubset[name] = + exports.getInstanceSubset(realPropOrChild, subsetPropOrChild, seenReferences) end end @@ -181,13 +184,9 @@ local function getInstanceSubset(instance: any, subset: any, seenReferences_: an subsetClassName = rawget(subset, "ClassName") end - return InstanceSubset.new(instance.ClassName, trimmed), InstanceSubset.new(subsetClassName, newSubset) + local found = InstanceSubset.new(instance.ClassName, foundSubset) + local expected = InstanceSubset.new(subsetClassName, expectedSubset) + return found, expected end -return { - getRobloxProperties = getRobloxProperties, - getRobloxDefaults = getRobloxDefaults, - instanceSubsetEquality = instanceSubsetEquality, - InstanceSubset = InstanceSubset, - getInstanceSubset = getInstanceSubset, -} +return exports diff --git a/src/jest-roblox-shared/src/__tests__/RobloxInstance.roblox.spec.lua b/src/jest-roblox-shared/src/__tests__/RobloxInstance.roblox.spec.lua index e90c90b5..e01c4261 100644 --- a/src/jest-roblox-shared/src/__tests__/RobloxInstance.roblox.spec.lua +++ b/src/jest-roblox-shared/src/__tests__/RobloxInstance.roblox.spec.lua @@ -12,6 +12,9 @@ * See the License for the specific language governing permissions and * limitations under the License. ]] +local LuauPolyfill = require("@pkg/@jsdotlua/luau-polyfill") +local Object = LuauPolyfill.Object + local JestGlobals = require("@pkg/@jsdotlua/jest-globals") local expect = JestGlobals.expect local describe = JestGlobals.describe @@ -20,73 +23,89 @@ local it = JestGlobals.it local RobloxInstance = require("../RobloxInstance") local instanceSubsetEquality = RobloxInstance.instanceSubsetEquality local getInstanceSubset = RobloxInstance.getInstanceSubset -local getRobloxProperties = RobloxInstance.getRobloxProperties -local getRobloxDefaults = RobloxInstance.getRobloxDefaults +local listProps = RobloxInstance.listProps +local listDefaultProps = RobloxInstance.listDefaultProps local InstanceSubset = RobloxInstance.InstanceSubset -describe("getRobloxProperties()", function() - it("returns properties for Instance", function() - expect(getRobloxProperties("Instance")).toEqual({ "Archivable", "ClassName", "Name", "Parent" }) +describe("listDefaultProps()", function() + it("doesn't return properties for abstract superclasses", function() + expect(function() + listDefaultProps("Instance") + end).toThrow("abstract or not creatable") end) it("doesn't return protected properties", function() - expect(getRobloxProperties("ModuleScript")).never.toContain({ "Source" }) + expect(listDefaultProps("ModuleScript")).never.toHaveProperty("Source") end) it("doesn't return hidden properties", function() - expect(getRobloxProperties("TextLabel")).never.toContain({ "LocalizedText", "Transparency" }) - end) - - it("returns all properties and inherited properties of Frame", function() - expect(getRobloxProperties("Frame")).toEqual({ - "AbsolutePosition", - "AbsoluteRotation", - "AbsoluteSize", - "Active", - "AnchorPoint", - "Archivable", - "AutoLocalize", - "AutomaticSize", - "BackgroundColor3", - "BackgroundTransparency", - "BorderColor3", - "BorderMode", - "BorderSizePixel", - "ClassName", - "ClipsDescendants", - "LayoutOrder", - "Name", - "NextSelectionDown", - "NextSelectionLeft", - "NextSelectionRight", - "NextSelectionUp", - "Parent", - "Position", - "RootLocalizationTable", - "Rotation", - "Selectable", - "SelectionImageObject", - "Size", - "SizeConstraint", - "Style", - "Visible", - "ZIndex", - }) + expect(listDefaultProps("TextLabel")).never.toHaveProperty("LocalizedText") + expect(listDefaultProps("TextLabel")).never.toHaveProperty("Transparency") + end) + + it("returns inherited properties", function() + expect(listDefaultProps("Part")).toHaveProperty("Anchored") + end) + + it("returns nil properties as None", function() + expect(listDefaultProps("Part")).toHaveProperty("Parent", Object.None) end) -end) -describe("getRobloxDefaults()", function() it("returns default properties and values for TextLabel", function() - local defaults = getRobloxDefaults("TextLabel") + local defaults = listDefaultProps("TextLabel") expect(defaults).toMatchSnapshot() end) it("returns default properties and values for Camera", function() - local defaults = getRobloxDefaults("Camera") - expect(defaults).toMatchSnapshot({ - DiagonalFieldOfView = expect.closeTo(88.87651, 3), + local defaults = listDefaultProps("Camera") + expect(defaults).toMatchSnapshot() + end) +end) + +describe("listProps()", function() + it("returns properties for a simple instance", function() + local simpleInstance = Instance.new("ObjectValue") + simpleInstance.Name = "Bryan" + simpleInstance.Value = simpleInstance + expect(listProps(simpleInstance)).toEqual({ + Archivable = true, + ClassName = "ObjectValue", + Name = "Bryan", + Parent = Object.None, + Value = simpleInstance, }) end) + + it("doesn't return protected properties", function() + local moduleScript = Instance.new("ModuleScript") + expect(listProps(moduleScript)).never.toHaveProperty("Source") + end) + + it("doesn't return hidden properties", function() + local textLabel = Instance.new("TextLabel") + expect(listProps(textLabel)).never.toHaveProperty("LocalizedText") + expect(listProps(textLabel)).never.toHaveProperty("Transparency") + end) + + it("returns inherited properties", function() + local part = Instance.new("Part") + expect(listProps(part)).toHaveProperty("Anchored") + end) + + it("returns nil properties as None", function() + local part = Instance.new("Part") + expect(listProps(part)).toHaveProperty("Parent", Object.None) + end) + + it("returns properties and values for TextLabel", function() + local props = listProps(Instance.new("TextLabel")) + expect(props).toMatchSnapshot() + end) + + it("returns properties and values for Camera", function() + local props = listProps(Instance.new("Camera")) + expect(props).toMatchSnapshot() + end) end) describe("instanceSubsetEquality()", function() diff --git a/src/jest-roblox-shared/src/__tests__/__snapshots__/RobloxInstance.roblox.spec.snap.lua b/src/jest-roblox-shared/src/__tests__/__snapshots__/RobloxInstance.roblox.spec.snap.lua index 9ee504f1..b2366469 100644 --- a/src/jest-roblox-shared/src/__tests__/__snapshots__/RobloxInstance.roblox.spec.snap.lua +++ b/src/jest-roblox-shared/src/__tests__/__snapshots__/RobloxInstance.roblox.spec.snap.lua @@ -1,10 +1,11 @@ -- Jest Roblox Snapshot v1, http://roblox.github.io/jest-roblox-internal/snapshot-testing local exports = {} -exports[ [=[getRobloxDefaults() returns default properties and values for Camera 1]=] ] = [=[ +exports[ [=[listDefaultProps() returns default properties and values for Camera 1]=] ] = [=[ Table { "Archivable": true, "CFrame": CFrame(0, 20, 20, 1, 0, -0, -0, 0.707106829, 0.707106829, 0, -0.707106829, 0.707106829), + "CameraSubject": Object.None, "CameraType": EnumItem(Enum.CameraType.Fixed), "ClassName": "Camera", "DiagonalFieldOfView": NumberCloseTo 88.87651 (3 digits), @@ -16,11 +17,12 @@ Table { "MaxAxisFieldOfView": 70, "Name": "Camera", "NearPlaneZ": -0.5, + "Parent": Object.None, "ViewportSize": Vector2(1, 1), } ]=] -exports[ [=[getRobloxDefaults() returns default properties and values for TextLabel 1]=] ] = [=[ +exports[ [=[listDefaultProps() returns default properties and values for TextLabel 1]=] ] = [=[ Table { "AbsolutePosition": Vector2(0, 0), @@ -44,10 +46,94 @@ Table { "LineHeight": 1, "MaxVisibleGraphemes": -1, "Name": "TextLabel", + "NextSelectionDown": Object.None, + "NextSelectionLeft": Object.None, + "NextSelectionRight": Object.None, + "NextSelectionUp": Object.None, + "Parent": Object.None, "Position": UDim2({0, 0}, {0, 0}), "RichText": false, + "RootLocalizationTable": Object.None, "Rotation": 0, "Selectable": false, + "SelectionImageObject": Object.None, + "Size": UDim2({0, 0}, {0, 0}), + "SizeConstraint": EnumItem(Enum.SizeConstraint.RelativeXY), + "Text": "Label", + "TextBounds": Vector2(0, 0), + "TextColor3": Color3(0.105882, 0.164706, 0.207843), + "TextFits": false, + "TextScaled": false, + "TextSize": 8, + "TextStrokeColor3": Color3(0, 0, 0), + "TextStrokeTransparency": 1, + "TextTransparency": 0, + "TextTruncate": EnumItem(Enum.TextTruncate.None), + "TextWrapped": false, + "TextXAlignment": EnumItem(Enum.TextXAlignment.Center), + "TextYAlignment": EnumItem(Enum.TextYAlignment.Center), + "Visible": true, + "ZIndex": 1, +} +]=] + +exports[ [=[listProps() returns properties and values for Camera 1]=] ] = [=[ + +Table { + "Archivable": true, + "CFrame": CFrame(0, 20, 20, 1, 0, -0, -0, 0.707106829, 0.707106829, 0, -0.707106829, 0.707106829), + "CameraSubject": Object.None, + "CameraType": EnumItem(Enum.CameraType.Fixed), + "ClassName": "Camera", + "DiagonalFieldOfView": 88.87651062011719, + "FieldOfView": 70, + "FieldOfViewMode": EnumItem(Enum.FieldOfViewMode.Vertical), + "Focus": CFrame(0, 0, -5, 1, 0, 0, 0, 1, 0, 0, 0, 1), + "HeadLocked": true, + "HeadScale": 1, + "MaxAxisFieldOfView": 70, + "Name": "Camera", + "NearPlaneZ": -0.5, + "Parent": Object.None, + "ViewportSize": Vector2(1, 1), +} +]=] + +exports[ [=[listProps() returns properties and values for TextLabel 1]=] ] = [=[ + +Table { + "AbsolutePosition": Vector2(0, 0), + "AbsoluteRotation": 0, + "AbsoluteSize": Vector2(0, 0), + "Active": false, + "AnchorPoint": Vector2(0, 0), + "Archivable": true, + "AutoLocalize": true, + "AutomaticSize": EnumItem(Enum.AutomaticSize.None), + "BackgroundColor3": Color3(0.639216, 0.635294, 0.647059), + "BackgroundTransparency": 0, + "BorderColor3": Color3(0.105882, 0.164706, 0.207843), + "BorderMode": EnumItem(Enum.BorderMode.Outline), + "BorderSizePixel": 1, + "ClassName": "TextLabel", + "ClipsDescendants": false, + "ContentText": "Label", + "Font": EnumItem(Enum.Font.Legacy), + "LayoutOrder": 0, + "LineHeight": 1, + "MaxVisibleGraphemes": -1, + "Name": "TextLabel", + "NextSelectionDown": Object.None, + "NextSelectionLeft": Object.None, + "NextSelectionRight": Object.None, + "NextSelectionUp": Object.None, + "Parent": Object.None, + "Position": UDim2({0, 0}, {0, 0}), + "RichText": false, + "RootLocalizationTable": Object.None, + "Rotation": 0, + "Selectable": false, + "SelectionImageObject": Object.None, "Size": UDim2({0, 0}, {0, 0}), "SizeConstraint": EnumItem(Enum.SizeConstraint.RelativeXY), "Text": "Label", diff --git a/src/jest-snapshot/src/__tests__/utils.roblox.spec.lua b/src/jest-roblox-shared/src/__tests__/getParent.test.lua similarity index 97% rename from src/jest-snapshot/src/__tests__/utils.roblox.spec.lua rename to src/jest-roblox-shared/src/__tests__/getParent.test.lua index 5dbd2026..097d5865 100644 --- a/src/jest-snapshot/src/__tests__/utils.roblox.spec.lua +++ b/src/jest-roblox-shared/src/__tests__/getParent.test.lua @@ -19,7 +19,7 @@ local expect = JestGlobals.expect local describe = JestGlobals.describe local it = JestGlobals.it -local getParent = require("../utils").robloxGetParent +local getParent = require("../getParent") describe("getParent", function() it("works on Unix paths", function() diff --git a/src/jest-roblox-shared/src/cleanLoadStringStack.lua b/src/jest-roblox-shared/src/cleanLoadStringStack.lua new file mode 100644 index 00000000..9855c737 --- /dev/null +++ b/src/jest-roblox-shared/src/cleanLoadStringStack.lua @@ -0,0 +1,21 @@ +local loadModuleEnabled = pcall((debug :: any).loadmodule, Instance.new("ModuleScript")) + +return function(line: string): string + if not loadModuleEnabled then + local spacing, filePath, lineNumber, extra = line:match('(%s*)%[string "(.-)"%]:(%d+)(.*)') + if filePath then + local match = filePath + if spacing then + match = spacing .. match + end + if lineNumber then + match = match .. ":" .. lineNumber + end + if extra then + match = match .. extra + end + return match + end + end + return line +end diff --git a/src/jest-roblox-shared/src/ensureDirectoryExists.lua b/src/jest-roblox-shared/src/ensureDirectoryExists.lua new file mode 100644 index 00000000..dd004b83 --- /dev/null +++ b/src/jest-roblox-shared/src/ensureDirectoryExists.lua @@ -0,0 +1,30 @@ +-- moved from jest-snapshot/utils.lua + +local LuauPolyfill = require("@pkg/@jsdotlua/luau-polyfill") +local Error = LuauPolyfill.Error + +local getParent = require("./getParent") +local getDataModelService = require("./getDataModelService") + +local FileSystemService = getDataModelService("FileSystemService") + +local function ensureDirectoryExists(filePath: string) + -- ROBLOX deviation: gets path of parent directory, GetScriptFilePath can only be called on ModuleScripts + local path = getParent(filePath, 1) + local ok, err = pcall(function() + if FileSystemService and not FileSystemService:Exists(path) then + FileSystemService:CreateDirectories(path) + end + end) + + if not ok and err:find("Error%(13%): Access Denied%. Path is outside of sandbox%.") then + error( + Error.new( + "Provided path is invalid: you likely need to provide a different argument to --fs.readwrite.\n" + .. "You may need to pass in `--fs.readwrite=$PWD`" + ) + ) + end +end + +return ensureDirectoryExists diff --git a/src/jest-roblox-shared/src/getDataModelService.lua b/src/jest-roblox-shared/src/getDataModelService.lua new file mode 100644 index 00000000..ae0906e6 --- /dev/null +++ b/src/jest-roblox-shared/src/getDataModelService.lua @@ -0,0 +1,10 @@ +-- checks that the service exists and is accessible before returning it, otherwise returns nil +return function(service: string) + local success, result = pcall(function() + local service = game:GetService(service) + local _ = service.Name + return service + end) + + return success and result or nil +end diff --git a/src/jest-roblox-shared/src/getParent.lua b/src/jest-roblox-shared/src/getParent.lua new file mode 100644 index 00000000..4381785b --- /dev/null +++ b/src/jest-roblox-shared/src/getParent.lua @@ -0,0 +1,22 @@ +-- ROBLOX deviation: added to handle file paths in snapshot/State +local function getParent(path: string, level_: number?): string + local level = if level_ then level_ else 0 + + local isUnixPath = string.sub(path, 1, 1) == "/" + local t = {} + + for p in string.gmatch(path, "[^\\/][^\\/]*") do + table.insert(t, p) + end + if level > 0 then + t = { table.unpack(t, 1, #t - level) } + end + + if isUnixPath then + return "/" .. table.concat(t, "/") + end + + return table.concat(t, "\\") +end + +return getParent diff --git a/src/jest-roblox-shared/src/init.lua b/src/jest-roblox-shared/src/init.lua index 7acbf3ff..d4efa4c4 100644 --- a/src/jest-roblox-shared/src/init.lua +++ b/src/jest-roblox-shared/src/init.lua @@ -16,8 +16,12 @@ local nodeUtilsModule = require("./nodeUtils") export type NodeJS_WriteStream = nodeUtilsModule.NodeJS_WriteStream local exports = { + cleanLoadStringStack = require("./cleanLoadStringStack"), dedent = require("./dedent").dedent, escapePatternCharacters = require("./escapePatternCharacters").escapePatternCharacters, + ensureDirectoryExists = require("./ensureDirectoryExists"), + getDataModelService = require("./getDataModelService"), + getParent = require("./getParent"), expect = require("./expect"), getRelativePath = require("./getRelativePath"), RobloxInstance = require("./RobloxInstance"), diff --git a/src/jest-roblox-shared/src/pruneDeps.lua b/src/jest-roblox-shared/src/pruneDeps.lua index 1f65ab0f..cb39ff51 100644 --- a/src/jest-roblox-shared/src/pruneDeps.lua +++ b/src/jest-roblox-shared/src/pruneDeps.lua @@ -18,6 +18,8 @@ local pruneMatch = { "@jsdotlua.promise.", } +local cleanLoadStringStack = require("./cleanLoadStringStack") + local function pruneDeps(str: string?): string? if str == nil then return nil @@ -35,6 +37,8 @@ local function pruneDeps(str: string?): string? if not matched then table.insert(newLines, line) end + line = cleanLoadStringStack(line) + table.insert(newLines, line) end return table.concat(newLines, "\n") end diff --git a/src/jest-runner/README.md b/src/jest-runner/README.md index 4d3ae7ea..00c8475c 100644 --- a/src/jest-runner/README.md +++ b/src/jest-runner/README.md @@ -1,19 +1,7 @@ # jest-reporters -Status: :hammer: In progress - -Source: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-runner - -Version: v27.4.7 +Upstream: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-runner --- ### :pencil2: Notes - -### :x: Excluded -``` -``` - -### :package: [Dependencies](https://github.com/facebook/jest/blob/v27.4.7/packages/jest-runner/package.json) -| Package | Version | Status | Notes | -| ------------------ | ------- | ------------------------- | ------------------------------------------------ | diff --git a/src/jest-runner/src/init.lua b/src/jest-runner/src/init.lua index 36fe7e71..51704fbb 100644 --- a/src/jest-runner/src/init.lua +++ b/src/jest-runner/src/init.lua @@ -20,6 +20,11 @@ type Promise = LuauPolyfill.Promise local Promise = require("@pkg/@jsdotlua/promise") +-- ROBLOX deviation START: additional function to construct file path from ModuleScript +local getDataModelService = require("@pkg/@jsdotlua/jest-roblox-shared").getDataModelService +local CoreScriptSyncService = getDataModelService("CoreScriptSyncService") +-- ROBLOX deviation END + local exports = {} -- ROBLOX deviation: chalk used only in parallel tests @@ -174,6 +179,9 @@ function TestRunner:_createInBandTestRun( -- process.env.JEST_WORKER_ID = "1" local mutex = throat(1) :: ThroatLateBound return Array.reduce(tests, function(promise: Promise, test: Test) + if CoreScriptSyncService then + test.path = CoreScriptSyncService:GetScriptFilePath(test.script) + end return mutex(function() -- ROBLOX FIXME START: Promise type doesn't support changing return type with :andThen call return (promise :: Promise) diff --git a/src/jest-runner/src/runTest.lua b/src/jest-runner/src/runTest.lua index 0f153891..a170a4d4 100644 --- a/src/jest-runner/src/runTest.lua +++ b/src/jest-runner/src/runTest.lua @@ -18,6 +18,8 @@ type Promise = LuauPolyfill.Promise -- ROBLOX deviation START: additional function to construct file path from ModuleScript local getRelativePath = require("@pkg/@jsdotlua/jest-roblox-shared").getRelativePath +local getDataModelService = require("@pkg/@jsdotlua/jest-roblox-shared").getDataModelService +local CoreScriptSyncService = getDataModelService("CoreScriptSyncService") -- ROBLOX deviation END local exports = {} @@ -366,7 +368,12 @@ local function runTestInternal( slow = testRuntime / 1000 > projectConfig.slowTestThreshold, start = start, } - result.testFilePath = getRelativePath(path, projectConfig.rootDir) + -- ROBLOX deviation: resolve to a FS path if CoreScriptSyncService is available + if CoreScriptSyncService then + result.testFilePath = CoreScriptSyncService:GetScriptFilePath(path) + else + result.testFilePath = getRelativePath(path, projectConfig.rootDir) + end result.console = testConsole:getBuffer() result.skipped = testCount == result.numPendingTests result.displayName = projectConfig.displayName diff --git a/src/jest-runtime/README.md b/src/jest-runtime/README.md index 513fee8f..06d500d8 100644 --- a/src/jest-runtime/README.md +++ b/src/jest-runtime/README.md @@ -1,19 +1,7 @@ # jest-runtime -Status: :heavy_check_mark: Ported - -Source: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-runtime - -Version: v27.4.7 +Upstream: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-runtime --- ### :pencil2: Notes - -### :x: Excluded -``` -``` - -### :package: [Dependencies](https://github.com/facebook/jest/blob/v27.4.7/packages/jest-runtime/package.json) -| Package | Version | Status | Notes | -| ------------------ | ------- | ------------------------- | ------------------------------------------------ | diff --git a/src/jest-runtime/src/init.lua b/src/jest-runtime/src/init.lua index 1d8fac4b..73999d21 100644 --- a/src/jest-runtime/src/init.lua +++ b/src/jest-runtime/src/init.lua @@ -1,3 +1,4 @@ +--!nonstrict -- ROBLOX upstream: https://github.com/facebook/jest/blob/v28.0.0/packages/jest-runtime/src/index.ts --[[* * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. @@ -101,10 +102,13 @@ local jestMockModule = require("@pkg/@jsdotlua/jest-mock") -- type MockFunctionMetadata = jestMockModule.MockFunctionMetadata -- ROBLOX deviation END type ModuleMocker = jestMockModule.ModuleMocker - -- ROBLOX deviation START: (addition) importing ModuleMocker class instead of injecting it via runTests local ModuleMocker = jestMockModule.ModuleMocker -- ROBLOX deviation END +-- ROBLOX deviation START: mocking globals +local jestMockGenvModule = require("@pkg/@jsdotlua/jest-mock-genv") +local GlobalMocker = jestMockGenvModule.GlobalMocker +type GlobalMocker = jestMockGenvModule.GlobalMocker -- ROBLOX deviation START: skipped -- local escapePathForRegex = require("@pkg/jest-regex-util").escapePathForRegex -- local jestResolveModule = require("@pkg/jest-resolve") @@ -184,6 +188,7 @@ type InternalModuleOptions = Object -- supportsTopLevelAwait = false, -- } -- ROBLOX deviation END + -- ROBLOX deviation START: custom type implementation -- type InitialModule = Omit type Module = { @@ -283,23 +288,12 @@ export type Runtime = { -- unstable as it should be replaced by https://github.c -- unstable_importModule: (self: Runtime, from: Config_Path, moduleName: string?) -> Promise, -- ROBLOX deviation END -- ROBLOX deviation START: using ModuleScript instead of string - -- requireModule: ( - -- self: Runtime, - -- from: Config_Path, - -- moduleName: string?, - -- options: InternalModuleOptions?, - -- isRequireActual_: boolean? - -- ) -> T, - -- requireInternalModule: (self: Runtime, from: Config_Path, to: string?) -> T, - -- requireActual: (self: Runtime, from: Config_Path, moduleName: string) -> T, - -- requireMock: (self: Runtime, from: Config_Path, moduleName: string) -> T, requireModule: ( self: Runtime, from: ModuleScript, moduleName: ModuleScript?, options: InternalModuleOptions?, isRequireActual_: boolean?, - -- ROBLOX NOTE: additional param to not require return from test files noModuleReturnRequired: boolean? ) -> T, @@ -308,7 +302,6 @@ export type Runtime = { -- unstable as it should be replaced by https://github.c requireMock: (self: Runtime, from: ModuleScript, moduleName: ModuleScript) -> T, -- ROBLOX deviation END -- ROBLOX deviation START: modified signature, use ModuleScript instead of string - -- requireModuleOrMock: (self: Runtime, from: Config_Path, moduleName: string) -> T, requireModuleOrMock: (self: Runtime, moduleName: ModuleScript) -> T, -- ROBLOX deviation END isolateModules: (self: Runtime, fn: () -> ()) -> (), @@ -325,8 +318,6 @@ export type Runtime = { -- unstable as it should be replaced by https://github.c setMock: ( self: Runtime, -- ROBLOX deviation START: using ModuleScript instead of string - -- from: string, - -- moduleName: string, from: ModuleScript, moduleName: ModuleScript, -- ROBLOX deviation END @@ -347,16 +338,6 @@ type Runtime_private = { -- -- unstable_importModule: (self: Runtime_private, from: Config_Path, moduleName: string?) -> Promise, -- ROBLOX deviation END -- ROBLOX deviation START: using ModuleScript instead of string - -- requireModule: ( - -- self: Runtime_private, - -- from: Config_Path, - -- moduleName: string?, - -- options: InternalModuleOptions?, - -- isRequireActual_: boolean? - -- ) -> T, - -- requireInternalModule: (self: Runtime_private, from: Config_Path, to: string?) -> T, - -- requireActual: (self: Runtime_private, from: Config_Path, moduleName: string) -> T, - -- requireMock: (self: Runtime_private, from: Config_Path, moduleName: string) -> T, requireModule: ( self: Runtime_private, from: ModuleScript, @@ -372,7 +353,6 @@ type Runtime_private = { -- requireMock: (self: Runtime_private, from: ModuleScript, moduleName: ModuleScript) -> T, -- ROBLOX deviation END -- ROBLOX deviation START: modified signature, use ModuleScript instead of string - -- requireModuleOrMock: (self: Runtime_private, from: Config_Path, moduleName: string) -> T, requireModuleOrMock: (self: Runtime_private, moduleName: ModuleScript) -> T, -- ROBLOX deviation END isolateModules: (self: Runtime_private, fn: () -> ()) -> (), @@ -389,8 +369,6 @@ type Runtime_private = { -- setMock: ( self: Runtime_private, -- ROBLOX deviation START: using ModuleScript instead of string - -- from: string, - -- moduleName: string, from: ModuleScript, moduleName: ModuleScript, -- ROBLOX deviation END @@ -456,6 +434,8 @@ type Runtime_private = { -- -- _moduleMockFactories: Map unknown>, -- ROBLOX deviation END _moduleMocker: ModuleMocker, + -- ROBLOX deviation: mocking globals + _globalMocker: GlobalMocker, _isolatedModuleRegistry: ModuleRegistry | nil, _moduleRegistry: ModuleRegistry, -- ROBLOX deviation START: skipped @@ -513,7 +493,7 @@ type Runtime_private = { -- -- importMock: ( -- self: Runtime_private, -- from: Config_Path, - -- moduleName: string, + -- moduleName: ModuleScript, -- context: VMContext -- ) -> Promise, -- getExportsOfCjs: (self: Runtime_private, modulePath: Config_Path) -> any, @@ -522,9 +502,6 @@ type Runtime_private = { -- self: Runtime_private, localModule: InitialModule, -- ROBLOX deviation START: using ModuleScript instead of string - -- from: Config_Path, - -- moduleName: string | nil, - -- modulePath: Config_Path, from: ModuleScript, moduleName: ModuleScript | nil, modulePath: ModuleScript, @@ -543,8 +520,10 @@ type Runtime_private = { -- -- ROBLOX deviation END setModuleMock: ( self: Runtime_private, - from: string, - moduleName: string, + -- ROBLOX deviation START: using ModuleScript instead of string + from: ModuleScript, + moduleName: ModuleScript, + -- ROBLOX deviation END mockFactory: () -> Promise | unknown, options: { virtual: boolean? }? ) -> (), @@ -558,10 +537,10 @@ type Runtime_private = { -- -- _requireResolve: ( -- self: Runtime_private, -- from: Config_Path, - -- moduleName: string?, + -- moduleName: ModuleScript?, -- options_: ResolveOptions? -- ) -> any, - -- _requireResolvePaths: (self: Runtime_private, from: Config_Path, moduleName: string?) -> any, + -- _requireResolvePaths: (self: Runtime_private, from: Config_Path, moduleName: ModuleScript?) -> any, -- ROBLOX deviation END _execModule: ( self: Runtime_private, @@ -584,19 +563,16 @@ type Runtime_private = { -- -- options: InternalModuleOptions? -- ) -> Promise, -- createScriptFromCode: (self: Runtime_private, scriptSource: string, filename: string) -> any, - -- _requireCoreModule: (self: Runtime_private, moduleName: string, supportPrefix: boolean) -> any, - -- _importCoreModule: (self: Runtime_private, moduleName: string, context: VMContext) -> any, + -- _requireCoreModule: (self: Runtime_private, moduleName: ModuleScript, supportPrefix: boolean) -> any, + -- _importCoreModule: (self: Runtime_private, moduleName: ModuleScript, context: VMContext) -> any, -- _getMockedNativeModule: ( -- self: Runtime_private -- ) -> typeof(__unhandledIdentifier__ --[[ ROBLOX TODO: Unhandled node for type: TSQualifiedName ]] --[[ nativeModule.Module ]]), - -- _generateMock: (self: Runtime_private, from: Config_Path, moduleName: string) -> any, + -- _generateMock: (self: Runtime_private, from: Config_Path, moduleName: ModuleScript) -> any, -- ROBLOX deviation END _shouldMock: ( self: Runtime_private, -- ROBLOX deviation: accept ModuleScript instead of string - -- from: Config_Path, - -- moduleName: string, - -- explicitShouldMock: Map, from: ModuleScript, moduleName: ModuleScript, explicitShouldMock: Map, @@ -701,6 +677,13 @@ function Runtime.new(loadedModuleFns: Map?): Runtime -- self._moduleMocker = self._environment.moduleMocker self._moduleMocker = ModuleMocker.new() -- ROBLOX deviation END + -- ROBLOX deviation START: mocking globals + self._globalMocker = GlobalMocker.new(jestMockGenvModule.MOCKABLE_GLOBALS) + -- TODO: if we want to mock more specific function environment members then + -- this will have to be rethought, but for what's being mocked now, it's + -- fine to draw from the global function environment. + self._moduleMocker:mockGlobals(self._globalMocker, getfenv(0)) + -- ROBLOX deviation END self._isolatedModuleRegistry = nil self._isolatedMockRegistry = nil self._moduleRegistry = Map.new() @@ -1069,7 +1052,7 @@ end -- return module -- end) -- end --- function Runtime_private:unstable_importModule(from: Config_Path, moduleName: string?): Promise +-- function Runtime_private:unstable_importModule(from: Config_Path, moduleName: ModuleScript?): Promise -- return Promise.resolve():andThen(function() -- invariant( -- runtimeSupportsVmModules, @@ -1101,7 +1084,7 @@ end -- end, { context = context, identifier = modulePath }) -- return evaluateSyntheticModule(module) -- end --- function Runtime_private:importMock(from: Config_Path, moduleName: string, context: VMContext): Promise +-- function Runtime_private:importMock(from: Config_Path, moduleName: ModuleScript, context: VMContext): Promise -- return Promise.resolve():andThen(function() -- local moduleID = -- self._resolver:getModuleID(self._virtualModuleMocks, from, moduleName, { conditions = self.esmConditions }) @@ -1151,8 +1134,6 @@ end -- ROBLOX deviation END function Runtime_private:requireModule( -- ROBLOX deviation START: using ModuleScript instead of string - -- from: Config_Path, - -- moduleName: string?, from: ModuleScript, moduleName_: ModuleScript?, -- ROBLOX deviation END @@ -1313,13 +1294,12 @@ function Runtime_private:requireInternalModule(from: ModuleScript, to: Module }) end -- ROBLOX deviation START: using ModuleScript instead of string --- function Runtime_private:requireActual(from: Config_Path, moduleName: string): T +-- function Runtime_private:requireActual(from: Config_Path, moduleName: ModuleScript): T function Runtime_private:requireActual(from: ModuleScript, moduleName: ModuleScript): T -- ROBLOX deviation END return self:requireModule(from, moduleName, nil, true) end -- ROBLOX deviation START: using ModuleScript instead of string --- function Runtime_private:requireMock(from: Config_Path, moduleName: string): T -- local moduleID = -- self._resolver:getModuleID(self._virtualMocks, from, moduleName, { conditions = self.cjsConditions }) function Runtime_private:requireMock(from: ModuleScript, moduleName: ModuleScript): T @@ -1405,9 +1385,6 @@ end function Runtime_private:_loadModule( localModule: InitialModule, -- ROBLOX deviation START: using ModuleScript instead of string - -- from: Config_Path, - -- moduleName: string | nil, - -- modulePath: Config_Path, from: ModuleScript, moduleName: ModuleScript | nil, modulePath: ModuleScript, @@ -1446,12 +1423,11 @@ end -- end -- ROBLOX deviation END -- ROBLOX deviation START: modified signature, use ModuleScript instead of string --- function Runtime_private:requireModuleOrMock(from: Config_Path, moduleName: string): T function Runtime_private:requireModuleOrMock(moduleName: ModuleScript): T local from = moduleName -- ROBLOX deviation END -- ROBLOX deviation START: additional interception - if moduleName == script or moduleName == script.Parent then + if moduleName == script or (typeof(script.Parent) == "ModuleScript" and moduleName == script.Parent) then -- ROBLOX NOTE: Need to cast require because analyze cannot figure out scriptInstance path return (require :: any)(moduleName) end @@ -1685,8 +1661,6 @@ end -- ROBLOX deviation END function Runtime_private:setMock( -- ROBLOX deviation START: using module script instead of string moduleName - -- from: string, - -- moduleName: string, from: ModuleScript, moduleName: ModuleScript, -- ROBLOX deviation END @@ -1711,7 +1685,7 @@ end -- ROBLOX deviation START: skipped -- function Runtime_private:setModuleMock( -- from: string, --- moduleName: string, +-- moduleName: ModuleScript, -- mockFactory: () -> Promise | unknown, -- options: { virtual: boolean? }? -- ): () @@ -1735,6 +1709,8 @@ function Runtime_private:clearAllMocks(): () self._moduleMocker:clearAllMocks() end function Runtime_private:teardown(): () + -- ROBLOX deviation: mocking globals + self._moduleMocker:unmockGlobals(self._globalMocker) self:restoreAllMocks() self:resetAllMocks() self:resetModules() @@ -1779,7 +1755,7 @@ end -- function Runtime_private:_resolveModule(from: Config_Path, to: string | nil, options: ResolveModuleConfig?) -- return if Boolean.toJSBoolean(to) then self._resolver:resolveModule(from, to, options) else from -- end --- function Runtime_private:_requireResolve(from: Config_Path, moduleName: string?, options_: ResolveOptions?) +-- function Runtime_private:_requireResolve(from: Config_Path, moduleName: ModuleScript?, options_: ResolveOptions?) -- local options: ResolveOptions = if options_ ~= nil then options_ else {} -- if -- moduleName == nil --[[ ROBLOX CHECK: loose equality used upstream ]] @@ -1836,7 +1812,7 @@ end -- end -- end -- end --- function Runtime_private:_requireResolvePaths(from: Config_Path, moduleName: string?) +-- function Runtime_private:_requireResolvePaths(from: Config_Path, moduleName: ModuleScript?) -- if -- moduleName == nil --[[ ROBLOX CHECK: loose equality used upstream ]] -- then @@ -1966,6 +1942,7 @@ function Runtime_private:_execModule( local moduleFunction, defaultEnvironment, errorMessage, cleanupFn local modulePath = localModule.filename + local loadModuleEnabled = pcall((debug :: any).loadmodule, Instance.new("ModuleScript")) if self._loadedModuleFns and self._loadedModuleFns:has(modulePath) then local loadedModule = self._loadedModuleFns:get(modulePath) :: { any } @@ -1974,8 +1951,12 @@ function Runtime_private:_execModule( else -- Narrowing this type here lets us appease the type checker while still -- counting on types for the rest of this file - local loadmodule: (ModuleScript) -> (any, string, () -> any) = debug["loadmodule"] - moduleFunction, errorMessage, cleanupFn = loadmodule(modulePath) + if loadModuleEnabled then + local loadmodule: (ModuleScript) -> (any, string, () -> any) = debug["loadmodule"] + moduleFunction, errorMessage, cleanupFn = loadmodule(modulePath) + else + moduleFunction = loadstring(modulePath.Source, modulePath:GetFullName()) + end -- ROBLOX NOTE: we are not using assert() as it throws a bare string and we need to throw an Error object if moduleFunction == nil then error(Error.new(errorMessage)) @@ -1998,44 +1979,76 @@ function Runtime_private:_execModule( -- a new module instance but with the same environment table as `moduleFunction` itself at the -- time of invocation. In order to properly sanbox module instances, we need to ensure that -- each instance has its own distinct environment table containing the specific overrides for it, - -- but still inherits from the default parent environment for non-overriden environment goodies. + -- but still inherits from the default parent environment for non-overriden + -- environment goodies. -- local isInternal = false -- if options ~= nil and options.isInternalModule then options.isInternalModule else false local isInternal = if options ~= nil and options.isInternalModule then options.isInternalModule else false - setfenv( - moduleFunction, - setmetatable( - Object.assign( - { - --[[ - ROBLOX NOTE: - Adding `script` directly into a table so that it is accessible to the debugger - It seems to be a similar issue to code inside of __index function not being debuggable - ]] - script = defaultEnvironment.script, - require = if isInternal - then function(scriptInstance: ModuleScript) - return self:requireInternalModule(scriptInstance) - end - else function(scriptInstance: ModuleScript) - return self:requireModuleOrMock(scriptInstance) - end, - }, - if isInternal - then {} - else { - delay = self._fakeTimersImplementation.delayOverride, - tick = self._fakeTimersImplementation.tickOverride, - time = self._fakeTimersImplementation.timeOverride, - DateTime = self._fakeTimersImplementation.dateTimeOverride, - os = self._fakeTimersImplementation.osOverride, - task = self._fakeTimersImplementation.taskOverride, - } - ) :: Object, - { __index = defaultEnvironment } - ) :: any - ) + -- This is the 'least mocked' environment that scripts will be able to see. + -- The final function environment inherits from this sandbox. + -- This is separate so that, in the future, `globalEnv` could expose these + -- 'unmocked' functions instead of the ones in the global environment. + local sandboxEnvironment = setmetatable({ + --[[ + ROBLOX NOTE: + Adding `script` directly into a table so that it is accessible to the debugger + It seems to be a similar issue to code inside of __index function not being debuggable + ]] + script = if loadModuleEnabled then defaultEnvironment.script else modulePath, + require = if isInternal + then function(scriptInstance: ModuleScript) + return self:requireInternalModule(scriptInstance) + end + else function(scriptInstance: ModuleScript) + return self:requireModuleOrMock(scriptInstance) + end, + }, { + __index = defaultEnvironment, + }) + if not isInternal then + Object.assign(sandboxEnvironment, { + delay = self._fakeTimersImplementation.delayOverride, + tick = self._fakeTimersImplementation.tickOverride, + time = self._fakeTimersImplementation.timeOverride, + DateTime = self._fakeTimersImplementation.dateTimeOverride, + os = self._fakeTimersImplementation.osOverride, + task = self._fakeTimersImplementation.taskOverride, + }) + end + + -- This is the environment actually passed to scripts, including all global + -- mocks and other customisations the user might choose to apply. + local mockedSandboxEnvironment = setmetatable({}, { + __index = sandboxEnvironment, + }) + local function setupAutomocks(automocks: Object, sourceEnv: any, saveInto: any) + for name, automock in automocks do + if automock._isGlobalAutomockFn then + local original = sourceEnv[name] + -- Disguise the mock callable table as a function on the + -- outside, so it retains the same behaviour when observed in + -- various ways by almost all code (except debug library stuff) + saveInto[name] = function(...) + if automock._maybeMock == nil then + error(Error.new("Code should not be running when globalEnv is uninitialised")) + end + return automock._maybeMock(...) + end + else + local subSourceEnv = sourceEnv[name] + local subSaveInto = setmetatable({}, { + __index = subSourceEnv, + }) + saveInto[name] = subSaveInto + setupAutomocks(automock, subSourceEnv, subSaveInto) + end + end + end + setupAutomocks(self._globalMocker.automocks, sandboxEnvironment, mockedSandboxEnvironment) + + setfenv(moduleFunction, mockedSandboxEnvironment :: any) local moduleResult = table.pack(moduleFunction()) + if moduleResult.n ~= 1 and noModuleReturnRequired ~= true then error( string.format( @@ -2120,7 +2133,7 @@ end -- end -- end -- end --- function Runtime_private:_requireCoreModule(moduleName: string, supportPrefix: boolean) +-- function Runtime_private:_requireCoreModule(moduleName: ModuleScript, supportPrefix: boolean) -- local moduleWithoutNodePrefix = if Boolean.toJSBoolean( -- if Boolean.toJSBoolean(supportPrefix) then moduleName:startsWith("node:") else supportPrefix -- ) @@ -2134,7 +2147,7 @@ end -- end -- return require_(moduleWithoutNodePrefix) -- end --- function Runtime_private:_importCoreModule(moduleName: string, context: VMContext) +-- function Runtime_private:_importCoreModule(moduleName: ModuleScript, context: VMContext) -- local required = self:_requireCoreModule(moduleName, supportsNodeColonModulePrefixInImport) -- local module = SyntheticModule.new( -- Array.concat({}, { "default" }, Array.spread(Object.keys(required))), @@ -2219,7 +2232,7 @@ end -- self._moduleImplementation = Module -- return Module -- end --- function Runtime_private:_generateMock(from: Config_Path, moduleName: string) +-- function Runtime_private:_generateMock(from: Config_Path, moduleName: ModuleScript) -- local ref = self._resolver:resolveStubModuleName(from, moduleName) -- local modulePath = Boolean.toJSBoolean(ref) and ref -- or self:_resolveModule(from, moduleName, { conditions = self.cjsConditions }) @@ -2259,9 +2272,6 @@ end -- ROBLOX deviation END function Runtime_private:_shouldMock( -- ROBLOX deviation: accept ModuleScript instead of string - -- from: Config_Path, - -- moduleName: string, - -- explicitShouldMock: Map, from: ModuleScript, moduleName: ModuleScript, explicitShouldMock: Map, @@ -2351,7 +2361,7 @@ function Runtime_private:_shouldMock( end -- ROBLOX deviation START: skipped -- function Runtime_private:_createRequireImplementation(from: InitialModule, options: InternalModuleOptions?): NodeRequire --- local function resolve(moduleName: string, resolveOptions: ResolveOptions?) +-- local function resolve(moduleName: ModuleScript, resolveOptions: ResolveOptions?) -- local resolved = self:_requireResolve(from.filename, moduleName, resolveOptions) -- if -- Boolean.toJSBoolean((function() @@ -2367,11 +2377,11 @@ end -- end -- return resolved -- end --- resolve.paths = function(_self: any, moduleName: string) +-- resolve.paths = function(_self: any, moduleName: ModuleScript) -- return self:_requireResolvePaths(from.filename, moduleName) -- end -- local moduleRequire = if Boolean.toJSBoolean(if typeof(options) == "table" then options.isInternalModule else nil) --- then function(moduleName: string) +-- then function(moduleName: ModuleScript) -- return self:requireInternalModule(from.filename, moduleName) -- end -- else self.requireModuleOrMock:bind(self, from.filename) :: NodeRequire @@ -2404,6 +2414,7 @@ end -- return moduleRequire -- end -- ROBLOX deviation END + -- ROBLOX deviation START: using ModuleScript instead of Config_Path -- function Runtime_private:_createJestObjectFor(from: Config_Path): Jest function Runtime_private:_createJestObjectFor(from: ModuleScript): Jest @@ -2423,7 +2434,6 @@ function Runtime_private:_createJestObjectFor(from: ModuleScript): Jest -- end -- ROBLOX deviation END -- ROBLOX deviation START: using ModuleScript instead of string - -- local function unmock(moduleName: string) local function unmock(moduleName: ModuleScript) -- ROBLOX deviation END -- ROBLOX deviation START: using module script instead of string moduleName @@ -2435,7 +2445,7 @@ function Runtime_private:_createJestObjectFor(from: ModuleScript): Jest return jestObject end -- ROBLOX deviation START: not implemented yet - -- local function deepUnmock(moduleName: string) + -- local function deepUnmock(moduleName: ModuleScript) -- local moduleID = -- self._resolver:getModuleID(self._virtualMocks, from, moduleName, { conditions = self.cjsConditions }) -- self._explicitShouldMock:set(moduleID, false) @@ -2460,7 +2470,6 @@ function Runtime_private:_createJestObjectFor(from: ModuleScript): Jest return jestObject end -- ROBLOX deviation START: using ModuleScript instead of string and predefine function - -- local function setMockFactory(moduleName: string, mockFactory: () -> unknown, options: { virtual: boolean? }?) function setMockFactory(moduleName: ModuleScript, mockFactory: () -> unknown, options: { virtual: boolean? }?) -- ROBLOX deviation END self:setMock(from, moduleName, mockFactory, options) @@ -2538,13 +2547,17 @@ function Runtime_private:_createJestObjectFor(from: ModuleScript): Jest return jestObject end -- ROBLOX deviation START: no built-in bind support in Luau - local fn = function(implementation: any) - return self._moduleMocker:fn(implementation) + local fn = function(...) + return self._moduleMocker:fn(...) + end + local spyOn = function(...) + return self._moduleMocker:spyOn(...) end -- local fn = self._moduleMocker.fn:bind(self._moduleMocker) - -- ROBLOX deviation END -- local spyOn = self._moduleMocker.spyOn:bind(self._moduleMocker) + -- ROBLOX deviation END + -- ROBLOX deviation START: not implemented yet -- local ref = if typeof(self._moduleMocker.mocked) == "table" then self._moduleMocker.mocked.bind else nil -- local ref = if ref ~= nil then ref(self._moduleMocker) else nil @@ -2595,7 +2608,7 @@ function Runtime_private:_createJestObjectFor(from: ModuleScript): Jest return _getFakeTimers():clearAllTimers() end, -- ROBLOX TODO START: not implemented yet - -- createMockFromModule = function(moduleName: string) + -- createMockFromModule = function(moduleName: ModuleScript) -- return self:_generateMock(from, moduleName) -- end, -- deepUnmock = deepUnmock, @@ -2608,7 +2621,7 @@ function Runtime_private:_createJestObjectFor(from: ModuleScript): Jest -- ROBLOX TODO END fn = fn, -- ROBLOX TODO START: not implemented yet - -- genMockFromModule = function(moduleName: string) + -- genMockFromModule = function(moduleName: ModuleScript) -- return self:_generateMock(from, moduleName) -- end, -- ROBLOX TODO END @@ -2623,6 +2636,8 @@ function Runtime_private:_createJestObjectFor(from: ModuleScript): Jest getTimerCount = function() return _getFakeTimers():getTimerCount() end, + -- ROBLOX deviation: mocking globals + globalEnv = self._globalMocker.envObject, isMockFunction = self._moduleMocker.isMockFunction, isolateModules = isolateModules, mock = mock, @@ -2666,7 +2681,6 @@ function Runtime_private:_createJestObjectFor(from: ModuleScript): Jest jestTimers = _getFakeTimers(), -- ROBLOX deviation END -- ROBLOX deviation START: using ModuleScript instead of string moduleName & virtual mocks not supported - -- setMock = function(moduleName: string, mock: unknown) setMock = function(moduleName: ModuleScript, mock: unknown) -- ROBLOX deviation END return setMockFactory(moduleName, function() @@ -2683,12 +2697,12 @@ function Runtime_private:_createJestObjectFor(from: ModuleScript): Jest end, -- ROBLOX TODO START: not implemented yet -- setTimeout = setTimeout, - -- spyOn = spyOn, - -- ROBOX TODO END + -- ROBLOX TODO END + spyOn = spyOn, unmock = unmock, -- ROBLOX TODO START: not implemented yet -- unstable_mockModule = mockModule, - -- ROBOX TODO END + -- ROBLOX TODO END useFakeTimers = useFakeTimers, useRealTimers = useRealTimers, } diff --git a/src/jest-snapshot-serializer-raw/README.md b/src/jest-snapshot-serializer-raw/README.md index bb1766f3..36c6242d 100644 --- a/src/jest-snapshot-serializer-raw/README.md +++ b/src/jest-snapshot-serializer-raw/README.md @@ -1,21 +1,8 @@ # jest-util -Status: :heavy_check_mark: Ported - -Source: https://github.com/ikatyang/jest-snapshot-serializer-raw/tree/v1.2.0 - -Version: v1.2.0 +Upstream: https://github.com/ikatyang/jest-snapshot-serializer-raw/tree/v1.2.0 --- ### :pencil2: Notes -### :x: Excluded - - -### :package: [Dependencies](https://github.com/ikatyang/jest-snapshot-serializer-raw/blob/v1.2.0/package.json) - -| Package | Version | Status | Notes | -| ------- | ------- | ------ | ----- | - - diff --git a/src/jest-snapshot/README.md b/src/jest-snapshot/README.md index 48748c28..ed377196 100644 --- a/src/jest-snapshot/README.md +++ b/src/jest-snapshot/README.md @@ -1,10 +1,9 @@ # jest-snapshot -Status: :hammer: In Progress +Upstream: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-snapshot -Source: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-snapshot +This package implements snapshot testing capabilities of Jest. You can find its documentation in the [Jest documentation](https://roblox.github.io/jest-roblox-internal). -Version: v27.4.7 --- diff --git a/src/jest-snapshot/src/SnapshotResolver.lua b/src/jest-snapshot/src/SnapshotResolver.lua index f9f1666e..54dfcaa0 100644 --- a/src/jest-snapshot/src/SnapshotResolver.lua +++ b/src/jest-snapshot/src/SnapshotResolver.lua @@ -29,17 +29,10 @@ type Config_ProjectConfig = typesModule.Config_ProjectConfig -- ROBLOX deviation END -- ROBLOX deviation START: additinal dependencies -local utils = require("./utils") -local getParent = utils.robloxGetParent - -local function getCoreScriptSyncService() - local success, result = pcall(function() - return game:GetService("CoreScriptSyncService") - end) - - return success and result or nil -end -local CoreScriptSyncService = nil +local RobloxShared = require("@pkg/@jsdotlua/jest-roblox-shared") +local getParent = RobloxShared.getParent +local getDataModelService = RobloxShared.getDataModelService +local CoreScriptSyncService = getDataModelService("CoreScriptSyncService") -- ROBLOX deviation END -- ROBLOX deviation START: predefine functions @@ -137,9 +130,6 @@ function createDefaultSnapshotResolver(): SnapshotResolver return snapshotPath end, getPath = function() - if CoreScriptSyncService == nil then - CoreScriptSyncService = getCoreScriptSyncService() or false - end if not CoreScriptSyncService then error( Error( diff --git a/src/jest-snapshot/src/__tests__/__snapshots__/snapshot.roblox.spec.snap.lua b/src/jest-snapshot/src/__tests__/__snapshots__/snapshot.roblox.spec.snap.lua index 82e8045b..5cb6386d 100644 --- a/src/jest-snapshot/src/__tests__/__snapshots__/snapshot.roblox.spec.snap.lua +++ b/src/jest-snapshot/src/__tests__/__snapshots__/snapshot.roblox.spec.snap.lua @@ -1,7 +1,5 @@ -- Jest Roblox Snapshot v1, http://roblox.github.io/jest-roblox-internal/snapshot-testing - local exports = {} - exports[ [=[custom snapshot matchers: toMatchTrimmedSnapshot 1]=] ] = [=[ "extra long"]=] diff --git a/src/jest-snapshot/src/init.lua b/src/jest-snapshot/src/init.lua index 6f5ac3f3..5b28438b 100644 --- a/src/jest-snapshot/src/init.lua +++ b/src/jest-snapshot/src/init.lua @@ -13,6 +13,11 @@ local Error = LuauPolyfill.Error local instanceof = LuauPolyfill.instanceof local AssertionError = LuauPolyfill.AssertionError +-- ROBLOX deviation START: additional dependencies +local RobloxShared = require("@pkg/@jsdotlua/jest-roblox-shared") +local cleanLoadStringStack = RobloxShared.cleanLoadStringStack +-- ROBLOX deviation END + local getType = require("@pkg/@jsdotlua/jest-get-type").getType -- ROBLOX TODO: ADO-1633 fix Jest Types imports @@ -407,6 +412,9 @@ function _toThrowErrorMatchingSnapshot(config: types.MatchSnapshotConfig, fromPr error_ = tostring(error_) end + -- ROBLOX deviation: if loadstring is used, format the loadstring stacktrace to look like a path + error_ = cleanLoadStringStack(error_) + return _toMatchSnapshot({ context = context, hint = hint, diff --git a/src/jest-snapshot/src/utils.lua b/src/jest-snapshot/src/utils.lua index 9681667b..8e62abc2 100644 --- a/src/jest-snapshot/src/utils.lua +++ b/src/jest-snapshot/src/utils.lua @@ -10,14 +10,9 @@ -- corresponds to the functions needed by the other translated files. We plan -- on filling the rest of utils out as we continue with the jest-snapshot file. -local function getFileSystemService() - local success, result = pcall(function() - return game:GetService("FileSystemService") - end) - - return success and result or nil -end -local FileSystemService = nil +local RobloxShared = require("@pkg/@jsdotlua/jest-roblox-shared") +local getDataModelService = RobloxShared.getDataModelService +local FileSystemService = getDataModelService("FileSystemService") local LuauPolyfill = require("@pkg/@jsdotlua/luau-polyfill") local Array = LuauPolyfill.Array @@ -221,26 +216,8 @@ local function printBacktickString(str: string): string return "[=[\n" .. str .. "]=]" end -local function ensureDirectoryExists(filePath: string) - -- ROBLOX deviation: gets path of parent directory, GetScriptFilePath can only be called on ModuleScripts - local pathComponents = filePath:split("/") - pathComponents = table.pack(table.unpack(pathComponents, 1, #pathComponents - 1)) - local path = table.concat(pathComponents, "/") - local ok, err = pcall(function() - if not FileSystemService:Exists(path) then - FileSystemService:CreateDirectories(path) - end - end) - - if not ok and err:find("Error%(13%): Access Denied%. Path is outside of sandbox%.") then - error( - Error.new( - "Provided path is invalid: you likely need to provide a different argument to --fs.readwrite.\n" - .. "You may need to pass in `--fs.readwrite=$PWD`" - ) - ) - end -end +-- ROBLOX deviation: moved to RobloxShared +local ensureDirectoryExists = RobloxShared.ensureDirectoryExists function normalizeNewLines(string_: string) string_ = string.gsub(string_, "\r\n", "\n") @@ -282,9 +259,6 @@ local function saveSnapshotFile(snapshotData: SnapshotData, snapshotPath: Config end table.insert(snapshots, "return exports") - if FileSystemService == nil then - FileSystemService = getFileSystemService() or false - end -- ROBLOX deviation: error when FileSystemService doesn't exist if not FileSystemService then error(Error("Attempting to save snapshots in an environment where FileSystemService is inaccessible.")) @@ -341,27 +315,6 @@ function deepMerge(target: any, source: any): any return target end --- ROBLOX deviation: added to handle file paths in snapshot/State -local function robloxGetParent(path: string, level_: number?): string - local level = if level_ then level_ else 0 - - local isUnixPath = string.sub(path, 1, 1) == "/" - local t = {} - - for p in string.gmatch(path, "[^\\/][^\\/]*") do - table.insert(t, p) - end - if level > 0 then - t = { table.unpack(t, 1, #t - level) } - end - - if isUnixPath then - return "/" .. table.concat(t, "/") - end - - return table.concat(t, "\\") -end - return { testNameToKey = testNameToKey, keyToTestName = keyToTestName, @@ -374,6 +327,4 @@ return { escapeBacktickString = escapeBacktickString, saveSnapshotFile = saveSnapshotFile, deepMerge = deepMerge, - -- ROBLOX deviation: not in upstream - robloxGetParent = robloxGetParent, } diff --git a/src/jest-test-result/README.md b/src/jest-test-result/README.md index 0324cf9f..66e5dfce 100644 --- a/src/jest-test-result/README.md +++ b/src/jest-test-result/README.md @@ -1,19 +1,7 @@ # jest-test-result -Status: :heavy_check_mark: Ported - -Source: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-test-result - -Version: v27.4.7 +Upstream: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-test-result --- ### :pencil2: Notes - -### :x: Excluded -``` -``` - -### :package: [Dependencies](https://github.com/facebook/jest/blob/v27.4.7/packages/jest-test-result/package.json) -| Package | Version | Status | Notes | -| ------------------ | ------- | ------------------------- | ------------------------------------------------ | diff --git a/src/jest-types/README.md b/src/jest-types/README.md index 1daa4248..9e869ae6 100644 --- a/src/jest-types/README.md +++ b/src/jest-types/README.md @@ -1,21 +1,7 @@ # jest-types -Status: :hammer: In Progress - -Source: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-types - -Version: v27.4.7 +Upstream: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-types --- ### :pencil2: Notes - -### :x: Excluded -``` -__typechecks__/* -``` - -### :package: [Dependencies](https://github.com/facebook/jest/blob/v27.4.7/packages/jest-types/package.json) -| Package | Version | Status | Notes | -| ---------- | ------- | ----------------- | ---------------------------------------- | -| `@mlh-tsd` | 4.2.4 | :x: Will not port | Uses TypeScript compiler to assert types | diff --git a/src/jest-util/README.md b/src/jest-util/README.md index 59fb5844..ea0c1456 100644 --- a/src/jest-util/README.md +++ b/src/jest-util/README.md @@ -1,26 +1,7 @@ # jest-util -Status: :hammer: In Progress - -Source: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-util - -Version: v27.4.7 +Upstream: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-util --- ### :pencil2: Notes - -### :x: Excluded - - -### :package: [Dependencies](https://github.com/facebook/jest/blob/v27.4.7/packages/jest-util/package.json) - -| Package | Version | Status | Notes | -|---------------|---------|---------------------------|-------| -| `@jest/types` | 27.4.2 | :heavy_check_mark: Ported | | -| `@types/node` | * | :x: Will not port | | -| `chalk` | 4.0.0 | :heavy_check_mark: Ported | | -| `ci-info` | 3.2.0 | :x: Will not port | | -| `graceful-fs` | 4.2.4 | :hammer: In Progress | | -| `picomatch` | 2.2.3 | :hammer: In Progress | | - diff --git a/src/jest-util/src/getFileSystemService.lua b/src/jest-util/src/getFileSystemService.lua index dda482ff..cb06df81 100644 --- a/src/jest-util/src/getFileSystemService.lua +++ b/src/jest-util/src/getFileSystemService.lua @@ -17,9 +17,12 @@ local LuauPolyfill = require("@pkg/@jsdotlua/luau-polyfill") local Error = LuauPolyfill.Error +local RobloxShared = require("@pkg/@jsdotlua/jest-roblox-shared") +local getDataModelService = RobloxShared.getDataModelService + local function getFileSystemService() local success, result = pcall(function() - return _G.__MOCK_FILE_SYSTEM__ or game:GetService("FileSystemService") + return _G.__MOCK_FILE_SYSTEM__ or getDataModelService("FileSystemService") end) if not success then diff --git a/src/jest-validate/README.md b/src/jest-validate/README.md index eb9f8a04..31d656fd 100644 --- a/src/jest-validate/README.md +++ b/src/jest-validate/README.md @@ -1,21 +1,8 @@ # jest-validate -Status: :hammer: In Progress - -Source: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-validate - -Version: v27.4.7 +Upstream: https://github.com/facebook/jest/tree/v27.4.7/packages/jest-validate --- ### :pencil2: Notes -### :x: Excluded - - -### :package: [Dependencies](https://github.com/facebook/jest/blob/v27.4.7/packages/jest-validate/package.json) - -| Package | Version | Status | Notes | -| ------- | ------- | ------ | ----- | - - diff --git a/src/jest/README.md b/src/jest/README.md index f805e9b6..f79d3e67 100644 --- a/src/jest/README.md +++ b/src/jest/README.md @@ -1,19 +1,9 @@ # jest -Status: :hammer: In Progress +Upstream: https://github.com/facebook/jest/tree/v27.4.7/packages/jest -Source: https://github.com/facebook/jest/tree/v27.4.7/packages/jest - -Version: v27.4.7 +This package exports the `Jest` object used in Jest. The main entrypoint to the test framework should be `JestGlobals`. You can find its documentation in the [Jest documentation](https://roblox.github.io/jest-roblox-internal). --- ### :pencil2: Notes - -### :x: Excluded -``` -``` - -### :package: [Dependencies](https://github.com/facebook/jest/blob/v27.4.7/packages/jest/package.json) -| Package | Version | Status | Notes | -| ------- | ------- | ------ | ----- | diff --git a/src/pretty-format/README.md b/src/pretty-format/README.md index 85ca4dd0..9ec0a8ec 100644 --- a/src/pretty-format/README.md +++ b/src/pretty-format/README.md @@ -1,10 +1,10 @@ # pretty-format -Status: :hammer: In Progress +Upstream: https://github.com/facebook/jest/tree/v27.4.7/packages/pretty-format -Source: https://github.com/facebook/jest/tree/v27.4.7/packages/pretty-format - -Version: v27.4.7 +Stringify any Luau value +* Supports Luau builtins and Roblox Instances. +* Can be extended with user defined plugins. --- @@ -20,17 +20,3 @@ Version: v27.4.7 * `getConfig` is rewritten to avoid ternary operators. loop is rewritten with a `for` loop instead of an `iterator.next()`. * `Collections.lua` deviates from upstream substantially since Lua only has tables. We only have two functions: `printTableEntries` for formatting key, value pairs and `printListItems` for formatting arrays. - -### :x: Excluded -``` -perf -src/plugins/ConvertAnsi.ts -src/__tests__/ConvertAnsi.test.ts -``` - -### :package: [Dependencies](https://github.com/facebook/jest/blob/v27.4.7/packages/pretty-format/package.json) -| Package | Version | Status | Notes | -| ------------- | ------------------- | ------------------------- | ---------------------------------------- | -| `ansi-regex` | 5.0.1 | :x: Will not port | Console output styling is not a priority | -| `ansi-styles` | 5.0.0 | :x: Will not port | See above | -| `react-is` | see roact-alignment | :heavy_check_mark: Ported | Imported from roact-alignment | diff --git a/src/pretty-format/src/plugins/RobloxInstance.lua b/src/pretty-format/src/plugins/RobloxInstance.lua index dad810a9..d90b96de 100644 --- a/src/pretty-format/src/plugins/RobloxInstance.lua +++ b/src/pretty-format/src/plugins/RobloxInstance.lua @@ -20,12 +20,11 @@ local JestGetType = require("@pkg/@jsdotlua/jest-get-type") local getType = JestGetType.getType local LuauPolyfill = require("@pkg/@jsdotlua/luau-polyfill") +local Object = LuauPolyfill.Object local Array = LuauPolyfill.Array local instanceof = LuauPolyfill.instanceof local RobloxInstance = require("@pkg/@jsdotlua/jest-roblox-shared").RobloxInstance -local getRobloxProperties = RobloxInstance.getRobloxProperties -local getRobloxDefaults = RobloxInstance.getRobloxDefaults local InstanceSubset = RobloxInstance.InstanceSubset local printTableEntries = require("../Collections").printTableEntries @@ -45,44 +44,44 @@ local function printInstance( ): string local result = "" - local children = val:GetChildren() - table.sort(children, function(a, b) + local printChildrenList = val:GetChildren() + table.sort(printChildrenList, function(a, b) return a.Name < b.Name end) - local props - local defaults - if config.printInstanceDefaults then - props = getRobloxProperties(val.ClassName) - else - defaults, props = getRobloxDefaults(val.ClassName) - end + local propertiesMap = RobloxInstance.listProps(val) + local printPropsList = Object.keys(propertiesMap) if not config.printInstanceDefaults then - props = Array.filter(props, function(propertyName) - return defaults[propertyName] ~= val[propertyName] + local defaultsMap = RobloxInstance.listDefaultProps(val.ClassName) + printPropsList = Array.filter(printPropsList, function(name) + return propertiesMap[name] ~= defaultsMap[name] end) end + table.sort(printPropsList) + + local willPrintProps = #printPropsList > 0 + local willPrintChildren = #printChildrenList > 0 - if #props > 0 or #children > 0 then + if willPrintProps or willPrintChildren then result = result .. config.spacingOuter local indentationNext = indentation .. config.indent -- print properties of Instance - for i, propertyName in ipairs(props) do - local name = printer(propertyName, config, indentationNext, depth, refs) - local value = val[propertyName] + for propOrder, propName in ipairs(printPropsList) do + local propValue = propertiesMap[propName] + if propValue == Object.None then + propValue = nil + end -- collapses output for Instance values to avoid loops - if getType(value) == "Instance" then - value = printer(value, config, indentationNext, math.huge, refs) - else - value = printer(value, config, indentationNext, depth, refs) - end + local valueDepth = if getType(propValue) == "Instance" then math.huge else depth + local printName = printer(propName, config, indentationNext, depth, refs) + local printValue = printer(propValue, config, indentationNext, valueDepth, refs) - result = string.format("%s%s%s: %s", result, indentationNext, name, value) + result = string.format("%s%s%s: %s", result, indentationNext, printName, printValue) - if i < #props or #children > 0 then + if propOrder ~= #printPropsList or willPrintChildren then result = result .. "," .. config.spacingInner elseif not config.min then result = result .. "," @@ -90,13 +89,13 @@ local function printInstance( end -- recursively print children of Instance - for i, v in ipairs(children) do - local name = printer(v.Name, config, indentationNext, depth, refs) - local value = printer(v, config, indentationNext, depth, refs) + for childOrder, child in ipairs(printChildrenList) do + local printName = printer(child.Name, config, indentationNext, depth, refs) + local printValue = printer(child, config, indentationNext, depth, refs) - result = string.format("%s%s%s: %s", result, indentationNext, name, value) + result = string.format("%s%s%s: %s", result, indentationNext, printName, printValue) - if i < #children then + if childOrder ~= #printChildrenList then result = result .. "," .. config.spacingInner elseif not config.min then result = result .. "," diff --git a/src/test-utils/README.md b/src/test-utils/README.md index 0eb1370c..665ca221 100644 --- a/src/test-utils/README.md +++ b/src/test-utils/README.md @@ -1,19 +1,7 @@ # test-utils -Status: :hammer: In Progress - -Source: - -Version: +Upstream: https://github.com/jestjs/jest/tree/v28.0.0/packages/test-utils --- ### :pencil2: Notes - -### :x: Excluded -``` -``` - -### :package: [Dependencies]() -| Package | Version | Status | Notes | -| ------- | ------- | ------ | ----- | diff --git a/src/throat/README.md b/src/throat/README.md index 09b05b17..7141dc3c 100644 --- a/src/throat/README.md +++ b/src/throat/README.md @@ -1,19 +1,7 @@ # throat -Status: :heavy_check_mark: Ported - -Source: https://github.com/ForbesLindesay/throat/tree/6.0.1 - -Version: 6.0.1 +Upstream: https://github.com/ForbesLindesay/throat/tree/6.0.1 --- ### :pencil2: Notes - -### :x: Excluded -``` -``` - -### :package: [Dependencies]() -| Package | Version | Status | Notes | -| ------- | ------- | ------ | ----- |