Match-id-5307a7597a01d34f236b49f9591642b74fe13a06

This commit is contained in:
* 2023-09-26 15:53:11 +08:00
parent dfffef41e2
commit e1ca5eb919
264 changed files with 108042 additions and 0 deletions

View File

@ -0,0 +1,18 @@
# https://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
max_line_length = 80
trim_trailing_whitespace = true
[*.md]
max_line_length = 0
trim_trailing_whitespace = false
[COMMIT_EDITMSG]
max_line_length = 0

9
packages/inula-test/.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
node_modules
.idea
.vscode
.run
path.json
package-lock.json
coverage
packages/libs

View File

@ -0,0 +1,6 @@
chromedriver_cdnurl=http://mirrors.tools.huawei.com/chromedriver
no-proxy=.huawei.com
@cloudsop:registry=https://cmc.centralrepo.rnd.huawei.com/artifactory/api/npm/product_npm
registry=https://cmc.centralrepo.rnd.huawei.com/npm
electron_mirror=http://mirrors.tools.huawei.com/electron/
xprofiler_mirror=https://mirrors.tools.huawei.com/xprofiler/

View File

@ -0,0 +1,8 @@
packages/react-devtools-core/dist
packages/react-devtools-extensions/chrome/build
packages/react-devtools-extensions/firefox/build
packages/react-devtools-extensions/shared/build
packages/react-devtools-inline/dist
packages/react-devtools-shell/dist
packages/react-devtools-scheduling-profiler/dist
packages/react-devtools-scheduling-profiler/static

View File

@ -0,0 +1,21 @@
'use strict';
const {esNextPaths} = require('./scripts/shared/pathsByLanguageVersion');
module.exports = {
bracketSpacing: false,
singleQuote: true,
jsxBracketSameLine: true,
trailingComma: 'es5',
printWidth: 80,
parser: 'babel',
overrides: [
{
files: esNextPaths,
options: {
trailingComma: 'all',
},
},
],
};

View File

@ -0,0 +1,8 @@
# horizon-test
1. 大家下载这个工程放在和horizon同一层级的文件夹中
2. 修改一下path.json中的horizon-path路径
3. 执行npm i -f 或 yarn
4. 执行node link.js
5. horizon项目下执行 node link.js
6. npm run test

View File

@ -0,0 +1,39 @@
'use strict';
module.exports = {
presets: [
[
'@babel/preset-env',
{
"loose": true
}
],
'@babel/preset-react',
'@babel/preset-typescript',
],
plugins: [
'@babel/plugin-syntax-jsx',
'@babel/plugin-transform-react-jsx',
'@babel/plugin-transform-flow-strip-types',
['@babel/plugin-proposal-class-properties', { loose: true }],
'syntax-trailing-function-commas',
[
'@babel/plugin-proposal-object-rest-spread',
{ loose: true, useBuiltIns: true },
],
['@babel/plugin-transform-template-literals', { loose: true }],
'@babel/plugin-transform-literals',
'@babel/plugin-transform-arrow-functions',
'@babel/plugin-transform-block-scoped-functions',
'@babel/plugin-transform-object-super',
'@babel/plugin-transform-shorthand-properties',
'@babel/plugin-transform-computed-properties',
'@babel/plugin-transform-for-of',
['@babel/plugin-transform-spread', { loose: true, useBuiltIns: true }],
'@babel/plugin-transform-parameters',
['@babel/plugin-transform-destructuring', { loose: true, useBuiltIns: true }],
['@babel/plugin-transform-block-scoping', { throwIfClosureRequired: true }],
'@babel/plugin-transform-classes',
'@babel/plugin-transform-runtime'
],
};

View File

@ -0,0 +1,27 @@
const symlink = require('symlink-dir');
const fs = require('fs');
const path = require('path');
const pathJson = require('./path.json');
fs.readdirSync(path.join(__dirname, 'packages')).forEach(name => {
symlink('packages/' + name, 'node_modules/' + name)
.then(() => {
console.log('源码构建:为' + name + '创建node_modules链接');
})
.catch(err => {
console.log('Error:为' + name + '创建node_modules链接失败');
});
});
const libs = [
'inula',
];
libs.forEach((name) => {
symlink(`${pathJson['horizon-path']}/packages/` + name, 'node_modules/' + name)
.then(() => {
console.log('源码构建:为' + name + '创建node_modules链接...');
})
.catch(err => {
console.log('Error:为' + name + '创建node_modules链接失败');
});
});

View File

@ -0,0 +1,122 @@
{
"private": true,
"workspaces": [
"packages/*"
],
"devDependencies": {
"@babel/cli": "^7.10.5",
"@babel/code-frame": "^7.10.4",
"@babel/core": "^7.11.1",
"@babel/eslint-parser": "^7.11.4",
"@babel/helper-module-imports": "^7.10.4",
"@babel/parser": "^7.11.3",
"@babel/plugin-external-helpers": "^7.10.4",
"@babel/plugin-proposal-class-properties": "^7.14.5",
"@babel/plugin-proposal-object-rest-spread": "^7.11.0",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-jsx": "^7.10.4",
"@babel/plugin-transform-arrow-functions": "^7.10.4",
"@babel/plugin-transform-async-to-generator": "^7.10.4",
"@babel/plugin-transform-block-scoped-functions": "^7.10.4",
"@babel/plugin-transform-block-scoping": "^7.11.1",
"@babel/plugin-transform-classes": "^7.14.2",
"@babel/plugin-transform-computed-properties": "^7.10.4",
"@babel/plugin-transform-destructuring": "^7.10.4",
"@babel/plugin-transform-for-of": "^7.10.4",
"@babel/plugin-transform-literals": "^7.10.4",
"@babel/plugin-transform-modules-commonjs": "^7.10.4",
"@babel/plugin-transform-object-super": "^7.10.4",
"@babel/plugin-transform-parameters": "^7.10.5",
"@babel/plugin-transform-react-jsx-source": "^7.10.5",
"@babel/plugin-transform-runtime": "^7.14.5",
"@babel/plugin-transform-shorthand-properties": "^7.10.4",
"@babel/plugin-transform-spread": "^7.11.0",
"@babel/plugin-transform-template-literals": "^7.10.5",
"@babel/preset-env": "^7.14.7",
"@babel/preset-flow": "^7.10.4",
"@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "7.8.3",
"@babel/register": "^7.14.5",
"@babel/traverse": "^7.11.0",
"@mattiasbuelens/web-streams-polyfill": "^0.3.2",
"babel-eslint": "^10.0.3",
"babel-loader": "^8.2.2",
"babel-plugin-syntax-trailing-function-commas": "^6.5.0",
"chalk": "^3.0.0",
"clean-webpack-plugin": "^4.0.0-alpha.0",
"cli-table": "^0.3.1",
"coffee-script": "^1.12.7",
"concurrently": "^6.2.0",
"confusing-browser-globals": "^1.0.9",
"copy-webpack-plugin": "5.0.4",
"core-js": "^3.6.4",
"coveralls": "^3.0.9",
"create-react-class": "^15.6.3",
"cross-env": "^7.0.3",
"css-loader": "3.4.2",
"danger": "^9.2.10",
"error-stack-parser": "^2.0.6",
"eslint": "^7.7.0",
"eslint-config-fbjs": "^1.1.1",
"eslint-config-prettier": "^6.9.0",
"eslint-plugin-babel": "^5.3.0",
"eslint-plugin-flowtype": "^2.25.0",
"eslint-plugin-jest": "^22.15.0",
"eslint-plugin-no-for-of-loops": "^1.0.0",
"eslint-plugin-no-function-declare-after-return": "^1.0.0",
"eslint-plugin-react": "^6.7.1",
"express": "^4.17.1",
"fbjs-scripts": "1.2.0",
"flow-bin": "0.97",
"glob": "^7.1.6",
"glob-stream": "^6.1.0",
"gzip-size": "^5.1.1",
"html-webpack-plugin": "4.4.1",
"jasmine-check": "^1.0.0-rc.0",
"jest": "^25.2.7",
"jest-cli": "^25.2.7",
"jest-diff": "^25.2.6",
"jest-environment-jsdom-sixteen": "^1.0.3",
"jest-react": "0.12.0",
"jest-snapshot-serializer-raw": "^1.1.0",
"less": "3.11.1",
"less-loader": "5.0.0",
"minimist": "^1.2.3",
"ncp": "^2.0.0",
"object-assign": "^4.1.1",
"prettier": "1.19.1",
"prop-types": "^15.6.2",
"random-seed": "^0.3.0",
"react-is": "^17.0.2",
"react-lifecycles-compat": "^3.0.4",
"rimraf": "^3.0.0",
"semver": "^7.1.1",
"targz": "^1.0.1",
"through2": "^3.0.1",
"tmp": "^0.1.0",
"typescript": "^3.7.5",
"wait-on": "^6.0.0",
"yargs": "^15.3.1"
},
"devEngines": {
"node": "8.x || 9.x || 10.x || 11.x || 12.x || 13.x || 14.x"
},
"jest": {
"testRegex": "/scripts/jest/dont-run-jest-directly\\.js$"
},
"scripts": {
"debug-test": "yarn test --debug",
"test": "node ./scripts/jest/jest-cli.js",
"test-report": "node ./scripts/jest/jest-cli.js --coverage",
"postinstall": "node ./link.js",
"benchmark": "cd ./packages/benchmarks && yarn start"
},
"dependencies": {
"@elg/speedscope": "^1.9.0-a6f84db",
"babel-code-frame": "^6.26.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-shallow-renderer": "^16.14.1",
"symlink-dir": "^4.2.0"
}
}

View File

@ -0,0 +1,2 @@
horizon.production.js
remote-repo

View File

@ -0,0 +1,29 @@
# Horizon Benchmarking
## Commands
```bash
# will compare local repo vs remote merge base repo
yarn start
# will compare local repo vs remote merge base repo
# this can significantly improve bench times due to no build
yarn start --skip-build
# will only build and run local repo against benchmarks (no remote values will be shown)
yarn start --local
# will only build and run remote merge base repo against benchmarks (no local values will be shown)
yarn start --remote
# will only build and run remote main repo against benchmarks
yarn start --remote=main
# same as "yarn start"
yarn start --remote --local
# runs benchmarks with Chrome in headless mode
yarn start --headless
# runs only specific string matching benchmarks
yarn start --benchmark=hacker
```

View File

@ -0,0 +1,130 @@
'use strict';
const Lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');
const stats = require('stats-analysis');
const config = require('lighthouse/lighthouse-core/config/perf-config');
const spawn = require('child_process').spawn;
const os = require('os');
const timesToRun = 2;
function wait(val) {
return new Promise(resolve => setTimeout(resolve, val));
}
async function runScenario(benchmark, chrome) {
const port = chrome.port;
const results = await Lighthouse(
`http://localhost:8080/${benchmark}/index.html`,
{
output: 'json',
port,
},
config
);
const perfMarkings = results.lhr.audits['user-timings'].details.items;
const entries = perfMarkings
.filter(({timingType}) => timingType !== 'Mark')
.map(({duration, name}) => ({
entry: name,
time: duration,
}));
entries.push({
entry: 'First Meaningful Paint',
time: results.lhr.audits['first-meaningful-paint'].rawValue,
});
return entries;
}
function bootstrap(data) {
const len = data.length;
const arr = Array(len);
for (let j = 0; j < len; j++) {
arr[j] = data[(Math.random() * len) | 0];
}
return arr;
}
function calculateStandardErrorOfMean(data) {
const means = [];
for (let i = 0; i < 10000; i++) {
means.push(stats.mean(bootstrap(data)));
}
return stats.stdev(means);
}
function calculateAverages(runs) {
const data = [];
const averages = [];
runs.forEach((entries, x) => {
entries.forEach(({entry, time}, i) => {
if (i >= averages.length) {
data.push([time]);
averages.push({
entry,
mean: 0,
sem: 0,
});
} else {
data[i].push(time);
if (x === runs.length - 1) {
const dataWithoutOutliers = stats.filterMADoutliers(data[i]);
averages[i].mean = stats.mean(dataWithoutOutliers);
averages[i].sem = calculateStandardErrorOfMean(data[i]);
}
}
});
});
return averages;
}
async function initChrome() {
const platform = os.platform();
if (platform === 'linux') {
process.env.XVFBARGS = '-screen 0, 1024x768x16';
process.env.LIGHTHOUSE_CHROMIUM_PATH = 'chromium-browser';
const child = spawn('xvfb start', [{detached: true, stdio: ['ignore']}]);
child.unref();
// wait for chrome to load then continue
await wait(3000);
return child;
}
}
async function launchChrome(headless) {
return await chromeLauncher.launch({
chromeFlags: [headless ? '--headless' : ''],
});
}
async function runBenchmark(benchmark, headless) {
const results = {
runs: [],
averages: [],
};
await initChrome();
for (let i = 0; i < timesToRun; i++) {
let chrome = await launchChrome(headless);
results.runs.push(await runScenario(benchmark, chrome));
// add a delay or sometimes it confuses lighthouse and it hangs
await wait(500);
try {
await chrome.kill();
} catch (e) {}
}
results.averages = calculateAverages(results.runs);
return results;
}
module.exports = runBenchmark;

View File

@ -0,0 +1,315 @@
(function() {
'use strict';
const e = Horizon.createElement;
function timeAge(time) {
const now = new Date().getTime() / 1000;
const minutes = (now - time) / 60;
if (minutes < 60) {
return Math.round(minutes) + ' minutes ago';
}
return Math.round(minutes / 60) + ' hours ago';
}
function getHostUrl(url) {
return (url + '')
.replace('https://', '')
.replace('http://', '')
.split('/')[0];
}
function HeaderBar() {
return e(
'tr',
{
style: {
backgroundColor: '#222',
},
},
e(
'table',
{
style: {
padding: 4,
},
width: '100%',
cellSpacing: 0,
cellPadding: 0,
},
e(
'tbody',
null,
e(
'tr',
null,
e(
'td',
{
style: {
width: 18,
paddingRight: 4,
},
},
e(
'a',
{
href: '#',
},
e('img', {
src: 'logo.png',
width: 16,
height: 16,
style: {
border: '1px solid #00d8ff',
},
})
)
),
e(
'td',
{
style: {
lineHeight: '12pt',
},
height: 10,
},
e(
'span',
{
className: 'pagetop',
},
e('b', {className: 'hnname'}, 'Horizon HN Benchmark'),
e('a', {href: '#'}, 'new'),
' | ',
e('a', {href: '#'}, 'comments'),
' | ',
e('a', {href: '#'}, 'show'),
' | ',
e('a', {href: '#'}, 'ask'),
' | ',
e('a', {href: '#'}, 'jobs'),
' | ',
e('a', {href: '#'}, 'submit')
)
)
)
)
)
);
}
function Story({story, rank}) {
return [
e(
'tr',
{
className: 'athing',
},
e(
'td',
{
style: {
verticalAlign: 'top',
textAlign: 'right',
},
className: 'title',
},
e(
'span',
{
className: 'rank',
},
`${rank}.`
)
),
e(
'td',
{
className: 'votelinks',
style: {
verticalAlign: 'top',
},
},
e(
'center',
null,
e(
'a',
{
href: '#',
},
e('div', {
className: 'votearrow',
title: 'upvote',
})
)
)
),
e(
'td',
{
className: 'title',
},
e(
'a',
{
href: '#',
className: 'storylink',
},
story.title
),
story.url
? e(
'span',
{
className: 'sitebit comhead',
},
' (',
e(
'a',
{
href: '#',
},
getHostUrl(story.url)
),
')'
)
: null
)
),
e(
'tr',
null,
e('td', {
colSpan: 2,
}),
e(
'td',
{
className: 'subtext',
},
e(
'span',
{
className: 'score',
},
`${story.score} points`
),
' by ',
e(
'a',
{
href: '#',
className: 'hnuser',
},
story.by
),
' ',
e(
'span',
{
className: 'age',
},
e(
'a',
{
href: '#',
},
timeAge(story.time)
)
),
' | ',
e(
'a',
{
href: '#',
},
'hide'
),
' | ',
e(
'a',
{
href: '#',
},
`${story.descendants || 0} comments`
)
)
),
e('tr', {
style: {
height: 5,
},
className: 'spacer',
}),
];
}
function StoryList({stories}) {
return e(
'tr',
null,
e(
'td',
null,
e(
'table',
{
cellPadding: 0,
cellSpacing: 0,
classList: 'itemlist',
},
e(
'tbody',
null,
stories.map((story, i) =>
e(Story, {story, rank: ++i, key: story.id})
)
)
)
)
);
}
function App({stories}) {
return e(
'center',
null,
e(
'table',
{
id: 'hnmain',
border: 0,
cellPadding: 0,
cellSpacing: 0,
width: '85%',
style: {
'background-color': '#f6f6ef',
},
},
e(
'tbody',
null,
e(HeaderBar, null),
e('tr', {height: 10}),
e(StoryList, {
stories,
})
)
)
);
}
const app = document.getElementById('app');
window.render = function render() {
Horizon.render(
Horizon.createElement(App, {
stories: window.stories,
}),
app
);
};
})();

View File

@ -0,0 +1,13 @@
'use strict';
const {join} = require('path');
async function build(horizonPath, asyncCopyTo) {
// copy the UMD bundles
await asyncCopyTo(
join(horizonPath, 'build', 'horizon', 'umd', 'horizon.production.js'),
join(__dirname, 'horizon.production.js')
);
}
module.exports = build;

View File

@ -0,0 +1,33 @@
'use strict';
const fetch = require('node-fetch');
const {writeFileSync} = require('fs');
const stories = 50;
async function getStory(id) {
const storyRes = await fetch(
`https://hacker-news.firebaseio.com/v0/item/${id}.json`
);
return await storyRes.json();
}
async function getTopStories() {
const topStoriesRes = await fetch(
'https://hacker-news.firebaseio.com/v0/topstories.js'
);
const topStoriesIds = await topStoriesRes.json();
const topStories = [];
for (let i = 0; i < stories; i++) {
const topStoriesId = topStoriesIds[i];
topStories.push(await getStory(topStoriesId));
}
writeFileSync(
'top-stories.json',
`window.stories = ${JSON.stringify(topStories)}`
);
}
getTopStories();

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 B

View File

@ -0,0 +1,28 @@
<html>
<head>
<title>Horizon Hacker News Benchmark</title>
<link rel="stylesheet" type="text/css" href="style.css" />
</head>
<body>
<div id="app"></div>
<script>
performance.mark('Load Horizon');
</script>
<script src="horizon.production.js"></script>
<script>
performance.measure('Load Horizon', 'Load Horizon');
</script>
<script src="top-stories.js"></script>
<script src="benchmark.js"></script>
<script>
performance.mark('Initial Render');
render();
performance.measure('Initial Render', 'Initial Render');
requestAnimationFrame(() => {
performance.mark('Update Render');
render();
performance.measure('Update Render', 'Update Render');
});
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

View File

@ -0,0 +1,57 @@
body {
font-family: Verdana, Geneva, sans-serif
}
.pagetop {
font-family: Verdana, Geneva, sans-serif;
font-size: 10pt;
color: #00d8ff;
}
.hnname {
margin-right: 10px;
color: #fff;
}
.pagetop a, .pagetop a:visited {
color: #00d8ff;
text-decoration: none;
}
.title {
font-family: Verdana, Geneva, sans-serif;
font-size: 10pt;
color: #828282;
}
.subtext {
font-family: Verdana, Geneva, sans-serif;
font-size: 7pt;
color: #828282;
}
.comhead a:link, .subtext a, .subtext a:visited {
color: #828282;
text-decoration: none;
}
.votearrow {
width: 10px;
height: 10px;
border: 0px;
margin: 3px 2px 6px;
background: url(grayarrow.gif) no-repeat;
}
.title, .title a {
font-family: Verdana, Geneva, sans-serif;
font-size: 10pt;
color: #000;
text-decoration: none;
}
.comhead, .comhead a {
font-family: Verdana, Geneva, sans-serif;
font-size: 8pt;
color: #828282;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,13 @@
'use strict';
const {join} = require('path');
async function build(horizonPath, asyncCopyTo) {
// copy the UMD bundles
await asyncCopyTo(
join(horizonPath, 'build', 'horizon', 'umd', 'horizon.production.js'),
join(__dirname, 'horizon.production.js')
);
}
module.exports = build;

View File

@ -0,0 +1,23 @@
<html>
<body>
<div id="app"></div>
<script>
performance.mark('Load Horizon');
</script>
<script src="horizon.production.js"></script>
<script>
performance.measure('Load Horizon', 'Load Horizon');
</script>
<script src="benchmark.js"></script>
<script>
performance.mark('Initial Render');
render();
performance.measure('Initial Render', 'Initial Render');
requestAnimationFrame(() => {
performance.mark('Update Render');
render();
performance.measure('Update Render', 'Update Render');
});
</script>
</body>
</html>

View File

@ -0,0 +1,13 @@
'use strict';
const {join} = require('path');
async function build(horizonPath, asyncCopyTo) {
// copy the UMD bundles
await asyncCopyTo(
join(horizonPath, 'build', 'horizon', 'umd', 'horizon.production.js'),
join(__dirname, 'horizon.production.js')
);
}
module.exports = build;

View File

@ -0,0 +1,23 @@
<html>
<body>
<div id="app"></div>
<script>
performance.mark('Load Horizon');
</script>
<script src="horizon.production.js"></script>
<script>
performance.measure('Load Horizon', 'Load Horizon');
</script>
<script src="benchmark.js"></script>
<script>
performance.mark('Initial Render');
render();
performance.measure('Initial Render', 'Initial Render');
requestAnimationFrame(() => {
performance.mark('Update Render');
render();
performance.measure('Update Render', 'Update Render');
});
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,13 @@
'use strict';
const {join} = require('path');
async function build(horizonPath, asyncCopyTo) {
// copy the UMD bundles
await asyncCopyTo(
join(horizonPath, 'build', 'horizon', 'umd', 'horizon.production.js'),
join(__dirname, 'horizon.production.js')
);
}
module.exports = build;

View File

@ -0,0 +1,23 @@
<html>
<body>
<div id="app"></div>
<script>
performance.mark('Load Horizon');
</script>
<script src="horizon.production.js"></script>
<script>
performance.measure('Load Horizon', 'Load Horizon');
</script>
<script src="benchmark.js"></script>
<script>
performance.mark('Initial Render');
render();
performance.measure('Initial Render', 'Initial Render');
requestAnimationFrame(() => {
performance.mark('Update Render');
render();
performance.measure('Update Render', 'Update Render');
});
</script>
</body>
</html>

View File

@ -0,0 +1,103 @@
'use strict';
const rimraf = require('rimraf');
const ncp = require('ncp').ncp;
const {existsSync} = require('fs');
const exec = require('child_process').exec;
const {join} = require('path');
const horizonUrl = 'ssh://git@szv-open.codehub.huawei.com:2222/innersource/shanhai/wutong/horizon/horizon-core.git';
function cleanDir() {
return new Promise(_resolve => rimraf('remote-repo', _resolve));
}
function executeCommand(command, options) {
return new Promise(_resolve =>
exec(command, options, error => {
if (!error) {
_resolve();
} else {
console.error(error);
process.exit(1);
}
})
);
}
function asyncCopyTo(from, to) {
return new Promise(_resolve => {
ncp(from, to, error => {
if (error) {
console.error(error);
process.exit(1);
}
_resolve();
});
});
}
function getDefaultReactPath() {
return join(__dirname, 'remote-repo');
}
async function buildBenchmark(reactPath = getDefaultReactPath(), benchmark) {
// get the build.js from the benchmark directory and execute it
await require(join(__dirname, 'benchmarks', benchmark, 'build.js'))(
reactPath,
asyncCopyTo
);
}
// async function getMergeBaseFromLocalGitRepo(localRepo) {
// const repo = await Git.Repository.open(localRepo);
// return await Git.Merge.base(
// repo,
// await repo.getHeadCommit(),
// await repo.getBranchCommit('main')
// );
// }
async function buildBenchmarkBundlesFromGitRepo(
commitId,
skipBuild,
url = horizonUrl,
clean
) {
let repo;
const remoteRepoDir = getDefaultReactPath();
if (!skipBuild) {
if (clean) {
//clear remote-repo folder
await cleanDir(remoteRepoDir);
}
// check if remote-repo directory already exists
if (existsSync(remoteRepoDir)) {
await executeCommand(`git pull`, { cwd: remoteRepoDir})
} else {
// if not, clone the repo to remote-repo folder
await executeCommand(`git clone ${url} ${remoteRepoDir}`)
}
await buildHorizonBundles();
}
}
async function buildHorizonBundles(horizonPath = getDefaultReactPath(), skipBuild) {
if (!skipBuild) {
await executeCommand(
`cd ${horizonPath} && yarn && yarn build`
);
}
}
// if run directly via CLI
// if (require.main === module) {
// buildBenchmarkBundlesFromGitRepo();
// }
module.exports = {
buildHorizonBundles,
buildBenchmark,
buildBenchmarkBundlesFromGitRepo,
};

View File

@ -0,0 +1,24 @@
{
"name": "react-benchmark",
"version": "0.0.1",
"main": "runner.js",
"scripts": {
"start": "node runner.js",
"benchmark": "yarn start --skip-build",
"benchmark:local": "yarn start --local --skip-build",
"benchmark:remote": "yarn start --remote"
},
"dependencies": {
"chalk": "^2.1.0",
"chrome-launcher": "^0.10.5",
"cli-table": "^0.3.1",
"http-server": "^0.10.0",
"http2": "^3.3.6",
"lighthouse": "^3.2.1",
"mime": "^1.3.6",
"minimist": "^1.2.3",
"ncp": "^2.0.0",
"rimraf": "^2.6.1",
"stats-analysis": "^2.0.0"
}
}

View File

@ -0,0 +1,132 @@
'use strict';
const {readdirSync, statSync} = require('fs');
const {join} = require('path');
const horizonPath = require('../../scripts/shared/horizonPath')
const runBenchmark = require('./benchmark');
const {
buildHorizonBundles,
buildBenchmark,
buildBenchmarkBundlesFromGitRepo,
getMergeBaseFromLocalGitRepo,
} = require('./build');
const argv = require('minimist')(process.argv.slice(2));
const chalk = require('chalk');
const printResults = require('./stats');
const serveBenchmark = require('./server');
function getBenchmarkNames() {
return readdirSync(join(__dirname, 'benchmarks')).filter(file =>
statSync(join(__dirname, 'benchmarks', file)).isDirectory()
);
}
function wait(val) {
return new Promise(resolve => setTimeout(resolve, val));
}
const runRemote = argv.remote;
const runLocal = argv.local;
const benchmarkFilter = argv.benchmark;
const headless = argv.headless;
const skipBuild = argv['skip-build'];
async function runBenchmarks(horizonPath) {
const benchmarkNames = getBenchmarkNames();
const results = {};
const server = serveBenchmark();
await wait(1000);
for (let i = 0; i < benchmarkNames.length; i++) {
const benchmarkName = benchmarkNames[i];
if (
!benchmarkFilter ||
(benchmarkFilter && benchmarkName.indexOf(benchmarkFilter) !== -1)
) {
console.log(
chalk.gray(`- Building benchmark "${chalk.white(benchmarkName)}"...`)
);
await buildBenchmark(horizonPath, benchmarkName);
console.log(
chalk.gray(`- Running benchmark "${chalk.white(benchmarkName)}"...`)
);
results[benchmarkName] = await runBenchmark(benchmarkName, headless);
}
}
await wait(500);
server.close();
// http-server.close() is async but they don't provide a callback..
await wait(500);
return results;
}
// get the performance benchmark results
// from remote main (default React repo)
async function benchmarkRemoteMaster() {
console.log(chalk.gray(`- Building Horizon Remote bundles...`));
let commit = argv.remote;
await buildBenchmarkBundlesFromGitRepo(commit, skipBuild);
return {
benchmarks: await runBenchmarks(),
};
}
// get the performance benchmark results
// of the local react repo
async function benchmarkLocal(horizonPath) {
console.log(chalk.gray(`- Building Horizon bundles...`));
await buildHorizonBundles(horizonPath, skipBuild);
return {
benchmarks: await runBenchmarks(horizonPath),
};
}
async function runLocalBenchmarks(showResults) {
console.log(
chalk.white.bold('Running benchmarks for ') +
chalk.green.bold('Local (Current Branch)')
);
const localResults = await benchmarkLocal(horizonPath);
if (showResults) {
printResults(localResults, null);
}
return localResults;
}
async function runRemoteBenchmarks(showResults) {
console.log(
chalk.white.bold('Running benchmarks for ') +
chalk.yellow.bold('Remote (Merge Base)')
);
const remoteMasterResults = await benchmarkRemoteMaster();
if (showResults) {
printResults(null, remoteMasterResults);
}
return remoteMasterResults;
}
async function compareLocalToMaster() {
console.log(
chalk.white.bold('Comparing ') +
chalk.green.bold('Local (Current Branch)') +
chalk.white.bold(' to ') +
chalk.yellow.bold('Remote (Merge Base)')
);
const localResults = await runLocalBenchmarks(false);
const remoteMasterResults = await runRemoteBenchmarks(false);
printResults(localResults, remoteMasterResults);
}
if ((runLocal && runRemote) || (!runLocal && !runRemote)) {
compareLocalToMaster().then(() => process.exit(0));
} else if (runLocal) {
runLocalBenchmarks(true).then(() => process.exit(0));
} else if (runRemote) {
runRemoteBenchmarks(true).then(() => process.exit(0));
}

View File

@ -0,0 +1,80 @@
'use strict';
const http2Server = require('http2');
const httpServer = require('http-server');
const {existsSync, statSync, createReadStream} = require('fs');
const {join} = require('path');
const argv = require('minimist')(process.argv.slice(2));
const mime = require('mime');
function sendFile(filename, response) {
response.setHeader('Content-Type', mime.lookup(filename));
response.writeHead(200);
const fileStream = createReadStream(filename);
fileStream.pipe(response);
fileStream.on('finish', response.end);
}
function createHTTP2Server(benchmark) {
const server = http2Server.createServer({}, (request, response) => {
const filename = join(
__dirname,
'benchmarks',
benchmark,
request.url
).replace(/\?.*/g, '');
if (existsSync(filename) && statSync(filename).isFile()) {
sendFile(filename, response);
} else {
const indexHtmlPath = join(filename, 'index.html');
if (existsSync(indexHtmlPath)) {
sendFile(indexHtmlPath, response);
} else {
response.writeHead(404);
response.end();
}
}
});
server.listen(8080);
return server;
}
function createHTTPServer() {
const server = httpServer.createServer({
root: join(__dirname, 'benchmarks'),
robots: true,
cache: 'no-store',
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': 'true',
},
});
server.listen(8080, () => {
console.log('Running at http://localhost:8080');
});
return server;
}
function serveBenchmark(benchmark, http2) {
if (http2) {
return createHTTP2Server(benchmark);
} else {
return createHTTPServer();
}
}
// if run directly via CLI
if (require.main === module) {
const benchmarkInput = argv._[0];
if (benchmarkInput) {
serveBenchmark(benchmarkInput);
} else {
console.error('Please specify a benchmark directory to serve!');
process.exit(1);
}
}
module.exports = serveBenchmark;

