diff --git a/agent/Agent.js b/agent/Agent.js index 640fa38e4e..19762756c2 100644 --- a/agent/Agent.js +++ b/agent/Agent.js @@ -80,6 +80,7 @@ import type Bridge from './Bridge'; * - mount * - update * - unmount + * - sendGetterValue */ class Agent extends EventEmitter { // the window or global -> used to "make a value available in the console" @@ -153,6 +154,7 @@ class Agent extends EventEmitter { bridge.on('setState', this._setState.bind(this)); bridge.on('setProps', this._setProps.bind(this)); bridge.on('setContext', this._setContext.bind(this)); + bridge.on('evalGetter', this._evalGetter.bind(this)); bridge.on('makeGlobal', this._makeGlobal.bind(this)); bridge.on('highlight', id => this.highlight(id)); bridge.on('highlightMany', id => this.highlightMany(id)); @@ -216,6 +218,7 @@ class Agent extends EventEmitter { bridge.forget(id); }); this.on('setSelection', data => bridge.send('select', data)); + this.on('sendGetterValue', (data) => bridge.send('evalgetterresult', data)); this.on('setInspectEnabled', data => bridge.send('setInspectEnabled', data)); } @@ -338,6 +341,15 @@ class Agent extends EventEmitter { } } + _evalGetter({id, path}: {id: ElementID, path: Array}) { + var data = this.elementData.get(id); + if (!data) { + return; + } + var value = path.reduce((obj_, attr) => obj_ ? obj_[attr] : null, data); + this.emit('sendGetterValue', {id, path, value}); + } + _makeGlobal({id, path}: {id: ElementID, path: Array}) { var data = this.elementData.get(id); if (!data) { @@ -449,7 +461,7 @@ class Agent extends EventEmitter { if (!id) { return; } - + this.highlight(id); } } diff --git a/agent/__tests__/dehydrate-test.js b/agent/__tests__/dehydrate-test.js index c889aa4d3d..ad206cf9a5 100644 --- a/agent/__tests__/dehydrate-test.js +++ b/agent/__tests__/dehydrate-test.js @@ -63,4 +63,18 @@ describe('dehydrate', () => { var result = dehydrate(object, cleaned); expect(result.a).toEqual({type: 'date', name: d.toString(), meta: {uninspectable: true}}); }); + + it('cleans getters', () => { + var grand = { + get getter() { + return 'a'; + }, + }; + + var parent = Object.create(grand); + var object = Object.create(parent); + var cleaned = []; + var result = dehydrate(object, cleaned); + expect(result.getter).toEqual( { name: 'getter', type: 'getter' }); + }); }); diff --git a/agent/dehydrate.js b/agent/dehydrate.js index 764d63fba0..a7ea21ee57 100644 --- a/agent/dehydrate.js +++ b/agent/dehydrate.js @@ -155,7 +155,15 @@ function dehydrate(data: Object, cleaned: Array>, path?: Array>, path?: Array, path: Array, val return copyWithSetImpl(obj, path, 0, value); } +function assignWithDescriptors(source) { + /* eslint-disable no-proto */ + var target = Object.create(source.__proto__); + /* eslint-enable no-proto */ + + Object.defineProperties(target, Object.keys(source).reduce((descriptors, key) => { + var descriptor = Object.getOwnPropertyDescriptor(source, key); + if (descriptor.hasOwnProperty('writable')) { + descriptor.writable = true; + } + descriptors[key] = descriptor; + return descriptors; + }, {})); + return target; +} + module.exports = copyWithSet; diff --git a/frontend/DataView/DataView.js b/frontend/DataView/DataView.js index ab0c7045fc..385fb1058e 100644 --- a/frontend/DataView/DataView.js +++ b/frontend/DataView/DataView.js @@ -15,6 +15,7 @@ import type {Theme, DOMEvent} from '../types'; var {sansSerif} = require('../Themes/Fonts'); var React = require('react'); var Simple = require('./Simple'); +var Getter = require('./Getter'); var consts = require('../../agent/consts'); var previewComplex = require('./previewComplex'); @@ -142,6 +143,7 @@ DataView.contextTypes = { class DataItem extends React.Component { context: { onChange: (path: Array, checked: boolean) => void, + isPropertyFrozen: (path: Array) => boolean, theme: Theme, }; props: { @@ -174,6 +176,14 @@ class DataItem extends React.Component { } } + shouldComponentUpdate(nextProps) { + if (nextProps.value[consts.type] === 'getter') { + return !this.context.isPropertyFrozen(nextProps.path); + } else { + return true; + } + } + inspect() { this.setState({loading: true, open: true}); this.props.inspect(this.props.path, () => { @@ -214,6 +224,9 @@ class DataItem extends React.Component { /> ); complex = false; + } else if (data[consts.type] === 'getter') { + preview = ; + complex = false; } else { preview = previewComplex(data, theme); } @@ -294,6 +307,7 @@ class DataItem extends React.Component { DataItem.contextTypes = { onChange: React.PropTypes.func, + isPropertyFrozen: React.PropTypes.func, theme: React.PropTypes.object.isRequired, }; diff --git a/frontend/DataView/Getter.js b/frontend/DataView/Getter.js new file mode 100644 index 0000000000..9ee6259634 --- /dev/null +++ b/frontend/DataView/Getter.js @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + */ +'use strict'; +var React = require('react'); + +class Getter extends React.Component { + handleClick() { + this.context.onEvalGetter(this.props.path); + } + + constructor(props: Object) { + super(props); + } + + render() { + return
(…)
; + } +} + +const style = { + 'cursor': 'pointer', +}; + +Getter.propTypes = { + data: React.PropTypes.any, + path: React.PropTypes.array, +}; + +Getter.contextTypes = { + onEvalGetter: React.PropTypes.func, +}; + +module.exports = Getter; diff --git a/frontend/PropState.js b/frontend/PropState.js index 370398333d..7fdb7c8791 100644 --- a/frontend/PropState.js +++ b/frontend/PropState.js @@ -34,6 +34,12 @@ class PropState extends React.Component { onChange: (path, val) => { this.props.onChange(path, val); }, + onEvalGetter: (path) => { + this.props.onEvalGetter(path); + }, + isPropertyFrozen: (path) => { + return this.props.isPathFrozen(path); + }, }; } @@ -176,6 +182,9 @@ PropState.contextTypes = { PropState.childContextTypes = { onChange: React.PropTypes.func, + onEvalGetter: React.PropTypes.func, + isPropertyFrozen: React.PropTypes.func, + }; var WrappedPropState = decorate({ @@ -189,6 +198,9 @@ var WrappedPropState = decorate({ id: store.selected, node, canEditTextContent: store.capabilities.editTextContent, + isPathFrozen(path) { + return store.isPathFrozen(path); + }, onChangeText(text) { store.changeTextContent(store.selected, text); }, @@ -206,6 +218,9 @@ var WrappedPropState = decorate({ showMenu(e, val, path, name) { store.showContextMenu('attr', e, store.selected, node, val, path, name); }, + onEvalGetter(path) { + store.evalGetter(store.selected, path); + }, inspect: store.inspect.bind(store, store.selected), }; }, diff --git a/frontend/PropVal.js b/frontend/PropVal.js index 8abce4d154..5bd7133e3e 100644 --- a/frontend/PropVal.js +++ b/frontend/PropVal.js @@ -122,23 +122,29 @@ function previewProp(val: any, nested: boolean, inverted: boolean, theme: Theme) return {`${val[consts.name]}[${val[consts.meta].length}]`}; } case 'iterator': { - style = { - color: inverted ? getInvertedWeak(theme.state02) : theme.base05, + style = { + color: inverted ? getInvertedWeak(theme.state02) : theme.base05, }; return {val[consts.name] + '(…)'}; } case 'symbol': { - style = { - color: inverted ? getInvertedWeak(theme.state02) : theme.base05, + style = { + color: inverted ? getInvertedWeak(theme.state02) : theme.base05, }; // the name is "Symbol(something)" return {val[consts.name]}; } + case 'getter': { + style = { + color: inverted ? getInvertedWeak(theme.state02) : theme.base05, + }; + return (…); + } } if (nested) { - style = { - color: inverted ? getInvertedWeak(theme.state02) : theme.base05, + style = { + color: inverted ? getInvertedWeak(theme.state02) : theme.base05, }; return {'{…}'}; } diff --git a/frontend/Store.js b/frontend/Store.js index b09512ce73..59121f3d45 100644 --- a/frontend/Store.js +++ b/frontend/Store.js @@ -66,6 +66,7 @@ type ContextMenu = { * - toggleCollapse * - toggleAllChildrenNodes * - setProps/State/Context + * - evalGetter * - makeGlobal(id, path) * - setHover(id, isHovered, isBottomTag) * - selectTop(id) @@ -85,6 +86,7 @@ class Store extends EventEmitter { _nodes: Map; _parents: Map; _nodesByName: Map; + _frozenPaths: Map; _eventQueue: Array; _eventTimer: ?number; @@ -118,6 +120,7 @@ class Store extends EventEmitter { this._nodes = new Map(); this._parents = new Map(); this._nodesByName = new Map(); + this._frozenPaths = new Map(); this._bridge = bridge; // Public state @@ -159,6 +162,7 @@ class Store extends EventEmitter { this._bridge.on('mount', (data) => this._mountComponent(data)); this._bridge.on('update', (data) => this._updateComponent(data)); this._bridge.on('unmount', id => this._unmountComponent(id)); + this._bridge.on('evalgetterresult', (data) => this._setGetterValue(data)); this._bridge.on('setInspectEnabled', (data) => this.setInspectEnabled(data)); this._bridge.on('select', ({id, quiet, offsetFromLeaf = 0}) => { // Backtrack if we want to skip leaf nodes @@ -397,17 +401,24 @@ class Store extends EventEmitter { } setProps(id: ElementID, path: Array, value: any) { + this._frozenPaths = this._frozenPaths.deleteIn([id, 'props', ...path.slice(0, path.length-1)]); this._bridge.send('setProps', {id, path, value}); } setState(id: ElementID, path: Array, value: any) { + this._frozenPaths = this._frozenPaths.deleteIn([id, 'state', ...path.slice(0, path.length-1)]); this._bridge.send('setState', {id, path, value}); } setContext(id: ElementID, path: Array, value: any) { + this._frozenPaths = this._frozenPaths.deleteIn([id, 'context', ...path.slice(0, path.length-1)]); this._bridge.send('setContext', {id, path, value}); } + evalGetter(id: ElementID, path: Array) { + this._bridge.send('evalGetter', {id, path}); + } + makeGlobal(id: ElementID, path: Array) { this._bridge.send('makeGlobal', {id, path}); } @@ -463,6 +474,7 @@ class Store extends EventEmitter { select(id: ?ElementID, noHighlight?: boolean, keepBreadcrumb?: boolean) { var oldSel = this.selected; this.selected = id; + this._frozenPaths.clear(); if (oldSel) { this.emit(oldSel); } @@ -529,6 +541,7 @@ class Store extends EventEmitter { inspect(id: ElementID, path: Array, cb: () => void) { invariant(path[0] === 'props' || path[0] === 'state' || path[0] === 'context', 'Inspected path must be one of props, state, or context'); + this._bridge.inspect(id, path, value => { var base = this.get(id).get(path[0]); var inspected = path.slice(1).reduce((obj, attr) => obj ? obj[attr] : null, base); @@ -540,6 +553,10 @@ class Store extends EventEmitter { }); } + isPathFrozen(path: Array) { + return this._frozenPaths.getIn( [this.selected, ...path]) === true; + } + changeTraceUpdates(state: ControlState) { this.traceupdatesState = state; this.emit('traceupdatesstatechange'); @@ -640,6 +657,7 @@ class Store extends EventEmitter { return; } data.renders = node.get('renders') + 1; + this._nodes = this._nodes.mergeIn([data.id], Map(data)); if (data.children && data.children.forEach) { data.children.forEach(cid => { @@ -697,6 +715,18 @@ class Store extends EventEmitter { this._nodesByName = this._nodesByName.set(node.get('name'), this._nodesByName.get(node.get('name')).delete(id)); } } + + _setGetterValue(data: DataType) { + var node = this._nodes.get(data.id); + var obj = node.get(data.path[0]); + var last = data.path.length - 1; + var propObj = data.path.slice(1, last).reduce((obj_, attr) => obj_ ? obj_[attr] : null, obj); + propObj[data.path[last]] = data.value; + var newProps = assign({}, obj); + this._nodes = this._nodes.setIn([data.id, data.path[0]], newProps); + this._frozenPaths = this._frozenPaths.setIn([data.id, ...data.path], true ); + this.emit(data.id); + } } module.exports = Store; diff --git a/test/example/target.js b/test/example/target.js index eba3d26b69..66c19d0632 100644 --- a/test/example/target.js +++ b/test/example/target.js @@ -398,6 +398,27 @@ for (var mCount = 200; mCount--;) { } +var protoWithGetter = { + get upperName() { + return this.name.toUpperCase(); + }, +}; + +var getterOnProtoProp = Object.create(protoWithGetter); +getterOnProtoProp.name = 'Foo'; + +let setterTestProp = { + _value: 'hello', + _editsCounter: 0, + get lol() { + return this._value.toUpperCase(); + }, + set lol(val) { + this._editsCounter ++; + this._value = val; + }, +}; + class Wrap extends React.Component { render() { return ( @@ -421,6 +442,7 @@ class Wrap extends React.Component { + ); }