Match-id-cd6b6945978e1da6b3a4e5e91f3b66e13a834dfd
This commit is contained in:
commit
8c062ba6b7
|
@ -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
|
|
@ -0,0 +1,9 @@
|
|||
node_modules
|
||||
.idea
|
||||
.vscode
|
||||
.run
|
||||
path.json
|
||||
package-lock.json
|
||||
|
||||
coverage
|
||||
packages/libs
|
|
@ -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/
|
|
@ -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
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
|
@ -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
|
|
@ -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'
|
||||
],
|
||||
};
|
|
@ -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链接失败');
|
||||
});
|
||||
});
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
horizon.production.js
|
||||
remote-repo
|
|
@ -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
|
||||
```
|
|
@ -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;
|
|
@ -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
|
||||
);
|
||||
};
|
||||
})();
|
|
@ -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;
|
|
@ -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 |
|
@ -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 |
|
@ -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
File diff suppressed because it is too large
Load Diff
|
@ -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;
|
|
@ -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
|
@ -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;
|
|
@ -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
|
@ -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;
|
|
@ -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>
|
|
@ -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,
|
||||
};
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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 sequence–with complete mock events–is 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});
|
||||
```
|
|
@ -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"`;
|
|
@ -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,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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,
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"private": true,
|
||||
"name": "dom-event-testing-library",
|
||||
"version": "0.0.0"
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
# memlab内存泄露分析
|
||||
```shell
|
||||
npm run runPage
|
||||
npm run memlab
|
||||
```
|
||||
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@babel/preset-env',
|
||||
'@babel/preset-react'
|
||||
],
|
||||
};
|
|
@ -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 };
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>)
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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')
|
||||
);
|
|
@ -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>
|
|
@ -0,0 +1,3 @@
|
|||
.link {
|
||||
margin-right: 1rem;
|
||||
}
|
|
@ -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;
|
|
@ -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.
|
|
@ -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';
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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.
|
|
@ -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';
|
|
@ -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/"
|
||||
]
|
||||
}
|
|
@ -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';
|
|
@ -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
|
||||
);
|
|
@ -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
|
@ -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.
|
|
@ -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';
|
|
@ -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/"
|
||||
]
|
||||
}
|
|
@ -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';
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
42
packages/inula-test/packages/react-test-renderer/src/ReactDebugCurrentFrame.js
vendored
Normal file
42
packages/inula-test/packages/react-test-renderer/src/ReactDebugCurrentFrame.js
vendored
Normal 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;
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
239
packages/inula-test/packages/react-test-renderer/src/ReactTestHostConfig.js
vendored
Normal file
239
packages/inula-test/packages/react-test-renderer/src/ReactTestHostConfig.js
vendored
Normal 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
|
||||
}
|
|
@ -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,
|
||||
};
|
|
@ -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';
|
||||
|
|
@ -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"`);
|
||||
});
|
||||
});
|
117
packages/inula-test/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js
vendored
Normal file
117
packages/inula-test/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js
vendored
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
105
packages/inula-test/packages/react-test-renderer/src/__tests__/ReactTestRendererAct-test.js
vendored
Normal file
105
packages/inula-test/packages/react-test-renderer/src/__tests__/ReactTestRendererAct-test.js
vendored
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
241
packages/inula-test/packages/react-test-renderer/src/__tests__/ReactTestRendererTraversal-test.js
vendored
Normal file
241
packages/inula-test/packages/react-test-renderer/src/__tests__/ReactTestRendererTraversal-test.js
vendored
Normal 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);
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 it’s 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,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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.',
|
||||
);
|
||||
});
|
||||
});
|
480
packages/inula-test/packages/react-test-renderer/src/__tests__/__react-dom__/ReactComponent-test.js
vendored
Normal file
480
packages/inula-test/packages/react-test-renderer/src/__tests__/__react-dom__/ReactComponent-test.js
vendored
Normal 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'});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
});
|
||||
});
|
File diff suppressed because it is too large
Load Diff
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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'],
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
301
packages/inula-test/packages/react-test-renderer/src/__tests__/__react-dom__/ReactDOM-test.js
vendored
Normal file
301
packages/inula-test/packages/react-test-renderer/src/__tests__/__react-dom__/ReactDOM-test.js
vendored
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -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
Loading…
Reference in New Issue