View File

@ -0,0 +1,112 @@
'use strict';
const chalk = require('chalk');
const Table = require('cli-table');
function percentChange(prev, current, prevSem, currentSem) {
const [mean, sd] = calculateMeanAndSdOfRatioFromDeltaMethod(
prev,
current,
prevSem,
currentSem
);
const pctChange = +(mean * 100).toFixed(1);
const ci95 = +(100 * 1.96 * sd).toFixed(1);
const ciInfo = ci95 > 0 ? ` +- ${ci95} %` : '';
const text = `${pctChange > 0 ? '+' : ''}${pctChange} %${ciInfo}`;
if (pctChange + ci95 < 0) {
return chalk.green(text);
} else if (pctChange - ci95 > 0) {
return chalk.red(text);
} else {
// Statistically insignificant.
return text;
}
}
function calculateMeanAndSdOfRatioFromDeltaMethod(
meanControl,
meanTest,
semControl,
semTest
) {
const mean =
(meanTest - meanControl) / meanControl -
(Math.pow(semControl, 2) * meanTest) / Math.pow(meanControl, 3);
const variance =
Math.pow(semTest / meanControl, 2) +
Math.pow(semControl * meanTest, 2) / Math.pow(meanControl, 4);
return [mean, Math.sqrt(variance)];
}
function addBenchmarkResults(table, localResults, remoteMasterResults) {
const benchmarks = Object.keys(
(localResults && localResults.benchmarks) ||
(remoteMasterResults && remoteMasterResults.benchmarks)
);
benchmarks.forEach(benchmark => {
const rowHeader = [chalk.white.bold(benchmark)];
if (remoteMasterResults) {
rowHeader.push(chalk.white.bold('Time'));
}
if (localResults) {
rowHeader.push(chalk.white.bold('Time'));
}
if (localResults && remoteMasterResults) {
rowHeader.push(chalk.white.bold('Diff'));
}
table.push(rowHeader);
const measurements =
(localResults && localResults.benchmarks[benchmark].averages) ||
(remoteMasterResults &&
remoteMasterResults.benchmarks[benchmark].averages);
measurements.forEach((measurement, i) => {
const row = [chalk.gray(measurement.entry)];
let remoteMean;
let remoteSem;
if (remoteMasterResults) {
remoteMean = remoteMasterResults.benchmarks[benchmark].averages[i].mean;
remoteSem = remoteMasterResults.benchmarks[benchmark].averages[i].sem;
// https://en.wikipedia.org/wiki/1.96 gives a 99% confidence interval.
const ci95 = remoteSem * 1.96;
row.push(
chalk.white(+remoteMean.toFixed(2) + ' ms +- ' + ci95.toFixed(2))
);
}
let localMean;
let localSem;
if (localResults) {
localMean = localResults.benchmarks[benchmark].averages[i].mean;
localSem = localResults.benchmarks[benchmark].averages[i].sem;
const ci95 = localSem * 1.96;
row.push(
chalk.white(+localMean.toFixed(2) + ' ms +- ' + ci95.toFixed(2))
);
}
if (localResults && remoteMasterResults) {
row.push(percentChange(remoteMean, localMean, remoteSem, localSem));
}
table.push(row);
});
});
}
function printResults(localResults, remoteMasterResults) {
const head = [''];
if (remoteMasterResults) {
head.push(chalk.yellow.bold('Remote (Merge Base)'));
}
if (localResults) {
head.push(chalk.green.bold('Local (Current Branch)'));
}
if (localResults && remoteMasterResults) {
head.push('');
}
const table = new Table({head});
addBenchmarkResults(table, localResults, remoteMasterResults);
console.log(table.toString());
}
module.exports = printResults;

View File

@ -0,0 +1,98 @@
# `dom-event-testing-library`
A library for unit testing events via high-level interactions, e.g., `pointerdown`,
that produce realistic and complete DOM event sequences.
There are number of challenges involved in unit testing modules that work with
DOM events.
1. Gesture recognizers may need to support environments with and without support for
the `PointerEvent` API.
2. Gesture recognizers may need to support various user interaction modes including
mouse, touch, and pen use.
3. Gesture recognizers must account for the actual event sequences browsers produce
(e.g., emulated touch and mouse events.)
4. Gesture recognizers must work with "virtual" events produced by tools like
screen-readers.
Writing unit tests to cover all these scenarios is tedious and error prone. This
event testing library is designed to solve these issues by allowing developers to
more easily dispatch events in unit tests, and to more reliably test pointer
interactions using a high-level API based on `PointerEvent`. Here's a basic example:
```js
import {
describeWithPointerEvent,
testWithPointerType,
createEventTarget,
setPointerEvent,
resetActivePointers
} from 'dom-event-testing-library';
describeWithPointerEvent('useTap', hasPointerEvent => {
beforeEach(() => {
// basic PointerEvent mock
setPointerEvent(hasPointerEvent);
});
afterEach(() => {
// clear active pointers between test runs
resetActivePointers();
});
// test all the pointer types supported by the environment
testWithPointerType('pointer down', pointerType => {
const ref = createRef(null);
const onTapStart = jest.fn();
render(() => {
useTap(ref, { onTapStart });
return <div ref={ref} />
});
// create an event target
const target = createEventTarget(ref.current);
// dispatch high-level pointer event
target.pointerdown({ pointerType });
expect(onTapStart).toBeCalled();
});
});
```
This tests the interaction in multiple scenarios. In each case, a realistic DOM
event sequencewith complete mock eventsis produced. When running in a mock
environment without the `PointerEvent` API, the test runs for both `mouse` and
`touch` pointer types. When `touch` is the pointer type it produces emulated mouse
events. When running in a mock environment with the `PointerEvent` API, the test
runs for `mouse`, `touch`, and `pen` pointer types.
It's important to cover all these scenarios because it's very easy to introduce
bugs e.g., double calling of callbacks if not accounting for emulated mouse
events, differences in target capturing between `touch` and `mouse` pointers, and
the different semantics of `button` across event APIs.
Default values are provided for the expected native events properties. They can
also be customized as needed in a test.
```js
target.pointerdown({
button: 0,
buttons: 1,
pageX: 10,
pageY: 10,
pointerType,
// NOTE: use x,y instead of clientX,clientY
x: 10,
y: 10
});
```
Tests that dispatch multiple pointer events will dispatch multi-touch native events
on the target.
```js
// first pointer is active
target.pointerdown({pointerId: 1, pointerType});
// second pointer is active
target.pointerdown({pointerId: 2, pointerType});
```

View File

@ -0,0 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`describeWithPointerEvent: MouseEvent/TouchEvent provides boolean to tests 1`] = `false`;
exports[`describeWithPointerEvent: MouseEvent/TouchEvent testWithPointerType: mouse 1`] = `"mouse"`;
exports[`describeWithPointerEvent: MouseEvent/TouchEvent testWithPointerType: touch 1`] = `"touch"`;
exports[`describeWithPointerEvent: PointerEvent provides boolean to tests 1`] = `true`;
exports[`describeWithPointerEvent: PointerEvent testWithPointerType: mouse 1`] = `"mouse"`;
exports[`describeWithPointerEvent: PointerEvent testWithPointerType: pen 1`] = `"pen"`;
exports[`describeWithPointerEvent: PointerEvent testWithPointerType: touch 1`] = `"touch"`;

View File

@ -0,0 +1,361 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
import {defaultBrowserChromeSize} from '../constants';
import {
createEventTarget,
describeWithPointerEvent,
testWithPointerType,
resetActivePointers,
} from 'dom-event-testing-library';
/**
* Unit test helpers
*/
describeWithPointerEvent('describeWithPointerEvent', pointerEvent => {
test('provides boolean to tests', () => {
expect(pointerEvent).toMatchSnapshot();
});
testWithPointerType('testWithPointerType', pointerType => {
expect(pointerType).toMatchSnapshot();
});
});
/**
* createEventTarget
*/
describe('createEventTarget', () => {
let node;
beforeEach(() => {
node = document.createElement('div');
});
afterEach(() => {
node = null;
resetActivePointers();
});
test('returns expected API', () => {
const target = createEventTarget(node);
expect(target.node).toEqual(node);
expect(Object.keys(target)).toMatchInlineSnapshot(`
Array [
"node",
"blur",
"click",
"focus",
"keydown",
"keyup",
"scroll",
"virtualclick",
"contextmenu",
"pointercancel",
"pointerdown",
"pointerhover",
"pointermove",
"pointerenter",
"pointerexit",
"pointerup",
"tap",
"setBoundingClientRect",
]
`);
});
/**
* Simple events
*/
describe('.blur()', () => {
test('default', () => {
const target = createEventTarget(node);
node.addEventListener('blur', e => {
expect(e.relatedTarget).toMatchInlineSnapshot(`null`);
});
target.blur();
});
test('custom payload', () => {
const target = createEventTarget(node);
node.addEventListener('blur', e => {
expect(e.relatedTarget).toMatchInlineSnapshot(`null`);
});
target.blur();
});
});
describe('.click()', () => {
test('default', () => {
const target = createEventTarget(node);
node.addEventListener('click', e => {
expect(e.altKey).toEqual(false);
expect(e.button).toEqual(0);
expect(e.buttons).toEqual(0);
expect(e.clientX).toEqual(0);
expect(e.clientY).toEqual(0);
expect(e.ctrlKey).toEqual(false);
expect(e.detail).toEqual(1);
expect(typeof e.getModifierState).toEqual('function');
expect(e.metaKey).toEqual(false);
expect(e.movementX).toEqual(0);
expect(e.movementY).toEqual(0);
expect(e.offsetX).toEqual(0);
expect(e.offsetY).toEqual(0);
expect(e.pageX).toEqual(0);
expect(e.pageY).toEqual(0);
expect(typeof e.preventDefault).toEqual('function');
expect(e.screenX).toEqual(0);
expect(e.screenY).toEqual(defaultBrowserChromeSize);
expect(e.shiftKey).toEqual(false);
expect(typeof e.timeStamp).toEqual('number');
});
target.click();
});
test('custom payload', () => {
const target = createEventTarget(node);
node.addEventListener('click', e => {
expect(e.altKey).toEqual(true);
expect(e.button).toEqual(1);
expect(e.buttons).toEqual(4);
expect(e.clientX).toEqual(10);
expect(e.clientY).toEqual(20);
expect(e.ctrlKey).toEqual(true);
expect(e.metaKey).toEqual(true);
expect(e.movementX).toEqual(1);
expect(e.movementY).toEqual(2);
expect(e.offsetX).toEqual(5);
expect(e.offsetY).toEqual(5);
expect(e.pageX).toEqual(50);
expect(e.pageY).toEqual(50);
expect(e.screenX).toEqual(10);
expect(e.screenY).toEqual(20 + defaultBrowserChromeSize);
expect(e.shiftKey).toEqual(true);
});
target.click({
altKey: true,
button: 1,
buttons: 4,
x: 10,
y: 20,
ctrlKey: true,
metaKey: true,
movementX: 1,
movementY: 2,
offsetX: 5,
offsetY: 5,
pageX: 50,
pageY: 50,
shiftKey: true,
});
});
});
describe('.focus()', () => {
test('default', () => {
const target = createEventTarget(node);
node.addEventListener('focus', e => {
expect(e.relatedTarget).toMatchInlineSnapshot(`null`);
});
target.blur();
});
test('custom payload', () => {
const target = createEventTarget(node);
node.addEventListener('focus', e => {
expect(e.relatedTarget).toMatchInlineSnapshot(`null`);
});
target.blur();
});
});
describe('.keydown()', () => {
test('default', () => {
const target = createEventTarget(node);
node.addEventListener('keydown', e => {
expect(e.altKey).toEqual(false);
expect(e.ctrlKey).toEqual(false);
expect(typeof e.getModifierState).toEqual('function');
expect(e.key).toEqual('');
expect(e.metaKey).toEqual(false);
expect(typeof e.preventDefault).toEqual('function');
expect(e.shiftKey).toEqual(false);
expect(typeof e.timeStamp).toEqual('number');
});
target.keydown();
});
test('custom payload', () => {
const target = createEventTarget(node);
node.addEventListener('keydown', e => {
expect(e.altKey).toEqual(true);
expect(e.ctrlKey).toEqual(true);
expect(e.isComposing).toEqual(true);
expect(e.key).toEqual('Enter');
expect(e.metaKey).toEqual(true);
expect(e.shiftKey).toEqual(true);
});
target.keydown({
altKey: true,
ctrlKey: true,
isComposing: true,
key: 'Enter',
metaKey: true,
shiftKey: true,
});
});
});
describe('.keyup()', () => {
test('default', () => {
const target = createEventTarget(node);
node.addEventListener('keyup', e => {
expect(e.altKey).toEqual(false);
expect(e.ctrlKey).toEqual(false);
expect(typeof e.getModifierState).toEqual('function');
expect(e.key).toEqual('');
expect(e.metaKey).toEqual(false);
expect(typeof e.preventDefault).toEqual('function');
expect(e.shiftKey).toEqual(false);
expect(typeof e.timeStamp).toEqual('number');
});
target.keydown();
});
test('custom payload', () => {
const target = createEventTarget(node);
node.addEventListener('keyup', e => {
expect(e.altKey).toEqual(true);
expect(e.ctrlKey).toEqual(true);
expect(e.isComposing).toEqual(true);
expect(e.key).toEqual('Enter');
expect(e.metaKey).toEqual(true);
expect(e.shiftKey).toEqual(true);
});
target.keyup({
altKey: true,
ctrlKey: true,
isComposing: true,
key: 'Enter',
metaKey: true,
shiftKey: true,
});
});
});
describe('.scroll()', () => {
test('default', () => {
const target = createEventTarget(node);
node.addEventListener('scroll', e => {
expect(e.type).toEqual('scroll');
});
target.scroll();
});
});
describe('.virtualclick()', () => {
test('default', () => {
const target = createEventTarget(node);
node.addEventListener('click', e => {
expect(e.altKey).toEqual(false);
expect(e.button).toEqual(0);
expect(e.buttons).toEqual(0);
expect(e.clientX).toEqual(0);
expect(e.clientY).toEqual(0);
expect(e.ctrlKey).toEqual(false);
expect(e.detail).toEqual(0);
expect(typeof e.getModifierState).toEqual('function');
expect(e.metaKey).toEqual(false);
expect(e.movementX).toEqual(0);
expect(e.movementY).toEqual(0);
expect(e.offsetX).toEqual(0);
expect(e.offsetY).toEqual(0);
expect(e.pageX).toEqual(0);
expect(e.pageY).toEqual(0);
expect(typeof e.preventDefault).toEqual('function');
expect(e.screenX).toEqual(0);
expect(e.screenY).toEqual(0);
expect(e.shiftKey).toEqual(false);
expect(typeof e.timeStamp).toEqual('number');
});
target.virtualclick();
});
test('custom payload', () => {
const target = createEventTarget(node);
node.addEventListener('click', e => {
// expect most of the custom payload to be ignored
expect(e.altKey).toEqual(true);
expect(e.button).toEqual(1);
expect(e.buttons).toEqual(0);
expect(e.clientX).toEqual(0);
expect(e.clientY).toEqual(0);
expect(e.ctrlKey).toEqual(true);
expect(e.detail).toEqual(0);
expect(e.metaKey).toEqual(true);
expect(e.pageX).toEqual(0);
expect(e.pageY).toEqual(0);
expect(e.screenX).toEqual(0);
expect(e.screenY).toEqual(0);
expect(e.shiftKey).toEqual(true);
});
target.virtualclick({
altKey: true,
button: 1,
buttons: 4,
x: 10,
y: 20,
ctrlKey: true,
metaKey: true,
pageX: 50,
pageY: 50,
shiftKey: true,
});
});
});
/**
* Complex event sequences
*
* ...coming soon
*/
/**
* Other APIs
*/
test('.setBoundingClientRect()', () => {
const target = createEventTarget(node);
// expect(node.getBoundingClientRect()).toMatchInlineSnapshot(`
// Object {
// "bottom": 0,
// "height": 0,
// "left": 0,
// "right": 0,
// "top": 0,
// "width": 0,
// }
// `);
target.setBoundingClientRect({x: 10, y: 20, width: 100, height: 200});
expect(node.getBoundingClientRect()).toMatchInlineSnapshot(`
Object {
"bottom": 220,
"height": 200,
"left": 10,
"right": 110,
"top": 20,
"width": 100,
}
`);
});
});

View File

@ -0,0 +1,65 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
export const defaultPointerId = 1;
export const defaultPointerSize = 23;
export const defaultBrowserChromeSize = 50;
/**
* Button property
* This property only guarantees to indicate which buttons are pressed during events caused by pressing or
* releasing one or multiple buttons. As such, it is not reliable for events such as 'mouseenter', 'mouseleave',
* 'mouseover', 'mouseout' or 'mousemove'. Furthermore, the semantics differ for PointerEvent, where the value
* for 'pointermove' will always be -1.
*/
export const buttonType = {
// no change since last event
none: -1,
// left-mouse
// touch contact
// pen contact
primary: 0,
// right-mouse
// pen barrel button
secondary: 2,
// middle mouse
auxiliary: 1,
// back mouse
back: 3,
// forward mouse
forward: 4,
// pen eraser
eraser: 5,
};
/**
* Buttons bitmask
*/
export const buttonsType = {
none: 0,
// left-mouse
// touch contact
// pen contact
primary: 1,
// right-mouse
// pen barrel button
secondary: 2,
// middle mouse
auxiliary: 4,
// back mouse
back: 8,
// forward mouse
forward: 16,
// pen eraser
eraser: 32,
};

View File

@ -0,0 +1,67 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
/**
* Change environment support for PointerEvent.
*/
const emptyFunction = function() {};
export function hasPointerEvent() {
return global != null && global.PointerEvent != null;
}
export function setPointerEvent(bool) {
const pointerCaptureFn = name => id => {
if (typeof id !== 'number') {
if (isDev) {
console.error('A pointerId must be passed to "%s"', name);
}
}
};
global.PointerEvent = bool ? emptyFunction : undefined;
global.HTMLElement.prototype.setPointerCapture = bool
? pointerCaptureFn('setPointerCapture')
: undefined;
global.HTMLElement.prototype.releasePointerCapture = bool
? pointerCaptureFn('releasePointerCapture')
: undefined;
}
/**
* Change environment host platform.
*/
const platformGetter = jest.spyOn(global.navigator, 'platform', 'get');
export const platform = {
clear() {
platformGetter.mockClear();
},
get() {
return global.navigator.platform === 'MacIntel' ? 'mac' : 'windows';
},
set(name: 'mac' | 'windows') {
switch (name) {
case 'mac': {
platformGetter.mockReturnValue('MacIntel');
break;
}
case 'windows': {
platformGetter.mockReturnValue('Win32');
break;
}
default: {
break;
}
}
},
};

View File

@ -0,0 +1,361 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
import {
buttonType,
buttonsType,
defaultPointerId,
defaultPointerSize,
defaultBrowserChromeSize,
} from './constants';
import * as domEvents from './domEvents';
import {hasPointerEvent, platform} from './domEnvironment';
import * as touchStore from './touchStore';
/**
* Converts a PointerEvent payload to a Touch
*/
function createTouch(target, payload) {
const {
height = defaultPointerSize,
pageX,
pageY,
pointerId,
pressure = 1,
twist = 0,
width = defaultPointerSize,
x = 0,
y = 0,
} = payload;
return {
clientX: x,
clientY: y,
force: pressure,
identifier: pointerId,
pageX: pageX || x,
pageY: pageY || y,
radiusX: width / 2,
radiusY: height / 2,
rotationAngle: twist,
target,
screenX: x,
screenY: y + defaultBrowserChromeSize,
};
}
/**
* Converts a PointerEvent to a TouchEvent
*/
function createTouchEventPayload(target, touch, payload) {
const {
altKey = false,
ctrlKey = false,
metaKey = false,
preventDefault,
shiftKey = false,
timeStamp,
} = payload;
return {
altKey,
changedTouches: [touch],
ctrlKey,
metaKey,
preventDefault,
shiftKey,
targetTouches: touchStore.getTargetTouches(target),
timeStamp,
touches: touchStore.getTouches(),
};
}
function getPointerType(payload) {
let pointerType = 'mouse';
if (payload != null && payload.pointerType != null) {
pointerType = payload.pointerType;
}
return pointerType;
}
/**
* Pointer events sequences.
*
* Creates representative browser event sequences for high-level gestures based on pointers.
* This allows unit tests to be written in terms of simple pointer interactions while testing
* that the responses to those interactions account for the complex sequence of events that
* browsers produce as a result.
*
* Every time a new pointer touches the surface a 'touchstart' event should be dispatched.
* - 'changedTouches' contains the new touch.
* - 'targetTouches' contains all the active pointers for the target.
* - 'touches' contains all the active pointers on the surface.
*
* Every time an existing pointer moves a 'touchmove' event should be dispatched.
* - 'changedTouches' contains the updated touch.
*
* Every time an existing pointer leaves the surface a 'touchend' event should be dispatched.
* - 'changedTouches' contains the released touch.
* - 'targetTouches' contains any of the remaining active pointers for the target.
*/
export function contextmenu(
target,
defaultPayload,
{pointerType = 'mouse', modified} = {},
) {
const dispatch = arg => target.dispatchEvent(arg);
const payload = {
pointerId: defaultPointerId,
pointerType,
...defaultPayload,
};
const preventDefault = payload.preventDefault;
if (pointerType === 'touch') {
if (hasPointerEvent()) {
dispatch(
domEvents.pointerdown({
...payload,
button: buttonType.primary,
buttons: buttonsType.primary,
}),
);
}
const touch = createTouch(target, payload);
touchStore.addTouch(touch);
const touchEventPayload = createTouchEventPayload(target, touch, payload);
dispatch(domEvents.touchstart(touchEventPayload));
dispatch(
domEvents.contextmenu({
button: buttonType.primary,
buttons: buttonsType.none,
preventDefault,
}),
);
touchStore.removeTouch(touch);
} else if (pointerType === 'mouse') {
if (modified === true) {
const button = buttonType.primary;
const buttons = buttonsType.primary;
const ctrlKey = true;
if (hasPointerEvent()) {
dispatch(
domEvents.pointerdown({button, buttons, ctrlKey, pointerType}),
);
}
dispatch(domEvents.mousedown({button, buttons, ctrlKey}));
if (platform.get() === 'mac') {
dispatch(
domEvents.contextmenu({button, buttons, ctrlKey, preventDefault}),
);
}
} else {
const button = buttonType.secondary;
const buttons = buttonsType.secondary;
if (hasPointerEvent()) {
dispatch(domEvents.pointerdown({button, buttons, pointerType}));
}
dispatch(domEvents.mousedown({button, buttons}));
dispatch(domEvents.contextmenu({button, buttons, preventDefault}));
}
}
}
export function pointercancel(target, defaultPayload) {
const dispatchEvent = arg => target.dispatchEvent(arg);
const pointerType = getPointerType(defaultPayload);
const payload = {
pointerId: defaultPointerId,
pointerType,
...defaultPayload,
};
if (hasPointerEvent()) {
dispatchEvent(domEvents.pointercancel(payload));
} else {
if (pointerType === 'mouse') {
dispatchEvent(domEvents.dragstart(payload));
} else {
const touch = createTouch(target, payload);
touchStore.removeTouch(touch);
const touchEventPayload = createTouchEventPayload(target, touch, payload);
dispatchEvent(domEvents.touchcancel(touchEventPayload));
}
}
}
export function pointerdown(target, defaultPayload) {
const dispatch = arg => target.dispatchEvent(arg);
const pointerType = getPointerType(defaultPayload);
const payload = {
button: buttonType.primary,
buttons: buttonsType.primary,
pointerId: defaultPointerId,
pointerType,
...defaultPayload,
};
if (pointerType === 'mouse') {
if (hasPointerEvent()) {
dispatch(domEvents.pointerover(payload));
dispatch(domEvents.pointerenter(payload));
}
dispatch(domEvents.mouseover(payload));
dispatch(domEvents.mouseenter(payload));
if (hasPointerEvent()) {
dispatch(domEvents.pointerdown(payload));
}
dispatch(domEvents.mousedown(payload));
if (document.activeElement !== target) {
dispatch(domEvents.focus());
}
} else {
if (hasPointerEvent()) {
dispatch(domEvents.pointerover(payload));
dispatch(domEvents.pointerenter(payload));
dispatch(domEvents.pointerdown(payload));
}
const touch = createTouch(target, payload);
touchStore.addTouch(touch);
const touchEventPayload = createTouchEventPayload(target, touch, payload);
dispatch(domEvents.touchstart(touchEventPayload));
if (hasPointerEvent()) {
dispatch(domEvents.gotpointercapture(payload));
}
}
}
export function pointerenter(target, defaultPayload) {
const dispatch = arg => target.dispatchEvent(arg);
const payload = {
pointerId: defaultPointerId,
...defaultPayload,
};
if (hasPointerEvent()) {
dispatch(domEvents.pointerover(payload));
dispatch(domEvents.pointerenter(payload));
}
dispatch(domEvents.mouseover(payload));
dispatch(domEvents.mouseenter(payload));
}
export function pointerexit(target, defaultPayload) {
const dispatch = arg => target.dispatchEvent(arg);
const payload = {
pointerId: defaultPointerId,
...defaultPayload,
};
if (hasPointerEvent()) {
dispatch(domEvents.pointerout(payload));
dispatch(domEvents.pointerleave(payload));
}
dispatch(domEvents.mouseout(payload));
dispatch(domEvents.mouseleave(payload));
}
export function pointerhover(target, defaultPayload) {
const dispatch = arg => target.dispatchEvent(arg);
const payload = {
pointerId: defaultPointerId,
...defaultPayload,
};
if (hasPointerEvent()) {
dispatch(domEvents.pointermove(payload));
}
dispatch(domEvents.mousemove(payload));
}
export function pointermove(target, defaultPayload) {
const dispatch = arg => target.dispatchEvent(arg);
const pointerType = getPointerType(defaultPayload);
const payload = {
pointerId: defaultPointerId,
pointerType,
...defaultPayload,
};
if (hasPointerEvent()) {
dispatch(
domEvents.pointermove({
pressure: pointerType === 'touch' ? 1 : 0.5,
...payload,
}),
);
} else {
if (pointerType === 'mouse') {
dispatch(domEvents.mousemove(payload));
} else {
const touch = createTouch(target, payload);
touchStore.updateTouch(touch);
const touchEventPayload = createTouchEventPayload(target, touch, payload);
dispatch(domEvents.touchmove(touchEventPayload));
}
}
}
export function pointerup(target, defaultPayload) {
const dispatch = arg => target.dispatchEvent(arg);
const pointerType = getPointerType(defaultPayload);
const payload = {
pointerId: defaultPointerId,
pointerType,
...defaultPayload,
};
if (pointerType === 'mouse') {
if (hasPointerEvent()) {
dispatch(domEvents.pointerup(payload));
}
dispatch(domEvents.mouseup(payload));
dispatch(domEvents.click(payload));
} else {
if (hasPointerEvent()) {
dispatch(domEvents.pointerup(payload));
dispatch(domEvents.lostpointercapture(payload));
dispatch(domEvents.pointerout(payload));
dispatch(domEvents.pointerleave(payload));
}
const touch = createTouch(target, payload);
touchStore.removeTouch(touch);
const touchEventPayload = createTouchEventPayload(target, touch, payload);
dispatch(domEvents.touchend(touchEventPayload));
dispatch(domEvents.mouseover(payload));
dispatch(domEvents.mousemove(payload));
dispatch(domEvents.mousedown(payload));
if (document.activeElement !== target) {
dispatch(domEvents.focus());
}
dispatch(domEvents.mouseup(payload));
dispatch(domEvents.click(payload));
}
}
/**
* This function should be called after each test to ensure the touchStore is cleared
* in cases where the mock pointers weren't released before the test completed
* (e.g., a test failed or ran a partial gesture).
*/
export function resetActivePointers() {
touchStore.clear();
}

