29 | );
30 | });
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | archive/
8 |
9 | # Runtime data
10 | pids
11 | *.pid
12 | *.seed
13 | *.pid.lock
14 |
15 | # Directory for instrumented libs generated by jscoverage/JSCover
16 | lib-cov
17 |
18 | # Coverage directory used by tools like istanbul
19 | coverage
20 |
21 | # nyc test coverage
22 | .nyc_output
23 |
24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
25 | .grunt
26 |
27 | # Bower dependency directory (https://bower.io/)
28 | bower_components
29 |
30 | # node-waf configuration
31 | .lock-wscript
32 |
33 | # Compiled binary addons (http://nodejs.org/api/addons.html)
34 | build/Release
35 |
36 | # Dependency directories
37 | node_modules/
38 | jspm_packages/
39 |
40 | # Typescript v1 declaration files
41 | typings/
42 |
43 | # Optional npm cache directory
44 | .npm
45 |
46 | # Optional eslint cache
47 | .eslintcache
48 |
49 | # Optional REPL history
50 | .node_repl_history
51 |
52 | # Output of 'npm pack'
53 | *.tgz
54 |
55 | # Yarn Integrity file
56 | .yarn-integrity
57 |
58 | # dotenv environment variables file
59 | .env
60 |
61 | build
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Microsoft Corporation. All rights reserved.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE
22 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | SatchelJS Cookbook welcomes contributions from the community. By contributing, you agree to abide by the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct).
4 |
5 | ## Problems and Questions
6 |
7 | If you encounter a problem with this project, please search in the [issue tracker]((https://github.com/Microsoft/satcheljs-cookbook/issues)) to see if it has already been reported. If not, create a new issue.
8 |
9 | ## Development
10 |
11 | In order to contribute code to this project, you must first sign the [CLA](https://cla.opensource.microsoft.com/Microsoft/satcheljs-cookbook), which can be found at https://cla.opensource.microsoft.com/Microsoft/satcheljs-cookbook. If you haven't signed it before submitting a Pull Request, you can access it through the `Details` link in the `license/cla` check.
12 |
13 | In order to submit a pull request:
14 |
15 | 1. Fork and clone this repository
16 | 2. Create a feature branch
17 | 3. Make your changes and ensure they build: `yarn build`
18 | 4. Add or update tests and ensure they pass: `yarn test`
19 | 5. If necessary, update documentation in the `REAMDE.md`
20 | 6. Create a [Pull Request](https://github.com/Microsoft/satcheljs-cookbook/pulls)
21 |
--------------------------------------------------------------------------------
/recipes/06-persist-update-on-server.md:
--------------------------------------------------------------------------------
1 | # Side Effects: Persist Update to Server
2 |
3 | ## Problem
4 | You have finished the work on client side and need to persist some state on a server via an API
5 |
6 | ## Solution
7 | Use an orchestrator to call these service calls.
8 |
9 | ## Discussion
10 | Orchestrator is the only place within the SatchelJS library that allows for asynchronous calls. This is the perfect place to call into those network calls or asynchronous side effect function calls. Since orchestrators themselves cannot modify the state, it forces developers to separate coordination logic (business logic) from the store update logic. Just like the `removeBookFromCart` orchestrator from the previous recipe, the [`buy`](../src/orchestrators/buy.ts) orchestrator coordinates the asynchronous "network call" `makePurchase` and various actions. One of the actions, `finishBuying` is subscribed by many mutators to complete the purchase flow. The new async / await pair of API provided by the newer browser & Typescript itself is a nice way to write these orchestrator:
11 |
12 | ```typescript
13 | orchestrator(buy, async() => {
14 | const store = getStore();
15 | beginBuying();
16 | await makePurchase(store.cart.books);
17 | finishBuying();
18 | });
19 | ```
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/components/CategoryList.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import getStore from '../store/store';
3 | import selectCategory from '../actions/selectCategory';
4 | import { observer } from 'mobx-react';
5 | import * as classnames from 'classnames/bind';
6 |
7 | const cx = classnames.bind(require('./AppStyles.css'));
8 |
9 | export default observer(function CategoryList() {
10 | const store = getStore();
11 | const selectedCategoryId = getStore().selectedCategoryId;
12 | return (
13 |
41 | );
42 | });
43 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "satcheljs-cookbook",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node --openssl-legacy-provider server.js",
8 | "test": "jest"
9 | },
10 | "husky": {
11 | "hooks": {
12 | "pre-commit": "lint-staged"
13 | }
14 | },
15 | "lint-staged": {
16 | "*.{ts,tsx}": [
17 | "prettier --write",
18 | "git add"
19 | ]
20 | },
21 | "author": "",
22 | "license": "ISC",
23 | "devDependencies": {
24 | "@types/classnames": "^2.2.3",
25 | "@types/jest": "^29.1.1",
26 | "@types/react": "^16.8.8",
27 | "@types/react-dom": "^16.8.2",
28 | "css-loader": "^2.1.1",
29 | "express": "^4.15.4",
30 | "husky": "^1.3.1",
31 | "jest": "^29.1.2",
32 | "lint-staged": "^8.1.5",
33 | "opn": "^5.4.0",
34 | "prettier": "^1.19.1",
35 | "pug": "^3.0.1",
36 | "style-loader": "^0.23.1",
37 | "ts-jest": "^29.0.3",
38 | "ts-loader": "^8.2.0",
39 | "typescript": "^4.8.4",
40 | "webpack": "^4.41.6",
41 | "webpack-dev-middleware": "^3.7.2",
42 | "webpack-dev-server": "^3.9.0"
43 | },
44 | "dependencies": {
45 | "classnames": "^2.2.5",
46 | "mobx": "~4.5.0",
47 | "mobx-react": "~5.2.8",
48 | "mobx-react-lite": "^1.3.1",
49 | "react": "^16.9.0",
50 | "react-dom": "^16.9.0",
51 | "satcheljs": "4.0.1"
52 | },
53 | "jest": {
54 | "transform": {
55 | "^.+\\.tsx?$": "/node_modules/ts-jest"
56 | },
57 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.tsx?$",
58 | "moduleFileExtensions": [
59 | "ts",
60 | "tsx",
61 | "js",
62 | "json",
63 | "jsx"
64 | ]
65 | }
66 | }
--------------------------------------------------------------------------------
/recipes/03-update-shared-state.md:
--------------------------------------------------------------------------------
1 | # Update Shared State Across Different Component Subtrees
2 |
3 | ## Problem
4 | You have multiple parts of your UI that need to make changes to a shared state. This is the classic example from Facebook's Flux architecture. Their example shows a single counter that needs to be incremented (maybe by different amounts) by separate parts of the application.
5 |
6 | ## Solution
7 | Use the SatchelJS state tree. Then simply update the shared state by dispatching actions of the same type from muliple places. Since there is only one mutator subscribing the action being dispatched, only one kind of mutation happens at a time.
8 |
9 | ## Discussion
10 | Flux, at its heart, requires a bit of shared state outside of the component tree. It also requires the use of a dispatcher that coordinates action messages. Implementing these two parts in SatchelJS are the dispatching action creator and the mutator. This means any component file can simply have an import of the action creator and begin dispatching action messages. The shared state tree is observable, and is observed by one or more components which would cause those components to re-render.
11 |
12 | Selection scenarios within our example illustrate this point. For example, selecting a book inside the book list will deselect the item inside the cart and vice versa. This is needed so that the detail description for the book is shared between the cart and the book list [src/mutators/cart/selectedBookId.ts](../src/mutators/cart/selectedBookId.ts):
13 |
14 | ```typescript
15 | mutator(selectBook, (msg) => {
16 | getStore().cart.selectedBookId = null;
17 | });
18 |
19 | mutator(selectBookInCart, (msg) => {
20 | getStore().cart.selectedBookId = msg.bookId;
21 | });
22 | ```
23 |
24 | One single state is being acted on by two separate action. Placing these two mutators in a single file helps code maintainers see what actions could affect this `store.cart.selectedBookId` field.
25 |
--------------------------------------------------------------------------------
/recipes/05-related-state-change.md:
--------------------------------------------------------------------------------
1 | # Independent State Changes
2 |
3 | This example shows how SatchelJS orchestrators coordinate related state changes
4 |
5 | ## Problem
6 | You have an action that triggers a series of mutators that needs to happen in serial.
7 |
8 | ## Solution
9 | Use an orchestrator to coordinate work.
10 |
11 | ## Discussion
12 | Orchestrator cannot actually make changes to the store. All changes are done inside mutators. Orchestrators is a concept provided by SatchelJS to help code readers to understand more complex flows of a scenario. In the example, after the user adds multiple items into the shopping cart, she can then remove the entire entry from the cart by pressing the "Remove" button. The removal of an item in the shopping cart is an example of a complex action. In our example, the behavior specified is this: when the remove button is pressed, the selection goes to the first item in the shopping cart. Alternatively, when the shopping cart is empty from a removal action, the first book of the currently selected category is selected. Having this sort of business logic captured inside the orchestrator truly helps maintainability of the scenario. Anyone unfamiliar with this flow can simply refer to this one orchestrator to understand all the logic behind removing an item from a cart.
13 |
14 | A few conventions that we've adopted here that is recommended:
15 |
16 | 1. mutators are structured exactly like the keys inside the schema of the state tree
17 | 2. many action creators that are related CAN be placed in one file
18 | 3. mixing and matching independent and related state change patterns is okay as long as they are aggregated inside an orchestrator
19 | 4. an action that is not expected to be called by outside the orchestrator are prefixed with a "_" to let other developers know not to call these from anywhere else
20 |
21 | An example of an orchestrator that does dispatches out to multiple actions in order is [removeBookFromCart.ts](../src/orchestrators/removeBookFromCart.ts):
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SatchelJS Cookbook
2 |
3 | This cookbook centers around a single example of a book shop. There are several recipes in the form of Markdown files under the "recipes" directory that point to various parts of the example inside the discussion section.
4 |
5 | ## Recipes
6 |
7 | 1. [Simple state changes](./recipes/01-simple-state-changes.md)
8 | 2. [Access store data from inside components](./recipes/02-store-data-inside-component.md)
9 | 3. [Update shared state from different parts of the UI](./recipes/03-update-shared-state.md)
10 | 4. [Independent state changes from one single action](./recipes/04-independent-state-change.md)
11 | 5. [Coordinating related state changes from one single action](./recipes/05-related-state-change.md)
12 | 6. [Persist updates on server with network calls](./recipes/06-persist-update-on-server.md)
13 |
14 | ## Running the examples
15 |
16 | ```
17 | git clone https://github.com/microsoft/satcheljs-cookbook
18 | yarn
19 | yarn start
20 | ```
21 |
22 | Open your browser to [http://localhost:3000](http://localhost:3000) and click on an example.
23 |
24 | ## Running tests
25 |
26 | ```
27 | yarn test
28 | ```
29 |
30 | ## Contributing
31 |
32 | This project welcomes contributions and suggestions. Most contributions require you to agree to a
33 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
34 | the rights to use your contribution. For details, visit https://cla.microsoft.com.
35 |
36 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
37 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
38 | provided by the bot. You will only need to do this once across all repos using our CLA.
39 |
40 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
41 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
42 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
43 |
--------------------------------------------------------------------------------
/recipes/02-store-data-inside-component.md:
--------------------------------------------------------------------------------
1 | # Accessing Store Data From Inside a Component
2 |
3 | This example demonstrates how to access data from the SatchelJS state tree from inside a component. Also, an enzyme test is provided to show how a component test can be written using a Jasmine spy on the ```getStore()``` function.
4 |
5 | ## Problem
6 | You want to display some piece of data inside the state tree inside a component.
7 |
8 | ## Solution
9 | Simply use a *selector* function inside the render function of the component.
10 |
11 | ## Discussion
12 | A *selector* in SatchelJS is a function that retrieves a piece of the state tree. The function itself doesn't keep state. Keep these functions simple so that it is easy to create a spy to override these selector's behavior during tests. An example is in [getSelectedBookId.ts](../src/selectors/getSelectedBookId.ts):
13 |
14 | ```typescript
15 | function getSelectedBookId() {
16 | const store = getStore();
17 | return store.selectedBookId || store.cart.selectedBookId;
18 | }
19 | ```
20 |
21 | We encourage this so much that the ```createStore()``` function actually returns a selector rather than the store itself. This allows you to test the mutators, orchestrators, and components even if it accesses the root level of the store.
22 |
23 | To use it, just call the selector inside your render function:
24 |
25 | ```jsx
26 | export default class CategoryList extends React.Component {
27 | render() {
28 | const store = getStore();
29 | const selectedCategoryId = getStore().selectedCategoryId;
30 | return (
)
43 | }
44 | }
45 | ```
--------------------------------------------------------------------------------
/recipes/01-simple-state-changes.md:
--------------------------------------------------------------------------------
1 | # Simple State Change
2 |
3 | This example shows how to create a basic state change with:
4 |
5 | * actionCreator that creates action messages
6 | * dispatching action message
7 | * mutator to receive this message and change the state
8 | * observer to update views accordingly
9 |
10 | ## Problem
11 |
12 | You have a simple state change that needs to be reflected in the view.
13 |
14 | ## Solution
15 |
16 | Use a mutator to change the state. The component observer will automatically cause React to re-render. To accomplish this, you'll need to:
17 |
18 | 1. create an action message
19 | 2. dispatch this action message
20 | 3. set up a mutator that would listen to this action message while modifying the state
21 | 4. component observer will pick up the change and re-render
22 |
23 | ## Discussion
24 |
25 | In [selectedBookId.ts](../src/mutators/selectedBookId.ts) we demonstrate the basic APIs of SatchelJS. We will not use the convenience APIs available to illustrate all the pieces of Flux working together.
26 |
27 | To create an action message, we use an action creator function.
28 |
29 | ```typescript
30 | action('selectBook', (id: string) => ({id}));
31 | ```
32 |
33 | `action()` returns a function that can be subscribed by _mutators_ and _orchestrators_. In addition, it will also dispatch the action when the function is called. If you are more used to the tradition action creators that merely returns an **message object** that represents the type of message along with a payload then you can use the `actionCreator()` API instead. **Note** messages created by `actionCreator()` need to be dispatched with ```dispatch()``` in order for SatchelJS to pass this action message along to all the mutators that are listening to this action.
34 |
35 | Finally, SatchelJS utilizes ```mobx-react``` for its component ```observer``` decorator. The state change causes the component to re-render when the observable state has been accessed inside the ```render()```. In fact, the entire state tree in SatchelJS is a large observable object: anything stored inside the state tree is automatically observable. Because this pattern is so often use, most developers will choose to write actions with `action()` API.
36 |
--------------------------------------------------------------------------------
/recipes/04-independent-state-change.md:
--------------------------------------------------------------------------------
1 | # Independent State Changes
2 |
3 | ## Problem
4 | You have multiple pieces of data inside the state tree that need to change based on the result of dispatching a single action message.
5 |
6 | ## Solution
7 | Since mutators subscribe to actions, simply have two or more mutators listen for a single action to have a single action update two parts of the state tree.
8 |
9 | ## Discussion
10 | In our example, when a category is selected, we call the `selectCategory()` action. There are two separate mutators that subscribe this action. The intended behavior is to have this action both trigger a selection of the category and also the first book in the category. This action creator is consumed inside `selectedCategoryId` and `selectedBookId` mutators. The first one sets the id of the selected category while the second one does a quick linear search for the first book in the books list that is in the selecte category.
11 |
12 | For the purpose of clarity, we moved the action creators out into its own folder. Also, for consistency, we kept the `selectBook()` action a plain action rather than a `mutatorAction`. Note as well we try to keep our mutators in the same shape as the tree structure itself. The file `mutators/selectedCategoryId.ts` matches the name of the key of the top level tree that stores the `selectedCategoryId`. This consistency is a convention that you adopt to make it clear which part of the tree is being mutated by the mutators. Also note that the mutators have to be registered inside some place like inside the `store.ts`.
13 |
14 | The relevants files are here:
15 | * [selectedBookId.ts](../src/mutators/selectedBookId.ts)
16 | ```typescript
17 | mutator(selectCategory, (msg) => {
18 | const store = getStore();
19 |
20 | let found: string | null = null;
21 |
22 | Object.keys(store.books).forEach(bookId => {
23 | const book = store.books[bookId];
24 | if (book.categoryId == msg.id && !found) {
25 | found = bookId;
26 | }
27 | });
28 |
29 | store.selectedBookId = found;
30 | });
31 | ```
32 |
33 | * [selectedCategoryId.ts](../src/mutators/selectedCategoryId.ts)
34 | ```typescript
35 | mutator(selectCategory, (msg) => {
36 | getStore().selectedCategoryId = msg.id;
37 | });
38 | ```
--------------------------------------------------------------------------------
/src/store/BookStore.ts:
--------------------------------------------------------------------------------
1 | export interface BookStore {
2 | books: {
3 | [id: string]: Book;
4 | };
5 | categories: {
6 | [id: string]: Category;
7 | };
8 | selectedBookId: string | null;
9 | selectedCategoryId: string;
10 | cart: Cart;
11 | }
12 |
13 | export interface Book {
14 | name: string;
15 | categoryId: string;
16 | description: string;
17 | price: number;
18 | }
19 |
20 | export interface Category {
21 | name: string;
22 | }
23 |
24 | export interface Cart {
25 | books: {
26 | bookId: string;
27 | quantity: number;
28 | }[];
29 | selectedBookId: string | null;
30 | isBuying: boolean;
31 | }
32 |
33 | export const sampleData: BookStore = {
34 | books: {
35 | '1': {
36 | name: 'Boiling Water',
37 | categoryId: '1',
38 | description:
39 | 'A thrilling recipe book about how to perform one of the most important tasks in human history. Beautifully illustrated step by step instruction sets this book apart from all other boiling water recipe book.',
40 | price: 19.95,
41 | },
42 | '2': {
43 | name: 'Grilling: The One True Form of Cooking',
44 | categoryId: '1',
45 | description:
46 | 'The definitive guide on grilling everything from pizza, cakes, and even a salad.',
47 | price: 39.95,
48 | },
49 | '3': {
50 | name: 'Ready Player One',
51 | categoryId: '2',
52 | description: 'A novel where a boy tries to impress a girl with his nerdiness.',
53 | price: 16.95,
54 | },
55 | '4': {
56 | name: 'Off to Be the Wizard',
57 | categoryId: '2',
58 | description: 'Another novel where a boy tries to impress a girl with his nerdiness.',
59 | price: 9.95,
60 | },
61 | '5': {
62 | name: 'A Wrinkle in Time',
63 | categoryId: '2',
64 | description: 'The Darkness Has You',
65 | price: 19.95,
66 | },
67 | },
68 | categories: {
69 | '1': {
70 | name: 'Cookbook',
71 | },
72 | '2': {
73 | name: 'Science Fiction',
74 | },
75 | },
76 | selectedCategoryId: '1',
77 | selectedBookId: '1',
78 | cart: {
79 | books: [],
80 | selectedBookId: null,
81 | isBuying: false,
82 | },
83 | };
84 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Security
4 |
5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
6 |
7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below.
8 |
9 | ## Reporting Security Issues
10 |
11 | **Please do not report security vulnerabilities through public GitHub issues.**
12 |
13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report).
14 |
15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey).
16 |
17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc).
18 |
19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
20 |
21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
22 | * Full paths of source file(s) related to the manifestation of the issue
23 | * The location of the affected source code (tag/branch/commit or direct URL)
24 | * Any special configuration required to reproduce the issue
25 | * Step-by-step instructions to reproduce the issue
26 | * Proof-of-concept or exploit code (if possible)
27 | * Impact of the issue, including how an attacker might exploit the issue
28 |
29 | This information will help us triage your report more quickly.
30 |
31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs.
32 |
33 | ## Preferred Languages
34 |
35 | We prefer all communications to be in English.
36 |
37 | ## Policy
38 |
39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd).
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/__tests__/mutators/cart/books.spec.ts:
--------------------------------------------------------------------------------
1 | import { mutator } from 'satcheljs';
2 | import * as store from '../../../store/store';
3 | import { BookStore } from '../../../store/BookStore';
4 | import { addBookToCart, _removeBookFromCart, finishBuying } from '../../../actions/cart';
5 |
6 | import '../../../mutators/cart/books';
7 |
8 | describe('cart.books mutators', () => {
9 | it('should add book with the quantity of 1 to cart if the cart does not have the book already', () => {
10 | // Arrange
11 | const state: any = {
12 | books: {
13 | '1': {
14 | name: 'hi',
15 | },
16 | },
17 | cart: {
18 | books: [],
19 | },
20 | };
21 |
22 | jest.spyOn(store, 'default').mockReturnValue(state);
23 |
24 | // Act
25 | addBookToCart('1');
26 |
27 | // Assert
28 | expect(state.cart.books.length).toBe(1);
29 | expect(state.cart.books[0].bookId).toBe('1');
30 | });
31 |
32 | it('should increase the quantity if the cart has the book already', () => {
33 | // Arrange
34 | const state: any = {
35 | books: {
36 | '1': {
37 | name: 'hi',
38 | },
39 | },
40 | cart: {
41 | books: [
42 | {
43 | bookId: '1',
44 | quantity: 1,
45 | },
46 | ],
47 | },
48 | };
49 |
50 | jest.spyOn(store, 'default').mockReturnValue(state);
51 |
52 | // Act
53 | addBookToCart('1');
54 |
55 | // Assert
56 | expect(state.cart.books.length).toBe(1);
57 | expect(state.cart.books[0].bookId).toBe('1');
58 | expect(state.cart.books[0].quantity).toBe(2);
59 | });
60 |
61 | it('should remove the entire entry from cart', () => {
62 | // Arrange
63 | const state: any = {
64 | books: {
65 | '1': {
66 | name: 'hi',
67 | },
68 | },
69 | cart: {
70 | books: [
71 | {
72 | bookId: '1',
73 | quantity: 10,
74 | },
75 | ],
76 | },
77 | };
78 |
79 | jest.spyOn(store, 'default').mockReturnValue(state);
80 |
81 | // Act
82 | _removeBookFromCart('1');
83 |
84 | // Assert
85 | expect(state.cart.books.length).toBe(0);
86 | });
87 |
88 | it('should ignore any request to remove entries that cannot be found in cart', () => {
89 | // Arrange
90 | const state: any = {
91 | books: {
92 | '1': {
93 | name: 'hi',
94 | },
95 | },
96 | cart: {
97 | books: [
98 | {
99 | bookId: '1',
100 | quantity: 10,
101 | },
102 | ],
103 | },
104 | };
105 |
106 | jest.spyOn(store, 'default').mockReturnValue(state);
107 |
108 | // Act
109 | _removeBookFromCart('5');
110 |
111 | // Assert
112 | expect(state.cart.books.length).toBe(1);
113 | });
114 |
115 | it('should empty the cart when finished buying', () => {
116 | // Arrange
117 | const state: any = {
118 | books: {
119 | '1': {
120 | name: 'hi',
121 | },
122 | },
123 | cart: {
124 | books: [
125 | {
126 | bookId: '1',
127 | quantity: 10,
128 | },
129 | ],
130 | },
131 | };
132 |
133 | jest.spyOn(store, 'default').mockReturnValue(state);
134 |
135 | // Act
136 | finishBuying();
137 |
138 | // Assert
139 | expect(state.cart.books.length).toBe(0);
140 | });
141 | });
142 |
--------------------------------------------------------------------------------