Skip to content

Commit

Permalink
Shipping Rates Pre-Fetch (#385)
Browse files Browse the repository at this point in the history
* start on shipping manager refactor

* added action/reducers

* more state changes + UI updates

* progress on shipping rates

* state progress

* small state changes

* small changes to settings actions

* small changes

* pr changes

* pr changes

* Update packages/frontend/src/state/reducers/profiles/profileReducer.js

* Update packages/frontend/src/state/reducers/profiles/profileReducer.js

* rm unnecessary error state declaration

* filter names and rate input autofilled based on selection done

* small fixes

* Update packages/frontend/src/profiles/shippingRates.jsx

Co-Authored-By: walmat <matthew.wallt@gmail.com>

* Update packages/frontend/src/profiles/shippingRates.jsx

Co-Authored-By: walmat <matthew.wallt@gmail.com>

* pr changes

* some layout changes + delete/clear buttons

* added delete rate action, needs work still

* fixed delete rate action/reducer

* rm unnecessary id field

* attach settings middleware for other fields

* added webhook regex and errors map

* fixed product validator error mapping

* Store the selected shipping rate

This commit updates the checkout class to keep track of the selected shipping rate. This will be used by a subclass of the Task Runner to fetch the cheapest shipping rate.

* Refactor Run Loop to a single function

This commit updates the task runner to use a single function in the start method to handle all loop logic. This will allow subclasses to override the default behavior if needed.

* Add Shipping Rates Runner

This commit adds a new type of runner that looks for shipping rates from a specific site. This allows the rates to be "pre-fetched" and then used in actual task runs.

The ShippingRatesRunner is only supported on api checkout methods at this time. Attempting to create the shipping rates runner for a frontend checkout site will cause it to throw an error.

The run loop has been modified to stop if the super implementation errors out, or if the shipping rates have been received.

Further, all messages emitted from this runner will be attached with a special type flag. This will allow the frontend to tell that this runner is not a normal runner.

* Fix lint

This commit fixes a lint caused by an unused variable.

* small css tweaks with cursor:pointer

* Generalize Runner Type Tag

This commit updates the TaskRunner to add a type property. A set of constants has been defined for further use if needed.

Now, payloads emitted from the runner will include it's type. This prevents the need for the ShippingRatesRunner to inject it in using a method override.

Finally, a special "done" flag is emitted when the task runner stops. This will allow the frontend to easily determine when the runner has stopped.

* progress on settings middleware

* separated middleware, added more action chains for fetching shipping rates

* fixed rates proptypes

* small naming fix

* Update Naming

This commit updates the naming of the runner type to be more consistent with the rest of the package. Instead of ShippingRate, the name is now ShippingRates.

* Add Support for Starting the ShippingRatesRunner

This commit updates the managers, as well as the runner scripts to create an instances of the ShippingRatesRunner based on an incoming type. This allows TaskManagers to start ShippingRatesRunners.

The change is backwards compatible and passing no specific type will by default spawn a TaskRunner. This means that the current usage in the Frontend is unaffected.

* Move Runners to a Separate Folder

This commit restructures the task-runner source code structure to group runners together in a separate folder. This keeps the top level clean if/when more runners are added.

* Export Task Runner Types

This commit updates the exported object of the task-runner package so the frontend can reference the specific type it wants to spawn by variable instead of by value.

* Fix Run Loop Bug

This commit fixes a typo where the run loop was not invoked.

* Update Task Status when emitting message

This commit updates the emit task event to update the contexts end status with the message. This allows custom messages to be reused when reaching an end-runner state (aborted, errored, finished).

When handling captchas, the context status is cleared to show the generic end state message.

* Switch to use end runner states instead of generic Stopped

This commit updates the task-runner sub processes to use a distinct end-runner state (Aborted, Errored, Finished) instead of using the generic Stopped event. This provides more context to the specific end state, but also allows the "done" payload to be properly emitted in all cases.

* Fixes to existing test failures

* test progress, broke some others in the progress...

* settings actions tests

* added shipping manager component tests

* pulled in srr changes

* rm unnecessary comments

* some test fixes

* split out shipping reducer

* small change

* revert init state for rates

* added migration pattern

* added random size default in preload

* revert type parameter

* fixed a lot of tests..

* test fixes, code coverage increase

* fix lint, fix scss

* fix lint

* task list reducer test coverage increase

* shipping rates component tests

* test coverage improvements

* increase test coverage

* more code coverage increased

* bug fixes with srr

* rm unused file

* test fix

* added profile reducer

* fixed test

* small code coverage increased

* small bug fix with copy task

* pr changes, test updates

* Update packages/frontend/src/state/migrators/v0.2.0/index.js

Co-Authored-By: walmat <matthew.wallt@gmail.com>

* lint fix

* filtered out shipping rate runner status messages

* task event registration handler split into multiple handlers

* added selectedProfile and currentProfile reducer fixes, and implemented task runner using the shipping rate

* test fix progress

* lint fix

* fix deregister function

* migration changes

* prevented srr from handling restocks

* Add Setup and Cleanup actions for Shipping Rates

This commit adds private setup/cleanup actions for
fetching shipping rates so the reducer can correctly
enable/disable the shipping rates button when there
is a run in progress

* Add Shipping Status Flag

This commit updates the shipping manager definitions
to add a status flag that will be updated by the setup and
cleanup settings actions.

A migrator (v0.2.1) was added to initialize the status flag
if it wasn't previously set. Tests were added for the migrator.

* Update Settings Reducer to handle actions

This commit updates the settings reducer to handle the
setup/cleanup shipping actions. Tests have been added
to ensure proper function.

* Disable Fetch Shipping Rates Button when in progress

This commit updates the shipping manager component to
disable/reenable the fetch button when the shipping manager
is running. This should prevent multiple runs of the shipping
manager from happening.

Tests were added to ensure the component renders properly.

* styling updates, code coverage, and render bug fixes

* bug fix w/ srr skipping fetching rates step

* added price field in

* srr caching rates bug fixed

* rm debug statements

* fixed shipping site username/password resetting bug

* fixed invalid proptype error

* fixed graceful shutdown of srr

* added some more code coverage

* on stop srr

* Implement Cancel feature for SRR

This commit updates the preload script to add a
cancellation feature the shipping rates runner.
This cancellation allows the srr to be stopped
in the task manager if it is no longer needed.

* Update Stop Shipping to Cleanup Shipping State

This commit updates the preload script to notify the caller
of the stop shipping request whether or not the cleanup
shipping action should be dispatched. The settings actions
are updated to handle this and cleanup the shipping redux
state accordingly.

* Address PR Comment

The constructor for the Shipping Rates Runner has been
updated to explicitly reference parameters. This should
prevent initialization bugs where too many/too few
parameters are passed and the type parameter gets
misplaced in the parameter list.

* code coverage increased
  • Loading branch information
walmat authored and pr1sm committed Mar 30, 2019
1 parent cffb309 commit 2c6958a
Show file tree
Hide file tree
Showing 107 changed files with 9,565 additions and 993 deletions.
2 changes: 1 addition & 1 deletion .prettierrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ module.exports = {
files: ["*.js", "*.jsx", "*.es", "*.es6", "*.mjs"],
options: {
printWidth: 100,
parser: "babylon"
parser: "babel"
}
},
{
Expand Down
125 changes: 105 additions & 20 deletions packages/frontend/lib/common/bridge/mainPreload.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
// eslint-disable-next-line import/no-extraneous-dependencies
const { ipcRenderer } = require('electron');
const { TaskRunnerTypes } = require('@nebula/task-runner').shopify;

const IPCKeys = require('../constants');
const nebulaEnv = require('../../_electron/env');
const { base, util } = require('./index');

nebulaEnv.setUpEnvironment();

let srrRequest = null;
let handlers = [];
const taskEventHandler = (...params) => handlers.forEach(h => h(...params));

const _checkForUpdates = () => {
util.sendEvent(IPCKeys.RequestCheckForUpdates);
};
Expand All @@ -33,37 +38,53 @@ const _launchCaptchaHarvester = opts => {
* Sends a listener for task events to launcher.js
*/
const _registerForTaskEvents = handler => {
util.sendEvent(IPCKeys.RequestRegisterTaskEventHandler);
ipcRenderer.once(IPCKeys.RequestRegisterTaskEventHandler, (event, eventKey) => {
// Check and make sure we have a key to listen on
if (eventKey) {
util.handleEvent(eventKey, handler);
} else {
console.error('Unable to Register for Task Events!');
}
});
if (handlers.length > 0) {
handlers.push(handler);
} else {
util.sendEvent(IPCKeys.RequestRegisterTaskEventHandler);
ipcRenderer.once(IPCKeys.RequestRegisterTaskEventHandler, (event, eventKey) => {
// Check and make sure we have a key to listen on
if (eventKey) {
handlers.push(handler);
util.handleEvent(eventKey, taskEventHandler);
} else {
console.error('Unable to Register for Task Events!');
}
});
}
};

/**
* Removes a listener for task events to launcher.js
*/
const _deregisterForTaskEvents = handler => {
util.sendEvent(IPCKeys.RequestDeregisterTaskEventHandler);
ipcRenderer.once(IPCKeys.RequestDeregisterTaskEventHandler, (event, eventKey) => {
// Check and make sure we have a key to deregister from
if (eventKey) {
util.removeEvent(eventKey, handler);
} else {
console.error('Unable to Deregister from Task Events!');
}
});
if (handlers.length === 1) {
util.sendEvent(IPCKeys.RequestDeregisterTaskEventHandler);
ipcRenderer.once(IPCKeys.RequestDeregisterTaskEventHandler, (event, eventKey) => {
// Check and make sure we have a key to deregister from
if (eventKey) {
util.removeEvent(eventKey, taskEventHandler);
handlers = [];
} else {
console.error('Unable to Deregister from Task Events!');
}
});
}
handlers = handlers.filter(h => h !== handler);
};

/**
* Removes all listeners if the window was closed
*/
window.onbeforeunload = () => {
handlers.forEach(h => _deregisterForTaskEvents(h));
};

/**
* Sends task(s) that should be started to launcher.js
*/
const _startTasks = tasks => {
util.sendEvent(IPCKeys.RequestStartTasks, tasks);
const _startTasks = (tasks, options) => {
util.sendEvent(IPCKeys.RequestStartTasks, tasks, options);
};

/**
Expand All @@ -73,6 +94,68 @@ const _stopTasks = tasks => {
util.sendEvent(IPCKeys.RequestStopTasks, tasks);
};

const _startShippingRatesRunner = task => {
const request = {
task: { ...task, id: 1000, sizes: ['Random'] },
cancel: () => {},
promise: null,
};

if (srrRequest) {
return Promise.reject(new Error('Shipping Rates Runner has already been started!'));
}

request.promise = new Promise((resolve, reject) => {
const response = {};

// Define srr message handler to retrive data
const srrMessageHandler = (_, id, payload) => {
// Only respond to specific type
if (payload.type === TaskRunnerTypes.ShippingRates) {
// Runner type is exposed from the task-runner package
response.rates = payload.rates || response.rates; // update rates if it exists
response.selectedRate = payload.selected || response.selectedRate; // update selected if it exists

if (payload.done) {
// SRR is done
_deregisterForTaskEvents(srrMessageHandler);
if (!response.rates || !response.selectedRate) {
// Reject since we don't have the required data
reject(new Error('Data was not provided!'));
} else {
// Resolve since we have the required data
resolve(response);
}
srrRequest = null;
}
}
};

// Define cancel method for request
request.cancel = () => {
_deregisterForTaskEvents(srrMessageHandler);
_stopTasks(request.task);
srrRequest = null;
reject(new Error('Runner was cancelled'));
};

srrRequest = request;
_registerForTaskEvents(srrMessageHandler);
_startTasks(request.task, { type: TaskRunnerTypes.ShippingRates });
});

return request.promise;
};

const _stopShippingRatesRunner = () => {
if (!srrRequest) {
return Promise.reject(new Error('No SRR Running'));
}
srrRequest.cancel();
srrRequest = null;
return Promise.resolve();
};

/**
* Sends proxies(s) that should be add to launcher.js
*/
Expand Down Expand Up @@ -113,6 +196,8 @@ process.once('loaded', () => {
checkForUpdates: _checkForUpdates,
launchCaptchaHarvester: _launchCaptchaHarvester,
setTheme: _setTheme,
startShippingRatesRunner: _startShippingRatesRunner,
stopShippingRatesRunner: _stopShippingRatesRunner,
closeAllCaptchaWindows: _closeAllCaptchaWindows,
deactivate: _deactivate,
registerForTaskEvents: _registerForTaskEvents,
Expand Down
27 changes: 0 additions & 27 deletions packages/frontend/lib/common/typeDef.js

This file was deleted.

6 changes: 3 additions & 3 deletions packages/frontend/lib/task/adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,11 @@ class TaskManagerAdapter {
this._taskManager.harvestCaptchaToken(runnerId, token);
}

_onStartTasksRequest(_, tasks) {
_onStartTasksRequest(_, tasks, options) {
if (tasks instanceof Array) {
this._taskManager.startAll(tasks);
this._taskManager.startAll(tasks, options);
} else {
this._taskManager.start(tasks);
this._taskManager.start(tasks, options);
}
}

Expand Down
8 changes: 7 additions & 1 deletion packages/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,15 @@
"!src/tasks/old/*.{js,jsx}",
"!src/server/old/*.{js,jsx}"
],
"coveragePathIgnorePatterns": [
"/node_modules/",
".eslintrc.js",
"setupTests.js"
],
"testPathIgnorePatterns": [
"/node_modules/",
".eslintrc.js"
".eslintrc.js",
"setupTests.js"
]
}
}
73 changes: 73 additions & 0 deletions packages/frontend/src/__tests__/app.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Profiles from '../profiles/profiles';
import Server from '../server/server';
import Settings from '../settings/settings';
import { ROUTES, globalActions } from '../state/actions';
import { THEMES, mapThemeToColor, mapToNextTheme } from '../constants/themes';

import getByTestId from '../__testUtils__/getByTestId';

Expand All @@ -32,11 +33,83 @@ describe('Top Level App', () => {
expect(wrapper.find('#container-wrapper')).toHaveLength(1);
expect(getByTestId(wrapper, 'App.button.close')).toHaveLength(1);
expect(getByTestId(wrapper, 'App.button.deactivate')).toHaveLength(1);
expect(getByTestId(wrapper, 'App.button.theme')).toHaveLength(1);
getByTestId(wrapper, 'App.button.deactivate').simulate('keyPress');
expect(wrapper.instance().props.store.getState).toHaveBeenCalled();
wrapper.unmount();
});

describe('Theme Button', () => {
let Bridge;

afterEach(() => {
if (Bridge && window.Bridge) {
delete window.Bridge;
}
});

it('should render with correct props', () => {
const wrapper = appProvider();
const themeButton = getByTestId(wrapper, 'App.button.theme');
expect(themeButton.prop('className')).toBe('theme-icon');
expect(themeButton.prop('role')).toBe('button');
expect(themeButton.prop('title')).toBe('theme');
expect(themeButton.prop('onKeyPress')).toBeDefined();
expect(themeButton.prop('onClick')).toBeDefined();
});

describe("should not call window bridge method if it isn't defined", () => {
test('when current theme is not in the theme map', () => {
const onKeyPress = jest.fn();
const wrapper = appProvider({ onKeyPress });
const { store } = wrapper.instance().props;
const initialTheme = 'RANDOM_TEST_STRING';
const themeButton = getByTestId(wrapper, 'App.button.theme');
themeButton.simulate('click');
themeButton.simulate('keyPress');
expect(onKeyPress).toHaveBeenCalled();
expect(store.dispatch).toHaveBeenCalled();
expect(store.dispatch.mock.calls[0][0]).toEqual(
globalActions.setTheme(mapToNextTheme[initialTheme] || THEMES.LIGHT),
);
expect(store.theme).toEqual(mapToNextTheme[initialTheme]);
});

test('when current theme is in the theme map', () => {
const onKeyPress = jest.fn();
const wrapper = appProvider({ onKeyPress });
const { store } = wrapper.instance().props;
const { theme } = store;
const themeButton = getByTestId(wrapper, 'App.button.theme');
themeButton.simulate('click');
themeButton.simulate('keyPress');
expect(onKeyPress).toHaveBeenCalled();
expect(store.dispatch).toHaveBeenCalled();
expect(store.dispatch.mock.calls[0][0]).toEqual(
globalActions.setTheme(mapToNextTheme[theme] || THEMES.LIGHT),
);
expect(store.theme).toEqual(mapToNextTheme[theme]);
});
});

test('should call window.Bridge.setTheme if it is defined', () => {
const wrapper = appProvider();
const { store } = wrapper.instance().props;
const { theme } = store;
const nextTheme = mapToNextTheme[theme] || THEMES.LIGHT;
const backgroundColor = mapThemeToColor[nextTheme];
const themeButton = getByTestId(wrapper, 'App.button.theme');
Bridge = {
setTheme: jest.fn(),
};
window.Bridge = Bridge;
themeButton.simulate('click');
expect(store.dispatch).toHaveBeenCalled();
expect(store.dispatch.mock.calls[0][0]).toEqual(globalActions.setTheme(nextTheme));
expect(Bridge.setTheme).toHaveBeenCalledWith({ backgroundColor });
});
});

describe('Deactivate Button', () => {
let Bridge;

Expand Down
40 changes: 40 additions & 0 deletions packages/frontend/src/__tests__/constants/getAllSizes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,44 @@ describe('getAllSizes', () => {
});
});
});

test("should build the correct sizes for US/UK Men's category", () => {
const category = "US/UK Men's";

const expected = expectedSizes[2].options.filter(
s => s.label !== 'Random' && s.label !== 'Full Size Run',
);
const actual = buildSizesForCategory(category);
expect(actual).toEqual(expected);
});

test('should build the correct sizes for Clothing category', () => {
const category = 'Clothing';

const expected = expectedSizes[1].options.filter(
s => s.label !== 'Random' && s.label !== 'Full Size Run',
);
const actual = buildSizesForCategory(category);
expect(actual).toEqual(expected);
});

test("should build the correct sizes for EU Men's category", () => {
const category = "EU Men's";

const expected = expectedSizes[3].options.filter(
s => s.label !== 'Random' && s.label !== 'Full Size Run',
);
const actual = buildSizesForCategory(category);
expect(actual).toEqual(expected);
});

test('should build the correct sizes for Generic category', () => {
const category = 'Generic';

const expected = expectedSizes[0].options.filter(
s => s.label !== 'Random' && s.label !== 'Full Size Run',
);
const actual = buildSizesForCategory(category);
expect(actual).toEqual(expected);
});
});
Loading

0 comments on commit 2c6958a

Please sign in to comment.