View File

@ -0,0 +1,442 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
import {
buttonType,
buttonsType,
defaultPointerSize,
defaultBrowserChromeSize,
} from './constants';
/**
* Native event object mocks for higher-level events.
*
* 1. Each event type defines the exact object that it accepts. This ensures
* that no arbitrary properties can be assigned to events, and the properties
* that don't exist on specific event types (e.g., 'pointerType') are not added
* to the respective native event.
*
* 2. Properties that cannot be relied on due to inconsistent browser support (e.g., 'x' and 'y') are not
* added to the native event. Others that shouldn't be arbitrarily customized (e.g., 'screenX')
* are automatically inferred from associated values.
*
* 3. PointerEvent and TouchEvent fields are normalized (e.g., 'rotationAngle' -> 'twist')
*/
function emptyFunction() {}
function createEvent(type, data = {}) {
const event = document.createEvent('CustomEvent');
event.initCustomEvent(type, true, true);
if (data != null) {
Object.keys(data).forEach(key => {
const value = data[key];
if (key === 'timeStamp' && !value) {
return;
}
Object.defineProperty(event, key, {value});
});
}
return event;
}
function createGetModifierState(keyArg, data) {
if (keyArg === 'Alt') {
return data.altKey || false;
}
if (keyArg === 'Control') {
return data.ctrlKey || false;
}
if (keyArg === 'Meta') {
return data.metaKey || false;
}
if (keyArg === 'Shift') {
return data.shiftKey || false;
}
}
function createPointerEvent(
type,
{
altKey = false,
button = buttonType.none,
buttons = buttonsType.none,
ctrlKey = false,
detail = 1,
height,
metaKey = false,
movementX = 0,
movementY = 0,
offsetX = 0,
offsetY = 0,
pageX,
pageY,
pointerId,
pressure = 0,
preventDefault = emptyFunction,
pointerType = 'mouse',
screenX,
screenY,
shiftKey = false,
tangentialPressure = 0,
tiltX = 0,
tiltY = 0,
timeStamp,
twist = 0,
width,
x = 0,
y = 0,
} = {},
) {
const modifierState = {altKey, ctrlKey, metaKey, shiftKey};
const isMouse = pointerType === 'mouse';
return createEvent(type, {
altKey,
button,
buttons,
clientX: x,
clientY: y,
ctrlKey,
detail,
getModifierState(keyArg) {
return createGetModifierState(keyArg, modifierState);
},
height: isMouse ? 1 : height != null ? height : defaultPointerSize,
metaKey,
movementX,
movementY,
offsetX,
offsetY,
pageX: pageX || x,
pageY: pageY || y,
pointerId,
pointerType,
pressure,
preventDefault,
releasePointerCapture: emptyFunction,
screenX: screenX === 0 ? screenX : x,
screenY: screenY === 0 ? screenY : y + defaultBrowserChromeSize,
setPointerCapture: emptyFunction,
shiftKey,
tangentialPressure,
tiltX,
tiltY,
timeStamp,
twist,
width: isMouse ? 1 : width != null ? width : defaultPointerSize,
});
}
function createKeyboardEvent(
type,
{
altKey = false,
ctrlKey = false,
isComposing = false,
key = '',
metaKey = false,
preventDefault = emptyFunction,
shiftKey = false,
} = {},
) {
const modifierState = {altKey, ctrlKey, metaKey, shiftKey};
return createEvent(type, {
altKey,
ctrlKey,
getModifierState(keyArg) {
return createGetModifierState(keyArg, modifierState);
},
isComposing,
key,
metaKey,
preventDefault,
shiftKey,
});
}
function createMouseEvent(
type,
{
altKey = false,
button = buttonType.none,
buttons = buttonsType.none,
ctrlKey = false,
detail = 1,
metaKey = false,
movementX = 0,
movementY = 0,
offsetX = 0,
offsetY = 0,
pageX,
pageY,
preventDefault = emptyFunction,
screenX,
screenY,
shiftKey = false,
timeStamp,
x = 0,
y = 0,
} = {},
) {
const modifierState = {altKey, ctrlKey, metaKey, shiftKey};
return createEvent(type, {
altKey,
button,
buttons,
clientX: x,
clientY: y,
ctrlKey,
detail,
getModifierState(keyArg) {
return createGetModifierState(keyArg, modifierState);
},
metaKey,
movementX,
movementY,
offsetX,
offsetY,
pageX: pageX || x,
pageY: pageY || y,
preventDefault,
screenX: screenX === 0 ? screenX : x,
screenY: screenY === 0 ? screenY : y + defaultBrowserChromeSize,
shiftKey,
timeStamp,
});
}
function createTouchEvent(type, payload) {
return createEvent(type, {
...payload,
detail: 0,
sourceCapabilities: {
firesTouchEvents: true,
},
});
}
/**
* Mock event objects
*/
export function blur({relatedTarget} = {}) {
return new FocusEvent('blur', {relatedTarget});
}
export function focusOut({relatedTarget} = {}) {
return new FocusEvent('focusout', {relatedTarget, bubbles: true});
}
export function click(payload) {
return createMouseEvent('click', {
button: buttonType.primary,
...payload,
});
}
export function contextmenu(payload) {
return createMouseEvent('contextmenu', {
...payload,
detail: 0,
});
}
export function dragstart(payload) {
return createMouseEvent('dragstart', {
...payload,
detail: 0,
});
}
export function focus({relatedTarget} = {}) {
return new FocusEvent('focus', {relatedTarget});
}
export function focusIn({relatedTarget} = {}) {
return new FocusEvent('focusin', {relatedTarget, bubbles: true});
}
export function scroll() {
return createEvent('scroll');
}
export function virtualclick(payload) {
return createMouseEvent('click', {
button: 0,
...payload,
buttons: 0,
detail: 0,
height: 1,
pageX: 0,
pageY: 0,
pressure: 0,
screenX: 0,
screenY: 0,
width: 1,
x: 0,
y: 0,
});
}
/**
* Key events
*/
export function keydown(payload) {
return createKeyboardEvent('keydown', payload);
}
export function keyup(payload) {
return createKeyboardEvent('keyup', payload);
}
/**
* Pointer events
*/
export function gotpointercapture(payload) {
return createPointerEvent('gotpointercapture', payload);
}
export function lostpointercapture(payload) {
return createPointerEvent('lostpointercapture', payload);
}
export function pointercancel(payload) {
return createPointerEvent('pointercancel', {
...payload,
buttons: 0,
detail: 0,
height: 1,
pageX: 0,
pageY: 0,
pressure: 0,
screenX: 0,
screenY: 0,
width: 1,
x: 0,
y: 0,
});
}
export function pointerdown(payload) {
const isTouch = payload != null && payload.pointerType === 'touch';
return createPointerEvent('pointerdown', {
button: buttonType.primary,
buttons: buttonsType.primary,
pressure: isTouch ? 1 : 0.5,
...payload,
});
}
export function pointerenter(payload) {
return createPointerEvent('pointerenter', payload);
}
export function pointerleave(payload) {
return createPointerEvent('pointerleave', payload);
}
export function pointermove(payload) {
return createPointerEvent('pointermove', {
...payload,
button: buttonType.none,
});
}
export function pointerout(payload) {
return createPointerEvent('pointerout', payload);
}
export function pointerover(payload) {
return createPointerEvent('pointerover', payload);
}
export function pointerup(payload) {
return createPointerEvent('pointerup', {
button: buttonType.primary,
...payload,
buttons: buttonsType.none,
pressure: 0,
});
}
/**
* Mouse events
*/
export function mousedown(payload) {
// The value of 'button' and 'buttons' for 'mousedown' must not be none.
const button =
payload == null || payload.button === buttonType.none
? buttonType.primary
: payload.button;
const buttons =
payload == null || payload.buttons === buttonsType.none
? buttonsType.primary
: payload.buttons;
return createMouseEvent('mousedown', {
...payload,
button,
buttons,
});
}
export function mouseenter(payload) {
return createMouseEvent('mouseenter', payload);
}
export function mouseleave(payload) {
return createMouseEvent('mouseleave', payload);
}
export function mousemove(payload) {
return createMouseEvent('mousemove', payload);
}
export function mouseout(payload) {
return createMouseEvent('mouseout', payload);
}
export function mouseover(payload) {
return createMouseEvent('mouseover', payload);
}
export function mouseup(payload) {
return createMouseEvent('mouseup', {
button: buttonType.primary,
...payload,
buttons: buttonsType.none,
});
}
/**
* Touch events
*/
export function touchcancel(payload) {
return createTouchEvent('touchcancel', payload);
}
export function touchend(payload) {
return createTouchEvent('touchend', payload);
}
export function touchmove(payload) {
return createTouchEvent('touchmove', payload);
}
export function touchstart(payload) {
return createTouchEvent('touchstart', payload);
}

View File

@ -0,0 +1,121 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
import {buttonType, buttonsType} from './constants';
import * as domEvents from './domEvents';
import * as domEventSequences from './domEventSequences';
import {hasPointerEvent, setPointerEvent, platform} from './domEnvironment';
import {describeWithPointerEvent, testWithPointerType} from './testHelpers';
const createEventTarget = node => ({
node,
/**
* Simple events abstraction.
*/
blur(payload) {
node.dispatchEvent(domEvents.blur(payload));
node.dispatchEvent(domEvents.focusOut(payload));
},
click(payload) {
node.dispatchEvent(domEvents.click(payload));
},
focus(payload) {
node.dispatchEvent(domEvents.focus(payload));
node.dispatchEvent(domEvents.focusIn(payload));
node.focus();
},
keydown(payload) {
node.dispatchEvent(domEvents.keydown(payload));
},
keyup(payload) {
node.dispatchEvent(domEvents.keyup(payload));
},
scroll(payload) {
node.dispatchEvent(domEvents.scroll(payload));
},
virtualclick(payload) {
node.dispatchEvent(domEvents.virtualclick(payload));
},
/**
* PointerEvent abstraction.
* Dispatches the expected sequence of PointerEvents, MouseEvents, and
* TouchEvents for a given environment.
*/
contextmenu(payload, options) {
domEventSequences.contextmenu(node, payload, options);
},
// node no longer receives events for the pointer
pointercancel(payload) {
domEventSequences.pointercancel(node, payload);
},
// node dispatches down events
pointerdown(payload) {
domEventSequences.pointerdown(node, payload);
},
// node dispatches move events (pointer is not down)
pointerhover(payload) {
domEventSequences.pointerhover(node, payload);
},
// node dispatches move events (pointer is down)
pointermove(payload) {
domEventSequences.pointermove(node, payload);
},
// node dispatches enter & over events
pointerenter(payload) {
domEventSequences.pointerenter(node, payload);
},
// node dispatches exit & leave events
pointerexit(payload) {
domEventSequences.pointerexit(node, payload);
},
// node dispatches up events
pointerup(payload) {
domEventSequences.pointerup(node, payload);
},
/**
* Gesture abstractions.
* Helpers for event sequences expected in a gesture.
* target.tap({ pointerType: 'touch' })
*/
tap(payload) {
domEventSequences.pointerdown(payload);
domEventSequences.pointerup(payload);
},
/**
* Utilities
*/
setBoundingClientRect({x, y, width, height}) {
node.getBoundingClientRect = function() {
return {
width,
height,
left: x,
right: x + width,
top: y,
bottom: y + height,
};
};
},
});
const resetActivePointers = domEventSequences.resetActivePointers;
export {
buttonType,
buttonsType,
createEventTarget,
describeWithPointerEvent,
platform,
hasPointerEvent,
resetActivePointers,
setPointerEvent,
testWithPointerType,
};

View File

@ -0,0 +1,5 @@
{
"private": true,
"name": "dom-event-testing-library",
"version": "0.0.0"
}

View File

@ -0,0 +1,35 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
import {hasPointerEvent, setPointerEvent} from './domEnvironment';
export function describeWithPointerEvent(message, describeFn) {
const pointerEvent = 'PointerEvent';
const fallback = 'MouseEvent/TouchEvent';
describe.each`
value | name
${true} | ${pointerEvent}
${false} | ${fallback}
`(`${message}: $name`, entry => {
const hasPointerEvents = entry.value;
setPointerEvent(hasPointerEvents);
describeFn(hasPointerEvents);
});
}
export function testWithPointerType(message, testFn) {
const table = hasPointerEvent()
? ['mouse', 'touch', 'pen']
: ['mouse', 'touch'];
test.each(table)(`${message}: %s`, pointerType => {
testFn(pointerType);
});
}

View File

@ -0,0 +1,82 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
/**
* Touch events state machine.
*
* Keeps track of the active pointers and allows them to be reflected in touch events.
*/
const activeTouches = new Map();
export function addTouch(touch) {
const identifier = touch.identifier;
const target = touch.target;
if (!activeTouches.has(target)) {
activeTouches.set(target, new Map());
}
if (activeTouches.get(target).get(identifier)) {
// Do not allow existing touches to be overwritten
console.error(
'Touch with identifier %s already exists. Did not record touch start.',
identifier,
);
} else {
activeTouches.get(target).set(identifier, touch);
}
}
export function updateTouch(touch) {
const identifier = touch.identifier;
const target = touch.target;
if (activeTouches.get(target) != null) {
activeTouches.get(target).set(identifier, touch);
} else {
console.error(
'Touch with identifier %s does not exist. Cannot record touch move without a touch start.',
identifier,
);
}
}
export function removeTouch(touch) {
const identifier = touch.identifier;
const target = touch.target;
if (activeTouches.get(target) != null) {
if (activeTouches.get(target).has(identifier)) {
activeTouches.get(target).delete(identifier);
} else {
console.error(
'Touch with identifier %s does not exist. Cannot record touch end without a touch start.',
identifier,
);
}
}
}
export function getTouches() {
const touches = [];
activeTouches.forEach((_, target) => {
touches.push(...getTargetTouches(target));
});
return touches;
}
export function getTargetTouches(target) {
if (activeTouches.get(target) != null) {
return Array.from(activeTouches.get(target).values());
}
return [];
}
export function clear() {
activeTouches.clear();
}

View File

@ -0,0 +1,7 @@
# memlab内存泄露分析
```shell
npm run runPage
npm run memlab
```

View File

@ -0,0 +1,8 @@
'use strict';
module.exports = {
presets: [
'@babel/preset-env',
'@babel/preset-react'
],
};

View File

@ -0,0 +1,20 @@
// initial page load's url
function url() {
return "http://localhost:9000/";
}
// action where you suspect the memory leak might be happening
async function action(page) {
await page.click('[href="#/table"]');
await page.click('[data-id="tr0-expand"]');
await page.click('[data-id="tr1-expand"]');
await page.click('[data-id="tr3-expand"]');
await page.click('[data-id="tr4-expand"]');
}
// how to go back to the state before action
async function back(page) {
await page.click('[href="#/"]');
}
module.exports = { action, back, url };

View File

@ -0,0 +1,26 @@
{
"devDependencies": {
"@cloudsop/eview-ui": "^0.1.65",
"babel-loader": "^8.2.2",
"clean-webpack-plugin": "^4.0.0-alpha.0",
"cross-env": "^7.0.3",
"css-loader": "3.4.2",
"express": "^4.17.1",
"memlab": "1.1.28",
"html-webpack-plugin": "4.4.1",
"react-router-dom": "^5.2.0",
"style-loader": "1.0.0",
"url-loader": "^4.1.1",
"webpack": "^4.46.0",
"webpack-dev-server": "^3.10.3",
"webpack-cli": "3.3.6"
},
"scripts": {
"memlab": "memlab run --scenario ./memlab/scenario.js",
"runPage": "webpack-dev-server --config ./webpack.config.js --mode development"
},
"dependencies": {
"@cloudsop/horizon": "0.0.22",
"memlab": "^1.1.28"
}
}

View File

@ -0,0 +1,105 @@
import React, { useState } from 'react';
import { Form, TextField, RadioGroup, MultipleSelect, Toggle, GridLayout } from '@cloudsop/eview-ui';
const { Row, Col } = GridLayout;
export default function FormDemo() {
const [name, setName] = useState('');
const [toggle, setToggle] = useState(1);
const [cellphone, setCellphone] = useState('');
const [gender, setGender] = useState(1);
const [category, setCategory] = useState([]);
let props1 = {
action: '',
title: 'Single Column Form',
style: {
border: '1px solid #ccc',
}
};
let textFiledProps = {
name: 'name',
label: 'Name:',
required: true,
value: name,
className: 'eui_form_ctrl',
onChange: v => setName(v)
};
let toggleProps = {
name: 'toggle',
label: 'toggleProps:',
data: [1, 2],
value: toggle,
required: true,
className: 'eui_form_ctrl',
onChange: v => setToggle(v)
};
let numberTextFiledProps = {
required: true,
name: 'cellphone',
label: 'Cellphone number:',
value: cellphone,
className: 'eui_form_ctrl',
onChange: v => setCellphone(v)
};
let genderRadioGroupProps = {
name: 'gender',
value: gender,
required: true,
data: [{ value: 1, text: 'MALE' }, { value: 2, text: 'FEMALE' }],
label: 'Gender:',
className: 'eui_form_ctrl',
onChange: v => setGender(v)
};
let categoryMultipleSelectProps = {
name: 'category',
label: 'category:',
required: true,
selectedValue: category,
options: [
{ value: '1', text: 'The internet' },
{ value: '2', text: 'Network' },
{ value: '3', text: 'item 03' },
{ value: '4', text: 'item 04' },
{ value: '5', text: 'item 05' },
{ value: '6', text: 'item 06' },
{ value: '7', text: 'item 07' },
{ value: '8', text: 'item 08' },
{ value: '9', text: 'item 09' },
{ value: '10', text: 'item 10' }
],
className: 'eui_form_ctrl',
onChange: v => setCategory(v)
};
return (
<div style={{ margin: '20px' }}>
<Form {...props1}>
<Row totalCols={12}>
<Col cols={12}>
<TextField {...textFiledProps} id="text-field1" />
</Col>
</Row>
<Row totalCols={12}>
<Col cols={12}>
<RadioGroup {...genderRadioGroupProps} id="text-field2" />
</Col>
</Row>
<Row totalCols={12}>
<Col cols={12}>
<MultipleSelect {...categoryMultipleSelectProps} id="multip-select1" />
</Col>
</Row>
<Row totalCols={12}>
<Col cols={12}>
<TextField {...numberTextFiledProps} id="text-field3" />
</Col>
</Row>
<Row totalCols={12}>
<Col cols={12}>
<Toggle {...toggleProps} id="toggle1" />
</Col>
</Row>
</Form>
</div>
)
}

View File

@ -0,0 +1,18 @@
import React from 'react';
import { Link } from 'react-router-dom';
import '../less/home.less';
export default function Home() {
return (
<>
<Link to="/" className="link">Home</Link>
<Link to="/layout" className="link">Layout</Link>
<Link to="/form" className="link">Form</Link>
<Link to="/wizards" className="link">Wizards</Link>
<Link to="/table" className="link">Table</Link>
<Link to="/tree" className="link">Tree</Link>
<Link to="/panel" className="link">Panel</Link>
<br />
</>
);
}

View File

@ -0,0 +1,39 @@
import React from 'react';
import { LabelField, Layout } from '@cloudsop/eview-ui';
export default function LayoutDemo() {
let leftPanel = <div id="leftPanel" key="leftPanel" className={'eui_layout_fix'} style={{ background: '#ffffff', width: '200px' }} />;
let rightPanel = <div id="rightPanel" key="rightPanel" style={{ background: '#ffffff' }} />;
let topPanel = <div id="topPanel" key="topPanel" style={{ background: '#ffffff' }} />;
let bottomPanel = <div id="bottomPanel" key="bottomPanel" style={{ background: '#ffffff' }} />;
let horizontalLayout = <Layout type={'horizontal'} leftPanel={leftPanel} rightPanel={rightPanel} />
let verticalLayout = <Layout type={'vertical'} leftPanel={topPanel} rightPanel={bottomPanel} />
return (
<div id="LayoutDemo" style={{ margin: '20px' }}>
<LabelField text={'Type : Full'} />
<div id="full" style={{ background: '#e8e8e8', height: '200px' }}>
<Layout type={'full'} style={{ background: '#fff' }} />
</div>
<LabelField text={'Type : horizontal'} />
<div id="horizontal" style={{ background: '#e8e8e8', height: '200px' }}>
{horizontalLayout}
</div>
<LabelField text={'Type : vertical'} />
<div id="vertical" style={{ background: '#e8e8e8', height: '200px' }}>
{verticalLayout}
</div>
<LabelField text={'Combined Layout'} />
<div id="Combined" style={{ background: '#e8e8e8', height: '200px' }}>
<Layout type={'horizontal'}
leftPanel={<div id="CombinedLeft" key="CombinedLeft" className={'eui_layout_fix'} style={{ background: '#fff', width: '100px', marginRight: '20px' }} />}
rightPanel={verticalLayout} />
</div>
</div>
);
}

View File

@ -0,0 +1,44 @@
import React from 'react';
import { DivMessage } from '@cloudsop/eview-ui';
import PanelE from '@cloudsop/eview-ui/Panel';
const { Panel, PanelItem } = PanelE;
export default class PanelEventExample extends React.Component {
title = () => {
return <div>Custom Title Area</div>
};
state = {
selectedIndex: [0],
msg: '',
};
handlePanelExpand = (index) => {
this.setState({ msg: 'onExpand event callback on Panel, Index Id : ' + index,selectedIndex: [index]})
};
handlePanelClose = (index,e,collapsed) => {
this.setState({ msg: 'onClose event callback on Panel, Index Id : ' + index + ' collpased : ' + collapsed, selectedIndex:[] })
};
render() {
return (<div style={{ width: 500 }}>
<Panel enableMultiExpand={true} selectedIndex={this.state.selectedIndex} onExpand={this.handlePanelExpand} onClose={this.handlePanelClose}>
<PanelItem title={this.title()} >
<div style={{ padding: 50 }}>Content of My Folding Panel 1</div>
</PanelItem>
<PanelItem title={this.title()}>
<div style={{ padding: 50 }}>Content of My Folding Panel 2</div>
</PanelItem>
</Panel>
{this.state.msg ? <DivMessage text={this.state.msg} type="success" style={{ marginTop: '10px', width: '600px' }} /> : ''}
</div>)
}
}

View File

@ -0,0 +1,147 @@
/* eslint-disable no-shadow */
/* eslint-disable react-internal/no-production-logging */
/* eslint-disable no-unused-expressions */
import React from 'react';
import { Table } from '@cloudsop/eview-ui';
const rows = [];
rows.push(['R&D VPN2', 'TO BE', 3, 'ICMP Echo']);
rows.push(['Shop Storage', 'Active', 3, 'ICMP Echo']);
rows.push(['IT Link', 'Active', 7, 'Critical']);
rows.push(['R&D VPN1', 'Active', 3, 'Major']);
rows.push(['R&D VPN6', 'TO BE', 5, 'ICMP Echo']);
const childTableRowCheckRecord = {};
export default class TableBasic extends React.Component {
state = {
checkedRows: []
};
handleHeaderCheck = (row, checkedRows, e) => {
console.log('header checked...')
};
handleRowCheckForChildTable = (selects, curParentRowIndex) => {
childTableRowCheckRecord[curParentRowIndex] = selects;
};
handleRowExpend = (row) => {
let columns = [
{
title: 'VPN Name',
width: 300,
key: 'col_1',
},
{
title: 'State',
width: 300,
key: 'col_2',
},
{
title: 'Site',
width: 300,
key: 'col_3',
},
{
title: 'Alarm',
allowSort: false,
key: 'col_4',
},
];
const rowsdata = [];
for (let i = 0; i < 5; i++) {
rowsdata.push([`R&D VPN2${i}`, 'Active', i, 'ICMP Echo']);
}
return (<div style={{ padding: '20px', background: '#efefef' }}>
<Table
columns={columns}
dataset={rowsdata}
enableCheckBox={true}
headerCheckBoxSortAllow={true}
checkedRows={childTableRowCheckRecord[row.rowIndex]}
onRowCheck={(curRow, rows) => this.handleRowCheckForChildTable(rows, row.rowIndex)}
ref={innerTable => {
this.innerTable = innerTable;
}} /></div>);
}
handleItemClick1 = (item) => {
let res = [];
if (item.value === 1) {
rows.forEach((_, index) => res.push(index));
} else if (item.value === 2) {
rows.forEach((_, index) => {
(index + 1) % 2 !== 0 && res.push(index);
});
} else if (item.value === 3) {
rows.forEach((_, index) => {
(index + 1) % 2 === 0 && res.push(index);
});
}
this.setState({ checkedRows: res })
};
renderStateColumn(cell) {
if (cell === 'TO BE') {
return (
<div>
TO BE
</div>
);
}
return (
<div>
Active
</div>
);
}
render() {
const columns = [
{
title: 'VPN Name',
key: 'c_1',
id: 'col_1',
},
{
title: 'State',
key: 'c_2',
render: cell => this.renderStateColumn(cell),
allowSort: false,
id: 'col_2',
},
{
title: 'Site',
display: true, // Hide column,
id: 'col_3',
key: 'c_3',
},
{
title: 'Alarm',
key: 'c_4',
},
];
let options = [{ text: 'Select All Data', value: 1 },
{ text: 'Select Odd Rows', value: 2 },
{ text: 'Select Even Rows', value: 3 }];
return (
<div>
<Table
columns={columns} // Column Details
dataset={rows} // Data Set
selectedRowIndex={2} // Row ID 2 is selected.
enableColumnFilter={true} //Allow column to be selected.
checkBoxOptions={options}
onRowExpend={this.handleRowExpend}
onRowCheck={this.handleHeaderCheck}
enableCheckBox={true}
checkedRows={this.state.checkedRows}
enableRowExpand={true}
disableRowExpand={[2]}
checkBoxPopupData={{ data: options, optionStyle: { width: '15rem' }, onItemClick: this.handleItemClick1 }}
/>
</div>
);
}
}

View File

@ -0,0 +1,69 @@
import React from 'react';
import { DivMessage, Tree } from '@cloudsop/eview-ui';
export default class TreeExample extends React.Component {
state = {
checkedKeys: [],
selectedKeys: [1],
expandedKeys: [1],
msg: ''
};
handleSelect = (selectedKeys, node, event) => {
let eventType = event.type;
this.setState({ selectedKeys: selectedKeys, msg: `OnSelect event callback: node id is ${node.props.eventKey}, event type is ${eventType}` });
};
handleCheck = (checkedKeys, node) => {
this.setState({ checkedKeys: checkedKeys, msg: 'OnCheck event callback: node id is' + node.props.eventKey });
};
handleExpand = (expandedKeys, node) => {
this.setState({ expandedKeys: expandedKeys, msg: 'OnExpand event callback: node id is' + node.props.eventKey });
};
render() {
let data = [
{
text: 'Root Node', id: 1,
children: [
{
text: 'Subnet0', id: 11,
children: [
{
text: 'NE_1', id: 21,
children: [
{ text: 'NE_1', id: 31 },
{ text: 'NE_2', id: 32 }]
},
{ text: 'NE_2', id: 22 }]
},
{
text: 'NE_3', id: 12,
children: [
{ text: 'NE_1', id: 23 },
{ text: 'NE_2', id: 24 }]
}]
}
];
return (
<div>
<Tree
data={data}
nodeKey={'id'}
enableCheckbox={true}
onSelect={this.handleSelect}
onCheck={this.handleCheck}
onExpand={this.handleExpand}
checkedKeys={this.state.checkedKeys}
selectedKeys={this.state.selectedKeys}
style={{ width: '400px', height: '600px' }}
expandedKeys={this.state.expandedKeys}
treeTextStyle={{ width: '400px' }}
/>
{this.state.msg ? <DivMessage text={this.state.msg} type="success" style={{ marginTop: '10px', width: '560px' }} /> : ''}
</div>);
}
}

View File

@ -0,0 +1,37 @@
import React from 'react';
import { Button, Wizards } from '@cloudsop/eview-ui';
export default class WizardsDemo extends React.Component {
state = {
index: 1,
disabled: false
};
next = () => {
this.setState({
index: this.state.index + 1
})
};
pre = () => {
this.setState({
index: this.state.index - 1
})
};
data = [{ text: 'Basic Information', value: '1' }, { text: 'Condition', value: '2' }, { text: 'Action', value: '3' }]
render() {
const param = {
data: this.data,
currentStep: this.data[this.state.index].value
};
const { disabled } = this.state
return (
<div style={{ marginLeft: '20px' }}>
<Wizards {...param} className="eui_wizard_container_sm" disabled={disabled} /><br />
<Button onClick={() => this.setState({ disabled: !disabled })} text={'disabled'} style={{ marginRight: '20px' }}>disabled</Button>
<Button onClick={this.pre} text={'Previous'} style={{ marginRight: '20px' }} disabled={this.state.index === 0 || disabled} />
<Button onClick={this.next} text={'Next'} disabled={this.state.index === this.data.length - 1 || disabled} />
</div>
)
}
}

View File

