Skip to content

Commit

Permalink
Merge pull request #2 from MisterUncloaked/shallow-wrapper
Browse files Browse the repository at this point in the history
Shallow Rendering Documentation Edits
  • Loading branch information
jeparlefrancais authored Aug 14, 2019
2 parents 4bb8234 + 6d677c3 commit 89b3a8e
Showing 1 changed file with 42 additions and 49 deletions.
91 changes: 42 additions & 49 deletions docs/advanced/shallow-rendering.md
Original file line number Diff line number Diff line change
@@ -1,58 +1,50 @@
Shallow rendering is when you mount only a certain part an element tree. Technically, Roact does not provide the ability to shallow render a tree yet, but you can obtain a ShallowWrapper object that will mimic how shallow rendering works.
To facilitate writing robust unit tests when using Roact, utilities are provided for shallow rendering and snapshotting components. Shallow rendering involves rendering 1 or more levels deep to assert facts about the rendered elements, allowing components to be tested as units. Snaphotting involves converting these shallow renders to serializable Lua modules for debugging purposes or to verify that components have not changed unexpectedly.

## ShallowWrapper
## Shallow Wrapper

When writing tests for Roact components, you can mount your component using `Roact.mount` and then retrieve a ShallowWrapper object from the returned `VirtualTree`.
When writing tests for Roact components, the `VirtualTree` returned by `Roact.mount` can provide a `ShallowWrapper` object with an interface designed to help make assertions about the expected behavior of a component.

```lua
-- let's assume there is a ComponentToTest that we want to test

local virtualTree = Roact.mount(ComponentToTest)
local tree = Roact.mount(ComponentToTest)

local shallowWrapper = tree:getShallowWrapper()
```

The ShallowWrapper is meant to provide an interface to help make assertions about behavior you expect from your component.
### TODO: Testing Shallows Using Find
We should have a section on how and when to use find/findUnique

---

## Snapshot Tests
## Snapshots

### What are Snapshots
A snapshot is a serializable representation of a shallow rendered Roact tree that can be serialized as a complete Lua `ModuleScript`. The snapshot can either be compared with a past snapshot written to `ReplicatedStorage` or returned as a string for debugging purposes.

Snapshots are files that contains serialized data. In Roact's case, snapshots of ShallowWrapper objects can be generated. More specifically, the data contained from a ShallowWrapper is converted to Lua code, which is then saved into a ModuleScript.

!!! Note
Currently, the generated snapshot will be stored in a StringValue. Often, the permission level where the test are ran does not make it possible to create a ModuleScript and assign it's Source property. For now, we rely on other tools like Roact's SnapshotsPlugin to copy the generated StringValue from Run mode to ModuleScript in Edit mode.
### Snapshot Testing

During the tests execution, these snapshots are used to verify that they do not change through time.

