Compare commits

..

113 Commits

Author SHA1 Message Date
Eli White
0e78755102 Prettier 2020-01-17 16:21:28 -08:00
Eli White
598739c2f4 [Native] Migrate focus/blur to call TextInputState with the host component 2020-01-17 16:04:28 -08:00
Andrew Clark
95bd7aad7d Remove renderPhaseUpdates Map (#17625)
* Remove renderPhaseUpdates Map

Follow up to #17484, which was reverted due to a bug found in www.

* Failing test: Dropped updates

When resetting render phase updates after a throw, we should only clear
the pending queue of hooks that were already processed.

* Fix non-render-phase updates being dropped

Detects if a queue has been processed by whether the hook was cloned.
If we change the implementation to an array instead of a list, we'll
need some other mechanism to determine whether the hook was processed.

* Regression test: startTransition in render phase

useTransition uses the state hook as part of its implementation, so we
need to fork it in the dispatcher used for re-renders, too.
2020-01-17 16:00:35 -05:00
Bogdan
29b4d0721c Allow debug-test commands to run on Windows (#17122)
Fix #17121
2020-01-17 10:20:09 -08:00
Dominic Gannaway
b43b36dd91 Remove ReactInstanceMap_DO_NOT_USE shim (#17861) 2020-01-17 12:37:55 +00:00
Dominic Gannaway
9e075d16b2 [react-interactions] Remove deprecated Scope APIs + update Focus components/docs (#17859) 2020-01-17 12:16:39 +00:00
Brian Vaughn
bc1f3e1a69 Fix DevTools Flow error for key-sort function (#17849) 2020-01-15 09:45:16 -08:00
Dominic Gannaway
b6173e643a [react-interactions] Add DO_NOT_USE to Scope methods (#17835) 2020-01-15 09:50:09 +00:00
Brian Vaughn
8aefb1995c Removed 'reactInternal' reference in DevTools overlay highlighter (#17841) 2020-01-14 15:38:09 -08:00
Jason Williams
643dcb5526 Add support for Node v13 for development (#17837) 2020-01-14 23:20:42 +00:00
Brian Vaughn
1e1a989422 Re-enabled DevTools context menu option in Firefox to inspect function prop source (#17838) 2020-01-14 14:00:28 -08:00
Nicolas Gallagher
3bd6adceda Update typescript package and jest preprocessor (#17839) 2020-01-14 13:52:52 -08:00
Nicolas Gallagher
50eafef07e Update babel-eslint and eslint packages (#17829) 2020-01-14 09:26:44 -08:00
André
4f480d0645 Fix typo in TestCase description (#17077) 2020-01-12 11:54:12 -08:00
Dan
f4ca909e0e react-refresh@0.7.2 2020-01-12 17:54:30 +00:00
Dan Abramov
255d9ac5f5 [Fresh] Fix edge case with early function call (#17824) 2020-01-12 17:53:50 +00:00
Nicolas Gallagher
64aae7b06f Fix issue template for questions (#17817) 2020-01-10 10:57:34 -08:00
Emily
d8570ec6ea Create ReactFabric.stopSurface and use that for bridgeless mode binding (#16164) 2020-01-10 10:21:32 -08:00
Nicolas Gallagher
05e487a12f Add GitHub issue templates (#17815) 2020-01-09 16:16:37 -08:00
Ganapati V S
5d6129b5d1 Update ReactSuspenseList-test.internal.js (#17034)
Typo
2020-01-09 14:14:00 -08:00
Dawid Loranc
ff900095b9 Fix message typo in printOperationsArray function (#17039) 2020-01-09 14:13:03 -08:00
Alican Çubukçuoğlu
1b6e269751 Create SECURITY.md (#15784)
Adds SECURITY.md as mentioned in #15722.
2020-01-09 14:07:41 -08:00
Tao
3ac81a57fb Update create-react-app note for eslint-plugin-react-hooks (#16982)
eslint-plugin-react-hooks was added in react-scripts 3.0.0
2020-01-09 13:47:05 -08:00
Brian Vaughn
435259f189 Add better test coverage for hooks in DevTools Profiler's change-detection (#17816) 2020-01-09 13:15:19 -08:00
Nicolas Gallagher
2e4948a34d Add configuration for the "Stale" GitHub App (#17806)
Configuration for the bot that marks and closes stale issues and pull requests.
https://probot.github.io/apps/stale/
2020-01-09 11:13:41 -08:00
Dan Abramov
e706721490 Update Flow to 0.84 (#17805)
* Update Flow to 0.84

* Fix violations

* Use inexact object syntax in files from fbsource

* Fix warning extraction to use a modern parser

* Codemod inexact objects to new syntax

* Tighten types that can be exact

* Revert unintentional formatting changes from codemod
2020-01-09 14:50:44 +00:00
Dan Abramov
b979db4e72 Bump Prettier (#17811)
* Bump Prettier

* Reformat

* Use non-deprecated option
2020-01-09 13:54:11 +00:00
Sebastian Markbåge
18875b2401 Remove special casing of toString values when enableTrustedTypesIntegration (#17774)
* Remove toString of dangerouslySetInnerHTML

As far as I can tell, the toString call was added here:

caae627cd5 (diff-5574f655d491348f422bca600ff6711dR887)

It was never really needed. Subsequently when we added Trusted Types,
this needed to be changed to a special call but we really should just
always let it pass through.

* Remove special casing of toString values when enableTrustedTypesIntegration

As far as I can tell, we only toString in user space because of IE8/9.

We don't really support IE8/9 anymore and by the time this flag is on,
we should be able to deprecate it.

Unless this is also an issue in IE11. I haven't tested yet.
2020-01-08 14:40:07 -08:00
Eli White
19f6fe170c Revert "Revert "Dispatch commands to both UIManagers from both renderers (#17211)" (#17232)" (#17799)
* Revert "Revert "Dispatch commands to both UIManagers from both renderers (#17211)" (#17232)"

This reverts commit d0fc0ba0a6.

* Clean up another __DEV__ warning check
2020-01-07 16:47:19 -08:00
Brian Vaughn
6250462bed Renamed "ReactDOM-fb" imports to "ReactDOM" in www shims (#17797)
* Renamed "ReactDOM-fb" imports to "ReactDOM" in www shims

* Removed ReactDOMComponentTree_DO_NOT_USE.js
2020-01-07 13:05:36 -08:00
Dominic Gannaway
5e21157d86 [react-interactions] Fix Hover issues with portals (#17765) 2020-01-07 19:45:26 +00:00
Brian Vaughn
40568e9773 Updated DevTools CHANGELOG to add pending release details 2020-01-06 10:37:20 -08:00
Brian Vaughn
5586b3022c Merge branch 'GasimGasimzada-scope-devtools-styles' 2020-01-06 10:36:13 -08:00
Brian Vaughn
7580d6717c Fixed padding problem in standalone shell 2020-01-06 10:35:36 -08:00
Brian Vaughn
80e32487b9 Fixed box-sizing problem in test shell 2020-01-06 10:23:55 -08:00
Brian Vaughn
c1b573bf2f Merge branch 'scope-devtools-styles' of https://github.com/GasimGasimzada/react into GasimGasimzada-scope-devtools-styles 2020-01-06 10:13:35 -08:00
Brian Vaughn
fc921a1693 Updated DevTools CHANGELOG to add pending release details 2020-01-06 10:12:26 -08:00
Brian Vaughn
1c63b2e416 Renamed "backend.js" to "react_devtools_backend.js" (#17790) 2020-01-06 09:29:20 -08:00
Brian Vaughn
2bb227ef80 DevTools: Improve function props display for inspected elements (#17789) 2020-01-06 09:19:59 -08:00
Brian Vaughn
24f824250f DevTools Profiler: Improve how empty commits are filtered (#17771)
Previously, DevTools filtered empty commits on the backend, while profiling, through the use of a bailout heuristic that React currently happens to use. This approach was brittle and may have exacerbated the long-standing Profiler bug #16446.

This PR removes that heuristic and adds as a post-processing filtering pass instead. This removes the coupling between DevTools and a React internal implementation detail that may change.

I believe DevTools has two choices of criteria for this filtering:
* Filter commits that have no actual duration metadata.
* Filter commits that have no recorded operations (no mutations to the tree, no changed tree base durations).

I chose the first option, filtering by commits that have no reported metadata. It will miss an edge case, e.g. , but we would have nothing meaningful to show in the Profiler for those cases anyway. (This particular edge case is why one of the snapshots changed with this commit.)

The second option, filtering by recorded operations, could potentially miss a more important edge case: where a component *did* render, but its  didn't change. (In that event, there would be no operations to send.)
2020-01-04 16:50:39 -08:00
Sebastian Markbåge
edeea07207 Remove toString of dangerouslySetInnerHTML (#17773)
As far as I can tell, the toString call was added here:

caae627cd5 (diff-5574f655d491348f422bca600ff6711dR887)

It was never really needed. Subsequently when we added Trusted Types,
this needed to be changed to a special call but we really should just
always let it pass through.
2020-01-04 05:47:23 -08:00
Gasim Gasimzada
9cc094a19a Scope dev tools wildcard styles within DevTools CSS class 2020-01-04 14:04:15 +04:00
Brian Vaughn
f749045a5b DevTools 4.3.0 -> 4.4.0 2020-01-03 10:02:50 -08:00
Sebastian Markbåge
59f21f1b24 HostText needs to copy over from current if it is unchanged in persistent mode (#17538)
stateNode is not one of the fields that gets copied over by createWorkInProgress.

This is bad for persistent mode since it's not the same instance.

In HostComponent we happened to always end up transferring the current stateNode when we bail.
However, we didn't do this for HostText.
2020-01-03 17:35:17 +00:00
Brian Vaughn
7e2ab87a61 DevTools: Replaced unsafe hasOwnProperty() calls (#17768)
DevTools previously called  in several places with user-defined values. This could lead to runtime errors if those values had an overriden  attribute. This commit replaces those callse with  instead.

New test cases have been added.
2020-01-03 09:34:12 -08:00
Brian Vaughn
5d3d71b1dd Partial fix DevTools Profiler ''Could not find node…'' error (#17759)
The Profiler stores:

1. A snapshot of the React tree when profiling started
2. The operations array for each commit
3. Profiling metadata (e.g. durations, what changed, etc) for each commit

It uses this information (snapshot + operations diff) to reconstruct the state of the application for a given commit as it's viewed in the Profiler UI. Because of this, it's very important that the operations and metadata arrays align. If they don't align, the profiler will be unable to correctly reconstruct the tree, and it will likely throw errors (like 'Could not find node…')

#16446 tracks a long-standing bug where these two arrays get misaligned. I am still not entirely sure what causes this bug, but with PR #17253, I exacerbated things by introducing another potential way for it to happen. This PR addresses the regression at least (and adds test coverage for it).

I will follow up this afternoon on the original #16446 issue. I think I may have a lead on what's happening at least, if not exactly an idea of how to reproduce it.
2020-01-03 09:28:31 -08:00
Brian Vaughn
cca994c6a1 DevTools Profiler commit selector wraps around (#17760)
I used to disable the <- and -> buttons when you reached the beginning or end of the profiler data. This can be kind of annoying though when you just want to get to the last commit, and I don't think there's a good reason to enforce it anyway, so I backed that change out. The buttons now wrap around at the beginning or end of the list.
2020-01-02 14:44:07 -08:00
Brian Vaughn
b05cd611ba Fixed CircleCI DevTools build artifact tar internal structure (#17758) 2020-01-02 09:28:20 -08:00
Brian Vaughn
195b3db618 Added missing guard to DevTools for Objects with null proto (#17757)
This prevents a runtime error from occurring when these objects are inspected.
2020-01-02 08:27:29 -08:00
Brian Vaughn
2c1e5d2b22 Build and store DevTools artifacts in Circle CI (#17746) 2020-01-01 07:58:47 -08:00
Brian Vaughn
86883d7aac Updated DevTools pending CHANGELOG 2019-12-30 09:12:49 -08:00
dependabot[bot]
b4e2fac09d Bump handlebars from 4.0.11 to 4.5.3 in /fixtures/expiration (#17729)
Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.0.11 to 4.5.3.
- [Release notes](https://github.com/wycats/handlebars.js/releases)
- [Changelog](https://github.com/wycats/handlebars.js/blob/master/release-notes.md)
- [Commits](https://github.com/wycats/handlebars.js/compare/v4.0.11...v4.5.3)

Signed-off-by: dependabot[bot] <support@github.com>
2019-12-30 08:57:08 -08:00
dependabot[bot]
e6226e6493 Bump handlebars from 4.0.6 to 4.5.3 in /fixtures/dom (#17730)
Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.0.6 to 4.5.3.
- [Release notes](https://github.com/wycats/handlebars.js/releases)
- [Changelog](https://github.com/wycats/handlebars.js/blob/master/release-notes.md)
- [Commits](https://github.com/wycats/handlebars.js/compare/v4.0.6...v4.5.3)

Signed-off-by: dependabot[bot] <support@github.com>
2019-12-30 08:56:44 -08:00
dependabot[bot]
3bec713b85 Bump handlebars from 4.0.10 to 4.5.3 in /fixtures/attribute-behavior (#17728)
Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.0.10 to 4.5.3.
- [Release notes](https://github.com/wycats/handlebars.js/releases)
- [Changelog](https://github.com/wycats/handlebars.js/blob/master/release-notes.md)
- [Commits](https://github.com/wycats/handlebars.js/compare/v4.0.10...v4.5.3)

Signed-off-by: dependabot[bot] <support@github.com>
2019-12-30 08:56:11 -08:00
dependabot[bot]
ee38c00bd5 Bump handlebars from 4.0.6 to 4.5.3 in /fixtures/fiber-debugger (#17727)
Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.0.6 to 4.5.3.
- [Release notes](https://github.com/wycats/handlebars.js/releases)
- [Changelog](https://github.com/wycats/handlebars.js/blob/master/release-notes.md)
- [Commits](https://github.com/wycats/handlebars.js/compare/v4.0.6...v4.5.3)

Signed-off-by: dependabot[bot] <support@github.com>
2019-12-30 08:56:05 -08:00
dependabot[bot]
4d463fce4e Bump handlebars from 4.1.2 to 4.5.3 (#17732)
Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.1.2 to 4.5.3.
- [Release notes](https://github.com/wycats/handlebars.js/releases)
- [Changelog](https://github.com/wycats/handlebars.js/blob/master/release-notes.md)
- [Commits](https://github.com/wycats/handlebars.js/compare/v4.1.2...v4.5.3)

Signed-off-by: dependabot[bot] <support@github.com>
2019-12-30 08:46:41 -08:00
Karan Sharma
d259f45813 Fix various typos (#17724) 2019-12-29 13:47:51 -08:00
Chris Watson
9357a483e9 Fix for the standalone devtools not responding to clicks. (#17584) 2019-12-29 13:46:27 -08:00
Brian Vaughn
22ef96ae63 Devtools renable copy attr context menu for firefox (#17740)
* Use exportFunction() to share clipboard copy with JS running in document/page context.

* Remove no-longer-used option to disable copy operation.
2019-12-29 13:27:44 -08:00
Brian Vaughn
2b903da355 Make DevTools check document.contentType before injecting (#17739)
It should only inject the global hook into HTML documents. This will avoid breaking syntax highlighting for e.g. XML documents.
2019-12-29 13:02:50 -08:00
Brian Vaughn
0eac01abcd Added missing Flow type coverage to DevTools context menu (#17733)
The  param should probably be a generic type, but I'm not sure how to satisfy Flow with the current top-level  Map. At least this adds basic coverage (which was missing before, oops).
2019-12-29 08:44:24 -08:00
Brian Vaughn
f887d1aa27 [DevTools] Display RegExp values in props/state (#17690)
Previously, when props/state contained a regexp, it was shown as an
empty object. This commit adds regexps as values in need of special
rehydration (like Symbols or TypedArrays), and display them as a user
might expect.

Co-authored-by: Zirak <zirakertan@gmail.com>
2019-12-22 11:49:28 -08:00
Brian Vaughn
1959671d45 Add tooltip to interaction name in DevTools Profiler sidebar (#17689) 2019-12-22 08:52:53 -08:00
Nick S. Plekhanov
ccc6100d7f Fix comments typos (#17550)
* Fix comments typos

* Fix comments typos

* Fix more typos

Co-authored-by: Brian Vaughn <brian.david.vaughn@gmail.com>
2019-12-22 08:44:34 -08:00
Maxime Bouveron
6215e1c2db Add Shift+Enter to go to previous search result (#17005) 2019-12-22 08:38:14 -08:00
Dominic Gannaway
85d9655d6f [react-dom] Refactor event priority handling to its own module (#17678) 2019-12-20 23:15:36 +00:00
Brian Vaughn
3e09677836 DevTools v4.2.1 -> 4.3.0 2019-12-20 09:11:37 -08:00
Nicolas Gallagher
31350237fa Add unit tests to dom-event-testing-library (#17680)
Unit tests for the simple event mocks
2019-12-20 16:35:02 +00:00
Brian Vaughn
79734771cd Disable "copy to clipboard" and "go to definition" context menu options for Firefox addon (#17668) 2019-12-20 08:31:38 -08:00
Dominic Gannaway
dbc46ac63e [react-interactions] Rename test + fix master (#17679) 2019-12-20 16:00:39 +00:00
Brian Vaughn
1b9328cd9b Null stateNode after unmount (#17666) 2019-12-19 13:32:14 -08:00
Dan Abramov
897976600c [ESLint] Allow partial matches for custom Effect Hooks (#17663) 2019-12-19 17:04:17 +00:00
Nicolas Gallagher
72592310a8 Create packages/dom-event-testing-library (#17660)
Moves the unit testing library for events into the `packages` directory so it can more easily be used in tests for other react packages, and mirrored internally to help with testing of event hooks we prototype in www.
2019-12-19 16:51:25 +00:00
Dominic Gannaway
e7494c86c5 [react-interactions] Remove batchedUpdates from responder lifecycles (#17659) 2019-12-19 13:07:00 +00:00
Nicolas Gallagher
a5e951d4cc [react-interactions] Event testing library improvements (#17614)
Introduces a state machine around pointer events to produce more accurate mock
touch events. This allows multi-touch unit tests to be written entirely in
terms of mock pointer interactions, while producing the expected
'changedTouches', 'targetTouches', and 'touches' fields for mock touch events.
2019-12-18 23:32:48 +00:00
Brian Vaughn
95056b6836 DevTools build script enhancements (#17653)
* Updated DevTools local development instructions to mention experimental build step

* Added a command to download latest experimental release (for DevTools)

* Updated build instructions for clarity

* Added build-for-devtools package alias
2019-12-18 14:34:40 -08:00
Brian Vaughn
fd2a3ae57f Updated DevTools changelog 2019-12-18 12:51:18 -08:00
Brian Vaughn
933f6a07ca DevTools context menu (#17608)
* Added rudimentary context menu hook and menu UI

* Added backend support for copying a value at a specific path for the inspected element

* Added backend support for storing a value (at a specified path) as a global variable

* Added special casing to enable copying undefined/unserializable values to the clipboard

* Added copy and store-as-global context menu options to selected element props panel

* Store global variables separately, with auto-incremented name (like browsers do)

* Added tests for new copy and store-as-global backend functions

* Fixed some ownerDocument/contentWindow edge cases

* Refactored context menu to support dynamic options

Used this mechanism to add a conditional menu option for inspecting the current value (if it's a function)

* Renamed "safeSerialize" to "serializeToString" and added inline comment
2019-12-18 12:12:34 -08:00
Sebastian Markbåge
7dc9745427 [Flight] Chunks API (#17398)
* Add feature flags

* Add Chunk type and constructor

* Wire up Chunk support in the reconciler

* Update reconciler to reconcile Chunks against the render method

This allows the query and args to be updated.

* Drop the ref. Chunks cannot have refs anyway.

* Add Chunk checks in more missing cases

* Rename secondArg

* Add test and fix lazy chunks

Not really a supported use case but for consistency I guess.

* Fix fragment test
2019-12-18 18:25:43 +00:00
Eli White
9354dd2752 Make HostComponent inexact (#17412) 2019-12-18 18:21:42 +00:00
Sebastian Markbåge
4c270375e9 Favor fallthrough switch instead of case statements for work tags (#17648)
* Favor fallthrough switch instead of case statements for work tags

Currently we're inconsistently handling tags that are only relevant
for certain flags. We should throw if the tag is not part of the built
feature flags. This should also mean that the case statements can be
eliminated.

We can achieve this effect by putting the invariant outside of the switch
and always early return in the switch. We already do this in beginWork.
This PR makes this consistent in other places.

* Fail if fundamental/scope tags are discovered without the flag on
2019-12-18 15:53:27 +00:00
Dan Abramov
6fef7c47a9 Add a regression test for switching from Fragment to a component (#17647)
* Add a regression test for switching from Fragment to a component

* Add a few more tests
2019-12-18 15:21:42 +00:00
Dominic Gannaway
9fe1031244 [react-interactions] Rename Flare APIs to deprecated and remove from RN (#17644) 2019-12-18 10:24:46 +00:00
Brian Vaughn
e84327ba0c Added more forwardRef test cases for DevTools (#17642) 2019-12-17 12:45:10 -08:00
Dan Abramov
4b0cdf29a8 Build FB RN targets only in experimental mode (#17641) 2019-12-17 20:22:49 +00:00
zthxxx
24dbe851e8 fix(dev-tools): fix show correct displayName with forwardRef in Dev Tools (#17613)
* fix(dev-tools): fix show correct displayName with forwardRef in Dev Tools

allow set `displayName` after `React.forwardRef()`,
makesure Dev Tools show displayName as same as `getWrappedName` in `shared/getComponentName.js`

* Removed a little unnecessary/redundant code.

* Fixed lint error (removed unused var)
2019-12-17 11:29:16 -08:00
Brian Vaughn
bb75a4fa89 Improved selection and toggling for props/hooks/state (#17588)
1. Enable nested values to be expanded/collapsed by clicking on values as well as keys.
2. Enable keys and values to be selectable (for copy-pasting purposes)
2019-12-17 11:28:37 -08:00
伊撒尔
7309c5f934 Use zero-fill right shift instead of Math.floor (#17616)
For positive numbers, binary displacement is better.
2019-12-17 11:16:04 -08:00
Dan Abramov
9bb3fbe893 Move DevTools CI job to experimental workflow (#17640) 2019-12-17 18:21:38 +00:00
Dan Abramov
3c54df0914 Fix missing stacks in WWW warnings (#17638) 2019-12-17 15:21:18 +00:00
Dan Abramov
b66e86d957 react-refresh@0.7.1 2019-12-17 13:50:57 +00:00
Dan Abramov
c2d1561c60 [Fast Refresh] Support injecting runtime after renderer executes (#17633) 2019-12-17 13:49:39 +00:00
Dan Abramov
0253ee9a2e Additional test infra changes for toErrorDev rename (#17632) 2019-12-17 13:31:47 +00:00
Dan Abramov
36a6e29bb3 Fix test_build_devtools CI job to run test-build-devtools (#17631)
* Skip abandoned project folders in Jest config

This fixes a problem that occurs after renaming a package.

* Fix test_build_devtools to run test-build-devtools

* Exclude console.error plugin for DevTools packages

* Use correct release channel for DevTools tests

This should fix the createRoot error.

* Fix TZ dependent test

* Change DT job dependencies
2019-12-16 16:03:12 -08:00
Dan Abramov
7c21bf72ac Fix release script to ignore empty package folders 2019-12-16 19:16:58 +00:00
Dan Abramov
f42431abe1 Revert "Remove renderPhaseUpdates Map (#17484)" (#17623)
This reverts commit 8a347ed024.
2019-12-16 19:03:00 +00:00
Dan Abramov
0b5a26a489 Rename toWarnDev -> toErrorDev, toLowPriorityWarnDev -> toWarnDev (#17605)
* Rename toWarnDev -> toErrorDev in tests

* Rename toWarnDev matcher implementation to toErrorDev

* Rename toLowPriorityWarnDev -> toWarnDev in tests and implementation
2019-12-16 12:48:16 +00:00
Dan Abramov
0cf22a56a1 Use console directly instead of warning() modules (#17599)
* Replace all warning/lowPriWarning with console calls

* Replace console.warn/error with a custom wrapper at build time

* Fail the build for console.error/warn() where we can't read the stack
2019-12-14 18:09:25 +00:00
Dan Abramov
b6c423daad Use matching test command for equivalence tests (#17604) 2019-12-14 17:52:29 +00:00
Sebastian Markbåge
8a347ed024 Remove renderPhaseUpdates Map (#17484)
* Render phase updates can now be extracted from the pending queue

* Use a custom dispatcher for the second render pass

* Discard render phase updates if component throws

When aborting a render, we also need to throw out render phase updates.
Remove the updates from the queues so they do not persist to the next
render. We already did a single pass through the whole list of hooks, so
we know that any pending updates must have been dispatched during the
render phase. The ones that were dispatched before we started rendering
were already transferred to the current hook's queue.
2019-12-13 16:45:09 -08:00
Dominic Gannaway
be603f5a51 [react-events] Remove lastNativeEvent in favor of SystemFlags (#17585) 2019-12-13 17:03:35 +00:00
Dan Abramov
b15bf36750 Add component stacks to (almost) all warnings (#17586) 2019-12-12 23:47:55 +00:00
Nicolas Gallagher
612a76812f [react-interactions] Mock touchend events should use empty array for 'touches' (#17589)
The 'touches' value should be an empty array rather than 'null'
2019-12-12 22:21:30 +00:00
Brian Vaughn
12c000412d Improved inspected element props with inline previews (#17579)
* Improved inspected element props with inline previews

This mimics the inline preview shown by the brower console and dramatically improves the UX when inspecting deep values. I also updated tests to add more coverage for this new functionality.

* Cleaned up the DataView vs typed array check

* Added early bailouts to DevTools when generating preview strings for iterables/objects/arrays, to avoid doing unnecessary work
2019-12-11 17:52:17 -08:00
Dominic Gannaway
2afeebdcc4 [react-interactions] Remove responder root event types + revert commit phase change (#17577) 2019-12-11 16:48:46 +00:00
Laura buns
9ac42dd074 Remove the condition argument from warning() (#17568)
* prep for codemod

* prep warnings

* rename lint rules

* codemod for ifs

* shim www functions

* Handle more cases in the transform

* Thanks De Morgan

* Run the codemod

* Delete the transform

* Fix up confusing conditions manually

* Fix up www shims to match expected API

* Also check for low-pri warning in the lint rule
2019-12-11 03:28:14 +00:00
Andrew Clark
7bf40e1cfd Initialize update queue object on mount (#17560)
* Refactor Update Queues to Fix Rebasing Bug

Fixes a bug related to rebasing updates. Once an update has committed,
it should never un-commit, even if interrupted by a higher priority
update. The fix includes a refactor of how update queues work.

This commit is a combination of two PRs:

- #17483 by @sebmarkbage refactors the hook update queue
- #17510 by @acdlite refactors the class and root update queue

Landing one without the other would cause state updates to sometimes be
inconsistent across components, so I've combined them into a single
commit in case they need to be reverted.

Co-authored-by: Sebastian Markbåge <sema@fb.com>
Co-authored-by: Andrew Clark <git@andrewclark.io>

* Initialize update queue object on mount

Instead of lazily initializing update queue objects on the first update,
class and host root queues are created on mount. This simplifies the
logic for appending new updates and matches what we do for hooks.
2019-12-10 18:42:42 -06:00
Brian Vaughn
031a5aaffb Show component location for selected element in bottom/right of props panel (#17567)
* Show component location info for selected element in bottom/right of props panel

* Moved RegExp declaration into function basedon PR feedback
2019-12-10 09:24:34 -08:00
Andrew Clark
e039e690b5 Revert Update Queue Refactor
Reverts b617db3d96.

Found some bugs when attempting to land in www. Reverting to fix master.
I'll land again *after* the change successfully land downstream.
2019-12-09 15:16:09 -08:00
Andrew Clark
b617db3d96 Refactor Update Queues to Fix Rebasing Bug
Fixes a bug related to rebasing updates. Once an update has committed,
it should never un-commit, even if interrupted by a higher priority
update. The fix includes a refactor of how update queues work.

This commit is a combination of two PRs:

- #17483 by @sebmarkbage refactors the hook update queue
- #17510 by @acdlite refactors the class and root update queue

Landing one without the other would cause state updates to sometimes be
inconsistent across components, so I've combined them into a single
commit in case they need to be reverted.

Co-authored-by: Sebastian Markbåge <sema@fb.com>
Co-authored-by: Andrew Clark <git@andrewclark.io>
2019-12-09 13:19:26 -08:00
Dominic Gannaway
3c1efa0d77 [react-interactions] Remove Focus/FocusWithin root event types (#17555) 2019-12-09 14:27:45 +00:00
Nicolas Gallagher
9e937e7e86 [react-interactions] fix Press/Tap behavior for virtual middle clicks (#17554)
Tools like BetterTouchTool for macOS trigger middle-clicks with a 'buttons'
value that doesn't correspond to the middle-mouse button. To account for this
we also inspect the value of 'button'.

Close #17367
2019-12-09 13:04:24 +00:00
Laura buns
b43eec7eaa Replace wrap-warning-with-env-check with an eslint plugin (#17540)
* Replace Babel plugin with an ESLint plugin

* Fix ESLint rule violations

* Move shared conditions higher

* Test formatting nits

* Tweak ESLint rule

* Bugfix: inside else branch, 'if' tests are not satisfactory

* Use a stricter check for exactly if (__DEV__)

This makes it easier to see what's going on and matches dominant style in the codebase.

* Fix remaining files after stricter check
2019-12-06 18:25:54 +00:00
535 changed files with 13582 additions and 12012 deletions

View File

@@ -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:

View File

@@ -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: {

View File

@@ -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
View 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
View 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
View 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

View File

@@ -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
View 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!

View File

@@ -8,7 +8,7 @@ module.exports = {
jsxBracketSameLine: true,
trailingComma: 'es5',
printWidth: 80,
parser: 'babylon',
parser: 'babel',
overrides: [
{

7
SECURITY.md Normal file
View 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

View File

@@ -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) => {

View File

@@ -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"

View File

@@ -55,4 +55,4 @@ class IframeSubtree extends React.Component {
}
}
export default (ReactDOM.createPortal ? IframePortal : IframeSubtree);
export default ReactDOM.createPortal ? IframePortal : IframeSubtree;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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)
),

View File

@@ -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"

View File

@@ -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]);
}

View File

@@ -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"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"

View File

@@ -95,7 +95,7 @@ trace('initial_render', performance.now(), () => {
root.render(
createElement(TestApp),
wrap(() => {
log.app('commited');
log.app('committed');
})
);
});

View File

@@ -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",

View File

@@ -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,
});
});

View File

@@ -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> {

View File

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

View File

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

View File

@@ -0,0 +1,361 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
import {defaultBrowserChromeSize} from '../constants';
import {
createEventTarget,
describeWithPointerEvent,
testWithPointerType,
resetActivePointers,
} from '../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,
}
`);
});
});

View File

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

View File

@@ -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,
};

View File

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

View File

@@ -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,
});

View File

@@ -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,
};

View File

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

View File

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

View File

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

View File

@@ -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:

View File

@@ -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: `

View File

@@ -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?

View File

@@ -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>;
/**

View File

@@ -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`.');
}
};
}

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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,
...
};

View File

@@ -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(

View File

@@ -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();

View File

@@ -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<>;

View File

@@ -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,

View File

@@ -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.');
}
}
}
}

View File

@@ -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(). ' +

View File

@@ -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(

View File

@@ -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.');

View File

@@ -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 **)',

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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.',

View File

@@ -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>;

View File

@@ -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",

View File

@@ -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.

View File

@@ -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/

View File

@@ -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"
],

View File

@@ -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'
);
};

View File

@@ -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": [
{

View File

@@ -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'},
);
}

View File

@@ -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,
}),
);

View File

@@ -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 {

View File

@@ -24,7 +24,7 @@ module.exports = {
},
output: {
path: __dirname + '/build',
filename: '[name].js',
filename: 'react_devtools_backend.js',
},
resolve: {
alias: {

View File

@@ -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).

View File

@@ -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",

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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]

View File

@@ -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();
});
});

View File

@@ -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,

View File

@@ -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),
);
});
});

View File

@@ -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>;

View File

@@ -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);
});
});

View File

@@ -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$')];

View File

@@ -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,

View File

@@ -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...">

View File

@@ -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) {

View File

@@ -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);
},
),

View File

@@ -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,
};
}

View File

@@ -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) {

View File

@@ -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,
};
}

View File

@@ -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,
...
};

View File

@@ -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;
});
}

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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.

View File

@@ -0,0 +1,7 @@
.ContextMenu {
position: absolute;
background-color: var(--color-context-background);
border-radius: 0.25rem;
overflow: hidden;
z-index: 10000002;
}

View 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,
);
}
}

View File

@@ -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);
}

View 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>
);
}

View 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,
});

View 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