@ -0,0 +1,46 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { HashRouter as Router, Route } from 'react-router-dom';
import '@cloudsop/eview-ui/style/aui2.less';
import Home from './Home';
import LayoutDemo from './LayoutDemo';
import FormDemo from './FormDemo';
import WizardsDemo from './WizardsDemo';
import TableDemo from './TableDemo';
import TreeDemo from './TreeDemo';
import PanelDemo from './PanelDemo';
function App() {
return (
<>
<Router>
<Route path="/">
<Home />
</Route>
<Route path="/layout">
<LayoutDemo />
</Route>
<Route path="/form">
<FormDemo />
</Route>
<Route path="/wizards">
<WizardsDemo />
</Route>
<Route path="/table">
<TableDemo />
</Route>
<Route path="/tree">
<TreeDemo />
</Route>
<Route path="/panel">
<PanelDemo />
</Route>
</Router>
</>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>e2e-test</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@ -0,0 +1,3 @@
.link {
margin-right: 1rem;
}

View File

@ -0,0 +1,54 @@
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
let config = {
mode: 'development',
entry: path.resolve(__dirname, 'src/components/index.jsx'),
output: {
path: path.resolve(__dirname, 'build'),
filename: 'bundle.js'
},
devServer: {
port: 9000,
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: [
{ loader: 'babel-loader' }
]
},
{
test: /\.less$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' },
{ loader: 'less-loader' }
]
},
{
test: /\.(svg|gif)$/,
use: ['url-loader']
}
]
},
resolve: {
extensions: ['.js', '.jsx'],
alias: {
'react': '@cloudsop/horizon',
'react-dom': '@cloudsop/horizon'
}
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: path.join(__dirname, `src/index.html`),
filename: 'index.html'
}),
],
devtool: 'source-map',
};
module.exports = config;

View File

@ -0,0 +1,16 @@
# react-cache
A basic cache for React applications. It also serves as a reference for more
advanced caching implementations.
This package is meant to be used alongside yet-to-be-released, experimental
React features. It's unlikely to be useful in any other context.
**Do not use in a real application.** We're publishing this early for
demonstration purposes.
**Use it at your own risk.**
# No, Really, It Is Unstable
The API ~~may~~ will change wildly between versions.

View File

@ -0,0 +1,12 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
'use strict';
export * from './src/ReactCacheOld';

View File

@ -0,0 +1,22 @@
{
"private": true,
"name": "react-cache",
"description": "A basic cache for React applications",
"version": "2.0.0-alpha.0",
"repository": {
"type" : "git",
"url" : "https://github.com/facebook/react.git",
"directory": "packages/react-cache"
},
"files": [
"LICENSE",
"README.md",
"build-info.json",
"index.js",
"cjs/",
"umd/"
],
"peerDependencies": {
"react": "^17.0.0"
}
}

View File

@ -0,0 +1,149 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import * as Scheduler from 'scheduler';
// Intentionally not named imports because Rollup would
// use dynamic dispatch for CommonJS interop named imports.
const {
runAsync,
unstable_IdlePriority: IdlePriority,
} = Scheduler;
type Entry<T> = {|
value: T,
onDelete: () => mixed,
previous: Entry<T>,
next: Entry<T>,
|};
export function createLRU<T>(limit: number) {
let LIMIT = limit;
// Circular, doubly-linked list
let first: Entry<T> | null = null;
let size: number = 0;
let cleanUpIsScheduled: boolean = false;
function scheduleCleanUp() {
if (cleanUpIsScheduled === false && size > LIMIT) {
// The cache size exceeds the limit. Schedule a callback to delete the
// least recently used entries.
cleanUpIsScheduled = true;
runAsync(cleanUp, IdlePriority);
}
}
function cleanUp() {
cleanUpIsScheduled = false;
deleteLeastRecentlyUsedEntries(LIMIT);
}
function deleteLeastRecentlyUsedEntries(targetSize: number) {
// Delete entries from the cache, starting from the end of the list.
if (first !== null) {
const resolvedFirst: Entry<T> = (first: any);
let last = resolvedFirst.previous;
while (size > targetSize && last !== null) {
const onDelete = last.onDelete;
const previous = last.previous;
last.onDelete = (null: any);
// Remove from the list
last.previous = last.next = (null: any);
if (last === first) {
// Reached the head of the list.
first = last = null;
} else {
(first: any).previous = previous;
previous.next = (first: any);
last = previous;
}
size -= 1;
// Call the destroy method after removing the entry from the list. If it
// throws, the rest of cache will not be deleted, but it will be in a
// valid state.
onDelete();
}
}
}
function add(value: Object, onDelete: () => mixed): Entry<Object> {
const entry = {
value,
onDelete,
next: (null: any),
previous: (null: any),
};
if (first === null) {
entry.previous = entry.next = entry;
first = entry;
} else {
// Append to head
const last = first.previous;
last.next = entry;
entry.previous = last;
first.previous = entry;
entry.next = first;
first = entry;
}
size += 1;
return entry;
}
function update(entry: Entry<T>, newValue: T): void {
entry.value = newValue;
}
function access(entry: Entry<T>): T {
const next = entry.next;
if (next !== null) {
// Entry already cached
const resolvedFirst: Entry<T> = (first: any);
if (first !== entry) {
// Remove from current position
const previous = entry.previous;
previous.next = next;
next.previous = previous;
// Append to head
const last = resolvedFirst.previous;
last.next = entry;
entry.previous = last;
resolvedFirst.previous = entry;
entry.next = resolvedFirst;
first = entry;
}
} else {
// Cannot access a deleted entry
// TODO: Error? Warning?
}
scheduleCleanUp();
return entry.value;
}
function setLimit(newLimit: number) {
LIMIT = newLimit;
scheduleCleanUp();
}
return {
add,
update,
access,
setLimit,
};
}

View File

@ -0,0 +1,184 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {PromiseLike} from 'inula/src/renderer/Types';
import * as React from 'horizon-external';
import {UseContextHookMapping} from 'inula/src/renderer/hooks/HookExternal';
import {createLRU} from './LRU';
type Suspender = {then(resolve: () => mixed, reject: () => mixed): mixed, ...};
type PendingResult = {|
status: 0,
value: Suspender,
|};
type ResolvedResult<V> = {|
status: 1,
value: V,
|};
type RejectedResult = {|
status: 2,
value: mixed,
|};
type Result<V> = PendingResult | ResolvedResult<V> | RejectedResult;
type Resource<I, V> = {
read(I): V,
preload(I): void,
...
};
const Pending = 0;
const Resolved = 1;
const Rejected = 2;
const HookMapping = UseContextHookMapping;
function readContext(Context) {
const dispatcher = HookMapping.val;
if (dispatcher === null) {
throw new Error(
'react-cache: read and preload may only be called from within a ' +
"component's render. They are not supported in event handlers or " +
'lifecycle methods.',
);
}
return dispatcher.readContext(Context);
}
function identityHashFn(input) {
if (isDev) {
if (
typeof input !== 'string' &&
typeof input !== 'number' &&
typeof input !== 'boolean' &&
input !== undefined &&
input !== null
) {
console.error(
'Invalid key type. Expected a string, number, symbol, or boolean, ' +
'but instead received: %s' +
'\n\nTo use non-primitive values as keys, you must pass a hash ' +
'function as the second argument to createResource().',
input,
);
}
}
return input;
}
const CACHE_LIMIT = 500;
const lru = createLRU(CACHE_LIMIT);
const entries: Map<Resource<any, any>, Map<any, any>> = new Map();
const CacheContext = React.createContext(null);
function accessResult<I, K, V>(
resource: any,
fetch: I => PromiseLike<V>,
input: I,
key: K,
): Result<V> {
let entriesForResource = entries.get(resource);
if (entriesForResource === undefined) {
entriesForResource = new Map();
entries.set(resource, entriesForResource);
}
const entry = entriesForResource.get(key);
if (entry === undefined) {
const thenable = fetch(input);
thenable.then(
value => {
if (newResult.status === Pending) {
const resolvedResult: ResolvedResult<V> = (newResult: any);
resolvedResult.status = Resolved;
resolvedResult.value = value;
}
},
error => {
if (newResult.status === Pending) {
const rejectedResult: RejectedResult = (newResult: any);
rejectedResult.status = Rejected;
rejectedResult.value = error;
}
},
);
const newResult: PendingResult = {
status: Pending,
value: thenable,
};
const newEntry = lru.add(newResult, deleteEntry.bind(null, resource, key));
entriesForResource.set(key, newEntry);
return newResult;
} else {
return (lru.access(entry): any);
}
}
function deleteEntry(resource, key) {
const entriesForResource = entries.get(resource);
if (entriesForResource !== undefined) {
entriesForResource.delete(key);
if (entriesForResource.size === 0) {
entries.delete(resource);
}
}
}
export function unstable_createResource<I, K: string | number, V>(
fetch: I => PromiseLike<V>,
maybeHashInput?: I => K,
): Resource<I, V> {
const hashInput: I => K =
maybeHashInput !== undefined ? maybeHashInput : (identityHashFn: any);
const resource = {
read(input: I): V {
// react-cache currently doesn't rely on context, but it may in the
// future, so we read anyway to prevent access outside of render.
const key = hashInput(input);
const result: Result<V> = accessResult(resource, fetch, input, key);
switch (result.status) {
case Pending: {
const suspender = result.value;
throw suspender;
}
case Resolved: {
const value = result.value;
return value;
}
case Rejected: {
const error = result.value;
throw error;
}
default:
// Should be unreachable
return (undefined: any);
}
},
preload(input: I): void {
// react-cache currently doesn't rely on context, but it may in the
// future, so we read anyway to prevent access outside of render.
const key = hashInput(input);
accessResult(resource, fetch, input, key);
},
};
return resource;
}
export function unstable_setGlobalCacheLimit(limit: number) {
lru.setLimit(limit);
}

View File

@ -0,0 +1,4 @@
# `react-noop-renderer`
This package is the renderer we use for debugging [Fiber](https://github.com/facebook/react/issues/6170).
It is not intended to be used directly.

View File

@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export * from './src/ReactNoop';

View File

@ -0,0 +1,33 @@
{
"name": "react-noop-renderer",
"version": "16.0.0",
"private": true,
"description": "React package for testing the Fiber, Fizz and Flight reconcilers.",
"main": "index.js",
"repository": {
"type" : "git",
"url" : "https://github.com/facebook/react.git",
"directory": "packages/react-noop-renderer"
},
"license": "MIT",
"dependencies": {
"object-assign": "^4.1.1",
"renderer": "*",
"react-client": "*"
},
"peerDependencies": {
"react": "^17.0.0"
},
"files": [
"LICENSE",
"README.md",
"build-info.json",
"index.js",
"persistent.js",
"flight-client.js",
"flight-modules.js",
"flight-server.js",
"flight-server-runtime.js",
"cjs/"
]
}

View File

@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export * from './src/ReactNoopPersistent';

View File

@ -0,0 +1,56 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
/**
* This is a renderer of React that doesn't have a render target output.
* It is useful to demonstrate the internals of the reconciler in isolation
* and for testing semantics of reconciliation separate from the host
* environment.
*/
import Reconciler from 'renderer';
import createReactNoop from './createReactNoop';
export const {
_Scheduler,
getChildren,
getPendingChildren,
getOrCreateRootContainer,
createRoot,
createBlockingRoot,
createLegacyRoot,
getChildrenAsJSX,
getPendingChildrenAsJSX,
createPortal,
render,
renderLegacySyncRoot,
renderToRootWithID,
unmountRootWithID,
findInstance,
flushNextYield,
flushWithHostCounters,
runWithHostCounters,
expire,
flushExpired,
asyncUpdates,
deferredUpdates,
syncUpdates,
discreteUpdates,
flushDiscreteUpdates,
flushSync,
flushPassiveEffects,
act,
dumpTree,
getRoot,
// TODO: Remove this after callers migrate to alternatives.
runSync,
} = createReactNoop(
Reconciler, // reconciler
true, // useMutation
);

View File

@ -0,0 +1,56 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
/**
* This is a renderer of React that doesn't have a render target output.
* It is useful to demonstrate the internals of the reconciler in isolation
* and for testing semantics of reconciliation separate from the host
* environment.
*/
import Reconciler from 'renderer';
import createReactNoop from './createReactNoop';
export const {
_Scheduler,
getChildren,
getPendingChildren,
getOrCreateRootContainer,
createRoot,
createBlockingRoot,
createLegacyRoot,
getChildrenAsJSX,
getPendingChildrenAsJSX,
createPortal,
render,
renderLegacySyncRoot,
renderToRootWithID,
unmountRootWithID,
findInstance,
flushNextYield,
flushWithHostCounters,
expire,
flushExpired,
batchedUpdates,
deferredUpdates,
unbatchedUpdates,
discreteUpdates,
flushDiscreteUpdates,
flushSync,
runAsyncEffects,
act,
dumpTree,
getRoot,
// TODO: Remove this once callers migrate to alternatives.
// This should only be used by React internals.
runSync,
} = createReactNoop(
Reconciler, // reconciler
false, // useMutation
);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
# `react-test-renderer`
This package provides an experimental React renderer that can be used to render React components to pure JavaScript objects, without depending on the DOM or a native mobile environment.
Essentially, this package makes it easy to grab a snapshot of the "DOM tree" rendered by a React DOM or React Native component without using a browser or jsdom.
Documentation:
[https://reactjs.org/docs/test-renderer.html](https://reactjs.org/docs/test-renderer.html)
Usage:
```jsx
const ReactTestRenderer = require('react-test-renderer');
const renderer = ReactTestRenderer.create(
<Link page="https://www.facebook.com/">Facebook</Link>
);
console.log(renderer.toJSON());
// { type: 'a',
// props: { href: 'https://www.facebook.com/' },
// children: [ 'Facebook' ] }
```
You can also use Jest's snapshot testing feature to automatically save a copy of the JSON tree to a file and check in your tests that it hasn't changed: https://facebook.github.io/jest/blog/2016/07/27/jest-14.html.

View File

@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export * from './src/ReactTestRenderer';

View File

@ -0,0 +1,39 @@
{
"name": "react-test-renderer",
"version": "17.0.0",
"description": "React package for snapshot testing.",
"main": "index.js",
"repository": {
"type": "git",
"url": "https://github.com/facebook/react.git",
"directory": "packages/react-test-renderer"
},
"keywords": [
"react",
"react-native",
"react-testing"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/facebook/react/issues"
},
"homepage": "https://reactjs.org/",
"dependencies": {
"object-assign": "^4.1.1",
"react-is": "^17.0.0",
"react-shallow-renderer": "^16.13.1",
"scheduler": "^0.20.0"
},
"peerDependencies": {
"react": "17.0.0"
},
"files": [
"LICENSE",
"README.md",
"build-info.json",
"index.js",
"shallow.js",
"cjs/",
"umd/"
]
}

View File

@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export {default} from 'react-shallow-renderer';

View File

@ -0,0 +1,207 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
let currentTime: number = 0;
let scheduledCallback: ((boolean, number) => void) | null = null;
let scheduledTimeout: (number => void) | null = null;
let timeoutTime: number = -1;
let yieldedValues: Array<mixed> | null = null;
let expectedNumberOfYields: number = -1;
let didStop: boolean = false;
let isFlushing: boolean = false;
let needsPaint: boolean = false;
let shouldYieldForPaint: boolean = false;
export function requestBrowserCallback(callback: boolean => void) {
scheduledCallback = callback;
}
export function isOverTime(): boolean {
if (
(expectedNumberOfYields !== -1 &&
yieldedValues !== null &&
yieldedValues.length >= expectedNumberOfYields) ||
(shouldYieldForPaint && needsPaint)
) {
// We yielded at least as many values as expected. Stop flushing.
didStop = true;
return true;
}
return false;
}
export function now(): number {
return currentTime;
}
export function reset() {
if (isFlushing) {
throw new Error('Cannot reset while already flushing work.');
}
currentTime = 0;
scheduledCallback = null;
scheduledTimeout = null;
timeoutTime = -1;
yieldedValues = null;
expectedNumberOfYields = -1;
didStop = false;
isFlushing = false;
needsPaint = false;
}
// Should only be used via an assertion helper that inspects the yielded values.
export function unstable_flushNumberOfYields(count: number): void {
if (isFlushing) {
throw new Error('Already flushing work.');
}
if (scheduledCallback !== null) {
const cb = scheduledCallback;
expectedNumberOfYields = count;
isFlushing = true;
try {
let hasMoreWork = true;
do {
hasMoreWork = cb(currentTime);
} while (hasMoreWork && !didStop);
if (!hasMoreWork) {
scheduledCallback = null;
}
} finally {
expectedNumberOfYields = -1;
didStop = false;
isFlushing = false;
}
}
}
export function unstable_flushUntilNextPaint(): void {
if (isFlushing) {
throw new Error('Already flushing work.');
}
if (scheduledCallback !== null) {
const cb = scheduledCallback;
shouldYieldForPaint = true;
needsPaint = false;
isFlushing = true;
try {
let hasMoreWork = true;
do {
hasMoreWork = cb(true, currentTime);
} while (hasMoreWork && !didStop);
if (!hasMoreWork) {
scheduledCallback = null;
}
} finally {
shouldYieldForPaint = false;
didStop = false;
isFlushing = false;
}
}
}
export function unstable_flushExpired() {
if (isFlushing) {
throw new Error('Already flushing work.');
}
if (scheduledCallback !== null) {
isFlushing = true;
try {
const hasMoreWork = scheduledCallback(false, currentTime);
if (!hasMoreWork) {
scheduledCallback = null;
}
} finally {
isFlushing = false;
}
}
}
export function unstable_flushAllWithoutAsserting(): boolean {
// Returns false if no work was flushed.
if (isFlushing) {
throw new Error('Already flushing work.');
}
if (scheduledCallback !== null) {
const cb = scheduledCallback;
isFlushing = true;
try {
let hasMoreWork = true;
do {
hasMoreWork = cb(true, currentTime);
} while (hasMoreWork);
if (!hasMoreWork) {
scheduledCallback = null;
}
return true;
} finally {
isFlushing = false;
}
} else {
return false;
}
}
export function unstable_clearYields(): Array<mixed> {
if (yieldedValues === null) {
return [];
}
const values = yieldedValues;
yieldedValues = null;
return values;
}
export function unstable_flushAll(): void {
if (yieldedValues !== null) {
throw new Error(
'Log is not empty. Assert on the log of yielded values before ' +
'flushing additional work.',
);
}
unstable_flushAllWithoutAsserting();
if (yieldedValues !== null) {
throw new Error(
'While flushing work, something yielded a value. Use an ' +
'assertion helper to assert on the log of yielded values, e.g. ' +
'expect(Scheduler).toFlushAndYield([...])',
);
}
}
export function unstable_yieldValue(value: mixed): void {
// eslint-disable-next-line react-internal/no-production-logging
if (console.log.name === 'disabledLog') {
// If console.log has been patched, we assume we're in render
// replaying and we ignore any values yielding in the second pass.
return;
}
if (yieldedValues === null) {
yieldedValues = [value];
} else {
yieldedValues.push(value);
}
}
export function unstable_advanceTime(ms: number) {
// eslint-disable-next-line react-internal/no-production-logging
if (console.log.name === 'disabledLog') {
// If console.log has been patched, we assume we're in render
// replaying and we ignore any time advancing in the second pass.
return;
}
currentTime += ms;
if (scheduledTimeout !== null && timeoutTime <= currentTime) {
scheduledTimeout(currentTime);
timeoutTime = -1;
scheduledTimeout = null;
}
}
export function requestPaint() {
needsPaint = true;
}

View File

@ -0,0 +1,9 @@
/**
* Used by act() to track whether you're inside an act() scope.
* @flow
*/
const IsSomeRendererActing = {
current: (false: boolean),
};
export default IsSomeRendererActing;

View File

@ -0,0 +1,42 @@
/**
* @flow
*/
const ReactDebugCurrentFrame = {};
let currentExtraStackFrame = (null: null | string);
export function setExtraStackFrame(stack: null | string) {
if (isDev) {
currentExtraStackFrame = stack;
}
}
if (isDev) {
ReactDebugCurrentFrame.setExtraStackFrame = function(stack: null | string) {
if (isDev) {
currentExtraStackFrame = stack;
}
};
// Stack implementation injected by the current renderer.
ReactDebugCurrentFrame.getCurrentStack = (null: null | (() => string));
ReactDebugCurrentFrame.getStackAddendum = function(): string {
let stack = '';
// Add an extra top frame while an element is being validated
if (currentExtraStackFrame) {
stack += currentExtraStackFrame;
}
// Delegate to the injected renderer-specific implementation
const impl = ReactDebugCurrentFrame.getCurrentStack;
if (impl) {
stack += impl() || '';
}
return stack;
};
}
export default ReactDebugCurrentFrame;

View File

@ -0,0 +1,168 @@
import * as Scheduler from 'scheduler';
import enqueueTask from 'react-test-renderer/src/enqueueTask';
import {asyncUpdates} from 'inula/src/renderer/Renderer';
import IsSomeRendererActing from 'react-test-renderer/src/IsSomeRendererActing';
let actingUpdatesScopeDepth = 0;
let didWarnAboutUsingActInProd = false;
const flushMockScheduler = Scheduler.unstable_flushAllWithoutAsserting;
const isSchedulerMocked = typeof flushMockScheduler === 'function';
// `act` testing API
//
// TODO: This is mostly a copy-paste from the legacy `act`, which does not have
// access to the same internals that we do here. Some trade offs in the
// implementation no longer make sense.
let isFlushingAct = false;
let isInsideThisAct = false;
// Returns whether additional work was scheduled. Caller should keep flushing
// until there's no work left.
function flushActWork(): boolean {
if (flushMockScheduler !== undefined) {
const prevIsFlushing = isFlushingAct;
isFlushingAct = true;
try {
return flushMockScheduler();
} finally {
isFlushingAct = prevIsFlushing;
}
} else {
// No mock scheduler available. However, the only type of pending work is
// passive effects, which we control. So we can flush that.
const prevIsFlushing = isFlushingAct;
isFlushingAct = true;
try {
let didFlushWork = false;
while (flushPassiveEffects()) {
didFlushWork = true;
}
return didFlushWork;
} finally {
isFlushingAct = prevIsFlushing;
}
}
}
function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) {
try {
flushActWork();
enqueueTask(() => {
if (flushActWork()) {
flushWorkAndMicroTasks(onDone);
} else {
onDone();
}
});
} catch (err) {
onDone(err);
}
}
// a 'shared' variable that changes when act() opens/closes in tests.
// export const IsThisRendererActing = { current: (false: boolean) };
export function act(callback: () => Thenable<mixed>): Thenable<void> {
if (!isDev) {
if (didWarnAboutUsingActInProd === false) {
didWarnAboutUsingActInProd = true;
// eslint-disable-next-line react-internal/no-production-logging
console.error(
'act(...)不支持在生产环境中使用',
);
}
}
actingUpdatesScopeDepth++;
const previousIsSomeRendererActing = IsSomeRendererActing.current;
const previousIsInsideThisAct = isInsideThisAct;
IsSomeRendererActing.current = true;
isInsideThisAct = true;
function onDone() {
actingUpdatesScopeDepth--;
IsSomeRendererActing.current = previousIsSomeRendererActing;
isInsideThisAct = previousIsInsideThisAct;
}
let result;
try {
result = asyncUpdates(callback);
} catch (error) {
// on sync errors, we still want to 'cleanup' and decrement actingUpdatesScopeDepth
onDone();
throw error;
}
if (
result !== null &&
typeof result === 'object' &&
typeof result.then === 'function'
) {
// setup a boolean that gets set to true only
// once this act() call is await-ed
let called = false;
// in the async case, the returned thenable runs the callback, flushes
// effects and microtasks in a loop until flushPassiveEffects() === false,
// and cleans up
return {
then(resolve, reject) {
called = true;
result.then(
() => {
if (
actingUpdatesScopeDepth > 1 ||
(isSchedulerMocked === true &&
previousIsSomeRendererActing === true)
) {
onDone();
resolve();
return;
}
// we're about to exit the act() scope,
// now's the time to flush tasks/effects
flushWorkAndMicroTasks((err: ?Error) => {
onDone();
if (err) {
reject(err);
} else {
resolve();
}
});
},
err => {
onDone();
reject(err);
},
);
},
};
} else {
// flush effects until none remain, and cleanup
try {
if (
actingUpdatesScopeDepth === 1 &&
(isSchedulerMocked === false || previousIsSomeRendererActing === false)
) {
// we're about to exit the act() scope,
// now's the time to flush effects
flushActWork();
}
onDone();
} catch (err) {
onDone();
throw err;
}
// in the sync case, the returned thenable only warns *if* await-ed
return {
then(resolve) {
resolve();
},
};
}
}

View File

@ -0,0 +1,239 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import {DomComponent, DomText} from 'inula/src/renderer/vnode/VNodeTags';
export type Type = string;
export type Props = Object;
export type Container = {|
children: Array<Instance | TextInstance>,
createNodeMock: Function,
tag: 'CONTAINER',
|};
export type Instance = {|
type: string,
props: Object,
isHidden: boolean,
children: Array<Instance | TextInstance>,
internalInstanceHandle: Object,
rootContainerInstance: Container,
tag: 'INSTANCE',
|};
export type TextInstance = {|
text: string,
isHidden: boolean,
tag: 'TEXT',
|};
export type HydratableInstance = Instance | TextInstance;
export type PublicInstance = Instance | TextInstance;
export type HostContext = Object;
export type UpdatePayload = Object;
export type NoTimeout = -1;
export type RendererInspectionConfig = $ReadOnly<{||}>;
const NO_CONTEXT = {};
const UPDATE_SIGNAL = {};
const nodeToInstanceMap = new WeakMap();
if (isDev) {
Object.freeze(NO_CONTEXT);
Object.freeze(UPDATE_SIGNAL);
}
export function appendChildElement(
// isContainer: boolean,
parentInstance: Instance | Container,
child: Instance | TextInstance,
) {
if (isDev) {
if (!Array.isArray(parentInstance.children)) {
console.error(
'An invalid container has been provided. ' +
'This may indicate that another renderer is being used in addition to the test renderer. ' +
'(For example, ReactDOM.createPortal inside of a ReactTestRenderer tree.) ' +
'This is not supported.',
);
}
}
const index = parentInstance.children.indexOf(child);
if (index !== -1) {
parentInstance.children.splice(index, 1);
}
parentInstance.children.push(child);
}
export function insertDomBefore(
// isContainer: boolean,
parentInstance: Instance | Container,
child: Instance | TextInstance,
beforeChild: Instance | TextInstance,
) {
const index = parentInstance.children.indexOf(child);
if (index !== -1) {
parentInstance.children.splice(index, 1);
}
const beforeIndex = parentInstance.children.indexOf(beforeChild);
parentInstance.children.splice(beforeIndex, 0, child);
}
export function insertBefore(
parentInstance: Instance | Container,
child: Instance | TextInstance,
beforeChild: Instance | TextInstance,
): void {
const index = parentInstance.children.indexOf(child);
if (index !== -1) {
parentInstance.children.splice(index, 1);
}
const beforeIndex = parentInstance.children.indexOf(beforeChild);
parentInstance.children.splice(beforeIndex, 0, child);
}
export function removeChildDom(
// isContainer: boolean,
parentInstance: Instance | Container,
child: Instance | TextInstance,
) {
const index = parentInstance.children.indexOf(child);
parentInstance.children.splice(index, 1);
}
export function clearContainer(container: Container): void {
container.children.splice(0);
}
export function getNSCtx(
rootContainerInstance: Container,
parentHostContext: HostContext,
type: string,
) {
return NO_CONTEXT;
}
export function prepareForSubmit(): void {
}
export function resetAfterSubmit(): void {
// noop
}
export function newDom(
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: Object,
internalInstanceHandle: Object,
): Instance {
return {
type:type,
props,
isHidden: false,
children: [],
internalInstanceHandle,
rootContainerInstance,
tag: 'INSTANCE',
};
}
export function initDomProps(
testElement: Instance,
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: Object,
): boolean {
return false;
}
export function getPropChangeList(
testElement: Instance,
type: string,
oldProps: Props,
newProps: Props,
rootContainerInstance: Container,
hostContext: Object,
): null | {...} {
return UPDATE_SIGNAL;
}
export function isTextChild(type: string, props: Props): boolean {
return false;
}
export function newTextDom(
text: string,
rootContainerInstance: Container,
hostContext: Object,
internalInstanceHandle: Object,
): TextInstance {
return {
text,
isHidden: false,
tag: 'TEXT',
};
}
export const isPrimaryRenderer = false;
export const warnsIfNotActing = true;
export const scheduleTimeout = setTimeout;
export const cancelTimeout = clearTimeout;
export const noTimeout = -1;
// -------------------
// Mutation
// -------------------
export function submitDomUpdate(
tag: string,
vNode: Object,
): void {
if (tag === DomComponent) {
const newProps = vNode.props;
const instance = vNode.realNode;
instance.type = vNode.type;
instance.props = newProps;
} else if (tag === DomText) {
const instance = vNode.realNode;
instance.text = vNode.props;
}
}
export function submitMount(
instance: Instance,
type: string,
newProps: Props,
internalInstanceHandle: Object,
): void {
// noop
}
export function clearText(testElement: Instance): void {
// noop
}
export function hideDom(tag: number, instance: Instance) : void {
instance.isHidden = true;
}
export function unHideDom(tag: number, instance: Instance, props: Props) :void {
instance.isHidden = false;
}
export function beforeActiveInstanceBlur() {
// noop
}
export function afterActiveInstanceBlur() {
// noop
}
export function prePortal(portalInstance: Instance): void {
// noop
}

View File

@ -0,0 +1,656 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {VNode, PromiseLike} from 'inula/src/renderer/Types';
import type {Instance, TextInstance} from './ReactTestHostConfig';
import * as Scheduler from 'react-test-renderer/src/Scheduler_mock';
import {
getFirstCustomDom,
createVNode,
startUpdate,
asyncUpdates,
} from 'inula/src/renderer/Renderer';
import {act} from './ReactFiberAct';
import {getSiblingVNode} from 'inula/src/renderer/vnode/VNodeUtils';
import {
Fragment,
FunctionComponent,
ClassComponent,
DomComponent,
DomPortal,
DomText,
TreeRoot,
ContextConsumer,
ContextProvider,
ForwardRef,
Profiler,
MemoComponent,
IncompleteClassComponent,
} from 'inula/src/renderer/vnode/VNodeTags';
import enqueueTask from 'react-test-renderer/src/enqueueTask';
import {throwIfTrue} from 'inula/src/renderer/utils/throwIfTrue';
import IsSomeRendererActing from 'react-test-renderer/src/IsSomeRendererActing';
type TestRendererOptions = {
createNodeMock: (element: React$Element<any>) => any,
unstable_isConcurrent: boolean,
...
};
type ReactTestRendererJSON = {|
type: string,
props: {[propName: string]: any, ...},
children: null | Array<ReactTestRendererNode>,
vtype?: Symbol, // Optional because we add it with defineProperty().
|};
type ReactTestRendererNode = ReactTestRendererJSON | string;
type FindOptions = $Shape<{
// performs a "greedy" search: if a matching node is found, will continue
// to search within the matching node's children. (default: true)
deep: boolean,
...
}>;
export type Predicate = (node: ReactTestInstance) => ?boolean;
const defaultTestOptions = {
createNodeMock: function() {
return null;
},
};
function toJSON(inst: Instance | TextInstance): ReactTestRendererNode | null {
if (inst.isHidden) {
// Omit timed out children from output entirely. This seems like the least
// surprising behavior. We could perhaps add a separate API that includes
// them, if it turns out people need it.
return null;
}
switch (inst.tag) {
case 'TEXT':
return inst.text;
case 'INSTANCE': {
/* eslint-disable no-unused-vars */
// We don't include the `children` prop in JSON.
// Instead, we will include the actual rendered children.
const {children, ...props} = inst.props;
/* eslint-enable */
let renderedChildren = null;
if (inst.children && inst.children.length) {
for (let i = 0; i < inst.children.length; i++) {
const renderedChild = toJSON(inst.children[i]);
if (renderedChild !== null) {
if (renderedChildren === null) {
renderedChildren = [renderedChild];
} else {
renderedChildren.push(renderedChild);
}
}
}
}
const json: ReactTestRendererJSON = {
type: inst.type,
props: props,
children: renderedChildren,
};
Object.defineProperty(json, 'vtype', {
value: Symbol.for('react.test.json'),
});
return json;
}
default:
throw new Error(`Unexpected node type in toJSON: ${inst.tag}`);
}
}
function childrenToTree(node) {
if (!node) {
return null;
}
const children = nodeAndSiblingsArray(node);
if (children.length === 0) {
return null;
} else if (children.length === 1) {
return toTree(children[0]);
}
return flatten(children.map(toTree));
}
function nodeAndSiblingsArray(nodeWithSibling) {
const array = [];
let node = nodeWithSibling;
while (node != null) {
array.push(node);
node = node.next;
}
return array;
}
function flatten(arr) {
const result = [];
const stack = [{i: 0, array: arr}];
while (stack.length) {
const n = stack.pop();
while (n.i < n.array.length) {
const el = n.array[n.i];
n.i += 1;
if (Array.isArray(el)) {
stack.push(n);
stack.push({i: 0, array: el});
break;
}
result.push(el);
}
}
return result;
}
function toTree(node: ?VNode) {
if (node == null) {
return null;
}
switch (node.tag) {
case TreeRoot:
return childrenToTree(node.child);
case DomPortal:
return childrenToTree(node.child);
case ClassComponent:
return {
nodeType: 'component',
type: node.type,
// props: {...node.oldProps},
props: {...node.props},
instance: node.realNode,
rendered: childrenToTree(node.child),
};
case FunctionComponent:
return {
nodeType: 'component',
type: node.type,
props: {...node.props},
instance: null,
rendered: childrenToTree(node.child),
};
case DomComponent: {
return {
nodeType: 'host',
type: node.type,
props: {...node.props},
instance: null, // TODO: use createNodeMock here somehow?
rendered: flatten(nodeAndSiblingsArray(node.child).map(toTree)),
};
}
case DomText:
return node.realNode.text;
case Fragment:
case ContextProvider:
case ContextConsumer:
case Profiler:
case ForwardRef:
case MemoComponent:
case IncompleteClassComponent:
default:
throwIfTrue(
true,
'toTree() does not yet know how to handle nodes with tag=%s',
node.tag,
);
}
}
const validWrapperTypes = new Set([
FunctionComponent,
ClassComponent,
DomComponent,
ForwardRef,
MemoComponent,
// Normally skipped, but used when there's more than one root child.
TreeRoot,
]);
function getChildren(parent: VNode) {
const children = [];
const startingNode = parent;
let node: VNode = startingNode;
if (node.child === null) {
return children;
}
node.child.parent = node;
node = node.child;
outer: while (true) {
let descend = false;
if (validWrapperTypes.has(node.tag)) {
children.push(wrapFiber(node));
} else if (node.tag === DomText) {
children.push('' + node.props);
} else {
descend = true;
}
if (descend && node.child !== null) {
node.child.parent = node;
node = node.child;
continue;
}
while (node.next === null) {
if (node.parent === startingNode) {
break outer;
}
node = (node.parent: any);
}
(node.next: any).parent = node.parent;
node = (node.next: any);
}
return children;
}
class ReactTestInstance {
_fiber: VNode;
_currentFiber(): VNode {
// Throws if this component has been unmounted.
const fiber = this._fiber;
throwIfTrue(
fiber === null,
"Can't read from currently-mounting component. This error is likely " +
'caused by a bug in React. Please file an issue.',
);
return fiber;
return this._fiber
}
constructor(fiber: VNode) {
throwIfTrue(
!validWrapperTypes.has(fiber.tag),
'Unexpected object passed to ReactTestInstance constructor (tag: %s). ' +
'This is probably a bug in React.',
fiber.tag,
);
this._fiber = fiber;
}
get type() {
return this._fiber.type;
}
get props(): Object {
return this._currentFiber().props;
}
get parent(): ?ReactTestInstance {
let parent = this._fiber.parent;
while (parent !== null) {
if (validWrapperTypes.has(parent.tag)) {
if (parent.tag === TreeRoot) {
// Special case: we only "materialize" instances for roots
// if they have more than a single child. So we'll check that now.
if (getChildren(parent).length < 2) {
return null;
}
}
return wrapFiber(parent);
}
parent = parent.parent;
}
return null;
}
get children(): Array<ReactTestInstance | string> {
return getChildren(this._currentFiber());
}
// Custom search functions
find(predicate: Predicate): ReactTestInstance {
return expectOne(
this.findAll(predicate, {deep: false}),
`matching custom predicate: ${predicate.toString()}`,
);
}
findByType(type: any): ReactTestInstance {
return expectOne(
this.findAllByType(type, {deep: false}),
`with node type: "Unknown"`,
);
}
findByProps(props: Object): ReactTestInstance {
return expectOne(
this.findAllByProps(props, {deep: false}),
`with props: ${JSON.stringify(props)}`,
);
}
findAll(
predicate: Predicate,
options: ?FindOptions = null,
): Array<ReactTestInstance> {
return findAll(this, predicate, options);
}
findAllByType(
type: any,
options: ?FindOptions = null,
): Array<ReactTestInstance> {
return findAll(this, node => node.type === type, options);
}
findAllByProps(
props: Object,
options: ?FindOptions = null,
): Array<ReactTestInstance> {
return findAll(
this,
node => node.props && propsMatch(node.props, props),
options,
);
}
}
function findAll(
root: ReactTestInstance,
predicate: Predicate,
options: ?FindOptions,
): Array<ReactTestInstance> {
const deep = options ? options.deep : true;
const results = [];
if (predicate(root)) {
results.push(root);
if (!deep) {
return results;
}
}
root.children.forEach(child => {
if (typeof child === 'string') {
return;
}
results.push(...findAll(child, predicate, options));
});
return results;
}
function expectOne(
all: Array<ReactTestInstance>,
message: string,
): ReactTestInstance {
if (all.length === 1) {
return all[0];
}
const prefix =
all.length === 0
? 'No instances found '
: `Expected 1 but found ${all.length} instances `;
throw new Error(prefix + message);
}
function propsMatch(props: Object, filter: Object): boolean {
for (const key in filter) {
if (props[key] !== filter[key]) {
return false;
}
}
return true;
}
function create(element: React$Element<any>, options: TestRendererOptions) {
let createNodeMock = defaultTestOptions.createNodeMock;
if (typeof options === 'object' && options !== null) {
if (typeof options.createNodeMock === 'function') {
createNodeMock = options.createNodeMock;
}
}
let container = {
children: [],
createNodeMock,
tag: 'CONTAINER',
};
let root: VNode | null = createVNode(TreeRoot, container);
throwIfTrue(root == null, 'something went wrong');
startUpdate(element, root, null);
const entry = {
_Scheduler: Scheduler,
root: undefined, // makes flow happy
// we define a 'getter' for 'root' below using 'Object.defineProperty'
toJSON(): Array<ReactTestRendererNode> | ReactTestRendererNode | null {
if (root == null || container == null) {
return null;
}
if (container.children.length === 0) {
return null;
}
if (container.children.length === 1) {
return toJSON(container.children[0]);
}
if (
container.children.length === 2 &&
container.children[0].isHidden === true &&
container.children[1].isHidden === false
) {
// Omit timed out children from output entirely, including the fact that we
// temporarily wrap fallback and timed out children in an array.
return toJSON(container.children[1]);
}
let renderedChildren = null;
if (container.children && container.children.length) {
for (let i = 0; i < container.children.length; i++) {
const renderedChild = toJSON(container.children[i]);
if (renderedChild !== null) {
if (renderedChildren === null) {
renderedChildren = [renderedChild];
} else {
renderedChildren.push(renderedChild);
}
}
}
}
return renderedChildren;
},
toTree() {
if (root == null) {
return null;
}
return toTree(root);
},
update(newElement: React$Element<any>) {
if (root == null) {
return;
}
startUpdate(newElement, root, null);
},
unmount() {
if (root == null) {
return;
}
startUpdate(null, root, null);
container = null;
root = null;
},
getInstance() {
if (root == null) {
return null;
}
return getFirstCustomDom(root);
},
unstable_flushSync<T>(fn: () => T): T {
return fn();
},
};
Object.defineProperty(
entry,
'root',
({
configurable: true,
enumerable: true,
get: function() {
if (root === null) {
throw new Error("Can't access .root on unmounted test renderer");
}
const children = getChildren(root);
if (children.length === 0) {
throw new Error("Can't access .root on unmounted test renderer");
} else if (children.length === 1) {
// Normally, we skip the root and just give you the child.
return children[0];
} else {
// However, we give you the root if there's more than one root child.
// We could make this the behavior for all cases but it would be a breaking change.
return wrapFiber(root);
}
},
}: Object),
);
return entry;
}
const fiberToWrapper = new WeakMap();
function wrapFiber(fiber: VNode): ReactTestInstance {
let wrapper = fiberToWrapper.get(fiber);
if (wrapper === undefined && fiber.twins !== null) {
wrapper = fiberToWrapper.get(fiber.twins);
}
if (wrapper === undefined) {
wrapper = new ReactTestInstance(fiber);
fiberToWrapper.set(fiber, wrapper);
}
return wrapper;
}
let actingUpdatesScopeDepth = 0;
// This version of `act` is only used by our tests. Unlike the public version
// of `act`, it's designed to work identically in both production and
// development. It may have slightly different behavior from the public
// version, too, since our constraints in our test suite are not the same as
// those of developers using React — we're testing React itself, as opposed to
// building an app with React.
// TODO: Migrate our tests to use ReactNoop. Although we would need to figure
// out a solution for Relay, which has some Concurrent Mode tests.
function unstable_concurrentAct(scope: () => PromiseLike<mixed> | void) {
if (Scheduler.unstable_flushAllWithoutAsserting === undefined) {
throw Error(
'This version of `act` requires a special mock build of Scheduler.',
);
}
if (setTimeout._isMockFunction !== true) {
throw Error(
"This version of `act` requires Jest's timer mocks " +
'(i.e. jest.useFakeTimers).',
);
}
const previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
const previousIsSomeRendererActing = IsSomeRendererActing.current;
IsSomeRendererActing.current = true;
actingUpdatesScopeDepth++;
const unwind = () => {
actingUpdatesScopeDepth--;
IsSomeRendererActing.current = previousIsSomeRendererActing;
if (isDev) {
if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
// if it's _less than_ previousActingUpdatesScopeDepth, then we can
// assume the 'other' one has warned
console.error(
'You seem to have overlapping act() calls, this is not supported. ' +
'Be sure to await previous act() calls before making a new one. ',
);
}
}
};
// TODO: This would be way simpler if 1) we required a promise to be
// returned and 2) we could use async/await. Since it's only our used in
// our test suite, we should be able to.
try {
const thenable = asyncUpdates(scope);
if (
typeof thenable === 'object' &&
thenable !== null &&
typeof thenable.then === 'function'
) {
return {
then(resolve: () => void, reject: (error: mixed) => void) {
thenable.then(
() => {
flushActWork(
() => {
unwind();
resolve();
},
error => {
unwind();
reject(error);
},
);
},
error => {
unwind();
reject(error);
},
);
},
};
} else {
try {
// TODO: Let's not support non-async scopes at all in our tests. Need to
// migrate existing tests.
let didFlushWork;
do {
didFlushWork = Scheduler.unstable_flushAllWithoutAsserting();
} while (didFlushWork);
} finally {
unwind();
}
}
} catch (error) {
unwind();
throw error;
}
}
function flushActWork(resolve, reject) {
// Flush suspended fallbacks
// $FlowFixMe: Flow doesn't know about global Jest object
jest.runOnlyPendingTimers();
enqueueTask(() => {
try {
const didFlushWork = Scheduler.unstable_flushAllWithoutAsserting();
if (didFlushWork) {
flushActWork(resolve, reject);
} else {
resolve();
}
} catch (error) {
reject(error);
}
});
}
export {
Scheduler as _Scheduler,
create,
/* eslint-disable-next-line camelcase */
asyncUpdates as unstable_batchedUpdates,
act,
unstable_concurrentAct,
};

