* Move renderer `act` to work loop
* Delete `flushSuspenseFallbacksInTests`
This was meant to be a temporary hack to unblock the `act` work, but it
quickly spread throughout our tests.
What it's meant to do is force fallbacks to flush inside `act` even in
Concurrent Mode. It does this by wrapping the `setTimeout` call in a
check to see if it's in an `act` context. If so, it skips the delay and
immediately commits the fallback.
Really this is only meant for our internal React tests that need to
incrementally render. Nobody outside our team (and Relay) needs to do
that, yet. Even if/when we do support that, it may or may not be with
the same `flushAndYield` pattern we use internally.
However, even for our internal purposes, the behavior isn't right
because a really common reason we flush work incrementally is to make
assertions on the "suspended" state, before the fallback has committed.
There's no way to do that from inside `act` with the behavior of this
flag, because it causes the fallback to immediately commit. This has led
us to *not* use `act` in a lot of our tests, or to write code that
doesn't match what would actually happen in a real environment.
What we really want is for the fallbacks to be flushed at the *end` of
the `act` scope. Not within it.
This only affects the noop and test renderer versions of `act`, which
are implemented inside the reconciler. Whereas `ReactTestUtils.act` is
implemented in "userspace" for backwards compatibility. This is fine
because we didn't have any DOM Suspense tests that relied on this flag;
they all use test renderer or noop.
In the future, we'll probably want to move always use the reconciler
implementation of `act`. It will not affect the prod bundle, because we
currently only plan to support `act` in dev. Though we still haven't
completely figured that out. However, regardless of whether we support a
production `act` for users, we'll still need to write internal React
tests in production mode. For that use case, we'll likely add our own
internal version of `act` that assumes a mock Scheduler and might rely
on hacks that don't 100% align up with the public one.
I made a mistake when setting these up a while ago. Setting the NODE_ENV
in the CircleCI config doesn't work because it's also set in the node
script command.
The number of test commands is getting out of control. Might need to fix
it at some point. Not today for me.
* Migrate conditional tests to gate pragma
I searched through the codebase for this pattern:
```js
describe('test suite', () => {
if (!__EXPERIMENTAL__) { // or some other condition
test("empty test so Jest doesn't complain", () => {});
return;
}
// Unless we're in experimental mode, none of the tests in this block
// will run.
})
```
and converted them to the `@gate` pragma instead.
The reason this pattern isn't preferred is because you end up disabling
more tests than you need to.
* Add flag for www release channels
Using a heuristic where I check a flag that is known to only be enabled
in www. I left a TODO to instead set the release channel explicitly in
each test config.
* Add pragma for feature testing: @gate
The `@gate` pragma declares under which conditions a test is expected to
pass.
If the gate condition passes, then the test runs normally (same as if
there were no pragma).
If the conditional fails, then the test runs and is *expected to fail*.
An alternative to `it.experimental` and similar proposals.
Examples
--------
Basic:
```js
// @gate enableBlocksAPI
test('passes only if Blocks API is available', () => {/*...*/})
```
Negation:
```js
// @gate !disableLegacyContext
test('depends on a deprecated feature', () => {/*...*/})
```
Multiple flags:
```js
// @gate enableNewReconciler
// @gate experimental
test('needs both useEvent and Blocks', () => {/*...*/})
```
Logical operators (yes, I'm sorry):
```js
// @gate experimental && (enableNewReconciler || disableSchedulerTimeoutBasedOnReactExpirationTime)
test('concurrent mode, doesn\'t work in old fork unless Scheduler timeout flag is disabled', () => {/*...*/})
```
Strings, and comparion operators
No use case yet but I figure eventually we'd use this to gate on
different release channels:
```js
// @gate channel === "experimental" || channel === "modern"
test('works in OSS experimental or www modern', () => {/*...*/})
```
How does it work?
I'm guessing those last two examples might be controversial. Supporting
those cases did require implementing a mini-parser.
The output of the transform is very straightforward, though.
Input:
```js
// @gate a && (b || c)
test('some test', () => {/*...*/})
```
Output:
```js
_test_gate(ctx => ctx.a && (ctx.b || ctx.c, 'some test'), () => {/*...*/});
```
It also works with `it`, `it.only`, and `fit`. It leaves `it.skip` and
`xit` alone because those tests are disabled anyway.
`_test_gate` is a global method that I set up in our Jest config. It
works about the same as the existing `it.experimental` helper.
The context (`ctx`) argument is whatever we want it to be. I set it up
so that it throws if you try to access a flag that doesn't exist. I also
added some shortcuts for common gating conditions, like `old`
and `new`:
```js
// @gate experimental
test('experimental feature', () => {/*...*/})
// @gate new
test('only passes in new reconciler', () => {/*...*/})
```
Why implement this as a pragma instead of a runtime API?
- Doesn't require monkey patching built-in Jest methods. Instead it
compiles to a runtime function that composes Jest's API.
- Will be easy to upgrade if Jest ever overhauls their API or we switch
to a different testing framework (unlikely but who knows).
- It feels lightweight so hopefully people won't feel gross using it.
For example, adding or removing a gate pragma will never affect the
indentation of the test, unlike if you wrapped the test in a
conditional block.
* Compatibility with console error/warning tracking
We patch console.error and console.warning to track unexpected calls
in our tests. If there's an unexpected call, we usually throw inside
an `afterEach` hook. However, that's too late for tests that we
expect to fail, because our `_test_gate` runtime can't capture the
error. So I also check for unexpected calls inside `_test_gate`.
* Move test flags to dedicated file
Added some instructions for how the flags are set up and how to
use them.
* Add dynamic version of gate API
Receives same flags as the pragma.
If we ever decide to revert the pragma, we can codemod them to use
this instead.
* Add more edge cases to fixture
Also adjust some expectations. I think the column should ideally be 1 but varies.
The Example row is one line off because it throws on the hook but should ideally be the component.
Similarly class components with constructors may have the line in the constructor.
* Account for the construct call taking a stack frame
We do this by first searching for the first different frame, then find
the same frames and then find the first different frame again.
* Throw controls
Otherwise they don't get a stack frame associated with them in IE.
* Protect against generating stacks failing
Errors while generating stacks will bubble to the root. Since this technique
is a bit sketchy, we should probably protect against it.
* Don't construct the thing that throws
Instead, we pass the prototype as the "this". It's new every time anyway.
* Implement component stack extraction hack
* Normalize errors in tests
This drops the requirement to include owner to pass the test.
* Special case tests
* Add destructuring to force toObject which throws before the side-effects
This ensures that we don't double call yieldValue or advanceTime in tests.
Ideally we could use empty destructuring but ES lint doesn't like it.
* Cache the result in DEV
In DEV it's somewhat likely that we'll see many logs that add component
stacks. This could be slow so we cache the results of previous components.
* Fixture
* Add Reflect to lint
* Log if out of range.
* Fix special case when the function call throws in V8
In V8 we need to ignore the first line. Normally we would never get there
because the stacks would differ before that, but the stacks are the same if
we end up throwing at the same place as the control.
Modules that belong to one fork should not import modules that belong to
the other fork.
Helps make sure you correctly update imports when syncing changes across
implementations.
Also could help protect against code size regressions that might happen
if one of the forks accidentally depends on two copies of the same
module.
* Remove unnecessary workInProgress line
* Mutate workInProgress instead of returning
We were ambivalent about this before.
* Make handleError a void method too
We currently use the expiration time to represent the timeout of a
transition. Since we intend to stop treating work priority as a
timeline, we can no longer use this trick.
In this commit, I've changed it to store the event time on the update
object instead. Long term, we will store event time on the root as a map
of transition -> event time. I'm only storing it on the update object
as a temporary workaround to unblock the rest of the changes.
We can't patch the row. We could give these their own "built-in" stack
frame since they're conceptually HoCs. However, from a debugging
perspective this is not very useful meta data and quite noisy. So I'm
just going to exclude them.
Adds command `yarn merge-fork`.
```sh
yarn merge-fork --base-dir=packages/react-reconciler/src ReactFiberWorkLoop
```
This will take all the changes in `ReactFiberWorkLoop.new.js` and apply
them to `ReactFiberWorkLoop.old.js`.
You can merge multiple modules at a time:
```sh
yarn merge-fork \
--base-dir=packages/react-reconciler/src \
ReactFiberWorkLoop \
ReactFiberBeginWork \
ReactFiberCompleteWork \
ReactFiberCommitWork
```
You can provide explicit "old" and "new" file names. This only works
for one module at a time:
```sh
yarn merge-fork \
--base-dir=packages/react-reconciler/src \
--old=ReactFiberExpirationTime.js \
--new=ReactFiberLane.js
```
The default is to merge changes from the new module to the old one. To
merge changes in the opposite direction, use `--reverse`.
```sh
yarn merge-fork \
--reverse \
--base-dir=packages/react-reconciler/src \
ReactFiberWorkLoop
```
By default, the changes are compared to HEAD. You can use `--base-ref`
to compare to any rev. For example, while working on a PR, you might
make multiple commits to the new fork before you're ready to backport
them to the old one. In that case, you want to compare to the merge
base of your PR branch:
```sh
yarn merge-fork \
--base-ref=$(git merge-base HEAD origin/master)
--base-dir=packages/react-reconciler/src \
ReactFiberWorkLoop
```
All changes in this commit were generated by the following commands.
Copy each module that ends with `.old` to a new file that ends
with `.new`:
```sh
for f in packages/react-reconciler/src/*.old.js; do cp "$f" "$(echo "$f" | sed s/\.old/\.new/)"; done
```
Then transform the internal imports:
```sh
grep -rl --include="*.new.js" '.old' packages/react-reconciler/src/| xargs sed -i '' "s/\.old\'/\.new\'/g"
```
Some of our internal reconciler types have leaked into other packages.
Usually, these types are treated as opaque; we don't read and write
to its fields. This is good.
However, the type is often passed back to a reconciler method. For
example, React DOM creates a FiberRoot with `createContainer`, then
passes that root to `updateContainer`. It doesn't do anything with the
root except pass it through, but because `updateContainer` expects a
full FiberRoot, React DOM is still coupled to all its fields.
I don't know if there's an idiomatic way to handle this in Flow. Opaque
types are simlar, but those only work within a single file. AFAIK,
there's no way to use a package as the boundary for opaqueness.
The immediate problem this presents is that the reconciler refactor will
involve changes to our internal data structures. I don't want to have to
fork every single package that happens to pass through a Fiber or
FiberRoot, or access any one of its fields. So my current plan is to
share the same Flow type across both forks. The shared type will be a
superset of each implementation's type, e.g. Fiber will have both an
`expirationTime` field and a `lanes` field. The implementations will
diverge, but not the types.
To do this, I lifted the type definitions into a separate module.