Skeletor Scaffolding

Architecture & DevOps for Progressive Decoupling

Presented By
Erin Marchak
Yuriy Savenko

The Goal

Integrating progressively decoupling into your workflow.

The Challenges

  1. Progressively decoupled – React inside of Drupal
  2. Different teams with different repositories
  3. Confidence in our React components

The Topics

  1. Building a Progressively Decoupled site with Skeletor
  2. Continuous Integration for Progressive Decoupling
  3. Unit testing React components in Drupal
  4. Test for data consistency with JSON Schema
About Us
Yuriy Savenko
Solutions Architect, Javascript Practices

Profile

Yuriy is a Front-end Developer and Solutions Architect with Myplanet, specializing in Front-end and Drupal development. Originally joining the team remotely from Ukraine as a Creative Lead, Yuriy made the switch to full-time coding—and to Canada—in 2013. He holds his Acquia certifications for Development and Front-end Development.

Erin Marchak
Associate Director, Drupal Practices

Profile

Erin has 7 years of Drupal experience and excels at Accessible front-end development and custom Drupal modules. She has deployed several Drupal 8 projects, and is passionate about customer service and creating emotional connections with people using technology. She has her ‘Grand Master’ Drupal certification.

1. Skeletor

Building a Progressively Decoupled site with Skeletor

Skeletor is our framework for scaffolding your decoupled apps
1. Skeletor

Rest UI + JSON Schema

Endpoints to provide the data, and a schema to describe it.
1. Skeletor

“A user interface for configuring Drupal 8's REST module.

drupal.org/projects/restui
1. Skeletor
REST Resource

										{
											"nid":  [{ "value": 1 }],
											"type": [
												{
												"target_id":   "attendee",
												"target_type": "node_type",
												"target_uuid": "232c8ff1-47f9-4783-9e5c-e747c75ce558"
												}
											],
											"field_bio":       [{ "value": "Qui natus et ..." }],
											"field_firstname": [{ "value": "Dana" }],
											"field_lastname":  [{ "value": "Oneil" }],
											"field_presenter": [{ "value": true }],
											"field_title":     [{ "value": "Ms." }]
											...
										}
									
/node/1?_format=json
1. Skeletor

“Creating an open description format for API services that is vendor neutral, portable and open is critical to accelerating the vision of a truly connected world.