View File

@ -0,0 +1,22 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
export * from 'inula/src/renderer/taskExecutor/TaskExecutor';
export {
unstable_flushAllWithoutAsserting,
unstable_flushNumberOfYields,
unstable_flushExpired,
unstable_clearYields,
unstable_flushUntilNextPaint,
unstable_flushAll,
unstable_yieldValue,
unstable_advanceTime,
} from 'inula/src/renderer/taskExecutor/BrowserAsync';

View File

@ -0,0 +1,724 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
* @jest-environment node
*/
'use strict';
const ReactTestRenderer = require('react-test-renderer');
const React = require('horizon-external');
const prettyFormat = require('pretty-format');
// Isolate noop renderer
jest.resetModules();
const ReactNoop = require('react-noop-renderer');
const Scheduler = require('scheduler');
// Kind of hacky, but we nullify all the instances to test the tree structure
// with jasmine's deep equality function, and test the instances separate. We
// also delete children props because testing them is more annoying and not
// really important to verify.
function cleanNodeOrArray(node) {
if (!node) {
return;
}
if (Array.isArray(node)) {
node.forEach(cleanNodeOrArray);
return;
}
if (node && node.instance) {
node.instance = null;
}
if (node && node.props && node.props.children) {
// eslint-disable-next-line no-unused-vars
const {children, ...props} = node.props;
node.props = props;
}
if (Array.isArray(node.rendered)) {
node.rendered.forEach(cleanNodeOrArray);
} else if (typeof node.rendered === 'object') {
cleanNodeOrArray(node.rendered);
}
}
describe('ReactTestRenderer', () => {
it('renders a simple component', () => {
function Link() {
return <a role="link" />;
}
const renderer = ReactTestRenderer.create(<Link />);
expect(renderer.toJSON()).toEqual({
type: 'a',
props: {role: 'link'},
children: null,
});
});
it('renders a top-level empty component', () => {
function Empty() {
return null;
}
const renderer = ReactTestRenderer.create(<Empty />);
expect(renderer.toJSON()).toEqual(null);
});
it('exposes a type flag', () => {
function Link() {
return <a role="link" />;
}
const renderer = ReactTestRenderer.create(<Link />);
const object = renderer.toJSON();
expect(object.vtype).toBe(Symbol.for('react.test.json'));
// vtype should not be enumerable.
for (const key in object) {
if (object.hasOwnProperty(key)) {
expect(key).not.toBe('vtype');
}
}
});
it('can render a composite component', () => {
class Component extends React.Component {
render() {
return (
<div className="purple">
<Child />
</div>
);
}
}
const Child = () => {
return <moo />;
};
const renderer = ReactTestRenderer.create(<Component />);
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {className: 'purple'},
children: [{type: 'moo', props: {}, children: null}],
});
});
it('renders some basics with an update', () => {
let renders = 0;
class Component extends React.Component {
state = {x: 3};
render() {
renders++;
return (
<div className="purple">
{this.state.x}
<Child />
<Null />
</div>
);
}
componentDidMount() {
this.setState({x: 7});
}
}
const Child = () => {
renders++;
return <moo />;
};
const Null = () => {
renders++;
return null;
};
const renderer = ReactTestRenderer.create(<Component />);
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {className: 'purple'},
children: ['7', {type: 'moo', props: {}, children: null}],
});
expect(renders).toBe(6);
});
it('exposes the instance', () => {
class Mouse extends React.Component {
constructor() {
super();
this.state = {mouse: 'mouse'};
}
handleMoose() {
this.setState({mouse: 'moose'});
}
render() {
return <div>{this.state.mouse}</div>;
}
}
const renderer = ReactTestRenderer.create(<Mouse />);
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {},
children: ['mouse'],
});
const mouse = renderer.getInstance();
mouse.handleMoose();
expect(renderer.toJSON()).toEqual({
type: 'div',
children: ['moose'],
props: {},
});
});
it('updates types', () => {
const renderer = ReactTestRenderer.create(<div>mouse</div>);
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {},
children: ['mouse'],
});
renderer.update(<span>mice</span>);
expect(renderer.toJSON()).toEqual({
type: 'span',
props: {},
children: ['mice'],
});
});
it('updates children', () => {
const renderer = ReactTestRenderer.create(
<div>
<span key="a">A</span>
<span key="b">B</span>
<span key="c">C</span>
</div>,
);
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {},
children: [
{type: 'span', props: {}, children: ['A']},
{type: 'span', props: {}, children: ['B']},
{type: 'span', props: {}, children: ['C']},
],
});
renderer.update(
<div>
<span key="d">D</span>
<span key="c">C</span>
<span key="b">B</span>
</div>,
);
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {},
children: [
{type: 'span', props: {}, children: ['D']},
{type: 'span', props: {}, children: ['C']},
{type: 'span', props: {}, children: ['B']},
],
});
});
it('does the full lifecycle', () => {
const log = [];
class Log extends React.Component {
render() {
log.push('render ' + this.props.name);
return <div />;
}
componentDidMount() {
log.push('mount ' + this.props.name);
}
componentWillUnmount() {
log.push('unmount ' + this.props.name);
}
}
const renderer = ReactTestRenderer.create(<Log key="foo" name="Foo" />);
renderer.update(<Log key="bar" name="Bar" />);
renderer.unmount();
expect(log).toEqual([
'render Foo',
'mount Foo',
'render Bar',
'unmount Foo',
'mount Bar',
'unmount Bar',
]);
});
it('supports unmounting when using refs', () => {
class Foo extends React.Component {
render() {
return <div ref="foo" />;
}
}
const inst = ReactTestRenderer.create(<Foo />, {
createNodeMock: () => 'foo',
});
expect(() => inst.unmount()).not.toThrow();
});
it('supports unmounting inner instances', () => {
let count = 0;
class Foo extends React.Component {
componentWillUnmount() {
count++;
}
render() {
return <div />;
}
}
const inst = ReactTestRenderer.create(
<div>
<Foo />
</div>,
{
createNodeMock: () => 'foo',
},
);
expect(() => inst.unmount()).not.toThrow();
expect(count).toEqual(1);
});
it('supports error boundaries', () => {
const log = [];
class Angry extends React.Component {
render() {
log.push('Angry render');
throw new Error('Please, do not render me.');
}
componentDidMount() {
log.push('Angry componentDidMount');
}
componentWillUnmount() {
log.push('Angry componentWillUnmount');
}
}
class Boundary extends React.Component {
constructor(props) {
super(props);
this.state = {error: false};
}
render() {
log.push('Boundary render');
if (!this.state.error) {
return (
<div>
<button onClick={this.onClick}>ClickMe</button>
<Angry />
</div>
);
} else {
return <div>Happy Birthday!</div>;
}
}
componentDidMount() {
log.push('Boundary componentDidMount');
}
componentWillUnmount() {
log.push('Boundary componentWillUnmount');
}
onClick() {
/* do nothing */
}
componentDidCatch() {
log.push('Boundary componentDidCatch');
this.setState({error: true});
}
}
const renderer = ReactTestRenderer.create(<Boundary />);
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {},
children: ['Happy Birthday!'],
});
expect(log).toEqual([
'Boundary render',
'Angry render',
'Angry componentDidMount',
'Boundary componentDidMount',
'Angry componentWillUnmount',
'Boundary componentDidCatch',
'Boundary render',
]);
});
it('can update text nodes', () => {
class Component extends React.Component {
render() {
return <div>{this.props.children}</div>;
}
}
const renderer = ReactTestRenderer.create(<Component>Hi</Component>);
expect(renderer.toJSON()).toEqual({
type: 'div',
children: ['Hi'],
props: {},
});
renderer.update(<Component>{['Hi', 'Bye']}</Component>);
expect(renderer.toJSON()).toEqual({
type: 'div',
children: ['Hi', 'Bye'],
props: {},
});
renderer.update(<Component>Bye</Component>);
expect(renderer.toJSON()).toEqual({
type: 'div',
children: ['Bye'],
props: {},
});
renderer.update(<Component>{42}</Component>);
expect(renderer.toJSON()).toEqual({
type: 'div',
children: ['42'],
props: {},
});
renderer.update(
<Component>
<div />
</Component>,
);
expect(renderer.toJSON()).toEqual({
type: 'div',
children: [
{
type: 'div',
children: null,
props: {},
},
],
props: {},
});
});
it('toTree() renders simple components returning host components', () => {
const Qoo = () => <span className="Qoo">Hello World!</span>;
const renderer = ReactTestRenderer.create(<Qoo />);
const tree = renderer.toTree();
cleanNodeOrArray(tree);
expect(prettyFormat(tree)).toEqual(
prettyFormat({
nodeType: 'component',
type: Qoo,
props: {},
instance: null,
rendered: {
nodeType: 'host',
type: 'span',
props: {className: 'Qoo'},
instance: null,
rendered: ['Hello World!'],
},
}),
);
});
it('toTree() handles null rendering components', () => {
class Foo extends React.Component {
render() {
return null;
}
}
const renderer = ReactTestRenderer.create(<Foo />);
const tree = renderer.toTree();
expect(tree.instance).toBeInstanceOf(Foo);
cleanNodeOrArray(tree);
expect(tree).toEqual({
type: Foo,
nodeType: 'component',
props: {},
instance: null,
rendered: null,
});
});
it('toTree() handles simple components that return arrays', () => {
const Foo = ({children}) => children;
const renderer = ReactTestRenderer.create(
<Foo>
<div>One</div>
<div>Two</div>
</Foo>,
);
const tree = renderer.toTree();
cleanNodeOrArray(tree);
expect(prettyFormat(tree)).toEqual(
prettyFormat({
type: Foo,
nodeType: 'component',
props: {},
instance: null,
rendered: [
{
instance: null,
nodeType: 'host',
props: {},
rendered: ['One'],
type: 'div',
},
{
instance: null,
nodeType: 'host',
props: {},
rendered: ['Two'],
type: 'div',
},
],
}),
);
});
it('toTree() handles complicated tree of arrays', () => {
class Foo extends React.Component {
render() {
return this.props.children;
}
}
const renderer = ReactTestRenderer.create(
<div>
<Foo>
<div>One</div>
<div>Two</div>
<Foo>
<div>Three</div>
</Foo>
</Foo>
<div>Four</div>
</div>,
);
const tree = renderer.toTree();
cleanNodeOrArray(tree);
expect(prettyFormat(tree)).toEqual(
prettyFormat({
type: 'div',
instance: null,
nodeType: 'host',
props: {},
rendered: [
{
type: Foo,
nodeType: 'component',
props: {},
instance: null,
rendered: [
{
type: 'div',
nodeType: 'host',
props: {},
instance: null,
rendered: ['One'],
},
{
type: 'div',
nodeType: 'host',
props: {},
instance: null,
rendered: ['Two'],
},
{
type: Foo,
nodeType: 'component',
props: {},
instance: null,
rendered: {
type: 'div',
nodeType: 'host',
props: {},
instance: null,
rendered: ['Three'],
},
},
],
},
{
type: 'div',
nodeType: 'host',
props: {},
instance: null,
rendered: ['Four'],
},
],
}),
);
});
it('toTree() renders complicated trees of composites and hosts', () => {
// SFC returning host. no children props.
const Qoo = () => <span className="Qoo">Hello World!</span>;
// SFC returning host. passes through children.
const Foo = ({className, children}) => (
<div className={'Foo ' + className}>
<span className="Foo2">Literal</span>
{children}
</div>
);
// class composite returning composite. passes through children.
class Bar extends React.Component {
render() {
const {special, children} = this.props;
return <Foo className={special ? 'special' : 'normal'}>{children}</Foo>;
}
}
// class composite return composite. no children props.
class Bam extends React.Component {
render() {
return (
<Bar special={true}>
<Qoo />
</Bar>
);
}
}
const renderer = ReactTestRenderer.create(<Bam />);
const tree = renderer.toTree();
// we test for the presence of instances before nulling them out
expect(tree.instance).toBeInstanceOf(Bam);
expect(tree.rendered.instance).toBeInstanceOf(Bar);
cleanNodeOrArray(tree);
expect(prettyFormat(tree)).toEqual(
prettyFormat({
type: Bam,
nodeType: 'component',
props: {},
instance: null,
rendered: {
type: Bar,
nodeType: 'component',
props: {special: true},
instance: null,
rendered: {
type: Foo,
nodeType: 'component',
props: {className: 'special'},
instance: null,
rendered: {
type: 'div',
nodeType: 'host',
props: {className: 'Foo special'},
instance: null,
rendered: [
{
type: 'span',
nodeType: 'host',
props: {className: 'Foo2'},
instance: null,
rendered: ['Literal'],
},
{
type: Qoo,
nodeType: 'component',
props: {},
instance: null,
rendered: {
type: 'span',
nodeType: 'host',
props: {className: 'Qoo'},
instance: null,
rendered: ['Hello World!'],
},
},
],
},
},
},
}),
);
});
it('can update text nodes when rendered as root', () => {
const renderer = ReactTestRenderer.create(['Hello', 'world']);
expect(renderer.toJSON()).toEqual(['Hello', 'world']);
renderer.update(42);
expect(renderer.toJSON()).toEqual('42');
renderer.update([42, 'world']);
expect(renderer.toJSON()).toEqual(['42', 'world']);
});
it('can render and update root fragments', () => {
const Component = props => props.children;
const renderer = ReactTestRenderer.create([
<Component key="a">Hi</Component>,
<Component key="b">Bye</Component>,
]);
expect(renderer.toJSON()).toEqual(['Hi', 'Bye']);
renderer.update(<div />);
expect(renderer.toJSON()).toEqual({
type: 'div',
children: null,
props: {},
});
renderer.update([<div key="a">goodbye</div>, 'world']);
expect(renderer.toJSON()).toEqual([
{
type: 'div',
children: ['goodbye'],
props: {},
},
'world',
]);
});
it('can concurrently render context with a "primary" renderer', () => {
const Context = React.createContext(null);
const Indirection = React.Fragment;
const App = () => (
<Context.Provider value={null}>
<Indirection>
<Context.Consumer>{() => null}</Context.Consumer>
</Indirection>
</Context.Provider>
);
ReactNoop.render(<App />);
expect(Scheduler).toFlushWithoutYielding();
ReactTestRenderer.create(<App />);
});
it('calling findByType() with an invalid component will fall back to "Unknown" for component name', () => {
const App = () => null;
const renderer = ReactTestRenderer.create(<App />);
const NonComponent = {};
expect(() => {
renderer.root.findByType(NonComponent);
}).toThrowError(`No instances found with node type: "Unknown"`);
});
});

View File

