Compare commits
113 Commits
walaura-es
...
fabric-foc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e78755102 | ||
|
|
598739c2f4 | ||
|
|
95bd7aad7d | ||
|
|
29b4d0721c | ||
|
|
b43b36dd91 | ||
|
|
9e075d16b2 | ||
|
|
bc1f3e1a69 | ||
|
|
b6173e643a | ||
|
|
8aefb1995c | ||
|
|
643dcb5526 | ||
|
|
1e1a989422 | ||
|
|
3bd6adceda | ||
|
|
50eafef07e | ||
|
|
4f480d0645 | ||
|
|
f4ca909e0e | ||
|
|
255d9ac5f5 | ||
|
|
64aae7b06f | ||
|
|
d8570ec6ea | ||
|
|
05e487a12f | ||
|
|
5d6129b5d1 | ||
|
|
ff900095b9 | ||
|
|
1b6e269751 | ||
|
|
3ac81a57fb | ||
|
|
435259f189 | ||
|
|
2e4948a34d | ||
|
|
e706721490 | ||
|
|
b979db4e72 | ||
|
|
18875b2401 | ||
|
|
19f6fe170c | ||
|
|
6250462bed | ||
|
|
5e21157d86 | ||
|
|
40568e9773 | ||
|
|
5586b3022c | ||
|
|
7580d6717c | ||
|
|
80e32487b9 | ||
|
|
c1b573bf2f | ||
|
|
fc921a1693 | ||
|
|
1c63b2e416 | ||
|
|
2bb227ef80 | ||
|
|
24f824250f | ||
|
|
edeea07207 | ||
|
|
9cc094a19a | ||
|
|
f749045a5b | ||
|
|
59f21f1b24 | ||
|
|
7e2ab87a61 | ||
|
|
5d3d71b1dd | ||
|
|
cca994c6a1 | ||
|
|
b05cd611ba | ||
|
|
195b3db618 | ||
|
|
2c1e5d2b22 | ||
|
|
86883d7aac | ||
|
|
b4e2fac09d | ||
|
|
e6226e6493 | ||
|
|
3bec713b85 | ||
|
|
ee38c00bd5 | ||
|
|
4d463fce4e | ||
|
|
d259f45813 | ||
|
|
9357a483e9 | ||
|
|
22ef96ae63 | ||
|
|
2b903da355 | ||
|
|
0eac01abcd | ||
|
|
f887d1aa27 | ||
|
|
1959671d45 | ||
|
|
ccc6100d7f | ||
|
|
6215e1c2db | ||
|
|
85d9655d6f | ||
|
|
3e09677836 | ||
|
|
31350237fa | ||
|
|
79734771cd | ||
|
|
dbc46ac63e | ||
|
|
1b9328cd9b | ||
|
|
897976600c | ||
|
|
72592310a8 | ||
|
|
e7494c86c5 | ||
|
|
a5e951d4cc | ||
|
|
95056b6836 | ||
|
|
fd2a3ae57f | ||
|
|
933f6a07ca | ||
|
|
7dc9745427 | ||
|
|
9354dd2752 | ||
|
|
4c270375e9 | ||
|
|
6fef7c47a9 | ||
|
|
9fe1031244 | ||
|
|
e84327ba0c | ||
|
|
4b0cdf29a8 | ||
|
|
24dbe851e8 | ||
|
|
bb75a4fa89 | ||
|
|
7309c5f934 | ||
|
|
9bb3fbe893 | ||
|
|
3c54df0914 | ||
|
|
b66e86d957 | ||
|
|
c2d1561c60 | ||
|
|
0253ee9a2e | ||
|
|
36a6e29bb3 | ||
|
|
7c21bf72ac | ||
|
|
f42431abe1 | ||
|
|
0b5a26a489 | ||
|
|
0cf22a56a1 | ||
|
|
b6c423daad | ||
|
|
8a347ed024 | ||
|
|
be603f5a51 | ||
|
|
b15bf36750 | ||
|
|
612a76812f | ||
|
|
12c000412d | ||
|
|
2afeebdcc4 | ||
|
|
9ac42dd074 | ||
|
|
7bf40e1cfd | ||
|
|
031a5aaffb | ||
|
|
e039e690b5 | ||
|
|
b617db3d96 | ||
|
|
3c1efa0d77 | ||
|
|
9e937e7e86 | ||
|
|
b43eec7eaa |
@@ -193,6 +193,22 @@ jobs:
|
||||
- dist
|
||||
- sizes/*.json
|
||||
|
||||
build_devtools_and_process_artifacts:
|
||||
docker: *docker
|
||||
environment: *environment
|
||||
parallelism: 20
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace: *attach_workspace
|
||||
- *restore_yarn_cache
|
||||
- *run_yarn
|
||||
- run:
|
||||
environment:
|
||||
RELEASE_CHANNEL: experimental
|
||||
command: ./scripts/circleci/pack_and_store_devtools_artifacts.sh
|
||||
- store_artifacts:
|
||||
path: ./build/devtools.tgz
|
||||
|
||||
# These jobs are named differently so we can distinguish the stable and
|
||||
# and experimental artifacts
|
||||
process_artifacts: *process_artifacts
|
||||
@@ -267,7 +283,7 @@ jobs:
|
||||
RELEASE_CHANNEL: experimental
|
||||
command: yarn test-build --maxWorkers=2
|
||||
|
||||
test_build_devtools:
|
||||
test_devtools:
|
||||
docker: *docker
|
||||
environment: *environment
|
||||
steps:
|
||||
@@ -277,8 +293,8 @@ jobs:
|
||||
- *run_yarn
|
||||
- run:
|
||||
environment:
|
||||
RELEASE_CHANNEL: stable
|
||||
command: yarn test-build --maxWorkers=2
|
||||
RELEASE_CHANNEL: experimental
|
||||
command: yarn test-build-devtools --maxWorkers=2
|
||||
|
||||
test_dom_fixtures:
|
||||
docker: *docker
|
||||
@@ -374,9 +390,6 @@ workflows:
|
||||
- test_build_prod:
|
||||
requires:
|
||||
- build
|
||||
- test_build_devtools:
|
||||
requires:
|
||||
- build
|
||||
- test_dom_fixtures:
|
||||
requires:
|
||||
- build
|
||||
@@ -405,6 +418,12 @@ workflows:
|
||||
- lint_build:
|
||||
requires:
|
||||
- build_experimental
|
||||
- test_devtools:
|
||||
requires:
|
||||
- build_experimental
|
||||
- build_devtools_and_process_artifacts:
|
||||
requires:
|
||||
- build_experimental
|
||||
|
||||
fuzz_tests:
|
||||
triggers:
|
||||
|
||||
25
.eslintrc.js
25
.eslintrc.js
@@ -9,7 +9,10 @@ const OFF = 0;
|
||||
const ERROR = 2;
|
||||
|
||||
module.exports = {
|
||||
extends: 'fbjs',
|
||||
extends: [
|
||||
'fbjs',
|
||||
'prettier'
|
||||
],
|
||||
|
||||
// Stop ESLint from looking for a configuration file in parent folders
|
||||
root: true,
|
||||
@@ -30,10 +33,10 @@ module.exports = {
|
||||
rules: {
|
||||
'accessor-pairs': OFF,
|
||||
'brace-style': [ERROR, '1tbs'],
|
||||
'comma-dangle': [ERROR, 'always-multiline'],
|
||||
'consistent-return': OFF,
|
||||
'dot-location': [ERROR, 'property'],
|
||||
'dot-notation': ERROR,
|
||||
// We use console['error']() as a signal to not transform it:
|
||||
'dot-notation': [ERROR, {allowPattern: '^(error|warn)$'}],
|
||||
'eol-last': ERROR,
|
||||
eqeqeq: [ERROR, 'allow-null'],
|
||||
indent: OFF,
|
||||
@@ -92,7 +95,8 @@ module.exports = {
|
||||
// the second argument of warning/invariant should be a literal string
|
||||
'react-internal/no-primitive-constructors': ERROR,
|
||||
'react-internal/no-to-warn-dev-within-to-throw': ERROR,
|
||||
'react-internal/warning-and-invariant-args': ERROR,
|
||||
'react-internal/invariant-args': ERROR,
|
||||
'react-internal/warning-args': ERROR,
|
||||
'react-internal/no-production-logging': ERROR,
|
||||
},
|
||||
|
||||
@@ -134,6 +138,19 @@ module.exports = {
|
||||
'jest/valid-expect-in-promise': ERROR,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'**/__tests__/**/*.js',
|
||||
'scripts/**/*.js',
|
||||
'packages/*/npm/**/*.js',
|
||||
'packages/dom-event-testing-library/**/*.js',
|
||||
'packages/react-devtools*/**/*.js'
|
||||
],
|
||||
rules: {
|
||||
'react-internal/no-production-logging': OFF,
|
||||
'react-internal/warning-args': OFF,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/react-native-renderer/**/*.js'],
|
||||
globals: {
|
||||
|
||||
16
.github/ISSUE_TEMPLATE.md
vendored
16
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,14 +1,4 @@
|
||||
<!--
|
||||
Note: if the issue is about documentation or the website, please file it at:
|
||||
https://github.com/reactjs/reactjs.org/issues/new
|
||||
-->
|
||||
👉 Please follow one of these issue templates:
|
||||
- https://github.com/facebook/react/issues/new/choose
|
||||
|
||||
**Do you want to request a *feature* or report a *bug*?**
|
||||
|
||||
**What is the current behavior?**
|
||||
|
||||
**If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn't have dependencies other than React. Paste the link to your JSFiddle (https://jsfiddle.net/Luktwrdm/) or CodeSandbox (https://codesandbox.io/s/new) example below:**
|
||||
|
||||
**What is the expected behavior?**
|
||||
|
||||
**Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?**
|
||||
Note: to keep the backlog clean and actionable, issues may be immediately closed if they do not follow one of the above issue templates.
|
||||
|
||||
41
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
41
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: "🐛 Bug Report"
|
||||
about: Report a reproducible bug or regression.
|
||||
title: 'Bug: '
|
||||
labels: 'Status: Unconfirmed'
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
Please provide a clear and concise description of what the bug is. Include
|
||||
screenshots if needed. Please test using the latest version of the relevant
|
||||
React packages to make sure your issue has not already been fixed.
|
||||
-->
|
||||
|
||||
React version:
|
||||
|
||||
## Steps To Reproduce
|
||||
|
||||
1.
|
||||
2.
|
||||
|
||||
<!--
|
||||
Your bug will get fixed much faster if we can run your code and it doesn't
|
||||
have dependencies other than React. Issues without reproduction steps or
|
||||
code examples may be immediately closed as not actionable.
|
||||
-->
|
||||
|
||||
Link to code example:
|
||||
|
||||
<!--
|
||||
Please provide a CodeSandbox (https://codesandbox.io/s/new), a link to a
|
||||
repository on GitHub, or provide a minimal code example that reproduces the
|
||||
problem. You may provide a screenshot of the application if you think it is
|
||||
relevant to your bug report. Here are some tips for providing a minimal
|
||||
example: https://stackoverflow.com/help/mcve.
|
||||
-->
|
||||
|
||||
## The current behavior
|
||||
|
||||
|
||||
## The expected behavior
|
||||
13
.github/ISSUE_TEMPLATE/documentation.md
vendored
Normal file
13
.github/ISSUE_TEMPLATE/documentation.md
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
name: "📃 Documentation Issue"
|
||||
about: This issue tracker is not for documentation issues. Please file documentation issues at https://github.com/reactjs/reactjs.org.
|
||||
title: 'Docs: '
|
||||
labels: 'Resolution: Invalid'
|
||||
|
||||
---
|
||||
|
||||
🚨 This issue tracker is not for documentation issues. 🚨
|
||||
|
||||
The React website is hosted on a separate repository. You may let the
|
||||
team know about any issues with the documentation by opening an issue there:
|
||||
- https://github.com/reactjs/reactjs.org/issues/new
|
||||
29
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
29
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: "🤔 Questions and Help"
|
||||
about: This issue tracker is not for questions. Please ask questions at https://stackoverflow.com/questions/tagged/react.
|
||||
title: 'Question: '
|
||||
labels: 'Resolution: Invalid, Type: Question'
|
||||
|
||||
---
|
||||
|
||||
🚨 This issue tracker is not for questions. 🚨
|
||||
|
||||
As it happens, support requests that are created as issues are likely to be closed. We want to make sure you are able to find the help you seek. Please take a look at the following resources.
|
||||
|
||||
## Coding Questions
|
||||
|
||||
If you have a coding question related to React and React DOM, it might be better suited for Stack Overflow. It's a great place to browse through frequent questions about using React, as well as ask for help with specific questions.
|
||||
|
||||
https://stackoverflow.com/questions/tagged/react
|
||||
|
||||
## Talk to other React developers
|
||||
|
||||
There are many online forums which are a great place for discussion about best practices and application architecture as well as the future of React.
|
||||
|
||||
https://reactjs.org/community/support.html#popular-discussion-forums
|
||||
|
||||
## Proposals
|
||||
|
||||
If you'd like to discuss topics related to the future of React, or would like to propose a new feature or change before sending a pull request, please check out the discussions and proposals repository.
|
||||
|
||||
https://github.com/reactjs/rfcs
|
||||
37
.github/PULL_REQUEST_TEMPLATE.md
vendored
37
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,14 +1,27 @@
|
||||
**Before submitting a pull request,** please make sure the following is done:
|
||||
<!--
|
||||
Thanks for submitting a pull request!
|
||||
We appreciate you spending the time to work on these changes. Please provide enough information so that others can review your pull request. The three fields below are mandatory.
|
||||
|
||||
1. Fork [the repository](https://github.com/facebook/react) and create your branch from `master`.
|
||||
2. Run `yarn` in the repository root.
|
||||
3. If you've fixed a bug or added code that should be tested, add tests!
|
||||
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch TestName` is helpful in development.
|
||||
5. Run `yarn test-prod` to test in the production environment. It supports the same options as `yarn test`.
|
||||
6. If you need a debugger, run `yarn debug-test --watch TestName`, open `chrome://inspect`, and press "Inspect".
|
||||
7. Format your code with [prettier](https://github.com/prettier/prettier) (`yarn prettier`).
|
||||
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only check changed files.
|
||||
9. Run the [Flow](https://flowtype.org/) typechecks (`yarn flow`).
|
||||
10. If you haven't already, complete the CLA.
|
||||
Before submitting a pull request, please make sure the following is done:
|
||||
|
||||
**Learn more about contributing:** https://reactjs.org/docs/how-to-contribute.html
|
||||
1. Fork [the repository](https://github.com/facebook/react) and create your branch from `master`.
|
||||
2. Run `yarn` in the repository root.
|
||||
3. If you've fixed a bug or added code that should be tested, add tests!
|
||||
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch TestName` is helpful in development.
|
||||
5. Run `yarn test-prod` to test in the production environment. It supports the same options as `yarn test`.
|
||||
6. If you need a debugger, run `yarn debug-test --watch TestName`, open `chrome://inspect`, and press "Inspect".
|
||||
7. Format your code with [prettier](https://github.com/prettier/prettier) (`yarn prettier`).
|
||||
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only check changed files.
|
||||
9. Run the [Flow](https://flowtype.org/) typechecks (`yarn flow`).
|
||||
10. If you haven't already, complete the CLA.
|
||||
|
||||
Learn more about contributing: https://reactjs.org/docs/how-to-contribute.html
|
||||
-->
|
||||
|
||||
## Summary
|
||||
|
||||
<!-- Explain the **motivation** for making this change. What existing problem does the pull request solve? -->
|
||||
|
||||
## Test Plan
|
||||
|
||||
<!-- Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes the user interface. -->
|
||||
|
||||
31
.github/stale.yml
vendored
Normal file
31
.github/stale.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
# Configuration for probot-stale - https://github.com/probot/stale
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 90
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- "Partner"
|
||||
- "React Core Team"
|
||||
- "Resolution: Backlog"
|
||||
- "Type: Bug"
|
||||
- "Type: Discussion"
|
||||
- "Type: Regression"
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: "Resolution: Stale"
|
||||
issues:
|
||||
# Comment to post when marking an issue as stale.
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.
|
||||
Thank you for your contribution.
|
||||
# Comment to post when closing a stale issue.
|
||||
closeComment: >
|
||||
Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please create a new issue with up-to-date information. Thank you!
|
||||
pulls:
|
||||
# Comment to post when marking a pull request as stale.
|
||||
markComment: >
|
||||
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.
|
||||
Thank you for your contribution.
|
||||
# Comment to post when closing a stale pull request.
|
||||
closeComment: >
|
||||
Closing this pull request after a prolonged period of inactivity. If this issue is still present in the latest release, please ask for this pull request to be reopened. Thank you!
|
||||
@@ -8,7 +8,7 @@ module.exports = {
|
||||
jsxBracketSameLine: true,
|
||||
trailingComma: 'es5',
|
||||
printWidth: 80,
|
||||
parser: 'babylon',
|
||||
parser: 'babel',
|
||||
|
||||
overrides: [
|
||||
{
|
||||
|
||||
7
SECURITY.md
Normal file
7
SECURITY.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Reporting Security Issues
|
||||
|
||||
If you believe you have found a security vulnerability in React, we encourage you to let us know right away. We will investigate all legitimate reports and do our best to quickly fix the problem.
|
||||
|
||||
Please refer to the following page for our responsible disclosure policy, reward guidelines, and those things that should not be reported:
|
||||
|
||||
https://www.facebook.com/whitehat
|
||||
@@ -461,9 +461,7 @@ function prepareState(initGlobals) {
|
||||
hasSameBehaviorForAll,
|
||||
rowPatternHash,
|
||||
// "Good enough" id that we can store in localStorage
|
||||
rowIdHash: `${attribute.name} ${attribute.tagName} ${
|
||||
attribute.overrideStringValue
|
||||
}`,
|
||||
rowIdHash: `${attribute.name} ${attribute.tagName} ${attribute.overrideStringValue}`,
|
||||
};
|
||||
const rowGroup = rowPatternHashes.get(rowPatternHash) || new Set();
|
||||
rowGroup.add(row);
|
||||
@@ -866,14 +864,12 @@ class App extends React.Component {
|
||||
// Sort
|
||||
switch (sortOrder) {
|
||||
case ALPHABETICAL:
|
||||
return filteredAttributes.sort(
|
||||
(attr1, attr2) =>
|
||||
attr1.name.toLowerCase() < attr2.name.toLowerCase() ? -1 : 1
|
||||
return filteredAttributes.sort((attr1, attr2) =>
|
||||
attr1.name.toLowerCase() < attr2.name.toLowerCase() ? -1 : 1
|
||||
);
|
||||
case REV_ALPHABETICAL:
|
||||
return filteredAttributes.sort(
|
||||
(attr1, attr2) =>
|
||||
attr1.name.toLowerCase() < attr2.name.toLowerCase() ? 1 : -1
|
||||
return filteredAttributes.sort((attr1, attr2) =>
|
||||
attr1.name.toLowerCase() < attr2.name.toLowerCase() ? 1 : -1
|
||||
);
|
||||
case GROUPED_BY_ROW_PATTERN: {
|
||||
return filteredAttributes.sort((attr1, attr2) => {
|
||||
|
||||
@@ -268,7 +268,7 @@ async-each@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
|
||||
|
||||
async@^1.4.0, async@^1.5.2:
|
||||
async@^1.5.2:
|
||||
version "1.5.2"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
|
||||
|
||||
@@ -1448,6 +1448,10 @@ commander@2.11.x, commander@~2.11.0:
|
||||
version "2.11.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563"
|
||||
|
||||
commander@~2.20.3:
|
||||
version "2.20.3"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
||||
|
||||
commondir@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
|
||||
@@ -2838,14 +2842,14 @@ handle-thing@^1.2.5:
|
||||
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4"
|
||||
|
||||
handlebars@^4.0.3:
|
||||
version "4.0.10"
|
||||
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.10.tgz#3d30c718b09a3d96f23ea4cc1f403c4d3ba9ff4f"
|
||||
version "4.5.3"
|
||||
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.5.3.tgz#5cf75bd8714f7605713511a56be7c349becb0482"
|
||||
dependencies:
|
||||
async "^1.4.0"
|
||||
neo-async "^2.6.0"
|
||||
optimist "^0.6.1"
|
||||
source-map "^0.4.4"
|
||||
source-map "^0.6.1"
|
||||
optionalDependencies:
|
||||
uglify-js "^2.6"
|
||||
uglify-js "^3.1.4"
|
||||
|
||||
har-schema@^1.0.5:
|
||||
version "1.0.5"
|
||||
@@ -4180,6 +4184,10 @@ negotiator@0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
|
||||
|
||||
neo-async@^2.6.0:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
|
||||
|
||||
no-case@^2.2.0:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.1.tgz#7aeba1c73a52184265554b7dc03baf720df80081"
|
||||
@@ -5696,11 +5704,9 @@ source-map@0.5.x, source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, sourc
|
||||
version "0.5.7"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
|
||||
|
||||
source-map@^0.4.4:
|
||||
version "0.4.4"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
|
||||
dependencies:
|
||||
amdefine ">=0.0.4"
|
||||
source-map@^0.6.1, source-map@~0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
|
||||
|
||||
source-map@~0.2.0:
|
||||
version "0.2.0"
|
||||
@@ -6090,7 +6096,7 @@ uglify-js@3.0.x, uglify-js@^3.0.13:
|
||||
commander "~2.11.0"
|
||||
source-map "~0.5.1"
|
||||
|
||||
uglify-js@^2.6, uglify-js@^2.8.29:
|
||||
uglify-js@^2.8.29:
|
||||
version "2.8.29"
|
||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"
|
||||
dependencies:
|
||||
@@ -6099,6 +6105,13 @@ uglify-js@^2.6, uglify-js@^2.8.29:
|
||||
optionalDependencies:
|
||||
uglify-to-browserify "~1.0.0"
|
||||
|
||||
uglify-js@^3.1.4:
|
||||
version "3.7.3"
|
||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.7.3.tgz#f918fce9182f466d5140f24bb0ff35c2d32dcc6a"
|
||||
dependencies:
|
||||
commander "~2.20.3"
|
||||
source-map "~0.6.1"
|
||||
|
||||
uglify-to-browserify@~1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
|
||||
|
||||
@@ -55,4 +55,4 @@ class IframeSubtree extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default (ReactDOM.createPortal ? IframePortal : IframeSubtree);
|
||||
export default ReactDOM.createPortal ? IframePortal : IframeSubtree;
|
||||
|
||||
@@ -13,7 +13,7 @@ export default class ButtonTestCases extends React.Component {
|
||||
<FixtureSet title="Buttons">
|
||||
<TestCase
|
||||
title="onClick with disabled buttons"
|
||||
description="The onClick event handler should not be invoked when clicking on a disabled buyaton">
|
||||
description="The onClick event handler should not be invoked when clicking on a disabled button">
|
||||
<TestCase.Steps>
|
||||
<li>Click on the disabled button</li>
|
||||
</TestCase.Steps>
|
||||
|
||||
@@ -33,7 +33,8 @@ export default function Home() {
|
||||
<td>
|
||||
<a href="https://www.mozilla.org/en-US/firefox/organizations/">
|
||||
ESR<sup>†</sup>
|
||||
</a>, Latest
|
||||
</a>
|
||||
, Latest
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -172,7 +172,7 @@ class SelectFixture extends React.Component {
|
||||
<option value="monkey">monkey</option>
|
||||
<option value="lion">lion</option>
|
||||
<option value="mongoose">mongoose</option>
|
||||
<option value="tiger">tiget</option>
|
||||
<option value="tiger">tiger</option>
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -213,8 +213,8 @@ class TextInputFixtures extends React.Component {
|
||||
|
||||
<Fixture>
|
||||
<SuspendyTree>
|
||||
<img src="https://upload.wikimedia.org/wikipedia/commons/e/ee/Atom_%282%29.png" />React
|
||||
is cool
|
||||
<img src="https://upload.wikimedia.org/wikipedia/commons/e/ee/Atom_%282%29.png" />
|
||||
React is cool
|
||||
</SuspendyTree>
|
||||
</Fixture>
|
||||
</TestCase>
|
||||
@@ -308,7 +308,9 @@ class TextInputFixtures extends React.Component {
|
||||
<div style={{height: 200, overflow: 'scroll'}}>
|
||||
{Array(20)
|
||||
.fill()
|
||||
.map((_, i) => <h2 key={i}>{i + 1}</h2>)}
|
||||
.map((_, i) => (
|
||||
<h2 key={i}>{i + 1}</h2>
|
||||
))}
|
||||
</div>
|
||||
</SuspendyTree>
|
||||
</Fixture>
|
||||
|
||||
@@ -205,9 +205,7 @@ const createMatcherFor = consoleMethod =>
|
||||
if (withoutStack !== warningsWithoutComponentStack.length) {
|
||||
return {
|
||||
message: () =>
|
||||
`Expected ${withoutStack} warnings without a component stack but received ${
|
||||
warningsWithoutComponentStack.length
|
||||
}:\n` +
|
||||
`Expected ${withoutStack} warnings without a component stack but received ${warningsWithoutComponentStack.length}:\n` +
|
||||
warningsWithoutComponentStack.map(warning =>
|
||||
this.utils.printReceived(warning)
|
||||
),
|
||||
|
||||
@@ -338,26 +338,16 @@ async-each@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
|
||||
|
||||
async@^1.4.0, async@^1.5.2:
|
||||
async@^1.5.2:
|
||||
version "1.5.2"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
|
||||
|
||||
async@^2.1.2, async@^2.4.1:
|
||||
async@^2.1.2, async@^2.1.4, async@^2.4.1:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d"
|
||||
dependencies:
|
||||
lodash "^4.14.0"
|
||||
|
||||
async@^2.1.4:
|
||||
version "2.1.4"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-2.1.4.tgz#2d2160c7788032e4dd6cbe2502f1f9a2c8f6cde4"
|
||||
dependencies:
|
||||
lodash "^4.14.0"
|
||||
|
||||
async@~0.2.6:
|
||||
version "0.2.10"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
|
||||
|
||||
asynckit@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||
@@ -1682,6 +1672,11 @@ commander@^2.9.0:
|
||||
dependencies:
|
||||
graceful-readlink ">= 1.0.0"
|
||||
|
||||
commander@~2.20.3:
|
||||
version "2.20.3"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
||||
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
|
||||
|
||||
commondir@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
|
||||
@@ -2723,7 +2718,7 @@ faye-websocket@~0.11.0:
|
||||
|
||||
fb-watchman@^1.8.0:
|
||||
version "1.9.0"
|
||||
resolved "http://registry.npmjs.org/fb-watchman/-/fb-watchman-1.9.0.tgz#6f268f1f347a6b3c875d1e89da7e1ed79adfc0ec"
|
||||
resolved "https://registry.npmjs.org/fb-watchman/-/fb-watchman-1.9.0.tgz#6f268f1f347a6b3c875d1e89da7e1ed79adfc0ec"
|
||||
dependencies:
|
||||
bser "^1.0.2"
|
||||
|
||||
@@ -3141,14 +3136,15 @@ handle-thing@^1.2.5:
|
||||
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4"
|
||||
|
||||
handlebars@^4.0.3:
|
||||
version "4.0.6"
|
||||
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.6.tgz#2ce4484850537f9c97a8026d5399b935c4ed4ed7"
|
||||
version "4.5.3"
|
||||
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.5.3.tgz#5cf75bd8714f7605713511a56be7c349becb0482"
|
||||
integrity sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==
|
||||
dependencies:
|
||||
async "^1.4.0"
|
||||
neo-async "^2.6.0"
|
||||
optimist "^0.6.1"
|
||||
source-map "^0.4.4"
|
||||
source-map "^0.6.1"
|
||||
optionalDependencies:
|
||||
uglify-js "^2.6"
|
||||
uglify-js "^3.1.4"
|
||||
|
||||
har-schema@^1.0.5:
|
||||
version "1.0.5"
|
||||
@@ -3597,7 +3593,7 @@ is-plain-obj@^1.0.0:
|
||||
|
||||
is-posix-bracket@^0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "http://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4"
|
||||
resolved "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4"
|
||||
|
||||
is-primitive@^2.0.0:
|
||||
version "2.0.0"
|
||||
@@ -4461,7 +4457,7 @@ minimatch@^3.0.4:
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@0.0.8, minimist@~0.0.1:
|
||||
minimist@0.0.8:
|
||||
version "0.0.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
|
||||
|
||||
@@ -4469,6 +4465,11 @@ minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
|
||||
|
||||
minimist@~0.0.1:
|
||||
version "0.0.10"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
|
||||
integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=
|
||||
|
||||
mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
|
||||
version "0.5.1"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
|
||||
@@ -4520,6 +4521,11 @@ negotiator@0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
|
||||
|
||||
neo-async@^2.6.0:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
|
||||
integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
|
||||
|
||||
no-case@^2.2.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.0.tgz#ca2825ccb76b18e6f79d573dcfbf1eace33dd164"
|
||||
@@ -4775,6 +4781,7 @@ opn@5.1.0:
|
||||
optimist@^0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
|
||||
integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY=
|
||||
dependencies:
|
||||
minimist "~0.0.1"
|
||||
wordwrap "~0.0.2"
|
||||
@@ -6146,11 +6153,10 @@ source-map@0.5.6, source-map@0.5.x, source-map@^0.5.0, source-map@^0.5.3, source
|
||||
version "0.5.6"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
|
||||
|
||||
source-map@^0.4.4:
|
||||
version "0.4.4"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
|
||||
dependencies:
|
||||
amdefine ">=0.0.4"
|
||||
source-map@^0.6.1, source-map@~0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
|
||||
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
|
||||
|
||||
source-map@~0.2.0:
|
||||
version "0.2.0"
|
||||
@@ -6580,15 +6586,6 @@ uglify-js@3.0.x, uglify-js@^3.0.13:
|
||||
commander "~2.11.0"
|
||||
source-map "~0.5.1"
|
||||
|
||||
uglify-js@^2.6:
|
||||
version "2.7.5"
|
||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.7.5.tgz#4612c0c7baaee2ba7c487de4904ae122079f2ca8"
|
||||
dependencies:
|
||||
async "~0.2.6"
|
||||
source-map "~0.5.1"
|
||||
uglify-to-browserify "~1.0.0"
|
||||
yargs "~3.10.0"
|
||||
|
||||
uglify-js@^2.8.29:
|
||||
version "2.8.29"
|
||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"
|
||||
@@ -6598,6 +6595,14 @@ uglify-js@^2.8.29:
|
||||
optionalDependencies:
|
||||
uglify-to-browserify "~1.0.0"
|
||||
|
||||
uglify-js@^3.1.4:
|
||||
version "3.7.3"
|
||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.7.3.tgz#f918fce9182f466d5140f24bb0ff35c2d32dcc6a"
|
||||
integrity sha512-7tINm46/3puUA4hCkKYo4Xdts+JDaVC9ZPRcG8Xw9R4nhO/gZgUM3TENq8IF4Vatk8qCig4MzP/c8G4u2BkVQg==
|
||||
dependencies:
|
||||
commander "~2.20.3"
|
||||
source-map "~0.6.1"
|
||||
|
||||
uglify-to-browserify@~1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
|
||||
@@ -6941,6 +6946,7 @@ wordwrap@0.0.2:
|
||||
wordwrap@~0.0.2:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
|
||||
integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc=
|
||||
|
||||
wordwrap@~1.0.0:
|
||||
version "1.0.0"
|
||||
|
||||
@@ -8,22 +8,19 @@ function Comment({comment, commentSource}) {
|
||||
const currentUserID = comment.viewer.id;
|
||||
const environment = RelayEnvironment.forUser(currentUserID);
|
||||
const commentID = nullthrows(comment.id);
|
||||
useEffect(
|
||||
() => {
|
||||
const subscription = SubscriptionCounter.subscribeOnce(
|
||||
`StoreSubscription_${commentID}`,
|
||||
() =>
|
||||
StoreSubscription.subscribe(
|
||||
environment,
|
||||
{
|
||||
comment_id: commentID,
|
||||
},
|
||||
currentUserID,
|
||||
commentSource
|
||||
)
|
||||
);
|
||||
return () => subscription.dispose();
|
||||
},
|
||||
[commentID, commentSource, currentUserID, environment]
|
||||
);
|
||||
useEffect(() => {
|
||||
const subscription = SubscriptionCounter.subscribeOnce(
|
||||
`StoreSubscription_${commentID}`,
|
||||
() =>
|
||||
StoreSubscription.subscribe(
|
||||
environment,
|
||||
{
|
||||
comment_id: commentID,
|
||||
},
|
||||
currentUserID,
|
||||
commentSource
|
||||
)
|
||||
);
|
||||
return () => subscription.dispose();
|
||||
}, [commentID, commentSource, currentUserID, environment]);
|
||||
}
|
||||
|
||||
@@ -83,10 +83,6 @@ alphanum-sort@^1.0.1, alphanum-sort@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
|
||||
|
||||
amdefine@>=0.0.4:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
|
||||
|
||||
ansi-align@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-1.1.0.tgz#2f0c1658829739add5ebb15e6b0c6e3423f016ba"
|
||||
@@ -260,7 +256,7 @@ async-each@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
|
||||
|
||||
async@^1.4.0, async@^1.5.2:
|
||||
async@^1.5.2:
|
||||
version "1.5.2"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
|
||||
|
||||
@@ -1419,6 +1415,10 @@ commander@2.11.x, commander@~2.11.0:
|
||||
version "2.11.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563"
|
||||
|
||||
commander@~2.20.3:
|
||||
version "2.20.3"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
||||
|
||||
commondir@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
|
||||
@@ -2818,14 +2818,14 @@ handle-thing@^1.2.5:
|
||||
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4"
|
||||
|
||||
handlebars@^4.0.3:
|
||||
version "4.0.11"
|
||||
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.11.tgz#630a35dfe0294bc281edae6ffc5d329fc7982dcc"
|
||||
version "4.5.3"
|
||||
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.5.3.tgz#5cf75bd8714f7605713511a56be7c349becb0482"
|
||||
dependencies:
|
||||
async "^1.4.0"
|
||||
neo-async "^2.6.0"
|
||||
optimist "^0.6.1"
|
||||
source-map "^0.4.4"
|
||||
source-map "^0.6.1"
|
||||
optionalDependencies:
|
||||
uglify-js "^2.6"
|
||||
uglify-js "^3.1.4"
|
||||
|
||||
har-schema@^1.0.5:
|
||||
version "1.0.5"
|
||||
@@ -4179,6 +4179,10 @@ negotiator@0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
|
||||
|
||||
neo-async@^2.6.0:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
|
||||
|
||||
no-case@^2.2.0:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac"
|
||||
@@ -5723,12 +5727,6 @@ source-map@0.5.x, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, sourc
|
||||
version "0.5.7"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
|
||||
|
||||
source-map@^0.4.4:
|
||||
version "0.4.4"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
|
||||
dependencies:
|
||||
amdefine ">=0.0.4"
|
||||
|
||||
source-map@^0.6.1, source-map@~0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
|
||||
@@ -6119,7 +6117,7 @@ uglify-js@3.1.x, uglify-js@^3.0.13:
|
||||
commander "~2.11.0"
|
||||
source-map "~0.6.1"
|
||||
|
||||
uglify-js@^2.6, uglify-js@^2.8.29:
|
||||
uglify-js@^2.8.29:
|
||||
version "2.8.29"
|
||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"
|
||||
dependencies:
|
||||
@@ -6128,6 +6126,13 @@ uglify-js@^2.6, uglify-js@^2.8.29:
|
||||
optionalDependencies:
|
||||
uglify-to-browserify "~1.0.0"
|
||||
|
||||
uglify-js@^3.1.4:
|
||||
version "3.7.3"
|
||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.7.3.tgz#f918fce9182f466d5140f24bb0ff35c2d32dcc6a"
|
||||
dependencies:
|
||||
commander "~2.20.3"
|
||||
source-map "~0.6.1"
|
||||
|
||||
uglify-to-browserify@~1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
|
||||
|
||||
@@ -192,8 +192,7 @@ class App extends Component {
|
||||
}
|
||||
/>
|
||||
<p>
|
||||
Step {currentStep}
|
||||
: {friendlyAction} (
|
||||
Step {currentStep}: {friendlyAction} (
|
||||
<a style={{color: 'gray'}} onClick={this.handleEdit} href="#">
|
||||
Edit
|
||||
</a>
|
||||
|
||||
@@ -330,83 +330,76 @@ export default function Fibers({fibers, show, graphSettings, ...rest}) {
|
||||
]}
|
||||
</div>
|
||||
</Vertex>,
|
||||
fiber.child &&
|
||||
show.child && (
|
||||
<Edge
|
||||
source={fiber.id}
|
||||
target={fiber.child}
|
||||
kind="child"
|
||||
weight={1000}
|
||||
key={`${fiber.id}-${fiber.child}-child`}>
|
||||
child
|
||||
</Edge>
|
||||
),
|
||||
fiber.sibling &&
|
||||
show.sibling && (
|
||||
<Edge
|
||||
source={fiber.id}
|
||||
target={fiber.sibling}
|
||||
kind="sibling"
|
||||
weight={2000}
|
||||
key={`${fiber.id}-${fiber.sibling}-sibling`}>
|
||||
sibling
|
||||
</Edge>
|
||||
),
|
||||
fiber.return &&
|
||||
show.return && (
|
||||
<Edge
|
||||
source={fiber.id}
|
||||
target={fiber.return}
|
||||
kind="return"
|
||||
weight={1000}
|
||||
key={`${fiber.id}-${fiber.return}-return`}>
|
||||
return
|
||||
</Edge>
|
||||
),
|
||||
fiber.nextEffect &&
|
||||
show.fx && (
|
||||
<Edge
|
||||
source={fiber.id}
|
||||
target={fiber.nextEffect}
|
||||
kind="fx"
|
||||
weight={100}
|
||||
key={`${fiber.id}-${fiber.nextEffect}-nextEffect`}>
|
||||
nextFx
|
||||
</Edge>
|
||||
),
|
||||
fiber.firstEffect &&
|
||||
show.fx && (
|
||||
<Edge
|
||||
source={fiber.id}
|
||||
target={fiber.firstEffect}
|
||||
kind="fx"
|
||||
weight={100}
|
||||
key={`${fiber.id}-${fiber.firstEffect}-firstEffect`}>
|
||||
firstFx
|
||||
</Edge>
|
||||
),
|
||||
fiber.lastEffect &&
|
||||
show.fx && (
|
||||
<Edge
|
||||
source={fiber.id}
|
||||
target={fiber.lastEffect}
|
||||
kind="fx"
|
||||
weight={100}
|
||||
key={`${fiber.id}-${fiber.lastEffect}-lastEffect`}>
|
||||
lastFx
|
||||
</Edge>
|
||||
),
|
||||
fiber.alternate &&
|
||||
show.alt && (
|
||||
<Edge
|
||||
source={fiber.id}
|
||||
target={fiber.alternate}
|
||||
kind="alt"
|
||||
weight={10}
|
||||
key={`${fiber.id}-${fiber.alternate}-alt`}>
|
||||
alt
|
||||
</Edge>
|
||||
),
|
||||
fiber.child && show.child && (
|
||||
<Edge
|
||||
source={fiber.id}
|
||||
target={fiber.child}
|
||||
kind="child"
|
||||
weight={1000}
|
||||
key={`${fiber.id}-${fiber.child}-child`}>
|
||||
child
|
||||
</Edge>
|
||||
),
|
||||
fiber.sibling && show.sibling && (
|
||||
<Edge
|
||||
source={fiber.id}
|
||||
target={fiber.sibling}
|
||||
kind="sibling"
|
||||
weight={2000}
|
||||
key={`${fiber.id}-${fiber.sibling}-sibling`}>
|
||||
sibling
|
||||
</Edge>
|
||||
),
|
||||
fiber.return && show.return && (
|
||||
<Edge
|
||||
source={fiber.id}
|
||||
target={fiber.return}
|
||||
kind="return"
|
||||
weight={1000}
|
||||
key={`${fiber.id}-${fiber.return}-return`}>
|
||||
return
|
||||
</Edge>
|
||||
),
|
||||
fiber.nextEffect && show.fx && (
|
||||
<Edge
|
||||
source={fiber.id}
|
||||
target={fiber.nextEffect}
|
||||
kind="fx"
|
||||
weight={100}
|
||||
key={`${fiber.id}-${fiber.nextEffect}-nextEffect`}>
|
||||
nextFx
|
||||
</Edge>
|
||||
),
|
||||
fiber.firstEffect && show.fx && (
|
||||
<Edge
|
||||
source={fiber.id}
|
||||
target={fiber.firstEffect}
|
||||
kind="fx"
|
||||
weight={100}
|
||||
key={`${fiber.id}-${fiber.firstEffect}-firstEffect`}>
|
||||
firstFx
|
||||
</Edge>
|
||||
),
|
||||
fiber.lastEffect && show.fx && (
|
||||
<Edge
|
||||
source={fiber.id}
|
||||
target={fiber.lastEffect}
|
||||
kind="fx"
|
||||
weight={100}
|
||||
key={`${fiber.id}-${fiber.lastEffect}-lastEffect`}>
|
||||
lastFx
|
||||
</Edge>
|
||||
),
|
||||
fiber.alternate && show.alt && (
|
||||
<Edge
|
||||
source={fiber.id}
|
||||
target={fiber.alternate}
|
||||
kind="alt"
|
||||
weight={10}
|
||||
key={`${fiber.id}-${fiber.alternate}-alt`}>
|
||||
alt
|
||||
</Edge>
|
||||
),
|
||||
])}
|
||||
</Graph>
|
||||
</div>
|
||||
|
||||
@@ -194,7 +194,7 @@ async@^0.9.0:
|
||||
version "0.9.2"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
|
||||
|
||||
async@^1.3.0, async@^1.4.0, async@^1.4.2, async@^1.5.0:
|
||||
async@^1.3.0, async@^1.4.2, async@^1.5.0:
|
||||
version "1.5.2"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
|
||||
|
||||
@@ -1236,6 +1236,10 @@ commander@2.9.x, commander@^2.9.0:
|
||||
dependencies:
|
||||
graceful-readlink ">= 1.0.0"
|
||||
|
||||
commander@~2.20.3:
|
||||
version "2.20.3"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
||||
|
||||
commondir@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
|
||||
@@ -2302,14 +2306,14 @@ gzip-size@3.0.0:
|
||||
duplexer "^0.1.1"
|
||||
|
||||
handlebars@^4.0.3:
|
||||
version "4.0.6"
|
||||
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.6.tgz#2ce4484850537f9c97a8026d5399b935c4ed4ed7"
|
||||
version "4.5.3"
|
||||
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.5.3.tgz#5cf75bd8714f7605713511a56be7c349becb0482"
|
||||
dependencies:
|
||||
async "^1.4.0"
|
||||
neo-async "^2.6.0"
|
||||
optimist "^0.6.1"
|
||||
source-map "^0.4.4"
|
||||
source-map "^0.6.1"
|
||||
optionalDependencies:
|
||||
uglify-js "^2.6"
|
||||
uglify-js "^3.1.4"
|
||||
|
||||
har-validator@~2.0.6:
|
||||
version "2.0.6"
|
||||
@@ -3369,7 +3373,7 @@ minimatch@3.0.3, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3:
|
||||
dependencies:
|
||||
brace-expansion "^1.0.0"
|
||||
|
||||
minimist@0.0.8, minimist@~0.0.1:
|
||||
minimist@0.0.8:
|
||||
version "0.0.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
|
||||
|
||||
@@ -3377,6 +3381,10 @@ minimist@^1.1.1, minimist@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
|
||||
|
||||
minimist@~0.0.1:
|
||||
version "0.0.10"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
|
||||
|
||||
"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
|
||||
version "0.5.1"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
|
||||
@@ -3413,6 +3421,10 @@ negotiator@0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
|
||||
|
||||
neo-async@^2.6.0:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
|
||||
|
||||
no-case@^2.2.0:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.1.tgz#7aeba1c73a52184265554b7dc03baf720df80081"
|
||||
@@ -4650,7 +4662,7 @@ source-map-support@^0.4.2:
|
||||
dependencies:
|
||||
source-map "^0.5.3"
|
||||
|
||||
source-map@0.4.x, source-map@^0.4.4, source-map@~0.4.1:
|
||||
source-map@0.4.x, source-map@~0.4.1:
|
||||
version "0.4.4"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
|
||||
dependencies:
|
||||
@@ -4660,6 +4672,10 @@ source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, sour
|
||||
version "0.5.6"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
|
||||
|
||||
source-map@^0.6.1, source-map@~0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
|
||||
|
||||
source-map@~0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d"
|
||||
@@ -4952,7 +4968,7 @@ ua-parser-js@^0.7.9:
|
||||
version "0.7.12"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.12.tgz#04c81a99bdd5dc52263ea29d24c6bf8d4818a4bb"
|
||||
|
||||
uglify-js@2.7.x, uglify-js@^2.6, uglify-js@~2.7.3:
|
||||
uglify-js@2.7.x, uglify-js@~2.7.3:
|
||||
version "2.7.5"
|
||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.7.5.tgz#4612c0c7baaee2ba7c487de4904ae122079f2ca8"
|
||||
dependencies:
|
||||
@@ -4961,6 +4977,13 @@ uglify-js@2.7.x, uglify-js@^2.6, uglify-js@~2.7.3:
|
||||
uglify-to-browserify "~1.0.0"
|
||||
yargs "~3.10.0"
|
||||
|
||||
uglify-js@^3.1.4:
|
||||
version "3.7.3"
|
||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.7.3.tgz#f918fce9182f466d5140f24bb0ff35c2d32dcc6a"
|
||||
dependencies:
|
||||
commander "~2.20.3"
|
||||
source-map "~0.6.1"
|
||||
|
||||
uglify-to-browserify@~1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
|
||||
|
||||
@@ -95,7 +95,7 @@ trace('initial_render', performance.now(), () => {
|
||||
root.render(
|
||||
createElement(TestApp),
|
||||
wrap(() => {
|
||||
log.app('commited');
|
||||
log.app('committed');
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
22
package.json
22
package.json
@@ -8,6 +8,7 @@
|
||||
"@babel/code-frame": "^7.0.0",
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/helper-module-imports": "^7.0.0",
|
||||
"@babel/parser": "^7.0.0",
|
||||
"@babel/plugin-external-helpers": "^7.0.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.0.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.0.0",
|
||||
@@ -34,9 +35,8 @@
|
||||
"@babel/traverse": "^7.0.0",
|
||||
"@mattiasbuelens/web-streams-polyfill": "0.1.0",
|
||||
"art": "^0.10.1",
|
||||
"babel-eslint": "^10.0.0",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-plugin-syntax-trailing-function-commas": "^6.5.0",
|
||||
"babylon": "6.18.0",
|
||||
"chalk": "^1.1.3",
|
||||
"cli-table": "^0.3.1",
|
||||
"coffee-script": "^1.8.0",
|
||||
@@ -46,8 +46,9 @@
|
||||
"cross-env": "^5.1.1",
|
||||
"danger": "^9.1.8",
|
||||
"error-stack-parser": "^2.0.2",
|
||||
"eslint": "^6.1.0",
|
||||
"eslint": "^6.8.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",
|
||||
@@ -56,7 +57,7 @@
|
||||
"eslint-plugin-react-internal": "link:./scripts/eslint-rules",
|
||||
"fbjs-scripts": "^0.8.3",
|
||||
"filesize": "^3.5.6",
|
||||
"flow-bin": "^0.72.0",
|
||||
"flow-bin": "^0.84.0",
|
||||
"glob": "^6.0.4",
|
||||
"glob-stream": "^6.1.0",
|
||||
"google-closure-compiler": "20190301.0.0",
|
||||
@@ -71,7 +72,7 @@
|
||||
"ncp": "^2.0.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"pacote": "^9.5.6",
|
||||
"prettier": "1.13.7",
|
||||
"prettier": "1.19.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"random-seed": "^0.3.0",
|
||||
"react-lifecycles-compat": "^3.0.2",
|
||||
@@ -87,32 +88,33 @@
|
||||
"targz": "^1.0.1",
|
||||
"through2": "^2.0.0",
|
||||
"tmp": "~0.0.28",
|
||||
"typescript": "~1.8.10",
|
||||
"typescript": "^3.7.4",
|
||||
"webpack": "^4.41.2"
|
||||
},
|
||||
"devEngines": {
|
||||
"node": "8.x || 9.x || 10.x || 11.x || 12.x"
|
||||
"node": "8.x || 9.x || 10.x || 11.x || 12.x || 13.x"
|
||||
},
|
||||
"jest": {
|
||||
"testRegex": "/scripts/jest/dont-run-jest-directly\\.js$"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node ./scripts/rollup/build.js",
|
||||
"build-for-devtools": "cross-env RELEASE_CHANNEL=experimental yarn build -- react/index,react-dom,react-is,react-debug-tools,scheduler,react-test-renderer --type=NODE",
|
||||
"linc": "node ./scripts/tasks/linc.js",
|
||||
"lint": "node ./scripts/tasks/eslint.js",
|
||||
"lint-build": "node ./scripts/rollup/validate/index.js",
|
||||
"extract-errors": "yarn build --type=dev --extract-errors",
|
||||
"postinstall": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json && node ./scripts/flow/createFlowConfigs.js && node ./scripts/yarn/downloadReactIsForPrettyFormat.js",
|
||||
"debug-test": "cross-env NODE_ENV=development node --inspect-brk node_modules/.bin/jest --config ./scripts/jest/config.source.js --runInBand",
|
||||
"debug-test": "cross-env NODE_ENV=development node --inspect-brk node_modules/jest/bin/jest.js --config ./scripts/jest/config.source.js --runInBand",
|
||||
"test": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source.js",
|
||||
"test-persistent": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source-persistent.js",
|
||||
"debug-test-persistent": "cross-env NODE_ENV=development node --inspect-brk node_modules/.bin/jest --config ./scripts/jest/config.source-persistent.js --runInBand",
|
||||
"debug-test-persistent": "cross-env NODE_ENV=development node --inspect-brk node_modules/jest/bin/jest.js --config ./scripts/jest/config.source-persistent.js --runInBand",
|
||||
"test-prod": "cross-env NODE_ENV=production jest --config ./scripts/jest/config.source.js",
|
||||
"test-prod-build": "yarn test-build-prod",
|
||||
"test-build": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.build.js",
|
||||
"test-build-prod": "cross-env NODE_ENV=production jest --config ./scripts/jest/config.build.js",
|
||||
"test-build-devtools": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.build-devtools.js",
|
||||
"debug-test-build-devtools": "cross-env NODE_ENV=development node --inspect-brk node_modules/.bin/jest --config ./scripts/jest/config.build-devtools.js",
|
||||
"debug-test-build-devtools": "cross-env NODE_ENV=development node --inspect-brk node_modules/jest/bin/jest.js --config ./scripts/jest/config.build-devtools.js",
|
||||
"test-dom-fixture": "cd fixtures/dom && yarn && yarn prestart && yarn test",
|
||||
"flow": "node ./scripts/tasks/flow.js",
|
||||
"flow-ci": "node ./scripts/tasks/flow-ci.js",
|
||||
|
||||
@@ -118,7 +118,10 @@ describe('createSubscription', () => {
|
||||
const Subscription = createSubscription({
|
||||
getCurrentValue: source => undefined,
|
||||
subscribe: (source, callback) => {
|
||||
source.then(value => callback(value), value => callback(value));
|
||||
source.then(
|
||||
value => callback(value),
|
||||
value => callback(value),
|
||||
);
|
||||
// (Can't unsubscribe from a Promise)
|
||||
return () => {};
|
||||
},
|
||||
@@ -196,7 +199,10 @@ describe('createSubscription', () => {
|
||||
const Subscription = createSubscription({
|
||||
getCurrentValue: source => undefined,
|
||||
subscribe: (source, callback) => {
|
||||
source.then(value => callback(value), value => callback(value));
|
||||
source.then(
|
||||
value => callback(value),
|
||||
value => callback(value),
|
||||
);
|
||||
// (Can't unsubscribe from a Promise)
|
||||
return () => {};
|
||||
},
|
||||
@@ -452,7 +458,7 @@ describe('createSubscription', () => {
|
||||
},
|
||||
() => null,
|
||||
);
|
||||
}).toWarnDev('Subscription must specify a getCurrentValue function', {
|
||||
}).toErrorDev('Subscription must specify a getCurrentValue function', {
|
||||
withoutStack: true,
|
||||
});
|
||||
});
|
||||
@@ -465,7 +471,7 @@ describe('createSubscription', () => {
|
||||
},
|
||||
() => null,
|
||||
);
|
||||
}).toWarnDev('Subscription must specify a subscribe function', {
|
||||
}).toErrorDev('Subscription must specify a subscribe function', {
|
||||
withoutStack: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
import React from 'react';
|
||||
import invariant from 'shared/invariant';
|
||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
|
||||
type Unsubscribe = () => void;
|
||||
|
||||
@@ -30,31 +29,29 @@ export function createSubscription<Property, Value>(
|
||||
callback: (value: Value | void) => void,
|
||||
) => Unsubscribe,
|
||||
|}>,
|
||||
): React$ComponentType<{
|
||||
): React$ComponentType<{|
|
||||
children: (value: Value | void) => React$Node,
|
||||
source: Property,
|
||||
}> {
|
||||
|}> {
|
||||
const {getCurrentValue, subscribe} = config;
|
||||
|
||||
if (__DEV__) {
|
||||
warningWithoutStack(
|
||||
typeof getCurrentValue === 'function',
|
||||
'Subscription must specify a getCurrentValue function',
|
||||
);
|
||||
warningWithoutStack(
|
||||
typeof subscribe === 'function',
|
||||
'Subscription must specify a subscribe function',
|
||||
);
|
||||
if (typeof getCurrentValue !== 'function') {
|
||||
console.error('Subscription must specify a getCurrentValue function');
|
||||
}
|
||||
if (typeof subscribe !== 'function') {
|
||||
console.error('Subscription must specify a subscribe function');
|
||||
}
|
||||
}
|
||||
|
||||
type Props = {
|
||||
type Props = {|
|
||||
children: (value: Value) => React$Element<any>,
|
||||
source: Property,
|
||||
};
|
||||
type State = {
|
||||
|};
|
||||
type State = {|
|
||||
source: Property,
|
||||
value: Value | void,
|
||||
};
|
||||
|};
|
||||
|
||||
// Reference: https://gist.github.com/bvaughn/d569177d70b50b58bff69c3c4a5353f3
|
||||
class Subscription extends React.Component<Props, State> {
|
||||
|
||||
98
packages/dom-event-testing-library/README.md
Normal file
98
packages/dom-event-testing-library/README.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# `dom-event-testing-library`
|
||||
|
||||
A library for unit testing events via high-level interactions, e.g., `pointerdown`,
|
||||
that produce realistic and complete DOM event sequences.
|
||||
|
||||
There are number of challenges involved in unit testing modules that work with
|
||||
DOM events.
|
||||
|
||||
1. Gesture recognizers may need to support environments with and without support for
|
||||
the `PointerEvent` API.
|
||||
2. Gesture recognizers may need to support various user interaction modes including
|
||||
mouse, touch, and pen use.
|
||||
3. Gesture recognizers must account for the actual event sequences browsers produce
|
||||
(e.g., emulated touch and mouse events.)
|
||||
4. Gesture recognizers must work with "virtual" events produced by tools like
|
||||
screen-readers.
|
||||
|
||||
Writing unit tests to cover all these scenarios is tedious and error prone. This
|
||||
event testing library is designed to solve these issues by allowing developers to
|
||||
more easily dispatch events in unit tests, and to more reliably test pointer
|
||||
interactions using a high-level API based on `PointerEvent`. Here's a basic example:
|
||||
|
||||
```js
|
||||
import {
|
||||
describeWithPointerEvent,
|
||||
testWithPointerType,
|
||||
createEventTarget,
|
||||
setPointerEvent,
|
||||
resetActivePointers
|
||||
} from 'dom-event-testing-library';
|
||||
|
||||
describeWithPointerEvent('useTap', hasPointerEvent => {
|
||||
beforeEach(() => {
|
||||
// basic PointerEvent mock
|
||||
setPointerEvent(hasPointerEvent);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// clear active pointers between test runs
|
||||
resetActivePointers();
|
||||
});
|
||||
|
||||
// test all the pointer types supported by the environment
|
||||
testWithPointerType('pointer down', pointerType => {
|
||||
const ref = createRef(null);
|
||||
const onTapStart = jest.fn();
|
||||
render(() => {
|
||||
useTap(ref, { onTapStart });
|
||||
return <div ref={ref} />
|
||||
});
|
||||
|
||||
// create an event target
|
||||
const target = createEventTarget(ref.current);
|
||||
// dispatch high-level pointer event
|
||||
target.pointerdown({ pointerType });
|
||||
|
||||
expect(onTapStart).toBeCalled();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
This tests the interaction in multiple scenarios. In each case, a realistic DOM
|
||||
event 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 '../index';
|
||||
|
||||
/**
|
||||
* 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,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
65
packages/dom-event-testing-library/constants.js
Normal file
65
packages/dom-event-testing-library/constants.js
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
export const defaultPointerId = 1;
|
||||
export const defaultPointerSize = 23;
|
||||
export const defaultBrowserChromeSize = 50;
|
||||
|
||||
/**
|
||||
* Button property
|
||||
* This property only guarantees to indicate which buttons are pressed during events caused by pressing or
|
||||
* releasing one or multiple buttons. As such, it is not reliable for events such as 'mouseenter', 'mouseleave',
|
||||
* 'mouseover', 'mouseout' or 'mousemove'. Furthermore, the semantics differ for PointerEvent, where the value
|
||||
* for 'pointermove' will always be -1.
|
||||
*/
|
||||
|
||||
export const buttonType = {
|
||||
// no change since last event
|
||||
none: -1,
|
||||
// left-mouse
|
||||
// touch contact
|
||||
// pen contact
|
||||
primary: 0,
|
||||
// right-mouse
|
||||
// pen barrel button
|
||||
secondary: 2,
|
||||
// middle mouse
|
||||
auxiliary: 1,
|
||||
// back mouse
|
||||
back: 3,
|
||||
// forward mouse
|
||||
forward: 4,
|
||||
// pen eraser
|
||||
eraser: 5,
|
||||
};
|
||||
|
||||
/**
|
||||
* Buttons bitmask
|
||||
*/
|
||||
|
||||
export const buttonsType = {
|
||||
none: 0,
|
||||
// left-mouse
|
||||
// touch contact
|
||||
// pen contact
|
||||
primary: 1,
|
||||
// right-mouse
|
||||
// pen barrel button
|
||||
secondary: 2,
|
||||
// middle mouse
|
||||
auxiliary: 4,
|
||||
// back mouse
|
||||
back: 8,
|
||||
// forward mouse
|
||||
forward: 16,
|
||||
// pen eraser
|
||||
eraser: 32,
|
||||
};
|
||||
@@ -22,7 +22,9 @@ export function hasPointerEvent() {
|
||||
export function setPointerEvent(bool) {
|
||||
const pointerCaptureFn = name => id => {
|
||||
if (typeof id !== 'number') {
|
||||
console.error(`A pointerId must be passed to "${name}"`);
|
||||
if (__DEV__) {
|
||||
console.error('A pointerId must be passed to "%s"', name);
|
||||
}
|
||||
}
|
||||
};
|
||||
global.PointerEvent = bool ? emptyFunction : undefined;
|
||||
@@ -63,26 +65,3 @@ export const platform = {
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
361
packages/dom-event-testing-library/domEventSequences.js
Normal file
361
packages/dom-event-testing-library/domEventSequences.js
Normal file
@@ -0,0 +1,361 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import {
|
||||
buttonType,
|
||||
buttonsType,
|
||||
defaultPointerId,
|
||||
defaultPointerSize,
|
||||
defaultBrowserChromeSize,
|
||||
} from './constants';
|
||||
import * as domEvents from './domEvents';
|
||||
import {hasPointerEvent, platform} from './domEnvironment';
|
||||
import * as touchStore from './touchStore';
|
||||
|
||||
/**
|
||||
* Converts a PointerEvent payload to a Touch
|
||||
*/
|
||||
function createTouch(target, payload) {
|
||||
const {
|
||||
height = defaultPointerSize,
|
||||
pageX,
|
||||
pageY,
|
||||
pointerId,
|
||||
pressure = 1,
|
||||
twist = 0,
|
||||
width = defaultPointerSize,
|
||||
x = 0,
|
||||
y = 0,
|
||||
} = payload;
|
||||
|
||||
return {
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
force: pressure,
|
||||
identifier: pointerId,
|
||||
pageX: pageX || x,
|
||||
pageY: pageY || y,
|
||||
radiusX: width / 2,
|
||||
radiusY: height / 2,
|
||||
rotationAngle: twist,
|
||||
target,
|
||||
screenX: x,
|
||||
screenY: y + defaultBrowserChromeSize,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a PointerEvent to a TouchEvent
|
||||
*/
|
||||
function createTouchEventPayload(target, touch, payload) {
|
||||
const {
|
||||
altKey = false,
|
||||
ctrlKey = false,
|
||||
metaKey = false,
|
||||
preventDefault,
|
||||
shiftKey = false,
|
||||
timeStamp,
|
||||
} = payload;
|
||||
|
||||
return {
|
||||
altKey,
|
||||
changedTouches: [touch],
|
||||
ctrlKey,
|
||||
metaKey,
|
||||
preventDefault,
|
||||
shiftKey,
|
||||
targetTouches: touchStore.getTargetTouches(target),
|
||||
timeStamp,
|
||||
touches: touchStore.getTouches(),
|
||||
};
|
||||
}
|
||||
|
||||
function getPointerType(payload) {
|
||||
let pointerType = 'mouse';
|
||||
if (payload != null && payload.pointerType != null) {
|
||||
pointerType = payload.pointerType;
|
||||
}
|
||||
return pointerType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pointer events sequences.
|
||||
*
|
||||
* Creates representative browser event sequences for high-level gestures based on pointers.
|
||||
* This allows unit tests to be written in terms of simple pointer interactions while testing
|
||||
* that the responses to those interactions account for the complex sequence of events that
|
||||
* browsers produce as a result.
|
||||
*
|
||||
* Every time a new pointer touches the surface a 'touchstart' event should be dispatched.
|
||||
* - 'changedTouches' contains the new touch.
|
||||
* - 'targetTouches' contains all the active pointers for the target.
|
||||
* - 'touches' contains all the active pointers on the surface.
|
||||
*
|
||||
* Every time an existing pointer moves a 'touchmove' event should be dispatched.
|
||||
* - 'changedTouches' contains the updated touch.
|
||||
*
|
||||
* Every time an existing pointer leaves the surface a 'touchend' event should be dispatched.
|
||||
* - 'changedTouches' contains the released touch.
|
||||
* - 'targetTouches' contains any of the remaining active pointers for the target.
|
||||
*/
|
||||
|
||||
export function contextmenu(
|
||||
target,
|
||||
defaultPayload,
|
||||
{pointerType = 'mouse', modified} = {},
|
||||
) {
|
||||
const dispatch = arg => target.dispatchEvent(arg);
|
||||
|
||||
const payload = {
|
||||
pointerId: defaultPointerId,
|
||||
pointerType,
|
||||
...defaultPayload,
|
||||
};
|
||||
|
||||
const preventDefault = payload.preventDefault;
|
||||
|
||||
if (pointerType === 'touch') {
|
||||
if (hasPointerEvent()) {
|
||||
dispatch(
|
||||
domEvents.pointerdown({
|
||||
...payload,
|
||||
button: buttonType.primary,
|
||||
buttons: buttonsType.primary,
|
||||
}),
|
||||
);
|
||||
}
|
||||
const touch = createTouch(target, payload);
|
||||
touchStore.addTouch(touch);
|
||||
const touchEventPayload = createTouchEventPayload(target, touch, payload);
|
||||
dispatch(domEvents.touchstart(touchEventPayload));
|
||||
dispatch(
|
||||
domEvents.contextmenu({
|
||||
button: buttonType.primary,
|
||||
buttons: buttonsType.none,
|
||||
preventDefault,
|
||||
}),
|
||||
);
|
||||
touchStore.removeTouch(touch);
|
||||
} else if (pointerType === 'mouse') {
|
||||
if (modified === true) {
|
||||
const button = buttonType.primary;
|
||||
const buttons = buttonsType.primary;
|
||||
const ctrlKey = true;
|
||||
if (hasPointerEvent()) {
|
||||
dispatch(
|
||||
domEvents.pointerdown({button, buttons, ctrlKey, pointerType}),
|
||||
);
|
||||
}
|
||||
dispatch(domEvents.mousedown({button, buttons, ctrlKey}));
|
||||
if (platform.get() === 'mac') {
|
||||
dispatch(
|
||||
domEvents.contextmenu({button, buttons, ctrlKey, preventDefault}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const button = buttonType.secondary;
|
||||
const buttons = buttonsType.secondary;
|
||||
if (hasPointerEvent()) {
|
||||
dispatch(domEvents.pointerdown({button, buttons, pointerType}));
|
||||
}
|
||||
dispatch(domEvents.mousedown({button, buttons}));
|
||||
dispatch(domEvents.contextmenu({button, buttons, preventDefault}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function pointercancel(target, defaultPayload) {
|
||||
const dispatchEvent = arg => target.dispatchEvent(arg);
|
||||
const pointerType = getPointerType(defaultPayload);
|
||||
|
||||
const payload = {
|
||||
pointerId: defaultPointerId,
|
||||
pointerType,
|
||||
...defaultPayload,
|
||||
};
|
||||
|
||||
if (hasPointerEvent()) {
|
||||
dispatchEvent(domEvents.pointercancel(payload));
|
||||
} else {
|
||||
if (pointerType === 'mouse') {
|
||||
dispatchEvent(domEvents.dragstart(payload));
|
||||
} else {
|
||||
const touch = createTouch(target, payload);
|
||||
touchStore.removeTouch(touch);
|
||||
const touchEventPayload = createTouchEventPayload(target, touch, payload);
|
||||
dispatchEvent(domEvents.touchcancel(touchEventPayload));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function pointerdown(target, defaultPayload) {
|
||||
const dispatch = arg => target.dispatchEvent(arg);
|
||||
const pointerType = getPointerType(defaultPayload);
|
||||
|
||||
const payload = {
|
||||
button: buttonType.primary,
|
||||
buttons: buttonsType.primary,
|
||||
pointerId: defaultPointerId,
|
||||
pointerType,
|
||||
...defaultPayload,
|
||||
};
|
||||
|
||||
if (pointerType === 'mouse') {
|
||||
if (hasPointerEvent()) {
|
||||
dispatch(domEvents.pointerover(payload));
|
||||
dispatch(domEvents.pointerenter(payload));
|
||||
}
|
||||
dispatch(domEvents.mouseover(payload));
|
||||
dispatch(domEvents.mouseenter(payload));
|
||||
if (hasPointerEvent()) {
|
||||
dispatch(domEvents.pointerdown(payload));
|
||||
}
|
||||
dispatch(domEvents.mousedown(payload));
|
||||
if (document.activeElement !== target) {
|
||||
dispatch(domEvents.focus());
|
||||
}
|
||||
} else {
|
||||
if (hasPointerEvent()) {
|
||||
dispatch(domEvents.pointerover(payload));
|
||||
dispatch(domEvents.pointerenter(payload));
|
||||
dispatch(domEvents.pointerdown(payload));
|
||||
}
|
||||
const touch = createTouch(target, payload);
|
||||
touchStore.addTouch(touch);
|
||||
const touchEventPayload = createTouchEventPayload(target, touch, payload);
|
||||
dispatch(domEvents.touchstart(touchEventPayload));
|
||||
if (hasPointerEvent()) {
|
||||
dispatch(domEvents.gotpointercapture(payload));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function pointerenter(target, defaultPayload) {
|
||||
const dispatch = arg => target.dispatchEvent(arg);
|
||||
|
||||
const payload = {
|
||||
pointerId: defaultPointerId,
|
||||
...defaultPayload,
|
||||
};
|
||||
|
||||
if (hasPointerEvent()) {
|
||||
dispatch(domEvents.pointerover(payload));
|
||||
dispatch(domEvents.pointerenter(payload));
|
||||
}
|
||||
dispatch(domEvents.mouseover(payload));
|
||||
dispatch(domEvents.mouseenter(payload));
|
||||
}
|
||||
|
||||
export function pointerexit(target, defaultPayload) {
|
||||
const dispatch = arg => target.dispatchEvent(arg);
|
||||
|
||||
const payload = {
|
||||
pointerId: defaultPointerId,
|
||||
...defaultPayload,
|
||||
};
|
||||
|
||||
if (hasPointerEvent()) {
|
||||
dispatch(domEvents.pointerout(payload));
|
||||
dispatch(domEvents.pointerleave(payload));
|
||||
}
|
||||
dispatch(domEvents.mouseout(payload));
|
||||
dispatch(domEvents.mouseleave(payload));
|
||||
}
|
||||
|
||||
export function pointerhover(target, defaultPayload) {
|
||||
const dispatch = arg => target.dispatchEvent(arg);
|
||||
|
||||
const payload = {
|
||||
pointerId: defaultPointerId,
|
||||
...defaultPayload,
|
||||
};
|
||||
|
||||
if (hasPointerEvent()) {
|
||||
dispatch(domEvents.pointermove(payload));
|
||||
}
|
||||
dispatch(domEvents.mousemove(payload));
|
||||
}
|
||||
|
||||
export function pointermove(target, defaultPayload) {
|
||||
const dispatch = arg => target.dispatchEvent(arg);
|
||||
const pointerType = getPointerType(defaultPayload);
|
||||
|
||||
const payload = {
|
||||
pointerId: defaultPointerId,
|
||||
pointerType,
|
||||
...defaultPayload,
|
||||
};
|
||||
|
||||
if (hasPointerEvent()) {
|
||||
dispatch(
|
||||
domEvents.pointermove({
|
||||
pressure: pointerType === 'touch' ? 1 : 0.5,
|
||||
...payload,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
if (pointerType === 'mouse') {
|
||||
dispatch(domEvents.mousemove(payload));
|
||||
} else {
|
||||
const touch = createTouch(target, payload);
|
||||
touchStore.updateTouch(touch);
|
||||
const touchEventPayload = createTouchEventPayload(target, touch, payload);
|
||||
dispatch(domEvents.touchmove(touchEventPayload));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function pointerup(target, defaultPayload) {
|
||||
const dispatch = arg => target.dispatchEvent(arg);
|
||||
const pointerType = getPointerType(defaultPayload);
|
||||
|
||||
const payload = {
|
||||
pointerId: defaultPointerId,
|
||||
pointerType,
|
||||
...defaultPayload,
|
||||
};
|
||||
|
||||
if (pointerType === 'mouse') {
|
||||
if (hasPointerEvent()) {
|
||||
dispatch(domEvents.pointerup(payload));
|
||||
}
|
||||
dispatch(domEvents.mouseup(payload));
|
||||
dispatch(domEvents.click(payload));
|
||||
} else {
|
||||
if (hasPointerEvent()) {
|
||||
dispatch(domEvents.pointerup(payload));
|
||||
dispatch(domEvents.lostpointercapture(payload));
|
||||
dispatch(domEvents.pointerout(payload));
|
||||
dispatch(domEvents.pointerleave(payload));
|
||||
}
|
||||
const touch = createTouch(target, payload);
|
||||
touchStore.removeTouch(touch);
|
||||
const touchEventPayload = createTouchEventPayload(target, touch, payload);
|
||||
dispatch(domEvents.touchend(touchEventPayload));
|
||||
dispatch(domEvents.mouseover(payload));
|
||||
dispatch(domEvents.mousemove(payload));
|
||||
dispatch(domEvents.mousedown(payload));
|
||||
if (document.activeElement !== target) {
|
||||
dispatch(domEvents.focus());
|
||||
}
|
||||
dispatch(domEvents.mouseup(payload));
|
||||
dispatch(domEvents.click(payload));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function should be called after each test to ensure the touchStore is cleared
|
||||
* in cases where the mock pointers weren't released before the test completed
|
||||
* (e.g., a test failed or ran a partial gesture).
|
||||
*/
|
||||
export function resetActivePointers() {
|
||||
touchStore.clear();
|
||||
}
|
||||
@@ -9,7 +9,12 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import {buttonsType} from './domEnvironment';
|
||||
import {
|
||||
buttonType,
|
||||
buttonsType,
|
||||
defaultPointerSize,
|
||||
defaultBrowserChromeSize,
|
||||
} from './constants';
|
||||
|
||||
/**
|
||||
* Native event object mocks for higher-level events.
|
||||
@@ -26,9 +31,6 @@ import {buttonsType} from './domEnvironment';
|
||||
* 3. PointerEvent and TouchEvent fields are normalized (e.g., 'rotationAngle' -> 'twist')
|
||||
*/
|
||||
|
||||
const defaultPointerSize = 23;
|
||||
const defaultBrowserChromeSize = 50;
|
||||
|
||||
function emptyFunction() {}
|
||||
|
||||
function createEvent(type, data = {}) {
|
||||
@@ -65,6 +67,7 @@ function createPointerEvent(
|
||||
type,
|
||||
{
|
||||
altKey = false,
|
||||
button = buttonType.none,
|
||||
buttons = buttonsType.none,
|
||||
ctrlKey = false,
|
||||
detail = 1,
|
||||
@@ -76,7 +79,7 @@ function createPointerEvent(
|
||||
offsetY = 0,
|
||||
pageX,
|
||||
pageY,
|
||||
pointerId = 1,
|
||||
pointerId,
|
||||
pressure = 0,
|
||||
preventDefault = emptyFunction,
|
||||
pointerType = 'mouse',
|
||||
@@ -98,6 +101,7 @@ function createPointerEvent(
|
||||
|
||||
return createEvent(type, {
|
||||
altKey,
|
||||
button,
|
||||
buttons,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
@@ -164,6 +168,7 @@ function createMouseEvent(
|
||||
type,
|
||||
{
|
||||
altKey = false,
|
||||
button = buttonType.none,
|
||||
buttons = buttonsType.none,
|
||||
ctrlKey = false,
|
||||
detail = 1,
|
||||
@@ -187,6 +192,7 @@ function createMouseEvent(
|
||||
|
||||
return createEvent(type, {
|
||||
altKey,
|
||||
button,
|
||||
buttons,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
@@ -211,80 +217,12 @@ function createMouseEvent(
|
||||
}
|
||||
|
||||
function createTouchEvent(type, payload) {
|
||||
const touchesPayload = Array.isArray(payload) ? payload : [payload];
|
||||
const firstTouch = touchesPayload[0];
|
||||
let altKey = false;
|
||||
let ctrlKey = false;
|
||||
let metaKey = false;
|
||||
let preventDefault = emptyFunction;
|
||||
let shiftKey = false;
|
||||
let timeStamp;
|
||||
|
||||
if (firstTouch != null) {
|
||||
if (firstTouch.altKey != null) {
|
||||
altKey = firstTouch.altKey;
|
||||
}
|
||||
if (firstTouch.ctrlKey != null) {
|
||||
ctrlKey = firstTouch.ctrlKey;
|
||||
}
|
||||
if (firstTouch.metaKey != null) {
|
||||
metaKey = firstTouch.metaKey;
|
||||
}
|
||||
if (firstTouch.preventDefault != null) {
|
||||
preventDefault = firstTouch.preventDefault;
|
||||
}
|
||||
if (firstTouch.shiftKey != null) {
|
||||
shiftKey = firstTouch.shiftKey;
|
||||
}
|
||||
if (firstTouch.timeStamp != null) {
|
||||
timeStamp = firstTouch.timeStamp;
|
||||
}
|
||||
}
|
||||
|
||||
const touches = touchesPayload.map(
|
||||
({
|
||||
height = defaultPointerSize,
|
||||
pageX,
|
||||
pageY,
|
||||
pointerId = 1,
|
||||
pressure = 1,
|
||||
twist = 0,
|
||||
width = defaultPointerSize,
|
||||
x = 0,
|
||||
y = 0,
|
||||
} = {}) => {
|
||||
return {
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
force: pressure,
|
||||
identifier: pointerId,
|
||||
pageX: pageX || x,
|
||||
pageY: pageY || y,
|
||||
radiusX: width / 2,
|
||||
radiusY: height / 2,
|
||||
rotationAngle: twist,
|
||||
screenX: x,
|
||||
screenY: y + defaultBrowserChromeSize,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const activeTouches = type !== 'touchend' ? touches : null;
|
||||
|
||||
return createEvent(type, {
|
||||
altKey,
|
||||
changedTouches: touches,
|
||||
ctrlKey,
|
||||
...payload,
|
||||
detail: 0,
|
||||
metaKey,
|
||||
preventDefault,
|
||||
shiftKey,
|
||||
sourceCapabilities: {
|
||||
firesTouchEvents: true,
|
||||
},
|
||||
targetTouches: activeTouches,
|
||||
timeStamp,
|
||||
touches: activeTouches,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -293,11 +231,14 @@ function createTouchEvent(type, payload) {
|
||||
*/
|
||||
|
||||
export function blur({relatedTarget} = {}) {
|
||||
return createEvent('blur', {relatedTarget});
|
||||
return new FocusEvent('blur', {relatedTarget});
|
||||
}
|
||||
|
||||
export function click(payload) {
|
||||
return createMouseEvent('click', payload);
|
||||
return createMouseEvent('click', {
|
||||
button: buttonType.primary,
|
||||
...payload,
|
||||
});
|
||||
}
|
||||
|
||||
export function contextmenu(payload) {
|
||||
@@ -315,7 +256,7 @@ export function dragstart(payload) {
|
||||
}
|
||||
|
||||
export function focus({relatedTarget} = {}) {
|
||||
return createEvent('focus', {relatedTarget});
|
||||
return new FocusEvent('focus', {relatedTarget});
|
||||
}
|
||||
|
||||
export function scroll() {
|
||||
@@ -324,6 +265,7 @@ export function scroll() {
|
||||
|
||||
export function virtualclick(payload) {
|
||||
return createMouseEvent('click', {
|
||||
button: 0,
|
||||
...payload,
|
||||
buttons: 0,
|
||||
detail: 0,
|
||||
@@ -383,6 +325,7 @@ export function pointercancel(payload) {
|
||||
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,
|
||||
@@ -398,7 +341,10 @@ export function pointerleave(payload) {
|
||||
}
|
||||
|
||||
export function pointermove(payload) {
|
||||
return createPointerEvent('pointermove', payload);
|
||||
return createPointerEvent('pointermove', {
|
||||
...payload,
|
||||
button: buttonType.none,
|
||||
});
|
||||
}
|
||||
|
||||
export function pointerout(payload) {
|
||||
@@ -411,6 +357,7 @@ export function pointerover(payload) {
|
||||
|
||||
export function pointerup(payload) {
|
||||
return createPointerEvent('pointerup', {
|
||||
button: buttonType.primary,
|
||||
...payload,
|
||||
buttons: buttonsType.none,
|
||||
pressure: 0,
|
||||
@@ -422,13 +369,18 @@ export function pointerup(payload) {
|
||||
*/
|
||||
|
||||
export function mousedown(payload) {
|
||||
// The value of 'buttons' for 'mousedown' must not be 0
|
||||
// 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 === 0
|
||||
payload == null || payload.buttons === buttonsType.none
|
||||
? buttonsType.primary
|
||||
: payload.buttons;
|
||||
return createMouseEvent('mousedown', {
|
||||
...payload,
|
||||
button,
|
||||
buttons,
|
||||
});
|
||||
}
|
||||
@@ -455,6 +407,7 @@ export function mouseover(payload) {
|
||||
|
||||
export function mouseup(payload) {
|
||||
return createMouseEvent('mouseup', {
|
||||
button: buttonType.primary,
|
||||
...payload,
|
||||
buttons: buttonsType.none,
|
||||
});
|
||||
@@ -9,19 +9,16 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import {buttonType, buttonsType} from './constants';
|
||||
import * as domEvents from './domEvents';
|
||||
import * as domEventSequences from './domEventSequences';
|
||||
import {
|
||||
buttonsType,
|
||||
hasPointerEvent,
|
||||
setPointerEvent,
|
||||
platform,
|
||||
} from './domEnvironment';
|
||||
import {hasPointerEvent, setPointerEvent, platform} from './domEnvironment';
|
||||
import {describeWithPointerEvent, testWithPointerType} from './testHelpers';
|
||||
|
||||
const createEventTarget = node => ({
|
||||
node,
|
||||
/**
|
||||
* General events abstraction.
|
||||
* Simple events abstraction.
|
||||
*/
|
||||
blur(payload) {
|
||||
node.dispatchEvent(domEvents.blur(payload));
|
||||
@@ -29,59 +26,30 @@ const createEventTarget = node => ({
|
||||
click(payload) {
|
||||
node.dispatchEvent(domEvents.click(payload));
|
||||
},
|
||||
contextmenu(payload, options) {
|
||||
domEventSequences.contextmenu(node, payload, options);
|
||||
},
|
||||
focus(payload) {
|
||||
node.dispatchEvent(domEvents.focus(payload));
|
||||
node.focus();
|
||||
},
|
||||
scroll(payload) {
|
||||
node.dispatchEvent(domEvents.scroll(payload));
|
||||
},
|
||||
/**
|
||||
* KeyboardEvent abstraction.
|
||||
*/
|
||||
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));
|
||||
},
|
||||
tabNext() {
|
||||
node.dispatchEvent(
|
||||
domEvents.keydown({
|
||||
key: 'Tab',
|
||||
}),
|
||||
);
|
||||
node.dispatchEvent(
|
||||
domEvents.keyup({
|
||||
key: 'Tab',
|
||||
}),
|
||||
);
|
||||
},
|
||||
tabPrevious() {
|
||||
node.dispatchEvent(
|
||||
domEvents.keydown({
|
||||
key: 'Tab',
|
||||
shiftKey: true,
|
||||
}),
|
||||
);
|
||||
node.dispatchEvent(
|
||||
domEvents.keyup({
|
||||
key: 'Tab',
|
||||
shiftKey: true,
|
||||
}),
|
||||
);
|
||||
},
|
||||
/**
|
||||
* 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);
|
||||
@@ -136,35 +104,16 @@ const createEventTarget = node => ({
|
||||
},
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
function testWithPointerType(message, testFn) {
|
||||
const table = hasPointerEvent()
|
||||
? ['mouse', 'touch', 'pen']
|
||||
: ['mouse', 'touch'];
|
||||
test.each(table)(`${message}: %s`, pointerType => {
|
||||
testFn(pointerType);
|
||||
});
|
||||
}
|
||||
const resetActivePointers = domEventSequences.resetActivePointers;
|
||||
|
||||
export {
|
||||
buttonType,
|
||||
buttonsType,
|
||||
createEventTarget,
|
||||
describeWithPointerEvent,
|
||||
platform,
|
||||
hasPointerEvent,
|
||||
resetActivePointers,
|
||||
setPointerEvent,
|
||||
testWithPointerType,
|
||||
};
|
||||
5
packages/dom-event-testing-library/package.json
Normal file
5
packages/dom-event-testing-library/package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "dom-event-testing-library",
|
||||
"version": "0.0.0"
|
||||
}
|
||||
35
packages/dom-event-testing-library/testHelpers.js
Normal file
35
packages/dom-event-testing-library/testHelpers.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import {hasPointerEvent, setPointerEvent} from './domEnvironment';
|
||||
|
||||
export function describeWithPointerEvent(message, describeFn) {
|
||||
const pointerEvent = 'PointerEvent';
|
||||
const fallback = 'MouseEvent/TouchEvent';
|
||||
describe.each`
|
||||
value | name
|
||||
${true} | ${pointerEvent}
|
||||
${false} | ${fallback}
|
||||
`(`${message}: $name`, entry => {
|
||||
const hasPointerEvents = entry.value;
|
||||
setPointerEvent(hasPointerEvents);
|
||||
describeFn(hasPointerEvents);
|
||||
});
|
||||
}
|
||||
|
||||
export function testWithPointerType(message, testFn) {
|
||||
const table = hasPointerEvent()
|
||||
? ['mouse', 'touch', 'pen']
|
||||
: ['mouse', 'touch'];
|
||||
test.each(table)(`${message}: %s`, pointerType => {
|
||||
testFn(pointerType);
|
||||
});
|
||||
}
|
||||
82
packages/dom-event-testing-library/touchStore.js
Normal file
82
packages/dom-event-testing-library/touchStore.js
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Touch events state machine.
|
||||
*
|
||||
* Keeps track of the active pointers and allows them to be reflected in touch events.
|
||||
*/
|
||||
|
||||
const activeTouches = new Map();
|
||||
|
||||
export function addTouch(touch) {
|
||||
const identifier = touch.identifier;
|
||||
const target = touch.target;
|
||||
if (!activeTouches.has(target)) {
|
||||
activeTouches.set(target, new Map());
|
||||
}
|
||||
if (activeTouches.get(target).get(identifier)) {
|
||||
// Do not allow existing touches to be overwritten
|
||||
console.error(
|
||||
'Touch with identifier %s already exists. Did not record touch start.',
|
||||
identifier,
|
||||
);
|
||||
} else {
|
||||
activeTouches.get(target).set(identifier, touch);
|
||||
}
|
||||
}
|
||||
|
||||
export function updateTouch(touch) {
|
||||
const identifier = touch.identifier;
|
||||
const target = touch.target;
|
||||
if (activeTouches.get(target) != null) {
|
||||
activeTouches.get(target).set(identifier, touch);
|
||||
} else {
|
||||
console.error(
|
||||
'Touch with identifier %s does not exist. Cannot record touch move without a touch start.',
|
||||
identifier,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function removeTouch(touch) {
|
||||
const identifier = touch.identifier;
|
||||
const target = touch.target;
|
||||
if (activeTouches.get(target) != null) {
|
||||
if (activeTouches.get(target).has(identifier)) {
|
||||
activeTouches.get(target).delete(identifier);
|
||||
} else {
|
||||
console.error(
|
||||
'Touch with identifier %s does not exist. Cannot record touch end without a touch start.',
|
||||
identifier,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getTouches() {
|
||||
const touches = [];
|
||||
activeTouches.forEach((_, target) => {
|
||||
touches.push(...getTargetTouches(target));
|
||||
});
|
||||
return touches;
|
||||
}
|
||||
|
||||
export function getTargetTouches(target) {
|
||||
if (activeTouches.get(target) != null) {
|
||||
return Array.from(activeTouches.get(target).values());
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function clear() {
|
||||
activeTouches.clear();
|
||||
}
|
||||
@@ -6,7 +6,7 @@ It is a part of the [Hooks API](https://reactjs.org/docs/hooks-intro.html) for R
|
||||
|
||||
## Installation
|
||||
|
||||
**Note: If you're using Create React App, please wait for a corresponding release of `react-scripts` that includes this rule instead of adding it directly.**
|
||||
**Note: If you're using Create React App, please use `react-scripts` >= 3 instead of adding it directly.**
|
||||
|
||||
Assuming you already have ESLint installed, run:
|
||||
|
||||
|
||||
@@ -3154,6 +3154,37 @@ const tests = {
|
||||
`and use that variable in the cleanup function.`,
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
function MyComponent() {
|
||||
const myRef = useRef();
|
||||
useLayoutEffect_SAFE_FOR_SSR(() => {
|
||||
const handleMove = () => {};
|
||||
myRef.current.addEventListener('mousemove', handleMove);
|
||||
return () => myRef.current.removeEventListener('mousemove', handleMove);
|
||||
});
|
||||
return <div ref={myRef} />;
|
||||
}
|
||||
`,
|
||||
output: `
|
||||
function MyComponent() {
|
||||
const myRef = useRef();
|
||||
useLayoutEffect_SAFE_FOR_SSR(() => {
|
||||
const handleMove = () => {};
|
||||
myRef.current.addEventListener('mousemove', handleMove);
|
||||
return () => myRef.current.removeEventListener('mousemove', handleMove);
|
||||
});
|
||||
return <div ref={myRef} />;
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
`The ref value 'myRef.current' will likely have changed by the time ` +
|
||||
`this effect cleanup function runs. If this ref points to a node ` +
|
||||
`rendered by React, copy 'myRef.current' to a variable inside the effect, ` +
|
||||
`and use that variable in the cleanup function.`,
|
||||
],
|
||||
options: [{additionalHooks: 'useLayoutEffect_SAFE_FOR_SSR'}],
|
||||
},
|
||||
{
|
||||
// Autofix ignores constant primitives (leaving the ones that are there).
|
||||
code: `
|
||||
|
||||
@@ -81,7 +81,7 @@ export default {
|
||||
// Get the reactive hook node.
|
||||
const reactiveHook = node.parent.callee;
|
||||
const reactiveHookName = getNodeWithoutReactNamespace(reactiveHook).name;
|
||||
const isEffect = reactiveHookName.endsWith('Effect');
|
||||
const isEffect = /Effect($|[^a-z])/g.test(reactiveHookName);
|
||||
|
||||
// Get the declared dependencies for this reactive hook. If there is no
|
||||
// second argument then the reactive callback will re-run on every render.
|
||||
@@ -613,21 +613,15 @@ export default {
|
||||
context.report({
|
||||
node: declaredDependencyNode,
|
||||
message:
|
||||
`The ${
|
||||
declaredDependencyNode.raw
|
||||
} literal is not a valid dependency ` +
|
||||
`The ${declaredDependencyNode.raw} literal is not a valid dependency ` +
|
||||
`because it never changes. ` +
|
||||
`Did you mean to include ${
|
||||
declaredDependencyNode.value
|
||||
} in the array instead?`,
|
||||
`Did you mean to include ${declaredDependencyNode.value} in the array instead?`,
|
||||
});
|
||||
} else {
|
||||
context.report({
|
||||
node: declaredDependencyNode,
|
||||
message:
|
||||
`The ${
|
||||
declaredDependencyNode.raw
|
||||
} literal is not a valid dependency ` +
|
||||
`The ${declaredDependencyNode.raw} literal is not a valid dependency ` +
|
||||
'because it never changes. You can safely remove it.',
|
||||
});
|
||||
}
|
||||
@@ -697,22 +691,16 @@ export default {
|
||||
bareFunctions.forEach(({fn, suggestUseCallback}) => {
|
||||
let message =
|
||||
`The '${fn.name.name}' function makes the dependencies of ` +
|
||||
`${reactiveHookName} Hook (at line ${
|
||||
declaredDependenciesNode.loc.start.line
|
||||
}) ` +
|
||||
`${reactiveHookName} Hook (at line ${declaredDependenciesNode.loc.start.line}) ` +
|
||||
`change on every render.`;
|
||||
if (suggestUseCallback) {
|
||||
message +=
|
||||
` To fix this, ` +
|
||||
`wrap the '${
|
||||
fn.name.name
|
||||
}' definition into its own useCallback() Hook.`;
|
||||
`wrap the '${fn.name.name}' definition into its own useCallback() Hook.`;
|
||||
} else {
|
||||
message +=
|
||||
` Move it inside the ${reactiveHookName} callback. ` +
|
||||
`Alternatively, wrap the '${
|
||||
fn.name.name
|
||||
}' definition into its own useCallback() Hook.`;
|
||||
`Alternatively, wrap the '${fn.name.name}' definition into its own useCallback() Hook.`;
|
||||
}
|
||||
// TODO: What if the function needs to change on every render anyway?
|
||||
// Should we suggest removing effect deps as an appropriate fix too?
|
||||
|
||||
@@ -16,7 +16,7 @@ import type {
|
||||
|
||||
import invariant from 'shared/invariant';
|
||||
|
||||
type NamesToPlugins = {[key: PluginName]: PluginModule<AnyNativeEvent>};
|
||||
type NamesToPlugins = {[key: PluginName]: PluginModule<AnyNativeEvent>, ...};
|
||||
type EventPluginOrder = null | Array<PluginName>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
import {invokeGuardedCallbackAndCatchFirstError} from 'shared/ReactErrorUtils';
|
||||
import invariant from 'shared/invariant';
|
||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
|
||||
export let getFiberCurrentPropsFromNode = null;
|
||||
export let getInstanceFromNode = null;
|
||||
@@ -22,11 +21,12 @@ export function setComponentTree(
|
||||
getInstanceFromNode = getInstanceFromNodeImpl;
|
||||
getNodeFromInstance = getNodeFromInstanceImpl;
|
||||
if (__DEV__) {
|
||||
warningWithoutStack(
|
||||
getNodeFromInstance && getInstanceFromNode,
|
||||
'EventPluginUtils.setComponentTree(...): Injected ' +
|
||||
'module is missing getNodeFromInstance or getInstanceFromNode.',
|
||||
);
|
||||
if (!getNodeFromInstance || !getInstanceFromNode) {
|
||||
console.error(
|
||||
'EventPluginUtils.setComponentTree(...): Injected ' +
|
||||
'module is missing getNodeFromInstance or getInstanceFromNode.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,20 +40,19 @@ if (__DEV__) {
|
||||
const listenersLen = listenersIsArr
|
||||
? dispatchListeners.length
|
||||
: dispatchListeners
|
||||
? 1
|
||||
: 0;
|
||||
? 1
|
||||
: 0;
|
||||
|
||||
const instancesIsArr = Array.isArray(dispatchInstances);
|
||||
const instancesLen = instancesIsArr
|
||||
? dispatchInstances.length
|
||||
: dispatchInstances
|
||||
? 1
|
||||
: 0;
|
||||
? 1
|
||||
: 0;
|
||||
|
||||
warningWithoutStack(
|
||||
instancesIsArr === listenersIsArr && instancesLen === listenersLen,
|
||||
'EventPluginUtils: Invalid `event`.',
|
||||
);
|
||||
if (instancesIsArr !== listenersIsArr || instancesLen !== listenersLen) {
|
||||
console.error('EventPluginUtils: Invalid `event`.');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
traverseTwoPhase,
|
||||
traverseEnterLeave,
|
||||
} from 'shared/ReactTreeTraversal';
|
||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
|
||||
import {getListener} from './EventPluginHub';
|
||||
import accumulateInto from './accumulateInto';
|
||||
@@ -46,7 +45,9 @@ function listenerAtPhase(inst, event, propagationPhase: PropagationPhases) {
|
||||
*/
|
||||
function accumulateDirectionalDispatches(inst, phase, event) {
|
||||
if (__DEV__) {
|
||||
warningWithoutStack(inst, 'Dispatching inst must not be null');
|
||||
if (!inst) {
|
||||
console.error('Dispatching inst must not be null');
|
||||
}
|
||||
}
|
||||
const listener = listenerAtPhase(inst, event, phase);
|
||||
if (listener) {
|
||||
|
||||
@@ -15,3 +15,4 @@ export const IS_PASSIVE = 1 << 2;
|
||||
export const IS_ACTIVE = 1 << 3;
|
||||
export const PASSIVE_NOT_SUPPORTED = 1 << 4;
|
||||
export const IS_REPLAYED = 1 << 5;
|
||||
export const IS_FIRST_ANCESTOR = 1 << 6;
|
||||
|
||||
@@ -15,7 +15,7 @@ import type {
|
||||
import type {TopLevelType} from './TopLevelEventTypes';
|
||||
import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
|
||||
|
||||
export type EventTypes = {[key: string]: DispatchConfig};
|
||||
export type EventTypes = {[key: string]: DispatchConfig, ...};
|
||||
|
||||
export type AnyNativeEvent = Event | KeyboardEvent | MouseEvent | Touch;
|
||||
|
||||
@@ -31,4 +31,5 @@ export type PluginModule<NativeEvent> = {
|
||||
eventSystemFlags: EventSystemFlags,
|
||||
) => ?ReactSyntheticEvent,
|
||||
tapMoveThreshold?: number,
|
||||
...
|
||||
};
|
||||
|
||||
@@ -33,8 +33,12 @@ function restoreStateOfTarget(target) {
|
||||
'setRestoreImplementation() needs to be called to handle a target for controlled ' +
|
||||
'events. This error is likely caused by a bug in React. Please file an issue.',
|
||||
);
|
||||
const props = getFiberCurrentPropsFromNode(internalInstance.stateNode);
|
||||
restoreImpl(internalInstance.stateNode, internalInstance.type, props);
|
||||
const stateNode = internalInstance.stateNode;
|
||||
// Guard against Fiber being unmounted.
|
||||
if (stateNode) {
|
||||
const props = getFiberCurrentPropsFromNode(stateNode);
|
||||
restoreImpl(internalInstance.stateNode, internalInstance.type, props);
|
||||
}
|
||||
}
|
||||
|
||||
export function setRestoreImplementation(
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
restoreStateIfNeeded,
|
||||
} from './ReactControlledComponent';
|
||||
|
||||
import {enableFlareAPI} from 'shared/ReactFeatureFlags';
|
||||
import {enableDeprecatedFlareAPI} from 'shared/ReactFeatureFlags';
|
||||
import {invokeGuardedCallbackAndCatchFirstError} from 'shared/ReactErrorUtils';
|
||||
|
||||
// Used as a way to call batchedUpdates when we don't have a reference to
|
||||
@@ -118,8 +118,9 @@ export function flushDiscreteUpdatesIfNeeded(timeStamp: number) {
|
||||
// behaviour as we had before this change, so the risks are low.
|
||||
if (
|
||||
!isInsideEventHandler &&
|
||||
(!enableFlareAPI ||
|
||||
(timeStamp === 0 || lastFlushedEventTimeStamp !== timeStamp))
|
||||
(!enableDeprecatedFlareAPI ||
|
||||
timeStamp === 0 ||
|
||||
lastFlushedEventTimeStamp !== timeStamp)
|
||||
) {
|
||||
lastFlushedEventTimeStamp = timeStamp;
|
||||
flushDiscreteUpdatesImpl();
|
||||
|
||||
@@ -12,17 +12,17 @@ import type {Fiber} from 'react-reconciler/src/ReactFiber';
|
||||
import type {EventPriority} from 'shared/ReactTypes';
|
||||
import type {TopLevelType} from './TopLevelEventTypes';
|
||||
|
||||
export type DispatchConfig = {
|
||||
export type DispatchConfig = {|
|
||||
dependencies: Array<TopLevelType>,
|
||||
phasedRegistrationNames?: {
|
||||
phasedRegistrationNames?: {|
|
||||
bubbled: string,
|
||||
captured: string,
|
||||
},
|
||||
|},
|
||||
registrationName?: string,
|
||||
eventPriority: EventPriority,
|
||||
};
|
||||
|};
|
||||
|
||||
export type ReactSyntheticEvent = {
|
||||
export type ReactSyntheticEvent = {|
|
||||
dispatchConfig: DispatchConfig,
|
||||
getPooled: (
|
||||
dispatchConfig: DispatchConfig,
|
||||
@@ -31,4 +31,4 @@ export type ReactSyntheticEvent = {
|
||||
nativeEventTarget: EventTarget,
|
||||
) => ReactSyntheticEvent,
|
||||
isPersistent: () => boolean,
|
||||
} & SyntheticEvent<>;
|
||||
|} & SyntheticEvent<>;
|
||||
|
||||
@@ -350,10 +350,10 @@ function setResponderAndExtractTransfer(
|
||||
const shouldSetEventType = isStartish(topLevelType)
|
||||
? eventTypes.startShouldSetResponder
|
||||
: isMoveish(topLevelType)
|
||||
? eventTypes.moveShouldSetResponder
|
||||
: topLevelType === TOP_SELECTION_CHANGE
|
||||
? eventTypes.selectionChangeShouldSetResponder
|
||||
: eventTypes.scrollShouldSetResponder;
|
||||
? eventTypes.moveShouldSetResponder
|
||||
: topLevelType === TOP_SELECTION_CHANGE
|
||||
? eventTypes.selectionChangeShouldSetResponder
|
||||
: eventTypes.scrollShouldSetResponder;
|
||||
|
||||
// TODO: stop one short of the current responder.
|
||||
const bubbleShouldSetFrom = !responderInst
|
||||
@@ -515,9 +515,11 @@ const ResponderEventPlugin = {
|
||||
if (trackedTouchCount >= 0) {
|
||||
trackedTouchCount -= 1;
|
||||
} else {
|
||||
console.warn(
|
||||
'Ended a touch event which was not counted in `trackedTouchCount`.',
|
||||
);
|
||||
if (__DEV__) {
|
||||
console.warn(
|
||||
'Ended a touch event which was not counted in `trackedTouchCount`.',
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -548,10 +550,10 @@ const ResponderEventPlugin = {
|
||||
const incrementalTouch = isResponderTouchStart
|
||||
? eventTypes.responderStart
|
||||
: isResponderTouchMove
|
||||
? eventTypes.responderMove
|
||||
: isResponderTouchEnd
|
||||
? eventTypes.responderEnd
|
||||
: null;
|
||||
? eventTypes.responderMove
|
||||
: isResponderTouchEnd
|
||||
? eventTypes.responderEnd
|
||||
: null;
|
||||
|
||||
if (incrementalTouch) {
|
||||
const gesture = ResponderSyntheticEvent.getPooled(
|
||||
@@ -575,8 +577,8 @@ const ResponderEventPlugin = {
|
||||
const finalTouch = isResponderTerminate
|
||||
? eventTypes.responderTerminate
|
||||
: isResponderRelease
|
||||
? eventTypes.responderRelease
|
||||
: null;
|
||||
? eventTypes.responderRelease
|
||||
: null;
|
||||
if (finalTouch) {
|
||||
const finalEvent = ResponderSyntheticEvent.getPooled(
|
||||
finalTouch,
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
*/
|
||||
|
||||
import invariant from 'shared/invariant';
|
||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
|
||||
import {isStartish, isMoveish, isEndish} from './ResponderTopLevelEventTypes';
|
||||
|
||||
@@ -17,7 +16,7 @@ import {isStartish, isMoveish, isEndish} from './ResponderTopLevelEventTypes';
|
||||
* should typically only see IDs in the range of 1-20 because IDs get recycled
|
||||
* when touches end and start again.
|
||||
*/
|
||||
type TouchRecord = {
|
||||
type TouchRecord = {|
|
||||
touchActive: boolean,
|
||||
startPageX: number,
|
||||
startPageY: number,
|
||||
@@ -28,7 +27,7 @@ type TouchRecord = {
|
||||
previousPageX: number,
|
||||
previousPageY: number,
|
||||
previousTimeStamp: number,
|
||||
};
|
||||
|};
|
||||
|
||||
const MAX_TOUCH_BANK = 20;
|
||||
const touchBank: Array<TouchRecord> = [];
|
||||
@@ -47,10 +46,12 @@ type Touch = {
|
||||
pageX: number,
|
||||
pageY: number,
|
||||
timestamp: number,
|
||||
...
|
||||
};
|
||||
type TouchEvent = {
|
||||
changedTouches: Array<Touch>,
|
||||
touches: Array<Touch>,
|
||||
...
|
||||
};
|
||||
|
||||
function timestampForTouch(touch: Touch): number {
|
||||
@@ -95,13 +96,14 @@ function resetTouchRecord(touchRecord: TouchRecord, touch: Touch): void {
|
||||
function getTouchIdentifier({identifier}: Touch): number {
|
||||
invariant(identifier != null, 'Touch object is missing identifier.');
|
||||
if (__DEV__) {
|
||||
warningWithoutStack(
|
||||
identifier <= MAX_TOUCH_BANK,
|
||||
'Touch identifier %s is greater than maximum supported %s which causes ' +
|
||||
'performance issues backfilling array locations for all of the indices.',
|
||||
identifier,
|
||||
MAX_TOUCH_BANK,
|
||||
);
|
||||
if (identifier > MAX_TOUCH_BANK) {
|
||||
console.error(
|
||||
'Touch identifier %s is greater than maximum supported %s which causes ' +
|
||||
'performance issues backfilling array locations for all of the indices.',
|
||||
identifier,
|
||||
MAX_TOUCH_BANK,
|
||||
);
|
||||
}
|
||||
}
|
||||
return identifier;
|
||||
}
|
||||
@@ -129,12 +131,15 @@ function recordTouchMove(touch: Touch): void {
|
||||
touchRecord.currentTimeStamp = timestampForTouch(touch);
|
||||
touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
|
||||
} else {
|
||||
console.warn(
|
||||
'Cannot record touch move without a touch start.\n' + 'Touch Move: %s\n',
|
||||
'Touch Bank: %s',
|
||||
printTouch(touch),
|
||||
printTouchBank(),
|
||||
);
|
||||
if (__DEV__) {
|
||||
console.warn(
|
||||
'Cannot record touch move without a touch start.\n' +
|
||||
'Touch Move: %s\n' +
|
||||
'Touch Bank: %s',
|
||||
printTouch(touch),
|
||||
printTouchBank(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,12 +155,15 @@ function recordTouchEnd(touch: Touch): void {
|
||||
touchRecord.currentTimeStamp = timestampForTouch(touch);
|
||||
touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
|
||||
} else {
|
||||
console.warn(
|
||||
'Cannot record touch end without a touch start.\n' + 'Touch End: %s\n',
|
||||
'Touch Bank: %s',
|
||||
printTouch(touch),
|
||||
printTouchBank(),
|
||||
);
|
||||
if (__DEV__) {
|
||||
console.warn(
|
||||
'Cannot record touch end without a touch start.\n' +
|
||||
'Touch End: %s\n' +
|
||||
'Touch Bank: %s',
|
||||
printTouch(touch),
|
||||
printTouchBank(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,10 +208,9 @@ const ResponderTouchHistoryStore = {
|
||||
}
|
||||
if (__DEV__) {
|
||||
const activeRecord = touchBank[touchHistory.indexOfSingleActiveTouch];
|
||||
warningWithoutStack(
|
||||
activeRecord != null && activeRecord.touchActive,
|
||||
'Cannot find single active touch.',
|
||||
);
|
||||
if (activeRecord == null || !activeRecord.touchActive) {
|
||||
console.error('Cannot find single active touch.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
/* eslint valid-typeof: 0 */
|
||||
|
||||
import invariant from 'shared/invariant';
|
||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
|
||||
const EVENT_POOL_SIZE = 10;
|
||||
|
||||
@@ -283,10 +282,8 @@ function getPooledWarningPropertyDefinition(propName, getVal) {
|
||||
}
|
||||
|
||||
function warn(action, result) {
|
||||
const warningCondition = false;
|
||||
if (__DEV__) {
|
||||
warningWithoutStack(
|
||||
warningCondition,
|
||||
console.error(
|
||||
"This synthetic event is reused for performance reasons. If you're seeing this, " +
|
||||
"you're %s `%s` on a released/nullified synthetic event. %s. " +
|
||||
'If you must keep the original synthetic event around, use event.persist(). ' +
|
||||
|
||||
@@ -79,12 +79,12 @@ const _touchConfig = function(
|
||||
topType === 'topTouchStart'
|
||||
? allTouchObjects
|
||||
: topType === 'topTouchMove'
|
||||
? allTouchObjects
|
||||
: topType === 'topTouchEnd'
|
||||
? antiSubsequence(allTouchObjects, changedIndices)
|
||||
: topType === 'topTouchCancel'
|
||||
? antiSubsequence(allTouchObjects, changedIndices)
|
||||
: null;
|
||||
? allTouchObjects
|
||||
: topType === 'topTouchEnd'
|
||||
? antiSubsequence(allTouchObjects, changedIndices)
|
||||
: topType === 'topTouchCancel'
|
||||
? antiSubsequence(allTouchObjects, changedIndices)
|
||||
: null;
|
||||
|
||||
return {
|
||||
nativeEvent: touchEvent(
|
||||
|
||||
4
packages/react-art/src/ReactARTHostConfig.js
vendored
4
packages/react-art/src/ReactARTHostConfig.js
vendored
@@ -426,7 +426,7 @@ export function unhideTextInstance(textInstance, text): void {
|
||||
// Noop
|
||||
}
|
||||
|
||||
export function mountResponderInstance(
|
||||
export function DEPRECATED_mountResponderInstance(
|
||||
responder: ReactEventResponder<any, any>,
|
||||
responderInstance: ReactEventResponderInstance<any, any>,
|
||||
props: Object,
|
||||
@@ -436,7 +436,7 @@ export function mountResponderInstance(
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function unmountResponderInstance(
|
||||
export function DEPRECATED_unmountResponderInstance(
|
||||
responderInstance: ReactEventResponderInstance<any, any>,
|
||||
): void {
|
||||
throw new Error('Not yet implemented.');
|
||||
|
||||
@@ -214,7 +214,9 @@ describe('ReactART', () => {
|
||||
const chars = this.props.chars.split('');
|
||||
return (
|
||||
<Surface>
|
||||
{chars.map(text => <Shape key={text} title={text} />)}
|
||||
{chars.map(text => (
|
||||
<Shape key={text} title={text} />
|
||||
))}
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
@@ -420,7 +422,7 @@ describe('ReactARTComponents', () => {
|
||||
ReactTestRenderer.create(
|
||||
<Circle stroke="green" strokeWidth={3} fill="blue" />,
|
||||
),
|
||||
).toWarnDev(
|
||||
).toErrorDev(
|
||||
'Warning: Failed prop type: The prop `radius` is marked as required in `Circle`, ' +
|
||||
'but its value is `undefined`.' +
|
||||
'\n in Circle (at **)',
|
||||
@@ -437,7 +439,7 @@ describe('ReactARTComponents', () => {
|
||||
it('should warn if width/height is missing on a Rectangle component', () => {
|
||||
expect(() =>
|
||||
ReactTestRenderer.create(<Rectangle stroke="green" fill="blue" />),
|
||||
).toWarnDev([
|
||||
).toErrorDev([
|
||||
'Warning: Failed prop type: The prop `width` is marked as required in `Rectangle`, ' +
|
||||
'but its value is `undefined`.' +
|
||||
'\n in Rectangle (at **)',
|
||||
@@ -462,7 +464,7 @@ describe('ReactARTComponents', () => {
|
||||
});
|
||||
|
||||
it('should warn if outerRadius/startAngle/endAngle is missing on a Wedge component', () => {
|
||||
expect(() => ReactTestRenderer.create(<Wedge fill="blue" />)).toWarnDev([
|
||||
expect(() => ReactTestRenderer.create(<Wedge fill="blue" />)).toErrorDev([
|
||||
'Warning: Failed prop type: The prop `outerRadius` is marked as required in `Wedge`, ' +
|
||||
'but its value is `undefined`.' +
|
||||
'\n in Wedge (at **)',
|
||||
|
||||
34
packages/react-cache/src/ReactCache.js
vendored
34
packages/react-cache/src/ReactCache.js
vendored
@@ -8,17 +8,15 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
|
||||
import {createLRU} from './LRU';
|
||||
|
||||
type Thenable<T> = {
|
||||
then(resolve: (T) => mixed, reject: (mixed) => mixed): mixed,
|
||||
...
|
||||
};
|
||||
|
||||
type Suspender = {
|
||||
then(resolve: () => mixed, reject: () => mixed): mixed,
|
||||
};
|
||||
type Suspender = {then(resolve: () => mixed, reject: () => mixed): mixed, ...};
|
||||
|
||||
type PendingResult = {|
|
||||
status: 0,
|
||||
@@ -40,6 +38,7 @@ type Result<V> = PendingResult | ResolvedResult<V> | RejectedResult;
|
||||
type Resource<I, V> = {
|
||||
read(I): V,
|
||||
preload(I): void,
|
||||
...
|
||||
};
|
||||
|
||||
const Pending = 0;
|
||||
@@ -64,18 +63,21 @@ function readContext(Context, observedBits) {
|
||||
|
||||
function identityHashFn(input) {
|
||||
if (__DEV__) {
|
||||
warningWithoutStack(
|
||||
typeof input === 'string' ||
|
||||
typeof input === 'number' ||
|
||||
typeof input === 'boolean' ||
|
||||
input === undefined ||
|
||||
input === null,
|
||||
'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,
|
||||
);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -33,46 +33,53 @@ describe('ReactCache', () => {
|
||||
ReactTestRenderer = require('react-test-renderer');
|
||||
Scheduler = require('scheduler');
|
||||
|
||||
TextResource = createResource(([text, ms = 0]) => {
|
||||
let listeners = null;
|
||||
let status = 'pending';
|
||||
let value = null;
|
||||
return {
|
||||
then(resolve, reject) {
|
||||
switch (status) {
|
||||
case 'pending': {
|
||||
if (listeners === null) {
|
||||
listeners = [{resolve, reject}];
|
||||
setTimeout(() => {
|
||||
if (textResourceShouldFail) {
|
||||
Scheduler.unstable_yieldValue(`Promise rejected [${text}]`);
|
||||
status = 'rejected';
|
||||
value = new Error('Failed to load: ' + text);
|
||||
listeners.forEach(listener => listener.reject(value));
|
||||
} else {
|
||||
Scheduler.unstable_yieldValue(`Promise resolved [${text}]`);
|
||||
status = 'resolved';
|
||||
value = text;
|
||||
listeners.forEach(listener => listener.resolve(value));
|
||||
}
|
||||
}, ms);
|
||||
} else {
|
||||
listeners.push({resolve, reject});
|
||||
TextResource = createResource(
|
||||
([text, ms = 0]) => {
|
||||
let listeners = null;
|
||||
let status = 'pending';
|
||||
let value = null;
|
||||
return {
|
||||
then(resolve, reject) {
|
||||
switch (status) {
|
||||
case 'pending': {
|
||||
if (listeners === null) {
|
||||
listeners = [{resolve, reject}];
|
||||
setTimeout(() => {
|
||||
if (textResourceShouldFail) {
|
||||
Scheduler.unstable_yieldValue(
|
||||
`Promise rejected [${text}]`,
|
||||
);
|
||||
status = 'rejected';
|
||||
value = new Error('Failed to load: ' + text);
|
||||
listeners.forEach(listener => listener.reject(value));
|
||||
} else {
|
||||
Scheduler.unstable_yieldValue(
|
||||
`Promise resolved [${text}]`,
|
||||
);
|
||||
status = 'resolved';
|
||||
value = text;
|
||||
listeners.forEach(listener => listener.resolve(value));
|
||||
}
|
||||
}, ms);
|
||||
} else {
|
||||
listeners.push({resolve, reject});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'resolved': {
|
||||
resolve(value);
|
||||
break;
|
||||
}
|
||||
case 'rejected': {
|
||||
reject(value);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'resolved': {
|
||||
resolve(value);
|
||||
break;
|
||||
}
|
||||
case 'rejected': {
|
||||
reject(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}, ([text, ms]) => text);
|
||||
},
|
||||
};
|
||||
},
|
||||
([text, ms]) => text,
|
||||
);
|
||||
|
||||
textResourceShouldFail = false;
|
||||
});
|
||||
@@ -172,15 +179,12 @@ describe('ReactCache', () => {
|
||||
if (__DEV__) {
|
||||
expect(() => {
|
||||
expect(Scheduler).toFlushAndYield(['App', 'Loading...']);
|
||||
}).toWarnDev(
|
||||
[
|
||||
'Invalid key type. Expected a string, number, symbol, or ' +
|
||||
'boolean, but instead received: Hi,100\n\n' +
|
||||
'To use non-primitive values as keys, you must pass a hash ' +
|
||||
'function as the second argument to createResource().',
|
||||
],
|
||||
{withoutStack: true},
|
||||
);
|
||||
}).toErrorDev([
|
||||
'Invalid key type. Expected a string, number, symbol, or ' +
|
||||
'boolean, but instead received: Hi,100\n\n' +
|
||||
'To use non-primitive values as keys, you must pass a hash ' +
|
||||
'function as the second argument to createResource().',
|
||||
]);
|
||||
} else {
|
||||
expect(Scheduler).toFlushAndYield(['App', 'Loading...']);
|
||||
}
|
||||
@@ -296,26 +300,29 @@ describe('ReactCache', () => {
|
||||
|
||||
it('if a thenable resolves multiple times, does not update the first cached value', () => {
|
||||
let resolveThenable;
|
||||
const BadTextResource = createResource(([text, ms = 0]) => {
|
||||
let listeners = null;
|
||||
let value = null;
|
||||
return {
|
||||
then(resolve, reject) {
|
||||
if (value !== null) {
|
||||
resolve(value);
|
||||
} else {
|
||||
if (listeners === null) {
|
||||
listeners = [resolve];
|
||||
resolveThenable = v => {
|
||||
listeners.forEach(listener => listener(v));
|
||||
};
|
||||
const BadTextResource = createResource(
|
||||
([text, ms = 0]) => {
|
||||
let listeners = null;
|
||||
let value = null;
|
||||
return {
|
||||
then(resolve, reject) {
|
||||
if (value !== null) {
|
||||
resolve(value);
|
||||
} else {
|
||||
listeners.push(resolve);
|
||||
if (listeners === null) {
|
||||
listeners = [resolve];
|
||||
resolveThenable = v => {
|
||||
listeners.forEach(listener => listener(v));
|
||||
};
|
||||
} else {
|
||||
listeners.push(resolve);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}, ([text, ms]) => text);
|
||||
},
|
||||
};
|
||||
},
|
||||
([text, ms]) => text,
|
||||
);
|
||||
|
||||
function BadAsyncText(props) {
|
||||
const text = props.text;
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
SimpleMemoComponent,
|
||||
ContextProvider,
|
||||
ForwardRef,
|
||||
Chunk,
|
||||
} from 'shared/ReactWorkTags';
|
||||
|
||||
type CurrentDispatcherRef = typeof ReactSharedInternals.ReactCurrentDispatcher;
|
||||
@@ -35,6 +36,7 @@ type HookLogEntry = {
|
||||
primitive: string,
|
||||
stackError: Error,
|
||||
value: mixed,
|
||||
...
|
||||
};
|
||||
|
||||
let hookLog: Array<HookLogEntry> = [];
|
||||
@@ -115,8 +117,8 @@ function useState<S>(
|
||||
hook !== null
|
||||
? hook.memoizedState
|
||||
: typeof initialState === 'function'
|
||||
? initialState()
|
||||
: initialState;
|
||||
? initialState()
|
||||
: initialState;
|
||||
hookLog.push({primitive: 'State', stackError: new Error(), value: state});
|
||||
return [state, (action: BasicStateAction<S>) => {}];
|
||||
}
|
||||
@@ -141,7 +143,7 @@ function useReducer<S, I, A>(
|
||||
return [state, (action: A) => {}];
|
||||
}
|
||||
|
||||
function useRef<T>(initialValue: T): {current: T} {
|
||||
function useRef<T>(initialValue: T): {|current: T|} {
|
||||
let hook = nextHook();
|
||||
let ref = hook !== null ? hook.memoizedState : {current: initialValue};
|
||||
hookLog.push({
|
||||
@@ -173,7 +175,7 @@ function useEffect(
|
||||
}
|
||||
|
||||
function useImperativeHandle<T>(
|
||||
ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
|
||||
ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,
|
||||
create: () => T,
|
||||
inputs: Array<mixed> | void | null,
|
||||
): void {
|
||||
@@ -284,6 +286,7 @@ export type HooksNode = {
|
||||
name: string,
|
||||
value: mixed,
|
||||
subHooks: Array<HooksNode>,
|
||||
...
|
||||
};
|
||||
export type HooksTree = Array<HooksNode>;
|
||||
|
||||
@@ -623,7 +626,8 @@ export function inspectHooksOfFiber(
|
||||
if (
|
||||
fiber.tag !== FunctionComponent &&
|
||||
fiber.tag !== SimpleMemoComponent &&
|
||||
fiber.tag !== ForwardRef
|
||||
fiber.tag !== ForwardRef &&
|
||||
fiber.tag !== Chunk
|
||||
) {
|
||||
throw new Error(
|
||||
'Unknown Fiber. Needs to be a function component to inspect hooks.',
|
||||
|
||||
@@ -17,16 +17,16 @@ describe('ReactHooksInspection', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
const ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.enableFlareAPI = true;
|
||||
ReactFeatureFlags.enableDeprecatedFlareAPI = true;
|
||||
React = require('react');
|
||||
ReactDebugTools = require('react-debug-tools');
|
||||
});
|
||||
|
||||
it('should inspect a simple useResponder hook', () => {
|
||||
const TestResponder = React.unstable_createResponder('TestResponder', {});
|
||||
const TestResponder = React.DEPRECATED_createResponder('TestResponder', {});
|
||||
|
||||
function Foo(props) {
|
||||
const listener = React.unstable_useResponder(TestResponder, {
|
||||
const listener = React.DEPRECATED_useResponder(TestResponder, {
|
||||
preventDefault: false,
|
||||
});
|
||||
return <div DEPRECATED_flareListeners={listener}>Hello world</div>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-devtools-core",
|
||||
"version": "4.2.1",
|
||||
"version": "4.4.0",
|
||||
"description": "Use react-devtools outside of the browser",
|
||||
"license": "MIT",
|
||||
"main": "./dist/backend.js",
|
||||
|
||||
10
packages/react-devtools-core/src/backend.js
vendored
10
packages/react-devtools-core/src/backend.js
vendored
@@ -27,15 +27,14 @@ type ConnectOptions = {
|
||||
resolveRNStyle?: ResolveNativeStyle,
|
||||
isAppActive?: () => boolean,
|
||||
websocket?: ?WebSocket,
|
||||
...
|
||||
};
|
||||
|
||||
installHook(window);
|
||||
|
||||
const hook: DevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
||||
|
||||
let savedComponentFilters: Array<
|
||||
ComponentFilter,
|
||||
> = getDefaultComponentFilters();
|
||||
let savedComponentFilters: Array<ComponentFilter> = getDefaultComponentFilters();
|
||||
|
||||
function debug(methodName: string, ...args) {
|
||||
if (__DEBUG__) {
|
||||
@@ -56,8 +55,7 @@ export function connectToDevTools(options: ?ConnectOptions) {
|
||||
websocket,
|
||||
resolveRNStyle = null,
|
||||
isAppActive = () => true,
|
||||
} =
|
||||
options || {};
|
||||
} = options || {};
|
||||
|
||||
let retryTimeoutID: TimeoutID | null = null;
|
||||
|
||||
@@ -123,7 +121,7 @@ export function connectToDevTools(options: ?ConnectOptions) {
|
||||
});
|
||||
bridge.addListener(
|
||||
'inspectElement',
|
||||
({id, rendererID}: {id: number, rendererID: number}) => {
|
||||
({id, rendererID}: {id: number, rendererID: number, ...}) => {
|
||||
const renderer = agent.rendererInterfaces[rendererID];
|
||||
if (renderer != null) {
|
||||
// Send event for RN to highlight.
|
||||
|
||||
@@ -6,16 +6,26 @@ The easiest way to install this extension is as a browser add-on:
|
||||
* [Chrome web store](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en)
|
||||
* [Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/react-devtools/)
|
||||
|
||||
## Development
|
||||
|
||||
## Local development
|
||||
You can also build and install this extension from source.
|
||||
|
||||
DevTools embeds local versions of several NPM packages also in this workspace. If you have not already built them, you'll need to do that before getting started by running the following command in the root directory of this repository:
|
||||
```sh
|
||||
yarn build -- react,react-dom,react-is,scheduler --type=NODE
|
||||
```
|
||||
### Prerequisite steps
|
||||
DevTools depends on local versions of several NPM packages<sup>1</sup> also in this workspace. You'll need to either build or download those packages first.
|
||||
|
||||
Once the above packages have been built, you can build the extension by running:
|
||||
<sup>1</sup> Note that at this time, an _experimental_ build is required because DevTools depends on the `createRoot` API.
|
||||
|
||||
#### Build from source
|
||||
To build dependencies from source, run the following command from the root of the repository:
|
||||
```sh
|
||||
yarn build-for-devtools
|
||||
```
|
||||
#### Download from CI
|
||||
To use the latest build from CI, run the following command from the root of the repository:
|
||||
```sh
|
||||
./scripts/release/download-experimental-build.js
|
||||
```
|
||||
### Build steps
|
||||
Once the above packages have been built or downloaded, you can build the extension by running:
|
||||
```sh
|
||||
cd packages/react-devtools-extensions/
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"manifest_version": 2,
|
||||
"name": "React Developer Tools",
|
||||
"description": "Adds React debugging tools to the Chrome Developer Tools.",
|
||||
"version": "4.2.1",
|
||||
"version_name": "4.2.1",
|
||||
"version": "4.4.0",
|
||||
"version_name": "4.4.0",
|
||||
|
||||
"minimum_chrome_version": "49",
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"web_accessible_resources": [
|
||||
"main.html",
|
||||
"panel.html",
|
||||
"build/backend.js",
|
||||
"build/react_devtools_backend.js",
|
||||
"build/renderer.js"
|
||||
],
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ const main = async () => {
|
||||
console.log('WEB_EXT_FIREFOX=nightly yarn run test:firefox');
|
||||
console.log(chalk.gray('\n# You can test against older versions too:'));
|
||||
console.log(
|
||||
'WEB_EXT_FIREFOX=/Applications/Firefox52.app/Contents/MacOS/firefox-bin yarn run test:firefox'
|
||||
'WEB_EXT_FIREFOX=/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox-bin yarn run test:firefox'
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 2,
|
||||
"name": "React Developer Tools",
|
||||
"description": "Adds React debugging tools to the Firefox Developer Tools.",
|
||||
"version": "4.2.1",
|
||||
"version": "4.4.0",
|
||||
|
||||
"applications": {
|
||||
"gecko": {
|
||||
@@ -36,7 +36,7 @@
|
||||
"web_accessible_resources": [
|
||||
"main.html",
|
||||
"panel.html",
|
||||
"build/backend.js",
|
||||
"build/react_devtools_backend.js",
|
||||
"build/renderer.js"
|
||||
],
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"scripts": ["build/background.js"]
|
||||
},
|
||||
|
||||
"permissions": ["file:///*", "http://*/*", "https://*/*"],
|
||||
"permissions": ["file:///*", "http://*/*", "https://*/*", "clipboardWrite"],
|
||||
|
||||
"content_scripts": [
|
||||
{
|
||||
|
||||
@@ -35,7 +35,7 @@ window.addEventListener('message', function(evt) {
|
||||
chrome.runtime.sendMessage(lastDetectionResult);
|
||||
} else if (evt.data.source === 'react-devtools-inject-backend') {
|
||||
const script = document.createElement('script');
|
||||
script.src = chrome.runtime.getURL('build/backend.js');
|
||||
script.src = chrome.runtime.getURL('build/react_devtools_backend.js');
|
||||
document.documentElement.appendChild(script);
|
||||
script.parentNode.removeChild(script);
|
||||
}
|
||||
@@ -86,8 +86,30 @@ if (sessionStorageGetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY) === 'true') {
|
||||
injectCode(rendererCode);
|
||||
}
|
||||
|
||||
// Inject a `__REACT_DEVTOOLS_GLOBAL_HOOK__` global so that React can detect that the
|
||||
// devtools are installed (and skip its suggestion to install the devtools).
|
||||
injectCode(
|
||||
';(' + installHook.toString() + '(window))' + saveNativeValues + detectReact,
|
||||
);
|
||||
// Inject a __REACT_DEVTOOLS_GLOBAL_HOOK__ global for React to interact with.
|
||||
// Only do this for HTML documents though, to avoid e.g. breaking syntax highlighting for XML docs.
|
||||
if (document.contentType === 'text/html') {
|
||||
injectCode(
|
||||
';(' +
|
||||
installHook.toString() +
|
||||
'(window))' +
|
||||
saveNativeValues +
|
||||
detectReact,
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof exportFunction === 'function') {
|
||||
// eslint-disable-next-line no-undef
|
||||
exportFunction(
|
||||
text => {
|
||||
// Call clipboard.writeText from the extension content script
|
||||
// (as it has the clipboardWrite permission) and return a Promise
|
||||
// accessible to the webpage js code.
|
||||
return new window.Promise((resolve, reject) =>
|
||||
window.navigator.clipboard.writeText(text).then(resolve, reject),
|
||||
);
|
||||
},
|
||||
window.wrappedJSObject.__REACT_DEVTOOLS_GLOBAL_HOOK__,
|
||||
{defineAs: 'clipboardCopyText'},
|
||||
);
|
||||
}
|
||||
|
||||
62
packages/react-devtools-extensions/src/main.js
vendored
62
packages/react-devtools-extensions/src/main.js
vendored
@@ -4,11 +4,7 @@ import {createElement} from 'react';
|
||||
import {createRoot, flushSync} from 'react-dom';
|
||||
import Bridge from 'react-devtools-shared/src/bridge';
|
||||
import Store from 'react-devtools-shared/src/devtools/store';
|
||||
import {
|
||||
createViewElementSource,
|
||||
getBrowserName,
|
||||
getBrowserTheme,
|
||||
} from './utils';
|
||||
import {getBrowserName, getBrowserTheme} from './utils';
|
||||
import {LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY} from 'react-devtools-shared/src/constants';
|
||||
import {
|
||||
getSavedComponentFilters,
|
||||
@@ -155,10 +151,54 @@ function createPanelIfReactLoaded() {
|
||||
},
|
||||
);
|
||||
|
||||
const viewElementSourceFunction = createViewElementSource(
|
||||
bridge,
|
||||
store,
|
||||
);
|
||||
const viewAttributeSourceFunction = (id, path) => {
|
||||
const rendererID = store.getRendererIDForElement(id);
|
||||
if (rendererID != null) {
|
||||
// Ask the renderer interface to find the specified attribute,
|
||||
// and store it as a global variable on the window.
|
||||
bridge.send('viewAttributeSource', {id, path, rendererID});
|
||||
|
||||
setTimeout(() => {
|
||||
// Ask Chrome to display the location of the attribute,
|
||||
// assuming the renderer found a match.
|
||||
chrome.devtools.inspectedWindow.eval(`
|
||||
if (window.$attribute != null) {
|
||||
inspect(window.$attribute);
|
||||
}
|
||||
`);
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
const viewElementSourceFunction = id => {
|
||||
const rendererID = store.getRendererIDForElement(id);
|
||||
if (rendererID != null) {
|
||||
// Ask the renderer interface to determine the component function,
|
||||
// and store it as a global variable on the window
|
||||
bridge.send('viewElementSource', {id, rendererID});
|
||||
|
||||
setTimeout(() => {
|
||||
// Ask Chrome to display the location of the component function,
|
||||
// or a render method if it is a Class (ideally Class instance, not type)
|
||||
// assuming the renderer found one.
|
||||
chrome.devtools.inspectedWindow.eval(`
|
||||
if (window.$type != null) {
|
||||
if (
|
||||
window.$type &&
|
||||
window.$type.prototype &&
|
||||
window.$type.prototype.isReactComponent
|
||||
) {
|
||||
// inspect Component.render, not constructor
|
||||
inspect(window.$type.prototype.render);
|
||||
} else {
|
||||
// inspect Functional Component
|
||||
inspect(window.$type);
|
||||
}
|
||||
}
|
||||
`);
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
root = createRoot(document.createElement('div'));
|
||||
|
||||
@@ -170,11 +210,13 @@ function createPanelIfReactLoaded() {
|
||||
bridge,
|
||||
browserTheme: getBrowserTheme(),
|
||||
componentsPortalContainer,
|
||||
enabledInspectedElementContextMenu: true,
|
||||
overrideTab,
|
||||
profilerPortalContainer,
|
||||
showTabBar: false,
|
||||
warnIfUnsupportedVersionDetected: true,
|
||||
store,
|
||||
warnIfUnsupportedVersionDetected: true,
|
||||
viewAttributeSourceFunction,
|
||||
viewElementSourceFunction,
|
||||
}),
|
||||
);
|
||||
|
||||
32
packages/react-devtools-extensions/src/utils.js
vendored
32
packages/react-devtools-extensions/src/utils.js
vendored
@@ -2,38 +2,6 @@
|
||||
|
||||
const IS_CHROME = navigator.userAgent.indexOf('Firefox') < 0;
|
||||
|
||||
export function createViewElementSource(bridge: Bridge, store: Store) {
|
||||
return function viewElementSource(id) {
|
||||
const rendererID = store.getRendererIDForElement(id);
|
||||
if (rendererID != null) {
|
||||
// Ask the renderer interface to determine the component function,
|
||||
// and store it as a global variable on the window
|
||||
bridge.send('viewElementSource', {id, rendererID});
|
||||
|
||||
setTimeout(() => {
|
||||
// Ask Chrome to display the location of the component function,
|
||||
// or a render method if it is a Class (ideally Class instance, not type)
|
||||
// assuming the renderer found one.
|
||||
chrome.devtools.inspectedWindow.eval(`
|
||||
if (window.$type != null) {
|
||||
if (
|
||||
window.$type &&
|
||||
window.$type.prototype &&
|
||||
window.$type.prototype.isReactComponent
|
||||
) {
|
||||
// inspect Component.render, not constructor
|
||||
inspect(window.$type.prototype.render);
|
||||
} else {
|
||||
// inspect Functional Component
|
||||
inspect(window.$type);
|
||||
}
|
||||
}
|
||||
`);
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export type BrowserName = 'Chrome' | 'Firefox';
|
||||
|
||||
export function getBrowserName(): BrowserName {
|
||||
|
||||
@@ -24,7 +24,7 @@ module.exports = {
|
||||
},
|
||||
output: {
|
||||
path: __dirname + '/build',
|
||||
filename: '[name].js',
|
||||
filename: 'react_devtools_backend.js',
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
@@ -153,9 +153,28 @@ iframe.onload = () => {
|
||||
};
|
||||
```
|
||||
|
||||
## Development
|
||||
## Local development
|
||||
You can also build and test this package from source.
|
||||
|
||||
Watch for changes made to the source code and rebuild:
|
||||
### Prerequisite steps
|
||||
DevTools depends on local versions of several NPM packages<sup>1</sup> also in this workspace. You'll need to either build or download those packages first.
|
||||
|
||||
<sup>1</sup> Note that at this time, an _experimental_ build is required because DevTools depends on the `createRoot` API.
|
||||
|
||||
#### Build from source
|
||||
To build dependencies from source, run the following command from the root of the repository:
|
||||
```sh
|
||||
yarn build-for-devtools
|
||||
```
|
||||
#### Download from CI
|
||||
To use the latest build from CI, run the following command from the root of the repository:
|
||||
```sh
|
||||
./scripts/release/download-experimental-build.js
|
||||
```
|
||||
### Build steps
|
||||
Once the above packages have been built or downloaded, you can watch for changes made to the source code and automatically rebuild by running:
|
||||
```sh
|
||||
yarn start
|
||||
```
|
||||
```
|
||||
|
||||
To test package changes, refer to the [`react-devtools-shell` README](https://github.com/facebook/react/blob/master/packages/react-devtools-shell/README.md).
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-devtools-inline",
|
||||
"version": "4.2.1",
|
||||
"version": "4.4.0",
|
||||
"description": "Embed react-devtools within a website",
|
||||
"license": "MIT",
|
||||
"main": "./dist/backend.js",
|
||||
|
||||
@@ -485,8 +485,14 @@ exports[`InspectedElementContext should support complex data types: 1: Inspected
|
||||
"context": null,
|
||||
"hooks": null,
|
||||
"props": {
|
||||
"anonymous_fn": {},
|
||||
"array_buffer": {},
|
||||
"array_of_arrays": [
|
||||
{}
|
||||
],
|
||||
"big_int": {},
|
||||
"bound_fn": {},
|
||||
"data_view": {},
|
||||
"date": {},
|
||||
"fn": {},
|
||||
"html_element": {},
|
||||
@@ -503,7 +509,11 @@ exports[`InspectedElementContext should support complex data types: 1: Inspected
|
||||
"0": {},
|
||||
"1": {}
|
||||
},
|
||||
"object_of_objects": {
|
||||
"inner": {}
|
||||
},
|
||||
"react_element": {},
|
||||
"regexp": {},
|
||||
"set": {
|
||||
"0": "abc",
|
||||
"1": 123
|
||||
@@ -539,6 +549,39 @@ exports[`InspectedElementContext should support custom objects with enumerable p
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should support objects with no prototype: 1: Inspected element 2 1`] = `
|
||||
{
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
"context": null,
|
||||
"hooks": null,
|
||||
"props": {
|
||||
"object": {
|
||||
"string": "abc",
|
||||
"number": 123,
|
||||
"boolean": true
|
||||
}
|
||||
},
|
||||
"state": null
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should support objects with overridden hasOwnProperty: 1: Inspected element 2 1`] = `
|
||||
{
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
"context": null,
|
||||
"hooks": null,
|
||||
"props": {
|
||||
"object": {
|
||||
"name": "blah",
|
||||
"hasOwnProperty": true
|
||||
}
|
||||
},
|
||||
"state": null
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should support simple data types: 1: Initial inspection 1`] = `
|
||||
{
|
||||
"id": 2,
|
||||
|
||||
@@ -1500,17 +1500,7 @@ Object {
|
||||
|
||||
exports[`ProfilingCache should collect data for each root (including ones added or mounted after profiling started): Data for root Parent 3`] = `
|
||||
Object {
|
||||
"commitData": Array [
|
||||
Object {
|
||||
"changeDescriptions": Map {},
|
||||
"duration": 0,
|
||||
"fiberActualDurations": Map {},
|
||||
"fiberSelfDurations": Map {},
|
||||
"interactionIDs": Array [],
|
||||
"priorityLevel": "Immediate",
|
||||
"timestamp": 34,
|
||||
},
|
||||
],
|
||||
"commitData": Array [],
|
||||
"displayName": "Parent",
|
||||
"initialTreeBaseDurations": Map {
|
||||
6 => 11,
|
||||
@@ -1520,19 +1510,7 @@ Object {
|
||||
},
|
||||
"interactionCommits": Map {},
|
||||
"interactions": Map {},
|
||||
"operations": Array [
|
||||
Array [
|
||||
1,
|
||||
6,
|
||||
0,
|
||||
2,
|
||||
4,
|
||||
9,
|
||||
8,
|
||||
7,
|
||||
6,
|
||||
],
|
||||
],
|
||||
"operations": Array [],
|
||||
"rootID": 6,
|
||||
"snapshots": Map {
|
||||
6 => Object {
|
||||
@@ -2066,17 +2044,7 @@ Object {
|
||||
"snapshots": Array [],
|
||||
},
|
||||
Object {
|
||||
"commitData": Array [
|
||||
Object {
|
||||
"changeDescriptions": Array [],
|
||||
"duration": 0,
|
||||
"fiberActualDurations": Array [],
|
||||
"fiberSelfDurations": Array [],
|
||||
"interactionIDs": Array [],
|
||||
"priorityLevel": "Immediate",
|
||||
"timestamp": 34,
|
||||
},
|
||||
],
|
||||
"commitData": Array [],
|
||||
"displayName": "Parent",
|
||||
"initialTreeBaseDurations": Array [
|
||||
Array [
|
||||
@@ -2098,19 +2066,7 @@ Object {
|
||||
],
|
||||
"interactionCommits": Array [],
|
||||
"interactions": Array [],
|
||||
"operations": Array [
|
||||
Array [
|
||||
1,
|
||||
6,
|
||||
0,
|
||||
2,
|
||||
4,
|
||||
9,
|
||||
8,
|
||||
7,
|
||||
6,
|
||||
],
|
||||
],
|
||||
"operations": Array [],
|
||||
"rootID": 6,
|
||||
"snapshots": Array [
|
||||
Array [
|
||||
@@ -2165,6 +2121,437 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`ProfilingCache should properly detect changed hooks: CommitDetails commitIndex: 0 1`] = `
|
||||
Object {
|
||||
"changeDescriptions": Map {
|
||||
3 => Object {
|
||||
"context": null,
|
||||
"didHooksChange": false,
|
||||
"isFirstMount": true,
|
||||
"props": null,
|
||||
"state": null,
|
||||
},
|
||||
},
|
||||
"duration": 0,
|
||||
"fiberActualDurations": Map {
|
||||
1 => 0,
|
||||
2 => 0,
|
||||
3 => 0,
|
||||
},
|
||||
"fiberSelfDurations": Map {
|
||||
1 => 0,
|
||||
2 => 0,
|
||||
3 => 0,
|
||||
},
|
||||
"interactionIDs": Array [],
|
||||
"priorityLevel": "Immediate",
|
||||
"timestamp": 0,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`ProfilingCache should properly detect changed hooks: CommitDetails commitIndex: 1 1`] = `
|
||||
Object {
|
||||
"changeDescriptions": Map {
|
||||
3 => Object {
|
||||
"context": null,
|
||||
"didHooksChange": false,
|
||||
"isFirstMount": false,
|
||||
"props": Array [
|
||||
"count",
|
||||
],
|
||||
"state": null,
|
||||
},
|
||||
},
|
||||
"duration": 0,
|
||||
"fiberActualDurations": Map {
|
||||
3 => 0,
|
||||
2 => 0,
|
||||
1 => 0,
|
||||
},
|
||||
"fiberSelfDurations": Map {
|
||||
3 => 0,
|
||||
2 => 0,
|
||||
1 => 0,
|
||||
},
|
||||
"interactionIDs": Array [],
|
||||
"priorityLevel": "Immediate",
|
||||
"timestamp": 0,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`ProfilingCache should properly detect changed hooks: CommitDetails commitIndex: 2 1`] = `
|
||||
Object {
|
||||
"changeDescriptions": Map {
|
||||
3 => Object {
|
||||
"context": null,
|
||||
"didHooksChange": true,
|
||||
"isFirstMount": false,
|
||||
"props": Array [],
|
||||
"state": null,
|
||||
},
|
||||
},
|
||||
"duration": 0,
|
||||
"fiberActualDurations": Map {
|
||||
3 => 0,
|
||||
},
|
||||
"fiberSelfDurations": Map {
|
||||
3 => 0,
|
||||
},
|
||||
"interactionIDs": Array [],
|
||||
"priorityLevel": "Immediate",
|
||||
"timestamp": 0,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`ProfilingCache should properly detect changed hooks: CommitDetails commitIndex: 3 1`] = `
|
||||
Object {
|
||||
"changeDescriptions": Map {
|
||||
3 => Object {
|
||||
"context": null,
|
||||
"didHooksChange": true,
|
||||
"isFirstMount": false,
|
||||
"props": Array [],
|
||||
"state": null,
|
||||
},
|
||||
},
|
||||
"duration": 0,
|
||||
"fiberActualDurations": Map {
|
||||
3 => 0,
|
||||
},
|
||||
"fiberSelfDurations": Map {
|
||||
3 => 0,
|
||||
},
|
||||
"interactionIDs": Array [],
|
||||
"priorityLevel": "Immediate",
|
||||
"timestamp": 0,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`ProfilingCache should properly detect changed hooks: CommitDetails commitIndex: 4 1`] = `
|
||||
Object {
|
||||
"changeDescriptions": Map {
|
||||
3 => Object {
|
||||
"context": null,
|
||||
"didHooksChange": false,
|
||||
"isFirstMount": false,
|
||||
"props": Array [],
|
||||
"state": null,
|
||||
},
|
||||
},
|
||||
"duration": 0,
|
||||
"fiberActualDurations": Map {
|
||||
3 => 0,
|
||||
2 => 0,
|
||||
1 => 0,
|
||||
},
|
||||
"fiberSelfDurations": Map {
|
||||
3 => 0,
|
||||
2 => 0,
|
||||
1 => 0,
|
||||
},
|
||||
"interactionIDs": Array [],
|
||||
"priorityLevel": "Immediate",
|
||||
"timestamp": 0,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`ProfilingCache should properly detect changed hooks: imported data 1`] = `
|
||||
Object {
|
||||
"dataForRoots": Array [
|
||||
Object {
|
||||
"commitData": Array [
|
||||
Object {
|
||||
"changeDescriptions": Array [
|
||||
Array [
|
||||
3,
|
||||
Object {
|
||||
"context": null,
|
||||
"didHooksChange": false,
|
||||
"isFirstMount": true,
|
||||
"props": null,
|
||||
"state": null,
|
||||
},
|
||||
],
|
||||
],
|
||||
"duration": 0,
|
||||
"fiberActualDurations": Array [
|
||||
Array [
|
||||
1,
|
||||
0,
|
||||
],
|
||||
Array [
|
||||
2,
|
||||
0,
|
||||
],
|
||||
Array [
|
||||
3,
|
||||
0,
|
||||
],
|
||||
],
|
||||
"fiberSelfDurations": Array [
|
||||
Array [
|
||||
1,
|
||||
0,
|
||||
],
|
||||
Array [
|
||||
2,
|
||||
0,
|
||||
],
|
||||
Array [
|
||||
3,
|
||||
0,
|
||||
],
|
||||
],
|
||||
"interactionIDs": Array [],
|
||||
"priorityLevel": "Immediate",
|
||||
"timestamp": 0,
|
||||
},
|
||||
Object {
|
||||
"changeDescriptions": Array [
|
||||
Array [
|
||||
3,
|
||||
Object {
|
||||
"context": null,
|
||||
"didHooksChange": false,
|
||||
"isFirstMount": false,
|
||||
"props": Array [
|
||||
"count",
|
||||
],
|
||||
"state": null,
|
||||
},
|
||||
],
|
||||
],
|
||||
"duration": 0,
|
||||
"fiberActualDurations": Array [
|
||||
Array [
|
||||
3,
|
||||
0,
|
||||
],
|
||||
Array [
|
||||
2,
|
||||
0,
|
||||
],
|
||||
Array [
|
||||
1,
|
||||
0,
|
||||
],
|
||||
],
|
||||
"fiberSelfDurations": Array [
|
||||
Array [
|
||||
3,
|
||||
0,
|
||||
],
|
||||
Array [
|
||||
2,
|
||||
0,
|
||||
],
|
||||
Array [
|
||||
1,
|
||||
0,
|
||||
],
|
||||
],
|
||||
"interactionIDs": Array [],
|
||||
"priorityLevel": "Immediate",
|
||||
"timestamp": 0,
|
||||
},
|
||||
Object {
|
||||
"changeDescriptions": Array [
|
||||
Array [
|
||||
3,
|
||||
Object {
|
||||
"context": null,
|
||||
"didHooksChange": true,
|
||||
"isFirstMount": false,
|
||||
"props": Array [],
|
||||
"state": null,
|
||||
},
|
||||
],
|
||||
],
|
||||
"duration": 0,
|
||||
"fiberActualDurations": Array [
|
||||
Array [
|
||||
3,
|
||||
0,
|
||||
],
|
||||
],
|
||||
"fiberSelfDurations": Array [
|
||||
Array [
|
||||
3,
|
||||
0,
|
||||
],
|
||||
],
|
||||
"interactionIDs": Array [],
|
||||
"priorityLevel": "Immediate",
|
||||
"timestamp": 0,
|
||||
},
|
||||
Object {
|
||||
"changeDescriptions": Array [
|
||||
Array [
|
||||
3,
|
||||
Object {
|
||||
"context": null,
|
||||
"didHooksChange": true,
|
||||
"isFirstMount": false,
|
||||
"props": Array [],
|
||||
"state": null,
|
||||
},
|
||||
],
|
||||
],
|
||||
"duration": 0,
|
||||
"fiberActualDurations": Array [
|
||||
Array [
|
||||
3,
|
||||
0,
|
||||
],
|
||||
],
|
||||
"fiberSelfDurations": Array [
|
||||
Array [
|
||||
3,
|
||||
0,
|
||||
],
|
||||
],
|
||||
"interactionIDs": Array [],
|
||||
"priorityLevel": "Immediate",
|
||||
"timestamp": 0,
|
||||
},
|
||||
Object {
|
||||
"changeDescriptions": Array [
|
||||
Array [
|
||||
3,
|
||||
Object {
|
||||
"context": null,
|
||||
"didHooksChange": false,
|
||||
"isFirstMount": false,
|
||||
"props": Array [],
|
||||
"state": null,
|
||||
},
|
||||
],
|
||||
],
|
||||
"duration": 0,
|
||||
"fiberActualDurations": Array [
|
||||
Array [
|
||||
3,
|
||||
0,
|
||||
],
|
||||
Array [
|
||||
2,
|
||||
0,
|
||||
],
|
||||
Array [
|
||||
1,
|
||||
0,
|
||||
],
|
||||
],
|
||||
"fiberSelfDurations": Array [
|
||||
Array [
|
||||
3,
|
||||
0,
|
||||
],
|
||||
Array [
|
||||
2,
|
||||
0,
|
||||
],
|
||||
Array [
|
||||
1,
|
||||
0,
|
||||
],
|
||||
],
|
||||
"interactionIDs": Array [],
|
||||
"priorityLevel": "Immediate",
|
||||
"timestamp": 0,
|
||||
},
|
||||
],
|
||||
"displayName": "Component",
|
||||
"initialTreeBaseDurations": Array [],
|
||||
"interactionCommits": Array [],
|
||||
"interactions": Array [],
|
||||
"operations": Array [
|
||||
Array [
|
||||
1,
|
||||
1,
|
||||
27,
|
||||
16,
|
||||
67,
|
||||
111,
|
||||
110,
|
||||
116,
|
||||
101,
|
||||
120,
|
||||
116,
|
||||
46,
|
||||
80,
|
||||
114,
|
||||
111,
|
||||
118,
|
||||
105,
|
||||
100,
|
||||
101,
|
||||
114,
|
||||
9,
|
||||
67,
|
||||
111,
|
||||
109,
|
||||
112,
|
||||
111,
|
||||
110,
|
||||
101,
|
||||
110,
|
||||
116,
|
||||
1,
|
||||
1,
|
||||
11,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
4,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
3,
|
||||
5,
|
||||
2,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
4,
|
||||
3,
|
||||
0,
|
||||
],
|
||||
Array [
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
],
|
||||
Array [
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
],
|
||||
Array [
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
],
|
||||
Array [
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
],
|
||||
],
|
||||
"rootID": 1,
|
||||
"snapshots": Array [],
|
||||
},
|
||||
],
|
||||
"version": 4,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`ProfilingCache should record changed props/state/context/hooks: CommitDetails commitIndex: 0 1`] = `
|
||||
Object {
|
||||
"changeDescriptions": Map {
|
||||
|
||||
@@ -618,6 +618,9 @@ exports[`Store should show the right display names for special component types 1
|
||||
▾ <App>
|
||||
<MyComponent>
|
||||
<MyComponent> [ForwardRef]
|
||||
▾ <Anonymous> [ForwardRef]
|
||||
<MyComponent>
|
||||
<Custom> [ForwardRef]
|
||||
<MyComponent> [Memo]
|
||||
▾ <MyComponent> [Memo]
|
||||
<MyComponent> [ForwardRef]
|
||||
|
||||
@@ -8,7 +8,11 @@
|
||||
*/
|
||||
|
||||
import typeof ReactTestRenderer from 'react-test-renderer';
|
||||
import type {GetInspectedElementPath} from 'react-devtools-shared/src/devtools/views/Components/InspectedElementContext';
|
||||
import type {
|
||||
CopyInspectedElementPath,
|
||||
GetInspectedElementPath,
|
||||
StoreAsGlobal,
|
||||
} from 'react-devtools-shared/src/devtools/views/Components/InspectedElementContext';
|
||||
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
|
||||
import type Store from 'react-devtools-shared/src/devtools/store';
|
||||
|
||||
@@ -511,13 +515,26 @@ describe('InspectedElementContext', () => {
|
||||
|
||||
const Example = () => null;
|
||||
|
||||
const arrayOfArrays = [[['abc', 123, true], []]];
|
||||
const div = document.createElement('div');
|
||||
const exampleFunction = () => {};
|
||||
const exampleDateISO = '2019-12-31T23:42:42.000Z';
|
||||
const setShallow = new Set(['abc', 123]);
|
||||
const mapShallow = new Map([['name', 'Brian'], ['food', 'sushi']]);
|
||||
const mapShallow = new Map([
|
||||
['name', 'Brian'],
|
||||
['food', 'sushi'],
|
||||
]);
|
||||
const setOfSets = new Set([new Set(['a', 'b', 'c']), new Set([1, 2, 3])]);
|
||||
const mapOfMaps = new Map([['first', mapShallow], ['second', mapShallow]]);
|
||||
const mapOfMaps = new Map([
|
||||
['first', mapShallow],
|
||||
['second', mapShallow],
|
||||
]);
|
||||
const objectOfObjects = {
|
||||
inner: {string: 'abc', number: 123, boolean: true},
|
||||
};
|
||||
const typedArray = Int8Array.from([100, -100, 0]);
|
||||
const arrayBuffer = typedArray.buffer;
|
||||
const dataView = new DataView(arrayBuffer);
|
||||
const immutableMap = Immutable.fromJS({
|
||||
a: [{hello: 'there'}, 'fixed', true],
|
||||
b: 123,
|
||||
@@ -527,20 +544,31 @@ describe('InspectedElementContext', () => {
|
||||
},
|
||||
});
|
||||
|
||||
class Class {
|
||||
anonymousFunction = () => {};
|
||||
}
|
||||
const instance = new Class();
|
||||
|
||||
const container = document.createElement('div');
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(
|
||||
<Example
|
||||
array_buffer={typedArray.buffer}
|
||||
anonymous_fn={instance.anonymousFunction}
|
||||
array_buffer={arrayBuffer}
|
||||
array_of_arrays={arrayOfArrays}
|
||||
// eslint-disable-next-line no-undef
|
||||
big_int={BigInt(123)}
|
||||
date={new Date()}
|
||||
bound_fn={exampleFunction.bind(this)}
|
||||
data_view={dataView}
|
||||
date={new Date(exampleDateISO)}
|
||||
fn={exampleFunction}
|
||||
html_element={div}
|
||||
immutable={immutableMap}
|
||||
map={mapShallow}
|
||||
map_of_maps={mapOfMaps}
|
||||
object_of_objects={objectOfObjects}
|
||||
react_element={<span />}
|
||||
regexp={/abc/giu}
|
||||
set={setShallow}
|
||||
set_of_sets={setOfSets}
|
||||
symbol={Symbol('symbol')}
|
||||
@@ -578,73 +606,154 @@ describe('InspectedElementContext', () => {
|
||||
expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
|
||||
|
||||
const {
|
||||
anonymous_fn,
|
||||
array_buffer,
|
||||
array_of_arrays,
|
||||
big_int,
|
||||
bound_fn,
|
||||
data_view,
|
||||
date,
|
||||
fn,
|
||||
html_element,
|
||||
immutable,
|
||||
map,
|
||||
map_of_maps,
|
||||
object_of_objects,
|
||||
react_element,
|
||||
regexp,
|
||||
set,
|
||||
set_of_sets,
|
||||
symbol,
|
||||
typed_array,
|
||||
} = (inspectedElement: any).props;
|
||||
|
||||
expect(anonymous_fn[meta.inspectable]).toBe(false);
|
||||
expect(anonymous_fn[meta.name]).toBe('function');
|
||||
expect(anonymous_fn[meta.type]).toBe('function');
|
||||
expect(anonymous_fn[meta.preview_long]).toBe('ƒ () {}');
|
||||
expect(anonymous_fn[meta.preview_short]).toBe('ƒ () {}');
|
||||
|
||||
expect(array_buffer[meta.size]).toBe(3);
|
||||
expect(array_buffer[meta.inspectable]).toBe(false);
|
||||
expect(array_buffer[meta.name]).toBe('ArrayBuffer');
|
||||
expect(array_buffer[meta.type]).toBe('array_buffer');
|
||||
expect(array_buffer[meta.preview_short]).toBe('ArrayBuffer(3)');
|
||||
expect(array_buffer[meta.preview_long]).toBe('ArrayBuffer(3)');
|
||||
|
||||
expect(array_of_arrays[0][meta.size]).toBe(2);
|
||||
expect(array_of_arrays[0][meta.inspectable]).toBe(true);
|
||||
expect(array_of_arrays[0][meta.name]).toBe('Array');
|
||||
expect(array_of_arrays[0][meta.type]).toBe('array');
|
||||
expect(array_of_arrays[0][meta.preview_long]).toBe('[Array(3), Array(0)]');
|
||||
expect(array_of_arrays[0][meta.preview_short]).toBe('Array(2)');
|
||||
|
||||
expect(big_int[meta.inspectable]).toBe(false);
|
||||
expect(big_int[meta.name]).toBe('123');
|
||||
expect(big_int[meta.type]).toBe('bigint');
|
||||
expect(big_int[meta.preview_long]).toBe('123n');
|
||||
expect(big_int[meta.preview_short]).toBe('123n');
|
||||
|
||||
expect(bound_fn[meta.inspectable]).toBe(false);
|
||||
expect(bound_fn[meta.name]).toBe('bound exampleFunction');
|
||||
expect(bound_fn[meta.type]).toBe('function');
|
||||
expect(bound_fn[meta.preview_long]).toBe('ƒ bound exampleFunction() {}');
|
||||
expect(bound_fn[meta.preview_short]).toBe('ƒ bound exampleFunction() {}');
|
||||
|
||||
expect(data_view[meta.size]).toBe(3);
|
||||
expect(data_view[meta.inspectable]).toBe(false);
|
||||
expect(data_view[meta.name]).toBe('DataView');
|
||||
expect(data_view[meta.type]).toBe('data_view');
|
||||
expect(data_view[meta.preview_long]).toBe('DataView(3)');
|
||||
expect(data_view[meta.preview_short]).toBe('DataView(3)');
|
||||
|
||||
expect(date[meta.inspectable]).toBe(false);
|
||||
expect(date[meta.type]).toBe('date');
|
||||
expect(new Date(date[meta.preview_long]).toISOString()).toBe(
|
||||
exampleDateISO,
|
||||
);
|
||||
expect(new Date(date[meta.preview_short]).toISOString()).toBe(
|
||||
exampleDateISO,
|
||||
);
|
||||
|
||||
expect(fn[meta.inspectable]).toBe(false);
|
||||
expect(fn[meta.name]).toBe('exampleFunction');
|
||||
expect(fn[meta.type]).toBe('function');
|
||||
expect(fn[meta.preview_long]).toBe('ƒ exampleFunction() {}');
|
||||
expect(fn[meta.preview_short]).toBe('ƒ exampleFunction() {}');
|
||||
|
||||
expect(html_element[meta.inspectable]).toBe(false);
|
||||
expect(html_element[meta.name]).toBe('DIV');
|
||||
expect(html_element[meta.type]).toBe('html_element');
|
||||
expect(html_element[meta.preview_long]).toBe('<div />');
|
||||
expect(html_element[meta.preview_short]).toBe('<div />');
|
||||
|
||||
expect(immutable[meta.inspectable]).toBeUndefined(); // Complex type
|
||||
expect(immutable[meta.name]).toBe('Map');
|
||||
expect(immutable[meta.type]).toBe('iterator');
|
||||
expect(immutable[meta.preview_long]).toBe(
|
||||
'Map(3) {"a" => List(3), "b" => 123, "c" => Map(2)}',
|
||||
);
|
||||
expect(immutable[meta.preview_short]).toBe('Map(3)');
|
||||
|
||||
expect(map[meta.inspectable]).toBeUndefined(); // Complex type
|
||||
expect(map[meta.name]).toBe('Map');
|
||||
expect(map[meta.type]).toBe('iterator');
|
||||
expect(map[0][meta.type]).toBe('array');
|
||||
expect(map[meta.preview_long]).toBe(
|
||||
'Map(2) {"name" => "Brian", "food" => "sushi"}',
|
||||
);
|
||||
expect(map[meta.preview_short]).toBe('Map(2)');
|
||||
|
||||
expect(map_of_maps[meta.inspectable]).toBeUndefined(); // Complex type
|
||||
expect(map_of_maps[meta.name]).toBe('Map');
|
||||
expect(map_of_maps[meta.type]).toBe('iterator');
|
||||
expect(map_of_maps[0][meta.type]).toBe('array');
|
||||
expect(map_of_maps[meta.preview_long]).toBe(
|
||||
'Map(2) {"first" => Map(2), "second" => Map(2)}',
|
||||
);
|
||||
expect(map_of_maps[meta.preview_short]).toBe('Map(2)');
|
||||
|
||||
expect(object_of_objects.inner[meta.size]).toBe(3);
|
||||
expect(object_of_objects.inner[meta.inspectable]).toBe(true);
|
||||
expect(object_of_objects.inner[meta.name]).toBe('');
|
||||
expect(object_of_objects.inner[meta.type]).toBe('object');
|
||||
expect(object_of_objects.inner[meta.preview_long]).toBe(
|
||||
'{boolean: true, number: 123, string: "abc"}',
|
||||
);
|
||||
expect(object_of_objects.inner[meta.preview_short]).toBe('{…}');
|
||||
|
||||
expect(react_element[meta.inspectable]).toBe(false);
|
||||
expect(react_element[meta.name]).toBe('span');
|
||||
expect(react_element[meta.type]).toBe('react_element');
|
||||
expect(react_element[meta.preview_long]).toBe('<span />');
|
||||
expect(react_element[meta.preview_short]).toBe('<span />');
|
||||
|
||||
expect(regexp[meta.inspectable]).toBe(false);
|
||||
expect(regexp[meta.name]).toBe('/abc/giu');
|
||||
expect(regexp[meta.preview_long]).toBe('/abc/giu');
|
||||
expect(regexp[meta.preview_short]).toBe('/abc/giu');
|
||||
expect(regexp[meta.type]).toBe('regexp');
|
||||
|
||||
expect(set[meta.inspectable]).toBeUndefined(); // Complex type
|
||||
expect(set[meta.name]).toBe('Set');
|
||||
expect(set[meta.type]).toBe('iterator');
|
||||
expect(set[0]).toBe('abc');
|
||||
expect(set[1]).toBe(123);
|
||||
expect(set[meta.preview_long]).toBe('Set(2) {"abc", 123}');
|
||||
expect(set[meta.preview_short]).toBe('Set(2)');
|
||||
|
||||
expect(set_of_sets[meta.inspectable]).toBeUndefined(); // Complex type
|
||||
expect(set_of_sets[meta.name]).toBe('Set');
|
||||
expect(set_of_sets[meta.type]).toBe('iterator');
|
||||
expect(set_of_sets['0'][meta.inspectable]).toBe(true);
|
||||
expect(set_of_sets[meta.preview_long]).toBe('Set(2) {Set(3), Set(3)}');
|
||||
expect(set_of_sets[meta.preview_short]).toBe('Set(2)');
|
||||
|
||||
expect(symbol[meta.inspectable]).toBe(false);
|
||||
expect(symbol[meta.name]).toBe('Symbol(symbol)');
|
||||
expect(symbol[meta.type]).toBe('symbol');
|
||||
expect(symbol[meta.preview_long]).toBe('Symbol(symbol)');
|
||||
expect(symbol[meta.preview_short]).toBe('Symbol(symbol)');
|
||||
|
||||
expect(typed_array[meta.inspectable]).toBeUndefined(); // Complex type
|
||||
expect(typed_array[meta.size]).toBe(3);
|
||||
@@ -653,6 +762,103 @@ describe('InspectedElementContext', () => {
|
||||
expect(typed_array[0]).toBe(100);
|
||||
expect(typed_array[1]).toBe(-100);
|
||||
expect(typed_array[2]).toBe(0);
|
||||
expect(typed_array[meta.preview_long]).toBe('Int8Array(3) [100, -100, 0]');
|
||||
expect(typed_array[meta.preview_short]).toBe('Int8Array(3)');
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should support objects with no prototype', async done => {
|
||||
const Example = () => null;
|
||||
|
||||
const object = Object.create(null);
|
||||
object.string = 'abc';
|
||||
object.number = 123;
|
||||
object.boolean = true;
|
||||
|
||||
const container = document.createElement('div');
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(<Example object={object} />, container),
|
||||
);
|
||||
|
||||
const id = ((store.getElementIDAtIndex(0): any): number);
|
||||
|
||||
let inspectedElement = null;
|
||||
|
||||
function Suspender({target}) {
|
||||
const {getInspectedElement} = React.useContext(InspectedElementContext);
|
||||
inspectedElement = getInspectedElement(id);
|
||||
return null;
|
||||
}
|
||||
|
||||
await utils.actAsync(
|
||||
() =>
|
||||
TestRenderer.create(
|
||||
<Contexts
|
||||
defaultSelectedElementID={id}
|
||||
defaultSelectedElementIndex={0}>
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender target={id} />
|
||||
</React.Suspense>
|
||||
</Contexts>,
|
||||
),
|
||||
false,
|
||||
);
|
||||
|
||||
expect(inspectedElement).not.toBeNull();
|
||||
expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
|
||||
expect(inspectedElement.props.object).toEqual({
|
||||
boolean: true,
|
||||
number: 123,
|
||||
string: 'abc',
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should support objects with overridden hasOwnProperty', async done => {
|
||||
const Example = () => null;
|
||||
|
||||
const object = {
|
||||
name: 'blah',
|
||||
hasOwnProperty: true,
|
||||
};
|
||||
|
||||
const container = document.createElement('div');
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(<Example object={object} />, container),
|
||||
);
|
||||
|
||||
const id = ((store.getElementIDAtIndex(0): any): number);
|
||||
|
||||
let inspectedElement = null;
|
||||
|
||||
function Suspender({target}) {
|
||||
const {getInspectedElement} = React.useContext(InspectedElementContext);
|
||||
inspectedElement = getInspectedElement(id);
|
||||
return null;
|
||||
}
|
||||
|
||||
await utils.actAsync(
|
||||
() =>
|
||||
TestRenderer.create(
|
||||
<Contexts
|
||||
defaultSelectedElementID={id}
|
||||
defaultSelectedElementIndex={0}>
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender target={id} />
|
||||
</React.Suspense>
|
||||
</Contexts>,
|
||||
),
|
||||
false,
|
||||
);
|
||||
|
||||
expect(inspectedElement).not.toBeNull();
|
||||
expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
|
||||
expect(inspectedElement.props.object).toEqual({
|
||||
name: 'blah',
|
||||
hasOwnProperty: true,
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
@@ -1131,4 +1337,139 @@ describe('InspectedElementContext', () => {
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should enable inspected values to be stored as global variables', async done => {
|
||||
const Example = () => null;
|
||||
|
||||
const nestedObject = {
|
||||
a: {
|
||||
value: 1,
|
||||
b: {
|
||||
value: 1,
|
||||
c: {
|
||||
value: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(
|
||||
<Example nestedObject={nestedObject} />,
|
||||
document.createElement('div'),
|
||||
),
|
||||
);
|
||||
|
||||
const id = ((store.getElementIDAtIndex(0): any): number);
|
||||
|
||||
let storeAsGlobal: StoreAsGlobal = ((null: any): StoreAsGlobal);
|
||||
|
||||
function Suspender({target}) {
|
||||
const context = React.useContext(InspectedElementContext);
|
||||
storeAsGlobal = context.storeAsGlobal;
|
||||
return null;
|
||||
}
|
||||
|
||||
await utils.actAsync(
|
||||
() =>
|
||||
TestRenderer.create(
|
||||
<Contexts
|
||||
defaultSelectedElementID={id}
|
||||
defaultSelectedElementIndex={0}>
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender target={id} />
|
||||
</React.Suspense>
|
||||
</Contexts>,
|
||||
),
|
||||
false,
|
||||
);
|
||||
expect(storeAsGlobal).not.toBeNull();
|
||||
|
||||
const logSpy = jest.fn();
|
||||
spyOn(console, 'log').and.callFake(logSpy);
|
||||
|
||||
// Should store the whole value (not just the hydrated parts)
|
||||
storeAsGlobal(id, ['props', 'nestedObject']);
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(logSpy).toHaveBeenCalledWith('$reactTemp1');
|
||||
expect(global.$reactTemp1).toBe(nestedObject);
|
||||
|
||||
logSpy.mockReset();
|
||||
|
||||
// Should store the nested property specified (not just the outer value)
|
||||
storeAsGlobal(id, ['props', 'nestedObject', 'a', 'b']);
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(logSpy).toHaveBeenCalledWith('$reactTemp2');
|
||||
expect(global.$reactTemp2).toBe(nestedObject.a.b);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should enable inspected values to be copied to the clipboard', async done => {
|
||||
const Example = () => null;
|
||||
|
||||
const nestedObject = {
|
||||
a: {
|
||||
value: 1,
|
||||
b: {
|
||||
value: 1,
|
||||
c: {
|
||||
value: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(
|
||||
<Example nestedObject={nestedObject} />,
|
||||
document.createElement('div'),
|
||||
),
|
||||
);
|
||||
|
||||
const id = ((store.getElementIDAtIndex(0): any): number);
|
||||
|
||||
let copyPath: CopyInspectedElementPath = ((null: any): CopyInspectedElementPath);
|
||||
|
||||
function Suspender({target}) {
|
||||
const context = React.useContext(InspectedElementContext);
|
||||
copyPath = context.copyInspectedElementPath;
|
||||
return null;
|
||||
}
|
||||
|
||||
await utils.actAsync(
|
||||
() =>
|
||||
TestRenderer.create(
|
||||
<Contexts
|
||||
defaultSelectedElementID={id}
|
||||
defaultSelectedElementIndex={0}>
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender target={id} />
|
||||
</React.Suspense>
|
||||
</Contexts>,
|
||||
),
|
||||
false,
|
||||
);
|
||||
expect(copyPath).not.toBeNull();
|
||||
|
||||
// Should copy the whole value (not just the hydrated parts)
|
||||
copyPath(id, ['props', 'nestedObject']);
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
|
||||
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
|
||||
JSON.stringify(nestedObject),
|
||||
);
|
||||
|
||||
global.mockClipboardCopy.mockReset();
|
||||
|
||||
// Should copy the nested property specified (not just the outer value)
|
||||
copyPath(id, ['props', 'nestedObject', 'a', 'b']);
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
|
||||
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
|
||||
JSON.stringify(nestedObject.a.b),
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -126,8 +126,14 @@ Object {
|
||||
"context": {},
|
||||
"hooks": null,
|
||||
"props": {
|
||||
"anonymous_fn": {},
|
||||
"array_buffer": {},
|
||||
"array_of_arrays": [
|
||||
{}
|
||||
],
|
||||
"big_int": {},
|
||||
"bound_fn": {},
|
||||
"data_view": {},
|
||||
"date": {},
|
||||
"fn": {},
|
||||
"html_element": {},
|
||||
@@ -144,7 +150,11 @@ Object {
|
||||
"0": {},
|
||||
"1": {}
|
||||
},
|
||||
"object_of_objects": {
|
||||
"inner": {}
|
||||
},
|
||||
"react_element": {},
|
||||
"regexp": {},
|
||||
"set": {
|
||||
"0": "abc",
|
||||
"1": 123
|
||||
@@ -185,6 +195,47 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should support objects with no prototype: 1: Initial inspection 1`] = `
|
||||
Object {
|
||||
"id": 2,
|
||||
"type": "full-data",
|
||||
"value": {
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
"context": {},
|
||||
"hooks": null,
|
||||
"props": {
|
||||
"object": {
|
||||
"string": "abc",
|
||||
"number": 123,
|
||||
"boolean": true
|
||||
}
|
||||
},
|
||||
"state": null
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should support objects with overridden hasOwnProperty: 1: Initial inspection 1`] = `
|
||||
Object {
|
||||
"id": 2,
|
||||
"type": "full-data",
|
||||
"value": {
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
"context": {},
|
||||
"hooks": null,
|
||||
"props": {
|
||||
"object": {
|
||||
"name": "blah",
|
||||
"hasOwnProperty": true
|
||||
}
|
||||
},
|
||||
"state": null
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should support simple data types: 1: Initial inspection 1`] = `
|
||||
Object {
|
||||
"id": 2,
|
||||
|
||||
@@ -147,13 +147,25 @@ describe('InspectedElementContext', () => {
|
||||
|
||||
const Example = () => null;
|
||||
|
||||
const arrayOfArrays = [[['abc', 123, true], []]];
|
||||
const div = document.createElement('div');
|
||||
const exampleFunction = () => {};
|
||||
const setShallow = new Set(['abc', 123]);
|
||||
const mapShallow = new Map([['name', 'Brian'], ['food', 'sushi']]);
|
||||
const mapShallow = new Map([
|
||||
['name', 'Brian'],
|
||||
['food', 'sushi'],
|
||||
]);
|
||||
const setOfSets = new Set([new Set(['a', 'b', 'c']), new Set([1, 2, 3])]);
|
||||
const mapOfMaps = new Map([['first', mapShallow], ['second', mapShallow]]);
|
||||
const mapOfMaps = new Map([
|
||||
['first', mapShallow],
|
||||
['second', mapShallow],
|
||||
]);
|
||||
const objectOfObjects = {
|
||||
inner: {string: 'abc', number: 123, boolean: true},
|
||||
};
|
||||
const typedArray = Int8Array.from([100, -100, 0]);
|
||||
const arrayBuffer = typedArray.buffer;
|
||||
const dataView = new DataView(arrayBuffer);
|
||||
const immutableMap = Immutable.fromJS({
|
||||
a: [{hello: 'there'}, 'fixed', true],
|
||||
b: 123,
|
||||
@@ -163,19 +175,30 @@ describe('InspectedElementContext', () => {
|
||||
},
|
||||
});
|
||||
|
||||
class Class {
|
||||
anonymousFunction = () => {};
|
||||
}
|
||||
const instance = new Class();
|
||||
|
||||
act(() =>
|
||||
ReactDOM.render(
|
||||
<Example
|
||||
array_buffer={typedArray.buffer}
|
||||
anonymous_fn={instance.anonymousFunction}
|
||||
array_buffer={arrayBuffer}
|
||||
array_of_arrays={arrayOfArrays}
|
||||
// eslint-disable-next-line no-undef
|
||||
big_int={BigInt(123)}
|
||||
date={new Date()}
|
||||
bound_fn={exampleFunction.bind(this)}
|
||||
data_view={dataView}
|
||||
date={new Date(123)}
|
||||
fn={exampleFunction}
|
||||
html_element={div}
|
||||
immutable={immutableMap}
|
||||
map={mapShallow}
|
||||
map_of_maps={mapOfMaps}
|
||||
object_of_objects={objectOfObjects}
|
||||
react_element={<span />}
|
||||
regexp={/abc/giu}
|
||||
set={setShallow}
|
||||
set_of_sets={setOfSets}
|
||||
symbol={Symbol('symbol')}
|
||||
@@ -191,36 +214,70 @@ describe('InspectedElementContext', () => {
|
||||
expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
|
||||
|
||||
const {
|
||||
anonymous_fn,
|
||||
array_buffer,
|
||||
array_of_arrays,
|
||||
big_int,
|
||||
bound_fn,
|
||||
data_view,
|
||||
date,
|
||||
fn,
|
||||
html_element,
|
||||
immutable,
|
||||
map,
|
||||
map_of_maps,
|
||||
object_of_objects,
|
||||
react_element,
|
||||
regexp,
|
||||
set,
|
||||
set_of_sets,
|
||||
symbol,
|
||||
typed_array,
|
||||
} = inspectedElement.value.props;
|
||||
|
||||
expect(anonymous_fn[meta.inspectable]).toBe(false);
|
||||
expect(anonymous_fn[meta.name]).toBe('function');
|
||||
expect(anonymous_fn[meta.type]).toBe('function');
|
||||
expect(anonymous_fn[meta.preview_long]).toBe('ƒ () {}');
|
||||
expect(anonymous_fn[meta.preview_short]).toBe('ƒ () {}');
|
||||
|
||||
expect(array_buffer[meta.size]).toBe(3);
|
||||
expect(array_buffer[meta.inspectable]).toBe(false);
|
||||
expect(array_buffer[meta.name]).toBe('ArrayBuffer');
|
||||
expect(array_buffer[meta.type]).toBe('array_buffer');
|
||||
expect(array_buffer[meta.preview_short]).toBe('ArrayBuffer(3)');
|
||||
expect(array_buffer[meta.preview_long]).toBe('ArrayBuffer(3)');
|
||||
|
||||
expect(array_of_arrays[0][meta.size]).toBe(2);
|
||||
expect(array_of_arrays[0][meta.inspectable]).toBe(true);
|
||||
expect(array_of_arrays[0][meta.name]).toBe('Array');
|
||||
expect(array_of_arrays[0][meta.type]).toBe('array');
|
||||
expect(array_of_arrays[0][meta.preview_long]).toBe('[Array(3), Array(0)]');
|
||||
expect(array_of_arrays[0][meta.preview_short]).toBe('Array(2)');
|
||||
|
||||
expect(big_int[meta.inspectable]).toBe(false);
|
||||
expect(big_int[meta.name]).toBe('123');
|
||||
expect(big_int[meta.type]).toBe('bigint');
|
||||
|
||||
expect(bound_fn[meta.inspectable]).toBe(false);
|
||||
expect(bound_fn[meta.name]).toBe('bound exampleFunction');
|
||||
expect(bound_fn[meta.type]).toBe('function');
|
||||
expect(bound_fn[meta.preview_long]).toBe('ƒ bound exampleFunction() {}');
|
||||
expect(bound_fn[meta.preview_short]).toBe('ƒ bound exampleFunction() {}');
|
||||
|
||||
expect(data_view[meta.size]).toBe(3);
|
||||
expect(data_view[meta.inspectable]).toBe(false);
|
||||
expect(data_view[meta.name]).toBe('DataView');
|
||||
expect(data_view[meta.type]).toBe('data_view');
|
||||
|
||||
expect(date[meta.inspectable]).toBe(false);
|
||||
expect(date[meta.type]).toBe('date');
|
||||
|
||||
expect(fn[meta.inspectable]).toBe(false);
|
||||
expect(fn[meta.name]).toBe('exampleFunction');
|
||||
expect(fn[meta.type]).toBe('function');
|
||||
expect(fn[meta.preview_long]).toBe('ƒ exampleFunction() {}');
|
||||
expect(fn[meta.preview_short]).toBe('ƒ exampleFunction() {}');
|
||||
|
||||
expect(html_element[meta.inspectable]).toBe(false);
|
||||
expect(html_element[meta.name]).toBe('DIV');
|
||||
@@ -240,10 +297,25 @@ describe('InspectedElementContext', () => {
|
||||
expect(map_of_maps[meta.type]).toBe('iterator');
|
||||
expect(map_of_maps[0][meta.type]).toBe('array');
|
||||
|
||||
expect(object_of_objects.inner[meta.size]).toBe(3);
|
||||
expect(object_of_objects.inner[meta.inspectable]).toBe(true);
|
||||
expect(object_of_objects.inner[meta.name]).toBe('');
|
||||
expect(object_of_objects.inner[meta.type]).toBe('object');
|
||||
expect(object_of_objects.inner[meta.preview_long]).toBe(
|
||||
'{boolean: true, number: 123, string: "abc"}',
|
||||
);
|
||||
expect(object_of_objects.inner[meta.preview_short]).toBe('{…}');
|
||||
|
||||
expect(react_element[meta.inspectable]).toBe(false);
|
||||
expect(react_element[meta.name]).toBe('span');
|
||||
expect(react_element[meta.type]).toBe('react_element');
|
||||
|
||||
expect(regexp[meta.inspectable]).toBe(false);
|
||||
expect(regexp[meta.name]).toBe('/abc/giu');
|
||||
expect(regexp[meta.preview_long]).toBe('/abc/giu');
|
||||
expect(regexp[meta.preview_short]).toBe('/abc/giu');
|
||||
expect(regexp[meta.type]).toBe('regexp');
|
||||
|
||||
expect(set[meta.inspectable]).toBeUndefined(); // Complex type
|
||||
expect(set[meta.name]).toBe('Set');
|
||||
expect(set[meta.type]).toBe('iterator');
|
||||
@@ -270,6 +342,61 @@ describe('InspectedElementContext', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should support objects with no prototype', async done => {
|
||||
const Example = () => null;
|
||||
|
||||
const object = Object.create(null);
|
||||
object.string = 'abc';
|
||||
object.number = 123;
|
||||
object.boolean = true;
|
||||
|
||||
act(() =>
|
||||
ReactDOM.render(
|
||||
<Example object={object} />,
|
||||
document.createElement('div'),
|
||||
),
|
||||
);
|
||||
|
||||
const id = ((store.getElementIDAtIndex(0): any): number);
|
||||
const inspectedElement = await read(id);
|
||||
|
||||
expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
|
||||
expect(inspectedElement.value.props.object).toEqual({
|
||||
boolean: true,
|
||||
number: 123,
|
||||
string: 'abc',
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should support objects with overridden hasOwnProperty', async done => {
|
||||
const Example = () => null;
|
||||
|
||||
const object = {
|
||||
name: 'blah',
|
||||
hasOwnProperty: true,
|
||||
};
|
||||
|
||||
act(() =>
|
||||
ReactDOM.render(
|
||||
<Example object={object} />,
|
||||
document.createElement('div'),
|
||||
),
|
||||
);
|
||||
|
||||
const id = ((store.getElementIDAtIndex(0): any): number);
|
||||
const inspectedElement = await read(id);
|
||||
|
||||
expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
|
||||
expect(inspectedElement.value.props.object).toEqual({
|
||||
name: 'blah',
|
||||
hasOwnProperty: true,
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should support custom objects with enumerable properties and getters', async done => {
|
||||
class CustomData {
|
||||
_number = 42;
|
||||
@@ -357,4 +484,109 @@ describe('InspectedElementContext', () => {
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should enable inspected values to be stored as global variables', () => {
|
||||
const Example = () => null;
|
||||
|
||||
const nestedObject = {
|
||||
a: {
|
||||
value: 1,
|
||||
b: {
|
||||
value: 1,
|
||||
c: {
|
||||
value: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
act(() =>
|
||||
ReactDOM.render(
|
||||
<Example nestedObject={nestedObject} />,
|
||||
document.createElement('div'),
|
||||
),
|
||||
);
|
||||
|
||||
const id = ((store.getElementIDAtIndex(0): any): number);
|
||||
const rendererID = ((store.getRendererIDForElement(id): any): number);
|
||||
|
||||
const logSpy = jest.fn();
|
||||
spyOn(console, 'log').and.callFake(logSpy);
|
||||
|
||||
// Should store the whole value (not just the hydrated parts)
|
||||
bridge.send('storeAsGlobal', {
|
||||
count: 1,
|
||||
id,
|
||||
path: ['props', 'nestedObject'],
|
||||
rendererID,
|
||||
});
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(logSpy).toHaveBeenCalledWith('$reactTemp1');
|
||||
expect(global.$reactTemp1).toBe(nestedObject);
|
||||
|
||||
logSpy.mockReset();
|
||||
|
||||
// Should store the nested property specified (not just the outer value)
|
||||
bridge.send('storeAsGlobal', {
|
||||
count: 2,
|
||||
id,
|
||||
path: ['props', 'nestedObject', 'a', 'b'],
|
||||
rendererID,
|
||||
});
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(logSpy).toHaveBeenCalledWith('$reactTemp2');
|
||||
expect(global.$reactTemp2).toBe(nestedObject.a.b);
|
||||
});
|
||||
|
||||
it('should enable inspected values to be copied to the clipboard', () => {
|
||||
const Example = () => null;
|
||||
|
||||
const nestedObject = {
|
||||
a: {
|
||||
value: 1,
|
||||
b: {
|
||||
value: 1,
|
||||
c: {
|
||||
value: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
act(() =>
|
||||
ReactDOM.render(
|
||||
<Example nestedObject={nestedObject} />,
|
||||
document.createElement('div'),
|
||||
),
|
||||
);
|
||||
|
||||
const id = ((store.getElementIDAtIndex(0): any): number);
|
||||
const rendererID = ((store.getRendererIDForElement(id): any): number);
|
||||
|
||||
// Should copy the whole value (not just the hydrated parts)
|
||||
bridge.send('copyElementPath', {
|
||||
id,
|
||||
path: ['props', 'nestedObject'],
|
||||
rendererID,
|
||||
});
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
|
||||
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
|
||||
JSON.stringify(nestedObject),
|
||||
);
|
||||
|
||||
global.mockClipboardCopy.mockReset();
|
||||
|
||||
// Should copy the nested property specified (not just the outer value)
|
||||
bridge.send('copyElementPath', {
|
||||
id,
|
||||
path: ['props', 'nestedObject', 'a', 'b'],
|
||||
rendererID,
|
||||
});
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
|
||||
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
|
||||
JSON.stringify(nestedObject.a.b),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,7 +63,9 @@ describe('Store (legacy)', () => {
|
||||
);
|
||||
const Parent = ({count}) => (
|
||||
<div>
|
||||
{new Array(count).fill(true).map((_, index) => <Child key={index} />)}
|
||||
{new Array(count).fill(true).map((_, index) => (
|
||||
<Child key={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
const Child = () => <div>Hi!</div>;
|
||||
@@ -83,7 +85,9 @@ describe('Store (legacy)', () => {
|
||||
it('should support mount and update operations for multiple roots', () => {
|
||||
const Parent = ({count}) => (
|
||||
<div>
|
||||
{new Array(count).fill(true).map((_, index) => <Child key={index} />)}
|
||||
{new Array(count).fill(true).map((_, index) => (
|
||||
<Child key={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
const Child = () => <div>Hi!</div>;
|
||||
@@ -155,7 +159,9 @@ describe('Store (legacy)', () => {
|
||||
);
|
||||
const Parent = ({count}) => (
|
||||
<div>
|
||||
{new Array(count).fill(true).map((_, index) => <Child key={index} />)}
|
||||
{new Array(count).fill(true).map((_, index) => (
|
||||
<Child key={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
const Child = () => <div>Hi!</div>;
|
||||
@@ -261,7 +267,9 @@ describe('Store (legacy)', () => {
|
||||
it('should support mount and update operations', () => {
|
||||
const Parent = ({count}) => (
|
||||
<div>
|
||||
{new Array(count).fill(true).map((_, index) => <Child key={index} />)}
|
||||
{new Array(count).fill(true).map((_, index) => (
|
||||
<Child key={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
const Child = () => <div>Hi!</div>;
|
||||
@@ -297,7 +305,9 @@ describe('Store (legacy)', () => {
|
||||
it('should support mount and update operations for multiple roots', () => {
|
||||
const Parent = ({count}) => (
|
||||
<div>
|
||||
{new Array(count).fill(true).map((_, index) => <Child key={index} />)}
|
||||
{new Array(count).fill(true).map((_, index) => (
|
||||
<Child key={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
const Child = () => <div>Hi!</div>;
|
||||
@@ -375,7 +385,9 @@ describe('Store (legacy)', () => {
|
||||
);
|
||||
const Parent = ({count}) => (
|
||||
<div>
|
||||
{new Array(count).fill(true).map((_, index) => <Child key={index} />)}
|
||||
{new Array(count).fill(true).map((_, index) => (
|
||||
<Child key={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
const Child = () => <div>Hi!</div>;
|
||||
|
||||
@@ -118,5 +118,6 @@ describe('ProfilerStore', () => {
|
||||
const root = store.roots[0];
|
||||
const data = store.profilerStore.getDataForRoot(root);
|
||||
expect(data.commitData).toHaveLength(1);
|
||||
expect(data.operations).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -308,6 +308,130 @@ describe('ProfilingCache', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should properly detect changed hooks', () => {
|
||||
const Context = React.createContext(0);
|
||||
|
||||
function reducer(state, action) {
|
||||
switch (action.type) {
|
||||
case 'invert':
|
||||
return {value: !state.value};
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
|
||||
let dispatch = null;
|
||||
let setState = null;
|
||||
|
||||
const Component = ({count, string}) => {
|
||||
// These hooks may change and initiate re-renders.
|
||||
setState = React.useState('abc')[1];
|
||||
dispatch = React.useReducer(reducer, {value: true})[1];
|
||||
|
||||
// This hook's return value may change between renders,
|
||||
// but the hook itself isn't stateful.
|
||||
React.useContext(Context);
|
||||
|
||||
// These hooks and their dependencies may not change between renders.
|
||||
// We're using them to ensure that they don't trigger false positives.
|
||||
React.useCallback(() => () => {}, [string]);
|
||||
React.useMemo(() => string, [string]);
|
||||
|
||||
// These hooks never "change".
|
||||
React.useEffect(() => {}, [string]);
|
||||
React.useLayoutEffect(() => {}, [string]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const container = document.createElement('div');
|
||||
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
utils.act(() =>
|
||||
ReactDOM.render(
|
||||
<Context.Provider value={true}>
|
||||
<Component count={1} />
|
||||
</Context.Provider>,
|
||||
container,
|
||||
),
|
||||
);
|
||||
|
||||
// Second render has no changed hooks, only changed props.
|
||||
utils.act(() =>
|
||||
ReactDOM.render(
|
||||
<Context.Provider value={true}>
|
||||
<Component count={2} />
|
||||
</Context.Provider>,
|
||||
container,
|
||||
),
|
||||
);
|
||||
|
||||
// Third render has a changed reducer hook
|
||||
utils.act(() => dispatch({type: 'invert'}));
|
||||
|
||||
// Fourth render has a changed state hook
|
||||
utils.act(() => setState('def'));
|
||||
|
||||
// Fifth render has a changed context value, but no changed hook.
|
||||
// Technically, DevTools will miss this "context" change since it only tracks legacy context.
|
||||
utils.act(() =>
|
||||
ReactDOM.render(
|
||||
<Context.Provider value={false}>
|
||||
<Component count={2} />
|
||||
</Context.Provider>,
|
||||
container,
|
||||
),
|
||||
);
|
||||
|
||||
utils.act(() => store.profilerStore.stopProfiling());
|
||||
|
||||
const allCommitData = [];
|
||||
|
||||
function Validator({commitIndex, previousCommitDetails, rootID}) {
|
||||
const commitData = store.profilerStore.getCommitData(rootID, commitIndex);
|
||||
if (previousCommitDetails != null) {
|
||||
expect(commitData).toEqual(previousCommitDetails);
|
||||
} else {
|
||||
allCommitData.push(commitData);
|
||||
expect(commitData).toMatchSnapshot(
|
||||
`CommitDetails commitIndex: ${commitIndex}`,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const rootID = store.roots[0];
|
||||
|
||||
for (let commitIndex = 0; commitIndex < 5; commitIndex++) {
|
||||
utils.act(() => {
|
||||
TestRenderer.create(
|
||||
<Validator
|
||||
commitIndex={commitIndex}
|
||||
previousCommitDetails={null}
|
||||
rootID={rootID}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
expect(allCommitData).toHaveLength(5);
|
||||
|
||||
// Export and re-import profile data and make sure it is retained.
|
||||
utils.exportImportHelper(bridge, store);
|
||||
|
||||
for (let commitIndex = 0; commitIndex < 5; commitIndex++) {
|
||||
utils.act(() => {
|
||||
TestRenderer.create(
|
||||
<Validator
|
||||
commitIndex={commitIndex}
|
||||
previousCommitDetails={allCommitData[commitIndex]}
|
||||
rootID={rootID}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should calculate a self duration based on actual children (not filtered children)', () => {
|
||||
store.componentFilters = [utils.createDisplayNameFilter('^Parent$')];
|
||||
|
||||
|
||||
@@ -14,6 +14,13 @@ import type {
|
||||
|
||||
const env = jasmine.getEnv();
|
||||
env.beforeEach(() => {
|
||||
global.mockClipboardCopy = jest.fn();
|
||||
|
||||
// Test environment doesn't support document methods like execCommand()
|
||||
// Also once the backend components below have been required,
|
||||
// it's too late for a test to mock the clipboard-js modules.
|
||||
jest.mock('clipboard-js', () => ({copy: global.mockClipboardCopy}));
|
||||
|
||||
// These files should be required (and re-reuired) before each test,
|
||||
// rather than imported at the head of the module.
|
||||
// That's because we reset modules between tests,
|
||||
|
||||
@@ -856,6 +856,13 @@ describe('Store', () => {
|
||||
|
||||
const MyComponent = (props, ref) => null;
|
||||
const FowardRefComponent = React.forwardRef(MyComponent);
|
||||
const FowardRefComponentWithAnonymousFunction = React.forwardRef(() => (
|
||||
<MyComponent />
|
||||
));
|
||||
const FowardRefComponentWithCustomDisplayName = React.forwardRef(
|
||||
MyComponent,
|
||||
);
|
||||
FowardRefComponentWithCustomDisplayName.displayName = 'Custom';
|
||||
const MemoComponent = React.memo(MyComponent);
|
||||
const MemoForwardRefComponent = React.memo(FowardRefComponent);
|
||||
const LazyComponent = React.lazy(() => fakeImport(MyComponent));
|
||||
@@ -864,6 +871,8 @@ describe('Store', () => {
|
||||
<React.Fragment>
|
||||
<MyComponent />
|
||||
<FowardRefComponent />
|
||||
<FowardRefComponentWithAnonymousFunction />
|
||||
<FowardRefComponentWithCustomDisplayName />
|
||||
<MemoComponent />
|
||||
<MemoForwardRefComponent />
|
||||
<React.Suspense fallback="Loading...">
|
||||
|
||||
@@ -55,6 +55,19 @@ type ElementAndRendererID = {|
|
||||
rendererID: number,
|
||||
|};
|
||||
|
||||
type StoreAsGlobalParams = {|
|
||||
count: number,
|
||||
id: number,
|
||||
path: Array<string | number>,
|
||||
rendererID: number,
|
||||
|};
|
||||
|
||||
type CopyElementParams = {|
|
||||
id: number,
|
||||
path: Array<string | number>,
|
||||
rendererID: number,
|
||||
|};
|
||||
|
||||
type InspectElementParams = {|
|
||||
id: number,
|
||||
path?: Array<string | number>,
|
||||
@@ -96,7 +109,7 @@ export default class Agent extends EventEmitter<{|
|
||||
_bridge: BackendBridge;
|
||||
_isProfiling: boolean = false;
|
||||
_recordChangeDescriptions: boolean = false;
|
||||
_rendererInterfaces: {[key: RendererID]: RendererInterface} = {};
|
||||
_rendererInterfaces: {[key: RendererID]: RendererInterface, ...} = {};
|
||||
_persistedSelection: PersistedSelection | null = null;
|
||||
_persistedSelectionMatch: PathMatch | null = null;
|
||||
_traceUpdatesEnabled: boolean = false;
|
||||
@@ -126,6 +139,7 @@ export default class Agent extends EventEmitter<{|
|
||||
|
||||
this._bridge = bridge;
|
||||
|
||||
bridge.addListener('copyElementPath', this.copyElementPath);
|
||||
bridge.addListener('getProfilingData', this.getProfilingData);
|
||||
bridge.addListener('getProfilingStatus', this.getProfilingStatus);
|
||||
bridge.addListener('getOwnersList', this.getOwnersList);
|
||||
@@ -140,6 +154,7 @@ export default class Agent extends EventEmitter<{|
|
||||
bridge.addListener('setTraceUpdatesEnabled', this.setTraceUpdatesEnabled);
|
||||
bridge.addListener('startProfiling', this.startProfiling);
|
||||
bridge.addListener('stopProfiling', this.stopProfiling);
|
||||
bridge.addListener('storeAsGlobal', this.storeAsGlobal);
|
||||
bridge.addListener(
|
||||
'syncSelectionFromNativeElementsPanel',
|
||||
this.syncSelectionFromNativeElementsPanel,
|
||||
@@ -150,6 +165,7 @@ export default class Agent extends EventEmitter<{|
|
||||
this.updateAppendComponentStack,
|
||||
);
|
||||
bridge.addListener('updateComponentFilters', this.updateComponentFilters);
|
||||
bridge.addListener('viewAttributeSource', this.viewAttributeSource);
|
||||
bridge.addListener('viewElementSource', this.viewElementSource);
|
||||
|
||||
if (this._isProfiling) {
|
||||
@@ -169,10 +185,19 @@ export default class Agent extends EventEmitter<{|
|
||||
setupTraceUpdates(this);
|
||||
}
|
||||
|
||||
get rendererInterfaces(): {[key: RendererID]: RendererInterface} {
|
||||
get rendererInterfaces(): {[key: RendererID]: RendererInterface, ...} {
|
||||
return this._rendererInterfaces;
|
||||
}
|
||||
|
||||
copyElementPath = ({id, path, rendererID}: CopyElementParams) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
||||
} else {
|
||||
renderer.copyElementPath(id, path);
|
||||
}
|
||||
};
|
||||
|
||||
getInstanceAndStyle({
|
||||
id,
|
||||
rendererID,
|
||||
@@ -409,6 +434,15 @@ export default class Agent extends EventEmitter<{|
|
||||
this._bridge.send('profilingStatus', this._isProfiling);
|
||||
};
|
||||
|
||||
storeAsGlobal = ({count, id, path, rendererID}: StoreAsGlobalParams) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
||||
} else {
|
||||
renderer.storeAsGlobal(id, path, count);
|
||||
}
|
||||
};
|
||||
|
||||
updateAppendComponentStack = (appendComponentStack: boolean) => {
|
||||
// If the frontend preference has change,
|
||||
// or in the case of React Native- if the backend is just finding out the preference-
|
||||
@@ -430,6 +464,15 @@ export default class Agent extends EventEmitter<{|
|
||||
}
|
||||
};
|
||||
|
||||
viewAttributeSource = ({id, path, rendererID}: CopyElementParams) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
||||
} else {
|
||||
renderer.prepareViewAttributeSource(id, path);
|
||||
}
|
||||
};
|
||||
|
||||
viewElementSource = ({id, rendererID}: ElementAndRendererID) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
|
||||
@@ -30,6 +30,7 @@ export function initBackend(
|
||||
id: number,
|
||||
renderer: ReactRenderer,
|
||||
rendererInterface: RendererInterface,
|
||||
...
|
||||
}) => {
|
||||
agent.setRendererInterface(id, rendererInterface);
|
||||
|
||||
@@ -91,7 +92,7 @@ export function initBackend(
|
||||
subs.push(
|
||||
hook.sub(
|
||||
'renderer',
|
||||
({id, renderer}: {id: number, renderer: ReactRenderer}) => {
|
||||
({id, renderer}: {id: number, renderer: ReactRenderer, ...}) => {
|
||||
attachRenderer(id, renderer);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -15,8 +15,8 @@ import {
|
||||
ElementTypeOtherOrUnknown,
|
||||
} from 'react-devtools-shared/src/types';
|
||||
import {getUID, utfEncodeString, printOperationsArray} from '../../utils';
|
||||
import {cleanForBridge, copyWithSet} from '../utils';
|
||||
import {getDisplayName} from 'react-devtools-shared/src/utils';
|
||||
import {cleanForBridge, copyToClipboard, copyWithSet} from '../utils';
|
||||
import {getDisplayName, getInObject} from 'react-devtools-shared/src/utils';
|
||||
import {
|
||||
__DEBUG__,
|
||||
TREE_OPERATION_ADD,
|
||||
@@ -159,6 +159,11 @@ export function attach(
|
||||
};
|
||||
}
|
||||
|
||||
function getDisplayNameForFiberID(id: number): string | null {
|
||||
const internalInstance = idToInternalInstanceMap.get(id);
|
||||
return internalInstance ? getData(internalInstance).displayName : null;
|
||||
}
|
||||
|
||||
function getID(internalInstance: InternalInstance): number {
|
||||
if (typeof internalInstance !== 'object') {
|
||||
throw new Error('Invalid internal instance: ' + internalInstance);
|
||||
@@ -649,6 +654,30 @@ export function attach(
|
||||
}
|
||||
}
|
||||
|
||||
function storeAsGlobal(
|
||||
id: number,
|
||||
path: Array<string | number>,
|
||||
count: number,
|
||||
): void {
|
||||
const inspectedElement = inspectElementRaw(id);
|
||||
if (inspectedElement !== null) {
|
||||
const value = getInObject(inspectedElement, path);
|
||||
const key = `$reactTemp${count}`;
|
||||
|
||||
window[key] = value;
|
||||
|
||||
console.log(key);
|
||||
console.log(value);
|
||||
}
|
||||
}
|
||||
|
||||
function copyElementPath(id: number, path: Array<string | number>): void {
|
||||
const inspectedElement = inspectElementRaw(id);
|
||||
if (inspectedElement !== null) {
|
||||
copyToClipboard(getInObject(inspectedElement, path));
|
||||
}
|
||||
}
|
||||
|
||||
function inspectElement(
|
||||
id: number,
|
||||
path?: Array<string | number>,
|
||||
@@ -812,6 +841,16 @@ export function attach(
|
||||
}
|
||||
}
|
||||
|
||||
function prepareViewAttributeSource(
|
||||
id: number,
|
||||
path: Array<string | number>,
|
||||
): void {
|
||||
const inspectedElement = inspectElementRaw(id);
|
||||
if (inspectedElement !== null) {
|
||||
window.$attribute = getInObject(inspectedElement, path);
|
||||
}
|
||||
}
|
||||
|
||||
function prepareViewElementSource(id: number): void {
|
||||
const internalInstance = idToInternalInstanceMap.get(id);
|
||||
if (internalInstance == null) {
|
||||
@@ -927,8 +966,10 @@ export function attach(
|
||||
|
||||
return {
|
||||
cleanup,
|
||||
copyElementPath,
|
||||
flushInitialOperations,
|
||||
getBestMatchForTrackedPath,
|
||||
getDisplayNameForFiberID,
|
||||
getFiberIDForNative: getInternalIDForNative,
|
||||
getInstanceAndStyle,
|
||||
findNativeNodesForFiberID: (id: number) => {
|
||||
@@ -943,6 +984,7 @@ export function attach(
|
||||
inspectElement,
|
||||
logElementToConsole,
|
||||
overrideSuspense,
|
||||
prepareViewAttributeSource,
|
||||
prepareViewElementSource,
|
||||
renderer,
|
||||
setInContext,
|
||||
@@ -953,6 +995,7 @@ export function attach(
|
||||
setTrackedPath,
|
||||
startProfiling,
|
||||
stopProfiling,
|
||||
storeAsGlobal,
|
||||
updateComponentFilters,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export function decorate(object: Object, attr: string, fn: Function): Function {
|
||||
|
||||
export function decorateMany(
|
||||
source: Object,
|
||||
fns: {[attr: string]: Function},
|
||||
fns: {[attr: string]: Function, ...},
|
||||
): Object {
|
||||
const olds = {};
|
||||
for (const name in fns) {
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
utfEncodeString,
|
||||
} from 'react-devtools-shared/src/utils';
|
||||
import {sessionStorageGetItem} from 'react-devtools-shared/src/storage';
|
||||
import {cleanForBridge, copyWithSet} from './utils';
|
||||
import {cleanForBridge, copyToClipboard, copyWithSet} from './utils';
|
||||
import {
|
||||
__DEBUG__,
|
||||
SESSION_STORAGE_RELOAD_AND_PROFILE_KEY,
|
||||
@@ -76,7 +76,7 @@ import type {
|
||||
type getDisplayNameForFiberType = (fiber: Fiber) => string | null;
|
||||
type getTypeSymbolType = (type: any) => Symbol | number;
|
||||
|
||||
type ReactSymbolsType = {
|
||||
type ReactSymbolsType = {|
|
||||
CONCURRENT_MODE_NUMBER: number,
|
||||
CONCURRENT_MODE_SYMBOL_STRING: string,
|
||||
DEPRECATED_ASYNC_MODE_SYMBOL_STRING: string,
|
||||
@@ -94,7 +94,7 @@ type ReactSymbolsType = {
|
||||
STRICT_MODE_SYMBOL_STRING: string,
|
||||
SCOPE_NUMBER: number,
|
||||
SCOPE_SYMBOL_STRING: string,
|
||||
};
|
||||
|};
|
||||
|
||||
type ReactPriorityLevelsType = {|
|
||||
ImmediatePriority: number,
|
||||
@@ -347,7 +347,7 @@ export function getInternalReactConstants(
|
||||
|
||||
// NOTICE Keep in sync with shouldFilterFiber() and other get*ForFiber methods
|
||||
function getDisplayNameForFiber(fiber: Fiber): string | null {
|
||||
const {elementType, type, tag} = fiber;
|
||||
const {type, tag} = fiber;
|
||||
|
||||
let resolvedType = type;
|
||||
if (typeof type === 'object' && type !== null) {
|
||||
@@ -364,8 +364,10 @@ export function getInternalReactConstants(
|
||||
case IndeterminateComponent:
|
||||
return getDisplayName(resolvedType);
|
||||
case ForwardRef:
|
||||
// Mirror https://github.com/facebook/react/blob/7c21bf72ace77094fd1910cc350a548287ef8350/packages/shared/getComponentName.js#L27-L37
|
||||
return (
|
||||
resolvedType.displayName || getDisplayName(resolvedType, 'Anonymous')
|
||||
(type && type.displayName) ||
|
||||
getDisplayName(resolvedType, 'Anonymous')
|
||||
);
|
||||
case HostRoot:
|
||||
return null;
|
||||
@@ -377,11 +379,7 @@ export function getInternalReactConstants(
|
||||
return null;
|
||||
case MemoComponent:
|
||||
case SimpleMemoComponent:
|
||||
if (elementType.displayName) {
|
||||
return elementType.displayName;
|
||||
} else {
|
||||
return getDisplayName(resolvedType, 'Anonymous');
|
||||
}
|
||||
return getDisplayName(resolvedType, 'Anonymous');
|
||||
case SuspenseComponent:
|
||||
return 'Suspense';
|
||||
case SuspenseListComponent:
|
||||
@@ -1023,9 +1021,14 @@ export function attach(
|
||||
pendingSimulatedUnmountedIDs.length === 0 &&
|
||||
pendingUnmountedRootID === null
|
||||
) {
|
||||
// If we're currently profiling, send an "operations" method even if there are no mutations to the tree.
|
||||
// The frontend needs this no-op info to know how to reconstruct the tree for each commit,
|
||||
// even if a particular commit didn't change the shape of the tree.
|
||||
// If we aren't profiling, we can just bail out here.
|
||||
// No use sending an empty update over the bridge.
|
||||
//
|
||||
// The Profiler stores metadata for each commit and reconstructs the app tree per commit using:
|
||||
// (1) an initial tree snapshot and
|
||||
// (2) the operations array for each commit
|
||||
// Because of this, it's important that the operations and metadata arrays align,
|
||||
// So it's important not to ommit even empty operations while profiing is active.
|
||||
if (!isProfiling) {
|
||||
return;
|
||||
}
|
||||
@@ -1370,6 +1373,8 @@ export function attach(
|
||||
if (isProfiling) {
|
||||
const {alternate} = fiber;
|
||||
|
||||
// It's important to update treeBaseDuration even if the current Fiber did not render,
|
||||
// becuase it's possible that one of its descednants did.
|
||||
if (
|
||||
alternate == null ||
|
||||
treeBaseDuration !== alternate.treeBaseDuration
|
||||
@@ -1757,14 +1762,6 @@ export function attach(
|
||||
const current = root.current;
|
||||
const alternate = current.alternate;
|
||||
|
||||
// Certain types of updates bail out at the root without doing any actual render work.
|
||||
// React should probably not call the DevTools commit hook in this case,
|
||||
// but if it does- we can detect it and filter them out from the profiler.
|
||||
const didBailoutAtRoot =
|
||||
alternate !== null &&
|
||||
alternate.expirationTime === 0 &&
|
||||
alternate.childExpirationTime === 0;
|
||||
|
||||
currentRootID = getFiberID(getPrimaryFiber(current));
|
||||
|
||||
// Before the traversals, remember to start tracking
|
||||
@@ -1781,7 +1778,7 @@ export function attach(
|
||||
// where some v16 renderers support profiling and others don't.
|
||||
const isProfilingSupported = root.memoizedInteractions != null;
|
||||
|
||||
if (isProfiling && isProfilingSupported && !didBailoutAtRoot) {
|
||||
if (isProfiling && isProfilingSupported) {
|
||||
// If profiling is active, store commit time and duration, and the current interactions.
|
||||
// The frontend may request this information after profiling has stopped.
|
||||
currentCommitProfilingMetadata = {
|
||||
@@ -1825,7 +1822,7 @@ export function attach(
|
||||
mountFiberRecursively(current, null, false, false);
|
||||
}
|
||||
|
||||
if (isProfiling && isProfilingSupported && !didBailoutAtRoot) {
|
||||
if (isProfiling && isProfilingSupported) {
|
||||
const commitProfilingMetadata = ((rootToCommitProfilingMetadataMap: any): CommitProfilingMetadataMap).get(
|
||||
currentRootID,
|
||||
);
|
||||
@@ -1910,6 +1907,11 @@ export function attach(
|
||||
}
|
||||
}
|
||||
|
||||
function getDisplayNameForFiberID(id) {
|
||||
const fiber = idToFiberMap.get(id);
|
||||
return fiber != null ? getDisplayNameForFiber(((fiber: any): Fiber)) : null;
|
||||
}
|
||||
|
||||
function getFiberIDForNative(
|
||||
hostInstance,
|
||||
findNearestUnfilteredAncestor = false,
|
||||
@@ -2115,6 +2117,19 @@ export function attach(
|
||||
}
|
||||
// END copied code
|
||||
|
||||
function prepareViewAttributeSource(
|
||||
id: number,
|
||||
path: Array<string | number>,
|
||||
): void {
|
||||
const isCurrent = isMostRecentlyInspectedElementCurrent(id);
|
||||
if (isCurrent) {
|
||||
window.$attribute = getInObject(
|
||||
((mostRecentlyInspectedElement: any): InspectedElement),
|
||||
path,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function prepareViewElementSource(id: number): void {
|
||||
let fiber = idToFiberMap.get(id);
|
||||
if (fiber == null) {
|
||||
@@ -2490,6 +2505,40 @@ export function attach(
|
||||
}
|
||||
}
|
||||
|
||||
function storeAsGlobal(
|
||||
id: number,
|
||||
path: Array<string | number>,
|
||||
count: number,
|
||||
): void {
|
||||
const isCurrent = isMostRecentlyInspectedElementCurrent(id);
|
||||
|
||||
if (isCurrent) {
|
||||
const value = getInObject(
|
||||
((mostRecentlyInspectedElement: any): InspectedElement),
|
||||
path,
|
||||
);
|
||||
const key = `$reactTemp${count}`;
|
||||
|
||||
window[key] = value;
|
||||
|
||||
console.log(key);
|
||||
console.log(value);
|
||||
}
|
||||
}
|
||||
|
||||
function copyElementPath(id: number, path: Array<string | number>): void {
|
||||
const isCurrent = isMostRecentlyInspectedElementCurrent(id);
|
||||
|
||||
if (isCurrent) {
|
||||
copyToClipboard(
|
||||
getInObject(
|
||||
((mostRecentlyInspectedElement: any): InspectedElement),
|
||||
path,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function inspectElement(
|
||||
id: number,
|
||||
path?: Array<string | number>,
|
||||
@@ -3131,9 +3180,11 @@ export function attach(
|
||||
|
||||
return {
|
||||
cleanup,
|
||||
copyElementPath,
|
||||
findNativeNodesForFiberID,
|
||||
flushInitialOperations,
|
||||
getBestMatchForTrackedPath,
|
||||
getDisplayNameForFiberID,
|
||||
getFiberIDForNative,
|
||||
getInstanceAndStyle,
|
||||
getOwnersList,
|
||||
@@ -3143,6 +3194,7 @@ export function attach(
|
||||
handleCommitFiberUnmount,
|
||||
inspectElement,
|
||||
logElementToConsole,
|
||||
prepareViewAttributeSource,
|
||||
prepareViewElementSource,
|
||||
overrideSuspense,
|
||||
renderer,
|
||||
@@ -3154,6 +3206,7 @@ export function attach(
|
||||
setTrackedPath,
|
||||
startProfiling,
|
||||
stopProfiling,
|
||||
storeAsGlobal,
|
||||
updateComponentFilters,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -39,6 +39,11 @@ export type RendererID = number;
|
||||
|
||||
type Dispatcher = any;
|
||||
|
||||
export type GetDisplayNameForFiberID = (
|
||||
id: number,
|
||||
findNearestUnfilteredAncestor?: boolean,
|
||||
) => string | null;
|
||||
|
||||
export type GetFiberIDForNative = (
|
||||
component: NativeType,
|
||||
findNearestUnfilteredAncestor?: boolean,
|
||||
@@ -48,13 +53,13 @@ export type FindNativeNodesForFiberID = (id: number) => ?Array<NativeType>;
|
||||
export type ReactProviderType<T> = {
|
||||
$$typeof: Symbol | number,
|
||||
_context: ReactContext<T>,
|
||||
...
|
||||
};
|
||||
|
||||
export type ReactRenderer = {
|
||||
findFiberByHostInstance: (hostInstance: NativeType) => ?Fiber,
|
||||
version: string,
|
||||
bundleType: BundleType,
|
||||
|
||||
// 16.9+
|
||||
overrideHookState?: ?(
|
||||
fiber: Object,
|
||||
@@ -62,30 +67,25 @@ export type ReactRenderer = {
|
||||
path: Array<string | number>,
|
||||
value: any,
|
||||
) => void,
|
||||
|
||||
// 16.7+
|
||||
overrideProps?: ?(
|
||||
fiber: Object,
|
||||
path: Array<string | number>,
|
||||
value: any,
|
||||
) => void,
|
||||
|
||||
// 16.9+
|
||||
scheduleUpdate?: ?(fiber: Object) => void,
|
||||
setSuspenseHandler?: ?(shouldSuspend: (fiber: Object) => boolean) => void,
|
||||
|
||||
// Only injected by React v16.8+ in order to support hooks inspection.
|
||||
currentDispatcherRef?: {|current: null | Dispatcher|},
|
||||
|
||||
// Only injected by React v16.9+ in DEV mode.
|
||||
// Enables DevTools to append owners-only component stack to error messages.
|
||||
getCurrentFiber?: () => Fiber | null,
|
||||
|
||||
// Uniquely identifies React DOM v15.
|
||||
ComponentTree?: any,
|
||||
|
||||
// Present for React DOM v12 (possibly earlier) through v15.
|
||||
Mount?: any,
|
||||
...
|
||||
};
|
||||
|
||||
export type ChangeDescription = {|
|
||||
@@ -225,10 +225,12 @@ export type InstanceAndStyle = {|
|
||||
|
||||
export type RendererInterface = {
|
||||
cleanup: () => void,
|
||||
copyElementPath: (id: number, path: Array<string | number>) => void,
|
||||
findNativeNodesForFiberID: FindNativeNodesForFiberID,
|
||||
flushInitialOperations: () => void,
|
||||
getBestMatchForTrackedPath: () => PathMatch | null,
|
||||
getFiberIDForNative: GetFiberIDForNative,
|
||||
getDisplayNameForFiberID: GetDisplayNameForFiberID,
|
||||
getInstanceAndStyle(id: number): InstanceAndStyle,
|
||||
getProfilingData(): ProfilingDataBackend,
|
||||
getOwnersList: (id: number) => Array<Owner> | null,
|
||||
@@ -241,6 +243,10 @@ export type RendererInterface = {
|
||||
) => InspectedElementPayload,
|
||||
logElementToConsole: (id: number) => void,
|
||||
overrideSuspense: (id: number, forceFallback: boolean) => void,
|
||||
prepareViewAttributeSource: (
|
||||
id: number,
|
||||
path: Array<string | number>,
|
||||
) => void,
|
||||
prepareViewElementSource: (id: number) => void,
|
||||
renderer: ReactRenderer | null,
|
||||
setInContext: (id: number, path: Array<string | number>, value: any) => void,
|
||||
@@ -256,13 +262,19 @@ export type RendererInterface = {
|
||||
setTrackedPath: (path: Array<PathFrame> | null) => void,
|
||||
startProfiling: (recordChangeDescriptions: boolean) => void,
|
||||
stopProfiling: () => void,
|
||||
storeAsGlobal: (
|
||||
id: number,
|
||||
path: Array<string | number>,
|
||||
count: number,
|
||||
) => void,
|
||||
updateComponentFilters: (componentFilters: Array<ComponentFilter>) => void,
|
||||
...
|
||||
};
|
||||
|
||||
export type Handler = (data: any) => void;
|
||||
|
||||
export type DevToolsHook = {
|
||||
listeners: {[key: string]: Array<Handler>},
|
||||
listeners: {[key: string]: Array<Handler>, ...},
|
||||
rendererInterfaces: Map<RendererID, RendererInterface>,
|
||||
renderers: Map<RendererID, ReactRenderer>,
|
||||
|
||||
@@ -289,4 +301,5 @@ export type DevToolsHook = {
|
||||
// Added in v16.9 to support Fast Refresh
|
||||
didError?: boolean,
|
||||
) => void,
|
||||
...
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import {copy} from 'clipboard-js';
|
||||
import {dehydrate} from '../hydration';
|
||||
|
||||
import type {DehydratedData} from 'react-devtools-shared/src/devtools/views/Components/types';
|
||||
@@ -37,6 +38,23 @@ export function cleanForBridge(
|
||||
}
|
||||
}
|
||||
|
||||
export function copyToClipboard(value: any): void {
|
||||
const safeToCopy = serializeToString(value);
|
||||
const text = safeToCopy === undefined ? 'undefined' : safeToCopy;
|
||||
const {clipboardCopyText} = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
||||
|
||||
// On Firefox navigator.clipboard.writeText has to be called from
|
||||
// the content script js code (because it requires the clipboardWrite
|
||||
// permission to be allowed out of a "user handling" callback),
|
||||
// clipboardCopyText is an helper injected into the page from.
|
||||
// injectGlobalHook.
|
||||
if (typeof clipboardCopyText === 'function') {
|
||||
clipboardCopyText(text).catch(err => {});
|
||||
} else {
|
||||
copy(text);
|
||||
}
|
||||
}
|
||||
|
||||
export function copyWithSet(
|
||||
obj: Object | Array<any>,
|
||||
path: Array<string | number>,
|
||||
@@ -52,3 +70,17 @@ export function copyWithSet(
|
||||
updated[key] = copyWithSet(obj[key], path, value, index + 1);
|
||||
return updated;
|
||||
}
|
||||
|
||||
export function serializeToString(data: any): string {
|
||||
const cache = new Set();
|
||||
// Use a custom replacer function to protect against circular references.
|
||||
return JSON.stringify(data, (key, value) => {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (cache.has(value)) {
|
||||
return;
|
||||
}
|
||||
cache.add(value);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import assign from 'object-assign';
|
||||
import {getElementDimensions, getNestedBoundingClientRect} from '../utils';
|
||||
|
||||
import type {DevToolsHook} from 'react-devtools-shared/src/backend/types';
|
||||
import type {Rect} from '../utils';
|
||||
|
||||
type Box = {|top: number, left: number, width: number, height: number|};
|
||||
@@ -226,9 +227,24 @@ export default class Overlay {
|
||||
|
||||
if (!name) {
|
||||
name = elements[0].nodeName.toLowerCase();
|
||||
const ownerName = getOwnerDisplayName(elements[0]);
|
||||
if (ownerName) {
|
||||
name += ' (in ' + ownerName + ')';
|
||||
|
||||
const node = elements[0];
|
||||
const hook: DevToolsHook =
|
||||
node.ownerDocument.defaultView.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
||||
if (hook != null && hook.rendererInterfaces != null) {
|
||||
let ownerName = null;
|
||||
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
|
||||
for (const rendererInterface of hook.rendererInterfaces.values()) {
|
||||
const id = rendererInterface.getFiberIDForNative(node, true);
|
||||
if (id !== null) {
|
||||
ownerName = rendererInterface.getDisplayNameForFiberID(id, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ownerName) {
|
||||
name += ' (in ' + ownerName + ')';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,37 +275,6 @@ export default class Overlay {
|
||||
}
|
||||
}
|
||||
|
||||
function getOwnerDisplayName(node) {
|
||||
const fiber = getFiber(node);
|
||||
if (fiber === null) {
|
||||
return null;
|
||||
}
|
||||
const owner = fiber._debugOwner;
|
||||
if (owner && owner.type) {
|
||||
const ownerName = owner.type.displayName || owner.type.name;
|
||||
return ownerName || null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
let lastFoundInternalKey = null;
|
||||
function getFiber(node) {
|
||||
if (
|
||||
lastFoundInternalKey !== null &&
|
||||
node.hasOwnProperty(lastFoundInternalKey)
|
||||
) {
|
||||
return (node: any)[lastFoundInternalKey];
|
||||
}
|
||||
let internalKey = Object.keys(node).find(
|
||||
key => key.indexOf('__reactInternalInstance') === 0,
|
||||
);
|
||||
if (internalKey) {
|
||||
lastFoundInternalKey = internalKey;
|
||||
return (node: any)[lastFoundInternalKey];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findTipPos(dims, bounds, tipSize) {
|
||||
const tipHeight = Math.max(tipSize.height, 20);
|
||||
const tipWidth = Math.max(tipSize.width, 60);
|
||||
|
||||
@@ -95,6 +95,7 @@ export default function setupHighlighter(
|
||||
openNativeElementsPanel: boolean,
|
||||
rendererID: number,
|
||||
scrollIntoView: boolean,
|
||||
...
|
||||
}) {
|
||||
const renderer = agent.rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
@@ -103,9 +104,9 @@ export default function setupHighlighter(
|
||||
|
||||
let nodes: ?Array<HTMLElement> = null;
|
||||
if (renderer !== null) {
|
||||
nodes = ((renderer.findNativeNodesForFiberID(id): any): ?Array<
|
||||
HTMLElement,
|
||||
>);
|
||||
nodes = ((renderer.findNativeNodesForFiberID(
|
||||
id,
|
||||
): any): ?Array<HTMLElement>);
|
||||
}
|
||||
|
||||
if (nodes != null && nodes[0] != null) {
|
||||
|
||||
@@ -14,6 +14,7 @@ export type Rect = {
|
||||
right: number,
|
||||
top: number,
|
||||
width: number,
|
||||
...
|
||||
};
|
||||
|
||||
// Get the window object for the document that a node belongs to,
|
||||
|
||||
19
packages/react-devtools-shared/src/bridge.js
vendored
19
packages/react-devtools-shared/src/bridge.js
vendored
@@ -51,11 +51,27 @@ type OverrideSuspense = {|
|
||||
forceFallback: boolean,
|
||||
|};
|
||||
|
||||
type CopyElementPathParams = {|
|
||||
...ElementAndRendererID,
|
||||
path: Array<string | number>,
|
||||
|};
|
||||
|
||||
type ViewAttributeSourceParams = {|
|
||||
...ElementAndRendererID,
|
||||
path: Array<string | number>,
|
||||
|};
|
||||
|
||||
type InspectElementParams = {|
|
||||
...ElementAndRendererID,
|
||||
path?: Array<string | number>,
|
||||
|};
|
||||
|
||||
type StoreAsGlobalParams = {|
|
||||
...ElementAndRendererID,
|
||||
count: number,
|
||||
path: Array<string | number>,
|
||||
|};
|
||||
|
||||
type NativeStyleEditor_RenameAttributeParams = {|
|
||||
...ElementAndRendererID,
|
||||
oldName: string,
|
||||
@@ -95,6 +111,7 @@ type BackendEvents = {|
|
||||
|
||||
type FrontendEvents = {|
|
||||
clearNativeElementHighlight: [],
|
||||
copyElementPath: [CopyElementPathParams],
|
||||
getOwnersList: [ElementAndRendererID],
|
||||
getProfilingData: [{|rendererID: RendererID|}],
|
||||
getProfilingStatus: [],
|
||||
@@ -115,8 +132,10 @@ type FrontendEvents = {|
|
||||
startProfiling: [boolean],
|
||||
stopInspectingNative: [boolean],
|
||||
stopProfiling: [],
|
||||
storeAsGlobal: [StoreAsGlobalParams],
|
||||
updateAppendComponentStack: [boolean],
|
||||
updateComponentFilters: [Array<ComponentFilter>],
|
||||
viewAttributeSource: [ViewAttributeSourceParams],
|
||||
viewElementSource: [ElementAndRendererID],
|
||||
|
||||
// React Native style editor plug-in.
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
.ContextMenu {
|
||||
position: absolute;
|
||||
background-color: var(--color-context-background);
|
||||
border-radius: 0.25rem;
|
||||
overflow: hidden;
|
||||
z-index: 10000002;
|
||||
}
|
||||
131
packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenu.js
vendored
Normal file
131
packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenu.js
vendored
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* 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 React, {
|
||||
useContext,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {createPortal} from 'react-dom';
|
||||
import {RegistryContext} from './Contexts';
|
||||
|
||||
import styles from './ContextMenu.css';
|
||||
|
||||
function respositionToFit(element: HTMLElement, pageX: number, pageY: number) {
|
||||
const ownerWindow = element.ownerDocument.defaultView;
|
||||
if (element !== null) {
|
||||
if (pageY + element.offsetHeight >= ownerWindow.innerHeight) {
|
||||
if (pageY - element.offsetHeight > 0) {
|
||||
element.style.top = `${pageY - element.offsetHeight}px`;
|
||||
} else {
|
||||
element.style.top = '0px';
|
||||
}
|
||||
} else {
|
||||
element.style.top = `${pageY}px`;
|
||||
}
|
||||
|
||||
if (pageX + element.offsetWidth >= ownerWindow.innerWidth) {
|
||||
if (pageX - element.offsetWidth > 0) {
|
||||
element.style.left = `${pageX - element.offsetWidth}px`;
|
||||
} else {
|
||||
element.style.left = '0px';
|
||||
}
|
||||
} else {
|
||||
element.style.left = `${pageX}px`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const HIDDEN_STATE = {
|
||||
data: null,
|
||||
isVisible: false,
|
||||
pageX: 0,
|
||||
pageY: 0,
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
children: (data: Object) => React$Node,
|
||||
id: string,
|
||||
|};
|
||||
|
||||
export default function ContextMenu({children, id}: Props) {
|
||||
const {registerMenu} = useContext(RegistryContext);
|
||||
|
||||
const [state, setState] = useState(HIDDEN_STATE);
|
||||
|
||||
const bodyAccessorRef = useRef(null);
|
||||
const containerRef = useRef(null);
|
||||
const menuRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const ownerDocument = bodyAccessorRef.current.ownerDocument;
|
||||
containerRef.current = ownerDocument.createElement('div');
|
||||
ownerDocument.body.appendChild(containerRef.current);
|
||||
return () => {
|
||||
ownerDocument.body.removeChild(containerRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const showMenu = ({data, pageX, pageY}) => {
|
||||
setState({data, isVisible: true, pageX, pageY});
|
||||
};
|
||||
const hideMenu = () => setState(HIDDEN_STATE);
|
||||
return registerMenu(id, showMenu, hideMenu);
|
||||
}, [id]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!state.isVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const menu = menuRef.current;
|
||||
|
||||
const hideUnlessContains = event => {
|
||||
if (!menu.contains(event.target)) {
|
||||
setState(HIDDEN_STATE);
|
||||
}
|
||||
};
|
||||
|
||||
const hide = event => {
|
||||
setState(HIDDEN_STATE);
|
||||
};
|
||||
|
||||
const ownerDocument = containerRef.current.ownerDocument;
|
||||
ownerDocument.addEventListener('mousedown', hideUnlessContains);
|
||||
ownerDocument.addEventListener('touchstart', hideUnlessContains);
|
||||
ownerDocument.addEventListener('keydown', hideUnlessContains);
|
||||
|
||||
const ownerWindow = ownerDocument.defaultView;
|
||||
ownerWindow.addEventListener('resize', hide);
|
||||
|
||||
respositionToFit(menu, state.pageX, state.pageY);
|
||||
|
||||
return () => {
|
||||
ownerDocument.removeEventListener('mousedown', hideUnlessContains);
|
||||
ownerDocument.removeEventListener('touchstart', hideUnlessContains);
|
||||
ownerDocument.removeEventListener('keydown', hideUnlessContains);
|
||||
|
||||
ownerWindow.removeEventListener('resize', hide);
|
||||
};
|
||||
}, [state]);
|
||||
|
||||
if (!state.isVisible) {
|
||||
return <div ref={bodyAccessorRef} />;
|
||||
} else {
|
||||
return createPortal(
|
||||
<div ref={menuRef} className={styles.ContextMenu}>
|
||||
{children(state.data)}
|
||||
</div>,
|
||||
containerRef.current,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
.ContextMenuItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--color-context-text);
|
||||
padding: 0.5rem 0.75rem;
|
||||
cursor: default;
|
||||
border-top: 1px solid var(--color-context-border);
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-sans-normal);
|
||||
}
|
||||
.ContextMenuItem:first-of-type {
|
||||
border-top: none;
|
||||
}
|
||||
.ContextMenuItem:hover,
|
||||
.ContextMenuItem:focus {
|
||||
outline: 0;
|
||||
background-color: var(--color-context-background-hover);
|
||||
}
|
||||
.ContextMenuItem:active {
|
||||
background-color: var(--color-context-background-selected);
|
||||
color: var(--color-context-text-selected);
|
||||
}
|
||||
37
packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenuItem.js
vendored
Normal file
37
packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenuItem.js
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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 React, {useContext} from 'react';
|
||||
import {RegistryContext} from './Contexts';
|
||||
|
||||
import styles from './ContextMenuItem.css';
|
||||
|
||||
type Props = {|
|
||||
children: React$Node,
|
||||
onClick: () => void,
|
||||
title: string,
|
||||
|};
|
||||
|
||||
export default function ContextMenuItem({children, onClick, title}: Props) {
|
||||
const {hideMenu} = useContext(RegistryContext);
|
||||
|
||||
const handleClick = event => {
|
||||
onClick();
|
||||
hideMenu();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.ContextMenuItem}
|
||||
onClick={handleClick}
|
||||
onTouchEnd={handleClick}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
62
packages/react-devtools-shared/src/devtools/ContextMenu/Contexts.js
vendored
Normal file
62
packages/react-devtools-shared/src/devtools/ContextMenu/Contexts.js
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 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 {createContext} from 'react';
|
||||
|
||||
export type ShowFn = ({|data: Object, pageX: number, pageY: number|}) => void;
|
||||
export type HideFn = () => void;
|
||||
|
||||
const idToShowFnMap = new Map<string, ShowFn>();
|
||||
const idToHideFnMap = new Map<string, HideFn>();
|
||||
|
||||
let currentHideFn = null;
|
||||
|
||||
function hideMenu() {
|
||||
if (typeof currentHideFn === 'function') {
|
||||
currentHideFn();
|
||||
}
|
||||
}
|
||||
|
||||
function showMenu({
|
||||
data,
|
||||
id,
|
||||
pageX,
|
||||
pageY,
|
||||
}: {|
|
||||
data: Object,
|
||||
id: string,
|
||||
pageX: number,
|
||||
pageY: number,
|
||||
|}) {
|
||||
const showFn = idToShowFnMap.get(id);
|
||||
if (typeof showFn === 'function') {
|
||||
currentHideFn = idToHideFnMap.get(id);
|
||||
showFn({data, pageX, pageY});
|
||||
}
|
||||
}
|
||||
|
||||
function registerMenu(id: string, showFn: ShowFn, hideFn: HideFn) {
|
||||
if (idToShowFnMap.has(id)) {
|
||||
throw Error(`Context menu with id "${id}" already registered.`);
|
||||
}
|
||||
|
||||
idToShowFnMap.set(id, showFn);
|
||||
idToHideFnMap.set(id, hideFn);
|
||||
|
||||
return function unregisterMenu() {
|
||||
idToShowFnMap.delete(id);
|
||||
idToHideFnMap.delete(id);
|
||||
};
|
||||
}
|
||||
|
||||
export const RegistryContext = createContext({
|
||||
hideMenu,
|
||||
showMenu,
|
||||
registerMenu,
|
||||
});
|
||||
46
packages/react-devtools-shared/src/devtools/ContextMenu/useContextMenu.js
vendored
Normal file
46
packages/react-devtools-shared/src/devtools/ContextMenu/useContextMenu.js
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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 {useContext, useEffect} from 'react';
|
||||
import {RegistryContext} from './Contexts';
|
||||
|
||||
import type {ElementRef} from 'react';
|
||||
|
||||
export default function useContextMenu({
|
||||
data,
|
||||
id,
|
||||
ref,
|
||||
}: {|
|
||||
data: Object,
|
||||
id: string,
|
||||
ref: ElementRef<HTMLElement>,
|
||||
|}) {
|
||||
const {showMenu} = useContext(RegistryContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current !== null) {
|
||||
const handleContextMenu = event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const pageX = event.pageX || (event.touches && event.touches[0].pageX);
|
||||
const pageY = event.pageY || (event.touches && event.touches[0].pageY);
|
||||
|
||||
showMenu({data, id, pageX, pageY});
|
||||
};
|
||||
|
||||
const trigger = ref.current;
|
||||
trigger.addEventListener('contextmenu', handleContextMenu);
|
||||
|
||||
return () => {
|
||||
trigger.removeEventListener('contextmenu', handleContextMenu);
|
||||
};
|
||||
}
|
||||
}, [data, id, showMenu]);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user