OpenAPIs.org
1. Skeletor
Schemata

										{
											"$schema":     "http://json-schema.org/draft-04/schema#",
											"id":          "http://local.skeletor.com/schemata/node/attendee?_format=schema_json&_describes=api_json",
											"type":        "object",
											"title":       "node:attendee Schema",
											"description": "Describes the payload for 'node' entities of the 'attendee' bundle.",
											"properties": {
											...
										}
									
/schemata/node/attendee?_format=schema_json&_describes=api_json
2. Continuous Integration

Continuous Integration for Progressive Decoupling

One docroot to rule them all
2. Continuous Integration

The Goal


											composer require myplanet/skeletor-scaffold
										
2. Continuous Integration
Register your dependency

										"repositories": [
										...
											{
												"type":    "package",
												"package": {
													"name":    "myplanet/react-redux-skeletor",
													"version": "1.0",
													"type":    "npm-package",
													"source": {
														"type":      "git",
														"reference": "presentation",
														"url":       "https://github.com/myplanet/react-redux-skeletor.git"
													},
													"require": {
														"composer/installers": "^1.2",
														"oomphinc/composer-installers-extender": "dev-master"
													}
												}
											}
										],
									
composer.json:repositories
Place your dependency

										"extra": {
											...
											"installer-paths": {
												...
												"docroot/modules/npm-packages/{$name}": ["type:npm-package"]
											},
											"installer-types": ["npm-package"]
										}
									
composer.json:extra
2. Continuous Integration

But how do we install the node packages?

I'm very lazy and want a robot to do this.
2. Continuous Integration
Register a post-install command...

										 "autoload": {
												"classmap": [
													...
													"scripts/skeletor/NpmPackage.php"
												]
										 },
										 "scripts": {
												...
												"npm-install": "DrupalSkeletor\\NpmPackage::npmInstall",
												"npm-build":   "DrupalSkeletor\\NpmPackage::npmBuild",
												"npm-test":    "DrupalSkeletor\\NpmPackage::npmTest",
												"post-install-cmd": [
													 "DrupalProject\\composer\\ScriptHandler::createRequiredFiles",
													 "DrupalComposer\\DrupalScaffold\\Plugin::scaffold",
													 "@composer npm-install",
													 "@composer npm-build"
										    ],
											}
										
composer.json:scripts & composer.json:autoload
... then build it!

											namespace DrupalSkeletor;

											use Composer\Script\Event;
											use Symfony\Component\Finder\Finder;
											use DrupalFinder\DrupalFinder;

											/**
											 * Class NpmPackage.
											 *
											 * @package DrupalSkeletor
											 */
											class NpmPackage {

												/**
												 * NPM Install.
												 *
												 * @param \Composer\Script\Event $event
												 *   Event to echo output.
												 */
												public static function npmInstall(Event $event) {
													static::runNpmCommand('install', $event);
												}
											...
										
scripts/skeletor/NpmPackage.php
3. Unit Testing

Unit testing React components in Drupal

3. Unit Testing

Moving parts of React + Redux app



  • Components

    We want to make sure that component behaves as expected when new features have been added to it or model has been changed.

  • Actions

    We want to make sure that when the action creator has been called, the right action (or sequence of actions) have been emitted.

  • Reducers

    We want to make sure that Redux state has been updated accordingly for the particular action.

3. Unit Testing

Jest



“Jest is used by Facebook to test all JavaScript code including React applications. One of Jest's philosophies is to provide an integrated "zero-configuration" experience. We observed that when engineers are provided with ready-to-use tools, they end up writing more tests, which in turn results in more stable and healthy code bases.”

  • Simple configuration.
  • Snapshot testing.
  • Jest is fast, by parallelizing test runs across workers.
  • It has built-in coverage reports.
3. Unit Testing

Enzyme



Enzyme is a JavaScript Testing utility for React that makes it easier to assert, manipulate, and traverse your React Components' output.

  • jQuery like API to work with rendered components.
  • Shallow rendering (which is useful to test just components you want to test).
3. Unit Testing
Component
Test file

										import React, { Component } from 'react';
										import PropTypes from 'prop-types';

										class AttendeeCard extends Component {
											render() {
												let {id, firstName, lastName, dorgId, bio, presenter}
												= this.props

												return (
													<div
														className=`attendee ${presenter ? '_presenter' : ''}`>
														<h3>{`${firstName} ${lastName}`}</h3>
														<p className="-bio">{bio}</p>
													</div>
												)
											}
										}

										AttendeeCard.propTypes = {
											id: PropTypes.number,
											name: PropTypes.string,
											dorgId: PropTypes.string,
											bio: PropTypes.string,
											presenter: PropTypes.bool
										};

										export default AttendeeCard;
									
components/AttendeeCard/AttendeeCard.js

										import React from 'react';
										import {shallow, render, mount} from 'enzyme';
										import AttendeeCard from './AttendeeCard'

										test('Attendee Card renders', () => {
											let data = {
												id: 1,
												firstName: 'John',
												lastName: 'Smith',
												bio: 'bio...'
											}

											const wrapper = render(
												<AttendeeCard
													{...data}
												/>
											);

											expect(wrapper.find('h3').text())
												.toEqual('John Smith');
											expect(wrapper.find('.-bio').text())
												.toEqual('bio...');
										});
									
components/AttendeeCard/AttendeeCard.test.js
3. Unit Testing
Actions
Test file

										export function getData() {
										  return function (dispatch, getState) {
											let endpointId = 'attendee'
											let endpoint = apiEndpoints[endpointId]
											dispatch(getDataPending())

											return axios({
											  url: endpoint.url,
											  method: endpoint.method,
											  timeout: endpoint.timeout
											})
											.then(res => {
											  var validate = ajv.compile(attendeeSchema)
											  var valid = validate(res.data)

											  if (!valid) {
												dispatch(getDataFail('Error fetching the data',
													validate.errors))
											  }
											  else {
												dispatch(getDataSuccess(res.data.attendee))
											  }
											})
											.catch(err => {
											  getDataFail('Error fetching the data')
											});
										  }
										}
									
actions/attendeeActions.js

										describe('Attendee actions', () => {
										  it('creates GET_DATA_SUCCESS', () => {
											let endpointId = 'attendee'
											let endpoint = apiEndpoints[endpointId]

											var mock = new MockAdapter(axios);
											const data = mockedObject;

											mock.onGet(endpoint.url).reply(200, data);

											const expectedActions = [
											  { type: 'GET_DATA_PENDING' },
											  { type: 'GET_DATA_SUCCESS', data: mockedObject}
											]
											const store = mockStore({})

											return store.dispatch(actions.getData()).then(() => {
											  expect(store.getActions()).toEqual(expectedActions)
											})
										  })
										});
									
actions/test/attendeeActions.test.js
3. Unit Testing
Reducer
Test file

										const default = {
											data: null,
											isFetching: false,
											isLoaded: false,
											error: null
										}

										export default function attendee(state = default, action) {
											switch(action.type) {
												case 'GET_DATA_SUCCESS' :
													return {
														...state,
														data: action.data,
														isLoaded: true,
														isFetching: false
													}
												case 'GET_DATA_PENDING' :
													return {
														...state,
														isFetching: true
													}
												...
												default:
													return state;
											}
										}
									
reducers/attendee.js

										import attendee from '../attendee'

										test('Attendee reducer test', () => {
											let initState = attendee(undefined, {type: null});

											let dataSuccess = {
												type: 'GET_DATA_SUCCESS',
												data: { test: "test" }
											};

											let attendeeSuccessUpdate = {
												isLoaded: true,
												isFetching: false
											};

											expect(attendee(initState, dataSuccess))
												.toEqual({
													...initialState,
													...attendeeSuccessUpdate,
													data: actionDataSuccess.data
												});
										});

									
reducers/tests/attendee.test.js
3. Unit Testing
Test results: Success

										$ npm run test

										> react-skeletor@1.0.0 test ~/DrupalCon
										> NODE_ENV=testing jest

										 PASS  src/components/AttendeeCard/AttendeeCard.test.js
										 PASS  src/reducers/reducers/tests/attendee.test.js
										 PASS  src/actions/tests/attendeeActions.test.js

										Test Suites: 3 passed, 3 total
										Tests:       3 passed, 3 total
										Snapshots:   0 total
										Time:        1.903s
										Ran all test suites.
									
3. Unit Testing
Test results: Failed

										$ npm run test

										> react-skeletor@1.0.0 test ~/DrupalCon
										> NODE_ENV=testing jest

										 FAIL  src/components/AttendeeCard/AttendeeCard.test.js
										  ● Attendee Card renders name

											expect(received).toEqual(expected)

											Expected value to equal:
											  "John Smith"
											Received:
											  "John Smith "

											...

										 PASS  src/reducers/reducers/tests/attendee.test.js
										 PASS  src/actions/tests/attendeeActions.test.js

										Test Suites: 1 failed, 2 passed, 3 total
										Tests:       1 failed, 2 passed, 3 total
										...
										Ran all test suites.
										
3. Unit Testing
Test results: Coverage report

$ npm run coverage

...

Test Suites: 3 passed, 3 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        1.725s
Ran all test suites.
-----------------------------|----------|----------|----------|----------|----------------|
File                         |  % Stmts | % Branch |  % Funcs |  % Lines |Uncovered Lines |
-----------------------------|----------|----------|----------|----------|----------------|
All files                    |    88.46 |    88.89 |    77.78 |    88.46 |                |
 src/actions                 |    81.25 |       50 |    71.43 |    81.25 |                |
  actionCreators.js          |    81.25 |       50 |    71.43 |    81.25 |       32,39,58 |
 src/components/AttendeeCard |      100 |      100 |      100 |      100 |                |
  AttendeeCard.js            |      100 |      100 |      100 |      100 |                |
 src/constants               |      100 |      100 |      100 |      100 |                |
  endpoints.js               |      100 |      100 |      100 |      100 |                |
 src/reducers                |      100 |      100 |      100 |      100 |                |
  attendee.js                |      100 |      100 |      100 |      100 |                |
-----------------------------|----------|----------|----------|----------|----------------|

										
4. Unit Testing

Summary



  • We have seen how main moving parts of React + Redux app can be tested.
    • Components
    • Actions
    • Reducers
  • Coverage report.
4. JSON Schema

Test for data consistency with JSON Schema

4. JSON Schema

What is JSON Schema



JSON Schema is a vocabulary that allows you to annotate and validate JSON documents.
2. JSON Schema

Problem

Javascript is not a strongly typed language, and as a developer you need to rely on additional tools which will allow you to develop stable apps with Javascript.


Solution

We recommend to use JSON schema validators if JSON is your communication protocol.

4. JSON Schema

JSON Schema validators

There are a lot of implementations for JSON Schema validators in javascript.

  • ajv (MIT)
  • djv (MIT)
  • jsonschema (MIT)
  • is-my-json-valid (MIT)
  • tv4 (MIT)
  • JaySchema (MIT)
  • z-schema (MIT)
  • ...

http://json-schema.org/implementations.html#validator-javascript
4. JSON Schema

									{
										"id": 1002,
										"title": "Ms.",
										"firstName": "James",
										"lastName": "Noname",
										"bio": "lorem ipsum...",
										"presenter": false
									}
								
mydomain/attendee

									{
										"$schema": "http://json-schema.org/draft-04/schema#",
										"id": "...?_format=schema_json&_describes=api_json",
										"title": "node:attendee Schema",
										"description": "Describes the payload for 'node' entities of the 'attendee' bundle.",
										"type": "object",
										"properties": {
											"id" : {"type": "integer"},
											"title" : {"type": "string"},
											"firstName" : {"type": "string"},
											"lastName" : {"type": "string"},
											"bio" : {"type": "string"},
											"presenter" : {"type": "boolean"}
										},
										"required": ["id", "firstName", "lastName"]
									}
								
/schemas/attendeeSchema.json
4. JSON Schema

								export function getAttendee() {
									return function (dispatch, getState) {
										let endpointId = 'attendee'
										let endpoint = apiEndpoints[endpointId] //import { apiEndpoints } from '../constants/endpoints'
										dispatch(getDataPending())

										axios({
											url: endpoint.url,
											method: endpoint.method,
											timeout: endpoint.timeout
										})
										.then(res => {
											// Validate response data.
											var validate = ajv.compile(attendeeSchema) // import Ajv from 'ajv'; let ajv = Ajv({allErrors: true});
											var valid = validate(res.data)
											if (!valid) {
												getDataFail('Error fetching the data', validate.errors)
											}
											else {
												dispatch(getDataSuccess(res.data))
											}
										.catch(err => {
											getDataFail('Error fetching the data')
										});
									}
								}
							
actions/attendeeActions.js
4. JSON Schema

Summary



  • We have seen how we can use JSON schema validation to make sure that React app receives data in the expected format.
  • You can make your React app to massage the data on the entry point of your app (before app or components start render) and perform the conversions and re-validate data again.
  • You can make your React app report the problem with the data back to the Drupal if validation fails.
  • You can validate the data React app sends back to the Drupal.
  • You can use JSON schema validators in your unit tests, by testing payload submitted with actions or test if reducers update state properly.

  • And consider always using React PropTypes for the React components.

In the future?

  • React ought to define the schema that it needs.
  • Private Packagist for teams with many dependencies.
  • Acquia Node.js environments for fully decoupled.

Questions?

Tweet us at @myplanet or @emarchak if you're shy

Contribution Sprints

Friday, 29 September, 2017

Mentored Core Sprint 9:00-18:00 Room: Stolz 2
First time Sprinter Workshop 9:00-12:00 Room: Lehar 1 - Lehar 2
General Sprint 9:00-18:00 Room: Mall
#drupalsprints