@ -0,0 +1,117 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
let ReactDOM;
let React;
let ReactCache;
let ReactTestRenderer;
let Scheduler;
describe('ReactTestRenderer', () => {
beforeEach(() => {
jest.resetModules();
ReactDOM = require('horizon');
// Isolate test renderer.
jest.resetModules();
ReactTestRenderer = require('react-test-renderer');
React = require('horizon-external');
ReactCache = require('react-cache');
Scheduler = require('scheduler');
});
it('should warn if used to render a ReactDOM portal', () => {
const container = document.createElement('div');
expect(() => {
expect(() => {
ReactTestRenderer.create(ReactDOM.createPortal('foo', container));
}).toThrow();
}).toErrorDev('An invalid container has been provided.', {
withoutStack: true,
});
});
describe('timed out Suspense hidden subtrees should not be observable via toJSON', () => {
let AsyncText;
let PendingResources;
let TextResource;
beforeEach(() => {
PendingResources = {};
TextResource = ReactCache.unstable_createResource(
text =>
new Promise(resolve => {
PendingResources[text] = resolve;
}),
text => text,
);
AsyncText = ({text}) => {
const value = TextResource.read(text);
return value;
};
});
it('for root Suspense components', async done => {
const App = ({text}) => {
return (
<React.Suspense fallback="fallback">
<AsyncText text={text} />
</React.Suspense>
);
};
const root = ReactTestRenderer.create(<App text="initial" />);
PendingResources.initial('initial');
await Promise.resolve();
Scheduler.unstable_flushAll();
expect(root.toJSON()).toEqual('initial');
root.update(<App text="dynamic" />);
expect(root.toJSON()).toEqual('fallback');
PendingResources.dynamic('dynamic');
await Promise.resolve();
Scheduler.unstable_flushAll();
expect(root.toJSON()).toEqual('dynamic');
done();
});
it('for nested Suspense components', async done => {
const App = ({text}) => {
return (
<div>
<React.Suspense fallback="fallback">
<AsyncText text={text} />
</React.Suspense>
</div>
);
};
const root = ReactTestRenderer.create(<App text="initial" />);
PendingResources.initial('initial');
await Promise.resolve();
Scheduler.unstable_flushAll();
expect(root.toJSON().children).toEqual(['initial']);
root.update(<App text="dynamic" />);
expect(root.toJSON().children).toEqual(['fallback']);
PendingResources.dynamic('dynamic');
await Promise.resolve();
Scheduler.unstable_flushAll();
expect(root.toJSON().children).toEqual(['dynamic']);
done();
});
});
});

View File

@ -0,0 +1,105 @@
jest.useRealTimers();
let React;
let ReactTestRenderer;
let Scheduler;
let act;
describe('ReactTestRenderer.act()', () => {
beforeEach(() => {
jest.resetModules();
ReactTestRenderer = require('react-test-renderer');
React = require('horizon-external');
Scheduler = require('scheduler');
act = ReactTestRenderer.act;
});
it('can use .act() to flush effects', () => {
function App(props) {
const [ctr, setCtr] = React.useState(0);
React.useEffect(() => {
props.callback();
setCtr(1);
}, []);
return ctr;
}
const calledLog = [];
let root;
act(() => {
root = ReactTestRenderer.create(
<App
callback={() => {
calledLog.push(calledLog.length);
}}
/>,
);
});
expect(calledLog).toEqual([0]);
expect(root.toJSON()).toEqual('1');
});
describe('async', () => {
it('should work with async/await', async () => {
function fetch(url) {
return Promise.resolve({
details: [1, 2, 3],
});
}
function App() {
const [details, setDetails] = React.useState(0);
React.useEffect(() => {
async function fetchDetails() {
const response = await fetch();
setDetails(response.details);
}
fetchDetails();
}, []);
return details;
}
let root;
await ReactTestRenderer.act(async () => {
root = ReactTestRenderer.create(<App />);
});
expect(root.toJSON()).toEqual(['1', '2', '3']);
});
it('should not flush effects without also flushing microtasks', async () => {
const {useEffect, useReducer} = React;
const alreadyResolvedPromise = Promise.resolve();
function App() {
// This component will keep updating itself until step === 3
const [step, proceed] = useReducer(s => (s === 3 ? 3 : s + 1), 1);
useEffect(() => {
Scheduler.unstable_yieldValue('Effect');
alreadyResolvedPromise.then(() => {
Scheduler.unstable_yieldValue('Microtask');
proceed();
});
});
return step;
}
const root = ReactTestRenderer.create(null);
await act(async () => {
root.update(<App />);
});
expect(Scheduler).toHaveYielded([
// Should not flush effects without also flushing microtasks
// First render:
'Effect',
'Microtask',
// Second render:
'Effect',
'Microtask',
// Final render:
'Effect',
'Microtask',
]);
expect(root).toMatchRenderedOutput('3');
});
});
});

View File

@ -0,0 +1,241 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
* @jest-environment node
*/
'use strict';
jest.resetModules();
let ReactTestRenderer = require('react-test-renderer');
let React = require('horizon-external');
let Context = React.createContext(null);
const RCTView = 'RCTView';
const View = props => <RCTView {...props} />;
describe('ReactTestRendererTraversal', () => {
class Example extends React.Component {
render() {
return (
<View>
<View foo="foo">
<View bar="bar" />
<View bar="bar" baz="baz" itself="itself" />
<View />
<ExampleSpread bar="bar" />
<ExampleFn bar="bar" bing="bing" />
<ExampleNull bar="bar" />
<ExampleNull null="null">
<View void="void" />
<View void="void" />
</ExampleNull>
<React.Profiler id="test" onRender={() => {}}>
<ExampleForwardRef qux="qux" />
</React.Profiler>
<>
<>
<Context.Provider value={null}>
<Context.Consumer>
{() => <View nested={true} />}
</Context.Consumer>
</Context.Provider>
</>
<View nested={true} />
<View nested={true} />
</>
</View>
</View>
);
}
}
class ExampleSpread extends React.Component {
render = () => <View {...this.props} />;
}
const ExampleFn = props => <View baz="baz" />;
const ExampleNull = props => null;
const ExampleForwardRef = React.forwardRef((props, ref) => (
<View {...props} ref={ref} />
));
it('initializes', () => {
const render = ReactTestRenderer.create(<Example />);
const hasFooProp = node => node.props.hasOwnProperty('foo');
// assert .props, .type and .parent attributes
const foo = render.root.find(hasFooProp);
expect(foo.props.children).toHaveLength(9);
expect(foo.type).toBe(View);
expect(render.root.parent).toBe(null);
expect(foo.children[0].parent).toBe(foo);
});
it('searches via .find() / .findAll()', () => {
const render = ReactTestRenderer.create(<Example />);
const hasFooProp = node => node.props.hasOwnProperty('foo');
const hasBarProp = node => node.props.hasOwnProperty('bar');
const hasBazProp = node => node.props.hasOwnProperty('baz');
const hasBingProp = node => node.props.hasOwnProperty('bing');
const hasNullProp = node => node.props.hasOwnProperty('null');
const hasVoidProp = node => node.props.hasOwnProperty('void');
const hasItselfProp = node => node.props.hasOwnProperty('itself');
const hasNestedProp = node => node.props.hasOwnProperty('nested');
expect(() => render.root.find(hasFooProp)).not.toThrow(); // 1 match
expect(() => render.root.find(hasBarProp)).toThrow(); // >1 matches
expect(() => render.root.find(hasBazProp)).toThrow(); // >1 matches
expect(() => render.root.find(hasBingProp)).not.toThrow(); // 1 match
expect(() => render.root.find(hasNullProp)).not.toThrow(); // 1 match
expect(() => render.root.find(hasVoidProp)).toThrow(); // 0 matches
expect(() => render.root.find(hasNestedProp)).toThrow(); // >1 matches
// same assertion as .find(), but confirm length
expect(render.root.findAll(hasFooProp, {deep: false})).toHaveLength(1);
expect(render.root.findAll(hasBarProp, {deep: false})).toHaveLength(5);
expect(render.root.findAll(hasBazProp, {deep: false})).toHaveLength(2);
expect(render.root.findAll(hasBingProp, {deep: false})).toHaveLength(1);
expect(render.root.findAll(hasNullProp, {deep: false})).toHaveLength(1);
expect(render.root.findAll(hasVoidProp, {deep: false})).toHaveLength(0);
expect(render.root.findAll(hasNestedProp, {deep: false})).toHaveLength(3);
// note: with {deep: true}, .findAll() will continue to
// search children, even after finding a match
expect(render.root.findAll(hasFooProp)).toHaveLength(2);
expect(render.root.findAll(hasBarProp)).toHaveLength(9);
expect(render.root.findAll(hasBazProp)).toHaveLength(4);
expect(render.root.findAll(hasBingProp)).toHaveLength(1); // no spread
expect(render.root.findAll(hasNullProp)).toHaveLength(1); // no spread
expect(render.root.findAll(hasVoidProp)).toHaveLength(0);
expect(render.root.findAll(hasNestedProp, {deep: false})).toHaveLength(3);
const bing = render.root.find(hasBingProp);
expect(bing.find(hasBarProp)).toBe(bing);
expect(bing.find(hasBingProp)).toBe(bing);
expect(bing.findAll(hasBazProp, {deep: false})).toHaveLength(1);
expect(bing.findAll(hasBazProp)).toHaveLength(2);
const foo = render.root.find(hasFooProp);
expect(foo.findAll(hasFooProp, {deep: false})).toHaveLength(1);
expect(foo.findAll(hasFooProp)).toHaveLength(2);
const itself = foo.find(hasItselfProp);
expect(itself.find(hasBarProp)).toBe(itself);
expect(itself.find(hasBazProp)).toBe(itself);
expect(itself.findAll(hasBazProp, {deep: false})).toHaveLength(1);
expect(itself.findAll(hasBazProp)).toHaveLength(2);
});
it('searches via .findByType() / .findAllByType()', () => {
const render = ReactTestRenderer.create(<Example />);
expect(() => render.root.findByType(ExampleFn)).not.toThrow(); // 1 match
expect(() => render.root.findByType(View)).not.toThrow(); // 1 match
expect(() => render.root.findByType(ExampleForwardRef)).not.toThrow(); // 1 match
// note: there are clearly multiple <View /> in general, but there
// is only one being rendered at root node level
expect(() => render.root.findByType(ExampleNull)).toThrow(); // 2 matches
expect(render.root.findAllByType(ExampleFn)).toHaveLength(1);
expect(render.root.findAllByType(View, {deep: false})).toHaveLength(1);
expect(render.root.findAllByType(View)).toHaveLength(11);
expect(render.root.findAllByType(ExampleNull)).toHaveLength(2);
expect(render.root.findAllByType(ExampleForwardRef)).toHaveLength(1);
const nulls = render.root.findAllByType(ExampleNull);
expect(nulls[0].findAllByType(View)).toHaveLength(0);
expect(nulls[1].findAllByType(View)).toHaveLength(0);
const fn = render.root.findAllByType(ExampleFn);
expect(fn[0].findAllByType(View)).toHaveLength(1);
});
it('searches via .findByProps() / .findAllByProps()', () => {
const render = ReactTestRenderer.create(<Example />);
const foo = 'foo';
const bar = 'bar';
const baz = 'baz';
const qux = 'qux';
expect(() => render.root.findByProps({foo})).not.toThrow(); // 1 match
expect(() => render.root.findByProps({bar})).toThrow(); // >1 matches
expect(() => render.root.findByProps({baz})).toThrow(); // >1 matches
expect(() => render.root.findByProps({qux})).not.toThrow(); // 1 match
expect(render.root.findAllByProps({foo}, {deep: false})).toHaveLength(1);
expect(render.root.findAllByProps({bar}, {deep: false})).toHaveLength(5);
expect(render.root.findAllByProps({baz}, {deep: false})).toHaveLength(2);
expect(render.root.findAllByProps({qux}, {deep: false})).toHaveLength(1);
expect(render.root.findAllByProps({foo})).toHaveLength(2);
expect(render.root.findAllByProps({bar})).toHaveLength(9);
expect(render.root.findAllByProps({baz})).toHaveLength(4);
expect(render.root.findAllByProps({qux})).toHaveLength(3);
});
it('skips special nodes', () => {
const render = ReactTestRenderer.create(<Example />);
expect(render.root.findAllByType(React.Fragment)).toHaveLength(0);
expect(render.root.findAllByType(Context.Consumer)).toHaveLength(0);
expect(render.root.findAllByType(Context.Provider)).toHaveLength(0);
const expectedParent = render.root.findByProps({foo: 'foo'}, {deep: false})
.children[0];
const nestedViews = render.root.findAllByProps(
{nested: true},
{deep: false},
);
expect(nestedViews.length).toBe(3);
expect(nestedViews[0].parent).toBe(expectedParent);
expect(nestedViews[1].parent).toBe(expectedParent);
expect(nestedViews[2].parent).toBe(expectedParent);
});
it('can have special nodes as roots', () => {
const FR = React.forwardRef((props, ref) => <section {...props} />);
expect(
ReactTestRenderer.create(
<FR>
<div />
<div />
</FR>,
).root.findAllByType('div').length,
).toBe(2);
expect(
ReactTestRenderer.create(
<>
<div />
<div />
</>,
).root.findAllByType('div').length,
).toBe(2);
expect(
ReactTestRenderer.create(
<React.Fragment key="foo">
<div />
<div />
</React.Fragment>,
).root.findAllByType('div').length,
).toBe(2);
expect(
ReactTestRenderer.create(
<React.Fragment>
<div />
<div />
</React.Fragment>,
).root.findAllByType('div').length,
).toBe(2);
expect(
ReactTestRenderer.create(
<Context.Provider value={null}>
<div />
<div />
</Context.Provider>,
).root.findAllByType('div').length,
).toBe(2);
});
});

View File

@ -0,0 +1,161 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
const React = require('horizon-external');
const ReactDOM = require('horizon');
describe('CSSPropertyOperations', () => {
it('should set style attribute when styles exist', () => {
const styles = {
backgroundColor: '#000',
display: 'none',
};
let div = <div style={styles} />;
const root = document.createElement('div');
div = ReactDOM.render(div, root);
expect(/style=".*"/.test(root.innerHTML)).toBe(true);
});
xit('should warn when using hyphenated style names', () => {
class Comp extends React.Component {
static displayName = 'Comp';
render() {
return <div style={{'background-color': 'crimson'}} />;
}
}
const root = document.createElement('div');
expect(() => ReactDOM.render(<Comp />, root)).toErrorDev(
'Warning: The CSS style attribute name `background-color` is recommended to be set to `backgroundColor`.'
);
});
xit('should warn when updating hyphenated style names', () => {
class Comp extends React.Component {
static displayName = 'Comp';
render() {
return <div style={this.props.style} />;
}
}
const styles = {
'-ms-transform': 'translate3d(0, 0, 0)',
'-webkit-transform': 'translate3d(0, 0, 0)',
};
const root = document.createElement('div');
ReactDOM.render(<Comp />, root);
expect(() => ReactDOM.render(<Comp style={styles} />, root)).toErrorDev([
'Warning: The CSS style attribute name `-ms-transform` is recommended to be set to `msTransform`.',
'Warning: The CSS style attribute name `-webkit-transform` is recommended to be set to `WebkitTransform`.',
]);
});
it('warns when miscapitalizing vendored style names', () => {
class Comp extends React.Component {
static displayName = 'Comp';
render() {
return (
<div
style={{
msTransform: 'translate3d(0, 0, 0)',
oTransform: 'translate3d(0, 0, 0)',
webkitTransform: 'translate3d(0, 0, 0)',
}}
/>
);
}
}
const root = document.createElement('div');
expect(() => ReactDOM.render(<Comp />, root)).not.toThrow();
});
it('should warn about style having a trailing semicolon', () => {
class Comp extends React.Component {
static displayName = 'Comp';
render() {
return (
<div
style={{
fontFamily: 'Helvetica, arial',
backgroundImage: 'url(foo;bar)',
backgroundColor: 'blue;',
color: 'red; ',
}}
/>
);
}
}
const root = document.createElement('div');
expect(() => ReactDOM.render(<Comp />, root)).not.toThrow();
});
it('should warn about style containing a NaN value', () => {
class Comp extends React.Component {
static displayName = 'Comp';
render() {
return <div style={{fontSize: NaN}} />;
}
}
const root = document.createElement('div');
expect(() => ReactDOM.render(<Comp />, root)).not.toThrow();
});
it('should not warn when setting CSS custom properties', () => {
class Comp extends React.Component {
render() {
return <div style={{'--foo-primary': 'red', backgroundColor: 'red'}} />;
}
}
const root = document.createElement('div');
ReactDOM.render(<Comp />, root);
});
it('should warn about style containing a Infinity value', () => {
class Comp extends React.Component {
static displayName = 'Comp';
render() {
return <div style={{fontSize: 1 / 0}} />;
}
}
const root = document.createElement('div');
expect(() => ReactDOM.render(<Comp />, root)).not.toThrow();
});
xit('should not add units to CSS custom properties', () => {
class Comp extends React.Component {
render() {
return <div style={{'--foo': '5'}} />;
}
}
const root = document.createElement('div');
ReactDOM.render(<Comp />, root);
expect(root.children[0].style.getPropertyValue('--foo')).toEqual('5');
});
});

View File

@ -0,0 +1,186 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
describe('DOMPropertyOperations', () => {
let React;
let ReactDOM;
beforeEach(() => {
jest.resetModules();
React = require('horizon-external');
ReactDOM = require('horizon');
});
describe('setValueForProperty', () => {
it('should set values as properties by default', () => {
const container = document.createElement('div');
ReactDOM.render(<div title="Tip!" />, container);
expect(container.firstChild.title).toBe('Tip!');
});
it('should set values as attributes if necessary', () => {
const container = document.createElement('div');
ReactDOM.render(<div role="#" />, container);
expect(container.firstChild.getAttribute('role')).toBe('#');
expect(container.firstChild.role).toBeUndefined();
});
it('should set values as namespace attributes if necessary', () => {
const container = document.createElementNS(
'http://www.w3.org/2000/svg',
'svg',
);
ReactDOM.render(<image xlinkHref="about:blank" />, container);
expect(
container.firstChild.getAttributeNS(
'http://www.w3.org/1999/xlink',
'href',
),
).toBe('about:blank');
});
it('should set values as boolean properties', () => {
const container = document.createElement('div');
ReactDOM.render(<div disabled="disabled" />, container);
expect(container.firstChild.getAttribute('disabled')).toBe('');
ReactDOM.render(<div disabled={true} />, container);
expect(container.firstChild.getAttribute('disabled')).toBe('');
ReactDOM.render(<div disabled={false} />, container);
expect(container.firstChild.getAttribute('disabled')).toBe(null);
ReactDOM.render(<div disabled={true} />, container);
ReactDOM.render(<div disabled={null} />, container);
expect(container.firstChild.getAttribute('disabled')).toBe(null);
ReactDOM.render(<div disabled={true} />, container);
ReactDOM.render(<div disabled={undefined} />, container);
expect(container.firstChild.getAttribute('disabled')).toBe(null);
});
it('should convert attribute values to string first', () => {
// Browsers default to this behavior, but some test environments do not.
// This ensures that we have consistent behavior.
const obj = {
toString: function() {
return 'css-class';
},
};
const container = document.createElement('div');
ReactDOM.render(<div className={obj} />, container);
expect(container.firstChild.getAttribute('class')).toBe('css-class');
});
it('should not remove empty attributes for special input properties', () => {
const container = document.createElement('div');
ReactDOM.render(<input value="" onChange={() => {}} />, container);
expect(container.firstChild.getAttribute('value')).toBe('');
expect(container.firstChild.value).toBe('');
});
it('should not remove empty attributes for special option properties', () => {
const container = document.createElement('div');
ReactDOM.render(
<select>
<option value="">empty</option>
<option>filled</option>
</select>,
container,
);
// Regression test for https://github.com/facebook/react/issues/6219
expect(container.firstChild.firstChild.value).toBe('');
expect(container.firstChild.lastChild.value).toBe('filled');
});
it('should remove for falsey boolean properties', () => {
const container = document.createElement('div');
ReactDOM.render(<div allowFullScreen={false} />, container);
expect(container.firstChild.hasAttribute('allowFullScreen')).toBe(false);
});
it('should remove when setting custom attr to null', () => {
const container = document.createElement('div');
ReactDOM.render(<div data-foo="bar" />, container);
expect(container.firstChild.hasAttribute('data-foo')).toBe(true);
ReactDOM.render(<div data-foo={null} />, container);
expect(container.firstChild.hasAttribute('data-foo')).toBe(false);
});
it('should set className to empty string instead of null', () => {
const container = document.createElement('div');
ReactDOM.render(<div className="selected" />, container);
expect(container.firstChild.className).toBe('selected');
ReactDOM.render(<div className={null} />, container);
// className should be '', not 'null' or null (which becomes 'null' in
// some browsers)
expect(container.firstChild.className).toBe('');
expect(container.firstChild.getAttribute('class')).toBe(null);
});
it('should remove property properly for boolean properties', () => {
const container = document.createElement('div');
ReactDOM.render(<div hidden={true} />, container);
expect(container.firstChild.hasAttribute('hidden')).toBe(true);
ReactDOM.render(<div hidden={false} />, container);
expect(container.firstChild.hasAttribute('hidden')).toBe(false);
});
it('should always assign the value attribute for non-inputs', function() {
const container = document.createElement('div');
ReactDOM.render(<progress />, container);
spyOnDevAndProd(container.firstChild, 'setAttribute');
ReactDOM.render(<progress value={30} />, container);
ReactDOM.render(<progress value="30" />, container);
expect(container.firstChild.setAttribute).toHaveBeenCalledTimes(2);
});
it('should return the progress to intermediate state on null value', () => {
const container = document.createElement('div');
ReactDOM.render(<progress value={30} />, container);
ReactDOM.render(<progress value={null} />, container);
// Ensure we move progress back to an indeterminate state.
// Regression test for https://github.com/facebook/react/issues/6119
expect(container.firstChild.hasAttribute('value')).toBe(false);
});
});
describe('deleteValueForProperty', () => {
it('should remove attributes for normal properties', () => {
const container = document.createElement('div');
ReactDOM.render(<div title="foo" />, container);
expect(container.firstChild.getAttribute('title')).toBe('foo');
ReactDOM.render(<div />, container);
expect(container.firstChild.getAttribute('title')).toBe(null);
});
it('should not remove attributes for special properties', () => {
const container = document.createElement('div');
ReactDOM.render(
<input type="text" value="foo" onChange={function() {}} />,
container,
);
expect(container.firstChild.getAttribute('value')).toBe('foo');
expect(container.firstChild.value).toBe('foo');
expect(() =>
ReactDOM.render(
<input type="text" onChange={function() {}} />,
container,
),
).not.toThrow();
expect(container.firstChild.getAttribute('value')).toBe('foo');
expect(container.firstChild.value).toBe('foo');
});
it('should not remove attributes for custom component tag', () => {
const container = document.createElement('div');
ReactDOM.render(<my-icon size="5px" />, container);
expect(container.firstChild.getAttribute('size')).toBe('5px');
});
});
});

View File

@ -0,0 +1,76 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict'
describe('InvalidEventListeners', () => {
let React;
let ReactDOM;
let container;
beforeEach(() => {
jest.resetModules();
React = require('horizon-external');
ReactDOM = require('horizon');
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
container = null;
});
xit('should prevent non-function listeners, at dispatch', () => {
let node;
expect(() => {
node = ReactDOM.render(<div onClick="not a function" />, container);
}).toErrorDev(
'`onClick` should be a function, but its a `string` type.',
);
spyOnProd(console, 'error');
const uncaughtErrors = [];
function handleWindowError(e) {
uncaughtErrors.push(e.error);
}
window.addEventListener('error', handleWindowError);
try {
node.dispatchEvent(
new MouseEvent('click', {
bubbles: true,
}),
);
} finally {
window.removeEventListener('error', handleWindowError);
}
expect(uncaughtErrors.length).toBe(1);
expect(uncaughtErrors[0]).toEqual(
expect.objectContaining({
message:
'`onClick` listener should be a function.',
}),
);
if (!isDev) {
expect(console.error).toHaveBeenCalledTimes(1);
}
});
it('should not prevent null listeners, at dispatch', () => {
const node = ReactDOM.render(<div onClick={null} />, container);
node.dispatchEvent(
new MouseEvent('click', {
bubbles: true,
}),
);
});
});

View File

@ -0,0 +1,246 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
let React;
let ReactDOM;
let ReactTestUtils;
let idCallOrder;
const recordID = function(id) {
idCallOrder.push(id);
};
const recordIDAndStopPropagation = function(id, event) {
recordID(id);
event.stopPropagation();
};
const recordIDAndReturnFalse = function(id, event) {
recordID(id);
return false;
};
const LISTENER = jest.fn();
const ON_CLICK_KEY = 'onClick';
const ON_MOUSE_ENTER_KEY = 'onMouseEnter';
let GRANDPARENT;
let PARENT;
let CHILD;
let BUTTON;
let putListener;
let deleteAllListeners;
let container;
// This test is written in a bizarre way because it was previously using internals.
// It should probably be rewritten but we're keeping it for some extra coverage.
describe('ReactBrowserEventEmitter', () => {
beforeEach(() => {
jest.resetModules();
LISTENER.mockClear();
React = require('horizon-external');
ReactDOM = require('horizon');
ReactTestUtils = require('react-test-renderer/test-utils');
container = document.createElement('div');
document.body.appendChild(container);
let GRANDPARENT_PROPS = {};
let PARENT_PROPS = {};
let CHILD_PROPS = {};
let BUTTON_PROPS = {};
function Child(props) {
return <div ref={c => (CHILD = c)} {...props} />;
}
class ChildWrapper extends React.PureComponent {
render() {
return <Child {...this.props} />;
}
}
function renderTree() {
ReactDOM.render(
<div ref={c => (GRANDPARENT = c)} {...GRANDPARENT_PROPS}>
<div ref={c => (PARENT = c)} {...PARENT_PROPS}>
<ChildWrapper {...CHILD_PROPS} />
<button disabled={true} ref={c => (BUTTON = c)} {...BUTTON_PROPS} />
</div>
</div>,
container,
);
}
renderTree();
putListener = function(node, eventName, listener) {
switch (node) {
case CHILD:
CHILD_PROPS[eventName] = listener;
break;
case PARENT:
PARENT_PROPS[eventName] = listener;
break;
case GRANDPARENT:
GRANDPARENT_PROPS[eventName] = listener;
break;
case BUTTON:
BUTTON_PROPS[eventName] = listener;
break;
}
// Rerender with new event listeners
renderTree();
};
deleteAllListeners = function(node) {
switch (node) {
case CHILD:
CHILD_PROPS = {};
break;
case PARENT:
PARENT_PROPS = {};
break;
case GRANDPARENT:
GRANDPARENT_PROPS = {};
break;
case BUTTON:
BUTTON_PROPS = {};
break;
}
renderTree();
};
idCallOrder = [];
});
afterEach(() => {
document.body.removeChild(container);
container = null;
});
it('should bubble simply', () => {
putListener(CHILD, ON_CLICK_KEY, recordID.bind(null, CHILD));
putListener(PARENT, ON_CLICK_KEY, recordID.bind(null, PARENT));
putListener(GRANDPARENT, ON_CLICK_KEY, recordID.bind(null, GRANDPARENT));
CHILD.click();
expect(idCallOrder.length).toBe(3);
expect(idCallOrder[0]).toBe(CHILD);
expect(idCallOrder[1]).toBe(PARENT);
expect(idCallOrder[2]).toBe(GRANDPARENT);
});
// ReactErrorUtils相关让DOM事件全部执行完再抛出错误先删除处理
xit('should continue bubbling if an error is thrown', () => {
putListener(CHILD, ON_CLICK_KEY, recordID.bind(null, CHILD));
putListener(PARENT, ON_CLICK_KEY, function() {
recordID(PARENT);
throw new Error('Handler interrupted');
});
putListener(GRANDPARENT, ON_CLICK_KEY, recordID.bind(null, GRANDPARENT));
expect(function() {
ReactTestUtils.Simulate.click(CHILD);
}).toThrow();
expect(idCallOrder.length).toBe(3);
expect(idCallOrder[0]).toBe(CHILD);
expect(idCallOrder[1]).toBe(PARENT);
expect(idCallOrder[2]).toBe(GRANDPARENT);
});
it('should set currentTarget', () => {
putListener(CHILD, ON_CLICK_KEY, function(event) {
recordID(CHILD);
expect(event.currentTarget).toBe(CHILD);
});
putListener(PARENT, ON_CLICK_KEY, function(event) {
recordID(PARENT);
expect(event.currentTarget).toBe(PARENT);
});
putListener(GRANDPARENT, ON_CLICK_KEY, function(event) {
recordID(GRANDPARENT);
expect(event.currentTarget).toBe(GRANDPARENT);
});
CHILD.click();
expect(idCallOrder.length).toBe(3);
expect(idCallOrder[0]).toBe(CHILD);
expect(idCallOrder[1]).toBe(PARENT);
expect(idCallOrder[2]).toBe(GRANDPARENT);
});
it('should support stopPropagation()', () => {
putListener(CHILD, ON_CLICK_KEY, recordID.bind(null, CHILD));
putListener(
PARENT,
ON_CLICK_KEY,
recordIDAndStopPropagation.bind(null, PARENT),
);
putListener(GRANDPARENT, ON_CLICK_KEY, recordID.bind(null, GRANDPARENT));
CHILD.click();
expect(idCallOrder.length).toBe(2);
expect(idCallOrder[0]).toBe(CHILD);
expect(idCallOrder[1]).toBe(PARENT);
});
it('should support overriding .isPropagationStopped()', () => {
// Ew. See D4504876.
putListener(CHILD, ON_CLICK_KEY, recordID.bind(null, CHILD));
putListener(PARENT, ON_CLICK_KEY, function(e) {
recordID(PARENT, e);
// This stops React bubbling but avoids touching the native event
e.isPropagationStopped = () => true;
});
putListener(GRANDPARENT, ON_CLICK_KEY, recordID.bind(null, GRANDPARENT));
CHILD.click();
expect(idCallOrder.length).toBe(2);
expect(idCallOrder[0]).toBe(CHILD);
expect(idCallOrder[1]).toBe(PARENT);
});
it('should stop after first dispatch if stopPropagation', () => {
putListener(
CHILD,
ON_CLICK_KEY,
recordIDAndStopPropagation.bind(null, CHILD),
);
putListener(PARENT, ON_CLICK_KEY, recordID.bind(null, PARENT));
putListener(GRANDPARENT, ON_CLICK_KEY, recordID.bind(null, GRANDPARENT));
CHILD.click();
expect(idCallOrder.length).toBe(1);
expect(idCallOrder[0]).toBe(CHILD);
});
it('should not stopPropagation if false is returned', () => {
putListener(CHILD, ON_CLICK_KEY, recordIDAndReturnFalse.bind(null, CHILD));
putListener(PARENT, ON_CLICK_KEY, recordID.bind(null, PARENT));
putListener(GRANDPARENT, ON_CLICK_KEY, recordID.bind(null, GRANDPARENT));
CHILD.click();
expect(idCallOrder.length).toBe(3);
expect(idCallOrder[0]).toBe(CHILD);
expect(idCallOrder[1]).toBe(PARENT);
expect(idCallOrder[2]).toBe(GRANDPARENT);
});
it('should not invoke newly inserted handlers while bubbling', () => {
const handleParentClick = jest.fn();
const handleChildClick = function(event) {
putListener(PARENT, ON_CLICK_KEY, handleParentClick);
};
putListener(CHILD, ON_CLICK_KEY, handleChildClick);
CHILD.click();
expect(handleParentClick).toHaveBeenCalledTimes(0);
});
xit('should have mouse enter simulated by test utils', () => {
putListener(CHILD, ON_MOUSE_ENTER_KEY, recordID.bind(null, CHILD));
ReactTestUtils.Simulate.mouseEnter(CHILD);
expect(idCallOrder.length).toBe(1);
expect(idCallOrder[0]).toBe(CHILD);
});
});

