Skip to content
This repository has been archived by the owner on Jun 11, 2024. It is now read-only.

Add support for React Native #179

Closed
wants to merge 4 commits into from
Closed

Conversation

lelandrichardson
Copy link
Collaborator

to: @lencioni @trotzig

This PR is a work in progress, and so far only works for ios (but is almost ready for android). It's best viewed by commit, as it includes a lot of RN template code in the first 2 commits.

Ultimately, I think it makes sense for us to implement this as a plugin system or something, so this PR will likely never get merged, but I thought it would be a good way for us to start the discussion.

I haven't made much of an attempt to share code, though I did refactor some methods into modules so they could be shared across implementations.

How it works:

running happo run-ios does the following things:

  1. Starts a websockets server that the ios app will use to communicate between the two processes
  2. Starts appium, which then launches the simulator with the happo app
  3. The happo app starts, and requests an RN bundle from localhost:8081/happo.bundle which is assuming that the consumer of happo has the RN packager turned on (to default port of 8081) with a happo.js entry file at the root.
  4. The happo.js file needs to at some point require the happo/native js module, which will register itself with the happo server, and the happo app.
  5. Consumer of happo will register happo stories with import StoryManager from 'happo/native';
  6. At startup, happo will send the list of story names over websockets to happo, then render them one by one, taking a snapshot of each, and saving to a temp file. Temp files + story names will be sent across websockets to the happo server.
  7. Happo server diffs files appropriately and does its thing

Copy link
Contributor

@lencioni lencioni left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a great start!

render() {
const { story } = this.state;
return (
<View style={{ paddingTop: 20 }}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why padding on the top?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i didn't check if they did or not, but i was worried that the screenshots would include the status bar

import React, { Component } from 'react';
import HappoJob from './HappoJob';

class Scaffold extends Component {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does RN not support SFCs?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

they work. i avoid them mostly just because they break hot module reloading

render() {
const { Component } = this.props;
return (
<View ref={this.setRef} onLayout={this.onLayout} style={styles.container}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this shrink to be the size of what it contains or does it work more like display: block?


# node.js
#
node_modules/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have this

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol this list is long 🚋

@@ -55,7 +58,12 @@
"mkdirp": "^0.5.1",
"png-crop": "0.0.1",
"pngjs": "^2.3.1",
"react": "15.4.1",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we remove this from devDepedencies at the same time? I suppose it won't matter once we actually do the plugin stuff we talked about.

(snapshots) => {
resolve(snapshots);
}
).then(initializeIOSdriver);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have the initializeWebdriver call in the cli script, but you put initializeIOSdriver inside of runIOSVisualDiffs. I don't think it matters too much where it goes either way, but I think it would be good to have consistency.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I did that to make it easier to test runVisualDiffs.

return new Promise((resolve) => {
setupWebsockets(
(/* name, uri */) => {
// do nothing
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this callback for? This comment would be clearer if it explained when we are doing nothing and why we are doing nothing.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, maybe setupWebsockets should take an object argument instead of positional arguments? That would make this clearer.

}

module.exports = function runIOSVisualDiffs() {
const viewportName = 'ios';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is quite right. There different sizes on ios, right (e.g. big iphone, small iphone, ipad)? If so, then we'd want this to match at least one of those sizes. Ideally, this would be configurable so you could take screenshots in as many or few of the sizes as you like. Unless maybe that doesn't ever come into play in an ios app? I honestly have no idea how any of that works.

@@ -0,0 +1,18 @@
function saveResultToFile(runResult) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be exported, eh?


return new Promise(resolve => {
http.listen(5000, function() {
console.log('listening on *:5000');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this comment mention what is listening on this port?

buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar with iOS development, but I'm assuming that this is generated in some way. Are there any parts of this configuration that "stand out" that are worth mentioning? Or is it mostly a standard setup?

* 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.
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do these need to be here?

#

lib_deps = []
for jarfile in glob(['libs/*.jar']):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do people normally add dependencies to source control or manage them through maven/ivy etc for android apps? This isn't important right now, I'm mostly asking to make sure I'm understanding what's going on.

eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"

exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this copied/generated from somewhere or did you hand-write it? I'm assuming copied/generated.

// this is needed to support socket.io
window.navigator.userAgent = 'react-native';

const { AppRegistry } = require('react-native');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we move this to the top? Or is the import depending on userAgent to be set?

} from 'react-native';
import io from 'socket.io-client';
import StoryRenderer from './StoryRenderer';
import { getStories, getStory } from './StoryManager';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the name "story". Assuming it's the same thing that we use for "example" in the js version, I could consider changing it globally to "story".


HappoJob.propTypes = propTypes;

module.exports = HappoJob;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at this makes me think that we should switch to using React for rendering examples/stories in the browser version as well.

import React, { Component } from 'react';
import HappoJob from './HappoJob';

class Scaffold extends Component {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


# node.js
#
node_modules/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol this list is long 🚋

@@ -8,6 +8,8 @@
"scripts": {
"build": "npm run --silent clean && babel src/server/ -d lib/server/ --ignore __tests__ && npm run --silent webpack",
"clean": "rimraf lib public/*.worker.js public/*.bundle.js*",
"babel:watch": "babel src/server -d lib/server --ignore __tests__ --watch",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been meaning to add this for a while, thanks! I do think we could extract a separate babel step so that we don't have to duplicate the params in build.

(snapshots) => {
resolve(snapshots);
}
).then(initializeIOSdriver);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I did that to make it easier to test runVisualDiffs.

@trotzig
Copy link
Contributor

trotzig commented Jan 7, 2017

Thanks for working on this! I'm excited to see the progress.

As someone who hasn't done any iOS or Android development, what are the steps necessary to try this out? You don't have to be specific, just list e.g "install xcode", "pay for developer's license", "read up on objective-c" etc. :)

trotzig added a commit that referenced this pull request Jan 12, 2017
This work was prepared by @lelandrichardson as part of PR #179. I'm
cherry-picking them here to simplify work going forward.

These methods are all general-purpose, and should be possible to reuse
over multiple targets.
@lelandrichardson
Copy link
Collaborator Author

Closing in favor of #185

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants