Generic state management for React GridField

Renaming state operations from 'campaign' to 'record'.
Implemented API endpoint retrieval of GridField data.

Added more mock data into CampaignAdmin (rather than hardcoding in client),
to be replaced by CampaignAdmin API endpoint querying the real datamodel.

Using more native isomorphic-fetch instead of jQuery.ajax
to minimise dependencies and get into a more forward-thinking API.

Also catching errors in ReactJS API backend:
Emulate jQuery.ajax() behaviour. Might change at a later point
if we have a general purpose backend with a promise-based catch()
implementation.
This commit is contained in:
Ingo Schommer 2016-03-29 15:38:48 +13:00
parent 47dd7b48af
commit 2a5c92e491
16 changed files with 459 additions and 257 deletions

View File

@ -9,12 +9,15 @@
class CampaignAdmin extends LeftAndMain implements PermissionProvider { class CampaignAdmin extends LeftAndMain implements PermissionProvider {
private static $allowed_actions = [ private static $allowed_actions = [
'item',
'items',
'schema',
'DetailEditForm',
'readCampaigns',
'createCampaign', 'createCampaign',
'readCampaign', 'readCampaign',
'updateCampaign', 'updateCampaign',
'deleteCampaign', 'deleteCampaign',
'schema',
'DetailEditForm'
]; ];
private static $menu_priority = 11; private static $menu_priority = 11;
@ -96,27 +99,33 @@ class CampaignAdmin extends LeftAndMain implements PermissionProvider {
"customValidationMessage": "", "customValidationMessage": "",
"attributes": [], "attributes": [],
"data": { "data": {
"collectionReadUrl": { "recordType": "ChangeSet",
"collectionReadEndpoint": {
"url": "admin\/campaigns\/items", "url": "admin\/campaigns\/items",
"method": "GET" "method": "GET"
}, },
"itemReadUrl": { "itemReadEndpoint": {
"url": "admin\/campaigns\/item\/:id", "url": "admin\/campaigns\/item\/:id",
"method": "GET" "method": "GET"
}, },
"itemUpdateUrl": { "itemUpdateEndpoint": {
"url": "admin\/campaigns\/item\/:id", "url": "admin\/campaigns\/item\/:id",
"method": "PUT" "method": "PUT"
}, },
"itemCreateUrl": { "itemCreateEndpoint": {
"url": "admin\/campaigns\/item\/:id", "url": "admin\/campaigns\/item\/:id",
"method": "POST" "method": "POST"
}, },
"itemDeleteUrl": { "itemDeleteEndpoint": {
"url": "admin\/campaigns\/item\/:id", "url": "admin\/campaigns\/item\/:id",
"method": "DELETE" "method": "DELETE"
}, },
"editFormSchemaUrl": "admin\/campaigns\/schema\/DetailEditForm" "editFormSchemaEndpoint": "admin\/campaigns\/schema\/DetailEditForm",
"columns": [
{"name": "Title", "field": "Name"},
{"name": "Changes", "field": "_embedded.ChangeSetItems.length"},
{"name": "Description", "field": "Description"}
]
} }
}, { }, {
"name": "SecurityID", "name": "SecurityID",
@ -177,7 +186,182 @@ JSON;
public function readCampaigns(SS_HTTPRequest $request) { public function readCampaigns(SS_HTTPRequest $request) {
$response = new SS_HTTPResponse(); $response = new SS_HTTPResponse();
$response->addHeader('Content-Type', 'application/json'); $response->addHeader('Content-Type', 'application/json');
$response->setBody(Convert::raw2json(['campaigns' => 'read'])); $json = <<<JSON
{
"_links": {
"self": {
"href": "/api/ChangeSet/"
}
},
"count": 3,
"total": 3,
"_embedded": {
"ChangeSets": [
{
"_links": {
"self": {
"href": "/api/ChangeSet/show/1"
}
},
"ID": 1,
"Created": "2016-01-01 00:00:00",
"LastEdited": "2016-01-01 00:00:00",
"Name": "March 2016 release",
"Description": "All the stuff related to the 4.0 announcement",
"State": "open",
"_embedded": {
"ChangeSetItems": [
{
"_links": {
"self": {
"href": "/api/ChangeSetItem/show/1"
},
"owns": [
{"href": "/api/ChangeSetItem/show/3"},
{"href": "/api/ChangeSetItem/show/4"}
]
},
"ID": 1,
"Created": "2016-01-01 00:00:00",
"LastEdited": "2016-01-01 00:00:00",
"VersionBefore": 1,
"VersionAfter": 2,
"State": "open",
"_embedded": {
"Object": [
{
"_links": {
"self": {
"href": "/api/SiteTree/show/1"
}
},
"ID": 1,
"ChangeSetCategory": "Page",
"Title": "Home",
"StatusFlags": ["addedtodraft"]
}
]
}
},
{
"_links": {
"self": {
"href": "/api/ChangeSetItem/show/2"
},
"owns": [
{"href": "/api/ChangeSetItem/show/4"}
]
},
"ID": 2,
"Created": "2016-01-01 00:00:00",
"LastEdited": "2016-01-01 00:00:00",
"VersionBefore": 1,
"VersionAfter": 2,
"State": "open",
"_embedded": {
"Object": [
{
"_links": {
"self": {
"href": "/api/SiteTree/show/2"
}
},
"ID": 2,
"ChangeSetCategory": "Page",
"Title": "Features",
"StatusFlags": ["modified"]
}
]
}
},
{
"_links": {
"self": {
"href": "/api/ChangeSetItem/show/3"
},
"ownedby": [
{"href": "/api/ChangeSetItem/show/1"}
]
},
"ID": 3,
"Created": "2016-01-01 00:00:00",
"LastEdited": "2016-01-01 00:00:00",
"VersionBefore": 1,
"VersionAfter": 2,
"State": "open",
"_embedded": {
"Object": [
{
"_links": {
"self": {
"href": "/api/File/show/1"
}
},
"ID": 1,
"ChangeSetCategory": "File",
"Title": "A picture of George",
"PreviewThumbnailURL": "/george.jpg",
"StatusFlags": ["modified"]
}
]
}
},
{
"_links": {
"self": {
"href": "/api/ChangeSetItem/show/4"
},
"ownedby": [
{"href": "/api/ChangeSetItem/show/1"},
{"href": "/api/ChangeSetItem/show/2"}
]
},
"ID": 4,
"Created": "2016-01-01 00:00:00",
"LastEdited": "2016-01-01 00:00:00",
"VersionBefore": 1,
"VersionAfter": 2,
"State": "open",
"_embedded": {
"Object": [
{
"_links": {
"self": {
"href": "/api/File/show/2"
}
},
"ID": 2,
"ChangeSetCategory": "File",
"Title": "Out team",
"PreviewThumbnailURL": "/team.jpg",
"StatusFlags": ["modified"]
}
]
}
}
]
}
},
{
"_links": {
"self": {
"href": "/api/ChangeSet/show/2"
}
},
"ID": 2,
"Created": "2016-02-01 00:00:00",
"LastEdited": "2016-02-01 00:00:00",
"Name": "Shop products",
"State": "open",
"_embedded": {
"ChangeSetItems": []
}
}
]
}
}
JSON;
$response->setBody($json);
return $response; return $response;
} }
@ -192,7 +376,7 @@ JSON;
public function readCampaign(SS_HTTPRequest $request) { public function readCampaign(SS_HTTPRequest $request) {
$response = new SS_HTTPResponse(); $response = new SS_HTTPResponse();
$response->addHeader('Content-Type', 'application/json'); $response->addHeader('Content-Type', 'application/json');
$response->setBody(Convert::raw2json(['campaign' => 'read'])); $response->setBody('');
return $response; return $response;
} }

View File

@ -7,6 +7,7 @@ import reducerRegister from 'reducer-register';
import * as configActions from 'state/config/actions'; import * as configActions from 'state/config/actions';
import ConfigReducer from 'state/config/reducer'; import ConfigReducer from 'state/config/reducer';
import SchemaReducer from 'state/schema/reducer'; import SchemaReducer from 'state/schema/reducer';
import RecordsReducer from 'state/records/reducer';
// Sections // Sections
import CampaignAdmin from 'sections/campaign-admin'; import CampaignAdmin from 'sections/campaign-admin';
@ -14,6 +15,7 @@ import CampaignAdmin from 'sections/campaign-admin';
function appBoot() { function appBoot() {
reducerRegister.add('config', ConfigReducer); reducerRegister.add('config', ConfigReducer);
reducerRegister.add('schemas', SchemaReducer); reducerRegister.add('schemas', SchemaReducer);
reducerRegister.add('records', RecordsReducer);
const initialState = {}; const initialState = {};
const rootReducer = combineReducers(reducerRegister.getAll()); const rootReducer = combineReducers(reducerRegister.getAll());

View File

@ -8,6 +8,10 @@ import FormComponent from 'components/form';
import TextField from 'components/text-field'; import TextField from 'components/text-field';
import HiddenField from 'components/hidden-field'; import HiddenField from 'components/hidden-field';
import GridField from 'components/grid-field'; import GridField from 'components/grid-field';
import fetch from 'isomorphic-fetch';
import es6promise from 'es6-promise';
es6promise.polyfill();
// Using this to map field types to components until we implement dependency injection. // Using this to map field types to components until we implement dependency injection.
var fakeInjector = { var fakeInjector = {
@ -120,14 +124,17 @@ export class FormBuilderComponent extends SilverStripeComponent {
headerValues.push('state'); headerValues.push('state');
} }
this.formSchemaPromise = $.ajax({ this.formSchemaPromise = fetch(this.props.schemaUrl, {
method: 'GET',
headers: { 'X-FormSchema-Request': headerValues.join() }, headers: { 'X-FormSchema-Request': headerValues.join() },
url: this.props.schemaUrl credentials: 'same-origin'
}).done((data, status, xhr) => { })
this.isFetching = false; .then(response => {
this.props.actions.setSchema(data); return response.json();
}); })
.then(json => {
this.isFetching = false;
this.props.actions.setSchema(json);
});
this.isFetching = true; this.isFetching = true;

View File

@ -1,4 +1,6 @@
import React from 'react'; import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import SilverStripeComponent from 'silverstripe-component.js'; import SilverStripeComponent from 'silverstripe-component.js';
import GridFieldTable from './table'; import GridFieldTable from './table';
import GridFieldHeader from './header'; import GridFieldHeader from './header';
@ -6,55 +8,43 @@ import GridFieldHeaderCell from './header-cell';
import GridFieldRow from './row'; import GridFieldRow from './row';
import GridFieldCell from './cell'; import GridFieldCell from './cell';
import GridFieldAction from './action'; import GridFieldAction from './action';
import * as actions from 'state/records/actions';
/**
* The component acts as a container for a grid field,
* with smarts around data retrieval from external sources.
*
* @todo Convert to higher order component which hooks up form schema data to an API backend as a grid data source
* @todo Replace "dumb" inner components with third party library (e.g. https://griddlegriddle.github.io)
*/
class GridField extends SilverStripeComponent { class GridField extends SilverStripeComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.deleteCampaign = this.deleteCampaign.bind(this); this.deleteRecord = this.deleteRecord.bind(this);
this.editCampaign = this.editCampaign.bind(this); this.editRecord = this.editRecord.bind(this);
}
// TODO: This will be an AJAX call and it's response stored in state. componentDidMount() {
this.mockData = { super.componentDidMount();
campaigns: [
{ let data = this.props.data;
title: 'SilverStripe 4.0 release',
description: 'All the stuff related to the 4.0 announcement', this.props.actions.fetchRecords(data.recordType, data.collectionReadEndpoint.method, data.collectionReadEndpoint.url);
changes: 20
},
{
title: 'March release',
description: 'march release stuff',
changes: 2
},
{
title: 'About us',
description: 'The team',
changes: 1345
}
]
};
} }
render() { render() {
const columns = [ const records = this.props.records;
{ if(!records) {
name: 'title' return <div></div>;
}, }
{
name: 'changes', const columns = this.props.data.columns;
width: 2
},
{
name: 'description',
width: 6
}
];
const actions = [ const actions = [
<GridFieldAction icon={'cog'} handleClick={this.editCampaign} />, <GridFieldAction icon={'cog'} handleClick={this.editRecord} />,
<GridFieldAction icon={'cancel'} handleClick={this.deleteCampaign} /> <GridFieldAction icon={'cancel'} handleClick={this.deleteRecord} />
]; ];
// Placeholder to align the headers correctly with the content // Placeholder to align the headers correctly with the content
@ -63,9 +53,11 @@ class GridField extends SilverStripeComponent {
const headerCells = columns.map((column, i) => <GridFieldHeaderCell key={i} width={column.width}>{column.name}</GridFieldHeaderCell>); const headerCells = columns.map((column, i) => <GridFieldHeaderCell key={i} width={column.width}>{column.name}</GridFieldHeaderCell>);
const header = <GridFieldHeader>{headerCells.concat(actionPlaceholder)}</GridFieldHeader>; const header = <GridFieldHeader>{headerCells.concat(actionPlaceholder)}</GridFieldHeader>;
const rows = this.mockData.campaigns.map((campaign, i) => { const rows = records.map((record, i) => {
var cells = columns.map((column, i) => { var cells = columns.map((column, i) => {
return <GridFieldCell key={i} width={column.width}>{campaign[column.name]}</GridFieldCell> // Get value by dot notation
var val = column.field.split('.').reduce((a, b) => a[b], record)
return <GridFieldCell key={i} width={column.width}>{val}</GridFieldCell>
}); });
var rowActions = actions.map((action, j) => { var rowActions = actions.map((action, j) => {
@ -82,14 +74,35 @@ class GridField extends SilverStripeComponent {
); );
} }
deleteCampaign(event) { deleteRecord(event) {
// delete campaign // delete record
} }
editCampaign(event) { editRecord(event) {
// edit campaign // edit record
} }
} }
export default GridField; GridField.propTypes = {
data: React.PropTypes.shape({
recordType: React.PropTypes.string.isRequired,
headerColumns: React.PropTypes.array,
collectionReadEndpoint: React.PropTypes.object
})
};
function mapStateToProps(state, ownProps) {
let recordType = ownProps.data ? ownProps.data.recordType : null;
return {
records: (state.records && recordType) ? state.records[recordType] : []
}
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(actions, dispatch)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(GridField);

View File

@ -4,10 +4,6 @@ import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import CampaignAdmin from './controller'; import CampaignAdmin from './controller';
import campaignsReducer from 'state/campaigns/reducer';
// TODO: Move this to the controller.
reducerRegister.add('campaigns', campaignsReducer);
$.entwine('ss', function ($) { $.entwine('ss', function ($) {

View File

@ -1,16 +1,36 @@
import $ from 'jQuery'; import fetch from 'isomorphic-fetch';
import es6promise from 'es6-promise';
es6promise.polyfill();
/**
* @see https://github.com/github/fetch#handling-http-error-statuses
*/
function checkStatus(response) {
if (response.status >= 200 && response.status < 300) {
return response
} else {
var error = new Error(response.statusText)
error.response = response
throw error
}
}
class SilverStripeBackend { class SilverStripeBackend {
constructor() {
// Allow mocking
this.fetch = fetch;
}
/** /**
* Makes a network request using the GET HTTP verb. * Makes a network request using the GET HTTP verb.
* *
* @param string url - Endpoint URL. * @param string url - Endpoint URL.
* * @return object - Promise
* @return object - jqXHR. See http://api.jquery.com/Types/#jqXHR
*/ */
get(url) { get(url) {
return $.ajax({ type: 'GET', url }); return this.fetch(url, { method: 'get', credentials: 'same-origin' })
.then(checkStatus);
} }
/** /**
@ -18,11 +38,11 @@ class SilverStripeBackend {
* *
* @param string url - Endpoint URL. * @param string url - Endpoint URL.
* @param object data - Data to send with the request. * @param object data - Data to send with the request.
* * @return object - Promise
* @return object - jqXHR. See http://api.jquery.com/Types/#jqXHR
*/ */
post(url, data) { post(url, data) {
return $.ajax({ type: 'POST', url, data }); return this.fetch(url, { method: 'post', credentials: 'same-origin', body: data })
.then(checkStatus);
} }
/** /**
@ -30,11 +50,11 @@ class SilverStripeBackend {
* *
* @param string url - Endpoint URL. * @param string url - Endpoint URL.
* @param object data - Data to send with the request. * @param object data - Data to send with the request.
* * @return object - Promise
* @return object - jqXHR. See http://api.jquery.com/Types/#jqXHR
*/ */
put(url, data) { put(url, data) {
return $.ajax({ type: 'PUT', url, data }); return this.fetch(url, { method: 'put', credentials: 'same-origin', body: data })
.then(checkStatus);
} }
/** /**
@ -42,11 +62,11 @@ class SilverStripeBackend {
* *
* @param string url - Endpoint URL. * @param string url - Endpoint URL.
* @param object data - Data to send with the request. * @param object data - Data to send with the request.
* * @return object - Promise
* @return object - jqXHR. See http://api.jquery.com/Types/#jqXHR
*/ */
delete(url, data) { delete(url, data) {
return $.ajax({ type: 'DELETE', url, data }); return this.fetch(url, { method: 'delete', credentials: 'same-origin', body: data })
.then(checkStatus);
} }
} }

View File

@ -1,8 +0,0 @@
export default {
CREATE_CAMPAIGN: 'CREATE_CAMPAIGN',
UPDATE_CAMPAIGN: 'UPDATE_CAMPAIGN',
DELETE_CAMPAIGN: 'DELETE_CAMPAIGN',
FETCH_CAMPAIGN_REQUEST: 'FETCH_CAMPAIGN_REQUEST',
FETCH_CAMPAIGN_FAILURE: 'FETCH_CAMPAIGN_FAILURE',
FETCH_CAMPAIGN_SUCCESS: 'FETCH_CAMPAIGN_SUCCESS'
};

View File

@ -1,49 +0,0 @@
import deepFreeze from 'deep-freeze';
import ACTION_TYPES from './action-types';
const initialState = {
isFetching: false,
items: []
};
function campaignsReducer(state = initialState, action) {
switch (action.type) {
case ACTION_TYPES.CREATE_CAMPAIGN:
return deepFreeze(Object.assign({}, state, {
}));
case ACTION_TYPES.UPDATE_CAMPAIGN:
return deepFreeze(Object.assign({}, state, {
}));
case ACTION_TYPES.DELETE_CAMPAIGN:
return deepFreeze(Object.assign({}, state, {
}));
case ACTION_TYPES.FETCH_CAMPAIGN_REQUEST:
return deepFreeze(Object.assign({}, state, {
isFetching: true
}));
case ACTION_TYPES.FETCH_CAMPAIGN_FAILURE:
return deepFreeze(Object.assign({}, state, {
isFetching: false
}));
case ACTION_TYPES.FETCH_CAMPAIGN_SUCCESS:
return deepFreeze(Object.assign({}, state, {
isFetching: false
}));
default:
return state;
}
}
export default campaignsReducer;

View File

@ -1,70 +0,0 @@
jest.dontMock('deep-freeze');
jest.dontMock('../reducer');
jest.dontMock('../action-types');
var campaignsReducer = require('../reducer').default,
ACTION_TYPES = require('../action-types').default;
describe('campaignsReducer', () => {
describe('CREATE_CAMPAIGN', () => {
});
describe('UPDATE_CAMPAIGN', () => {
});
describe('DELETE_CAMPAIGN', () => {
});
describe('FETCH_CAMPAIGN_REQUEST', () => {
it('should set the "isFetching" flag', () => {
const initialState = {
isFetching: false
};
const action = { type: ACTION_TYPES.FETCH_CAMPAIGN_REQUEST };
const nextState = campaignsReducer(initialState, action);
expect(nextState.isFetching).toBe(true);
});
});
describe('FETCH_CAMPAIGN_FAILURE', () => {
it('should unset the "isFetching" flag', () => {
const initialState = {
isFetching: true
};
const action = { type: ACTION_TYPES.FETCH_CAMPAIGN_FAILURE };
const nextState = campaignsReducer(initialState, action);
expect(nextState.isFetching).toBe(false);
});
});
describe('FETCH_CAMPAIGN_SUCCESS', () => {
it('should unset the "isFetching" flag', () => {
const initialState = {
isFetching: true
};
const action = { type: ACTION_TYPES.FETCH_CAMPAIGN_FAILURE };
const nextState = campaignsReducer(initialState, action);
expect(nextState.isFetching).toBe(false);
});
});
});

View File

@ -0,0 +1,8 @@
export default {
CREATE_RECORD: 'CREATE_RECORD',
UPDATE_RECORD: 'UPDATE_RECORD',
DELETE_RECORD: 'DELETE_RECORD',
FETCH_RECORDS_REQUEST: 'FETCH_RECORDS_REQUEST',
FETCH_RECORDS_FAILURE: 'FETCH_RECORDS_FAILURE',
FETCH_RECORDS_SUCCESS: 'FETCH_RECORDS_SUCCESS'
};

View File

@ -0,0 +1,22 @@
import ACTION_TYPES from './action-types';
import fetch from 'isomorphic-fetch';
import backend from 'silverstripe-backend.js';
/**
* Retrieves all records
*
* @param string recordType Type of record (the "class name")
* @param string method HTTP methods
* @param string url API endpoint
*/
export function fetchRecords(recordType, method, url) {
return (dispatch, getState) => {
dispatch ({type: ACTION_TYPES.FETCH_RECORDS_REQUEST, payload: {recordType: recordType}});
return backend[method.toLowerCase()](url)
.then(response => response.json())
.then(json => dispatch({type: ACTION_TYPES.FETCH_RECORDS_SUCCESS, payload: {recordType: recordType, data: json}}))
.catch((err) => {
dispatch({type: ACTION_TYPES.FETCH_RECORDS_FAILURE, payload: {error: err, recordType: recordType}})
});
}
}

View File

@ -0,0 +1,45 @@
import deepFreeze from 'deep-freeze';
import ACTION_TYPES from './action-types';
const initialState = {
};
function recordsReducer(state = initialState, action) {
switch (action.type) {
case ACTION_TYPES.CREATE_RECORD:
return deepFreeze(Object.assign({}, state, {
}));
case ACTION_TYPES.UPDATE_RECORD:
return deepFreeze(Object.assign({}, state, {
}));
case ACTION_TYPES.DELETE_RECORD:
return deepFreeze(Object.assign({}, state, {
}));
case ACTION_TYPES.FETCH_RECORDS_REQUEST:
return state;
case ACTION_TYPES.FETCH_RECORDS_FAILURE:
return state;
case ACTION_TYPES.FETCH_RECORDS_SUCCESS:
let recordType = action.payload.recordType;
// TODO Automatic pluralisation from recordType
let records = action.payload.data._embedded[recordType + 's'];
return deepFreeze(Object.assign({}, state, {
[recordType]: records
}));
default:
return state;
}
}
export default recordsReducer;

View File

@ -0,0 +1,36 @@
jest.dontMock('deep-freeze');
jest.dontMock('../reducer');
jest.dontMock('../action-types');
var recordsReducer = require('../reducer').default,
ACTION_TYPES = require('../action-types').default;
describe('recordsReducer', () => {
describe('DELETE_RECORD_SUCCESS', () => {
const initialState = {
TypeA: [
{ID: 1},
{ID: 2}
],
TypeB: [
{ID: 1},
{ID: 2}
]
};
it('removes records from the declared type', () => {
const nextState = recordsReducer(initialState, {
type: ACTION_TYPES.DELETE_RECORD_SUCCESS,
payload: { recordType: 'TypeA', id: 2 }
});
expect(nextState.TypeA.length).toBe(1);
expect(nextState.TypeA[0].ID).toBe(1);
expect(nextState.TypeB.length).toBe(2);
expect(nextState.TypeB[0].ID).toBe(1);
expect(nextState.TypeB[1].ID).toBe(2);
})
});
});

View File

@ -1,42 +1,48 @@
jest.mock('jQuery'); jest.mock('isomorphic-fetch');
jest.unmock('../silverstripe-backend'); jest.unmock('../silverstripe-backend');
import $ from 'jQuery'; import fetch from 'isomorphic-fetch';
import backend from '../silverstripe-backend'; import backend from '../silverstripe-backend';
var getFetchMock = function(data) {
let mock = jest.genMockFunction();
let promise = new Promise((resolve, reject) => {
process.nextTick(() => resolve(data));
});
mock.mockReturnValue(promise);
return mock;
};
describe('SilverStripeBackend', () => { describe('SilverStripeBackend', () => {
beforeAll(() => {
let fetchMock = getFetchMock();
backend.fetch = fetchMock;
});
describe('get()', () => { describe('get()', () => {
it('should return a jqXHR', () => { it('should return a promise', () => {
var jqxhr = backend.get('http://example.com'); var promise = backend.get('http://example.com');
expect(typeof promise).toBe('object');
expect(typeof jqxhr).toBe('object');
expect(typeof jqxhr.done).toBe('function');
expect(typeof jqxhr.fail).toBe('function');
expect(typeof jqxhr.always).toBe('function');
}); });
it('should send a GET request to an endpoint', () => { it('should send a GET request to an endpoint', () => {
backend.get('http://example.com'); backend.get('http://example.com');
expect(backend.fetch).toBeCalledWith(
expect($.ajax).toBeCalledWith({ 'http://example.com',
type: 'GET', {method: 'get', credentials: 'same-origin'}
url: 'http://example.com' );
});
}); });
}); });
describe('post()', () => { describe('post()', () => {
it('should return a jqXHR', () => { it('should return a promise', () => {
var jqxhr = backend.get('http://example.com/item'); var promise = backend.get('http://example.com/item');
expect(typeof promise).toBe('object');
expect(typeof jqxhr).toBe('object');
expect(typeof jqxhr.done).toBe('function');
expect(typeof jqxhr.fail).toBe('function');
expect(typeof jqxhr.always).toBe('function');
}); });
it('should send a POST request to an endpoint', () => { it('should send a POST request to an endpoint', () => {
@ -44,24 +50,19 @@ describe('SilverStripeBackend', () => {
backend.post('http://example.com', postData); backend.post('http://example.com', postData);
expect($.ajax).toBeCalledWith({ expect(backend.fetch).toBeCalledWith(
type: 'POST', 'http://example.com',
url: 'http://example.com', {method: 'post', body: postData, credentials: 'same-origin'}
data: postData );
});
}); });
}); });
describe('put()', () => { describe('put()', () => {
it('should return a jqXHR', () => { it('should return a promise', () => {
var jqxhr = backend.get('http://example.com/item'); var promise = backend.get('http://example.com/item');
expect(typeof promise).toBe('object');
expect(typeof jqxhr).toBe('object');
expect(typeof jqxhr.done).toBe('function');
expect(typeof jqxhr.fail).toBe('function');
expect(typeof jqxhr.always).toBe('function');
}); });
it('should send a PUT request to an endpoint', () => { it('should send a PUT request to an endpoint', () => {
@ -69,24 +70,20 @@ describe('SilverStripeBackend', () => {
backend.put('http://example.com', putData); backend.put('http://example.com', putData);
expect($.ajax).toBeCalledWith({ expect(backend.fetch).toBeCalledWith(
type: 'PUT', 'http://example.com',
url: 'http://example.com', {method: 'put', body: putData, credentials: 'same-origin'}
data: putData );
});
}); });
}); });
describe('delete()', () => { describe('delete()', () => {
it('should return a jqXHR', () => { it('should return a promise', () => {
var jqxhr = backend.get('http://example.com/item'); var promise = backend.get('http://example.com/item');
expect(typeof jqxhr).toBe('object'); expect(typeof promise).toBe('object');
expect(typeof jqxhr.done).toBe('function');
expect(typeof jqxhr.fail).toBe('function');
expect(typeof jqxhr.always).toBe('function');
}); });
it('should send a DELETE request to an endpoint', () => { it('should send a DELETE request to an endpoint', () => {
@ -94,11 +91,10 @@ describe('SilverStripeBackend', () => {
backend.delete('http://example.com', deleteData); backend.delete('http://example.com', deleteData);
expect($.ajax).toBeCalledWith({ expect(backend.fetch).toBeCalledWith(
type: 'DELETE', 'http://example.com',
url: 'http://example.com', {method: 'delete', body: deleteData, credentials: 'same-origin'}
data: deleteData );
});
}); });
}); });

View File

@ -39,15 +39,15 @@
"blueimp-tmpl": "^1.0.2", "blueimp-tmpl": "^1.0.2",
"bootstrap": "^4.0.0-alpha.2", "bootstrap": "^4.0.0-alpha.2",
"deep-freeze": "0.0.1", "deep-freeze": "0.0.1",
"es6-promise": "^3.1.2",
"isomorphic-fetch": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
"jquery-sizes": "^0.33.0", "jquery-sizes": "^0.33.0",
"npm-shrinkwrap": "^5.4.1",
"page.js": "^4.13.3", "page.js": "^4.13.3",
"react": "^0.14.7", "react": "^0.14.7",
"react-addons-css-transition-group": "^0.14.7", "react-addons-css-transition-group": "^0.14.7",
"react-addons-test-utils": "^0.14.7",
"react-dom": "^0.14.7", "react-dom": "^0.14.7",
"react-redux": "^4.0.6", "react-redux": "^4.0.6",
"redux": "^3.3.1", "redux": "https://registry.npmjs.org/redux/-/redux-3.0.5.tgz",
"redux-thunk": "^1.0.3", "redux-thunk": "^1.0.3",
"tinymce": "^4.3.3" "tinymce": "^4.3.3"
}, },