mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
* 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.
110 lines
3.3 KiB
JavaScript
110 lines
3.3 KiB
JavaScript
'use strict';
|
|
|
|
const path = require('path');
|
|
|
|
const babel = require('@babel/core');
|
|
const coffee = require('coffee-script');
|
|
|
|
const tsPreprocessor = require('./typescript/preprocessor');
|
|
const createCacheKeyFunction = require('fbjs-scripts/jest/createCacheKeyFunction');
|
|
|
|
const pathToBabel = path.join(
|
|
require.resolve('@babel/core'),
|
|
'../..',
|
|
'package.json'
|
|
);
|
|
const pathToBabelPluginDevWithCode = require.resolve(
|
|
'../error-codes/transform-error-messages'
|
|
);
|
|
const pathToBabelPluginReplaceConsoleCalls = require.resolve(
|
|
'../babel/transform-replace-console-calls'
|
|
);
|
|
const pathToBabelPluginAsyncToGenerator = require.resolve(
|
|
'@babel/plugin-transform-async-to-generator'
|
|
);
|
|
const pathToTransformInfiniteLoops = require.resolve(
|
|
'../babel/transform-prevent-infinite-loops'
|
|
);
|
|
const pathToTransformTestGatePragma = require.resolve(
|
|
'../babel/transform-test-gate-pragma'
|
|
);
|
|
const pathToBabelrc = path.join(__dirname, '..', '..', 'babel.config.js');
|
|
const pathToErrorCodes = require.resolve('../error-codes/codes.json');
|
|
|
|
const babelOptions = {
|
|
plugins: [
|
|
// For Node environment only. For builds, Rollup takes care of ESM.
|
|
require.resolve('@babel/plugin-transform-modules-commonjs'),
|
|
|
|
pathToBabelPluginDevWithCode,
|
|
|
|
// Keep stacks detailed in tests.
|
|
// Don't put this in .babelrc so that we don't embed filenames
|
|
// into ReactART builds that include JSX.
|
|
// TODO: I have not verified that this actually works.
|
|
require.resolve('@babel/plugin-transform-react-jsx-source'),
|
|
|
|
pathToTransformInfiniteLoops,
|
|
pathToTransformTestGatePragma,
|
|
|
|
// This optimization is important for extremely performance-sensitive (e.g. React source).
|
|
// It's okay to disable it for tests.
|
|
[
|
|
require.resolve('@babel/plugin-transform-block-scoping'),
|
|
{throwIfClosureRequired: false},
|
|
],
|
|
],
|
|
retainLines: true,
|
|
};
|
|
|
|
module.exports = {
|
|
process: function(src, filePath) {
|
|
if (filePath.match(/\.coffee$/)) {
|
|
return coffee.compile(src, {bare: true});
|
|
}
|
|
if (filePath.match(/\.ts$/) && !filePath.match(/\.d\.ts$/)) {
|
|
return tsPreprocessor.compile(src, filePath);
|
|
}
|
|
if (filePath.match(/\.json$/)) {
|
|
return src;
|
|
}
|
|
if (!filePath.match(/\/third_party\//)) {
|
|
// for test files, we also apply the async-await transform, but we want to
|
|
// make sure we don't accidentally apply that transform to product code.
|
|
const isTestFile = !!filePath.match(/\/__tests__\//);
|
|
const isInDevToolsPackages = !!filePath.match(
|
|
/\/packages\/react-devtools.*\//
|
|
);
|
|
const testOnlyPlugins = [pathToBabelPluginAsyncToGenerator];
|
|
const sourceOnlyPlugins = [];
|
|
if (process.env.NODE_ENV === 'development' && !isInDevToolsPackages) {
|
|
sourceOnlyPlugins.push(pathToBabelPluginReplaceConsoleCalls);
|
|
}
|
|
const plugins = (isTestFile ? testOnlyPlugins : sourceOnlyPlugins).concat(
|
|
babelOptions.plugins
|
|
);
|
|
return babel.transform(
|
|
src,
|
|
Object.assign(
|
|
{filename: path.relative(process.cwd(), filePath)},
|
|
babelOptions,
|
|
{
|
|
plugins,
|
|
}
|
|
)
|
|
);
|
|
}
|
|
return src;
|
|
},
|
|
|
|
getCacheKey: createCacheKeyFunction([
|
|
__filename,
|
|
pathToBabel,
|
|
pathToBabelrc,
|
|
pathToBabelPluginDevWithCode,
|
|
pathToTransformInfiniteLoops,
|
|
pathToTransformTestGatePragma,
|
|
pathToErrorCodes,
|
|
]),
|
|
};
|