View File

@ -0,0 +1,146 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
// NOTE: We're explicitly not using JSX here. This is intended to test
// the current stack addendum without having source location added by babel.
'use strict';
let React;
let ReactTestUtils;
describe('ReactChildReconciler', () => {
beforeEach(() => {
jest.resetModules();
React = require('horizon-external');
ReactTestUtils = require('react-test-renderer/test-utils');
});
function createIterable(array) {
return {
'@@iterator': function() {
let i = 0;
return {
next() {
const next = {
value: i < array.length ? array[i] : undefined,
done: i === array.length,
};
i++;
return next;
},
};
},
};
}
function makeIterableFunction(value) {
const fn = () => {};
fn['@@iterator'] = function iterator() {
let timesCalled = 0;
return {
next() {
const done = timesCalled++ > 0;
return {done, value: done ? undefined : value};
},
};
};
return fn;
}
xit('does not treat functions as iterables', () => {
let node;
const iterableFunction = makeIterableFunction('foo');
expect(() => {
node = ReactTestUtils.renderIntoDocument(
<div>
<h1>{iterableFunction}</h1>
</div>,
);
}).toErrorDev(
'horizon child can not be functions. use string / object / array.',
);
expect(node.innerHTML).toContain(''); // h1
});
xit('warns for duplicated array keys', () => {
class Component extends React.Component {
render() {
return <div>{[<div key="1" />, <div key="1" />]}</div>;
}
}
expect(() => ReactTestUtils.renderIntoDocument(<Component />)).toErrorDev(
'Components with the same key value cannot exist: `1`, which may cause an error.',
);
});
xit('warns for duplicated array keys with component stack info', () => {
class Component extends React.Component {
render() {
return <div>{[<div key="1" />, <div key="1" />]}</div>;
}
}
class Parent extends React.Component {
render() {
return React.cloneElement(this.props.child);
}
}
class GrandParent extends React.Component {
render() {
return <Parent child={<Component />} />;
}
}
expect(() => ReactTestUtils.renderIntoDocument(<GrandParent />)).toErrorDev(
'Components with the same key value cannot exist: `1`, which may cause an error.',
);
});
xit('warns for duplicated iterable keys', () => {
class Component extends React.Component {
render() {
return <div>{createIterable([<div key="1" />, <div key="1" />])}</div>;
}
}
expect(() => ReactTestUtils.renderIntoDocument(<Component />)).toErrorDev(
'Components with the same key value cannot exist: `1`, which may cause an error.',
);
});
xit('warns for duplicated iterable keys with component stack info', () => {
class Component extends React.Component {
render() {
return <div>{createIterable([<div key="1" />, <div key="1" />])}</div>;
}
}
class Parent extends React.Component {
render() {
return React.cloneElement(this.props.child);
}
}
class GrandParent extends React.Component {
render() {
return <Parent child={<Component />} />;
}
}
expect(() => ReactTestUtils.renderIntoDocument(<GrandParent />)).toErrorDev(
'Components with the same key value cannot exist: `1`, which may cause an error.',
);
});
});

View File

@ -0,0 +1,480 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
let React;
let ReactDOM;
let ReactTestUtils;
describe('ReactComponent', () => {
beforeEach(() => {
jest.resetModules();
React = require('horizon-external');
ReactDOM = require('horizon');
ReactTestUtils = require('react-test-renderer/test-utils');
});
xit('should throw when supplying a ref outside of render method', () => {
let instance = <div ref="badDiv" />;
expect(function() {
instance = ReactTestUtils.renderIntoDocument(instance);
}).toThrow();
});
it('should support refs on owned components', () => {
const innerObj = {};
const outerObj = {};
class Wrapper extends React.Component {
getObject = () => {
return this.props.object;
};
render() {
return <div>{this.props.children}</div>;
}
}
class Component extends React.Component {
render() {
const inner = <Wrapper object={innerObj} ref="inner" />;
const outer = (
<Wrapper object={outerObj} ref="outer">
{inner}
</Wrapper>
);
return outer;
}
componentDidMount() {
expect(this.refs.inner.getObject()).toEqual(innerObj);
expect(this.refs.outer.getObject()).toEqual(outerObj);
}
}
ReactTestUtils.renderIntoDocument(<Component />);
});
it('should not have refs on unmounted components', () => {
class Parent extends React.Component {
render() {
return (
<Child>
<div ref="test" />
</Child>
);
}
componentDidMount() {
expect(this.refs && this.refs.test).toEqual(undefined);
}
}
class Child extends React.Component {
render() {
return <div />;
}
}
ReactTestUtils.renderIntoDocument(<Parent child={<span />} />);
});
it('should support callback-style refs', () => {
const innerObj = {};
const outerObj = {};
class Wrapper extends React.Component {
getObject = () => {
return this.props.object;
};
render() {
return <div>{this.props.children}</div>;
}
}
let mounted = false;
class Component extends React.Component {
render() {
const inner = (
<Wrapper object={innerObj} ref={c => (this.innerRef = c)} />
);
const outer = (
<Wrapper object={outerObj} ref={c => (this.outerRef = c)}>
{inner}
</Wrapper>
);
return outer;
}
componentDidMount() {
expect(this.innerRef.getObject()).toEqual(innerObj);
expect(this.outerRef.getObject()).toEqual(outerObj);
mounted = true;
}
}
ReactTestUtils.renderIntoDocument(<Component />);
expect(mounted).toBe(true);
});
it('should support object-style refs', () => {
const innerObj = {};
const outerObj = {};
class Wrapper extends React.Component {
getObject = () => {
return this.props.object;
};
render() {
return <div>{this.props.children}</div>;
}
}
let mounted = false;
class Component extends React.Component {
constructor() {
super();
this.innerRef = React.createRef();
this.outerRef = React.createRef();
}
render() {
const inner = <Wrapper object={innerObj} ref={this.innerRef} />;
const outer = (
<Wrapper object={outerObj} ref={this.outerRef}>
{inner}
</Wrapper>
);
return outer;
}
componentDidMount() {
expect(this.innerRef.current.getObject()).toEqual(innerObj);
expect(this.outerRef.current.getObject()).toEqual(outerObj);
mounted = true;
}
}
ReactTestUtils.renderIntoDocument(<Component />);
expect(mounted).toBe(true);
});
it('should support new-style refs with mixed-up owners', () => {
class Wrapper extends React.Component {
getTitle = () => {
return this.props.title;
};
render() {
return this.props.getContent();
}
}
let mounted = false;
class Component extends React.Component {
getInner = () => {
// (With old-style refs, it's impossible to get a ref to this div
// because Wrapper is the current owner when this function is called.)
return <div className="inner" ref={c => (this.innerRef = c)} />;
};
render() {
return (
<Wrapper
title="wrapper"
ref={c => (this.wrapperRef = c)}
getContent={this.getInner}
/>
);
}
componentDidMount() {
// Check .props.title to make sure we got the right elements back
expect(this.wrapperRef.getTitle()).toBe('wrapper');
expect(this.innerRef.className).toBe('inner');
mounted = true;
}
}
ReactTestUtils.renderIntoDocument(<Component />);
expect(mounted).toBe(true);
});
it('should call refs at the correct time', () => {
const log = [];
class Inner extends React.Component {
render() {
log.push(`inner ${this.props.id} render`);
return <div />;
}
componentDidMount() {
log.push(`inner ${this.props.id} componentDidMount`);
}
componentDidUpdate() {
log.push(`inner ${this.props.id} componentDidUpdate`);
}
componentWillUnmount() {
log.push(`inner ${this.props.id} componentWillUnmount`);
}
}
class Outer extends React.Component {
render() {
return (
<div>
<Inner
id={1}
ref={c => {
log.push(`ref 1 got ${c ? `instance ${c.props.id}` : 'null'}`);
}}
/>
<Inner
id={2}
ref={c => {
log.push(`ref 2 got ${c ? `instance ${c.props.id}` : 'null'}`);
}}
/>
</div>
);
}
componentDidMount() {
log.push('outer componentDidMount');
}
componentDidUpdate() {
log.push('outer componentDidUpdate');
}
componentWillUnmount() {
log.push('outer componentWillUnmount');
}
}
// mount, update, unmount
const el = document.createElement('div');
log.push('start mount');
ReactDOM.render(<Outer />, el);
log.push('start update');
ReactDOM.render(<Outer />, el);
log.push('start unmount');
ReactDOM.unmountComponentAtNode(el);
/* eslint-disable indent */
expect(log).toEqual([
'start mount',
'inner 1 render',
'inner 2 render',
'inner 1 componentDidMount',
'ref 1 got instance 1',
'inner 2 componentDidMount',
'ref 2 got instance 2',
'outer componentDidMount',
'start update',
// Previous (equivalent) refs get cleared
// Fiber renders first, resets refs later
'inner 1 render',
'inner 2 render',
'ref 1 got null',
'ref 2 got null',
'inner 1 componentDidUpdate',
'ref 1 got instance 1',
'inner 2 componentDidUpdate',
'ref 2 got instance 2',
'outer componentDidUpdate',
'start unmount',
'outer componentWillUnmount',
'ref 1 got null',
'inner 1 componentWillUnmount',
'ref 2 got null',
'inner 2 componentWillUnmount',
]);
/* eslint-enable indent */
});
it('fires the callback after a component is rendered', () => {
const callback = jest.fn();
const container = document.createElement('div');
ReactDOM.render(<div />, container, callback);
expect(callback).toHaveBeenCalledTimes(1);
ReactDOM.render(<div className="foo" />, container, callback);
expect(callback).toHaveBeenCalledTimes(2);
ReactDOM.render(<span />, container, callback);
expect(callback).toHaveBeenCalledTimes(3);
});
it('throws usefully when rendering badly-typed elements', () => {
const X = undefined;
expect(() => {
expect(() => ReactTestUtils.renderIntoDocument(<X />)).toErrorDev(
'React.createElement: type is invalid -- expected a string (for built-in components) ' +
'or a class/function (for composite components) but got: undefined.',
);
}).toThrowError('Component type is invalid, got: undefined');
const Y = null;
expect(() => {
expect(() => ReactTestUtils.renderIntoDocument(<Y />)).toErrorDev(
'React.createElement: type is invalid -- expected a string (for built-in components) ' +
'or a class/function (for composite components) but got: null.',
);
}).toThrowError('Component type is invalid, got: null');
});
it('includes owner name in the error about badly-typed elements', () => {
const X = undefined;
function Indirection(props) {
return <div>{props.children}</div>;
}
function Bar() {
return (
<Indirection>
<X />
</Indirection>
);
}
function Foo() {
return <Bar />;
}
expect(() => {
expect(() => ReactTestUtils.renderIntoDocument(<Foo />)).toErrorDev(
'React.createElement: type is invalid -- expected a string (for built-in components) ' +
'or a class/function (for composite components) but got: undefined.',
);
}).toThrowError('Component type is invalid, got: undefined');
});
xit('throws if a plain object is used as a child', () => {
const children = {
x: <span />,
y: <span />,
z: <span />,
};
const element = <div>{[children]}</div>;
const container = document.createElement('div');
expect(() => {
ReactDOM.render(element, container);
}).toThrowError(
'horizon child can not be object. use string / function / array.',
);
});
xit('throws if a plain object even if it is in an owner', () => {
class Foo extends React.Component {
render() {
const children = {
a: <span />,
b: <span />,
c: <span />,
};
return <div>{[children]}</div>;
}
}
const container = document.createElement('div');
expect(() => {
ReactDOM.render(<Foo />, container);
}).toThrowError(
'horizon child can not be object. use string / function / array.',
);
});
describe('with new features', () => {
xit('warns on function as a return value from a function', () => {
function Foo() {
return Foo;
}
const container = document.createElement('div');
expect(() => ReactDOM.render(<Foo />, container)).toErrorDev(
'Warning: horizon child can not be functions. use string / object / array.',
);
});
xit('warns on function as a return value from a class', () => {
class Foo extends React.Component {
render() {
return Foo;
}
}
const container = document.createElement('div');
expect(() => ReactDOM.render(<Foo />, container)).toErrorDev(
'Warning: horizon child can not be functions. use string / object / array.',
);
});
xit('warns on function as a child to host component', () => {
function Foo() {
return (
<div>
<span>{Foo}</span>
</div>
);
}
const container = document.createElement('div');
expect(() => ReactDOM.render(<Foo />, container)).toErrorDev(
'Warning: horizon child can not be functions. use string / object / array.',
);
});
it('does not warn for function-as-a-child that gets resolved', () => {
function Bar(props) {
return props.children();
}
function Foo() {
return <Bar>{() => 'Hello'}</Bar>;
}
const container = document.createElement('div');
ReactDOM.render(<Foo />, container);
expect(container.innerHTML).toBe('Hello');
});
xit('deduplicates function type warnings based on component type', () => {
class Foo extends React.PureComponent {
constructor() {
super();
this.state = {type: 'mushrooms'};
}
render() {
return (
<div>
{Foo}
{Foo}
<span>
{Foo}
{Foo}
</span>
</div>
);
}
}
const container = document.createElement('div');
let component;
expect(() => {
component = ReactDOM.render(<Foo />, container);
}).toErrorDev([
'Warning: horizon child can not be functions. use string / object / array.',
'Warning: horizon child can not be functions. use string / object / array.',
]);
component.setState({type: 'portobello mushrooms'});
});
});
});

View File

@ -0,0 +1,755 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
let React;
let ReactDOM;
let ReactTestUtils;
let PropTypes;
const clone = function(o) {
return JSON.parse(JSON.stringify(o));
};
const GET_load_STATE_RETURN_VAL = {
hasWillMountCompleted: false,
hasRenderCompleted: false,
hasDidMountCompleted: false,
hasWillUnmountCompleted: false,
};
const INIT_RENDER_STATE = {
hasWillMountCompleted: true,
hasRenderCompleted: false,
hasDidMountCompleted: false,
hasWillUnmountCompleted: false,
};
const DID_MOUNT_STATE = {
hasWillMountCompleted: true,
hasRenderCompleted: true,
hasDidMountCompleted: false,
hasWillUnmountCompleted: false,
};
const NEXT_RENDER_STATE = {
hasWillMountCompleted: true,
hasRenderCompleted: true,
hasDidMountCompleted: true,
hasWillUnmountCompleted: false,
};
const WILL_UNMOUNT_STATE = {
hasWillMountCompleted: true,
hasDidMountCompleted: true,
hasRenderCompleted: true,
hasWillUnmountCompleted: false,
};
const POST_WILL_UNMOUNT_STATE = {
hasWillMountCompleted: true,
hasDidMountCompleted: true,
hasRenderCompleted: true,
hasWillUnmountCompleted: true,
};
/**
* Every React component is in one of these life cycles.
*/
type ComponentLifeCycle =
/**
* Mounted components have a DOM node representation and are capable of
* receiving new props.
*/
| 'MOUNTED'
/**
* Unmounted components are inactive and cannot receive new props.
*/
| 'UNMOUNTED';
/**
* TODO: We should make any setState calls fail in
* `getInitialState` and `componentWillMount`. They will usually fail
* anyways because `this._renderedComponent` is empty, however, if a component
* is *reused*, then that won't be the case and things will appear to work in
* some cases. Better to just block all updates in initialization.
*/
describe('ReactComponentLifeCycle', () => {
beforeEach(() => {
jest.resetModules();
React = require('horizon-external');
ReactDOM = require('horizon');
ReactTestUtils = require('react-test-renderer/test-utils');
PropTypes = require('prop-types');
});
it('should not reuse an instance when it has been unmounted', () => {
const container = document.createElement('div');
class StatefulComponent extends React.Component {
state = {};
render() {
return <div />;
}
}
const element = <StatefulComponent />;
const firstInstance = ReactDOM.render(element, container);
ReactDOM.unmountComponentAtNode(container);
const secondInstance = ReactDOM.render(element, container);
expect(firstInstance).not.toBe(secondInstance);
});
it('should support state change in componentWillMount and componentWillReceiveProps', () => {
const container = document.createElement('div');
const logger = [];
class Inner extends React.Component {
state = {};
componentWillReceiveProps() {
this.state = {text: 'cWRP'};
}
componentWillMount() {
this.state = {text: 'cWM'};
}
render() {
logger.push(this.state.text);
return <div>{this.state.text}</div>;
}
}
class Outer extends React.Component {
state = {a: ''};
update = () => {
this.setState({a: 'change'});
}
render() {
return <Inner />;
}
}
const element = <Outer />;
const instance = ReactDOM.render(element, container);
instance.update();
expect(logger).toStrictEqual(['cWM', 'cWRP']);
});
/**
* If a state update triggers rerendering that in turn fires an onDOMReady,
* that second onDOMReady should not fail.
*/
it('it should fire onDOMReady when already in onDOMReady', () => {
const _testJournal = [];
class Child extends React.Component {
componentDidMount() {
_testJournal.push('Child:onDOMReady');
}
render() {
return <div />;
}
}
class SwitcherParent extends React.Component {
constructor(props) {
super(props);
_testJournal.push('SwitcherParent:getInitialState');
this.state = {showHasOnDOMReadyComponent: false};
}
componentDidMount() {
_testJournal.push('SwitcherParent:onDOMReady');
this.switchIt();
}
switchIt = () => {
this.setState({showHasOnDOMReadyComponent: true});
};
render() {
return (
<div>
{this.state.showHasOnDOMReadyComponent ? <Child /> : <div />}
</div>
);
}
}
ReactTestUtils.renderIntoDocument(<SwitcherParent />);
expect(_testJournal).toEqual([
'SwitcherParent:getInitialState',
'SwitcherParent:onDOMReady',
'Child:onDOMReady',
]);
});
// You could assign state here, but not access members of it, unless you
// had provided a getInitialState method.
it('throws when accessing state in componentWillMount', () => {
class StatefulComponent extends React.Component {
UNSAFE_componentWillMount() {
void this.state.yada;
}
render() {
return <div />;
}
}
let instance = <StatefulComponent />;
expect(function() {
instance = ReactTestUtils.renderIntoDocument(instance);
}).toThrow();
});
it('should allow update state inside of componentWillMount', () => {
class StatefulComponent extends React.Component {
UNSAFE_componentWillMount() {
this.setState({stateField: 'something'});
}
render() {
return <div />;
}
}
let instance = <StatefulComponent />;
expect(function() {
instance = ReactTestUtils.renderIntoDocument(instance);
}).not.toThrow();
});
xit('isMounted should return false when unmounted', () => {
class Component extends React.Component {
render() {
return <div />;
}
}
const container = document.createElement('div');
const instance = ReactDOM.render(<Component />, container);
// No longer a public API, but we can test that it works internally by
// reaching into the updater.
expect(instance._isMounted(instance)).toBe(true);
ReactDOM.unmountComponentAtNode(container);
expect(instance._isMounted(instance)).toBe(false);
});
it('should carry through each of the phases of setup', () => {
class LifeCycleComponent extends React.Component {
constructor(props, context) {
super(props, context);
this._testJournal = {};
const initState = {
hasWillMountCompleted: false,
hasDidMountCompleted: false,
hasRenderCompleted: false,
hasWillUnmountCompleted: false,
};
this._testJournal.returnedFromGetInitialState = clone(initState);
this.state = initState;
}
UNSAFE_componentWillMount() {
this._testJournal.stateAtStartOfWillMount = clone(this.state);
this.state.hasWillMountCompleted = true;
}
componentDidMount() {
this._testJournal.stateAtStartOfDidMount = clone(this.state);
this.setState({hasDidMountCompleted: true});
}
render() {
const isInitialRender = !this.state.hasRenderCompleted;
if (isInitialRender) {
this._testJournal.stateInInitialRender = clone(this.state);
} else {
this._testJournal.stateInLaterRender = clone(this.state);
}
// you would *NEVER* do anything like this in real code!
this.state.hasRenderCompleted = true;
return <div ref="theDiv">I am the inner DIV</div>;
}
componentWillUnmount() {
this._testJournal.stateAtStartOfWillUnmount = clone(this.state);
this.state.hasWillUnmountCompleted = true;
}
}
// A component that is merely "constructed" (as in "constructor") but not
// yet initialized, or rendered.
//
const container = document.createElement('div');
let instance = ReactDOM.render(<LifeCycleComponent />, container);
// getInitialState
expect(instance._testJournal.returnedFromGetInitialState).toEqual(
GET_load_STATE_RETURN_VAL,
);
// componentWillMount
expect(instance._testJournal.stateAtStartOfWillMount).toEqual(
instance._testJournal.returnedFromGetInitialState,
);
// componentDidMount
expect(instance._testJournal.stateAtStartOfDidMount).toEqual(
DID_MOUNT_STATE,
);
// initial render
expect(instance._testJournal.stateInInitialRender).toEqual(
INIT_RENDER_STATE,
);
// Now *update the component*
instance.forceUpdate();
// render 2nd time
expect(instance._testJournal.stateInLaterRender).toEqual(NEXT_RENDER_STATE);
ReactDOM.unmountComponentAtNode(container);
expect(instance._testJournal.stateAtStartOfWillUnmount).toEqual(
WILL_UNMOUNT_STATE,
);
// But the current lifecycle of the component is unmounted.
expect(instance.state).toEqual(POST_WILL_UNMOUNT_STATE);
});
it('should not throw when updating an auxiliary component', () => {
class Tooltip extends React.Component {
render() {
return <div>{this.props.children}</div>;
}
componentDidMount() {
this.container = document.createElement('div');
this.updateTooltip();
}
componentDidUpdate() {
this.updateTooltip();
}
updateTooltip = () => {
// Even though this.props.tooltip has an owner, updating it shouldn't
// throw here because it's mounted as a root component
ReactDOM.render(this.props.tooltip, this.container);
};
}
class Component extends React.Component {
render() {
return (
<Tooltip ref="tooltip" tooltip={<div>{this.props.tooltipText}</div>}>
{this.props.text}
</Tooltip>
);
}
}
const container = document.createElement('div');
ReactDOM.render(<Component text="uno" tooltipText="one" />, container);
// Since `instance` is a root component, we can set its props. This also
// makes Tooltip rerender the tooltip component, which shouldn't throw.
ReactDOM.render(<Component text="dos" tooltipText="two" />, container);
});
it('should allow state updates in componentDidMount', () => {
/**
* calls setState in an componentDidMount.
*/
class SetStateInComponentDidMount extends React.Component {
state = {
stateField: this.props.valueToUseInitially,
};
componentDidMount() {
this.setState({stateField: this.props.valueToUseInOnDOMReady});
}
render() {
return <div />;
}
}
let instance = (
<SetStateInComponentDidMount
valueToUseInitially="hello"
valueToUseInOnDOMReady="goodbye"
/>
);
instance = ReactTestUtils.renderIntoDocument(instance);
expect(instance.state.stateField).toBe('goodbye');
});
it('should call nested legacy lifecycle methods in the right order', () => {
let log;
const logger = function(msg) {
return function() {
// return true for shouldComponentUpdate
log.push(msg);
return true;
};
};
class Outer extends React.Component {
UNSAFE_componentWillMount = logger('outer componentWillMount');
componentDidMount = logger('outer componentDidMount');
UNSAFE_componentWillReceiveProps = logger(
'outer componentWillReceiveProps',
);
shouldComponentUpdate = logger('outer shouldComponentUpdate');
UNSAFE_componentWillUpdate = logger('outer componentWillUpdate');
componentDidUpdate = logger('outer componentDidUpdate');
componentWillUnmount = logger('outer componentWillUnmount');
render() {
return (
<div>
<Inner x={this.props.x} />
</div>
);
}
}
class Inner extends React.Component {
UNSAFE_componentWillMount = logger('inner componentWillMount');
componentDidMount = logger('inner componentDidMount');
UNSAFE_componentWillReceiveProps = logger(
'inner componentWillReceiveProps',
);
shouldComponentUpdate = logger('inner shouldComponentUpdate');
UNSAFE_componentWillUpdate = logger('inner componentWillUpdate');
componentDidUpdate = logger('inner componentDidUpdate');
componentWillUnmount = logger('inner componentWillUnmount');
render() {
return <span>{this.props.x}</span>;
}
}
const container = document.createElement('div');
log = [];
ReactDOM.render(<Outer x={1} />, container);
expect(log).toEqual([
'outer componentWillMount',
'inner componentWillMount',
'inner componentDidMount',
'outer componentDidMount',
]);
// Dedup warnings
log = [];
ReactDOM.render(<Outer x={2} />, container);
expect(log).toEqual([
'outer componentWillReceiveProps',
'outer shouldComponentUpdate',
'outer componentWillUpdate',
'inner componentWillReceiveProps',
'inner shouldComponentUpdate',
'inner componentWillUpdate',
'inner componentDidUpdate',
'outer componentDidUpdate',
]);
log = [];
ReactDOM.unmountComponentAtNode(container);
expect(log).toEqual([
'outer componentWillUnmount',
'inner componentWillUnmount',
]);
});
it('should call nested new lifecycle methods in the right order', () => {
let log;
const logger = function(msg) {
return function() {
// return true for shouldComponentUpdate
log.push(msg);
return true;
};
};
class Outer extends React.Component {
state = {};
static getDerivedStateFromProps(props, prevState) {
log.push('outer getDerivedStateFromProps');
return null;
}
componentDidMount = logger('outer componentDidMount');
shouldComponentUpdate = logger('outer shouldComponentUpdate');
getSnapshotBeforeUpdate = logger('outer getSnapshotBeforeUpdate');
componentDidUpdate = logger('outer componentDidUpdate');
componentWillUnmount = logger('outer componentWillUnmount');
render() {
return (
<div>
<Inner x={this.props.x} />
</div>
);
}
}
class Inner extends React.Component {
state = {};
static getDerivedStateFromProps(props, prevState) {
log.push('inner getDerivedStateFromProps');
return null;
}
componentDidMount = logger('inner componentDidMount');
shouldComponentUpdate = logger('inner shouldComponentUpdate');
getSnapshotBeforeUpdate = logger('inner getSnapshotBeforeUpdate');
componentDidUpdate = logger('inner componentDidUpdate');
componentWillUnmount = logger('inner componentWillUnmount');
render() {
return <span>{this.props.x}</span>;
}
}
const container = document.createElement('div');
log = [];
ReactDOM.render(<Outer x={1} />, container);
expect(log).toEqual([
'outer getDerivedStateFromProps',
'inner getDerivedStateFromProps',
'inner componentDidMount',
'outer componentDidMount',
]);
// Dedup warnings
log = [];
ReactDOM.render(<Outer x={2} />, container);
expect(log).toEqual([
'outer getDerivedStateFromProps',
'outer shouldComponentUpdate',
'inner getDerivedStateFromProps',
'inner shouldComponentUpdate',
'inner getSnapshotBeforeUpdate',
'outer getSnapshotBeforeUpdate',
'inner componentDidUpdate',
'outer componentDidUpdate',
]);
log = [];
ReactDOM.unmountComponentAtNode(container);
expect(log).toEqual([
'outer componentWillUnmount',
'inner componentWillUnmount',
]);
});
it('should not override state with stale values if prevState is spread within getDerivedStateFromProps', () => {
const divRef = React.createRef();
let childInstance;
class Child extends React.Component {
state = {local: 0};
static getDerivedStateFromProps(nextProps, prevState) {
return {...prevState, remote: nextProps.remote};
}
updateState = () => {
this.setState(state => ({local: state.local + 1}));
this.props.onChange(this.state.remote + 1);
};
render() {
childInstance = this;
return (
<div
onClick={this.updateState}
ref={
divRef
}>{`remote:${this.state.remote}, local:${this.state.local}`}</div>
);
}
}
class Parent extends React.Component {
state = {value: 0};
handleChange = value => {
this.setState({value});
};
render() {
return <Child remote={this.state.value} onChange={this.handleChange} />;
}
}
const container = document.createElement('div');
document.body.appendChild(container);
try {
ReactDOM.render(<Parent />, container);
expect(divRef.current.textContent).toBe('remote:0, local:0');
// Trigger setState() calls
childInstance.updateState();
expect(divRef.current.textContent).toBe('remote:1, local:1');
// Trigger batched setState() calls
divRef.current.click();
expect(divRef.current.textContent).toBe('remote:2, local:2');
} finally {
document.body.removeChild(container);
}
});
it('should pass the return value from getSnapshotBeforeUpdate to componentDidUpdate', () => {
const log = [];
class MyComponent extends React.Component {
state = {
value: 0,
};
static getDerivedStateFromProps(nextProps, prevState) {
return {
value: prevState.value + 1,
};
}
getSnapshotBeforeUpdate(prevProps, prevState) {
log.push(
`getSnapshotBeforeUpdate() prevProps:${prevProps.value} prevState:${prevState.value}`,
);
return 'abc';
}
componentDidUpdate(prevProps, prevState, snapshot) {
log.push(
`componentDidUpdate() prevProps:${prevProps.value} prevState:${prevState.value} snapshot:${snapshot}`,
);
}
render() {
log.push('render');
return null;
}
}
const div = document.createElement('div');
ReactDOM.render(
<div>
<MyComponent value="foo" />
</div>,
div,
);
expect(log).toEqual(['render']);
log.length = 0;
ReactDOM.render(
<div>
<MyComponent value="bar" />
</div>,
div,
);
expect(log).toEqual([
'render',
'getSnapshotBeforeUpdate() prevProps:foo prevState:1',
'componentDidUpdate() prevProps:foo prevState:1 snapshot:abc',
]);
log.length = 0;
ReactDOM.render(
<div>
<MyComponent value="baz" />
</div>,
div,
);
expect(log).toEqual([
'render',
'getSnapshotBeforeUpdate() prevProps:bar prevState:2',
'componentDidUpdate() prevProps:bar prevState:2 snapshot:abc',
]);
log.length = 0;
ReactDOM.render(<div />, div);
expect(log).toEqual([]);
});
it('should pass previous state to shouldComponentUpdate even with getDerivedStateFromProps', () => {
const divRef = React.createRef();
class SimpleComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
value: props.value,
};
}
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.value === prevState.value) {
return null;
}
return {value: nextProps.value};
}
shouldComponentUpdate(nextProps, nextState) {
return nextState.value !== this.state.value;
}
render() {
return <div ref={divRef}>value: {this.state.value}</div>;
}
}
const div = document.createElement('div');
ReactDOM.render(<SimpleComponent value="initial" />, div);
expect(divRef.current.textContent).toBe('value: initial');
ReactDOM.render(<SimpleComponent value="updated" />, div);
expect(divRef.current.textContent).toBe('value: updated');
});
it('should call getSnapshotBeforeUpdate before mutations are committed', () => {
const log = [];
class MyComponent extends React.Component {
divRef = React.createRef();
getSnapshotBeforeUpdate(prevProps, prevState) {
log.push('getSnapshotBeforeUpdate');
expect(this.divRef.current.textContent).toBe(
`value:${prevProps.value}`,
);
return 'foobar';
}
componentDidUpdate(prevProps, prevState, snapshot) {
log.push('componentDidUpdate');
expect(this.divRef.current.textContent).toBe(
`value:${this.props.value}`,
);
expect(snapshot).toBe('foobar');
}
render() {
log.push('render');
return <div ref={this.divRef}>{`value:${this.props.value}`}</div>;
}
}
const div = document.createElement('div');
ReactDOM.render(<MyComponent value="foo" />, div);
expect(log).toEqual(['render']);
log.length = 0;
ReactDOM.render(<MyComponent value="bar" />, div);
expect(log).toEqual([
'render',
'getSnapshotBeforeUpdate',
'componentDidUpdate',
]);
log.length = 0;
});
});

