React port nearing completetion. Adding tests

This commit is contained in:
Colin McLeod
2016-01-27 00:52:57 -08:00
parent 8227a4e361
commit b42a812a45
23 changed files with 362 additions and 298 deletions

View File

@@ -10,6 +10,8 @@ Coriolis was created using assets and imagery from Elite: Dangerous, with the pe
## Contributing
Chat to us on [HipChat](https://www.hipchat.com/gfYQiZcmy)!
Please [submit issues](https://github.com/cmmcleod/coriolis/issues), or better yet [pull requests](https://github.com/cmmcleod/coriolis/pulls) for any corrections or additions to the database or the code.
### Feature Requests, Suggestions & Bugs

View File

@@ -16,7 +16,7 @@
"Miner": "25A5A5A4D4A5A5C0s0s24242l2l---04054a1q02022o27"
},
"cobra_mk_iii": {
"Example": "24A4A4A3D3A3A4C0s0s2d2d0m0445032b2o2753.AwRj4yKA.CwBhEYyrKhmMQ===",
"Example": "24A4A4A3D3A3A4C0s0s2d2d0m0445032b2o2753.AwRj4yKA.CwBhEYyrKhmMQ==="
},
"imperial_clipper": {
"Cargo": "03A5D5A5D4D5D4C--0s0s----0605450302020101",

View File

@@ -1,196 +0,0 @@
xdescribe('Import Controller', function() {
beforeEach(module('app'));
var importController, $rootScope, $stateParams, scope;
var eventStub = {
preventDefault: function(){ },
stopPropagation: function(){ }
};
beforeEach(inject(function(_$rootScope_, $controller) {
$rootScope = _$rootScope_;
$rootScope.discounts = {
ship: 1,
components: 1
};
$stateParams = { };
scope = $rootScope.$new();
scope.$parent.dismiss = function() {};
var store = {};
spyOn(localStorage, 'getItem').and.callFake(function (key) {
return store[key];
});
spyOn(localStorage, 'setItem').and.callFake(function (key, value) {
return store[key] = value + '';
});
spyOn(localStorage, 'clear').and.callFake(function () {
store = {};
});
importController = $controller('ImportController', { $rootScope: $rootScope, $scope: scope, $stateParams: $stateParams });
}));
describe('Import Backup', function() {
it('imports a valid backup', function() {
var importData = __json__['fixtures/valid-backup'];
scope.importString = angular.toJson(importData);
scope.validateImport();
expect(scope.importValid).toBeTruthy();
expect(scope.errorMsg).toEqual(null);
scope.process();
expect(scope.processed).toBeTruthy();
scope.import();
expect(angular.fromJson(localStorage.getItem('builds'))).toEqual(importData.builds);
expect(angular.fromJson(localStorage.getItem('comparisons'))).toEqual(importData.comparisons);
expect(localStorage.getItem('insurance')).toEqual(importData.insurance);
expect(angular.fromJson(localStorage.getItem('discounts'))).toEqual(importData.discounts);
});
it('imports an old valid backup', function() {
var importData = __json__['fixtures/old-valid-export'];
scope.importString = angular.toJson(importData);
scope.validateImport();
expect(scope.importValid).toBeTruthy();
expect(scope.errorMsg).toEqual(null);
scope.process();
expect(scope.processed).toBeTruthy();
scope.import();
expect(angular.fromJson(localStorage.getItem('builds'))).toEqual(importData.builds);
});
it('catches an invalid backup', function() {
var importData = __json__['fixtures/valid-backup'];
scope.importString = 'null';
scope.validateImport();
expect(scope.importValid).toBeFalsy();
expect(scope.errorMsg).toEqual('Must be an object or array!');
scope.importString = '{ "builds": "Should not be a string" }';
scope.validateImport();
expect(scope.importValid).toBeFalsy();
expect(scope.errorMsg).toEqual('builds must be an object!');
scope.importString = angular.toJson(importData).replace('anaconda', 'invalid_ship');
scope.validateImport();
expect(scope.importValid).toBeFalsy();
expect(scope.errorMsg).toEqual('"invalid_ship" is not a valid Ship Id!');
scope.importString = angular.toJson(importData).replace('Dream', '');
scope.validateImport();
expect(scope.importValid).toBeFalsy();
expect(scope.errorMsg).toEqual('Imperial Clipper build "" must be a string at least 1 character long!');
invalidImportData = angular.copy(importData);
invalidImportData.builds.asp = null; // Remove Asp Miner build used in comparison
scope.importString = angular.toJson(invalidImportData);
scope.validateImport();
expect(scope.importValid).toBeFalsy();
expect(scope.errorMsg).toEqual('asp build "Miner" data is missing!');
});
});
describe('Import Detailed Build', function() {
it('imports a valid v1 build', function() {
var importData = __json__['fixtures/anaconda-test-detailed-export-v1'];
scope.importString = angular.toJson(importData);
scope.validateImport();
expect(scope.importValid).toBeTruthy();
expect(scope.errorMsg).toEqual(null);
scope.process();
expect(scope.processed).toBeTruthy();
scope.import();
expect(angular.fromJson(localStorage.getItem('builds'))).toEqual({
anaconda: { 'Test': '48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.Iw18ZlA=.Aw18ZlA=' }
});
});
it('imports a valid v2 build', function() {
var importData = __json__['fixtures/anaconda-test-detailed-export-v2'];
scope.importString = angular.toJson(importData);
scope.validateImport();
expect(scope.importValid).toBeTruthy();
expect(scope.errorMsg).toEqual(null);
scope.process();
expect(scope.processed).toBeTruthy();
scope.import();
expect(angular.fromJson(localStorage.getItem('builds'))).toEqual({
anaconda: { 'Test': '48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA' }
});
});
it('catches an invalid build', function() {
var importData = __json__['fixtures/anaconda-test-detailed-export-v2'];
scope.importString = angular.toJson(importData).replace('components', 'comps');
scope.validateImport();
expect(scope.importValid).toBeFalsy();
expect(scope.errorMsg).toEqual('Anaconda Build "Test": Invalid data');
});
});
describe('Import Detaild Builds Array', function() {
it('imports all builds', function() {
var importData = __json__['fixtures/valid-detailed-export'];
var expectedBuilds = __json__['fixtures/expected-builds'];
scope.importString = angular.toJson(importData);
scope.validateImport();
expect(scope.importValid).toBeTruthy();
expect(scope.errorMsg).toEqual(null);
scope.process();
expect(scope.processed).toBeTruthy();
scope.import();
var builds = angular.fromJson(localStorage.getItem('builds'));
for (var s in builds) {
for (var b in builds[s]) {
expect(builds[s][b]).toEqual(expectedBuilds[s][b]);
}
}
});
});
describe('Import E:D Shipyard Builds', function() {
it('imports a valid builds', function() {
var imports = __json__['fixtures/ed-shipyard-import-valid'];
for (var i = 0; i < imports.length; i++ ) {
scope.importString = imports[i].buildText;
scope.validateImport();
expect(scope.importValid).toBeTruthy();
expect(scope.errorMsg).toEqual(null, 'Build #' + i + ': ' + imports[i].buildName);
if (scope.importValid) { // No point in carrying out other assertions
scope.process();
expect(scope.processed).toBeTruthy();
scope.import();
var allBuilds = angular.fromJson(localStorage.getItem('builds'));
var shipBuilds = allBuilds ? allBuilds[imports[i].shipId] : null;
var build = shipBuilds ? shipBuilds[imports[i].buildName] : null;
expect(build).toEqual(imports[i].buildCode, 'Build #' + i + ': ' + imports[i].buildName);
}
}
});
it('catches invalid builds', function() {
var imports = __json__['fixtures/ed-shipyard-import-invalid'];
for (var i = 0; i < imports.length; i++ ) {
scope.importString = imports[i].buildText;
scope.validateImport();
expect(scope.importValid).toBeFalsy();
expect(scope.errorMsg).toEqual(imports[i].errorMsg);
}
});
});
});

218
__tests__/test-import.js Normal file
View File

@@ -0,0 +1,218 @@
jest.dontMock('../src/app/stores/Persist');
jest.dontMock('../src/app/components/TranslatedComponent');
jest.dontMock('../src/app/components/ModalImport');
import React from 'react';
import ReactDOM from 'react-dom';
import TU from 'react-testutils-additions';
import Utils from './testUtils';
import Persist from '../src/app/stores/Persist';
import { getLanguage } from '../src/app/i18n/Language';
describe('Import Controller', function() {
const Persist = require('../src/app/stores/Persist').default;
const ModalImport = require('../src/app/components/ModalImport').default;
const mockContext = {
language: getLanguage('en'),
sizeRatio: 1,
openMenu: jest.genMockFunction(),
closeMenu: jest.genMockFunction(),
showModal: jest.genMockFunction(),
hideModal: jest.genMockFunction(),
tooltip: jest.genMockFunction(),
termtip: jest.genMockFunction(),
onWindowResize: jest.genMockFunction()
};
let modal, render, ContextProvider = Utils.createContextProvider(mockContext);
/**
* Clear saved builds, and reset React DOM
*/
function reset() {
Persist.deleteAll();
render = TU.renderIntoDocument(<ContextProvider><ModalImport /></ContextProvider>);
modal = TU.findRenderedComponentWithType(render, ModalImport);
}
/**
* Simulate user import text entry / paste
* @param {string} text Import text / raw data
*/
function pasteText(text) {
let textarea = TU.findRenderedDOMComponentWithTag(render, 'textarea');
TU.Simulate.change(textarea, { target: { value: text } });
}
/**
* Simulate click on Proceed button
*/
function clickProceed() {
let proceedButton = TU.findRenderedDOMComponentWithId(render, 'proceed');
TU.Simulate.click(proceedButton);
}
/**
* Simulate click on Import button
*/
function clickImport() {
let importButton = TU.findRenderedDOMComponentWithId(render, 'import');
TU.Simulate.click(importButton);
}
describe('Import Backup', function() {
beforeEach(reset);
it('imports a valid backup', function() {
let importData = require('./fixtures/valid-backup');
let importString = JSON.stringify(importData);
expect(modal.state.importValid).toEqual(false);
expect(modal.state.errorMsg).toEqual(null);
pasteText(importString);
expect(modal.state.importValid).toBe(true);
expect(modal.state.errorMsg).toEqual(null);
expect(modal.state.builds).toEqual(importData.builds);
expect(modal.state.comparisons).toEqual(importData.comparisons);
expect(modal.state.discounts).toEqual(importData.discounts);
expect(modal.state.insurance).toBe(importData.insurance.toLowerCase());
clickProceed();
expect(modal.state.processed).toBe(true);
expect(modal.state.errorMsg).toEqual(null);
clickImport();
expect(Persist.getBuilds()).toEqual(importData.builds);
expect(Persist.getComparisons()).toEqual(importData.comparisons);
expect(Persist.getInsurance()).toEqual(importData.insurance.toLowerCase());
expect(Persist.getShipDiscount()).toEqual(importData.discounts[0]);
expect(Persist.getModuleDiscount()).toEqual(importData.discounts[1]);
});
it('imports an old valid backup', function() {
let importData = require('./fixtures/old-valid-export');
let importStr = JSON.stringify(importData);
pasteText(importStr);
expect(modal.state.builds).toEqual(importData.builds);
expect(modal.state.importValid).toBe(true);
expect(modal.state.errorMsg).toEqual(null);
clickProceed();
expect(modal.state.processed).toBeTruthy();
clickImport();
expect(Persist.getBuilds()).toEqual(importData.builds);
});
it('catches an invalid backup', function() {
let importData = require('./fixtures/valid-backup');
let invalidImportData = Object.assign({}, importData);
invalidImportData.builds.asp = null; // Remove Asp Miner build used in comparison
pasteText('"this is not valid"');
expect(modal.state.importValid).toBeFalsy();
expect(modal.state.errorMsg).toEqual('Must be an object or array!');
pasteText('{ "builds": "Should not be a string" }');
expect(modal.state.importValid).toBeFalsy();
expect(modal.state.errorMsg).toEqual('builds must be an object!');
pasteText(JSON.stringify(importData).replace('anaconda', 'invalid_ship'));
expect(modal.state.importValid).toBeFalsy();
expect(modal.state.errorMsg).toEqual('"invalid_ship" is not a valid Ship Id!');
pasteText(JSON.stringify(importData).replace('Dream', ''));
expect(modal.state.importValid).toBeFalsy();
expect(modal.state.errorMsg).toEqual('Imperial Clipper build "" must be a string at least 1 character long!');
pasteText(JSON.stringify(invalidImportData));
expect(modal.state.importValid).toBeFalsy();
expect(modal.state.errorMsg).toEqual('asp build "Miner" data is missing!');
});
});
describe('Import Detailed Build', function() {
beforeEach(reset);
it('imports a valid v3 build', function() {
let importData = require('./fixtures/anaconda-test-detailed-export-v3');
pasteText(JSON.stringify(importData));
expect(modal.state.importValid).toBeTruthy();
expect(modal.state.errorMsg).toEqual(null);
clickProceed();
expect(modal.state.processed).toBeTruthy();
clickImport();
expect(Persist.getBuilds()).toEqual({
anaconda: { 'Test': '48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA' }
});
});
it('catches an invalid build', function() {
let importData = require('./fixtures/anaconda-test-detailed-export-v3');
pasteText(JSON.stringify(importData).replace('components', 'comps'));
expect(modal.state.importValid).toBeFalsy();
expect(modal.state.errorMsg).toEqual('Anaconda Build "Test": Invalid data');
});
});
describe('Import Detaild Builds Array', function() {
beforeEach(reset);
it('imports all builds', function() {
let importData = require('./fixtures/valid-detailed-export');
let expectedBuilds = require('./fixtures/expected-builds');
pasteText(JSON.stringify(importData));
expect(modal.state.importValid).toBeTruthy();
expect(modal.state.errorMsg).toEqual(null);
clickProceed();
expect(modal.state.processed).toBeTruthy();
clickImport();
let builds = Persist.getBuilds();
for (let s in builds) {
for (let b in builds[s]) {
expect(builds[s][b]).toEqual(expectedBuilds[s][b]);
}
}
});
});
describe('Import E:D Shipyard Builds', function() {
it('imports a valid builds', function() {
let imports = require('./fixtures/ed-shipyard-import-valid');
for (let i = 0; i < imports.length; i++ ) {
reset();
pasteText(imports[i].buildText);
expect(modal.state.importValid).toBeTruthy();
expect(modal.state.errorMsg).toEqual(null, 'Build #' + i + ': ' + imports[i].buildName);
clickProceed();
expect(modal.state.processed).toBeTruthy();
clickImport();
let allBuilds = Persist.getBuilds();
let shipBuilds = allBuilds ? allBuilds[imports[i].shipId] : null;
let build = shipBuilds ? shipBuilds[imports[i].buildName] : null;
expect(build).toEqual(imports[i].buildCode, 'Build #' + i + ': ' + imports[i].buildName);
}
});
it('catches invalid builds', function() {
let imports = require('./fixtures/ed-shipyard-import-invalid');
for (let i = 0; i < imports.length; i++ ) {
reset();
pasteText(imports[i].buildText);
expect(modal.state.importValid).toBeFalsy();
expect(modal.state.errorMsg).toEqual(imports[i].errorMsg);
}
});
});
});

24
__tests__/testUtils.js Normal file
View File

@@ -0,0 +1,24 @@
import React from 'react';
const TestUtils = {
createContextProvider: function(context) {
var _contextTypes = {};
Object.keys(context).forEach(function(key) {
_contextTypes[key] = React.PropTypes.any;
});
return React.createClass({
displayName: 'ContextProvider',
childContextTypes: _contextTypes,
getChildContext() { return context; },
render() {
return React.Children.only(this.props.children);
}
});
}
};
export default TestUtils;

View File

@@ -5,6 +5,7 @@ var config = require('./webpack.config.dev');
new WebpackDevServer(webpack(config), {
publicPath: config.output.publicPath,
hot: true,
headers: { "Access-Control-Allow-Origin": "*" },
historyApiFallback: {
rewrites: [
// For some reason connect-history-api-fallback does not allow '.' in the URL for history fallback...

View File

@@ -36,6 +36,7 @@
"<rootDir>/node_modules/react",
"<rootDir>/node_modules/react-dom",
"<rootDir>/node_modules/react-addons-test-utils",
"<rootDir>/node_modules/react-testutils-additions",
"<rootDir>/node_modules/fbjs",
"<rootDir>/node_modules/fbemitter",
"<rootDir>/node_modules/classnames",
@@ -44,7 +45,8 @@
"<rootDir>/node_modules/coriolis-data",
"<rootDir>/src/app/shipyard",
"<rootDir>/src/app/i18n",
"<rootDir>/src/app/utils"
"<rootDir>/src/app/utils",
"<rootDir>/__tests__"
]
},
"devDependencies": {
@@ -69,6 +71,7 @@
"less": "^2.5.3",
"less-loader": "^2.2.1",
"react-addons-test-utils": "^0.14.6",
"react-testutils-additions": "^0.16.0",
"rimraf": "^2.4.3",
"style-loader": "^0.13.0",
"url-loader": "^0.5.6",

View File

@@ -238,13 +238,11 @@ export default class Coriolis extends React.Component {
* @return {React.Component} The main app
*/
render() {
return (
<div onClick={this._closeMenu}>
return <div onClick={this._closeMenu}>
<Header appCacheUpdate={this.state.appCacheUpdate} currentMenu={this.state.currentMenu} />
{ this.state.page ? <this.state.page currentMenu={this.state.currentMenu} /> : <NotFoundPage/> }
{ this.state.modal }
{ this.state.tooltip }
</div>
);
</div>;
}
}

View File

@@ -5,6 +5,7 @@ import cn from 'classnames';
/**
* Returns true if the current window location equals the link
* @param {string} href URL/Href
* @return {boolean} If matches
*/
function isActive(href) {

View File

@@ -613,6 +613,7 @@ export default class CostSection extends TranslatedComponent {
if (nextProps.ship != this.props.ship || nextProps.code != this.props.code) {
this._updateAmmoCosts(nextProps.ship);
this._updateRetrofit(nextProps.ship, retrofitShip);
this.setState({ total: nextProps.ship.totalCost });
this._sortCost(nextProps.ship);
}
}

View File

@@ -260,7 +260,7 @@ export default class Header extends TranslatedComponent {
<br/>
<span className='cap ptr' onClick={this._toggleTooltips} >
{translate('tooltips')}
<div className={cn({ disabled: !tips, 'primary-disabled': tips })} style={{ marginLeft: '0.5em', display: 'inline-block' }}>{(tips ? '' : '')}</div>
<div className={cn({ disabled: !tips, 'primary-disabled': tips })} style={{ marginLeft: '0.5em', display: 'inline-block' }}>{(tips ? '' : '')}</div>
</span>
<br/>
{translate('insurance')}

View File

@@ -88,10 +88,13 @@ export default class LineChart extends TranslatedComponent {
y0 = func(x0),
tips = this.tipContainer,
yTotal = 0,
flip = (xPos / innerWidth > 0.65),
flip = (xPos / innerWidth > 0.60),
tipWidth = 0,
tipHeightPx = tips.selectAll('rect').node().getBoundingClientRect().height;
xPos = xScale(x0); // Clamp xPos
tips.selectAll('text.label.y').text(function(d, i) {
let yVal = series ? y0[series[i]] : y0;
yTotal += yVal;
@@ -110,7 +113,7 @@ export default class LineChart extends TranslatedComponent {
tips.attr('transform', 'translate(' + xPos + ',' + tipY + ')');
tips.selectAll('text.label').attr('x', flip ? -12 : 12).style('text-anchor', flip ? 'end' : 'start');
tips.selectAll('text.label.x').text(formats.f2(x0)).append('tspan').attr('class', 'metric').text(' ' + xUnit);
tips.selectAll('rect').attr('width', tipWidth + 4).attr('x', flip ? -tipWidth - 12 : 8).style('text-anchor', flip ? 'end' : 'start');
tips.selectAll('rect').attr('width', tipWidth + 4).attr('x', flip ? -tipWidth - 12 : 8).attr('y', 0).style('text-anchor', flip ? 'end' : 'start');
this.markersContainer.selectAll('circle').attr('cx', xPos).attr('cy', (d, i) => yScale(series ? y0[series[i]] : y0));
}
@@ -136,9 +139,10 @@ export default class LineChart extends TranslatedComponent {
* @param {SyntheticEvent} e Event
*/
_showTip(e) {
this._moveTip(e);
e.preventDefault();
this.tipContainer.style('display', null);
this.markersContainer.style('display', null);
this._moveTip(e);
}
/**
@@ -146,13 +150,16 @@ export default class LineChart extends TranslatedComponent {
* @param {SyntheticEvent} e Event
*/
_moveTip(e) {
this._tooltip(Math.round(e.clientX - e.target.getBoundingClientRect().left));
let clientX = e.touches ? e.touches[0].clientX : e.clientX;
this._tooltip(Math.round(clientX - e.currentTarget.getBoundingClientRect().left));
}
/**
* Hide tooltip
* @param {SyntheticEvent} e Event
*/
_hideTip() {
_hideTip(e) {
e.preventDefault();
this.tipContainer.style('display', 'none');
this.markersContainer.style('display', 'none');
}
@@ -238,8 +245,8 @@ export default class LineChart extends TranslatedComponent {
<tspan className='metric'>{` (${yUnit})`}</tspan>
</text>
</g>
<g ref={(g) => this.tipContainer = d3.select(g)} className='tooltip' style={{ display: 'none' }}>
<rect className='tip' style={{ height: tipHeight + 'em' }}></rect>
<g ref={(g) => this.tipContainer = d3.select(g)} style={{ display: 'none' }}>
<rect className='tooltip' height={tipHeight + 'em'}></rect>
{detailElems}
</g>
<g ref={(g) => this.markersContainer = d3.select(g)} style={{ display: 'none' }}>

View File

@@ -4,7 +4,7 @@ import TranslatedComponent from './TranslatedComponent';
import Persist from '../stores/Persist';
import { Ships } from 'coriolis-data';
import Ship from '../shipyard/Ship';
import { ModuleNameToGroup } from '../shipyard/Constants';
import { ModuleNameToGroup, Insurance } from '../shipyard/Constants';
import * as ModuleUtils from '../shipyard/ModuleUtils';
import { fromDetailedBuild } from '../shipyard/Serializer';
import { Download } from './SvgIcons';
@@ -146,8 +146,14 @@ export default class ModalImport extends TranslatedComponent {
if (importData.discounts instanceof Array && importData.discounts.length == 2) {
this.setState({ discounts: importData.discounts });
}
if (typeof importData.insurance == 'string' && importData.insurance.length > 3) {
this.setState({ insurance: importData.insurance });
if (typeof importData.insurance == 'string') {
let insurance = importData.insurance.toLowerCase();
if (Insurance[insurance] !== undefined) {
this.setState({ insurance });
} else {
throw 'Invalid insurance type: ' + insurance;
}
}
}
@@ -220,7 +226,7 @@ export default class ModalImport extends TranslatedComponent {
if (!slot) { throw 'No hardpoint slot available for: "' + line + '"'; }
group = ModuleNameToGroup[name.trim()];
group = ModuleNameToGroup[name.toLowerCase()];
let hp = ModuleUtils.findHardpoint(group, cl, rating, group ? null : name, mount, missile);
@@ -238,7 +244,7 @@ export default class ModalImport extends TranslatedComponent {
if (ship.standard[standardIndex].maxClass < cl) { throw name + ' exceeds max class for the ' + ship.name; }
ship.use(ship.standard[standardIndex], cl + rating, ModuleUtils.standard(standardIndex, cl + rating), true);
ship.use(ship.standard[standardIndex], ModuleUtils.standard(standardIndex, cl + rating), true);
} else {
throw 'Unknown component: "' + line + '"';
}
@@ -249,13 +255,13 @@ export default class ModalImport extends TranslatedComponent {
if (!slot) { throw 'No internal slot available for: "' + line + '"'; }
group = ModuleNameToGroup[name.trim()];
group = ModuleNameToGroup[name.toLowerCase()];
let intComp = ModuleUtils.findInternal(group, cl, rating, group ? null : name);
if (!intComp) { throw 'Unknown component: "' + line + '"'; }
ship.use(slot, intComp.id, intComp);
ship.use(slot, intComp);
}
}
@@ -272,7 +278,7 @@ export default class ModalImport extends TranslatedComponent {
*/
_validateImport(event) {
let importData = null;
let importString = event.target.value;
let importString = event.target.value.trim();
this.setState({
builds: null,
comparisons: null,
@@ -334,7 +340,7 @@ export default class ModalImport extends TranslatedComponent {
}
if (this.state.comparisons) {
let comparisons = this.state.comparisons;
comparisons = this.state.comparisons;
for (let name in comparisons) {
comparisons[name].useName = name;
}
@@ -371,8 +377,9 @@ export default class ModalImport extends TranslatedComponent {
}
}
if (this.state.discounts) {
Persist.setDiscount(this.state.discounts);
if (this.state.discounts && this.state.discounts.length == 2) {
Persist.setShipDiscount(this.state.discounts[0]);
Persist.setModuleDiscount(this.state.discounts[1]);
}
if (this.state.insurance) {
@@ -384,11 +391,11 @@ export default class ModalImport extends TranslatedComponent {
/**
* Capture build name changes
* @param {Object} build Build import object
* @param {Object} item Build/Comparison import object
* @param {SyntheticEvent} e Event
*/
_changeBuildName(build, e) {
build.useName = e.target.value;
_changeName(item, e) {
item.useName = e.target.value;
this.forceUpdate();
}
@@ -415,7 +422,7 @@ export default class ModalImport extends TranslatedComponent {
importStage = (
<div>
<textarea className='cb json' onChange={this._validateImport} defaultValue={this.state.importString} placeholder={translate('PHRASE_IMPORT')} />
<button className='l cap' onClick={this._process} disabled={!state.importValid} >{translate('proceed')}</button>
<button id='proceed' className='l cap' onClick={this._process} disabled={!state.importValid} >{translate('proceed')}</button>
<div className='l warning' style={{ marginLeft:'3em' }}>{state.errorMsg}</div>
</div>
);
@@ -426,14 +433,14 @@ export default class ModalImport extends TranslatedComponent {
for (let name in state.comparisons) {
let comparison = state.comparisons[name];
let hasComparison = Persist.hasComparison(name);
let hasComparison = Persist.hasComparison(comparison.useName);
comparisonRows.push(
<tr key={name} className='cb'>
<td>
<input type='text' value={comparison.useName}/>
<input type='text' onChange={this._changeName.bind(this, comparison)} value={comparison.useName}/>
</td>
<td style={{ textAlign:'center' }} className={ cn('cap', { warning: hasComparison, disabled: comparison.useName == '' }) }>
{translate(comparison.useName == '' ? 'skip' : (hasComparison ? 'overwrite' : 'create'))}>
{translate(comparison.useName == '' ? 'skip' : (hasComparison ? 'overwrite' : 'create'))}
</td>
</tr>
);
@@ -467,7 +474,7 @@ export default class ModalImport extends TranslatedComponent {
buildRows.push(
<tr key={shipId + buildName} className='cb'>
<td>{Ships[shipId].properties.name}</td>
<td><input type='text' onChange={this._changeBuildName.bind(this, b)} value={b.useName}/></td>
<td><input type='text' onChange={this._changeName.bind(this, b)} value={b.useName}/></td>
<td style={{ textAlign: 'center' }} className={cn('cap', { warning: hasBuild, disabled: b.useName == '' })}>
{translate(b.useName == '' ? 'skip' : (hasBuild ? 'overwrite' : 'create'))}
</td>
@@ -491,7 +498,7 @@ export default class ModalImport extends TranslatedComponent {
</tbody>
</table>
{comparisonTable}
<button className='cl l' onClick={this._import}><Download/> {translate('import')}</button>
<button id='import' className='cl l' onClick={this._import}><Download/> {translate('import')}</button>
{edit}
</div>
);

View File

@@ -130,9 +130,9 @@ export default class PowerManagement extends TranslatedComponent {
<td className='ptr le shorten cap' onClick={toggleEnabled}>{slotName(translate, slot)}</td>
<td className='ptr' onClick={toggleEnabled}><u>{translate(slot.type)}</u></td>
<td>
<span className='flip ptr' onClick={this._priority.bind(this, slot, -1)}>&#9658;</span>
<span className='flip ptr btn' onClick={this._priority.bind(this, slot, -1)}>&#9658;</span>
{' ' + (slot.priority + 1) + ' '}
<span className='ptr' onClick={this._priority.bind(this, slot, 1)}>&#9658;</span>
<span className='ptr btn' onClick={this._priority.bind(this, slot, 1)}>&#9658;</span>
</td>
<td className='ri ptr' style={{ width: '3.25em' }} onClick={toggleEnabled}>{pwr(m.power)}</td>
<td className='ri ptr' style={{ width: '3em' }} onClick={toggleEnabled}><u>{pct(m.power / ship.powerAvailable)}</u></td>

View File

@@ -32,29 +32,35 @@ export default class Slider extends React.Component {
/**
* On Mouse down handler
* @param {SyntheticEvent} e Event
* @param {SyntheticEvent} event Event
*/
down(e) {
let rect = e.currentTarget.getBoundingClientRect();
down(event) {
if (this.move) {
this.up(event);
} else {
let rect = event.currentTarget.getBoundingClientRect();
this.move = this._updatePercent.bind(this, rect.left, rect.width);
this.move(e);
document.addEventListener('mousemove', this.move);
document.addEventListener('mouseup', this.up);
this.move(event);
document.addEventListener('mousemove', this.move, true);
document.addEventListener('mouseup', this.up, true);
}
}
/**
* On Mouse up handler
* @param {Event} event DOM Event
*/
up() {
document.removeEventListener('mousemove', this.move);
document.removeEventListener('mouseup', this.up);
up(event) {
document.removeEventListener('mousemove', this.move, true);
document.removeEventListener('mouseup', this.up, true);
this.move = null;
}
/**
* Update the slider percentage
* @param {number} left Slider left position
* @param {number} width Slider width
* @param {Event} event Event
* @param {Event} event DOM Event
*/
_updatePercent(left, width, event) {
this.props.onChange(Math.min(Math.max((event.clientX - left) / width, 0), 1));
@@ -88,7 +94,7 @@ export default class Slider extends React.Component {
<rect className='primary' style={{ opacity: 0.3 }} y='0.25em' rx='0.3em' ry='0.3em' width='100%' height='0.7em' />
<rect className='primary-disabled'y='0.45em' rx='0.15em' ry='0.15em' width={pctStr} height='0.3em' />
<circle className='primary' r='0.6em' cy='0.6em' cx={pctStr} />
<rect width='100%' height='100%' fillOpacity='0' onMouseDown={this.down}/>
<rect width='100%' height='100%' fillOpacity='0' onMouseDown={this.down} onClick={this.click} />
{axisGroup}
</svg>;
}

View File

@@ -328,10 +328,10 @@ export default class OutfittingPage extends Page {
</div>
<div className='group half'>
<table style={{ width: '100%', lineHeight: '1em' }}>
<table style={{ width: '100%', lineHeight: '1em', backgroundColor: 'transparent' }}>
<tbody >
<tr>
<td style={{ verticalAlign: 'top', padding:0 }}><Fuel className='xl primary-disabled' /></td>
<td style={{ verticalAlign: 'top', padding: 0, width: '2.5em' }}><Fuel className='xl primary-disabled' /></td>
<td><Slider axis={true} onChange={this._fuelChange} axisUnit={translate('T')} percent={fuelLevel} max={fuelCapacity} /></td>
<td className='primary' style={{ width: '10em', verticalAlign: 'top', fontSize: '0.9em' }}>{formats.f2(fuelLevel * fuelCapacity)}{units.T} {formats.pct1(fuelLevel)}</td>
</tr>

View File

@@ -415,12 +415,10 @@ export default class Ship {
this.updatePower()
.updateJumpStats()
.updateShieldStrength()
.updateTopSpeed()
.updatePowerPrioritesString()
.updatePowerEnabledString();
.updateTopSpeed();
}
return this;
return this.updatePowerPrioritesString().updatePowerEnabledString();
}
/**
@@ -570,12 +568,12 @@ export default class Ship {
if (newPriority >= 0 && newPriority < this.priorityBands.length) {
let oldPriority = slot.priority;
slot.priority = newPriority;
this.updatePowerPrioritesString();
if (slot.enabled) { // Only update power if the slot is enabled
let usage = powerUsageType(slot, slot.m);
this.priorityBands[oldPriority][usage] -= slot.m.power;
this.priorityBands[newPriority][usage] += slot.m.power;
this.updatePowerPrioritesString();
this.updatePower();
}
return true;

View File

@@ -14,8 +14,8 @@ let LS;
// Safe check to determine if localStorage is enabled
try {
localStorage.setItem('s', 1);
localStorage.removeItem('s');
localStorage.setItem('test_string', 1);
localStorage.removeItem('test_string');
LS = localStorage;
} catch(e) {
LS = null;
@@ -38,7 +38,7 @@ function _put(key, value) {
* @return {string} The stored string
*/
function _getString(key) {
return LS.getItem(key);
return LS ? LS.getItem(key) : null;
}
/**
@@ -80,7 +80,7 @@ class Persist extends EventEmitter {
this.comparisons = comparisonJson ? comparisonJson : {};
this.buildCount = Object.keys(this.builds).length;
this.langCode = _getString(LS_KEY_LANG) || 'en';
this.insurance = _getString(LS_KEY_INSURANCE);
this.insurance = _getString(LS_KEY_INSURANCE) || 'standard';
this.discounts = _get(LS_KEY_DISCOUNTS) || [1, 1];
this.costTab = _getString(LS_KEY_COST_TAB);
this.state = _get(LS_KEY_STATE);
@@ -129,16 +129,13 @@ class Persist extends EventEmitter {
* @param {string} code The serialized code
*/
saveBuild(shipId, name, code) {
if (LS) {
if (!this.builds[shipId]) {
this.builds[shipId] = {};
}
let newBuild = !this.builds[shipId][name];
this.builds[shipId][name] = code;
_put(LS_KEY_BUILDS, this.builds);
this.emit('buildSaved', shipId, name, code);
}
};
/**
* Get the serialized code/string for a build. Returns null if a
@@ -153,7 +150,7 @@ class Persist extends EventEmitter {
return this.builds[shipId][name];
}
return null;
};
}
/**
* Get all builds (object) or builds for a specific ship (array)
@@ -225,7 +222,7 @@ class Persist extends EventEmitter {
_put(LS_KEY_COMPARISONS, this.comparisons);
this.emit('buildDeleted', shipId, name);
}
};
}
/**
* Persist a comparison in localstorage.
@@ -244,7 +241,7 @@ class Persist extends EventEmitter {
};
_put(LS_KEY_COMPARISONS, this.comparisons);
this.emit('comparisons', this.comparisons);
};
}
/**
* [getComparison description]
@@ -256,7 +253,7 @@ class Persist extends EventEmitter {
return this.comparisons[name];
}
return null;
};
}
/**
* Get all saved comparisons
@@ -293,7 +290,7 @@ class Persist extends EventEmitter {
_put(LS_KEY_COMPARISONS, this.comparisons);
this.emit('comparisons', this.comparisons);
}
};
}
/**
* Delete all builds and comparisons from localStorage
@@ -304,7 +301,7 @@ class Persist extends EventEmitter {
_delete(LS_KEY_BUILDS);
_delete(LS_KEY_COMPARISONS);
this.emit('deletedAll');
};
}
/**
* Get all saved data and settings
@@ -317,25 +314,25 @@ class Persist extends EventEmitter {
data[LS_KEY_INSURANCE] = this.getInsurance();
data[LS_KEY_DISCOUNTS] = this.discounts;
return data;
};
}
/**
* Get the saved insurance type
* @return {string} The name of the saved insurance type of null
*/
getInsurance() {
return this.insurance;
};
return this.insurance.toLowerCase();
}
/**
* Persist selected insurance type
* @param {string} insurance Insurance type name
*/
setInsurance(insurance) {
this.insurance = insurance;
this.insurance = insurance.toLowerCase();
_put(LS_KEY_INSURANCE, insurance);
this.emit('insurance', insurance);
};
}
/**
* Persist selected ship discount
@@ -345,7 +342,7 @@ class Persist extends EventEmitter {
this.discounts[0] = shipDiscount;
_put(LS_KEY_DISCOUNTS, this.discounts);
this.emit('discounts', this.discounts);
};
}
/**
* Get the saved ship discount
@@ -363,7 +360,7 @@ class Persist extends EventEmitter {
this.discounts[1] = moduleDiscount;
_put(LS_KEY_DISCOUNTS, this.discounts);
this.emit('discounts', this.discounts);
};
}
/**
* Get the saved ship discount
@@ -371,7 +368,7 @@ class Persist extends EventEmitter {
*/
getModuleDiscount() {
return this.discounts[1];
};
}
/**
* Persist selected cost tab
@@ -380,7 +377,7 @@ class Persist extends EventEmitter {
setCostTab(tabName) {
this.costTab = tabName;
_put(LS_KEY_COST_TAB, tabName);
};
}
/**
* Get the saved discount
@@ -388,7 +385,7 @@ class Persist extends EventEmitter {
*/
getCostTab() {
return this.costTab;
};
}
/**
* Retrieve the last router state from local storage
@@ -396,7 +393,7 @@ class Persist extends EventEmitter {
*/
getState() {
return this.state;
};
}
/**
* Save the current router state to localstorage
@@ -405,7 +402,7 @@ class Persist extends EventEmitter {
setState(state) {
this.state = state;
_put(LS_KEY_STATE, state);
};
}
/**
* Retrieve the last router state from local storage
@@ -413,7 +410,7 @@ class Persist extends EventEmitter {
*/
getSizeRatio() {
return this.sizeRatio;
};
}
/**
* Save the current size ratio to localstorage
@@ -425,7 +422,7 @@ class Persist extends EventEmitter {
_put(LS_KEY_SIZE_RATIO, sizeRatio);
this.emit('sizeRatio', sizeRatio);
}
};
}
/**
* Check if localStorage is enabled/active

View File

@@ -2,6 +2,7 @@
<html {%= o.htmlWebpackPlugin.options.appCache ? 'manifest=/' + o.htmlWebpackPlugin.options.appCache : '' %} >
<head>
<title>Coriolis</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="{%= o.htmlWebpackPlugin.files.css[0] %}">
<!-- Standard headers -->
<meta name="description" content="A ship builder, outfitting and comparison tool for Elite Dangerous">

View File

@@ -58,7 +58,7 @@ svg {
fill: @fg;
}
.tip {
.tooltip {
fill: @bgBlack;
stroke: @secondary;
stroke-width: 1px;

View File

@@ -17,7 +17,7 @@
transform:translate(-50%,-50%);
-webkit-transform:translate(-50%,-50%);
width: 800px;
max-height: 100%;
max-height: 90%;
padding: 2em;
background-color: @bgBlack;
box-sizing: border-box;

View File

@@ -235,14 +235,10 @@
font-size: 0.8em;
}
table tbody tr td {
&:nth-child(4) {
span {
span.btn {
vertical-align: middle;
font-size: 1.6em;
}
}
}
});
.medPhone({