!!! Note
For those using [luacheck](https://github.com/mpeterv/luacheck/) to analyse their Lua files, make sure to run the tool on the generated files. The format of the generated snapshots will probably fail luacheck (often just because of unused variables). There are no advantage to have these files match a specific format.

---

### What are Snapshot Tests

The goal of snapshot tests is to make sure that the current serialized version of a snapshot matches the new generated one. This can be done through the `matchSnapshot` method on the ShallowWrapper. The string passed to the method will be to find the previous snapshot.
The goal of a snapshot test is to verify that the shallow render matches the version saved previously. If a snapshot does not match, then the component may have changed unexpectedly. Snapshot testing can be done through the `matchSnapshot` method on the `ShallowWrapper`. The single argument of `matchSnapshot` is the name of the `ModuleScript` in `ReplicatedStorage` to look for.

```lua
shallowWrapper:matchSnapshot("ComponentToTest")
```

Here is a break down of what happen behind this method.
Here is a breakdown of how matching is performed:

1. Check if there is an existing snapshot with the given name.
2. If no snapshot exists, generate a new one from the ShallowWrapper and exit, otherwise continue
3. Require the ModuleScript (the snapshot) to obtain the table containing the data
4. Compare the loaded data with the generated data from the ShallowWrapper
5. Throw an error if the data is different from the loaded one and generate a new ModuleScript that contains the new generated snapshot (useful for comparison)
1. Check if there is an existing snapshot with the given name
2. If no snapshot exists, generate a new one from the `ShallowWrapper` and exit, else continue
3. Load the existing snapshot by calling require on the `ModuleScript`
4. Compare the existing snapshot with the one generated by the `ShallowWrapper`
5. If the snapshots match, exit, else save the new `ModuleScript` (useful for comparison) and throw an error

!!! Note
Currently, the `ModuleScript` for new snapshots will be stored in a `StringValue`. The permission level where the test are ran does not make it possible to create a `ModuleScript` and assign its `Source` property. Tools like Roact's SnapshotsPlugin can copy the `StringValue` from Run mode to a `ModuleScript` in Edit mode.

!!! Note
The scripts generated by `matchSnapshot` are not guaranteed to pass [luacheck](https://github.com/mpeterv/luacheck/) due to the potential for unused variables, so be careful not to run `luacheck` on them.
---

#### Workflow Example

For a concrete example, suppose the following component (probably in a script named `ComponentToTest`).
For a concrete example, suppose the following component `ComponentToTest`.

```lua
local function ComponentToTest(props)
Expand All @@ -62,7 +54,7 @@ local function ComponentToTest(props)
end
```

A snapshot test could be written this way (probably in a script named `ComponentToTest.spec`).
A snapshot test could be written this way:

```lua
it("should match the snapshot", function()
Expand All @@ -74,7 +66,7 @@ it("should match the snapshot", function()
end)
```

After the first run, the test will have created a new script under `RoactSnapshots` called `ComponentToTest` that contains the following Lua code.
After the first run, the test will have created a new script under `RoactSnapshots` in `ReplicatedStorage` called `ComponentToTest` that contains the following Lua code:

```lua
return function(dependencies)
Expand All @@ -96,9 +88,9 @@ return function(dependencies)
end
```

Since these tests require the previous snapshots to compare with the current generated one, snapshots need to be committed to the version control software used for development. So the new component, the test and the generated snapshot would be commit and ready for review. The reviewer(s) will be able to review your snapshot as part of the normal review process.
Since these tests require the previous snapshots to compare with the current generated one, snapshots should be saved (if using Studio) or committed to version control (if using file system development).

Suppose now ComponentToTest needs a change. We update it to the following snippet.
Suppose now `ComponentToTest` is updated as follows:

```lua
local function ComponentToTest(props)
Expand All @@ -108,7 +100,7 @@ local function ComponentToTest(props)
end
```

When we run back the previous test, it will fail and the message is going to tell us that the snapshots did not match. There will be a new script under `RoactSnapshots` called `ComponentToTest.NEW` that shows the new version of the snapshot.
When the test is run again, it will fail, noting that the snapshots did not match. There will be a new script under `RoactSnapshots` called `ComponentToTest.NEW` that shows the new version of the snapshot.

```lua
return function(dependencies)
Expand All @@ -130,11 +122,11 @@ return function(dependencies)
end
```

Since this example is trivial, it is easy to diff with human eyes and see that only the `Text` prop value changed from *foo* to *bar*. Since these changes are expected from the modification made to the component, we can delete the old snapshot and remove the `.NEW` from the newest one. If the tests are run again, they should all pass now.
Only the `Text` prop value changed, from *foo* to *bar*. Since these changes are expected from the modification made to the component, we can delete the old snapshot and remove the `.NEW` from the newest one. If the tests are run again, they will once again pass.

Again, the updated snapshot will be committed to source control along with the component changes. That way, the reviewer will see exactly what changed in the snapshot, so they can make sure the changes are expected. But why go through all this process for such a trivial change?
Updated snapshots should be saved / committed along with the component changes to make it clear why the snapshot is being changed.

Well, in most project complexity arise soon and components start to have more behavior. To make sure that certain behavior is not lost with a change, snapshot tests can assert that a button has a certain state after being clicked or while hovered.
Most snapshots will be more complex than this example and act as a powerful line of defense against unexpected changes to components.

---

Expand All @@ -146,34 +138,35 @@ Snapshot tests really shine when comes the time to test for regression.

##### Carefully Reviewed

Changes made to a snapshot file needs to be reviewed carefully as if it was hand written code. A reviewer needs to be able to catch any unexpected changes to a component. Any source control software should provide some way to see a diff of the changes that are going to be submitted. If a snapshot diff shows a difference of color property for a change that is supposed to update the sizing, the reviewer should point it to the developer and make sure the issue is solved because accepting the changes.
Changes made to a snapshot file needs to be reviewed carefully as if it was hand written code. A reviewer needs to be able to catch any unexpected changes to a component. Any source control software should provide some way to see a diff of the changes that are going to be submitted. If a snapshot diff shows a difference in the color property for a change that is supposed to update sizing, the reviewer should verify that the change is intended.

---

#### Where They Are Bad

##### Large Snapshots

If a snapshot is created from a top level component with a ShallowWrapper that renders deeply, it can produce a really large snapshot file with lots of details. What is bad with this snapshot, is that everytime a child of this component will change, the snapshot will fail.
If a snapshot is created from a top level component with a ShallowWrapper that renders many levels deep, it can produce a large snapshot file with potentially hundreds of lines. The larger the snapshot, the more likely it is to fail due to a reason unrelated to the component being tested.

This snapshot test will soon become an inconvenience and developers will slowly stop caring about it. The snapsot will not be reviewed correctly, because developers will be used to see the snapshot update on every new change submitted.
Large snapshots also become an inconvenience and may not be reviewed correctly due to their size or frequency of needing an update, or both.

To avoid this situation, it is truly important that each snapshot is kept as simple and as small as possible. That is why the ShallowWrapper is deeply linked with the snapshot generation: it is needed to abstract the children of a component instead of making a snapshot that contains the whole tree.
To avoid this situation, it is important that each snapshot is kept as simple and as small as possible. That is why the `ShallowWrapper` is deeply linked with snapshot generation and defaults to rendering only 1 level deep. Render more deeply only as needed to exercise the component being tested.

---

## Managing Snapshots
#### Managing Snapshots

### Within Roblox Studio
When the tests are executed in Run mode (after Run is pressed), snapshots are serialized and saved as `StringValue` objects inside of `ReplicatedStorage.RoactSnapshots`. Pressing Stop to go back to Edit mode will delete any newly created snapshots values. Preserving the serialized snapshots and saving them as module scripts is necessary to ensure that there are snapshots to match with during future test runs. The method of preserving them varies based on the development environment being used.

When the tests are executed in Run mode (after Run is pressed), the snapshots are going to be created as StringValue objects inside a folder (ReplicatedStorage.RoactSnapshots). Then, pressing Stop to go back edit the place will delete all the new created snapshots values. In order to keep those values, a special plugin is needed to serialize the RoactSnapshots content into a plugin setting. Once the place go back in edit mode, the same plugin will detect the new content in the setting value and recreate the snapshots as ModuleScript objects.
##### Roblox Studio
If using Roblox Studio for development, install the `RoactSnapshots` plugin, which will preserve the `StringValue` objects and save them as `ModuleScript` objects upon returning to Edit mode.

---

### On the File System
##### File System

Some users might be using a tool like `rojo` to sync files to Roblox Studio. To work with snapshots, something will be needed to sync the generated files from Roblox Studio to the file system.
If using a tool like `rojo` to sync files to Roblox Studio, a tool like `run-in-roblox` can help write the module scripts back to the file system.

[`run-in-roblox`](https://github.com/LPGhatguy/run-in-roblox/) is [`Rust`](https://www.rust-lang.org/) project that allow Roblox to be run from the command line and sends the Roblox output content to the shell window. Using this tool, a script can be written to execute studio with a specific test runner that will print out the new snapshots in a special format. Then, the output can be parsed to find the new snapshots and write them to files.
[`run-in-roblox`](https://github.com/LPGhatguy/run-in-roblox/) is a [`Rust`](https://www.rust-lang.org/) command line tool that runs Roblox Studio and sends content from the output to the shell window. Using this tool, a script can be written to open a place file and run it with a specific test runner that can print out the new snapshots in a special format. Then, the output can be parsed to find the new snapshots and write them to files.

You can find these scripts written in Lua ([link](../scripts/sync-snapshots-with-lua.md)) or in python ([link](../scripts/sync-snapshots-with-python.md)) (compatible with version 2 and 3). These scripts will assume that you have the rojo and run-in-roblox commands available. They contain the same functionalities: it builds a place from a rojo configuration file, then it runs a specific script inside studio that should print the snapshots. The output is parsed to find the snapshots and write them.
Here are examples of this kind of script written in Lua ([link](../scripts/sync-snapshots-with-lua.md)) and in python ([link](../scripts/sync-snapshots-with-python.md)) (compatible with version 2 and 3). These example scripts assume that the `rojo` and `run-in-roblox` commands are available. They build a place from a `rojo` configuration file, run a specific script inside studio, print the serialized snapshots, parse them from the output, and write them to the file system.

0 comments on commit 89b3a8e

Please sign in to comment.