View File

@ -0,0 +1,80 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
// Requires
let React;
let ReactDOM;
let ReactTestUtils;
// Test components
let LowerLevelComposite;
let MyCompositeComponent;
let expectSingleChildlessDiv;
/**
* Integration test, testing the combination of JSX with our unit of
* abstraction, `ReactCompositeComponent` does not ever add superfluous DOM
* nodes.
*/
describe('ReactCompositeComponentDOMMinimalism', () => {
beforeEach(() => {
React = require('horizon-external');
ReactDOM = require('horizon');
ReactTestUtils = require('react-test-renderer/test-utils');
LowerLevelComposite = class extends React.Component {
render() {
return <div>{this.props.children}</div>;
}
};
MyCompositeComponent = class extends React.Component {
render() {
return <LowerLevelComposite>{this.props.children}</LowerLevelComposite>;
}
};
expectSingleChildlessDiv = function(instance) {
const el = ReactDOM.findDOMNode(instance);
expect(el.tagName).toBe('DIV');
expect(el.children.length).toBe(0);
};
});
it('should not render extra nodes for non-interpolated text', () => {
let instance = <MyCompositeComponent>A string child</MyCompositeComponent>;
instance = ReactTestUtils.renderIntoDocument(instance);
expectSingleChildlessDiv(instance);
});
it('should not render extra nodes for non-interpolated text', () => {
let instance = (
<MyCompositeComponent>{'Interpolated String Child'}</MyCompositeComponent>
);
instance = ReactTestUtils.renderIntoDocument(instance);
expectSingleChildlessDiv(instance);
});
it('should not render extra nodes for non-interpolated text', () => {
let instance = (
<MyCompositeComponent>
<ul>This text causes no children in ul, just innerHTML</ul>
</MyCompositeComponent>
);
instance = ReactTestUtils.renderIntoDocument(instance);
const el = ReactDOM.findDOMNode(instance);
expect(el.tagName).toBe('DIV');
expect(el.children.length).toBe(1);
expect(el.children[0].tagName).toBe('UL');
expect(el.children[0].children.length).toBe(0);
});
});

View File

@ -0,0 +1,119 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
let React;
let ReactDOM;
describe('ReactCompositeComponentNestedState-state', () => {
beforeEach(() => {
React = require('horizon-external');
ReactDOM = require('horizon');
});
it('should provide up to date values for props', () => {
class ParentComponent extends React.Component {
state = {color: 'blue'};
handleColor = color => {
this.props.logger('parent-handleColor', this.state.color);
this.setState({color: color}, function() {
this.props.logger('parent-after-setState', this.state.color);
});
};
render() {
this.props.logger('parent-render', this.state.color);
return (
<ChildComponent
logger={this.props.logger}
color={this.state.color}
onSelectColor={this.handleColor}
/>
);
}
}
class ChildComponent extends React.Component {
constructor(props) {
super(props);
props.logger('getInitialState', props.color);
this.state = {hue: 'dark ' + props.color};
}
handleHue = (shade, color) => {
this.props.logger('handleHue', this.state.hue, this.props.color);
this.props.onSelectColor(color);
this.setState(
function(state, props) {
this.props.logger(
'setState-this',
this.state.hue,
this.props.color,
);
this.props.logger('setState-args', state.hue, props.color);
return {hue: shade + ' ' + props.color};
},
function() {
this.props.logger(
'after-setState',
this.state.hue,
this.props.color,
);
},
);
};
render() {
this.props.logger('render', this.state.hue, this.props.color);
return (
<div>
<button onClick={this.handleHue.bind(this, 'dark', 'blue')}>
Dark Blue
</button>
<button onClick={this.handleHue.bind(this, 'light', 'blue')}>
Light Blue
</button>
<button onClick={this.handleHue.bind(this, 'dark', 'green')}>
Dark Green
</button>
<button onClick={this.handleHue.bind(this, 'light', 'green')}>
Light Green
</button>
</div>
);
}
}
const container = document.createElement('div');
document.body.appendChild(container);
const logger = jest.fn();
void ReactDOM.render(<ParentComponent logger={logger} />, container);
// click "light green"
container.childNodes[0].childNodes[3].click();
expect(logger.mock.calls).toEqual([
['parent-render', 'blue'],
['getInitialState', 'blue'],
['render', 'dark blue', 'blue'],
['handleHue', 'dark blue', 'blue'],
['parent-handleColor', 'blue'],
['parent-render', 'green'],
['setState-this', 'dark blue', 'blue'],
['setState-args', 'dark blue', 'green'],
['render', 'light green', 'green'],
['after-setState', 'light green', 'green'],
['parent-after-setState', 'green'],
]);
});
});

View File

@ -0,0 +1,435 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
let React;
let ReactDOM;
let TestComponent;
describe('ReactCompositeComponent-state', () => {
beforeEach(() => {
React = require('horizon-external');
ReactDOM = require('horizon');
TestComponent = class extends React.Component {
constructor(props) {
super(props);
this.peekAtState('getInitialState', undefined, props);
this.state = {color: 'red'};
}
peekAtState = (from, state = this.state, props = this.props) => {
props.stateListener(from, state && state.color);
};
peekAtCallback = from => {
return () => this.peekAtState(from);
};
setFavoriteColor(nextColor) {
this.setState(
{color: nextColor},
this.peekAtCallback('setFavoriteColor'),
);
}
render() {
this.peekAtState('render');
return <div>{this.state.color}</div>;
}
UNSAFE_componentWillMount() {
this.peekAtState('componentWillMount-start');
this.setState(function(state) {
this.peekAtState('before-setState-sunrise', state);
});
this.setState(
{color: 'sunrise'},
this.peekAtCallback('setState-sunrise'),
);
this.setState(function(state) {
this.peekAtState('after-setState-sunrise', state);
});
this.peekAtState('componentWillMount-after-sunrise');
this.setState(
{color: 'orange'},
this.peekAtCallback('setState-orange'),
);
this.setState(function(state) {
this.peekAtState('after-setState-orange', state);
});
this.peekAtState('componentWillMount-end');
}
componentDidMount() {
this.peekAtState('componentDidMount-start');
this.setState(
{color: 'yellow'},
this.peekAtCallback('setState-yellow'),
);
this.peekAtState('componentDidMount-end');
}
UNSAFE_componentWillReceiveProps(newProps) {
this.peekAtState('componentWillReceiveProps-start');
if (newProps.nextColor) {
this.setState(function(state) {
this.peekAtState('before-setState-receiveProps', state);
return {color: newProps.nextColor};
});
// No longer a public API, but we can test that it works internally by
// reaching into the updater.
this.setState(function(state) {
this.peekAtState('before-setState-again-receiveProps', state);
return {color: newProps.nextColor};
}, this.peekAtCallback('setState-receiveProps'));
this.setState(function(state) {
this.peekAtState('after-setState-receiveProps', state);
});
}
this.peekAtState('componentWillReceiveProps-end');
}
shouldComponentUpdate(nextProps, nextState) {
this.peekAtState('shouldComponentUpdate-currentState');
this.peekAtState('shouldComponentUpdate-nextState', nextState);
return true;
}
UNSAFE_componentWillUpdate(nextProps, nextState) {
this.peekAtState('componentWillUpdate-currentState');
this.peekAtState('componentWillUpdate-nextState', nextState);
}
componentDidUpdate(prevProps, prevState) {
this.peekAtState('componentDidUpdate-currentState');
this.peekAtState('componentDidUpdate-prevState', prevState);
}
componentWillUnmount() {
this.peekAtState('componentWillUnmount');
}
};
});
it('should support setting state', () => {
const container = document.createElement('div');
document.body.appendChild(container);
const stateListener = jest.fn();
const instance = ReactDOM.render(
<TestComponent stateListener={stateListener} />,
container,
function peekAtInitialCallback() {
this.peekAtState('initial-callback');
},
);
ReactDOM.render(
<TestComponent stateListener={stateListener} nextColor="green" />,
container,
instance.peekAtCallback('setProps'),
);
instance.setFavoriteColor('blue');
instance.forceUpdate(instance.peekAtCallback('forceUpdate'));
ReactDOM.unmountComponentAtNode(container);
const expected = [
// there is no state when getInitialState() is called
['getInitialState', null],
['componentWillMount-start', 'red'],
// setState()'s only enqueue pending states.
['componentWillMount-after-sunrise', 'red'],
['componentWillMount-end', 'red'],
// pending state queue is processed
['before-setState-sunrise', 'red'],
['after-setState-sunrise', 'sunrise'],
['after-setState-orange', 'orange'],
// pending state has been applied
['render', 'orange'],
['componentDidMount-start', 'orange'],
// setState-sunrise and setState-orange should be called here,
// after the bug in #1740
// componentDidMount() called setState({color:'yellow'}), which is async.
// The update doesn't happen until the next flush.
['componentDidMount-end', 'orange'],
['setState-sunrise', 'orange'],
['setState-orange', 'orange'],
['initial-callback', 'orange'],
['shouldComponentUpdate-currentState', 'orange'],
['shouldComponentUpdate-nextState', 'yellow'],
['componentWillUpdate-currentState', 'orange'],
['componentWillUpdate-nextState', 'yellow'],
['render', 'yellow'],
['componentDidUpdate-currentState', 'yellow'],
['componentDidUpdate-prevState', 'orange'],
['setState-yellow', 'yellow'],
['componentWillReceiveProps-start', 'yellow'],
// setState({color:'green'}) only enqueues a pending state.
['componentWillReceiveProps-end', 'yellow'],
// pending state queue is processed
// We keep updates in the queue to support
['before-setState-receiveProps', 'yellow'],
['before-setState-again-receiveProps', 'green'],
['after-setState-receiveProps', 'green'],
['shouldComponentUpdate-currentState', 'yellow'],
['shouldComponentUpdate-nextState', 'green'],
['componentWillUpdate-currentState', 'yellow'],
['componentWillUpdate-nextState', 'green'],
['render', 'green'],
['componentDidUpdate-currentState', 'green'],
['componentDidUpdate-prevState', 'yellow'],
['setState-receiveProps', 'green'],
['setProps', 'green'],
// setFavoriteColor('blue')
['shouldComponentUpdate-currentState', 'green'],
['shouldComponentUpdate-nextState', 'blue'],
['componentWillUpdate-currentState', 'green'],
['componentWillUpdate-nextState', 'blue'],
['render', 'blue'],
['componentDidUpdate-currentState', 'blue'],
['componentDidUpdate-prevState', 'green'],
['setFavoriteColor', 'blue'],
// forceUpdate()
['componentWillUpdate-currentState', 'blue'],
['componentWillUpdate-nextState', 'blue'],
['render', 'blue'],
['componentDidUpdate-currentState', 'blue'],
['componentDidUpdate-prevState', 'blue'],
['forceUpdate', 'blue'],
// unmountComponent()
// state is available within `componentWillUnmount()`
['componentWillUnmount', 'blue'],
];
expect(stateListener.mock.calls.join('\n')).toEqual(expected.join('\n'));
});
it('should call componentDidUpdate of children first', () => {
const container = document.createElement('div');
let ops = [];
let child = null;
let parent = null;
class Child extends React.Component {
state = {bar: false};
componentDidMount() {
child = this;
}
componentDidUpdate() {
ops.push('child did update');
}
render() {
return <div />;
}
}
let shouldUpdate = true;
class Intermediate extends React.Component {
shouldComponentUpdate() {
return shouldUpdate;
}
render() {
return <Child />;
}
}
class Parent extends React.Component {
state = {foo: false};
componentDidMount() {
parent = this;
}
componentDidUpdate() {
ops.push('parent did update');
}
render() {
return <Intermediate />;
}
}
ReactDOM.render(<Parent />, container);
ReactDOM.unstable_batchedUpdates(() => {
parent.setState({foo: true});
child.setState({bar: true});
});
// When we render changes top-down in a batch, children's componentDidUpdate
// happens before the parent.
expect(ops).toEqual(['child did update', 'parent did update']);
shouldUpdate = false;
ops = [];
ReactDOM.unstable_batchedUpdates(() => {
parent.setState({foo: false});
child.setState({bar: false});
});
// We expect the same thing to happen if we bail out in the middle.
expect(ops).toEqual(['child did update', 'parent did update']);
});
it('should batch unmounts', () => {
class Inner extends React.Component {
render() {
return <div />;
}
componentWillUnmount() {
// This should get silently ignored (maybe with a warning), but it
// shouldn't break React.
outer.setState({showInner: false});
}
}
class Outer extends React.Component {
state = {showInner: true};
render() {
return <div>{this.state.showInner && <Inner />}</div>;
}
}
const container = document.createElement('div');
const outer = ReactDOM.render(<Outer />, container);
expect(() => {
ReactDOM.unmountComponentAtNode(container);
}).not.toThrow();
});
it('should update state when called from child cWRP', function() {
const log = [];
class Parent extends React.Component {
state = {value: 'one'};
render() {
log.push('parent render ' + this.state.value);
return <Child parent={this} value={this.state.value} />;
}
}
let updated = false;
class Child extends React.Component {
UNSAFE_componentWillReceiveProps() {
if (updated) {
return;
}
log.push('child componentWillReceiveProps ' + this.props.value);
this.props.parent.setState({value: 'two'});
log.push('child componentWillReceiveProps done ' + this.props.value);
updated = true;
}
render() {
log.push('child render ' + this.props.value);
return <div>{this.props.value}</div>;
}
}
const container = document.createElement('div');
ReactDOM.render(<Parent />, container);
ReactDOM.render(<Parent />, container);
expect(log).toEqual([
'parent render one',
'child render one',
'parent render one',
'child componentWillReceiveProps one',
'child componentWillReceiveProps done one',
'child render one',
'parent render two',
'child render two',
]);
});
it('should merge state when sCU returns false', function() {
const log = [];
class Test extends React.Component {
state = {a: 0};
render() {
return null;
}
shouldComponentUpdate(nextProps, nextState) {
log.push(
'scu from ' +
Object.keys(this.state) +
' to ' +
Object.keys(nextState),
);
return false;
}
}
const container = document.createElement('div');
const test = ReactDOM.render(<Test />, container);
test.setState({b: 0});
expect(log.length).toBe(1);
test.setState({c: 0});
expect(log.length).toBe(2);
expect(log).toEqual(['scu from a to a,b', 'scu from a,b to a,b,c']);
});
xit('should support getDerivedStateFromProps for module pattern components', () => {
function Child() {
return {
state: {
count: 1,
},
render() {
return <div>{`count:${this.state.count}`}</div>;
},
};
}
Child.getDerivedStateFromProps = (props, prevState) => {
return {
count: prevState.count + props.incrementBy,
};
};
const el = document.createElement('div');
ReactDOM.render(<Child incrementBy={0} />, el);
expect(el.textContent).toBe('count:1');
ReactDOM.render(<Child incrementBy={2} />, el);
expect(el.textContent).toBe('count:3');
ReactDOM.render(<Child incrementBy={1} />, el);
expect(el.textContent).toBe('count:4');
});
it('should support setState in componentWillUnmount', () => {
let subscription;
class A extends React.Component {
componentWillUnmount() {
subscription();
}
render() {
return 'A';
}
}
class B extends React.Component {
state = {siblingUnmounted: false};
UNSAFE_componentWillMount() {
subscription = () => this.setState({siblingUnmounted: true});
}
render() {
return 'B' + (this.state.siblingUnmounted ? ' No Sibling' : '');
}
}
const el = document.createElement('div');
ReactDOM.render(<A />, el);
expect(el.textContent).toBe('A');
ReactDOM.render(<B />, el);
expect(el.textContent).toBe('B No Sibling');
});
});

View File

@ -0,0 +1,301 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
let React;
let ReactDOM;
let ReactTestUtils;
describe('ReactDOM', () => {
beforeEach(() => {
jest.resetModules();
React = require('horizon-external');
ReactDOM = require('horizon');
ReactTestUtils = require('react-test-renderer/test-utils');
});
it('should bubble onSubmit', function() {
const container = document.createElement('div');
let count = 0;
let buttonRef;
function Parent() {
return (
<div
onSubmit={event => {
event.preventDefault();
count++;
}}>
<Child />
</div>
);
}
function Child() {
return (
<form>
<input type="submit" ref={button => (buttonRef = button)} />
</form>
);
}
document.body.appendChild(container);
try {
ReactDOM.render(<Parent />, container);
buttonRef.click();
expect(count).toBe(1);
} finally {
document.body.removeChild(container);
}
});
it('allows a DOM element to be used with a string', () => {
const element = React.createElement('div', {className: 'foo'});
const node = ReactTestUtils.renderIntoDocument(element);
expect(node.tagName).toBe('DIV');
});
it('should allow children to be passed as an argument', () => {
const argNode = ReactTestUtils.renderIntoDocument(
React.createElement('div', null, 'child'),
);
expect(argNode.innerHTML).toBe('child');
});
it('should overwrite props.children with children argument', () => {
const conflictNode = ReactTestUtils.renderIntoDocument(
React.createElement('div', {children: 'fakechild'}, 'child'),
);
expect(conflictNode.innerHTML).toBe('child');
});
/**
* We need to make sure that updates occur to the actual node that's in the
* DOM, instead of a stale cache.
*/
it('should purge the DOM cache when removing nodes', () => {
let myDiv = ReactTestUtils.renderIntoDocument(
<div>
<div key="theDog" className="dog" />,
<div key="theBird" className="bird" />
</div>,
);
// Warm the cache with theDog
myDiv = ReactTestUtils.renderIntoDocument(
<div>
<div key="theDog" className="dogbeforedelete" />,
<div key="theBird" className="bird" />,
</div>,
);
// Remove theDog - this should purge the cache
myDiv = ReactTestUtils.renderIntoDocument(
<div>
<div key="theBird" className="bird" />,
</div>,
);
// Now, put theDog back. It's now a different DOM node.
myDiv = ReactTestUtils.renderIntoDocument(
<div>
<div key="theDog" className="dog" />,
<div key="theBird" className="bird" />,
</div>,
);
// Change the className of theDog. It will use the same element
myDiv = ReactTestUtils.renderIntoDocument(
<div>
<div key="theDog" className="bigdog" />,
<div key="theBird" className="bird" />,
</div>,
);
const dog = myDiv.childNodes[0];
expect(dog.className).toBe('bigdog');
});
it('preserves focus', () => {
let input;
let input2;
class A extends React.Component {
render() {
return (
<div>
<input id="one" ref={r => (input = input || r)} />
{this.props.showTwo && (
<input id="two" ref={r => (input2 = input2 || r)} />
)}
</div>
);
}
componentDidUpdate() {
// Focus should have been restored to the original input
expect(document.activeElement.id).toBe('one');
input2.focus();
expect(document.activeElement.id).toBe('two');
log.push('input2 focused');
}
}
const log = [];
const container = document.createElement('div');
document.body.appendChild(container);
try {
ReactDOM.render(<A showTwo={false} />, container);
input.focus();
// When the second input is added, let's simulate losing focus, which is
// something that could happen when manipulating DOM nodes (but is hard to
// deterministically force without relying intensely on React DOM
// implementation details)
const div = container.firstChild;
['appendChild', 'insertBefore'].forEach(name => {
const mutator = div[name];
div[name] = function() {
if (input) {
input.blur();
expect(document.activeElement.tagName).toBe('BODY');
log.push('input2 inserted');
}
return mutator.apply(this, arguments);
};
});
expect(document.activeElement.id).toBe('one');
ReactDOM.render(<A showTwo={true} />, container);
// input2 gets added, which causes input to get blurred. Then
// componentDidUpdate focuses input2 and that should make it down to here,
// not get overwritten by focus restoration.
expect(document.activeElement.id).toBe('two');
expect(log).toEqual(['input2 inserted', 'input2 focused']);
} finally {
document.body.removeChild(container);
}
});
it('calls focus() on autoFocus elements after they have been mounted to the DOM', () => {
const originalFocus = HTMLElement.prototype.focus;
try {
let focusedElement;
let inputFocusedAfterMount = false;
// This test needs to determine that focus is called after mount.
// Can't check document.activeElement because PhantomJS is too permissive;
// It doesn't require element to be in the DOM to be focused.
HTMLElement.prototype.focus = function() {
focusedElement = this;
inputFocusedAfterMount = !!this.parentNode;
};
const container = document.createElement('div');
document.body.appendChild(container);
ReactDOM.render(
<div>
<h1>Auto-focus Test</h1>
<input autoFocus={true} />
<p>The above input should be focused after mount.</p>
</div>,
container,
);
expect(inputFocusedAfterMount).toBe(true);
expect(focusedElement.tagName).toBe('INPUT');
} finally {
HTMLElement.prototype.focus = originalFocus;
}
});
it("shouldn't fire duplicate event handler while handling other nested dispatch", () => {
const actual = [];
class Wrapper extends React.Component {
componentDidMount() {
this.ref1.click();
}
render() {
return (
<div>
<div
onClick={() => {
actual.push('1st node clicked');
this.ref2.click();
}}
ref={ref => (this.ref1 = ref)}
/>
<div
onClick={ref => {
actual.push("2nd node clicked imperatively from 1st's handler");
}}
ref={ref => (this.ref2 = ref)}
/>
</div>
);
}
}
const container = document.createElement('div');
document.body.appendChild(container);
try {
ReactDOM.render(<Wrapper />, container);
const expected = [
'1st node clicked',
"2nd node clicked imperatively from 1st's handler",
];
expect(actual).toEqual(expected);
} finally {
document.body.removeChild(container);
}
});
it('should not crash with devtools installed', () => {
try {
global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
inject: function() {},
onCommitFiberRoot: function() {},
onCommitFiberUnmount: function() {},
supportsFiber: true,
};
jest.resetModules();
React = require('horizon-external');
ReactDOM = require('horizon');
class Component extends React.Component {
render() {
return <div />;
}
}
ReactDOM.render(<Component />, document.createElement('container'));
} finally {
delete global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
}
});
it('should not crash calling findDOMNode inside a function component', () => {
const container = document.createElement('div');
class Component extends React.Component {
render() {
return <div />;
}
}
const instance = ReactTestUtils.renderIntoDocument(<Component />);
const App = () => {
ReactDOM.findDOMNode(instance);
return <div />;
};
if (isDev) {
ReactDOM.render(<App />, container);
}
});
});

View File

@ -0,0 +1,98 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
describe('ReactDOM unknown attribute', () => {
let React;
let ReactDOM;
beforeEach(() => {
jest.resetModules();
React = require('horizon-external');
ReactDOM = require('horizon');
});
function testUnknownAttributeRemoval(givenValue) {
const el = document.createElement('div');
ReactDOM.render(<div unknown="something" />, el);
expect(el.firstChild.getAttribute('unknown')).toBe('something');
ReactDOM.render(<div unknown={givenValue} />, el);
expect(el.firstChild.hasAttribute('unknown')).toBe(false);
}
function testUnknownAttributeAssignment(givenValue, expectedDOMValue) {
const el = document.createElement('div');
ReactDOM.render(<div unknown="something" />, el);
expect(el.firstChild.getAttribute('unknown')).toBe('something');
ReactDOM.render(<div unknown={givenValue} />, el);
expect(el.firstChild.getAttribute('unknown')).toBe(expectedDOMValue);
}
describe('unknown attributes', () => {
it('removes values null and undefined', () => {
testUnknownAttributeRemoval(null);
testUnknownAttributeRemoval(undefined);
});
it('removes unknown attributes that were rendered but are now missing', () => {
const el = document.createElement('div');
ReactDOM.render(<div unknown="something" />, el);
expect(el.firstChild.getAttribute('unknown')).toBe('something');
ReactDOM.render(<div />, el);
expect(el.firstChild.hasAttribute('unknown')).toBe(false);
});
it('passes through strings', () => {
testUnknownAttributeAssignment('a string', 'a string');
});
it('coerces numbers to strings', () => {
testUnknownAttributeAssignment(0, '0');
testUnknownAttributeAssignment(-1, '-1');
testUnknownAttributeAssignment(42, '42');
testUnknownAttributeAssignment(9000.99, '9000.99');
});
it('coerces objects to strings and warns', () => {
const lol = {
toString() {
return 'lol';
},
};
testUnknownAttributeAssignment({hello: 'world'}, '[object Object]');
testUnknownAttributeAssignment(lol, 'lol');
});
xit('removes symbols and warns', () => {
expect(() => testUnknownAttributeRemoval(Symbol('foo'))).toErrorDev(
'Warning: Invalid value for prop `unknown` on <div> tag.'
);
});
xit('removes functions and warns', () => {
expect(() =>
testUnknownAttributeRemoval(function someFunction() {}),
).toErrorDev(
'Warning: Invalid value for prop `unknown` on <div> tag.'
);
});
it('allows camelCase unknown attributes and warns', () => {
const el = document.createElement('div');
expect(() =>
ReactDOM.render(<div helloWorld="something" />, el),
).not.toThrow();
expect(el.firstChild.getAttribute('helloworld')).toBe('something');
});
});
});

Some files were not shown because too many files have changed in this diff Show More