[
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# TypeScript v1 declaration files\ntypings/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n.env.test\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n\n# Next.js build output\n.next\n\n# Nuxt.js build / generate output\n.nuxt\ndist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and *not* Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n\n# SQLite Databases\n*.sqlite\n\n# .DS_Store\n.DS_Store\n\n# Built JS bundles\nbundle.js\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "<h1 align=center>Testing JavaScript Applications</h1>\n<h4 align=center><i>A <a href=\"https://www.manning.com/\">Manning</a> book by <a href=\"https://www.lucasfcosta.com\">Lucas da Costa</a></i></h4>\n\n<br>\n\n<h5 align=center>➡ Available at <a href=\"https://www.manning.com/books/testing-javascript-applications\">Manning.com</a></h5>\n\n<br>\n\n## 📕 About this book\n\nTesting JavaScript Applications will help you write high-quality software in less time, with more confidence.\n\nIn the last five years, I have been deeply involved in the JavaScript testing scene. I am a core-maintainer of both Chai.js and Sinon.js, two of the most popular testing libraries in JavaScript, and I closely follow projects like Jest and Mocha. In this book, I expect to teach you what I've learned during those years in which I've been involved in vetting and implementing features, defining best-practices, and designing the libraries that thousands of people use every day.\n\nThroughout this book's pages, you will learn how to write effective tests through various diagrams and practical examples we'll build together. Because I believe the best way to learn something is by doing it yourself, **I've put in this repository all of the book's examples**, so that you can experiment with them on your own and compare your solutions to the ones which I've implemented.\n\nBesides covering specific tools, like Jest, and techniques, like TDD, it will teach you how to think about tests from a business perspective. You will learn what to take into account when designing tests, and how to make optimal decisions for _your_ specific context.\n\nI've written Testing JavaScript Applications thinking mostly about Junior Developers. They are the ones who will benefit the most from this book's approach to tests, which covers both the _\"hows\"_ and the _\"whys\"_ of writing automated tests.\n\nEven though Junior Developers will be the ones who will benefit the most from this book, it also contains topics which cater to senior and mid-level developers. It includes my thoughts on how tests impact a business, how they structure relationships within teams, and other aspects involved in building what I'd call \"a culture of quality\".\n\nTo get the most out of \"Testing JavaScript Applications\", you must have a basic understanding of JavaScript. You should know how to use objects, functions, callbacks, and, especially, Promises. Basic knowledge of CSS and HTML is also required for the chapters in which we'll test a front-end application.\n\nBecause I've tried to make the examples in this book as close to reality as possible, there will be chapters in which we'll test a Node.js back-end application, and others in which we'll test a React application. Therefore, it will be necessary to have elementary knowledge about these tools. Reading their \"getting started\" guides should take you approximately 15 minutes each, and will be enough for you to follow along with the testing examples.\n\nIf you have any questions, comments, or suggestions, I'd love to hear them. With your invaluable feedback, we'll build a better book together.\n\nI wish you all a productive, pleasant, and exciting journey.\n\n_— Lucas da Costa_\n\n<br>\n\n## 📁 About this repository\n\n**This repository contains all of the examples in the book _Testing JavaScript Applications_**.\n\nI have organised examples in a separate folder for each chapter and, within a chapter, I've separated them by section. Sometimes, even within sections, you will find sub-divisions with the multiple stages of an exercise or with different approaches to solving the same problem.\n\n<br>\n\n## 💻 Running these examples\n\nI've built these examples using [Node.js](https://nodejs.org) v12 and [NPM](https://www.npmjs.com) v6.\n\nBefore executing any of these examples, `cd` into the folder you want to try and run `npm install` to install its dependencies.\n\nMost of the examples have an [NPM script](https://www.keithcirkel.co.uk/how-to-use-npm-as-a-build-tool/) named `test`. Which means that you can execute tests for that example by using `npm test`.\n\n<br>\n\n## 🤝 How to contribute\n\nTogether, we can build better content.\n\nIf you happen to find _any_ problems in _any_ of these examples, feel free to submit a Pull Request explaining what the problem was and how you solved it.\n\nIn case you have a _better_ solution for any of the exercises, I'd love to see it. In that case, explain in your PR why you think that the proposed solution is better. Even though I might not agree, I will treat everyone with the respect they deserve, and will carefully read through their thoughts and comments.\n\n<br>\n\n## 🔗 Where to find more about me and my book\n\nI'd love to hear your thoughts on the book and keep in touch with you.\n\nSend me a tweet [@thewizardlucas](https://twitter.com/thewizardlucas) and let's have a chat!\n\n- [lucasfcosta.com - My Personal Website](https://lucasfcosta.com)\n- [@thewizardlucas on Twitter](https://twitter.com/thewizardlucas)\n- [@lucasfcosta on GitHub](https://github.com/lucasfcosta)\n- [LinkedIn](https://www.linkedin.com/in/lucasfdacosta)\n- [Manning Books' Website](https://www.manning.com/)\n\nFor discussing any topics related to this book, you can email me at testing.javascript.applications@lucasfcosta.com.\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/fixtures/example.json",
    "content": "{\n  \"name\": \"Using fixtures to represent data\",\n  \"email\": \"hello@cypress.io\",\n  \"body\": \"Fixtures are a great way to mock data for responses to routes\"\n}\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/actions.spec.js",
    "content": "/// <reference types=\"cypress\" />\n\ncontext(\"Actions\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress.io/commands/actions\");\n  });\n\n  // https://on.cypress.io/interacting-with-elements\n\n  it(\".type() - type into a DOM element\", () => {\n    // https://on.cypress.io/type\n    cy.get(\".action-email\")\n      .type(\"fake@email.com\")\n      .should(\"have.value\", \"fake@email.com\")\n\n      // .type() with special character sequences\n      .type(\"{leftarrow}{rightarrow}{uparrow}{downarrow}\")\n      .type(\"{del}{selectall}{backspace}\")\n\n      // .type() with key modifiers\n      .type(\"{alt}{option}\") //these are equivalent\n      .type(\"{ctrl}{control}\") //these are equivalent\n      .type(\"{meta}{command}{cmd}\") //these are equivalent\n      .type(\"{shift}\")\n\n      // Delay each keypress by 0.1 sec\n      .type(\"slow.typing@email.com\", { delay: 100 })\n      .should(\"have.value\", \"slow.typing@email.com\");\n\n    cy.get(\".action-disabled\")\n      // Ignore error checking prior to type\n      // like whether the input is visible or disabled\n      .type(\"disabled error checking\", { force: true })\n      .should(\"have.value\", \"disabled error checking\");\n  });\n\n  it(\".focus() - focus on a DOM element\", () => {\n    // https://on.cypress.io/focus\n    cy.get(\".action-focus\")\n      .focus()\n      .should(\"have.class\", \"focus\")\n      .prev()\n      .should(\"have.attr\", \"style\", \"color: orange;\");\n  });\n\n  it(\".blur() - blur off a DOM element\", () => {\n    // https://on.cypress.io/blur\n    cy.get(\".action-blur\")\n      .type(\"About to blur\")\n      .blur()\n      .should(\"have.class\", \"error\")\n      .prev()\n      .should(\"have.attr\", \"style\", \"color: red;\");\n  });\n\n  it(\".clear() - clears an input or textarea element\", () => {\n    // https://on.cypress.io/clear\n    cy.get(\".action-clear\")\n      .type(\"Clear this text\")\n      .should(\"have.value\", \"Clear this text\")\n      .clear()\n      .should(\"have.value\", \"\");\n  });\n\n  it(\".submit() - submit a form\", () => {\n    // https://on.cypress.io/submit\n    cy.get(\".action-form\")\n      .find('[type=\"text\"]')\n      .type(\"HALFOFF\");\n\n    cy.get(\".action-form\")\n      .submit()\n      .next()\n      .should(\"contain\", \"Your form has been submitted!\");\n  });\n\n  it(\".click() - click on a DOM element\", () => {\n    // https://on.cypress.io/click\n    cy.get(\".action-btn\").click();\n\n    // You can click on 9 specific positions of an element:\n    //  -----------------------------------\n    // | topLeft        top       topRight |\n    // |                                   |\n    // |                                   |\n    // |                                   |\n    // | left          center        right |\n    // |                                   |\n    // |                                   |\n    // |                                   |\n    // | bottomLeft   bottom   bottomRight |\n    //  -----------------------------------\n\n    // clicking in the center of the element is the default\n    cy.get(\"#action-canvas\").click();\n\n    cy.get(\"#action-canvas\").click(\"topLeft\");\n    cy.get(\"#action-canvas\").click(\"top\");\n    cy.get(\"#action-canvas\").click(\"topRight\");\n    cy.get(\"#action-canvas\").click(\"left\");\n    cy.get(\"#action-canvas\").click(\"right\");\n    cy.get(\"#action-canvas\").click(\"bottomLeft\");\n    cy.get(\"#action-canvas\").click(\"bottom\");\n    cy.get(\"#action-canvas\").click(\"bottomRight\");\n\n    // .click() accepts an x and y coordinate\n    // that controls where the click occurs :)\n\n    cy.get(\"#action-canvas\")\n      .click(80, 75) // click 80px on x coord and 75px on y coord\n      .click(170, 75)\n      .click(80, 165)\n      .click(100, 185)\n      .click(125, 190)\n      .click(150, 185)\n      .click(170, 165);\n\n    // click multiple elements by passing multiple: true\n    cy.get(\".action-labels>.label\").click({ multiple: true });\n\n    // Ignore error checking prior to clicking\n    cy.get(\".action-opacity>.btn\").click({ force: true });\n  });\n\n  it(\".dblclick() - double click on a DOM element\", () => {\n    // https://on.cypress.io/dblclick\n\n    // Our app has a listener on 'dblclick' event in our 'scripts.js'\n    // that hides the div and shows an input on double click\n    cy.get(\".action-div\")\n      .dblclick()\n      .should(\"not.be.visible\");\n    cy.get(\".action-input-hidden\").should(\"be.visible\");\n  });\n\n  it(\".rightclick() - right click on a DOM element\", () => {\n    // https://on.cypress.io/rightclick\n\n    // Our app has a listener on 'contextmenu' event in our 'scripts.js'\n    // that hides the div and shows an input on right click\n    cy.get(\".rightclick-action-div\")\n      .rightclick()\n      .should(\"not.be.visible\");\n    cy.get(\".rightclick-action-input-hidden\").should(\"be.visible\");\n  });\n\n  it(\".check() - check a checkbox or radio element\", () => {\n    // https://on.cypress.io/check\n\n    // By default, .check() will check all\n    // matching checkbox or radio elements in succession, one after another\n    cy.get('.action-checkboxes [type=\"checkbox\"]')\n      .not(\"[disabled]\")\n      .check()\n      .should(\"be.checked\");\n\n    cy.get('.action-radios [type=\"radio\"]')\n      .not(\"[disabled]\")\n      .check()\n      .should(\"be.checked\");\n\n    // .check() accepts a value argument\n    cy.get('.action-radios [type=\"radio\"]')\n      .check(\"radio1\")\n      .should(\"be.checked\");\n\n    // .check() accepts an array of values\n    cy.get('.action-multiple-checkboxes [type=\"checkbox\"]')\n      .check([\"checkbox1\", \"checkbox2\"])\n      .should(\"be.checked\");\n\n    // Ignore error checking prior to checking\n    cy.get(\".action-checkboxes [disabled]\")\n      .check({ force: true })\n      .should(\"be.checked\");\n\n    cy.get('.action-radios [type=\"radio\"]')\n      .check(\"radio3\", { force: true })\n      .should(\"be.checked\");\n  });\n\n  it(\".uncheck() - uncheck a checkbox element\", () => {\n    // https://on.cypress.io/uncheck\n\n    // By default, .uncheck() will uncheck all matching\n    // checkbox elements in succession, one after another\n    cy.get('.action-check [type=\"checkbox\"]')\n      .not(\"[disabled]\")\n      .uncheck()\n      .should(\"not.be.checked\");\n\n    // .uncheck() accepts a value argument\n    cy.get('.action-check [type=\"checkbox\"]')\n      .check(\"checkbox1\")\n      .uncheck(\"checkbox1\")\n      .should(\"not.be.checked\");\n\n    // .uncheck() accepts an array of values\n    cy.get('.action-check [type=\"checkbox\"]')\n      .check([\"checkbox1\", \"checkbox3\"])\n      .uncheck([\"checkbox1\", \"checkbox3\"])\n      .should(\"not.be.checked\");\n\n    // Ignore error checking prior to unchecking\n    cy.get(\".action-check [disabled]\")\n      .uncheck({ force: true })\n      .should(\"not.be.checked\");\n  });\n\n  it(\".select() - select an option in a <select> element\", () => {\n    // https://on.cypress.io/select\n\n    // at first, no option should be selected\n    cy.get(\".action-select\").should(\"have.value\", \"--Select a fruit--\");\n\n    // Select option(s) with matching text content\n    cy.get(\".action-select\").select(\"apples\");\n    // confirm the apples were selected\n    // note that each value starts with \"fr-\" in our HTML\n    cy.get(\".action-select\").should(\"have.value\", \"fr-apples\");\n\n    cy.get(\".action-select-multiple\")\n      .select([\"apples\", \"oranges\", \"bananas\"])\n      // when getting multiple values, invoke \"val\" method first\n      .invoke(\"val\")\n      .should(\"deep.equal\", [\"fr-apples\", \"fr-oranges\", \"fr-bananas\"]);\n\n    // Select option(s) with matching value\n    cy.get(\".action-select\")\n      .select(\"fr-bananas\")\n      // can attach an assertion right away to the element\n      .should(\"have.value\", \"fr-bananas\");\n\n    cy.get(\".action-select-multiple\")\n      .select([\"fr-apples\", \"fr-oranges\", \"fr-bananas\"])\n      .invoke(\"val\")\n      .should(\"deep.equal\", [\"fr-apples\", \"fr-oranges\", \"fr-bananas\"]);\n\n    // assert the selected values include oranges\n    cy.get(\".action-select-multiple\")\n      .invoke(\"val\")\n      .should(\"include\", \"fr-oranges\");\n  });\n\n  it(\".scrollIntoView() - scroll an element into view\", () => {\n    // https://on.cypress.io/scrollintoview\n\n    // normally all of these buttons are hidden,\n    // because they're not within\n    // the viewable area of their parent\n    // (we need to scroll to see them)\n    cy.get(\"#scroll-horizontal button\").should(\"not.be.visible\");\n\n    // scroll the button into view, as if the user had scrolled\n    cy.get(\"#scroll-horizontal button\")\n      .scrollIntoView()\n      .should(\"be.visible\");\n\n    cy.get(\"#scroll-vertical button\").should(\"not.be.visible\");\n\n    // Cypress handles the scroll direction needed\n    cy.get(\"#scroll-vertical button\")\n      .scrollIntoView()\n      .should(\"be.visible\");\n\n    cy.get(\"#scroll-both button\").should(\"not.be.visible\");\n\n    // Cypress knows to scroll to the right and down\n    cy.get(\"#scroll-both button\")\n      .scrollIntoView()\n      .should(\"be.visible\");\n  });\n\n  it(\".trigger() - trigger an event on a DOM element\", () => {\n    // https://on.cypress.io/trigger\n\n    // To interact with a range input (slider)\n    // we need to set its value & trigger the\n    // event to signal it changed\n\n    // Here, we invoke jQuery's val() method to set\n    // the value and trigger the 'change' event\n    cy.get(\".trigger-input-range\")\n      .invoke(\"val\", 25)\n      .trigger(\"change\")\n      .get(\"input[type=range]\")\n      .siblings(\"p\")\n      .should(\"have.text\", \"25\");\n  });\n\n  it(\"cy.scrollTo() - scroll the window or element to a position\", () => {\n    // https://on.cypress.io/scrollTo\n\n    // You can scroll to 9 specific positions of an element:\n    //  -----------------------------------\n    // | topLeft        top       topRight |\n    // |                                   |\n    // |                                   |\n    // |                                   |\n    // | left          center        right |\n    // |                                   |\n    // |                                   |\n    // |                                   |\n    // | bottomLeft   bottom   bottomRight |\n    //  -----------------------------------\n\n    // if you chain .scrollTo() off of cy, we will\n    // scroll the entire window\n    cy.scrollTo(\"bottom\");\n\n    cy.get(\"#scrollable-horizontal\").scrollTo(\"right\");\n\n    // or you can scroll to a specific coordinate:\n    // (x axis, y axis) in pixels\n    cy.get(\"#scrollable-vertical\").scrollTo(250, 250);\n\n    // or you can scroll to a specific percentage\n    // of the (width, height) of the element\n    cy.get(\"#scrollable-both\").scrollTo(\"75%\", \"25%\");\n\n    // control the easing of the scroll (default is 'swing')\n    cy.get(\"#scrollable-vertical\").scrollTo(\"center\", { easing: \"linear\" });\n\n    // control the duration of the scroll (in ms)\n    cy.get(\"#scrollable-both\").scrollTo(\"center\", { duration: 2000 });\n  });\n});\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/aliasing.spec.js",
    "content": "/// <reference types=\"cypress\" />\n\ncontext(\"Aliasing\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress.io/commands/aliasing\");\n  });\n\n  it(\".as() - alias a DOM element for later use\", () => {\n    // https://on.cypress.io/as\n\n    // Alias a DOM element for use later\n    // We don't have to traverse to the element\n    // later in our code, we reference it with @\n\n    cy.get(\".as-table\")\n      .find(\"tbody>tr\")\n      .first()\n      .find(\"td\")\n      .first()\n      .find(\"button\")\n      .as(\"firstBtn\");\n\n    // when we reference the alias, we place an\n    // @ in front of its name\n    cy.get(\"@firstBtn\").click();\n\n    cy.get(\"@firstBtn\")\n      .should(\"have.class\", \"btn-success\")\n      .and(\"contain\", \"Changed\");\n  });\n\n  it(\".as() - alias a route for later use\", () => {\n    // Alias the route to wait for its response\n    cy.server();\n    cy.route(\"GET\", \"comments/*\").as(\"getComment\");\n\n    // we have code that gets a comment when\n    // the button is clicked in scripts.js\n    cy.get(\".network-btn\").click();\n\n    // https://on.cypress.io/wait\n    cy.wait(\"@getComment\")\n      .its(\"status\")\n      .should(\"eq\", 200);\n  });\n});\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/assertions.spec.js",
    "content": "/// <reference types=\"cypress\" />\n\ncontext(\"Assertions\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress.io/commands/assertions\");\n  });\n\n  describe(\"Implicit Assertions\", () => {\n    it(\".should() - make an assertion about the current subject\", () => {\n      // https://on.cypress.io/should\n      cy.get(\".assertion-table\")\n        .find(\"tbody tr:last\")\n        .should(\"have.class\", \"success\")\n        .find(\"td\")\n        .first()\n        // checking the text of the <td> element in various ways\n        .should(\"have.text\", \"Column content\")\n        .should(\"contain\", \"Column content\")\n        .should(\"have.html\", \"Column content\")\n        // chai-jquery uses \"is()\" to check if element matches selector\n        .should(\"match\", \"td\")\n        // to match text content against a regular expression\n        // first need to invoke jQuery method text()\n        // and then match using regular expression\n        .invoke(\"text\")\n        .should(\"match\", /column content/i);\n\n      // a better way to check element's text content against a regular expression\n      // is to use \"cy.contains\"\n      // https://on.cypress.io/contains\n      cy.get(\".assertion-table\")\n        .find(\"tbody tr:last\")\n        // finds first <td> element with text content matching regular expression\n        .contains(\"td\", /column content/i)\n        .should(\"be.visible\");\n\n      // for more information about asserting element's text\n      // see https://on.cypress.io/using-cypress-faq#How-do-I-get-an-element’s-text-contents\n    });\n\n    it(\".and() - chain multiple assertions together\", () => {\n      // https://on.cypress.io/and\n      cy.get(\".assertions-link\")\n        .should(\"have.class\", \"active\")\n        .and(\"have.attr\", \"href\")\n        .and(\"include\", \"cypress.io\");\n    });\n  });\n\n  describe(\"Explicit Assertions\", () => {\n    // https://on.cypress.io/assertions\n    it(\"expect - make an assertion about a specified subject\", () => {\n      // We can use Chai's BDD style assertions\n      expect(true).to.be.true;\n      const o = { foo: \"bar\" };\n\n      expect(o).to.equal(o);\n      expect(o).to.deep.equal({ foo: \"bar\" });\n      // matching text using regular expression\n      expect(\"FooBar\").to.match(/bar$/i);\n    });\n\n    it(\"pass your own callback function to should()\", () => {\n      // Pass a function to should that can have any number\n      // of explicit assertions within it.\n      // The \".should(cb)\" function will be retried\n      // automatically until it passes all your explicit assertions or times out.\n      cy.get(\".assertions-p\")\n        .find(\"p\")\n        .should($p => {\n          // https://on.cypress.io/$\n          // return an array of texts from all of the p's\n          // @ts-ignore TS6133 unused variable\n          const texts = $p.map((i, el) => Cypress.$(el).text());\n\n          // jquery map returns jquery object\n          // and .get() convert this to simple array\n          const paragraphs = texts.get();\n\n          // array should have length of 3\n          expect(paragraphs, \"has 3 paragraphs\").to.have.length(3);\n\n          // use second argument to expect(...) to provide clear\n          // message with each assertion\n          expect(paragraphs, \"has expected text in each paragraph\").to.deep.eq([\n            \"Some text from first p\",\n            \"More text from second p\",\n            \"And even more text from third p\"\n          ]);\n        });\n    });\n\n    it(\"finds element by class name regex\", () => {\n      cy.get(\".docs-header\")\n        .find(\"div\")\n        // .should(cb) callback function will be retried\n        .should($div => {\n          expect($div).to.have.length(1);\n\n          const className = $div[0].className;\n\n          expect(className).to.match(/heading-/);\n        })\n        // .then(cb) callback is not retried,\n        // it either passes or fails\n        .then($div => {\n          expect($div, \"text content\").to.have.text(\"Introduction\");\n        });\n    });\n\n    it(\"can throw any error\", () => {\n      cy.get(\".docs-header\")\n        .find(\"div\")\n        .should($div => {\n          if ($div.length !== 1) {\n            // you can throw your own errors\n            throw new Error(\"Did not find 1 element\");\n          }\n\n          const className = $div[0].className;\n\n          if (!className.match(/heading-/)) {\n            throw new Error(`Could not find class \"heading-\" in ${className}`);\n          }\n        });\n    });\n\n    it(\"matches unknown text between two elements\", () => {\n      /**\n       * Text from the first element.\n       * @type {string}\n       */\n      let text;\n\n      /**\n       * Normalizes passed text,\n       * useful before comparing text with spaces and different capitalization.\n       * @param {string} s Text to normalize\n       */\n      const normalizeText = s => s.replace(/\\s/g, \"\").toLowerCase();\n\n      cy.get(\".two-elements\")\n        .find(\".first\")\n        .then($first => {\n          // save text from the first element\n          text = normalizeText($first.text());\n        });\n\n      cy.get(\".two-elements\")\n        .find(\".second\")\n        .should($div => {\n          // we can massage text before comparing\n          const secondText = normalizeText($div.text());\n\n          expect(secondText, \"second text\").to.equal(text);\n        });\n    });\n\n    it(\"assert - assert shape of an object\", () => {\n      const person = {\n        name: \"Joe\",\n        age: 20\n      };\n\n      assert.isObject(person, \"value is object\");\n    });\n\n    it(\"retries the should callback until assertions pass\", () => {\n      cy.get(\"#random-number\").should($div => {\n        const n = parseFloat($div.text());\n\n        expect(n)\n          .to.be.gte(1)\n          .and.be.lte(10);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/connectors.spec.js",
    "content": "/// <reference types=\"cypress\" />\n\ncontext(\"Connectors\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress.io/commands/connectors\");\n  });\n\n  it(\".each() - iterate over an array of elements\", () => {\n    // https://on.cypress.io/each\n    cy.get(\".connectors-each-ul>li\").each(($el, index, $list) => {\n      console.log($el, index, $list);\n    });\n  });\n\n  it(\".its() - get properties on the current subject\", () => {\n    // https://on.cypress.io/its\n    cy.get(\".connectors-its-ul>li\")\n      // calls the 'length' property yielding that value\n      .its(\"length\")\n      .should(\"be.gt\", 2);\n  });\n\n  it(\".invoke() - invoke a function on the current subject\", () => {\n    // our div is hidden in our script.js\n    // $('.connectors-div').hide()\n\n    // https://on.cypress.io/invoke\n    cy.get(\".connectors-div\")\n      .should(\"be.hidden\")\n      // call the jquery method 'show' on the 'div.container'\n      .invoke(\"show\")\n      .should(\"be.visible\");\n  });\n\n  it(\".spread() - spread an array as individual args to callback function\", () => {\n    // https://on.cypress.io/spread\n    const arr = [\"foo\", \"bar\", \"baz\"];\n\n    cy.wrap(arr).spread((foo, bar, baz) => {\n      expect(foo).to.eq(\"foo\");\n      expect(bar).to.eq(\"bar\");\n      expect(baz).to.eq(\"baz\");\n    });\n  });\n\n  describe(\".then()\", () => {\n    it(\"invokes a callback function with the current subject\", () => {\n      // https://on.cypress.io/then\n      cy.get(\".connectors-list > li\").then($lis => {\n        expect($lis, \"3 items\").to.have.length(3);\n        expect($lis.eq(0), \"first item\").to.contain(\"Walk the dog\");\n        expect($lis.eq(1), \"second item\").to.contain(\"Feed the cat\");\n        expect($lis.eq(2), \"third item\").to.contain(\"Write JavaScript\");\n      });\n    });\n\n    it(\"yields the returned value to the next command\", () => {\n      cy.wrap(1)\n        .then(num => {\n          expect(num).to.equal(1);\n\n          return 2;\n        })\n        .then(num => {\n          expect(num).to.equal(2);\n        });\n    });\n\n    it(\"yields the original subject without return\", () => {\n      cy.wrap(1)\n        .then(num => {\n          expect(num).to.equal(1);\n          // note that nothing is returned from this callback\n        })\n        .then(num => {\n          // this callback receives the original unchanged value 1\n          expect(num).to.equal(1);\n        });\n    });\n\n    it(\"yields the value yielded by the last Cypress command inside\", () => {\n      cy.wrap(1)\n        .then(num => {\n          expect(num).to.equal(1);\n          // note how we run a Cypress command\n          // the result yielded by this Cypress command\n          // will be passed to the second \".then\"\n          cy.wrap(2);\n        })\n        .then(num => {\n          // this callback receives the value yielded by \"cy.wrap(2)\"\n          expect(num).to.equal(2);\n        });\n    });\n  });\n});\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/cookies.spec.js",
    "content": "/// <reference types=\"cypress\" />\n\ncontext(\"Cookies\", () => {\n  beforeEach(() => {\n    Cypress.Cookies.debug(true);\n\n    cy.visit(\"https://example.cypress.io/commands/cookies\");\n\n    // clear cookies again after visiting to remove\n    // any 3rd party cookies picked up such as cloudflare\n    cy.clearCookies();\n  });\n\n  it(\"cy.getCookie() - get a browser cookie\", () => {\n    // https://on.cypress.io/getcookie\n    cy.get(\"#getCookie .set-a-cookie\").click();\n\n    // cy.getCookie() yields a cookie object\n    cy.getCookie(\"token\").should(\"have.property\", \"value\", \"123ABC\");\n  });\n\n  it(\"cy.getCookies() - get browser cookies\", () => {\n    // https://on.cypress.io/getcookies\n    cy.getCookies().should(\"be.empty\");\n\n    cy.get(\"#getCookies .set-a-cookie\").click();\n\n    // cy.getCookies() yields an array of cookies\n    cy.getCookies()\n      .should(\"have.length\", 1)\n      .should(cookies => {\n        // each cookie has these properties\n        expect(cookies[0]).to.have.property(\"name\", \"token\");\n        expect(cookies[0]).to.have.property(\"value\", \"123ABC\");\n        expect(cookies[0]).to.have.property(\"httpOnly\", false);\n        expect(cookies[0]).to.have.property(\"secure\", false);\n        expect(cookies[0]).to.have.property(\"domain\");\n        expect(cookies[0]).to.have.property(\"path\");\n      });\n  });\n\n  it(\"cy.setCookie() - set a browser cookie\", () => {\n    // https://on.cypress.io/setcookie\n    cy.getCookies().should(\"be.empty\");\n\n    cy.setCookie(\"foo\", \"bar\");\n\n    // cy.getCookie() yields a cookie object\n    cy.getCookie(\"foo\").should(\"have.property\", \"value\", \"bar\");\n  });\n\n  it(\"cy.clearCookie() - clear a browser cookie\", () => {\n    // https://on.cypress.io/clearcookie\n    cy.getCookie(\"token\").should(\"be.null\");\n\n    cy.get(\"#clearCookie .set-a-cookie\").click();\n\n    cy.getCookie(\"token\").should(\"have.property\", \"value\", \"123ABC\");\n\n    // cy.clearCookies() yields null\n    cy.clearCookie(\"token\").should(\"be.null\");\n\n    cy.getCookie(\"token\").should(\"be.null\");\n  });\n\n  it(\"cy.clearCookies() - clear browser cookies\", () => {\n    // https://on.cypress.io/clearcookies\n    cy.getCookies().should(\"be.empty\");\n\n    cy.get(\"#clearCookies .set-a-cookie\").click();\n\n    cy.getCookies().should(\"have.length\", 1);\n\n    // cy.clearCookies() yields null\n    cy.clearCookies();\n\n    cy.getCookies().should(\"be.empty\");\n  });\n});\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/cypress_api.spec.js",
    "content": "/// <reference types=\"cypress\" />\n\ncontext(\"Cypress.Commands\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress.io/cypress-api\");\n  });\n\n  // https://on.cypress.io/custom-commands\n\n  it(\".add() - create a custom command\", () => {\n    Cypress.Commands.add(\n      \"console\",\n      {\n        prevSubject: true\n      },\n      (subject, method) => {\n        // the previous subject is automatically received\n        // and the commands arguments are shifted\n\n        // allow us to change the console method used\n        method = method || \"log\";\n\n        // log the subject to the console\n        // @ts-ignore TS7017\n        console[method](\"The subject is\", subject);\n\n        // whatever we return becomes the new subject\n        // we don't want to change the subject so\n        // we return whatever was passed in\n        return subject;\n      }\n    );\n\n    // @ts-ignore TS2339\n    cy.get(\"button\")\n      .console(\"info\")\n      .then($button => {\n        // subject is still $button\n      });\n  });\n});\n\ncontext(\"Cypress.Cookies\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress.io/cypress-api\");\n  });\n\n  // https://on.cypress.io/cookies\n  it(\".debug() - enable or disable debugging\", () => {\n    Cypress.Cookies.debug(true);\n\n    // Cypress will now log in the console when\n    // cookies are set or cleared\n    cy.setCookie(\"fakeCookie\", \"123ABC\");\n    cy.clearCookie(\"fakeCookie\");\n    cy.setCookie(\"fakeCookie\", \"123ABC\");\n    cy.clearCookie(\"fakeCookie\");\n    cy.setCookie(\"fakeCookie\", \"123ABC\");\n  });\n\n  it(\".preserveOnce() - preserve cookies by key\", () => {\n    // normally cookies are reset after each test\n    cy.getCookie(\"fakeCookie\").should(\"not.be.ok\");\n\n    // preserving a cookie will not clear it when\n    // the next test starts\n    cy.setCookie(\"lastCookie\", \"789XYZ\");\n    Cypress.Cookies.preserveOnce(\"lastCookie\");\n  });\n\n  it(\".defaults() - set defaults for all cookies\", () => {\n    // now any cookie with the name 'session_id' will\n    // not be cleared before each new test runs\n    Cypress.Cookies.defaults({\n      whitelist: \"session_id\"\n    });\n  });\n});\n\ncontext(\"Cypress.Server\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress.io/cypress-api\");\n  });\n\n  // Permanently override server options for\n  // all instances of cy.server()\n\n  // https://on.cypress.io/cypress-server\n  it(\".defaults() - change default config of server\", () => {\n    Cypress.Server.defaults({\n      delay: 0,\n      force404: false\n    });\n  });\n});\n\ncontext(\"Cypress.arch\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress.io/cypress-api\");\n  });\n\n  it(\"Get CPU architecture name of underlying OS\", () => {\n    // https://on.cypress.io/arch\n    expect(Cypress.arch).to.exist;\n  });\n});\n\ncontext(\"Cypress.config()\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress.io/cypress-api\");\n  });\n\n  it(\"Get and set configuration options\", () => {\n    // https://on.cypress.io/config\n    let myConfig = Cypress.config();\n\n    expect(myConfig).to.have.property(\"animationDistanceThreshold\", 5);\n    expect(myConfig).to.have.property(\"baseUrl\", null);\n    expect(myConfig).to.have.property(\"defaultCommandTimeout\", 4000);\n    expect(myConfig).to.have.property(\"requestTimeout\", 5000);\n    expect(myConfig).to.have.property(\"responseTimeout\", 30000);\n    expect(myConfig).to.have.property(\"viewportHeight\", 660);\n    expect(myConfig).to.have.property(\"viewportWidth\", 1000);\n    expect(myConfig).to.have.property(\"pageLoadTimeout\", 60000);\n    expect(myConfig).to.have.property(\"waitForAnimations\", true);\n\n    expect(Cypress.config(\"pageLoadTimeout\")).to.eq(60000);\n\n    // this will change the config for the rest of your tests!\n    Cypress.config(\"pageLoadTimeout\", 20000);\n\n    expect(Cypress.config(\"pageLoadTimeout\")).to.eq(20000);\n\n    Cypress.config(\"pageLoadTimeout\", 60000);\n  });\n});\n\ncontext(\"Cypress.dom\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress.io/cypress-api\");\n  });\n\n  // https://on.cypress.io/dom\n  it(\".isHidden() - determine if a DOM element is hidden\", () => {\n    let hiddenP = Cypress.$(\".dom-p p.hidden\").get(0);\n    let visibleP = Cypress.$(\".dom-p p.visible\").get(0);\n\n    // our first paragraph has css class 'hidden'\n    expect(Cypress.dom.isHidden(hiddenP)).to.be.true;\n    expect(Cypress.dom.isHidden(visibleP)).to.be.false;\n  });\n});\n\ncontext(\"Cypress.env()\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress.io/cypress-api\");\n  });\n\n  // We can set environment variables for highly dynamic values\n\n  // https://on.cypress.io/environment-variables\n  it(\"Get environment variables\", () => {\n    // https://on.cypress.io/env\n    // set multiple environment variables\n    Cypress.env({\n      host: \"veronica.dev.local\",\n      api_server: \"http://localhost:8888/v1/\"\n    });\n\n    // get environment variable\n    expect(Cypress.env(\"host\")).to.eq(\"veronica.dev.local\");\n\n    // set environment variable\n    Cypress.env(\"api_server\", \"http://localhost:8888/v2/\");\n    expect(Cypress.env(\"api_server\")).to.eq(\"http://localhost:8888/v2/\");\n\n    // get all environment variable\n    expect(Cypress.env()).to.have.property(\"host\", \"veronica.dev.local\");\n    expect(Cypress.env()).to.have.property(\n      \"api_server\",\n      \"http://localhost:8888/v2/\"\n    );\n  });\n});\n\ncontext(\"Cypress.log\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress.io/cypress-api\");\n  });\n\n  it(\"Control what is printed to the Command Log\", () => {\n    // https://on.cypress.io/cypress-log\n  });\n});\n\ncontext(\"Cypress.platform\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress.io/cypress-api\");\n  });\n\n  it(\"Get underlying OS name\", () => {\n    // https://on.cypress.io/platform\n    expect(Cypress.platform).to.be.exist;\n  });\n});\n\ncontext(\"Cypress.version\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress.io/cypress-api\");\n  });\n\n  it(\"Get current version of Cypress being run\", () => {\n    // https://on.cypress.io/version\n    expect(Cypress.version).to.be.exist;\n  });\n});\n\ncontext(\"Cypress.spec\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress.io/cypress-api\");\n  });\n\n  it(\"Get current spec information\", () => {\n    // https://on.cypress.io/spec\n    // wrap the object so we can inspect it easily by clicking in the command log\n    cy.wrap(Cypress.spec).should(\"include.keys\", [\n      \"name\",\n      \"relative\",\n      \"absolute\"\n    ]);\n  });\n});\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/files.spec.js",
    "content": "/// <reference types=\"cypress\" />\n\n/// JSON fixture file can be loaded directly using\n// the built-in JavaScript bundler\n// @ts-ignore\nconst requiredExample = require(\"../../fixtures/example\");\n\ncontext(\"Files\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress.io/commands/files\");\n  });\n\n  beforeEach(() => {\n    // load example.json fixture file and store\n    // in the test context object\n    cy.fixture(\"example.json\").as(\"example\");\n  });\n\n  it(\"cy.fixture() - load a fixture\", () => {\n    // https://on.cypress.io/fixture\n\n    // Instead of writing a response inline you can\n    // use a fixture file's content.\n\n    cy.server();\n    cy.fixture(\"example.json\").as(\"comment\");\n    // when application makes an Ajax request matching \"GET comments/*\"\n    // Cypress will intercept it and reply with object\n    // from the \"comment\" alias\n    cy.route(\"GET\", \"comments/*\", \"@comment\").as(\"getComment\");\n\n    // we have code that gets a comment when\n    // the button is clicked in scripts.js\n    cy.get(\".fixture-btn\").click();\n\n    cy.wait(\"@getComment\")\n      .its(\"responseBody\")\n      .should(\"have.property\", \"name\")\n      .and(\"include\", \"Using fixtures to represent data\");\n\n    // you can also just write the fixture in the route\n    cy.route(\"GET\", \"comments/*\", \"fixture:example.json\").as(\"getComment\");\n\n    // we have code that gets a comment when\n    // the button is clicked in scripts.js\n    cy.get(\".fixture-btn\").click();\n\n    cy.wait(\"@getComment\")\n      .its(\"responseBody\")\n      .should(\"have.property\", \"name\")\n      .and(\"include\", \"Using fixtures to represent data\");\n\n    // or write fx to represent fixture\n    // by default it assumes it's .json\n    cy.route(\"GET\", \"comments/*\", \"fx:example\").as(\"getComment\");\n\n    // we have code that gets a comment when\n    // the button is clicked in scripts.js\n    cy.get(\".fixture-btn\").click();\n\n    cy.wait(\"@getComment\")\n      .its(\"responseBody\")\n      .should(\"have.property\", \"name\")\n      .and(\"include\", \"Using fixtures to represent data\");\n  });\n\n  it(\"cy.fixture() or require - load a fixture\", function() {\n    // we are inside the \"function () { ... }\"\n    // callback and can use test context object \"this\"\n    // \"this.example\" was loaded in \"beforeEach\" function callback\n    expect(this.example, \"fixture in the test context\").to.deep.equal(\n      requiredExample\n    );\n\n    // or use \"cy.wrap\" and \"should('deep.equal', ...)\" assertion\n    // @ts-ignore\n    cy.wrap(this.example, \"fixture vs require\").should(\n      \"deep.equal\",\n      requiredExample\n    );\n  });\n\n  it(\"cy.readFile() - read file contents\", () => {\n    // https://on.cypress.io/readfile\n\n    // You can read a file and yield its contents\n    // The filePath is relative to your project's root.\n    cy.readFile(\"cypress.json\").then(json => {\n      expect(json).to.be.an(\"object\");\n    });\n  });\n\n  it(\"cy.writeFile() - write to a file\", () => {\n    // https://on.cypress.io/writefile\n\n    // You can write to a file\n\n    // Use a response from a request to automatically\n    // generate a fixture file for use later\n    cy.request(\"https://jsonplaceholder.cypress.io/users\").then(response => {\n      cy.writeFile(\"cypress/fixtures/users.json\", response.body);\n    });\n\n    cy.fixture(\"users\").should(users => {\n      expect(users[0].name).to.exist;\n    });\n\n    // JavaScript arrays and objects are stringified\n    // and formatted into text.\n    cy.writeFile(\"cypress/fixtures/profile.json\", {\n      id: 8739,\n      name: \"Jane\",\n      email: \"jane@example.com\"\n    });\n\n    cy.fixture(\"profile\").should(profile => {\n      expect(profile.name).to.eq(\"Jane\");\n    });\n  });\n});\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/local_storage.spec.js",
    "content": "/// <reference types=\"cypress\" />\n\ncontext(\"Local Storage\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress.io/commands/local-storage\");\n  });\n  // Although local storage is automatically cleared\n  // in between tests to maintain a clean state\n  // sometimes we need to clear the local storage manually\n\n  it(\"cy.clearLocalStorage() - clear all data in local storage\", () => {\n    // https://on.cypress.io/clearlocalstorage\n    cy.get(\".ls-btn\")\n      .click()\n      .should(() => {\n        expect(localStorage.getItem(\"prop1\")).to.eq(\"red\");\n        expect(localStorage.getItem(\"prop2\")).to.eq(\"blue\");\n        expect(localStorage.getItem(\"prop3\")).to.eq(\"magenta\");\n      });\n\n    // clearLocalStorage() yields the localStorage object\n    cy.clearLocalStorage().should(ls => {\n      expect(ls.getItem(\"prop1\")).to.be.null;\n      expect(ls.getItem(\"prop2\")).to.be.null;\n      expect(ls.getItem(\"prop3\")).to.be.null;\n    });\n\n    // Clear key matching string in Local Storage\n    cy.get(\".ls-btn\")\n      .click()\n      .should(() => {\n        expect(localStorage.getItem(\"prop1\")).to.eq(\"red\");\n        expect(localStorage.getItem(\"prop2\")).to.eq(\"blue\");\n        expect(localStorage.getItem(\"prop3\")).to.eq(\"magenta\");\n      });\n\n    cy.clearLocalStorage(\"prop1\").should(ls => {\n      expect(ls.getItem(\"prop1\")).to.be.null;\n      expect(ls.getItem(\"prop2\")).to.eq(\"blue\");\n      expect(ls.getItem(\"prop3\")).to.eq(\"magenta\");\n    });\n\n    // Clear keys matching regex in Local Storage\n    cy.get(\".ls-btn\")\n      .click()\n      .should(() => {\n        expect(localStorage.getItem(\"prop1\")).to.eq(\"red\");\n        expect(localStorage.getItem(\"prop2\")).to.eq(\"blue\");\n        expect(localStorage.getItem(\"prop3\")).to.eq(\"magenta\");\n      });\n\n    cy.clearLocalStorage(/prop1|2/).should(ls => {\n      expect(ls.getItem(\"prop1\")).to.be.null;\n      expect(ls.getItem(\"prop2\")).to.be.null;\n      expect(ls.getItem(\"prop3\")).to.eq(\"magenta\");\n    });\n  });\n});\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/location.spec.js",
    "content": "/// <reference types=\"cypress\" />\n\ncontext(\"Location\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress.io/commands/location\");\n  });\n\n  it(\"cy.hash() - get the current URL hash\", () => {\n    // https://on.cypress.io/hash\n    cy.hash().should(\"be.empty\");\n  });\n\n  it(\"cy.location() - get window.location\", () => {\n    // https://on.cypress.io/location\n    cy.location().should(location => {\n      expect(location.hash).to.be.empty;\n      expect(location.href).to.eq(\n        \"https://example.cypress.io/commands/location\"\n      );\n      expect(location.host).to.eq(\"example.cypress.io\");\n      expect(location.hostname).to.eq(\"example.cypress.io\");\n      expect(location.origin).to.eq(\"https://example.cypress.io\");\n      expect(location.pathname).to.eq(\"/commands/location\");\n      expect(location.port).to.eq(\"\");\n      expect(location.protocol).to.eq(\"https:\");\n      expect(location.search).to.be.empty;\n    });\n  });\n\n  it(\"cy.url() - get the current URL\", () => {\n    // https://on.cypress.io/url\n    cy.url().should(\"eq\", \"https://example.cypress.io/commands/location\");\n  });\n});\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/misc.spec.js",
    "content": "/// <reference types=\"cypress\" />\n\ncontext(\"Misc\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress.io/commands/misc\");\n  });\n\n  it(\".end() - end the command chain\", () => {\n    // https://on.cypress.io/end\n\n    // cy.end is useful when you want to end a chain of commands\n    // and force Cypress to re-query from the root element\n    cy.get(\".misc-table\").within(() => {\n      // ends the current chain and yields null\n      cy.contains(\"Cheryl\")\n        .click()\n        .end();\n\n      // queries the entire table again\n      cy.contains(\"Charles\").click();\n    });\n  });\n\n  it(\"cy.exec() - execute a system command\", () => {\n    // execute a system command.\n    // so you can take actions necessary for\n    // your test outside the scope of Cypress.\n    // https://on.cypress.io/exec\n\n    // we can use Cypress.platform string to\n    // select appropriate command\n    // https://on.cypress/io/platform\n    cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`);\n\n    // on CircleCI Windows build machines we have a failure to run bash shell\n    // https://github.com/cypress-io/cypress/issues/5169\n    // so skip some of the tests by passing flag \"--env circle=true\"\n    const isCircleOnWindows =\n      Cypress.platform === \"win32\" && Cypress.env(\"circle\");\n\n    if (isCircleOnWindows) {\n      cy.log(\"Skipping test on CircleCI\");\n\n      return;\n    }\n\n    // cy.exec problem on Shippable CI\n    // https://github.com/cypress-io/cypress/issues/6718\n    const isShippable =\n      Cypress.platform === \"linux\" && Cypress.env(\"shippable\");\n\n    if (isShippable) {\n      cy.log(\"Skipping test on ShippableCI\");\n\n      return;\n    }\n\n    cy.exec(\"echo Jane Lane\")\n      .its(\"stdout\")\n      .should(\"contain\", \"Jane Lane\");\n\n    if (Cypress.platform === \"win32\") {\n      cy.exec(\"print cypress.json\")\n        .its(\"stderr\")\n        .should(\"be.empty\");\n    } else {\n      cy.exec(\"cat cypress.json\")\n        .its(\"stderr\")\n        .should(\"be.empty\");\n\n      cy.exec(\"pwd\")\n        .its(\"code\")\n        .should(\"eq\", 0);\n    }\n  });\n\n  it(\"cy.focused() - get the DOM element that has focus\", () => {\n    // https://on.cypress.io/focused\n    cy.get(\".misc-form\")\n      .find(\"#name\")\n      .click();\n    cy.focused().should(\"have.id\", \"name\");\n\n    cy.get(\".misc-form\")\n      .find(\"#description\")\n      .click();\n    cy.focused().should(\"have.id\", \"description\");\n  });\n\n  context(\"Cypress.Screenshot\", function() {\n    it(\"cy.screenshot() - take a screenshot\", () => {\n      // https://on.cypress.io/screenshot\n      cy.screenshot(\"my-image\");\n    });\n\n    it(\"Cypress.Screenshot.defaults() - change default config of screenshots\", function() {\n      Cypress.Screenshot.defaults({\n        blackout: [\".foo\"],\n        capture: \"viewport\",\n        clip: { x: 0, y: 0, width: 200, height: 200 },\n        scale: false,\n        disableTimersAndAnimations: true,\n        screenshotOnRunFailure: true,\n        onBeforeScreenshot() {},\n        onAfterScreenshot() {}\n      });\n    });\n  });\n\n  it(\"cy.wrap() - wrap an object\", () => {\n    // https://on.cypress.io/wrap\n    cy.wrap({ foo: \"bar\" })\n      .should(\"have.property\", \"foo\")\n      .and(\"include\", \"bar\");\n  });\n});\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/navigation.spec.js",
    "content": "/// <reference types=\"cypress\" />\n\ncontext(\"Navigation\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress.io\");\n    cy.get(\".navbar-nav\")\n      .contains(\"Commands\")\n      .click();\n    cy.get(\".dropdown-menu\")\n      .contains(\"Navigation\")\n      .click();\n  });\n\n  it(\"cy.go() - go back or forward in the browser's history\", () => {\n    // https://on.cypress.io/go\n\n    cy.location(\"pathname\").should(\"include\", \"navigation\");\n\n    cy.go(\"back\");\n    cy.location(\"pathname\").should(\"not.include\", \"navigation\");\n\n    cy.go(\"forward\");\n    cy.location(\"pathname\").should(\"include\", \"navigation\");\n\n    // clicking back\n    cy.go(-1);\n    cy.location(\"pathname\").should(\"not.include\", \"navigation\");\n\n    // clicking forward\n    cy.go(1);\n    cy.location(\"pathname\").should(\"include\", \"navigation\");\n  });\n\n  it(\"cy.reload() - reload the page\", () => {\n    // https://on.cypress.io/reload\n    cy.reload();\n\n    // reload the page without using the cache\n    cy.reload(true);\n  });\n\n  it(\"cy.visit() - visit a remote url\", () => {\n    // https://on.cypress.io/visit\n\n    // Visit any sub-domain of your current domain\n\n    // Pass options to the visit\n    cy.visit(\"https://example.cypress.io/commands/navigation\", {\n      timeout: 50000, // increase total time for the visit to resolve\n      onBeforeLoad(contentWindow) {\n        // contentWindow is the remote page's window object\n        expect(typeof contentWindow === \"object\").to.be.true;\n      },\n      onLoad(contentWindow) {\n        // contentWindow is the remote page's window object\n        expect(typeof contentWindow === \"object\").to.be.true;\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/network_requests.spec.js",
    "content": "/// <reference types=\"cypress\" />\n\ncontext(\"Network Requests\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress.io/commands/network-requests\");\n  });\n\n  // Manage AJAX / XHR requests in your app\n\n  it(\"cy.server() - control behavior of network requests and responses\", () => {\n    // https://on.cypress.io/server\n\n    cy.server().should(server => {\n      // the default options on server\n      // you can override any of these options\n      expect(server.delay).to.eq(0);\n      expect(server.method).to.eq(\"GET\");\n      expect(server.status).to.eq(200);\n      expect(server.headers).to.be.null;\n      expect(server.response).to.be.null;\n      expect(server.onRequest).to.be.undefined;\n      expect(server.onResponse).to.be.undefined;\n      expect(server.onAbort).to.be.undefined;\n\n      // These options control the server behavior\n      // affecting all requests\n\n      // pass false to disable existing route stubs\n      expect(server.enable).to.be.true;\n      // forces requests that don't match your routes to 404\n      expect(server.force404).to.be.false;\n      // whitelists requests from ever being logged or stubbed\n      expect(server.whitelist).to.be.a(\"function\");\n    });\n\n    cy.server({\n      method: \"POST\",\n      delay: 1000,\n      status: 422,\n      response: {}\n    });\n\n    // any route commands will now inherit the above options\n    // from the server. anything we pass specifically\n    // to route will override the defaults though.\n  });\n\n  it(\"cy.request() - make an XHR request\", () => {\n    // https://on.cypress.io/request\n    cy.request(\"https://jsonplaceholder.cypress.io/comments\").should(\n      response => {\n        expect(response.status).to.eq(200);\n        // the server sometimes gets an extra comment posted from another machine\n        // which gets returned as 1 extra object\n        expect(response.body)\n          .to.have.property(\"length\")\n          .and.be.oneOf([500, 501]);\n        expect(response).to.have.property(\"headers\");\n        expect(response).to.have.property(\"duration\");\n      }\n    );\n  });\n\n  it(\"cy.request() - verify response using BDD syntax\", () => {\n    cy.request(\"https://jsonplaceholder.cypress.io/comments\").then(response => {\n      // https://on.cypress.io/assertions\n      expect(response)\n        .property(\"status\")\n        .to.equal(200);\n      expect(response)\n        .property(\"body\")\n        .to.have.property(\"length\")\n        .and.be.oneOf([500, 501]);\n      expect(response).to.include.keys(\"headers\", \"duration\");\n    });\n  });\n\n  it(\"cy.request() with query parameters\", () => {\n    // will execute request\n    // https://jsonplaceholder.cypress.io/comments?postId=1&id=3\n    cy.request({\n      url: \"https://jsonplaceholder.cypress.io/comments\",\n      qs: {\n        postId: 1,\n        id: 3\n      }\n    })\n      .its(\"body\")\n      .should(\"be.an\", \"array\")\n      .and(\"have.length\", 1)\n      .its(\"0\") // yields first element of the array\n      .should(\"contain\", {\n        postId: 1,\n        id: 3\n      });\n  });\n\n  it(\"cy.request() - pass result to the second request\", () => {\n    // first, let's find out the userId of the first user we have\n    cy.request(\"https://jsonplaceholder.cypress.io/users?_limit=1\")\n      .its(\"body\") // yields the response object\n      .its(\"0\") // yields the first element of the returned list\n      // the above two commands its('body').its('0')\n      // can be written as its('body.0')\n      // if you do not care about TypeScript checks\n      .then(user => {\n        expect(user)\n          .property(\"id\")\n          .to.be.a(\"number\");\n        // make a new post on behalf of the user\n        cy.request(\"POST\", \"https://jsonplaceholder.cypress.io/posts\", {\n          userId: user.id,\n          title: \"Cypress Test Runner\",\n          body:\n            \"Fast, easy and reliable testing for anything that runs in a browser.\"\n        });\n      })\n      // note that the value here is the returned value of the 2nd request\n      // which is the new post object\n      .then(response => {\n        expect(response)\n          .property(\"status\")\n          .to.equal(201); // new entity created\n        expect(response)\n          .property(\"body\")\n          .to.contain({\n            title: \"Cypress Test Runner\"\n          });\n\n        // we don't know the exact post id - only that it will be > 100\n        // since JSONPlaceholder has built-in 100 posts\n        expect(response.body)\n          .property(\"id\")\n          .to.be.a(\"number\")\n          .and.to.be.gt(100);\n\n        // we don't know the user id here - since it was in above closure\n        // so in this test just confirm that the property is there\n        expect(response.body)\n          .property(\"userId\")\n          .to.be.a(\"number\");\n      });\n  });\n\n  it(\"cy.request() - save response in the shared test context\", () => {\n    // https://on.cypress.io/variables-and-aliases\n    cy.request(\"https://jsonplaceholder.cypress.io/users?_limit=1\")\n      .its(\"body\")\n      .its(\"0\") // yields the first element of the returned list\n      .as(\"user\") // saves the object in the test context\n      .then(function() {\n        // NOTE 👀\n        //  By the time this callback runs the \"as('user')\" command\n        //  has saved the user object in the test context.\n        //  To access the test context we need to use\n        //  the \"function () { ... }\" callback form,\n        //  otherwise \"this\" points at a wrong or undefined object!\n        cy.request(\"POST\", \"https://jsonplaceholder.cypress.io/posts\", {\n          userId: this.user.id,\n          title: \"Cypress Test Runner\",\n          body:\n            \"Fast, easy and reliable testing for anything that runs in a browser.\"\n        })\n          .its(\"body\")\n          .as(\"post\"); // save the new post from the response\n      })\n      .then(function() {\n        // When this callback runs, both \"cy.request\" API commands have finished\n        // and the test context has \"user\" and \"post\" objects set.\n        // Let's verify them.\n        expect(this.post, \"post has the right user id\")\n          .property(\"userId\")\n          .to.equal(this.user.id);\n      });\n  });\n\n  it(\"cy.route() - route responses to matching requests\", () => {\n    // https://on.cypress.io/route\n\n    let message = \"whoa, this comment does not exist\";\n\n    cy.server();\n\n    // Listen to GET to comments/1\n    cy.route(\"GET\", \"comments/*\").as(\"getComment\");\n\n    // we have code that gets a comment when\n    // the button is clicked in scripts.js\n    cy.get(\".network-btn\").click();\n\n    // https://on.cypress.io/wait\n    cy.wait(\"@getComment\")\n      .its(\"status\")\n      .should(\"eq\", 200);\n\n    // Listen to POST to comments\n    cy.route(\"POST\", \"/comments\").as(\"postComment\");\n\n    // we have code that posts a comment when\n    // the button is clicked in scripts.js\n    cy.get(\".network-post\").click();\n    cy.wait(\"@postComment\").should(xhr => {\n      expect(xhr.requestBody).to.include(\"email\");\n      expect(xhr.requestHeaders).to.have.property(\"Content-Type\");\n      expect(xhr.responseBody).to.have.property(\n        \"name\",\n        \"Using POST in cy.route()\"\n      );\n    });\n\n    // Stub a response to PUT comments/ ****\n    cy.route({\n      method: \"PUT\",\n      url: \"comments/*\",\n      status: 404,\n      response: { error: message },\n      delay: 500\n    }).as(\"putComment\");\n\n    // we have code that puts a comment when\n    // the button is clicked in scripts.js\n    cy.get(\".network-put\").click();\n\n    cy.wait(\"@putComment\");\n\n    // our 404 statusCode logic in scripts.js executed\n    cy.get(\".network-put-comment\").should(\"contain\", message);\n  });\n});\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/querying.spec.js",
    "content": "/// <reference types=\"cypress\" />\n\ncontext(\"Querying\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress.io/commands/querying\");\n  });\n\n  // The most commonly used query is 'cy.get()', you can\n  // think of this like the '$' in jQuery\n\n  it(\"cy.get() - query DOM elements\", () => {\n    // https://on.cypress.io/get\n\n    cy.get(\"#query-btn\").should(\"contain\", \"Button\");\n\n    cy.get(\".query-btn\").should(\"contain\", \"Button\");\n\n    cy.get(\"#querying .well>button:first\").should(\"contain\", \"Button\");\n    //              ↲\n    // Use CSS selectors just like jQuery\n\n    cy.get('[data-test-id=\"test-example\"]').should(\"have.class\", \"example\");\n\n    // 'cy.get()' yields jQuery object, you can get its attribute\n    // by invoking `.attr()` method\n    cy.get('[data-test-id=\"test-example\"]')\n      .invoke(\"attr\", \"data-test-id\")\n      .should(\"equal\", \"test-example\");\n\n    // or you can get element's CSS property\n    cy.get('[data-test-id=\"test-example\"]')\n      .invoke(\"css\", \"position\")\n      .should(\"equal\", \"static\");\n\n    // or use assertions directly during 'cy.get()'\n    // https://on.cypress.io/assertions\n    cy.get('[data-test-id=\"test-example\"]')\n      .should(\"have.attr\", \"data-test-id\", \"test-example\")\n      .and(\"have.css\", \"position\", \"static\");\n  });\n\n  it(\"cy.contains() - query DOM elements with matching content\", () => {\n    // https://on.cypress.io/contains\n    cy.get(\".query-list\")\n      .contains(\"bananas\")\n      .should(\"have.class\", \"third\");\n\n    // we can pass a regexp to `.contains()`\n    cy.get(\".query-list\")\n      .contains(/^b\\w+/)\n      .should(\"have.class\", \"third\");\n\n    cy.get(\".query-list\")\n      .contains(\"apples\")\n      .should(\"have.class\", \"first\");\n\n    // passing a selector to contains will\n    // yield the selector containing the text\n    cy.get(\"#querying\")\n      .contains(\"ul\", \"oranges\")\n      .should(\"have.class\", \"query-list\");\n\n    cy.get(\".query-button\")\n      .contains(\"Save Form\")\n      .should(\"have.class\", \"btn\");\n  });\n\n  it(\".within() - query DOM elements within a specific element\", () => {\n    // https://on.cypress.io/within\n    cy.get(\".query-form\").within(() => {\n      cy.get(\"input:first\").should(\"have.attr\", \"placeholder\", \"Email\");\n      cy.get(\"input:last\").should(\"have.attr\", \"placeholder\", \"Password\");\n    });\n  });\n\n  it(\"cy.root() - query the root DOM element\", () => {\n    // https://on.cypress.io/root\n\n    // By default, root is the document\n    cy.root().should(\"match\", \"html\");\n\n    cy.get(\".query-ul\").within(() => {\n      // In this within, the root is now the ul DOM element\n      cy.root().should(\"have.class\", \"query-ul\");\n    });\n  });\n\n  it(\"best practices - selecting elements\", () => {\n    // https://on.cypress.io/best-practices#Selecting-Elements\n    cy.get(\"[data-cy=best-practices-selecting-elements]\").within(() => {\n      // Worst - too generic, no context\n      cy.get(\"button\").click();\n\n      // Bad. Coupled to styling. Highly subject to change.\n      cy.get(\".btn.btn-large\").click();\n\n      // Average. Coupled to the `name` attribute which has HTML semantics.\n      cy.get(\"[name=submission]\").click();\n\n      // Better. But still coupled to styling or JS event listeners.\n      cy.get(\"#main\").click();\n\n      // Slightly better. Uses an ID but also ensures the element\n      // has an ARIA role attribute\n      cy.get(\"#main[role=button]\").click();\n\n      // Much better. But still coupled to text content that may change.\n      cy.contains(\"Submit\").click();\n\n      // Best. Insulated from all changes.\n      cy.get(\"[data-cy=submit]\").click();\n    });\n  });\n});\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/spies_stubs_clocks.spec.js",
    "content": "/// <reference types=\"cypress\" />\n// remove no check once Cypress.sinon is typed\n// https://github.com/cypress-io/cypress/issues/6720\n\ncontext(\"Spies, Stubs, and Clock\", () => {\n  it(\"cy.spy() - wrap a method in a spy\", () => {\n    // https://on.cypress.io/spy\n    cy.visit(\"https://example.cypress.io/commands/spies-stubs-clocks\");\n\n    const obj = {\n      foo() {}\n    };\n\n    const spy = cy.spy(obj, \"foo\").as(\"anyArgs\");\n\n    obj.foo();\n\n    expect(spy).to.be.called;\n  });\n\n  it(\"cy.spy() retries until assertions pass\", () => {\n    cy.visit(\"https://example.cypress.io/commands/spies-stubs-clocks\");\n\n    const obj = {\n      /**\n       * Prints the argument passed\n       * @param x {any}\n       */\n      foo(x) {\n        console.log(\"obj.foo called with\", x);\n      }\n    };\n\n    cy.spy(obj, \"foo\").as(\"foo\");\n\n    setTimeout(() => {\n      obj.foo(\"first\");\n    }, 500);\n\n    setTimeout(() => {\n      obj.foo(\"second\");\n    }, 2500);\n\n    cy.get(\"@foo\").should(\"have.been.calledTwice\");\n  });\n\n  it(\"cy.stub() - create a stub and/or replace a function with stub\", () => {\n    // https://on.cypress.io/stub\n    cy.visit(\"https://example.cypress.io/commands/spies-stubs-clocks\");\n\n    const obj = {\n      /**\n       * prints both arguments to the console\n       * @param a {string}\n       * @param b {string}\n       */\n      foo(a, b) {\n        console.log(\"a\", a, \"b\", b);\n      }\n    };\n\n    const stub = cy.stub(obj, \"foo\").as(\"foo\");\n\n    obj.foo(\"foo\", \"bar\");\n\n    expect(stub).to.be.called;\n  });\n\n  it(\"cy.clock() - control time in the browser\", () => {\n    // https://on.cypress.io/clock\n\n    // create the date in UTC so its always the same\n    // no matter what local timezone the browser is running in\n    const now = new Date(Date.UTC(2017, 2, 14)).getTime();\n\n    cy.clock(now);\n    cy.visit(\"https://example.cypress.io/commands/spies-stubs-clocks\");\n    cy.get(\"#clock-div\")\n      .click()\n      .should(\"have.text\", \"1489449600\");\n  });\n\n  it(\"cy.tick() - move time in the browser\", () => {\n    // https://on.cypress.io/tick\n\n    // create the date in UTC so its always the same\n    // no matter what local timezone the browser is running in\n    const now = new Date(Date.UTC(2017, 2, 14)).getTime();\n\n    cy.clock(now);\n    cy.visit(\"https://example.cypress.io/commands/spies-stubs-clocks\");\n    cy.get(\"#tick-div\")\n      .click()\n      .should(\"have.text\", \"1489449600\");\n\n    cy.tick(10000); // 10 seconds passed\n    cy.get(\"#tick-div\")\n      .click()\n      .should(\"have.text\", \"1489449610\");\n  });\n\n  it(\"cy.stub() matches depending on arguments\", () => {\n    // see all possible matchers at\n    // https://sinonjs.org/releases/latest/matchers/\n    const greeter = {\n      /**\n       * Greets a person\n       * @param {string} name\n       */\n      greet(name) {\n        return `Hello, ${name}!`;\n      }\n    };\n\n    cy.stub(greeter, \"greet\")\n      .callThrough() // if you want non-matched calls to call the real method\n      .withArgs(Cypress.sinon.match.string)\n      .returns(\"Hi\")\n      .withArgs(Cypress.sinon.match.number)\n      .throws(new Error(\"Invalid name\"));\n\n    expect(greeter.greet(\"World\")).to.equal(\"Hi\");\n    // @ts-ignore\n    expect(() => greeter.greet(42)).to.throw(\"Invalid name\");\n    expect(greeter.greet).to.have.been.calledTwice;\n\n    // non-matched calls goes the actual method\n    // @ts-ignore\n    expect(greeter.greet()).to.equal(\"Hello, undefined!\");\n  });\n\n  it(\"matches call arguments using Sinon matchers\", () => {\n    // see all possible matchers at\n    // https://sinonjs.org/releases/latest/matchers/\n    const calculator = {\n      /**\n       * returns the sum of two arguments\n       * @param a {number}\n       * @param b {number}\n       */\n      add(a, b) {\n        return a + b;\n      }\n    };\n\n    const spy = cy.spy(calculator, \"add\").as(\"add\");\n\n    expect(calculator.add(2, 3)).to.equal(5);\n\n    // if we want to assert the exact values used during the call\n    expect(spy).to.be.calledWith(2, 3);\n\n    // let's confirm \"add\" method was called with two numbers\n    expect(spy).to.be.calledWith(\n      Cypress.sinon.match.number,\n      Cypress.sinon.match.number\n    );\n\n    // alternatively, provide the value to match\n    expect(spy).to.be.calledWith(\n      Cypress.sinon.match(2),\n      Cypress.sinon.match(3)\n    );\n\n    // match any value\n    expect(spy).to.be.calledWith(Cypress.sinon.match.any, 3);\n\n    // match any value from a list\n    expect(spy).to.be.calledWith(Cypress.sinon.match.in([1, 2, 3]), 3);\n\n    /**\n     * Returns true if the given number is event\n     * @param {number} x\n     */\n    const isEven = x => x % 2 === 0;\n\n    // expect the value to pass a custom predicate function\n    // the second argument to \"sinon.match(predicate, message)\" is\n    // shown if the predicate does not pass and assertion fails\n    expect(spy).to.be.calledWith(Cypress.sinon.match(isEven, \"isEven\"), 3);\n\n    /**\n     * Returns a function that checks if a given number is larger than the limit\n     * @param {number} limit\n     * @returns {(x: number) => boolean}\n     */\n    const isGreaterThan = limit => x => x > limit;\n\n    /**\n     * Returns a function that checks if a given number is less than the limit\n     * @param {number} limit\n     * @returns {(x: number) => boolean}\n     */\n    const isLessThan = limit => x => x < limit;\n\n    // you can combine several matchers using \"and\", \"or\"\n    expect(spy).to.be.calledWith(\n      Cypress.sinon.match.number,\n      Cypress.sinon\n        .match(isGreaterThan(2), \"> 2\")\n        .and(Cypress.sinon.match(isLessThan(4), \"< 4\"))\n    );\n\n    expect(spy).to.be.calledWith(\n      Cypress.sinon.match.number,\n      Cypress.sinon\n        .match(isGreaterThan(200), \"> 200\")\n        .or(Cypress.sinon.match(3))\n    );\n\n    // matchers can be used from BDD assertions\n    cy.get(\"@add\").should(\n      \"have.been.calledWith\",\n      Cypress.sinon.match.number,\n      Cypress.sinon.match(3)\n    );\n\n    // you can alias matchers for shorter test code\n    const { match: M } = Cypress.sinon;\n\n    cy.get(\"@add\").should(\"have.been.calledWith\", M.number, M(3));\n  });\n});\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/traversal.spec.js",
    "content": "/// <reference types=\"cypress\" />\n\ncontext(\"Traversal\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress.io/commands/traversal\");\n  });\n\n  it(\".children() - get child DOM elements\", () => {\n    // https://on.cypress.io/children\n    cy.get(\".traversal-breadcrumb\")\n      .children(\".active\")\n      .should(\"contain\", \"Data\");\n  });\n\n  it(\".closest() - get closest ancestor DOM element\", () => {\n    // https://on.cypress.io/closest\n    cy.get(\".traversal-badge\")\n      .closest(\"ul\")\n      .should(\"have.class\", \"list-group\");\n  });\n\n  it(\".eq() - get a DOM element at a specific index\", () => {\n    // https://on.cypress.io/eq\n    cy.get(\".traversal-list>li\")\n      .eq(1)\n      .should(\"contain\", \"siamese\");\n  });\n\n  it(\".filter() - get DOM elements that match the selector\", () => {\n    // https://on.cypress.io/filter\n    cy.get(\".traversal-nav>li\")\n      .filter(\".active\")\n      .should(\"contain\", \"About\");\n  });\n\n  it(\".find() - get descendant DOM elements of the selector\", () => {\n    // https://on.cypress.io/find\n    cy.get(\".traversal-pagination\")\n      .find(\"li\")\n      .find(\"a\")\n      .should(\"have.length\", 7);\n  });\n\n  it(\".first() - get first DOM element\", () => {\n    // https://on.cypress.io/first\n    cy.get(\".traversal-table td\")\n      .first()\n      .should(\"contain\", \"1\");\n  });\n\n  it(\".last() - get last DOM element\", () => {\n    // https://on.cypress.io/last\n    cy.get(\".traversal-buttons .btn\")\n      .last()\n      .should(\"contain\", \"Submit\");\n  });\n\n  it(\".next() - get next sibling DOM element\", () => {\n    // https://on.cypress.io/next\n    cy.get(\".traversal-ul\")\n      .contains(\"apples\")\n      .next()\n      .should(\"contain\", \"oranges\");\n  });\n\n  it(\".nextAll() - get all next sibling DOM elements\", () => {\n    // https://on.cypress.io/nextall\n    cy.get(\".traversal-next-all\")\n      .contains(\"oranges\")\n      .nextAll()\n      .should(\"have.length\", 3);\n  });\n\n  it(\".nextUntil() - get next sibling DOM elements until next el\", () => {\n    // https://on.cypress.io/nextuntil\n    cy.get(\"#veggies\")\n      .nextUntil(\"#nuts\")\n      .should(\"have.length\", 3);\n  });\n\n  it(\".not() - remove DOM elements from set of DOM elements\", () => {\n    // https://on.cypress.io/not\n    cy.get(\".traversal-disabled .btn\")\n      .not(\"[disabled]\")\n      .should(\"not.contain\", \"Disabled\");\n  });\n\n  it(\".parent() - get parent DOM element from DOM elements\", () => {\n    // https://on.cypress.io/parent\n    cy.get(\".traversal-mark\")\n      .parent()\n      .should(\"contain\", \"Morbi leo risus\");\n  });\n\n  it(\".parents() - get parent DOM elements from DOM elements\", () => {\n    // https://on.cypress.io/parents\n    cy.get(\".traversal-cite\")\n      .parents()\n      .should(\"match\", \"blockquote\");\n  });\n\n  it(\".parentsUntil() - get parent DOM elements from DOM elements until el\", () => {\n    // https://on.cypress.io/parentsuntil\n    cy.get(\".clothes-nav\")\n      .find(\".active\")\n      .parentsUntil(\".clothes-nav\")\n      .should(\"have.length\", 2);\n  });\n\n  it(\".prev() - get previous sibling DOM element\", () => {\n    // https://on.cypress.io/prev\n    cy.get(\".birds\")\n      .find(\".active\")\n      .prev()\n      .should(\"contain\", \"Lorikeets\");\n  });\n\n  it(\".prevAll() - get all previous sibling DOM elements\", () => {\n    // https://on.cypress.io/prevAll\n    cy.get(\".fruits-list\")\n      .find(\".third\")\n      .prevAll()\n      .should(\"have.length\", 2);\n  });\n\n  it(\".prevUntil() - get all previous sibling DOM elements until el\", () => {\n    // https://on.cypress.io/prevUntil\n    cy.get(\".foods-list\")\n      .find(\"#nuts\")\n      .prevUntil(\"#veggies\")\n      .should(\"have.length\", 3);\n  });\n\n  it(\".siblings() - get all sibling DOM elements\", () => {\n    // https://on.cypress.io/siblings\n    cy.get(\".traversal-pills .active\")\n      .siblings()\n      .should(\"have.length\", 2);\n  });\n});\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/utilities.spec.js",
    "content": "/// <reference types=\"cypress\" />\n\ncontext(\"Utilities\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress.io/utilities\");\n  });\n\n  it(\"Cypress._ - call a lodash method\", () => {\n    // https://on.cypress.io/_\n    cy.request(\"https://jsonplaceholder.cypress.io/users\").then(response => {\n      let ids = Cypress._.chain(response.body)\n        .map(\"id\")\n        .take(3)\n        .value();\n\n      expect(ids).to.deep.eq([1, 2, 3]);\n    });\n  });\n\n  it(\"Cypress.$ - call a jQuery method\", () => {\n    // https://on.cypress.io/$\n    let $li = Cypress.$(\".utility-jquery li:first\");\n\n    cy.wrap($li)\n      .should(\"not.have.class\", \"active\")\n      .click()\n      .should(\"have.class\", \"active\");\n  });\n\n  it(\"Cypress.Blob - blob utilities and base64 string conversion\", () => {\n    // https://on.cypress.io/blob\n    cy.get(\".utility-blob\").then($div => {\n      // https://github.com/nolanlawson/blob-util#imgSrcToDataURL\n      // get the dataUrl string for the javascript-logo\n      return Cypress.Blob.imgSrcToDataURL(\n        \"https://example.cypress.io/assets/img/javascript-logo.png\",\n        undefined,\n        \"anonymous\"\n      ).then(dataUrl => {\n        // create an <img> element and set its src to the dataUrl\n        let img = Cypress.$(\"<img />\", { src: dataUrl });\n\n        // need to explicitly return cy here since we are initially returning\n        // the Cypress.Blob.imgSrcToDataURL promise to our test\n        // append the image\n        $div.append(img);\n\n        cy.get(\".utility-blob img\")\n          .click()\n          .should(\"have.attr\", \"src\", dataUrl);\n      });\n    });\n  });\n\n  it(\"Cypress.minimatch - test out glob patterns against strings\", () => {\n    // https://on.cypress.io/minimatch\n    let matching = Cypress.minimatch(\"/users/1/comments\", \"/users/*/comments\", {\n      matchBase: true\n    });\n\n    expect(matching, \"matching wildcard\").to.be.true;\n\n    matching = Cypress.minimatch(\"/users/1/comments/2\", \"/users/*/comments\", {\n      matchBase: true\n    });\n\n    expect(matching, \"comments\").to.be.false;\n\n    // ** matches against all downstream path segments\n    matching = Cypress.minimatch(\"/foo/bar/baz/123/quux?a=b&c=2\", \"/foo/**\", {\n      matchBase: true\n    });\n\n    expect(matching, \"comments\").to.be.true;\n\n    // whereas * matches only the next path segment\n\n    matching = Cypress.minimatch(\"/foo/bar/baz/123/quux?a=b&c=2\", \"/foo/*\", {\n      matchBase: false\n    });\n\n    expect(matching, \"comments\").to.be.false;\n  });\n\n  it(\"Cypress.moment() - format or parse dates using a moment method\", () => {\n    // https://on.cypress.io/moment\n    const time = Cypress.moment(\"2014-04-25T19:38:53.196Z\")\n      .utc()\n      .format(\"h:mm A\");\n\n    expect(time).to.be.a(\"string\");\n\n    cy.get(\".utility-moment\")\n      .contains(\"3:38 PM\")\n      .should(\"have.class\", \"badge\");\n\n    // the time in the element should be between 3pm and 5pm\n    const start = Cypress.moment(\"3:00 PM\", \"LT\");\n    const end = Cypress.moment(\"5:00 PM\", \"LT\");\n\n    cy.get(\".utility-moment .badge\").should($el => {\n      // parse American time like \"3:38 PM\"\n      const m = Cypress.moment($el.text().trim(), \"LT\");\n\n      // display hours + minutes + AM|PM\n      const f = \"h:mm A\";\n\n      expect(\n        m.isBetween(start, end),\n        `${m.format(f)} should be between ${start.format(f)} and ${end.format(\n          f\n        )}`\n      ).to.be.true;\n    });\n  });\n\n  it(\"Cypress.Promise - instantiate a bluebird promise\", () => {\n    // https://on.cypress.io/promise\n    let waited = false;\n\n    /**\n     * @return Bluebird<string>\n     */\n    function waitOneSecond() {\n      // return a promise that resolves after 1 second\n      // @ts-ignore TS2351 (new Cypress.Promise)\n      return new Cypress.Promise((resolve, reject) => {\n        setTimeout(() => {\n          // set waited to true\n          waited = true;\n\n          // resolve with 'foo' string\n          resolve(\"foo\");\n        }, 1000);\n      });\n    }\n\n    cy.then(() => {\n      // return a promise to cy.then() that\n      // is awaited until it resolves\n      // @ts-ignore TS7006\n      return waitOneSecond().then(str => {\n        expect(str).to.eq(\"foo\");\n        expect(waited).to.be.true;\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/viewport.spec.js",
    "content": "/// <reference types=\"cypress\" />\n\ncontext(\"Viewport\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress.io/commands/viewport\");\n  });\n\n  it(\"cy.viewport() - set the viewport size and dimension\", () => {\n    // https://on.cypress.io/viewport\n\n    cy.get(\"#navbar\").should(\"be.visible\");\n    cy.viewport(320, 480);\n\n    // the navbar should have collapse since our screen is smaller\n    cy.get(\"#navbar\").should(\"not.be.visible\");\n    cy.get(\".navbar-toggle\")\n      .should(\"be.visible\")\n      .click();\n    cy.get(\".nav\")\n      .find(\"a\")\n      .should(\"be.visible\");\n\n    // lets see what our app looks like on a super large screen\n    cy.viewport(2999, 2999);\n\n    // cy.viewport() accepts a set of preset sizes\n    // to easily set the screen to a device's width and height\n\n    // We added a cy.wait() between each viewport change so you can see\n    // the change otherwise it is a little too fast to see :)\n\n    cy.viewport(\"macbook-15\");\n    cy.wait(200);\n    cy.viewport(\"macbook-13\");\n    cy.wait(200);\n    cy.viewport(\"macbook-11\");\n    cy.wait(200);\n    cy.viewport(\"ipad-2\");\n    cy.wait(200);\n    cy.viewport(\"ipad-mini\");\n    cy.wait(200);\n    cy.viewport(\"iphone-6+\");\n    cy.wait(200);\n    cy.viewport(\"iphone-6\");\n    cy.wait(200);\n    cy.viewport(\"iphone-5\");\n    cy.wait(200);\n    cy.viewport(\"iphone-4\");\n    cy.wait(200);\n    cy.viewport(\"iphone-3\");\n    cy.wait(200);\n\n    // cy.viewport() accepts an orientation for all presets\n    // the default orientation is 'portrait'\n    cy.viewport(\"ipad-2\", \"portrait\");\n    cy.wait(200);\n    cy.viewport(\"iphone-4\", \"landscape\");\n    cy.wait(200);\n\n    // The viewport will be reset back to the default dimensions\n    // in between tests (the  default can be set in cypress.json)\n  });\n});\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/waiting.spec.js",
    "content": "/// <reference types=\"cypress\" />\n\ncontext(\"Waiting\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress.io/commands/waiting\");\n  });\n  // BE CAREFUL of adding unnecessary wait times.\n  // https://on.cypress.io/best-practices#Unnecessary-Waiting\n\n  // https://on.cypress.io/wait\n  it(\"cy.wait() - wait for a specific amount of time\", () => {\n    cy.get(\".wait-input1\").type(\"Wait 1000ms after typing\");\n    cy.wait(1000);\n    cy.get(\".wait-input2\").type(\"Wait 1000ms after typing\");\n    cy.wait(1000);\n    cy.get(\".wait-input3\").type(\"Wait 1000ms after typing\");\n    cy.wait(1000);\n  });\n\n  it(\"cy.wait() - wait for a specific route\", () => {\n    cy.server();\n\n    // Listen to GET to comments/1\n    cy.route(\"GET\", \"comments/*\").as(\"getComment\");\n\n    // we have code that gets a comment when\n    // the button is clicked in scripts.js\n    cy.get(\".network-btn\").click();\n\n    // wait for GET comments/1\n    cy.wait(\"@getComment\")\n      .its(\"status\")\n      .should(\"eq\", 200);\n  });\n});\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/window.spec.js",
    "content": "/// <reference types=\"cypress\" />\n\ncontext(\"Window\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress.io/commands/window\");\n  });\n\n  it(\"cy.window() - get the global window object\", () => {\n    // https://on.cypress.io/window\n    cy.window().should(\"have.property\", \"top\");\n  });\n\n  it(\"cy.document() - get the document object\", () => {\n    // https://on.cypress.io/document\n    cy.document()\n      .should(\"have.property\", \"charset\")\n      .and(\"eq\", \"UTF-8\");\n  });\n\n  it(\"cy.title() - get the title\", () => {\n    // https://on.cypress.io/title\n    cy.title().should(\"include\", \"Kitchen Sink\");\n  });\n});\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/plugins/index.js",
    "content": "/// <reference types=\"cypress\" />\n// ***********************************************************\n// This example plugins/index.js can be used to load plugins\n//\n// You can change the location of this file or turn off loading\n// the plugins file with the 'pluginsFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/plugins-guide\n// ***********************************************************\n\n// This function is called when a project is opened or re-opened (e.g. due to\n// the project's config changing)\n\n/**\n * @type {Cypress.PluginConfig}\n */\nmodule.exports = (on, config) => {\n  // `on` is used to hook into various events Cypress emits\n  // `config` is the resolved Cypress config\n};\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/support/commands.js",
    "content": "// ***********************************************\n// This example commands.js shows you how to\n// create various custom commands and overwrite\n// existing commands.\n//\n// For more comprehensive examples of custom\n// commands please read more here:\n// https://on.cypress.io/custom-commands\n// ***********************************************\n//\n//\n// -- This is a parent command --\n// Cypress.Commands.add(\"login\", (email, password) => { ... })\n//\n//\n// -- This is a child command --\n// Cypress.Commands.add(\"drag\", { prevSubject: 'element'}, (subject, options) => { ... })\n//\n//\n// -- This is a dual command --\n// Cypress.Commands.add(\"dismiss\", { prevSubject: 'optional'}, (subject, options) => { ... })\n//\n//\n// -- This will overwrite an existing command --\n// Cypress.Commands.overwrite(\"visit\", (originalFn, url, options) => { ... })\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/support/index.js",
    "content": "// ***********************************************************\n// This example support/index.js is processed and\n// loaded automatically before your test files.\n//\n// This is a great place to put global configuration and\n// behavior that modifies Cypress.\n//\n// You can change the location of this file or turn off\n// automatically serving support files with the\n// 'supportFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/configuration\n// ***********************************************************\n\n// Import commands.js using ES2015 syntax:\nimport \"./commands\";\n\n// Alternatively you can use CommonJS syntax:\n// require('./commands')\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress.json",
    "content": "{}\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/package.json",
    "content": "{\n  \"name\": \"1_setting_up_cypress\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"cypress:open\": \"cypress open\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"cypress\": \"^4.12.1\"\n  }\n}\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/2_writing_your_first_tests/cypress/dbConnection.js",
    "content": "const environmentName = process.env.NODE_ENV;\nconst db = require(\"knex\")(require(\"./knexfile\")[environmentName]);\n\nconst closeConnection = () => db.destroy();\n\nmodule.exports = {\n  db,\n  closeConnection\n};\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/2_writing_your_first_tests/cypress/fixtures/example.json",
    "content": "{\n  \"name\": \"Using fixtures to represent data\",\n  \"email\": \"hello@cypress.io\",\n  \"body\": \"Fixtures are a great way to mock data for responses to routes\"\n}\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/2_writing_your_first_tests/cypress/integration/itemSubmission.spec.js",
    "content": "describe(\"item submission\", () => {\n  beforeEach(() => cy.task(\"emptyInventory\"));\n\n  it(\"can add items through the form\", () => {\n    cy.visit(\"http://localhost:8080\");\n    cy.get('input[placeholder=\"Item name\"]').type(\"cheesecake\");\n    cy.get('input[placeholder=\"Quantity\"]').type(\"10\");\n    cy.get('button[type=\"submit\"]')\n      .contains(\"Add to inventory\")\n      .click();\n\n    cy.get(\"li\").contains(\"cheesecake - Quantity: 10\");\n  });\n\n  it(\"can update an item's quantity\", () => {\n    cy.task(\"seedItem\", { itemName: \"cheesecake\", quantity: 5 });\n    cy.visit(\"http://localhost:8080\");\n    cy.get('input[placeholder=\"Item name\"]').type(\"cheesecake\");\n    cy.get('input[placeholder=\"Quantity\"]').type(\"10\");\n    cy.get('button[type=\"submit\"]')\n      .contains(\"Add to inventory\")\n      .click();\n\n    cy.get(\"li\").contains(\"cheesecake - Quantity: 15\");\n  });\n\n  it(\"can undo submitted items\", () => {\n    cy.visit(\"http://localhost:8080\");\n    cy.get('input[placeholder=\"Item name\"]').type(\"cheesecake\");\n    cy.get('input[placeholder=\"Quantity\"]').type(\"10\");\n    cy.get('button[type=\"submit\"]')\n      .contains(\"Add to inventory\")\n      .click();\n\n    cy.get('input[placeholder=\"Quantity\"]')\n      .clear()\n      .type(\"5\");\n    cy.get('button[type=\"submit\"]')\n      .contains(\"Add to inventory\")\n      .click();\n\n    cy.get(\"button\")\n      .contains(\"Undo\")\n      .click();\n\n    cy.get(\"p\")\n      .then(p => {\n        return Array.from(p).filter(p => {\n          return p.innerText.includes(\n            'The inventory has been updated - {\"cheesecake\":10}'\n          );\n        });\n      })\n      .should(\"have.length\", 2);\n  });\n\n  it(\"saves each submission to the action log\", () => {\n    cy.visit(\"http://localhost:8080\");\n    cy.get('input[placeholder=\"Item name\"]').type(\"cheesecake\");\n    cy.get('input[placeholder=\"Quantity\"]').type(\"10\");\n    cy.get('button[type=\"submit\"]')\n      .contains(\"Add to inventory\")\n      .click();\n\n    cy.get('input[placeholder=\"Quantity\"]')\n      .clear()\n      .type(\"5\");\n    cy.get('button[type=\"submit\"]')\n      .contains(\"Add to inventory\")\n      .click();\n\n    cy.get(\"button\")\n      .contains(\"Undo\")\n      .click();\n\n    cy.get(\"p\").contains(\"The inventory has been updated - {}\");\n    cy.get(\"p\")\n      .then(p => {\n        return Array.from(p).filter(p => {\n          return p.innerText.includes(\n            'The inventory has been updated - {\"cheesecake\":10}'\n          );\n        });\n      })\n      .should(\"have.length\", 2);\n    cy.get(\"p\").contains('The inventory has been updated - {\"cheesecake\":15}');\n  });\n\n  describe(\"given a user enters an invalid item name\", () => {\n    it(\"disables the form's submission button\", () => {\n      cy.visit(\"http://localhost:8080\");\n      cy.get('input[placeholder=\"Item name\"]').type(\"boat\");\n      cy.get('input[placeholder=\"Quantity\"]').type(\"10\");\n      cy.get('button[type=\"submit\"]')\n        .contains(\"Add to inventory\")\n        .should(\"be.disabled\");\n    });\n  });\n});\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/2_writing_your_first_tests/cypress/knexfile.js",
    "content": "module.exports = {\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"../../server/dev.sqlite\" },\n    useNullAsDefault: true\n  }\n};\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/2_writing_your_first_tests/cypress/plugins/dbPlugin.js",
    "content": "const { db } = require(\"../dbConnection\");\n\nconst dbPlugin = (on, config) => {\n  on(\n    \"task\",\n    {\n      emptyInventory: () => db(\"inventory\").truncate(),\n      seedItem: itemRow => db(\"inventory\").insert(itemRow)\n    },\n    config\n  );\n\n  return config;\n};\n\nmodule.exports = dbPlugin;\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/2_writing_your_first_tests/cypress/plugins/index.js",
    "content": "/// <reference types=\"cypress\" />\n// ***********************************************************\n// This example plugins/index.js can be used to load plugins\n//\n// You can change the location of this file or turn off loading\n// the plugins file with the 'pluginsFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/plugins-guide\n// ***********************************************************\n\n// This function is called when a project is opened or re-opened (e.g. due to\n// the project's config changing)\n\nconst dbPlugin = require(\"./dbPlugin\");\n\nmodule.exports = (on, config) => {\n  dbPlugin(on, config);\n};\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/2_writing_your_first_tests/cypress/support/commands.js",
    "content": "// ***********************************************\n// This example commands.js shows you how to\n// create various custom commands and overwrite\n// existing commands.\n//\n// For more comprehensive examples of custom\n// commands please read more here:\n// https://on.cypress.io/custom-commands\n// ***********************************************\n//\n//\n// -- This is a parent command --\n// Cypress.Commands.add(\"login\", (email, password) => { ... })\n//\n//\n// -- This is a child command --\n// Cypress.Commands.add(\"drag\", { prevSubject: 'element'}, (subject, options) => { ... })\n//\n//\n// -- This is a dual command --\n// Cypress.Commands.add(\"dismiss\", { prevSubject: 'optional'}, (subject, options) => { ... })\n//\n//\n// -- This will overwrite an existing command --\n// Cypress.Commands.overwrite(\"visit\", (originalFn, url, options) => { ... })\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/2_writing_your_first_tests/cypress/support/index.js",
    "content": "// ***********************************************************\n// This example support/index.js is processed and\n// loaded automatically before your test files.\n//\n// This is a great place to put global configuration and\n// behavior that modifies Cypress.\n//\n// You can change the location of this file or turn off\n// automatically serving support files with the\n// 'supportFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/configuration\n// ***********************************************************\n\n// Import commands.js using ES2015 syntax:\nimport \"./commands\";\n\n// Alternatively you can use CommonJS syntax:\n// require('./commands')\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/2_writing_your_first_tests/cypress.json",
    "content": "{\n  \"nodeVersion\": \"system\"\n}\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/2_writing_your_first_tests/package.json",
    "content": "{\n  \"name\": \"2_writing_your_first_tests\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"cypress:open\": \"NODE_ENV=development cypress open\",\n    \"cypress:run\": \"NODE_ENV=development cypress run\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"cypress\": \"^4.12.1\",\n    \"knex\": \"^0.20.13\",\n    \"sqlite3\": \"4.1.1\"\n  }\n}\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/3_sending_http_requests/cypress/dbConnection.js",
    "content": "const environmentName = process.env.NODE_ENV;\nconst db = require(\"knex\")(require(\"./knexfile\")[environmentName]);\n\nconst closeConnection = () => db.destroy();\n\nmodule.exports = {\n  db,\n  closeConnection\n};\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/3_sending_http_requests/cypress/fixtures/example.json",
    "content": "{\n  \"name\": \"Using fixtures to represent data\",\n  \"email\": \"hello@cypress.io\",\n  \"body\": \"Fixtures are a great way to mock data for responses to routes\"\n}\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/3_sending_http_requests/cypress/integration/itemListUpdates.spec.js",
    "content": "describe(\"item list updates\", () => {\n  beforeEach(() => cy.task(\"emptyInventory\"));\n\n  describe(\"when the application loads for the first time\", () => {\n    it(\"loads the initial list of items\", () => {\n      cy.addItem(\"cheesecake\", 2);\n      cy.addItem(\"apple pie\", 5);\n      cy.addItem(\"carrot cake\", 96);\n      cy.visit(\"http://localhost:8080\");\n\n      cy.get(\"li\").contains(\"cheesecake - Quantity: 2\");\n      cy.get(\"li\").contains(\"apple pie - Quantity: 5\");\n      cy.get(\"li\").contains(\"carrot cake - Quantity: 96\");\n    });\n  });\n\n  describe(\"as other users add items\", () => {\n    it(\"updates the item list\", () => {\n      cy.visit(\"http://localhost:8080\");\n      cy.wait(2000);\n      cy.addItem(\"cheesecake\", 22);\n      cy.get(\"li\").contains(\"cheesecake - Quantity: 22\");\n    });\n  });\n});\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/3_sending_http_requests/cypress/integration/itemSubmission.spec.js",
    "content": "describe(\"item submission\", () => {\n  beforeEach(() => cy.task(\"emptyInventory\"));\n\n  it(\"can add items through the form\", () => {\n    cy.visit(\"http://localhost:8080\");\n    cy.get('input[placeholder=\"Item name\"]').type(\"cheesecake\");\n    cy.get('input[placeholder=\"Quantity\"]').type(\"10\");\n    cy.get('button[type=\"submit\"]')\n      .contains(\"Add to inventory\")\n      .click();\n\n    cy.get(\"li\").contains(\"cheesecake - Quantity: 10\");\n  });\n\n  it(\"can update an item's quantity\", () => {\n    cy.task(\"seedItem\", { itemName: \"cheesecake\", quantity: 5 });\n    cy.visit(\"http://localhost:8080\");\n    cy.get('input[placeholder=\"Item name\"]').type(\"cheesecake\");\n    cy.get('input[placeholder=\"Quantity\"]').type(\"10\");\n    cy.get('button[type=\"submit\"]')\n      .contains(\"Add to inventory\")\n      .click();\n\n    cy.get(\"li\").contains(\"cheesecake - Quantity: 15\");\n  });\n\n  it(\"can undo submitted items\", () => {\n    cy.visit(\"http://localhost:8080\");\n    cy.get('input[placeholder=\"Item name\"]').type(\"cheesecake\");\n    cy.get('input[placeholder=\"Quantity\"]').type(\"10\");\n    cy.get('button[type=\"submit\"]')\n      .contains(\"Add to inventory\")\n      .click();\n\n    cy.get('input[placeholder=\"Quantity\"]')\n      .clear()\n      .type(\"5\");\n    cy.get('button[type=\"submit\"]')\n      .contains(\"Add to inventory\")\n      .click();\n\n    cy.get(\"button\")\n      .contains(\"Undo\")\n      .click();\n\n    cy.get(\"p\")\n      .then(p => {\n        return Array.from(p).filter(p => {\n          return p.innerText.includes(\n            'The inventory has been updated - {\"cheesecake\":10}'\n          );\n        });\n      })\n      .should(\"have.length\", 2);\n  });\n\n  it(\"saves each submission to the action log\", () => {\n    cy.visit(\"http://localhost:8080\");\n    cy.get('input[placeholder=\"Item name\"]').type(\"cheesecake\");\n    cy.get('input[placeholder=\"Quantity\"]').type(\"10\");\n    cy.get('button[type=\"submit\"]')\n      .contains(\"Add to inventory\")\n      .click();\n\n    cy.get('input[placeholder=\"Quantity\"]')\n      .clear()\n      .type(\"5\");\n    cy.get('button[type=\"submit\"]')\n      .contains(\"Add to inventory\")\n      .click();\n\n    cy.get(\"button\")\n      .contains(\"Undo\")\n      .click();\n\n    cy.get(\"p\").contains(\"The inventory has been updated - {}\");\n    cy.get(\"p\").contains('The inventory has been updated - {\"cheesecake\":15}');\n    cy.get(\"p\")\n      .then(p => {\n        return Array.from(p).filter(p => {\n          return p.innerText.includes(\n            'The inventory has been updated - {\"cheesecake\":10}'\n          );\n        });\n      })\n      .should(\"have.length\", 2);\n  });\n\n  describe(\"given a user enters an invalid item name\", () => {\n    it(\"disables the form's submission button\", () => {\n      cy.visit(\"http://localhost:8080\");\n      cy.get('input[placeholder=\"Item name\"]').type(\"boat\");\n      cy.get('input[placeholder=\"Quantity\"]').type(\"10\");\n      cy.get('button[type=\"submit\"]')\n        .contains(\"Add to inventory\")\n        .should(\"be.disabled\");\n    });\n  });\n});\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/3_sending_http_requests/cypress/knexfile.js",
    "content": "module.exports = {\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"../../server/dev.sqlite\" },\n    useNullAsDefault: true\n  }\n};\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/3_sending_http_requests/cypress/plugins/dbPlugin.js",
    "content": "const { db } = require(\"../dbConnection\");\n\nconst dbPlugin = (on, config) => {\n  on(\n    \"task\",\n    {\n      emptyInventory: () => db(\"inventory\").truncate(),\n      seedItem: itemRow => db(\"inventory\").insert(itemRow)\n    },\n    config\n  );\n\n  return config;\n};\n\nmodule.exports = dbPlugin;\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/3_sending_http_requests/cypress/plugins/index.js",
    "content": "/// <reference types=\"cypress\" />\n// ***********************************************************\n// This example plugins/index.js can be used to load plugins\n//\n// You can change the location of this file or turn off loading\n// the plugins file with the 'pluginsFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/plugins-guide\n// ***********************************************************\n\n// This function is called when a project is opened or re-opened (e.g. due to\n// the project's config changing)\n\nconst dbPlugin = require(\"./dbPlugin\");\n\nmodule.exports = (on, config) => {\n  dbPlugin(on, config);\n};\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/3_sending_http_requests/cypress/support/commands.js",
    "content": "Cypress.Commands.add(\"addItem\", (itemName, quantity) => {\n  return cy.request({\n    url: `http://localhost:3000/inventory/${itemName}`,\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ quantity })\n  });\n});\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/3_sending_http_requests/cypress/support/index.js",
    "content": "// ***********************************************************\n// This example support/index.js is processed and\n// loaded automatically before your test files.\n//\n// This is a great place to put global configuration and\n// behavior that modifies Cypress.\n//\n// You can change the location of this file or turn off\n// automatically serving support files with the\n// 'supportFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/configuration\n// ***********************************************************\n\n// Import commands.js using ES2015 syntax:\nimport \"./commands\";\n\n// Alternatively you can use CommonJS syntax:\n// require('./commands')\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/3_sending_http_requests/cypress.json",
    "content": "{\n  \"nodeVersion\": \"system\"\n}\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/3_sending_http_requests/package.json",
    "content": "{\n  \"name\": \"3_sending_http_requests\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"cypress:open\": \"NODE_ENV=development cypress open\",\n    \"cypress:run\": \"NODE_ENV=development cypress run\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"cypress\": \"^4.12.1\",\n    \"knex\": \"^0.20.13\",\n    \"sqlite3\": \"4.1.1\"\n  }\n}\n"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/dbConnection.js",
    "content": "const environmentName = process.env.NODE_ENV;\nconst db = require(\"knex\")(require(\"./knexfile\")[environmentName]);\n\nconst closeConnection = () => db.destroy();\n\nmodule.exports = {\n  db,\n  closeConnection\n};\n"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/fixtures/example.json",
    "content": "{\n  \"name\": \"Using fixtures to represent data\",\n  \"email\": \"hello@cypress.io\",\n  \"body\": \"Fixtures are a great way to mock data for responses to routes\"\n}\n"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/integration/itemListUpdates.spec.js",
    "content": "import { InventoryManagement } from \"../pageObjects/inventoryManagement\";\n\ndescribe(\"item list updates\", () => {\n  beforeEach(() => cy.task(\"emptyInventory\"));\n\n  describe(\"when the application loads for the first time\", () => {\n    it(\"loads the initial list of items\", () => {\n      cy.addItem(\"cheesecake\", 2);\n      cy.addItem(\"apple pie\", 5);\n      cy.addItem(\"carrot cake\", 96);\n      cy.visit(\"http://localhost:8080\");\n\n      InventoryManagement.findItemEntry(\"cheesecake\", \"2\");\n      InventoryManagement.findItemEntry(\"apple pie\", \"5\");\n      InventoryManagement.findItemEntry(\"carrot cake\", \"96\");\n    });\n  });\n\n  describe(\"as other users add items\", () => {\n    it(\"updates the item list\", () => {\n      cy.visit(\"http://localhost:8080\");\n      InventoryManagement.findAction({});\n      cy.addItem(\"cheesecake\", 22);\n      InventoryManagement.findItemEntry(\"cheesecake\", \"22\");\n    });\n  });\n});\n"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/integration/itemSubmission.spec.js",
    "content": "import { InventoryManagement } from \"../pageObjects/inventoryManagement\";\n\ndescribe(\"item submission\", () => {\n  beforeEach(() => cy.task(\"emptyInventory\"));\n\n  it(\"can add items through the form\", () => {\n    InventoryManagement.visit();\n    InventoryManagement.addItem(\"cheesecake\", \"10\");\n    InventoryManagement.findItemEntry(\"cheesecake\", \"10\");\n  });\n\n  it(\"can update an item's quantity\", () => {\n    cy.task(\"seedItem\", { itemName: \"cheesecake\", quantity: 5 });\n    InventoryManagement.visit();\n    InventoryManagement.addItem(\"cheesecake\", \"10\");\n    InventoryManagement.findItemEntry(\"cheesecake\", \"15\");\n  });\n\n  it(\"can undo submitted items\", () => {\n    InventoryManagement.visit();\n    InventoryManagement.addItem(\"cheesecake\", \"10\");\n    InventoryManagement.addItem(\"cheesecake\", \"5\");\n    InventoryManagement.undo();\n    InventoryManagement.findItemEntry(\"cheesecake\", \"10\");\n  });\n\n  it(\"saves each submission to the action log\", () => {\n    InventoryManagement.visit();\n    InventoryManagement.addItem(\"cheesecake\", \"10\");\n    InventoryManagement.addItem(\"cheesecake\", \"5\");\n    InventoryManagement.undo();\n    InventoryManagement.findItemEntry(\"cheesecake\", \"10\");\n\n    InventoryManagement.findAction({});\n    InventoryManagement.findAction({ cheesecake: 10 }).should(\"have.length\", 2);\n    InventoryManagement.findAction({ cheesecake: 15 });\n  });\n\n  describe(\"given a user enters an invalid item name\", () => {\n    it(\"disables the form's submission button\", () => {\n      InventoryManagement.visit();\n      InventoryManagement.enterItemName(\"boat\");\n      InventoryManagement.enterQuantity(10);\n      InventoryManagement.getSubmitButton().should(\"be.disabled\");\n    });\n  });\n});\n"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/knexfile.js",
    "content": "module.exports = {\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"../../server/dev.sqlite\" },\n    useNullAsDefault: true\n  }\n};\n"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/pageObjects/inventoryManagement.js",
    "content": "export class InventoryManagement {\n  static visit() {\n    cy.visit(\"http://localhost:8080\");\n  }\n\n  static enterItemName(itemName) {\n    return cy\n      .get('input[placeholder=\"Item name\"]')\n      .clear()\n      .type(itemName);\n  }\n\n  static enterQuantity(quantity) {\n    return cy\n      .get('input[placeholder=\"Quantity\"]')\n      .clear()\n      .type(quantity);\n  }\n\n  static getSubmitButton() {\n    return cy.get('button[type=\"submit\"]').contains(\"Add to inventory\");\n  }\n\n  static addItem(itemName, quantity) {\n    InventoryManagement.enterItemName(itemName);\n    InventoryManagement.enterQuantity(quantity);\n    InventoryManagement.getSubmitButton().click();\n  }\n\n  static findItemEntry(itemName, quantity) {\n    return cy.contains(\"li\", `${itemName} - Quantity: ${quantity}`);\n  }\n\n  static undo() {\n    return cy\n      .get(\"button\")\n      .contains(\"Undo\")\n      .click();\n  }\n\n  static findAction(inventoryState) {\n    return cy.get(\"p:not(:nth-of-type(1))\").then(p => {\n      return Array.from(p).filter(p => {\n        return p.innerText.includes(\n          `The inventory has been updated - ${JSON.stringify(inventoryState)}`\n        );\n      });\n    });\n  }\n}\n"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/plugins/dbPlugin.js",
    "content": "const { db } = require(\"../dbConnection\");\n\nconst dbPlugin = (on, config) => {\n  on(\n    \"task\",\n    {\n      emptyInventory: () => db(\"inventory\").truncate(),\n      seedItem: itemRow => db(\"inventory\").insert(itemRow)\n    },\n    config\n  );\n\n  return config;\n};\n\nmodule.exports = dbPlugin;\n"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/plugins/index.js",
    "content": "/// <reference types=\"cypress\" />\n// ***********************************************************\n// This example plugins/index.js can be used to load plugins\n//\n// You can change the location of this file or turn off loading\n// the plugins file with the 'pluginsFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/plugins-guide\n// ***********************************************************\n\n// This function is called when a project is opened or re-opened (e.g. due to\n// the project's config changing)\n\nconst dbPlugin = require(\"./dbPlugin\");\n\nmodule.exports = (on, config) => {\n  dbPlugin(on, config);\n};\n"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/support/commands.js",
    "content": "Cypress.Commands.add(\"addItem\", (itemName, quantity) => {\n  return cy.request({\n    url: `http://localhost:3000/inventory/${itemName}`,\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ quantity })\n  });\n});\n"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/support/index.js",
    "content": "// ***********************************************************\n// This example support/index.js is processed and\n// loaded automatically before your test files.\n//\n// This is a great place to put global configuration and\n// behavior that modifies Cypress.\n//\n// You can change the location of this file or turn off\n// automatically serving support files with the\n// 'supportFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/configuration\n// ***********************************************************\n\n// Import commands.js using ES2015 syntax:\nimport \"./commands\";\n\n// Alternatively you can use CommonJS syntax:\n// require('./commands')\n"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress.json",
    "content": "{\n  \"nodeVersion\": \"system\"\n}\n"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/package.json",
    "content": "{\n  \"name\": \"1_page_objects\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"cypress:open\": \"NODE_ENV=development cypress open\",\n    \"cypress:run\": \"NODE_ENV=development cypress run\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"cypress\": \"^4.12.1\",\n    \"knex\": \"^0.20.13\",\n    \"sqlite3\": \"4.1.1\"\n  }\n}\n"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/dbConnection.js",
    "content": "const environmentName = process.env.NODE_ENV;\nconst db = require(\"knex\")(require(\"./knexfile\")[environmentName]);\n\nconst closeConnection = () => db.destroy();\n\nmodule.exports = {\n  db,\n  closeConnection\n};\n"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/fixtures/example.json",
    "content": "{\n  \"name\": \"Using fixtures to represent data\",\n  \"email\": \"hello@cypress.io\",\n  \"body\": \"Fixtures are a great way to mock data for responses to routes\"\n}\n"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/integration/itemListUpdates.spec.js",
    "content": "import { InventoryManagement } from \"../pageObjects/inventoryManagement\";\n\ndescribe(\"item list updates\", () => {\n  beforeEach(() => cy.task(\"emptyInventory\"));\n\n  describe(\"when the application loads for the first time\", () => {\n    it(\"loads the initial list of items\", () => {\n      cy.addItem(\"cheesecake\", 2);\n      cy.addItem(\"apple pie\", 5);\n      cy.addItem(\"carrot cake\", 96);\n      cy.visit(\"http://localhost:8080\");\n\n      InventoryManagement.findItemEntry(\"cheesecake\", \"2\");\n      InventoryManagement.findItemEntry(\"apple pie\", \"5\");\n      InventoryManagement.findItemEntry(\"carrot cake\", \"96\");\n    });\n  });\n\n  describe(\"as other users add items\", () => {\n    it(\"updates the item list\", () => {\n      cy.visit(\"http://localhost:8080\");\n      InventoryManagement.findAction({});\n      cy.addItem(\"cheesecake\", 22);\n      InventoryManagement.findItemEntry(\"cheesecake\", \"22\");\n    });\n  });\n});\n"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/integration/itemSubmission.spec.js",
    "content": "import { InventoryManagement } from \"../pageObjects/inventoryManagement\";\n\ndescribe(\"item submission\", () => {\n  beforeEach(() => cy.task(\"emptyInventory\"));\n\n  it(\"can add items through the form\", () => {\n    InventoryManagement.visit();\n    InventoryManagement.addItem(\"cheesecake\", \"10\");\n    InventoryManagement.findItemEntry(\"cheesecake\", \"10\");\n  });\n\n  it(\"can update an item's quantity\", () => {\n    cy.task(\"seedItem\", { itemName: \"cheesecake\", quantity: 5 });\n    InventoryManagement.visit();\n    InventoryManagement.addItem(\"cheesecake\", \"10\");\n    InventoryManagement.findItemEntry(\"cheesecake\", \"15\");\n  });\n\n  it(\"can undo submitted items\", () => {\n    InventoryManagement.visit();\n    cy.wait(1000);\n\n    InventoryManagement.findAction({});\n    cy.window().then(({ handleAddItem }) => handleAddItem(\"cheesecake\", 10));\n    cy.wait(1000);\n\n    InventoryManagement.findItemEntry(\"cheesecake\", \"10\");\n    cy.window().then(({ handleAddItem }) => handleAddItem(\"cheesecake\", 5));\n    cy.wait(1000);\n\n    InventoryManagement.undo();\n    InventoryManagement.findItemEntry(\"cheesecake\", \"10\");\n  });\n\n  it(\"saves each submission to the action log\", () => {\n    InventoryManagement.visit();\n    InventoryManagement.addItem(\"cheesecake\", \"10\");\n    InventoryManagement.addItem(\"cheesecake\", \"5\");\n    InventoryManagement.undo();\n    InventoryManagement.findItemEntry(\"cheesecake\", \"10\");\n\n    InventoryManagement.findAction({});\n    InventoryManagement.findAction({ cheesecake: 10 }).should(\"have.length\", 2);\n    InventoryManagement.findAction({ cheesecake: 15 });\n  });\n\n  describe(\"given a user enters an invalid item name\", () => {\n    it(\"disables the form's submission button\", () => {\n      InventoryManagement.visit();\n      InventoryManagement.enterItemName(\"boat\");\n      InventoryManagement.enterQuantity(10);\n      InventoryManagement.getSubmitButton().should(\"be.disabled\");\n    });\n  });\n});\n"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/knexfile.js",
    "content": "module.exports = {\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"../../server/dev.sqlite\" },\n    useNullAsDefault: true\n  }\n};\n"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/pageObjects/inventoryManagement.js",
    "content": "export class InventoryManagement {\n  static visit() {\n    cy.visit(\"http://localhost:8080\");\n  }\n\n  static enterItemName(itemName) {\n    return cy\n      .get('input[placeholder=\"Item name\"]')\n      .clear()\n      .type(itemName);\n  }\n\n  static enterQuantity(quantity) {\n    return cy\n      .get('input[placeholder=\"Quantity\"]')\n      .clear()\n      .type(quantity);\n  }\n\n  static getSubmitButton() {\n    return cy.get('button[type=\"submit\"]').contains(\"Add to inventory\");\n  }\n\n  static addItem(itemName, quantity) {\n    InventoryManagement.enterItemName(itemName);\n    InventoryManagement.enterQuantity(quantity);\n    InventoryManagement.getSubmitButton().click();\n  }\n\n  static findItemEntry(itemName, quantity) {\n    return cy.contains(\"li\", `${itemName} - Quantity: ${quantity}`);\n  }\n\n  static undo() {\n    return cy\n      .get(\"button\")\n      .contains(\"Undo\")\n      .click();\n  }\n\n  static findAction(inventoryState) {\n    return cy.get(\"p:not(:nth-of-type(1))\").then(p => {\n      return Array.from(p).filter(p => {\n        return p.innerText.includes(\n          `The inventory has been updated - ${JSON.stringify(inventoryState)}`\n        );\n      });\n    });\n  }\n}\n"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/plugins/dbPlugin.js",
    "content": "const { db } = require(\"../dbConnection\");\n\nconst dbPlugin = (on, config) => {\n  on(\n    \"task\",\n    {\n      emptyInventory: () => db(\"inventory\").truncate(),\n      seedItem: itemRow => db(\"inventory\").insert(itemRow)\n    },\n    config\n  );\n\n  return config;\n};\n\nmodule.exports = dbPlugin;\n"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/plugins/index.js",
    "content": "/// <reference types=\"cypress\" />\n// ***********************************************************\n// This example plugins/index.js can be used to load plugins\n//\n// You can change the location of this file or turn off loading\n// the plugins file with the 'pluginsFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/plugins-guide\n// ***********************************************************\n\n// This function is called when a project is opened or re-opened (e.g. due to\n// the project's config changing)\n\nconst dbPlugin = require(\"./dbPlugin\");\n\nmodule.exports = (on, config) => {\n  dbPlugin(on, config);\n};\n"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/support/commands.js",
    "content": "Cypress.Commands.add(\"addItem\", (itemName, quantity) => {\n  return cy.request({\n    url: `http://localhost:3000/inventory/${itemName}`,\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ quantity })\n  });\n});\n"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/support/index.js",
    "content": "// ***********************************************************\n// This example support/index.js is processed and\n// loaded automatically before your test files.\n//\n// This is a great place to put global configuration and\n// behavior that modifies Cypress.\n//\n// You can change the location of this file or turn off\n// automatically serving support files with the\n// 'supportFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/configuration\n// ***********************************************************\n\n// Import commands.js using ES2015 syntax:\nimport \"./commands\";\n\n// Alternatively you can use CommonJS syntax:\n// require('./commands')\n"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress.json",
    "content": "{\n  \"nodeVersion\": \"system\"\n}\n"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/package.json",
    "content": "{\n  \"name\": \"2_application_actions\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"cypress:open\": \"NODE_ENV=development cypress open\",\n    \"cypress:run\": \"NODE_ENV=development cypress run\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"cypress\": \"^4.12.1\",\n    \"knex\": \"^0.20.13\",\n    \"sqlite3\": \"4.1.1\"\n  }\n}\n"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/dbConnection.js",
    "content": "const environmentName = process.env.NODE_ENV;\nconst db = require(\"knex\")(require(\"./knexfile\")[environmentName]);\n\nconst closeConnection = () => db.destroy();\n\nmodule.exports = {\n  db,\n  closeConnection\n};\n"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/fixtures/example.json",
    "content": "{\n  \"name\": \"Using fixtures to represent data\",\n  \"email\": \"hello@cypress.io\",\n  \"body\": \"Fixtures are a great way to mock data for responses to routes\"\n}\n"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/integration/itemListUpdates.spec.js",
    "content": "import { InventoryManagement } from \"../pageObjects/inventoryManagement\";\n\ndescribe(\"item list updates\", () => {\n  beforeEach(() => cy.task(\"emptyInventory\"));\n\n  describe(\"when the application loads for the first time\", () => {\n    it(\"loads the initial list of items\", () => {\n      cy.addItem(\"cheesecake\", 2);\n      cy.addItem(\"apple pie\", 5);\n      cy.addItem(\"carrot cake\", 96);\n      cy.visit(\"http://localhost:8080\");\n\n      InventoryManagement.findItemEntry(\"cheesecake\", \"2\");\n      InventoryManagement.findItemEntry(\"apple pie\", \"5\");\n      InventoryManagement.findItemEntry(\"carrot cake\", \"96\");\n    });\n  });\n\n  describe(\"as other users add items\", () => {\n    it(\"updates the item list\", () => {\n      cy.server()\n        .route(\"http://localhost:3000/inventory\")\n        .as(\"inventoryRequest\");\n      cy.visit(\"http://localhost:8080\");\n      cy.wait(\"@inventoryRequest\");\n      cy.addItem(\"cheesecake\", 22);\n      InventoryManagement.findItemEntry(\"cheesecake\", \"22\");\n    });\n  });\n});\n"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/integration/itemSubmission.spec.js",
    "content": "import { InventoryManagement } from \"../pageObjects/inventoryManagement\";\n\ndescribe(\"item submission\", () => {\n  beforeEach(() => cy.task(\"emptyInventory\"));\n\n  it(\"can add items through the form\", () => {\n    InventoryManagement.visit();\n    InventoryManagement.addItem(\"cheesecake\", \"10\");\n    InventoryManagement.findItemEntry(\"cheesecake\", \"10\");\n  });\n\n  it(\"can update an item's quantity\", () => {\n    cy.task(\"seedItem\", { itemName: \"cheesecake\", quantity: 5 });\n    InventoryManagement.visit();\n    InventoryManagement.addItem(\"cheesecake\", \"10\");\n    InventoryManagement.findItemEntry(\"cheesecake\", \"15\");\n  });\n\n  it(\"can undo submitted items\", () => {\n    InventoryManagement.visit();\n    InventoryManagement.findAction({});\n\n    cy.window().then(({ handleAddItem }) => handleAddItem(\"cheesecake\", 10));\n    InventoryManagement.findAction({ cheesecake: 10 });\n\n    cy.window().then(({ handleAddItem }) => handleAddItem(\"cheesecake\", 5));\n    InventoryManagement.findAction({ cheesecake: 15 });\n\n    InventoryManagement.undo();\n    InventoryManagement.findItemEntry(\"cheesecake\", \"10\");\n  });\n\n  it(\"saves each submission to the action log\", () => {\n    InventoryManagement.visit();\n    InventoryManagement.addItem(\"cheesecake\", \"10\");\n    InventoryManagement.addItem(\"cheesecake\", \"5\");\n    InventoryManagement.undo();\n    InventoryManagement.findItemEntry(\"cheesecake\", \"10\");\n\n    InventoryManagement.findAction({}).should(\"have.length\", 1);\n    InventoryManagement.findAction({ cheesecake: 10 }).should(\"have.length\", 2);\n    InventoryManagement.findAction({ cheesecake: 15 }).should(\"have.length\", 1);\n  });\n\n  describe(\"given a user enters an invalid item name\", () => {\n    it(\"disables the form's submission button\", () => {\n      InventoryManagement.visit();\n      InventoryManagement.enterItemName(\"boat\");\n      InventoryManagement.enterQuantity(10);\n      InventoryManagement.getSubmitButton().should(\"be.disabled\");\n    });\n  });\n});\n"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/knexfile.js",
    "content": "module.exports = {\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"../../server/dev.sqlite\" },\n    useNullAsDefault: true\n  }\n};\n"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/pageObjects/inventoryManagement.js",
    "content": "export class InventoryManagement {\n  static visit() {\n    cy.visit(\"http://localhost:8080\");\n  }\n\n  static enterItemName(itemName) {\n    return cy\n      .get('input[placeholder=\"Item name\"]')\n      .clear()\n      .type(itemName);\n  }\n\n  static enterQuantity(quantity) {\n    return cy\n      .get('input[placeholder=\"Quantity\"]')\n      .clear()\n      .type(quantity);\n  }\n\n  static getSubmitButton() {\n    return cy.get('button[type=\"submit\"]').contains(\"Add to inventory\");\n  }\n\n  static addItem(itemName, quantity) {\n    InventoryManagement.enterItemName(itemName);\n    InventoryManagement.enterQuantity(quantity);\n    InventoryManagement.getSubmitButton().click();\n  }\n\n  static findItemEntry(itemName, quantity) {\n    return cy.contains(\"li\", `${itemName} - Quantity: ${quantity}`);\n  }\n\n  static undo() {\n    return cy\n      .get(\"button\")\n      .contains(\"Undo\")\n      .click();\n  }\n\n  static findAction(inventoryState) {\n    return cy.get(\"p:not(:nth-of-type(1))\").then(p => {\n      return Array.from(p).filter(p => {\n        return p.innerText.includes(\n          `The inventory has been updated - ${JSON.stringify(inventoryState)}`\n        );\n      });\n    });\n  }\n}\n"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/plugins/dbPlugin.js",
    "content": "const { db } = require(\"../dbConnection\");\n\nconst dbPlugin = (on, config) => {\n  on(\n    \"task\",\n    {\n      emptyInventory: () => db(\"inventory\").truncate(),\n      seedItem: itemRow => db(\"inventory\").insert(itemRow)\n    },\n    config\n  );\n\n  return config;\n};\n\nmodule.exports = dbPlugin;\n"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/plugins/index.js",
    "content": "/// <reference types=\"cypress\" />\n// ***********************************************************\n// This example plugins/index.js can be used to load plugins\n//\n// You can change the location of this file or turn off loading\n// the plugins file with the 'pluginsFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/plugins-guide\n// ***********************************************************\n\n// This function is called when a project is opened or re-opened (e.g. due to\n// the project's config changing)\n\nconst dbPlugin = require(\"./dbPlugin\");\n\nmodule.exports = (on, config) => {\n  dbPlugin(on, config);\n};\n"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/support/commands.js",
    "content": "import \"cypress-wait-until\";\n\nCypress.Commands.add(\"addItem\", (itemName, quantity) => {\n  return cy.request({\n    url: `http://localhost:3000/inventory/${itemName}`,\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ quantity })\n  });\n});\n"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/support/index.js",
    "content": "// ***********************************************************\n// This example support/index.js is processed and\n// loaded automatically before your test files.\n//\n// This is a great place to put global configuration and\n// behavior that modifies Cypress.\n//\n// You can change the location of this file or turn off\n// automatically serving support files with the\n// 'supportFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/configuration\n// ***********************************************************\n\n// Import commands.js using ES2015 syntax:\nimport \"./commands\";\n\n// Alternatively you can use CommonJS syntax:\n// require('./commands')\n"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress.json",
    "content": "{\n  \"nodeVersion\": \"system\",\n  \"experimentalFetchPolyfill\": true\n}\n"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/package.json",
    "content": "{\n  \"name\": \"1_avoiding_waiting_for_fixed_amounts_of_time\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"cypress:open\": \"NODE_ENV=development cypress open\",\n    \"cypress:run\": \"NODE_ENV=development cypress run\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"cypress\": \"^4.12.1\",\n    \"cypress-wait-until\": \"^1.7.1\",\n    \"knex\": \"^0.20.13\",\n    \"sqlite3\": \"4.1.1\"\n  }\n}\n"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/dbConnection.js",
    "content": "const environmentName = process.env.NODE_ENV;\nconst db = require(\"knex\")(require(\"./knexfile\")[environmentName]);\n\nconst closeConnection = () => db.destroy();\n\nmodule.exports = {\n  db,\n  closeConnection\n};\n"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/fixtures/example.json",
    "content": "{\n  \"name\": \"Using fixtures to represent data\",\n  \"email\": \"hello@cypress.io\",\n  \"body\": \"Fixtures are a great way to mock data for responses to routes\"\n}\n"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/integration/itemListUpdates.spec.js",
    "content": "import { InventoryManagement } from \"../pageObjects/inventoryManagement\";\n\ndescribe(\"item list updates\", () => {\n  beforeEach(() => cy.task(\"emptyInventory\"));\n\n  describe(\"when the application loads for the first time\", () => {\n    it(\"loads the initial list of items\", () => {\n      cy.addItem(\"cheesecake\", 2);\n      cy.addItem(\"apple pie\", 5);\n      cy.addItem(\"carrot cake\", 96);\n      cy.visit(\"http://localhost:8080\");\n\n      InventoryManagement.findItemEntry(\"cheesecake\", \"2\");\n      InventoryManagement.findItemEntry(\"apple pie\", \"5\");\n      InventoryManagement.findItemEntry(\"carrot cake\", \"96\");\n    });\n  });\n\n  describe(\"as other users add items\", () => {\n    it(\"updates the item list\", () => {\n      cy.server()\n        .route(\"http://localhost:3000/inventory\")\n        .as(\"inventoryRequest\");\n      cy.visit(\"http://localhost:8080\");\n      cy.wait(\"@inventoryRequest\");\n      cy.addItem(\"cheesecake\", 22);\n      InventoryManagement.findItemEntry(\"cheesecake\", \"22\");\n    });\n  });\n});\n"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/integration/itemSubmission.spec.js",
    "content": "import { InventoryManagement } from \"../pageObjects/inventoryManagement\";\n\ndescribe(\"item submission\", () => {\n  beforeEach(() => cy.task(\"emptyInventory\"));\n  beforeEach(() => {\n    cy.server();\n    cy.route(\"GET\", \"/inventory/cheesecake\", {\n      recipes: [\n        { href: \"http://example.com/always-the-same-url/first-recipe\" },\n        { href: \"http://example.com/always-the-same-url/second-recipe\" },\n        { href: \"http://example.com/always-the-same-url/third-recipe\" }\n      ]\n    });\n  });\n\n  it(\"can add items through the form\", () => {\n    InventoryManagement.visit();\n    cy.window().then(w => cy.stub(w.Math, \"random\").returns(0.5));\n    InventoryManagement.addItem(\"cheesecake\", \"10\");\n    InventoryManagement.findItemEntry(\"cheesecake\", \"10\")\n      .get(\"a\")\n      .should(\n        \"have.attr\",\n        \"href\",\n        \"http://example.com/always-the-same-url/second-recipe\"\n      );\n  });\n\n  it(\"can update an item's quantity\", () => {\n    cy.task(\"seedItem\", { itemName: \"cheesecake\", quantity: 5 });\n    InventoryManagement.visit();\n    InventoryManagement.addItem(\"cheesecake\", \"10\");\n    InventoryManagement.findItemEntry(\"cheesecake\", \"15\");\n  });\n\n  it(\"can undo submitted items\", () => {\n    InventoryManagement.visit();\n    InventoryManagement.findAction({});\n\n    cy.window().then(({ handleAddItem }) => handleAddItem(\"cheesecake\", 10));\n    InventoryManagement.findAction({ cheesecake: 10 });\n\n    cy.window().then(({ handleAddItem }) => handleAddItem(\"cheesecake\", 5));\n    InventoryManagement.findAction({ cheesecake: 15 });\n\n    InventoryManagement.undo();\n    InventoryManagement.findItemEntry(\"cheesecake\", \"10\");\n  });\n\n  it(\"saves each submission to the action log\", () => {\n    InventoryManagement.visit();\n    InventoryManagement.findAction({});\n    cy.clock().tick(1000);\n\n    InventoryManagement.addItem(\"cheesecake\", \"10\");\n    InventoryManagement.findAction({ cheesecake: 10 });\n    cy.clock().tick(1000);\n\n    InventoryManagement.addItem(\"cheesecake\", \"5\");\n    InventoryManagement.findAction({ cheesecake: 15 });\n    cy.clock().tick(1000);\n\n    InventoryManagement.undo();\n\n    InventoryManagement.findItemEntry(\"cheesecake\", \"10\");\n    InventoryManagement.findAction({ cheesecake: 10 });\n  });\n\n  describe(\"given a user enters an invalid item name\", () => {\n    it(\"disables the form's submission button\", () => {\n      InventoryManagement.visit();\n      InventoryManagement.enterItemName(\"boat\");\n      InventoryManagement.enterQuantity(10);\n      InventoryManagement.getSubmitButton().should(\"be.disabled\");\n    });\n  });\n});\n"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/knexfile.js",
    "content": "module.exports = {\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"../../server/dev.sqlite\" },\n    useNullAsDefault: true\n  }\n};\n"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/pageObjects/inventoryManagement.js",
    "content": "export class InventoryManagement {\n  static visit() {\n    cy.visit(\"http://localhost:8080\");\n  }\n\n  static enterItemName(itemName) {\n    return cy\n      .get('input[placeholder=\"Item name\"]')\n      .clear()\n      .type(itemName);\n  }\n\n  static enterQuantity(quantity) {\n    return cy\n      .get('input[placeholder=\"Quantity\"]')\n      .clear()\n      .type(quantity);\n  }\n\n  static getSubmitButton() {\n    return cy.get('button[type=\"submit\"]').contains(\"Add to inventory\");\n  }\n\n  static addItem(itemName, quantity) {\n    InventoryManagement.enterItemName(itemName);\n    InventoryManagement.enterQuantity(quantity);\n    InventoryManagement.getSubmitButton().click();\n  }\n\n  static findItemEntry(itemName, quantity) {\n    return cy.contains(\"li\", `${itemName} - Quantity: ${quantity}`);\n  }\n\n  static undo() {\n    return cy\n      .get(\"button\")\n      .contains(\"Undo\")\n      .click();\n  }\n\n  static findAction(inventoryState) {\n    return cy.clock(c => {\n      const dateText = new Date(c.details().now).toISOString();\n      return cy\n        .get(\"p:not(:nth-of-type(1))\")\n        .contains(\n          `[${dateText}]` +\n            \" The inventory has been updated - \" +\n            JSON.stringify(inventoryState)\n        );\n    });\n  }\n}\n"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/plugins/dbPlugin.js",
    "content": "const { db } = require(\"../dbConnection\");\n\nconst dbPlugin = (on, config) => {\n  on(\n    \"task\",\n    {\n      emptyInventory: () => db(\"inventory\").truncate(),\n      seedItem: itemRow => db(\"inventory\").insert(itemRow)\n    },\n    config\n  );\n\n  return config;\n};\n\nmodule.exports = dbPlugin;\n"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/plugins/index.js",
    "content": "/// <reference types=\"cypress\" />\n// ***********************************************************\n// This example plugins/index.js can be used to load plugins\n//\n// You can change the location of this file or turn off loading\n// the plugins file with the 'pluginsFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/plugins-guide\n// ***********************************************************\n\n// This function is called when a project is opened or re-opened (e.g. due to\n// the project's config changing)\n\nconst dbPlugin = require(\"./dbPlugin\");\n\nmodule.exports = (on, config) => {\n  dbPlugin(on, config);\n};\n"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/support/commands.js",
    "content": "import \"cypress-wait-until\";\n\nCypress.Commands.add(\"addItem\", (itemName, quantity) => {\n  return cy.request({\n    url: `http://localhost:3000/inventory/${itemName}`,\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ quantity })\n  });\n});\n"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/support/index.js",
    "content": "// ***********************************************************\n// This example support/index.js is processed and\n// loaded automatically before your test files.\n//\n// This is a great place to put global configuration and\n// behavior that modifies Cypress.\n//\n// You can change the location of this file or turn off\n// automatically serving support files with the\n// 'supportFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/configuration\n// ***********************************************************\n\n// Import commands.js using ES2015 syntax:\nimport \"./commands\";\n\n// Alternatively you can use CommonJS syntax:\n// require('./commands')\n\nbeforeEach(() => cy.clock(Date.now()).as(\"fakeTimer\"));\n"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress.json",
    "content": "{\n  \"nodeVersion\": \"system\",\n  \"experimentalFetchPolyfill\": true\n}\n"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/package.json",
    "content": "{\n  \"name\": \"2_stubbing_uncontrollable_factors\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"cypress:open\": \"NODE_ENV=development cypress open\",\n    \"cypress:run\": \"NODE_ENV=development cypress run\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"cypress\": \"^4.12.1\",\n    \"cypress-wait-until\": \"^1.7.1\",\n    \"knex\": \"^0.20.13\",\n    \"sqlite3\": \"4.1.1\"\n  }\n}\n"
  },
  {
    "path": "chapter11/4_visual_regression_tests/cypress/dbConnection.js",
    "content": "const environmentName = process.env.NODE_ENV;\nconst db = require(\"knex\")(require(\"./knexfile\")[environmentName]);\n\nconst closeConnection = () => db.destroy();\n\nmodule.exports = {\n  db,\n  closeConnection\n};\n"
  },
  {
    "path": "chapter11/4_visual_regression_tests/cypress/fixtures/example.json",
    "content": "{\n  \"name\": \"Using fixtures to represent data\",\n  \"email\": \"hello@cypress.io\",\n  \"body\": \"Fixtures are a great way to mock data for responses to routes\"\n}\n"
  },
  {
    "path": "chapter11/4_visual_regression_tests/cypress/integration/itemList.spec.js",
    "content": "import { InventoryManagement } from \"../pageObjects/inventoryManagement\";\n\nconst now = new Date(1996, 5, 2).getTime();\n\ndescribe(\"item list\", () => {\n  beforeEach(() => cy.task(\"emptyInventory\"));\n\n  it(\"can update an item's quantity\", () => {\n    cy.task(\"seedItem\", { itemName: \"cheesecake\", quantity: 1 });\n    InventoryManagement.visit();\n    InventoryManagement.findItemEntry(\"cheesecake\", \"1\");\n    cy.percySnapshot();\n  });\n});\n"
  },
  {
    "path": "chapter11/4_visual_regression_tests/cypress/integration/itemListUpdates.spec.js",
    "content": "import { InventoryManagement } from \"../pageObjects/inventoryManagement\";\n\ndescribe(\"item list updates\", () => {\n  beforeEach(() => cy.task(\"emptyInventory\"));\n\n  describe(\"when the application loads for the first time\", () => {\n    it.only(\"loads the initial list of items\", () => {\n      cy.addItem(\"cheesecake\", 2);\n      cy.addItem(\"apple pie\", 5);\n      cy.addItem(\"carrot cake\", 96);\n      cy.visit(\"http://localhost:8080\");\n      cy.wait(1);\n\n      InventoryManagement.findItemEntry(\"cheesecake\", \"2\");\n      InventoryManagement.findItemEntry(\"apple pie\", \"5\");\n      InventoryManagement.findItemEntry(\"carrot cake\", \"96\");\n    });\n  });\n\n  describe(\"as other users add items\", () => {\n    it(\"updates the item list\", () => {\n      cy.server()\n        .route(\"http://localhost:3000/inventory\")\n        .as(\"inventoryRequest\");\n      cy.visit(\"http://localhost:8080\");\n      cy.wait(\"@inventoryRequest\");\n      cy.addItem(\"cheesecake\", 22);\n      InventoryManagement.findItemEntry(\"cheesecake\", \"22\");\n    });\n  });\n});\n"
  },
  {
    "path": "chapter11/4_visual_regression_tests/cypress/integration/itemSubmission.spec.js",
    "content": "import { InventoryManagement } from \"../pageObjects/inventoryManagement\";\n\ndescribe(\"item submission\", () => {\n  beforeEach(() => cy.task(\"emptyInventory\"));\n  beforeEach(() => {\n    cy.server();\n    cy.route(\"GET\", \"/inventory/cheesecake\", {\n      recipes: [\n        { href: \"http://example.com/always-the-same-url/first-recipe\" },\n        { href: \"http://example.com/always-the-same-url/second-recipe\" },\n        { href: \"http://example.com/always-the-same-url/third-recipe\" }\n      ]\n    });\n  });\n\n  it(\"can add items through the form\", () => {\n    InventoryManagement.visit();\n    cy.window().then(w => cy.stub(w.Math, \"random\").returns(0.5));\n    InventoryManagement.addItem(\"cheesecake\", \"10\");\n    InventoryManagement.findItemEntry(\"cheesecake\", \"10\")\n      .get(\"a\")\n      .should(\n        \"have.attr\",\n        \"href\",\n        \"http://example.com/always-the-same-url/second-recipe\"\n      );\n  });\n\n  it(\"can update an item's quantity\", () => {\n    cy.task(\"seedItem\", { itemName: \"cheesecake\", quantity: 5 });\n    InventoryManagement.visit();\n    InventoryManagement.addItem(\"cheesecake\", \"10\");\n    InventoryManagement.findItemEntry(\"cheesecake\", \"15\");\n  });\n\n  it(\"can undo submitted items\", () => {\n    InventoryManagement.visit();\n    InventoryManagement.findAction({});\n\n    cy.window().then(({ handleAddItem }) => handleAddItem(\"cheesecake\", 10));\n    InventoryManagement.findAction({ cheesecake: 10 });\n\n    cy.window().then(({ handleAddItem }) => handleAddItem(\"cheesecake\", 5));\n    InventoryManagement.findAction({ cheesecake: 15 });\n\n    InventoryManagement.undo();\n    InventoryManagement.findItemEntry(\"cheesecake\", \"10\");\n  });\n\n  it.only(\"saves each submission to the action log\", () => {\n    cy.clock();\n    InventoryManagement.visit();\n    InventoryManagement.findAction({});\n    cy.clock().tick(2000);\n\n    InventoryManagement.addItem(\"cheesecake\", \"10\");\n    InventoryManagement.findAction({ cheesecake: 10 });\n    cy.clock().tick(2000);\n\n    InventoryManagement.addItem(\"cheesecake\", \"5\");\n    InventoryManagement.findAction({ cheesecake: 15 });\n    cy.clock().tick(2000);\n\n    InventoryManagement.undo();\n\n    InventoryManagement.findItemEntry(\"cheesecake\", \"10\");\n    InventoryManagement.findAction({ cheesecake: 10 });\n  });\n\n  describe(\"given a user enters an invalid item name\", () => {\n    it(\"disables the form's submission button\", () => {\n      InventoryManagement.visit();\n      InventoryManagement.enterItemName(\"boat\");\n      InventoryManagement.enterQuantity(10);\n      InventoryManagement.getSubmitButton().should(\"be.disabled\");\n    });\n  });\n});\n"
  },
  {
    "path": "chapter11/4_visual_regression_tests/cypress/knexfile.js",
    "content": "module.exports = {\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"../server/dev.sqlite\" },\n    useNullAsDefault: true\n  }\n};\n"
  },
  {
    "path": "chapter11/4_visual_regression_tests/cypress/pageObjects/inventoryManagement.js",
    "content": "export class InventoryManagement {\n  static visit() {\n    cy.visit(\"http://localhost:8080\");\n  }\n\n  static enterItemName(itemName) {\n    return cy\n      .get('input[placeholder=\"Item name\"]')\n      .clear()\n      .type(itemName);\n  }\n\n  static enterQuantity(quantity) {\n    return cy\n      .get('input[placeholder=\"Quantity\"]')\n      .clear()\n      .type(quantity);\n  }\n\n  static getSubmitButton() {\n    return cy.get('button[type=\"submit\"]').contains(\"Add to inventory\");\n  }\n\n  static addItem(itemName, quantity) {\n    InventoryManagement.enterItemName(itemName);\n    InventoryManagement.enterQuantity(quantity);\n    InventoryManagement.getSubmitButton().click();\n  }\n\n  static findItemEntry(itemName, quantity) {\n    return cy.contains(\"li\", `${itemName} - Quantity: ${quantity}`);\n  }\n\n  static undo() {\n    return cy\n      .get(\"button\")\n      .contains(\"Undo\")\n      .click();\n  }\n\n  static findAction(inventoryState) {\n    return cy.clock(c => {\n      const dateText = new Date(c.details().now).toISOString();\n      return cy\n        .get(\"p:not(:nth-of-type(1))\")\n        .contains(\n          `[${dateText}]` +\n            \" The inventory has been updated - \" +\n            JSON.stringify(inventoryState)\n        );\n    });\n  }\n}\n"
  },
  {
    "path": "chapter11/4_visual_regression_tests/cypress/plugins/dbPlugin.js",
    "content": "const { db } = require(\"../dbConnection\");\n\nconst dbPlugin = (on, config) => {\n  on(\n    \"task\",\n    {\n      emptyInventory: () => db(\"inventory\").truncate(),\n      seedItem: itemRow => db(\"inventory\").insert(itemRow)\n    },\n    config\n  );\n\n  return config;\n};\n\nmodule.exports = dbPlugin;\n"
  },
  {
    "path": "chapter11/4_visual_regression_tests/cypress/plugins/index.js",
    "content": "/// <reference types=\"cypress\" />\n// ***********************************************************\n// This example plugins/index.js can be used to load plugins\n//\n// You can change the location of this file or turn off loading\n// the plugins file with the 'pluginsFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/plugins-guide\n// ***********************************************************\n\n// This function is called when a project is opened or re-opened (e.g. due to\n// the project's config changing)\n\nconst percyHealthCheck = require(\"@percy/cypress/task\");\nconst dbPlugin = require(\"./dbPlugin\");\n\nmodule.exports = (on, config) => {\n  dbPlugin(on, config);\n  on(\"task\", percyHealthCheck);\n};\n"
  },
  {
    "path": "chapter11/4_visual_regression_tests/cypress/support/commands.js",
    "content": "import \"@percy/cypress\";\nimport \"cypress-wait-until\";\n\nCypress.Commands.add(\"addItem\", (itemName, quantity) => {\n  return cy.request({\n    url: `http://localhost:3000/inventory/${itemName}`,\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ quantity })\n  });\n});\n"
  },
  {
    "path": "chapter11/4_visual_regression_tests/cypress/support/index.js",
    "content": "// ***********************************************************\n// This example support/index.js is processed and\n// loaded automatically before your test files.\n//\n// This is a great place to put global configuration and\n// behavior that modifies Cypress.\n//\n// You can change the location of this file or turn off\n// automatically serving support files with the\n// 'supportFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/configuration\n// ***********************************************************\n\n// Import commands.js using ES2015 syntax:\nimport \"./commands\";\n\n// Alternatively you can use CommonJS syntax:\n// require('./commands')\n\nbeforeEach(() => cy.clock(Date.now()).as(\"fakeTimer\"));\n"
  },
  {
    "path": "chapter11/4_visual_regression_tests/cypress.json",
    "content": "{\n  \"nodeVersion\": \"system\",\n  \"experimentalFetchPolyfill\": true\n}\n"
  },
  {
    "path": "chapter11/4_visual_regression_tests/package.json",
    "content": "{\n  \"name\": \"4_visual_regression_tests\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"cypress:open\": \"NODE_ENV=development cypress open\",\n    \"cypress:run\": \"NODE_ENV=development percy exec -- cypress run\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"@percy/cypress\": \"^2.3.1\",\n    \"cypress\": \"^4.12.1\",\n    \"cypress-wait-until\": \"^1.7.1\",\n    \"knex\": \"^0.20.13\",\n    \"sqlite3\": \"4.1.1\"\n  }\n}\n"
  },
  {
    "path": "chapter11/client/domController.js",
    "content": "const { API_ADDR, addItem, data } = require(\"./inventoryController\");\n\nconst updateItemList = inventory => {\n  if (inventory === null) return;\n\n  localStorage.setItem(\"inventory\", JSON.stringify(inventory));\n\n  const inventoryList = window.document.getElementById(\"item-list\");\n\n  // Clears the list\n  inventoryList.innerHTML = \"\";\n\n  Object.entries(inventory).forEach(async ([itemName, quantity]) => {\n    const listItem = window.document.createElement(\"li\");\n    const listLink = window.document.createElement(\"a\");\n    listItem.appendChild(listLink);\n\n    const recipeResponse = await fetch(`${API_ADDR}/inventory/${itemName}`);\n    const recipeList = (await recipeResponse.json()).recipes;\n    const randomRecipe = Math.floor(Math.random() * recipeList.length - 1) + 1;\n    listLink.innerHTML = `${itemName} - Quantity: ${quantity}`;\n    listLink.href = recipeList[randomRecipe]\n      ? recipeList[randomRecipe].href\n      : \"#\";\n\n    if (quantity < 5) {\n      listItem.className = \"almost-soldout\";\n    }\n\n    inventoryList.appendChild(listItem);\n  });\n\n  const inventoryContents = JSON.stringify(inventory);\n  const p = window.document.createElement(\"p\");\n  p.innerHTML = `[${new Date().toISOString()}] The inventory has been updated - ${inventoryContents}`;\n\n  window.document.body.appendChild(p);\n};\n\nconst handleAddItem = event => {\n  // Prevent the page from reloading as it would by default\n  event.preventDefault();\n\n  const { name, quantity } = event.target.elements;\n  addItem(name.value, parseInt(quantity.value, 10));\n\n  history.pushState({ inventory: { ...data.inventory } }, document.title);\n\n  updateItemList(data.inventory);\n};\n\nif (window.Cypress) {\n  window.handleAddItem = (name, quantity) => {\n    const e = {\n      preventDefault: () => {},\n      target: {\n        elements: {\n          name: { value: name },\n          quantity: { value: quantity }\n        }\n      }\n    };\n\n    return handleAddItem(e);\n  };\n}\n\nconst validItems = [\"cheesecake\", \"apple pie\", \"carrot cake\"];\nconst checkFormValues = () => {\n  const itemName = document.querySelector(`input[name=\"name\"]`).value;\n  const quantity = document.querySelector(`input[name=\"quantity\"]`).value;\n\n  const itemNameIsEmpty = itemName === \"\";\n  const itemNameIsInvalid = !validItems.includes(itemName);\n  const quantityIsEmpty = quantity === \"\";\n\n  const errorMsg = window.document.getElementById(\"error-msg\");\n  if (itemNameIsEmpty) {\n    errorMsg.innerHTML = \"\";\n  } else if (itemNameIsInvalid) {\n    errorMsg.innerHTML = `${itemName} is not a valid item.`;\n  } else {\n    errorMsg.innerHTML = `${itemName} is valid!`;\n  }\n\n  const submitButton = document.querySelector(`button[type=\"submit\"]`);\n  if (itemNameIsEmpty || itemNameIsInvalid || quantityIsEmpty) {\n    submitButton.disabled = true;\n  } else {\n    submitButton.disabled = false;\n  }\n};\n\nconst handleUndo = () => {\n  if (history.state === null) return;\n  history.back();\n};\n\nconst handlePopstate = () => {\n  data.inventory = history.state ? history.state.inventory : {};\n  updateItemList(data.inventory);\n};\n\nmodule.exports = {\n  updateItemList,\n  handleAddItem,\n  checkFormValues,\n  handleUndo,\n  handlePopstate\n};\n"
  },
  {
    "path": "chapter11/client/domController.test.js",
    "content": "const nock = require(\"nock\");\nconst fs = require(\"fs\");\nconst initialHtml = fs.readFileSync(\"./index.html\");\nconst { getByText, screen } = require(\"@testing-library/dom\");\n\nconst {\n  updateItemList,\n  handleAddItem,\n  checkFormValues,\n  handleUndo,\n  handlePopstate\n} = require(\"./domController\");\n\nconst { clearHistoryHook, detachPopstateHandlers } = require(\"./testUtils\");\n\nconst { API_ADDR, data } = require(\"./inventoryController\");\n\nbeforeEach(() => {\n  document.body.innerHTML = initialHtml;\n});\n\ndescribe(\"updateItemList\", () => {\n  beforeEach(() => localStorage.clear());\n\n  test(\"updates the DOM with the inventory items\", () => {\n    const inventory = {\n      cheesecake: 5,\n      \"apple pie\": 2,\n      \"carrot cake\": 6\n    };\n    updateItemList(inventory);\n\n    const itemList = document.getElementById(\"item-list\");\n    expect(itemList.childNodes).toHaveLength(3);\n\n    expect(getByText(itemList, \"cheesecake - Quantity: 5\")).toBeInTheDocument();\n    expect(getByText(itemList, \"apple pie - Quantity: 2\")).toBeInTheDocument();\n    expect(\n      getByText(itemList, \"carrot cake - Quantity: 6\")\n    ).toBeInTheDocument();\n  });\n\n  test(\"highlighting in red elements whose quantity is below five\", () => {\n    const inventory = { cheesecake: 5, \"apple pie\": 2, \"carrot cake\": 6 };\n    updateItemList(inventory);\n\n    expect(screen.getByText(\"apple pie - Quantity: 2\")).toHaveStyle({\n      color: \"red\"\n    });\n  });\n\n  test(\"adding a paragraph indicating what was the update\", () => {\n    const inventory = { cheesecake: 5, \"apple pie\": 2 };\n    updateItemList(inventory);\n\n    expect(\n      screen.getByText(\n        `The inventory has been updated - ${JSON.stringify(inventory)}`\n      )\n    ).toBeTruthy();\n  });\n\n  test(\"updates the localStorage with the inventory\", () => {\n    const inventory = { cheesecake: 5, \"apple pie\": 2 };\n    updateItemList(inventory);\n\n    expect(localStorage.getItem(\"inventory\")).toEqual(\n      JSON.stringify(inventory)\n    );\n  });\n\n  test(\"does not update the inventory when passing null\", () => {\n    localStorage.setItem(\"inventory\", JSON.stringify({ cheesecake: 5 }));\n    updateItemList(null);\n\n    expect(localStorage.getItem(\"inventory\")).toEqual(\n      JSON.stringify({ cheesecake: 5 })\n    );\n  });\n});\n\ndescribe(\"handleAddItem\", () => {\n  beforeEach(() => (data.inventory = {}));\n\n  test(\"adding items to the page\", () => {\n    nock(API_ADDR)\n      .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 6 }))\n      .reply(200);\n\n    const event = {\n      preventDefault: jest.fn(),\n      target: {\n        elements: {\n          name: { value: \"cheesecake\" },\n          quantity: { value: \"6\" }\n        }\n      }\n    };\n\n    handleAddItem(event);\n\n    // Checking if the form's default reload is prevent\n    expect(event.preventDefault.mock.calls).toHaveLength(1);\n\n    const itemList = document.getElementById(\"item-list\");\n    expect(getByText(itemList, \"cheesecake - Quantity: 6\")).toBeInTheDocument();\n\n    if (!nock.isDone())\n      throw new Error(\"POST /inventory/cheesecake was not reached\");\n  });\n\n  test(\"updating the application's history\", () => {\n    nock(API_ADDR)\n      .post(/inventory\\/.*$/)\n      .reply(200);\n\n    const event = {\n      preventDefault: jest.fn(),\n      target: {\n        elements: {\n          name: { value: \"cheesecake\" },\n          quantity: { value: \"6\" }\n        }\n      }\n    };\n\n    handleAddItem(event);\n\n    expect(history.state).toEqual({ inventory: { cheesecake: 6 } });\n  });\n});\n\ndescribe(\"checkFormValues\", () => {\n  test(\"entering valid item values\", () => {\n    document.querySelector(`input[name=\"name\"]`).value = \"cheesecake\";\n    document.querySelector(`input[name=\"quantity\"]`).value = \"1\";\n    checkFormValues();\n    expect(screen.getByText(\"Add to inventory\")).toBeEnabled();\n  });\n\n  test(\"entering invalid item names\", () => {\n    document.querySelector(`input[name=\"name\"]`).value = \"invalid\";\n    document.querySelector(`input[name=\"quantity\"]`).value = \"1\";\n    checkFormValues();\n    expect(screen.getByText(\"Add to inventory\")).toBeDisabled();\n\n    document.querySelector(`input[name=\"name\"]`).value = \"cheesecake\";\n    document.querySelector(`input[name=\"quantity\"]`).value = \"\";\n    checkFormValues();\n    expect(screen.getByText(\"Add to inventory\")).toBeDisabled();\n  });\n});\n\ndescribe(\"tests with history\", () => {\n  beforeEach(() => jest.spyOn(window, \"addEventListener\"));\n\n  afterEach(detachPopstateHandlers);\n\n  beforeEach(clearHistoryHook);\n\n  describe(\"handleUndo\", () => {\n    test(\"going back from a non-initial state\", done => {\n      window.addEventListener(\"popstate\", () => {\n        expect(history.state).toEqual(null);\n        done();\n      });\n\n      history.pushState({ inventory: { cheesecake: 5 } }, \"title\");\n      handleUndo();\n    });\n\n    test(\"going back from an initial state\", () => {\n      jest.spyOn(history, \"back\");\n      handleUndo();\n\n      // This assertion doesn't care about whether\n      // a call to `history.back` would have finished,\n      // it only checks whether it's been called\n      expect(history.back.mock.calls).toHaveLength(0);\n    });\n  });\n\n  describe(\"handlePopstate\", () => {\n    test(\"updating the item list with the current state\", () => {\n      history.pushState(\n        { inventory: { cheesecake: 5, \"carrot cake\": 2 } },\n        \"title\"\n      );\n\n      handlePopstate();\n\n      const itemList = document.getElementById(\"item-list\");\n      expect(itemList.childNodes).toHaveLength(2);\n      expect(\n        getByText(itemList, \"cheesecake - Quantity: 5\")\n      ).toBeInTheDocument();\n      expect(\n        getByText(itemList, \"carrot cake - Quantity: 2\")\n      ).toBeInTheDocument();\n    });\n  });\n});\n"
  },
  {
    "path": "chapter11/client/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Inventory Manager</title>\n    <style>\n      .almost-soldout {\n        color: red;\n      }\n\n      a {\n        color: inherit;\n      }\n\n      @media only percy {\n        p:not(:first-child) {\n          visibility: hidden;\n        }\n      }\n    </style>\n  </head>\n  <body>\n    <h1 data-testid=\"page-header\">Inventory Contents</h1>\n    <ul id=\"item-list\"></ul>\n    <p id=\"error-msg\"></p>\n    <form id=\"add-item-form\">\n      <input type=\"text\" name=\"name\" placeholder=\"Item name\" />\n      <input type=\"number\" name=\"quantity\" placeholder=\"Quantity\" />\n      <button type=\"submit\">Add to inventory</button>\n    </form>\n\n    <button id=\"undo-button\">Undo</button>\n\n    <script src=\"bundle.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter11/client/inventoryController.js",
    "content": "const data = { inventory: {} };\n\nconst API_ADDR = \"http://localhost:3000\";\n\nconst addItem = (itemName, quantity) => {\n  const { client } = require(\"./socket\");\n  const currentQuantity = data.inventory[itemName] || 0;\n  data.inventory[itemName] = currentQuantity + quantity;\n\n  fetch(`${API_ADDR}/inventory/${itemName}`, {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n      \"x-socket-client-id\": client.id\n    },\n    body: JSON.stringify({ quantity })\n  });\n\n  return data.inventory;\n};\n\nmodule.exports = { API_ADDR, data, addItem };\n"
  },
  {
    "path": "chapter11/client/inventoryController.test.js",
    "content": "const nock = require(\"nock\");\nconst { API_ADDR, addItem, data } = require(\"./inventoryController\");\nconst { start, stop } = require(\"./testSocketServer\");\nconst { client, connect } = require(\"./socket\");\n\nafterEach(() => {\n  if (!nock.isDone()) {\n    nock.cleanAll();\n    throw new Error(\"Not all mocked endpoints received requests.\");\n  }\n});\n\ndescribe(\"addItem\", () => {\n  beforeEach(() => (data.inventory = {}));\n\n  test(\"adding new items to the inventory\", () => {\n    // Respond to all post requests\n    // to POST /inventory/:itemName\n    nock(API_ADDR)\n      .post(/inventory\\/.*$/)\n      .reply(200);\n\n    addItem(\"cheesecake\", 5);\n    expect(data.inventory.cheesecake).toBe(5);\n  });\n\n  test(\"sending requests when adding new items\", () => {\n    nock(API_ADDR)\n      .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 5 }))\n      .reply(200);\n\n    addItem(\"cheesecake\", 5);\n  });\n\n  describe(\"live-updates\", () => {\n    beforeAll(start);\n\n    beforeAll(async () => {\n      nock.cleanAll();\n      await connect();\n    });\n\n    afterAll(stop);\n\n    test(\"sending a x-socket-client-id header\", () => {\n      const clientId = client.id;\n\n      nock(API_ADDR, { reqheaders: { \"x-socket-client-id\": clientId } })\n        .post(/inventory\\/.*$/)\n        .reply(200);\n\n      addItem(\"cheesecake\", 5);\n    });\n  });\n});\n"
  },
  {
    "path": "chapter11/client/jest.config.js",
    "content": "module.exports = {\n  setupFilesAfterEnv: [\n    \"<rootDir>/setupGlobalFetch.js\",\n    \"<rootDir>/setupJestDom.js\"\n  ]\n};\n"
  },
  {
    "path": "chapter11/client/main.js",
    "content": "const { connect } = require(\"./socket\");\n\nconst {\n  handleAddItem,\n  checkFormValues,\n  handleUndo,\n  handlePopstate,\n  updateItemList\n} = require(\"./domController\");\n\nconst { API_ADDR, data } = require(\"./inventoryController\");\n\nconst form = document.getElementById(\"add-item-form\");\nform.addEventListener(\"submit\", handleAddItem);\nform.addEventListener(\"input\", checkFormValues);\n\nconst undoButton = document.getElementById(\"undo-button\");\nundoButton.addEventListener(\"click\", handleUndo);\n\nwindow.addEventListener(\"popstate\", handlePopstate);\n\n// Run `checkFormValues` once to see if the initial state is valid\ncheckFormValues();\n\nconst loadInitialData = async () => {\n  try {\n    const inventoryResponse = await fetch(`${API_ADDR}/inventory`);\n    data.inventory = await inventoryResponse.json();\n    return updateItemList(data.inventory);\n  } catch (e) {\n    // Restore the inventory if the request fails\n    const storedInventory = JSON.parse(localStorage.getItem(\"inventory\"));\n\n    if (storedInventory) {\n      data.inventory = storedInventory;\n      updateItemList(data.inventory);\n    }\n  }\n};\n\nconnect();\n\nmodule.exports = loadInitialData();\n"
  },
  {
    "path": "chapter11/client/main.test.js",
    "content": "const nock = require(\"nock\");\nconst fs = require(\"fs\");\nconst initialHtml = fs.readFileSync(\"./index.html\");\nconst { screen, getByText, fireEvent } = require(\"@testing-library/dom\");\nconst { API_ADDR } = require(\"./inventoryController\");\n\nconst { clearHistoryHook, detachPopstateHandlers } = require(\"./testUtils.js\");\n\nbeforeEach(clearHistoryHook);\n\nbeforeEach(() => localStorage.clear());\n\nbeforeEach(async () => {\n  document.body.innerHTML = initialHtml;\n\n  // You must execute main.js again so that it can attach the\n  // event listener to the form every time the body changes.\n  // Here you must use `jest.resetModules` because otherwise\n  // Jest will have cached `main.js` and it will _not_ run again.\n  jest.resetModules();\n\n  nock(API_ADDR)\n    .get(\"/inventory\")\n    .replyWithError({ code: 500 });\n  await require(\"./main\");\n\n  // You can only spy on `window.addEventListener` after `main.js`\n  // has been executed. Otherwise `detachPopstateHandlers` will\n  // also detach the handlers that `main.js` attached to the page.\n  jest.spyOn(window, \"addEventListener\");\n});\n\nafterEach(detachPopstateHandlers);\n\nafterEach(() => {\n  if (!nock.isDone()) {\n    nock.cleanAll();\n    throw new Error(\"Not all mocked endpoints received requests.\");\n  }\n});\n\ntest(\"persists items between sessions\", async () => {\n  nock(API_ADDR)\n    .post(/inventory\\/.*$/)\n    .reply(200);\n\n  nock(API_ADDR)\n    .get(\"/inventory\")\n    .replyWithError({ code: 500 });\n\n  const submitBtn = screen.getByText(\"Add to inventory\");\n  const itemField = screen.getByPlaceholderText(\"Item name\");\n  fireEvent.input(itemField, {\n    target: { value: \"cheesecake\" },\n    bubbles: true\n  });\n\n  const quantityField = screen.getByPlaceholderText(\"Quantity\");\n  fireEvent.input(quantityField, { target: { value: \"6\" }, bubbles: true });\n\n  fireEvent.click(submitBtn);\n\n  const itemListBefore = document.getElementById(\"item-list\");\n  expect(itemListBefore.childNodes).toHaveLength(1);\n  expect(\n    getByText(itemListBefore, \"cheesecake - Quantity: 6\")\n  ).toBeInTheDocument();\n\n  // This is equivalent to reloading the page\n  document.body.innerHTML = initialHtml;\n  jest.resetModules();\n\n  await require(\"./main\");\n\n  const itemListAfter = document.getElementById(\"item-list\");\n  expect(itemListAfter.childNodes).toHaveLength(1);\n  expect(\n    getByText(itemListAfter, \"cheesecake - Quantity: 6\")\n  ).toBeInTheDocument();\n});\n\ndescribe(\"adding items\", () => {\n  test(\"updating the item list\", () => {\n    nock(API_ADDR)\n      .post(/inventory\\/.*$/)\n      .reply(200);\n\n    const submitBtn = screen.getByText(\"Add to inventory\");\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n    fireEvent.input(itemField, {\n      target: { value: \"cheesecake\" },\n      bubbles: true\n    });\n\n    const quantityField = screen.getByPlaceholderText(\"Quantity\");\n    fireEvent.input(quantityField, { target: { value: \"6\" }, bubbles: true });\n\n    fireEvent.click(submitBtn);\n\n    const itemList = document.getElementById(\"item-list\");\n    expect(getByText(itemList, \"cheesecake - Quantity: 6\")).toBeInTheDocument();\n  });\n\n  test(\"sending a request to update the item list\", () => {\n    nock(API_ADDR)\n      .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 6 }))\n      .reply(200);\n\n    const submitBtn = screen.getByText(\"Add to inventory\");\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n    fireEvent.input(itemField, {\n      target: { value: \"cheesecake\" },\n      bubbles: true\n    });\n\n    const quantityField = screen.getByPlaceholderText(\"Quantity\");\n    fireEvent.input(quantityField, { target: { value: \"6\" }, bubbles: true });\n\n    fireEvent.click(submitBtn);\n\n    if (!nock.isDone())\n      throw new Error(\"POST /inventory/cheesecake was not reached\");\n  });\n\n  test(\"undo to one item\", done => {\n    // You must specify the encoded URL here because\n    // nock struggles with encoded urls\n    nock(API_ADDR)\n      .post(\"/inventory/carrot%20cake\")\n      .reply(200);\n\n    nock(API_ADDR)\n      .post(\"/inventory/cheesecake\")\n      .reply(200);\n\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n    const quantityField = screen.getByPlaceholderText(\"Quantity\");\n    const submitBtn = screen.getByText(\"Add to inventory\");\n\n    fireEvent.input(itemField, {\n      target: { value: \"cheesecake\" },\n      bubbles: true\n    });\n    fireEvent.input(quantityField, { target: { value: \"6\" }, bubbles: true });\n    fireEvent.click(submitBtn);\n\n    fireEvent.input(itemField, {\n      target: { value: \"carrot cake\" },\n      bubbles: true\n    });\n    fireEvent.input(quantityField, { target: { value: \"5\" }, bubbles: true });\n    fireEvent.click(submitBtn);\n\n    window.addEventListener(\"popstate\", () => {\n      const itemList = document.getElementById(\"item-list\");\n      expect(itemList.children).toHaveLength(1);\n      expect(\n        getByText(itemList, \"cheesecake - Quantity: 6\")\n      ).toBeInTheDocument();\n      done();\n    });\n\n    fireEvent.click(screen.getByText(\"Undo\"));\n  });\n\n  test(\"undo to empty list\", done => {\n    nock(API_ADDR)\n      .post(/inventory\\/.*$/)\n      .reply(200);\n\n    const submitBtn = screen.getByText(\"Add to inventory\");\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n    fireEvent.input(itemField, {\n      target: { value: \"cheesecake\" },\n      bubbles: true\n    });\n\n    const quantityField = screen.getByPlaceholderText(\"Quantity\");\n    fireEvent.input(quantityField, { target: { value: \"6\" }, bubbles: true });\n\n    fireEvent.click(submitBtn);\n\n    expect(history.state).toEqual({ inventory: { cheesecake: 6 } });\n\n    window.addEventListener(\"popstate\", () => {\n      const itemList = document.getElementById(\"item-list\");\n      expect(itemList).toBeEmpty();\n      done();\n    });\n\n    fireEvent.click(screen.getByText(\"Undo\"));\n  });\n});\n\ndescribe(\"item name validation\", () => {\n  test(\"entering valid item names \", () => {\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n\n    fireEvent.input(itemField, {\n      target: { value: \"cheesecake\" },\n      bubbles: true\n    });\n\n    expect(screen.getByText(\"cheesecake is valid!\")).toBeInTheDocument();\n  });\n\n  test(\"entering invalid item names \", () => {\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n\n    fireEvent.input(itemField, { target: { value: \"book\" }, bubbles: true });\n\n    expect(screen.getByText(\"book is not a valid item.\")).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "chapter11/client/package.json",
    "content": "{\n  \"name\": \"1_http_requests\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"start\": \"http-server ./\",\n    \"test\": \"jest\",\n    \"build\": \"browserify main.js -o bundle.js\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"@testing-library/dom\": \"^7.2.2\",\n    \"@testing-library/jest-dom\": \"^5.5.0\",\n    \"browserify\": \"^16.5.1\",\n    \"http-server\": \"^0.12.1\",\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"jest\": \"^24.9.0\",\n    \"nock\": \"^12.0.3\",\n    \"socket.io\": \"^2.3.0\"\n  },\n  \"dependencies\": {\n    \"http-shutdown\": \"^1.2.2\",\n    \"socket.io-client\": \"^2.3.0\"\n  }\n}\n"
  },
  {
    "path": "chapter11/client/setupGlobalFetch.js",
    "content": "const fetch = require(\"isomorphic-fetch\");\n\nglobal.window.fetch = fetch;\n"
  },
  {
    "path": "chapter11/client/setupJestDom.js",
    "content": "const jestDom = require(\"@testing-library/jest-dom\");\n\nexpect.extend(jestDom);\n"
  },
  {
    "path": "chapter11/client/socket.js",
    "content": "const { API_ADDR, data } = require(\"./inventoryController\");\nconst { updateItemList } = require(\"./domController\");\n\nconst client = { id: null };\n\nconst io = require(\"socket.io-client\");\n\nconst handleAddItemMsg = ({ itemName, quantity }) => {\n  const currentQuantity = data.inventory[itemName] || 0;\n  data.inventory[itemName] = currentQuantity + quantity;\n  return updateItemList(data.inventory);\n};\n\nconst connect = () => {\n  return new Promise(resolve => {\n    const socket = io(API_ADDR);\n\n    socket.on(\"connect\", () => {\n      client.id = socket.id;\n      resolve(socket);\n    });\n\n    socket.on(\"add_item\", handleAddItemMsg);\n  });\n};\n\nmodule.exports = { client, connect, handleAddItemMsg };\n"
  },
  {
    "path": "chapter11/client/socket.test.js",
    "content": "const nock = require(\"nock\");\nconst fs = require(\"fs\");\nconst initialHtml = fs.readFileSync(\"./index.html\");\nconst { getByText } = require(\"@testing-library/dom\");\nconst { data } = require(\"./inventoryController\");\nconst { start, stop, sendMsg } = require(\"./testSocketServer\");\n\nconst { handleAddItemMsg, connect } = require(\"./socket\");\n\nbeforeEach(() => {\n  document.body.innerHTML = initialHtml;\n});\n\nbeforeEach(() => {\n  data.inventory = {};\n});\n\ndescribe(\"handleAddItemMsg\", () => {\n  test(\"updating the inventory and the item list\", () => {\n    handleAddItemMsg({ itemName: \"cheesecake\", quantity: 6 });\n\n    expect(data.inventory).toEqual({ cheesecake: 6 });\n    const itemList = document.getElementById(\"item-list\");\n    expect(itemList.childNodes).toHaveLength(1);\n    expect(getByText(itemList, \"cheesecake - Quantity: 6\")).toBeInTheDocument();\n  });\n});\n\ndescribe(\"handling real messages\", () => {\n  beforeAll(start);\n\n  beforeAll(async () => {\n    nock.cleanAll();\n    await connect();\n  });\n\n  afterAll(stop);\n\n  test(\"handling add_item messages\", async () => {\n    sendMsg(\"add_item\", { itemName: \"cheesecake\", quantity: 6 });\n\n    await new Promise(resolve => setTimeout(resolve, 1000));\n\n    expect(data.inventory).toEqual({ cheesecake: 6 });\n    const itemList = document.getElementById(\"item-list\");\n    expect(itemList.childNodes).toHaveLength(1);\n    expect(getByText(itemList, \"cheesecake - Quantity: 6\")).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "chapter11/client/testSocketServer.js",
    "content": "const server = require(\"http\").createServer();\nconst io = require(\"socket.io\")(server);\n\nconst sendMsg = (msgType, content) => {\n  io.sockets.emit(msgType, content);\n};\n\nconst start = () =>\n  new Promise(resolve => {\n    server.listen(3000, resolve);\n  });\n\nconst stop = () =>\n  new Promise(resolve => {\n    server.close(resolve);\n  });\n\nmodule.exports = { start, stop, sendMsg };\n"
  },
  {
    "path": "chapter11/client/testUtils.js",
    "content": "const clearHistoryHook = done => {\n  const clearHistory = () => {\n    if (history.state === null) {\n      window.removeEventListener(\"popstate\", clearHistory);\n      return done();\n    }\n\n    history.back();\n  };\n\n  window.addEventListener(\"popstate\", clearHistory);\n\n  clearHistory();\n};\n\nconst detachPopstateHandlers = () => {\n  const popstateListeners = window.addEventListener.mock.calls.filter(\n    ([eventName]) => {\n      return eventName === \"popstate\";\n    }\n  );\n\n  popstateListeners.forEach(([eventName, handlerFn]) => {\n    window.removeEventListener(eventName, handlerFn);\n  });\n\n  jest.restoreAllMocks();\n};\n\nmodule.exports = { clearHistoryHook, detachPopstateHandlers };\n"
  },
  {
    "path": "chapter11/server/README.md",
    "content": "# Chapter 5 Server\n\nTo better support the client-side application we'll build on Chapter 5, I've had to do a few updates to the server from Chapter 4.\n\nIn case you want to update the back-end from Chapter 4 yourself, here's the list of changes I've done:\n\n- For the server to accept the requests coming from the client, you'll need to use [`@koa/cors`](https://github.com/koajs/cors)\n- To enable running tests while the server is running, I bind it to different ports depending on whether I am in a test or development environment.\n- At `POST /inventory/:itemName` I have added a route which adds an item to the inventory. It takes a `body` containing the `quantity` to add.\n- At `GET /inventory` I have added a route which lists all items in the inventory.\n- At `DELETE /inventory/:itemName` I have added a route which let's you delete inventory items so that you can use to fix the `undo` functionality\n- I've used `koa-socket-2` to add support for `socket.io`\n- The `POST /inventory/:itemName` will now push updates to all clients but the one which added an item.\n"
  },
  {
    "path": "chapter11/server/authenticationController.js",
    "content": "const crypto = require(\"crypto\");\nconst { db } = require(\"./dbConnection\");\n\nconst hashPassword = password => {\n  const hash = crypto.createHash(\"sha256\");\n  hash.update(password);\n  return hash.digest(\"hex\");\n};\n\nconst credentialsAreValid = async (username, password) => {\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  if (!user) return false;\n  return hashPassword(password) === user.passwordHash;\n};\n\nconst authenticationMiddleware = async (ctx, next) => {\n  try {\n    const authHeader = ctx.request.headers.authorization;\n    const credentials = Buffer.from(\n      authHeader.slice(\"basic\".length + 1),\n      \"base64\"\n    ).toString();\n    const [username, password] = credentials.split(\":\");\n\n    const validCredentialsSent = await credentialsAreValid(username, password);\n    if (!validCredentialsSent) throw new Error(\"invalid credentials\");\n  } catch (e) {\n    ctx.status = 401;\n    ctx.body = { message: \"please provide valid credentials\" };\n    return;\n  }\n\n  await next();\n};\n\nmodule.exports = {\n  hashPassword,\n  credentialsAreValid,\n  authenticationMiddleware\n};\n"
  },
  {
    "path": "chapter11/server/authenticationController.test.js",
    "content": "const crypto = require(\"crypto\");\nconst {\n  hashPassword,\n  credentialsAreValid,\n  authenticationMiddleware\n} = require(\"./authenticationController\");\nconst { user: globalUser } = require(\"./userTestUtils\");\n\ndescribe(\"hashPassword\", () => {\n  test(\"hashing passwords\", () => {\n    const plainTextPassword = \"password_example\";\n    const hash = crypto.createHash(\"sha256\");\n    hash.update(plainTextPassword);\n    const expectedHash = hash.digest(\"hex\");\n    expect(hashPassword(plainTextPassword)).toBe(expectedHash);\n  });\n});\n\ndescribe(\"credentialsAreValid\", () => {\n  test(\"validating credentials\", async () => {\n    expect(await credentialsAreValid(globalUser.username, \"a_password\")).toBe(\n      true\n    );\n  });\n});\n\ndescribe(\"authenticationMiddleware\", () => {\n  test(\"returning an error if the credentials are not valid\", async () => {\n    const fakeAuth = Buffer.from(\"invalid:credentials\").toString(\"base64\");\n    const ctx = {\n      request: {\n        headers: { authorization: `Basic ${fakeAuth}` }\n      }\n    };\n\n    const next = jest.fn();\n    await authenticationMiddleware(ctx, next);\n    expect(next.mock.calls).toHaveLength(0);\n    expect(ctx).toEqual({\n      ...ctx,\n      status: 401,\n      body: { message: \"please provide valid credentials\" }\n    });\n  });\n\n  test(\"authenticating properly\", async () => {\n    const ctx = {\n      request: {\n        headers: { authorization: globalUser.authHeader }\n      }\n    };\n\n    const next = jest.fn();\n    await authenticationMiddleware(ctx, next);\n    expect(next.mock.calls).toHaveLength(1);\n  });\n});\n"
  },
  {
    "path": "chapter11/server/cartController.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { removeFromInventory } = require(\"./inventoryController\");\nconst logger = require(\"./logger\");\n\nconst addItemToCart = async (username, itemName) => {\n  await removeFromInventory(itemName);\n\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  if (!user) {\n    const userNotFound = new Error(\"user not found\");\n    userNotFound.code = 404;\n  }\n\n  const itemEntry = await db\n    .select()\n    .from(\"carts_items\")\n    .where({ userId: user.id, itemName })\n    .first();\n\n  if (itemEntry && itemEntry.quantity + 1 > 3) {\n    const limitError = new Error(\n      \"You can't have more than three units of an item in your cart\"\n    );\n    limitError.code = 400;\n    throw limitError;\n  }\n\n  if (itemEntry) {\n    await db(\"carts_items\")\n      .increment(\"quantity\")\n      .update({ updatedAt: new Date().toISOString() })\n      .where({\n        userId: itemEntry.userId,\n        itemName\n      });\n  } else {\n    await db(\"carts_items\").insert({\n      userId: user.id,\n      itemName,\n      quantity: 1,\n      updatedAt: new Date().toISOString()\n    });\n  }\n\n  logger.log(`${itemName} added to ${username}'s cart`);\n  return db\n    .select(\"itemName\", \"quantity\")\n    .from(\"carts_items\")\n    .where({ userId: user.id });\n};\n\nconst hoursInMs = n => 1000 * 60 * 60 * n;\n\nconst removeStaleItems = async () => {\n  const fourHoursAgo = new Date(Date.now() - hoursInMs(4)).toISOString();\n\n  const staleItems = await db\n    .select()\n    .from(\"carts_items\")\n    .where(\"updatedAt\", \"<\", fourHoursAgo);\n\n  if (staleItems.length === 0) return;\n\n  // Put stale items back in the inventory\n  const inventoryUpdates = staleItems.map(staleItem =>\n    db(\"inventory\")\n      .increment(\"quantity\", staleItem.quantity)\n      .where({ itemName: staleItem.itemName })\n  );\n  await Promise.all(inventoryUpdates);\n\n  // Delete stale items from cart\n  const staleItemTuples = staleItems.map(i => [i.itemName, i.userId]);\n  await db(\"carts_items\")\n    .del()\n    .whereIn([\"itemName\", \"userId\"], staleItemTuples);\n};\n\nconst monitorStaleItems = () => setInterval(removeStaleItems, hoursInMs(2));\n\nmodule.exports = { addItemToCart, monitorStaleItems };\n"
  },
  {
    "path": "chapter11/server/cartController.test.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { addItemToCart, monitorStaleItems } = require(\"./cartController\");\nconst { hashPassword } = require(\"./authenticationController\");\nconst { user: globalUser } = require(\"./userTestUtils\");\nconst FakeTimers = require(\"@sinonjs/fake-timers\");\n\nconst fs = require(\"fs\");\n\ndescribe(\"addItemToCart\", () => {\n  beforeEach(() => {\n    fs.writeFileSync(\"/tmp/logs.out\", \"\");\n  });\n\n  test(\"adding unavailable items to cart\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 0 });\n\n    try {\n      await addItemToCart(globalUser.username, \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(\"cheesecake is unavailable\");\n      expectedError.code = 400;\n\n      expect(e).toEqual(expectedError);\n    }\n\n    const finalCartContent = await db\n      .select(\"carts_items.*\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n\n    expect(finalCartContent).toEqual([]);\n    expect.assertions(2);\n  });\n\n  test(\"adding items above limit to cart\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 1 });\n    await db(\"carts_items\").insert({\n      userId: globalUser.id,\n      itemName: \"cheesecake\",\n      quantity: 3\n    });\n\n    try {\n      await addItemToCart(globalUser.username, \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(\n        \"You can't have more than three units of an item in your cart\"\n      );\n      expectedError.code = 400;\n      expect(e).toEqual(expectedError);\n    }\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n\n    expect(finalCartContent).toEqual([{ itemName: \"cheesecake\", quantity: 3 }]);\n    expect.assertions(2);\n  });\n\n  test(\"logging added items\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 1 });\n    await db(\"carts_items\").insert({\n      userId: globalUser.id,\n      itemName: \"cheesecake\",\n      quantity: 1\n    });\n\n    await addItemToCart(globalUser.username, \"cheesecake\");\n\n    const logs = fs.readFileSync(\"/tmp/logs.out\", \"utf-8\");\n    expect(logs).toContain(\n      `cheesecake added to ${globalUser.username}'s cart\\n`\n    );\n  });\n});\n\nconst withRetries = async fn => {\n  // Capture the assertion error since Jest does not export it\n  const JestAssertionError = (() => {\n    try {\n      expect(false).toBe(true);\n    } catch (e) {\n      return e.constructor;\n    }\n  })();\n\n  try {\n    await fn();\n  } catch (e) {\n    if (e.constructor === JestAssertionError) {\n      // Wait 100ms before retrying\n      await new Promise(resolve => setTimeout(resolve, 100));\n      await withRetries(fn);\n    } else {\n      throw e;\n    }\n  }\n};\n\ndescribe(\"timers\", () => {\n  const hoursInMs = n => 1000 * 60 * 60 * n;\n\n  let clock;\n  beforeEach(() => {\n    clock = FakeTimers.install({ toFake: [\"Date\", \"setInterval\"] });\n  });\n\n  afterEach(() => {\n    clock = clock.uninstall();\n  });\n\n  test(\"removing stale items\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 1 });\n    await addItemToCart(globalUser.username, \"cheesecake\");\n\n    clock.tick(hoursInMs(4));\n    timer = monitorStaleItems();\n    clock.tick(hoursInMs(2));\n\n    await withRetries(async () => {\n      const finalCartContent = await db\n        .select()\n        .from(\"carts_items\")\n        .join(\"users\", \"users.id\", \"carts_items.userId\")\n        .where(\"users.username\", globalUser.username);\n\n      expect(finalCartContent).toEqual([]);\n    });\n\n    await withRetries(async () => {\n      const inventoryContent = await db\n        .select(\"itemName\", \"quantity\")\n        .from(\"inventory\");\n\n      expect(inventoryContent).toEqual([\n        { itemName: \"cheesecake\", quantity: 1 }\n      ]);\n    });\n  });\n});\n"
  },
  {
    "path": "chapter11/server/dbConnection.js",
    "content": "const environmentName = process.env.NODE_ENV;\nconst db = require(\"knex\")(require(\"./knexfile\")[environmentName]);\n\nconst closeConnection = () => db.destroy();\n\nmodule.exports = {\n  db,\n  closeConnection\n};\n"
  },
  {
    "path": "chapter11/server/disconnectFromDb.js",
    "content": "const { db } = require(\"./dbConnection\");\n\nafterAll(() => db.destroy());\n"
  },
  {
    "path": "chapter11/server/inventoryController.js",
    "content": "const { db } = require(\"./dbConnection\");\n\nconst removeFromInventory = async itemName => {\n  const inventoryEntry = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName })\n    .first();\n\n  if (!inventoryEntry || inventoryEntry.quantity === 0) {\n    const err = new Error(`${itemName} is unavailable`);\n    err.code = 400;\n    throw err;\n  }\n\n  await db(\"inventory\")\n    .decrement(\"quantity\")\n    .where({ itemName });\n};\n\nmodule.exports = { removeFromInventory };\n"
  },
  {
    "path": "chapter11/server/jest.config.js",
    "content": "module.exports = {\n  testEnvironment: \"node\",\n  globalSetup: \"./migrateDatabases.js\",\n  setupFilesAfterEnv: [\n    \"<rootDir>/truncateTables.js\",\n    \"<rootDir>/seedUser.js\",\n    \"<rootDir>/disconnectFromDb.js\"\n  ]\n};\n"
  },
  {
    "path": "chapter11/server/knexfile.js",
    "content": "module.exports = {\n  test: {\n    client: \"sqlite3\",\n    connection: { filename: \"./test.sqlite\" },\n    useNullAsDefault: true\n  },\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"./dev.sqlite\" },\n    useNullAsDefault: true\n  }\n};\n"
  },
  {
    "path": "chapter11/server/logger.js",
    "content": "const fs = require(\"fs\");\n\nconst logger = {\n  log: msg => fs.appendFileSync(\"/tmp/logs.out\", msg + \"\\n\")\n};\n\nmodule.exports = logger;\n"
  },
  {
    "path": "chapter11/server/migrateDatabases.js",
    "content": "const environmentName = process.env.NODE_ENV || \"test\";\nconst environmentConfig = require(\"./knexfile\")[environmentName];\nconst db = require(\"knex\")(environmentConfig);\n\nmodule.exports = async () => {\n  // Migrate the database to the latest state\n  await db.migrate.latest();\n\n  // Close the connection to the database so that tests won't hang\n  await db.destroy();\n};\n"
  },
  {
    "path": "chapter11/server/migrations/20200325082401_initial_schema.js",
    "content": "exports.up = async knex => {\n  await knex.schema.createTable(\"users\", table => {\n    table.increments(\"id\");\n    table.string(\"username\");\n    table.unique(\"username\");\n    table.string(\"email\");\n    table.string(\"passwordHash\");\n  });\n\n  await knex.schema.createTable(\"carts_items\", table => {\n    table.integer(\"userId\").references(\"users.id\");\n    table.string(\"itemName\");\n    table.unique(\"itemName\");\n    table.integer(\"quantity\");\n  });\n\n  await knex.schema.createTable(\"inventory\", table => {\n    table.increments(\"id\");\n    table.string(\"itemName\");\n    table.unique(\"itemName\");\n    table.integer(\"quantity\");\n  });\n};\n\nexports.down = async knex => {\n  await knex.schema.dropTable(\"users\");\n  await knex.schema.dropTable(\"carts_items\");\n  await knex.schema.dropTable(\"inventory\");\n};\n"
  },
  {
    "path": "chapter11/server/migrations/20200331210311_updatedAt_field.js",
    "content": "exports.up = knex => {\n  return knex.schema.alterTable(\"carts_items\", table => {\n    table.timestamp(\"updatedAt\");\n  });\n};\n\nexports.down = knex => {\n  return knex.schema.alterTable(\"carts_items\", table => {\n    table.dropColumn(\"updatedAt\");\n  });\n};\n"
  },
  {
    "path": "chapter11/server/package.json",
    "content": "{\n  \"name\": \"chapter5_server\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"test\": \"jest --runInBand\",\n    \"start\": \"cross-env NODE_ENV=development node server.js\",\n    \"migrate:dev\": \"knex migrate:latest --env development\",\n    \"seed:dev\": \"knex seed:run\"\n  },\n  \"devDependencies\": {\n    \"@sinonjs/fake-timers\": \"github:sinonjs/fake-timers\",\n    \"jest\": \"^24.9.0\",\n    \"supertest\": \"^4.0.2\"\n  },\n  \"dependencies\": {\n    \"@koa/cors\": \"^3.0.0\",\n    \"cross-env\": \"^7.0.2\",\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"knex\": \"^0.20.13\",\n    \"koa\": \"^2.11.0\",\n    \"koa-body-parser\": \"^1.1.2\",\n    \"koa-router\": \"^7.4.0\",\n    \"koa-socket-2\": \"^1.2.0\",\n    \"nock\": \"^12.0.3\",\n    \"socket.io\": \"^2.3.0\",\n    \"sqlite3\": \"^4.1.1\"\n  },\n  \"main\": \"alertController.spec.js\",\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"description\": \"\"\n}\n"
  },
  {
    "path": "chapter11/server/seedUser.js",
    "content": "const { createUser } = require(\"./userTestUtils\");\n\nbeforeEach(createUser);\n"
  },
  {
    "path": "chapter11/server/seeds/initial_inventory.js",
    "content": "exports.seed = async knex => {\n  await knex(\"inventory\").del();\n  return knex(\"inventory\").insert([\n    { itemName: \"cheesecake\", quantity: 8 },\n    { itemName: \"apple pie\", quantity: 2 },\n    { itemName: \"carrot cake\", quantity: 5 }\n  ]);\n};\n"
  },
  {
    "path": "chapter11/server/server.js",
    "content": "const fetch = require(\"isomorphic-fetch\");\nconst Koa = require(\"koa\");\nconst http = require(\"http\");\nconst IO = require(\"koa-socket-2\");\nconst cors = require(\"@koa/cors\");\nconst Router = require(\"koa-router\");\nconst bodyParser = require(\"koa-body-parser\");\n\nconst { db } = require(\"./dbConnection\");\n\nconst { addItemToCart } = require(\"./cartController\");\nconst {\n  hashPassword,\n  authenticationMiddleware\n} = require(\"./authenticationController\");\n\nconst PORT = process.env.NODE_ENV === \"test\" ? 5000 : 3000;\n\nconst app = new Koa();\nconst io = new IO();\nio.attach(app);\n\nconst router = new Router();\n\napp.use(cors());\n\napp.use(bodyParser());\n\napp.use(async (ctx, next) => {\n  if (ctx.url.startsWith(\"/carts\")) {\n    return await authenticationMiddleware(ctx, next);\n  }\n\n  await next();\n});\n\nrouter.put(\"/users/:username\", async ctx => {\n  const { username } = ctx.params;\n  const { email, password } = ctx.request.body;\n\n  const userAlreadyExists = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n\n  if (userAlreadyExists) {\n    ctx.body = { message: `${username} already exists` };\n    ctx.status = 409;\n    return;\n  }\n\n  await db(\"users\").insert({\n    username,\n    email,\n    passwordHash: hashPassword(password)\n  });\n\n  return (ctx.body = { message: `${username} created successfully` });\n});\n\nrouter.post(\"/carts/:username/items\", async ctx => {\n  const { username } = ctx.params;\n  const { item, quantity } = ctx.request.body;\n\n  for (let i = 0; i < quantity; i++) {\n    try {\n      const newItems = await addItemToCart(username, item);\n      ctx.body = newItems;\n    } catch (e) {\n      ctx.body = { message: e.message };\n      ctx.status = e.code;\n      return;\n    }\n  }\n});\n\nrouter.delete(\"/carts/:username/items/:item\", async ctx => {\n  const { username, item } = ctx.params;\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n\n  if (!user) {\n    ctx.body = { message: \"user not found\" };\n    ctx.status = 404;\n    return;\n  }\n\n  const itemEntry = await db\n    .select()\n    .from(\"carts_items\")\n    .where({ userId: user.id, itemName: item })\n    .first();\n\n  if (!itemEntry || itemEntry.quantity === 0) {\n    ctx.body = { message: `${item} is not in the cart` };\n    ctx.status = 400;\n    return;\n  }\n\n  await db(\"carts_items\")\n    .decrement(\"quantity\")\n    .where({ userId: user.id, itemName: item });\n\n  const inventoryEntry = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName: item })\n    .first();\n  if (inventoryEntry) {\n    await db(\"inventory\")\n      .increment(\"quantity\")\n      .where({ userId: itemEntry.userId, itemName: item });\n  } else {\n    await db(\"inventory\").insert({ itemName: item, quantity: 1 });\n  }\n\n  ctx.body = await db\n    .select(\"itemName\", \"quantity\")\n    .from(\"carts_items\")\n    .where({ userId: user.id });\n});\n\nrouter.post(\"/inventory/:itemName\", async ctx => {\n  const { itemName } = ctx.params;\n  const { quantity } = ctx.request.body;\n  const clientId = ctx.request.headers[\"x-socket-client-id\"];\n\n  const current = await db\n    .select(\"itemName\", \"quantity\")\n    .from(\"inventory\")\n    .where({ itemName })\n    .first();\n\n  const itemExists = current && current.quantity > 0;\n  const newRecord = {\n    itemName,\n    quantity: (itemExists ? current.quantity : 0) + quantity\n  };\n\n  if (current) {\n    await db(\"inventory\")\n      .increment(\"quantity\", quantity)\n      .where({ itemName });\n  } else {\n    await db(\"inventory\").insert(newRecord);\n  }\n\n  Object.entries(io.socket.sockets.connected).forEach(([id, socket]) => {\n    if (id === clientId) return;\n    socket.emit(\"add_item\", { itemName, quantity });\n  });\n\n  ctx.body = newRecord;\n});\n\nrouter.delete(\"/inventory/:itemName\", async ctx => {\n  const { itemName } = ctx.params;\n  const { quantity } = ctx.request.body;\n\n  const current = await db\n    .select(\"itemName\", \"quantity\")\n    .from(\"inventory\")\n    .where({ itemName })\n    .first();\n\n  const canDelete = current && current.quantity > quantity;\n\n  if (canDelete) {\n    await db(\"inventory\")\n      .decrement(\"quantity\", quantity)\n      .where({ itemName });\n    ctx.body = { message: `Removed ${quantity} units of ${itemName}` };\n  } else {\n    ctx.status = 404;\n    ctx.body = {\n      message: `There aren't ${quantity} units of ${itemName} available.`\n    };\n  }\n});\n\nrouter.get(\"/inventory\", async ctx => {\n  const inventoryContent = await db\n    .select(\"itemName\", \"quantity\")\n    .from(\"inventory\")\n    .where(\"quantity\", \">\", 0)\n    .orderBy(\"quantity\", \"desc\");\n\n  ctx.body = inventoryContent.reduce((acc, { itemName, quantity }) => {\n    return { ...acc, [itemName]: quantity };\n  }, {});\n});\n\nrouter.get(\"/inventory/:itemName\", async ctx => {\n  const { itemName } = ctx.params;\n\n  const response = await fetch(`http://recipepuppy.com/api?i=${itemName}`);\n  const { title, href, results: recipes } = await response.json();\n  const inventoryItem = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName })\n    .first();\n\n  ctx.body = {\n    ...inventoryItem,\n    info: `Data obtained from ${title} - ${href}`,\n    recipes\n  };\n});\n\napp.use(router.routes());\n\nmodule.exports = { app: app.listen(PORT, \"127.0.0.1\") };\n"
  },
  {
    "path": "chapter11/server/server.test.js",
    "content": "const { user: globalUser } = require(\"./userTestUtils\");\nconst { db } = require(\"./dbConnection\");\nconst request = require(\"supertest\");\nconst { app } = require(\"./server.js\");\nconst { hashPassword } = require(\"./authenticationController.js\");\nconst nock = require(\"nock\");\n\nafterAll(() => app.close());\n\ndescribe(\"add items to a cart\", () => {\n  test(\"adding available items\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 3 });\n    const response = await request(app)\n      .post(`/carts/${globalUser.username}/items`)\n      .set(\"authorization\", globalUser.authHeader)\n      .send({ item: \"cheesecake\", quantity: 3 })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    const newItems = [{ itemName: \"cheesecake\", quantity: 3 }];\n    expect(response.body).toEqual(newItems);\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(0);\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n\n    expect(finalCartContent).toEqual(newItems);\n  });\n\n  test(\"adding unavailable items\", async () => {\n    const response = await request(app)\n      .post(`/carts/${globalUser.username}/items`)\n      .set(\"authorization\", globalUser.authHeader)\n      .send({ item: \"cheesecake\", quantity: 1 })\n      .expect(400)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"cheesecake is unavailable\"\n    });\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n    expect(finalCartContent).toEqual([]);\n  });\n});\n\ndescribe(\"removing items from a cart\", () => {\n  test(\"removing existing items\", async () => {\n    await db(\"carts_items\").insert({\n      userId: globalUser.id,\n      itemName: \"cheesecake\",\n      quantity: 1\n    });\n\n    const response = await request(app)\n      .del(`/carts/${globalUser.username}/items/cheesecake`)\n      .set(\"authorization\", globalUser.authHeader)\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    const expectedFinalContent = [{ itemName: \"cheesecake\", quantity: 0 }];\n\n    expect(response.body).toEqual(expectedFinalContent);\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n    expect(finalCartContent).toEqual(expectedFinalContent);\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(1);\n  });\n\n  test(\"removing non-existing items\", async () => {\n    await db(\"inventory\").insert({\n      itemName: \"cheesecake\",\n      quantity: 0\n    });\n\n    const response = await request(app)\n      .del(`/carts/${globalUser.username}/items/cheesecake`)\n      .set(\"authorization\", globalUser.authHeader)\n      .expect(400)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"cheesecake is not in the cart\"\n    });\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(0);\n  });\n});\n\ndescribe(\"create accounts\", () => {\n  test(\"creating a new account\", async () => {\n    const response = await request(app)\n      .put(\"/users/another_user\")\n      .send({ email: \"another_user@example.org\", password: \"a_password\" })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"another_user created successfully\"\n    });\n\n    const savedUser = await db\n      .select(\"email\", \"passwordHash\")\n      .from(\"users\")\n      .where({ username: \"another_user\" })\n      .first();\n\n    expect(savedUser).toEqual({\n      email: \"another_user@example.org\",\n      passwordHash: hashPassword(\"a_password\")\n    });\n  });\n\n  test(\"creating a duplicate account\", async () => {\n    const response = await request(app)\n      .put(`/users/${globalUser.username}`)\n      .send({ email: globalUser.email, password: \"a_password\" })\n      .expect(409)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: `${globalUser.username} already exists`\n    });\n  });\n});\n\ndescribe(\"list inventory items\", () => {\n  const eggs = { itemName: \"eggs\", quantity: 3 };\n  const applePie = { itemName: \"apple pie\", quantity: 1 };\n  const carrotCake = { itemName: \"carrot cake\", quantity: 0 };\n\n  beforeEach(async () => {\n    await db(\"inventory\").insert([eggs, applePie, carrotCake]);\n  });\n\n  test(\"fetching all available items\", async () => {\n    const { body } = await request(app)\n      .get(\"/inventory\")\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(body).toEqual({ eggs: 3, \"apple pie\": 1 });\n  });\n});\n\ndescribe(\"add inventory items\", () => {\n  test(\"adding a new item\", async () => {\n    const { body } = await request(app)\n      .post(\"/inventory/eggs\")\n      .send({ quantity: 3 })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(body).toEqual({ itemName: \"eggs\", quantity: 3 });\n\n    expect(\n      await db\n        .select(\"itemName\", \"quantity\")\n        .from(\"inventory\")\n        .where(\"itemName\", \"eggs\")\n        .first()\n    ).toEqual({ itemName: \"eggs\", quantity: 3 });\n  });\n\n  test(\"adding an existing item\", async () => {\n    const eggs = { itemName: \"eggs\", quantity: 2 };\n    await db(\"inventory\").insert(eggs);\n\n    const { body } = await request(app)\n      .post(\"/inventory/eggs\")\n      .send({ quantity: 3 })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(body).toEqual({ itemName: \"eggs\", quantity: 5 });\n\n    expect(\n      await db\n        .select(\"itemName\", \"quantity\")\n        .from(\"inventory\")\n        .where(\"itemName\", \"eggs\")\n        .first()\n    ).toEqual({ itemName: \"eggs\", quantity: 5 });\n  });\n});\n\ndescribe(\"remove inventory items\", () => {\n  beforeEach(async () => {\n    await db(\"inventory\").insert({ itemName: \"eggs\", quantity: 3 });\n  });\n\n  test(\"removing an item\", async () => {\n    const { body } = await request(app)\n      .del(\"/inventory/eggs\")\n      .send({ quantity: 2 })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(body).toEqual({\n      message: \"Removed 2 units of eggs\"\n    });\n\n    expect(\n      await db\n        .select(\"itemName\", \"quantity\")\n        .from(\"inventory\")\n        .where(\"itemName\", \"eggs\")\n        .first()\n    ).toEqual({ itemName: \"eggs\", quantity: 1 });\n  });\n\n  test(\"removing more than the inventory quantity\", async () => {\n    const { body } = await request(app)\n      .del(\"/inventory/eggs\")\n      .send({ quantity: 4 })\n      .expect(404)\n      .expect(\"Content-Type\", /json/);\n\n    expect(body).toEqual({\n      message: \"There aren't 4 units of eggs available.\"\n    });\n\n    expect(\n      await db\n        .select(\"itemName\", \"quantity\")\n        .from(\"inventory\")\n        .where(\"itemName\", \"eggs\")\n        .first()\n    ).toEqual({ itemName: \"eggs\", quantity: 3 });\n  });\n});\n\ndescribe(\"fetch inventory items\", () => {\n  const eggs = { itemName: \"eggs\", quantity: 3 };\n  const applePie = { itemName: \"apple pie\", quantity: 1 };\n\n  beforeEach(async () => {\n    await db(\"inventory\").insert([eggs, applePie]);\n    const { id: eggsId } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"eggs\" })\n      .first();\n    eggs.id = eggsId;\n  });\n\n  test(\"fetching an item from the inventory\", async () => {\n    const eggsResponse = {\n      title: \"FakeAPI\",\n      href: \"example.org\",\n      results: [{ name: \"Omelette du Fromage\" }]\n    };\n\n    nock(\"http://recipepuppy.com\")\n      .get(\"/api\")\n      .query({ i: \"eggs\" })\n      .reply(200, eggsResponse);\n\n    const response = await request(app)\n      .get(`/inventory/eggs`)\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      ...eggs,\n      info: `Data obtained from ${eggsResponse.title} - ${eggsResponse.href}`,\n      recipes: eggsResponse.results\n    });\n  });\n});\n"
  },
  {
    "path": "chapter11/server/truncateTables.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst tablesToTruncate = [\"users\", \"inventory\", \"carts_items\"];\n\nbeforeEach(() => {\n  return Promise.all(tablesToTruncate.map(t => db(t).truncate()));\n});\n"
  },
  {
    "path": "chapter11/server/userTestUtils.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { hashPassword } = require(\"./authenticationController\");\n\nconst username = \"test_user\";\nconst password = \"a_password\";\nconst passwordHash = hashPassword(password);\nconst email = \"test_user@example.org\";\nconst validAuth = Buffer.from(`${username}:${password}`).toString(\"base64\");\nconst authHeader = `Basic ${validAuth}`;\n\nconst user = {\n  username,\n  password,\n  email,\n  authHeader\n};\n\nconst createUser = async () => {\n  await db(\"users\").insert({ username, email, passwordHash });\n  const { id } = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  user.id = id;\n};\n\nmodule.exports = { user, createUser };\n"
  },
  {
    "path": "chapter13/1_type_systems/1_no_types/orderQueue.js",
    "content": "const state = {\n  deliveries: []\n};\n\nconst addToDeliveryQueue = order => {\n  if (order.status !== \"done\") {\n    throw new Error(\"Can't add unfinished orders to the delivery queue.\");\n  }\n  state.deliveries.push(order);\n};\n\nmodule.exports = { state, addToDeliveryQueue };\n"
  },
  {
    "path": "chapter13/1_type_systems/1_no_types/orderQueue.spec.js",
    "content": "const { state, addToDeliveryQueue } = require(\"./orderQueue\");\n\ntest(\"adding unfinished orders to the queue\", () => {\n  state.deliveries = [];\n  const newOrder = {\n    items: [\"cheesecake\"],\n    status: \"in progress\"\n  };\n  expect(() => addToDeliveryQueue(newOrder)).toThrow();\n  expect(state.deliveries).toEqual([]);\n});\n"
  },
  {
    "path": "chapter13/1_type_systems/1_no_types/package.json",
    "content": "{\n  \"name\": \"1_no_types\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"orderQueue.js\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"jest\": \"^26.6.0\"\n  }\n}\n"
  },
  {
    "path": "chapter13/1_type_systems/2_with_types/orderQueue.js",
    "content": "\"use strict\";\nexports.__esModule = true;\nexports.addToDeliveryQueue = exports.state = void 0;\nexports.state = {\n  deliveries: []\n};\nvar addToDeliveryQueue = function(order) {\n  exports.state.deliveries.push(order);\n};\nexports.addToDeliveryQueue = addToDeliveryQueue;\n"
  },
  {
    "path": "chapter13/1_type_systems/2_with_types/orderQueue.spec.js",
    "content": "\"use strict\";\nexports.__esModule = true;\nvar orderQueue_1 = require(\"./orderQueue\");\ntest(\"adding finished items to the queue\", function() {\n  orderQueue_1.state.deliveries = [];\n  var newOrder = {\n    items: [\"cheesecake\"],\n    status: \"done\"\n  };\n  orderQueue_1.addToDeliveryQueue(newOrder);\n  expect(orderQueue_1.state.deliveries).toEqual([newOrder]);\n});\n"
  },
  {
    "path": "chapter13/1_type_systems/2_with_types/orderQueue.spec.ts",
    "content": "import { state, addToDeliveryQueue, DoneOrder } from \"./orderQueue\";\n\ntest(\"adding finished items to the queue\", () => {\n  state.deliveries = [];\n  const newOrder: DoneOrder = {\n    items: [\"cheesecake\"],\n    status: \"done\"\n  };\n  addToDeliveryQueue(newOrder);\n  expect(state.deliveries).toEqual([newOrder]);\n});\n"
  },
  {
    "path": "chapter13/1_type_systems/2_with_types/orderQueue.ts",
    "content": "type OrderItems = { 0: string } & Array<string>;\n\ntype Order = {\n  status: \"in progress\" | \"done\";\n  items: OrderItems;\n};\n\nexport type DoneOrder = Order & { status: \"done\" };\n\nexport const state: { deliveries: Array<Order> } = {\n  deliveries: []\n};\n\nexport const addToDeliveryQueue = (order: DoneOrder) => {\n  state.deliveries.push(order);\n};\n"
  },
  {
    "path": "chapter13/1_type_systems/2_with_types/package.json",
    "content": "{\n  \"name\": \"1_no_types\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"orderQueue.js\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"@types/jest\": \"^26.0.15\",\n    \"jest\": \"^26.6.0\"\n  },\n  \"dependencies\": {\n    \"typescript\": \"^4.1.2\"\n  }\n}\n"
  },
  {
    "path": "chapter13/1_type_systems/2_with_types/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    /* Visit https://aka.ms/tsconfig.json to read more about this file */\n\n    /* Basic Options */\n    // \"incremental\": true,                   /* Enable incremental compilation */\n    \"target\": \"es5\" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,\n    \"module\": \"commonjs\" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,\n    // \"lib\": [],                             /* Specify library files to be included in the compilation. */\n    // \"allowJs\": true,                       /* Allow javascript files to be compiled. */\n    // \"checkJs\": true,                       /* Report errors in .js files. */\n    // \"jsx\": \"preserve\",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */\n    // \"declaration\": true,                   /* Generates corresponding '.d.ts' file. */\n    // \"declarationMap\": true,                /* Generates a sourcemap for each corresponding '.d.ts' file. */\n    // \"sourceMap\": true,                     /* Generates corresponding '.map' file. */\n    // \"outFile\": \"./\",                       /* Concatenate and emit output to single file. */\n    // \"outDir\": \"./\",                        /* Redirect output structure to the directory. */\n    // \"rootDir\": \"./\",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */\n    // \"composite\": true,                     /* Enable project compilation */\n    // \"tsBuildInfoFile\": \"./\",               /* Specify file to store incremental compilation information */\n    // \"removeComments\": true,                /* Do not emit comments to output. */\n    // \"noEmit\": true,                        /* Do not emit outputs. */\n    // \"importHelpers\": true,                 /* Import emit helpers from 'tslib'. */\n    // \"downlevelIteration\": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */\n    // \"isolatedModules\": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */\n\n    /* Strict Type-Checking Options */\n    \"strict\": true /* Enable all strict type-checking options. */,\n    // \"noImplicitAny\": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */\n    // \"strictNullChecks\": true,              /* Enable strict null checks. */\n    // \"strictFunctionTypes\": true,           /* Enable strict checking of function types. */\n    // \"strictBindCallApply\": true,           /* Enable strict 'bind', 'call', and 'apply' methods on functions. */\n    // \"strictPropertyInitialization\": true,  /* Enable strict checking of property initialization in classes. */\n    // \"noImplicitThis\": true,                /* Raise error on 'this' expressions with an implied 'any' type. */\n    // \"alwaysStrict\": true,                  /* Parse in strict mode and emit \"use strict\" for each source file. */\n\n    /* Additional Checks */\n    // \"noUnusedLocals\": true,                /* Report errors on unused locals. */\n    // \"noUnusedParameters\": true,            /* Report errors on unused parameters. */\n    // \"noImplicitReturns\": true,             /* Report error when not all code paths in function return a value. */\n    // \"noFallthroughCasesInSwitch\": true,    /* Report errors for fallthrough cases in switch statement. */\n    // \"noUncheckedIndexedAccess\": true,      /* Include 'undefined' in index signature results */\n\n    /* Module Resolution Options */\n    // \"moduleResolution\": \"node\",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */\n    // \"baseUrl\": \"./\",                       /* Base directory to resolve non-absolute module names. */\n    // \"paths\": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */\n    // \"rootDirs\": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */\n    // \"typeRoots\": [],                       /* List of folders to include type definitions from. */\n    // \"types\": [],                           /* Type declaration files to be included in compilation. */\n    // \"allowSyntheticDefaultImports\": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */\n    \"esModuleInterop\": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,\n    // \"preserveSymlinks\": true,              /* Do not resolve the real path of symlinks. */\n    // \"allowUmdGlobalAccess\": true,          /* Allow accessing UMD globals from modules. */\n\n    /* Source Map Options */\n    // \"sourceRoot\": \"\",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */\n    // \"mapRoot\": \"\",                         /* Specify the location where debugger should locate map files instead of generated locations. */\n    // \"inlineSourceMap\": true,               /* Emit a single file with source maps instead of having a separate file. */\n    // \"inlineSources\": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */\n\n    /* Experimental Options */\n    // \"experimentalDecorators\": true,        /* Enables experimental support for ES7 decorators. */\n    // \"emitDecoratorMetadata\": true,         /* Enables experimental support for emitting type metadata for decorators. */\n\n    /* Advanced Options */\n    \"skipLibCheck\": true /* Skip type checking of declaration files. */,\n    \"forceConsistentCasingInFileNames\": true /* Disallow inconsistently-cased references to the same file. */\n  }\n}\n"
  },
  {
    "path": "chapter2/2_unit_tests/1_raw_tests/Cart.js",
    "content": "class Cart {\n  constructor() {\n    this.items = [];\n  }\n\n  addToCart(item) {\n    this.items.push(item);\n  }\n}\n\nmodule.exports = Cart;\n"
  },
  {
    "path": "chapter2/2_unit_tests/1_raw_tests/Cart.test.js",
    "content": "const Cart = require(\"./Cart.js\");\n\nconst cart = new Cart();\ncart.addToCart(\"cheesecake\");\n\nconst hasOneItem = cart.items.length === 1;\nconst hasACheesecake = cart.items[0] === \"cheesecake\";\n\nif (hasOneItem && hasACheesecake) {\n  console.log(\"The addToCart function can add an item to the cart\");\n} else {\n  const actualContent = cart.items.join(\", \");\n\n  console.error(\"The addToCart function didn't do what we expect!\");\n  console.error(`Here is the actual content of the cart: ${actualContent}`);\n\n  throw new Error(\"Test failed!\");\n}\n"
  },
  {
    "path": "chapter2/2_unit_tests/2_node_assert/Cart.js",
    "content": "class Cart {\n  constructor() {\n    this.items = [];\n  }\n\n  addToCart(item) {\n    this.items.push(item);\n  }\n}\n\nmodule.exports = Cart;\n"
  },
  {
    "path": "chapter2/2_unit_tests/2_node_assert/Cart.test.js",
    "content": "const assert = require(\"assert\");\nconst Cart = require(\"./Cart.js\");\n\nconst cart = new Cart();\ncart.addToCart(\"cheesecake\");\n\nassert.deepStrictEqual(cart.items, [\"cheesecake\"]);\n\nconsole.log(\"The addToCart function can add an item to the cart\");\n"
  },
  {
    "path": "chapter2/2_unit_tests/3_jest_multiple_tests/Cart.js",
    "content": "class Cart {\n  constructor() {\n    this.items = [];\n  }\n\n  addToCart(item) {\n    this.items.push(item);\n  }\n\n  removeFromCart(item) {\n    for (let i = 0; i < this.items.length; i++) {\n      const currentItem = this.items[i];\n      if (currentItem === item) {\n        this.items.splice(i, 1);\n      }\n    }\n  }\n}\n\nmodule.exports = Cart;\n"
  },
  {
    "path": "chapter2/2_unit_tests/3_jest_multiple_tests/Cart.test.js",
    "content": "const assert = require(\"assert\");\nconst Cart = require(\"./Cart.js\");\n\ntest(\"The addToCart function can add an item to the cart\", () => {\n  const cart = new Cart();\n  cart.addToCart(\"cheesecake\");\n\n  assert.deepStrictEqual(cart.items, [\"cheesecake\"]);\n});\n\ntest(\"The addToCart function can add an item to the cart\", () => {\n  const cart = new Cart();\n  cart.addToCart(\"cheesecake\");\n  cart.removeFromCart(\"cheesecake\");\n\n  assert.deepStrictEqual(cart.items, []);\n});\n"
  },
  {
    "path": "chapter2/2_unit_tests/3_jest_multiple_tests/package.json",
    "content": "{\n  \"name\": \"3_jest_multiple_tests\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"Cart.js\",\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\"\n}\n"
  },
  {
    "path": "chapter2/2_unit_tests/4_jest_assertions/Cart.js",
    "content": "class Cart {\n  constructor() {\n    this.items = [];\n  }\n\n  addToCart(item) {\n    this.items.push(item);\n  }\n\n  removeFromCart(item) {\n    for (let i = 0; i < this.items.length; i++) {\n      const currentItem = this.items[i];\n      if (currentItem === item) {\n        this.items.splice(i, 1);\n      }\n    }\n  }\n}\n\nmodule.exports = Cart;\n"
  },
  {
    "path": "chapter2/2_unit_tests/4_jest_assertions/Cart.test.js",
    "content": "const Cart = require(\"./Cart.js\");\n\ntest(\"The addToCart function can add an item to the cart\", () => {\n  const cart = new Cart();\n  cart.addToCart(\"cheesecake\");\n\n  expect(cart.items).toEqual([\"cheesecake\"]);\n});\n\ntest(\"The removeFromCart function can remove an item from the cart\", () => {\n  const cart = new Cart();\n  cart.addToCart(\"cheesecake\");\n  cart.removeFromCart(\"cheesecake\");\n\n  expect(cart.items).toEqual([]);\n});\n"
  },
  {
    "path": "chapter2/2_unit_tests/4_jest_assertions/package.json",
    "content": "{\n  \"name\": \"4_jest_assertions\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"Cart.js\",\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\"\n}\n"
  },
  {
    "path": "chapter2/2_unit_tests/5_npm_scripts/Cart.js",
    "content": "class Cart {\n  constructor() {\n    this.items = [];\n  }\n\n  addToCart(item) {\n    this.items.push(item);\n  }\n\n  removeFromCart(item) {\n    for (let i = 0; i < this.items.length; i++) {\n      const currentItem = this.items[i];\n      if (currentItem === item) {\n        this.items.splice(i, 1);\n      }\n    }\n  }\n}\n\nmodule.exports = Cart;\n"
  },
  {
    "path": "chapter2/2_unit_tests/5_npm_scripts/Cart.test.js",
    "content": "const Cart = require(\"./Cart.js\");\n\ntest(\"The addToCart function can add an item to the cart\", () => {\n  const cart = new Cart();\n  cart.addToCart(\"cheesecake\");\n\n  expect(cart.items).toEqual([\"cheesecake\"]);\n});\n\ntest(\"The addToCart function can add an item to the cart\", () => {\n  const cart = new Cart();\n  cart.addToCart(\"cheesecake\");\n  cart.removeFromCart(\"cheesecake\");\n\n  expect(cart.items).toEqual([]);\n});\n"
  },
  {
    "path": "chapter2/2_unit_tests/5_npm_scripts/package.json",
    "content": "{\n  \"name\": \"5_global_jest\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"devDependencies\": {\n    \"jest\": \"^24.9.0\"\n  }\n}\n"
  },
  {
    "path": "chapter2/3_integration_tests/1_knex_tests_promise/cart.js",
    "content": "const { db } = require(\"./dbConnection\");\n\nconst createCart = username => {\n  return db(\"carts\").insert({ username });\n};\n\nconst addItem = (cartId, itemName) => {\n  return db(\"carts_items\").insert({ cartId, itemName });\n};\n\nmodule.exports = {\n  createCart,\n  addItem\n};\n"
  },
  {
    "path": "chapter2/3_integration_tests/1_knex_tests_promise/cart.test.js",
    "content": "const { db, closeConnection } = require(\"./dbConnection\");\nconst { createCart } = require(\"./cart\");\n\ntest(\"createCart creates a cart for a username\", async () => {\n  await db(\"carts\").truncate();\n  await createCart(\"Lucas da Costa\");\n  const result = await db.select(\"username\").from(\"carts\");\n  expect(result).toEqual([{ username: \"Lucas da Costa\" }]);\n  await closeConnection();\n});\n"
  },
  {
    "path": "chapter2/3_integration_tests/1_knex_tests_promise/dbConnection.js",
    "content": "const db = require(\"knex\")(require(\"./knexfile\").development);\n\nconst closeConnection = () => db.destroy();\n\nmodule.exports = {\n  db,\n  closeConnection\n};\n"
  },
  {
    "path": "chapter2/3_integration_tests/1_knex_tests_promise/knexfile.js",
    "content": "module.exports = {\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"./dev.sqlite\" },\n    useNullAsDefault: true\n  }\n};\n"
  },
  {
    "path": "chapter2/3_integration_tests/1_knex_tests_promise/migrations/20191230210750_create_carts.js",
    "content": "exports.up = async knex => {\n  await knex.schema.createTable(\"carts\", table => {\n    table.increments(\"id\");\n    table.string(\"username\");\n  });\n\n  await knex.schema.createTable(\"carts_items\", table => {\n    table.integer(\"cartId\").references(\"carts.id\");\n    table.string(\"itemName\");\n  });\n};\n\nexports.down = async knex => {\n  await knex.schema.dropTable(\"carts\");\n  await knex.schema.dropTable(\"carts_items\");\n};\n"
  },
  {
    "path": "chapter2/3_integration_tests/1_knex_tests_promise/package.json",
    "content": "{\n  \"name\": \"1_knex_tests_promise\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"dependencies\": {\n    \"knex\": \"^0.20.6\",\n    \"sqlite3\": \"^4.1.1\"\n  },\n  \"devDependencies\": {\n    \"jest\": \"^24.9.0\"\n  }\n}\n"
  },
  {
    "path": "chapter2/3_integration_tests/2_knex_tests_done_cb/cart.js",
    "content": "const { db } = require(\"./dbConnection\");\n\nconst createCart = username => {\n  return db(\"carts\").insert({ username });\n};\n\nconst addItem = (cartId, itemName) => {\n  return db(\"carts_items\").insert({ cartId, itemName });\n};\n\nmodule.exports = {\n  createCart,\n  addItem\n};\n"
  },
  {
    "path": "chapter2/3_integration_tests/2_knex_tests_done_cb/cart.test.js",
    "content": "const { db, closeConnection } = require(\"./dbConnection\");\nconst { createCart } = require(\"./cart\");\n\ntest(\"createCart creates a cart for a username\", done => {\n  db(\"carts\")\n    .truncate()\n    .then(() => createCart(\"Lucas da Costa\"))\n    .then(() => db.select(\"username\").from(\"carts\"))\n    .then(result => {\n      expect(result).toEqual([{ username: \"Lucas da Costa\" }]);\n    })\n    .then(closeConnection)\n    .then(done);\n});\n"
  },
  {
    "path": "chapter2/3_integration_tests/2_knex_tests_done_cb/dbConnection.js",
    "content": "const db = require(\"knex\")(require(\"./knexfile\").development);\n\nconst closeConnection = () => db.destroy();\n\nmodule.exports = {\n  db,\n  closeConnection\n};\n"
  },
  {
    "path": "chapter2/3_integration_tests/2_knex_tests_done_cb/knexfile.js",
    "content": "module.exports = {\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"./dev.sqlite\" },\n    useNullAsDefault: true\n  }\n};\n"
  },
  {
    "path": "chapter2/3_integration_tests/2_knex_tests_done_cb/migrations/20191230210750_create_carts.js",
    "content": "exports.up = async knex => {\n  await knex.schema.createTable(\"carts\", table => {\n    table.increments(\"id\");\n    table.string(\"username\");\n  });\n\n  await knex.schema.createTable(\"carts_items\", table => {\n    table.integer(\"cartId\").references(\"carts.id\");\n    table.string(\"itemName\");\n  });\n};\n\nexports.down = async knex => {\n  await knex.schema.dropTable(\"carts\");\n  await knex.schema.dropTable(\"carts_items\");\n};\n"
  },
  {
    "path": "chapter2/3_integration_tests/2_knex_tests_done_cb/package.json",
    "content": "{\n  \"name\": \"2_knex_tests_done_cb\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"dependencies\": {\n    \"knex\": \"^0.20.6\",\n    \"sqlite3\": \"^4.1.1\"\n  },\n  \"devDependencies\": {\n    \"jest\": \"^24.9.0\"\n  }\n}\n"
  },
  {
    "path": "chapter2/3_integration_tests/3_knex_tests_hooks/cart.js",
    "content": "const { db } = require(\"./dbConnection\");\n\nconst createCart = username => {\n  return db(\"carts\").insert({ username });\n};\n\nconst addItem = (cartId, itemName) => {\n  return db(\"carts_items\").insert({ cartId, itemName });\n};\n\nmodule.exports = {\n  createCart,\n  addItem\n};\n"
  },
  {
    "path": "chapter2/3_integration_tests/3_knex_tests_hooks/cart.test.js",
    "content": "const { db, closeConnection } = require(\"./dbConnection\");\nconst { createCart, addItem } = require(\"./cart\");\n\nbeforeEach(async () => {\n  await db(\"carts_items\").truncate();\n  await db(\"carts\").truncate();\n});\n\nafterAll(async () => await closeConnection());\n\ntest(\"createCart creates a cart for a username\", async () => {\n  await createCart(\"Lucas da Costa\");\n  const result = await db.select(\"username\").from(\"carts\");\n  expect(result).toEqual([{ username: \"Lucas da Costa\" }]);\n});\n\ntest(\"addItem adds an item to the cart\", async () => {\n  const username = \"Lucas da Costa\";\n  await createCart(username);\n  const { id: cartId } = await db\n    .select()\n    .from(\"carts\")\n    .where({ username });\n  await addItem(cartId, \"cheesecake\");\n  const result = await db.select(\"itemName\").from(\"carts_items\");\n  expect(result).toEqual([{ cartId, itemName: \"cheesecake\" }]);\n});\n"
  },
  {
    "path": "chapter2/3_integration_tests/3_knex_tests_hooks/dbConnection.js",
    "content": "const db = require(\"knex\")(require(\"./knexfile\").development);\n\nconst closeConnection = () => db.destroy();\n\nmodule.exports = {\n  db,\n  closeConnection\n};\n"
  },
  {
    "path": "chapter2/3_integration_tests/3_knex_tests_hooks/knexfile.js",
    "content": "module.exports = {\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"./dev.sqlite\" },\n    useNullAsDefault: true\n  }\n};\n"
  },
  {
    "path": "chapter2/3_integration_tests/3_knex_tests_hooks/migrations/20191230210750_create_carts.js",
    "content": "exports.up = async knex => {\n  await knex.schema.createTable(\"carts\", table => {\n    table.increments(\"id\");\n    table.string(\"username\");\n  });\n\n  await knex.schema.createTable(\"carts_items\", table => {\n    table.integer(\"cartId\").references(\"carts.id\");\n    table.string(\"itemName\");\n  });\n};\n\nexports.down = async knex => {\n  await knex.schema.dropTable(\"carts\");\n  await knex.schema.dropTable(\"carts_items\");\n};\n"
  },
  {
    "path": "chapter2/3_integration_tests/3_knex_tests_hooks/package.json",
    "content": "{\n  \"name\": \"1_knex_tests_promise\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"dependencies\": {\n    \"knex\": \"^0.20.6\",\n    \"sqlite3\": \"^4.1.1\"\n  },\n  \"devDependencies\": {\n    \"jest\": \"^24.9.0\"\n  }\n}\n"
  },
  {
    "path": "chapter2/4_end_to_end_tests/1_http_api_tests/package.json",
    "content": "{\n  \"name\": \"1_http_api_tests\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"devDependencies\": {\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"jest\": \"^24.9.0\"\n  },\n  \"dependencies\": {\n    \"koa\": \"^2.11.0\",\n    \"koa-router\": \"^7.4.0\"\n  }\n}\n"
  },
  {
    "path": "chapter2/4_end_to_end_tests/1_http_api_tests/server.js",
    "content": "const Koa = require(\"koa\");\nconst Router = require(\"koa-router\");\n\nconst app = new Koa();\nconst router = new Router();\n\nconst carts = new Map();\n\nrouter.get(\"/carts/:username/items\", ctx => {\n  const cart = carts.get(ctx.params.username);\n  cart ? (ctx.body = cart) : (ctx.status = 404);\n});\n\nrouter.post(\"/carts/:username/items/:item\", ctx => {\n  const { username, item } = ctx.params;\n  const newItems = (carts.get(username) || []).concat(item);\n  carts.set(username, newItems);\n  ctx.body = newItems;\n});\n\napp.use(router.routes());\n\nmodule.exports = app.listen(3000);\n"
  },
  {
    "path": "chapter2/4_end_to_end_tests/1_http_api_tests/server.test.js",
    "content": "const app = require(\"./server\");\nconst fetch = require(\"isomorphic-fetch\");\n\nconst apiRoot = \"http://localhost:3000\";\n\nconst addItem = (username, item) => {\n  return fetch(`${apiRoot}/carts/${username}/items/${item}`, {\n    method: \"POST\"\n  });\n};\n\nconst getItems = username => {\n  return fetch(`${apiRoot}/carts/${username}/items`, { method: \"GET\" });\n};\n\ntest(\"adding items to a cart\", async () => {\n  const initialItemsResponse = await getItems(\"lucas\");\n  expect(initialItemsResponse.status).toBe(404);\n\n  const addItemResponse = await addItem(\"lucas\", \"cheesecake\");\n  expect(await addItemResponse.json()).toEqual([\"cheesecake\"]);\n\n  const finalItemsResponse = await getItems(\"lucas\");\n  expect(await finalItemsResponse.json()).toEqual([\"cheesecake\"]);\n});\n\nafterAll(() => app.close());\n"
  },
  {
    "path": "chapter2/4_end_to_end_tests/2_http_api_with_remove_item/package.json",
    "content": "{\n  \"name\": \"1_http_api_tests\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"devDependencies\": {\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"jest\": \"^26.6.0\"\n  },\n  \"dependencies\": {\n    \"koa\": \"^2.11.0\",\n    \"koa-router\": \"^7.4.0\"\n  }\n}\n"
  },
  {
    "path": "chapter2/4_end_to_end_tests/2_http_api_with_remove_item/server.js",
    "content": "const Koa = require(\"koa\");\nconst Router = require(\"koa-router\");\n\nconst app = new Koa();\nconst router = new Router();\n\nlet carts = new Map();\n\nrouter.get(\"/carts/:username/items\", ctx => {\n  const cart = carts.get(ctx.params.username);\n  cart ? (ctx.body = cart) : (ctx.status = 404);\n});\n\nrouter.post(\"/carts/:username/items/:item\", ctx => {\n  const { username, item } = ctx.params;\n  const newItems = (carts.get(username) || []).concat(item);\n  carts.set(username, newItems);\n  ctx.body = newItems;\n});\n\nrouter.delete(\"/carts/:username/items/:item\", ctx => {\n  const { username, item } = ctx.params;\n  const newItems = (carts.get(username) || []).filter(i => i !== item);\n  carts.set(username, newItems);\n  ctx.body = newItems;\n});\n\napp.use(router.routes());\n\n// This method is designed especifically for testability.\n// Because we keep `carts` in memory, we must reset it back\n// to its inital state by deleting all items in it.\n// If you were dealing with a database, you'd have to do\n// something similar in your tests by ensuring the database\n// is reset to its initial state before each test.\nconst resetState = () => {\n  carts = new Map();\n};\n\nmodule.exports = {\n  app: app.listen(3000),\n  resetState\n};\n"
  },
  {
    "path": "chapter2/4_end_to_end_tests/2_http_api_with_remove_item/server.test.js",
    "content": "const { app, resetState } = require(\"./server\");\nconst fetch = require(\"isomorphic-fetch\");\n\nconst apiRoot = \"http://localhost:3000\";\n\nconst addItem = (username, item) => {\n  return fetch(`${apiRoot}/carts/${username}/items/${item}`, {\n    method: \"POST\"\n  });\n};\n\nconst removeItem = (username, item) => {\n  return fetch(`${apiRoot}/carts/${username}/items/${item}`, {\n    method: \"DELETE\"\n  });\n};\n\nconst getItems = username => {\n  return fetch(`${apiRoot}/carts/${username}/items`, { method: \"GET\" });\n};\n\ntest(\"adding items to a cart\", async () => {\n  const initialItemsResponse = await getItems(\"lucas\");\n  expect(initialItemsResponse.status).toBe(404);\n\n  const addItemResponse = await addItem(\"lucas\", \"cheesecake\");\n  expect(await addItemResponse.json()).toEqual([\"cheesecake\"]);\n\n  const finalItemsResponse = await getItems(\"lucas\");\n  expect(await finalItemsResponse.json()).toEqual([\"cheesecake\"]);\n});\n\ntest(\"removing items from a cart\", async () => {\n  const initialItemsResponse = await getItems(\"lucas\");\n  expect(initialItemsResponse.status).toBe(404);\n\n  await addItem(\"lucas\", \"cheesecake\");\n\n  const removeItemsResponse = await removeItem(\"lucas\", \"cheesecake\");\n  expect(await removeItemsResponse.json()).toEqual([]);\n\n  const finalItemsResponse = await getItems(\"lucas\");\n  expect(await finalItemsResponse.json()).toEqual([]);\n});\n\n// We must clean-up our server's state before each test.\n// If you kept state in a database, you'd need to ensure\n// your database is reset to its initial state.\nbeforeEach(() => resetState());\nafterAll(() => app.close());\n"
  },
  {
    "path": "chapter2/5_tests_cost_and_revenue/1_good_vs_bad/badly_written.test.js",
    "content": "const { app, resetState } = require(\"./server\");\nconst fetch = require(\"isomorphic-fetch\");\n\ntest(\"adding items to a cart\", done => {\n  resetState();\n  return fetch(`http://localhost:3000/carts/lucas/items`, {\n    method: \"GET\"\n  })\n    .then(initialItemsResponse => {\n      expect(initialItemsResponse.status).toEqual(404);\n      return fetch(`http://localhost:3000/carts/lucas/items/cheesecake`, {\n        method: \"POST\"\n      }).then(response => response.json());\n    })\n    .then(addItemResponse => {\n      expect(addItemResponse).toEqual([\"cheesecake\"]);\n      return fetch(`http://localhost:3000/carts/lucas/items`, {\n        method: \"GET\"\n      }).then(response => response.json());\n    })\n    .then(finalItemsResponse => {\n      expect(finalItemsResponse).toEqual([\"cheesecake\"]);\n    })\n    .then(() => {\n      app.close();\n      done();\n    });\n});\n"
  },
  {
    "path": "chapter2/5_tests_cost_and_revenue/1_good_vs_bad/package.json",
    "content": "{\n  \"name\": \"1_good_vs_bad\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"test-good\": \"jest well_written.test.js\",\n    \"test-bad\": \"jest badly_written.test.js\"\n  },\n  \"devDependencies\": {\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"jest\": \"^24.9.0\"\n  },\n  \"dependencies\": {\n    \"koa\": \"^2.11.0\",\n    \"koa-router\": \"^7.4.0\"\n  }\n}\n"
  },
  {
    "path": "chapter2/5_tests_cost_and_revenue/1_good_vs_bad/server.js",
    "content": "const Koa = require(\"koa\");\nconst Router = require(\"koa-router\");\n\nconst app = new Koa();\nconst router = new Router();\n\nlet carts = new Map();\n\nrouter.get(\"/carts/:username/items\", ctx => {\n  const cart = carts.get(ctx.params.username);\n  cart ? (ctx.body = cart) : (ctx.status = 404);\n});\n\nrouter.post(\"/carts/:username/items/:item\", ctx => {\n  const { username, item } = ctx.params;\n  const newItems = (carts.get(username) || []).concat(item);\n  carts.set(username, newItems);\n  ctx.body = newItems;\n});\n\nrouter.delete(\"/carts/:username/items/:item\", ctx => {\n  const { username, item } = ctx.params;\n  const newItems = (carts.get(username) || []).filter(i => i !== item);\n  carts.set(username, newItems);\n  ctx.body = newItems;\n});\n\napp.use(router.routes());\n\n// This method is designed especifically for testability.\n// Because we keep `carts` in memory, we must reset it back\n// to its inital state by deleting all items in it.\n// If you were dealing with a database, you'd have to do\n// something similar in your tests by ensuring the database\n// is reset to its initial state before each test.\nconst resetState = () => {\n  carts = new Map();\n};\n\nmodule.exports = {\n  app: app.listen(3000),\n  resetState\n};\n"
  },
  {
    "path": "chapter2/5_tests_cost_and_revenue/1_good_vs_bad/well_written.test.js",
    "content": "const { app, resetState } = require(\"./server\");\nconst fetch = require(\"isomorphic-fetch\");\n\nconst apiRoot = \"http://localhost:3000\";\n\nconst addItem = (username, item) => {\n  return fetch(`${apiRoot}/carts/${username}/items/${item}`, {\n    method: \"POST\"\n  });\n};\n\nconst getItems = username => {\n  return fetch(`${apiRoot}/carts/${username}/items`, { method: \"GET\" });\n};\n\nbeforeEach(() => resetState());\nafterAll(() => app.close());\n\ntest(\"adding items to a cart\", async () => {\n  const initialItemsResponse = await getItems(\"lucas\");\n  expect(initialItemsResponse.status).toBe(404);\n\n  const addItemResponse = await addItem(\"lucas\", \"cheesecake\");\n  expect(await addItemResponse.json()).toEqual([\"cheesecake\"]);\n\n  const finalItemsResponse = await getItems(\"lucas\");\n  expect(await finalItemsResponse.json()).toEqual([\"cheesecake\"]);\n});\n"
  },
  {
    "path": "chapter2/5_tests_cost_and_revenue/2_test_coupling/package.json",
    "content": "{\n  \"name\": \"2_test_coupling\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"jest\": \"^24.9.0\"\n  }\n}\n"
  },
  {
    "path": "chapter2/5_tests_cost_and_revenue/2_test_coupling/pow.test.js",
    "content": "//const pow = require(\"./pow_recursive\");\nconst pow = require(\"./pow_loop\");\n\ntest(\"calculates powers\", () => {\n  expect(pow(2, 0)).toBe(1);\n  expect(pow(2, -3)).toBe(0.125);\n  expect(pow(2, 2)).toBe(4);\n  expect(pow(2, 5)).toBe(32);\n  expect(pow(0, 5)).toBe(0);\n  expect(pow(1, 4)).toBe(1);\n});\n"
  },
  {
    "path": "chapter2/5_tests_cost_and_revenue/2_test_coupling/pow_loop.js",
    "content": "const pow = (a, b) => {\n  let result = 1;\n  for (let i = 0; i < Math.abs(b); i++) {\n    if (b < 0) result = result / a;\n    if (b > 0) result = result * a;\n  }\n\n  return result;\n};\n\nmodule.exports = pow;\n"
  },
  {
    "path": "chapter2/5_tests_cost_and_revenue/2_test_coupling/pow_recursive.js",
    "content": "const pow = (a, b, acc = 1) => {\n  if (b === 0) return acc;\n  const nextB = b < 0 ? b + 1 : b - 1;\n  const nextAcc = b < 0 ? acc / a : acc * a;\n  return pow(a, nextB, nextAcc);\n};\n\nmodule.exports = pow;\n"
  },
  {
    "path": "chapter3/1_organising_test_suites/1_breaking_down_tests_big_tests/package.json",
    "content": "{\n  \"name\": \"1_breaking_down_tests_big_tests\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"devDependencies\": {\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"jest\": \"^24.9.0\"\n  },\n  \"dependencies\": {\n    \"koa\": \"^2.11.0\",\n    \"koa-router\": \"^7.4.0\"\n  }\n}\n"
  },
  {
    "path": "chapter3/1_organising_test_suites/1_breaking_down_tests_big_tests/server.js",
    "content": "const Koa = require(\"koa\");\nconst Router = require(\"koa-router\");\n\nconst app = new Koa();\nconst router = new Router();\n\nconst carts = new Map();\nconst inventory = new Map();\n\nrouter.post(\"/carts/:username/items/:item\", ctx => {\n  const { username, item } = ctx.params;\n  if (!inventory.get(item)) {\n    ctx.status = 404;\n    return;\n  }\n\n  inventory.set(item, inventory.get(item) - 1);\n  const newItems = (carts.get(username) || []).concat(item);\n  carts.set(username, newItems);\n  ctx.body = newItems;\n});\n\napp.use(router.routes());\n\nmodule.exports = {\n  app: app.listen(3000),\n  inventory,\n  carts\n};\n"
  },
  {
    "path": "chapter3/1_organising_test_suites/1_breaking_down_tests_big_tests/server.test.js",
    "content": "const { app, inventory, carts } = require(\"./server\");\nconst fetch = require(\"isomorphic-fetch\");\n\nconst apiRoot = \"http://localhost:3000\";\n\nconst addItem = (username, item) => {\n  return fetch(`${apiRoot}/carts/${username}/items/${item}`, {\n    method: \"POST\"\n  });\n};\n\ndescribe(\"addItem\", () => {\n  test(\"adding items to a cart\", async () => {\n    inventory.set(\"cheesecake\", 1);\n    const addItemResponse = await addItem(\"lucas\", \"cheesecake\");\n    expect(await addItemResponse.json()).toEqual([\"cheesecake\"]);\n    expect(inventory.get(\"cheesecake\")).toBe(0);\n\n    expect(carts.get(\"lucas\")).toEqual([\"cheesecake\"]);\n\n    const failedAddItem = await addItem(\"lucas\", \"cheesecake\");\n    expect(failedAddItem.status).toBe(404);\n  });\n});\n\nafterAll(() => app.close());\n"
  },
  {
    "path": "chapter3/1_organising_test_suites/2_breaking_down_tests_small_tests/package.json",
    "content": "{\n  \"name\": \"2_breaking_down_tests_small_tests\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"devDependencies\": {\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"jest\": \"^24.9.0\"\n  },\n  \"dependencies\": {\n    \"koa\": \"^2.11.0\",\n    \"koa-router\": \"^7.4.0\"\n  }\n}\n"
  },
  {
    "path": "chapter3/1_organising_test_suites/2_breaking_down_tests_small_tests/server.js",
    "content": "const Koa = require(\"koa\");\nconst Router = require(\"koa-router\");\n\nconst app = new Koa();\nconst router = new Router();\n\nconst carts = new Map();\nconst inventory = new Map();\n\nrouter.post(\"/carts/:username/items/:item\", ctx => {\n  const { username, item } = ctx.params;\n  if (!inventory.get(item)) {\n    ctx.status = 404;\n    return;\n  }\n\n  inventory.set(item, inventory.get(item) - 1);\n  const newItems = (carts.get(username) || []).concat(item);\n  carts.set(username, newItems);\n  ctx.body = newItems;\n});\n\napp.use(router.routes());\n\nmodule.exports = {\n  app: app.listen(3000),\n  inventory,\n  carts\n};\n"
  },
  {
    "path": "chapter3/1_organising_test_suites/2_breaking_down_tests_small_tests/server.test.js",
    "content": "const { app, inventory, carts } = require(\"./server\");\nconst fetch = require(\"isomorphic-fetch\");\n\nconst apiRoot = \"http://localhost:3000\";\n\nconst addItem = (username, item) => {\n  return fetch(`${apiRoot}/carts/${username}/items/${item}`, {\n    method: \"POST\"\n  });\n};\n\ndescribe(\"addItem\", () => {\n  beforeEach(() => carts.forEach((value, key) => carts.delete(key)));\n  beforeEach(() => inventory.set(\"cheesecake\", 1));\n\n  test(\"correct response\", async () => {\n    const addItemResponse = await addItem(\"lucas\", \"cheesecake\");\n    expect(await addItemResponse.json()).toEqual([\"cheesecake\"]);\n  });\n\n  test(\"inventory update\", async () => {\n    await addItem(\"lucas\", \"cheesecake\");\n    expect(inventory.get(\"cheesecake\")).toBe(0);\n  });\n\n  test(\"cart update\", async () => {\n    await addItem(\"keith\", \"cheesecake\");\n    expect(carts.get(\"keith\")).toEqual([\"cheesecake\"]);\n  });\n\n  test(\"soldout items\", async () => {\n    inventory.set(\"cheesecake\", 0);\n    const failedAddItem = await addItem(\"lucas\", \"cheesecake\");\n    expect(failedAddItem.status).toBe(404);\n  });\n});\n\nafterAll(() => app.close());\n"
  },
  {
    "path": "chapter3/1_organising_test_suites/3_global_hooks/dummy.test.js",
    "content": "describe(\"placeholder tests\", () => {\n  test(\"placeholder 1\", () => {});\n  test(\"placeholder 2\", () => {});\n  test(\"placeholder 3\", () => {});\n});\n"
  },
  {
    "path": "chapter3/1_organising_test_suites/3_global_hooks/globalSetup.js",
    "content": "const setup = () => {\n  global._accessibleOnTeardown = \"Look, I was set on the setup file\";\n  console.log(\"\\nsetup executed\\n\");\n};\n\nmodule.exports = setup;\n"
  },
  {
    "path": "chapter3/1_organising_test_suites/3_global_hooks/globalTeardown.js",
    "content": "const teardown = () => {\n  console.log(`The value set on setup was: ${global._accessibleOnTeardown}`);\n  console.log(\"teardown executed\");\n};\n\nmodule.exports = teardown;\n"
  },
  {
    "path": "chapter3/1_organising_test_suites/3_global_hooks/jest.config.js",
    "content": "module.exports = {\n  globalSetup: \"./globalSetup.js\",\n  globalTeardown: \"./globalTeardown.js\",\n  testEnvironment: \"node\"\n};\n"
  },
  {
    "path": "chapter3/1_organising_test_suites/3_global_hooks/package.json",
    "content": "{\n  \"name\": \"1_organising_test_suites\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"jest\": \"^25.1.0\"\n  }\n}\n"
  },
  {
    "path": "chapter3/2_writing_good_assertions/1_assertion_checks/inventoryController.js",
    "content": "const inventory = new Map();\n\nconst addToInventory = (item, n) => {\n  if (typeof n !== \"number\") throw new Error(\"quantity must be a number\");\n  const currentQuantity = inventory.get(item) || 0;\n  const newQuantity = currentQuantity + n;\n  inventory.set(item, newQuantity);\n  return newQuantity;\n};\n\nmodule.exports = { inventory, addToInventory };\n"
  },
  {
    "path": "chapter3/2_writing_good_assertions/1_assertion_checks/inventoryController.test.js",
    "content": "const { inventory, addToInventory } = require(\"./inventoryController\");\n\nbeforeEach(() => inventory.set(\"cheesecake\", 0));\n\ntest(\"cancels operation for invalid quantities\", () => {\n  expect.assertions(2);\n\n  try {\n    addToInventory(\"cheesecake\", \"not a number\");\n  } catch (e) {\n    expect(inventory.get(\"cheesecake\")).toBe(0);\n  }\n\n  expect(Array.from(inventory.entries())).toHaveLength(1);\n});\n"
  },
  {
    "path": "chapter3/2_writing_good_assertions/1_assertion_checks/package.json",
    "content": "{\n  \"name\": \"1_assertion_checks\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"jest\": \"^25.1.0\"\n  }\n}\n"
  },
  {
    "path": "chapter3/2_writing_good_assertions/2_assertion_checks_toThrow/inventoryController.js",
    "content": "const inventory = new Map();\n\nconst addToInventory = (item, n) => {\n  if (typeof n !== \"number\") throw new Error(\"quantity must be a number\");\n  const currentQuantity = inventory.get(item) || 0;\n  const newQuantity = currentQuantity + n;\n  inventory.set(item, newQuantity);\n  return newQuantity;\n};\n\nmodule.exports = { inventory, addToInventory };\n"
  },
  {
    "path": "chapter3/2_writing_good_assertions/2_assertion_checks_toThrow/inventoryController.test.js",
    "content": "const { inventory, addToInventory } = require(\"./inventoryController\");\n\nbeforeEach(() => inventory.set(\"cheesecake\", 0));\n\ntest(\"cancels operation for invalid quantities\", () => {\n  expect(() => addToInventory(\"cheesecake\", \"not a number\")).not.toThrow();\n  expect(inventory.get(\"cheesecake\")).toBe(0);\n  expect(Array.from(inventory.entries())).toHaveLength(1);\n});\n"
  },
  {
    "path": "chapter3/2_writing_good_assertions/2_assertion_checks_toThrow/package.json",
    "content": "{\n  \"name\": \"1_assertion_checks\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"jest\": \"^25.1.0\"\n  }\n}\n"
  },
  {
    "path": "chapter3/2_writing_good_assertions/3_loose_assertions/inventoryController.js",
    "content": "const inventory = new Map();\n\nconst addToInventory = (item, n) => {\n  if (typeof n !== \"number\") throw new Error(\"quantity must be a number\");\n  const currentQuantity = inventory.get(item) || 0;\n  const newQuantity = currentQuantity + n;\n  inventory.set(item, newQuantity);\n  return newQuantity;\n};\n\nmodule.exports = { inventory, addToInventory };\n"
  },
  {
    "path": "chapter3/2_writing_good_assertions/3_loose_assertions/inventoryController.test.js",
    "content": "const { inventory, addToInventory } = require(\"./inventoryController\");\n\nbeforeEach(() => {\n  inventory.forEach((value, key) => inventory.delete(key));\n});\n\ntest(\"returned value\", () => {\n  const result = addToInventory(\"cheesecake\", 2);\n  expect(typeof result).toBe(\"number\");\n});\n"
  },
  {
    "path": "chapter3/2_writing_good_assertions/3_loose_assertions/package.json",
    "content": "{\n  \"name\": \"2_loose_assertions\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"jest\": \"^25.1.0\"\n  }\n}\n"
  },
  {
    "path": "chapter3/2_writing_good_assertions/4_asymmetric_matchers/inventoryController.js",
    "content": "const inventory = new Map();\n\nconst addToInventory = (item, n) => {\n  if (typeof n !== \"number\") throw new Error(\"quantity must be a number\");\n  const currentQuantity = inventory.get(item) || 0;\n  const newQuantity = currentQuantity + n;\n  inventory.set(item, newQuantity);\n  return newQuantity;\n};\n\nconst getInventory = () => {\n  const contentArray = Array.from(inventory.entries());\n  const contents = contentArray.reduce((contents, [name, quantity]) => {\n    return { ...contents, [name]: quantity };\n  }, {});\n\n  return { ...contents, generatedAt: new Date() };\n};\n\nmodule.exports = { inventory, addToInventory, getInventory };\n"
  },
  {
    "path": "chapter3/2_writing_good_assertions/4_asymmetric_matchers/inventoryController.test.js",
    "content": "const { inventory, getInventory } = require(\"./inventoryController\");\n\ntest(\"inventory contents\", () => {\n  inventory\n    .set(\"cheesecake\", 1)\n    .set(\"macarroon\", 3)\n    .set(\"croissant\", 3)\n    .set(\"eclaire\", 7);\n  const result = getInventory();\n\n  expect(result).toEqual({\n    cheesecake: 1,\n    macarroon: 3,\n    croissant: 3,\n    eclaire: 7,\n    generatedAt: expect.any(Date)\n  });\n});\n"
  },
  {
    "path": "chapter3/2_writing_good_assertions/4_asymmetric_matchers/package.json",
    "content": "{\n  \"name\": \"3_asymmetric_matchers\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"jest\": \"^25.1.0\"\n  }\n}\n"
  },
  {
    "path": "chapter3/2_writing_good_assertions/5_manual_assertions/inventoryController.js",
    "content": "const inventory = new Map();\n\nconst addToInventory = (item, n) => {\n  if (typeof n !== \"number\") throw new Error(\"quantity must be a number\");\n  const currentQuantity = inventory.get(item) || 0;\n  const newQuantity = currentQuantity + n;\n  inventory.set(item, newQuantity);\n  return newQuantity;\n};\n\nconst getInventory = () => {\n  const contentArray = Array.from(inventory.entries());\n  const contents = contentArray.reduce((contents, [name, quantity]) => {\n    return { ...contents, [name]: quantity };\n  }, {});\n\n  // To make the tests in this folder pass, update this\n  // line so that it doesn't set the new Date's year to 3000.\n  return { ...contents, generatedAt: new Date(new Date().setYear(3000)) };\n};\n\nmodule.exports = { inventory, addToInventory, getInventory };\n"
  },
  {
    "path": "chapter3/2_writing_good_assertions/5_manual_assertions/inventoryController.test.js",
    "content": "const { getInventory } = require(\"./inventoryController\");\n\n// This test _will_ fail.\n// Here I'm trying to demonstrate Jest's output\n// when an assertion like `.toBe(true)` fails.\ntest(\"generatedAt in the past\", () => {\n  const result = getInventory();\n  const currentTime = Date.now() + 1;\n  const isPastTimestamp = result.generatedAt.getTime() <= currentTime;\n  expect(isPastTimestamp).toBe(true);\n});\n"
  },
  {
    "path": "chapter3/2_writing_good_assertions/5_manual_assertions/package.json",
    "content": "{\n  \"name\": \"4_manual_assertions\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"jest\": \"^25.1.0\"\n  }\n}\n"
  },
  {
    "path": "chapter3/2_writing_good_assertions/6_custom_matchers/inventoryController.js",
    "content": "const inventory = new Map();\n\nconst addToInventory = (item, n) => {\n  if (typeof n !== \"number\") throw new Error(\"quantity must be a number\");\n  const currentQuantity = inventory.get(item) || 0;\n  const newQuantity = currentQuantity + n;\n  inventory.set(item, newQuantity);\n  return newQuantity;\n};\n\nconst getInventory = () => {\n  const contentArray = Array.from(inventory.entries());\n  const contents = contentArray.reduce((contents, [name, quantity]) => {\n    return { ...contents, [name]: quantity };\n  }, {});\n\n  return { ...contents, generatedAt: new Date(new Date().setYear(3000)) };\n};\n\nmodule.exports = { inventory, addToInventory, getInventory };\n"
  },
  {
    "path": "chapter3/2_writing_good_assertions/6_custom_matchers/inventoryController.test.js",
    "content": "const { getInventory } = require(\"./inventoryController\");\n\ntest(\"generatedAt in the past\", () => {\n  const result = getInventory();\n  const currentTime = new Date(Date.now() + 1);\n  expect(result.generatedAt).toBeBefore(currentTime);\n});\n"
  },
  {
    "path": "chapter3/2_writing_good_assertions/6_custom_matchers/jest.config.js",
    "content": "module.exports = {\n  testEnvironment: \"node\",\n  setupFilesAfterEnv: [\"jest-extended\"]\n};\n"
  },
  {
    "path": "chapter3/2_writing_good_assertions/6_custom_matchers/package.json",
    "content": "{\n  \"name\": \"5_custom_matchers\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"jest.config.js\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"jest\": \"^25.1.0\",\n    \"jest-extended\": \"^0.11.5\"\n  }\n}\n"
  },
  {
    "path": "chapter3/2_writing_good_assertions/7_circular_assertions/inventoryController.js",
    "content": "const inventory = new Map();\n\nconst getInventory = () => {\n  const contentArray = Array.from(inventory.entries());\n  const contents = contentArray.reduce((contents, [name]) => {\n    return { ...contents, [name]: 1000 };\n  }, {});\n\n  return { ...contents, generatedAt: new Date() };\n};\n\nmodule.exports = { inventory, getInventory };\n"
  },
  {
    "path": "chapter3/2_writing_good_assertions/7_circular_assertions/package.json",
    "content": "{\n  \"name\": \"6_circular_assertions\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"jest\": \"^25.1.0\"\n  },\n  \"dependencies\": {\n    \"koa\": \"^2.11.0\",\n    \"koa-router\": \"^8.0.8\"\n  }\n}\n"
  },
  {
    "path": "chapter3/2_writing_good_assertions/7_circular_assertions/server.js",
    "content": "const Koa = require(\"koa\");\nconst Router = require(\"koa-router\");\nconst { getInventory } = require(\"./inventoryController\");\n\nconst app = new Koa();\nconst router = new Router();\n\nrouter.get(\"/inventory\", ctx => (ctx.body = getInventory()));\n\napp.use(router.routes());\n\nmodule.exports = app.listen(3000);\n"
  },
  {
    "path": "chapter3/2_writing_good_assertions/7_circular_assertions/server.test.js",
    "content": "const app = require(\"./server\");\nconst fetch = require(\"isomorphic-fetch\");\nconst { inventory, getInventory } = require(\"./inventoryController\");\n\nconst apiRoot = \"http://localhost:3000\";\n\nconst sendGetInventoryRequest = () => {\n  return fetch(`${apiRoot}/inventory`, { method: \"GET\" });\n};\n\ntest(\"fetching inventory\", async () => {\n  inventory.set(\"cheesecake\", 1).set(\"macarroon\", 2);\n  const getInventoryResponse = await sendGetInventoryRequest(\"lucas\");\n  const expected = { ...getInventory(), generatedAt: expect.anything() };\n\n  expect(await getInventoryResponse.json()).toEqual(expected);\n});\n\nafterAll(() => app.close());\n"
  },
  {
    "path": "chapter3/3_mocks_stubs_and_spies/1_mocking_objects/inventoryController.js",
    "content": "const logger = require(\"./logger\");\n\nconst inventory = new Map();\n\nconst addToInventory = (item, quantity) => {\n  if (typeof quantity !== \"number\") {\n    logger.logError(\n      { quantity },\n      \"could not add item to inventory because quantity was not a number\"\n    );\n    throw new Error(\"quantity must be a number\");\n  }\n  const currentQuantity = inventory.get(item) || 0;\n  const newQuantity = currentQuantity + quantity;\n  inventory.set(item, newQuantity);\n  logger.logInfo(\n    { item, quantity, memoryUsage: process.memoryUsage().rss },\n    \"item added to the inventory\"\n  );\n  return newQuantity;\n};\n\nconst getInventory = () => {\n  const contentArray = Array.from(inventory.entries());\n  const contents = contentArray.reduce((contents, [name, quantity]) => {\n    return { ...contents, [name]: quantity };\n  }, {});\n\n  logger.logInfo({ contents }, \"inventory items fetched\");\n  return { ...contents, generatedAt: new Date(new Date().setYear(3000)) };\n};\n\nmodule.exports = { inventory, addToInventory, getInventory };\n"
  },
  {
    "path": "chapter3/3_mocks_stubs_and_spies/1_mocking_objects/inventoryController.test.js",
    "content": "const logger = require(\"./logger\");\nconst {\n  inventory,\n  addToInventory,\n  getInventory\n} = require(\"./inventoryController\");\n\n// Clearing the inventory before each test\nbeforeEach(() => {\n  inventory.forEach((value, key) => inventory.delete(key));\n});\n\nbeforeAll(() => jest.spyOn(logger, \"logInfo\").mockImplementation(jest.fn()));\nbeforeAll(() => jest.spyOn(logger, \"logError\").mockImplementation(jest.fn()));\n\nafterEach(() => jest.resetAllMocks());\n\ndescribe(\"addToInventory\", () => {\n  beforeEach(() => {\n    jest\n      .spyOn(process, \"memoryUsage\")\n      .mockReturnValue({ rss: 123456, heapTotal: 1, heapUsed: 2, external: 3 });\n  });\n\n  test(\"logging new items\", () => {\n    addToInventory(\"cheesecake\", 2);\n\n    expect(logger.logInfo.mock.calls).toHaveLength(1);\n\n    const firstCallArgs = logger.logInfo.mock.calls[0];\n    const [firstArg, secondArg] = firstCallArgs;\n\n    expect(firstArg).toEqual({\n      item: \"cheesecake\",\n      quantity: 2,\n      memoryUsage: 123456\n    });\n    expect(secondArg).toEqual(\"item added to the inventory\");\n  });\n\n  test(\"logging logErrors\", () => {\n    try {\n      addToInventory(\"cheesecake\", \"not a number\");\n    } catch (e) {\n      // No-op\n    }\n\n    expect(logger.logError.mock.calls).toHaveLength(1);\n\n    const firstCallArgs = logger.logError.mock.calls[0];\n    const [firstArg, secondArg] = firstCallArgs;\n\n    expect(firstArg).toEqual({ quantity: \"not a number\" });\n    expect(secondArg).toEqual(\n      \"could not add item to inventory because quantity was not a number\"\n    );\n  });\n});\n\ndescribe(\"getInventory\", () => {\n  test(\"logging fetches\", () => {\n    inventory.set(\"cheesecake\", 2);\n    getInventory(\"cheesecake\", 2);\n\n    expect(logger.logInfo.mock.calls).toHaveLength(1);\n\n    const firstCallArgs = logger.logInfo.mock.calls[0];\n    const [firstArg, secondArg] = firstCallArgs;\n\n    expect(firstArg).toEqual({ contents: { cheesecake: 2 } });\n    expect(secondArg).toEqual(\"inventory items fetched\");\n  });\n});\n"
  },
  {
    "path": "chapter3/3_mocks_stubs_and_spies/1_mocking_objects/logger.js",
    "content": "const pino = require(\"pino\");\n\nconst pinoInstance = pino();\n\nconst logger = {\n  logInfo: pinoInstance.info.bind(pinoInstance),\n  logError: pinoInstance.error.bind(pinoInstance)\n};\n\nmodule.exports = logger;\n"
  },
  {
    "path": "chapter3/3_mocks_stubs_and_spies/1_mocking_objects/package.json",
    "content": "{\n  \"name\": \"1_mocking_objects\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"inventoryController.js\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"jest\": \"^25.1.0\"\n  },\n  \"dependencies\": {\n    \"pino\": \"^5.16.0\"\n  }\n}\n"
  },
  {
    "path": "chapter3/3_mocks_stubs_and_spies/2_mocking_imports/inventoryController.js",
    "content": "const { logInfo, logError } = require(\"./logger\");\n\nconst inventory = new Map();\n\nconst addToInventory = (item, quantity) => {\n  if (typeof quantity !== \"number\") {\n    logError(\n      { quantity },\n      \"could not add item to inventory because quantity was not a number\"\n    );\n    throw new Error(\"quantity must be a number\");\n  }\n  const currentQuantity = inventory.get(item) || 0;\n  const newQuantity = currentQuantity + quantity;\n  inventory.set(item, newQuantity);\n  logInfo(\n    { item, quantity, memoryUsage: process.memoryUsage().rss },\n    \"item added to the inventory\"\n  );\n  return newQuantity;\n};\n\nconst getInventory = () => {\n  const contentArray = Array.from(inventory.entries());\n  const contents = contentArray.reduce((contents, [name, quantity]) => {\n    return { ...contents, [name]: quantity };\n  }, {});\n\n  logInfo({ contents }, \"inventory items fetched\");\n  return { ...contents, generatedAt: new Date(new Date().setYear(3000)) };\n};\n\nmodule.exports = { inventory, addToInventory, getInventory };\n"
  },
  {
    "path": "chapter3/3_mocks_stubs_and_spies/2_mocking_imports/inventoryController.test.js",
    "content": "const logger = require(\"./logger\");\nconst {\n  inventory,\n  addToInventory,\n  getInventory\n} = require(\"./inventoryController\");\n\njest.mock(\"./logger\", () => ({\n  logInfo: jest.fn(),\n  logError: jest.fn()\n}));\n\n// Clearing the inventory before each test\nbeforeEach(() => {\n  inventory.forEach((value, key) => inventory.delete(key));\n});\n\nafterEach(() => jest.resetAllMocks());\n\ndescribe(\"addToInventory\", () => {\n  beforeEach(() => {\n    jest\n      .spyOn(process, \"memoryUsage\")\n      .mockReturnValue({ rss: 123456, heapTotal: 1, heapUsed: 2, external: 3 });\n  });\n\n  test(\"logging new items\", () => {\n    addToInventory(\"cheesecake\", 2);\n\n    expect(logger.logInfo.mock.calls).toHaveLength(1);\n\n    const firstCallArgs = logger.logInfo.mock.calls[0];\n    const [firstArg, secondArg] = firstCallArgs;\n\n    expect(firstArg).toEqual({\n      item: \"cheesecake\",\n      quantity: 2,\n      memoryUsage: 123456\n    });\n    expect(secondArg).toEqual(\"item added to the inventory\");\n  });\n\n  test(\"logging logErrors\", () => {\n    try {\n      addToInventory(\"cheesecake\", \"not a number\");\n    } catch (e) {\n      // No-op\n    }\n\n    expect(logger.logError.mock.calls).toHaveLength(1);\n\n    const firstCallArgs = logger.logError.mock.calls[0];\n    const [firstArg, secondArg] = firstCallArgs;\n\n    expect(firstArg).toEqual({ quantity: \"not a number\" });\n    expect(secondArg).toEqual(\n      \"could not add item to inventory because quantity was not a number\"\n    );\n  });\n});\n\ndescribe(\"getInventory\", () => {\n  test(\"logging fetches\", () => {\n    inventory.set(\"cheesecake\", 2);\n    getInventory(\"cheesecake\", 2);\n\n    expect(logger.logInfo.mock.calls).toHaveLength(1);\n\n    const firstCallArgs = logger.logInfo.mock.calls[0];\n    const [firstArg, secondArg] = firstCallArgs;\n\n    expect(firstArg).toEqual({ contents: { cheesecake: 2 } });\n    expect(secondArg).toEqual(\"inventory items fetched\");\n  });\n});\n"
  },
  {
    "path": "chapter3/3_mocks_stubs_and_spies/2_mocking_imports/logger.js",
    "content": "const pino = require(\"pino\");\n\nconst pinoInstance = pino();\n\nconst logger = {\n  logInfo: pinoInstance.info.bind(pinoInstance),\n  logError: pinoInstance.error.bind(pinoInstance)\n};\n\nmodule.exports = logger;\n"
  },
  {
    "path": "chapter3/3_mocks_stubs_and_spies/2_mocking_imports/package.json",
    "content": "{\n  \"name\": \"2_mocking_imports\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"inventoryController.js\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"jest\": \"^25.1.0\"\n  },\n  \"dependencies\": {\n    \"pino\": \"^5.16.0\"\n  }\n}\n"
  },
  {
    "path": "chapter3/3_mocks_stubs_and_spies/3_manual_mocks/__mocks__/logger.js",
    "content": "module.exports = {\n  logInfo: jest.fn(),\n  logError: jest.fn()\n};\n"
  },
  {
    "path": "chapter3/3_mocks_stubs_and_spies/3_manual_mocks/inventoryController.js",
    "content": "const { logInfo, logError } = require(\"./logger\");\n\nconst inventory = new Map();\n\nconst addToInventory = (item, quantity) => {\n  if (typeof quantity !== \"number\") {\n    logError(\n      { quantity },\n      \"could not add item to inventory because quantity was not a number\"\n    );\n    throw new Error(\"quantity must be a number\");\n  }\n  const currentQuantity = inventory.get(item) || 0;\n  const newQuantity = currentQuantity + quantity;\n  inventory.set(item, newQuantity);\n  logInfo(\n    { item, quantity, memoryUsage: process.memoryUsage().rss },\n    \"item added to the inventory\"\n  );\n  return newQuantity;\n};\n\nconst getInventory = () => {\n  const contentArray = Array.from(inventory.entries());\n  const contents = contentArray.reduce((contents, [name, quantity]) => {\n    return { ...contents, [name]: quantity };\n  }, {});\n\n  logInfo({ contents }, \"inventory items fetched\");\n  return { ...contents, generatedAt: new Date(new Date().setYear(3000)) };\n};\n\nmodule.exports = { inventory, addToInventory, getInventory };\n"
  },
  {
    "path": "chapter3/3_mocks_stubs_and_spies/3_manual_mocks/inventoryController.test.js",
    "content": "const logger = require(\"./logger\");\nconst {\n  inventory,\n  addToInventory,\n  getInventory\n} = require(\"./inventoryController\");\n\n// Clearing the inventory before each test\nbeforeEach(() => {\n  inventory.forEach((value, key) => inventory.delete(key));\n});\n\nafterEach(() => jest.resetAllMocks());\n\njest.mock(\"./logger\");\n\ndescribe(\"addToInventory\", () => {\n  beforeEach(() => {\n    jest\n      .spyOn(process, \"memoryUsage\")\n      .mockReturnValue({ rss: 123456, heapTotal: 1, heapUsed: 2, external: 3 });\n  });\n\n  test(\"logging new items\", () => {\n    addToInventory(\"cheesecake\", 2);\n\n    expect(logger.logInfo.mock.calls).toHaveLength(1);\n\n    const firstCallArgs = logger.logInfo.mock.calls[0];\n    const [firstArg, secondArg] = firstCallArgs;\n\n    expect(firstArg).toEqual({\n      item: \"cheesecake\",\n      quantity: 2,\n      memoryUsage: 123456\n    });\n    expect(secondArg).toEqual(\"item added to the inventory\");\n  });\n\n  test(\"logging logErrors\", () => {\n    try {\n      addToInventory(\"cheesecake\", \"not a number\");\n    } catch (e) {\n      // No-op\n    }\n\n    expect(logger.logError.mock.calls).toHaveLength(1);\n\n    const firstCallArgs = logger.logError.mock.calls[0];\n    const [firstArg, secondArg] = firstCallArgs;\n\n    expect(firstArg).toEqual({ quantity: \"not a number\" });\n    expect(secondArg).toEqual(\n      \"could not add item to inventory because quantity was not a number\"\n    );\n  });\n});\n\ndescribe(\"getInventory\", () => {\n  test(\"logging fetches\", () => {\n    inventory.set(\"cheesecake\", 2);\n    getInventory(\"cheesecake\", 2);\n\n    expect(logger.logInfo.mock.calls).toHaveLength(1);\n\n    const firstCallArgs = logger.logInfo.mock.calls[0];\n    const [firstArg, secondArg] = firstCallArgs;\n\n    expect(firstArg).toEqual({ contents: { cheesecake: 2 } });\n    expect(secondArg).toEqual(\"inventory items fetched\");\n  });\n});\n"
  },
  {
    "path": "chapter3/3_mocks_stubs_and_spies/3_manual_mocks/logger.js",
    "content": "const pino = require(\"pino\");\n\nconst pinoInstance = pino();\n\nconst logger = {\n  logInfo: pinoInstance.info.bind(pinoInstance),\n  logError: pinoInstance.error.bind(pinoInstance)\n};\n\nmodule.exports = logger;\n"
  },
  {
    "path": "chapter3/3_mocks_stubs_and_spies/3_manual_mocks/package.json",
    "content": "{\n  \"name\": \"3_manual_mocks\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"inventoryController.js\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"jest\": \"^26.6.0\"\n  },\n  \"dependencies\": {\n    \"pino\": \"^5.16.0\"\n  }\n}\n"
  },
  {
    "path": "chapter3/4_code_coverage/1_measuring_code_coverage/__mocks__/logger.js",
    "content": "module.exports = {\n  logInfo: jest.fn(),\n  logError: jest.fn()\n};\n"
  },
  {
    "path": "chapter3/4_code_coverage/1_measuring_code_coverage/inventoryController.js",
    "content": "const { logInfo, logError } = require(\"./logger\");\n\nconst inventory = new Map();\n\nconst addToInventory = (item, quantity) => {\n  if (typeof quantity !== \"number\") {\n    logError(\n      { quantity },\n      \"could not add item to inventory because quantity was not a number\"\n    );\n    throw new Error(\"quantity must be a number\");\n  }\n  const currentQuantity = inventory.get(item) || 0;\n  const newQuantity = currentQuantity + quantity;\n  inventory.set(item, newQuantity);\n  logInfo(\n    { item, quantity, memoryUsage: process.memoryUsage().rss },\n    \"item added to the inventory\"\n  );\n  return newQuantity;\n};\n\nmodule.exports = { inventory, addToInventory };\n"
  },
  {
    "path": "chapter3/4_code_coverage/1_measuring_code_coverage/inventoryController.test.js",
    "content": "const { inventory, addToInventory } = require(\"./inventoryController\");\n\njest.mock(\"./logger\");\n\nbeforeEach(() => inventory.clear());\n\ndescribe(\"addToInventory\", () => {\n  test(\"passing valid arguments\", () => {\n    addToInventory(\"cheesecake\", 2);\n  });\n\n  test(\"passing invalid arguments\", () => {\n    try {\n      addToInventory(\"cheesecake\", \"should throw\");\n    } catch (e) {\n      // ...\n    }\n  });\n});\n"
  },
  {
    "path": "chapter3/4_code_coverage/1_measuring_code_coverage/logger.js",
    "content": "const pino = require(\"pino\");\n\nconst pinoInstance = pino();\n\nconst logger = {\n  logInfo: pinoInstance.info.bind(pinoInstance),\n  logError: pinoInstance.error.bind(pinoInstance)\n};\n\nmodule.exports = logger;\n"
  },
  {
    "path": "chapter3/4_code_coverage/1_measuring_code_coverage/package.json",
    "content": "{\n  \"name\": \"1_measuring_code_coverage\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"inventoryController.js\",\n  \"scripts\": {\n    \"test\": \"jest\",\n    \"coverage\": \"jest --coverage\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"jest\": \"^25.1.0\"\n  },\n  \"dependencies\": {\n    \"pino\": \"^5.16.0\"\n  }\n}\n"
  },
  {
    "path": "chapter3/4_code_coverage/2_what_coverage_is_good_for/math.js",
    "content": "function sumOrDivide(a, b) {\n  if (a % 2 === 0 && b % 2 === 0) {\n    return a + b;\n  } else {\n    return a / b;\n  }\n}\n"
  },
  {
    "path": "chapter3/4_code_coverage/2_what_coverage_is_good_for/math.test.js",
    "content": "test(\"sum\", () => {\n  sumOrDivide(2, 4);\n  // WARNING: No assertions!\n});\n\ntest(\"multiply\", () => {\n  sumOrDivide(2, 6);\n  // WARNING: No assertions!\n});\n"
  },
  {
    "path": "chapter3/4_code_coverage/2_what_coverage_is_good_for/package.json",
    "content": "{\n  \"name\": \"2_what_coverage_is_good_for\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"inventoryController.js\",\n  \"scripts\": {\n    \"test\": \"jest\",\n    \"coverage\": \"jest --coverage\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"jest\": \"^25.1.0\"\n  },\n  \"dependencies\": {\n    \"pino\": \"^5.16.0\"\n  }\n}\n"
  },
  {
    "path": "chapter4/1_setting_up_a_test_environment/1_exposing_modules/1_end_to_end_tests/package.json",
    "content": "{\n  \"name\": \"1_end_to_end_tests\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"devDependencies\": {\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"jest\": \"^24.9.0\"\n  },\n  \"dependencies\": {\n    \"koa\": \"^2.11.0\",\n    \"koa-router\": \"^7.4.0\"\n  }\n}\n"
  },
  {
    "path": "chapter4/1_setting_up_a_test_environment/1_exposing_modules/1_end_to_end_tests/server.js",
    "content": "const Koa = require(\"koa\");\nconst Router = require(\"koa-router\");\n\nconst app = new Koa();\nconst router = new Router();\n\nlet carts = new Map();\nlet inventory = new Map();\n\nrouter.get(\"/carts/:username/items\", ctx => {\n  const cart = carts.get(ctx.params.username);\n  cart ? (ctx.body = cart) : (ctx.status = 404);\n});\n\nrouter.post(\"/carts/:username/items/:item\", ctx => {\n  const { username, item } = ctx.params;\n  const isAvailable = inventory.has(item) && inventory.get(item) > 0;\n  if (!isAvailable) {\n    ctx.body = { message: `${item} is unavailable` };\n    ctx.status = 400;\n    return;\n  }\n\n  const newItems = (carts.get(username) || []).concat(item);\n  carts.set(username, newItems);\n  inventory.set(item, inventory.get(item) - 1);\n  ctx.body = newItems;\n});\n\nrouter.delete(\"/carts/:username/items/:item\", ctx => {\n  const { username, item } = ctx.params;\n  if (!carts.has(username) || !carts.get(username).includes(item)) {\n    ctx.body = { message: `${item} is not in the cart` };\n    ctx.status = 400;\n    return;\n  }\n\n  const newItems = (carts.get(username) || []).filter(i => i !== item);\n  inventory.set(item, (inventory.get(item) || 0) + 1);\n  carts.set(username, newItems);\n  ctx.body = newItems;\n});\n\napp.use(router.routes());\n\nmodule.exports = { app: app.listen(3000), carts, inventory };\n"
  },
  {
    "path": "chapter4/1_setting_up_a_test_environment/1_exposing_modules/1_end_to_end_tests/server.test.js",
    "content": "const { app, carts, inventory } = require(\"./server.js\");\n\nconst fetch = require(\"isomorphic-fetch\");\n\nconst apiRoot = \"http://localhost:3000\";\n\nafterAll(() => app.close());\n\nafterEach(() => inventory.clear());\nafterEach(() => carts.clear());\n\ndescribe(\"add items to a cart\", () => {\n  test(\"adding available items\", async () => {\n    inventory.set(\"cheesecake\", 1);\n    const response = await fetch(\n      `${apiRoot}/carts/test_user/items/cheesecake`,\n      { method: \"POST\" }\n    );\n\n    expect(response.status).toEqual(200);\n    expect(await response.json()).toEqual([\"cheesecake\"]);\n    expect(inventory.get(\"cheesecake\")).toEqual(0);\n    expect(carts.get(\"test_user\")).toEqual([\"cheesecake\"]);\n  });\n\n  test(\"adding unavailable items\", async () => {\n    carts.set(\"test_user\", []);\n    const response = await fetch(\n      `${apiRoot}/carts/test_user/items/cheesecake`,\n      { method: \"POST\" }\n    );\n\n    expect(response.status).toEqual(400);\n    expect(await response.json()).toEqual({\n      message: \"cheesecake is unavailable\"\n    });\n    expect(carts.get(\"test_user\")).toEqual([]);\n  });\n});\n\ndescribe(\"removing items from a cart\", () => {\n  test(\"removing existing items\", async () => {\n    carts.set(\"test_user\", [\"cheesecake\"]);\n    const response = await fetch(\n      `${apiRoot}/carts/test_user/items/cheesecake`,\n      { method: \"DELETE\" }\n    );\n\n    expect(response.status).toEqual(200);\n    expect(await response.json()).toEqual([]);\n    expect(carts.get(\"test_user\")).toEqual([]);\n    expect(inventory.get(\"cheesecake\")).toEqual(1);\n  });\n\n  test(\"removing non-existing items\", async () => {\n    inventory.set(\"cheesecake\", 0);\n    carts.set(\"test_user\", []);\n    const response = await fetch(\n      `${apiRoot}/carts/test_user/items/cheesecake`,\n      { method: \"DELETE\" }\n    );\n\n    expect(response.status).toEqual(400);\n    expect(await response.json()).toEqual({\n      message: \"cheesecake is not in the cart\"\n    });\n    expect(inventory.get(\"cheesecake\")).toEqual(0);\n  });\n});\n"
  },
  {
    "path": "chapter4/1_setting_up_a_test_environment/1_exposing_modules/2_integration_tests/cartController.js",
    "content": "const { removeFromInventory } = require(\"./inventoryController\");\nconst logger = require(\"./logger\");\n\nconst carts = new Map();\n\nconst addItemToCart = (username, item) => {\n  removeFromInventory(item);\n  const newItems = (carts.get(username) || []).concat(item);\n  carts.set(username, newItems);\n  logger.log(`${item} added to ${username}'s cart`);\n  return newItems;\n};\n\nmodule.exports = { addItemToCart, carts };\n"
  },
  {
    "path": "chapter4/1_setting_up_a_test_environment/1_exposing_modules/2_integration_tests/cartController.test.js",
    "content": "const { inventory } = require(\"./inventoryController\");\nconst { carts, addItemToCart } = require(\"./cartController\");\nconst fs = require(\"fs\");\n\nafterEach(() => inventory.clear());\nafterEach(() => carts.clear());\n\ndescribe(\"addItemToCart\", () => {\n  beforeEach(() => {\n    fs.writeFileSync(\"/tmp/logs.out\", \"\");\n  });\n\n  test(\"adding unavailable items to cart\", () => {\n    carts.set(\"test_user\", []);\n    inventory.set(\"cheesecake\", 0);\n\n    try {\n      addItemToCart(\"test_user\", \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(`cheesecake is unavailable`);\n      expectedError.code = 400;\n\n      expect(e).toEqual(expectedError);\n    }\n\n    expect(carts.get(\"test_user\")).toEqual([]);\n    expect.assertions(2);\n  });\n\n  test(\"logging added items\", () => {\n    carts.set(\"test_user\", []);\n    inventory.set(\"cheesecake\", 1);\n\n    addItemToCart(\"test_user\", \"cheesecake\");\n\n    const logs = fs.readFileSync(\"/tmp/logs.out\", \"utf-8\");\n    expect(logs).toContain(\"cheesecake added to test_user's cart\\n\");\n  });\n});\n"
  },
  {
    "path": "chapter4/1_setting_up_a_test_environment/1_exposing_modules/2_integration_tests/inventoryController.js",
    "content": "const inventory = new Map();\n\nconst removeFromInventory = item => {\n  if (!inventory.has(item) || !inventory.get(item) > 0) {\n    const err = new Error(`${item} is unavailable`);\n    err.code = 400;\n    throw err;\n  }\n\n  inventory.set(item, inventory.get(item) - 1);\n};\n\nmodule.exports = { removeFromInventory, inventory };\n"
  },
  {
    "path": "chapter4/1_setting_up_a_test_environment/1_exposing_modules/2_integration_tests/logger.js",
    "content": "const fs = require(\"fs\");\n\nconst logger = {\n  log: msg => fs.appendFileSync(\"/tmp/logs.out\", msg + \"\\n\")\n};\n\nmodule.exports = logger;\n"
  },
  {
    "path": "chapter4/1_setting_up_a_test_environment/1_exposing_modules/2_integration_tests/package.json",
    "content": "{\n  \"name\": \"1_integration-tests\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"devDependencies\": {\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"jest\": \"^24.9.0\"\n  },\n  \"dependencies\": {\n    \"koa\": \"^2.11.0\",\n    \"koa-router\": \"^7.4.0\"\n  }\n}\n"
  },
  {
    "path": "chapter4/1_setting_up_a_test_environment/1_exposing_modules/2_integration_tests/server.js",
    "content": "const Koa = require(\"koa\");\nconst Router = require(\"koa-router\");\n\nconst { carts, addItemToCart } = require(\"./cartController\");\nconst { inventory } = require(\"./inventoryController\");\n\nconst app = new Koa();\nconst router = new Router();\n\nrouter.get(\"/carts/:username/items\", ctx => {\n  const cart = carts.get(ctx.params.username);\n  cart ? (ctx.body = cart) : (ctx.status = 404);\n});\n\nrouter.post(\"/carts/:username/items/:item\", ctx => {\n  try {\n    const { username, item } = ctx.params;\n    const newItems = addItemToCart(username, item);\n    ctx.body = newItems;\n  } catch (e) {\n    ctx.body = { message: e.message };\n    ctx.status = e.code;\n    return;\n  }\n});\n\nrouter.delete(\"/carts/:username/items/:item\", ctx => {\n  const { username, item } = ctx.params;\n  if (!carts.has(username) || !carts.get(username).includes(item)) {\n    ctx.body = { message: `${item} is not in the cart` };\n    ctx.status = 400;\n    return;\n  }\n\n  const newItems = (carts.get(username) || []).filter(i => i !== item);\n  inventory.set(item, (inventory.get(item) || 0) + 1);\n  carts.set(username, newItems);\n  ctx.body = newItems;\n});\n\napp.use(router.routes());\n\nmodule.exports = { app: app.listen(3000), carts, inventory };\n"
  },
  {
    "path": "chapter4/1_setting_up_a_test_environment/1_exposing_modules/2_integration_tests/server.test.js",
    "content": "const { app } = require(\"./server.js\");\nconst { inventory } = require(\"./inventoryController.js\");\nconst { carts } = require(\"./cartController.js\");\n\nconst fetch = require(\"isomorphic-fetch\");\n\nconst apiRoot = \"http://localhost:3000\";\n\nafterAll(() => app.close());\n\nafterEach(() => inventory.clear());\nafterEach(() => carts.clear());\n\ndescribe(\"add items to a cart\", () => {\n  test(\"adding available items\", async () => {\n    inventory.set(\"cheesecake\", 1);\n    const response = await fetch(\n      `${apiRoot}/carts/test_user/items/cheesecake`,\n      { method: \"POST\" }\n    );\n\n    expect(response.status).toEqual(200);\n    expect(await response.json()).toEqual([\"cheesecake\"]);\n    expect(inventory.get(\"cheesecake\")).toEqual(0);\n    expect(carts.get(\"test_user\")).toEqual([\"cheesecake\"]);\n  });\n\n  test(\"adding unavailable items\", async () => {\n    carts.set(\"test_user\", []);\n    const response = await fetch(\n      `${apiRoot}/carts/test_user/items/cheesecake`,\n      { method: \"POST\" }\n    );\n\n    expect(response.status).toEqual(400);\n    expect(await response.json()).toEqual({\n      message: \"cheesecake is unavailable\"\n    });\n    expect(carts.get(\"test_user\")).toEqual([]);\n  });\n});\n\ndescribe(\"removing items from a cart\", () => {\n  test(\"removing existing items\", async () => {\n    carts.set(\"test_user\", [\"cheesecake\"]);\n    const response = await fetch(\n      `${apiRoot}/carts/test_user/items/cheesecake`,\n      { method: \"DELETE\" }\n    );\n\n    expect(response.status).toEqual(200);\n    expect(await response.json()).toEqual([]);\n    expect(carts.get(\"test_user\")).toEqual([]);\n    expect(inventory.get(\"cheesecake\")).toEqual(1);\n  });\n\n  test(\"removing non-existing items\", async () => {\n    inventory.set(\"cheesecake\", 0);\n    carts.set(\"test_user\", []);\n    const response = await fetch(\n      `${apiRoot}/carts/test_user/items/cheesecake`,\n      { method: \"DELETE\" }\n    );\n\n    expect(response.status).toEqual(400);\n    expect(await response.json()).toEqual({\n      message: \"cheesecake is not in the cart\"\n    });\n    expect(inventory.get(\"cheesecake\")).toEqual(0);\n  });\n});\n"
  },
  {
    "path": "chapter4/1_setting_up_a_test_environment/1_exposing_modules/3_unit_tests/cartController.js",
    "content": "const { inventory, removeFromInventory } = require(\"./inventoryController\");\nconst logger = require(\"./logger\");\n\nconst carts = new Map();\n\nconst addItemToCart = (username, item) => {\n  removeFromInventory(item);\n  const newItems = (carts.get(username) || []).concat(item);\n\n  if (!compliesToItemLimit(newItems)) {\n    inventory.set(item, inventory.get(item) + 1);\n    const limitError = new Error(\n      \"You can't have more than three units of an item in your cart\"\n    );\n    limitError.code = 400;\n    throw limitError;\n  }\n\n  carts.set(username, newItems);\n  logger.log(`${item} added to ${username}'s cart`);\n  return newItems;\n};\n\nconst compliesToItemLimit = cart => {\n  const unitsPerItem = cart.reduce((itemMap, itemName) => {\n    const quantity = (itemMap[itemName] || 0) + 1;\n    return { ...itemMap, [itemName]: quantity };\n  }, {});\n\n  return Object.values(unitsPerItem).every(quantity => quantity <= 3);\n};\n\nmodule.exports = { addItemToCart, carts, compliesToItemLimit };\n"
  },
  {
    "path": "chapter4/1_setting_up_a_test_environment/1_exposing_modules/3_unit_tests/cartController.test.js",
    "content": "const { inventory } = require(\"./inventoryController\");\nconst {\n  carts,\n  addItemToCart,\n  compliesToItemLimit\n} = require(\"./cartController\");\nconst fs = require(\"fs\");\n\nafterEach(() => inventory.clear());\nafterEach(() => carts.clear());\n\ndescribe(\"addItemToCart\", () => {\n  beforeEach(() => {\n    fs.writeFileSync(\"/tmp/logs.out\", \"\");\n  });\n\n  test(\"adding unavailable items to cart\", () => {\n    carts.set(\"test_user\", []);\n    inventory.set(\"cheesecake\", 0);\n\n    try {\n      addItemToCart(\"test_user\", \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(\"cheesecake is unavailable\");\n      expectedError.code = 400;\n\n      expect(e).toEqual(expectedError);\n    }\n\n    expect(carts.get(\"test_user\")).toEqual([]);\n    expect.assertions(2);\n  });\n\n  test(\"adding items above limit to cart\", () => {\n    const initialCartContent = [\"cheesecake\", \"cheesecake\", \"cheesecake\"];\n    carts.set(\"test_user\", initialCartContent);\n    inventory.set(\"cheesecake\", 1);\n\n    try {\n      addItemToCart(\"test_user\", \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(\n        \"You can't have more than three units of an item in your cart\"\n      );\n      expectedError.code = 400;\n      expect(e).toEqual(expectedError);\n    }\n\n    expect(carts.get(\"test_user\")).toEqual(initialCartContent);\n    expect(inventory.get('cheesecake')).toEqual(1);\n    expect.assertions(3);\n  });\n\n  test(\"logging added items\", () => {\n    carts.set(\"test_user\", []);\n    inventory.set(\"cheesecake\", 1);\n\n    addItemToCart(\"test_user\", \"cheesecake\");\n\n    const logs = fs.readFileSync(\"/tmp/logs.out\", \"utf-8\");\n    expect(logs).toContain(\"cheesecake added to test_user's cart\\n\");\n  });\n});\n\ndescribe(\"compliesToItemLimit\", () => {\n  test(\"returns true for cards with no more than 3 items of a kind\", () => {\n    const cart = [\"cheesecake\", \"cheesecake\", \"almond brownie\", \"apple pie\"];\n    expect(compliesToItemLimit(cart)).toBe(true);\n  });\n\n  test(\"returns false for cards with no more than 3 items of a kind\", () => {\n    const cart = [\n      \"cheesecake\",\n      \"cheesecake\",\n      \"almond brownie\",\n      \"cheesecake\",\n      \"cheesecake\"\n    ];\n    expect(compliesToItemLimit(cart)).toBe(false);\n  });\n});\n"
  },
  {
    "path": "chapter4/1_setting_up_a_test_environment/1_exposing_modules/3_unit_tests/inventoryController.js",
    "content": "const inventory = new Map();\n\nconst removeFromInventory = item => {\n  if (!inventory.has(item) || !inventory.get(item) > 0) {\n    const err = new Error(`${item} is unavailable`);\n    err.code = 400;\n    throw err;\n  }\n\n  inventory.set(item, inventory.get(item) - 1);\n};\n\nmodule.exports = { removeFromInventory, inventory };\n"
  },
  {
    "path": "chapter4/1_setting_up_a_test_environment/1_exposing_modules/3_unit_tests/logger.js",
    "content": "const fs = require(\"fs\");\n\nconst logger = {\n  log: msg => fs.appendFileSync(\"/tmp/logs.out\", msg + \"\\n\")\n};\n\nmodule.exports = logger;\n"
  },
  {
    "path": "chapter4/1_setting_up_a_test_environment/1_exposing_modules/3_unit_tests/package.json",
    "content": "{\n  \"name\": \"1_unit_tests\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"devDependencies\": {\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"jest\": \"^24.9.0\"\n  },\n  \"dependencies\": {\n    \"koa\": \"^2.11.0\",\n    \"koa-router\": \"^7.4.0\"\n  }\n}\n"
  },
  {
    "path": "chapter4/1_setting_up_a_test_environment/1_exposing_modules/3_unit_tests/server.js",
    "content": "const Koa = require(\"koa\");\nconst Router = require(\"koa-router\");\n\nconst { carts, addItemToCart } = require(\"./cartController\");\nconst { inventory } = require(\"./inventoryController\");\n\nconst app = new Koa();\nconst router = new Router();\n\nrouter.get(\"/carts/:username/items\", ctx => {\n  const cart = carts.get(ctx.params.username);\n  cart ? (ctx.body = cart) : (ctx.status = 404);\n});\n\nrouter.post(\"/carts/:username/items/:item\", ctx => {\n  try {\n    const { username, item } = ctx.params;\n    const newItems = addItemToCart(username, item);\n    ctx.body = newItems;\n  } catch (e) {\n    ctx.body = { message: e.message };\n    ctx.status = e.code;\n    return;\n  }\n});\n\nrouter.delete(\"/carts/:username/items/:item\", ctx => {\n  const { username, item } = ctx.params;\n  if (!carts.has(username) || !carts.get(username).includes(item)) {\n    ctx.body = { message: `${item} is not in the cart` };\n    ctx.status = 400;\n    return;\n  }\n\n  const newItems = (carts.get(username) || []).filter(i => i !== item);\n  inventory.set(item, (inventory.get(item) || 0) + 1);\n  carts.set(username, newItems);\n  ctx.body = newItems;\n});\n\napp.use(router.routes());\n\nmodule.exports = { app: app.listen(3000), carts, inventory };\n"
  },
  {
    "path": "chapter4/1_setting_up_a_test_environment/1_exposing_modules/3_unit_tests/server.test.js",
    "content": "const { app } = require(\"./server.js\");\nconst { inventory } = require(\"./inventoryController.js\");\nconst { carts } = require(\"./cartController.js\");\n\nconst fetch = require(\"isomorphic-fetch\");\n\nconst apiRoot = \"http://localhost:3000\";\n\nafterAll(() => app.close());\n\nafterEach(() => inventory.clear());\nafterEach(() => carts.clear());\n\ndescribe(\"add items to a cart\", () => {\n  test(\"adding available items\", async () => {\n    inventory.set(\"cheesecake\", 1);\n    const response = await fetch(\n      `${apiRoot}/carts/test_user/items/cheesecake`,\n      { method: \"POST\" }\n    );\n\n    expect(response.status).toEqual(200);\n    expect(await response.json()).toEqual([\"cheesecake\"]);\n    expect(inventory.get(\"cheesecake\")).toEqual(0);\n    expect(carts.get(\"test_user\")).toEqual([\"cheesecake\"]);\n  });\n\n  test(\"adding unavailable items\", async () => {\n    carts.set(\"test_user\", []);\n    const response = await fetch(\n      `${apiRoot}/carts/test_user/items/cheesecake`,\n      { method: \"POST\" }\n    );\n\n    expect(response.status).toEqual(400);\n    expect(await response.json()).toEqual({\n      message: \"cheesecake is unavailable\"\n    });\n    expect(carts.get(\"test_user\")).toEqual([]);\n  });\n});\n\ndescribe(\"removing items from a cart\", () => {\n  test(\"removing existing items\", async () => {\n    carts.set(\"test_user\", [\"cheesecake\"]);\n    const response = await fetch(\n      `${apiRoot}/carts/test_user/items/cheesecake`,\n      { method: \"DELETE\" }\n    );\n\n    expect(response.status).toEqual(200);\n    expect(await response.json()).toEqual([]);\n    expect(carts.get(\"test_user\")).toEqual([]);\n    expect(inventory.get(\"cheesecake\")).toEqual(1);\n  });\n\n  test(\"removing non-existing items\", async () => {\n    inventory.set(\"cheesecake\", 0);\n    carts.set(\"test_user\", []);\n    const response = await fetch(\n      `${apiRoot}/carts/test_user/items/cheesecake`,\n      { method: \"DELETE\" }\n    );\n\n    expect(response.status).toEqual(400);\n    expect(await response.json()).toEqual({\n      message: \"cheesecake is not in the cart\"\n    });\n    expect(inventory.get(\"cheesecake\")).toEqual(0);\n  });\n});\n"
  },
  {
    "path": "chapter4/2_testing_http_endpoints/1_using_supertest/cartController.js",
    "content": "const { removeFromInventory } = require(\"./inventoryController\");\nconst logger = require(\"./logger\");\n\nconst carts = new Map();\n\nconst addItemToCart = (username, item) => {\n  removeFromInventory(item);\n  const newItems = (carts.get(username) || []).concat(item);\n\n  if (!compliesToItemLimit(newItems)) {\n    const limitError = new Error(\n      \"You can't have more than three units of an item in your cart\"\n    );\n    limitError.code = 400;\n    throw limitError;\n  }\n\n  carts.set(username, newItems);\n  logger.log(`${item} added to ${username}'s cart`);\n  return newItems;\n};\n\nconst compliesToItemLimit = cart => {\n  const unitsPerItem = cart.reduce((itemMap, itemName) => {\n    const quantity = (itemMap[itemName] || 0) + 1;\n    return { ...itemMap, [itemName]: quantity };\n  }, {});\n\n  return Object.values(unitsPerItem).every(quantity => quantity <= 3);\n};\n\nmodule.exports = { addItemToCart, carts, compliesToItemLimit };\n"
  },
  {
    "path": "chapter4/2_testing_http_endpoints/1_using_supertest/cartController.test.js",
    "content": "const { inventory } = require(\"./inventoryController\");\nconst {\n  carts,\n  addItemToCart,\n  compliesToItemLimit\n} = require(\"./cartController\");\nconst fs = require(\"fs\");\n\nafterEach(() => inventory.clear());\nafterEach(() => carts.clear());\n\ndescribe(\"addItemToCart\", () => {\n  beforeEach(() => {\n    fs.writeFileSync(\"/tmp/logs.out\", \"\");\n  });\n\n  test(\"adding unavailable items to cart\", () => {\n    carts.set(\"test_user\", []);\n    inventory.set(\"cheesecake\", 0);\n\n    try {\n      addItemToCart(\"test_user\", \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(\"cheesecake is unavailable\");\n      expectedError.code = 400;\n\n      expect(e).toEqual(expectedError);\n    }\n\n    expect(carts.get(\"test_user\")).toEqual([]);\n    expect.assertions(2);\n  });\n\n  test(\"adding items above limit to cart\", () => {\n    const initialCartContent = [\"cheesecake\", \"cheesecake\", \"cheesecake\"];\n    carts.set(\"test_user\", initialCartContent);\n    inventory.set(\"cheesecake\", 1);\n\n    try {\n      addItemToCart(\"test_user\", \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(\n        \"You can't have more than three units of an item in your cart\"\n      );\n      expectedError.code = 400;\n      expect(e).toEqual(expectedError);\n    }\n\n    expect(carts.get(\"test_user\")).toEqual(initialCartContent);\n    expect.assertions(2);\n  });\n\n  test(\"logging added items\", () => {\n    carts.set(\"test_user\", []);\n    inventory.set(\"cheesecake\", 1);\n\n    addItemToCart(\"test_user\", \"cheesecake\");\n\n    const logs = fs.readFileSync(\"/tmp/logs.out\", \"utf-8\");\n    expect(logs).toContain(\"cheesecake added to test_user's cart\\n\");\n  });\n});\n\ndescribe(\"compliesToItemLimit\", () => {\n  test(\"returns true for cards with no more than 3 items of a kind\", () => {\n    const cart = [\"cheesecake\", \"cheesecake\", \"almond brownie\", \"apple pie\"];\n    expect(compliesToItemLimit(cart)).toBe(true);\n  });\n\n  test(\"returns true for cards with no more than 3 items of a kind\", () => {\n    const cart = [\n      \"cheesecake\",\n      \"cheesecake\",\n      \"almond brownie\",\n      \"cheesecake\",\n      \"cheesecake\"\n    ];\n    expect(compliesToItemLimit(cart)).toBe(false);\n  });\n});\n"
  },
  {
    "path": "chapter4/2_testing_http_endpoints/1_using_supertest/inventoryController.js",
    "content": "const inventory = new Map();\n\nconst removeFromInventory = item => {\n  if (!inventory.has(item) || !inventory.get(item) > 0) {\n    const err = new Error(`${item} is unavailable`);\n    err.code = 400;\n    throw err;\n  }\n\n  inventory.set(item, inventory.get(item) - 1);\n};\n\nmodule.exports = { removeFromInventory, inventory };\n"
  },
  {
    "path": "chapter4/2_testing_http_endpoints/1_using_supertest/logger.js",
    "content": "const fs = require(\"fs\");\n\nconst logger = {\n  log: msg => fs.appendFileSync(\"/tmp/logs.out\", msg + \"\\n\")\n};\n\nmodule.exports = logger;\n"
  },
  {
    "path": "chapter4/2_testing_http_endpoints/1_using_supertest/package.json",
    "content": "{\n  \"name\": \"2_using_supertest\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"devDependencies\": {\n    \"jest\": \"^24.9.0\",\n    \"supertest\": \"^4.0.2\"\n  },\n  \"dependencies\": {\n    \"koa\": \"^2.11.0\",\n    \"koa-body-parser\": \"^1.1.2\",\n    \"koa-router\": \"^7.4.0\"\n  }\n}\n"
  },
  {
    "path": "chapter4/2_testing_http_endpoints/1_using_supertest/server.js",
    "content": "const Koa = require(\"koa\");\nconst Router = require(\"koa-router\");\nconst bodyParser = require(\"koa-body-parser\");\n\nconst { carts, addItemToCart } = require(\"./cartController\");\nconst { inventory } = require(\"./inventoryController\");\n\nconst app = new Koa();\nconst router = new Router();\n\napp.use(bodyParser());\n\nrouter.get(\"/carts/:username/items\", ctx => {\n  const cart = carts.get(ctx.params.username);\n  cart ? (ctx.body = cart) : (ctx.status = 404);\n});\n\nrouter.post(\"/carts/:username/items\", ctx => {\n  const { username } = ctx.params;\n  const { item, quantity } = ctx.request.body;\n\n  for (let i = 0; i < quantity; i++) {\n    try {\n      const newItems = addItemToCart(username, item);\n      ctx.body = newItems;\n    } catch (e) {\n      ctx.body = { message: e.message };\n      ctx.status = e.code;\n      return;\n    }\n  }\n});\n\nrouter.delete(\"/carts/:username/items/:item\", ctx => {\n  const { username, item } = ctx.params;\n  if (!carts.has(username) || !carts.get(username).includes(item)) {\n    ctx.body = { message: `${item} is not in the cart` };\n    ctx.status = 400;\n    return;\n  }\n\n  const newItems = (carts.get(username) || []).filter(i => i !== item);\n  inventory.set(item, (inventory.get(item) || 0) + 1);\n  carts.set(username, newItems);\n  ctx.body = newItems;\n});\n\napp.use(router.routes());\n\nmodule.exports = { app: app.listen(3000), carts, inventory };\n"
  },
  {
    "path": "chapter4/2_testing_http_endpoints/1_using_supertest/server.test.js",
    "content": "const request = require(\"supertest\");\nconst { app } = require(\"./server.js\");\nconst { inventory } = require(\"./inventoryController.js\");\nconst { carts } = require(\"./cartController.js\");\n\nafterAll(() => app.close());\n\nafterEach(() => inventory.clear());\nafterEach(() => carts.clear());\n\ndescribe(\"add items to a cart\", () => {\n  test(\"adding available items\", async () => {\n    inventory.set(\"cheesecake\", 3);\n    const response = await request(app)\n      .post(\"/carts/test_user/items\")\n      .send({ item: \"cheesecake\", quantity: 3 })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    const newItems = [\"cheesecake\", \"cheesecake\", \"cheesecake\"];\n    expect(response.body).toEqual(newItems);\n    expect(inventory.get(\"cheesecake\")).toEqual(0);\n    expect(carts.get(\"test_user\")).toEqual(newItems);\n  });\n\n  test(\"adding unavailable items\", async () => {\n    carts.set(\"test_user\", []);\n    const response = await request(app)\n      .post(\"/carts/test_user/items\")\n      .send({ item: \"cheesecake\", quantity: 1 })\n      .expect(400)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"cheesecake is unavailable\"\n    });\n    expect(carts.get(\"test_user\")).toEqual([]);\n  });\n});\n\ndescribe(\"removing items from a cart\", () => {\n  test(\"removing existing items\", async () => {\n    carts.set(\"test_user\", [\"cheesecake\"]);\n    const response = await request(app)\n      .del(\"/carts/test_user/items/cheesecake\")\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual([]);\n    expect(carts.get(\"test_user\")).toEqual([]);\n    expect(inventory.get(\"cheesecake\")).toEqual(1);\n  });\n\n  test(\"removing non-existing items\", async () => {\n    inventory.set(\"cheesecake\", 0);\n    carts.set(\"test_user\", []);\n    const response = await request(app)\n      .del(\"/carts/test_user/items/cheesecake\")\n      .expect(400)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"cheesecake is not in the cart\"\n    });\n    expect(inventory.get(\"cheesecake\")).toEqual(0);\n  });\n});\n"
  },
  {
    "path": "chapter4/2_testing_http_endpoints/2_testing_middlewares/authenticationController.js",
    "content": "const crypto = require(\"crypto\");\nconst users = new Map();\n\nconst hashPassword = password => {\n  const hash = crypto.createHash(\"sha256\");\n  hash.update(password);\n  return hash.digest(\"hex\");\n};\n\nconst credentialsAreValid = (username, password) => {\n  const userExists = users.has(username);\n  if (!userExists) return false;\n\n  const currentPasswordHash = users.get(username).passwordHash;\n  return hashPassword(password) === currentPasswordHash;\n};\n\nconst authenticationMiddleware = async (ctx, next) => {\n  try {\n    const authHeader = ctx.request.headers.authorization;\n    const credentials = Buffer.from(\n      authHeader.slice(\"basic\".length + 1),\n      \"base64\"\n    ).toString();\n    const [username, password] = credentials.split(\":\");\n\n    if (!credentialsAreValid(username, password)) {\n      throw new Error(\"invalid credentials\");\n    }\n  } catch (e) {\n    ctx.status = 401;\n    ctx.body = { message: \"please provide valid credentials\" };\n    return;\n  }\n\n  await next();\n};\n\nmodule.exports = {\n  users,\n  hashPassword,\n  credentialsAreValid,\n  authenticationMiddleware\n};\n"
  },
  {
    "path": "chapter4/2_testing_http_endpoints/2_testing_middlewares/authenticationController.test.js",
    "content": "const crypto = require(\"crypto\");\nconst {\n  users,\n  hashPassword,\n  credentialsAreValid,\n  authenticationMiddleware\n} = require(\"./authenticationController\");\n\nafterEach(() => users.clear());\n\ndescribe(\"hashPassword\", () => {\n  test(\"hashing passwords\", () => {\n    const plainTextPassword = \"password_example\";\n    const hash = crypto.createHash(\"sha256\");\n    hash.update(plainTextPassword);\n    const expectedHash = hash.digest(\"hex\");\n    expect(hashPassword(plainTextPassword)).toBe(expectedHash);\n  });\n});\n\ndescribe(\"credentialsAreValid\", () => {\n  test(\"validating credentials\", () => {\n    users.set(\"test_user\", {\n      email: \"test_user@example.org\",\n      passwordHash: hashPassword(\"a_password\")\n    });\n\n    expect(credentialsAreValid(\"test_user\", \"a_password\")).toBe(true);\n  });\n});\n\ndescribe(\"authenticationMiddleware\", () => {\n  test(\"returning an error if the credentials are not valid\", async () => {\n    const fakeAuth = Buffer.from(\"invalid:credentials\").toString(\"base64\");\n    const ctx = {\n      request: {\n        headers: { authorization: `Basic ${fakeAuth}` }\n      }\n    };\n\n    const next = jest.fn();\n    await authenticationMiddleware(ctx, next);\n    expect(next.mock.calls).toHaveLength(0);\n    expect(ctx).toEqual({\n      ...ctx,\n      status: 401,\n      body: { message: \"please provide valid credentials\" }\n    });\n  });\n\n  test(\"authenticating properly\", async () => {\n    users.set(\"test_user\", {\n      email: \"test_user@example.org\",\n      passwordHash: hashPassword(\"a_password\")\n    });\n\n    const validAuth = Buffer.from(\"test_user:a_password\").toString(\"base64\");\n    const ctx = {\n      request: {\n        headers: { authorization: `Basic ${validAuth}` }\n      }\n    };\n\n    const next = jest.fn();\n    await authenticationMiddleware(ctx, next);\n    expect(next.mock.calls).toHaveLength(1);\n  });\n});\n"
  },
  {
    "path": "chapter4/2_testing_http_endpoints/2_testing_middlewares/cartController.js",
    "content": "const { removeFromInventory } = require(\"./inventoryController\");\nconst logger = require(\"./logger\");\n\nconst carts = new Map();\n\nconst addItemToCart = (username, item) => {\n  removeFromInventory(item);\n  const newItems = (carts.get(username) || []).concat(item);\n\n  if (!compliesToItemLimit(newItems)) {\n    const limitError = new Error(\n      \"You can't have more than three units of an item in your cart\"\n    );\n    limitError.code = 400;\n    throw limitError;\n  }\n\n  carts.set(username, newItems);\n  logger.log(`${item} added to ${username}'s cart`);\n  return newItems;\n};\n\nconst compliesToItemLimit = cart => {\n  const unitsPerItem = cart.reduce((itemMap, itemName) => {\n    const quantity = (itemMap[itemName] || 0) + 1;\n    return { ...itemMap, [itemName]: quantity };\n  }, {});\n\n  return Object.values(unitsPerItem).every(quantity => quantity <= 3);\n};\n\nmodule.exports = { addItemToCart, carts, compliesToItemLimit };\n"
  },
  {
    "path": "chapter4/2_testing_http_endpoints/2_testing_middlewares/cartController.test.js",
    "content": "const { inventory } = require(\"./inventoryController\");\nconst {\n  carts,\n  addItemToCart,\n  compliesToItemLimit\n} = require(\"./cartController\");\nconst fs = require(\"fs\");\n\nafterEach(() => inventory.clear());\nafterEach(() => carts.clear());\n\ndescribe(\"addItemToCart\", () => {\n  beforeEach(() => {\n    fs.writeFileSync(\"/tmp/logs.out\", \"\");\n  });\n\n  test(\"adding unavailable items to cart\", () => {\n    carts.set(\"test_user\", []);\n    inventory.set(\"cheesecake\", 0);\n\n    try {\n      addItemToCart(\"test_user\", \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(\"cheesecake is unavailable\");\n      expectedError.code = 400;\n\n      expect(e).toEqual(expectedError);\n    }\n\n    expect(carts.get(\"test_user\")).toEqual([]);\n    expect.assertions(2);\n  });\n\n  test(\"adding items above limit to cart\", () => {\n    const initialCartContent = [\"cheesecake\", \"cheesecake\", \"cheesecake\"];\n    carts.set(\"test_user\", initialCartContent);\n    inventory.set(\"cheesecake\", 1);\n\n    try {\n      addItemToCart(\"test_user\", \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(\n        \"You can't have more than three units of an item in your cart\"\n      );\n      expectedError.code = 400;\n      expect(e).toEqual(expectedError);\n    }\n\n    expect(carts.get(\"test_user\")).toEqual(initialCartContent);\n    expect.assertions(2);\n  });\n\n  test(\"logging added items\", () => {\n    carts.set(\"test_user\", []);\n    inventory.set(\"cheesecake\", 1);\n\n    addItemToCart(\"test_user\", \"cheesecake\");\n\n    const logs = fs.readFileSync(\"/tmp/logs.out\", \"utf-8\");\n    expect(logs).toContain(\"cheesecake added to test_user's cart\\n\");\n  });\n});\n\ndescribe(\"compliesToItemLimit\", () => {\n  test(\"returns true for cards with no more than 3 items of a kind\", () => {\n    const cart = [\"cheesecake\", \"cheesecake\", \"almond brownie\", \"apple pie\"];\n    expect(compliesToItemLimit(cart)).toBe(true);\n  });\n\n  test(\"returns true for cards with no more than 3 items of a kind\", () => {\n    const cart = [\n      \"cheesecake\",\n      \"cheesecake\",\n      \"almond brownie\",\n      \"cheesecake\",\n      \"cheesecake\"\n    ];\n    expect(compliesToItemLimit(cart)).toBe(false);\n  });\n});\n"
  },
  {
    "path": "chapter4/2_testing_http_endpoints/2_testing_middlewares/inventoryController.js",
    "content": "const inventory = new Map();\n\nconst removeFromInventory = item => {\n  if (!inventory.has(item) || !inventory.get(item) > 0) {\n    const err = new Error(`${item} is unavailable`);\n    err.code = 400;\n    throw err;\n  }\n\n  inventory.set(item, inventory.get(item) - 1);\n};\n\nmodule.exports = { removeFromInventory, inventory };\n"
  },
  {
    "path": "chapter4/2_testing_http_endpoints/2_testing_middlewares/logger.js",
    "content": "const fs = require(\"fs\");\n\nconst logger = {\n  log: msg => fs.appendFileSync(\"/tmp/logs.out\", msg + \"\\n\")\n};\n\nmodule.exports = logger;\n"
  },
  {
    "path": "chapter4/2_testing_http_endpoints/2_testing_middlewares/package.json",
    "content": "{\n  \"name\": \"2_testing_middlewares\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"devDependencies\": {\n    \"jest\": \"^24.9.0\",\n    \"supertest\": \"^4.0.2\"\n  },\n  \"dependencies\": {\n    \"koa\": \"^2.11.0\",\n    \"koa-body-parser\": \"^1.1.2\",\n    \"koa-router\": \"^7.4.0\"\n  }\n}\n"
  },
  {
    "path": "chapter4/2_testing_http_endpoints/2_testing_middlewares/server.js",
    "content": "const Koa = require(\"koa\");\nconst Router = require(\"koa-router\");\nconst bodyParser = require(\"koa-body-parser\");\n\nconst { carts, addItemToCart } = require(\"./cartController\");\nconst { inventory } = require(\"./inventoryController\");\nconst {\n  users,\n  hashPassword,\n  authenticationMiddleware\n} = require(\"./authenticationController\");\n\nconst app = new Koa();\nconst router = new Router();\n\napp.use(bodyParser());\n\napp.use(async (ctx, next) => {\n  if (ctx.url.startsWith(\"/carts\")) {\n    return await authenticationMiddleware(ctx, next);\n  }\n\n  await next();\n});\n\nrouter.put(\"/users/:username\", ctx => {\n  const { username } = ctx.params;\n  const { email, password } = ctx.request.body;\n  const userAlreadyExists = users.has(username);\n  if (userAlreadyExists) {\n    ctx.body = { message: `${username} already exists` };\n    ctx.status = 409;\n    return;\n  }\n\n  users.set(username, { email, passwordHash: hashPassword(password) });\n  return (ctx.body = { message: `${username} created successfully` });\n});\n\nrouter.post(\"/carts/:username/items\", ctx => {\n  const { username } = ctx.params;\n  const { item, quantity } = ctx.request.body;\n\n  for (let i = 0; i < quantity; i++) {\n    try {\n      const newItems = addItemToCart(username, item);\n      ctx.body = newItems;\n    } catch (e) {\n      ctx.body = { message: e.message };\n      ctx.status = e.code;\n      return;\n    }\n  }\n});\n\nrouter.delete(\"/carts/:username/items/:item\", ctx => {\n  const { username, item } = ctx.params;\n  if (!carts.has(username) || !carts.get(username).includes(item)) {\n    ctx.body = { message: `${item} is not in the cart` };\n    ctx.status = 400;\n    return;\n  }\n\n  const newItems = (carts.get(username) || []).filter(i => i !== item);\n  inventory.set(item, (inventory.get(item) || 0) + 1);\n  carts.set(username, newItems);\n  ctx.body = newItems;\n});\n\napp.use(router.routes());\n\nmodule.exports = { app: app.listen(3000), carts, inventory };\n"
  },
  {
    "path": "chapter4/2_testing_http_endpoints/2_testing_middlewares/server.test.js",
    "content": "const request = require(\"supertest\");\nconst { app } = require(\"./server.js\");\nconst { inventory } = require(\"./inventoryController.js\");\nconst { carts } = require(\"./cartController.js\");\nconst { users, hashPassword } = require(\"./authenticationController.js\");\n\nafterAll(() => app.close());\n\nafterEach(() => inventory.clear());\nafterEach(() => carts.clear());\nafterEach(() => users.clear());\n\nconst user = \"test_user\";\nconst password = \"a_password\";\nconst validAuth = Buffer.from(`${user}:${password}`).toString(\"base64\");\nconst authHeader = `Basic ${validAuth}`;\nconst createUser = () => {\n  users.set(user, {\n    email: \"test_user@example.org\",\n    passwordHash: hashPassword(password)\n  });\n};\n\ndescribe(\"add items to a cart\", () => {\n  beforeEach(createUser);\n\n  test(\"adding available items\", async () => {\n    inventory.set(\"cheesecake\", 3);\n    const response = await request(app)\n      .post(\"/carts/test_user/items\")\n      .set(\"authorization\", authHeader)\n      .send({ item: \"cheesecake\", quantity: 3 })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    const newItems = [\"cheesecake\", \"cheesecake\", \"cheesecake\"];\n    expect(response.body).toEqual(newItems);\n    expect(inventory.get(\"cheesecake\")).toEqual(0);\n    expect(carts.get(\"test_user\")).toEqual(newItems);\n  });\n\n  test(\"adding unavailable items\", async () => {\n    carts.set(\"test_user\", []);\n    const response = await request(app)\n      .post(\"/carts/test_user/items\")\n      .set(\"authorization\", authHeader)\n      .send({ item: \"cheesecake\", quantity: 1 })\n      .expect(400)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"cheesecake is unavailable\"\n    });\n    expect(carts.get(\"test_user\")).toEqual([]);\n  });\n});\n\ndescribe(\"removing items from a cart\", () => {\n  beforeEach(createUser);\n\n  test(\"removing existing items\", async () => {\n    carts.set(\"test_user\", [\"cheesecake\"]);\n    const response = await request(app)\n      .del(\"/carts/test_user/items/cheesecake\")\n      .set(\"authorization\", authHeader)\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual([]);\n    expect(carts.get(\"test_user\")).toEqual([]);\n    expect(inventory.get(\"cheesecake\")).toEqual(1);\n  });\n\n  test(\"removing non-existing items\", async () => {\n    inventory.set(\"cheesecake\", 0);\n    carts.set(\"test_user\", []);\n    const response = await request(app)\n      .del(\"/carts/test_user/items/cheesecake\")\n      .set(\"authorization\", authHeader)\n      .expect(400)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"cheesecake is not in the cart\"\n    });\n    expect(inventory.get(\"cheesecake\")).toEqual(0);\n  });\n});\n\ndescribe(\"create accounts\", () => {\n  test(\"creating a new account\", async () => {\n    const response = await request(app)\n      .put(\"/users/test_user\")\n      .send({ email: \"test_user@example.org\", password: \"a_password\" })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"test_user created successfully\"\n    });\n\n    expect(users.get(\"test_user\")).toEqual({\n      email: \"test_user@example.org\",\n      passwordHash: hashPassword(\"a_password\")\n    });\n  });\n\n  test(\"creating a duplicate account\", async () => {\n    users.set(\"test_user\", {\n      email: \"test_user@example.org\",\n      passwordHash: hashPassword(\"a_password\")\n    });\n\n    const response = await request(app)\n      .put(\"/users/test_user\")\n      .send({ email: \"test_user@example.org\", password: \"a_password\" })\n      .expect(409)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"test_user already exists\"\n    });\n  });\n});\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/1_database_integrations/authenticationController.js",
    "content": "const crypto = require(\"crypto\");\nconst { db } = require(\"./dbConnection\");\n\nconst hashPassword = password => {\n  const hash = crypto.createHash(\"sha256\");\n  hash.update(password);\n  return hash.digest(\"hex\");\n};\n\nconst credentialsAreValid = async (username, password) => {\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  if (!user) return false;\n  return hashPassword(password) === user.passwordHash;\n};\n\nconst authenticationMiddleware = async (ctx, next) => {\n  try {\n    const authHeader = ctx.request.headers.authorization;\n    const credentials = Buffer.from(\n      authHeader.slice(\"basic\".length + 1),\n      \"base64\"\n    ).toString();\n    const [username, password] = credentials.split(\":\");\n\n    const validCredentialsSent = await credentialsAreValid(username, password);\n    if (!validCredentialsSent) throw new Error(\"invalid credentials\");\n  } catch (e) {\n    ctx.status = 401;\n    ctx.body = { message: \"please provide valid credentials\" };\n    return;\n  }\n\n  await next();\n};\n\nmodule.exports = {\n  hashPassword,\n  credentialsAreValid,\n  authenticationMiddleware\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/1_database_integrations/authenticationController.test.js",
    "content": "const { db, closeConnection } = require(\"./dbConnection\");\nconst crypto = require(\"crypto\");\nconst {\n  hashPassword,\n  credentialsAreValid,\n  authenticationMiddleware\n} = require(\"./authenticationController\");\n\nbeforeEach(() => db(\"users\").truncate());\n\ndescribe(\"hashPassword\", () => {\n  test(\"hashing passwords\", () => {\n    const plainTextPassword = \"password_example\";\n    const hash = crypto.createHash(\"sha256\");\n    hash.update(plainTextPassword);\n    const expectedHash = hash.digest(\"hex\");\n    expect(hashPassword(plainTextPassword)).toBe(expectedHash);\n  });\n});\n\ndescribe(\"credentialsAreValid\", () => {\n  test(\"validating credentials\", async () => {\n    await db(\"users\").insert({\n      username: \"test_user\",\n      email: \"test_user@example.org\",\n      passwordHash: hashPassword(\"a_password\")\n    });\n\n    expect(await credentialsAreValid(\"test_user\", \"a_password\")).toBe(true);\n  });\n});\n\ndescribe(\"authenticationMiddleware\", () => {\n  test(\"returning an error if the credentials are not valid\", async () => {\n    const fakeAuth = Buffer.from(\"invalid:credentials\").toString(\"base64\");\n    const ctx = {\n      request: {\n        headers: { authorization: `Basic ${fakeAuth}` }\n      }\n    };\n\n    const next = jest.fn();\n    await authenticationMiddleware(ctx, next);\n    expect(next.mock.calls).toHaveLength(0);\n    expect(ctx).toEqual({\n      ...ctx,\n      status: 401,\n      body: { message: \"please provide valid credentials\" }\n    });\n  });\n\n  test(\"authenticating properly\", async () => {\n    await db(\"users\").insert({\n      username: \"test_user\",\n      email: \"test_user@example.org\",\n      passwordHash: hashPassword(\"a_password\")\n    });\n\n    const validAuth = Buffer.from(\"test_user:a_password\").toString(\"base64\");\n    const ctx = {\n      request: {\n        headers: { authorization: `Basic ${validAuth}` }\n      }\n    };\n\n    const next = jest.fn();\n    await authenticationMiddleware(ctx, next);\n    expect(next.mock.calls).toHaveLength(1);\n  });\n});\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/1_database_integrations/cartController.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { removeFromInventory } = require(\"./inventoryController\");\nconst logger = require(\"./logger\");\n\nconst addItemToCart = async (username, itemName) => {\n  await removeFromInventory(itemName);\n\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  if (!user) {\n    const userNotFound = new Error(\"user not found\");\n    userNotFound.code = 404;\n  }\n\n  const itemEntry = await db\n    .select()\n    .from(\"carts_items\")\n    .where({ userId: user.id, itemName })\n    .first();\n\n  if (itemEntry && itemEntry.quantity + 1 > 3) {\n    const limitError = new Error(\n      \"You can't have more than three units of an item in your cart\"\n    );\n    limitError.code = 400;\n    throw limitError;\n  }\n\n  if (itemEntry) {\n    await db(\"carts_items\")\n      .increment(\"quantity\")\n      .where({ userId: itemEntry.userId, itemName });\n  } else {\n    await db(\"carts_items\").insert({\n      userId: user.id,\n      itemName,\n      quantity: 1\n    });\n  }\n\n  logger.log(`${itemName} added to ${username}'s cart`);\n  return db\n    .select(\"itemName\", \"quantity\")\n    .from(\"carts_items\")\n    .where({ userId: user.id });\n};\n\nmodule.exports = { addItemToCart };\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/1_database_integrations/cartController.test.js",
    "content": "const { db, closeConnection } = require(\"./dbConnection\");\nconst { addItemToCart } = require(\"./cartController\");\nconst { hashPassword } = require(\"./authenticationController\");\n\nconst fs = require(\"fs\");\n\nbeforeEach(() => db(\"users\").truncate());\nbeforeEach(() => db(\"carts_items\").truncate());\nbeforeEach(() => db(\"inventory\").truncate());\n\ndescribe(\"addItemToCart\", () => {\n  beforeEach(() => {\n    fs.writeFileSync(\"/tmp/logs.out\", \"\");\n  });\n\n  test(\"adding unavailable items to cart\", async () => {\n    await db(\"users\").insert({\n      username: \"test_user\",\n      email: \"test_user@example.org\",\n      passwordHash: hashPassword(\"a_password\")\n    });\n\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 0 });\n\n    try {\n      await addItemToCart(\"test_user\", \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(\"cheesecake is unavailable\");\n      expectedError.code = 400;\n\n      expect(e).toEqual(expectedError);\n    }\n\n    const finalCartContent = await db\n      .select(\"carts_items.*\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", \"test_user\");\n\n    expect(finalCartContent).toEqual([]);\n    expect.assertions(2);\n  });\n\n  test(\"adding items above limit to cart\", async () => {\n    await db(\"users\").insert({\n      username: \"test_user\",\n      email: \"test_user@example.org\",\n      passwordHash: hashPassword(\"a_password\")\n    });\n\n    const { id: userId } = await db\n      .select()\n      .from(\"users\")\n      .where({ username: \"test_user\" })\n      .first();\n\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 1 });\n    await db(\"carts_items\").insert({\n      userId,\n      itemName: \"cheesecake\",\n      quantity: 3\n    });\n\n    try {\n      await addItemToCart(\"test_user\", \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(\n        \"You can't have more than three units of an item in your cart\"\n      );\n      expectedError.code = 400;\n      expect(e).toEqual(expectedError);\n    }\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", \"test_user\");\n\n    expect(finalCartContent).toEqual([{ itemName: \"cheesecake\", quantity: 3 }]);\n    expect.assertions(2);\n  });\n\n  test(\"logging added items\", async () => {\n    await db(\"users\").insert({\n      username: \"test_user\",\n      email: \"test_user@example.org\",\n      passwordHash: hashPassword(\"a_password\")\n    });\n\n    const { id: userId } = await db\n      .select()\n      .from(\"users\")\n      .where({ username: \"test_user\" })\n      .first();\n\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 1 });\n    await db(\"carts_items\").insert({\n      userId,\n      itemName: \"cheesecake\",\n      quantity: 1\n    });\n\n    await addItemToCart(\"test_user\", \"cheesecake\");\n\n    const logs = fs.readFileSync(\"/tmp/logs.out\", \"utf-8\");\n    expect(logs).toContain(\"cheesecake added to test_user's cart\\n\");\n  });\n});\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/1_database_integrations/dbConnection.js",
    "content": "const knex = require(\"knex\");\nconst knexConfig = require(\"./knexfile\").development;\n\nconst db = knex(knexConfig);\n\nconst closeConnection = () => db.destroy();\n\nmodule.exports = {\n  db,\n  closeConnection\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/1_database_integrations/inventoryController.js",
    "content": "const { db } = require(\"./dbConnection\");\n\nconst removeFromInventory = async itemName => {\n  const inventoryEntry = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName })\n    .first();\n\n  if (!inventoryEntry || inventoryEntry.quantity === 0) {\n    const err = new Error(`${itemName} is unavailable`);\n    err.code = 400;\n    throw err;\n  }\n\n  await db(\"inventory\")\n    .decrement(\"quantity\")\n    .where({ itemName });\n};\n\nmodule.exports = { removeFromInventory };\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/1_database_integrations/knexfile.js",
    "content": "module.exports = {\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"./dev.sqlite\" },\n    useNullAsDefault: true\n  }\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/1_database_integrations/logger.js",
    "content": "const fs = require(\"fs\");\n\nconst logger = {\n  log: msg => fs.appendFileSync(\"/tmp/logs.out\", msg + \"\\n\")\n};\n\nmodule.exports = logger;\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/1_database_integrations/migrations/20200325082401_initial_schema.js",
    "content": "exports.up = async knex => {\n  await knex.schema.createTable(\"users\", table => {\n    table.increments(\"id\");\n    table.string(\"username\");\n    table.unique(\"username\");\n    table.string(\"email\");\n    table.string(\"passwordHash\");\n  });\n\n  await knex.schema.createTable(\"carts_items\", table => {\n    table.integer(\"userId\").references(\"users.id\");\n    table.string(\"itemName\");\n    table.unique(\"itemName\");\n    table.integer(\"quantity\");\n  });\n\n  await knex.schema.createTable(\"inventory\", table => {\n    table.increments(\"id\");\n    table.string(\"itemName\");\n    table.unique(\"itemName\");\n    table.integer(\"quantity\");\n  });\n};\n\nexports.down = async knex => {\n  await knex.schema.dropTable(\"users\");\n  await knex.schema.dropTable(\"carts_items\");\n  await knex.schema.dropTable(\"inventory\");\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/1_database_integrations/package.json",
    "content": "{\n  \"name\": \"1_database_integrations\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"test\": \"jest --runInBand\"\n  },\n  \"devDependencies\": {\n    \"jest\": \"^24.9.0\",\n    \"supertest\": \"^4.0.2\"\n  },\n  \"dependencies\": {\n    \"knex\": \"^0.20.13\",\n    \"koa\": \"^2.11.0\",\n    \"koa-body-parser\": \"^1.1.2\",\n    \"koa-router\": \"^7.4.0\",\n    \"sqlite3\": \"^4.1.1\"\n  }\n}\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/1_database_integrations/server.js",
    "content": "const Koa = require(\"koa\");\nconst Router = require(\"koa-router\");\nconst bodyParser = require(\"koa-body-parser\");\n\nconst { db } = require(\"./dbConnection\");\n\nconst { addItemToCart } = require(\"./cartController\");\nconst {\n  hashPassword,\n  authenticationMiddleware\n} = require(\"./authenticationController\");\n\nconst app = new Koa();\nconst router = new Router();\n\napp.use(bodyParser());\n\napp.use(async (ctx, next) => {\n  if (ctx.url.startsWith(\"/carts\")) {\n    return await authenticationMiddleware(ctx, next);\n  }\n\n  await next();\n});\n\nrouter.put(\"/users/:username\", async ctx => {\n  const { username } = ctx.params;\n  const { email, password } = ctx.request.body;\n\n  const userAlreadyExists = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n\n  if (userAlreadyExists) {\n    ctx.body = { message: `${username} already exists` };\n    ctx.status = 409;\n    return;\n  }\n\n  await db(\"users\").insert({\n    username,\n    email,\n    passwordHash: hashPassword(password)\n  });\n\n  return (ctx.body = { message: `${username} created successfully` });\n});\n\nrouter.post(\"/carts/:username/items\", async ctx => {\n  const { username } = ctx.params;\n  const { item, quantity } = ctx.request.body;\n\n  for (let i = 0; i < quantity; i++) {\n    try {\n      const newItems = await addItemToCart(username, item);\n      ctx.body = newItems;\n    } catch (e) {\n      ctx.body = { message: e.message };\n      ctx.status = e.code;\n      return;\n    }\n  }\n});\n\nrouter.delete(\"/carts/:username/items/:item\", async ctx => {\n  const { username, item } = ctx.params;\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n\n  if (!user) {\n    ctx.body = { message: \"user not found\" };\n    ctx.status = 404;\n    return;\n  }\n\n  const itemEntry = await db\n    .select()\n    .from(\"carts_items\")\n    .where({ userId: user.id, itemName: item })\n    .first();\n\n  if (!itemEntry || itemEntry.quantity === 0) {\n    ctx.body = { message: `${item} is not in the cart` };\n    ctx.status = 400;\n    return;\n  }\n\n  await db(\"carts_items\")\n    .decrement(\"quantity\")\n    .where({ userId: user.id, itemName: item });\n\n  const inventoryEntry = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName: item })\n    .first();\n  if (inventoryEntry) {\n    await db(\"inventory\")\n      .increment(\"quantity\")\n      .where({ userId: itemEntry.userId, itemName: item });\n  } else {\n    await db(\"inventory\").insert({ itemName: item, quantity: 1 });\n  }\n\n  ctx.body = await db\n    .select(\"itemName\", \"quantity\")\n    .from(\"carts_items\")\n    .where({ userId: user.id });\n});\n\napp.use(router.routes());\n\nmodule.exports = { app: app.listen(3000) };\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/1_database_integrations/server.test.js",
    "content": "const { db, closeConnection } = require(\"./dbConnection\");\nconst request = require(\"supertest\");\nconst { app } = require(\"./server.js\");\nconst { hashPassword } = require(\"./authenticationController.js\");\n\nafterAll(() => app.close());\n\nbeforeEach(() => db(\"users\").truncate());\nbeforeEach(() => db(\"carts_items\").truncate());\nbeforeEach(() => db(\"inventory\").truncate());\n\nconst username = \"test_user\";\nconst password = \"a_password\";\nconst validAuth = Buffer.from(`${username}:${password}`).toString(\"base64\");\nconst authHeader = `Basic ${validAuth}`;\nconst createUser = async () => {\n  return await db(\"users\").insert({\n    username,\n    email: \"test_user@example.org\",\n    passwordHash: hashPassword(password)\n  });\n};\n\ndescribe(\"add items to a cart\", () => {\n  beforeEach(createUser);\n\n  test(\"adding available items\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 3 });\n    const response = await request(app)\n      .post(\"/carts/test_user/items\")\n      .set(\"authorization\", authHeader)\n      .send({ item: \"cheesecake\", quantity: 3 })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    const newItems = [{ itemName: \"cheesecake\", quantity: 3 }];\n    expect(response.body).toEqual(newItems);\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(0);\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", \"test_user\");\n\n    expect(finalCartContent).toEqual(newItems);\n  });\n\n  test(\"adding unavailable items\", async () => {\n    const response = await request(app)\n      .post(\"/carts/test_user/items\")\n      .set(\"authorization\", authHeader)\n      .send({ item: \"cheesecake\", quantity: 1 })\n      .expect(400)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"cheesecake is unavailable\"\n    });\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", \"test_user\");\n    expect(finalCartContent).toEqual([]);\n  });\n});\n\ndescribe(\"removing items from a cart\", () => {\n  let userId;\n  beforeEach(async () => {\n    userId = (await createUser())[0];\n  });\n\n  test(\"removing existing items\", async () => {\n    await db(\"carts_items\").insert({\n      userId,\n      itemName: \"cheesecake\",\n      quantity: 1\n    });\n\n    const response = await request(app)\n      .del(\"/carts/test_user/items/cheesecake\")\n      .set(\"authorization\", authHeader)\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    const expectedFinalContent = [{ itemName: \"cheesecake\", quantity: 0 }];\n\n    expect(response.body).toEqual(expectedFinalContent);\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", \"test_user\");\n    expect(finalCartContent).toEqual(expectedFinalContent);\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(1);\n  });\n\n  test(\"removing non-existing items\", async () => {\n    await db(\"inventory\").insert({\n      itemName: \"cheesecake\",\n      quantity: 0\n    });\n\n    const response = await request(app)\n      .del(\"/carts/test_user/items/cheesecake\")\n      .set(\"authorization\", authHeader)\n      .expect(400)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"cheesecake is not in the cart\"\n    });\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(0);\n  });\n});\n\ndescribe(\"create accounts\", () => {\n  test(\"creating a new account\", async () => {\n    const response = await request(app)\n      .put(\"/users/test_user\")\n      .send({ email: \"test_user@example.org\", password: \"a_password\" })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"test_user created successfully\"\n    });\n\n    const savedUser = await db\n      .select(\"email\", \"passwordHash\")\n      .from(\"users\")\n      .where({ username: \"test_user\" })\n      .first();\n\n    expect(savedUser).toEqual({\n      email: \"test_user@example.org\",\n      passwordHash: hashPassword(\"a_password\")\n    });\n  });\n\n  test(\"creating a duplicate account\", async () => {\n    await createUser();\n\n    const response = await request(app)\n      .put(\"/users/test_user\")\n      .send({ email: \"test_user@example.org\", password: \"a_password\" })\n      .expect(409)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"test_user already exists\"\n    });\n  });\n});\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/2_separate_database_instances/authenticationController.js",
    "content": "const crypto = require(\"crypto\");\nconst { db } = require(\"./dbConnection\");\n\nconst hashPassword = password => {\n  const hash = crypto.createHash(\"sha256\");\n  hash.update(password);\n  return hash.digest(\"hex\");\n};\n\nconst credentialsAreValid = async (username, password) => {\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  if (!user) return false;\n  return hashPassword(password) === user.passwordHash;\n};\n\nconst authenticationMiddleware = async (ctx, next) => {\n  try {\n    const authHeader = ctx.request.headers.authorization;\n    const credentials = Buffer.from(\n      authHeader.slice(\"basic\".length + 1),\n      \"base64\"\n    ).toString();\n    const [username, password] = credentials.split(\":\");\n\n    const validCredentialsSent = await credentialsAreValid(username, password);\n    if (!validCredentialsSent) throw new Error(\"invalid credentials\");\n  } catch (e) {\n    ctx.status = 401;\n    ctx.body = { message: \"please provide valid credentials\" };\n    return;\n  }\n\n  await next();\n};\n\nmodule.exports = {\n  hashPassword,\n  credentialsAreValid,\n  authenticationMiddleware\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/2_separate_database_instances/authenticationController.test.js",
    "content": "const { db, closeConnection } = require(\"./dbConnection\");\nconst crypto = require(\"crypto\");\nconst {\n  hashPassword,\n  credentialsAreValid,\n  authenticationMiddleware\n} = require(\"./authenticationController\");\n\nbeforeEach(() => db(\"users\").truncate());\n\ndescribe(\"hashPassword\", () => {\n  test(\"hashing passwords\", () => {\n    const plainTextPassword = \"password_example\";\n    const hash = crypto.createHash(\"sha256\");\n    hash.update(plainTextPassword);\n    const expectedHash = hash.digest(\"hex\");\n    expect(hashPassword(plainTextPassword)).toBe(expectedHash);\n  });\n});\n\ndescribe(\"credentialsAreValid\", () => {\n  test(\"validating credentials\", async () => {\n    await db(\"users\").insert({\n      username: \"test_user\",\n      email: \"test_user@example.org\",\n      passwordHash: hashPassword(\"a_password\")\n    });\n\n    expect(await credentialsAreValid(\"test_user\", \"a_password\")).toBe(true);\n  });\n});\n\ndescribe(\"authenticationMiddleware\", () => {\n  test(\"returning an error if the credentials are not valid\", async () => {\n    const fakeAuth = Buffer.from(\"invalid:credentials\").toString(\"base64\");\n    const ctx = {\n      request: {\n        headers: { authorization: `Basic ${fakeAuth}` }\n      }\n    };\n\n    const next = jest.fn();\n    await authenticationMiddleware(ctx, next);\n    expect(next.mock.calls).toHaveLength(0);\n    expect(ctx).toEqual({\n      ...ctx,\n      status: 401,\n      body: { message: \"please provide valid credentials\" }\n    });\n  });\n\n  test(\"authenticating properly\", async () => {\n    await db(\"users\").insert({\n      username: \"test_user\",\n      email: \"test_user@example.org\",\n      passwordHash: hashPassword(\"a_password\")\n    });\n\n    const validAuth = Buffer.from(\"test_user:a_password\").toString(\"base64\");\n    const ctx = {\n      request: {\n        headers: { authorization: `Basic ${validAuth}` }\n      }\n    };\n\n    const next = jest.fn();\n    await authenticationMiddleware(ctx, next);\n    expect(next.mock.calls).toHaveLength(1);\n  });\n});\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/2_separate_database_instances/cartController.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { removeFromInventory } = require(\"./inventoryController\");\nconst logger = require(\"./logger\");\n\nconst addItemToCart = async (username, itemName) => {\n  await removeFromInventory(itemName);\n\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  if (!user) {\n    const userNotFound = new Error(\"user not found\");\n    userNotFound.code = 404;\n  }\n\n  const itemEntry = await db\n    .select()\n    .from(\"carts_items\")\n    .where({ userId: user.id, itemName })\n    .first();\n\n  if (itemEntry && itemEntry.quantity + 1 > 3) {\n    const limitError = new Error(\n      \"You can't have more than three units of an item in your cart\"\n    );\n    limitError.code = 400;\n    throw limitError;\n  }\n\n  if (itemEntry) {\n    await db(\"carts_items\")\n      .increment(\"quantity\")\n      .where({ userId: itemEntry.userId, itemName });\n  } else {\n    await db(\"carts_items\").insert({\n      userId: user.id,\n      itemName,\n      quantity: 1\n    });\n  }\n\n  logger.log(`${itemName} added to ${username}'s cart`);\n  return db\n    .select(\"itemName\", \"quantity\")\n    .from(\"carts_items\")\n    .where({ userId: user.id });\n};\n\nmodule.exports = { addItemToCart };\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/2_separate_database_instances/cartController.test.js",
    "content": "const { db, closeConnection } = require(\"./dbConnection\");\nconst { addItemToCart } = require(\"./cartController\");\nconst { hashPassword } = require(\"./authenticationController\");\n\nconst fs = require(\"fs\");\n\nbeforeEach(() => db(\"users\").truncate());\nbeforeEach(() => db(\"carts_items\").truncate());\nbeforeEach(() => db(\"inventory\").truncate());\n\ndescribe(\"addItemToCart\", () => {\n  beforeEach(() => {\n    fs.writeFileSync(\"/tmp/logs.out\", \"\");\n  });\n\n  test(\"adding unavailable items to cart\", async () => {\n    await db(\"users\").insert({\n      username: \"test_user\",\n      email: \"test_user@example.org\",\n      passwordHash: hashPassword(\"a_password\")\n    });\n\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 0 });\n\n    try {\n      await addItemToCart(\"test_user\", \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(\"cheesecake is unavailable\");\n      expectedError.code = 400;\n\n      expect(e).toEqual(expectedError);\n    }\n\n    const finalCartContent = await db\n      .select(\"carts_items.*\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", \"test_user\");\n\n    expect(finalCartContent).toEqual([]);\n    expect.assertions(2);\n  });\n\n  test(\"adding items above limit to cart\", async () => {\n    await db(\"users\").insert({\n      username: \"test_user\",\n      email: \"test_user@example.org\",\n      passwordHash: hashPassword(\"a_password\")\n    });\n\n    const { id: userId } = await db\n      .select()\n      .from(\"users\")\n      .where({ username: \"test_user\" })\n      .first();\n\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 1 });\n    await db(\"carts_items\").insert({\n      userId,\n      itemName: \"cheesecake\",\n      quantity: 3\n    });\n\n    try {\n      await addItemToCart(\"test_user\", \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(\n        \"You can't have more than three units of an item in your cart\"\n      );\n      expectedError.code = 400;\n      expect(e).toEqual(expectedError);\n    }\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", \"test_user\");\n\n    expect(finalCartContent).toEqual([{ itemName: \"cheesecake\", quantity: 3 }]);\n    expect.assertions(2);\n  });\n\n  test(\"logging added items\", async () => {\n    await db(\"users\").insert({\n      username: \"test_user\",\n      email: \"test_user@example.org\",\n      passwordHash: hashPassword(\"a_password\")\n    });\n\n    const { id: userId } = await db\n      .select()\n      .from(\"users\")\n      .where({ username: \"test_user\" })\n      .first();\n\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 1 });\n    await db(\"carts_items\").insert({\n      userId,\n      itemName: \"cheesecake\",\n      quantity: 1\n    });\n\n    await addItemToCart(\"test_user\", \"cheesecake\");\n\n    const logs = fs.readFileSync(\"/tmp/logs.out\", \"utf-8\");\n    expect(logs).toContain(\"cheesecake added to test_user's cart\\n\");\n  });\n});\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/2_separate_database_instances/dbConnection.js",
    "content": "const knex = require(\"knex\");\nconst knexConfig = require(\"./knexfile\").development;\n\nconst db = knex(knexConfig);\n\nconst closeConnection = () => db.destroy();\n\nmodule.exports = {\n  db,\n  closeConnection\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/2_separate_database_instances/inventoryController.js",
    "content": "const { db } = require(\"./dbConnection\");\n\nconst removeFromInventory = async itemName => {\n  const inventoryEntry = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName })\n    .first();\n\n  if (!inventoryEntry || inventoryEntry.quantity === 0) {\n    const err = new Error(`${itemName} is unavailable`);\n    err.code = 400;\n    throw err;\n  }\n\n  await db(\"inventory\")\n    .decrement(\"quantity\")\n    .where({ itemName });\n};\n\nmodule.exports = { removeFromInventory };\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/2_separate_database_instances/knexfile.js",
    "content": "module.exports = {\n  test: {\n    client: \"sqlite3\",\n    connection: { filename: \"./test.sqlite\" },\n    useNullAsDefault: true\n  },\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"./dev.sqlite\" },\n    useNullAsDefault: true\n  }\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/2_separate_database_instances/logger.js",
    "content": "const fs = require(\"fs\");\n\nconst logger = {\n  log: msg => fs.appendFileSync(\"/tmp/logs.out\", msg + \"\\n\")\n};\n\nmodule.exports = logger;\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/2_separate_database_instances/migrations/20200325082401_initial_schema.js",
    "content": "exports.up = async knex => {\n  await knex.schema.createTable(\"users\", table => {\n    table.increments(\"id\");\n    table.string(\"username\");\n    table.unique(\"username\");\n    table.string(\"email\");\n    table.string(\"passwordHash\");\n  });\n\n  await knex.schema.createTable(\"carts_items\", table => {\n    table.integer(\"userId\").references(\"users.id\");\n    table.string(\"itemName\");\n    table.unique(\"itemName\");\n    table.integer(\"quantity\");\n  });\n\n  await knex.schema.createTable(\"inventory\", table => {\n    table.increments(\"id\");\n    table.string(\"itemName\");\n    table.unique(\"itemName\");\n    table.integer(\"quantity\");\n  });\n};\n\nexports.down = async knex => {\n  await knex.schema.dropTable(\"users\");\n  await knex.schema.dropTable(\"carts_items\");\n  await knex.schema.dropTable(\"inventory\");\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/2_separate_database_instances/package.json",
    "content": "{\n  \"name\": \"2_separate_database_instances\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"test\": \"jest --runInBand\"\n  },\n  \"devDependencies\": {\n    \"jest\": \"^24.9.0\",\n    \"supertest\": \"^4.0.2\"\n  },\n  \"dependencies\": {\n    \"knex\": \"^0.20.13\",\n    \"koa\": \"^2.11.0\",\n    \"koa-body-parser\": \"^1.1.2\",\n    \"koa-router\": \"^7.4.0\",\n    \"sqlite3\": \"^4.1.1\"\n  }\n}\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/2_separate_database_instances/server.js",
    "content": "const Koa = require(\"koa\");\nconst Router = require(\"koa-router\");\nconst bodyParser = require(\"koa-body-parser\");\n\nconst { db } = require(\"./dbConnection\");\n\nconst { addItemToCart } = require(\"./cartController\");\nconst {\n  hashPassword,\n  authenticationMiddleware\n} = require(\"./authenticationController\");\n\nconst app = new Koa();\nconst router = new Router();\n\napp.use(bodyParser());\n\napp.use(async (ctx, next) => {\n  if (ctx.url.startsWith(\"/carts\")) {\n    return await authenticationMiddleware(ctx, next);\n  }\n\n  await next();\n});\n\nrouter.put(\"/users/:username\", async ctx => {\n  const { username } = ctx.params;\n  const { email, password } = ctx.request.body;\n\n  const userAlreadyExists = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n\n  if (userAlreadyExists) {\n    ctx.body = { message: `${username} already exists` };\n    ctx.status = 409;\n    return;\n  }\n\n  await db(\"users\").insert({\n    username,\n    email,\n    passwordHash: hashPassword(password)\n  });\n\n  return (ctx.body = { message: `${username} created successfully` });\n});\n\nrouter.post(\"/carts/:username/items\", async ctx => {\n  const { username } = ctx.params;\n  const { item, quantity } = ctx.request.body;\n\n  for (let i = 0; i < quantity; i++) {\n    try {\n      const newItems = await addItemToCart(username, item);\n      ctx.body = newItems;\n    } catch (e) {\n      ctx.body = { message: e.message };\n      ctx.status = e.code;\n      return;\n    }\n  }\n});\n\nrouter.delete(\"/carts/:username/items/:item\", async ctx => {\n  const { username, item } = ctx.params;\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n\n  if (!user) {\n    ctx.body = { message: \"user not found\" };\n    ctx.status = 404;\n    return;\n  }\n\n  const itemEntry = await db\n    .select()\n    .from(\"carts_items\")\n    .where({ userId: user.id, itemName: item })\n    .first();\n\n  if (!itemEntry || itemEntry.quantity === 0) {\n    ctx.body = { message: `${item} is not in the cart` };\n    ctx.status = 400;\n    return;\n  }\n\n  await db(\"carts_items\")\n    .decrement(\"quantity\")\n    .where({ userId: user.id, itemName: item });\n\n  const inventoryEntry = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName: item })\n    .first();\n  if (inventoryEntry) {\n    await db(\"inventory\")\n      .increment(\"quantity\")\n      .where({ userId: itemEntry.userId, itemName: item });\n  } else {\n    await db(\"inventory\").insert({ itemName: item, quantity: 1 });\n  }\n\n  ctx.body = await db\n    .select(\"itemName\", \"quantity\")\n    .from(\"carts_items\")\n    .where({ userId: user.id });\n});\n\napp.use(router.routes());\n\nmodule.exports = { app: app.listen(3000) };\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/2_separate_database_instances/server.test.js",
    "content": "const { db, closeConnection } = require(\"./dbConnection\");\nconst request = require(\"supertest\");\nconst { app } = require(\"./server.js\");\nconst { hashPassword } = require(\"./authenticationController.js\");\n\nafterAll(() => app.close());\n\nbeforeEach(() => db(\"users\").truncate());\nbeforeEach(() => db(\"carts_items\").truncate());\nbeforeEach(() => db(\"inventory\").truncate());\n\nconst username = \"test_user\";\nconst password = \"a_password\";\nconst validAuth = Buffer.from(`${username}:${password}`).toString(\"base64\");\nconst authHeader = `Basic ${validAuth}`;\nconst createUser = async () => {\n  return await db(\"users\").insert({\n    username,\n    email: \"test_user@example.org\",\n    passwordHash: hashPassword(password)\n  });\n};\n\ndescribe(\"add items to a cart\", () => {\n  beforeEach(createUser);\n\n  test(\"adding available items\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 3 });\n    const response = await request(app)\n      .post(\"/carts/test_user/items\")\n      .set(\"authorization\", authHeader)\n      .send({ item: \"cheesecake\", quantity: 3 })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    const newItems = [{ itemName: \"cheesecake\", quantity: 3 }];\n    expect(response.body).toEqual(newItems);\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(0);\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", \"test_user\");\n\n    expect(finalCartContent).toEqual(newItems);\n  });\n\n  test(\"adding unavailable items\", async () => {\n    const response = await request(app)\n      .post(\"/carts/test_user/items\")\n      .set(\"authorization\", authHeader)\n      .send({ item: \"cheesecake\", quantity: 1 })\n      .expect(400)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"cheesecake is unavailable\"\n    });\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", \"test_user\");\n    expect(finalCartContent).toEqual([]);\n  });\n});\n\ndescribe(\"removing items from a cart\", () => {\n  let userId;\n  beforeEach(async () => {\n    userId = (await createUser())[0];\n  });\n\n  test(\"removing existing items\", async () => {\n    await db(\"carts_items\").insert({\n      userId,\n      itemName: \"cheesecake\",\n      quantity: 1\n    });\n\n    const response = await request(app)\n      .del(\"/carts/test_user/items/cheesecake\")\n      .set(\"authorization\", authHeader)\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    const expectedFinalContent = [{ itemName: \"cheesecake\", quantity: 0 }];\n\n    expect(response.body).toEqual(expectedFinalContent);\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", \"test_user\");\n    expect(finalCartContent).toEqual(expectedFinalContent);\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(1);\n  });\n\n  test(\"removing non-existing items\", async () => {\n    await db(\"inventory\").insert({\n      itemName: \"cheesecake\",\n      quantity: 0\n    });\n\n    const response = await request(app)\n      .del(\"/carts/test_user/items/cheesecake\")\n      .set(\"authorization\", authHeader)\n      .expect(400)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"cheesecake is not in the cart\"\n    });\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(0);\n  });\n});\n\ndescribe(\"create accounts\", () => {\n  test(\"creating a new account\", async () => {\n    const response = await request(app)\n      .put(\"/users/test_user\")\n      .send({ email: \"test_user@example.org\", password: \"a_password\" })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"test_user created successfully\"\n    });\n\n    const savedUser = await db\n      .select(\"email\", \"passwordHash\")\n      .from(\"users\")\n      .where({ username: \"test_user\" })\n      .first();\n\n    expect(savedUser).toEqual({\n      email: \"test_user@example.org\",\n      passwordHash: hashPassword(\"a_password\")\n    });\n  });\n\n  test(\"creating a duplicate account\", async () => {\n    await createUser();\n\n    const response = await request(app)\n      .put(\"/users/test_user\")\n      .send({ email: \"test_user@example.org\", password: \"a_password\" })\n      .expect(409)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"test_user already exists\"\n    });\n  });\n});\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/authenticationController.js",
    "content": "const crypto = require(\"crypto\");\nconst { db } = require(\"./dbConnection\");\n\nconst hashPassword = password => {\n  const hash = crypto.createHash(\"sha256\");\n  hash.update(password);\n  return hash.digest(\"hex\");\n};\n\nconst credentialsAreValid = async (username, password) => {\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  if (!user) return false;\n  return hashPassword(password) === user.passwordHash;\n};\n\nconst authenticationMiddleware = async (ctx, next) => {\n  try {\n    const authHeader = ctx.request.headers.authorization;\n    const credentials = Buffer.from(\n      authHeader.slice(\"basic\".length + 1),\n      \"base64\"\n    ).toString();\n    const [username, password] = credentials.split(\":\");\n\n    const validCredentialsSent = await credentialsAreValid(username, password);\n    if (!validCredentialsSent) throw new Error(\"invalid credentials\");\n  } catch (e) {\n    ctx.status = 401;\n    ctx.body = { message: \"please provide valid credentials\" };\n    return;\n  }\n\n  await next();\n};\n\nmodule.exports = {\n  hashPassword,\n  credentialsAreValid,\n  authenticationMiddleware\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/authenticationController.test.js",
    "content": "const crypto = require(\"crypto\");\nconst {\n  hashPassword,\n  credentialsAreValid,\n  authenticationMiddleware\n} = require(\"./authenticationController\");\nconst { user: globalUser } = require(\"./userTestUtils\");\n\ndescribe(\"hashPassword\", () => {\n  test(\"hashing passwords\", () => {\n    const plainTextPassword = \"password_example\";\n    const hash = crypto.createHash(\"sha256\");\n    hash.update(plainTextPassword);\n    const expectedHash = hash.digest(\"hex\");\n    expect(hashPassword(plainTextPassword)).toBe(expectedHash);\n  });\n});\n\ndescribe(\"credentialsAreValid\", () => {\n  test(\"validating credentials\", async () => {\n    expect(await credentialsAreValid(globalUser.username, \"a_password\")).toBe(\n      true\n    );\n  });\n});\n\ndescribe(\"authenticationMiddleware\", () => {\n  test(\"returning an error if the credentials are not valid\", async () => {\n    const fakeAuth = Buffer.from(\"invalid:credentials\").toString(\"base64\");\n    const ctx = {\n      request: {\n        headers: { authorization: `Basic ${fakeAuth}` }\n      }\n    };\n\n    const next = jest.fn();\n    await authenticationMiddleware(ctx, next);\n    expect(next.mock.calls).toHaveLength(0);\n    expect(ctx).toEqual({\n      ...ctx,\n      status: 401,\n      body: { message: \"please provide valid credentials\" }\n    });\n  });\n\n  test(\"authenticating properly\", async () => {\n    const ctx = {\n      request: {\n        headers: { authorization: globalUser.authHeader }\n      }\n    };\n\n    const next = jest.fn();\n    await authenticationMiddleware(ctx, next);\n    expect(next.mock.calls).toHaveLength(1);\n  });\n});\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/cartController.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { removeFromInventory } = require(\"./inventoryController\");\nconst logger = require(\"./logger\");\n\nconst addItemToCart = async (username, itemName) => {\n  await removeFromInventory(itemName);\n\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  if (!user) {\n    const userNotFound = new Error(\"user not found\");\n    userNotFound.code = 404;\n  }\n\n  const itemEntry = await db\n    .select()\n    .from(\"carts_items\")\n    .where({ userId: user.id, itemName })\n    .first();\n\n  if (itemEntry && itemEntry.quantity + 1 > 3) {\n    const limitError = new Error(\n      \"You can't have more than three units of an item in your cart\"\n    );\n    limitError.code = 400;\n    throw limitError;\n  }\n\n  if (itemEntry) {\n    await db(\"carts_items\")\n      .increment(\"quantity\")\n      .where({ userId: itemEntry.userId, itemName });\n  } else {\n    await db(\"carts_items\").insert({\n      userId: user.id,\n      itemName,\n      quantity: 1\n    });\n  }\n\n  logger.log(`${itemName} added to ${username}'s cart`);\n  return db\n    .select(\"itemName\", \"quantity\")\n    .from(\"carts_items\")\n    .where({ userId: user.id });\n};\n\nmodule.exports = { addItemToCart };\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/cartController.test.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { addItemToCart } = require(\"./cartController\");\nconst { hashPassword } = require(\"./authenticationController\");\nconst { user: globalUser } = require(\"./userTestUtils\");\n\nconst fs = require(\"fs\");\n\ndescribe(\"addItemToCart\", () => {\n  beforeEach(() => {\n    fs.writeFileSync(\"/tmp/logs.out\", \"\");\n  });\n\n  test(\"adding unavailable items to cart\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 0 });\n\n    try {\n      await addItemToCart(globalUser.username, \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(\"cheesecake is unavailable\");\n      expectedError.code = 400;\n\n      expect(e).toEqual(expectedError);\n    }\n\n    const finalCartContent = await db\n      .select(\"carts_items.*\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n\n    expect(finalCartContent).toEqual([]);\n    expect.assertions(2);\n  });\n\n  test(\"adding items above limit to cart\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 1 });\n    await db(\"carts_items\").insert({\n      userId: globalUser.id,\n      itemName: \"cheesecake\",\n      quantity: 3\n    });\n\n    try {\n      await addItemToCart(globalUser.username, \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(\n        \"You can't have more than three units of an item in your cart\"\n      );\n      expectedError.code = 400;\n      expect(e).toEqual(expectedError);\n    }\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n\n    expect(finalCartContent).toEqual([{ itemName: \"cheesecake\", quantity: 3 }]);\n    expect.assertions(2);\n  });\n\n  test(\"logging added items\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 1 });\n    await db(\"carts_items\").insert({\n      userId: globalUser.id,\n      itemName: \"cheesecake\",\n      quantity: 1\n    });\n\n    await addItemToCart(globalUser.username, \"cheesecake\");\n\n    const logs = fs.readFileSync(\"/tmp/logs.out\", \"utf-8\");\n    expect(logs).toContain(\n      `cheesecake added to ${globalUser.username}'s cart\\n`\n    );\n  });\n});\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/dbConnection.js",
    "content": "const environmentName = process.env.NODE_ENV;\nconst knex = require(\"knex\");\nconst knexConfig = require(\"./knexfile\")[environmentName];\n\nconst db = knex(knexConfig);\n\nconst closeConnection = () => db.destroy();\n\nmodule.exports = {\n  db,\n  closeConnection\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/disconnectFromDb.js",
    "content": "const { db } = require(\"./dbConnection\");\n\nafterAll(() => db.destroy());\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/inventoryController.js",
    "content": "const { db } = require(\"./dbConnection\");\n\nconst removeFromInventory = async itemName => {\n  const inventoryEntry = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName })\n    .first();\n\n  if (!inventoryEntry || inventoryEntry.quantity === 0) {\n    const err = new Error(`${itemName} is unavailable`);\n    err.code = 400;\n    throw err;\n  }\n\n  await db(\"inventory\")\n    .decrement(\"quantity\")\n    .where({ itemName });\n};\n\nmodule.exports = { removeFromInventory };\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/jest.config.js",
    "content": "module.exports = {\n  testEnvironment: \"node\",\n  globalSetup: \"./migrateDatabases.js\",\n  setupFilesAfterEnv: [\n    \"<rootDir>/truncateTables.js\",\n    \"<rootDir>/seedUser.js\",\n    \"<rootDir>/disconnectFromDb.js\"\n  ]\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/knexfile.js",
    "content": "module.exports = {\n  test: {\n    client: \"sqlite3\",\n    connection: { filename: \"./test.sqlite\" },\n    useNullAsDefault: true\n  },\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"./dev.sqlite\" },\n    useNullAsDefault: true\n  }\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/logger.js",
    "content": "const fs = require(\"fs\");\n\nconst logger = {\n  log: msg => fs.appendFileSync(\"/tmp/logs.out\", msg + \"\\n\")\n};\n\nmodule.exports = logger;\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/migrateDatabases.js",
    "content": "const environmentName = process.env.NODE_ENV || \"test\";\nconst environmentConfig = require(\"./knexfile\")[environmentName];\nconst db = require(\"knex\")(environmentConfig);\n\nmodule.exports = async () => {\n  // Migrate the database to the latest state\n  await db.migrate.latest();\n\n  // Close the connection to the database so that tests won't hang\n  await db.destroy();\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/migrations/20200325082401_initial_schema.js",
    "content": "exports.up = async knex => {\n  await knex.schema.createTable(\"users\", table => {\n    table.increments(\"id\");\n    table.string(\"username\");\n    table.unique(\"username\");\n    table.string(\"email\");\n    table.string(\"passwordHash\");\n  });\n\n  await knex.schema.createTable(\"carts_items\", table => {\n    table.integer(\"userId\").references(\"users.id\");\n    table.string(\"itemName\");\n    table.unique(\"itemName\");\n    table.integer(\"quantity\");\n  });\n\n  await knex.schema.createTable(\"inventory\", table => {\n    table.increments(\"id\");\n    table.string(\"itemName\");\n    table.unique(\"itemName\");\n    table.integer(\"quantity\");\n  });\n};\n\nexports.down = async knex => {\n  await knex.schema.dropTable(\"users\");\n  await knex.schema.dropTable(\"carts_items\");\n  await knex.schema.dropTable(\"inventory\");\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/package.json",
    "content": "{\n  \"name\": \"3_maintaining_a_pristine_state\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"test\": \"jest --runInBand\"\n  },\n  \"devDependencies\": {\n    \"jest\": \"^24.9.0\",\n    \"supertest\": \"^4.0.2\"\n  },\n  \"dependencies\": {\n    \"knex\": \"^0.20.13\",\n    \"koa\": \"^2.11.0\",\n    \"koa-body-parser\": \"^1.1.2\",\n    \"koa-router\": \"^7.4.0\",\n    \"sqlite3\": \"^4.1.1\"\n  }\n}\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/seedUser.js",
    "content": "const { createUser } = require(\"./userTestUtils\");\n\nbeforeEach(createUser);\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/server.js",
    "content": "const Koa = require(\"koa\");\nconst Router = require(\"koa-router\");\nconst bodyParser = require(\"koa-body-parser\");\n\nconst { db } = require(\"./dbConnection\");\n\nconst { addItemToCart } = require(\"./cartController\");\nconst {\n  hashPassword,\n  authenticationMiddleware\n} = require(\"./authenticationController\");\n\nconst app = new Koa();\nconst router = new Router();\n\napp.use(bodyParser());\n\napp.use(async (ctx, next) => {\n  if (ctx.url.startsWith(\"/carts\")) {\n    return await authenticationMiddleware(ctx, next);\n  }\n\n  await next();\n});\n\nrouter.put(\"/users/:username\", async ctx => {\n  const { username } = ctx.params;\n  const { email, password } = ctx.request.body;\n\n  const userAlreadyExists = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n\n  if (userAlreadyExists) {\n    ctx.body = { message: `${username} already exists` };\n    ctx.status = 409;\n    return;\n  }\n\n  await db(\"users\").insert({\n    username,\n    email,\n    passwordHash: hashPassword(password)\n  });\n\n  return (ctx.body = { message: `${username} created successfully` });\n});\n\nrouter.post(\"/carts/:username/items\", async ctx => {\n  const { username } = ctx.params;\n  const { item, quantity } = ctx.request.body;\n\n  for (let i = 0; i < quantity; i++) {\n    try {\n      const newItems = await addItemToCart(username, item);\n      ctx.body = newItems;\n    } catch (e) {\n      ctx.body = { message: e.message };\n      ctx.status = e.code;\n      return;\n    }\n  }\n});\n\nrouter.delete(\"/carts/:username/items/:item\", async ctx => {\n  const { username, item } = ctx.params;\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n\n  if (!user) {\n    ctx.body = { message: \"user not found\" };\n    ctx.status = 404;\n    return;\n  }\n\n  const itemEntry = await db\n    .select()\n    .from(\"carts_items\")\n    .where({ userId: user.id, itemName: item })\n    .first();\n\n  if (!itemEntry || itemEntry.quantity === 0) {\n    ctx.body = { message: `${item} is not in the cart` };\n    ctx.status = 400;\n    return;\n  }\n\n  await db(\"carts_items\")\n    .decrement(\"quantity\")\n    .where({ userId: user.id, itemName: item });\n\n  const inventoryEntry = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName: item })\n    .first();\n  if (inventoryEntry) {\n    await db(\"inventory\")\n      .increment(\"quantity\")\n      .where({ userId: itemEntry.userId, itemName: item });\n  } else {\n    await db(\"inventory\").insert({ itemName: item, quantity: 1 });\n  }\n\n  ctx.body = await db\n    .select(\"itemName\", \"quantity\")\n    .from(\"carts_items\")\n    .where({ userId: user.id });\n});\n\napp.use(router.routes());\n\nmodule.exports = { app: app.listen(3000) };\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/server.test.js",
    "content": "const { user: globalUser } = require(\"./userTestUtils\");\nconst { db } = require(\"./dbConnection\");\nconst request = require(\"supertest\");\nconst { app } = require(\"./server.js\");\nconst { hashPassword } = require(\"./authenticationController.js\");\n\nafterAll(() => app.close());\n\ndescribe(\"add items to a cart\", () => {\n  test(\"adding available items\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 3 });\n    const response = await request(app)\n      .post(`/carts/${globalUser.username}/items`)\n      .set(\"authorization\", globalUser.authHeader)\n      .send({ item: \"cheesecake\", quantity: 3 })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    const newItems = [{ itemName: \"cheesecake\", quantity: 3 }];\n    expect(response.body).toEqual(newItems);\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(0);\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n\n    expect(finalCartContent).toEqual(newItems);\n  });\n\n  test(\"adding unavailable items\", async () => {\n    const response = await request(app)\n      .post(`/carts/${globalUser.username}/items`)\n      .set(\"authorization\", globalUser.authHeader)\n      .send({ item: \"cheesecake\", quantity: 1 })\n      .expect(400)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"cheesecake is unavailable\"\n    });\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n    expect(finalCartContent).toEqual([]);\n  });\n});\n\ndescribe(\"removing items from a cart\", () => {\n  test(\"removing existing items\", async () => {\n    await db(\"carts_items\").insert({\n      userId: globalUser.id,\n      itemName: \"cheesecake\",\n      quantity: 1\n    });\n\n    const response = await request(app)\n      .del(`/carts/${globalUser.username}/items/cheesecake`)\n      .set(\"authorization\", globalUser.authHeader)\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    const expectedFinalContent = [{ itemName: \"cheesecake\", quantity: 0 }];\n\n    expect(response.body).toEqual(expectedFinalContent);\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n    expect(finalCartContent).toEqual(expectedFinalContent);\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(1);\n  });\n\n  test(\"removing non-existing items\", async () => {\n    await db(\"inventory\").insert({\n      itemName: \"cheesecake\",\n      quantity: 0\n    });\n\n    const response = await request(app)\n      .del(`/carts/${globalUser.username}/items/cheesecake`)\n      .set(\"authorization\", globalUser.authHeader)\n      .expect(400)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"cheesecake is not in the cart\"\n    });\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(0);\n  });\n});\n\ndescribe(\"create accounts\", () => {\n  test(\"creating a new account\", async () => {\n    const response = await request(app)\n      .put(\"/users/another_user\")\n      .send({ email: \"another_user@example.org\", password: \"a_password\" })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"another_user created successfully\"\n    });\n\n    const savedUser = await db\n      .select(\"email\", \"passwordHash\")\n      .from(\"users\")\n      .where({ username: \"another_user\" })\n      .first();\n\n    expect(savedUser).toEqual({\n      email: \"another_user@example.org\",\n      passwordHash: hashPassword(\"a_password\")\n    });\n  });\n\n  test(\"creating a duplicate account\", async () => {\n    const response = await request(app)\n      .put(`/users/${globalUser.username}`)\n      .send({ email: globalUser.email, password: \"a_password\" })\n      .expect(409)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: `${globalUser.username} already exists`\n    });\n  });\n});\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/truncateTables.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst tablesToTruncate = [\"users\", \"inventory\", \"carts_items\"];\n\nbeforeEach(() => {\n  return Promise.all(tablesToTruncate.map(t => db(t).truncate()));\n});\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/userTestUtils.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { hashPassword } = require(\"./authenticationController\");\n\nconst username = \"test_user\";\nconst password = \"a_password\";\nconst passwordHash = hashPassword(password);\nconst email = \"test_user@example.org\";\nconst validAuth = Buffer.from(`${username}:${password}`).toString(\"base64\");\nconst authHeader = `Basic ${validAuth}`;\n\nconst user = {\n  username,\n  password,\n  email,\n  authHeader\n};\n\nconst createUser = async () => {\n  await db(\"users\").insert({ username, email, passwordHash });\n  const { id } = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  user.id = id;\n};\n\nmodule.exports = { user, createUser };\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/authenticationController.js",
    "content": "const { user: globalUser } = require(\"./userTestUtils\");\nconst crypto = require(\"crypto\");\nconst { db } = require(\"./dbConnection\");\n\nconst hashPassword = password => {\n  const hash = crypto.createHash(\"sha256\");\n  hash.update(password);\n  return hash.digest(\"hex\");\n};\n\nconst credentialsAreValid = async (username, password) => {\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  if (!user) return false;\n  return hashPassword(password) === user.passwordHash;\n};\n\nconst authenticationMiddleware = async (ctx, next) => {\n  try {\n    const authHeader = ctx.request.headers.authorization;\n    const credentials = Buffer.from(\n      authHeader.slice(\"basic\".length + 1),\n      \"base64\"\n    ).toString();\n    const [username, password] = credentials.split(\":\");\n\n    const validCredentialsSent = await credentialsAreValid(username, password);\n    if (!validCredentialsSent) throw new Error(\"invalid credentials\");\n  } catch (e) {\n    ctx.status = 401;\n    ctx.body = { message: \"please provide valid credentials\" };\n    return;\n  }\n\n  await next();\n};\n\nmodule.exports = {\n  hashPassword,\n  credentialsAreValid,\n  authenticationMiddleware\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/authenticationController.test.js",
    "content": "const crypto = require(\"crypto\");\nconst {\n  hashPassword,\n  credentialsAreValid,\n  authenticationMiddleware\n} = require(\"./authenticationController\");\nconst { user: globalUser } = require(\"./userTestUtils\");\n\ndescribe(\"hashPassword\", () => {\n  test(\"hashing passwords\", () => {\n    const plainTextPassword = \"password_example\";\n    const hash = crypto.createHash(\"sha256\");\n    hash.update(plainTextPassword);\n    const expectedHash = hash.digest(\"hex\");\n    expect(hashPassword(plainTextPassword)).toBe(expectedHash);\n  });\n});\n\ndescribe(\"credentialsAreValid\", () => {\n  test(\"validating credentials\", async () => {\n    expect(await credentialsAreValid(globalUser.username, \"a_password\")).toBe(\n      true\n    );\n  });\n});\n\ndescribe(\"authenticationMiddleware\", () => {\n  test(\"returning an error if the credentials are not valid\", async () => {\n    const fakeAuth = Buffer.from(\"invalid:credentials\").toString(\"base64\");\n    const ctx = {\n      request: {\n        headers: { authorization: `Basic ${fakeAuth}` }\n      }\n    };\n\n    const next = jest.fn();\n    await authenticationMiddleware(ctx, next);\n    expect(next.mock.calls).toHaveLength(0);\n    expect(ctx).toEqual({\n      ...ctx,\n      status: 401,\n      body: { message: \"please provide valid credentials\" }\n    });\n  });\n\n  test(\"authenticating properly\", async () => {\n    const ctx = {\n      request: {\n        headers: { authorization: globalUser.authHeader }\n      }\n    };\n\n    const next = jest.fn();\n    await authenticationMiddleware(ctx, next);\n    expect(next.mock.calls).toHaveLength(1);\n  });\n});\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/cartController.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { removeFromInventory } = require(\"./inventoryController\");\nconst logger = require(\"./logger\");\n\nconst addItemToCart = async (username, itemName) => {\n  await removeFromInventory(itemName);\n\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  if (!user) {\n    const userNotFound = new Error(\"user not found\");\n    userNotFound.code = 404;\n  }\n\n  const itemEntry = await db\n    .select()\n    .from(\"carts_items\")\n    .where({ userId: user.id, itemName })\n    .first();\n\n  if (itemEntry && itemEntry.quantity + 1 > 3) {\n    const limitError = new Error(\n      \"You can't have more than three units of an item in your cart\"\n    );\n    limitError.code = 400;\n    throw limitError;\n  }\n\n  if (itemEntry) {\n    await db(\"carts_items\")\n      .increment(\"quantity\")\n      .where({ userId: itemEntry.userId, itemName });\n  } else {\n    await db(\"carts_items\").insert({\n      userId: user.id,\n      itemName,\n      quantity: 1\n    });\n  }\n\n  logger.log(`${itemName} added to ${username}'s cart`);\n  return db\n    .select(\"itemName\", \"quantity\")\n    .from(\"carts_items\")\n    .where({ userId: user.id });\n};\n\nmodule.exports = { addItemToCart };\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/cartController.test.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { addItemToCart } = require(\"./cartController\");\nconst { hashPassword } = require(\"./authenticationController\");\nconst { user: globalUser } = require(\"./userTestUtils\");\n\nconst fs = require(\"fs\");\n\ndescribe(\"addItemToCart\", () => {\n  beforeEach(() => {\n    fs.writeFileSync(\"/tmp/logs.out\", \"\");\n  });\n\n  test(\"adding unavailable items to cart\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 0 });\n\n    try {\n      await addItemToCart(globalUser.username, \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(\"cheesecake is unavailable\");\n      expectedError.code = 400;\n\n      expect(e).toEqual(expectedError);\n    }\n\n    const finalCartContent = await db\n      .select(\"carts_items.*\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n\n    expect(finalCartContent).toEqual([]);\n    expect.assertions(2);\n  });\n\n  test(\"adding items above limit to cart\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 1 });\n    await db(\"carts_items\").insert({\n      userId: globalUser.id,\n      itemName: \"cheesecake\",\n      quantity: 3\n    });\n\n    try {\n      await addItemToCart(globalUser.username, \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(\n        \"You can't have more than three units of an item in your cart\"\n      );\n      expectedError.code = 400;\n      expect(e).toEqual(expectedError);\n    }\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n\n    expect(finalCartContent).toEqual([{ itemName: \"cheesecake\", quantity: 3 }]);\n    expect.assertions(2);\n  });\n\n  test(\"logging added items\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 1 });\n    await db(\"carts_items\").insert({\n      userId: globalUser.id,\n      itemName: \"cheesecake\",\n      quantity: 1\n    });\n\n    await addItemToCart(globalUser.username, \"cheesecake\");\n\n    const logs = fs.readFileSync(\"/tmp/logs.out\", \"utf-8\");\n    expect(logs).toContain(\n      `cheesecake added to ${globalUser.username}'s cart\\n`\n    );\n  });\n});\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/dbConnection.js",
    "content": "const environmentName = process.env.NODE_ENV;\nconst knex = require(\"knex\");\nconst knexConfig = require(\"./knexfile\")[environmentName];\n\nconst db = knex(knexConfig);\n\nconst closeConnection = () => db.destroy();\n\nmodule.exports = {\n  db,\n  closeConnection\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/disconnectFromDb.js",
    "content": "const { db } = require(\"./dbConnection\");\n\nafterAll(() => db.destroy());\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/inventoryController.js",
    "content": "const { db } = require(\"./dbConnection\");\n\nconst removeFromInventory = async itemName => {\n  const inventoryEntry = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName })\n    .first();\n\n  if (!inventoryEntry || inventoryEntry.quantity === 0) {\n    const err = new Error(`${itemName} is unavailable`);\n    err.code = 400;\n    throw err;\n  }\n\n  await db(\"inventory\")\n    .decrement(\"quantity\")\n    .where({ itemName });\n};\n\nmodule.exports = { removeFromInventory };\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/jest.config.js",
    "content": "module.exports = {\n  testEnvironment: \"node\",\n  globalSetup: \"./migrateDatabases.js\",\n  setupFilesAfterEnv: [\n    \"<rootDir>/truncateTables.js\",\n    \"<rootDir>/seedUser.js\",\n    \"<rootDir>/disconnectFromDb.js\"\n  ]\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/knexfile.js",
    "content": "module.exports = {\n  test: {\n    client: \"sqlite3\",\n    connection: { filename: \"./test.sqlite\" },\n    useNullAsDefault: true\n  },\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"./dev.sqlite\" },\n    useNullAsDefault: true\n  }\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/logger.js",
    "content": "const fs = require(\"fs\");\n\nconst logger = {\n  log: msg => fs.appendFileSync(\"/tmp/logs.out\", msg + \"\\n\")\n};\n\nmodule.exports = logger;\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/migrateDatabases.js",
    "content": "const environmentName = process.env.NODE_ENV || \"test\";\nconst environmentConfig = require(\"./knexfile\")[environmentName];\nconst db = require(\"knex\")(environmentConfig);\n\nmodule.exports = async () => {\n  // Migrate the database to the latest state\n  await db.migrate.latest();\n\n  // Close the connection to the database so that tests won't hang\n  await db.destroy();\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/migrations/20200325082401_initial_schema.js",
    "content": "exports.up = async knex => {\n  await knex.schema.createTable(\"users\", table => {\n    table.increments(\"id\");\n    table.string(\"username\");\n    table.unique(\"username\");\n    table.string(\"email\");\n    table.string(\"passwordHash\");\n  });\n\n  await knex.schema.createTable(\"carts_items\", table => {\n    table.integer(\"userId\").references(\"users.id\");\n    table.string(\"itemName\");\n    table.unique(\"itemName\");\n    table.integer(\"quantity\");\n  });\n\n  await knex.schema.createTable(\"inventory\", table => {\n    table.increments(\"id\");\n    table.string(\"itemName\");\n    table.unique(\"itemName\");\n    table.integer(\"quantity\");\n  });\n};\n\nexports.down = async knex => {\n  await knex.schema.dropTable(\"users\");\n  await knex.schema.dropTable(\"carts_items\");\n  await knex.schema.dropTable(\"inventory\");\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/package.json",
    "content": "{\n  \"name\": \"4_integrations_with_other_apis\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"test\": \"jest --runInBand\"\n  },\n  \"devDependencies\": {\n    \"jest\": \"^24.9.0\",\n    \"supertest\": \"^4.0.2\"\n  },\n  \"dependencies\": {\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"knex\": \"^0.20.13\",\n    \"koa\": \"^2.11.0\",\n    \"koa-body-parser\": \"^1.1.2\",\n    \"koa-router\": \"^7.4.0\",\n    \"sqlite3\": \"^4.1.1\"\n  }\n}\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/seedUser.js",
    "content": "const { createUser } = require(\"./userTestUtils\");\n\nbeforeEach(createUser);\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/server.js",
    "content": "const fetch = require(\"isomorphic-fetch\");\nconst Koa = require(\"koa\");\nconst Router = require(\"koa-router\");\nconst bodyParser = require(\"koa-body-parser\");\n\nconst { db } = require(\"./dbConnection\");\n\nconst { addItemToCart } = require(\"./cartController\");\nconst {\n  hashPassword,\n  authenticationMiddleware\n} = require(\"./authenticationController\");\n\nconst app = new Koa();\nconst router = new Router();\n\napp.use(bodyParser());\n\napp.use(async (ctx, next) => {\n  if (ctx.url.startsWith(\"/carts\")) {\n    return await authenticationMiddleware(ctx, next);\n  }\n\n  await next();\n});\n\nrouter.put(\"/users/:username\", async ctx => {\n  const { username } = ctx.params;\n  const { email, password } = ctx.request.body;\n\n  const userAlreadyExists = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n\n  if (userAlreadyExists) {\n    ctx.body = { message: `${username} already exists` };\n    ctx.status = 409;\n    return;\n  }\n\n  await db(\"users\").insert({\n    username,\n    email,\n    passwordHash: hashPassword(password)\n  });\n\n  return (ctx.body = { message: `${username} created successfully` });\n});\n\nrouter.post(\"/carts/:username/items\", async ctx => {\n  const { username } = ctx.params;\n  const { item, quantity } = ctx.request.body;\n\n  for (let i = 0; i < quantity; i++) {\n    try {\n      const newItems = await addItemToCart(username, item);\n      ctx.body = newItems;\n    } catch (e) {\n      ctx.body = { message: e.message };\n      ctx.status = e.code;\n      return;\n    }\n  }\n});\n\nrouter.delete(\"/carts/:username/items/:item\", async ctx => {\n  const { username, item } = ctx.params;\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n\n  if (!user) {\n    ctx.body = { message: \"user not found\" };\n    ctx.status = 404;\n    return;\n  }\n\n  const itemEntry = await db\n    .select()\n    .from(\"carts_items\")\n    .where({ userId: user.id, itemName: item })\n    .first();\n\n  if (!itemEntry || itemEntry.quantity === 0) {\n    ctx.body = { message: `${item} is not in the cart` };\n    ctx.status = 400;\n    return;\n  }\n\n  await db(\"carts_items\")\n    .decrement(\"quantity\")\n    .where({ userId: user.id, itemName: item });\n\n  const inventoryEntry = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName: item })\n    .first();\n  if (inventoryEntry) {\n    await db(\"inventory\")\n      .increment(\"quantity\")\n      .where({ userId: itemEntry.userId, itemName: item });\n  } else {\n    await db(\"inventory\").insert({ itemName: item, quantity: 1 });\n  }\n\n  ctx.body = await db\n    .select(\"itemName\", \"quantity\")\n    .from(\"carts_items\")\n    .where({ userId: user.id });\n});\n\nrouter.get(\"/inventory/:itemName\", async ctx => {\n  const { itemName } = ctx.params;\n\n  const response = await fetch(`http://recipepuppy.com/api?i=${itemName}`);\n  const { title, href, results: recipes } = await response.json();\n  const inventoryItem = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName })\n    .first();\n\n  ctx.body = {\n    ...inventoryItem,\n    info: `Data obtained from ${title} - ${href}`,\n    recipes\n  };\n});\n\napp.use(router.routes());\n\nmodule.exports = { app: app.listen(3000) };\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/server.test.js",
    "content": "const fetch = require(\"isomorphic-fetch\");\nconst { user: globalUser } = require(\"./userTestUtils\");\nconst { db } = require(\"./dbConnection\");\nconst request = require(\"supertest\");\nconst { app } = require(\"./server.js\");\nconst { hashPassword } = require(\"./authenticationController.js\");\n\nafterAll(() => app.close());\n\ndescribe(\"add items to a cart\", () => {\n  test(\"adding available items\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 3 });\n    const response = await request(app)\n      .post(`/carts/${globalUser.username}/items`)\n      .set(\"authorization\", globalUser.authHeader)\n      .send({ item: \"cheesecake\", quantity: 3 })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    const newItems = [{ itemName: \"cheesecake\", quantity: 3 }];\n    expect(response.body).toEqual(newItems);\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(0);\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n\n    expect(finalCartContent).toEqual(newItems);\n  });\n\n  test(\"adding unavailable items\", async () => {\n    const response = await request(app)\n      .post(`/carts/${globalUser.username}/items`)\n      .set(\"authorization\", globalUser.authHeader)\n      .send({ item: \"cheesecake\", quantity: 1 })\n      .expect(400)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"cheesecake is unavailable\"\n    });\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n    expect(finalCartContent).toEqual([]);\n  });\n});\n\ndescribe(\"removing items from a cart\", () => {\n  test(\"removing existing items\", async () => {\n    await db(\"carts_items\").insert({\n      userId: globalUser.id,\n      itemName: \"cheesecake\",\n      quantity: 1\n    });\n\n    const response = await request(app)\n      .del(`/carts/${globalUser.username}/items/cheesecake`)\n      .set(\"authorization\", globalUser.authHeader)\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    const expectedFinalContent = [{ itemName: \"cheesecake\", quantity: 0 }];\n\n    expect(response.body).toEqual(expectedFinalContent);\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n    expect(finalCartContent).toEqual(expectedFinalContent);\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(1);\n  });\n\n  test(\"removing non-existing items\", async () => {\n    await db(\"inventory\").insert({\n      itemName: \"cheesecake\",\n      quantity: 0\n    });\n\n    const response = await request(app)\n      .del(`/carts/${globalUser.username}/items/cheesecake`)\n      .set(\"authorization\", globalUser.authHeader)\n      .expect(400)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"cheesecake is not in the cart\"\n    });\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(0);\n  });\n});\n\ndescribe(\"create accounts\", () => {\n  test(\"creating a new account\", async () => {\n    const response = await request(app)\n      .put(\"/users/another_user\")\n      .send({ email: \"another_user@example.org\", password: \"a_password\" })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"another_user created successfully\"\n    });\n\n    const savedUser = await db\n      .select(\"email\", \"passwordHash\")\n      .from(\"users\")\n      .where({ username: \"another_user\" })\n      .first();\n\n    expect(savedUser).toEqual({\n      email: \"another_user@example.org\",\n      passwordHash: hashPassword(\"a_password\")\n    });\n  });\n\n  test(\"creating a duplicate account\", async () => {\n    const response = await request(app)\n      .put(`/users/${globalUser.username}`)\n      .send({ email: globalUser.email, password: \"a_password\" })\n      .expect(409)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: `${globalUser.username} already exists`\n    });\n  });\n});\n\ndescribe(\"fetch inventory items\", () => {\n  const eggs = { itemName: \"eggs\", quantity: 3 };\n  const applePie = { itemName: \"apple pie\", quantity: 1 };\n\n  beforeEach(async () => {\n    await db(\"inventory\").insert([eggs, applePie]);\n    const { id: eggsId } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"eggs\" })\n      .first();\n    eggs.id = eggsId;\n  });\n\n  test(\"can fetch an item from the inventory\", async () => {\n    const thirdPartyResponse = await fetch(\"http://recipepuppy.com/api?i=eggs\");\n    const { title, href, results: recipes } = await thirdPartyResponse.json();\n\n    const response = await request(app)\n      .get(`/inventory/eggs`)\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      ...eggs,\n      info: `Data obtained from ${title} - ${href}`,\n      recipes\n    });\n  });\n});\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/truncateTables.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst tablesToTruncate = [\"users\", \"inventory\", \"carts_items\"];\n\nbeforeEach(() => {\n  return Promise.all(tablesToTruncate.map(t => db(t).truncate()));\n});\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/userTestUtils.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { hashPassword } = require(\"./authenticationController\");\n\nconst username = \"test_user\";\nconst password = \"a_password\";\nconst passwordHash = hashPassword(password);\nconst email = \"test_user@example.org\";\nconst validAuth = Buffer.from(`${username}:${password}`).toString(\"base64\");\nconst authHeader = `Basic ${validAuth}`;\n\nconst user = {\n  username,\n  password,\n  email,\n  authHeader\n};\n\nconst createUser = async () => {\n  await db(\"users\").insert({ username, email, passwordHash });\n  const { id } = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  user.id = id;\n};\n\nmodule.exports = { user, createUser };\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/authenticationController.js",
    "content": "const { user: globalUser } = require(\"./userTestUtils\");\nconst crypto = require(\"crypto\");\nconst { db } = require(\"./dbConnection\");\n\nconst hashPassword = password => {\n  const hash = crypto.createHash(\"sha256\");\n  hash.update(password);\n  return hash.digest(\"hex\");\n};\n\nconst credentialsAreValid = async (username, password) => {\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  if (!user) return false;\n  return hashPassword(password) === user.passwordHash;\n};\n\nconst authenticationMiddleware = async (ctx, next) => {\n  try {\n    const authHeader = ctx.request.headers.authorization;\n    const credentials = Buffer.from(\n      authHeader.slice(\"basic\".length + 1),\n      \"base64\"\n    ).toString();\n    const [username, password] = credentials.split(\":\");\n\n    const validCredentialsSent = await credentialsAreValid(username, password);\n    if (!validCredentialsSent) throw new Error(\"invalid credentials\");\n  } catch (e) {\n    ctx.status = 401;\n    ctx.body = { message: \"please provide valid credentials\" };\n    return;\n  }\n\n  await next();\n};\n\nmodule.exports = {\n  hashPassword,\n  credentialsAreValid,\n  authenticationMiddleware\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/authenticationController.test.js",
    "content": "const crypto = require(\"crypto\");\nconst {\n  hashPassword,\n  credentialsAreValid,\n  authenticationMiddleware\n} = require(\"./authenticationController\");\nconst { user: globalUser } = require(\"./userTestUtils\");\n\ndescribe(\"hashPassword\", () => {\n  test(\"hashing passwords\", () => {\n    const plainTextPassword = \"password_example\";\n    const hash = crypto.createHash(\"sha256\");\n    hash.update(plainTextPassword);\n    const expectedHash = hash.digest(\"hex\");\n    expect(hashPassword(plainTextPassword)).toBe(expectedHash);\n  });\n});\n\ndescribe(\"credentialsAreValid\", () => {\n  test(\"validating credentials\", async () => {\n    expect(await credentialsAreValid(globalUser.username, \"a_password\")).toBe(\n      true\n    );\n  });\n});\n\ndescribe(\"authenticationMiddleware\", () => {\n  test(\"returning an error if the credentials are not valid\", async () => {\n    const fakeAuth = Buffer.from(\"invalid:credentials\").toString(\"base64\");\n    const ctx = {\n      request: {\n        headers: { authorization: `Basic ${fakeAuth}` }\n      }\n    };\n\n    const next = jest.fn();\n    await authenticationMiddleware(ctx, next);\n    expect(next.mock.calls).toHaveLength(0);\n    expect(ctx).toEqual({\n      ...ctx,\n      status: 401,\n      body: { message: \"please provide valid credentials\" }\n    });\n  });\n\n  test(\"authenticating properly\", async () => {\n    const ctx = {\n      request: {\n        headers: { authorization: globalUser.authHeader }\n      }\n    };\n\n    const next = jest.fn();\n    await authenticationMiddleware(ctx, next);\n    expect(next.mock.calls).toHaveLength(1);\n  });\n});\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/cartController.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { removeFromInventory } = require(\"./inventoryController\");\nconst logger = require(\"./logger\");\n\nconst addItemToCart = async (username, itemName) => {\n  await removeFromInventory(itemName);\n\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  if (!user) {\n    const userNotFound = new Error(\"user not found\");\n    userNotFound.code = 404;\n  }\n\n  const itemEntry = await db\n    .select()\n    .from(\"carts_items\")\n    .where({ userId: user.id, itemName })\n    .first();\n\n  if (itemEntry && itemEntry.quantity + 1 > 3) {\n    const limitError = new Error(\n      \"You can't have more than three units of an item in your cart\"\n    );\n    limitError.code = 400;\n    throw limitError;\n  }\n\n  if (itemEntry) {\n    await db(\"carts_items\")\n      .increment(\"quantity\")\n      .where({ userId: itemEntry.userId, itemName });\n  } else {\n    await db(\"carts_items\").insert({\n      userId: user.id,\n      itemName,\n      quantity: 1\n    });\n  }\n\n  logger.log(`${itemName} added to ${username}'s cart`);\n  return db\n    .select(\"itemName\", \"quantity\")\n    .from(\"carts_items\")\n    .where({ userId: user.id });\n};\n\nmodule.exports = { addItemToCart };\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/cartController.test.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { addItemToCart } = require(\"./cartController\");\nconst { hashPassword } = require(\"./authenticationController\");\nconst { user: globalUser } = require(\"./userTestUtils\");\n\nconst fs = require(\"fs\");\n\ndescribe(\"addItemToCart\", () => {\n  beforeEach(() => {\n    fs.writeFileSync(\"/tmp/logs.out\", \"\");\n  });\n\n  test(\"adding unavailable items to cart\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 0 });\n\n    try {\n      await addItemToCart(globalUser.username, \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(\"cheesecake is unavailable\");\n      expectedError.code = 400;\n\n      expect(e).toEqual(expectedError);\n    }\n\n    const finalCartContent = await db\n      .select(\"carts_items.*\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n\n    expect(finalCartContent).toEqual([]);\n    expect.assertions(2);\n  });\n\n  test(\"adding items above limit to cart\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 1 });\n    await db(\"carts_items\").insert({\n      userId: globalUser.id,\n      itemName: \"cheesecake\",\n      quantity: 3\n    });\n\n    try {\n      await addItemToCart(globalUser.username, \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(\n        \"You can't have more than three units of an item in your cart\"\n      );\n      expectedError.code = 400;\n      expect(e).toEqual(expectedError);\n    }\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n\n    expect(finalCartContent).toEqual([{ itemName: \"cheesecake\", quantity: 3 }]);\n    expect.assertions(2);\n  });\n\n  test(\"logging added items\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 1 });\n    await db(\"carts_items\").insert({\n      userId: globalUser.id,\n      itemName: \"cheesecake\",\n      quantity: 1\n    });\n\n    await addItemToCart(globalUser.username, \"cheesecake\");\n\n    const logs = fs.readFileSync(\"/tmp/logs.out\", \"utf-8\");\n    expect(logs).toContain(\n      `cheesecake added to ${globalUser.username}'s cart\\n`\n    );\n  });\n});\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/dbConnection.js",
    "content": "const environmentName = process.env.NODE_ENV;\nconst knex = require(\"knex\");\nconst knexConfig = require(\"./knexfile\")[environmentName];\n\nconst db = knex(knexConfig);\n\nconst closeConnection = () => db.destroy();\n\nmodule.exports = {\n  db,\n  closeConnection\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/disconnectFromDb.js",
    "content": "const { db } = require(\"./dbConnection\");\n\nafterAll(() => db.destroy());\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/inventoryController.js",
    "content": "const { db } = require(\"./dbConnection\");\n\nconst removeFromInventory = async itemName => {\n  const inventoryEntry = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName })\n    .first();\n\n  if (!inventoryEntry || inventoryEntry.quantity === 0) {\n    const err = new Error(`${itemName} is unavailable`);\n    err.code = 400;\n    throw err;\n  }\n\n  await db(\"inventory\")\n    .decrement(\"quantity\")\n    .where({ itemName });\n};\n\nmodule.exports = { removeFromInventory };\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/jest.config.js",
    "content": "module.exports = {\n  testEnvironment: \"node\",\n  globalSetup: \"./migrateDatabases.js\",\n  setupFilesAfterEnv: [\n    \"<rootDir>/truncateTables.js\",\n    \"<rootDir>/seedUser.js\",\n    \"<rootDir>/disconnectFromDb.js\"\n  ]\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/knexfile.js",
    "content": "module.exports = {\n  test: {\n    client: \"sqlite3\",\n    connection: { filename: \"./test.sqlite\" },\n    useNullAsDefault: true\n  },\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"./dev.sqlite\" },\n    useNullAsDefault: true\n  }\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/logger.js",
    "content": "const fs = require(\"fs\");\n\nconst logger = {\n  log: msg => fs.appendFileSync(\"/tmp/logs.out\", msg + \"\\n\")\n};\n\nmodule.exports = logger;\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/migrateDatabases.js",
    "content": "const environmentName = process.env.NODE_ENV || \"test\";\nconst environmentConfig = require(\"./knexfile\")[environmentName];\nconst db = require(\"knex\")(environmentConfig);\n\nmodule.exports = async () => {\n  // Migrate the database to the latest state\n  await db.migrate.latest();\n\n  // Close the connection to the database so that tests won't hang\n  await db.destroy();\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/migrations/20200325082401_initial_schema.js",
    "content": "exports.up = async knex => {\n  await knex.schema.createTable(\"users\", table => {\n    table.increments(\"id\");\n    table.string(\"username\");\n    table.unique(\"username\");\n    table.string(\"email\");\n    table.string(\"passwordHash\");\n  });\n\n  await knex.schema.createTable(\"carts_items\", table => {\n    table.integer(\"userId\").references(\"users.id\");\n    table.string(\"itemName\");\n    table.unique(\"itemName\");\n    table.integer(\"quantity\");\n  });\n\n  await knex.schema.createTable(\"inventory\", table => {\n    table.increments(\"id\");\n    table.string(\"itemName\");\n    table.unique(\"itemName\");\n    table.integer(\"quantity\");\n  });\n};\n\nexports.down = async knex => {\n  await knex.schema.dropTable(\"users\");\n  await knex.schema.dropTable(\"carts_items\");\n  await knex.schema.dropTable(\"inventory\");\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/package.json",
    "content": "{\n  \"name\": \"5_using_mocks_to_avoid_requests\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"test\": \"jest --runInBand\"\n  },\n  \"devDependencies\": {\n    \"jest\": \"^24.9.0\",\n    \"jest-when\": \"^2.7.0\",\n    \"supertest\": \"^4.0.2\"\n  },\n  \"dependencies\": {\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"knex\": \"^0.20.13\",\n    \"koa\": \"^2.11.0\",\n    \"koa-body-parser\": \"^1.1.2\",\n    \"koa-router\": \"^7.4.0\",\n    \"sqlite3\": \"^4.1.1\"\n  }\n}\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/seedUser.js",
    "content": "const { createUser } = require(\"./userTestUtils\");\n\nbeforeEach(createUser);\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/server.js",
    "content": "const fetch = require(\"isomorphic-fetch\");\nconst Koa = require(\"koa\");\nconst Router = require(\"koa-router\");\nconst bodyParser = require(\"koa-body-parser\");\n\nconst { db } = require(\"./dbConnection\");\n\nconst { addItemToCart } = require(\"./cartController\");\nconst {\n  hashPassword,\n  authenticationMiddleware\n} = require(\"./authenticationController\");\n\nconst app = new Koa();\nconst router = new Router();\n\napp.use(bodyParser());\n\napp.use(async (ctx, next) => {\n  if (ctx.url.startsWith(\"/carts\")) {\n    return await authenticationMiddleware(ctx, next);\n  }\n\n  await next();\n});\n\nrouter.put(\"/users/:username\", async ctx => {\n  const { username } = ctx.params;\n  const { email, password } = ctx.request.body;\n\n  const userAlreadyExists = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n\n  if (userAlreadyExists) {\n    ctx.body = { message: `${username} already exists` };\n    ctx.status = 409;\n    return;\n  }\n\n  await db(\"users\").insert({\n    username,\n    email,\n    passwordHash: hashPassword(password)\n  });\n\n  return (ctx.body = { message: `${username} created successfully` });\n});\n\nrouter.post(\"/carts/:username/items\", async ctx => {\n  const { username } = ctx.params;\n  const { item, quantity } = ctx.request.body;\n\n  for (let i = 0; i < quantity; i++) {\n    try {\n      const newItems = await addItemToCart(username, item);\n      ctx.body = newItems;\n    } catch (e) {\n      ctx.body = { message: e.message };\n      ctx.status = e.code;\n      return;\n    }\n  }\n});\n\nrouter.delete(\"/carts/:username/items/:item\", async ctx => {\n  const { username, item } = ctx.params;\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n\n  if (!user) {\n    ctx.body = { message: \"user not found\" };\n    ctx.status = 404;\n    return;\n  }\n\n  const itemEntry = await db\n    .select()\n    .from(\"carts_items\")\n    .where({ userId: user.id, itemName: item })\n    .first();\n\n  if (!itemEntry || itemEntry.quantity === 0) {\n    ctx.body = { message: `${item} is not in the cart` };\n    ctx.status = 400;\n    return;\n  }\n\n  await db(\"carts_items\")\n    .decrement(\"quantity\")\n    .where({ userId: user.id, itemName: item });\n\n  const inventoryEntry = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName: item })\n    .first();\n  if (inventoryEntry) {\n    await db(\"inventory\")\n      .increment(\"quantity\")\n      .where({ userId: itemEntry.userId, itemName: item });\n  } else {\n    await db(\"inventory\").insert({ itemName: item, quantity: 1 });\n  }\n\n  ctx.body = await db\n    .select(\"itemName\", \"quantity\")\n    .from(\"carts_items\")\n    .where({ userId: user.id });\n});\n\nrouter.get(\"/inventory/:itemName\", async ctx => {\n  const { itemName } = ctx.params;\n\n  const response = await fetch(`http://recipepuppy.com/api?i=${itemName}`);\n  const { title, href, results: recipes } = await response.json();\n  const inventoryItem = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName })\n    .first();\n\n  ctx.body = {\n    ...inventoryItem,\n    info: `Data obtained from ${title} - ${href}`,\n    recipes\n  };\n});\n\napp.use(router.routes());\n\nmodule.exports = { app: app.listen(3000) };\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/server.test.js",
    "content": "const fetch = require(\"isomorphic-fetch\");\nconst { user: globalUser } = require(\"./userTestUtils\");\nconst { db } = require(\"./dbConnection\");\nconst request = require(\"supertest\");\nconst { app } = require(\"./server.js\");\nconst { hashPassword } = require(\"./authenticationController.js\");\nconst { when } = require(\"jest-when\");\n\nafterAll(() => app.close());\n\njest.mock(\"isomorphic-fetch\");\n\ndescribe(\"add items to a cart\", () => {\n  test(\"adding available items\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 3 });\n    const response = await request(app)\n      .post(`/carts/${globalUser.username}/items`)\n      .set(\"authorization\", globalUser.authHeader)\n      .send({ item: \"cheesecake\", quantity: 3 })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    const newItems = [{ itemName: \"cheesecake\", quantity: 3 }];\n    expect(response.body).toEqual(newItems);\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(0);\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n\n    expect(finalCartContent).toEqual(newItems);\n  });\n\n  test(\"adding unavailable items\", async () => {\n    const response = await request(app)\n      .post(`/carts/${globalUser.username}/items`)\n      .set(\"authorization\", globalUser.authHeader)\n      .send({ item: \"cheesecake\", quantity: 1 })\n      .expect(400)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"cheesecake is unavailable\"\n    });\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n    expect(finalCartContent).toEqual([]);\n  });\n});\n\ndescribe(\"removing items from a cart\", () => {\n  test(\"removing existing items\", async () => {\n    await db(\"carts_items\").insert({\n      userId: globalUser.id,\n      itemName: \"cheesecake\",\n      quantity: 1\n    });\n\n    const response = await request(app)\n      .del(`/carts/${globalUser.username}/items/cheesecake`)\n      .set(\"authorization\", globalUser.authHeader)\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    const expectedFinalContent = [{ itemName: \"cheesecake\", quantity: 0 }];\n\n    expect(response.body).toEqual(expectedFinalContent);\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n    expect(finalCartContent).toEqual(expectedFinalContent);\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(1);\n  });\n\n  test(\"removing non-existing items\", async () => {\n    await db(\"inventory\").insert({\n      itemName: \"cheesecake\",\n      quantity: 0\n    });\n\n    const response = await request(app)\n      .del(`/carts/${globalUser.username}/items/cheesecake`)\n      .set(\"authorization\", globalUser.authHeader)\n      .expect(400)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"cheesecake is not in the cart\"\n    });\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(0);\n  });\n});\n\ndescribe(\"create accounts\", () => {\n  test(\"creating a new account\", async () => {\n    const response = await request(app)\n      .put(\"/users/another_user\")\n      .send({ email: \"another_user@example.org\", password: \"a_password\" })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"another_user created successfully\"\n    });\n\n    const savedUser = await db\n      .select(\"email\", \"passwordHash\")\n      .from(\"users\")\n      .where({ username: \"another_user\" })\n      .first();\n\n    expect(savedUser).toEqual({\n      email: \"another_user@example.org\",\n      passwordHash: hashPassword(\"a_password\")\n    });\n  });\n\n  test(\"creating a duplicate account\", async () => {\n    const response = await request(app)\n      .put(`/users/${globalUser.username}`)\n      .send({ email: globalUser.email, password: \"a_password\" })\n      .expect(409)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: `${globalUser.username} already exists`\n    });\n  });\n});\n\ndescribe(\"fetch inventory items\", () => {\n  const eggs = { itemName: \"eggs\", quantity: 3 };\n  const applePie = { itemName: \"apple pie\", quantity: 1 };\n\n  beforeEach(async () => {\n    await db(\"inventory\").insert([eggs, applePie]);\n    const { id: eggsId } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"eggs\" })\n      .first();\n    eggs.id = eggsId;\n  });\n\n  test(\"can fetch an item from the inventory\", async () => {\n    const eggsResponse = {\n      title: \"FakeAPI\",\n      href: \"example.org\",\n      results: [{ name: \"Omelette du Fromage\" }]\n    };\n\n    fetch.mockRejectedValue(\"Not used as expected!\");\n    when(fetch)\n      .calledWith(\"http://recipepuppy.com/api?i=eggs\")\n      .mockResolvedValue({\n        json: jest.fn().mockResolvedValue(eggsResponse)\n      });\n\n    const response = await request(app)\n      .get(`/inventory/eggs`)\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      ...eggs,\n      info: `Data obtained from ${eggsResponse.title} - ${eggsResponse.href}`,\n      recipes: eggsResponse.results\n    });\n  });\n});\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/truncateTables.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst tablesToTruncate = [\"users\", \"inventory\", \"carts_items\"];\n\nbeforeEach(() => {\n  return Promise.all(tablesToTruncate.map(t => db(t).truncate()));\n});\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/userTestUtils.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { hashPassword } = require(\"./authenticationController\");\n\nconst username = \"test_user\";\nconst password = \"a_password\";\nconst passwordHash = hashPassword(password);\nconst email = \"test_user@example.org\";\nconst validAuth = Buffer.from(`${username}:${password}`).toString(\"base64\");\nconst authHeader = `Basic ${validAuth}`;\n\nconst user = {\n  username,\n  password,\n  email,\n  authHeader\n};\n\nconst createUser = async () => {\n  await db(\"users\").insert({ username, email, passwordHash });\n  const { id } = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  user.id = id;\n};\n\nmodule.exports = { user, createUser };\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/authenticationController.js",
    "content": "const crypto = require(\"crypto\");\nconst { db } = require(\"./dbConnection\");\n\nconst hashPassword = password => {\n  const hash = crypto.createHash(\"sha256\");\n  hash.update(password);\n  return hash.digest(\"hex\");\n};\n\nconst credentialsAreValid = async (username, password) => {\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  if (!user) return false;\n  return hashPassword(password) === user.passwordHash;\n};\n\nconst authenticationMiddleware = async (ctx, next) => {\n  try {\n    const authHeader = ctx.request.headers.authorization;\n    const credentials = Buffer.from(\n      authHeader.slice(\"basic\".length + 1),\n      \"base64\"\n    ).toString();\n    const [username, password] = credentials.split(\":\");\n\n    const validCredentialsSent = await credentialsAreValid(username, password);\n    if (!validCredentialsSent) throw new Error(\"invalid credentials\");\n  } catch (e) {\n    ctx.status = 401;\n    ctx.body = { message: \"please provide valid credentials\" };\n    return;\n  }\n\n  await next();\n};\n\nmodule.exports = {\n  hashPassword,\n  credentialsAreValid,\n  authenticationMiddleware\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/authenticationController.test.js",
    "content": "const crypto = require(\"crypto\");\nconst {\n  hashPassword,\n  credentialsAreValid,\n  authenticationMiddleware\n} = require(\"./authenticationController\");\nconst { user: globalUser } = require(\"./userTestUtils\");\n\ndescribe(\"hashPassword\", () => {\n  test(\"hashing passwords\", () => {\n    const plainTextPassword = \"password_example\";\n    const hash = crypto.createHash(\"sha256\");\n    hash.update(plainTextPassword);\n    const expectedHash = hash.digest(\"hex\");\n    expect(hashPassword(plainTextPassword)).toBe(expectedHash);\n  });\n});\n\ndescribe(\"credentialsAreValid\", () => {\n  test(\"validating credentials\", async () => {\n    expect(await credentialsAreValid(globalUser.username, \"a_password\")).toBe(\n      true\n    );\n  });\n});\n\ndescribe(\"authenticationMiddleware\", () => {\n  test(\"returning an error if the credentials are not valid\", async () => {\n    const fakeAuth = Buffer.from(\"invalid:credentials\").toString(\"base64\");\n    const ctx = {\n      request: {\n        headers: { authorization: `Basic ${fakeAuth}` }\n      }\n    };\n\n    const next = jest.fn();\n    await authenticationMiddleware(ctx, next);\n    expect(next.mock.calls).toHaveLength(0);\n    expect(ctx).toEqual({\n      ...ctx,\n      status: 401,\n      body: { message: \"please provide valid credentials\" }\n    });\n  });\n\n  test(\"authenticating properly\", async () => {\n    const ctx = {\n      request: {\n        headers: { authorization: globalUser.authHeader }\n      }\n    };\n\n    const next = jest.fn();\n    await authenticationMiddleware(ctx, next);\n    expect(next.mock.calls).toHaveLength(1);\n  });\n});\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/cartController.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { removeFromInventory } = require(\"./inventoryController\");\nconst logger = require(\"./logger\");\n\nconst addItemToCart = async (username, itemName) => {\n  await removeFromInventory(itemName);\n\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  if (!user) {\n    const userNotFound = new Error(\"user not found\");\n    userNotFound.code = 404;\n  }\n\n  const itemEntry = await db\n    .select()\n    .from(\"carts_items\")\n    .where({ userId: user.id, itemName })\n    .first();\n\n  if (itemEntry && itemEntry.quantity + 1 > 3) {\n    const limitError = new Error(\n      \"You can't have more than three units of an item in your cart\"\n    );\n    limitError.code = 400;\n    throw limitError;\n  }\n\n  if (itemEntry) {\n    await db(\"carts_items\")\n      .increment(\"quantity\")\n      .where({ userId: itemEntry.userId, itemName });\n  } else {\n    await db(\"carts_items\").insert({\n      userId: user.id,\n      itemName,\n      quantity: 1\n    });\n  }\n\n  logger.log(`${itemName} added to ${username}'s cart`);\n  return db\n    .select(\"itemName\", \"quantity\")\n    .from(\"carts_items\")\n    .where({ userId: user.id });\n};\n\nmodule.exports = { addItemToCart };\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/cartController.test.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { addItemToCart } = require(\"./cartController\");\nconst { hashPassword } = require(\"./authenticationController\");\nconst { user: globalUser } = require(\"./userTestUtils\");\n\nconst fs = require(\"fs\");\n\ndescribe(\"addItemToCart\", () => {\n  beforeEach(() => {\n    fs.writeFileSync(\"/tmp/logs.out\", \"\");\n  });\n\n  test(\"adding unavailable items to cart\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 0 });\n\n    try {\n      await addItemToCart(globalUser.username, \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(\"cheesecake is unavailable\");\n      expectedError.code = 400;\n\n      expect(e).toEqual(expectedError);\n    }\n\n    const finalCartContent = await db\n      .select(\"carts_items.*\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n\n    expect(finalCartContent).toEqual([]);\n    expect.assertions(2);\n  });\n\n  test(\"adding items above limit to cart\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 1 });\n    await db(\"carts_items\").insert({\n      userId: globalUser.id,\n      itemName: \"cheesecake\",\n      quantity: 3\n    });\n\n    try {\n      await addItemToCart(globalUser.username, \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(\n        \"You can't have more than three units of an item in your cart\"\n      );\n      expectedError.code = 400;\n      expect(e).toEqual(expectedError);\n    }\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n\n    expect(finalCartContent).toEqual([{ itemName: \"cheesecake\", quantity: 3 }]);\n    expect.assertions(2);\n  });\n\n  test(\"logging added items\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 1 });\n    await db(\"carts_items\").insert({\n      userId: globalUser.id,\n      itemName: \"cheesecake\",\n      quantity: 1\n    });\n\n    await addItemToCart(globalUser.username, \"cheesecake\");\n\n    const logs = fs.readFileSync(\"/tmp/logs.out\", \"utf-8\");\n    expect(logs).toContain(\n      `cheesecake added to ${globalUser.username}'s cart\\n`\n    );\n  });\n});\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/dbConnection.js",
    "content": "const environmentName = process.env.NODE_ENV;\nconst knex = require(\"knex\");\nconst knexConfig = require(\"./knexfile\")[environmentName];\n\nconst db = knex(knexConfig);\n\nconst closeConnection = () => db.destroy();\n\nmodule.exports = {\n  db,\n  closeConnection\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/disconnectFromDb.js",
    "content": "const { db } = require(\"./dbConnection\");\n\nafterAll(() => db.destroy());\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/inventoryController.js",
    "content": "const { db } = require(\"./dbConnection\");\n\nconst removeFromInventory = async itemName => {\n  const inventoryEntry = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName })\n    .first();\n\n  if (!inventoryEntry || inventoryEntry.quantity === 0) {\n    const err = new Error(`${itemName} is unavailable`);\n    err.code = 400;\n    throw err;\n  }\n\n  await db(\"inventory\")\n    .decrement(\"quantity\")\n    .where({ itemName });\n};\n\nmodule.exports = { removeFromInventory };\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/jest.config.js",
    "content": "module.exports = {\n  testEnvironment: \"node\",\n  globalSetup: \"./migrateDatabases.js\",\n  setupFilesAfterEnv: [\n    \"<rootDir>/truncateTables.js\",\n    \"<rootDir>/seedUser.js\",\n    \"<rootDir>/disconnectFromDb.js\"\n  ]\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/knexfile.js",
    "content": "module.exports = {\n  test: {\n    client: \"sqlite3\",\n    connection: { filename: \"./test.sqlite\" },\n    useNullAsDefault: true\n  },\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"./dev.sqlite\" },\n    useNullAsDefault: true\n  }\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/logger.js",
    "content": "const fs = require(\"fs\");\n\nconst logger = {\n  log: msg => fs.appendFileSync(\"/tmp/logs.out\", msg + \"\\n\")\n};\n\nmodule.exports = logger;\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/migrateDatabases.js",
    "content": "const environmentName = process.env.NODE_ENV || \"test\";\nconst environmentConfig = require(\"./knexfile\")[environmentName];\nconst db = require(\"knex\")(environmentConfig);\n\nmodule.exports = async () => {\n  // Migrate the database to the latest state\n  await db.migrate.latest();\n\n  // Close the connection to the database so that tests won't hang\n  await db.destroy();\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/migrations/20200325082401_initial_schema.js",
    "content": "exports.up = async knex => {\n  await knex.schema.createTable(\"users\", table => {\n    table.increments(\"id\");\n    table.string(\"username\");\n    table.unique(\"username\");\n    table.string(\"email\");\n    table.string(\"passwordHash\");\n  });\n\n  await knex.schema.createTable(\"carts_items\", table => {\n    table.integer(\"userId\").references(\"users.id\");\n    table.string(\"itemName\");\n    table.unique(\"itemName\");\n    table.integer(\"quantity\");\n  });\n\n  await knex.schema.createTable(\"inventory\", table => {\n    table.increments(\"id\");\n    table.string(\"itemName\");\n    table.unique(\"itemName\");\n    table.integer(\"quantity\");\n  });\n};\n\nexports.down = async knex => {\n  await knex.schema.dropTable(\"users\");\n  await knex.schema.dropTable(\"carts_items\");\n  await knex.schema.dropTable(\"inventory\");\n};\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/package.json",
    "content": "{\n  \"name\": \"4_integrations_with_other_apis\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"test\": \"jest --runInBand\"\n  },\n  \"devDependencies\": {\n    \"jest\": \"^24.9.0\",\n    \"supertest\": \"^4.0.2\"\n  },\n  \"dependencies\": {\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"knex\": \"^0.20.13\",\n    \"koa\": \"^2.11.0\",\n    \"koa-body-parser\": \"^1.1.2\",\n    \"koa-router\": \"^7.4.0\",\n    \"nock\": \"^12.0.3\",\n    \"sqlite3\": \"^4.1.1\"\n  }\n}\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/seedUser.js",
    "content": "const { createUser } = require(\"./userTestUtils\");\n\nbeforeEach(createUser);\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/server.js",
    "content": "const fetch = require(\"isomorphic-fetch\");\nconst Koa = require(\"koa\");\nconst Router = require(\"koa-router\");\nconst bodyParser = require(\"koa-body-parser\");\n\nconst { db } = require(\"./dbConnection\");\n\nconst { addItemToCart } = require(\"./cartController\");\nconst {\n  hashPassword,\n  authenticationMiddleware\n} = require(\"./authenticationController\");\n\nconst app = new Koa();\nconst router = new Router();\n\napp.use(bodyParser());\n\napp.use(async (ctx, next) => {\n  if (ctx.url.startsWith(\"/carts\")) {\n    return await authenticationMiddleware(ctx, next);\n  }\n\n  await next();\n});\n\nrouter.put(\"/users/:username\", async ctx => {\n  const { username } = ctx.params;\n  const { email, password } = ctx.request.body;\n\n  const userAlreadyExists = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n\n  if (userAlreadyExists) {\n    ctx.body = { message: `${username} already exists` };\n    ctx.status = 409;\n    return;\n  }\n\n  await db(\"users\").insert({\n    username,\n    email,\n    passwordHash: hashPassword(password)\n  });\n\n  return (ctx.body = { message: `${username} created successfully` });\n});\n\nrouter.post(\"/carts/:username/items\", async ctx => {\n  const { username } = ctx.params;\n  const { item, quantity } = ctx.request.body;\n\n  for (let i = 0; i < quantity; i++) {\n    try {\n      const newItems = await addItemToCart(username, item);\n      ctx.body = newItems;\n    } catch (e) {\n      ctx.body = { message: e.message };\n      ctx.status = e.code;\n      return;\n    }\n  }\n});\n\nrouter.delete(\"/carts/:username/items/:item\", async ctx => {\n  const { username, item } = ctx.params;\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n\n  if (!user) {\n    ctx.body = { message: \"user not found\" };\n    ctx.status = 404;\n    return;\n  }\n\n  const itemEntry = await db\n    .select()\n    .from(\"carts_items\")\n    .where({ userId: user.id, itemName: item })\n    .first();\n\n  if (!itemEntry || itemEntry.quantity === 0) {\n    ctx.body = { message: `${item} is not in the cart` };\n    ctx.status = 400;\n    return;\n  }\n\n  await db(\"carts_items\")\n    .decrement(\"quantity\")\n    .where({ userId: user.id, itemName: item });\n\n  const inventoryEntry = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName: item })\n    .first();\n  if (inventoryEntry) {\n    await db(\"inventory\")\n      .increment(\"quantity\")\n      .where({ userId: itemEntry.userId, itemName: item });\n  } else {\n    await db(\"inventory\").insert({ itemName: item, quantity: 1 });\n  }\n\n  ctx.body = await db\n    .select(\"itemName\", \"quantity\")\n    .from(\"carts_items\")\n    .where({ userId: user.id });\n});\n\nrouter.get(\"/inventory/:itemName\", async ctx => {\n  const { itemName } = ctx.params;\n\n  const response = await fetch(`http://recipepuppy.com/api?i=${itemName}`);\n  const { title, href, results: recipes } = await response.json();\n  const inventoryItem = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName })\n    .first();\n\n  ctx.body = {\n    ...inventoryItem,\n    info: `Data obtained from ${title} - ${href}`,\n    recipes\n  };\n});\n\napp.use(router.routes());\n\nmodule.exports = { app: app.listen(3000) };\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/server.test.js",
    "content": "const { user: globalUser } = require(\"./userTestUtils\");\nconst { db } = require(\"./dbConnection\");\nconst request = require(\"supertest\");\nconst { app } = require(\"./server.js\");\nconst { hashPassword } = require(\"./authenticationController.js\");\nconst nock = require(\"nock\");\n\nafterAll(() => app.close());\n\ndescribe(\"add items to a cart\", () => {\n  test(\"adding available items\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 3 });\n    const response = await request(app)\n      .post(`/carts/${globalUser.username}/items`)\n      .set(\"authorization\", globalUser.authHeader)\n      .send({ item: \"cheesecake\", quantity: 3 })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    const newItems = [{ itemName: \"cheesecake\", quantity: 3 }];\n    expect(response.body).toEqual(newItems);\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(0);\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n\n    expect(finalCartContent).toEqual(newItems);\n  });\n\n  test(\"adding unavailable items\", async () => {\n    const response = await request(app)\n      .post(`/carts/${globalUser.username}/items`)\n      .set(\"authorization\", globalUser.authHeader)\n      .send({ item: \"cheesecake\", quantity: 1 })\n      .expect(400)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"cheesecake is unavailable\"\n    });\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n    expect(finalCartContent).toEqual([]);\n  });\n});\n\ndescribe(\"removing items from a cart\", () => {\n  test(\"removing existing items\", async () => {\n    await db(\"carts_items\").insert({\n      userId: globalUser.id,\n      itemName: \"cheesecake\",\n      quantity: 1\n    });\n\n    const response = await request(app)\n      .del(`/carts/${globalUser.username}/items/cheesecake`)\n      .set(\"authorization\", globalUser.authHeader)\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    const expectedFinalContent = [{ itemName: \"cheesecake\", quantity: 0 }];\n\n    expect(response.body).toEqual(expectedFinalContent);\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n    expect(finalCartContent).toEqual(expectedFinalContent);\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(1);\n  });\n\n  test(\"removing non-existing items\", async () => {\n    await db(\"inventory\").insert({\n      itemName: \"cheesecake\",\n      quantity: 0\n    });\n\n    const response = await request(app)\n      .del(`/carts/${globalUser.username}/items/cheesecake`)\n      .set(\"authorization\", globalUser.authHeader)\n      .expect(400)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"cheesecake is not in the cart\"\n    });\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(0);\n  });\n});\n\ndescribe(\"create accounts\", () => {\n  test(\"creating a new account\", async () => {\n    const response = await request(app)\n      .put(\"/users/another_user\")\n      .send({ email: \"another_user@example.org\", password: \"a_password\" })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"another_user created successfully\"\n    });\n\n    const savedUser = await db\n      .select(\"email\", \"passwordHash\")\n      .from(\"users\")\n      .where({ username: \"another_user\" })\n      .first();\n\n    expect(savedUser).toEqual({\n      email: \"another_user@example.org\",\n      passwordHash: hashPassword(\"a_password\")\n    });\n  });\n\n  test(\"creating a duplicate account\", async () => {\n    const response = await request(app)\n      .put(`/users/${globalUser.username}`)\n      .send({ email: globalUser.email, password: \"a_password\" })\n      .expect(409)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: `${globalUser.username} already exists`\n    });\n  });\n});\n\ndescribe(\"fetch inventory items\", () => {\n  const eggs = { itemName: \"eggs\", quantity: 3 };\n  const applePie = { itemName: \"apple pie\", quantity: 1 };\n\n  beforeEach(async () => {\n    await db(\"inventory\").insert([eggs, applePie]);\n    const { id: eggsId } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"eggs\" })\n      .first();\n    eggs.id = eggsId;\n  });\n\n  beforeEach(() => {\n    nock.cleanAll();\n  });\n\n  afterEach(() => {\n    if (!nock.isDone()) {\n      throw new Error(\"Not all mocked endpoints received requests.\");\n    }\n  });\n\n  test(\"can fetch an item from the inventory\", async () => {\n    const eggsResponse = {\n      title: \"FakeAPI\",\n      href: \"example.org\",\n      results: [{ name: \"Omelette du Fromage\" }]\n    };\n\n    nock(\"http://recipepuppy.com\")\n      .get(\"/api\")\n      .query({ i: \"eggs\" })\n      .reply(200, eggsResponse);\n\n    const response = await request(app)\n      .get(`/inventory/eggs`)\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      ...eggs,\n      info: `Data obtained from ${eggsResponse.title} - ${eggsResponse.href}`,\n      recipes: eggsResponse.results\n    });\n  });\n});\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/truncateTables.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst tablesToTruncate = [\"users\", \"inventory\", \"carts_items\"];\n\nbeforeEach(() => {\n  return Promise.all(tablesToTruncate.map(t => db(t).truncate()));\n});\n"
  },
  {
    "path": "chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/userTestUtils.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { hashPassword } = require(\"./authenticationController\");\n\nconst username = \"test_user\";\nconst password = \"a_password\";\nconst passwordHash = hashPassword(password);\nconst email = \"test_user@example.org\";\nconst validAuth = Buffer.from(`${username}:${password}`).toString(\"base64\");\nconst authHeader = `Basic ${validAuth}`;\n\nconst user = {\n  username,\n  password,\n  email,\n  authHeader\n};\n\nconst createUser = async () => {\n  await db(\"users\").insert({ username, email, passwordHash });\n  const { id } = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  user.id = id;\n};\n\nmodule.exports = { user, createUser };\n"
  },
  {
    "path": "chapter5/1_eliminating_non_determinism/1_shared_resources/countModule.js",
    "content": "const fs = require(\"fs\");\nconst filepath = \"./state.txt\";\n\nconst getState = () => parseInt(fs.readFileSync(filepath), 10);\nconst setState = n => fs.writeFileSync(filepath, n);\nconst increment = () => fs.writeFileSync(filepath, getState() + 1);\nconst decrement = () => fs.writeFileSync(filepath, getState() - 1);\n\nmodule.exports = { getState, setState, increment, decrement };\n"
  },
  {
    "path": "chapter5/1_eliminating_non_determinism/1_shared_resources/decrement.test.js",
    "content": "const { getState, setState, decrement } = require(\"./countModule\");\n\ntest(\"decrementing the state 10 times\", () => {\n  setState(0);\n  for (let i = 0; i < 10; i++) {\n    decrement();\n  }\n\n  expect(getState()).toBe(-10);\n});\n"
  },
  {
    "path": "chapter5/1_eliminating_non_determinism/1_shared_resources/increment.test.js",
    "content": "const { getState, setState, increment } = require(\"./countModule\");\n\ntest(\"incrementing the state 10 times\", () => {\n  setState(0);\n  for (let i = 0; i < 10; i++) {\n    increment();\n  }\n\n  expect(getState()).toBe(10);\n});\n"
  },
  {
    "path": "chapter5/1_eliminating_non_determinism/1_shared_resources/package.json",
    "content": "{\n  \"name\": \"1_shared_resources\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"jest\": \"^26.6.0\"\n  }\n}\n"
  },
  {
    "path": "chapter5/1_eliminating_non_determinism/2_resource_pools/countModule.js",
    "content": "const fs = require(\"fs\");\n\nconst init = filepath => {\n  const getState = () => {\n    return parseInt(fs.readFileSync(filepath, \"utf-8\"), 10);\n  };\n  const setState = n => fs.writeFileSync(filepath, n);\n  const increment = () => fs.writeFileSync(filepath, getState() + 1);\n  const decrement = () => fs.writeFileSync(filepath, getState() - 1);\n\n  return { getState, setState, increment, decrement };\n};\n\nmodule.exports = { init };\n"
  },
  {
    "path": "chapter5/1_eliminating_non_determinism/2_resource_pools/decrement.test.js",
    "content": "const pool = require(\"./instancePool\");\nconst instance = pool.getInstance(process.env.JEST_WORKER_ID);\nconst { setState, getState, decrement } = instance;\n\ntest(\"decrementing the state 10 times\", () => {\n  setState(0);\n  for (let i = 0; i < 10; i++) {\n    decrement();\n  }\n\n  expect(getState()).toBe(-10);\n});\n"
  },
  {
    "path": "chapter5/1_eliminating_non_determinism/2_resource_pools/increment.test.js",
    "content": "const pool = require(\"./instancePool\");\nconst instance = pool.getInstance(process.env.JEST_WORKER_ID);\nconst { setState, getState, increment } = instance;\n\ntest(\"incrementing the state 10 times\", () => {\n  setState(0);\n  for (let i = 0; i < 10; i++) {\n    increment();\n  }\n\n  expect(getState()).toBe(10);\n});\n"
  },
  {
    "path": "chapter5/1_eliminating_non_determinism/2_resource_pools/instancePool.js",
    "content": "const { init } = require(\"./countModule\");\n\nconst instancePool = {};\n\nconst getInstance = workerId => {\n  if (!instancePool[workerId]) {\n    instancePool[workerId] = init(`/tmp/test_state_${workerId}.txt`);\n  }\n\n  return instancePool[workerId];\n};\n\nmodule.exports = { getInstance };\n"
  },
  {
    "path": "chapter5/1_eliminating_non_determinism/2_resource_pools/package.json",
    "content": "{\n  \"name\": \"2_resource_pools\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"jest\": \"^25.2.3\"\n  }\n}\n"
  },
  {
    "path": "chapter5/1_eliminating_non_determinism/3_dealing_with_time/authenticationController.js",
    "content": "const crypto = require(\"crypto\");\nconst { db } = require(\"./dbConnection\");\n\nconst hashPassword = password => {\n  const hash = crypto.createHash(\"sha256\");\n  hash.update(password);\n  return hash.digest(\"hex\");\n};\n\nconst credentialsAreValid = async (username, password) => {\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  if (!user) return false;\n  return hashPassword(password) === user.passwordHash;\n};\n\nconst authenticationMiddleware = async (ctx, next) => {\n  try {\n    const authHeader = ctx.request.headers.authorization;\n    const credentials = Buffer.from(\n      authHeader.slice(\"basic\".length + 1),\n      \"base64\"\n    ).toString();\n    const [username, password] = credentials.split(\":\");\n\n    const validCredentialsSent = await credentialsAreValid(username, password);\n    if (!validCredentialsSent) throw new Error(\"invalid credentials\");\n  } catch (e) {\n    ctx.status = 401;\n    ctx.body = { message: \"please provide valid credentials\" };\n    return;\n  }\n\n  await next();\n};\n\nmodule.exports = {\n  hashPassword,\n  credentialsAreValid,\n  authenticationMiddleware\n};\n"
  },
  {
    "path": "chapter5/1_eliminating_non_determinism/3_dealing_with_time/authenticationController.test.js",
    "content": "const crypto = require(\"crypto\");\nconst {\n  hashPassword,\n  credentialsAreValid,\n  authenticationMiddleware\n} = require(\"./authenticationController\");\nconst { user: globalUser } = require(\"./userTestUtils\");\n\ndescribe(\"hashPassword\", () => {\n  test(\"hashing passwords\", () => {\n    const plainTextPassword = \"password_example\";\n    const hash = crypto.createHash(\"sha256\");\n    hash.update(plainTextPassword);\n    const expectedHash = hash.digest(\"hex\");\n    expect(hashPassword(plainTextPassword)).toBe(expectedHash);\n  });\n});\n\ndescribe(\"credentialsAreValid\", () => {\n  test(\"validating credentials\", async () => {\n    expect(await credentialsAreValid(globalUser.username, \"a_password\")).toBe(\n      true\n    );\n  });\n});\n\ndescribe(\"authenticationMiddleware\", () => {\n  test(\"returning an error if the credentials are not valid\", async () => {\n    const fakeAuth = Buffer.from(\"invalid:credentials\").toString(\"base64\");\n    const ctx = {\n      request: {\n        headers: { authorization: `Basic ${fakeAuth}` }\n      }\n    };\n\n    const next = jest.fn();\n    await authenticationMiddleware(ctx, next);\n    expect(next.mock.calls).toHaveLength(0);\n    expect(ctx).toEqual({\n      ...ctx,\n      status: 401,\n      body: { message: \"please provide valid credentials\" }\n    });\n  });\n\n  test(\"authenticating properly\", async () => {\n    const ctx = {\n      request: {\n        headers: { authorization: globalUser.authHeader }\n      }\n    };\n\n    const next = jest.fn();\n    await authenticationMiddleware(ctx, next);\n    expect(next.mock.calls).toHaveLength(1);\n  });\n});\n"
  },
  {
    "path": "chapter5/1_eliminating_non_determinism/3_dealing_with_time/cartController.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { removeFromInventory } = require(\"./inventoryController\");\nconst logger = require(\"./logger\");\n\nconst addItemToCart = async (username, itemName) => {\n  await removeFromInventory(itemName);\n\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  if (!user) {\n    const userNotFound = new Error(\"user not found\");\n    userNotFound.code = 404;\n  }\n\n  const itemEntry = await db\n    .select()\n    .from(\"carts_items\")\n    .where({ userId: user.id, itemName })\n    .first();\n\n  if (itemEntry && itemEntry.quantity + 1 > 3) {\n    const limitError = new Error(\n      \"You can't have more than three units of an item in your cart\"\n    );\n    limitError.code = 400;\n    throw limitError;\n  }\n\n  if (itemEntry) {\n    await db(\"carts_items\")\n      .increment(\"quantity\")\n      .update({ updatedAt: new Date().toISOString() })\n      .where({\n        userId: itemEntry.userId,\n        itemName\n      });\n  } else {\n    await db(\"carts_items\").insert({\n      userId: user.id,\n      itemName,\n      quantity: 1,\n      updatedAt: new Date().toISOString()\n    });\n  }\n\n  logger.log(`${itemName} added to ${username}'s cart`);\n  return db\n    .select(\"itemName\", \"quantity\")\n    .from(\"carts_items\")\n    .where({ userId: user.id });\n};\n\nconst hoursInMs = n => 1000 * 60 * 60 * n;\n\nconst removeStaleItems = async () => {\n  const fourHoursAgo = new Date(Date.now() - hoursInMs(4)).toISOString();\n\n  const staleItems = await db\n    .select()\n    .from(\"carts_items\")\n    .where(\"updatedAt\", \"<\", fourHoursAgo);\n\n  if (staleItems.length === 0) return;\n\n  // Put stale items back in the inventory\n  const inventoryUpdates = staleItems.map(staleItem =>\n    db(\"inventory\")\n      .increment(\"quantity\", staleItem.quantity)\n      .where({ itemName: staleItem.itemName })\n  );\n  await Promise.all(inventoryUpdates);\n\n  // Delete stale items from cart\n  const staleItemTuples = staleItems.map(i => [i.itemName, i.userId]);\n  await db(\"carts_items\")\n    .del()\n    .whereIn([\"itemName\", \"userId\"], staleItemTuples);\n};\n\nconst monitorStaleItems = () => setInterval(removeStaleItems, hoursInMs(2));\n\nmodule.exports = { addItemToCart, monitorStaleItems };\n"
  },
  {
    "path": "chapter5/1_eliminating_non_determinism/3_dealing_with_time/cartController.test.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { addItemToCart, monitorStaleItems } = require(\"./cartController\");\nconst { hashPassword } = require(\"./authenticationController\");\nconst { user: globalUser } = require(\"./userTestUtils\");\nconst FakeTimers = require(\"@sinonjs/fake-timers\");\n\nconst fs = require(\"fs\");\n\ndescribe(\"addItemToCart\", () => {\n  beforeEach(() => {\n    fs.writeFileSync(\"/tmp/logs.out\", \"\");\n  });\n\n  test(\"adding unavailable items to cart\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 0 });\n\n    try {\n      await addItemToCart(globalUser.username, \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(\"cheesecake is unavailable\");\n      expectedError.code = 400;\n\n      expect(e).toEqual(expectedError);\n    }\n\n    const finalCartContent = await db\n      .select(\"carts_items.*\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n\n    expect(finalCartContent).toEqual([]);\n    expect.assertions(2);\n  });\n\n  test(\"adding items above limit to cart\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 1 });\n    await db(\"carts_items\").insert({\n      userId: globalUser.id,\n      itemName: \"cheesecake\",\n      quantity: 3\n    });\n\n    try {\n      await addItemToCart(globalUser.username, \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(\n        \"You can't have more than three units of an item in your cart\"\n      );\n      expectedError.code = 400;\n      expect(e).toEqual(expectedError);\n    }\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n\n    expect(finalCartContent).toEqual([{ itemName: \"cheesecake\", quantity: 3 }]);\n    expect.assertions(2);\n  });\n\n  test(\"logging added items\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 1 });\n    await db(\"carts_items\").insert({\n      userId: globalUser.id,\n      itemName: \"cheesecake\",\n      quantity: 1\n    });\n\n    await addItemToCart(globalUser.username, \"cheesecake\");\n\n    const logs = fs.readFileSync(\"/tmp/logs.out\", \"utf-8\");\n    expect(logs).toContain(\n      `cheesecake added to ${globalUser.username}'s cart\\n`\n    );\n  });\n});\n\nconst withRetries = async fn => {\n  // Capture the assertion error since Jest does not export it\n  const JestAssertionError = (() => {\n    try {\n      expect(false).toBe(true);\n    } catch (e) {\n      return e.constructor;\n    }\n  })();\n\n  try {\n    await fn();\n  } catch (e) {\n    if (e.constructor === JestAssertionError) {\n      // Wait 100ms before retrying\n      await new Promise(resolve => setTimeout(resolve, 100));\n      await withRetries(fn);\n    } else {\n      throw e;\n    }\n  }\n};\n\ndescribe(\"timers\", () => {\n  const hoursInMs = n => 1000 * 60 * 60 * n;\n\n  let clock;\n  beforeEach(() => {\n    clock = FakeTimers.install({ toFake: [\"Date\", \"setInterval\"] });\n  });\n\n  afterEach(() => {\n    clock = clock.uninstall();\n  });\n\n  test(\"removing stale items\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 1 });\n    await addItemToCart(globalUser.username, \"cheesecake\");\n\n    clock.tick(hoursInMs(4));\n    timer = monitorStaleItems();\n    clock.tick(hoursInMs(2));\n\n    await withRetries(async () => {\n      const finalCartContent = await db\n        .select()\n        .from(\"carts_items\")\n        .join(\"users\", \"users.id\", \"carts_items.userId\")\n        .where(\"users.username\", globalUser.username);\n\n      expect(finalCartContent).toEqual([]);\n    });\n\n    await withRetries(async () => {\n      const inventoryContent = await db\n        .select(\"itemName\", \"quantity\")\n        .from(\"inventory\");\n\n      expect(inventoryContent).toEqual([\n        { itemName: \"cheesecake\", quantity: 1 }\n      ]);\n    });\n  });\n});\n"
  },
  {
    "path": "chapter5/1_eliminating_non_determinism/3_dealing_with_time/dbConnection.js",
    "content": "const environmentName = process.env.NODE_ENV;\nconst knex = require(\"knex\");\nconst knexConfig = require(\"./knexfile\")[environmentName];\n\nconst db = knex(knexConfig);\n\nconst closeConnection = () => db.destroy();\n\nmodule.exports = {\n  db,\n  closeConnection\n};\n"
  },
  {
    "path": "chapter5/1_eliminating_non_determinism/3_dealing_with_time/disconnectFromDb.js",
    "content": "const { db } = require(\"./dbConnection\");\n\nafterAll(() => db.destroy());\n"
  },
  {
    "path": "chapter5/1_eliminating_non_determinism/3_dealing_with_time/inventoryController.js",
    "content": "const { db } = require(\"./dbConnection\");\n\nconst removeFromInventory = async itemName => {\n  const inventoryEntry = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName })\n    .first();\n\n  if (!inventoryEntry || inventoryEntry.quantity === 0) {\n    const err = new Error(`${itemName} is unavailable`);\n    err.code = 400;\n    throw err;\n  }\n\n  await db(\"inventory\")\n    .decrement(\"quantity\")\n    .where({ itemName });\n};\n\nmodule.exports = { removeFromInventory };\n"
  },
  {
    "path": "chapter5/1_eliminating_non_determinism/3_dealing_with_time/jest.config.js",
    "content": "module.exports = {\n  testEnvironment: \"node\",\n  globalSetup: \"./migrateDatabases.js\",\n  setupFilesAfterEnv: [\n    \"<rootDir>/truncateTables.js\",\n    \"<rootDir>/seedUser.js\",\n    \"<rootDir>/disconnectFromDb.js\"\n  ]\n};\n"
  },
  {
    "path": "chapter5/1_eliminating_non_determinism/3_dealing_with_time/knexfile.js",
    "content": "module.exports = {\n  test: {\n    client: \"sqlite3\",\n    connection: { filename: \"./test.sqlite\" },\n    useNullAsDefault: true\n  },\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"./dev.sqlite\" },\n    useNullAsDefault: true\n  }\n};\n"
  },
  {
    "path": "chapter5/1_eliminating_non_determinism/3_dealing_with_time/logger.js",
    "content": "const fs = require(\"fs\");\n\nconst logger = {\n  log: msg => fs.appendFileSync(\"/tmp/logs.out\", msg + \"\\n\")\n};\n\nmodule.exports = logger;\n"
  },
  {
    "path": "chapter5/1_eliminating_non_determinism/3_dealing_with_time/migrateDatabases.js",
    "content": "const environmentName = process.env.NODE_ENV || \"test\";\nconst environmentConfig = require(\"./knexfile\")[environmentName];\nconst db = require(\"knex\")(environmentConfig);\n\nmodule.exports = async () => {\n  // Migrate the database to the latest state\n  await db.migrate.latest();\n\n  // Close the connection to the database so that tests won't hang\n  await db.destroy();\n};\n"
  },
  {
    "path": "chapter5/1_eliminating_non_determinism/3_dealing_with_time/migrations/20200325082401_initial_schema.js",
    "content": "exports.up = async knex => {\n  await knex.schema.createTable(\"users\", table => {\n    table.increments(\"id\");\n    table.string(\"username\");\n    table.unique(\"username\");\n    table.string(\"email\");\n    table.string(\"passwordHash\");\n  });\n\n  await knex.schema.createTable(\"carts_items\", table => {\n    table.integer(\"userId\").references(\"users.id\");\n    table.string(\"itemName\");\n    table.unique(\"itemName\");\n    table.integer(\"quantity\");\n  });\n\n  await knex.schema.createTable(\"inventory\", table => {\n    table.increments(\"id\");\n    table.string(\"itemName\");\n    table.unique(\"itemName\");\n    table.integer(\"quantity\");\n  });\n};\n\nexports.down = async knex => {\n  await knex.schema.dropTable(\"users\");\n  await knex.schema.dropTable(\"carts_items\");\n  await knex.schema.dropTable(\"inventory\");\n};\n"
  },
  {
    "path": "chapter5/1_eliminating_non_determinism/3_dealing_with_time/migrations/20200331210311_updatedAt_field.js",
    "content": "exports.up = knex => {\n  return knex.schema.alterTable(\"carts_items\", table => {\n    table.timestamp(\"updatedAt\");\n  });\n};\n\nexports.down = knex => {\n  return knex.schema.alterTable(\"carts_items\", table => {\n    table.dropColumn(\"updatedAt\");\n  });\n};\n"
  },
  {
    "path": "chapter5/1_eliminating_non_determinism/3_dealing_with_time/package.json",
    "content": "{\n  \"name\": \"3_dealing_with_time\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"test\": \"jest --runInBand\",\n    \"start\": \"node server.js\"\n  },\n  \"devDependencies\": {\n    \"@sinonjs/fake-timers\": \"github:sinonjs/fake-timers\",\n    \"jest\": \"^26.6.0\",\n    \"supertest\": \"^4.0.2\"\n  },\n  \"dependencies\": {\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"knex\": \"^0.20.13\",\n    \"koa\": \"^2.11.0\",\n    \"koa-body-parser\": \"^1.1.2\",\n    \"koa-router\": \"^7.4.0\",\n    \"nock\": \"^12.0.3\",\n    \"sqlite3\": \"^4.1.1\"\n  },\n  \"main\": \"alertController.spec.js\",\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"description\": \"\"\n}\n"
  },
  {
    "path": "chapter5/1_eliminating_non_determinism/3_dealing_with_time/seedUser.js",
    "content": "const { createUser } = require(\"./userTestUtils\");\n\nbeforeEach(createUser);\n"
  },
  {
    "path": "chapter5/1_eliminating_non_determinism/3_dealing_with_time/server.js",
    "content": "const fetch = require(\"isomorphic-fetch\");\nconst Koa = require(\"koa\");\nconst Router = require(\"koa-router\");\nconst bodyParser = require(\"koa-body-parser\");\n\nconst { db } = require(\"./dbConnection\");\n\nconst { addItemToCart } = require(\"./cartController\");\nconst {\n  hashPassword,\n  authenticationMiddleware\n} = require(\"./authenticationController\");\n\nconst app = new Koa();\nconst router = new Router();\n\napp.use(bodyParser());\n\napp.use(async (ctx, next) => {\n  if (ctx.url.startsWith(\"/carts\")) {\n    return await authenticationMiddleware(ctx, next);\n  }\n\n  await next();\n});\n\nrouter.put(\"/users/:username\", async ctx => {\n  const { username } = ctx.params;\n  const { email, password } = ctx.request.body;\n\n  const userAlreadyExists = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n\n  if (userAlreadyExists) {\n    ctx.body = { message: `${username} already exists` };\n    ctx.status = 409;\n    return;\n  }\n\n  await db(\"users\").insert({\n    username,\n    email,\n    passwordHash: hashPassword(password)\n  });\n\n  return (ctx.body = { message: `${username} created successfully` });\n});\n\nrouter.post(\"/carts/:username/items\", async ctx => {\n  const { username } = ctx.params;\n  const { item, quantity } = ctx.request.body;\n\n  for (let i = 0; i < quantity; i++) {\n    try {\n      const newItems = await addItemToCart(username, item);\n      ctx.body = newItems;\n    } catch (e) {\n      ctx.body = { message: e.message };\n      ctx.status = e.code;\n      return;\n    }\n  }\n});\n\nrouter.delete(\"/carts/:username/items/:item\", async ctx => {\n  const { username, item } = ctx.params;\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n\n  if (!user) {\n    ctx.body = { message: \"user not found\" };\n    ctx.status = 404;\n    return;\n  }\n\n  const itemEntry = await db\n    .select()\n    .from(\"carts_items\")\n    .where({ userId: user.id, itemName: item })\n    .first();\n\n  if (!itemEntry || itemEntry.quantity === 0) {\n    ctx.body = { message: `${item} is not in the cart` };\n    ctx.status = 400;\n    return;\n  }\n\n  await db(\"carts_items\")\n    .decrement(\"quantity\")\n    .where({ userId: user.id, itemName: item });\n\n  const inventoryEntry = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName: item })\n    .first();\n  if (inventoryEntry) {\n    await db(\"inventory\")\n      .increment(\"quantity\")\n      .where({ userId: itemEntry.userId, itemName: item });\n  } else {\n    await db(\"inventory\").insert({ itemName: item, quantity: 1 });\n  }\n\n  ctx.body = await db\n    .select(\"itemName\", \"quantity\")\n    .from(\"carts_items\")\n    .where({ userId: user.id });\n});\n\nrouter.get(\"/inventory/:itemName\", async ctx => {\n  const { itemName } = ctx.params;\n\n  const response = await fetch(`http://recipepuppy.com/api?i=${itemName}`);\n  const { title, href, results: recipes } = await response.json();\n  const inventoryItem = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName })\n    .first();\n\n  ctx.body = {\n    ...inventoryItem,\n    info: `Data obtained from ${title} - ${href}`,\n    recipes\n  };\n});\n\napp.use(router.routes());\n\nmodule.exports = { app: app.listen(3000) };\n"
  },
  {
    "path": "chapter5/1_eliminating_non_determinism/3_dealing_with_time/server.test.js",
    "content": "const { user: globalUser } = require(\"./userTestUtils\");\nconst { db } = require(\"./dbConnection\");\nconst request = require(\"supertest\");\nconst { app } = require(\"./server.js\");\nconst { hashPassword } = require(\"./authenticationController.js\");\nconst nock = require(\"nock\");\n\nafterAll(() => app.close());\n\ndescribe(\"add items to a cart\", () => {\n  test(\"adding available items\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 3 });\n    const response = await request(app)\n      .post(`/carts/${globalUser.username}/items`)\n      .set(\"authorization\", globalUser.authHeader)\n      .send({ item: \"cheesecake\", quantity: 3 })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    const newItems = [{ itemName: \"cheesecake\", quantity: 3 }];\n    expect(response.body).toEqual(newItems);\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(0);\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n\n    expect(finalCartContent).toEqual(newItems);\n  });\n\n  test(\"adding unavailable items\", async () => {\n    const response = await request(app)\n      .post(`/carts/${globalUser.username}/items`)\n      .set(\"authorization\", globalUser.authHeader)\n      .send({ item: \"cheesecake\", quantity: 1 })\n      .expect(400)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"cheesecake is unavailable\"\n    });\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n    expect(finalCartContent).toEqual([]);\n  });\n});\n\ndescribe(\"removing items from a cart\", () => {\n  test(\"removing existing items\", async () => {\n    await db(\"carts_items\").insert({\n      userId: globalUser.id,\n      itemName: \"cheesecake\",\n      quantity: 1\n    });\n\n    const response = await request(app)\n      .del(`/carts/${globalUser.username}/items/cheesecake`)\n      .set(\"authorization\", globalUser.authHeader)\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    const expectedFinalContent = [{ itemName: \"cheesecake\", quantity: 0 }];\n\n    expect(response.body).toEqual(expectedFinalContent);\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n    expect(finalCartContent).toEqual(expectedFinalContent);\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(1);\n  });\n\n  test(\"removing non-existing items\", async () => {\n    await db(\"inventory\").insert({\n      itemName: \"cheesecake\",\n      quantity: 0\n    });\n\n    const response = await request(app)\n      .del(`/carts/${globalUser.username}/items/cheesecake`)\n      .set(\"authorization\", globalUser.authHeader)\n      .expect(400)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"cheesecake is not in the cart\"\n    });\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(0);\n  });\n});\n\ndescribe(\"create accounts\", () => {\n  test(\"creating a new account\", async () => {\n    const response = await request(app)\n      .put(\"/users/another_user\")\n      .send({ email: \"another_user@example.org\", password: \"a_password\" })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"another_user created successfully\"\n    });\n\n    const savedUser = await db\n      .select(\"email\", \"passwordHash\")\n      .from(\"users\")\n      .where({ username: \"another_user\" })\n      .first();\n\n    expect(savedUser).toEqual({\n      email: \"another_user@example.org\",\n      passwordHash: hashPassword(\"a_password\")\n    });\n  });\n\n  test(\"creating a duplicate account\", async () => {\n    const response = await request(app)\n      .put(`/users/${globalUser.username}`)\n      .send({ email: globalUser.email, password: \"a_password\" })\n      .expect(409)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: `${globalUser.username} already exists`\n    });\n  });\n});\n\ndescribe(\"fetch inventory items\", () => {\n  const eggs = { itemName: \"eggs\", quantity: 3 };\n  const applePie = { itemName: \"apple pie\", quantity: 1 };\n\n  beforeEach(async () => {\n    await db(\"inventory\").insert([eggs, applePie]);\n    const { id: eggsId } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"eggs\" })\n      .first();\n    eggs.id = eggsId;\n  });\n\n  test(\"can fetch an item from the inventory\", async () => {\n    const eggsResponse = {\n      title: \"FakeAPI\",\n      href: \"example.org\",\n      results: [{ name: \"Omelette du Fromage\" }]\n    };\n\n    nock(\"http://recipepuppy.com\")\n      .get(\"/api\")\n      .query({ i: \"eggs\" })\n      .reply(200, eggsResponse);\n\n    const response = await request(app)\n      .get(`/inventory/eggs`)\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      ...eggs,\n      info: `Data obtained from ${eggsResponse.title} - ${eggsResponse.href}`,\n      recipes: eggsResponse.results\n    });\n  });\n});\n"
  },
  {
    "path": "chapter5/1_eliminating_non_determinism/3_dealing_with_time/truncateTables.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst tablesToTruncate = [\"users\", \"inventory\", \"carts_items\"];\n\nbeforeEach(() => {\n  return Promise.all(tablesToTruncate.map(t => db(t).truncate()));\n});\n"
  },
  {
    "path": "chapter5/1_eliminating_non_determinism/3_dealing_with_time/userTestUtils.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { hashPassword } = require(\"./authenticationController\");\n\nconst username = \"test_user\";\nconst password = \"a_password\";\nconst passwordHash = hashPassword(password);\nconst email = \"test_user@example.org\";\nconst validAuth = Buffer.from(`${username}:${password}`).toString(\"base64\");\nconst authHeader = `Basic ${validAuth}`;\n\nconst user = {\n  username,\n  password,\n  email,\n  authHeader\n};\n\nconst createUser = async () => {\n  await db(\"users\").insert({ username, email, passwordHash });\n  const { id } = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  user.id = id;\n};\n\nmodule.exports = { user, createUser };\n"
  },
  {
    "path": "chapter6/1_introducing_jsdom/1_pure_html/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Inventory Manager</title>\n  </head>\n  <body>\n    <h1>Cheesecakes: <span id=\"count\">0</span></h1>\n    <button id=\"increment-button\">Add cheesecake</button>\n    <script src=\"main.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter6/1_introducing_jsdom/1_pure_html/main.js",
    "content": "let data = { cheesecakes: 0 };\n\nconst incrementCount = () => {\n  data.cheesecakes++;\n  window.document.getElementById(\"count\").innerText = data.cheesecakes;\n};\n\nconst incrementButton = window.document.getElementById(\"increment-button\");\nincrementButton.addEventListener(\"click\", incrementCount);\n"
  },
  {
    "path": "chapter6/1_introducing_jsdom/2_jsdom/example.js",
    "content": "const page = require(\"./page\");\n\nconsole.log(\"Initial page body:\");\nconsole.log(page.window.document.body.innerHTML);\n\nconsole.log(\"Initial contents of the count element:\");\nconsole.log(page.window.document.getElementById(\"count\").innerHTML);\n\n// Changing the count element's content\npage.window.document.getElementById(\"count\").innerHTML = 1337;\nconsole.log(\"Updated contents of the count element:\");\nconsole.log(page.window.document.getElementById(\"count\").innerHTML);\n\n// Appending a paragraph to the page\nconst paragraph = page.window.document.createElement(\"p\");\nparagraph.innerText = \"Look, I'm a new paragraph\";\npage.window.document.body.appendChild(paragraph);\n\nconsole.log(\"Final page body:\");\nconsole.log(page.window.document.body.innerHTML);\n"
  },
  {
    "path": "chapter6/1_introducing_jsdom/2_jsdom/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Inventory Manager</title>\n  </head>\n  <body>\n    <h1>Cheesecakes: <span id=\"count\">0</span></h1>\n    <button id=\"increment-button\">Add cheesecake</button>\n    <script src=\"main.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter6/1_introducing_jsdom/2_jsdom/main.js",
    "content": "let data = { cheesecakes: 0 };\n\nconst incrementCount = () => {\n  data.cheesecakes++;\n  window.document.getElementById(\"count\").innerText = data.cheesecakes;\n};\n\nconst incrementButton = window.document.getElementById(\"increment-button\");\nincrementButton.addEventListener(\"click\", incrementCount);\n"
  },
  {
    "path": "chapter6/1_introducing_jsdom/2_jsdom/package.json",
    "content": "{\n  \"name\": \"2_jsdom\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"main.js\",\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"dependencies\": {\n    \"jsdom\": \"^16.2.2\"\n  }\n}\n"
  },
  {
    "path": "chapter6/1_introducing_jsdom/2_jsdom/page.js",
    "content": "const fs = require(\"fs\");\nconst { JSDOM } = require(\"jsdom\");\n\nconst html = fs.readFileSync(\"./index.html\");\nconst page = new JSDOM(html);\n\nmodule.exports = page;\n"
  },
  {
    "path": "chapter6/1_introducing_jsdom/3_jest_jsdom/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Inventory Manager</title>\n  </head>\n  <body>\n    <h1>Cheesecakes: <span id=\"count\">0</span></h1>\n    <button id=\"increment-button\">Add cheesecake</button>\n    <script src=\"main.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter6/1_introducing_jsdom/3_jest_jsdom/jest.config.js",
    "content": "module.exports = {\n  testEnvironment: \"jsdom\"\n};\n"
  },
  {
    "path": "chapter6/1_introducing_jsdom/3_jest_jsdom/main.js",
    "content": "let data = { cheesecakes: 0 };\n\nconst incrementCount = () => {\n  data.cheesecakes++;\n  window.document.getElementById(\"count\").innerText = data.cheesecakes;\n};\n\nconst incrementButton = window.document.getElementById(\"increment-button\");\nincrementButton.addEventListener(\"click\", incrementCount);\n\nmodule.exports = { incrementCount, data };\n"
  },
  {
    "path": "chapter6/1_introducing_jsdom/3_jest_jsdom/main.test.js",
    "content": "const fs = require(\"fs\");\nwindow.document.body.innerHTML = fs.readFileSync(\"./index.html\");\n\nconst { incrementCount, data } = require(\"./main\");\n\ndescribe(\"incrementCount\", () => {\n  test(\"incrementing the count\", () => {\n    data.cheesecakes = 0;\n    incrementCount();\n    expect(data.cheesecakes).toBe(1);\n  });\n});\n"
  },
  {
    "path": "chapter6/1_introducing_jsdom/3_jest_jsdom/package.json",
    "content": "{\n  \"name\": \"3_jest_jsdom\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"main.js\",\n  \"scripts\": {\n    \"test\": \"jest\",\n    \"build\": \"browserify main.js -o bundle.js\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"dependencies\": {},\n  \"devDependencies\": {\n    \"browserify\": \"^16.5.1\",\n    \"jest\": \"^26.6.0\"\n  }\n}\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/1_finding_elements_by_dom_structure/domController.js",
    "content": "const updateItemList = inventory => {\n  const inventoryList = window.document.getElementById(\"item-list\");\n\n  // Clears the list\n  inventoryList.innerHTML = \"\";\n\n  Object.entries(inventory).forEach(([itemName, quantity]) => {\n    const listItem = window.document.createElement(\"li\");\n    listItem.innerHTML = `${itemName} - Quantity: ${quantity}`;\n    inventoryList.appendChild(listItem);\n  });\n};\n\nmodule.exports = { updateItemList };\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/1_finding_elements_by_dom_structure/domController.test.js",
    "content": "const fs = require(\"fs\");\ndocument.body.innerHTML = fs.readFileSync(\"./index.html\");\n\nconst { updateItemList } = require(\"./domController\");\n\ndescribe(\"updateItemList\", () => {\n  test(\"updates the DOM with the inventory items\", () => {\n    const inventory = {\n      cheesecake: 5,\n      \"apple pie\": 2,\n      \"carrot cake\": 6\n    };\n    updateItemList(inventory);\n\n    const itemList = document.querySelector(\"body > ul\");\n    expect(itemList.childNodes).toHaveLength(3);\n\n    // The `childNodes` property has a `length`, but it's _not_ an Array\n    const nodesText = Array.from(itemList.childNodes).map(\n      node => node.innerHTML\n    );\n    expect(nodesText).toContain(\"cheesecake - Quantity: 5\");\n    expect(nodesText).toContain(\"apple pie - Quantity: 2\");\n    expect(nodesText).toContain(\"carrot cake - Quantity: 6\");\n  });\n});\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/1_finding_elements_by_dom_structure/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Inventory Manager</title>\n  </head>\n  <body>\n    <h1>Inventory Contents</h1>\n    <ul id=\"item-list\"></ul>\n    <script src=\"bundle.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/1_finding_elements_by_dom_structure/inventoryController.js",
    "content": "const data = { inventory: {} };\n\nconst addItem = (itemName, quantity) => {\n  const currentQuantity = data.inventory[itemName] || 0;\n  data.inventory[itemName] = currentQuantity + quantity;\n};\n\nmodule.exports = { data, addItem };\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/1_finding_elements_by_dom_structure/inventoryController.test.js",
    "content": "const { addItem, data } = require(\"./inventoryController\");\n\ndescribe(\"addItem\", () => {\n  test(\"adding new items to the inventory\", () => {\n    data.inventory = {};\n    addItem(\"cheesecake\", 5);\n    expect(data.inventory.cheesecake).toBe(5);\n  });\n});\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/1_finding_elements_by_dom_structure/main.js",
    "content": "const { addItem, data } = require(\"./inventoryController\");\nconst { updateItemList } = require(\"./domController\");\n\naddItem(\"cheesecake\", 3);\naddItem(\"apple pie\", 8);\naddItem(\"carrot cake\", 7);\n\nupdateItemList(data.inventory);\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/1_finding_elements_by_dom_structure/package.json",
    "content": "{\n  \"name\": \"1_finding_elements_by_dom_structure\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"start\": \"http-server ./\",\n    \"test\": \"jest\",\n    \"build\": \"browserify main.js -o bundle.js\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"browserify\": \"^16.5.1\",\n    \"http-server\": \"^0.12.1\",\n    \"jest\": \"^24.9.0\"\n  }\n}\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/2_finding_elements_by_id/domController.js",
    "content": "const updateItemList = inventory => {\n  const inventoryList = window.document.getElementById(\"item-list\");\n\n  // Clears the list\n  inventoryList.innerHTML = \"\";\n\n  Object.entries(inventory).forEach(([itemName, quantity]) => {\n    const listItem = window.document.createElement(\"li\");\n    listItem.innerHTML = `${itemName} - Quantity: ${quantity}`;\n    inventoryList.appendChild(listItem);\n  });\n};\n\nmodule.exports = { updateItemList };\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/2_finding_elements_by_id/domController.test.js",
    "content": "const fs = require(\"fs\");\ndocument.body.innerHTML = fs.readFileSync(\"./index.html\");\n\nconst { updateItemList } = require(\"./domController\");\n\ndescribe(\"updateItemList\", () => {\n  test(\"updates the DOM with the inventory items\", () => {\n    const inventory = {\n      cheesecake: 5,\n      \"apple pie\": 2,\n      \"carrot cake\": 6\n    };\n    updateItemList(inventory);\n\n    const itemList = document.getElementById(\"item-list\");\n    expect(itemList.childNodes).toHaveLength(3);\n\n    // The `childNodes` property has a `length`, but it's _not_ an Array\n    const nodesText = Array.from(itemList.childNodes).map(\n      node => node.innerHTML\n    );\n    expect(nodesText).toContain(\"cheesecake - Quantity: 5\");\n    expect(nodesText).toContain(\"apple pie - Quantity: 2\");\n    expect(nodesText).toContain(\"carrot cake - Quantity: 6\");\n  });\n});\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/2_finding_elements_by_id/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Inventory Manager</title>\n  </head>\n  <body>\n    <h1>Inventory Contents</h1>\n    <div class=\"beautiful-styles\">\n      <ul id=\"item-list\"></ul>\n    </div>\n    <script src=\"bundle.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/2_finding_elements_by_id/inventoryController.js",
    "content": "const data = { inventory: {} };\n\nconst addItem = (itemName, quantity) => {\n  const currentQuantity = data.inventory[itemName] || 0;\n  data.inventory[itemName] = currentQuantity + quantity;\n};\n\nmodule.exports = { data, addItem };\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/2_finding_elements_by_id/inventoryController.test.js",
    "content": "const { addItem, data } = require(\"./inventoryController\");\n\ndescribe(\"addItem\", () => {\n  test(\"adding new items to the inventory\", () => {\n    data.inventory = {};\n    addItem(\"cheesecake\", 5);\n    expect(data.inventory.cheesecake).toBe(5);\n  });\n});\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/2_finding_elements_by_id/main.js",
    "content": "const { addItem, data } = require(\"./inventoryController\");\nconst { updateItemList } = require(\"./domController\");\n\naddItem(\"cheesecake\", 3);\naddItem(\"apple pie\", 8);\naddItem(\"carrot cake\", 7);\n\nupdateItemList(data.inventory);\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/2_finding_elements_by_id/package.json",
    "content": "{\n  \"name\": \"2_finding_elements_by_id\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"start\": \"http-server ./\",\n    \"test\": \"jest\",\n    \"build\": \"browserify main.js -o bundle.js\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"browserify\": \"^16.5.1\",\n    \"http-server\": \"^0.12.1\",\n    \"jest\": \"^24.9.0\"\n  }\n}\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/3_robust_element_queries/domController.js",
    "content": "const updateItemList = inventory => {\n  const inventoryList = window.document.getElementById(\"item-list\");\n\n  // Clears the list\n  inventoryList.innerHTML = \"\";\n\n  Object.entries(inventory).forEach(([itemName, quantity]) => {\n    const listItem = window.document.createElement(\"li\");\n    listItem.innerHTML = `${itemName} - Quantity: ${quantity}`;\n    inventoryList.appendChild(listItem);\n  });\n\n  const inventoryContents = JSON.stringify(inventory);\n  const p = window.document.createElement(\"p\");\n  p.innerHTML = `The inventory has been updated - ${inventoryContents}`;\n  window.document.body.appendChild(p);\n};\n\nmodule.exports = { updateItemList };\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/3_robust_element_queries/domController.test.js",
    "content": "const fs = require(\"fs\");\nconst initialHtml = fs.readFileSync(\"./index.html\");\n\nconst { updateItemList } = require(\"./domController\");\n\nbeforeEach(() => {\n  document.body.innerHTML = initialHtml;\n});\n\ndescribe(\"updateItemList\", () => {\n  test(\"updates the DOM with the inventory items\", () => {\n    const inventory = {\n      cheesecake: 5,\n      \"apple pie\": 2,\n      \"carrot cake\": 6\n    };\n    updateItemList(inventory);\n\n    const itemList = document.getElementById(\"item-list\");\n    expect(itemList.childNodes).toHaveLength(3);\n\n    // The `childNodes` property has a `length`, but it's _not_ an Array\n    const nodesText = Array.from(itemList.childNodes).map(\n      node => node.innerHTML\n    );\n    expect(nodesText).toContain(\"cheesecake - Quantity: 5\");\n    expect(nodesText).toContain(\"apple pie - Quantity: 2\");\n    expect(nodesText).toContain(\"carrot cake - Quantity: 6\");\n  });\n\n  test(\"adding a paragraph indicating what was the update\", () => {\n    const inventory = { cheesecake: 5, \"apple pie\": 2 };\n    updateItemList(inventory);\n    const paragraphs = Array.from(document.querySelectorAll(\"p\"));\n    const updateParagraphs = paragraphs.filter(p => {\n      return p.innerHTML.includes(\"The inventory has been updated\");\n    });\n\n    expect(updateParagraphs).toHaveLength(1);\n    expect(updateParagraphs[0].innerHTML).toBe(\n      `The inventory has been updated - ${JSON.stringify(inventory)}`\n    );\n  });\n});\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/3_robust_element_queries/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Inventory Manager</title>\n  </head>\n  <body>\n    <h1 data-testid=\"page-header\">Inventory Contents</h1>\n    <ul id=\"item-list\"></ul>\n    <script src=\"bundle.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/3_robust_element_queries/inventoryController.js",
    "content": "const data = { inventory: {} };\n\nconst addItem = (itemName, quantity) => {\n  const currentQuantity = data.inventory[itemName] || 0;\n  data.inventory[itemName] = currentQuantity + quantity;\n};\n\nmodule.exports = { data, addItem };\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/3_robust_element_queries/inventoryController.test.js",
    "content": "const { addItem, data } = require(\"./inventoryController\");\n\ndescribe(\"addItem\", () => {\n  test(\"adding new items to the inventory\", () => {\n    data.inventory = {};\n    addItem(\"cheesecake\", 5);\n    expect(data.inventory.cheesecake).toBe(5);\n  });\n});\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/3_robust_element_queries/main.js",
    "content": "const { addItem, data } = require(\"./inventoryController\");\nconst { updateItemList } = require(\"./domController\");\n\naddItem(\"cheesecake\", 3);\naddItem(\"apple pie\", 8);\naddItem(\"carrot cake\", 7);\n\nupdateItemList(data.inventory);\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/3_robust_element_queries/package.json",
    "content": "{\n  \"name\": \"3_robust_element_queries\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"start\": \"http-server ./\",\n    \"test\": \"jest\",\n    \"build\": \"browserify main.js -o bundle.js\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"browserify\": \"^16.5.1\",\n    \"http-server\": \"^0.12.1\",\n    \"jest\": \"^24.9.0\"\n  }\n}\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/4_finding_with_dom_testing_library/domController.js",
    "content": "const updateItemList = inventory => {\n  const inventoryList = window.document.getElementById(\"item-list\");\n\n  // Clears the list\n  inventoryList.innerHTML = \"\";\n\n  Object.entries(inventory).forEach(([itemName, quantity]) => {\n    const listItem = window.document.createElement(\"li\");\n    listItem.innerHTML = `${itemName} - Quantity: ${quantity}`;\n    inventoryList.appendChild(listItem);\n  });\n\n  const inventoryContents = JSON.stringify(inventory);\n  const p = window.document.createElement(\"p\");\n  p.innerHTML = `The inventory has been updated - ${inventoryContents}`;\n  window.document.body.appendChild(p);\n};\n\nmodule.exports = { updateItemList };\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/4_finding_with_dom_testing_library/domController.test.js",
    "content": "const fs = require(\"fs\");\nconst initialHtml = fs.readFileSync(\"./index.html\");\nconst { getByText, screen } = require(\"@testing-library/dom\");\n\nconst { updateItemList } = require(\"./domController\");\n\nbeforeEach(() => {\n  document.body.innerHTML = initialHtml;\n});\n\ndescribe(\"updateItemList\", () => {\n  test(\"updates the DOM with the inventory items\", () => {\n    const inventory = {\n      cheesecake: 5,\n      \"apple pie\": 2,\n      \"carrot cake\": 6\n    };\n    updateItemList(inventory);\n\n    const itemList = document.getElementById(\"item-list\");\n    expect(itemList.childNodes).toHaveLength(3);\n\n    expect(getByText(itemList, \"cheesecake - Quantity: 5\")).toBeTruthy();\n    expect(getByText(itemList, \"apple pie - Quantity: 2\")).toBeTruthy();\n    expect(getByText(itemList, \"carrot cake - Quantity: 6\")).toBeTruthy();\n  });\n\n  test(\"adding a paragraph indicating what was the update\", () => {\n    const inventory = { cheesecake: 5, \"apple pie\": 2 };\n    updateItemList(inventory);\n\n    expect(\n      screen.getByText(\n        `The inventory has been updated - ${JSON.stringify(inventory)}`\n      )\n    ).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/4_finding_with_dom_testing_library/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Inventory Manager</title>\n  </head>\n  <body>\n    <h1 data-testid=\"page-header\">Inventory Contents</h1>\n    <ul id=\"item-list\"></ul>\n    <script src=\"bundle.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/4_finding_with_dom_testing_library/inventoryController.js",
    "content": "const data = { inventory: {} };\n\nconst addItem = (itemName, quantity) => {\n  const currentQuantity = data.inventory[itemName] || 0;\n  data.inventory[itemName] = currentQuantity + quantity;\n};\n\nmodule.exports = { data, addItem };\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/4_finding_with_dom_testing_library/inventoryController.test.js",
    "content": "const { addItem, data } = require(\"./inventoryController\");\n\ndescribe(\"addItem\", () => {\n  test(\"adding new items to the inventory\", () => {\n    data.inventory = {};\n    addItem(\"cheesecake\", 5);\n    expect(data.inventory.cheesecake).toBe(5);\n  });\n});\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/4_finding_with_dom_testing_library/main.js",
    "content": "const { addItem, data } = require(\"./inventoryController\");\nconst { updateItemList } = require(\"./domController\");\n\naddItem(\"cheesecake\", 3);\naddItem(\"apple pie\", 8);\naddItem(\"carrot cake\", 7);\n\nupdateItemList(data.inventory);\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/4_finding_with_dom_testing_library/package.json",
    "content": "{\n  \"name\": \"4_finding_with_dom_testing_library\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"start\": \"http-server ./\",\n    \"test\": \"jest\",\n    \"build\": \"browserify main.js -o bundle.js\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"@testing-library/dom\": \"^7.2.2\",\n    \"browserify\": \"^16.5.1\",\n    \"http-server\": \"^0.12.1\",\n    \"jest\": \"^26.6.0\"\n  }\n}\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/5_writing_better_dom_assertions/domController.js",
    "content": "const updateItemList = inventory => {\n  const inventoryList = window.document.getElementById(\"item-list\");\n\n  // Clears the list\n  inventoryList.innerHTML = \"\";\n\n  Object.entries(inventory).forEach(([itemName, quantity]) => {\n    const listItem = window.document.createElement(\"li\");\n    listItem.innerHTML = `${itemName} - Quantity: ${quantity}`;\n\n    if (quantity < 5) {\n      listItem.className = \"almost-soldout\";\n    }\n\n    inventoryList.appendChild(listItem);\n  });\n\n  const inventoryContents = JSON.stringify(inventory);\n  const p = window.document.createElement(\"p\");\n  p.innerHTML = `The inventory has been updated - ${inventoryContents}`;\n\n  window.document.body.appendChild(p);\n};\n\nmodule.exports = { updateItemList };\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/5_writing_better_dom_assertions/domController.test.js",
    "content": "const fs = require(\"fs\");\nconst initialHtml = fs.readFileSync(\"./index.html\");\nconst { getByText, screen } = require(\"@testing-library/dom\");\n\nconst { updateItemList } = require(\"./domController\");\n\nbeforeEach(() => {\n  document.body.innerHTML = initialHtml;\n});\n\ndescribe(\"updateItemList\", () => {\n  test(\"updates the DOM with the inventory items\", () => {\n    const inventory = {\n      cheesecake: 5,\n      \"apple pie\": 2,\n      \"carrot cake\": 6\n    };\n    updateItemList(inventory);\n\n    const itemList = document.getElementById(\"item-list\");\n    expect(itemList.childNodes).toHaveLength(3);\n\n    expect(getByText(itemList, \"cheesecake - Quantity: 5\")).toBeInTheDocument();\n    expect(getByText(itemList, \"apple pie - Quantity: 2\")).toBeInTheDocument();\n    expect(\n      getByText(itemList, \"carrot cake - Quantity: 6\")\n    ).toBeInTheDocument();\n  });\n\n  test(\"highlighting in red elements whose quantity is below five\", () => {\n    const inventory = { cheesecake: 5, \"apple pie\": 2, \"carrot cake\": 6 };\n    updateItemList(inventory);\n\n    expect(screen.getByText(\"apple pie - Quantity: 2\")).toHaveStyle({\n      color: \"red\"\n    });\n  });\n\n  test(\"adding a paragraph indicating what was the update\", () => {\n    const inventory = { cheesecake: 5, \"apple pie\": 2 };\n    updateItemList(inventory);\n\n    expect(\n      screen.getByText(\n        `The inventory has been updated - ${JSON.stringify(inventory)}`\n      )\n    ).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/5_writing_better_dom_assertions/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Inventory Manager</title>\n    <style>\n      .almost-soldout {\n        color: red;\n      }\n    </style>\n  </head>\n  <body>\n    <h1 data-testid=\"page-header\">Inventory Contents</h1>\n    <ul id=\"item-list\"></ul>\n    <script src=\"bundle.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/5_writing_better_dom_assertions/inventoryController.js",
    "content": "const data = { inventory: {} };\n\nconst addItem = (itemName, quantity) => {\n  const currentQuantity = data.inventory[itemName] || 0;\n  data.inventory[itemName] = currentQuantity + quantity;\n};\n\nmodule.exports = { data, addItem };\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/5_writing_better_dom_assertions/inventoryController.test.js",
    "content": "const { addItem, data } = require(\"./inventoryController\");\n\ndescribe(\"addItem\", () => {\n  test(\"adding new items to the inventory\", () => {\n    data.inventory = {};\n    addItem(\"cheesecake\", 5);\n    expect(data.inventory.cheesecake).toBe(5);\n  });\n});\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/5_writing_better_dom_assertions/jest.config.js",
    "content": "module.exports = {\n  setupFilesAfterEnv: [\"./setupJestDom.js\"]\n};\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/5_writing_better_dom_assertions/main.js",
    "content": "const { addItem, data } = require(\"./inventoryController\");\nconst { updateItemList } = require(\"./domController\");\n\naddItem(\"cheesecake\", 3);\naddItem(\"apple pie\", 8);\naddItem(\"carrot cake\", 7);\n\nupdateItemList(data.inventory);\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/5_writing_better_dom_assertions/package.json",
    "content": "{\n  \"name\": \"5_writing_better_dom_assertions\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"start\": \"http-server ./\",\n    \"test\": \"jest\",\n    \"build\": \"browserify main.js -o bundle.js\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"@testing-library/dom\": \"^7.2.2\",\n    \"@testing-library/jest-dom\": \"^5.5.0\",\n    \"browserify\": \"^16.5.1\",\n    \"http-server\": \"^0.12.1\",\n    \"jest\": \"^24.9.0\"\n  }\n}\n"
  },
  {
    "path": "chapter6/2_asserting_on_the_dom/5_writing_better_dom_assertions/setupJestDom.js",
    "content": "const jestDom = require(\"@testing-library/jest-dom\");\n\nexpect.extend(jestDom);\n"
  },
  {
    "path": "chapter6/3_handling_events/1_handling_raw_events/domController.js",
    "content": "const { addItem, data } = require(\"./inventoryController\");\n\nconst updateItemList = inventory => {\n  const inventoryList = window.document.getElementById(\"item-list\");\n\n  // Clears the list\n  inventoryList.innerHTML = \"\";\n\n  Object.entries(inventory).forEach(([itemName, quantity]) => {\n    const listItem = window.document.createElement(\"li\");\n    listItem.innerHTML = `${itemName} - Quantity: ${quantity}`;\n\n    if (quantity < 5) {\n      listItem.className = \"almost-soldout\";\n    }\n\n    inventoryList.appendChild(listItem);\n  });\n\n  const inventoryContents = JSON.stringify(inventory);\n  const p = window.document.createElement(\"p\");\n  p.innerHTML = `The inventory has been updated - ${inventoryContents}`;\n\n  window.document.body.appendChild(p);\n};\n\nconst handleAddItem = event => {\n  // Prevent the page from reloading as it would by default\n  event.preventDefault();\n\n  const { name, quantity } = event.target.elements;\n  addItem(name.value, parseInt(quantity.value, 10));\n\n  updateItemList(data.inventory);\n};\n\nconst validItems = [\"cheesecake\", \"apple pie\", \"carrot cake\"];\nconst handleItemName = event => {\n  const itemName = event.target.value;\n\n  const errorMsg = window.document.getElementById(\"error-msg\");\n\n  if (itemName === \"\") {\n    errorMsg.innerHTML = \"\";\n  } else if (!validItems.includes(itemName)) {\n    errorMsg.innerHTML = `${itemName} is not a valid item.`;\n  } else {\n    errorMsg.innerHTML = `${itemName} is valid!`;\n  }\n};\n\nmodule.exports = { updateItemList, handleAddItem, handleItemName };\n"
  },
  {
    "path": "chapter6/3_handling_events/1_handling_raw_events/domController.test.js",
    "content": "const fs = require(\"fs\");\nconst initialHtml = fs.readFileSync(\"./index.html\");\nconst { getByText, screen } = require(\"@testing-library/dom\");\n\nconst {\n  updateItemList,\n  handleAddItem,\n  handleItemName\n} = require(\"./domController\");\n\nbeforeEach(() => {\n  document.body.innerHTML = initialHtml;\n});\n\ndescribe(\"updateItemList\", () => {\n  test(\"updates the DOM with the inventory items\", () => {\n    const inventory = {\n      cheesecake: 5,\n      \"apple pie\": 2,\n      \"carrot cake\": 6\n    };\n    updateItemList(inventory);\n\n    const itemList = document.getElementById(\"item-list\");\n    expect(itemList.childNodes).toHaveLength(3);\n\n    expect(getByText(itemList, \"cheesecake - Quantity: 5\")).toBeInTheDocument();\n    expect(getByText(itemList, \"apple pie - Quantity: 2\")).toBeInTheDocument();\n    expect(\n      getByText(itemList, \"carrot cake - Quantity: 6\")\n    ).toBeInTheDocument();\n  });\n\n  test(\"highlighting in red elements whose quantity is below five\", () => {\n    const inventory = { cheesecake: 5, \"apple pie\": 2, \"carrot cake\": 6 };\n    updateItemList(inventory);\n\n    expect(screen.getByText(\"apple pie - Quantity: 2\")).toHaveStyle({\n      color: \"red\"\n    });\n  });\n\n  test(\"adding a paragraph indicating what was the update\", () => {\n    const inventory = { cheesecake: 5, \"apple pie\": 2 };\n    updateItemList(inventory);\n\n    expect(\n      screen.getByText(\n        `The inventory has been updated - ${JSON.stringify(inventory)}`\n      )\n    ).toBeTruthy();\n  });\n});\n\ndescribe(\"handleAddItem\", () => {\n  test(\"adding items to the page\", () => {\n    const event = {\n      preventDefault: jest.fn(),\n      target: {\n        elements: {\n          name: { value: \"cheesecake\" },\n          quantity: { value: \"6\" }\n        }\n      }\n    };\n\n    handleAddItem(event);\n\n    // Checking if the form's default reload is prevent\n    expect(event.preventDefault.mock.calls).toHaveLength(1);\n\n    const itemList = document.getElementById(\"item-list\");\n    expect(getByText(itemList, \"cheesecake - Quantity: 6\")).toBeInTheDocument();\n  });\n});\n\ndescribe(\"handleItemName\", () => {\n  test(\"entering valid item names\", () => {\n    const event = {\n      preventDefault: jest.fn(),\n      target: { value: \"cheesecake\" }\n    };\n\n    handleItemName(event);\n\n    expect(screen.getByText(\"cheesecake is valid!\")).toBeInTheDocument();\n  });\n\n  test(\"entering invalid item names\", () => {\n    const event = {\n      preventDefault: jest.fn(),\n      target: { value: \"book\" }\n    };\n\n    handleItemName(event);\n\n    expect(screen.getByText(\"book is not a valid item.\")).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "chapter6/3_handling_events/1_handling_raw_events/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Inventory Manager</title>\n    <style>\n      .almost-soldout {\n        color: red;\n      }\n    </style>\n  </head>\n  <body>\n    <h1 data-testid=\"page-header\">Inventory Contents</h1>\n    <ul id=\"item-list\"></ul>\n    <p id=\"error-msg\"></p>\n    <form id=\"add-item-form\">\n      <input type=\"text\" name=\"name\" placeholder=\"Item name\" />\n      <input type=\"number\" name=\"quantity\" placeholder=\"Quantity\" />\n      <button type=\"submit\">Add to inventory</button>\n    </form>\n    <script src=\"bundle.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter6/3_handling_events/1_handling_raw_events/inventoryController.js",
    "content": "const data = { inventory: {} };\n\nconst addItem = (itemName, quantity) => {\n  const currentQuantity = data.inventory[itemName] || 0;\n  data.inventory[itemName] = currentQuantity + quantity;\n  return data.inventory;\n};\n\nmodule.exports = { data, addItem };\n"
  },
  {
    "path": "chapter6/3_handling_events/1_handling_raw_events/inventoryController.test.js",
    "content": "const { addItem, data } = require(\"./inventoryController\");\n\ndescribe(\"addItem\", () => {\n  test(\"adding new items to the inventory\", () => {\n    data.inventory = {};\n    addItem(\"cheesecake\", 5);\n    expect(data.inventory.cheesecake).toBe(5);\n  });\n});\n"
  },
  {
    "path": "chapter6/3_handling_events/1_handling_raw_events/jest.config.js",
    "content": "module.exports = {\n  setupFilesAfterEnv: [\"./setupJestDom.js\"]\n};\n"
  },
  {
    "path": "chapter6/3_handling_events/1_handling_raw_events/main.js",
    "content": "const { handleAddItem, handleItemName } = require(\"./domController\");\n\nconst form = document.getElementById(\"add-item-form\");\nform.addEventListener(\"submit\", handleAddItem);\n\nconst itemInput = document.querySelector(`input[name=\"name\"]`);\nitemInput.addEventListener(\"input\", handleItemName);\n"
  },
  {
    "path": "chapter6/3_handling_events/1_handling_raw_events/main.test.js",
    "content": "const fs = require(\"fs\");\nconst initialHtml = fs.readFileSync(\"./index.html\");\nconst { screen, getByText } = require(\"@testing-library/dom\");\n\nbeforeEach(() => {\n  document.body.innerHTML = initialHtml;\n\n  // You must execute main.js again so that it can attach the\n  // event listener to the form every time the body changes.\n  // Here you must use `jest.resetModules` because otherwise\n  // Jest will have cached `main.js` and it will _not_ run again.\n  jest.resetModules();\n  require(\"./main\");\n});\n\ntest(\"adding items through the form\", () => {\n  screen.getByPlaceholderText(\"Item name\").value = \"cheesecake\";\n  screen.getByPlaceholderText(\"Quantity\").value = \"6\";\n\n  const event = new Event(\"submit\");\n  const form = document.getElementById(\"add-item-form\");\n  form.dispatchEvent(event);\n\n  const itemList = document.getElementById(\"item-list\");\n  expect(getByText(itemList, \"cheesecake - Quantity: 6\")).toBeInTheDocument();\n});\n\ndescribe(\"item name validation\", () => {\n  test(\"entering valid item names \", () => {\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n    itemField.value = \"cheesecake\";\n    const inputEvent = new Event(\"input\");\n\n    itemField.dispatchEvent(inputEvent);\n\n    expect(screen.getByText(\"cheesecake is valid!\")).toBeInTheDocument();\n  });\n\n  test(\"entering invalid item names \", () => {\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n    itemField.value = \"book\";\n    const inputEvent = new Event(\"input\");\n\n    itemField.dispatchEvent(inputEvent);\n\n    expect(screen.getByText(\"book is not a valid item.\")).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "chapter6/3_handling_events/1_handling_raw_events/package.json",
    "content": "{\n  \"name\": \"1_handling_raw_events\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"start\": \"http-server ./\",\n    \"test\": \"jest\",\n    \"build\": \"browserify main.js -o bundle.js\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"@testing-library/dom\": \"^7.2.2\",\n    \"@testing-library/jest-dom\": \"^5.5.0\",\n    \"browserify\": \"^16.5.1\",\n    \"http-server\": \"^0.12.1\",\n    \"jest\": \"^24.9.0\"\n  }\n}\n"
  },
  {
    "path": "chapter6/3_handling_events/1_handling_raw_events/setupJestDom.js",
    "content": "const jestDom = require(\"@testing-library/jest-dom\");\n\nexpect.extend(jestDom);\n"
  },
  {
    "path": "chapter6/3_handling_events/2_bubbling_up_events/domController.js",
    "content": "const { addItem, data } = require(\"./inventoryController\");\n\nconst updateItemList = inventory => {\n  const inventoryList = window.document.getElementById(\"item-list\");\n\n  // Clears the list\n  inventoryList.innerHTML = \"\";\n\n  Object.entries(inventory).forEach(([itemName, quantity]) => {\n    const listItem = window.document.createElement(\"li\");\n    listItem.innerHTML = `${itemName} - Quantity: ${quantity}`;\n\n    if (quantity < 5) {\n      listItem.className = \"almost-soldout\";\n    }\n\n    inventoryList.appendChild(listItem);\n  });\n\n  const inventoryContents = JSON.stringify(inventory);\n  const p = window.document.createElement(\"p\");\n  p.innerHTML = `The inventory has been updated - ${inventoryContents}`;\n\n  window.document.body.appendChild(p);\n};\n\nconst handleAddItem = event => {\n  // Prevent the page from reloading as it would by default\n  event.preventDefault();\n\n  const { name, quantity } = event.target.elements;\n  addItem(name.value, parseInt(quantity.value, 10));\n\n  updateItemList(data.inventory);\n};\n\nconst validItems = [\"cheesecake\", \"apple pie\", \"carrot cake\"];\nconst checkFormValues = () => {\n  const itemName = document.querySelector(`input[name=\"name\"]`).value;\n  const quantity = document.querySelector(`input[name=\"quantity\"]`).value;\n\n  const itemNameIsEmpty = itemName === \"\";\n  const itemNameIsInvalid = !validItems.includes(itemName);\n  const quantityIsEmpty = quantity === \"\";\n\n  const errorMsg = window.document.getElementById(\"error-msg\");\n  if (itemNameIsEmpty) {\n    errorMsg.innerHTML = \"\";\n  } else if (itemNameIsInvalid) {\n    errorMsg.innerHTML = `${itemName} is not a valid item.`;\n  } else {\n    errorMsg.innerHTML = `${itemName} is valid!`;\n  }\n\n  const submitButton = document.querySelector(`button[type=\"submit\"]`);\n  if (itemNameIsEmpty || itemNameIsInvalid || quantityIsEmpty) {\n    submitButton.disabled = true;\n  } else {\n    submitButton.disabled = false;\n  }\n};\n\nmodule.exports = { updateItemList, handleAddItem, checkFormValues };\n"
  },
  {
    "path": "chapter6/3_handling_events/2_bubbling_up_events/domController.test.js",
    "content": "const fs = require(\"fs\");\nconst initialHtml = fs.readFileSync(\"./index.html\");\nconst { getByText, screen } = require(\"@testing-library/dom\");\n\nconst {\n  updateItemList,\n  handleAddItem,\n  checkFormValues\n} = require(\"./domController\");\n\nbeforeEach(() => {\n  document.body.innerHTML = initialHtml;\n});\n\ndescribe(\"updateItemList\", () => {\n  test(\"updates the DOM with the inventory items\", () => {\n    const inventory = {\n      cheesecake: 5,\n      \"apple pie\": 2,\n      \"carrot cake\": 6\n    };\n    updateItemList(inventory);\n\n    const itemList = document.getElementById(\"item-list\");\n    expect(itemList.childNodes).toHaveLength(3);\n\n    expect(getByText(itemList, \"cheesecake - Quantity: 5\")).toBeInTheDocument();\n    expect(getByText(itemList, \"apple pie - Quantity: 2\")).toBeInTheDocument();\n    expect(\n      getByText(itemList, \"carrot cake - Quantity: 6\")\n    ).toBeInTheDocument();\n  });\n\n  test(\"highlighting in red elements whose quantity is below five\", () => {\n    const inventory = { cheesecake: 5, \"apple pie\": 2, \"carrot cake\": 6 };\n    updateItemList(inventory);\n\n    expect(screen.getByText(\"apple pie - Quantity: 2\")).toHaveStyle({\n      color: \"red\"\n    });\n  });\n\n  test(\"adding a paragraph indicating what was the update\", () => {\n    const inventory = { cheesecake: 5, \"apple pie\": 2 };\n    updateItemList(inventory);\n\n    expect(\n      screen.getByText(\n        `The inventory has been updated - ${JSON.stringify(inventory)}`\n      )\n    ).toBeTruthy();\n  });\n});\n\ndescribe(\"handleAddItem\", () => {\n  test(\"adding items to the page\", () => {\n    const event = {\n      preventDefault: jest.fn(),\n      target: {\n        elements: {\n          name: { value: \"cheesecake\" },\n          quantity: { value: \"6\" }\n        }\n      }\n    };\n\n    handleAddItem(event);\n\n    // Checking if the form's default reload is prevent\n    expect(event.preventDefault.mock.calls).toHaveLength(1);\n\n    const itemList = document.getElementById(\"item-list\");\n    expect(getByText(itemList, \"cheesecake - Quantity: 6\")).toBeInTheDocument();\n  });\n});\n\ndescribe(\"checkFormValues\", () => {\n  test(\"entering valid item values\", () => {\n    document.querySelector(`input[name=\"name\"]`).value = \"cheesecake\";\n    document.querySelector(`input[name=\"quantity\"]`).value = \"1\";\n    checkFormValues();\n    expect(screen.getByText(\"Add to inventory\")).toBeEnabled();\n  });\n\n  test(\"entering invalid item names\", () => {\n    document.querySelector(`input[name=\"name\"]`).value = \"invalid\";\n    document.querySelector(`input[name=\"quantity\"]`).value = \"1\";\n    checkFormValues();\n    expect(screen.getByText(\"Add to inventory\")).toBeDisabled();\n\n    document.querySelector(`input[name=\"name\"]`).value = \"cheesecake\";\n    document.querySelector(`input[name=\"quantity\"]`).value = \"\";\n    checkFormValues();\n    expect(screen.getByText(\"Add to inventory\")).toBeDisabled();\n  });\n});\n"
  },
  {
    "path": "chapter6/3_handling_events/2_bubbling_up_events/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Inventory Manager</title>\n    <style>\n      .almost-soldout {\n        color: red;\n      }\n    </style>\n  </head>\n  <body>\n    <h1 data-testid=\"page-header\">Inventory Contents</h1>\n    <ul id=\"item-list\"></ul>\n    <p id=\"error-msg\"></p>\n    <form id=\"add-item-form\">\n      <input type=\"text\" name=\"name\" placeholder=\"Item name\" />\n      <input type=\"number\" name=\"quantity\" placeholder=\"Quantity\" />\n      <button type=\"submit\">Add to inventory</button>\n    </form>\n    <script src=\"bundle.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter6/3_handling_events/2_bubbling_up_events/inventoryController.js",
    "content": "const data = { inventory: {} };\n\nconst addItem = (itemName, quantity) => {\n  const currentQuantity = data.inventory[itemName] || 0;\n  data.inventory[itemName] = currentQuantity + quantity;\n  return data.inventory;\n};\n\nmodule.exports = { data, addItem };\n"
  },
  {
    "path": "chapter6/3_handling_events/2_bubbling_up_events/inventoryController.test.js",
    "content": "const { addItem, data } = require(\"./inventoryController\");\n\ndescribe(\"addItem\", () => {\n  test(\"adding new items to the inventory\", () => {\n    data.inventory = {};\n    addItem(\"cheesecake\", 5);\n    expect(data.inventory.cheesecake).toBe(5);\n  });\n});\n"
  },
  {
    "path": "chapter6/3_handling_events/2_bubbling_up_events/jest.config.js",
    "content": "module.exports = {\n  setupFilesAfterEnv: [\"./setupJestDom.js\"]\n};\n"
  },
  {
    "path": "chapter6/3_handling_events/2_bubbling_up_events/main.js",
    "content": "const { handleAddItem, checkFormValues } = require(\"./domController\");\n\nconst form = document.getElementById(\"add-item-form\");\nform.addEventListener(\"submit\", handleAddItem);\nform.addEventListener(\"input\", checkFormValues);\n\n// Run `checkFormValues` once to see if the initial state is valid\ncheckFormValues();\n"
  },
  {
    "path": "chapter6/3_handling_events/2_bubbling_up_events/main.test.js",
    "content": "const fs = require(\"fs\");\nconst initialHtml = fs.readFileSync(\"./index.html\");\nconst { screen, getByText } = require(\"@testing-library/dom\");\n\nbeforeEach(() => {\n  document.body.innerHTML = initialHtml;\n\n  // You must execute main.js again so that it can attach the\n  // event listener to the form every time the body changes.\n  // Here you must use `jest.resetModules` because otherwise\n  // Jest will have cached `main.js` and it will _not_ run again.\n  jest.resetModules();\n  require(\"./main\");\n});\n\ntest(\"adding items through the form\", () => {\n  screen.getByPlaceholderText(\"Item name\").value = \"cheesecake\";\n  screen.getByPlaceholderText(\"Quantity\").value = \"6\";\n\n  const event = new Event(\"submit\");\n  const form = document.getElementById(\"add-item-form\");\n  form.dispatchEvent(event);\n\n  const itemList = document.getElementById(\"item-list\");\n  expect(getByText(itemList, \"cheesecake - Quantity: 6\")).toBeInTheDocument();\n});\n\ndescribe(\"item name validation\", () => {\n  test(\"entering valid item names \", () => {\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n    itemField.value = \"cheesecake\";\n    const inputEvent = new Event(\"input\", { bubbles: true });\n\n    itemField.dispatchEvent(inputEvent);\n\n    expect(screen.getByText(\"cheesecake is valid!\")).toBeInTheDocument();\n  });\n\n  test(\"entering invalid item names \", () => {\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n    itemField.value = \"book\";\n    const inputEvent = new Event(\"input\", { bubbles: true });\n\n    itemField.dispatchEvent(inputEvent);\n\n    expect(screen.getByText(\"book is not a valid item.\")).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "chapter6/3_handling_events/2_bubbling_up_events/package.json",
    "content": "{\n  \"name\": \"1_handling_raw_events\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"start\": \"http-server ./\",\n    \"test\": \"jest\",\n    \"build\": \"browserify main.js -o bundle.js\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"@testing-library/dom\": \"^7.2.2\",\n    \"@testing-library/jest-dom\": \"^5.5.0\",\n    \"browserify\": \"^16.5.1\",\n    \"http-server\": \"^0.12.1\",\n    \"jest\": \"^24.9.0\"\n  }\n}\n"
  },
  {
    "path": "chapter6/3_handling_events/2_bubbling_up_events/setupJestDom.js",
    "content": "const jestDom = require(\"@testing-library/jest-dom\");\n\nexpect.extend(jestDom);\n"
  },
  {
    "path": "chapter6/3_handling_events/3_dom_testing_library_events/domController.js",
    "content": "const { addItem, data } = require(\"./inventoryController\");\n\nconst updateItemList = inventory => {\n  const inventoryList = window.document.getElementById(\"item-list\");\n\n  // Clears the list\n  inventoryList.innerHTML = \"\";\n\n  Object.entries(inventory).forEach(([itemName, quantity]) => {\n    const listItem = window.document.createElement(\"li\");\n    listItem.innerHTML = `${itemName} - Quantity: ${quantity}`;\n\n    if (quantity < 5) {\n      listItem.className = \"almost-soldout\";\n    }\n\n    inventoryList.appendChild(listItem);\n  });\n\n  const inventoryContents = JSON.stringify(inventory);\n  const p = window.document.createElement(\"p\");\n  p.innerHTML = `The inventory has been updated - ${inventoryContents}`;\n\n  window.document.body.appendChild(p);\n};\n\nconst handleAddItem = event => {\n  // Prevent the page from reloading as it would by default\n  event.preventDefault();\n\n  const { name, quantity } = event.target.elements;\n  addItem(name.value, parseInt(quantity.value, 10));\n\n  updateItemList(data.inventory);\n};\n\nconst validItems = [\"cheesecake\", \"apple pie\", \"carrot cake\"];\nconst checkFormValues = () => {\n  const itemName = document.querySelector(`input[name=\"name\"]`).value;\n  const quantity = document.querySelector(`input[name=\"quantity\"]`).value;\n\n  const itemNameIsEmpty = itemName === \"\";\n  const itemNameIsInvalid = !validItems.includes(itemName);\n  const quantityIsEmpty = quantity === \"\";\n\n  const errorMsg = window.document.getElementById(\"error-msg\");\n  if (itemNameIsEmpty) {\n    errorMsg.innerHTML = \"\";\n  } else if (itemNameIsInvalid) {\n    errorMsg.innerHTML = `${itemName} is not a valid item.`;\n  } else {\n    errorMsg.innerHTML = `${itemName} is valid!`;\n  }\n\n  const submitButton = document.querySelector(`button[type=\"submit\"]`);\n  if (itemNameIsEmpty || itemNameIsInvalid || quantityIsEmpty) {\n    submitButton.disabled = true;\n  } else {\n    submitButton.disabled = false;\n  }\n};\n\nmodule.exports = { updateItemList, handleAddItem, checkFormValues };\n"
  },
  {
    "path": "chapter6/3_handling_events/3_dom_testing_library_events/domController.test.js",
    "content": "const fs = require(\"fs\");\nconst initialHtml = fs.readFileSync(\"./index.html\");\nconst { getByText, screen } = require(\"@testing-library/dom\");\n\nconst {\n  updateItemList,\n  handleAddItem,\n  checkFormValues\n} = require(\"./domController\");\n\nbeforeEach(() => {\n  document.body.innerHTML = initialHtml;\n});\n\ndescribe(\"updateItemList\", () => {\n  test(\"updates the DOM with the inventory items\", () => {\n    const inventory = {\n      cheesecake: 5,\n      \"apple pie\": 2,\n      \"carrot cake\": 6\n    };\n    updateItemList(inventory);\n\n    const itemList = document.getElementById(\"item-list\");\n    expect(itemList.childNodes).toHaveLength(3);\n\n    expect(getByText(itemList, \"cheesecake - Quantity: 5\")).toBeInTheDocument();\n    expect(getByText(itemList, \"apple pie - Quantity: 2\")).toBeInTheDocument();\n    expect(\n      getByText(itemList, \"carrot cake - Quantity: 6\")\n    ).toBeInTheDocument();\n  });\n\n  test(\"highlighting in red elements whose quantity is below five\", () => {\n    const inventory = { cheesecake: 5, \"apple pie\": 2, \"carrot cake\": 6 };\n    updateItemList(inventory);\n\n    expect(screen.getByText(\"apple pie - Quantity: 2\")).toHaveStyle({\n      color: \"red\"\n    });\n  });\n\n  test(\"adding a paragraph indicating what was the update\", () => {\n    const inventory = { cheesecake: 5, \"apple pie\": 2 };\n    updateItemList(inventory);\n\n    expect(\n      screen.getByText(\n        `The inventory has been updated - ${JSON.stringify(inventory)}`\n      )\n    ).toBeTruthy();\n  });\n});\n\ndescribe(\"handleAddItem\", () => {\n  test(\"adding items to the page\", () => {\n    const event = {\n      preventDefault: jest.fn(),\n      target: {\n        elements: {\n          name: { value: \"cheesecake\" },\n          quantity: { value: \"6\" }\n        }\n      }\n    };\n\n    handleAddItem(event);\n\n    // Checking if the form's default reload is prevent\n    expect(event.preventDefault.mock.calls).toHaveLength(1);\n\n    const itemList = document.getElementById(\"item-list\");\n    expect(getByText(itemList, \"cheesecake - Quantity: 6\")).toBeInTheDocument();\n  });\n});\n\ndescribe(\"checkFormValues\", () => {\n  test(\"entering valid item values\", () => {\n    document.querySelector(`input[name=\"name\"]`).value = \"cheesecake\";\n    document.querySelector(`input[name=\"quantity\"]`).value = \"1\";\n    checkFormValues();\n    expect(screen.getByText(\"Add to inventory\")).toBeEnabled();\n  });\n\n  test(\"entering invalid item names\", () => {\n    document.querySelector(`input[name=\"name\"]`).value = \"invalid\";\n    document.querySelector(`input[name=\"quantity\"]`).value = \"1\";\n    checkFormValues();\n    expect(screen.getByText(\"Add to inventory\")).toBeDisabled();\n\n    document.querySelector(`input[name=\"name\"]`).value = \"cheesecake\";\n    document.querySelector(`input[name=\"quantity\"]`).value = \"\";\n    checkFormValues();\n    expect(screen.getByText(\"Add to inventory\")).toBeDisabled();\n  });\n});\n"
  },
  {
    "path": "chapter6/3_handling_events/3_dom_testing_library_events/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Inventory Manager</title>\n    <style>\n      .almost-soldout {\n        color: red;\n      }\n    </style>\n  </head>\n  <body>\n    <h1 data-testid=\"page-header\">Inventory Contents</h1>\n    <ul id=\"item-list\"></ul>\n    <p id=\"error-msg\"></p>\n    <form id=\"add-item-form\">\n      <input type=\"text\" name=\"name\" placeholder=\"Item name\" />\n      <input type=\"number\" name=\"quantity\" placeholder=\"Quantity\" />\n      <button type=\"submit\">Add to inventory</button>\n    </form>\n    <script src=\"bundle.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter6/3_handling_events/3_dom_testing_library_events/inventoryController.js",
    "content": "const data = { inventory: {} };\n\nconst addItem = (itemName, quantity) => {\n  const currentQuantity = data.inventory[itemName] || 0;\n  data.inventory[itemName] = currentQuantity + quantity;\n  return data.inventory;\n};\n\nmodule.exports = { data, addItem };\n"
  },
  {
    "path": "chapter6/3_handling_events/3_dom_testing_library_events/inventoryController.test.js",
    "content": "const { addItem, data } = require(\"./inventoryController\");\n\ndescribe(\"addItem\", () => {\n  test(\"adding new items to the inventory\", () => {\n    data.inventory = {};\n    addItem(\"cheesecake\", 5);\n    expect(data.inventory.cheesecake).toBe(5);\n  });\n});\n"
  },
  {
    "path": "chapter6/3_handling_events/3_dom_testing_library_events/jest.config.js",
    "content": "module.exports = {\n  setupFilesAfterEnv: [\"./setupJestDom.js\"]\n};\n"
  },
  {
    "path": "chapter6/3_handling_events/3_dom_testing_library_events/main.js",
    "content": "const { handleAddItem, checkFormValues } = require(\"./domController\");\n\nconst form = document.getElementById(\"add-item-form\");\nform.addEventListener(\"submit\", handleAddItem);\nform.addEventListener(\"input\", checkFormValues);\n\n// Run `checkFormValues` once to see if the initial state is valid\ncheckFormValues();\n"
  },
  {
    "path": "chapter6/3_handling_events/3_dom_testing_library_events/main.test.js",
    "content": "const fs = require(\"fs\");\nconst initialHtml = fs.readFileSync(\"./index.html\");\nconst { screen, getByText, fireEvent } = require(\"@testing-library/dom\");\n\nbeforeEach(() => {\n  document.body.innerHTML = initialHtml;\n\n  // You must execute main.js again so that it can attach the\n  // event listener to the form every time the body changes.\n  // Here you must use `jest.resetModules` because otherwise\n  // Jest will have cached `main.js` and it will _not_ run again.\n  jest.resetModules();\n  require(\"./main\");\n});\n\ntest(\"adding items through the form\", () => {\n  const itemField = screen.getByPlaceholderText(\"Item name\");\n  fireEvent.input(itemField, {\n    target: { value: \"cheesecake\" },\n    bubbles: true\n  });\n\n  const quantityField = screen.getByPlaceholderText(\"Quantity\");\n  fireEvent.input(quantityField, { target: { value: \"6\" }, bubbles: true });\n\n  const submitBtn = screen.getByText(\"Add to inventory\");\n  fireEvent.click(submitBtn);\n\n  const itemList = document.getElementById(\"item-list\");\n  expect(getByText(itemList, \"cheesecake - Quantity: 6\")).toBeInTheDocument();\n});\n\ndescribe(\"item name validation\", () => {\n  test(\"entering valid item names \", () => {\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n\n    fireEvent.input(itemField, {\n      target: { value: \"cheesecake\" },\n      bubbles: true\n    });\n\n    expect(screen.getByText(\"cheesecake is valid!\")).toBeInTheDocument();\n  });\n\n  test(\"entering invalid item names \", () => {\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n\n    fireEvent.input(itemField, { target: { value: \"book\" }, bubbles: true });\n\n    expect(screen.getByText(\"book is not a valid item.\")).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "chapter6/3_handling_events/3_dom_testing_library_events/package.json",
    "content": "{\n  \"name\": \"1_handling_raw_events\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"start\": \"http-server ./\",\n    \"test\": \"jest\",\n    \"build\": \"browserify main.js -o bundle.js\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"@testing-library/dom\": \"^7.2.2\",\n    \"@testing-library/jest-dom\": \"^5.5.0\",\n    \"browserify\": \"^16.5.1\",\n    \"http-server\": \"^0.12.1\",\n    \"jest\": \"^24.9.0\"\n  }\n}\n"
  },
  {
    "path": "chapter6/3_handling_events/3_dom_testing_library_events/setupJestDom.js",
    "content": "const jestDom = require(\"@testing-library/jest-dom\");\n\nexpect.extend(jestDom);\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/1_localstorage/domController.js",
    "content": "const { addItem, data } = require(\"./inventoryController\");\n\nconst updateItemList = inventory => {\n  if (inventory === null) return;\n\n  localStorage.setItem(\"inventory\", JSON.stringify(inventory));\n\n  const inventoryList = window.document.getElementById(\"item-list\");\n\n  // Clears the list\n  inventoryList.innerHTML = \"\";\n\n  Object.entries(inventory).forEach(([itemName, quantity]) => {\n    const listItem = window.document.createElement(\"li\");\n    listItem.innerHTML = `${itemName} - Quantity: ${quantity}`;\n\n    if (quantity < 5) {\n      listItem.className = \"almost-soldout\";\n    }\n\n    inventoryList.appendChild(listItem);\n  });\n\n  const inventoryContents = JSON.stringify(inventory);\n  const p = window.document.createElement(\"p\");\n  p.innerHTML = `The inventory has been updated - ${inventoryContents}`;\n\n  window.document.body.appendChild(p);\n};\n\nconst handleAddItem = event => {\n  // Prevent the page from reloading as it would by default\n  event.preventDefault();\n\n  const { name, quantity } = event.target.elements;\n  addItem(name.value, parseInt(quantity.value, 10));\n\n  updateItemList(data.inventory);\n};\n\nconst validItems = [\"cheesecake\", \"apple pie\", \"carrot cake\"];\nconst checkFormValues = () => {\n  const itemName = document.querySelector(`input[name=\"name\"]`).value;\n  const quantity = document.querySelector(`input[name=\"quantity\"]`).value;\n\n  const itemNameIsEmpty = itemName === \"\";\n  const itemNameIsInvalid = !validItems.includes(itemName);\n  const quantityIsEmpty = quantity === \"\";\n\n  const errorMsg = window.document.getElementById(\"error-msg\");\n  if (itemNameIsEmpty) {\n    errorMsg.innerHTML = \"\";\n  } else if (itemNameIsInvalid) {\n    errorMsg.innerHTML = `${itemName} is not a valid item.`;\n  } else {\n    errorMsg.innerHTML = `${itemName} is valid!`;\n  }\n\n  const submitButton = document.querySelector(`button[type=\"submit\"]`);\n  if (itemNameIsEmpty || itemNameIsInvalid || quantityIsEmpty) {\n    submitButton.disabled = true;\n  } else {\n    submitButton.disabled = false;\n  }\n};\n\nmodule.exports = { updateItemList, handleAddItem, checkFormValues };\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/1_localstorage/domController.test.js",
    "content": "const fs = require(\"fs\");\nconst initialHtml = fs.readFileSync(\"./index.html\");\nconst { getByText, screen } = require(\"@testing-library/dom\");\n\nconst {\n  updateItemList,\n  handleAddItem,\n  checkFormValues\n} = require(\"./domController\");\n\nbeforeEach(() => {\n  document.body.innerHTML = initialHtml;\n});\n\ndescribe(\"updateItemList\", () => {\n  beforeEach(() => localStorage.clear());\n\n  test(\"updates the DOM with the inventory items\", () => {\n    const inventory = {\n      cheesecake: 5,\n      \"apple pie\": 2,\n      \"carrot cake\": 6\n    };\n    updateItemList(inventory);\n\n    const itemList = document.getElementById(\"item-list\");\n    expect(itemList.childNodes).toHaveLength(3);\n\n    expect(getByText(itemList, \"cheesecake - Quantity: 5\")).toBeInTheDocument();\n    expect(getByText(itemList, \"apple pie - Quantity: 2\")).toBeInTheDocument();\n    expect(\n      getByText(itemList, \"carrot cake - Quantity: 6\")\n    ).toBeInTheDocument();\n  });\n\n  test(\"highlighting in red elements whose quantity is below five\", () => {\n    const inventory = { cheesecake: 5, \"apple pie\": 2, \"carrot cake\": 6 };\n    updateItemList(inventory);\n\n    expect(screen.getByText(\"apple pie - Quantity: 2\")).toHaveStyle({\n      color: \"red\"\n    });\n  });\n\n  test(\"adding a paragraph indicating what was the update\", () => {\n    const inventory = { cheesecake: 5, \"apple pie\": 2 };\n    updateItemList(inventory);\n\n    expect(\n      screen.getByText(\n        `The inventory has been updated - ${JSON.stringify(inventory)}`\n      )\n    ).toBeTruthy();\n  });\n\n  test(\"updates the localStorage with the inventory\", () => {\n    const inventory = { cheesecake: 5, \"apple pie\": 2 };\n    updateItemList(inventory);\n\n    expect(localStorage.getItem(\"inventory\")).toEqual(\n      JSON.stringify(inventory)\n    );\n  });\n\n  test(\"does not update the inventory when passing null\", () => {\n    localStorage.setItem(\"inventory\", JSON.stringify({ cheesecake: 5 }));\n    updateItemList(null);\n\n    expect(localStorage.getItem(\"inventory\")).toEqual(\n      JSON.stringify({ cheesecake: 5 })\n    );\n  });\n});\n\ndescribe(\"handleAddItem\", () => {\n  test(\"adding items to the page\", () => {\n    const event = {\n      preventDefault: jest.fn(),\n      target: {\n        elements: {\n          name: { value: \"cheesecake\" },\n          quantity: { value: \"6\" }\n        }\n      }\n    };\n\n    handleAddItem(event);\n\n    // Checking if the form's default reload is prevent\n    expect(event.preventDefault.mock.calls).toHaveLength(1);\n\n    const itemList = document.getElementById(\"item-list\");\n    expect(getByText(itemList, \"cheesecake - Quantity: 6\")).toBeInTheDocument();\n  });\n});\n\ndescribe(\"checkFormValues\", () => {\n  test(\"entering valid item values\", () => {\n    document.querySelector(`input[name=\"name\"]`).value = \"cheesecake\";\n    document.querySelector(`input[name=\"quantity\"]`).value = \"1\";\n    checkFormValues();\n    expect(screen.getByText(\"Add to inventory\")).toBeEnabled();\n  });\n\n  test(\"entering invalid item names\", () => {\n    document.querySelector(`input[name=\"name\"]`).value = \"invalid\";\n    document.querySelector(`input[name=\"quantity\"]`).value = \"1\";\n    checkFormValues();\n    expect(screen.getByText(\"Add to inventory\")).toBeDisabled();\n\n    document.querySelector(`input[name=\"name\"]`).value = \"cheesecake\";\n    document.querySelector(`input[name=\"quantity\"]`).value = \"\";\n    checkFormValues();\n    expect(screen.getByText(\"Add to inventory\")).toBeDisabled();\n  });\n});\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/1_localstorage/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Inventory Manager</title>\n    <style>\n      .almost-soldout {\n        color: red;\n      }\n    </style>\n  </head>\n  <body>\n    <h1 data-testid=\"page-header\">Inventory Contents</h1>\n    <ul id=\"item-list\"></ul>\n    <p id=\"error-msg\"></p>\n    <form id=\"add-item-form\">\n      <input type=\"text\" name=\"name\" placeholder=\"Item name\" />\n      <input type=\"number\" name=\"quantity\" placeholder=\"Quantity\" />\n      <button type=\"submit\">Add to inventory</button>\n    </form>\n    <script src=\"bundle.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/1_localstorage/inventoryController.js",
    "content": "const data = { inventory: {} };\n\nconst addItem = (itemName, quantity) => {\n  const currentQuantity = data.inventory[itemName] || 0;\n  data.inventory[itemName] = currentQuantity + quantity;\n  return data.inventory;\n};\n\nmodule.exports = { data, addItem };\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/1_localstorage/inventoryController.test.js",
    "content": "const { addItem, data } = require(\"./inventoryController\");\n\ndescribe(\"addItem\", () => {\n  test(\"adding new items to the inventory\", () => {\n    data.inventory = {};\n    addItem(\"cheesecake\", 5);\n    expect(data.inventory.cheesecake).toBe(5);\n  });\n});\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/1_localstorage/jest.config.js",
    "content": "module.exports = {\n  setupFilesAfterEnv: [\"./setupJestDom.js\"]\n};\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/1_localstorage/main.js",
    "content": "const {\n  handleAddItem,\n  checkFormValues,\n  updateItemList\n} = require(\"./domController\");\n\nconst { data } = require(\"./inventoryController\");\n\nconst form = document.getElementById(\"add-item-form\");\nform.addEventListener(\"submit\", handleAddItem);\nform.addEventListener(\"input\", checkFormValues);\n\n// Run `checkFormValues` once to see if the initial state is valid\ncheckFormValues();\n\n// Restore the inventory when the page loads\nconst storedInventory = JSON.parse(localStorage.getItem(\"inventory\"));\n\nif (storedInventory) {\n  data.inventory = storedInventory;\n  updateItemList(data.inventory);\n}\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/1_localstorage/main.test.js",
    "content": "const fs = require(\"fs\");\nconst initialHtml = fs.readFileSync(\"./index.html\");\nconst { screen, getByText, fireEvent } = require(\"@testing-library/dom\");\n\nbeforeEach(() => localStorage.clear());\n\nbeforeEach(() => {\n  document.body.innerHTML = initialHtml;\n\n  // You must execute main.js again so that it can attach the\n  // event listener to the form every time the body changes.\n  // Here you must use `jest.resetModules` because otherwise\n  // Jest will have cached `main.js` and it will _not_ run again.\n  jest.resetModules();\n  require(\"./main\");\n});\n\ntest(\"persists items between sessions\", () => {\n  const itemField = screen.getByPlaceholderText(\"Item name\");\n  fireEvent.input(itemField, {\n    target: { value: \"cheesecake\" },\n    bubbles: true\n  });\n\n  const quantityField = screen.getByPlaceholderText(\"Quantity\");\n  fireEvent.input(quantityField, { target: { value: \"6\" }, bubbles: true });\n\n  const submitBtn = screen.getByText(\"Add to inventory\");\n  fireEvent.click(submitBtn);\n\n  const itemListBefore = document.getElementById(\"item-list\");\n  expect(itemListBefore.childNodes).toHaveLength(1);\n  expect(\n    getByText(itemListBefore, \"cheesecake - Quantity: 6\")\n  ).toBeInTheDocument();\n\n  // This is equivalent to reloading the page\n  document.body.innerHTML = initialHtml;\n  jest.resetModules();\n  require(\"./main\");\n\n  const itemListAfter = document.getElementById(\"item-list\");\n  expect(itemListAfter.childNodes).toHaveLength(1);\n  expect(\n    getByText(itemListAfter, \"cheesecake - Quantity: 6\")\n  ).toBeInTheDocument();\n});\n\ndescribe(\"adding items\", () => {\n  test(\"updating the item list\", () => {\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n    const submitBtn = screen.getByText(\"Add to inventory\");\n    fireEvent.input(itemField, {\n      target: { value: \"cheesecake\" },\n      bubbles: true\n    });\n\n    const quantityField = screen.getByPlaceholderText(\"Quantity\");\n    fireEvent.input(quantityField, { target: { value: \"6\" }, bubbles: true });\n\n    fireEvent.click(submitBtn);\n\n    const itemList = document.getElementById(\"item-list\");\n    expect(getByText(itemList, \"cheesecake - Quantity: 6\")).toBeInTheDocument();\n  });\n});\n\ndescribe(\"item name validation\", () => {\n  test(\"entering valid item names \", () => {\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n\n    fireEvent.input(itemField, {\n      target: { value: \"cheesecake\" },\n      bubbles: true\n    });\n\n    expect(screen.getByText(\"cheesecake is valid!\")).toBeInTheDocument();\n  });\n\n  test(\"entering invalid item names \", () => {\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n\n    fireEvent.input(itemField, { target: { value: \"book\" }, bubbles: true });\n\n    expect(screen.getByText(\"book is not a valid item.\")).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/1_localstorage/package.json",
    "content": "{\n  \"name\": \"2_localstorage\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"start\": \"http-server ./\",\n    \"test\": \"jest\",\n    \"build\": \"browserify main.js -o bundle.js\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"@testing-library/dom\": \"^7.2.2\",\n    \"@testing-library/jest-dom\": \"^5.5.0\",\n    \"browserify\": \"^16.5.1\",\n    \"http-server\": \"^0.12.1\",\n    \"jest\": \"^24.9.0\"\n  }\n}\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/1_localstorage/setupJestDom.js",
    "content": "const jestDom = require(\"@testing-library/jest-dom\");\n\nexpect.extend(jestDom);\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/2_history_api/domController.js",
    "content": "const { addItem, data } = require(\"./inventoryController\");\n\nconst updateItemList = inventory => {\n  if (inventory === null) return;\n\n  localStorage.setItem(\"inventory\", JSON.stringify(inventory));\n\n  const inventoryList = window.document.getElementById(\"item-list\");\n\n  // Clears the list\n  inventoryList.innerHTML = \"\";\n\n  Object.entries(inventory).forEach(([itemName, quantity]) => {\n    const listItem = window.document.createElement(\"li\");\n    listItem.innerHTML = `${itemName} - Quantity: ${quantity}`;\n\n    if (quantity < 5) {\n      listItem.className = \"almost-soldout\";\n    }\n\n    inventoryList.appendChild(listItem);\n  });\n\n  const inventoryContents = JSON.stringify(inventory);\n  const p = window.document.createElement(\"p\");\n  p.innerHTML = `The inventory has been updated - ${inventoryContents}`;\n\n  window.document.body.appendChild(p);\n};\n\nconst handleAddItem = event => {\n  // Prevent the page from reloading as it would by default\n  event.preventDefault();\n\n  const { name, quantity } = event.target.elements;\n  addItem(name.value, parseInt(quantity.value, 10));\n\n  history.pushState({ inventory: { ...data.inventory } }, document.title);\n\n  updateItemList(data.inventory);\n};\n\nconst validItems = [\"cheesecake\", \"apple pie\", \"carrot cake\"];\nconst checkFormValues = () => {\n  const itemName = document.querySelector(`input[name=\"name\"]`).value;\n  const quantity = document.querySelector(`input[name=\"quantity\"]`).value;\n\n  const itemNameIsEmpty = itemName === \"\";\n  const itemNameIsInvalid = !validItems.includes(itemName);\n  const quantityIsEmpty = quantity === \"\";\n\n  const errorMsg = window.document.getElementById(\"error-msg\");\n  if (itemNameIsEmpty) {\n    errorMsg.innerHTML = \"\";\n  } else if (itemNameIsInvalid) {\n    errorMsg.innerHTML = `${itemName} is not a valid item.`;\n  } else {\n    errorMsg.innerHTML = `${itemName} is valid!`;\n  }\n\n  const submitButton = document.querySelector(`button[type=\"submit\"]`);\n  if (itemNameIsEmpty || itemNameIsInvalid || quantityIsEmpty) {\n    submitButton.disabled = true;\n  } else {\n    submitButton.disabled = false;\n  }\n};\n\nconst handleUndo = () => {\n  if (history.state === null) return;\n  history.back();\n};\n\nconst handlePopstate = () => {\n  data.inventory = history.state ? history.state.inventory : {};\n  updateItemList(data.inventory);\n};\n\nmodule.exports = {\n  updateItemList,\n  handleAddItem,\n  checkFormValues,\n  handleUndo,\n  handlePopstate\n};\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/2_history_api/domController.test.js",
    "content": "const fs = require(\"fs\");\nconst initialHtml = fs.readFileSync(\"./index.html\");\nconst { getByText, screen } = require(\"@testing-library/dom\");\n\nconst {\n  updateItemList,\n  handleAddItem,\n  checkFormValues,\n  handleUndo,\n  handlePopstate\n} = require(\"./domController\");\n\nconst { clearHistoryHook, detachPopstateHandlers } = require(\"./testUtils\");\n\nconst { data } = require(\"./inventoryController\");\n\nbeforeEach(() => {\n  document.body.innerHTML = initialHtml;\n});\n\ndescribe(\"updateItemList\", () => {\n  beforeEach(() => localStorage.clear());\n\n  test(\"updates the DOM with the inventory items\", () => {\n    const inventory = {\n      cheesecake: 5,\n      \"apple pie\": 2,\n      \"carrot cake\": 6\n    };\n    updateItemList(inventory);\n\n    const itemList = document.getElementById(\"item-list\");\n    expect(itemList.childNodes).toHaveLength(3);\n\n    expect(getByText(itemList, \"cheesecake - Quantity: 5\")).toBeInTheDocument();\n    expect(getByText(itemList, \"apple pie - Quantity: 2\")).toBeInTheDocument();\n    expect(\n      getByText(itemList, \"carrot cake - Quantity: 6\")\n    ).toBeInTheDocument();\n  });\n\n  test(\"highlighting in red elements whose quantity is below five\", () => {\n    const inventory = { cheesecake: 5, \"apple pie\": 2, \"carrot cake\": 6 };\n    updateItemList(inventory);\n\n    expect(screen.getByText(\"apple pie - Quantity: 2\")).toHaveStyle({\n      color: \"red\"\n    });\n  });\n\n  test(\"adding a paragraph indicating what was the update\", () => {\n    const inventory = { cheesecake: 5, \"apple pie\": 2 };\n    updateItemList(inventory);\n\n    expect(\n      screen.getByText(\n        `The inventory has been updated - ${JSON.stringify(inventory)}`\n      )\n    ).toBeTruthy();\n  });\n\n  test(\"updates the localStorage with the inventory\", () => {\n    const inventory = { cheesecake: 5, \"apple pie\": 2 };\n    updateItemList(inventory);\n\n    expect(localStorage.getItem(\"inventory\")).toEqual(\n      JSON.stringify(inventory)\n    );\n  });\n\n  test(\"does not update the inventory when passing null\", () => {\n    localStorage.setItem(\"inventory\", JSON.stringify({ cheesecake: 5 }));\n    updateItemList(null);\n\n    expect(localStorage.getItem(\"inventory\")).toEqual(\n      JSON.stringify({ cheesecake: 5 })\n    );\n  });\n});\n\ndescribe(\"handleAddItem\", () => {\n  beforeEach(() => (data.inventory = {}));\n\n  test(\"adding items to the page\", () => {\n    const event = {\n      preventDefault: jest.fn(),\n      target: {\n        elements: {\n          name: { value: \"cheesecake\" },\n          quantity: { value: \"6\" }\n        }\n      }\n    };\n\n    handleAddItem(event);\n\n    // Checking if the form's default reload is prevent\n    expect(event.preventDefault.mock.calls).toHaveLength(1);\n\n    const itemList = document.getElementById(\"item-list\");\n    expect(getByText(itemList, \"cheesecake - Quantity: 6\")).toBeInTheDocument();\n  });\n\n  test(\"updating the application's history\", () => {\n    const event = {\n      preventDefault: jest.fn(),\n      target: {\n        elements: {\n          name: { value: \"cheesecake\" },\n          quantity: { value: \"6\" }\n        }\n      }\n    };\n\n    handleAddItem(event);\n\n    expect(history.state).toEqual({ inventory: { cheesecake: 6 } });\n  });\n});\n\ndescribe(\"checkFormValues\", () => {\n  test(\"entering valid item values\", () => {\n    document.querySelector(`input[name=\"name\"]`).value = \"cheesecake\";\n    document.querySelector(`input[name=\"quantity\"]`).value = \"1\";\n    checkFormValues();\n    expect(screen.getByText(\"Add to inventory\")).toBeEnabled();\n  });\n\n  test(\"entering invalid item names\", () => {\n    document.querySelector(`input[name=\"name\"]`).value = \"invalid\";\n    document.querySelector(`input[name=\"quantity\"]`).value = \"1\";\n    checkFormValues();\n    expect(screen.getByText(\"Add to inventory\")).toBeDisabled();\n\n    document.querySelector(`input[name=\"name\"]`).value = \"cheesecake\";\n    document.querySelector(`input[name=\"quantity\"]`).value = \"\";\n    checkFormValues();\n    expect(screen.getByText(\"Add to inventory\")).toBeDisabled();\n  });\n});\n\ndescribe(\"tests with history\", () => {\n  beforeEach(() => jest.spyOn(window, \"addEventListener\"));\n\n  afterEach(detachPopstateHandlers);\n\n  beforeEach(clearHistoryHook);\n\n  describe(\"handleUndo\", () => {\n    test(\"going back from a non-initial state\", done => {\n      window.addEventListener(\"popstate\", () => {\n        expect(history.state).toEqual(null);\n        done();\n      });\n\n      history.pushState({ inventory: { cheesecake: 5 } }, \"title\");\n      handleUndo();\n    });\n\n    test(\"going back from an initial state\", () => {\n      jest.spyOn(history, \"back\");\n      handleUndo();\n\n      // This assertion doesn't care about whether\n      // a call to `history.back` would have finished,\n      // it only checks whether it's been called\n      expect(history.back.mock.calls).toHaveLength(0);\n    });\n  });\n\n  describe(\"handlePopstate\", () => {\n    test(\"updating the item list with the current state\", () => {\n      history.pushState(\n        { inventory: { cheesecake: 5, \"carrot cake\": 2 } },\n        \"title\"\n      );\n\n      handlePopstate();\n\n      const itemList = document.getElementById(\"item-list\");\n      expect(itemList.childNodes).toHaveLength(2);\n      expect(\n        getByText(itemList, \"cheesecake - Quantity: 5\")\n      ).toBeInTheDocument();\n      expect(\n        getByText(itemList, \"carrot cake - Quantity: 2\")\n      ).toBeInTheDocument();\n    });\n  });\n});\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/2_history_api/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Inventory Manager</title>\n    <style>\n      .almost-soldout {\n        color: red;\n      }\n    </style>\n  </head>\n  <body>\n    <h1 data-testid=\"page-header\">Inventory Contents</h1>\n    <ul id=\"item-list\"></ul>\n    <p id=\"error-msg\"></p>\n    <form id=\"add-item-form\">\n      <input type=\"text\" name=\"name\" placeholder=\"Item name\" />\n      <input type=\"number\" name=\"quantity\" placeholder=\"Quantity\" />\n      <button type=\"submit\">Add to inventory</button>\n    </form>\n\n    <button id=\"undo-button\">Undo</button>\n\n    <script src=\"bundle.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/2_history_api/inventoryController.js",
    "content": "const data = { inventory: {} };\n\nconst addItem = (itemName, quantity) => {\n  const currentQuantity = data.inventory[itemName] || 0;\n  data.inventory[itemName] = currentQuantity + quantity;\n  return data.inventory;\n};\n\nmodule.exports = { data, addItem };\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/2_history_api/inventoryController.test.js",
    "content": "const { addItem, data } = require(\"./inventoryController\");\n\ndescribe(\"addItem\", () => {\n  test(\"adding new items to the inventory\", () => {\n    data.inventory = {};\n    addItem(\"cheesecake\", 5);\n    expect(data.inventory.cheesecake).toBe(5);\n  });\n});\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/2_history_api/jest.config.js",
    "content": "module.exports = {\n  setupFilesAfterEnv: [\"./setupJestDom.js\"]\n};\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/2_history_api/main.js",
    "content": "const {\n  handleAddItem,\n  checkFormValues,\n  handleUndo,\n  handlePopstate,\n  updateItemList\n} = require(\"./domController\");\n\nconst { data } = require(\"./inventoryController\");\n\nconst form = document.getElementById(\"add-item-form\");\nform.addEventListener(\"submit\", handleAddItem);\nform.addEventListener(\"input\", checkFormValues);\n\nconst undoButton = document.getElementById(\"undo-button\");\nundoButton.addEventListener(\"click\", handleUndo);\n\nwindow.addEventListener(\"popstate\", handlePopstate);\n\n// Run `checkFormValues` once to see if the initial state is valid\ncheckFormValues();\n\n// Restore the inventory when the page loads\nconst storedInventory = JSON.parse(localStorage.getItem(\"inventory\"));\n\nif (storedInventory) {\n  data.inventory = storedInventory;\n  updateItemList(data.inventory);\n}\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/2_history_api/main.test.js",
    "content": "const fs = require(\"fs\");\nconst initialHtml = fs.readFileSync(\"./index.html\");\nconst { screen, getByText, fireEvent } = require(\"@testing-library/dom\");\n\nconst { clearHistoryHook, detachPopstateHandlers } = require(\"./testUtils.js\");\n\nbeforeEach(clearHistoryHook);\n\nbeforeEach(() => localStorage.clear());\n\nbeforeEach(() => {\n  document.body.innerHTML = initialHtml;\n\n  // You must execute main.js again so that it can attach the\n  // event listener to the form every time the body changes.\n  // Here you must use `jest.resetModules` because otherwise\n  // Jest will have cached `main.js` and it will _not_ run again.\n  jest.resetModules();\n  require(\"./main\");\n\n  // You can only spy on `window.addEventListener` after `main.js`\n  // has been executed. Otherwise `detachPopstateHandlers` will\n  // also detach the handlers that `main.js` attached to the page.\n  jest.spyOn(window, \"addEventListener\");\n});\n\nafterEach(detachPopstateHandlers);\n\ntest(\"persists items between sessions\", () => {\n  const itemField = screen.getByPlaceholderText(\"Item name\");\n  const submitBtn = screen.getByText(\"Add to inventory\");\n  fireEvent.input(itemField, {\n    target: { value: \"cheesecake\" },\n    bubbles: true\n  });\n\n  const quantityField = screen.getByPlaceholderText(\"Quantity\");\n  fireEvent.input(quantityField, { target: { value: \"6\" }, bubbles: true });\n\n  fireEvent.click(submitBtn);\n\n  const itemListBefore = document.getElementById(\"item-list\");\n  expect(itemListBefore.childNodes).toHaveLength(1);\n  expect(\n    getByText(itemListBefore, \"cheesecake - Quantity: 6\")\n  ).toBeInTheDocument();\n\n  // This is equivalent to reloading the page\n  document.body.innerHTML = initialHtml;\n  jest.resetModules();\n  require(\"./main\");\n\n  const itemListAfter = document.getElementById(\"item-list\");\n  expect(itemListAfter.childNodes).toHaveLength(1);\n  expect(\n    getByText(itemListAfter, \"cheesecake - Quantity: 6\")\n  ).toBeInTheDocument();\n});\n\ndescribe(\"adding items\", () => {\n  test(\"updating the item list\", () => {\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n    const submitBtn = screen.getByText(\"Add to inventory\");\n    fireEvent.input(itemField, {\n      target: { value: \"cheesecake\" },\n      bubbles: true\n    });\n\n    const quantityField = screen.getByPlaceholderText(\"Quantity\");\n    fireEvent.input(quantityField, { target: { value: \"6\" }, bubbles: true });\n\n    fireEvent.click(submitBtn);\n\n    const itemList = document.getElementById(\"item-list\");\n    expect(getByText(itemList, \"cheesecake - Quantity: 6\")).toBeInTheDocument();\n  });\n\n  test(\"undo to one item\", done => {\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n    const quantityField = screen.getByPlaceholderText(\"Quantity\");\n    const submitBtn = screen.getByText(\"Add to inventory\");\n\n    fireEvent.input(itemField, {\n      target: { value: \"cheesecake\" },\n      bubbles: true\n    });\n    fireEvent.input(quantityField, { target: { value: \"6\" }, bubbles: true });\n\n    fireEvent.click(submitBtn);\n\n    fireEvent.input(itemField, {\n      target: { value: \"carrot cake\" },\n      bubbles: true\n    });\n    fireEvent.input(quantityField, { target: { value: \"5\" }, bubbles: true });\n    fireEvent.click(submitBtn);\n\n    window.addEventListener(\"popstate\", () => {\n      const itemList = document.getElementById(\"item-list\");\n      expect(itemList.children).toHaveLength(1);\n      expect(\n        getByText(itemList, \"cheesecake - Quantity: 6\")\n      ).toBeInTheDocument();\n      done();\n    });\n\n    fireEvent.click(screen.getByText(\"Undo\"));\n  });\n\n  test(\"undo to empty list\", done => {\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n    const submitBtn = screen.getByText(\"Add to inventory\");\n    fireEvent.input(itemField, {\n      target: { value: \"cheesecake\" },\n      bubbles: true\n    });\n\n    const quantityField = screen.getByPlaceholderText(\"Quantity\");\n    fireEvent.input(quantityField, { target: { value: \"6\" }, bubbles: true });\n\n    fireEvent.click(submitBtn);\n\n    expect(history.state).toEqual({ inventory: { cheesecake: 6 } });\n\n    window.addEventListener(\"popstate\", () => {\n      const itemList = document.getElementById(\"item-list\");\n      expect(itemList).toBeEmpty();\n      done();\n    });\n\n    fireEvent.click(screen.getByText(\"Undo\"));\n  });\n});\n\ndescribe(\"item name validation\", () => {\n  test(\"entering valid item names \", () => {\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n\n    fireEvent.input(itemField, {\n      target: { value: \"cheesecake\" },\n      bubbles: true\n    });\n\n    expect(screen.getByText(\"cheesecake is valid!\")).toBeInTheDocument();\n  });\n\n  test(\"entering invalid item names \", () => {\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n\n    fireEvent.input(itemField, { target: { value: \"book\" }, bubbles: true });\n\n    expect(screen.getByText(\"book is not a valid item.\")).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/2_history_api/package.json",
    "content": "{\n  \"name\": \"2_localstorage\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"start\": \"http-server ./\",\n    \"test\": \"jest\",\n    \"build\": \"browserify main.js -o bundle.js\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"@testing-library/dom\": \"^7.2.2\",\n    \"@testing-library/jest-dom\": \"^5.5.0\",\n    \"browserify\": \"^16.5.1\",\n    \"http-server\": \"^0.12.1\",\n    \"jest\": \"^24.9.0\"\n  }\n}\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/2_history_api/setupJestDom.js",
    "content": "const jestDom = require(\"@testing-library/jest-dom\");\n\nexpect.extend(jestDom);\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/2_history_api/testUtils.js",
    "content": "const clearHistoryHook = done => {\n  const clearHistory = () => {\n    if (history.state === null) {\n      window.removeEventListener(\"popstate\", clearHistory);\n      return done();\n    }\n\n    history.back();\n  };\n\n  window.addEventListener(\"popstate\", clearHistory);\n\n  clearHistory();\n};\n\nconst detachPopstateHandlers = () => {\n  const popstateListeners = window.addEventListener.mock.calls.filter(\n    ([eventName]) => {\n      return eventName === \"popstate\";\n    }\n  );\n\n  popstateListeners.forEach(([eventName, handlerFn]) => {\n    window.removeEventListener(eventName, handlerFn);\n  });\n\n  jest.restoreAllMocks();\n};\n\nmodule.exports = { clearHistoryHook, detachPopstateHandlers };\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/server/README.md",
    "content": "# Chapter 5 Server\n\nTo better support the client-side application we'll build on Chapter 5, I've had to do a few updates to the server from Chapter 4.\n\nIn case you want to update the back-end from Chapter 4 yourself, here's the list of changes I've done:\n\n- For the server to accept the requests coming from the client, you'll need to use [`@koa/cors`](https://github.com/koajs/cors)\n- To enable running tests while the server is running, I bind it to different ports depending on whether I am in a test or development environment.\n- At `POST /inventory/:itemName` I have added a route which adds an item to the inventory. It takes a `body` containing the `quantity` to add.\n- At `GET /inventory` I have added a route which lists all items in the inventory.\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/server/authenticationController.js",
    "content": "const crypto = require(\"crypto\");\nconst { db } = require(\"./dbConnection\");\n\nconst hashPassword = password => {\n  const hash = crypto.createHash(\"sha256\");\n  hash.update(password);\n  return hash.digest(\"hex\");\n};\n\nconst credentialsAreValid = async (username, password) => {\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  if (!user) return false;\n  return hashPassword(password) === user.passwordHash;\n};\n\nconst authenticationMiddleware = async (ctx, next) => {\n  try {\n    const authHeader = ctx.request.headers.authorization;\n    const credentials = Buffer.from(\n      authHeader.slice(\"basic\".length + 1),\n      \"base64\"\n    ).toString();\n    const [username, password] = credentials.split(\":\");\n\n    const validCredentialsSent = await credentialsAreValid(username, password);\n    if (!validCredentialsSent) throw new Error(\"invalid credentials\");\n  } catch (e) {\n    ctx.status = 401;\n    ctx.body = { message: \"please provide valid credentials\" };\n    return;\n  }\n\n  await next();\n};\n\nmodule.exports = {\n  hashPassword,\n  credentialsAreValid,\n  authenticationMiddleware\n};\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/server/authenticationController.test.js",
    "content": "const crypto = require(\"crypto\");\nconst {\n  hashPassword,\n  credentialsAreValid,\n  authenticationMiddleware\n} = require(\"./authenticationController\");\nconst { user: globalUser } = require(\"./userTestUtils\");\n\ndescribe(\"hashPassword\", () => {\n  test(\"hashing passwords\", () => {\n    const plainTextPassword = \"password_example\";\n    const hash = crypto.createHash(\"sha256\");\n    hash.update(plainTextPassword);\n    const expectedHash = hash.digest(\"hex\");\n    expect(hashPassword(plainTextPassword)).toBe(expectedHash);\n  });\n});\n\ndescribe(\"credentialsAreValid\", () => {\n  test(\"validating credentials\", async () => {\n    expect(await credentialsAreValid(globalUser.username, \"a_password\")).toBe(\n      true\n    );\n  });\n});\n\ndescribe(\"authenticationMiddleware\", () => {\n  test(\"returning an error if the credentials are not valid\", async () => {\n    const fakeAuth = Buffer.from(\"invalid:credentials\").toString(\"base64\");\n    const ctx = {\n      request: {\n        headers: { authorization: `Basic ${fakeAuth}` }\n      }\n    };\n\n    const next = jest.fn();\n    await authenticationMiddleware(ctx, next);\n    expect(next.mock.calls).toHaveLength(0);\n    expect(ctx).toEqual({\n      ...ctx,\n      status: 401,\n      body: { message: \"please provide valid credentials\" }\n    });\n  });\n\n  test(\"authenticating properly\", async () => {\n    const ctx = {\n      request: {\n        headers: { authorization: globalUser.authHeader }\n      }\n    };\n\n    const next = jest.fn();\n    await authenticationMiddleware(ctx, next);\n    expect(next.mock.calls).toHaveLength(1);\n  });\n});\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/server/cartController.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { removeFromInventory } = require(\"./inventoryController\");\nconst logger = require(\"./logger\");\n\nconst addItemToCart = async (username, itemName) => {\n  await removeFromInventory(itemName);\n\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  if (!user) {\n    const userNotFound = new Error(\"user not found\");\n    userNotFound.code = 404;\n  }\n\n  const itemEntry = await db\n    .select()\n    .from(\"carts_items\")\n    .where({ userId: user.id, itemName })\n    .first();\n\n  if (itemEntry && itemEntry.quantity + 1 > 3) {\n    const limitError = new Error(\n      \"You can't have more than three units of an item in your cart\"\n    );\n    limitError.code = 400;\n    throw limitError;\n  }\n\n  if (itemEntry) {\n    await db(\"carts_items\")\n      .increment(\"quantity\")\n      .update({ updatedAt: new Date().toISOString() })\n      .where({\n        userId: itemEntry.userId,\n        itemName\n      });\n  } else {\n    await db(\"carts_items\").insert({\n      userId: user.id,\n      itemName,\n      quantity: 1,\n      updatedAt: new Date().toISOString()\n    });\n  }\n\n  logger.log(`${itemName} added to ${username}'s cart`);\n  return db\n    .select(\"itemName\", \"quantity\")\n    .from(\"carts_items\")\n    .where({ userId: user.id });\n};\n\nconst hoursInMs = n => 1000 * 60 * 60 * n;\n\nconst removeStaleItems = async () => {\n  const fourHoursAgo = new Date(Date.now() - hoursInMs(4)).toISOString();\n\n  const staleItems = await db\n    .select()\n    .from(\"carts_items\")\n    .where(\"updatedAt\", \"<\", fourHoursAgo);\n\n  if (staleItems.length === 0) return;\n\n  // Put stale items back in the inventory\n  const inventoryUpdates = staleItems.map(staleItem =>\n    db(\"inventory\")\n      .increment(\"quantity\", staleItem.quantity)\n      .where({ itemName: staleItem.itemName })\n  );\n  await Promise.all(inventoryUpdates);\n\n  // Delete stale items from cart\n  const staleItemTuples = staleItems.map(i => [i.itemName, i.userId]);\n  await db(\"carts_items\")\n    .del()\n    .whereIn([\"itemName\", \"userId\"], staleItemTuples);\n};\n\nconst monitorStaleItems = () => setInterval(removeStaleItems, hoursInMs(2));\n\nmodule.exports = { addItemToCart, monitorStaleItems };\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/server/cartController.test.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { addItemToCart, monitorStaleItems } = require(\"./cartController\");\nconst { hashPassword } = require(\"./authenticationController\");\nconst { user: globalUser } = require(\"./userTestUtils\");\nconst FakeTimers = require(\"@sinonjs/fake-timers\");\n\nconst fs = require(\"fs\");\n\ndescribe(\"addItemToCart\", () => {\n  beforeEach(() => {\n    fs.writeFileSync(\"/tmp/logs.out\", \"\");\n  });\n\n  test(\"adding unavailable items to cart\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 0 });\n\n    try {\n      await addItemToCart(globalUser.username, \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(\"cheesecake is unavailable\");\n      expectedError.code = 400;\n\n      expect(e).toEqual(expectedError);\n    }\n\n    const finalCartContent = await db\n      .select(\"carts_items.*\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n\n    expect(finalCartContent).toEqual([]);\n    expect.assertions(2);\n  });\n\n  test(\"adding items above limit to cart\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 1 });\n    await db(\"carts_items\").insert({\n      userId: globalUser.id,\n      itemName: \"cheesecake\",\n      quantity: 3\n    });\n\n    try {\n      await addItemToCart(globalUser.username, \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(\n        \"You can't have more than three units of an item in your cart\"\n      );\n      expectedError.code = 400;\n      expect(e).toEqual(expectedError);\n    }\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n\n    expect(finalCartContent).toEqual([{ itemName: \"cheesecake\", quantity: 3 }]);\n    expect.assertions(2);\n  });\n\n  test(\"logging added items\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 1 });\n    await db(\"carts_items\").insert({\n      userId: globalUser.id,\n      itemName: \"cheesecake\",\n      quantity: 1\n    });\n\n    await addItemToCart(globalUser.username, \"cheesecake\");\n\n    const logs = fs.readFileSync(\"/tmp/logs.out\", \"utf-8\");\n    expect(logs).toContain(\n      `cheesecake added to ${globalUser.username}'s cart\\n`\n    );\n  });\n});\n\nconst withRetries = async fn => {\n  // Capture the assertion error since Jest does not export it\n  const JestAssertionError = (() => {\n    try {\n      expect(false).toBe(true);\n    } catch (e) {\n      return e.constructor;\n    }\n  })();\n\n  try {\n    await fn();\n  } catch (e) {\n    if (e.constructor === JestAssertionError) {\n      // Wait 100ms before retrying\n      await new Promise(resolve => setTimeout(resolve, 100));\n      await withRetries(fn);\n    } else {\n      throw e;\n    }\n  }\n};\n\ndescribe(\"timers\", () => {\n  const hoursInMs = n => 1000 * 60 * 60 * n;\n\n  let clock;\n  beforeEach(() => {\n    clock = FakeTimers.install({ toFake: [\"Date\", \"setInterval\"] });\n  });\n\n  afterEach(() => {\n    clock = clock.uninstall();\n  });\n\n  test(\"removing stale items\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 1 });\n    await addItemToCart(globalUser.username, \"cheesecake\");\n\n    clock.tick(hoursInMs(4));\n    timer = monitorStaleItems();\n    clock.tick(hoursInMs(2));\n\n    await withRetries(async () => {\n      const finalCartContent = await db\n        .select()\n        .from(\"carts_items\")\n        .join(\"users\", \"users.id\", \"carts_items.userId\")\n        .where(\"users.username\", globalUser.username);\n\n      expect(finalCartContent).toEqual([]);\n    });\n\n    await withRetries(async () => {\n      const inventoryContent = await db\n        .select(\"itemName\", \"quantity\")\n        .from(\"inventory\");\n\n      expect(inventoryContent).toEqual([\n        { itemName: \"cheesecake\", quantity: 1 }\n      ]);\n    });\n  });\n});\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/server/dbConnection.js",
    "content": "const environmentName = process.env.NODE_ENV;\nconst db = require(\"knex\")(require(\"./knexfile\")[environmentName]);\n\nconst closeConnection = () => db.destroy();\n\nmodule.exports = {\n  db,\n  closeConnection\n};\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/server/disconnectFromDb.js",
    "content": "const { db } = require(\"./dbConnection\");\n\nafterAll(() => db.destroy());\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/server/inventoryController.js",
    "content": "const { db } = require(\"./dbConnection\");\n\nconst removeFromInventory = async itemName => {\n  const inventoryEntry = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName })\n    .first();\n\n  if (!inventoryEntry || inventoryEntry.quantity === 0) {\n    const err = new Error(`${itemName} is unavailable`);\n    err.code = 400;\n    throw err;\n  }\n\n  await db(\"inventory\")\n    .decrement(\"quantity\")\n    .where({ itemName });\n};\n\nmodule.exports = { removeFromInventory };\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/server/jest.config.js",
    "content": "module.exports = {\n  testEnvironment: \"node\",\n  globalSetup: \"./migrateDatabases.js\",\n  setupFilesAfterEnv: [\n    \"<rootDir>/truncateTables.js\",\n    \"<rootDir>/seedUser.js\",\n    \"<rootDir>/disconnectFromDb.js\"\n  ]\n};\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/server/knexfile.js",
    "content": "module.exports = {\n  test: {\n    client: \"sqlite3\",\n    connection: { filename: \"./test.sqlite\" },\n    useNullAsDefault: true\n  },\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"./dev.sqlite\" },\n    useNullAsDefault: true\n  }\n};\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/server/logger.js",
    "content": "const fs = require(\"fs\");\n\nconst logger = {\n  log: msg => fs.appendFileSync(\"/tmp/logs.out\", msg + \"\\n\")\n};\n\nmodule.exports = logger;\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/server/migrateDatabases.js",
    "content": "const environmentName = process.env.NODE_ENV || \"test\";\nconst environmentConfig = require(\"./knexfile\")[environmentName];\nconst db = require(\"knex\")(environmentConfig);\n\nmodule.exports = async () => {\n  // Migrate the database to the latest state\n  await db.migrate.latest();\n\n  // Close the connection to the database so that tests won't hang\n  await db.destroy();\n};\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/server/migrations/20200325082401_initial_schema.js",
    "content": "exports.up = async knex => {\n  await knex.schema.createTable(\"users\", table => {\n    table.increments(\"id\");\n    table.string(\"username\");\n    table.unique(\"username\");\n    table.string(\"email\");\n    table.string(\"passwordHash\");\n  });\n\n  await knex.schema.createTable(\"carts_items\", table => {\n    table.integer(\"userId\").references(\"users.id\");\n    table.string(\"itemName\");\n    table.unique(\"itemName\");\n    table.integer(\"quantity\");\n  });\n\n  await knex.schema.createTable(\"inventory\", table => {\n    table.increments(\"id\");\n    table.string(\"itemName\");\n    table.unique(\"itemName\");\n    table.integer(\"quantity\");\n  });\n};\n\nexports.down = async knex => {\n  await knex.schema.dropTable(\"users\");\n  await knex.schema.dropTable(\"carts_items\");\n  await knex.schema.dropTable(\"inventory\");\n};\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/server/migrations/20200331210311_updatedAt_field.js",
    "content": "exports.up = knex => {\n  return knex.schema.alterTable(\"carts_items\", table => {\n    table.timestamp(\"updatedAt\");\n  });\n};\n\nexports.down = knex => {\n  return knex.schema.alterTable(\"carts_items\", table => {\n    table.dropColumn(\"updatedAt\");\n  });\n};\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/server/package.json",
    "content": "{\n  \"name\": \"4_integrations_with_other_apis\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"test\": \"jest --runInBand\",\n    \"start\": \"cross-env NODE_ENV=development node server.js\",\n    \"migrate:dev\": \"knex migrate:latest --env development\",\n    \"seed:dev\": \"knex seed:run\"\n  },\n  \"devDependencies\": {\n    \"@sinonjs/fake-timers\": \"github:sinonjs/fake-timers\",\n    \"jest\": \"^24.9.0\",\n    \"supertest\": \"^4.0.2\"\n  },\n  \"dependencies\": {\n    \"@koa/cors\": \"^3.0.0\",\n    \"cross-env\": \"^7.0.2\",\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"knex\": \"^0.20.13\",\n    \"koa\": \"^2.11.0\",\n    \"koa-body-parser\": \"^1.1.2\",\n    \"koa-router\": \"^7.4.0\",\n    \"nock\": \"^12.0.3\",\n    \"sqlite3\": \"^4.1.1\"\n  },\n  \"main\": \"alertController.spec.js\",\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"description\": \"\"\n}\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/server/seedUser.js",
    "content": "const { createUser } = require(\"./userTestUtils\");\n\nbeforeEach(createUser);\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/server/seeds/initial_inventory.js",
    "content": "exports.seed = async knex => {\n  await knex(\"inventory\").del();\n  return knex(\"inventory\").insert([\n    { itemName: \"cheesecake\", quantity: 8 },\n    { itemName: \"apple pie\", quantity: 2 },\n    { itemName: \"carrot cake\", quantity: 5 }\n  ]);\n};\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/server/server.js",
    "content": "const fetch = require(\"isomorphic-fetch\");\nconst Koa = require(\"koa\");\nconst cors = require(\"@koa/cors\");\nconst Router = require(\"koa-router\");\nconst bodyParser = require(\"koa-body-parser\");\n\nconst { db } = require(\"./dbConnection\");\n\nconst { addItemToCart } = require(\"./cartController\");\nconst {\n  hashPassword,\n  authenticationMiddleware\n} = require(\"./authenticationController\");\n\nconst PORT = process.env.NODE_ENV === \"test\" ? 5000 : 3000;\n\nconst app = new Koa();\nconst router = new Router();\n\napp.use(cors());\n\napp.use(bodyParser());\n\napp.use(async (ctx, next) => {\n  if (ctx.url.startsWith(\"/carts\")) {\n    return await authenticationMiddleware(ctx, next);\n  }\n\n  await next();\n});\n\nrouter.put(\"/users/:username\", async ctx => {\n  const { username } = ctx.params;\n  const { email, password } = ctx.request.body;\n\n  const userAlreadyExists = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n\n  if (userAlreadyExists) {\n    ctx.body = { message: `${username} already exists` };\n    ctx.status = 409;\n    return;\n  }\n\n  await db(\"users\").insert({\n    username,\n    email,\n    passwordHash: hashPassword(password)\n  });\n\n  return (ctx.body = { message: `${username} created successfully` });\n});\n\nrouter.post(\"/carts/:username/items\", async ctx => {\n  const { username } = ctx.params;\n  const { item, quantity } = ctx.request.body;\n\n  for (let i = 0; i < quantity; i++) {\n    try {\n      const newItems = await addItemToCart(username, item);\n      ctx.body = newItems;\n    } catch (e) {\n      ctx.body = { message: e.message };\n      ctx.status = e.code;\n      return;\n    }\n  }\n});\n\nrouter.delete(\"/carts/:username/items/:item\", async ctx => {\n  const { username, item } = ctx.params;\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n\n  if (!user) {\n    ctx.body = { message: \"user not found\" };\n    ctx.status = 404;\n    return;\n  }\n\n  const itemEntry = await db\n    .select()\n    .from(\"carts_items\")\n    .where({ userId: user.id, itemName: item })\n    .first();\n\n  if (!itemEntry || itemEntry.quantity === 0) {\n    ctx.body = { message: `${item} is not in the cart` };\n    ctx.status = 400;\n    return;\n  }\n\n  await db(\"carts_items\")\n    .decrement(\"quantity\")\n    .where({ userId: user.id, itemName: item });\n\n  const inventoryEntry = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName: item })\n    .first();\n  if (inventoryEntry) {\n    await db(\"inventory\")\n      .increment(\"quantity\")\n      .where({ userId: itemEntry.userId, itemName: item });\n  } else {\n    await db(\"inventory\").insert({ itemName: item, quantity: 1 });\n  }\n\n  ctx.body = await db\n    .select(\"itemName\", \"quantity\")\n    .from(\"carts_items\")\n    .where({ userId: user.id });\n});\n\nrouter.post(\"/inventory/:itemName\", async ctx => {\n  const { itemName } = ctx.params;\n  const { quantity } = ctx.request.body;\n\n  const current = await db\n    .select(\"itemName\", \"quantity\")\n    .from(\"inventory\")\n    .where({ itemName })\n    .first();\n\n  const itemExists = current && current.quantity > 0;\n  const newRecord = {\n    itemName,\n    quantity: (itemExists ? current.quantity : 0) + quantity\n  };\n\n  if (current) {\n    await db(\"inventory\")\n      .increment(\"quantity\", quantity)\n      .where({ itemName });\n  } else {\n    await db(\"inventory\").insert(newRecord);\n  }\n\n  ctx.body = newRecord;\n});\n\nrouter.get(\"/inventory\", async ctx => {\n  ctx.body = await db\n    .select(\"itemName\", \"quantity\")\n    .from(\"inventory\")\n    .where(\"quantity\", \">\", 0)\n    .orderBy(\"quantity\", \"desc\");\n});\n\nrouter.get(\"/inventory/:itemName\", async ctx => {\n  const { itemName } = ctx.params;\n\n  const response = await fetch(`http://recipepuppy.com/api?i=${itemName}`);\n  const { title, href, results: recipes } = await response.json();\n  const inventoryItem = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName })\n    .first();\n\n  ctx.body = {\n    ...inventoryItem,\n    info: `Data obtained from ${title} - ${href}`,\n    recipes\n  };\n});\n\napp.use(router.routes());\n\nmodule.exports = { app: app.listen(PORT) };\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/server/server.test.js",
    "content": "const { user: globalUser } = require(\"./userTestUtils\");\nconst { db } = require(\"./dbConnection\");\nconst request = require(\"supertest\");\nconst { app } = require(\"./server.js\");\nconst { hashPassword } = require(\"./authenticationController.js\");\nconst nock = require(\"nock\");\n\nafterAll(() => app.close());\n\ndescribe(\"add items to a cart\", () => {\n  test(\"adding available items\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 3 });\n    const response = await request(app)\n      .post(`/carts/${globalUser.username}/items`)\n      .set(\"authorization\", globalUser.authHeader)\n      .send({ item: \"cheesecake\", quantity: 3 })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    const newItems = [{ itemName: \"cheesecake\", quantity: 3 }];\n    expect(response.body).toEqual(newItems);\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(0);\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n\n    expect(finalCartContent).toEqual(newItems);\n  });\n\n  test(\"adding unavailable items\", async () => {\n    const response = await request(app)\n      .post(`/carts/${globalUser.username}/items`)\n      .set(\"authorization\", globalUser.authHeader)\n      .send({ item: \"cheesecake\", quantity: 1 })\n      .expect(400)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"cheesecake is unavailable\"\n    });\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n    expect(finalCartContent).toEqual([]);\n  });\n});\n\ndescribe(\"removing items from a cart\", () => {\n  test(\"removing existing items\", async () => {\n    await db(\"carts_items\").insert({\n      userId: globalUser.id,\n      itemName: \"cheesecake\",\n      quantity: 1\n    });\n\n    const response = await request(app)\n      .del(`/carts/${globalUser.username}/items/cheesecake`)\n      .set(\"authorization\", globalUser.authHeader)\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    const expectedFinalContent = [{ itemName: \"cheesecake\", quantity: 0 }];\n\n    expect(response.body).toEqual(expectedFinalContent);\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n    expect(finalCartContent).toEqual(expectedFinalContent);\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(1);\n  });\n\n  test(\"removing non-existing items\", async () => {\n    await db(\"inventory\").insert({\n      itemName: \"cheesecake\",\n      quantity: 0\n    });\n\n    const response = await request(app)\n      .del(`/carts/${globalUser.username}/items/cheesecake`)\n      .set(\"authorization\", globalUser.authHeader)\n      .expect(400)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"cheesecake is not in the cart\"\n    });\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(0);\n  });\n});\n\ndescribe(\"create accounts\", () => {\n  test(\"creating a new account\", async () => {\n    const response = await request(app)\n      .put(\"/users/another_user\")\n      .send({ email: \"another_user@example.org\", password: \"a_password\" })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"another_user created successfully\"\n    });\n\n    const savedUser = await db\n      .select(\"email\", \"passwordHash\")\n      .from(\"users\")\n      .where({ username: \"another_user\" })\n      .first();\n\n    expect(savedUser).toEqual({\n      email: \"another_user@example.org\",\n      passwordHash: hashPassword(\"a_password\")\n    });\n  });\n\n  test(\"creating a duplicate account\", async () => {\n    const response = await request(app)\n      .put(`/users/${globalUser.username}`)\n      .send({ email: globalUser.email, password: \"a_password\" })\n      .expect(409)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: `${globalUser.username} already exists`\n    });\n  });\n});\n\ndescribe(\"list inventory items\", () => {\n  const eggs = { itemName: \"eggs\", quantity: 3 };\n  const applePie = { itemName: \"apple pie\", quantity: 1 };\n  const carrotCake = { itemName: \"carrot cake\", quantity: 0 };\n\n  beforeEach(async () => {\n    await db(\"inventory\").insert([eggs, applePie, carrotCake]);\n  });\n\n  test(\"fetching all available items\", async () => {\n    const { body } = await request(app)\n      .get(\"/inventory\")\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(body).toEqual([eggs, applePie]);\n  });\n});\n\ndescribe(\"add inventory items\", () => {\n  test(\"adding a new item\", async () => {\n    const { body } = await request(app)\n      .post(\"/inventory/eggs\")\n      .send({ quantity: 3 })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(body).toEqual({ itemName: \"eggs\", quantity: 3 });\n\n    expect(\n      await db\n        .select(\"itemName\", \"quantity\")\n        .from(\"inventory\")\n        .where(\"itemName\", \"eggs\")\n        .first()\n    ).toEqual({ itemName: \"eggs\", quantity: 3 });\n  });\n\n  test(\"adding an existing item\", async () => {\n    const eggs = { itemName: \"eggs\", quantity: 2 };\n    await db(\"inventory\").insert(eggs);\n\n    const { body } = await request(app)\n      .post(\"/inventory/eggs\")\n      .send({ quantity: 3 })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(body).toEqual({ itemName: \"eggs\", quantity: 5 });\n\n    expect(\n      await db\n        .select(\"itemName\", \"quantity\")\n        .from(\"inventory\")\n        .where(\"itemName\", \"eggs\")\n        .first()\n    ).toEqual({ itemName: \"eggs\", quantity: 5 });\n  });\n});\n\ndescribe(\"fetch inventory items\", () => {\n  const eggs = { itemName: \"eggs\", quantity: 3 };\n  const applePie = { itemName: \"apple pie\", quantity: 1 };\n\n  beforeEach(async () => {\n    await db(\"inventory\").insert([eggs, applePie]);\n    const { id: eggsId } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"eggs\" })\n      .first();\n    eggs.id = eggsId;\n  });\n\n  test(\"fetching an item from the inventory\", async () => {\n    const eggsResponse = {\n      title: \"FakeAPI\",\n      href: \"example.org\",\n      results: [{ name: \"Omelette du Fromage\" }]\n    };\n\n    nock(\"http://recipepuppy.com\")\n      .get(\"/api\")\n      .query({ i: \"eggs\" })\n      .reply(200, eggsResponse);\n\n    const response = await request(app)\n      .get(`/inventory/eggs`)\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      ...eggs,\n      info: `Data obtained from ${eggsResponse.title} - ${eggsResponse.href}`,\n      recipes: eggsResponse.results\n    });\n  });\n});\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/server/truncateTables.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst tablesToTruncate = [\"users\", \"inventory\", \"carts_items\"];\n\nbeforeEach(() => {\n  return Promise.all(tablesToTruncate.map(t => db(t).truncate()));\n});\n"
  },
  {
    "path": "chapter6/4_testing_and_browser_apis/server/userTestUtils.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { hashPassword } = require(\"./authenticationController\");\n\nconst username = \"test_user\";\nconst password = \"a_password\";\nconst passwordHash = hashPassword(password);\nconst email = \"test_user@example.org\";\nconst validAuth = Buffer.from(`${username}:${password}`).toString(\"base64\");\nconst authHeader = `Basic ${validAuth}`;\n\nconst user = {\n  username,\n  password,\n  email,\n  authHeader\n};\n\nconst createUser = async () => {\n  await db(\"users\").insert({ username, email, passwordHash });\n  const { id } = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  user.id = id;\n};\n\nmodule.exports = { user, createUser };\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/1_http_requests/domController.js",
    "content": "const { addItem, data } = require(\"./inventoryController\");\n\nconst updateItemList = inventory => {\n  if (inventory === null) return;\n\n  localStorage.setItem(\"inventory\", JSON.stringify(inventory));\n\n  const inventoryList = window.document.getElementById(\"item-list\");\n\n  // Clears the list\n  inventoryList.innerHTML = \"\";\n\n  Object.entries(inventory).forEach(([itemName, quantity]) => {\n    const listItem = window.document.createElement(\"li\");\n    listItem.innerHTML = `${itemName} - Quantity: ${quantity}`;\n\n    if (quantity < 5) {\n      listItem.className = \"almost-soldout\";\n    }\n\n    inventoryList.appendChild(listItem);\n  });\n\n  const inventoryContents = JSON.stringify(inventory);\n  const p = window.document.createElement(\"p\");\n  p.innerHTML = `The inventory has been updated - ${inventoryContents}`;\n\n  window.document.body.appendChild(p);\n};\n\nconst handleAddItem = event => {\n  // Prevent the page from reloading as it would by default\n  event.preventDefault();\n\n  const { name, quantity } = event.target.elements;\n  addItem(name.value, parseInt(quantity.value, 10));\n\n  history.pushState({ inventory: { ...data.inventory } }, document.title);\n\n  updateItemList(data.inventory);\n};\n\nconst validItems = [\"cheesecake\", \"apple pie\", \"carrot cake\"];\nconst checkFormValues = () => {\n  const itemName = document.querySelector(`input[name=\"name\"]`).value;\n  const quantity = document.querySelector(`input[name=\"quantity\"]`).value;\n\n  const itemNameIsEmpty = itemName === \"\";\n  const itemNameIsInvalid = !validItems.includes(itemName);\n  const quantityIsEmpty = quantity === \"\";\n\n  const errorMsg = window.document.getElementById(\"error-msg\");\n  if (itemNameIsEmpty) {\n    errorMsg.innerHTML = \"\";\n  } else if (itemNameIsInvalid) {\n    errorMsg.innerHTML = `${itemName} is not a valid item.`;\n  } else {\n    errorMsg.innerHTML = `${itemName} is valid!`;\n  }\n\n  const submitButton = document.querySelector(`button[type=\"submit\"]`);\n  if (itemNameIsEmpty || itemNameIsInvalid || quantityIsEmpty) {\n    submitButton.disabled = true;\n  } else {\n    submitButton.disabled = false;\n  }\n};\n\nconst handleUndo = () => {\n  if (history.state === null) return;\n  history.back();\n};\n\nconst handlePopstate = () => {\n  data.inventory = history.state ? history.state.inventory : {};\n  updateItemList(data.inventory);\n};\n\nmodule.exports = {\n  updateItemList,\n  handleAddItem,\n  checkFormValues,\n  handleUndo,\n  handlePopstate\n};\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/1_http_requests/domController.test.js",
    "content": "const nock = require(\"nock\");\nconst fs = require(\"fs\");\nconst initialHtml = fs.readFileSync(\"./index.html\");\nconst { getByText, screen } = require(\"@testing-library/dom\");\n\nconst {\n  updateItemList,\n  handleAddItem,\n  checkFormValues,\n  handleUndo,\n  handlePopstate\n} = require(\"./domController\");\n\nconst { clearHistoryHook, detachPopstateHandlers } = require(\"./testUtils\");\n\nconst { API_ADDR, data } = require(\"./inventoryController\");\n\nbeforeEach(() => {\n  document.body.innerHTML = initialHtml;\n});\n\ndescribe(\"updateItemList\", () => {\n  beforeEach(() => localStorage.clear());\n\n  test(\"updates the DOM with the inventory items\", () => {\n    const inventory = {\n      cheesecake: 5,\n      \"apple pie\": 2,\n      \"carrot cake\": 6\n    };\n    updateItemList(inventory);\n\n    const itemList = document.getElementById(\"item-list\");\n    expect(itemList.childNodes).toHaveLength(3);\n\n    expect(getByText(itemList, \"cheesecake - Quantity: 5\")).toBeInTheDocument();\n    expect(getByText(itemList, \"apple pie - Quantity: 2\")).toBeInTheDocument();\n    expect(\n      getByText(itemList, \"carrot cake - Quantity: 6\")\n    ).toBeInTheDocument();\n  });\n\n  test(\"highlighting in red elements whose quantity is below five\", () => {\n    const inventory = { cheesecake: 5, \"apple pie\": 2, \"carrot cake\": 6 };\n    updateItemList(inventory);\n\n    expect(screen.getByText(\"apple pie - Quantity: 2\")).toHaveStyle({\n      color: \"red\"\n    });\n  });\n\n  test(\"adding a paragraph indicating what was the update\", () => {\n    const inventory = { cheesecake: 5, \"apple pie\": 2 };\n    updateItemList(inventory);\n\n    expect(\n      screen.getByText(\n        `The inventory has been updated - ${JSON.stringify(inventory)}`\n      )\n    ).toBeTruthy();\n  });\n\n  test(\"updates the localStorage with the inventory\", () => {\n    const inventory = { cheesecake: 5, \"apple pie\": 2 };\n    updateItemList(inventory);\n\n    expect(localStorage.getItem(\"inventory\")).toEqual(\n      JSON.stringify(inventory)\n    );\n  });\n\n  test(\"does not update the inventory when passing null\", () => {\n    localStorage.setItem(\"inventory\", JSON.stringify({ cheesecake: 5 }));\n    updateItemList(null);\n\n    expect(localStorage.getItem(\"inventory\")).toEqual(\n      JSON.stringify({ cheesecake: 5 })\n    );\n  });\n});\n\ndescribe(\"handleAddItem\", () => {\n  beforeEach(() => (data.inventory = {}));\n\n  test(\"adding items to the page\", () => {\n    nock(API_ADDR)\n      .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 6 }))\n      .reply(200);\n\n    const event = {\n      preventDefault: jest.fn(),\n      target: {\n        elements: {\n          name: { value: \"cheesecake\" },\n          quantity: { value: \"6\" }\n        }\n      }\n    };\n\n    handleAddItem(event);\n\n    // Checking if the form's default reload is prevent\n    expect(event.preventDefault.mock.calls).toHaveLength(1);\n\n    const itemList = document.getElementById(\"item-list\");\n    expect(getByText(itemList, \"cheesecake - Quantity: 6\")).toBeInTheDocument();\n\n    if (!nock.isDone())\n      throw new Error(\"POST /inventory/cheesecake was not reached\");\n  });\n\n  test(\"updating the application's history\", () => {\n    nock(API_ADDR)\n      .post(/inventory\\/.*$/)\n      .reply(200);\n\n    const event = {\n      preventDefault: jest.fn(),\n      target: {\n        elements: {\n          name: { value: \"cheesecake\" },\n          quantity: { value: \"6\" }\n        }\n      }\n    };\n\n    handleAddItem(event);\n\n    expect(history.state).toEqual({ inventory: { cheesecake: 6 } });\n  });\n});\n\ndescribe(\"checkFormValues\", () => {\n  test(\"entering valid item values\", () => {\n    document.querySelector(`input[name=\"name\"]`).value = \"cheesecake\";\n    document.querySelector(`input[name=\"quantity\"]`).value = \"1\";\n    checkFormValues();\n    expect(screen.getByText(\"Add to inventory\")).toBeEnabled();\n  });\n\n  test(\"entering invalid item names\", () => {\n    document.querySelector(`input[name=\"name\"]`).value = \"invalid\";\n    document.querySelector(`input[name=\"quantity\"]`).value = \"1\";\n    checkFormValues();\n    expect(screen.getByText(\"Add to inventory\")).toBeDisabled();\n\n    document.querySelector(`input[name=\"name\"]`).value = \"cheesecake\";\n    document.querySelector(`input[name=\"quantity\"]`).value = \"\";\n    checkFormValues();\n    expect(screen.getByText(\"Add to inventory\")).toBeDisabled();\n  });\n});\n\ndescribe(\"tests with history\", () => {\n  beforeEach(() => jest.spyOn(window, \"addEventListener\"));\n\n  afterEach(detachPopstateHandlers);\n\n  beforeEach(clearHistoryHook);\n\n  describe(\"handleUndo\", () => {\n    test(\"going back from a non-initial state\", done => {\n      window.addEventListener(\"popstate\", () => {\n        expect(history.state).toEqual(null);\n        done();\n      });\n\n      history.pushState({ inventory: { cheesecake: 5 } }, \"title\");\n      handleUndo();\n    });\n\n    test(\"going back from an initial state\", () => {\n      jest.spyOn(history, \"back\");\n      handleUndo();\n\n      // This assertion doesn't care about whether\n      // a call to `history.back` would have finished,\n      // it only checks whether it's been called\n      expect(history.back.mock.calls).toHaveLength(0);\n    });\n  });\n\n  describe(\"handlePopstate\", () => {\n    test(\"updating the item list with the current state\", () => {\n      history.pushState(\n        { inventory: { cheesecake: 5, \"carrot cake\": 2 } },\n        \"title\"\n      );\n\n      handlePopstate();\n\n      const itemList = document.getElementById(\"item-list\");\n      expect(itemList.childNodes).toHaveLength(2);\n      expect(\n        getByText(itemList, \"cheesecake - Quantity: 5\")\n      ).toBeInTheDocument();\n      expect(\n        getByText(itemList, \"carrot cake - Quantity: 2\")\n      ).toBeInTheDocument();\n    });\n  });\n});\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/1_http_requests/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Inventory Manager</title>\n    <style>\n      .almost-soldout {\n        color: red;\n      }\n    </style>\n  </head>\n  <body>\n    <h1 data-testid=\"page-header\">Inventory Contents</h1>\n    <ul id=\"item-list\"></ul>\n    <p id=\"error-msg\"></p>\n    <form id=\"add-item-form\">\n      <input type=\"text\" name=\"name\" placeholder=\"Item name\" />\n      <input type=\"number\" name=\"quantity\" placeholder=\"Quantity\" />\n      <button type=\"submit\">Add to inventory</button>\n    </form>\n\n    <button id=\"undo-button\">Undo</button>\n\n    <script src=\"bundle.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/1_http_requests/inventoryController.js",
    "content": "const data = { inventory: {} };\n\nconst API_ADDR = \"http://localhost:3000\";\n\nconst addItem = (itemName, quantity) => {\n  const currentQuantity = data.inventory[itemName] || 0;\n  data.inventory[itemName] = currentQuantity + quantity;\n\n  fetch(`${API_ADDR}/inventory/${itemName}`, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ quantity })\n  });\n\n  return data.inventory;\n};\n\nmodule.exports = { API_ADDR, data, addItem };\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/1_http_requests/inventoryController.test.js",
    "content": "const nock = require(\"nock\");\nconst { API_ADDR, addItem, data } = require(\"./inventoryController\");\n\ndescribe(\"addItem\", () => {\n  test(\"adding new items to the inventory\", () => {\n    // Respond to all post requests\n    // to POST /inventory/:itemName\n    nock(API_ADDR)\n      .post(/inventory\\/.*$/)\n      .reply(200);\n\n    addItem(\"cheesecake\", 5);\n    expect(data.inventory.cheesecake).toBe(5);\n  });\n\n  test(\"sending requests when adding new items\", () => {\n    nock(API_ADDR)\n      .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 5 }))\n      .reply(200);\n\n    addItem(\"cheesecake\", 5);\n\n    if (!nock.isDone())\n      throw new Error(\"POST /inventory/cheesecake was not reached\");\n  });\n});\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/1_http_requests/jest.config.js",
    "content": "module.exports = {\n  setupFilesAfterEnv: [\n    \"<rootDir>/setupGlobalFetch.js\",\n    \"<rootDir>/setupJestDom.js\"\n  ]\n};\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/1_http_requests/main.js",
    "content": "const {\n  handleAddItem,\n  checkFormValues,\n  handleUndo,\n  handlePopstate,\n  updateItemList\n} = require(\"./domController\");\n\nconst { API_ADDR, data } = require(\"./inventoryController\");\n\nconst form = document.getElementById(\"add-item-form\");\nform.addEventListener(\"submit\", handleAddItem);\nform.addEventListener(\"input\", checkFormValues);\n\nconst undoButton = document.getElementById(\"undo-button\");\nundoButton.addEventListener(\"click\", handleUndo);\n\nwindow.addEventListener(\"popstate\", handlePopstate);\n\n// Run `checkFormValues` once to see if the initial state is valid\ncheckFormValues();\n\nconst loadInitialData = async () => {\n  try {\n    const inventoryResponse = await fetch(`${API_ADDR}/inventory`);\n    data.inventory = await inventoryResponse.json();\n    return updateItemList(data.inventory);\n  } catch (e) {\n    // Restore the inventory if the request fails\n    const storedInventory = JSON.parse(localStorage.getItem(\"inventory\"));\n\n    if (storedInventory) {\n      data.inventory = storedInventory;\n      updateItemList(data.inventory);\n    }\n  }\n};\n\nmodule.exports = loadInitialData();\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/1_http_requests/main.test.js",
    "content": "const nock = require(\"nock\");\nconst fs = require(\"fs\");\nconst initialHtml = fs.readFileSync(\"./index.html\");\nconst { screen, getByText, fireEvent } = require(\"@testing-library/dom\");\nconst { API_ADDR } = require(\"./inventoryController\");\n\nconst { clearHistoryHook, detachPopstateHandlers } = require(\"./testUtils.js\");\n\nbeforeEach(clearHistoryHook);\n\nbeforeEach(() => localStorage.clear());\n\nbeforeEach(async () => {\n  document.body.innerHTML = initialHtml;\n\n  // You must execute main.js again so that it can attach the\n  // event listener to the form every time the body changes.\n  // Here you must use `jest.resetModules` because otherwise\n  // Jest will have cached `main.js` and it will _not_ run again.\n  jest.resetModules();\n\n  nock(API_ADDR)\n    .get(\"/inventory\")\n    .replyWithError({ code: 500 });\n  await require(\"./main\");\n\n  // You can only spy on `window.addEventListener` after `main.js`\n  // has been executed. Otherwise `detachPopstateHandlers` will\n  // also detach the handlers that `main.js` attached to the page.\n  jest.spyOn(window, \"addEventListener\");\n});\n\nafterEach(detachPopstateHandlers);\n\nafterEach(() => {\n  if (!nock.isDone()) {\n    nock.cleanAll();\n    throw new Error(\"Not all mocked endpoints received requests.\");\n  }\n});\n\ntest(\"persists items between sessions\", async () => {\n  nock(API_ADDR)\n    .post(/inventory\\/.*$/)\n    .reply(200);\n\n  nock(API_ADDR)\n    .get(\"/inventory\")\n    .replyWithError({ code: 500 });\n\n  const itemField = screen.getByPlaceholderText(\"Item name\");\n  const submitBtn = screen.getByText(\"Add to inventory\");\n  fireEvent.input(itemField, {\n    target: { value: \"cheesecake\" },\n    bubbles: true\n  });\n\n  const quantityField = screen.getByPlaceholderText(\"Quantity\");\n  fireEvent.input(quantityField, { target: { value: \"6\" }, bubbles: true });\n\n  fireEvent.click(submitBtn);\n\n  const itemListBefore = document.getElementById(\"item-list\");\n  expect(itemListBefore.childNodes).toHaveLength(1);\n  expect(\n    getByText(itemListBefore, \"cheesecake - Quantity: 6\")\n  ).toBeInTheDocument();\n\n  // This is equivalent to reloading the page\n  document.body.innerHTML = initialHtml;\n  jest.resetModules();\n\n  await require(\"./main\");\n\n  const itemListAfter = document.getElementById(\"item-list\");\n  expect(itemListAfter.childNodes).toHaveLength(1);\n  expect(\n    getByText(itemListAfter, \"cheesecake - Quantity: 6\")\n  ).toBeInTheDocument();\n});\n\ndescribe(\"adding items\", () => {\n  test(\"updating the item list\", () => {\n    nock(API_ADDR)\n      .post(/inventory\\/.*$/)\n      .reply(200);\n\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n    const submitBtn = screen.getByText(\"Add to inventory\");\n    fireEvent.input(itemField, {\n      target: { value: \"cheesecake\" },\n      bubbles: true\n    });\n\n    const quantityField = screen.getByPlaceholderText(\"Quantity\");\n    fireEvent.input(quantityField, { target: { value: \"6\" }, bubbles: true });\n\n    fireEvent.click(submitBtn);\n\n    const itemList = document.getElementById(\"item-list\");\n    expect(getByText(itemList, \"cheesecake - Quantity: 6\")).toBeInTheDocument();\n  });\n\n  test(\"sending a request to update the item list\", () => {\n    nock(API_ADDR)\n      .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 6 }))\n      .reply(200);\n\n    const submitBtn = screen.getByText(\"Add to inventory\");\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n    fireEvent.input(itemField, {\n      target: { value: \"cheesecake\" },\n      bubbles: true\n    });\n\n    const quantityField = screen.getByPlaceholderText(\"Quantity\");\n    fireEvent.input(quantityField, { target: { value: \"6\" }, bubbles: true });\n\n    fireEvent.click(submitBtn);\n\n    if (!nock.isDone())\n      throw new Error(\"POST /inventory/cheesecake was not reached\");\n  });\n\n  test(\"undo to one item\", done => {\n    // You must specify the encoded URL here because\n    // nock struggles with encoded urls\n    nock(API_ADDR)\n      .post(\"/inventory/carrot%20cake\")\n      .reply(200);\n\n    nock(API_ADDR)\n      .post(\"/inventory/cheesecake\")\n      .reply(200);\n\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n    const quantityField = screen.getByPlaceholderText(\"Quantity\");\n    const submitBtn = screen.getByText(\"Add to inventory\");\n\n    fireEvent.input(itemField, {\n      target: { value: \"cheesecake\" },\n      bubbles: true\n    });\n    fireEvent.input(quantityField, { target: { value: \"6\" }, bubbles: true });\n    fireEvent.click(submitBtn);\n\n    fireEvent.input(itemField, {\n      target: { value: \"carrot cake\" },\n      bubbles: true\n    });\n    fireEvent.input(quantityField, { target: { value: \"5\" }, bubbles: true });\n    fireEvent.click(submitBtn);\n\n    window.addEventListener(\"popstate\", () => {\n      const itemList = document.getElementById(\"item-list\");\n      expect(itemList.children).toHaveLength(1);\n      expect(\n        getByText(itemList, \"cheesecake - Quantity: 6\")\n      ).toBeInTheDocument();\n      done();\n    });\n\n    fireEvent.click(screen.getByText(\"Undo\"));\n  });\n\n  test(\"undo to empty list\", done => {\n    nock(API_ADDR)\n      .post(/inventory\\/.*$/)\n      .reply(200);\n\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n    const submitBtn = screen.getByText(\"Add to inventory\");\n    fireEvent.input(itemField, {\n      target: { value: \"cheesecake\" },\n      bubbles: true\n    });\n\n    const quantityField = screen.getByPlaceholderText(\"Quantity\");\n    fireEvent.input(quantityField, { target: { value: \"6\" }, bubbles: true });\n\n    fireEvent.click(submitBtn);\n\n    expect(history.state).toEqual({ inventory: { cheesecake: 6 } });\n\n    window.addEventListener(\"popstate\", () => {\n      const itemList = document.getElementById(\"item-list\");\n      expect(itemList).toBeEmpty();\n      done();\n    });\n\n    fireEvent.click(screen.getByText(\"Undo\"));\n  });\n});\n\ndescribe(\"item name validation\", () => {\n  test(\"entering valid item names \", () => {\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n\n    fireEvent.input(itemField, {\n      target: { value: \"cheesecake\" },\n      bubbles: true\n    });\n\n    expect(screen.getByText(\"cheesecake is valid!\")).toBeInTheDocument();\n  });\n\n  test(\"entering invalid item names \", () => {\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n\n    fireEvent.input(itemField, { target: { value: \"book\" }, bubbles: true });\n\n    expect(screen.getByText(\"book is not a valid item.\")).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/1_http_requests/package.json",
    "content": "{\n  \"name\": \"1_http_requests\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"start\": \"http-server ./\",\n    \"test\": \"jest\",\n    \"build\": \"browserify main.js -o bundle.js\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"@testing-library/dom\": \"^7.2.2\",\n    \"@testing-library/jest-dom\": \"^5.5.0\",\n    \"browserify\": \"^16.5.1\",\n    \"http-server\": \"^0.12.1\",\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"jest\": \"^24.9.0\",\n    \"nock\": \"^12.0.3\"\n  }\n}\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/1_http_requests/setupGlobalFetch.js",
    "content": "const fetch = require(\"isomorphic-fetch\");\n\nglobal.window.fetch = fetch;\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/1_http_requests/setupJestDom.js",
    "content": "const jestDom = require(\"@testing-library/jest-dom\");\n\nexpect.extend(jestDom);\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/1_http_requests/testUtils.js",
    "content": "const clearHistoryHook = done => {\n  const clearHistory = () => {\n    if (history.state === null) {\n      window.removeEventListener(\"popstate\", clearHistory);\n      return done();\n    }\n\n    history.back();\n  };\n\n  window.addEventListener(\"popstate\", clearHistory);\n\n  clearHistory();\n};\n\nconst detachPopstateHandlers = () => {\n  const popstateListeners = window.addEventListener.mock.calls.filter(\n    ([eventName]) => {\n      return eventName === \"popstate\";\n    }\n  );\n\n  popstateListeners.forEach(([eventName, handlerFn]) => {\n    window.removeEventListener(eventName, handlerFn);\n  });\n\n  jest.restoreAllMocks();\n};\n\nmodule.exports = { clearHistoryHook, detachPopstateHandlers };\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/2_web_sockets/domController.js",
    "content": "const { addItem, data } = require(\"./inventoryController\");\n\nconst updateItemList = inventory => {\n  if (inventory === null) return;\n\n  localStorage.setItem(\"inventory\", JSON.stringify(inventory));\n\n  const inventoryList = window.document.getElementById(\"item-list\");\n\n  // Clears the list\n  inventoryList.innerHTML = \"\";\n\n  Object.entries(inventory).forEach(([itemName, quantity]) => {\n    const listItem = window.document.createElement(\"li\");\n    listItem.innerHTML = `${itemName} - Quantity: ${quantity}`;\n\n    if (quantity < 5) {\n      listItem.className = \"almost-soldout\";\n    }\n\n    inventoryList.appendChild(listItem);\n  });\n\n  const inventoryContents = JSON.stringify(inventory);\n  const p = window.document.createElement(\"p\");\n  p.innerHTML = `The inventory has been updated - ${inventoryContents}`;\n\n  window.document.body.appendChild(p);\n};\n\nconst handleAddItem = event => {\n  // Prevent the page from reloading as it would by default\n  event.preventDefault();\n\n  const { name, quantity } = event.target.elements;\n  addItem(name.value, parseInt(quantity.value, 10));\n\n  history.pushState({ inventory: { ...data.inventory } }, document.title);\n\n  updateItemList(data.inventory);\n};\n\nconst validItems = [\"cheesecake\", \"apple pie\", \"carrot cake\"];\nconst checkFormValues = () => {\n  const itemName = document.querySelector(`input[name=\"name\"]`).value;\n  const quantity = document.querySelector(`input[name=\"quantity\"]`).value;\n\n  const itemNameIsEmpty = itemName === \"\";\n  const itemNameIsInvalid = !validItems.includes(itemName);\n  const quantityIsEmpty = quantity === \"\";\n\n  const errorMsg = window.document.getElementById(\"error-msg\");\n  if (itemNameIsEmpty) {\n    errorMsg.innerHTML = \"\";\n  } else if (itemNameIsInvalid) {\n    errorMsg.innerHTML = `${itemName} is not a valid item.`;\n  } else {\n    errorMsg.innerHTML = `${itemName} is valid!`;\n  }\n\n  const submitButton = document.querySelector(`button[type=\"submit\"]`);\n  if (itemNameIsEmpty || itemNameIsInvalid || quantityIsEmpty) {\n    submitButton.disabled = true;\n  } else {\n    submitButton.disabled = false;\n  }\n};\n\nconst handleUndo = () => {\n  if (history.state === null) return;\n  history.back();\n};\n\nconst handlePopstate = () => {\n  data.inventory = history.state ? history.state.inventory : {};\n  updateItemList(data.inventory);\n};\n\nmodule.exports = {\n  updateItemList,\n  handleAddItem,\n  checkFormValues,\n  handleUndo,\n  handlePopstate\n};\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/2_web_sockets/domController.test.js",
    "content": "const nock = require(\"nock\");\nconst fs = require(\"fs\");\nconst initialHtml = fs.readFileSync(\"./index.html\");\nconst { getByText, screen } = require(\"@testing-library/dom\");\n\nconst {\n  updateItemList,\n  handleAddItem,\n  checkFormValues,\n  handleUndo,\n  handlePopstate\n} = require(\"./domController\");\n\nconst { clearHistoryHook, detachPopstateHandlers } = require(\"./testUtils\");\n\nconst { API_ADDR, data } = require(\"./inventoryController\");\n\nbeforeEach(() => {\n  document.body.innerHTML = initialHtml;\n});\n\ndescribe(\"updateItemList\", () => {\n  beforeEach(() => localStorage.clear());\n\n  test(\"updates the DOM with the inventory items\", () => {\n    const inventory = {\n      cheesecake: 5,\n      \"apple pie\": 2,\n      \"carrot cake\": 6\n    };\n    updateItemList(inventory);\n\n    const itemList = document.getElementById(\"item-list\");\n    expect(itemList.childNodes).toHaveLength(3);\n\n    expect(getByText(itemList, \"cheesecake - Quantity: 5\")).toBeInTheDocument();\n    expect(getByText(itemList, \"apple pie - Quantity: 2\")).toBeInTheDocument();\n    expect(\n      getByText(itemList, \"carrot cake - Quantity: 6\")\n    ).toBeInTheDocument();\n  });\n\n  test(\"highlighting in red elements whose quantity is below five\", () => {\n    const inventory = { cheesecake: 5, \"apple pie\": 2, \"carrot cake\": 6 };\n    updateItemList(inventory);\n\n    expect(screen.getByText(\"apple pie - Quantity: 2\")).toHaveStyle({\n      color: \"red\"\n    });\n  });\n\n  test(\"adding a paragraph indicating what was the update\", () => {\n    const inventory = { cheesecake: 5, \"apple pie\": 2 };\n    updateItemList(inventory);\n\n    expect(\n      screen.getByText(\n        `The inventory has been updated - ${JSON.stringify(inventory)}`\n      )\n    ).toBeTruthy();\n  });\n\n  test(\"updates the localStorage with the inventory\", () => {\n    const inventory = { cheesecake: 5, \"apple pie\": 2 };\n    updateItemList(inventory);\n\n    expect(localStorage.getItem(\"inventory\")).toEqual(\n      JSON.stringify(inventory)\n    );\n  });\n\n  test(\"does not update the inventory when passing null\", () => {\n    localStorage.setItem(\"inventory\", JSON.stringify({ cheesecake: 5 }));\n    updateItemList(null);\n\n    expect(localStorage.getItem(\"inventory\")).toEqual(\n      JSON.stringify({ cheesecake: 5 })\n    );\n  });\n});\n\ndescribe(\"handleAddItem\", () => {\n  beforeEach(() => (data.inventory = {}));\n\n  test(\"adding items to the page\", () => {\n    nock(API_ADDR)\n      .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 6 }))\n      .reply(200);\n\n    const event = {\n      preventDefault: jest.fn(),\n      target: {\n        elements: {\n          name: { value: \"cheesecake\" },\n          quantity: { value: \"6\" }\n        }\n      }\n    };\n\n    handleAddItem(event);\n\n    // Checking if the form's default reload is prevent\n    expect(event.preventDefault.mock.calls).toHaveLength(1);\n\n    const itemList = document.getElementById(\"item-list\");\n    expect(getByText(itemList, \"cheesecake - Quantity: 6\")).toBeInTheDocument();\n\n    if (!nock.isDone())\n      throw new Error(\"POST /inventory/cheesecake was not reached\");\n  });\n\n  test(\"updating the application's history\", () => {\n    nock(API_ADDR)\n      .post(/inventory\\/.*$/)\n      .reply(200);\n\n    const event = {\n      preventDefault: jest.fn(),\n      target: {\n        elements: {\n          name: { value: \"cheesecake\" },\n          quantity: { value: \"6\" }\n        }\n      }\n    };\n\n    handleAddItem(event);\n\n    expect(history.state).toEqual({ inventory: { cheesecake: 6 } });\n  });\n});\n\ndescribe(\"checkFormValues\", () => {\n  test(\"entering valid item values\", () => {\n    document.querySelector(`input[name=\"name\"]`).value = \"cheesecake\";\n    document.querySelector(`input[name=\"quantity\"]`).value = \"1\";\n    checkFormValues();\n    expect(screen.getByText(\"Add to inventory\")).toBeEnabled();\n  });\n\n  test(\"entering invalid item names\", () => {\n    document.querySelector(`input[name=\"name\"]`).value = \"invalid\";\n    document.querySelector(`input[name=\"quantity\"]`).value = \"1\";\n    checkFormValues();\n    expect(screen.getByText(\"Add to inventory\")).toBeDisabled();\n\n    document.querySelector(`input[name=\"name\"]`).value = \"cheesecake\";\n    document.querySelector(`input[name=\"quantity\"]`).value = \"\";\n    checkFormValues();\n    expect(screen.getByText(\"Add to inventory\")).toBeDisabled();\n  });\n});\n\ndescribe(\"tests with history\", () => {\n  beforeEach(() => jest.spyOn(window, \"addEventListener\"));\n\n  afterEach(detachPopstateHandlers);\n\n  beforeEach(clearHistoryHook);\n\n  describe(\"handleUndo\", () => {\n    test(\"going back from a non-initial state\", done => {\n      window.addEventListener(\"popstate\", () => {\n        expect(history.state).toEqual(null);\n        done();\n      });\n\n      history.pushState({ inventory: { cheesecake: 5 } }, \"title\");\n      handleUndo();\n    });\n\n    test(\"going back from an initial state\", () => {\n      jest.spyOn(history, \"back\");\n      handleUndo();\n\n      // This assertion doesn't care about whether\n      // a call to `history.back` would have finished,\n      // it only checks whether it's been called\n      expect(history.back.mock.calls).toHaveLength(0);\n    });\n  });\n\n  describe(\"handlePopstate\", () => {\n    test(\"updating the item list with the current state\", () => {\n      history.pushState(\n        { inventory: { cheesecake: 5, \"carrot cake\": 2 } },\n        \"title\"\n      );\n\n      handlePopstate();\n\n      const itemList = document.getElementById(\"item-list\");\n      expect(itemList.childNodes).toHaveLength(2);\n      expect(\n        getByText(itemList, \"cheesecake - Quantity: 5\")\n      ).toBeInTheDocument();\n      expect(\n        getByText(itemList, \"carrot cake - Quantity: 2\")\n      ).toBeInTheDocument();\n    });\n  });\n});\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/2_web_sockets/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Inventory Manager</title>\n    <style>\n      .almost-soldout {\n        color: red;\n      }\n    </style>\n  </head>\n  <body>\n    <h1 data-testid=\"page-header\">Inventory Contents</h1>\n    <ul id=\"item-list\"></ul>\n    <p id=\"error-msg\"></p>\n    <form id=\"add-item-form\">\n      <input type=\"text\" name=\"name\" placeholder=\"Item name\" />\n      <input type=\"number\" name=\"quantity\" placeholder=\"Quantity\" />\n      <button type=\"submit\">Add to inventory</button>\n    </form>\n\n    <button id=\"undo-button\">Undo</button>\n\n    <script src=\"bundle.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/2_web_sockets/inventoryController.js",
    "content": "const data = { inventory: {} };\n\nconst API_ADDR = \"http://localhost:3000\";\n\nconst addItem = (itemName, quantity) => {\n  const { client } = require(\"./socket\");\n  const currentQuantity = data.inventory[itemName] || 0;\n  data.inventory[itemName] = currentQuantity + quantity;\n\n  fetch(`${API_ADDR}/inventory/${itemName}`, {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n      \"x-socket-client-id\": client.id\n    },\n    body: JSON.stringify({ quantity })\n  });\n\n  return data.inventory;\n};\n\nmodule.exports = { API_ADDR, data, addItem };\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/2_web_sockets/inventoryController.test.js",
    "content": "const nock = require(\"nock\");\nconst { API_ADDR, addItem, data } = require(\"./inventoryController\");\nconst { start, stop } = require(\"./testSocketServer\");\nconst { client, connect } = require(\"./socket\");\n\nafterEach(() => {\n  if (!nock.isDone()) {\n    nock.cleanAll();\n    throw new Error(\"Not all mocked endpoints received requests.\");\n  }\n});\n\ndescribe(\"addItem\", () => {\n  test(\"adding new items to the inventory\", () => {\n    // Respond to all post requests\n    // to POST /inventory/:itemName\n    nock(API_ADDR)\n      .post(/inventory\\/.*$/)\n      .reply(200);\n\n    addItem(\"cheesecake\", 5);\n    expect(data.inventory.cheesecake).toBe(5);\n  });\n\n  test(\"sending requests when adding new items\", () => {\n    nock(API_ADDR)\n      .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 5 }))\n      .reply(200);\n\n    addItem(\"cheesecake\", 5);\n  });\n\n  describe(\"live-updates\", () => {\n    beforeAll(start);\n\n    beforeAll(async () => {\n      nock.cleanAll();\n      await connect();\n    });\n\n    afterAll(stop);\n\n    test(\"sending a x-socket-client-id header\", () => {\n      const clientId = client.id;\n\n      nock(API_ADDR, { reqheaders: { \"x-socket-client-id\": clientId } })\n        .post(/inventory\\/.*$/)\n        .reply(200);\n\n      addItem(\"cheesecake\", 5);\n    });\n  });\n});\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/2_web_sockets/jest.config.js",
    "content": "module.exports = {\n  setupFilesAfterEnv: [\n    \"<rootDir>/setupGlobalFetch.js\",\n    \"<rootDir>/setupJestDom.js\"\n  ]\n};\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/2_web_sockets/main.js",
    "content": "const { connect } = require(\"./socket\");\n\nconst {\n  handleAddItem,\n  checkFormValues,\n  handleUndo,\n  handlePopstate,\n  updateItemList\n} = require(\"./domController\");\n\nconst { API_ADDR, data } = require(\"./inventoryController\");\n\nconst form = document.getElementById(\"add-item-form\");\nform.addEventListener(\"submit\", handleAddItem);\nform.addEventListener(\"input\", checkFormValues);\n\nconst undoButton = document.getElementById(\"undo-button\");\nundoButton.addEventListener(\"click\", handleUndo);\n\nwindow.addEventListener(\"popstate\", handlePopstate);\n\n// Run `checkFormValues` once to see if the initial state is valid\ncheckFormValues();\n\nconst loadInitialData = async () => {\n  try {\n    const inventoryResponse = await fetch(`${API_ADDR}/inventory`);\n    data.inventory = await inventoryResponse.json();\n    return updateItemList(data.inventory);\n  } catch (e) {\n    // Restore the inventory if the request fails\n    const storedInventory = JSON.parse(localStorage.getItem(\"inventory\"));\n\n    if (storedInventory) {\n      data.inventory = storedInventory;\n      updateItemList(data.inventory);\n    }\n  }\n};\n\nconnect();\n\nmodule.exports = loadInitialData();\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/2_web_sockets/main.test.js",
    "content": "const nock = require(\"nock\");\nconst fs = require(\"fs\");\nconst initialHtml = fs.readFileSync(\"./index.html\");\nconst { screen, getByText, fireEvent } = require(\"@testing-library/dom\");\nconst { API_ADDR } = require(\"./inventoryController\");\n\nconst { clearHistoryHook, detachPopstateHandlers } = require(\"./testUtils.js\");\n\nbeforeEach(clearHistoryHook);\n\nbeforeEach(() => localStorage.clear());\n\nbeforeEach(async () => {\n  document.body.innerHTML = initialHtml;\n\n  // You must execute main.js again so that it can attach the\n  // event listener to the form every time the body changes.\n  // Here you must use `jest.resetModules` because otherwise\n  // Jest will have cached `main.js` and it will _not_ run again.\n  jest.resetModules();\n\n  nock(API_ADDR)\n    .get(\"/inventory\")\n    .replyWithError({ code: 500 });\n  await require(\"./main\");\n\n  // You can only spy on `window.addEventListener` after `main.js`\n  // has been executed. Otherwise `detachPopstateHandlers` will\n  // also detach the handlers that `main.js` attached to the page.\n  jest.spyOn(window, \"addEventListener\");\n});\n\nafterEach(detachPopstateHandlers);\n\nafterEach(() => {\n  if (!nock.isDone()) {\n    nock.cleanAll();\n    throw new Error(\"Not all mocked endpoints received requests.\");\n  }\n});\n\ntest(\"persists items between sessions\", async () => {\n  nock(API_ADDR)\n    .post(/inventory\\/.*$/)\n    .reply(200);\n\n  nock(API_ADDR)\n    .get(\"/inventory\")\n    .replyWithError({ code: 500 });\n\n  const submitBtn = screen.getByText(\"Add to inventory\");\n  const itemField = screen.getByPlaceholderText(\"Item name\");\n  fireEvent.input(itemField, {\n    target: { value: \"cheesecake\" },\n    bubbles: true\n  });\n\n  const quantityField = screen.getByPlaceholderText(\"Quantity\");\n  fireEvent.input(quantityField, { target: { value: \"6\" }, bubbles: true });\n\n  fireEvent.click(submitBtn);\n\n  const itemListBefore = document.getElementById(\"item-list\");\n  expect(itemListBefore.childNodes).toHaveLength(1);\n  expect(\n    getByText(itemListBefore, \"cheesecake - Quantity: 6\")\n  ).toBeInTheDocument();\n\n  // This is equivalent to reloading the page\n  document.body.innerHTML = initialHtml;\n  jest.resetModules();\n\n  await require(\"./main\");\n\n  const itemListAfter = document.getElementById(\"item-list\");\n  expect(itemListAfter.childNodes).toHaveLength(1);\n  expect(\n    getByText(itemListAfter, \"cheesecake - Quantity: 6\")\n  ).toBeInTheDocument();\n});\n\ndescribe(\"adding items\", () => {\n  test(\"updating the item list\", () => {\n    nock(API_ADDR)\n      .post(/inventory\\/.*$/)\n      .reply(200);\n\n    const submitBtn = screen.getByText(\"Add to inventory\");\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n    fireEvent.input(itemField, {\n      target: { value: \"cheesecake\" },\n      bubbles: true\n    });\n\n    const quantityField = screen.getByPlaceholderText(\"Quantity\");\n    fireEvent.input(quantityField, { target: { value: \"6\" }, bubbles: true });\n\n    fireEvent.click(submitBtn);\n\n    const itemList = document.getElementById(\"item-list\");\n    expect(getByText(itemList, \"cheesecake - Quantity: 6\")).toBeInTheDocument();\n  });\n\n  test(\"sending a request to update the item list\", () => {\n    nock(API_ADDR)\n      .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 6 }))\n      .reply(200);\n\n    const submitBtn = screen.getByText(\"Add to inventory\");\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n    fireEvent.input(itemField, {\n      target: { value: \"cheesecake\" },\n      bubbles: true\n    });\n\n    const quantityField = screen.getByPlaceholderText(\"Quantity\");\n    fireEvent.input(quantityField, { target: { value: \"6\" }, bubbles: true });\n\n    fireEvent.click(submitBtn);\n\n    if (!nock.isDone())\n      throw new Error(\"POST /inventory/cheesecake was not reached\");\n  });\n\n  test(\"undo to one item\", done => {\n    // You must specify the encoded URL here because\n    // nock struggles with encoded urls\n    nock(API_ADDR)\n      .post(\"/inventory/carrot%20cake\")\n      .reply(200);\n\n    nock(API_ADDR)\n      .post(\"/inventory/cheesecake\")\n      .reply(200);\n\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n    const quantityField = screen.getByPlaceholderText(\"Quantity\");\n    const submitBtn = screen.getByText(\"Add to inventory\");\n\n    fireEvent.input(itemField, {\n      target: { value: \"cheesecake\" },\n      bubbles: true\n    });\n    fireEvent.input(quantityField, { target: { value: \"6\" }, bubbles: true });\n    fireEvent.click(submitBtn);\n\n    fireEvent.input(itemField, {\n      target: { value: \"carrot cake\" },\n      bubbles: true\n    });\n    fireEvent.input(quantityField, { target: { value: \"5\" }, bubbles: true });\n    fireEvent.click(submitBtn);\n\n    window.addEventListener(\"popstate\", () => {\n      const itemList = document.getElementById(\"item-list\");\n      expect(itemList.children).toHaveLength(1);\n      expect(\n        getByText(itemList, \"cheesecake - Quantity: 6\")\n      ).toBeInTheDocument();\n      done();\n    });\n\n    fireEvent.click(screen.getByText(\"Undo\"));\n  });\n\n  test(\"undo to empty list\", done => {\n    nock(API_ADDR)\n      .post(/inventory\\/.*$/)\n      .reply(200);\n\n    const submitBtn = screen.getByText(\"Add to inventory\");\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n    fireEvent.input(itemField, {\n      target: { value: \"cheesecake\" },\n      bubbles: true\n    });\n\n    const quantityField = screen.getByPlaceholderText(\"Quantity\");\n    fireEvent.input(quantityField, { target: { value: \"6\" }, bubbles: true });\n\n    fireEvent.click(submitBtn);\n\n    expect(history.state).toEqual({ inventory: { cheesecake: 6 } });\n\n    window.addEventListener(\"popstate\", () => {\n      const itemList = document.getElementById(\"item-list\");\n      expect(itemList).toBeEmpty();\n      done();\n    });\n\n    fireEvent.click(screen.getByText(\"Undo\"));\n  });\n});\n\ndescribe(\"item name validation\", () => {\n  test(\"entering valid item names \", () => {\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n\n    fireEvent.input(itemField, {\n      target: { value: \"cheesecake\" },\n      bubbles: true\n    });\n\n    expect(screen.getByText(\"cheesecake is valid!\")).toBeInTheDocument();\n  });\n\n  test(\"entering invalid item names \", () => {\n    const itemField = screen.getByPlaceholderText(\"Item name\");\n\n    fireEvent.input(itemField, { target: { value: \"book\" }, bubbles: true });\n\n    expect(screen.getByText(\"book is not a valid item.\")).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/2_web_sockets/package.json",
    "content": "{\n  \"name\": \"1_http_requests\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"start\": \"http-server ./\",\n    \"test\": \"jest\",\n    \"build\": \"browserify main.js -o bundle.js\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"@testing-library/dom\": \"^7.2.2\",\n    \"@testing-library/jest-dom\": \"^5.5.0\",\n    \"browserify\": \"^16.5.1\",\n    \"http-server\": \"^0.12.1\",\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"jest\": \"^24.9.0\",\n    \"nock\": \"^12.0.3\",\n    \"socket.io\": \"^2.3.0\"\n  },\n  \"dependencies\": {\n    \"http-shutdown\": \"^1.2.2\",\n    \"socket.io-client\": \"^2.3.0\"\n  }\n}\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/2_web_sockets/setupGlobalFetch.js",
    "content": "const fetch = require(\"isomorphic-fetch\");\n\nglobal.window.fetch = fetch;\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/2_web_sockets/setupJestDom.js",
    "content": "const jestDom = require(\"@testing-library/jest-dom\");\n\nexpect.extend(jestDom);\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/2_web_sockets/socket.js",
    "content": "const { API_ADDR, data } = require(\"./inventoryController\");\nconst { updateItemList } = require(\"./domController\");\n\nconst client = { id: null };\n\nconst io = require(\"socket.io-client\");\n\nconst handleAddItemMsg = ({ itemName, quantity }) => {\n  const currentQuantity = data.inventory[itemName] || 0;\n  data.inventory[itemName] = currentQuantity + quantity;\n  return updateItemList(data.inventory);\n};\n\nconst connect = () => {\n  return new Promise(resolve => {\n    const socket = io(API_ADDR);\n\n    socket.on(\"connect\", () => {\n      client.id = socket.id;\n      resolve(socket);\n    });\n\n    socket.on(\"add_item\", handleAddItemMsg);\n  });\n};\n\nmodule.exports = { client, connect, handleAddItemMsg };\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/2_web_sockets/socket.test.js",
    "content": "const nock = require(\"nock\");\nconst fs = require(\"fs\");\nconst initialHtml = fs.readFileSync(\"./index.html\");\nconst { getByText } = require(\"@testing-library/dom\");\nconst { data } = require(\"./inventoryController\");\nconst { start, stop, sendMsg } = require(\"./testSocketServer\");\n\nconst { handleAddItemMsg, connect } = require(\"./socket\");\n\nbeforeEach(() => {\n  document.body.innerHTML = initialHtml;\n});\n\nbeforeEach(() => {\n  data.inventory = {};\n});\n\ndescribe(\"handleAddItemMsg\", () => {\n  test(\"updating the inventory and the item list\", () => {\n    handleAddItemMsg({ itemName: \"cheesecake\", quantity: 6 });\n\n    expect(data.inventory).toEqual({ cheesecake: 6 });\n    const itemList = document.getElementById(\"item-list\");\n    expect(itemList.childNodes).toHaveLength(1);\n    expect(getByText(itemList, \"cheesecake - Quantity: 6\")).toBeInTheDocument();\n  });\n});\n\ndescribe(\"handling real messages\", () => {\n  beforeAll(start);\n\n  beforeAll(async () => {\n    nock.cleanAll();\n    await connect();\n  });\n\n  afterAll(stop);\n\n  test(\"handling add_item messages\", async () => {\n    sendMsg(\"add_item\", { itemName: \"cheesecake\", quantity: 6 });\n\n    await new Promise(resolve => setTimeout(resolve, 1000));\n\n    expect(data.inventory).toEqual({ cheesecake: 6 });\n    const itemList = document.getElementById(\"item-list\");\n    expect(itemList.childNodes).toHaveLength(1);\n    expect(getByText(itemList, \"cheesecake - Quantity: 6\")).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/2_web_sockets/testSocketServer.js",
    "content": "const server = require(\"http\").createServer();\nconst io = require(\"socket.io\")(server);\n\nconst sendMsg = (msgType, content) => {\n  io.sockets.emit(msgType, content);\n};\n\nconst start = () =>\n  new Promise(resolve => {\n    server.listen(3000, resolve);\n  });\n\nconst stop = () =>\n  new Promise(resolve => {\n    server.close(resolve);\n  });\n\nmodule.exports = { start, stop, sendMsg };\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/2_web_sockets/testUtils.js",
    "content": "const clearHistoryHook = done => {\n  const clearHistory = () => {\n    if (history.state === null) {\n      window.removeEventListener(\"popstate\", clearHistory);\n      return done();\n    }\n\n    history.back();\n  };\n\n  window.addEventListener(\"popstate\", clearHistory);\n\n  clearHistory();\n};\n\nconst detachPopstateHandlers = () => {\n  const popstateListeners = window.addEventListener.mock.calls.filter(\n    ([eventName]) => {\n      return eventName === \"popstate\";\n    }\n  );\n\n  popstateListeners.forEach(([eventName, handlerFn]) => {\n    window.removeEventListener(eventName, handlerFn);\n  });\n\n  jest.restoreAllMocks();\n};\n\nmodule.exports = { clearHistoryHook, detachPopstateHandlers };\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/server/README.md",
    "content": "# Chapter 5 Server\n\nTo better support the client-side application we'll build on Chapter 5, I've had to do a few updates to the server from Chapter 4.\n\nIn case you want to update the back-end from Chapter 4 yourself, here's the list of changes I've done:\n\n- For the server to accept the requests coming from the client, you'll need to use [`@koa/cors`](https://github.com/koajs/cors)\n- To enable running tests while the server is running, I bind it to different ports depending on whether I am in a test or development environment.\n- At `POST /inventory/:itemName` I have added a route which adds an item to the inventory. It takes a `body` containing the `quantity` to add.\n- At `GET /inventory` I have added a route which lists all items in the inventory.\n- At `DELETE /inventory/:itemName` I have added a route which let's you delete inventory items so that you can use to fix the `undo` functionality\n- I've used `koa-socket-2` to add support for `socket.io`\n- The `POST /inventory/:itemName` will now push updates to all clients but the one which added an item.\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/server/authenticationController.js",
    "content": "const crypto = require(\"crypto\");\nconst { db } = require(\"./dbConnection\");\n\nconst hashPassword = password => {\n  const hash = crypto.createHash(\"sha256\");\n  hash.update(password);\n  return hash.digest(\"hex\");\n};\n\nconst credentialsAreValid = async (username, password) => {\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  if (!user) return false;\n  return hashPassword(password) === user.passwordHash;\n};\n\nconst authenticationMiddleware = async (ctx, next) => {\n  try {\n    const authHeader = ctx.request.headers.authorization;\n    const credentials = Buffer.from(\n      authHeader.slice(\"basic\".length + 1),\n      \"base64\"\n    ).toString();\n    const [username, password] = credentials.split(\":\");\n\n    const validCredentialsSent = await credentialsAreValid(username, password);\n    if (!validCredentialsSent) throw new Error(\"invalid credentials\");\n  } catch (e) {\n    ctx.status = 401;\n    ctx.body = { message: \"please provide valid credentials\" };\n    return;\n  }\n\n  await next();\n};\n\nmodule.exports = {\n  hashPassword,\n  credentialsAreValid,\n  authenticationMiddleware\n};\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/server/authenticationController.test.js",
    "content": "const crypto = require(\"crypto\");\nconst {\n  hashPassword,\n  credentialsAreValid,\n  authenticationMiddleware\n} = require(\"./authenticationController\");\nconst { user: globalUser } = require(\"./userTestUtils\");\n\ndescribe(\"hashPassword\", () => {\n  test(\"hashing passwords\", () => {\n    const plainTextPassword = \"password_example\";\n    const hash = crypto.createHash(\"sha256\");\n    hash.update(plainTextPassword);\n    const expectedHash = hash.digest(\"hex\");\n    expect(hashPassword(plainTextPassword)).toBe(expectedHash);\n  });\n});\n\ndescribe(\"credentialsAreValid\", () => {\n  test(\"validating credentials\", async () => {\n    expect(await credentialsAreValid(globalUser.username, \"a_password\")).toBe(\n      true\n    );\n  });\n});\n\ndescribe(\"authenticationMiddleware\", () => {\n  test(\"returning an error if the credentials are not valid\", async () => {\n    const fakeAuth = Buffer.from(\"invalid:credentials\").toString(\"base64\");\n    const ctx = {\n      request: {\n        headers: { authorization: `Basic ${fakeAuth}` }\n      }\n    };\n\n    const next = jest.fn();\n    await authenticationMiddleware(ctx, next);\n    expect(next.mock.calls).toHaveLength(0);\n    expect(ctx).toEqual({\n      ...ctx,\n      status: 401,\n      body: { message: \"please provide valid credentials\" }\n    });\n  });\n\n  test(\"authenticating properly\", async () => {\n    const ctx = {\n      request: {\n        headers: { authorization: globalUser.authHeader }\n      }\n    };\n\n    const next = jest.fn();\n    await authenticationMiddleware(ctx, next);\n    expect(next.mock.calls).toHaveLength(1);\n  });\n});\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/server/cartController.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { removeFromInventory } = require(\"./inventoryController\");\nconst logger = require(\"./logger\");\n\nconst addItemToCart = async (username, itemName) => {\n  await removeFromInventory(itemName);\n\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  if (!user) {\n    const userNotFound = new Error(\"user not found\");\n    userNotFound.code = 404;\n  }\n\n  const itemEntry = await db\n    .select()\n    .from(\"carts_items\")\n    .where({ userId: user.id, itemName })\n    .first();\n\n  if (itemEntry && itemEntry.quantity + 1 > 3) {\n    const limitError = new Error(\n      \"You can't have more than three units of an item in your cart\"\n    );\n    limitError.code = 400;\n    throw limitError;\n  }\n\n  if (itemEntry) {\n    await db(\"carts_items\")\n      .increment(\"quantity\")\n      .update({ updatedAt: new Date().toISOString() })\n      .where({\n        userId: itemEntry.userId,\n        itemName\n      });\n  } else {\n    await db(\"carts_items\").insert({\n      userId: user.id,\n      itemName,\n      quantity: 1,\n      updatedAt: new Date().toISOString()\n    });\n  }\n\n  logger.log(`${itemName} added to ${username}'s cart`);\n  return db\n    .select(\"itemName\", \"quantity\")\n    .from(\"carts_items\")\n    .where({ userId: user.id });\n};\n\nconst hoursInMs = n => 1000 * 60 * 60 * n;\n\nconst removeStaleItems = async () => {\n  const fourHoursAgo = new Date(Date.now() - hoursInMs(4)).toISOString();\n\n  const staleItems = await db\n    .select()\n    .from(\"carts_items\")\n    .where(\"updatedAt\", \"<\", fourHoursAgo);\n\n  if (staleItems.length === 0) return;\n\n  // Put stale items back in the inventory\n  const inventoryUpdates = staleItems.map(staleItem =>\n    db(\"inventory\")\n      .increment(\"quantity\", staleItem.quantity)\n      .where({ itemName: staleItem.itemName })\n  );\n  await Promise.all(inventoryUpdates);\n\n  // Delete stale items from cart\n  const staleItemTuples = staleItems.map(i => [i.itemName, i.userId]);\n  await db(\"carts_items\")\n    .del()\n    .whereIn([\"itemName\", \"userId\"], staleItemTuples);\n};\n\nconst monitorStaleItems = () => setInterval(removeStaleItems, hoursInMs(2));\n\nmodule.exports = { addItemToCart, monitorStaleItems };\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/server/cartController.test.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { addItemToCart, monitorStaleItems } = require(\"./cartController\");\nconst { hashPassword } = require(\"./authenticationController\");\nconst { user: globalUser } = require(\"./userTestUtils\");\nconst FakeTimers = require(\"@sinonjs/fake-timers\");\n\nconst fs = require(\"fs\");\n\ndescribe(\"addItemToCart\", () => {\n  beforeEach(() => {\n    fs.writeFileSync(\"/tmp/logs.out\", \"\");\n  });\n\n  test(\"adding unavailable items to cart\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 0 });\n\n    try {\n      await addItemToCart(globalUser.username, \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(\"cheesecake is unavailable\");\n      expectedError.code = 400;\n\n      expect(e).toEqual(expectedError);\n    }\n\n    const finalCartContent = await db\n      .select(\"carts_items.*\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n\n    expect(finalCartContent).toEqual([]);\n    expect.assertions(2);\n  });\n\n  test(\"adding items above limit to cart\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 1 });\n    await db(\"carts_items\").insert({\n      userId: globalUser.id,\n      itemName: \"cheesecake\",\n      quantity: 3\n    });\n\n    try {\n      await addItemToCart(globalUser.username, \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(\n        \"You can't have more than three units of an item in your cart\"\n      );\n      expectedError.code = 400;\n      expect(e).toEqual(expectedError);\n    }\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n\n    expect(finalCartContent).toEqual([{ itemName: \"cheesecake\", quantity: 3 }]);\n    expect.assertions(2);\n  });\n\n  test(\"logging added items\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 1 });\n    await db(\"carts_items\").insert({\n      userId: globalUser.id,\n      itemName: \"cheesecake\",\n      quantity: 1\n    });\n\n    await addItemToCart(globalUser.username, \"cheesecake\");\n\n    const logs = fs.readFileSync(\"/tmp/logs.out\", \"utf-8\");\n    expect(logs).toContain(\n      `cheesecake added to ${globalUser.username}'s cart\\n`\n    );\n  });\n});\n\nconst withRetries = async fn => {\n  // Capture the assertion error since Jest does not export it\n  const JestAssertionError = (() => {\n    try {\n      expect(false).toBe(true);\n    } catch (e) {\n      return e.constructor;\n    }\n  })();\n\n  try {\n    await fn();\n  } catch (e) {\n    if (e.constructor === JestAssertionError) {\n      // Wait 100ms before retrying\n      await new Promise(resolve => setTimeout(resolve, 100));\n      await withRetries(fn);\n    } else {\n      throw e;\n    }\n  }\n};\n\ndescribe(\"timers\", () => {\n  const hoursInMs = n => 1000 * 60 * 60 * n;\n\n  let clock;\n  beforeEach(() => {\n    clock = FakeTimers.install({ toFake: [\"Date\", \"setInterval\"] });\n  });\n\n  afterEach(() => {\n    clock = clock.uninstall();\n  });\n\n  test(\"removing stale items\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 1 });\n    await addItemToCart(globalUser.username, \"cheesecake\");\n\n    clock.tick(hoursInMs(4));\n    timer = monitorStaleItems();\n    clock.tick(hoursInMs(2));\n\n    await withRetries(async () => {\n      const finalCartContent = await db\n        .select()\n        .from(\"carts_items\")\n        .join(\"users\", \"users.id\", \"carts_items.userId\")\n        .where(\"users.username\", globalUser.username);\n\n      expect(finalCartContent).toEqual([]);\n    });\n\n    await withRetries(async () => {\n      const inventoryContent = await db\n        .select(\"itemName\", \"quantity\")\n        .from(\"inventory\");\n\n      expect(inventoryContent).toEqual([\n        { itemName: \"cheesecake\", quantity: 1 }\n      ]);\n    });\n  });\n});\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/server/dbConnection.js",
    "content": "const environmentName = process.env.NODE_ENV;\nconst db = require(\"knex\")(require(\"./knexfile\")[environmentName]);\n\nconst closeConnection = () => db.destroy();\n\nmodule.exports = {\n  db,\n  closeConnection\n};\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/server/disconnectFromDb.js",
    "content": "const { db } = require(\"./dbConnection\");\n\nafterAll(() => db.destroy());\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/server/inventoryController.js",
    "content": "const { db } = require(\"./dbConnection\");\n\nconst removeFromInventory = async itemName => {\n  const inventoryEntry = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName })\n    .first();\n\n  if (!inventoryEntry || inventoryEntry.quantity === 0) {\n    const err = new Error(`${itemName} is unavailable`);\n    err.code = 400;\n    throw err;\n  }\n\n  await db(\"inventory\")\n    .decrement(\"quantity\")\n    .where({ itemName });\n};\n\nmodule.exports = { removeFromInventory };\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/server/jest.config.js",
    "content": "module.exports = {\n  testEnvironment: \"node\",\n  globalSetup: \"./migrateDatabases.js\",\n  setupFilesAfterEnv: [\n    \"<rootDir>/truncateTables.js\",\n    \"<rootDir>/seedUser.js\",\n    \"<rootDir>/disconnectFromDb.js\"\n  ]\n};\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/server/knexfile.js",
    "content": "module.exports = {\n  test: {\n    client: \"sqlite3\",\n    connection: { filename: \"./test.sqlite\" },\n    useNullAsDefault: true\n  },\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"./dev.sqlite\" },\n    useNullAsDefault: true\n  }\n};\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/server/logger.js",
    "content": "const fs = require(\"fs\");\n\nconst logger = {\n  log: msg => fs.appendFileSync(\"/tmp/logs.out\", msg + \"\\n\")\n};\n\nmodule.exports = logger;\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/server/migrateDatabases.js",
    "content": "const environmentName = process.env.NODE_ENV || \"test\";\nconst environmentConfig = require(\"./knexfile\")[environmentName];\nconst db = require(\"knex\")(environmentConfig);\n\nmodule.exports = async () => {\n  // Migrate the database to the latest state\n  await db.migrate.latest();\n\n  // Close the connection to the database so that tests won't hang\n  await db.destroy();\n};\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/server/migrations/20200325082401_initial_schema.js",
    "content": "exports.up = async knex => {\n  await knex.schema.createTable(\"users\", table => {\n    table.increments(\"id\");\n    table.string(\"username\");\n    table.unique(\"username\");\n    table.string(\"email\");\n    table.string(\"passwordHash\");\n  });\n\n  await knex.schema.createTable(\"carts_items\", table => {\n    table.integer(\"userId\").references(\"users.id\");\n    table.string(\"itemName\");\n    table.unique(\"itemName\");\n    table.integer(\"quantity\");\n  });\n\n  await knex.schema.createTable(\"inventory\", table => {\n    table.increments(\"id\");\n    table.string(\"itemName\");\n    table.unique(\"itemName\");\n    table.integer(\"quantity\");\n  });\n};\n\nexports.down = async knex => {\n  await knex.schema.dropTable(\"users\");\n  await knex.schema.dropTable(\"carts_items\");\n  await knex.schema.dropTable(\"inventory\");\n};\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/server/migrations/20200331210311_updatedAt_field.js",
    "content": "exports.up = knex => {\n  return knex.schema.alterTable(\"carts_items\", table => {\n    table.timestamp(\"updatedAt\");\n  });\n};\n\nexports.down = knex => {\n  return knex.schema.alterTable(\"carts_items\", table => {\n    table.dropColumn(\"updatedAt\");\n  });\n};\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/server/package.json",
    "content": "{\n  \"name\": \"chapter5_server\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"test\": \"jest --runInBand\",\n    \"start\": \"cross-env NODE_ENV=development node server.js\",\n    \"migrate:dev\": \"knex migrate:latest --env development\",\n    \"seed:dev\": \"knex seed:run\"\n  },\n  \"devDependencies\": {\n    \"@sinonjs/fake-timers\": \"github:sinonjs/fake-timers\",\n    \"jest\": \"^24.9.0\",\n    \"supertest\": \"^4.0.2\"\n  },\n  \"dependencies\": {\n    \"@koa/cors\": \"^3.0.0\",\n    \"cross-env\": \"^7.0.2\",\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"knex\": \"^0.20.13\",\n    \"koa\": \"^2.11.0\",\n    \"koa-body-parser\": \"^1.1.2\",\n    \"koa-router\": \"^7.4.0\",\n    \"koa-socket-2\": \"^1.2.0\",\n    \"nock\": \"^12.0.3\",\n    \"socket.io\": \"^2.3.0\",\n    \"sqlite3\": \"^4.1.1\"\n  },\n  \"main\": \"alertController.spec.js\",\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"description\": \"\"\n}\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/server/seedUser.js",
    "content": "const { createUser } = require(\"./userTestUtils\");\n\nbeforeEach(createUser);\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/server/seeds/initial_inventory.js",
    "content": "exports.seed = async knex => {\n  await knex(\"inventory\").del();\n  return knex(\"inventory\").insert([\n    { itemName: \"cheesecake\", quantity: 8 },\n    { itemName: \"apple pie\", quantity: 2 },\n    { itemName: \"carrot cake\", quantity: 5 }\n  ]);\n};\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/server/server.js",
    "content": "const fetch = require(\"isomorphic-fetch\");\nconst Koa = require(\"koa\");\nconst http = require(\"http\");\nconst IO = require(\"koa-socket-2\");\nconst cors = require(\"@koa/cors\");\nconst Router = require(\"koa-router\");\nconst bodyParser = require(\"koa-body-parser\");\n\nconst { db } = require(\"./dbConnection\");\n\nconst { addItemToCart } = require(\"./cartController\");\nconst {\n  hashPassword,\n  authenticationMiddleware\n} = require(\"./authenticationController\");\n\nconst PORT = process.env.NODE_ENV === \"test\" ? 5000 : 3000;\n\nconst app = new Koa();\nconst io = new IO();\nio.attach(app);\n\nconst router = new Router();\n\napp.use(cors());\n\napp.use(bodyParser());\n\napp.use(async (ctx, next) => {\n  if (ctx.url.startsWith(\"/carts\")) {\n    return await authenticationMiddleware(ctx, next);\n  }\n\n  await next();\n});\n\nrouter.put(\"/users/:username\", async ctx => {\n  const { username } = ctx.params;\n  const { email, password } = ctx.request.body;\n\n  const userAlreadyExists = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n\n  if (userAlreadyExists) {\n    ctx.body = { message: `${username} already exists` };\n    ctx.status = 409;\n    return;\n  }\n\n  await db(\"users\").insert({\n    username,\n    email,\n    passwordHash: hashPassword(password)\n  });\n\n  return (ctx.body = { message: `${username} created successfully` });\n});\n\nrouter.post(\"/carts/:username/items\", async ctx => {\n  const { username } = ctx.params;\n  const { item, quantity } = ctx.request.body;\n\n  for (let i = 0; i < quantity; i++) {\n    try {\n      const newItems = await addItemToCart(username, item);\n      ctx.body = newItems;\n    } catch (e) {\n      ctx.body = { message: e.message };\n      ctx.status = e.code;\n      return;\n    }\n  }\n});\n\nrouter.delete(\"/carts/:username/items/:item\", async ctx => {\n  const { username, item } = ctx.params;\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n\n  if (!user) {\n    ctx.body = { message: \"user not found\" };\n    ctx.status = 404;\n    return;\n  }\n\n  const itemEntry = await db\n    .select()\n    .from(\"carts_items\")\n    .where({ userId: user.id, itemName: item })\n    .first();\n\n  if (!itemEntry || itemEntry.quantity === 0) {\n    ctx.body = { message: `${item} is not in the cart` };\n    ctx.status = 400;\n    return;\n  }\n\n  await db(\"carts_items\")\n    .decrement(\"quantity\")\n    .where({ userId: user.id, itemName: item });\n\n  const inventoryEntry = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName: item })\n    .first();\n  if (inventoryEntry) {\n    await db(\"inventory\")\n      .increment(\"quantity\")\n      .where({ userId: itemEntry.userId, itemName: item });\n  } else {\n    await db(\"inventory\").insert({ itemName: item, quantity: 1 });\n  }\n\n  ctx.body = await db\n    .select(\"itemName\", \"quantity\")\n    .from(\"carts_items\")\n    .where({ userId: user.id });\n});\n\nrouter.post(\"/inventory/:itemName\", async ctx => {\n  const { itemName } = ctx.params;\n  const { quantity } = ctx.request.body;\n  const clientId = ctx.request.headers[\"x-socket-client-id\"];\n\n  const current = await db\n    .select(\"itemName\", \"quantity\")\n    .from(\"inventory\")\n    .where({ itemName })\n    .first();\n\n  const itemExists = current && current.quantity > 0;\n  const newRecord = {\n    itemName,\n    quantity: (itemExists ? current.quantity : 0) + quantity\n  };\n\n  if (current) {\n    await db(\"inventory\")\n      .increment(\"quantity\", quantity)\n      .where({ itemName });\n  } else {\n    await db(\"inventory\").insert(newRecord);\n  }\n\n  Object.entries(io.socket.sockets.connected).forEach(([id, socket]) => {\n    if (id === clientId) return;\n    socket.emit(\"add_item\", { itemName, quantity });\n  });\n\n  ctx.body = newRecord;\n});\n\nrouter.delete(\"/inventory/:itemName\", async ctx => {\n  const { itemName } = ctx.params;\n  const { quantity } = ctx.request.body;\n\n  const current = await db\n    .select(\"itemName\", \"quantity\")\n    .from(\"inventory\")\n    .where({ itemName })\n    .first();\n\n  const canDelete = current && current.quantity > quantity;\n\n  if (canDelete) {\n    await db(\"inventory\")\n      .decrement(\"quantity\", quantity)\n      .where({ itemName });\n    ctx.body = { message: `Removed ${quantity} units of ${itemName}` };\n  } else {\n    ctx.status = 404;\n    ctx.body = {\n      message: `There aren't ${quantity} units of ${itemName} available.`\n    };\n  }\n});\n\nrouter.get(\"/inventory\", async ctx => {\n  const inventoryContent = await db\n    .select(\"itemName\", \"quantity\")\n    .from(\"inventory\")\n    .where(\"quantity\", \">\", 0)\n    .orderBy(\"quantity\", \"desc\");\n\n  ctx.body = inventoryContent.reduce((acc, { itemName, quantity }) => {\n    return { ...acc, [itemName]: quantity };\n  }, {});\n});\n\nrouter.get(\"/inventory/:itemName\", async ctx => {\n  const { itemName } = ctx.params;\n\n  const response = await fetch(`http://recipepuppy.com/api?i=${itemName}`);\n  const { title, href, results: recipes } = await response.json();\n  const inventoryItem = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName })\n    .first();\n\n  ctx.body = {\n    ...inventoryItem,\n    info: `Data obtained from ${title} - ${href}`,\n    recipes\n  };\n});\n\napp.use(router.routes());\n\nmodule.exports = { app: app.listen(PORT, \"127.0.0.1\") };\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/server/server.test.js",
    "content": "const { user: globalUser } = require(\"./userTestUtils\");\nconst { db } = require(\"./dbConnection\");\nconst request = require(\"supertest\");\nconst { app } = require(\"./server.js\");\nconst { hashPassword } = require(\"./authenticationController.js\");\nconst nock = require(\"nock\");\n\nafterAll(() => app.close());\n\ndescribe(\"add items to a cart\", () => {\n  test(\"adding available items\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 3 });\n    const response = await request(app)\n      .post(`/carts/${globalUser.username}/items`)\n      .set(\"authorization\", globalUser.authHeader)\n      .send({ item: \"cheesecake\", quantity: 3 })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    const newItems = [{ itemName: \"cheesecake\", quantity: 3 }];\n    expect(response.body).toEqual(newItems);\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(0);\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n\n    expect(finalCartContent).toEqual(newItems);\n  });\n\n  test(\"adding unavailable items\", async () => {\n    const response = await request(app)\n      .post(`/carts/${globalUser.username}/items`)\n      .set(\"authorization\", globalUser.authHeader)\n      .send({ item: \"cheesecake\", quantity: 1 })\n      .expect(400)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"cheesecake is unavailable\"\n    });\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n    expect(finalCartContent).toEqual([]);\n  });\n});\n\ndescribe(\"removing items from a cart\", () => {\n  test(\"removing existing items\", async () => {\n    await db(\"carts_items\").insert({\n      userId: globalUser.id,\n      itemName: \"cheesecake\",\n      quantity: 1\n    });\n\n    const response = await request(app)\n      .del(`/carts/${globalUser.username}/items/cheesecake`)\n      .set(\"authorization\", globalUser.authHeader)\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    const expectedFinalContent = [{ itemName: \"cheesecake\", quantity: 0 }];\n\n    expect(response.body).toEqual(expectedFinalContent);\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n    expect(finalCartContent).toEqual(expectedFinalContent);\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(1);\n  });\n\n  test(\"removing non-existing items\", async () => {\n    await db(\"inventory\").insert({\n      itemName: \"cheesecake\",\n      quantity: 0\n    });\n\n    const response = await request(app)\n      .del(`/carts/${globalUser.username}/items/cheesecake`)\n      .set(\"authorization\", globalUser.authHeader)\n      .expect(400)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"cheesecake is not in the cart\"\n    });\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(0);\n  });\n});\n\ndescribe(\"create accounts\", () => {\n  test(\"creating a new account\", async () => {\n    const response = await request(app)\n      .put(\"/users/another_user\")\n      .send({ email: \"another_user@example.org\", password: \"a_password\" })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"another_user created successfully\"\n    });\n\n    const savedUser = await db\n      .select(\"email\", \"passwordHash\")\n      .from(\"users\")\n      .where({ username: \"another_user\" })\n      .first();\n\n    expect(savedUser).toEqual({\n      email: \"another_user@example.org\",\n      passwordHash: hashPassword(\"a_password\")\n    });\n  });\n\n  test(\"creating a duplicate account\", async () => {\n    const response = await request(app)\n      .put(`/users/${globalUser.username}`)\n      .send({ email: globalUser.email, password: \"a_password\" })\n      .expect(409)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: `${globalUser.username} already exists`\n    });\n  });\n});\n\ndescribe(\"list inventory items\", () => {\n  const eggs = { itemName: \"eggs\", quantity: 3 };\n  const applePie = { itemName: \"apple pie\", quantity: 1 };\n  const carrotCake = { itemName: \"carrot cake\", quantity: 0 };\n\n  beforeEach(async () => {\n    await db(\"inventory\").insert([eggs, applePie, carrotCake]);\n  });\n\n  test(\"fetching all available items\", async () => {\n    const { body } = await request(app)\n      .get(\"/inventory\")\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(body).toEqual({ eggs: 3, \"apple pie\": 1 });\n  });\n});\n\ndescribe(\"add inventory items\", () => {\n  test(\"adding a new item\", async () => {\n    const { body } = await request(app)\n      .post(\"/inventory/eggs\")\n      .send({ quantity: 3 })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(body).toEqual({ itemName: \"eggs\", quantity: 3 });\n\n    expect(\n      await db\n        .select(\"itemName\", \"quantity\")\n        .from(\"inventory\")\n        .where(\"itemName\", \"eggs\")\n        .first()\n    ).toEqual({ itemName: \"eggs\", quantity: 3 });\n  });\n\n  test(\"adding an existing item\", async () => {\n    const eggs = { itemName: \"eggs\", quantity: 2 };\n    await db(\"inventory\").insert(eggs);\n\n    const { body } = await request(app)\n      .post(\"/inventory/eggs\")\n      .send({ quantity: 3 })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(body).toEqual({ itemName: \"eggs\", quantity: 5 });\n\n    expect(\n      await db\n        .select(\"itemName\", \"quantity\")\n        .from(\"inventory\")\n        .where(\"itemName\", \"eggs\")\n        .first()\n    ).toEqual({ itemName: \"eggs\", quantity: 5 });\n  });\n});\n\ndescribe(\"remove inventory items\", () => {\n  beforeEach(async () => {\n    await db(\"inventory\").insert({ itemName: \"eggs\", quantity: 3 });\n  });\n\n  test(\"removing an item\", async () => {\n    const { body } = await request(app)\n      .del(\"/inventory/eggs\")\n      .send({ quantity: 2 })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(body).toEqual({\n      message: \"Removed 2 units of eggs\"\n    });\n\n    expect(\n      await db\n        .select(\"itemName\", \"quantity\")\n        .from(\"inventory\")\n        .where(\"itemName\", \"eggs\")\n        .first()\n    ).toEqual({ itemName: \"eggs\", quantity: 1 });\n  });\n\n  test(\"removing more than the inventory quantity\", async () => {\n    const { body } = await request(app)\n      .del(\"/inventory/eggs\")\n      .send({ quantity: 4 })\n      .expect(404)\n      .expect(\"Content-Type\", /json/);\n\n    expect(body).toEqual({\n      message: \"There aren't 4 units of eggs available.\"\n    });\n\n    expect(\n      await db\n        .select(\"itemName\", \"quantity\")\n        .from(\"inventory\")\n        .where(\"itemName\", \"eggs\")\n        .first()\n    ).toEqual({ itemName: \"eggs\", quantity: 3 });\n  });\n});\n\ndescribe(\"fetch inventory items\", () => {\n  const eggs = { itemName: \"eggs\", quantity: 3 };\n  const applePie = { itemName: \"apple pie\", quantity: 1 };\n\n  beforeEach(async () => {\n    await db(\"inventory\").insert([eggs, applePie]);\n    const { id: eggsId } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"eggs\" })\n      .first();\n    eggs.id = eggsId;\n  });\n\n  test(\"fetching an item from the inventory\", async () => {\n    const eggsResponse = {\n      title: \"FakeAPI\",\n      href: \"example.org\",\n      results: [{ name: \"Omelette du Fromage\" }]\n    };\n\n    nock(\"http://recipepuppy.com\")\n      .get(\"/api\")\n      .query({ i: \"eggs\" })\n      .reply(200, eggsResponse);\n\n    const response = await request(app)\n      .get(`/inventory/eggs`)\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      ...eggs,\n      info: `Data obtained from ${eggsResponse.title} - ${eggsResponse.href}`,\n      recipes: eggsResponse.results\n    });\n  });\n});\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/server/truncateTables.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst tablesToTruncate = [\"users\", \"inventory\", \"carts_items\"];\n\nbeforeEach(() => {\n  return Promise.all(tablesToTruncate.map(t => db(t).truncate()));\n});\n"
  },
  {
    "path": "chapter6/5_web_sockets_and_http_requests/server/userTestUtils.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { hashPassword } = require(\"./authenticationController\");\n\nconst username = \"test_user\";\nconst password = \"a_password\";\nconst passwordHash = hashPassword(password);\nconst email = \"test_user@example.org\";\nconst validAuth = Buffer.from(`${username}:${password}`).toString(\"base64\");\nconst authHeader = `Basic ${validAuth}`;\n\nconst user = {\n  username,\n  password,\n  email,\n  authHeader\n};\n\nconst createUser = async () => {\n  await db(\"users\").insert({ username, email, passwordHash });\n  const { id } = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  user.id = id;\n};\n\nmodule.exports = { user, createUser };\n"
  },
  {
    "path": "chapter7/1_setting_up_a_test_environment/1_createElement_calls/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Inventory</title>\n  </head>\n  <body>\n    <div id=\"app\" />\n    <script src=\"bundle.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter7/1_setting_up_a_test_environment/1_createElement_calls/index.js",
    "content": "const ReactDOM = require(\"react-dom\");\nconst React = require(\"react\");\n\nconst header = React.createElement(\"h1\", null, \"Inventory Contents\");\nconst App = React.createElement(\"div\", null, header);\n\nReactDOM.render(App, document.getElementById(\"app\"));\n"
  },
  {
    "path": "chapter7/1_setting_up_a_test_environment/1_createElement_calls/package.json",
    "content": "{\n  \"name\": \"1_createelement_calls\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"build\": \"browserify index.js -o bundle.js\",\n    \"start\": \"http-server ./\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"browserify\": \"^16.5.1\",\n    \"http-server\": \"^0.12.3\"\n  },\n  \"dependencies\": {\n    \"react\": \"^16.13.1\",\n    \"react-dom\": \"^16.13.1\"\n  }\n}\n"
  },
  {
    "path": "chapter7/1_setting_up_a_test_environment/2_transforming_jsx/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Inventory</title>\n  </head>\n  <body>\n    <div id=\"app\" />\n    <script src=\"bundle.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter7/1_setting_up_a_test_environment/2_transforming_jsx/index.jsx",
    "content": "import ReactDOM from \"react-dom\";\nimport React from \"react\";\n\nconst App = () => {\n  return (\n    <div>\n      <h1>Inventory Contents</h1>\n    </div>\n  );\n};\n\nReactDOM.render(<App />, document.getElementById(\"app\"));\n"
  },
  {
    "path": "chapter7/1_setting_up_a_test_environment/2_transforming_jsx/package.json",
    "content": "{\n  \"name\": \"2_transforming_jsx\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"build\": \"browserify index.jsx -o bundle.js\",\n    \"start\": \"http-server ./\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.9.6\",\n    \"@babel/preset-env\": \"^7.9.6\",\n    \"@babel/preset-react\": \"^7.9.4\",\n    \"babelify\": \"^10.0.0\",\n    \"browserify\": \"^16.5.1\",\n    \"http-server\": \"^0.12.3\"\n  },\n  \"dependencies\": {\n    \"react\": \"^16.13.1\",\n    \"react-dom\": \"^16.13.1\"\n  },\n  \"browserify\": {\n    \"transform\": [\n      [\n        \"babelify\",\n        {\n          \"presets\": [\n            \"@babel/preset-env\",\n            \"@babel/preset-react\"\n          ]\n        }\n      ]\n    ]\n  }\n}\n"
  },
  {
    "path": "chapter7/1_setting_up_a_test_environment/3_setting_up_jest/App.jsx",
    "content": "import React from \"react\";\n\nexport const App = () => {\n  return (\n    <div>\n      <h1>Inventory Contents</h1>\n    </div>\n  );\n};\n"
  },
  {
    "path": "chapter7/1_setting_up_a_test_environment/3_setting_up_jest/app.test.js",
    "content": "import App from \"./App.jsx\";\n"
  },
  {
    "path": "chapter7/1_setting_up_a_test_environment/3_setting_up_jest/babel.config.js",
    "content": "module.exports = {\n  presets: [\n    [\n      \"@babel/preset-env\",\n      {\n        targets: {\n          node: \"current\"\n        }\n      }\n    ],\n    \"@babel/preset-react\"\n  ]\n};\n"
  },
  {
    "path": "chapter7/1_setting_up_a_test_environment/3_setting_up_jest/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Inventory</title>\n  </head>\n  <body>\n    <div id=\"app\" />\n    <script src=\"bundle.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter7/1_setting_up_a_test_environment/3_setting_up_jest/index.jsx",
    "content": "import ReactDOM from \"react-dom\";\nimport React from \"react\";\nimport { App } from \"./App.jsx\";\n\nReactDOM.render(<App />, document.getElementById(\"app\"));\n"
  },
  {
    "path": "chapter7/1_setting_up_a_test_environment/3_setting_up_jest/jest.config.js",
    "content": "// For a detailed explanation regarding each configuration property, visit:\n// https://jestjs.io/docs/en/configuration.html\n\nmodule.exports = {\n  // All imported modules in your tests should be mocked automatically\n  // automock: false,\n  // Stop running tests after `n` failures\n  // bail: 0,\n  // The directory where Jest should store its cached dependency information\n  // cacheDirectory: \"/private/var/folders/p0/0npmk50s57v5sgzb_s9z2xmc0000gn/T/jest_dx\",\n  // Automatically clear mock calls and instances between every test\n  // clearMocks: false,\n  // Indicates whether the coverage information should be collected while executing the test\n  // collectCoverage: false,\n  // An array of glob patterns indicating a set of files for which coverage information should be collected\n  // collectCoverageFrom: undefined,\n  // The directory where Jest should output its coverage files\n  // coverageDirectory: undefined,\n  // An array of regexp pattern strings used to skip coverage collection\n  // coveragePathIgnorePatterns: [\n  //   \"/node_modules/\"\n  // ],\n  // A list of reporter names that Jest uses when writing coverage reports\n  // coverageReporters: [\n  //   \"json\",\n  //   \"text\",\n  //   \"lcov\",\n  //   \"clover\"\n  // ],\n  // An object that configures minimum threshold enforcement for coverage results\n  // coverageThreshold: undefined,\n  // A path to a custom dependency extractor\n  // dependencyExtractor: undefined,\n  // Make calling deprecated APIs throw helpful error messages\n  // errorOnDeprecated: false,\n  // Force coverage collection from ignored files using an array of glob patterns\n  // forceCoverageMatch: [],\n  // A path to a module which exports an async function that is triggered once before all test suites\n  // globalSetup: undefined,\n  // A path to a module which exports an async function that is triggered once after all test suites\n  // globalTeardown: undefined,\n  // A set of global variables that need to be available in all test environments\n  // globals: {},\n  // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.\n  // maxWorkers: \"50%\",\n  // An array of directory names to be searched recursively up from the requiring module's location\n  // moduleDirectories: [\n  //   \"node_modules\"\n  // ],\n  // An array of file extensions your modules use\n  // moduleFileExtensions: [\n  //   \"js\",\n  //   \"json\",\n  //   \"jsx\",\n  //   \"ts\",\n  //   \"tsx\",\n  //   \"node\"\n  // ],\n  // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module\n  // moduleNameMapper: {},\n  // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader\n  // modulePathIgnorePatterns: [],\n  // Activates notifications for test results\n  // notify: false,\n  // An enum that specifies notification mode. Requires { notify: true }\n  // notifyMode: \"failure-change\",\n  // A preset that is used as a base for Jest's configuration\n  // preset: undefined,\n  // Run tests from one or more projects\n  // projects: undefined,\n  // Use this configuration option to add custom reporters to Jest\n  // reporters: undefined,\n  // Automatically reset mock state between every test\n  // resetMocks: false,\n  // Reset the module registry before running each individual test\n  // resetModules: false,\n  // A path to a custom resolver\n  // resolver: undefined,\n  // Automatically restore mock state between every test\n  // restoreMocks: false,\n  // The root directory that Jest should scan for tests and modules within\n  // rootDir: undefined,\n  // A list of paths to directories that Jest should use to search for files in\n  // roots: [\n  //   \"<rootDir>\"\n  // ],\n  // Allows you to use a custom runner instead of Jest's default test runner\n  // runner: \"jest-runner\",\n  // The paths to modules that run some code to configure or set up the testing environment before each test\n  // setupFiles: [],\n  // A list of paths to modules that run some code to configure or set up the testing framework before each test\n  // setupFilesAfterEnv: [],\n  // A list of paths to snapshot serializer modules Jest should use for snapshot testing\n  // snapshotSerializers: [],\n  // The test environment that will be used for testing\n  // testEnvironment: \"jest-environment-jsdom\",\n  // Options that will be passed to the testEnvironment\n  // testEnvironmentOptions: {},\n  // Adds a location field to test results\n  // testLocationInResults: false,\n  // The glob patterns Jest uses to detect test files\n  // testMatch: [\n  //   \"**/__tests__/**/*.[jt]s?(x)\",\n  //   \"**/?(*.)+(spec|test).[tj]s?(x)\"\n  // ],\n  // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped\n  // testPathIgnorePatterns: [\n  //   \"/node_modules/\"\n  // ],\n  // The regexp pattern or array of patterns that Jest uses to detect test files\n  // testRegex: [],\n  // This option allows the use of a custom results processor\n  // testResultsProcessor: undefined,\n  // This option allows use of a custom test runner\n  // testRunner: \"jasmine2\",\n  // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href\n  // testURL: \"http://localhost\",\n  // Setting this value to \"fake\" allows the use of fake timers for functions such as \"setTimeout\"\n  // timers: \"real\",\n  // A map from regular expressions to paths to transformers\n  // transform: undefined,\n  // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation\n  // transformIgnorePatterns: [\n  //   \"/node_modules/\"\n  // ],\n  // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them\n  // unmockedModulePathPatterns: undefined,\n  // Indicates whether each individual test should be reported during the run\n  // verbose: undefined,\n  // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode\n  // watchPathIgnorePatterns: [],\n  // Whether to use watchman for file crawling\n  // watchman: true,\n};\n"
  },
  {
    "path": "chapter7/1_setting_up_a_test_environment/3_setting_up_jest/package.json",
    "content": "{\n  \"name\": \"3_setting_up_jest\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"build\": \"browserify index.jsx -o bundle.js\",\n    \"start\": \"http-server ./\",\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.9.6\",\n    \"@babel/preset-env\": \"^7.9.6\",\n    \"@babel/preset-react\": \"^7.9.4\",\n    \"babelify\": \"^10.0.0\",\n    \"browserify\": \"^16.5.1\",\n    \"http-server\": \"^0.12.3\",\n    \"jest\": \"^24.9\"\n  },\n  \"dependencies\": {\n    \"react\": \"^16.13.1\",\n    \"react-dom\": \"^16.13.1\"\n  },\n  \"browserify\": {\n    \"transform\": [\n      [\n        \"babelify\",\n        {\n          \"presets\": [\n            \"@babel/preset-env\",\n            \"@babel/preset-react\"\n          ]\n        }\n      ]\n    ]\n  }\n}\n"
  },
  {
    "path": "chapter7/2_an_overview_of_react_testing_libraries/1_react_testing_utilities/App.jsx",
    "content": "import React from \"react\";\n\nexport const App = () => {\n  const [cheesecakes, setCheesecake] = React.useState(0);\n  return (\n    <div>\n      <h1>Inventory Contents</h1>\n      <p>Cheesecakes: {cheesecakes}</p>\n      <button onClick={() => setCheesecake(cheesecakes + 1)}>\n        Add cheesecake\n      </button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "chapter7/2_an_overview_of_react_testing_libraries/1_react_testing_utilities/App.test.jsx",
    "content": "import React from \"react\";\nimport { App } from \"./App.jsx\";\nimport { render } from \"react-dom\";\nimport { act } from \"react-dom/test-utils\";\nimport { screen, fireEvent } from \"@testing-library/dom\";\n\nconst root = document.createElement(\"div\");\ndocument.body.appendChild(root);\n\ntest(\"renders the appropriate header\", () => {\n  act(() => {\n    render(<App />, root);\n  });\n  expect(screen.getByText(\"Inventory Contents\")).toBeInTheDocument();\n});\n\ntest(\"increments the number of cheesecakes\", () => {\n  act(() => {\n    render(<App />, root);\n  });\n\n  expect(screen.getByText(\"Cheesecakes: 0\")).toBeInTheDocument();\n\n  const addCheesecakeBtn = screen.getByText(\"Add cheesecake\");\n  act(() => {\n    fireEvent.click(addCheesecakeBtn);\n  });\n\n  expect(screen.getByText(\"Cheesecakes: 1\")).toBeInTheDocument();\n});\n"
  },
  {
    "path": "chapter7/2_an_overview_of_react_testing_libraries/1_react_testing_utilities/babel.config.js",
    "content": "module.exports = {\n  presets: [\n    [\n      \"@babel/preset-env\",\n      {\n        targets: {\n          node: \"current\"\n        }\n      }\n    ],\n    \"@babel/preset-react\"\n  ]\n};\n"
  },
  {
    "path": "chapter7/2_an_overview_of_react_testing_libraries/1_react_testing_utilities/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Inventory</title>\n  </head>\n  <body>\n    <div id=\"app\" />\n    <script src=\"bundle.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter7/2_an_overview_of_react_testing_libraries/1_react_testing_utilities/index.jsx",
    "content": "import ReactDOM from \"react-dom\";\nimport React from \"react\";\nimport { App } from \"./App.jsx\";\n\nReactDOM.render(<App />, document.getElementById(\"app\"));\n"
  },
  {
    "path": "chapter7/2_an_overview_of_react_testing_libraries/1_react_testing_utilities/jest.config.js",
    "content": "module.exports = {\n  setupFilesAfterEnv: [\"<rootDir>/setupJestDom.js\"]\n};\n"
  },
  {
    "path": "chapter7/2_an_overview_of_react_testing_libraries/1_react_testing_utilities/package.json",
    "content": "{\n  \"name\": \"1_react_testing_utilities\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"build\": \"browserify index.jsx -o bundle.js\",\n    \"start\": \"http-server ./\",\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.9.6\",\n    \"@babel/preset-env\": \"^7.9.6\",\n    \"@babel/preset-react\": \"^7.9.4\",\n    \"@testing-library/dom\": \"^7.10.1\",\n    \"@testing-library/jest-dom\": \"^5.9.0\",\n    \"babelify\": \"^10.0.0\",\n    \"browserify\": \"^16.5.1\",\n    \"http-server\": \"^0.12.3\",\n    \"jest\": \"^24.9\"\n  },\n  \"dependencies\": {\n    \"react\": \"^16.13.1\",\n    \"react-dom\": \"^16.13.1\"\n  },\n  \"browserify\": {\n    \"transform\": [\n      [\n        \"babelify\",\n        {\n          \"presets\": [\n            \"@babel/preset-env\",\n            \"@babel/preset-react\"\n          ]\n        }\n      ]\n    ]\n  }\n}\n"
  },
  {
    "path": "chapter7/2_an_overview_of_react_testing_libraries/1_react_testing_utilities/setupJestDom.js",
    "content": "const jestDom = require(\"@testing-library/jest-dom\");\n\nexpect.extend(jestDom);\n"
  },
  {
    "path": "chapter7/2_an_overview_of_react_testing_libraries/2_react_testing_library/App.jsx",
    "content": "import React, { useEffect, useState, useRef } from \"react\";\nimport { API_ADDR } from \"./constants\";\nimport { ItemForm } from \"./ItemForm.jsx\";\nimport { ItemList } from \"./ItemList.jsx\";\n\nexport const App = () => {\n  const [items, setItems] = useState({});\n  const isMounted = useRef(null);\n\n  useEffect(() => {\n    isMounted.current = true;\n    const loadItems = async () => {\n      const response = await fetch(`${API_ADDR}/inventory`);\n      const responseBody = await response.json();\n      if (isMounted.current) setItems(responseBody);\n    };\n    loadItems();\n    return () => (isMounted.current = false);\n  }, []);\n\n  return (\n    <div>\n      <h1>Inventory Contents</h1>\n      <ItemList itemList={items} />\n      <ItemForm />\n    </div>\n  );\n};\n"
  },
  {
    "path": "chapter7/2_an_overview_of_react_testing_libraries/2_react_testing_library/App.test.jsx",
    "content": "import React from \"react\";\nimport nock from \"nock\";\nimport { API_ADDR } from \"./constants\";\nimport { App } from \"./App.jsx\";\nimport { render, waitFor } from \"@testing-library/react\";\n\nbeforeEach(() => {\n  nock(API_ADDR)\n    .get(\"/inventory\")\n    .reply(200, { cheesecake: 2, croissant: 5, macaroon: 96 });\n});\n\nafterEach(() => {\n  if (!nock.isDone()) {\n    nock.cleanAll();\n    throw new Error(\"Not all mocked endpoints received requests.\");\n  }\n});\n\ntest(\"renders the appropriate header\", () => {\n  const { getByText } = render(<App />);\n  expect(getByText(\"Inventory Contents\")).toBeInTheDocument();\n});\n\ntest(\"rendering the server's list of items\", async () => {\n  const { findByText } = render(<App />);\n\n  expect(await findByText(\"cheesecake - Quantity: 2\")).toBeInTheDocument();\n  expect(await findByText(\"croissant - Quantity: 5\")).toBeInTheDocument();\n  expect(await findByText(\"macaroon - Quantity: 96\")).toBeInTheDocument();\n\n  const listElement = document.querySelector(\"ul\");\n  expect(listElement.childElementCount).toBe(3);\n});\n"
  },
  {
    "path": "chapter7/2_an_overview_of_react_testing_libraries/2_react_testing_library/ItemForm.jsx",
    "content": "import React from \"react\";\nimport { API_ADDR } from \"./constants\";\n\nconst addItemRequest = (itemName, quantity) => {\n  fetch(`${API_ADDR}/inventory/${itemName}`, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ quantity })\n  });\n};\n\nexport const ItemForm = () => {\n  const [itemName, setItemName] = React.useState(\"\");\n  const [quantity, setQuantity] = React.useState(0);\n\n  const onSubmit = async e => {\n    e.preventDefault();\n    await addItemRequest(itemName, quantity);\n  };\n\n  return (\n    <form onSubmit={onSubmit}>\n      <input\n        onChange={e => setItemName(e.target.value)}\n        placeholder=\"Item name\"\n      />\n      <input\n        onChange={e => setQuantity(parseInt(e.target.value, 10))}\n        placeholder=\"Quantity\"\n      />\n      <button type=\"submit\">Add item</button>\n    </form>\n  );\n};\n"
  },
  {
    "path": "chapter7/2_an_overview_of_react_testing_libraries/2_react_testing_library/ItemForm.test.jsx",
    "content": "import React from \"react\";\nimport nock from \"nock\";\nimport { API_ADDR } from \"./constants\";\nimport { ItemForm } from \"./ItemForm.jsx\";\nimport { render, fireEvent } from \"@testing-library/react\";\n\ntest(\"form's elements\", () => {\n  const { getByText, getByPlaceholderText } = render(<ItemForm />);\n  expect(getByPlaceholderText(\"Item name\")).toBeInTheDocument();\n  expect(getByPlaceholderText(\"Quantity\")).toBeInTheDocument();\n  expect(getByText(\"Add item\")).toBeInTheDocument();\n});\n\ntest(\"sending requests\", () => {\n  const { getByText, getByPlaceholderText } = render(<ItemForm />);\n\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 2 }))\n    .reply(200);\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"2\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  expect(nock.isDone()).toBe(true);\n});\n"
  },
  {
    "path": "chapter7/2_an_overview_of_react_testing_libraries/2_react_testing_library/ItemList.jsx",
    "content": "import React from \"react\";\n\nexport const ItemList = ({ itemList }) => {\n  return (\n    <ul>\n      {Object.entries(itemList).map(([itemName, quantity]) => {\n        return (\n          <li key={itemName}>\n            {itemName} - Quantity: {quantity}\n          </li>\n        );\n      })}\n    </ul>\n  );\n};\n"
  },
  {
    "path": "chapter7/2_an_overview_of_react_testing_libraries/2_react_testing_library/ItemList.test.jsx",
    "content": "import React from \"react\";\nimport { ItemList } from \"./ItemList.jsx\";\nimport { render } from \"@testing-library/react\";\n\ntest(\"list items\", () => {\n  const itemList = { cheesecake: 2, croissant: 5, macaroon: 96 };\n  const { getByText } = render(<ItemList itemList={itemList} />);\n\n  const listElement = document.querySelector(\"ul\");\n  expect(listElement.childElementCount).toBe(3);\n  expect(getByText(\"cheesecake - Quantity: 2\")).toBeInTheDocument();\n  expect(getByText(\"croissant - Quantity: 5\")).toBeInTheDocument();\n  expect(getByText(\"macaroon - Quantity: 96\")).toBeInTheDocument();\n});\n"
  },
  {
    "path": "chapter7/2_an_overview_of_react_testing_libraries/2_react_testing_library/babel.config.js",
    "content": "module.exports = {\n  presets: [\n    [\n      \"@babel/preset-env\",\n      {\n        targets: {\n          node: \"current\"\n        }\n      }\n    ],\n    \"@babel/preset-react\"\n  ]\n};\n"
  },
  {
    "path": "chapter7/2_an_overview_of_react_testing_libraries/2_react_testing_library/constants.js",
    "content": "export const API_ADDR = \"http://localhost:3000\";\n"
  },
  {
    "path": "chapter7/2_an_overview_of_react_testing_libraries/2_react_testing_library/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Inventory</title>\n  </head>\n  <body>\n    <div id=\"app\" />\n    <script src=\"bundle.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter7/2_an_overview_of_react_testing_libraries/2_react_testing_library/index.jsx",
    "content": "import ReactDOM from \"react-dom\";\nimport React from \"react\";\nimport { App } from \"./App.jsx\";\n\nReactDOM.render(<App />, document.getElementById(\"app\"));\n"
  },
  {
    "path": "chapter7/2_an_overview_of_react_testing_libraries/2_react_testing_library/jest.config.js",
    "content": "module.exports = {\n  setupFilesAfterEnv: [\n    \"<rootDir>/setupJestDom.js\",\n    \"<rootDir>/setupGlobalFetch.js\"\n  ]\n};\n"
  },
  {
    "path": "chapter7/2_an_overview_of_react_testing_libraries/2_react_testing_library/package.json",
    "content": "{\n  \"name\": \"2_react-testing-library\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"build\": \"browserify index.jsx -o bundle.js\",\n    \"start\": \"http-server ./\",\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.9.6\",\n    \"@babel/preset-env\": \"^7.9.6\",\n    \"@babel/preset-react\": \"^7.9.4\",\n    \"@testing-library/dom\": \"^7.10.1\",\n    \"@testing-library/jest-dom\": \"^5.9.0\",\n    \"@testing-library/react\": \"^10.2.1\",\n    \"babelify\": \"^10.0.0\",\n    \"browserify\": \"^16.5.1\",\n    \"core-js\": \"^2.6.11\",\n    \"http-server\": \"^0.12.3\",\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"jest\": \"^25.5\",\n    \"nock\": \"^12.0.3\"\n  },\n  \"dependencies\": {\n    \"react\": \"^16.13.1\",\n    \"react-dom\": \"^16.13.1\"\n  },\n  \"browserify\": {\n    \"transform\": [\n      [\n        \"babelify\",\n        {\n          \"presets\": [\n            [\n              \"@babel/preset-env\",\n              {\n                \"useBuiltIns\": \"usage\",\n                \"corejs\": 2\n              }\n            ],\n            \"@babel/preset-react\"\n          ]\n        }\n      ]\n    ]\n  }\n}\n"
  },
  {
    "path": "chapter7/2_an_overview_of_react_testing_libraries/2_react_testing_library/setupGlobalFetch.js",
    "content": "const fetch = require(\"isomorphic-fetch\");\n\nglobal.window.fetch = fetch;\n"
  },
  {
    "path": "chapter7/2_an_overview_of_react_testing_libraries/2_react_testing_library/setupJestDom.js",
    "content": "const jestDom = require(\"@testing-library/jest-dom\");\n\nexpect.extend(jestDom);\n"
  },
  {
    "path": "chapter7/server/README.md",
    "content": "# Chapter 5 Server\n\nTo better support the client-side application we'll build on Chapter 5, I've had to do a few updates to the server from Chapter 4.\n\nIn case you want to update the back-end from Chapter 4 yourself, here's the list of changes I've done:\n\n- For the server to accept the requests coming from the client, you'll need to use [`@koa/cors`](https://github.com/koajs/cors)\n- To enable running tests while the server is running, I bind it to different ports depending on whether I am in a test or development environment.\n- At `POST /inventory/:itemName` I have added a route which adds an item to the inventory. It takes a `body` containing the `quantity` to add.\n- At `GET /inventory` I have added a route which lists all items in the inventory.\n- At `DELETE /inventory/:itemName` I have added a route which let's you delete inventory items so that you can use to fix the `undo` functionality\n- I've used `koa-socket-2` to add support for `socket.io`\n- The `POST /inventory/:itemName` will now push updates to all clients but the one which added an item.\n"
  },
  {
    "path": "chapter7/server/authenticationController.js",
    "content": "const crypto = require(\"crypto\");\nconst { db } = require(\"./dbConnection\");\n\nconst hashPassword = password => {\n  const hash = crypto.createHash(\"sha256\");\n  hash.update(password);\n  return hash.digest(\"hex\");\n};\n\nconst credentialsAreValid = async (username, password) => {\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  if (!user) return false;\n  return hashPassword(password) === user.passwordHash;\n};\n\nconst authenticationMiddleware = async (ctx, next) => {\n  try {\n    const authHeader = ctx.request.headers.authorization;\n    const credentials = Buffer.from(\n      authHeader.slice(\"basic\".length + 1),\n      \"base64\"\n    ).toString();\n    const [username, password] = credentials.split(\":\");\n\n    const validCredentialsSent = await credentialsAreValid(username, password);\n    if (!validCredentialsSent) throw new Error(\"invalid credentials\");\n  } catch (e) {\n    ctx.status = 401;\n    ctx.body = { message: \"please provide valid credentials\" };\n    return;\n  }\n\n  await next();\n};\n\nmodule.exports = {\n  hashPassword,\n  credentialsAreValid,\n  authenticationMiddleware\n};\n"
  },
  {
    "path": "chapter7/server/authenticationController.test.js",
    "content": "const crypto = require(\"crypto\");\nconst {\n  hashPassword,\n  credentialsAreValid,\n  authenticationMiddleware\n} = require(\"./authenticationController\");\nconst { user: globalUser } = require(\"./userTestUtils\");\n\ndescribe(\"hashPassword\", () => {\n  test(\"hashing passwords\", () => {\n    const plainTextPassword = \"password_example\";\n    const hash = crypto.createHash(\"sha256\");\n    hash.update(plainTextPassword);\n    const expectedHash = hash.digest(\"hex\");\n    expect(hashPassword(plainTextPassword)).toBe(expectedHash);\n  });\n});\n\ndescribe(\"credentialsAreValid\", () => {\n  test(\"validating credentials\", async () => {\n    expect(await credentialsAreValid(globalUser.username, \"a_password\")).toBe(\n      true\n    );\n  });\n});\n\ndescribe(\"authenticationMiddleware\", () => {\n  test(\"returning an error if the credentials are not valid\", async () => {\n    const fakeAuth = Buffer.from(\"invalid:credentials\").toString(\"base64\");\n    const ctx = {\n      request: {\n        headers: { authorization: `Basic ${fakeAuth}` }\n      }\n    };\n\n    const next = jest.fn();\n    await authenticationMiddleware(ctx, next);\n    expect(next.mock.calls).toHaveLength(0);\n    expect(ctx).toEqual({\n      ...ctx,\n      status: 401,\n      body: { message: \"please provide valid credentials\" }\n    });\n  });\n\n  test(\"authenticating properly\", async () => {\n    const ctx = {\n      request: {\n        headers: { authorization: globalUser.authHeader }\n      }\n    };\n\n    const next = jest.fn();\n    await authenticationMiddleware(ctx, next);\n    expect(next.mock.calls).toHaveLength(1);\n  });\n});\n"
  },
  {
    "path": "chapter7/server/cartController.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { removeFromInventory } = require(\"./inventoryController\");\nconst logger = require(\"./logger\");\n\nconst addItemToCart = async (username, itemName) => {\n  await removeFromInventory(itemName);\n\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  if (!user) {\n    const userNotFound = new Error(\"user not found\");\n    userNotFound.code = 404;\n  }\n\n  const itemEntry = await db\n    .select()\n    .from(\"carts_items\")\n    .where({ userId: user.id, itemName })\n    .first();\n\n  if (itemEntry && itemEntry.quantity + 1 > 3) {\n    const limitError = new Error(\n      \"You can't have more than three units of an item in your cart\"\n    );\n    limitError.code = 400;\n    throw limitError;\n  }\n\n  if (itemEntry) {\n    await db(\"carts_items\")\n      .increment(\"quantity\")\n      .update({ updatedAt: new Date().toISOString() })\n      .where({\n        userId: itemEntry.userId,\n        itemName\n      });\n  } else {\n    await db(\"carts_items\").insert({\n      userId: user.id,\n      itemName,\n      quantity: 1,\n      updatedAt: new Date().toISOString()\n    });\n  }\n\n  logger.log(`${itemName} added to ${username}'s cart`);\n  return db\n    .select(\"itemName\", \"quantity\")\n    .from(\"carts_items\")\n    .where({ userId: user.id });\n};\n\nconst hoursInMs = n => 1000 * 60 * 60 * n;\n\nconst removeStaleItems = async () => {\n  const fourHoursAgo = new Date(Date.now() - hoursInMs(4)).toISOString();\n\n  const staleItems = await db\n    .select()\n    .from(\"carts_items\")\n    .where(\"updatedAt\", \"<\", fourHoursAgo);\n\n  if (staleItems.length === 0) return;\n\n  // Put stale items back in the inventory\n  const inventoryUpdates = staleItems.map(staleItem =>\n    db(\"inventory\")\n      .increment(\"quantity\", staleItem.quantity)\n      .where({ itemName: staleItem.itemName })\n  );\n  await Promise.all(inventoryUpdates);\n\n  // Delete stale items from cart\n  const staleItemTuples = staleItems.map(i => [i.itemName, i.userId]);\n  await db(\"carts_items\")\n    .del()\n    .whereIn([\"itemName\", \"userId\"], staleItemTuples);\n};\n\nconst monitorStaleItems = () => setInterval(removeStaleItems, hoursInMs(2));\n\nmodule.exports = { addItemToCart, monitorStaleItems };\n"
  },
  {
    "path": "chapter7/server/cartController.test.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { addItemToCart, monitorStaleItems } = require(\"./cartController\");\nconst { hashPassword } = require(\"./authenticationController\");\nconst { user: globalUser } = require(\"./userTestUtils\");\nconst FakeTimers = require(\"@sinonjs/fake-timers\");\n\nconst fs = require(\"fs\");\n\ndescribe(\"addItemToCart\", () => {\n  beforeEach(() => {\n    fs.writeFileSync(\"/tmp/logs.out\", \"\");\n  });\n\n  test(\"adding unavailable items to cart\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 0 });\n\n    try {\n      await addItemToCart(globalUser.username, \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(\"cheesecake is unavailable\");\n      expectedError.code = 400;\n\n      expect(e).toEqual(expectedError);\n    }\n\n    const finalCartContent = await db\n      .select(\"carts_items.*\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n\n    expect(finalCartContent).toEqual([]);\n    expect.assertions(2);\n  });\n\n  test(\"adding items above limit to cart\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 1 });\n    await db(\"carts_items\").insert({\n      userId: globalUser.id,\n      itemName: \"cheesecake\",\n      quantity: 3\n    });\n\n    try {\n      await addItemToCart(globalUser.username, \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(\n        \"You can't have more than three units of an item in your cart\"\n      );\n      expectedError.code = 400;\n      expect(e).toEqual(expectedError);\n    }\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n\n    expect(finalCartContent).toEqual([{ itemName: \"cheesecake\", quantity: 3 }]);\n    expect.assertions(2);\n  });\n\n  test(\"logging added items\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 1 });\n    await db(\"carts_items\").insert({\n      userId: globalUser.id,\n      itemName: \"cheesecake\",\n      quantity: 1\n    });\n\n    await addItemToCart(globalUser.username, \"cheesecake\");\n\n    const logs = fs.readFileSync(\"/tmp/logs.out\", \"utf-8\");\n    expect(logs).toContain(\n      `cheesecake added to ${globalUser.username}'s cart\\n`\n    );\n  });\n});\n\nconst withRetries = async fn => {\n  // Capture the assertion error since Jest does not export it\n  const JestAssertionError = (() => {\n    try {\n      expect(false).toBe(true);\n    } catch (e) {\n      return e.constructor;\n    }\n  })();\n\n  try {\n    await fn();\n  } catch (e) {\n    if (e.constructor === JestAssertionError) {\n      // Wait 100ms before retrying\n      await new Promise(resolve => setTimeout(resolve, 100));\n      await withRetries(fn);\n    } else {\n      throw e;\n    }\n  }\n};\n\ndescribe(\"timers\", () => {\n  const hoursInMs = n => 1000 * 60 * 60 * n;\n\n  let clock;\n  beforeEach(() => {\n    clock = FakeTimers.install({ toFake: [\"Date\", \"setInterval\"] });\n  });\n\n  afterEach(() => {\n    clock = clock.uninstall();\n  });\n\n  test(\"removing stale items\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 1 });\n    await addItemToCart(globalUser.username, \"cheesecake\");\n\n    clock.tick(hoursInMs(4));\n    timer = monitorStaleItems();\n    clock.tick(hoursInMs(2));\n\n    await withRetries(async () => {\n      const finalCartContent = await db\n        .select()\n        .from(\"carts_items\")\n        .join(\"users\", \"users.id\", \"carts_items.userId\")\n        .where(\"users.username\", globalUser.username);\n\n      expect(finalCartContent).toEqual([]);\n    });\n\n    await withRetries(async () => {\n      const inventoryContent = await db\n        .select(\"itemName\", \"quantity\")\n        .from(\"inventory\");\n\n      expect(inventoryContent).toEqual([\n        { itemName: \"cheesecake\", quantity: 1 }\n      ]);\n    });\n  });\n});\n"
  },
  {
    "path": "chapter7/server/dbConnection.js",
    "content": "const environmentName = process.env.NODE_ENV;\nconst db = require(\"knex\")(require(\"./knexfile\")[environmentName]);\n\nconst closeConnection = () => db.destroy();\n\nmodule.exports = {\n  db,\n  closeConnection\n};\n"
  },
  {
    "path": "chapter7/server/disconnectFromDb.js",
    "content": "const { db } = require(\"./dbConnection\");\n\nafterAll(() => db.destroy());\n"
  },
  {
    "path": "chapter7/server/inventoryController.js",
    "content": "const { db } = require(\"./dbConnection\");\n\nconst removeFromInventory = async itemName => {\n  const inventoryEntry = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName })\n    .first();\n\n  if (!inventoryEntry || inventoryEntry.quantity === 0) {\n    const err = new Error(`${itemName} is unavailable`);\n    err.code = 400;\n    throw err;\n  }\n\n  await db(\"inventory\")\n    .decrement(\"quantity\")\n    .where({ itemName });\n};\n\nmodule.exports = { removeFromInventory };\n"
  },
  {
    "path": "chapter7/server/jest.config.js",
    "content": "module.exports = {\n  testEnvironment: \"node\",\n  globalSetup: \"./migrateDatabases.js\",\n  setupFilesAfterEnv: [\n    \"<rootDir>/truncateTables.js\",\n    \"<rootDir>/seedUser.js\",\n    \"<rootDir>/disconnectFromDb.js\"\n  ]\n};\n"
  },
  {
    "path": "chapter7/server/knexfile.js",
    "content": "module.exports = {\n  test: {\n    client: \"sqlite3\",\n    connection: { filename: \"./test.sqlite\" },\n    useNullAsDefault: true\n  },\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"./dev.sqlite\" },\n    useNullAsDefault: true\n  }\n};\n"
  },
  {
    "path": "chapter7/server/logger.js",
    "content": "const fs = require(\"fs\");\n\nconst logger = {\n  log: msg => fs.appendFileSync(\"/tmp/logs.out\", msg + \"\\n\")\n};\n\nmodule.exports = logger;\n"
  },
  {
    "path": "chapter7/server/migrateDatabases.js",
    "content": "const environmentName = process.env.NODE_ENV || \"test\";\nconst environmentConfig = require(\"./knexfile\")[environmentName];\nconst db = require(\"knex\")(environmentConfig);\n\nmodule.exports = async () => {\n  // Migrate the database to the latest state\n  await db.migrate.latest();\n\n  // Close the connection to the database so that tests won't hang\n  await db.destroy();\n};\n"
  },
  {
    "path": "chapter7/server/migrations/20200325082401_initial_schema.js",
    "content": "exports.up = async knex => {\n  await knex.schema.createTable(\"users\", table => {\n    table.increments(\"id\");\n    table.string(\"username\");\n    table.unique(\"username\");\n    table.string(\"email\");\n    table.string(\"passwordHash\");\n  });\n\n  await knex.schema.createTable(\"carts_items\", table => {\n    table.integer(\"userId\").references(\"users.id\");\n    table.string(\"itemName\");\n    table.unique(\"itemName\");\n    table.integer(\"quantity\");\n  });\n\n  await knex.schema.createTable(\"inventory\", table => {\n    table.increments(\"id\");\n    table.string(\"itemName\");\n    table.unique(\"itemName\");\n    table.integer(\"quantity\");\n  });\n};\n\nexports.down = async knex => {\n  await knex.schema.dropTable(\"users\");\n  await knex.schema.dropTable(\"carts_items\");\n  await knex.schema.dropTable(\"inventory\");\n};\n"
  },
  {
    "path": "chapter7/server/migrations/20200331210311_updatedAt_field.js",
    "content": "exports.up = knex => {\n  return knex.schema.alterTable(\"carts_items\", table => {\n    table.timestamp(\"updatedAt\");\n  });\n};\n\nexports.down = knex => {\n  return knex.schema.alterTable(\"carts_items\", table => {\n    table.dropColumn(\"updatedAt\");\n  });\n};\n"
  },
  {
    "path": "chapter7/server/package.json",
    "content": "{\n  \"name\": \"chapter5_server\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"test\": \"jest --runInBand\",\n    \"start\": \"cross-env NODE_ENV=development node server.js\",\n    \"migrate:dev\": \"knex migrate:latest --env development\",\n    \"seed:dev\": \"knex seed:run\"\n  },\n  \"devDependencies\": {\n    \"@sinonjs/fake-timers\": \"github:sinonjs/fake-timers\",\n    \"jest\": \"^24.9.0\",\n    \"supertest\": \"^4.0.2\"\n  },\n  \"dependencies\": {\n    \"@koa/cors\": \"^3.0.0\",\n    \"cross-env\": \"^7.0.2\",\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"knex\": \"^0.20.13\",\n    \"koa\": \"^2.11.0\",\n    \"koa-body-parser\": \"^1.1.2\",\n    \"koa-router\": \"^7.4.0\",\n    \"koa-socket-2\": \"^1.2.0\",\n    \"nock\": \"^12.0.3\",\n    \"socket.io\": \"^2.3.0\",\n    \"sqlite3\": \"^4.1.1\"\n  },\n  \"main\": \"alertController.spec.js\",\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"description\": \"\"\n}\n"
  },
  {
    "path": "chapter7/server/seedUser.js",
    "content": "const { createUser } = require(\"./userTestUtils\");\n\nbeforeEach(createUser);\n"
  },
  {
    "path": "chapter7/server/seeds/initial_inventory.js",
    "content": "exports.seed = async knex => {\n  await knex(\"inventory\").del();\n  return knex(\"inventory\").insert([\n    { itemName: \"cheesecake\", quantity: 8 },\n    { itemName: \"apple pie\", quantity: 2 },\n    { itemName: \"carrot cake\", quantity: 5 }\n  ]);\n};\n"
  },
  {
    "path": "chapter7/server/server.js",
    "content": "const fetch = require(\"isomorphic-fetch\");\nconst Koa = require(\"koa\");\nconst http = require(\"http\");\nconst IO = require(\"koa-socket-2\");\nconst cors = require(\"@koa/cors\");\nconst Router = require(\"koa-router\");\nconst bodyParser = require(\"koa-body-parser\");\n\nconst { db } = require(\"./dbConnection\");\n\nconst { addItemToCart } = require(\"./cartController\");\nconst {\n  hashPassword,\n  authenticationMiddleware\n} = require(\"./authenticationController\");\n\nconst PORT = process.env.NODE_ENV === \"test\" ? 5000 : 3000;\n\nconst app = new Koa();\nconst io = new IO();\nio.attach(app);\n\nconst router = new Router();\n\napp.use(cors());\n\napp.use(bodyParser());\n\napp.use(async (ctx, next) => {\n  if (ctx.url.startsWith(\"/carts\")) {\n    return await authenticationMiddleware(ctx, next);\n  }\n\n  await next();\n});\n\nrouter.put(\"/users/:username\", async ctx => {\n  const { username } = ctx.params;\n  const { email, password } = ctx.request.body;\n\n  const userAlreadyExists = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n\n  if (userAlreadyExists) {\n    ctx.body = { message: `${username} already exists` };\n    ctx.status = 409;\n    return;\n  }\n\n  await db(\"users\").insert({\n    username,\n    email,\n    passwordHash: hashPassword(password)\n  });\n\n  return (ctx.body = { message: `${username} created successfully` });\n});\n\nrouter.post(\"/carts/:username/items\", async ctx => {\n  const { username } = ctx.params;\n  const { item, quantity } = ctx.request.body;\n\n  for (let i = 0; i < quantity; i++) {\n    try {\n      const newItems = await addItemToCart(username, item);\n      ctx.body = newItems;\n    } catch (e) {\n      ctx.body = { message: e.message };\n      ctx.status = e.code;\n      return;\n    }\n  }\n});\n\nrouter.delete(\"/carts/:username/items/:item\", async ctx => {\n  const { username, item } = ctx.params;\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n\n  if (!user) {\n    ctx.body = { message: \"user not found\" };\n    ctx.status = 404;\n    return;\n  }\n\n  const itemEntry = await db\n    .select()\n    .from(\"carts_items\")\n    .where({ userId: user.id, itemName: item })\n    .first();\n\n  if (!itemEntry || itemEntry.quantity === 0) {\n    ctx.body = { message: `${item} is not in the cart` };\n    ctx.status = 400;\n    return;\n  }\n\n  await db(\"carts_items\")\n    .decrement(\"quantity\")\n    .where({ userId: user.id, itemName: item });\n\n  const inventoryEntry = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName: item })\n    .first();\n  if (inventoryEntry) {\n    await db(\"inventory\")\n      .increment(\"quantity\")\n      .where({ userId: itemEntry.userId, itemName: item });\n  } else {\n    await db(\"inventory\").insert({ itemName: item, quantity: 1 });\n  }\n\n  ctx.body = await db\n    .select(\"itemName\", \"quantity\")\n    .from(\"carts_items\")\n    .where({ userId: user.id });\n});\n\nrouter.post(\"/inventory/:itemName\", async ctx => {\n  const { itemName } = ctx.params;\n  const { quantity } = ctx.request.body;\n  const clientId = ctx.request.headers[\"x-socket-client-id\"];\n\n  const current = await db\n    .select(\"itemName\", \"quantity\")\n    .from(\"inventory\")\n    .where({ itemName })\n    .first();\n\n  const itemExists = current && current.quantity > 0;\n  const newRecord = {\n    itemName,\n    quantity: (itemExists ? current.quantity : 0) + quantity\n  };\n\n  if (current) {\n    await db(\"inventory\")\n      .increment(\"quantity\", quantity)\n      .where({ itemName });\n  } else {\n    await db(\"inventory\").insert(newRecord);\n  }\n\n  Object.entries(io.socket.sockets.connected).forEach(([id, socket]) => {\n    if (id === clientId) return;\n    socket.emit(\"add_item\", { itemName, quantity });\n  });\n\n  ctx.body = newRecord;\n});\n\nrouter.delete(\"/inventory/:itemName\", async ctx => {\n  const { itemName } = ctx.params;\n  const { quantity } = ctx.request.body;\n\n  const current = await db\n    .select(\"itemName\", \"quantity\")\n    .from(\"inventory\")\n    .where({ itemName })\n    .first();\n\n  const canDelete = current && current.quantity > quantity;\n\n  if (canDelete) {\n    await db(\"inventory\")\n      .decrement(\"quantity\", quantity)\n      .where({ itemName });\n    ctx.body = { message: `Removed ${quantity} units of ${itemName}` };\n  } else {\n    ctx.status = 404;\n    ctx.body = {\n      message: `There aren't ${quantity} units of ${itemName} available.`\n    };\n  }\n});\n\nrouter.get(\"/inventory\", async ctx => {\n  const inventoryContent = await db\n    .select(\"itemName\", \"quantity\")\n    .from(\"inventory\")\n    .where(\"quantity\", \">\", 0)\n    .orderBy(\"quantity\", \"desc\");\n\n  ctx.body = inventoryContent.reduce((acc, { itemName, quantity }) => {\n    return { ...acc, [itemName]: quantity };\n  }, {});\n});\n\nrouter.get(\"/inventory/:itemName\", async ctx => {\n  const { itemName } = ctx.params;\n\n  const response = await fetch(`http://recipepuppy.com/api?i=${itemName}`);\n  const { title, href, results: recipes } = await response.json();\n  const inventoryItem = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName })\n    .first();\n\n  ctx.body = {\n    ...inventoryItem,\n    info: `Data obtained from ${title} - ${href}`,\n    recipes\n  };\n});\n\napp.use(router.routes());\n\nmodule.exports = { app: app.listen(PORT, \"127.0.0.1\") };\n"
  },
  {
    "path": "chapter7/server/server.test.js",
    "content": "const { user: globalUser } = require(\"./userTestUtils\");\nconst { db } = require(\"./dbConnection\");\nconst request = require(\"supertest\");\nconst { app } = require(\"./server.js\");\nconst { hashPassword } = require(\"./authenticationController.js\");\nconst nock = require(\"nock\");\n\nafterAll(() => app.close());\n\ndescribe(\"add items to a cart\", () => {\n  test(\"adding available items\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 3 });\n    const response = await request(app)\n      .post(`/carts/${globalUser.username}/items`)\n      .set(\"authorization\", globalUser.authHeader)\n      .send({ item: \"cheesecake\", quantity: 3 })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    const newItems = [{ itemName: \"cheesecake\", quantity: 3 }];\n    expect(response.body).toEqual(newItems);\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(0);\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n\n    expect(finalCartContent).toEqual(newItems);\n  });\n\n  test(\"adding unavailable items\", async () => {\n    const response = await request(app)\n      .post(`/carts/${globalUser.username}/items`)\n      .set(\"authorization\", globalUser.authHeader)\n      .send({ item: \"cheesecake\", quantity: 1 })\n      .expect(400)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"cheesecake is unavailable\"\n    });\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n    expect(finalCartContent).toEqual([]);\n  });\n});\n\ndescribe(\"removing items from a cart\", () => {\n  test(\"removing existing items\", async () => {\n    await db(\"carts_items\").insert({\n      userId: globalUser.id,\n      itemName: \"cheesecake\",\n      quantity: 1\n    });\n\n    const response = await request(app)\n      .del(`/carts/${globalUser.username}/items/cheesecake`)\n      .set(\"authorization\", globalUser.authHeader)\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    const expectedFinalContent = [{ itemName: \"cheesecake\", quantity: 0 }];\n\n    expect(response.body).toEqual(expectedFinalContent);\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n    expect(finalCartContent).toEqual(expectedFinalContent);\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(1);\n  });\n\n  test(\"removing non-existing items\", async () => {\n    await db(\"inventory\").insert({\n      itemName: \"cheesecake\",\n      quantity: 0\n    });\n\n    const response = await request(app)\n      .del(`/carts/${globalUser.username}/items/cheesecake`)\n      .set(\"authorization\", globalUser.authHeader)\n      .expect(400)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"cheesecake is not in the cart\"\n    });\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(0);\n  });\n});\n\ndescribe(\"create accounts\", () => {\n  test(\"creating a new account\", async () => {\n    const response = await request(app)\n      .put(\"/users/another_user\")\n      .send({ email: \"another_user@example.org\", password: \"a_password\" })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"another_user created successfully\"\n    });\n\n    const savedUser = await db\n      .select(\"email\", \"passwordHash\")\n      .from(\"users\")\n      .where({ username: \"another_user\" })\n      .first();\n\n    expect(savedUser).toEqual({\n      email: \"another_user@example.org\",\n      passwordHash: hashPassword(\"a_password\")\n    });\n  });\n\n  test(\"creating a duplicate account\", async () => {\n    const response = await request(app)\n      .put(`/users/${globalUser.username}`)\n      .send({ email: globalUser.email, password: \"a_password\" })\n      .expect(409)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: `${globalUser.username} already exists`\n    });\n  });\n});\n\ndescribe(\"list inventory items\", () => {\n  const eggs = { itemName: \"eggs\", quantity: 3 };\n  const applePie = { itemName: \"apple pie\", quantity: 1 };\n  const carrotCake = { itemName: \"carrot cake\", quantity: 0 };\n\n  beforeEach(async () => {\n    await db(\"inventory\").insert([eggs, applePie, carrotCake]);\n  });\n\n  test(\"fetching all available items\", async () => {\n    const { body } = await request(app)\n      .get(\"/inventory\")\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(body).toEqual({ eggs: 3, \"apple pie\": 1 });\n  });\n});\n\ndescribe(\"add inventory items\", () => {\n  test(\"adding a new item\", async () => {\n    const { body } = await request(app)\n      .post(\"/inventory/eggs\")\n      .send({ quantity: 3 })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(body).toEqual({ itemName: \"eggs\", quantity: 3 });\n\n    expect(\n      await db\n        .select(\"itemName\", \"quantity\")\n        .from(\"inventory\")\n        .where(\"itemName\", \"eggs\")\n        .first()\n    ).toEqual({ itemName: \"eggs\", quantity: 3 });\n  });\n\n  test(\"adding an existing item\", async () => {\n    const eggs = { itemName: \"eggs\", quantity: 2 };\n    await db(\"inventory\").insert(eggs);\n\n    const { body } = await request(app)\n      .post(\"/inventory/eggs\")\n      .send({ quantity: 3 })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(body).toEqual({ itemName: \"eggs\", quantity: 5 });\n\n    expect(\n      await db\n        .select(\"itemName\", \"quantity\")\n        .from(\"inventory\")\n        .where(\"itemName\", \"eggs\")\n        .first()\n    ).toEqual({ itemName: \"eggs\", quantity: 5 });\n  });\n});\n\ndescribe(\"remove inventory items\", () => {\n  beforeEach(async () => {\n    await db(\"inventory\").insert({ itemName: \"eggs\", quantity: 3 });\n  });\n\n  test(\"removing an item\", async () => {\n    const { body } = await request(app)\n      .del(\"/inventory/eggs\")\n      .send({ quantity: 2 })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(body).toEqual({\n      message: \"Removed 2 units of eggs\"\n    });\n\n    expect(\n      await db\n        .select(\"itemName\", \"quantity\")\n        .from(\"inventory\")\n        .where(\"itemName\", \"eggs\")\n        .first()\n    ).toEqual({ itemName: \"eggs\", quantity: 1 });\n  });\n\n  test(\"removing more than the inventory quantity\", async () => {\n    const { body } = await request(app)\n      .del(\"/inventory/eggs\")\n      .send({ quantity: 4 })\n      .expect(404)\n      .expect(\"Content-Type\", /json/);\n\n    expect(body).toEqual({\n      message: \"There aren't 4 units of eggs available.\"\n    });\n\n    expect(\n      await db\n        .select(\"itemName\", \"quantity\")\n        .from(\"inventory\")\n        .where(\"itemName\", \"eggs\")\n        .first()\n    ).toEqual({ itemName: \"eggs\", quantity: 3 });\n  });\n});\n\ndescribe(\"fetch inventory items\", () => {\n  const eggs = { itemName: \"eggs\", quantity: 3 };\n  const applePie = { itemName: \"apple pie\", quantity: 1 };\n\n  beforeEach(async () => {\n    await db(\"inventory\").insert([eggs, applePie]);\n    const { id: eggsId } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"eggs\" })\n      .first();\n    eggs.id = eggsId;\n  });\n\n  test(\"fetching an item from the inventory\", async () => {\n    const eggsResponse = {\n      title: \"FakeAPI\",\n      href: \"example.org\",\n      results: [{ name: \"Omelette du Fromage\" }]\n    };\n\n    nock(\"http://recipepuppy.com\")\n      .get(\"/api\")\n      .query({ i: \"eggs\" })\n      .reply(200, eggsResponse);\n\n    const response = await request(app)\n      .get(`/inventory/eggs`)\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      ...eggs,\n      info: `Data obtained from ${eggsResponse.title} - ${eggsResponse.href}`,\n      recipes: eggsResponse.results\n    });\n  });\n});\n"
  },
  {
    "path": "chapter7/server/truncateTables.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst tablesToTruncate = [\"users\", \"inventory\", \"carts_items\"];\n\nbeforeEach(() => {\n  return Promise.all(tablesToTruncate.map(t => db(t).truncate()));\n});\n"
  },
  {
    "path": "chapter7/server/userTestUtils.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { hashPassword } = require(\"./authenticationController\");\n\nconst username = \"test_user\";\nconst password = \"a_password\";\nconst passwordHash = hashPassword(password);\nconst email = \"test_user@example.org\";\nconst validAuth = Buffer.from(`${username}:${password}`).toString(\"base64\");\nconst authHeader = `Basic ${validAuth}`;\n\nconst user = {\n  username,\n  password,\n  email,\n  authHeader\n};\n\nconst createUser = async () => {\n  await db(\"users\").insert({ username, email, passwordHash });\n  const { id } = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  user.id = id;\n};\n\nmodule.exports = { user, createUser };\n"
  },
  {
    "path": "chapter8/1_testing_component_interaction/1_component_integration_tests/App.jsx",
    "content": "import React, { useEffect, useState, useRef } from \"react\";\nimport { API_ADDR } from \"./constants\";\nimport { ItemForm } from \"./ItemForm.jsx\";\nimport { ItemList } from \"./ItemList.jsx\";\n\nexport const App = () => {\n  const [items, setItems] = useState({});\n  const isMounted = useRef(null);\n\n  useEffect(() => {\n    isMounted.current = true;\n    const loadItems = async () => {\n      const response = await fetch(`${API_ADDR}/inventory`);\n      const responseBody = await response.json();\n      if (isMounted.current) setItems(responseBody);\n    };\n    loadItems();\n    return () => (isMounted.current = false);\n  }, []);\n\n  const updateItems = (itemAdded, addedQuantity) => {\n    const currentQuantity = items[itemAdded] || 0;\n    setItems({ ...items, [itemAdded]: currentQuantity + addedQuantity });\n  };\n\n  return (\n    <div>\n      <h1>Inventory Contents</h1>\n      <ItemList itemList={items} />\n      <ItemForm onItemAdded={updateItems} />\n    </div>\n  );\n};\n"
  },
  {
    "path": "chapter8/1_testing_component_interaction/1_component_integration_tests/App.test.jsx",
    "content": "import React from \"react\";\nimport nock from \"nock\";\nimport { API_ADDR } from \"./constants\";\nimport { App } from \"./App.jsx\";\nimport { generateItemText } from \"./ItemList.jsx\";\nimport { render, fireEvent, waitFor } from \"@testing-library/react\";\n\nbeforeEach(() => {\n  nock(API_ADDR)\n    .get(\"/inventory\")\n    .reply(200, { cheesecake: 2, croissant: 5, macaroon: 96 });\n});\n\nafterEach(() => {\n  if (!nock.isDone()) {\n    nock.cleanAll();\n    throw new Error(\"Not all mocked endpoints received requests.\");\n  }\n});\n\ntest(\"renders the appropriate header\", () => {\n  const { getByText } = render(<App />);\n  expect(getByText(\"Inventory Contents\")).toBeInTheDocument();\n});\n\ntest(\"rendering the server's list of items\", async () => {\n  const { getByText } = render(<App />);\n\n  await waitFor(() => {\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n  });\n\n  expect(getByText(generateItemText(\"cheesecake\", 2))).toBeInTheDocument();\n  expect(getByText(generateItemText(\"croissant\", 5))).toBeInTheDocument();\n  expect(getByText(generateItemText(\"macaroon\", 96))).toBeInTheDocument();\n});\n\ntest(\"updating the list of items with new items\", async () => {\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 6 }))\n    .reply(200);\n\n  const { getByText, getByPlaceholderText } = render(<App />);\n\n  await waitFor(() => {\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n  });\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"6\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  await waitFor(() => {\n    expect(getByText(generateItemText(\"cheesecake\", 8))).toBeInTheDocument();\n  });\n\n  const listElement = document.querySelector(\"ul\");\n  expect(listElement.childElementCount).toBe(3);\n\n  expect(getByText(generateItemText(\"croissant\", 5))).toBeInTheDocument();\n  expect(getByText(generateItemText(\"macaroon\", 96))).toBeInTheDocument();\n});\n"
  },
  {
    "path": "chapter8/1_testing_component_interaction/1_component_integration_tests/ItemForm.jsx",
    "content": "import React from \"react\";\nimport { API_ADDR } from \"./constants\";\n\nconst addItemRequest = (itemName, quantity) => {\n  fetch(`${API_ADDR}/inventory/${itemName}`, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ quantity })\n  });\n};\n\nexport const ItemForm = ({ onItemAdded }) => {\n  const [itemName, setItemName] = React.useState(\"\");\n  const [quantity, setQuantity] = React.useState(0);\n\n  const onSubmit = async e => {\n    e.preventDefault();\n    await addItemRequest(itemName, quantity);\n    if (onItemAdded) onItemAdded(itemName, quantity);\n  };\n\n  return (\n    <form onSubmit={onSubmit}>\n      <input\n        onChange={e => setItemName(e.target.value)}\n        placeholder=\"Item name\"\n      />\n      <input\n        onChange={e => setQuantity(parseInt(e.target.value, 10))}\n        placeholder=\"Quantity\"\n      />\n      <button type=\"submit\">Add item</button>\n    </form>\n  );\n};\n"
  },
  {
    "path": "chapter8/1_testing_component_interaction/1_component_integration_tests/ItemForm.test.jsx",
    "content": "import React from \"react\";\nimport nock from \"nock\";\nimport { API_ADDR } from \"./constants\";\nimport { ItemForm } from \"./ItemForm.jsx\";\nimport { render, fireEvent, waitFor } from \"@testing-library/react\";\n\ntest(\"form's elements\", () => {\n  const { getByText, getByPlaceholderText } = render(<ItemForm />);\n  expect(getByPlaceholderText(\"Item name\")).toBeInTheDocument();\n  expect(getByPlaceholderText(\"Quantity\")).toBeInTheDocument();\n  expect(getByText(\"Add item\")).toBeInTheDocument();\n});\n\ntest(\"sending requests\", () => {\n  const { getByText, getByPlaceholderText } = render(<ItemForm />);\n\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 2 }))\n    .reply(200);\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"2\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  expect(nock.isDone()).toBe(true);\n});\n\ntest(\"invoking the onItemAdded callback\", async () => {\n  const onItemAdded = jest.fn();\n  const { getByText, getByPlaceholderText } = render(\n    <ItemForm onItemAdded={onItemAdded} />\n  );\n\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 2 }))\n    .reply(200);\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"2\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  await waitFor(() => expect(nock.isDone()).toBe(true));\n\n  expect(onItemAdded).toHaveBeenCalledTimes(1);\n  expect(onItemAdded).toHaveBeenCalledWith(\"cheesecake\", 2);\n});\n"
  },
  {
    "path": "chapter8/1_testing_component_interaction/1_component_integration_tests/ItemList.jsx",
    "content": "import React from \"react\";\n\nexport const generateItemText = (itemName, quantity) => {\n  const capitalizedItemName =\n    itemName.charAt(0).toUpperCase() + itemName.slice(1);\n  return `${capitalizedItemName} - Quantity: ${quantity}`;\n};\n\nexport const ItemList = ({ itemList }) => {\n  return (\n    <ul>\n      {Object.entries(itemList).map(([itemName, quantity]) => {\n        return <li key={itemName}>{generateItemText(itemName, quantity)}</li>;\n      })}\n    </ul>\n  );\n};\n"
  },
  {
    "path": "chapter8/1_testing_component_interaction/1_component_integration_tests/ItemList.test.jsx",
    "content": "import React from \"react\";\nimport { ItemList, generateItemText } from \"./ItemList.jsx\";\nimport { render } from \"@testing-library/react\";\n\ndescribe(\"generateItemText\", () => {\n  test(\"generating an item's text\", () => {\n    expect(generateItemText(\"cheesecake\", 3)).toBe(\"Cheesecake - Quantity: 3\");\n    expect(generateItemText(\"apple pie\", 22)).toBe(\"Apple pie - Quantity: 22\");\n  });\n});\n\ndescribe(\"ItemList Component\", () => {\n  test(\"list items\", () => {\n    const itemList = { cheesecake: 2, croissant: 5, macaroon: 96 };\n    const { getByText } = render(<ItemList itemList={itemList} />);\n\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n    expect(getByText(generateItemText(\"cheesecake\", 2))).toBeInTheDocument();\n    expect(getByText(generateItemText(\"croissant\", 5))).toBeInTheDocument();\n    expect(getByText(generateItemText(\"macaroon\", 96))).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "chapter8/1_testing_component_interaction/1_component_integration_tests/babel.config.js",
    "content": "module.exports = {\n  presets: [\n    [\n      \"@babel/preset-env\",\n      {\n        targets: {\n          node: \"current\"\n        }\n      }\n    ],\n    \"@babel/preset-react\"\n  ]\n};\n"
  },
  {
    "path": "chapter8/1_testing_component_interaction/1_component_integration_tests/constants.js",
    "content": "export const API_ADDR = \"http://localhost:3000\";\n"
  },
  {
    "path": "chapter8/1_testing_component_interaction/1_component_integration_tests/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Inventory</title>\n  </head>\n  <body>\n    <div id=\"app\" />\n    <script src=\"bundle.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter8/1_testing_component_interaction/1_component_integration_tests/index.jsx",
    "content": "import ReactDOM from \"react-dom\";\nimport React from \"react\";\nimport { App } from \"./App.jsx\";\n\nReactDOM.render(<App />, document.getElementById(\"app\"));\n"
  },
  {
    "path": "chapter8/1_testing_component_interaction/1_component_integration_tests/jest.config.js",
    "content": "module.exports = {\n  setupFilesAfterEnv: [\n    \"<rootDir>/setupJestDom.js\",\n    \"<rootDir>/setupGlobalFetch.js\"\n  ]\n};\n"
  },
  {
    "path": "chapter8/1_testing_component_interaction/1_component_integration_tests/package.json",
    "content": "{\n  \"name\": \"1_component_integration_tests\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"build\": \"browserify index.jsx -o bundle.js\",\n    \"start\": \"http-server ./\",\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.9.6\",\n    \"@babel/preset-env\": \"^7.9.6\",\n    \"@babel/preset-react\": \"^7.9.4\",\n    \"@testing-library/dom\": \"^7.10.1\",\n    \"@testing-library/jest-dom\": \"^5.9.0\",\n    \"@testing-library/react\": \"^10.2.1\",\n    \"babelify\": \"^10.0.0\",\n    \"browserify\": \"^16.5.1\",\n    \"core-js\": \"^2.6.11\",\n    \"http-server\": \"^0.12.3\",\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"jest\": \"^25.5\",\n    \"nock\": \"^12.0.3\"\n  },\n  \"dependencies\": {\n    \"react\": \"^16.13.1\",\n    \"react-dom\": \"^16.13.1\"\n  },\n  \"browserify\": {\n    \"transform\": [\n      [\n        \"babelify\",\n        {\n          \"presets\": [\n            [\n              \"@babel/preset-env\",\n              {\n                \"useBuiltIns\": \"usage\",\n                \"corejs\": 2\n              }\n            ],\n            \"@babel/preset-react\"\n          ]\n        }\n      ]\n    ]\n  }\n}\n"
  },
  {
    "path": "chapter8/1_testing_component_interaction/1_component_integration_tests/setupGlobalFetch.js",
    "content": "const fetch = require(\"isomorphic-fetch\");\n\nglobal.window.fetch = fetch;\n"
  },
  {
    "path": "chapter8/1_testing_component_interaction/1_component_integration_tests/setupJestDom.js",
    "content": "const jestDom = require(\"@testing-library/jest-dom\");\n\nexpect.extend(jestDom);\n"
  },
  {
    "path": "chapter8/1_testing_component_interaction/2_stubbing_components/App.jsx",
    "content": "import React, { useEffect, useState, useRef } from \"react\";\nimport { API_ADDR } from \"./constants\";\nimport { ItemForm } from \"./ItemForm.jsx\";\nimport { ItemList } from \"./ItemList.jsx\";\n\nexport const App = () => {\n  const [items, setItems] = useState({});\n  const isMounted = useRef(null);\n\n  useEffect(() => {\n    isMounted.current = true;\n    const loadItems = async () => {\n      const response = await fetch(`${API_ADDR}/inventory`);\n      const responseBody = await response.json();\n      if (isMounted.current) setItems(responseBody);\n    };\n    loadItems();\n    return () => (isMounted.current = false);\n  }, []);\n\n  const updateItems = (itemAdded, addedQuantity) => {\n    const currentQuantity = items[itemAdded] || 0;\n    setItems({ ...items, [itemAdded]: currentQuantity + addedQuantity });\n  };\n\n  return (\n    <div>\n      <h1>Inventory Contents</h1>\n      <ItemList itemList={items} />\n      <ItemForm onItemAdded={updateItems} />\n    </div>\n  );\n};\n"
  },
  {
    "path": "chapter8/1_testing_component_interaction/2_stubbing_components/App.test.jsx",
    "content": "import React from \"react\";\nimport nock from \"nock\";\nimport { API_ADDR } from \"./constants\";\nimport { App } from \"./App.jsx\";\nimport { generateItemText } from \"./ItemList.jsx\";\nimport { render, fireEvent, waitFor } from \"@testing-library/react\";\n\njest.mock(\"react-spring/renderprops\");\n\nbeforeEach(() => {\n  nock(API_ADDR)\n    .get(\"/inventory\")\n    .reply(200, { cheesecake: 2, croissant: 5, macaroon: 96 });\n});\n\nafterEach(() => {\n  if (!nock.isDone()) {\n    nock.cleanAll();\n    throw new Error(\"Not all mocked endpoints received requests.\");\n  }\n});\n\ntest(\"renders the appropriate header\", () => {\n  const { getByText } = render(<App />);\n  expect(getByText(\"Inventory Contents\")).toBeInTheDocument();\n});\n\ntest(\"rendering the server's list of items\", async () => {\n  const { getByText } = render(<App />);\n\n  await waitFor(() => {\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n  });\n\n  expect(getByText(generateItemText(\"cheesecake\", 2))).toBeInTheDocument();\n  expect(getByText(generateItemText(\"croissant\", 5))).toBeInTheDocument();\n  expect(getByText(generateItemText(\"macaroon\", 96))).toBeInTheDocument();\n});\n\ntest(\"updating the list of items with new items\", async () => {\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 6 }))\n    .reply(200);\n\n  const { getByText, getByPlaceholderText } = render(<App />);\n\n  await waitFor(() => {\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n  });\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"6\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  await waitFor(() => {\n    expect(getByText(generateItemText(\"cheesecake\", 8))).toBeInTheDocument();\n  });\n\n  const listElement = document.querySelector(\"ul\");\n  expect(listElement.childElementCount).toBe(3);\n\n  expect(getByText(generateItemText(\"croissant\", 5))).toBeInTheDocument();\n  expect(getByText(generateItemText(\"macaroon\", 96))).toBeInTheDocument();\n});\n"
  },
  {
    "path": "chapter8/1_testing_component_interaction/2_stubbing_components/ItemForm.jsx",
    "content": "import React from \"react\";\nimport { API_ADDR } from \"./constants\";\n\nconst addItemRequest = (itemName, quantity) => {\n  fetch(`${API_ADDR}/inventory/${itemName}`, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ quantity })\n  });\n};\n\nexport const ItemForm = ({ onItemAdded }) => {\n  const [itemName, setItemName] = React.useState(\"\");\n  const [quantity, setQuantity] = React.useState(0);\n\n  const onSubmit = async e => {\n    e.preventDefault();\n    await addItemRequest(itemName, quantity);\n    if (onItemAdded) onItemAdded(itemName, quantity);\n  };\n\n  return (\n    <form onSubmit={onSubmit}>\n      <input\n        onChange={e => setItemName(e.target.value)}\n        placeholder=\"Item name\"\n      />\n      <input\n        onChange={e => setQuantity(parseInt(e.target.value, 10))}\n        placeholder=\"Quantity\"\n      />\n      <button type=\"submit\">Add item</button>\n    </form>\n  );\n};\n"
  },
  {
    "path": "chapter8/1_testing_component_interaction/2_stubbing_components/ItemForm.test.jsx",
    "content": "import React from \"react\";\nimport nock from \"nock\";\nimport { API_ADDR } from \"./constants\";\nimport { ItemForm } from \"./ItemForm.jsx\";\nimport { render, fireEvent, waitFor } from \"@testing-library/react\";\n\ntest(\"form's elements\", () => {\n  const { getByText, getByPlaceholderText } = render(<ItemForm />);\n  expect(getByPlaceholderText(\"Item name\")).toBeInTheDocument();\n  expect(getByPlaceholderText(\"Quantity\")).toBeInTheDocument();\n  expect(getByText(\"Add item\")).toBeInTheDocument();\n});\n\ntest(\"sending requests\", () => {\n  const { getByText, getByPlaceholderText } = render(<ItemForm />);\n\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 2 }))\n    .reply(200);\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"2\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  expect(nock.isDone()).toBe(true);\n});\n\ntest(\"invoking the onItemAdded callback\", async () => {\n  const onItemAdded = jest.fn();\n  const { getByText, getByPlaceholderText } = render(\n    <ItemForm onItemAdded={onItemAdded} />\n  );\n\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 2 }))\n    .reply(200);\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"2\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  await waitFor(() => expect(nock.isDone()).toBe(true));\n\n  expect(onItemAdded).toHaveBeenCalledTimes(1);\n  expect(onItemAdded).toHaveBeenCalledWith(\"cheesecake\", 2);\n});\n"
  },
  {
    "path": "chapter8/1_testing_component_interaction/2_stubbing_components/ItemList.jsx",
    "content": "import React from \"react\";\nimport { Transition } from \"react-spring/renderprops\";\n\nexport const generateItemText = (itemName, quantity) => {\n  const capitalizedItemName =\n    itemName.charAt(0).toUpperCase() + itemName.slice(1);\n  return `${capitalizedItemName} - Quantity: ${quantity}`;\n};\n\nexport const ItemList = ({ itemList }) => {\n  const items = Object.entries(itemList);\n\n  return (\n    <ul>\n      <Transition\n        items={items}\n        initial={null}\n        keys={([itemName]) => itemName}\n        from={{ fontSize: 0, opacity: 0 }}\n        enter={{ fontSize: 18, opacity: 1 }}\n      >\n        {([itemName, quantity]) => styleProps => (\n          <li key={itemName} style={styleProps}>\n            {generateItemText(itemName, quantity)}\n          </li>\n        )}\n      </Transition>\n    </ul>\n  );\n};\n"
  },
  {
    "path": "chapter8/1_testing_component_interaction/2_stubbing_components/ItemList.test.jsx",
    "content": "import React from \"react\";\nimport { ItemList, generateItemText } from \"./ItemList.jsx\";\nimport { render } from \"@testing-library/react\";\n\njest.mock(\"react-spring/renderprops\");\n\ndescribe(\"generateItemText\", () => {\n  test(\"generating an item's text\", () => {\n    expect(generateItemText(\"cheesecake\", 3)).toBe(\"Cheesecake - Quantity: 3\");\n    expect(generateItemText(\"apple pie\", 22)).toBe(\"Apple pie - Quantity: 22\");\n  });\n});\n\ndescribe(\"ItemList Component\", () => {\n  test(\"list items\", () => {\n    const itemList = { cheesecake: 2, croissant: 5, macaroon: 96 };\n    const { getByText } = render(<ItemList itemList={itemList} />);\n\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n    expect(getByText(generateItemText(\"cheesecake\", 2))).toBeInTheDocument();\n    expect(getByText(generateItemText(\"croissant\", 5))).toBeInTheDocument();\n    expect(getByText(generateItemText(\"macaroon\", 96))).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "chapter8/1_testing_component_interaction/2_stubbing_components/__mocks__/react-spring/renderprops.jsx",
    "content": "const FakeReactSpringTransition = jest.fn(({ items, children }) => {\n  return items.map(item => {\n    return children(item)({ fakeStyles: \"fake \" });\n  });\n});\n\nexport { FakeReactSpringTransition as Transition };\n"
  },
  {
    "path": "chapter8/1_testing_component_interaction/2_stubbing_components/babel.config.js",
    "content": "module.exports = {\n  presets: [\n    [\n      \"@babel/preset-env\",\n      {\n        targets: {\n          node: \"current\"\n        }\n      }\n    ],\n    \"@babel/preset-react\"\n  ]\n};\n"
  },
  {
    "path": "chapter8/1_testing_component_interaction/2_stubbing_components/constants.js",
    "content": "export const API_ADDR = \"http://localhost:3000\";\n"
  },
  {
    "path": "chapter8/1_testing_component_interaction/2_stubbing_components/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Inventory</title>\n  </head>\n  <body>\n    <div id=\"app\" />\n    <script src=\"bundle.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter8/1_testing_component_interaction/2_stubbing_components/index.jsx",
    "content": "import ReactDOM from \"react-dom\";\nimport React from \"react\";\nimport { App } from \"./App.jsx\";\n\nReactDOM.render(<App />, document.getElementById(\"app\"));\n"
  },
  {
    "path": "chapter8/1_testing_component_interaction/2_stubbing_components/jest.config.js",
    "content": "module.exports = {\n  setupFilesAfterEnv: [\n    \"<rootDir>/setupJestDom.js\",\n    \"<rootDir>/setupGlobalFetch.js\"\n  ]\n};\n"
  },
  {
    "path": "chapter8/1_testing_component_interaction/2_stubbing_components/package.json",
    "content": "{\n  \"name\": \"2_stubbing_components\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"build\": \"browserify index.jsx -p esmify -o bundle.js\",\n    \"start\": \"http-server ./\",\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.9.6\",\n    \"@babel/preset-env\": \"^7.9.6\",\n    \"@babel/preset-react\": \"^7.9.4\",\n    \"@testing-library/dom\": \"^7.10.1\",\n    \"@testing-library/jest-dom\": \"^5.9.0\",\n    \"@testing-library/react\": \"^10.2.1\",\n    \"babelify\": \"^10.0.0\",\n    \"browserify\": \"^16.5.1\",\n    \"core-js\": \"^2.6.11\",\n    \"esmify\": \"^2.1.1\",\n    \"http-server\": \"^0.12.3\",\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"jest\": \"^25.5\",\n    \"nock\": \"^12.0.3\"\n  },\n  \"dependencies\": {\n    \"react\": \"^16.13.1\",\n    \"react-dom\": \"^16.13.1\",\n    \"react-spring\": \"^8.0.27\"\n  },\n  \"browserify\": {\n    \"transform\": [\n      [\n        \"babelify\",\n        {\n          \"presets\": [\n            [\n              \"@babel/preset-env\",\n              {\n                \"useBuiltIns\": \"usage\",\n                \"corejs\": 2\n              }\n            ],\n            \"@babel/preset-react\"\n          ]\n        }\n      ]\n    ]\n  }\n}\n"
  },
  {
    "path": "chapter8/1_testing_component_interaction/2_stubbing_components/setupGlobalFetch.js",
    "content": "const fetch = require(\"isomorphic-fetch\");\n\nglobal.window.fetch = fetch;\n"
  },
  {
    "path": "chapter8/1_testing_component_interaction/2_stubbing_components/setupJestDom.js",
    "content": "const jestDom = require(\"@testing-library/jest-dom\");\n\nexpect.extend(jestDom);\n"
  },
  {
    "path": "chapter8/2_snapshot_testing/1_component_snapshots/ActionLog.jsx",
    "content": "import React from \"react\";\n\nexport const ActionLog = ({ actions }) => {\n  return (\n    <div data-testid=\"action-log\">\n      <h2>Action Log</h2>\n      <ul>\n        {actions.map(({ time, message, data }, i) => {\n          const date = new Date(time).toUTCString();\n          return (\n            <li key={i}>\n              Date: {date} - Message: {message} - Data: {JSON.stringify(data)}\n            </li>\n          );\n        })}\n      </ul>\n    </div>\n  );\n};\n"
  },
  {
    "path": "chapter8/2_snapshot_testing/1_component_snapshots/ActionLog.test.jsx",
    "content": "import React from \"react\";\nimport { ActionLog } from \"./ActionLog\";\nimport { render } from \"@testing-library/react\";\n\nconst daysToMs = days => days * 24 * 60 * 60 * 1000;\n\ntest(\"logging actions\", () => {\n  const actions = [\n    {\n      time: new Date(daysToMs(1)),\n      message: \"Loaded item list\",\n      data: { cheesecake: 2, macaroon: 5 }\n    },\n    {\n      time: new Date(daysToMs(2)),\n      message: \"Item added\",\n      data: { cheesecake: 2 }\n    },\n    {\n      time: new Date(daysToMs(3)),\n      message: \"Item removed\",\n      data: { cheesecake: 1 }\n    },\n    {\n      time: new Date(daysToMs(4)),\n      message: \"Something weird happened\",\n      data: { error: \"The cheesecake is a lie\" }\n    }\n  ];\n\n  const { container } = render(<ActionLog actions={actions} />);\n  expect(container).toMatchSnapshot();\n});\n"
  },
  {
    "path": "chapter8/2_snapshot_testing/1_component_snapshots/App.jsx",
    "content": "import React, { useEffect, useState, useRef } from \"react\";\nimport { API_ADDR } from \"./constants\";\nimport { ItemForm } from \"./ItemForm.jsx\";\nimport { ItemList } from \"./ItemList.jsx\";\nimport { ActionLog } from \"./ActionLog.jsx\";\n\nexport const App = () => {\n  const [items, setItems] = useState({});\n  const [actions, setActions] = useState([]);\n  const isMounted = useRef(null);\n\n  useEffect(() => {\n    isMounted.current = true;\n    const loadItems = async () => {\n      const response = await fetch(`${API_ADDR}/inventory`);\n      const responseBody = await response.json();\n      if (isMounted.current) {\n        setItems(responseBody);\n        setActions(\n          actions.concat({\n            time: new Date().toISOString(),\n            message: \"Loaded items from the server\",\n            data: { status: response.status, body: responseBody }\n          })\n        );\n      }\n    };\n    loadItems();\n    return () => (isMounted.current = false);\n  }, []);\n\n  const updateItems = (itemAdded, addedQuantity) => {\n    const currentQuantity = items[itemAdded] || 0;\n    setItems({ ...items, [itemAdded]: currentQuantity + addedQuantity });\n    setActions(\n      actions.concat({\n        time: new Date().toISOString(),\n        message: \"Item added\",\n        data: { itemAdded, addedQuantity }\n      })\n    );\n  };\n\n  return (\n    <div>\n      <h1>Inventory Contents</h1>\n      <ItemList itemList={items} />\n      <ItemForm onItemAdded={updateItems} />\n      <ActionLog actions={actions} />\n    </div>\n  );\n};\n"
  },
  {
    "path": "chapter8/2_snapshot_testing/1_component_snapshots/App.test.jsx",
    "content": "import React from \"react\";\nimport nock from \"nock\";\nimport { API_ADDR } from \"./constants\";\nimport { App } from \"./App.jsx\";\nimport { generateItemText } from \"./ItemList.jsx\";\nimport { render, fireEvent, waitFor } from \"@testing-library/react\";\n\njest.mock(\"react-spring/renderprops\");\n\nbeforeEach(() => {\n  nock(API_ADDR)\n    .get(\"/inventory\")\n    .reply(200, { cheesecake: 2, croissant: 5, macaroon: 96 });\n});\n\nafterEach(() => {\n  if (!nock.isDone()) {\n    nock.cleanAll();\n    throw new Error(\"Not all mocked endpoints received requests.\");\n  }\n});\n\ntest(\"renders the appropriate header\", () => {\n  const { getByText } = render(<App />);\n  expect(getByText(\"Inventory Contents\")).toBeInTheDocument();\n});\n\ntest(\"rendering the server's list of items\", async () => {\n  const { getByText } = render(<App />);\n\n  await waitFor(() => {\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n  });\n\n  expect(getByText(generateItemText(\"cheesecake\", 2))).toBeInTheDocument();\n  expect(getByText(generateItemText(\"croissant\", 5))).toBeInTheDocument();\n  expect(getByText(generateItemText(\"macaroon\", 96))).toBeInTheDocument();\n});\n\ntest(\"updating the list of items with new items\", async () => {\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 6 }))\n    .reply(200);\n\n  const { getByText, getByPlaceholderText } = render(<App />);\n\n  await waitFor(() => {\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n  });\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"6\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  await waitFor(() => {\n    expect(getByText(generateItemText(\"cheesecake\", 8))).toBeInTheDocument();\n  });\n\n  const listElement = document.querySelector(\"ul\");\n  expect(listElement.childElementCount).toBe(3);\n\n  expect(getByText(generateItemText(\"croissant\", 5))).toBeInTheDocument();\n  expect(getByText(generateItemText(\"macaroon\", 96))).toBeInTheDocument();\n});\n\ntest(\"updating the action log when loading items\", async () => {\n  jest\n    .spyOn(Date.prototype, \"toISOString\")\n    .mockReturnValue(\"2020-06-20T13:37:00.000Z\");\n\n  const { getByTestId } = render(<App />);\n  await waitFor(() => {\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n  });\n\n  const actionLog = getByTestId(\"action-log\");\n  expect(actionLog).toMatchSnapshot();\n});\n\ntest(\"updating the action log adding an item\", async () => {\n  jest\n    .spyOn(Date.prototype, \"toISOString\")\n    .mockReturnValueOnce(\"2020-06-20T13:37:00.000Z\");\n  jest\n    .spyOn(Date.prototype, \"toISOString\")\n    .mockReturnValueOnce(\"2020-06-21T13:37:00.000Z\");\n\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 6 }))\n    .reply(200);\n\n  const { getByTestId, getByText, getByPlaceholderText } = render(<App />);\n  await waitFor(() => {\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n  });\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"6\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  await waitFor(() => {\n    expect(getByText(generateItemText(\"cheesecake\", 8))).toBeInTheDocument();\n  });\n\n  const actionLog = getByTestId(\"action-log\");\n  expect(actionLog).toMatchSnapshot();\n});\n"
  },
  {
    "path": "chapter8/2_snapshot_testing/1_component_snapshots/ItemForm.jsx",
    "content": "import React from \"react\";\nimport { API_ADDR } from \"./constants\";\n\nconst addItemRequest = (itemName, quantity) => {\n  fetch(`${API_ADDR}/inventory/${itemName}`, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ quantity })\n  });\n};\n\nexport const ItemForm = ({ onItemAdded }) => {\n  const [itemName, setItemName] = React.useState(\"\");\n  const [quantity, setQuantity] = React.useState(0);\n\n  const onSubmit = async e => {\n    e.preventDefault();\n    await addItemRequest(itemName, quantity);\n    if (onItemAdded) onItemAdded(itemName, quantity);\n  };\n\n  return (\n    <form onSubmit={onSubmit}>\n      <input\n        onChange={e => setItemName(e.target.value)}\n        placeholder=\"Item name\"\n      />\n      <input\n        onChange={e => setQuantity(parseInt(e.target.value, 10))}\n        placeholder=\"Quantity\"\n      />\n      <button type=\"submit\">Add item</button>\n    </form>\n  );\n};\n"
  },
  {
    "path": "chapter8/2_snapshot_testing/1_component_snapshots/ItemForm.test.jsx",
    "content": "import React from \"react\";\nimport nock from \"nock\";\nimport { API_ADDR } from \"./constants\";\nimport { ItemForm } from \"./ItemForm.jsx\";\nimport { render, fireEvent, waitFor } from \"@testing-library/react\";\n\ntest(\"form's elements\", () => {\n  const { getByText, getByPlaceholderText } = render(<ItemForm />);\n  expect(getByPlaceholderText(\"Item name\")).toBeInTheDocument();\n  expect(getByPlaceholderText(\"Quantity\")).toBeInTheDocument();\n  expect(getByText(\"Add item\")).toBeInTheDocument();\n});\n\ntest(\"sending requests\", () => {\n  const { getByText, getByPlaceholderText } = render(<ItemForm />);\n\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 2 }))\n    .reply(200);\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"2\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  expect(nock.isDone()).toBe(true);\n});\n\ntest(\"invoking the onItemAdded callback\", async () => {\n  const onItemAdded = jest.fn();\n  const { getByText, getByPlaceholderText } = render(\n    <ItemForm onItemAdded={onItemAdded} />\n  );\n\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 2 }))\n    .reply(200);\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"2\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  await waitFor(() => expect(nock.isDone()).toBe(true));\n\n  expect(onItemAdded).toHaveBeenCalledTimes(1);\n  expect(onItemAdded).toHaveBeenCalledWith(\"cheesecake\", 2);\n});\n"
  },
  {
    "path": "chapter8/2_snapshot_testing/1_component_snapshots/ItemList.jsx",
    "content": "import React from \"react\";\nimport { Transition } from \"react-spring/renderprops\";\n\nexport const generateItemText = (itemName, quantity) => {\n  const capitalizedItemName =\n    itemName.charAt(0).toUpperCase() + itemName.slice(1);\n  return `${capitalizedItemName} - Quantity: ${quantity}`;\n};\n\nexport const ItemList = ({ itemList }) => {\n  const items = Object.entries(itemList);\n\n  return (\n    <ul>\n      <Transition\n        items={items}\n        initial={null}\n        keys={([itemName]) => itemName}\n        from={{ fontSize: 0, opacity: 0 }}\n        enter={{ fontSize: 18, opacity: 1 }}\n        leave={{ fontSize: 0, opacity: 0 }}\n      >\n        {([itemName, quantity]) => styleProps => (\n          <li key={itemName} style={styleProps}>\n            {generateItemText(itemName, quantity)}\n          </li>\n        )}\n      </Transition>\n    </ul>\n  );\n};\n"
  },
  {
    "path": "chapter8/2_snapshot_testing/1_component_snapshots/ItemList.test.jsx",
    "content": "import React from \"react\";\nimport { ItemList, generateItemText } from \"./ItemList.jsx\";\nimport { render } from \"@testing-library/react\";\n\njest.mock(\"react-spring/renderprops\");\n\ndescribe(\"generateItemText\", () => {\n  test(\"generating an item's text\", () => {\n    expect(generateItemText(\"cheesecake\", 3)).toBe(\"Cheesecake - Quantity: 3\");\n    expect(generateItemText(\"apple pie\", 22)).toBe(\"Apple pie - Quantity: 22\");\n  });\n});\n\ndescribe(\"ItemList Component\", () => {\n  test(\"list items\", () => {\n    const itemList = { cheesecake: 2, croissant: 5, macaroon: 96 };\n    const { getByText } = render(<ItemList itemList={itemList} />);\n\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n    expect(getByText(generateItemText(\"cheesecake\", 2))).toBeInTheDocument();\n    expect(getByText(generateItemText(\"croissant\", 5))).toBeInTheDocument();\n    expect(getByText(generateItemText(\"macaroon\", 96))).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "chapter8/2_snapshot_testing/1_component_snapshots/__mocks__/react-spring/renderprops.jsx",
    "content": "const FakeReactSpringTransition = jest.fn(({ items, children }) => {\n  return items.map(item => {\n    return children(item)({ fakeStyles: \"fake \" });\n  });\n});\n\nexport { FakeReactSpringTransition as Transition };\n"
  },
  {
    "path": "chapter8/2_snapshot_testing/1_component_snapshots/__snapshots__/ActionLog.test.jsx.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`logging actions 1`] = `\n<div>\n  <div\n    data-testid=\"action-log\"\n  >\n    <h2>\n      Action Log\n    </h2>\n    <ul>\n      <li>\n        Date: \n        Fri, 02 Jan 1970 00:00:00 GMT\n         - Message: \n        Loaded item list\n         - Data: \n        {\"cheesecake\":2,\"macaroon\":5}\n      </li>\n      <li>\n        Date: \n        Sat, 03 Jan 1970 00:00:00 GMT\n         - Message: \n        Item added\n         - Data: \n        {\"cheesecake\":2}\n      </li>\n      <li>\n        Date: \n        Sun, 04 Jan 1970 00:00:00 GMT\n         - Message: \n        Item removed\n         - Data: \n        {\"cheesecake\":1}\n      </li>\n      <li>\n        Date: \n        Mon, 05 Jan 1970 00:00:00 GMT\n         - Message: \n        Something weird happened\n         - Data: \n        {\"error\":\"The cheesecake is a lie\"}\n      </li>\n    </ul>\n  </div>\n</div>\n`;\n"
  },
  {
    "path": "chapter8/2_snapshot_testing/1_component_snapshots/__snapshots__/App.test.jsx.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`updating the action log adding an item 1`] = `\n<div\n  data-testid=\"action-log\"\n>\n  <h2>\n    Action Log\n  </h2>\n  <ul>\n    <li>\n      Date: \n      Sat, 20 Jun 2020 13:37:00 GMT\n       - Message: \n      Loaded items from the server\n       - Data: \n      {\"status\":200,\"body\":{\"cheesecake\":2,\"croissant\":5,\"macaroon\":96}}\n    </li>\n    <li>\n      Date: \n      Sun, 21 Jun 2020 13:37:00 GMT\n       - Message: \n      Item added\n       - Data: \n      {\"itemAdded\":\"cheesecake\",\"addedQuantity\":6}\n    </li>\n  </ul>\n</div>\n`;\n\nexports[`updating the action log when loading items 1`] = `\n<div\n  data-testid=\"action-log\"\n>\n  <h2>\n    Action Log\n  </h2>\n  <ul>\n    <li>\n      Date: \n      Sat, 20 Jun 2020 13:37:00 GMT\n       - Message: \n      Loaded items from the server\n       - Data: \n      {\"status\":200,\"body\":{\"cheesecake\":2,\"croissant\":5,\"macaroon\":96}}\n    </li>\n  </ul>\n</div>\n`;\n"
  },
  {
    "path": "chapter8/2_snapshot_testing/1_component_snapshots/babel.config.js",
    "content": "module.exports = {\n  presets: [\n    [\n      \"@babel/preset-env\",\n      {\n        targets: {\n          node: \"current\"\n        }\n      }\n    ],\n    \"@babel/preset-react\"\n  ]\n};\n"
  },
  {
    "path": "chapter8/2_snapshot_testing/1_component_snapshots/constants.js",
    "content": "export const API_ADDR = \"http://localhost:3000\";\n"
  },
  {
    "path": "chapter8/2_snapshot_testing/1_component_snapshots/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Inventory</title>\n  </head>\n  <body>\n    <div id=\"app\" />\n    <script src=\"bundle.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter8/2_snapshot_testing/1_component_snapshots/index.jsx",
    "content": "import ReactDOM from \"react-dom\";\nimport React from \"react\";\nimport { App } from \"./App.jsx\";\n\nReactDOM.render(<App />, document.getElementById(\"app\"));\n"
  },
  {
    "path": "chapter8/2_snapshot_testing/1_component_snapshots/jest.config.js",
    "content": "module.exports = {\n  setupFilesAfterEnv: [\n    \"<rootDir>/setupJestDom.js\",\n    \"<rootDir>/setupGlobalFetch.js\"\n  ]\n};\n"
  },
  {
    "path": "chapter8/2_snapshot_testing/1_component_snapshots/package.json",
    "content": "{\n  \"name\": \"1_component_snapshots\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"build\": \"browserify index.jsx -p esmify -o bundle.js\",\n    \"start\": \"http-server ./\",\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.9.6\",\n    \"@babel/preset-env\": \"^7.9.6\",\n    \"@babel/preset-react\": \"^7.9.4\",\n    \"@testing-library/dom\": \"^7.10.1\",\n    \"@testing-library/jest-dom\": \"^5.9.0\",\n    \"@testing-library/react\": \"^10.2.1\",\n    \"babelify\": \"^10.0.0\",\n    \"browserify\": \"^16.5.1\",\n    \"core-js\": \"^2.6.11\",\n    \"esmify\": \"^2.1.1\",\n    \"http-server\": \"^0.12.3\",\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"jest\": \"^25.5\",\n    \"nock\": \"^12.0.3\"\n  },\n  \"dependencies\": {\n    \"react\": \"^16.13.1\",\n    \"react-dom\": \"^16.13.1\",\n    \"react-spring\": \"^8.0.27\"\n  },\n  \"browserify\": {\n    \"transform\": [\n      [\n        \"babelify\",\n        {\n          \"presets\": [\n            [\n              \"@babel/preset-env\",\n              {\n                \"useBuiltIns\": \"usage\",\n                \"corejs\": 2\n              }\n            ],\n            \"@babel/preset-react\"\n          ]\n        }\n      ]\n    ]\n  }\n}\n"
  },
  {
    "path": "chapter8/2_snapshot_testing/1_component_snapshots/setupGlobalFetch.js",
    "content": "const fetch = require(\"isomorphic-fetch\");\n\nglobal.window.fetch = fetch;\n"
  },
  {
    "path": "chapter8/2_snapshot_testing/1_component_snapshots/setupJestDom.js",
    "content": "const jestDom = require(\"@testing-library/jest-dom\");\n\nexpect.extend(jestDom);\n"
  },
  {
    "path": "chapter8/2_snapshot_testing/2_snapshots_beyond_components/__snapshots__/generate_report.test.js.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`generating a .txt report 1`] = `\n\"cheesecake - Quantity: 8 - Value: 176\ncarrot cake - Quantity: 3 - Value: 54\nmacaroon - Quantity: 40 - Value: 240\nchocolate cake - Quantity: 12 - Value: 204\nTotal value: 63\"\n`;\n"
  },
  {
    "path": "chapter8/2_snapshot_testing/2_snapshots_beyond_components/generate_report.js",
    "content": "const fs = require(\"fs\");\n\nconst inventory = [\n  { item: \"cheesecake\", quantity: 8, price: 22 },\n  { item: \"carrot cake\", quantity: 3, price: 18 },\n  { item: \"macaroon\", quantity: 40, price: 6 },\n  { item: \"chocolate cake\", quantity: 12, price: 17 }\n];\n\nmodule.exports.generateReport = items => {\n  const lines = items.map(({ item, quantity, price }) => {\n    return `${item} - Quantity: ${quantity} - Value: ${price * quantity}`;\n  });\n  const totalValue = items.reduce((sum, { price }) => {\n    return sum + price;\n  }, 0);\n\n  const content = lines.concat(`Total value: ${totalValue}`).join(\"\\n\");\n  fs.writeFileSync(\"/tmp/report.txt\", content);\n};\n\nmodule.exports.generateReport(inventory);\n"
  },
  {
    "path": "chapter8/2_snapshot_testing/2_snapshots_beyond_components/generate_report.test.js",
    "content": "const fs = require(\"fs\");\nconst { generateReport } = require(\"./generate_report\");\n\ntest(\"generating a .txt report\", () => {\n  const inventory = [\n    { item: \"cheesecake\", quantity: 8, price: 22 },\n    { item: \"carrot cake\", quantity: 3, price: 18 },\n    { item: \"macaroon\", quantity: 40, price: 6 },\n    { item: \"chocolate cake\", quantity: 12, price: 17 }\n  ];\n\n  generateReport(inventory);\n  const report = fs.readFileSync(\"/tmp/report.txt\", \"utf-8\");\n  expect(report).toMatchSnapshot();\n});\n"
  },
  {
    "path": "chapter8/2_snapshot_testing/2_snapshots_beyond_components/package.json",
    "content": "{\n  \"name\": \"2_snapshots_beyond_components\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"jest\": \"^25.5\"\n  },\n  \"dependencies\": {}\n}\n"
  },
  {
    "path": "chapter8/3_testing_styles/1_css_classes/ActionLog.jsx",
    "content": "import React from \"react\";\n\nexport const ActionLog = ({ actions }) => {\n  return (\n    <div data-testid=\"action-log\">\n      <h2>Action Log</h2>\n      <ul>\n        {actions.map(({ time, message, data }, i) => {\n          const date = new Date(time).toUTCString();\n          return (\n            <li key={i}>\n              Date: {date} - Message: {message} - Data: {JSON.stringify(data)}\n            </li>\n          );\n        })}\n      </ul>\n    </div>\n  );\n};\n"
  },
  {
    "path": "chapter8/3_testing_styles/1_css_classes/ActionLog.test.jsx",
    "content": "import React from \"react\";\nimport { ActionLog } from \"./ActionLog\";\nimport { render } from \"@testing-library/react\";\n\nconst daysToMs = days => days * 24 * 60 * 60 * 1000;\n\ntest(\"logging actions\", () => {\n  const actions = [\n    {\n      time: new Date(daysToMs(1)),\n      message: \"Loaded item list\",\n      data: { cheesecake: 2, macaroon: 5 }\n    },\n    {\n      time: new Date(daysToMs(2)),\n      message: \"Item added\",\n      data: { cheesecake: 2 }\n    },\n    {\n      time: new Date(daysToMs(3)),\n      message: \"Item removed\",\n      data: { cheesecake: 1 }\n    },\n    {\n      time: new Date(daysToMs(4)),\n      message: \"Something weird happened\",\n      data: { error: \"The cheesecake is a lie\" }\n    }\n  ];\n\n  const { container } = render(<ActionLog actions={actions} />);\n  expect(container).toMatchSnapshot();\n});\n"
  },
  {
    "path": "chapter8/3_testing_styles/1_css_classes/App.jsx",
    "content": "import React, { useEffect, useState, useRef } from \"react\";\nimport { API_ADDR } from \"./constants\";\nimport { ItemForm } from \"./ItemForm.jsx\";\nimport { ItemList } from \"./ItemList.jsx\";\nimport { ActionLog } from \"./ActionLog.jsx\";\n\nexport const App = () => {\n  const [items, setItems] = useState({});\n  const [actions, setActions] = useState([]);\n  const isMounted = useRef(null);\n\n  useEffect(() => {\n    isMounted.current = true;\n    const loadItems = async () => {\n      const response = await fetch(`${API_ADDR}/inventory`);\n      const responseBody = await response.json();\n      if (isMounted.current) {\n        setItems(responseBody);\n        setActions(\n          actions.concat({\n            time: new Date().toISOString(),\n            message: \"Loaded items from the server\",\n            data: { status: response.status, body: responseBody }\n          })\n        );\n      }\n    };\n    loadItems();\n    return () => (isMounted.current = false);\n  }, []);\n\n  const updateItems = (itemAdded, addedQuantity) => {\n    const currentQuantity = items[itemAdded] || 0;\n    setItems({ ...items, [itemAdded]: currentQuantity + addedQuantity });\n    setActions(\n      actions.concat({\n        time: new Date().toISOString(),\n        message: \"Item added\",\n        data: { itemAdded, addedQuantity }\n      })\n    );\n  };\n\n  return (\n    <div>\n      <h1>Inventory Contents</h1>\n      <ItemList itemList={items} />\n      <ItemForm onItemAdded={updateItems} />\n      <ActionLog actions={actions} />\n    </div>\n  );\n};\n"
  },
  {
    "path": "chapter8/3_testing_styles/1_css_classes/App.test.jsx",
    "content": "import React from \"react\";\nimport nock from \"nock\";\nimport { API_ADDR } from \"./constants\";\nimport { App } from \"./App.jsx\";\nimport { generateItemText } from \"./ItemList.jsx\";\nimport { render, fireEvent, waitFor } from \"@testing-library/react\";\n\njest.mock(\"react-spring/renderprops\");\n\nbeforeEach(() => {\n  nock(API_ADDR)\n    .get(\"/inventory\")\n    .reply(200, { cheesecake: 2, croissant: 5, macaroon: 96 });\n});\n\nafterEach(() => {\n  if (!nock.isDone()) {\n    nock.cleanAll();\n    throw new Error(\"Not all mocked endpoints received requests.\");\n  }\n});\n\ntest(\"renders the appropriate header\", () => {\n  const { getByText } = render(<App />);\n  expect(getByText(\"Inventory Contents\")).toBeInTheDocument();\n});\n\ntest(\"rendering the server's list of items\", async () => {\n  const { getByText } = render(<App />);\n\n  await waitFor(() => {\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n  });\n\n  expect(getByText(generateItemText(\"cheesecake\", 2))).toBeInTheDocument();\n  expect(getByText(generateItemText(\"croissant\", 5))).toBeInTheDocument();\n  expect(getByText(generateItemText(\"macaroon\", 96))).toBeInTheDocument();\n});\n\ntest(\"updating the list of items with new items\", async () => {\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 6 }))\n    .reply(200);\n\n  const { getByText, getByPlaceholderText } = render(<App />);\n\n  await waitFor(() => {\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n  });\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"6\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  await waitFor(() => {\n    expect(getByText(generateItemText(\"cheesecake\", 8))).toBeInTheDocument();\n  });\n\n  const listElement = document.querySelector(\"ul\");\n  expect(listElement.childElementCount).toBe(3);\n\n  expect(getByText(generateItemText(\"croissant\", 5))).toBeInTheDocument();\n  expect(getByText(generateItemText(\"macaroon\", 96))).toBeInTheDocument();\n});\n\ntest(\"updating the action log when loading items\", async () => {\n  jest\n    .spyOn(Date.prototype, \"toISOString\")\n    .mockReturnValue(\"2020-06-20T13:37:00.000Z\");\n\n  const { getByTestId } = render(<App />);\n  await waitFor(() => {\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n  });\n\n  const actionLog = getByTestId(\"action-log\");\n  expect(actionLog).toMatchSnapshot();\n});\n\ntest(\"updating the action log adding an item\", async () => {\n  jest\n    .spyOn(Date.prototype, \"toISOString\")\n    .mockReturnValueOnce(\"2020-06-20T13:37:00.000Z\");\n  jest\n    .spyOn(Date.prototype, \"toISOString\")\n    .mockReturnValueOnce(\"2020-06-21T13:37:00.000Z\");\n\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 6 }))\n    .reply(200);\n\n  const { getByTestId, getByText, getByPlaceholderText } = render(<App />);\n  await waitFor(() => {\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n  });\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"6\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  await waitFor(() => {\n    expect(getByText(generateItemText(\"cheesecake\", 8))).toBeInTheDocument();\n  });\n\n  const actionLog = getByTestId(\"action-log\");\n  expect(actionLog).toMatchSnapshot();\n});\n"
  },
  {
    "path": "chapter8/3_testing_styles/1_css_classes/ItemForm.jsx",
    "content": "import React from \"react\";\nimport { API_ADDR } from \"./constants\";\n\nconst addItemRequest = (itemName, quantity) => {\n  fetch(`${API_ADDR}/inventory/${itemName}`, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ quantity })\n  });\n};\n\nexport const ItemForm = ({ onItemAdded }) => {\n  const [itemName, setItemName] = React.useState(\"\");\n  const [quantity, setQuantity] = React.useState(0);\n\n  const onSubmit = async e => {\n    e.preventDefault();\n    await addItemRequest(itemName, quantity);\n    if (onItemAdded) onItemAdded(itemName, quantity);\n  };\n\n  return (\n    <form onSubmit={onSubmit}>\n      <input\n        onChange={e => setItemName(e.target.value)}\n        placeholder=\"Item name\"\n      />\n      <input\n        onChange={e => setQuantity(parseInt(e.target.value, 10))}\n        placeholder=\"Quantity\"\n      />\n      <button type=\"submit\">Add item</button>\n    </form>\n  );\n};\n"
  },
  {
    "path": "chapter8/3_testing_styles/1_css_classes/ItemForm.test.jsx",
    "content": "import React from \"react\";\nimport nock from \"nock\";\nimport { API_ADDR } from \"./constants\";\nimport { ItemForm } from \"./ItemForm.jsx\";\nimport { render, fireEvent, waitFor } from \"@testing-library/react\";\n\ntest(\"form's elements\", () => {\n  const { getByText, getByPlaceholderText } = render(<ItemForm />);\n  expect(getByPlaceholderText(\"Item name\")).toBeInTheDocument();\n  expect(getByPlaceholderText(\"Quantity\")).toBeInTheDocument();\n  expect(getByText(\"Add item\")).toBeInTheDocument();\n});\n\ntest(\"sending requests\", () => {\n  const { getByText, getByPlaceholderText } = render(<ItemForm />);\n\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 2 }))\n    .reply(200);\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"2\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  expect(nock.isDone()).toBe(true);\n});\n\ntest(\"invoking the onItemAdded callback\", async () => {\n  const onItemAdded = jest.fn();\n  const { getByText, getByPlaceholderText } = render(\n    <ItemForm onItemAdded={onItemAdded} />\n  );\n\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 2 }))\n    .reply(200);\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"2\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  await waitFor(() => expect(nock.isDone()).toBe(true));\n\n  expect(onItemAdded).toHaveBeenCalledTimes(1);\n  expect(onItemAdded).toHaveBeenCalledWith(\"cheesecake\", 2);\n});\n"
  },
  {
    "path": "chapter8/3_testing_styles/1_css_classes/ItemList.jsx",
    "content": "import React from \"react\";\nimport { Transition } from \"react-spring/renderprops\";\n\nexport const generateItemText = (itemName, quantity) => {\n  const capitalizedItemName =\n    itemName.charAt(0).toUpperCase() + itemName.slice(1);\n  return `${capitalizedItemName} - Quantity: ${quantity}`;\n};\n\nexport const ItemList = ({ itemList }) => {\n  const items = Object.entries(itemList);\n\n  return (\n    <ul>\n      <Transition\n        items={items}\n        initial={null}\n        keys={([itemName]) => itemName}\n        from={{ fontSize: 0, opacity: 0 }}\n        enter={{ fontSize: 18, opacity: 1 }}\n        leave={{ fontSize: 0, opacity: 0 }}\n      >\n        {([itemName, quantity]) => styleProps => (\n          <li\n            key={itemName}\n            className={quantity < 5 ? \"almost-out-of-stock\" : null}\n            style={styleProps}\n          >\n            {generateItemText(itemName, quantity)}\n          </li>\n        )}\n      </Transition>\n    </ul>\n  );\n};\n"
  },
  {
    "path": "chapter8/3_testing_styles/1_css_classes/ItemList.test.jsx",
    "content": "import React from \"react\";\nimport { ItemList, generateItemText } from \"./ItemList.jsx\";\nimport { render } from \"@testing-library/react\";\n\njest.mock(\"react-spring/renderprops\");\n\ndescribe(\"generateItemText\", () => {\n  test(\"generating an item's text\", () => {\n    expect(generateItemText(\"cheesecake\", 3)).toBe(\"Cheesecake - Quantity: 3\");\n    expect(generateItemText(\"apple pie\", 22)).toBe(\"Apple pie - Quantity: 22\");\n  });\n});\n\ndescribe(\"ItemList Component\", () => {\n  test(\"list items\", () => {\n    const itemList = { cheesecake: 2, croissant: 5, macaroon: 96 };\n    const { getByText } = render(<ItemList itemList={itemList} />);\n\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n    expect(getByText(generateItemText(\"cheesecake\", 2))).toBeInTheDocument();\n    expect(getByText(generateItemText(\"croissant\", 5))).toBeInTheDocument();\n    expect(getByText(generateItemText(\"macaroon\", 96))).toBeInTheDocument();\n  });\n\n  test(\"highlighting items that are almost out of stock\", () => {\n    const itemList = { cheesecake: 2, croissant: 5, macaroon: 96 };\n\n    const { getByText } = render(<ItemList itemList={itemList} />);\n    const cheesecakeItem = getByText(generateItemText(\"cheesecake\", 2));\n    expect(cheesecakeItem).toHaveClass(\"almost-out-of-stock\");\n  });\n});\n"
  },
  {
    "path": "chapter8/3_testing_styles/1_css_classes/__mocks__/react-spring/renderprops.jsx",
    "content": "const FakeReactSpringTransition = jest.fn(({ items, children }) => {\n  return items.map(item => {\n    return children(item)({ fakeStyles: \"fake \" });\n  });\n});\n\nexport { FakeReactSpringTransition as Transition };\n"
  },
  {
    "path": "chapter8/3_testing_styles/1_css_classes/__snapshots__/ActionLog.test.jsx.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`logging actions 1`] = `\n<div>\n  <div\n    data-testid=\"action-log\"\n  >\n    <h2>\n      Action Log\n    </h2>\n    <ul>\n      <li>\n        Date: \n        Fri, 02 Jan 1970 00:00:00 GMT\n         - Message: \n        Loaded item list\n         - Data: \n        {\"cheesecake\":2,\"macaroon\":5}\n      </li>\n      <li>\n        Date: \n        Sat, 03 Jan 1970 00:00:00 GMT\n         - Message: \n        Item added\n         - Data: \n        {\"cheesecake\":2}\n      </li>\n      <li>\n        Date: \n        Sun, 04 Jan 1970 00:00:00 GMT\n         - Message: \n        Item removed\n         - Data: \n        {\"cheesecake\":1}\n      </li>\n      <li>\n        Date: \n        Mon, 05 Jan 1970 00:00:00 GMT\n         - Message: \n        Something weird happened\n         - Data: \n        {\"error\":\"The cheesecake is a lie\"}\n      </li>\n    </ul>\n  </div>\n</div>\n`;\n"
  },
  {
    "path": "chapter8/3_testing_styles/1_css_classes/__snapshots__/App.test.jsx.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`updating the action log adding an item 1`] = `\n<div\n  data-testid=\"action-log\"\n>\n  <h2>\n    Action Log\n  </h2>\n  <ul>\n    <li>\n      Date: \n      Sat, 20 Jun 2020 13:37:00 GMT\n       - Message: \n      Loaded items from the server\n       - Data: \n      {\"status\":200,\"body\":{\"cheesecake\":2,\"croissant\":5,\"macaroon\":96}}\n    </li>\n    <li>\n      Date: \n      Sun, 21 Jun 2020 13:37:00 GMT\n       - Message: \n      Item added\n       - Data: \n      {\"itemAdded\":\"cheesecake\",\"addedQuantity\":6}\n    </li>\n  </ul>\n</div>\n`;\n\nexports[`updating the action log when loading items 1`] = `\n<div\n  data-testid=\"action-log\"\n>\n  <h2>\n    Action Log\n  </h2>\n  <ul>\n    <li>\n      Date: \n      Sat, 20 Jun 2020 13:37:00 GMT\n       - Message: \n      Loaded items from the server\n       - Data: \n      {\"status\":200,\"body\":{\"cheesecake\":2,\"croissant\":5,\"macaroon\":96}}\n    </li>\n  </ul>\n</div>\n`;\n"
  },
  {
    "path": "chapter8/3_testing_styles/1_css_classes/babel.config.js",
    "content": "module.exports = {\n  presets: [\n    [\n      \"@babel/preset-env\",\n      {\n        targets: {\n          node: \"current\"\n        }\n      }\n    ],\n    \"@babel/preset-react\"\n  ]\n};\n"
  },
  {
    "path": "chapter8/3_testing_styles/1_css_classes/constants.js",
    "content": "export const API_ADDR = \"http://localhost:3000\";\n"
  },
  {
    "path": "chapter8/3_testing_styles/1_css_classes/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Inventory</title>\n    <link rel=\"stylesheet\" href=\"./styles.css\" />\n  </head>\n  <body>\n    <div id=\"app\" />\n    <script src=\"bundle.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter8/3_testing_styles/1_css_classes/index.jsx",
    "content": "import ReactDOM from \"react-dom\";\nimport React from \"react\";\nimport { App } from \"./App.jsx\";\n\nReactDOM.render(<App />, document.getElementById(\"app\"));\n"
  },
  {
    "path": "chapter8/3_testing_styles/1_css_classes/jest.config.js",
    "content": "module.exports = {\n  setupFilesAfterEnv: [\n    \"<rootDir>/setupJestDom.js\",\n    \"<rootDir>/setupGlobalFetch.js\"\n  ]\n};\n"
  },
  {
    "path": "chapter8/3_testing_styles/1_css_classes/package.json",
    "content": "{\n  \"name\": \"1_testing_styles\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"build\": \"browserify index.jsx -p esmify -o bundle.js\",\n    \"start\": \"http-server ./\",\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.9.6\",\n    \"@babel/preset-env\": \"^7.9.6\",\n    \"@babel/preset-react\": \"^7.9.4\",\n    \"@testing-library/dom\": \"^7.10.1\",\n    \"@testing-library/jest-dom\": \"^5.9.0\",\n    \"@testing-library/react\": \"^10.2.1\",\n    \"babelify\": \"^10.0.0\",\n    \"browserify\": \"^16.5.1\",\n    \"core-js\": \"^2.6.11\",\n    \"esmify\": \"^2.1.1\",\n    \"http-server\": \"^0.12.3\",\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"jest\": \"^25.5\",\n    \"nock\": \"^12.0.3\"\n  },\n  \"dependencies\": {\n    \"react\": \"^16.13.1\",\n    \"react-dom\": \"^16.13.1\",\n    \"react-spring\": \"^8.0.27\"\n  },\n  \"browserify\": {\n    \"transform\": [\n      [\n        \"babelify\",\n        {\n          \"presets\": [\n            [\n              \"@babel/preset-env\",\n              {\n                \"useBuiltIns\": \"usage\",\n                \"corejs\": 2\n              }\n            ],\n            \"@babel/preset-react\"\n          ]\n        }\n      ]\n    ]\n  }\n}\n"
  },
  {
    "path": "chapter8/3_testing_styles/1_css_classes/setupGlobalFetch.js",
    "content": "const fetch = require(\"isomorphic-fetch\");\n\nglobal.window.fetch = fetch;\n"
  },
  {
    "path": "chapter8/3_testing_styles/1_css_classes/setupJestDom.js",
    "content": "const jestDom = require(\"@testing-library/jest-dom\");\n\nexpect.extend(jestDom);\n"
  },
  {
    "path": "chapter8/3_testing_styles/1_css_classes/styles.css",
    "content": ".almost-out-of-stock {\n  font-weight: bold;\n  color: red;\n}\n"
  },
  {
    "path": "chapter8/3_testing_styles/2_style_props/ActionLog.jsx",
    "content": "import React from \"react\";\n\nexport const ActionLog = ({ actions }) => {\n  return (\n    <div data-testid=\"action-log\">\n      <h2>Action Log</h2>\n      <ul>\n        {actions.map(({ time, message, data }, i) => {\n          const date = new Date(time).toUTCString();\n          return (\n            <li key={i}>\n              Date: {date} - Message: {message} - Data: {JSON.stringify(data)}\n            </li>\n          );\n        })}\n      </ul>\n    </div>\n  );\n};\n"
  },
  {
    "path": "chapter8/3_testing_styles/2_style_props/ActionLog.test.jsx",
    "content": "import React from \"react\";\nimport { ActionLog } from \"./ActionLog\";\nimport { render } from \"@testing-library/react\";\n\nconst daysToMs = days => days * 24 * 60 * 60 * 1000;\n\ntest(\"logging actions\", () => {\n  const actions = [\n    {\n      time: new Date(daysToMs(1)),\n      message: \"Loaded item list\",\n      data: { cheesecake: 2, macaroon: 5 }\n    },\n    {\n      time: new Date(daysToMs(2)),\n      message: \"Item added\",\n      data: { cheesecake: 2 }\n    },\n    {\n      time: new Date(daysToMs(3)),\n      message: \"Item removed\",\n      data: { cheesecake: 1 }\n    },\n    {\n      time: new Date(daysToMs(4)),\n      message: \"Something weird happened\",\n      data: { error: \"The cheesecake is a lie\" }\n    }\n  ];\n\n  const { container } = render(<ActionLog actions={actions} />);\n  expect(container).toMatchSnapshot();\n});\n"
  },
  {
    "path": "chapter8/3_testing_styles/2_style_props/App.jsx",
    "content": "import React, { useEffect, useState, useRef } from \"react\";\nimport { API_ADDR } from \"./constants\";\nimport { ItemForm } from \"./ItemForm.jsx\";\nimport { ItemList } from \"./ItemList.jsx\";\nimport { ActionLog } from \"./ActionLog.jsx\";\n\nexport const App = () => {\n  const [items, setItems] = useState({});\n  const [actions, setActions] = useState([]);\n  const isMounted = useRef(null);\n\n  useEffect(() => {\n    isMounted.current = true;\n    const loadItems = async () => {\n      const response = await fetch(`${API_ADDR}/inventory`);\n      const responseBody = await response.json();\n      if (isMounted.current) {\n        setItems(responseBody);\n        setActions(\n          actions.concat({\n            time: new Date().toISOString(),\n            message: \"Loaded items from the server\",\n            data: { status: response.status, body: responseBody }\n          })\n        );\n      }\n    };\n    loadItems();\n    return () => (isMounted.current = false);\n  }, []);\n\n  const updateItems = (itemAdded, addedQuantity) => {\n    const currentQuantity = items[itemAdded] || 0;\n    setItems({ ...items, [itemAdded]: currentQuantity + addedQuantity });\n    setActions(\n      actions.concat({\n        time: new Date().toISOString(),\n        message: \"Item added\",\n        data: { itemAdded, addedQuantity }\n      })\n    );\n  };\n\n  return (\n    <div>\n      <h1>Inventory Contents</h1>\n      <ItemList itemList={items} />\n      <ItemForm onItemAdded={updateItems} />\n      <ActionLog actions={actions} />\n    </div>\n  );\n};\n"
  },
  {
    "path": "chapter8/3_testing_styles/2_style_props/App.test.jsx",
    "content": "import React from \"react\";\nimport nock from \"nock\";\nimport { API_ADDR } from \"./constants\";\nimport { App } from \"./App.jsx\";\nimport { generateItemText } from \"./ItemList.jsx\";\nimport { render, fireEvent, waitFor } from \"@testing-library/react\";\n\njest.mock(\"react-spring/renderprops\");\n\nbeforeEach(() => {\n  nock(API_ADDR)\n    .get(\"/inventory\")\n    .reply(200, { cheesecake: 2, croissant: 5, macaroon: 96 });\n});\n\nafterEach(() => {\n  if (!nock.isDone()) {\n    nock.cleanAll();\n    throw new Error(\"Not all mocked endpoints received requests.\");\n  }\n});\n\ntest(\"renders the appropriate header\", () => {\n  const { getByText } = render(<App />);\n  expect(getByText(\"Inventory Contents\")).toBeInTheDocument();\n});\n\ntest(\"rendering the server's list of items\", async () => {\n  const { getByText } = render(<App />);\n\n  await waitFor(() => {\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n  });\n\n  expect(getByText(generateItemText(\"cheesecake\", 2))).toBeInTheDocument();\n  expect(getByText(generateItemText(\"croissant\", 5))).toBeInTheDocument();\n  expect(getByText(generateItemText(\"macaroon\", 96))).toBeInTheDocument();\n});\n\ntest(\"updating the list of items with new items\", async () => {\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 6 }))\n    .reply(200);\n\n  const { getByText, getByPlaceholderText } = render(<App />);\n\n  await waitFor(() => {\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n  });\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"6\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  await waitFor(() => {\n    expect(getByText(generateItemText(\"cheesecake\", 8))).toBeInTheDocument();\n  });\n\n  const listElement = document.querySelector(\"ul\");\n  expect(listElement.childElementCount).toBe(3);\n\n  expect(getByText(generateItemText(\"croissant\", 5))).toBeInTheDocument();\n  expect(getByText(generateItemText(\"macaroon\", 96))).toBeInTheDocument();\n});\n\ntest(\"updating the action log when loading items\", async () => {\n  jest\n    .spyOn(Date.prototype, \"toISOString\")\n    .mockReturnValue(\"2020-06-20T13:37:00.000Z\");\n\n  const { getByTestId } = render(<App />);\n  await waitFor(() => {\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n  });\n\n  const actionLog = getByTestId(\"action-log\");\n  expect(actionLog).toMatchSnapshot();\n});\n\ntest(\"updating the action log adding an item\", async () => {\n  jest\n    .spyOn(Date.prototype, \"toISOString\")\n    .mockReturnValueOnce(\"2020-06-20T13:37:00.000Z\");\n  jest\n    .spyOn(Date.prototype, \"toISOString\")\n    .mockReturnValueOnce(\"2020-06-21T13:37:00.000Z\");\n\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 6 }))\n    .reply(200);\n\n  const { getByTestId, getByText, getByPlaceholderText } = render(<App />);\n  await waitFor(() => {\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n  });\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"6\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  await waitFor(() => {\n    expect(getByText(generateItemText(\"cheesecake\", 8))).toBeInTheDocument();\n  });\n\n  const actionLog = getByTestId(\"action-log\");\n  expect(actionLog).toMatchSnapshot();\n});\n"
  },
  {
    "path": "chapter8/3_testing_styles/2_style_props/ItemForm.jsx",
    "content": "import React from \"react\";\nimport { API_ADDR } from \"./constants\";\n\nconst addItemRequest = (itemName, quantity) => {\n  fetch(`${API_ADDR}/inventory/${itemName}`, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ quantity })\n  });\n};\n\nexport const ItemForm = ({ onItemAdded }) => {\n  const [itemName, setItemName] = React.useState(\"\");\n  const [quantity, setQuantity] = React.useState(0);\n\n  const onSubmit = async e => {\n    e.preventDefault();\n    await addItemRequest(itemName, quantity);\n    if (onItemAdded) onItemAdded(itemName, quantity);\n  };\n\n  return (\n    <form onSubmit={onSubmit}>\n      <input\n        onChange={e => setItemName(e.target.value)}\n        placeholder=\"Item name\"\n      />\n      <input\n        onChange={e => setQuantity(parseInt(e.target.value, 10))}\n        placeholder=\"Quantity\"\n      />\n      <button type=\"submit\">Add item</button>\n    </form>\n  );\n};\n"
  },
  {
    "path": "chapter8/3_testing_styles/2_style_props/ItemForm.test.jsx",
    "content": "import React from \"react\";\nimport nock from \"nock\";\nimport { API_ADDR } from \"./constants\";\nimport { ItemForm } from \"./ItemForm.jsx\";\nimport { render, fireEvent, waitFor } from \"@testing-library/react\";\n\ntest(\"form's elements\", () => {\n  const { getByText, getByPlaceholderText } = render(<ItemForm />);\n  expect(getByPlaceholderText(\"Item name\")).toBeInTheDocument();\n  expect(getByPlaceholderText(\"Quantity\")).toBeInTheDocument();\n  expect(getByText(\"Add item\")).toBeInTheDocument();\n});\n\ntest(\"sending requests\", () => {\n  const { getByText, getByPlaceholderText } = render(<ItemForm />);\n\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 2 }))\n    .reply(200);\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"2\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  expect(nock.isDone()).toBe(true);\n});\n\ntest(\"invoking the onItemAdded callback\", async () => {\n  const onItemAdded = jest.fn();\n  const { getByText, getByPlaceholderText } = render(\n    <ItemForm onItemAdded={onItemAdded} />\n  );\n\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 2 }))\n    .reply(200);\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"2\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  await waitFor(() => expect(nock.isDone()).toBe(true));\n\n  expect(onItemAdded).toHaveBeenCalledTimes(1);\n  expect(onItemAdded).toHaveBeenCalledWith(\"cheesecake\", 2);\n});\n"
  },
  {
    "path": "chapter8/3_testing_styles/2_style_props/ItemList.jsx",
    "content": "import React from \"react\";\nimport { Transition } from \"react-spring/renderprops\";\n\nexport const generateItemText = (itemName, quantity) => {\n  const capitalizedItemName =\n    itemName.charAt(0).toUpperCase() + itemName.slice(1);\n  return `${capitalizedItemName} - Quantity: ${quantity}`;\n};\n\nconst almostOutOfStock = {\n  fontWeight: \"bold\",\n  color: \"red\"\n};\n\nexport const ItemList = ({ itemList }) => {\n  const items = Object.entries(itemList);\n\n  return (\n    <ul>\n      <Transition\n        items={items}\n        initial={null}\n        keys={([itemName]) => itemName}\n        from={{ fontSize: 0, opacity: 0 }}\n        enter={{ fontSize: 18, opacity: 1 }}\n        leave={{ fontSize: 0, opacity: 0 }}\n      >\n        {([itemName, quantity]) => styleProps => (\n          <li\n            key={itemName}\n            className={quantity < 5 ? \"almost-out-of-stock\" : null}\n            style={\n              quantity < 5 ? { ...styleProps, ...almostOutOfStock } : styleProps\n            }\n          >\n            {generateItemText(itemName, quantity)}\n          </li>\n        )}\n      </Transition>\n    </ul>\n  );\n};\n"
  },
  {
    "path": "chapter8/3_testing_styles/2_style_props/ItemList.test.jsx",
    "content": "import React from \"react\";\nimport { ItemList, generateItemText } from \"./ItemList.jsx\";\nimport { render } from \"@testing-library/react\";\n\njest.mock(\"react-spring/renderprops\");\n\ndescribe(\"generateItemText\", () => {\n  test(\"generating an item's text\", () => {\n    expect(generateItemText(\"cheesecake\", 3)).toBe(\"Cheesecake - Quantity: 3\");\n    expect(generateItemText(\"apple pie\", 22)).toBe(\"Apple pie - Quantity: 22\");\n  });\n});\n\ndescribe(\"ItemList Component\", () => {\n  test(\"list items\", () => {\n    const itemList = { cheesecake: 2, croissant: 5, macaroon: 96 };\n    const { getByText } = render(<ItemList itemList={itemList} />);\n\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n    expect(getByText(generateItemText(\"cheesecake\", 2))).toBeInTheDocument();\n    expect(getByText(generateItemText(\"croissant\", 5))).toBeInTheDocument();\n    expect(getByText(generateItemText(\"macaroon\", 96))).toBeInTheDocument();\n  });\n\n  test(\"highlighting items that are almost out of stock\", () => {\n    const itemList = { cheesecake: 2, croissant: 5, macaroon: 96 };\n\n    const { getByText } = render(<ItemList itemList={itemList} />);\n    const cheesecakeItem = getByText(generateItemText(\"cheesecake\", 2));\n    expect(cheesecakeItem).toHaveClass(\"almost-out-of-stock\");\n  });\n});\n"
  },
  {
    "path": "chapter8/3_testing_styles/2_style_props/__mocks__/react-spring/renderprops.jsx",
    "content": "const FakeReactSpringTransition = jest.fn(({ items, children }) => {\n  return items.map(item => {\n    return children(item)({ fakeStyles: \"fake \" });\n  });\n});\n\nexport { FakeReactSpringTransition as Transition };\n"
  },
  {
    "path": "chapter8/3_testing_styles/2_style_props/__snapshots__/ActionLog.test.jsx.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`logging actions 1`] = `\n<div>\n  <div\n    data-testid=\"action-log\"\n  >\n    <h2>\n      Action Log\n    </h2>\n    <ul>\n      <li>\n        Date: \n        Fri, 02 Jan 1970 00:00:00 GMT\n         - Message: \n        Loaded item list\n         - Data: \n        {\"cheesecake\":2,\"macaroon\":5}\n      </li>\n      <li>\n        Date: \n        Sat, 03 Jan 1970 00:00:00 GMT\n         - Message: \n        Item added\n         - Data: \n        {\"cheesecake\":2}\n      </li>\n      <li>\n        Date: \n        Sun, 04 Jan 1970 00:00:00 GMT\n         - Message: \n        Item removed\n         - Data: \n        {\"cheesecake\":1}\n      </li>\n      <li>\n        Date: \n        Mon, 05 Jan 1970 00:00:00 GMT\n         - Message: \n        Something weird happened\n         - Data: \n        {\"error\":\"The cheesecake is a lie\"}\n      </li>\n    </ul>\n  </div>\n</div>\n`;\n"
  },
  {
    "path": "chapter8/3_testing_styles/2_style_props/__snapshots__/App.test.jsx.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`updating the action log adding an item 1`] = `\n<div\n  data-testid=\"action-log\"\n>\n  <h2>\n    Action Log\n  </h2>\n  <ul>\n    <li>\n      Date: \n      Sat, 20 Jun 2020 13:37:00 GMT\n       - Message: \n      Loaded items from the server\n       - Data: \n      {\"status\":200,\"body\":{\"cheesecake\":2,\"croissant\":5,\"macaroon\":96}}\n    </li>\n    <li>\n      Date: \n      Sun, 21 Jun 2020 13:37:00 GMT\n       - Message: \n      Item added\n       - Data: \n      {\"itemAdded\":\"cheesecake\",\"addedQuantity\":6}\n    </li>\n  </ul>\n</div>\n`;\n\nexports[`updating the action log when loading items 1`] = `\n<div\n  data-testid=\"action-log\"\n>\n  <h2>\n    Action Log\n  </h2>\n  <ul>\n    <li>\n      Date: \n      Sat, 20 Jun 2020 13:37:00 GMT\n       - Message: \n      Loaded items from the server\n       - Data: \n      {\"status\":200,\"body\":{\"cheesecake\":2,\"croissant\":5,\"macaroon\":96}}\n    </li>\n  </ul>\n</div>\n`;\n"
  },
  {
    "path": "chapter8/3_testing_styles/2_style_props/babel.config.js",
    "content": "module.exports = {\n  presets: [\n    [\n      \"@babel/preset-env\",\n      {\n        targets: {\n          node: \"current\"\n        }\n      }\n    ],\n    \"@babel/preset-react\"\n  ]\n};\n"
  },
  {
    "path": "chapter8/3_testing_styles/2_style_props/constants.js",
    "content": "export const API_ADDR = \"http://localhost:3000\";\n"
  },
  {
    "path": "chapter8/3_testing_styles/2_style_props/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Inventory</title>\n    <link rel=\"stylesheet\" href=\"./styles.css\" />\n  </head>\n  <body>\n    <div id=\"app\" />\n    <script src=\"bundle.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter8/3_testing_styles/2_style_props/index.jsx",
    "content": "import ReactDOM from \"react-dom\";\nimport React from \"react\";\nimport { App } from \"./App.jsx\";\n\nReactDOM.render(<App />, document.getElementById(\"app\"));\n"
  },
  {
    "path": "chapter8/3_testing_styles/2_style_props/jest.config.js",
    "content": "module.exports = {\n  setupFilesAfterEnv: [\n    \"<rootDir>/setupJestDom.js\",\n    \"<rootDir>/setupGlobalFetch.js\"\n  ]\n};\n"
  },
  {
    "path": "chapter8/3_testing_styles/2_style_props/package.json",
    "content": "{\n  \"name\": \"1_testing_styles\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"build\": \"browserify index.jsx -p esmify -o bundle.js\",\n    \"start\": \"http-server ./\",\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.9.6\",\n    \"@babel/preset-env\": \"^7.9.6\",\n    \"@babel/preset-react\": \"^7.9.4\",\n    \"@testing-library/dom\": \"^7.10.1\",\n    \"@testing-library/jest-dom\": \"^5.9.0\",\n    \"@testing-library/react\": \"^10.2.1\",\n    \"babelify\": \"^10.0.0\",\n    \"browserify\": \"^16.5.1\",\n    \"core-js\": \"^2.6.11\",\n    \"esmify\": \"^2.1.1\",\n    \"http-server\": \"^0.12.3\",\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"jest\": \"^25.5\",\n    \"nock\": \"^12.0.3\"\n  },\n  \"dependencies\": {\n    \"react\": \"^16.13.1\",\n    \"react-dom\": \"^16.13.1\",\n    \"react-spring\": \"^8.0.27\"\n  },\n  \"browserify\": {\n    \"transform\": [\n      [\n        \"babelify\",\n        {\n          \"presets\": [\n            [\n              \"@babel/preset-env\",\n              {\n                \"useBuiltIns\": \"usage\",\n                \"corejs\": 2\n              }\n            ],\n            \"@babel/preset-react\"\n          ]\n        }\n      ]\n    ]\n  }\n}\n"
  },
  {
    "path": "chapter8/3_testing_styles/2_style_props/setupGlobalFetch.js",
    "content": "const fetch = require(\"isomorphic-fetch\");\n\nglobal.window.fetch = fetch;\n"
  },
  {
    "path": "chapter8/3_testing_styles/2_style_props/setupJestDom.js",
    "content": "const jestDom = require(\"@testing-library/jest-dom\");\n\nexpect.extend(jestDom);\n"
  },
  {
    "path": "chapter8/3_testing_styles/2_style_props/styles.css",
    "content": ".almost-out-of-stock {\n  font-weight: bold;\n  color: red;\n}\n"
  },
  {
    "path": "chapter8/3_testing_styles/3_css_in_js_snapshots/ActionLog.jsx",
    "content": "import React from \"react\";\n\nexport const ActionLog = ({ actions }) => {\n  return (\n    <div data-testid=\"action-log\">\n      <h2>Action Log</h2>\n      <ul>\n        {actions.map(({ time, message, data }, i) => {\n          const date = new Date(time).toUTCString();\n          return (\n            <li key={i}>\n              Date: {date} - Message: {message} - Data: {JSON.stringify(data)}\n            </li>\n          );\n        })}\n      </ul>\n    </div>\n  );\n};\n"
  },
  {
    "path": "chapter8/3_testing_styles/3_css_in_js_snapshots/ActionLog.test.jsx",
    "content": "import React from \"react\";\nimport { ActionLog } from \"./ActionLog\";\nimport { render } from \"@testing-library/react\";\n\nconst daysToMs = days => days * 24 * 60 * 60 * 1000;\n\ntest(\"logging actions\", () => {\n  const actions = [\n    {\n      time: new Date(daysToMs(1)),\n      message: \"Loaded item list\",\n      data: { cheesecake: 2, macaroon: 5 }\n    },\n    {\n      time: new Date(daysToMs(2)),\n      message: \"Item added\",\n      data: { cheesecake: 2 }\n    },\n    {\n      time: new Date(daysToMs(3)),\n      message: \"Item removed\",\n      data: { cheesecake: 1 }\n    },\n    {\n      time: new Date(daysToMs(4)),\n      message: \"Something weird happened\",\n      data: { error: \"The cheesecake is a lie\" }\n    }\n  ];\n\n  const { container } = render(<ActionLog actions={actions} />);\n  expect(container).toMatchSnapshot();\n});\n"
  },
  {
    "path": "chapter8/3_testing_styles/3_css_in_js_snapshots/App.jsx",
    "content": "import React, { useEffect, useState, useRef } from \"react\";\nimport { API_ADDR } from \"./constants\";\nimport { ItemForm } from \"./ItemForm.jsx\";\nimport { ItemList } from \"./ItemList.jsx\";\nimport { ActionLog } from \"./ActionLog.jsx\";\n\nexport const App = () => {\n  const [items, setItems] = useState({});\n  const [actions, setActions] = useState([]);\n  const isMounted = useRef(null);\n\n  useEffect(() => {\n    isMounted.current = true;\n    const loadItems = async () => {\n      const response = await fetch(`${API_ADDR}/inventory`);\n      const responseBody = await response.json();\n      if (isMounted.current) {\n        setItems(responseBody);\n        setActions(\n          actions.concat({\n            time: new Date().toISOString(),\n            message: \"Loaded items from the server\",\n            data: { status: response.status, body: responseBody }\n          })\n        );\n      }\n    };\n    loadItems();\n    return () => (isMounted.current = false);\n  }, []);\n\n  const updateItems = (itemAdded, addedQuantity) => {\n    const currentQuantity = items[itemAdded] || 0;\n    setItems({ ...items, [itemAdded]: currentQuantity + addedQuantity });\n    setActions(\n      actions.concat({\n        time: new Date().toISOString(),\n        message: \"Item added\",\n        data: { itemAdded, addedQuantity }\n      })\n    );\n  };\n\n  return (\n    <div>\n      <h1>Inventory Contents</h1>\n      <ItemList itemList={items} />\n      <ItemForm onItemAdded={updateItems} />\n      <ActionLog actions={actions} />\n    </div>\n  );\n};\n"
  },
  {
    "path": "chapter8/3_testing_styles/3_css_in_js_snapshots/App.test.jsx",
    "content": "import React from \"react\";\nimport nock from \"nock\";\nimport { API_ADDR } from \"./constants\";\nimport { App } from \"./App.jsx\";\nimport { generateItemText } from \"./ItemList.jsx\";\nimport { render, fireEvent, waitFor } from \"@testing-library/react\";\n\njest.mock(\"react-spring/renderprops\");\n\nbeforeEach(() => {\n  nock(API_ADDR)\n    .get(\"/inventory\")\n    .reply(200, { cheesecake: 2, croissant: 5, macaroon: 96 });\n});\n\nafterEach(() => {\n  if (!nock.isDone()) {\n    nock.cleanAll();\n    throw new Error(\"Not all mocked endpoints received requests.\");\n  }\n});\n\ntest(\"renders the appropriate header\", () => {\n  const { getByText } = render(<App />);\n  expect(getByText(\"Inventory Contents\")).toBeInTheDocument();\n});\n\ntest(\"rendering the server's list of items\", async () => {\n  const { getByText } = render(<App />);\n\n  await waitFor(() => {\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n  });\n\n  expect(getByText(generateItemText(\"cheesecake\", 2))).toBeInTheDocument();\n  expect(getByText(generateItemText(\"croissant\", 5))).toBeInTheDocument();\n  expect(getByText(generateItemText(\"macaroon\", 96))).toBeInTheDocument();\n});\n\ntest(\"updating the list of items with new items\", async () => {\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 6 }))\n    .reply(200);\n\n  const { getByText, getByPlaceholderText } = render(<App />);\n\n  await waitFor(() => {\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n  });\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"6\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  await waitFor(() => {\n    expect(getByText(generateItemText(\"cheesecake\", 8))).toBeInTheDocument();\n  });\n\n  const listElement = document.querySelector(\"ul\");\n  expect(listElement.childElementCount).toBe(3);\n\n  expect(getByText(generateItemText(\"croissant\", 5))).toBeInTheDocument();\n  expect(getByText(generateItemText(\"macaroon\", 96))).toBeInTheDocument();\n});\n\ntest(\"updating the action log when loading items\", async () => {\n  jest\n    .spyOn(Date.prototype, \"toISOString\")\n    .mockReturnValue(\"2020-06-20T13:37:00.000Z\");\n\n  const { getByTestId } = render(<App />);\n  await waitFor(() => {\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n  });\n\n  const actionLog = getByTestId(\"action-log\");\n  expect(actionLog).toMatchSnapshot();\n});\n\ntest(\"updating the action log adding an item\", async () => {\n  jest\n    .spyOn(Date.prototype, \"toISOString\")\n    .mockReturnValueOnce(\"2020-06-20T13:37:00.000Z\");\n  jest\n    .spyOn(Date.prototype, \"toISOString\")\n    .mockReturnValueOnce(\"2020-06-21T13:37:00.000Z\");\n\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 6 }))\n    .reply(200);\n\n  const { getByTestId, getByText, getByPlaceholderText } = render(<App />);\n  await waitFor(() => {\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n  });\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"6\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  await waitFor(() => {\n    expect(getByText(generateItemText(\"cheesecake\", 8))).toBeInTheDocument();\n  });\n\n  const actionLog = getByTestId(\"action-log\");\n  expect(actionLog).toMatchSnapshot();\n});\n"
  },
  {
    "path": "chapter8/3_testing_styles/3_css_in_js_snapshots/ItemForm.jsx",
    "content": "import React from \"react\";\nimport { API_ADDR } from \"./constants\";\n\nconst addItemRequest = (itemName, quantity) => {\n  fetch(`${API_ADDR}/inventory/${itemName}`, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ quantity })\n  });\n};\n\nexport const ItemForm = ({ onItemAdded }) => {\n  const [itemName, setItemName] = React.useState(\"\");\n  const [quantity, setQuantity] = React.useState(0);\n\n  const onSubmit = async e => {\n    e.preventDefault();\n    await addItemRequest(itemName, quantity);\n    if (onItemAdded) onItemAdded(itemName, quantity);\n  };\n\n  return (\n    <form onSubmit={onSubmit}>\n      <input\n        onChange={e => setItemName(e.target.value)}\n        placeholder=\"Item name\"\n      />\n      <input\n        onChange={e => setQuantity(parseInt(e.target.value, 10))}\n        placeholder=\"Quantity\"\n      />\n      <button type=\"submit\">Add item</button>\n    </form>\n  );\n};\n"
  },
  {
    "path": "chapter8/3_testing_styles/3_css_in_js_snapshots/ItemForm.test.jsx",
    "content": "import React from \"react\";\nimport nock from \"nock\";\nimport { API_ADDR } from \"./constants\";\nimport { ItemForm } from \"./ItemForm.jsx\";\nimport { render, fireEvent, waitFor } from \"@testing-library/react\";\n\ntest(\"form's elements\", () => {\n  const { getByText, getByPlaceholderText } = render(<ItemForm />);\n  expect(getByPlaceholderText(\"Item name\")).toBeInTheDocument();\n  expect(getByPlaceholderText(\"Quantity\")).toBeInTheDocument();\n  expect(getByText(\"Add item\")).toBeInTheDocument();\n});\n\ntest(\"sending requests\", () => {\n  const { getByText, getByPlaceholderText } = render(<ItemForm />);\n\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 2 }))\n    .reply(200);\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"2\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  expect(nock.isDone()).toBe(true);\n});\n\ntest(\"invoking the onItemAdded callback\", async () => {\n  const onItemAdded = jest.fn();\n  const { getByText, getByPlaceholderText } = render(\n    <ItemForm onItemAdded={onItemAdded} />\n  );\n\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 2 }))\n    .reply(200);\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"2\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  await waitFor(() => expect(nock.isDone()).toBe(true));\n\n  expect(onItemAdded).toHaveBeenCalledTimes(1);\n  expect(onItemAdded).toHaveBeenCalledWith(\"cheesecake\", 2);\n});\n"
  },
  {
    "path": "chapter8/3_testing_styles/3_css_in_js_snapshots/ItemList.jsx",
    "content": "/* @jsx jsx */\n\nimport { Transition } from \"react-spring/renderprops\";\nimport { css, keyframes, jsx } from \"@emotion/core\";\n\nexport const generateItemText = (itemName, quantity) => {\n  const capitalizedItemName =\n    itemName.charAt(0).toUpperCase() + itemName.slice(1);\n  return `${capitalizedItemName} - Quantity: ${quantity}`;\n};\n\nconst pulsate = keyframes`\n  0% { opacity: .3; }\n  50% { opacity: 1; }\n  100% { opacity: .3; }\n`;\n\nconst almostOutOfStock = css`\n  font-weight: bold;\n  color: red;\n  animation: ${pulsate} 2s infinite;\n`;\n\nexport const ItemList = ({ itemList }) => {\n  const items = Object.entries(itemList);\n\n  return (\n    <ul>\n      <Transition\n        items={items}\n        initial={null}\n        keys={([itemName]) => itemName}\n        from={{ fontSize: 0, opacity: 0 }}\n        enter={{ fontSize: 18, opacity: 1 }}\n        leave={{ fontSize: 0, opacity: 0 }}\n      >\n        {([itemName, quantity]) => styleProps => (\n          <li\n            key={itemName}\n            style={styleProps}\n            css={quantity < 5 ? almostOutOfStock : null}\n          >\n            {generateItemText(itemName, quantity)}\n          </li>\n        )}\n      </Transition>\n    </ul>\n  );\n};\n"
  },
  {
    "path": "chapter8/3_testing_styles/3_css_in_js_snapshots/ItemList.test.jsx",
    "content": "import React from \"react\";\nimport { ItemList, generateItemText } from \"./ItemList.jsx\";\nimport { render } from \"@testing-library/react\";\n\njest.mock(\"react-spring/renderprops\");\n\ndescribe(\"generateItemText\", () => {\n  test(\"generating an item's text\", () => {\n    expect(generateItemText(\"cheesecake\", 3)).toBe(\"Cheesecake - Quantity: 3\");\n    expect(generateItemText(\"apple pie\", 22)).toBe(\"Apple pie - Quantity: 22\");\n  });\n});\n\ndescribe(\"ItemList Component\", () => {\n  test(\"list items\", () => {\n    const itemList = { cheesecake: 2, croissant: 5, macaroon: 96 };\n    const { getByText } = render(<ItemList itemList={itemList} />);\n\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n    expect(getByText(generateItemText(\"cheesecake\", 2))).toBeInTheDocument();\n    expect(getByText(generateItemText(\"croissant\", 5))).toBeInTheDocument();\n    expect(getByText(generateItemText(\"macaroon\", 96))).toBeInTheDocument();\n  });\n\n  test(\"highlighting items that are almost out of stock\", () => {\n    const itemList = { cheesecake: 2, croissant: 5, macaroon: 96 };\n\n    const { getByText } = render(<ItemList itemList={itemList} />);\n    const cheesecakeItem = getByText(generateItemText(\"cheesecake\", 2));\n    expect(cheesecakeItem).toHaveStyle({ color: \"red\" });\n  });\n});\n"
  },
  {
    "path": "chapter8/3_testing_styles/3_css_in_js_snapshots/__mocks__/react-spring/renderprops.jsx",
    "content": "const FakeReactSpringTransition = jest.fn(({ items, children }) => {\n  return items.map(item => {\n    return children(item)({ fakeStyles: \"fake \" });\n  });\n});\n\nexport { FakeReactSpringTransition as Transition };\n"
  },
  {
    "path": "chapter8/3_testing_styles/3_css_in_js_snapshots/__snapshots__/ActionLog.test.jsx.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`logging actions 1`] = `\n<div>\n  <div\n    data-testid=\"action-log\"\n  >\n    <h2>\n      Action Log\n    </h2>\n    <ul>\n      <li>\n        Date: \n        Fri, 02 Jan 1970 00:00:00 GMT\n         - Message: \n        Loaded item list\n         - Data: \n        {\"cheesecake\":2,\"macaroon\":5}\n      </li>\n      <li>\n        Date: \n        Sat, 03 Jan 1970 00:00:00 GMT\n         - Message: \n        Item added\n         - Data: \n        {\"cheesecake\":2}\n      </li>\n      <li>\n        Date: \n        Sun, 04 Jan 1970 00:00:00 GMT\n         - Message: \n        Item removed\n         - Data: \n        {\"cheesecake\":1}\n      </li>\n      <li>\n        Date: \n        Mon, 05 Jan 1970 00:00:00 GMT\n         - Message: \n        Something weird happened\n         - Data: \n        {\"error\":\"The cheesecake is a lie\"}\n      </li>\n    </ul>\n  </div>\n</div>\n`;\n"
  },
  {
    "path": "chapter8/3_testing_styles/3_css_in_js_snapshots/__snapshots__/App.test.jsx.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`updating the action log adding an item 1`] = `\n<div\n  data-testid=\"action-log\"\n>\n  <h2>\n    Action Log\n  </h2>\n  <ul>\n    <li>\n      Date: \n      Sat, 20 Jun 2020 13:37:00 GMT\n       - Message: \n      Loaded items from the server\n       - Data: \n      {\"status\":200,\"body\":{\"cheesecake\":2,\"croissant\":5,\"macaroon\":96}}\n    </li>\n    <li>\n      Date: \n      Sun, 21 Jun 2020 13:37:00 GMT\n       - Message: \n      Item added\n       - Data: \n      {\"itemAdded\":\"cheesecake\",\"addedQuantity\":6}\n    </li>\n  </ul>\n</div>\n`;\n\nexports[`updating the action log when loading items 1`] = `\n<div\n  data-testid=\"action-log\"\n>\n  <h2>\n    Action Log\n  </h2>\n  <ul>\n    <li>\n      Date: \n      Sat, 20 Jun 2020 13:37:00 GMT\n       - Message: \n      Loaded items from the server\n       - Data: \n      {\"status\":200,\"body\":{\"cheesecake\":2,\"croissant\":5,\"macaroon\":96}}\n    </li>\n  </ul>\n</div>\n`;\n"
  },
  {
    "path": "chapter8/3_testing_styles/3_css_in_js_snapshots/__snapshots__/ItemList.test.jsx.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`ItemList Component highlighting items that are almost out of stock 1`] = `\n@keyframes animation-0 {\n  0% {\n    opacity: .3;\n  }\n\n  50% {\n    opacity: 1;\n  }\n\n  100% {\n    opacity: .3;\n  }\n}\n\n.emotion-0 {\n  font-weight: bold;\n  color: red;\n  -webkit-animation: animation-0 2s infinite;\n  animation: animation-0 2s infinite;\n}\n\n<li\n  class=\"emotion-0\"\n>\n  Cheesecake - Quantity: 2\n</li>\n`;\n"
  },
  {
    "path": "chapter8/3_testing_styles/3_css_in_js_snapshots/babel.config.js",
    "content": "module.exports = {\n  presets: [\n    [\n      \"@babel/preset-env\",\n      {\n        targets: {\n          node: \"current\"\n        }\n      }\n    ],\n    \"@babel/preset-react\"\n  ]\n};\n"
  },
  {
    "path": "chapter8/3_testing_styles/3_css_in_js_snapshots/constants.js",
    "content": "export const API_ADDR = \"http://localhost:3000\";\n"
  },
  {
    "path": "chapter8/3_testing_styles/3_css_in_js_snapshots/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Inventory</title>\n    <link rel=\"stylesheet\" href=\"./styles.css\" />\n  </head>\n  <body>\n    <div id=\"app\" />\n    <script src=\"bundle.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter8/3_testing_styles/3_css_in_js_snapshots/index.jsx",
    "content": "import ReactDOM from \"react-dom\";\nimport React from \"react\";\nimport { App } from \"./App.jsx\";\n\nReactDOM.render(<App />, document.getElementById(\"app\"));\n"
  },
  {
    "path": "chapter8/3_testing_styles/3_css_in_js_snapshots/jest.config.js",
    "content": "module.exports = {\n  snapshotSerializers: [\"jest-emotion\"],\n  setupFilesAfterEnv: [\n    \"<rootDir>/setupJestDom.js\",\n    \"<rootDir>/setupJestEmotion.js\",\n    \"<rootDir>/setupGlobalFetch.js\"\n  ]\n};\n"
  },
  {
    "path": "chapter8/3_testing_styles/3_css_in_js_snapshots/package.json",
    "content": "{\n  \"name\": \"1_testing_styles\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"build\": \"browserify index.jsx -p esmify -o bundle.js\",\n    \"start\": \"http-server ./\",\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.9.6\",\n    \"@babel/preset-env\": \"^7.9.6\",\n    \"@babel/preset-react\": \"^7.9.4\",\n    \"@testing-library/dom\": \"^7.10.1\",\n    \"@testing-library/jest-dom\": \"^5.9.0\",\n    \"@testing-library/react\": \"^10.2.1\",\n    \"babelify\": \"^10.0.0\",\n    \"browserify\": \"^16.5.1\",\n    \"core-js\": \"^2.6.11\",\n    \"esmify\": \"^2.1.1\",\n    \"http-server\": \"^0.12.3\",\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"jest\": \"^25.5\",\n    \"jest-emotion\": \"^10.0.32\",\n    \"nock\": \"^12.0.3\"\n  },\n  \"dependencies\": {\n    \"@emotion/core\": \"^10.0.28\",\n    \"react\": \"^16.13.1\",\n    \"react-dom\": \"^16.13.1\",\n    \"react-spring\": \"^8.0.27\"\n  },\n  \"browserify\": {\n    \"transform\": [\n      [\n        \"babelify\",\n        {\n          \"presets\": [\n            [\n              \"@babel/preset-env\",\n              {\n                \"useBuiltIns\": \"usage\",\n                \"corejs\": 2\n              }\n            ],\n            \"@babel/preset-react\"\n          ]\n        }\n      ]\n    ]\n  }\n}\n"
  },
  {
    "path": "chapter8/3_testing_styles/3_css_in_js_snapshots/setupGlobalFetch.js",
    "content": "const fetch = require(\"isomorphic-fetch\");\n\nglobal.window.fetch = fetch;\n"
  },
  {
    "path": "chapter8/3_testing_styles/3_css_in_js_snapshots/setupJestDom.js",
    "content": "const jestDom = require(\"@testing-library/jest-dom\");\n\nexpect.extend(jestDom);\n"
  },
  {
    "path": "chapter8/3_testing_styles/3_css_in_js_snapshots/setupJestEmotion.js",
    "content": "const { matchers } = require(\"jest-emotion\");\n\nexpect.extend(matchers);\n"
  },
  {
    "path": "chapter8/3_testing_styles/3_css_in_js_snapshots/styles.css",
    "content": ".almost-out-of-stock {\n  font-weight: bold;\n  color: red;\n}\n"
  },
  {
    "path": "chapter8/4_component_stories/1_stories/.storybook/main.js",
    "content": "module.exports = {\n  stories: [\"../**/*.stories.jsx\"],\n  addons: [\n    \"@storybook/addon-actions/register\",\n    \"@storybook/addon-knobs/register\"\n  ],\n  webpackFinal: async config => {\n    return {\n      ...config,\n      resolve: {\n        ...config.resolve,\n        alias: {\n          \"core-js/modules\": \"@storybook/core/node_modules/core-js/modules\",\n          \"core-js/features\": \"@storybook/core/node_modules/core-js/features\"\n        }\n      }\n    };\n  }\n};\n"
  },
  {
    "path": "chapter8/4_component_stories/1_stories/ActionLog.jsx",
    "content": "import React from \"react\";\n\nexport const ActionLog = ({ actions }) => {\n  return (\n    <div data-testid=\"action-log\">\n      <h2>Action Log</h2>\n      <ul>\n        {actions.map(({ time, message, data }, i) => {\n          const date = new Date(time).toUTCString();\n          return (\n            <li key={i}>\n              Date: {date} - Message: {message} - Data: {JSON.stringify(data)}\n            </li>\n          );\n        })}\n      </ul>\n    </div>\n  );\n};\n"
  },
  {
    "path": "chapter8/4_component_stories/1_stories/ActionLog.stories.jsx",
    "content": "import React from \"react\";\nimport { storiesOf } from \"@storybook/react\";\nimport { ActionLog } from \"./ActionLog\";\n\nconst actionLogStories = storiesOf(\"ActionLog\", module);\n\nactionLogStories.add(\"A log of actions\", () => {\n  return (\n    <ActionLog\n      actions={[\n        {\n          time: new Date().toISOString(),\n          message: \"First action\",\n          data: { i: 1 }\n        },\n        {\n          time: new Date().toISOString(),\n          message: \"Second action\",\n          data: { i: 2 }\n        },\n        {\n          time: new Date().toISOString(),\n          message: \"Third action\",\n          data: { i: 3 }\n        }\n      ]}\n    />\n  );\n});\n"
  },
  {
    "path": "chapter8/4_component_stories/1_stories/ActionLog.test.jsx",
    "content": "import React from \"react\";\nimport { ActionLog } from \"./ActionLog\";\nimport { render } from \"@testing-library/react\";\n\nconst daysToMs = days => days * 24 * 60 * 60 * 1000;\n\ntest(\"logging actions\", () => {\n  const actions = [\n    {\n      time: new Date(daysToMs(1)),\n      message: \"Loaded item list\",\n      data: { cheesecake: 2, macaroon: 5 }\n    },\n    {\n      time: new Date(daysToMs(2)),\n      message: \"Item added\",\n      data: { cheesecake: 2 }\n    },\n    {\n      time: new Date(daysToMs(3)),\n      message: \"Item removed\",\n      data: { cheesecake: 1 }\n    },\n    {\n      time: new Date(daysToMs(4)),\n      message: \"Something weird happened\",\n      data: { error: \"The cheesecake is a lie\" }\n    }\n  ];\n\n  const { container } = render(<ActionLog actions={actions} />);\n  expect(container).toMatchSnapshot();\n});\n"
  },
  {
    "path": "chapter8/4_component_stories/1_stories/App.jsx",
    "content": "import React, { useEffect, useState, useRef } from \"react\";\nimport { API_ADDR } from \"./constants\";\nimport { ItemForm } from \"./ItemForm.jsx\";\nimport { ItemList } from \"./ItemList.jsx\";\nimport { ActionLog } from \"./ActionLog.jsx\";\n\nexport const App = () => {\n  const [items, setItems] = useState({});\n  const [actions, setActions] = useState([]);\n  const isMounted = useRef(null);\n\n  useEffect(() => {\n    isMounted.current = true;\n    const loadItems = async () => {\n      const response = await fetch(`${API_ADDR}/inventory`);\n      const responseBody = await response.json();\n      if (isMounted.current) {\n        setItems(responseBody);\n        setActions(\n          actions.concat({\n            time: new Date().toISOString(),\n            message: \"Loaded items from the server\",\n            data: { status: response.status, body: responseBody }\n          })\n        );\n      }\n    };\n    loadItems();\n    return () => (isMounted.current = false);\n  }, []);\n\n  const updateItems = (itemAdded, addedQuantity) => {\n    const currentQuantity = items[itemAdded] || 0;\n    setItems({ ...items, [itemAdded]: currentQuantity + addedQuantity });\n    setActions(\n      actions.concat({\n        time: new Date().toISOString(),\n        message: \"Item added\",\n        data: { itemAdded, addedQuantity }\n      })\n    );\n  };\n\n  return (\n    <div>\n      <h1>Inventory Contents</h1>\n      <ItemList itemList={items} />\n      <ItemForm onItemAdded={updateItems} />\n      <ActionLog actions={actions} />\n    </div>\n  );\n};\n"
  },
  {
    "path": "chapter8/4_component_stories/1_stories/App.test.jsx",
    "content": "import React from \"react\";\nimport nock from \"nock\";\nimport { API_ADDR } from \"./constants\";\nimport { App } from \"./App.jsx\";\nimport { generateItemText } from \"./ItemList.jsx\";\nimport { render, fireEvent, waitFor } from \"@testing-library/react\";\n\njest.mock(\"react-spring/renderprops\");\n\nbeforeEach(() => {\n  nock(API_ADDR)\n    .get(\"/inventory\")\n    .reply(200, { cheesecake: 2, croissant: 5, macaroon: 96 });\n});\n\nafterEach(() => {\n  if (!nock.isDone()) {\n    nock.cleanAll();\n    throw new Error(\"Not all mocked endpoints received requests.\");\n  }\n});\n\ntest(\"renders the appropriate header\", () => {\n  const { getByText } = render(<App />);\n  expect(getByText(\"Inventory Contents\")).toBeInTheDocument();\n});\n\ntest(\"rendering the server's list of items\", async () => {\n  const { getByText } = render(<App />);\n\n  await waitFor(() => {\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n  });\n\n  expect(getByText(generateItemText(\"cheesecake\", 2))).toBeInTheDocument();\n  expect(getByText(generateItemText(\"croissant\", 5))).toBeInTheDocument();\n  expect(getByText(generateItemText(\"macaroon\", 96))).toBeInTheDocument();\n});\n\ntest(\"updating the list of items with new items\", async () => {\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 6 }))\n    .reply(200);\n\n  const { getByText, getByPlaceholderText } = render(<App />);\n\n  await waitFor(() => {\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n  });\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"6\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  await waitFor(() => {\n    expect(getByText(generateItemText(\"cheesecake\", 8))).toBeInTheDocument();\n  });\n\n  const listElement = document.querySelector(\"ul\");\n  expect(listElement.childElementCount).toBe(3);\n\n  expect(getByText(generateItemText(\"croissant\", 5))).toBeInTheDocument();\n  expect(getByText(generateItemText(\"macaroon\", 96))).toBeInTheDocument();\n});\n\ntest(\"updating the action log when loading items\", async () => {\n  jest\n    .spyOn(Date.prototype, \"toISOString\")\n    .mockReturnValue(\"2020-06-20T13:37:00.000Z\");\n\n  const { getByTestId } = render(<App />);\n  await waitFor(() => {\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n  });\n\n  const actionLog = getByTestId(\"action-log\");\n  expect(actionLog).toMatchSnapshot();\n});\n\ntest(\"updating the action log adding an item\", async () => {\n  jest\n    .spyOn(Date.prototype, \"toISOString\")\n    .mockReturnValueOnce(\"2020-06-20T13:37:00.000Z\");\n  jest\n    .spyOn(Date.prototype, \"toISOString\")\n    .mockReturnValueOnce(\"2020-06-21T13:37:00.000Z\");\n\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 6 }))\n    .reply(200);\n\n  const { getByTestId, getByText, getByPlaceholderText } = render(<App />);\n  await waitFor(() => {\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n  });\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"6\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  await waitFor(() => {\n    expect(getByText(generateItemText(\"cheesecake\", 8))).toBeInTheDocument();\n  });\n\n  const actionLog = getByTestId(\"action-log\");\n  expect(actionLog).toMatchSnapshot();\n});\n"
  },
  {
    "path": "chapter8/4_component_stories/1_stories/ItemForm.jsx",
    "content": "import React from \"react\";\nimport { API_ADDR } from \"./constants\";\n\nconst addItemRequest = (itemName, quantity) => {\n  fetch(`${API_ADDR}/inventory/${itemName}`, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ quantity })\n  });\n};\n\nexport const ItemForm = ({ onItemAdded }) => {\n  const [itemName, setItemName] = React.useState(\"\");\n  const [quantity, setQuantity] = React.useState(0);\n\n  const onSubmit = async e => {\n    e.preventDefault();\n    await addItemRequest(itemName, quantity);\n    if (onItemAdded) onItemAdded(itemName, quantity);\n  };\n\n  return (\n    <form onSubmit={onSubmit}>\n      <input\n        onChange={e => setItemName(e.target.value)}\n        placeholder=\"Item name\"\n      />\n      <input\n        onChange={e => setQuantity(parseInt(e.target.value, 10))}\n        placeholder=\"Quantity\"\n      />\n      <button type=\"submit\">Add item</button>\n    </form>\n  );\n};\n"
  },
  {
    "path": "chapter8/4_component_stories/1_stories/ItemForm.stories.jsx",
    "content": "import React, { useEffect } from \"react\";\nimport fetchMock from \"fetch-mock\";\nimport { action } from \"@storybook/addon-actions\";\nimport { API_ADDR } from \"./constants\";\nimport { ItemForm } from \"./ItemForm\";\n\nexport default {\n  title: \"ItemForm\",\n  component: ItemForm,\n  includeStories: [\"itemForm\"]\n};\n\nexport const itemForm = () => {\n  const ItemFormStory = () => {\n    useEffect(() => {\n      fetchMock.post(`glob:${API_ADDR}/inventory/*`, 200);\n      return () => fetchMock.restore();\n    }, []);\n\n    return <ItemForm onItemAdded={action(\"form-submission\")} />;\n  };\n\n  return <ItemFormStory />;\n};\n"
  },
  {
    "path": "chapter8/4_component_stories/1_stories/ItemForm.test.jsx",
    "content": "import React from \"react\";\nimport nock from \"nock\";\nimport { API_ADDR } from \"./constants\";\nimport { ItemForm } from \"./ItemForm.jsx\";\nimport { render, fireEvent, waitFor } from \"@testing-library/react\";\n\ntest(\"form's elements\", () => {\n  const { getByText, getByPlaceholderText } = render(<ItemForm />);\n  expect(getByPlaceholderText(\"Item name\")).toBeInTheDocument();\n  expect(getByPlaceholderText(\"Quantity\")).toBeInTheDocument();\n  expect(getByText(\"Add item\")).toBeInTheDocument();\n});\n\ntest(\"sending requests\", () => {\n  const { getByText, getByPlaceholderText } = render(<ItemForm />);\n\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 2 }))\n    .reply(200);\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"2\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  expect(nock.isDone()).toBe(true);\n});\n\ntest(\"invoking the onItemAdded callback\", async () => {\n  const onItemAdded = jest.fn();\n  const { getByText, getByPlaceholderText } = render(\n    <ItemForm onItemAdded={onItemAdded} />\n  );\n\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 2 }))\n    .reply(200);\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"2\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  await waitFor(() => expect(nock.isDone()).toBe(true));\n\n  expect(onItemAdded).toHaveBeenCalledTimes(1);\n  expect(onItemAdded).toHaveBeenCalledWith(\"cheesecake\", 2);\n});\n"
  },
  {
    "path": "chapter8/4_component_stories/1_stories/ItemList.jsx",
    "content": "/* @jsx jsx */\n\nimport { Transition } from \"react-spring/renderprops\";\nimport { css, keyframes, jsx } from \"@emotion/core\";\n\nexport const generateItemText = (itemName, quantity) => {\n  const capitalizedItemName =\n    itemName.charAt(0).toUpperCase() + itemName.slice(1);\n  return `${capitalizedItemName} - Quantity: ${quantity}`;\n};\n\nconst pulsate = keyframes`\n  0% { opacity: .3; }\n  50% { opacity: 1; }\n  100% { opacity: .3; }\n`;\n\nconst almostOutOfStock = css`\n  font-weight: bold;\n  color: red;\n  animation: ${pulsate} 2s infinite;\n`;\n\nexport const ItemList = ({ itemList }) => {\n  const items = Object.entries(itemList);\n\n  return (\n    <ul>\n      <Transition\n        items={items}\n        initial={null}\n        keys={([itemName]) => itemName}\n        from={{ fontSize: 0, opacity: 0 }}\n        enter={{ fontSize: 18, opacity: 1 }}\n        leave={{ fontSize: 0, opacity: 0 }}\n      >\n        {([itemName, quantity]) => styleProps => (\n          <li\n            key={itemName}\n            style={styleProps}\n            css={quantity < 5 ? almostOutOfStock : null}\n          >\n            {generateItemText(itemName, quantity)}\n          </li>\n        )}\n      </Transition>\n    </ul>\n  );\n};\n"
  },
  {
    "path": "chapter8/4_component_stories/1_stories/ItemList.stories.jsx",
    "content": "import React from \"react\";\nimport { withKnobs, object } from \"@storybook/addon-knobs\";\nimport { ItemList } from \"./ItemList\";\n\nexport default {\n  title: \"ItemList\",\n  component: ItemList,\n  includeStories: [\"staticItemList\", \"animatedItems\"],\n  decorators: [withKnobs]\n};\n\nexport const staticItemList = () => (\n  <ItemList\n    itemList={{\n      cheesecake: 2,\n      croissant: 5,\n      macaroon: 96\n    }}\n  />\n);\n\nexport const animatedItems = () => {\n  const knobLabel = \"Contents\";\n  const knobDefaultValue = { cheesecake: 2, croissant: 5 };\n  const itemList = object(knobLabel, knobDefaultValue);\n  return <ItemList itemList={itemList} />;\n};\n"
  },
  {
    "path": "chapter8/4_component_stories/1_stories/ItemList.test.jsx",
    "content": "import React from \"react\";\nimport { ItemList, generateItemText } from \"./ItemList.jsx\";\nimport { render } from \"@testing-library/react\";\n\njest.mock(\"react-spring/renderprops\");\n\ndescribe(\"generateItemText\", () => {\n  test(\"generating an item's text\", () => {\n    expect(generateItemText(\"cheesecake\", 3)).toBe(\"Cheesecake - Quantity: 3\");\n    expect(generateItemText(\"apple pie\", 22)).toBe(\"Apple pie - Quantity: 22\");\n  });\n});\n\ndescribe(\"ItemList Component\", () => {\n  test(\"list items\", () => {\n    const itemList = { cheesecake: 2, croissant: 5, macaroon: 96 };\n    const { getByText } = render(<ItemList itemList={itemList} />);\n\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n    expect(getByText(generateItemText(\"cheesecake\", 2))).toBeInTheDocument();\n    expect(getByText(generateItemText(\"croissant\", 5))).toBeInTheDocument();\n    expect(getByText(generateItemText(\"macaroon\", 96))).toBeInTheDocument();\n  });\n\n  test(\"highlighting items that are almost out of stock\", () => {\n    const itemList = { cheesecake: 2, croissant: 5, macaroon: 96 };\n\n    const { getByText } = render(<ItemList itemList={itemList} />);\n    const cheesecakeItem = getByText(generateItemText(\"cheesecake\", 2));\n    expect(cheesecakeItem).toMatchSnapshot();\n  });\n});\n"
  },
  {
    "path": "chapter8/4_component_stories/1_stories/__mocks__/react-spring/renderprops.jsx",
    "content": "const FakeReactSpringTransition = jest.fn(({ items, children }) => {\n  return items.map(item => {\n    return children(item)({ fakeStyles: \"fake \" });\n  });\n});\n\nexport { FakeReactSpringTransition as Transition };\n"
  },
  {
    "path": "chapter8/4_component_stories/1_stories/__snapshots__/ActionLog.test.jsx.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`logging actions 1`] = `\n<div>\n  <div\n    data-testid=\"action-log\"\n  >\n    <h2>\n      Action Log\n    </h2>\n    <ul>\n      <li>\n        Date: \n        Fri, 02 Jan 1970 00:00:00 GMT\n         - Message: \n        Loaded item list\n         - Data: \n        {\"cheesecake\":2,\"macaroon\":5}\n      </li>\n      <li>\n        Date: \n        Sat, 03 Jan 1970 00:00:00 GMT\n         - Message: \n        Item added\n         - Data: \n        {\"cheesecake\":2}\n      </li>\n      <li>\n        Date: \n        Sun, 04 Jan 1970 00:00:00 GMT\n         - Message: \n        Item removed\n         - Data: \n        {\"cheesecake\":1}\n      </li>\n      <li>\n        Date: \n        Mon, 05 Jan 1970 00:00:00 GMT\n         - Message: \n        Something weird happened\n         - Data: \n        {\"error\":\"The cheesecake is a lie\"}\n      </li>\n    </ul>\n  </div>\n</div>\n`;\n"
  },
  {
    "path": "chapter8/4_component_stories/1_stories/__snapshots__/App.test.jsx.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`updating the action log adding an item 1`] = `\n<div\n  data-testid=\"action-log\"\n>\n  <h2>\n    Action Log\n  </h2>\n  <ul>\n    <li>\n      Date: \n      Sat, 20 Jun 2020 13:37:00 GMT\n       - Message: \n      Loaded items from the server\n       - Data: \n      {\"status\":200,\"body\":{\"cheesecake\":2,\"croissant\":5,\"macaroon\":96}}\n    </li>\n    <li>\n      Date: \n      Sun, 21 Jun 2020 13:37:00 GMT\n       - Message: \n      Item added\n       - Data: \n      {\"itemAdded\":\"cheesecake\",\"addedQuantity\":6}\n    </li>\n  </ul>\n</div>\n`;\n\nexports[`updating the action log when loading items 1`] = `\n<div\n  data-testid=\"action-log\"\n>\n  <h2>\n    Action Log\n  </h2>\n  <ul>\n    <li>\n      Date: \n      Sat, 20 Jun 2020 13:37:00 GMT\n       - Message: \n      Loaded items from the server\n       - Data: \n      {\"status\":200,\"body\":{\"cheesecake\":2,\"croissant\":5,\"macaroon\":96}}\n    </li>\n  </ul>\n</div>\n`;\n"
  },
  {
    "path": "chapter8/4_component_stories/1_stories/__snapshots__/ItemList.test.jsx.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`ItemList Component highlighting items that are almost out of stock 1`] = `\n@keyframes animation-0 {\n  0% {\n    opacity: .3;\n  }\n\n  50% {\n    opacity: 1;\n  }\n\n  100% {\n    opacity: .3;\n  }\n}\n\n.emotion-0 {\n  font-weight: bold;\n  color: red;\n  -webkit-animation: animation-0 2s infinite;\n  animation: animation-0 2s infinite;\n}\n\n<li\n  class=\"emotion-0\"\n>\n  Cheesecake - Quantity: 2\n</li>\n`;\n"
  },
  {
    "path": "chapter8/4_component_stories/1_stories/babel.config.js",
    "content": "module.exports = {\n  presets: [\n    [\n      \"@babel/preset-env\",\n      {\n        targets: {\n          node: \"current\"\n        }\n      }\n    ],\n    \"@babel/preset-react\"\n  ]\n};\n"
  },
  {
    "path": "chapter8/4_component_stories/1_stories/constants.js",
    "content": "export const API_ADDR = \"http://localhost:3000\";\n"
  },
  {
    "path": "chapter8/4_component_stories/1_stories/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Inventory</title>\n    <link rel=\"stylesheet\" href=\"./styles.css\" />\n  </head>\n  <body>\n    <div id=\"app\" />\n    <script src=\"bundle.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter8/4_component_stories/1_stories/index.jsx",
    "content": "import ReactDOM from \"react-dom\";\nimport React from \"react\";\nimport { App } from \"./App.jsx\";\n\nReactDOM.render(<App />, document.getElementById(\"app\"));\n"
  },
  {
    "path": "chapter8/4_component_stories/1_stories/jest.config.js",
    "content": "module.exports = {\n  snapshotSerializers: [\"jest-emotion\"],\n  setupFilesAfterEnv: [\n    \"<rootDir>/setupJestDom.js\",\n    \"<rootDir>/setupJestEmotion.js\",\n    \"<rootDir>/setupGlobalFetch.js\"\n  ]\n};\n"
  },
  {
    "path": "chapter8/4_component_stories/1_stories/package.json",
    "content": "{\n  \"name\": \"1_component_stories\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"storybook\": \"start-storybook\",\n    \"build\": \"browserify index.jsx -p esmify -o bundle.js\",\n    \"start\": \"http-server ./\",\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.9.6\",\n    \"@babel/preset-env\": \"^7.9.6\",\n    \"@babel/preset-react\": \"^7.9.4\",\n    \"@storybook/addon-actions\": \"^6.0.28\",\n    \"@storybook/addon-knobs\": \"^6.0.28\",\n    \"@storybook/react\": \"^6.0.28\",\n    \"@testing-library/dom\": \"^7.10.1\",\n    \"@testing-library/jest-dom\": \"^5.9.0\",\n    \"@testing-library/react\": \"^10.2.1\",\n    \"babel-loader\": \"^8.1.0\",\n    \"babelify\": \"^10.0.0\",\n    \"browserify\": \"^16.5.1\",\n    \"core-js\": \"^2.6.11\",\n    \"esmify\": \"^2.1.1\",\n    \"fetch-mock\": \"^9.10.3\",\n    \"http-server\": \"^0.12.3\",\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"jest\": \"^25.5\",\n    \"jest-emotion\": \"^10.0.32\",\n    \"nock\": \"^12.0.3\"\n  },\n  \"dependencies\": {\n    \"@emotion/core\": \"^10.0.28\",\n    \"react\": \"^16.13.1\",\n    \"react-dom\": \"^16.13.1\",\n    \"react-spring\": \"^8.0.27\"\n  },\n  \"browserify\": {\n    \"transform\": [\n      [\n        \"babelify\",\n        {\n          \"presets\": [\n            [\n              \"@babel/preset-env\",\n              {\n                \"useBuiltIns\": \"usage\",\n                \"corejs\": 2\n              }\n            ],\n            \"@babel/preset-react\"\n          ]\n        }\n      ]\n    ]\n  }\n}\n"
  },
  {
    "path": "chapter8/4_component_stories/1_stories/setupGlobalFetch.js",
    "content": "const fetch = require(\"isomorphic-fetch\");\n\nglobal.window.fetch = fetch;\n"
  },
  {
    "path": "chapter8/4_component_stories/1_stories/setupJestDom.js",
    "content": "const jestDom = require(\"@testing-library/jest-dom\");\n\nexpect.extend(jestDom);\n"
  },
  {
    "path": "chapter8/4_component_stories/1_stories/setupJestEmotion.js",
    "content": "const { matchers } = require(\"jest-emotion\");\n\nexpect.extend(matchers);\n"
  },
  {
    "path": "chapter8/4_component_stories/1_stories/styles.css",
    "content": ".almost-out-of-stock {\n  font-weight: bold;\n  color: red;\n}\n"
  },
  {
    "path": "chapter8/4_component_stories/2_documentation/.storybook/main.js",
    "content": "module.exports = {\n  stories: [\"../**/*.stories.@(jsx|mdx)\"],\n  addons: [\n    \"@storybook/addon-knobs/register\",\n    \"@storybook/addon-actions/register\",\n    {\n      name: \"@storybook/addon-docs\",\n      options: { configureJSX: true }\n    }\n  ],\n  webpackFinal: async config => {\n    return {\n      ...config,\n      resolve: {\n        ...config.resolve,\n        alias: {\n          \"core-js/modules\": \"@storybook/core/node_modules/core-js/modules\",\n          \"core-js/features\": \"@storybook/core/node_modules/core-js/features\"\n        }\n      }\n    };\n  }\n};\n"
  },
  {
    "path": "chapter8/4_component_stories/2_documentation/ActionLog.jsx",
    "content": "import React from \"react\";\n\nexport const ActionLog = ({ actions }) => {\n  return (\n    <div data-testid=\"action-log\">\n      <h2>Action Log</h2>\n      <ul>\n        {actions.map(({ time, message, data }, i) => {\n          const date = new Date(time).toUTCString();\n          return (\n            <li key={i}>\n              Date: {date} - Message: {message} - Data: {JSON.stringify(data)}\n            </li>\n          );\n        })}\n      </ul>\n    </div>\n  );\n};\n"
  },
  {
    "path": "chapter8/4_component_stories/2_documentation/ActionLog.stories.jsx",
    "content": "import React from \"react\";\nimport { storiesOf } from \"@storybook/react\";\nimport { ActionLog } from \"./ActionLog\";\n\nconst actionLogStories = storiesOf(\"ActionLog\", module);\n\nactionLogStories.add(\"A log of actions\", () => {\n  return (\n    <ActionLog\n      actions={[\n        {\n          time: new Date().toISOString(),\n          message: \"First action\",\n          data: { i: 1 }\n        },\n        {\n          time: new Date().toISOString(),\n          message: \"Second action\",\n          data: { i: 2 }\n        },\n        {\n          time: new Date().toISOString(),\n          message: \"Third action\",\n          data: { i: 3 }\n        }\n      ]}\n    />\n  );\n});\n"
  },
  {
    "path": "chapter8/4_component_stories/2_documentation/ActionLog.test.jsx",
    "content": "import React from \"react\";\nimport { ActionLog } from \"./ActionLog\";\nimport { render } from \"@testing-library/react\";\n\nconst daysToMs = days => days * 24 * 60 * 60 * 1000;\n\ntest(\"logging actions\", () => {\n  const actions = [\n    {\n      time: new Date(daysToMs(1)),\n      message: \"Loaded item list\",\n      data: { cheesecake: 2, macaroon: 5 }\n    },\n    {\n      time: new Date(daysToMs(2)),\n      message: \"Item added\",\n      data: { cheesecake: 2 }\n    },\n    {\n      time: new Date(daysToMs(3)),\n      message: \"Item removed\",\n      data: { cheesecake: 1 }\n    },\n    {\n      time: new Date(daysToMs(4)),\n      message: \"Something weird happened\",\n      data: { error: \"The cheesecake is a lie\" }\n    }\n  ];\n\n  const { container } = render(<ActionLog actions={actions} />);\n  expect(container).toMatchSnapshot();\n});\n"
  },
  {
    "path": "chapter8/4_component_stories/2_documentation/App.jsx",
    "content": "import React, { useEffect, useState, useRef } from \"react\";\nimport { API_ADDR } from \"./constants\";\nimport { ItemForm } from \"./ItemForm.jsx\";\nimport { ItemList } from \"./ItemList.jsx\";\nimport { ActionLog } from \"./ActionLog.jsx\";\n\nexport const App = () => {\n  const [items, setItems] = useState({});\n  const [actions, setActions] = useState([]);\n  const isMounted = useRef(null);\n\n  useEffect(() => {\n    isMounted.current = true;\n    const loadItems = async () => {\n      const response = await fetch(`${API_ADDR}/inventory`);\n      const responseBody = await response.json();\n      if (isMounted.current) {\n        setItems(responseBody);\n        setActions(\n          actions.concat({\n            time: new Date().toISOString(),\n            message: \"Loaded items from the server\",\n            data: { status: response.status, body: responseBody }\n          })\n        );\n      }\n    };\n    loadItems();\n    return () => (isMounted.current = false);\n  }, []);\n\n  const updateItems = (itemAdded, addedQuantity) => {\n    const currentQuantity = items[itemAdded] || 0;\n    setItems({ ...items, [itemAdded]: currentQuantity + addedQuantity });\n    setActions(\n      actions.concat({\n        time: new Date().toISOString(),\n        message: \"Item added\",\n        data: { itemAdded, addedQuantity }\n      })\n    );\n  };\n\n  return (\n    <div>\n      <h1>Inventory Contents</h1>\n      <ItemList itemList={items} />\n      <ItemForm onItemAdded={updateItems} />\n      <ActionLog actions={actions} />\n    </div>\n  );\n};\n"
  },
  {
    "path": "chapter8/4_component_stories/2_documentation/App.test.jsx",
    "content": "import React from \"react\";\nimport nock from \"nock\";\nimport { API_ADDR } from \"./constants\";\nimport { App } from \"./App.jsx\";\nimport { generateItemText } from \"./ItemList.jsx\";\nimport { render, fireEvent, waitFor } from \"@testing-library/react\";\n\njest.mock(\"react-spring/renderprops\");\n\nbeforeEach(() => {\n  nock(API_ADDR)\n    .get(\"/inventory\")\n    .reply(200, { cheesecake: 2, croissant: 5, macaroon: 96 });\n});\n\nafterEach(() => {\n  if (!nock.isDone()) {\n    nock.cleanAll();\n    throw new Error(\"Not all mocked endpoints received requests.\");\n  }\n});\n\ntest(\"renders the appropriate header\", () => {\n  const { getByText } = render(<App />);\n  expect(getByText(\"Inventory Contents\")).toBeInTheDocument();\n});\n\ntest(\"rendering the server's list of items\", async () => {\n  const { getByText } = render(<App />);\n\n  await waitFor(() => {\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n  });\n\n  expect(getByText(generateItemText(\"cheesecake\", 2))).toBeInTheDocument();\n  expect(getByText(generateItemText(\"croissant\", 5))).toBeInTheDocument();\n  expect(getByText(generateItemText(\"macaroon\", 96))).toBeInTheDocument();\n});\n\ntest(\"updating the list of items with new items\", async () => {\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 6 }))\n    .reply(200);\n\n  const { getByText, getByPlaceholderText } = render(<App />);\n\n  await waitFor(() => {\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n  });\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"6\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  await waitFor(() => {\n    expect(getByText(generateItemText(\"cheesecake\", 8))).toBeInTheDocument();\n  });\n\n  const listElement = document.querySelector(\"ul\");\n  expect(listElement.childElementCount).toBe(3);\n\n  expect(getByText(generateItemText(\"croissant\", 5))).toBeInTheDocument();\n  expect(getByText(generateItemText(\"macaroon\", 96))).toBeInTheDocument();\n});\n\ntest(\"updating the action log when loading items\", async () => {\n  jest\n    .spyOn(Date.prototype, \"toISOString\")\n    .mockReturnValue(\"2020-06-20T13:37:00.000Z\");\n\n  const { getByTestId } = render(<App />);\n  await waitFor(() => {\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n  });\n\n  const actionLog = getByTestId(\"action-log\");\n  expect(actionLog).toMatchSnapshot();\n});\n\ntest(\"updating the action log adding an item\", async () => {\n  jest\n    .spyOn(Date.prototype, \"toISOString\")\n    .mockReturnValueOnce(\"2020-06-20T13:37:00.000Z\");\n  jest\n    .spyOn(Date.prototype, \"toISOString\")\n    .mockReturnValueOnce(\"2020-06-21T13:37:00.000Z\");\n\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 6 }))\n    .reply(200);\n\n  const { getByTestId, getByText, getByPlaceholderText } = render(<App />);\n  await waitFor(() => {\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n  });\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"6\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  await waitFor(() => {\n    expect(getByText(generateItemText(\"cheesecake\", 8))).toBeInTheDocument();\n  });\n\n  const actionLog = getByTestId(\"action-log\");\n  expect(actionLog).toMatchSnapshot();\n});\n"
  },
  {
    "path": "chapter8/4_component_stories/2_documentation/ItemForm.jsx",
    "content": "import React from \"react\";\nimport { API_ADDR } from \"./constants\";\n\nconst addItemRequest = (itemName, quantity) => {\n  fetch(`${API_ADDR}/inventory/${itemName}`, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ quantity })\n  });\n};\n\nexport const ItemForm = ({ onItemAdded }) => {\n  const [itemName, setItemName] = React.useState(\"\");\n  const [quantity, setQuantity] = React.useState(0);\n\n  const onSubmit = async e => {\n    e.preventDefault();\n    await addItemRequest(itemName, quantity);\n    if (onItemAdded) onItemAdded(itemName, quantity);\n  };\n\n  return (\n    <form onSubmit={onSubmit}>\n      <input\n        onChange={e => setItemName(e.target.value)}\n        placeholder=\"Item name\"\n      />\n      <input\n        onChange={e => setQuantity(parseInt(e.target.value, 10))}\n        placeholder=\"Quantity\"\n      />\n      <button type=\"submit\">Add item</button>\n    </form>\n  );\n};\n"
  },
  {
    "path": "chapter8/4_component_stories/2_documentation/ItemForm.stories.jsx",
    "content": "import React, { useEffect } from \"react\";\nimport fetchMock from \"fetch-mock\";\nimport { action } from \"@storybook/addon-actions\";\nimport { API_ADDR } from \"./constants\";\nimport { ItemForm } from \"./ItemForm\";\n\nexport default {\n  title: \"ItemForm\",\n  component: ItemForm,\n  includeStories: [\"itemForm\"]\n};\n\nexport const itemForm = () => {\n  const ItemFormStory = () => {\n    useEffect(() => {\n      fetchMock.post(`glob:${API_ADDR}/inventory/*`, 200);\n      return () => fetchMock.restore();\n    }, []);\n\n    return <ItemForm onItemAdded={action(\"form-submission\")} />;\n  };\n\n  return <ItemFormStory />;\n};\n"
  },
  {
    "path": "chapter8/4_component_stories/2_documentation/ItemForm.test.jsx",
    "content": "import React from \"react\";\nimport nock from \"nock\";\nimport { API_ADDR } from \"./constants\";\nimport { ItemForm } from \"./ItemForm.jsx\";\nimport { render, fireEvent, waitFor } from \"@testing-library/react\";\n\ntest(\"form's elements\", () => {\n  const { getByText, getByPlaceholderText } = render(<ItemForm />);\n  expect(getByPlaceholderText(\"Item name\")).toBeInTheDocument();\n  expect(getByPlaceholderText(\"Quantity\")).toBeInTheDocument();\n  expect(getByText(\"Add item\")).toBeInTheDocument();\n});\n\ntest(\"sending requests\", () => {\n  const { getByText, getByPlaceholderText } = render(<ItemForm />);\n\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 2 }))\n    .reply(200);\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"2\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  expect(nock.isDone()).toBe(true);\n});\n\ntest(\"invoking the onItemAdded callback\", async () => {\n  const onItemAdded = jest.fn();\n  const { getByText, getByPlaceholderText } = render(\n    <ItemForm onItemAdded={onItemAdded} />\n  );\n\n  nock(API_ADDR)\n    .post(\"/inventory/cheesecake\", JSON.stringify({ quantity: 2 }))\n    .reply(200);\n\n  fireEvent.change(getByPlaceholderText(\"Item name\"), {\n    target: { value: \"cheesecake\" }\n  });\n  fireEvent.change(getByPlaceholderText(\"Quantity\"), {\n    target: { value: \"2\" }\n  });\n  fireEvent.click(getByText(\"Add item\"));\n\n  await waitFor(() => expect(nock.isDone()).toBe(true));\n\n  expect(onItemAdded).toHaveBeenCalledTimes(1);\n  expect(onItemAdded).toHaveBeenCalledWith(\"cheesecake\", 2);\n});\n"
  },
  {
    "path": "chapter8/4_component_stories/2_documentation/ItemList.jsx",
    "content": "/* @jsx jsx */\n\nimport { Transition } from \"react-spring/renderprops\";\nimport { css, keyframes, jsx } from \"@emotion/core\";\n\nexport const generateItemText = (itemName, quantity) => {\n  const capitalizedItemName =\n    itemName.charAt(0).toUpperCase() + itemName.slice(1);\n  return `${capitalizedItemName} - Quantity: ${quantity}`;\n};\n\nconst pulsate = keyframes`\n  0% { opacity: .3; }\n  50% { opacity: 1; }\n  100% { opacity: .3; }\n`;\n\nconst almostOutOfStock = css`\n  font-weight: bold;\n  color: red;\n  animation: ${pulsate} 2s infinite;\n`;\n\nexport const ItemList = ({ itemList }) => {\n  const items = Object.entries(itemList);\n\n  return (\n    <ul>\n      <Transition\n        items={items}\n        initial={null}\n        keys={([itemName]) => itemName}\n        from={{ fontSize: 0, opacity: 0 }}\n        enter={{ fontSize: 18, opacity: 1 }}\n        leave={{ fontSize: 0, opacity: 0 }}\n      >\n        {([itemName, quantity]) => styleProps => (\n          <li\n            key={itemName}\n            style={styleProps}\n            css={quantity < 5 ? almostOutOfStock : null}\n          >\n            {generateItemText(itemName, quantity)}\n          </li>\n        )}\n      </Transition>\n    </ul>\n  );\n};\n"
  },
  {
    "path": "chapter8/4_component_stories/2_documentation/ItemList.stories.jsx",
    "content": "import React from \"react\";\nimport { withKnobs, object } from \"@storybook/addon-knobs\";\nimport { ItemList } from \"./ItemList\";\n\nexport default {\n  title: \"ItemList\",\n  component: ItemList,\n  includeStories: [\"staticItemList\", \"animatedItems\"],\n  decorators: [withKnobs]\n};\n\nexport const staticItemList = () => (\n  <ItemList\n    itemList={{\n      cheesecake: 2,\n      croissant: 5,\n      macaroon: 96\n    }}\n  />\n);\n\nexport const animatedItems = () => {\n  const knobLabel = \"Contents\";\n  const knobDefaultValue = { cheesecake: 2, croissant: 5 };\n  const itemList = object(knobLabel, knobDefaultValue);\n  return <ItemList itemList={itemList} />;\n};\n"
  },
  {
    "path": "chapter8/4_component_stories/2_documentation/ItemList.stories.mdx",
    "content": "import { Meta, Story, Preview } from \"@storybook/addon-docs/blocks\";\nimport { ItemList } from \"./ItemList\";\n\n<Meta title=\"ItemList\" component={ItemList} />\n\n# Item list\n\nThe `ItemList` component displays a list of inventory items.\n\nIt's capable of:\n\n- Animating new items\n- Highlighting items which are about to become unavailable\n\n## Props\n\n- An object in which each key represents an item's name, and each value represents its quantity.\n\n<Preview>\n  <Story name=\"A list of items\">\n    <ItemList\n      itemList={{\n        cheesecake: 2,\n        croissant: 5,\n        macaroon: 96\n      }}\n    />\n  </Story>\n</Preview>\n"
  },
  {
    "path": "chapter8/4_component_stories/2_documentation/ItemList.test.jsx",
    "content": "import React from \"react\";\nimport { ItemList, generateItemText } from \"./ItemList.jsx\";\nimport { render } from \"@testing-library/react\";\n\njest.mock(\"react-spring/renderprops\");\n\ndescribe(\"generateItemText\", () => {\n  test(\"generating an item's text\", () => {\n    expect(generateItemText(\"cheesecake\", 3)).toBe(\"Cheesecake - Quantity: 3\");\n    expect(generateItemText(\"apple pie\", 22)).toBe(\"Apple pie - Quantity: 22\");\n  });\n});\n\ndescribe(\"ItemList Component\", () => {\n  test(\"list items\", () => {\n    const itemList = { cheesecake: 2, croissant: 5, macaroon: 96 };\n    const { getByText } = render(<ItemList itemList={itemList} />);\n\n    const listElement = document.querySelector(\"ul\");\n    expect(listElement.childElementCount).toBe(3);\n    expect(getByText(generateItemText(\"cheesecake\", 2))).toBeInTheDocument();\n    expect(getByText(generateItemText(\"croissant\", 5))).toBeInTheDocument();\n    expect(getByText(generateItemText(\"macaroon\", 96))).toBeInTheDocument();\n  });\n\n  test(\"highlighting items that are almost out of stock\", () => {\n    const itemList = { cheesecake: 2, croissant: 5, macaroon: 96 };\n\n    const { getByText } = render(<ItemList itemList={itemList} />);\n    const cheesecakeItem = getByText(generateItemText(\"cheesecake\", 2));\n    expect(cheesecakeItem).toMatchSnapshot();\n  });\n});\n"
  },
  {
    "path": "chapter8/4_component_stories/2_documentation/__mocks__/react-spring/renderprops.jsx",
    "content": "const FakeReactSpringTransition = jest.fn(({ items, children }) => {\n  return items.map(item => {\n    return children(item)({ fakeStyles: \"fake \" });\n  });\n});\n\nexport { FakeReactSpringTransition as Transition };\n"
  },
  {
    "path": "chapter8/4_component_stories/2_documentation/__snapshots__/ActionLog.test.jsx.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`logging actions 1`] = `\n<div>\n  <div\n    data-testid=\"action-log\"\n  >\n    <h2>\n      Action Log\n    </h2>\n    <ul>\n      <li>\n        Date: \n        Fri, 02 Jan 1970 00:00:00 GMT\n         - Message: \n        Loaded item list\n         - Data: \n        {\"cheesecake\":2,\"macaroon\":5}\n      </li>\n      <li>\n        Date: \n        Sat, 03 Jan 1970 00:00:00 GMT\n         - Message: \n        Item added\n         - Data: \n        {\"cheesecake\":2}\n      </li>\n      <li>\n        Date: \n        Sun, 04 Jan 1970 00:00:00 GMT\n         - Message: \n        Item removed\n         - Data: \n        {\"cheesecake\":1}\n      </li>\n      <li>\n        Date: \n        Mon, 05 Jan 1970 00:00:00 GMT\n         - Message: \n        Something weird happened\n         - Data: \n        {\"error\":\"The cheesecake is a lie\"}\n      </li>\n    </ul>\n  </div>\n</div>\n`;\n"
  },
  {
    "path": "chapter8/4_component_stories/2_documentation/__snapshots__/App.test.jsx.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`updating the action log adding an item 1`] = `\n<div\n  data-testid=\"action-log\"\n>\n  <h2>\n    Action Log\n  </h2>\n  <ul>\n    <li>\n      Date: \n      Sat, 20 Jun 2020 13:37:00 GMT\n       - Message: \n      Loaded items from the server\n       - Data: \n      {\"status\":200,\"body\":{\"cheesecake\":2,\"croissant\":5,\"macaroon\":96}}\n    </li>\n    <li>\n      Date: \n      Sun, 21 Jun 2020 13:37:00 GMT\n       - Message: \n      Item added\n       - Data: \n      {\"itemAdded\":\"cheesecake\",\"addedQuantity\":6}\n    </li>\n  </ul>\n</div>\n`;\n\nexports[`updating the action log when loading items 1`] = `\n<div\n  data-testid=\"action-log\"\n>\n  <h2>\n    Action Log\n  </h2>\n  <ul>\n    <li>\n      Date: \n      Sat, 20 Jun 2020 13:37:00 GMT\n       - Message: \n      Loaded items from the server\n       - Data: \n      {\"status\":200,\"body\":{\"cheesecake\":2,\"croissant\":5,\"macaroon\":96}}\n    </li>\n  </ul>\n</div>\n`;\n"
  },
  {
    "path": "chapter8/4_component_stories/2_documentation/__snapshots__/ItemList.test.jsx.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`ItemList Component highlighting items that are almost out of stock 1`] = `\n@keyframes animation-0 {\n  0% {\n    opacity: .3;\n  }\n\n  50% {\n    opacity: 1;\n  }\n\n  100% {\n    opacity: .3;\n  }\n}\n\n.emotion-0 {\n  font-weight: bold;\n  color: red;\n  -webkit-animation: animation-0 2s infinite;\n  animation: animation-0 2s infinite;\n}\n\n<li\n  class=\"emotion-0\"\n>\n  Cheesecake - Quantity: 2\n</li>\n`;\n"
  },
  {
    "path": "chapter8/4_component_stories/2_documentation/babel.config.js",
    "content": "module.exports = {\n  presets: [\n    [\n      \"@babel/preset-env\",\n      {\n        targets: {\n          node: \"current\"\n        }\n      }\n    ],\n    \"@babel/preset-react\"\n  ]\n};\n"
  },
  {
    "path": "chapter8/4_component_stories/2_documentation/constants.js",
    "content": "export const API_ADDR = \"http://localhost:3000\";\n"
  },
  {
    "path": "chapter8/4_component_stories/2_documentation/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Inventory</title>\n    <link rel=\"stylesheet\" href=\"./styles.css\" />\n  </head>\n  <body>\n    <div id=\"app\" />\n    <script src=\"bundle.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter8/4_component_stories/2_documentation/index.jsx",
    "content": "import ReactDOM from \"react-dom\";\nimport React from \"react\";\nimport { App } from \"./App.jsx\";\n\nReactDOM.render(<App />, document.getElementById(\"app\"));\n"
  },
  {
    "path": "chapter8/4_component_stories/2_documentation/jest.config.js",
    "content": "module.exports = {\n  snapshotSerializers: [\"jest-emotion\"],\n  setupFilesAfterEnv: [\n    \"<rootDir>/setupJestDom.js\",\n    \"<rootDir>/setupJestEmotion.js\",\n    \"<rootDir>/setupGlobalFetch.js\"\n  ]\n};\n"
  },
  {
    "path": "chapter8/4_component_stories/2_documentation/package.json",
    "content": "{\n  \"name\": \"1_component_stories\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"storybook\": \"start-storybook\",\n    \"build\": \"browserify index.jsx -p esmify -o bundle.js\",\n    \"start\": \"http-server ./\",\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.9.6\",\n    \"@babel/preset-env\": \"^7.9.6\",\n    \"@babel/preset-react\": \"^7.9.4\",\n    \"@storybook/addon-actions\": \"^6.0.28\",\n    \"@storybook/addon-docs\": \"^5.3.19\",\n    \"@storybook/addon-knobs\": \"^6.0.28\",\n    \"@storybook/react\": \"^6.0.28\",\n    \"@testing-library/dom\": \"^7.10.1\",\n    \"@testing-library/jest-dom\": \"^5.9.0\",\n    \"@testing-library/react\": \"^10.2.1\",\n    \"babel-loader\": \"^8.1.0\",\n    \"babelify\": \"^10.0.0\",\n    \"browserify\": \"^16.5.1\",\n    \"core-js\": \"^2.6.11\",\n    \"esmify\": \"^2.1.1\",\n    \"fetch-mock\": \"^9.10.3\",\n    \"http-server\": \"^0.12.3\",\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"jest\": \"^25.5\",\n    \"jest-emotion\": \"^10.0.32\",\n    \"nock\": \"^12.0.3\",\n    \"react-is\": \"^16.13.1\"\n  },\n  \"dependencies\": {\n    \"@emotion/core\": \"^10.0.28\",\n    \"prop-types\": \"^15.7.2\",\n    \"react\": \"^16.13.1\",\n    \"react-dom\": \"^16.13.1\",\n    \"react-spring\": \"^8.0.27\"\n  },\n  \"browserify\": {\n    \"transform\": [\n      [\n        \"babelify\",\n        {\n          \"presets\": [\n            [\n              \"@babel/preset-env\",\n              {\n                \"useBuiltIns\": \"usage\",\n                \"corejs\": 2\n              }\n            ],\n            \"@babel/preset-react\"\n          ]\n        }\n      ]\n    ]\n  }\n}\n"
  },
  {
    "path": "chapter8/4_component_stories/2_documentation/setupGlobalFetch.js",
    "content": "const fetch = require(\"isomorphic-fetch\");\n\nglobal.window.fetch = fetch;\n"
  },
  {
    "path": "chapter8/4_component_stories/2_documentation/setupJestDom.js",
    "content": "const jestDom = require(\"@testing-library/jest-dom\");\n\nexpect.extend(jestDom);\n"
  },
  {
    "path": "chapter8/4_component_stories/2_documentation/setupJestEmotion.js",
    "content": "const { matchers } = require(\"jest-emotion\");\n\nexpect.extend(matchers);\n"
  },
  {
    "path": "chapter8/4_component_stories/2_documentation/styles.css",
    "content": ".almost-out-of-stock {\n  font-weight: bold;\n  color: red;\n}\n"
  },
  {
    "path": "chapter8/server/README.md",
    "content": "# Chapter 5 Server\n\nTo better support the client-side application we'll build on Chapter 5, I've had to do a few updates to the server from Chapter 4.\n\nIn case you want to update the back-end from Chapter 4 yourself, here's the list of changes I've done:\n\n- For the server to accept the requests coming from the client, you'll need to use [`@koa/cors`](https://github.com/koajs/cors)\n- To enable running tests while the server is running, I bind it to different ports depending on whether I am in a test or development environment.\n- At `POST /inventory/:itemName` I have added a route which adds an item to the inventory. It takes a `body` containing the `quantity` to add.\n- At `GET /inventory` I have added a route which lists all items in the inventory.\n- At `DELETE /inventory/:itemName` I have added a route which let's you delete inventory items so that you can use to fix the `undo` functionality\n- I've used `koa-socket-2` to add support for `socket.io`\n- The `POST /inventory/:itemName` will now push updates to all clients but the one which added an item.\n"
  },
  {
    "path": "chapter8/server/authenticationController.js",
    "content": "const crypto = require(\"crypto\");\nconst { db } = require(\"./dbConnection\");\n\nconst hashPassword = password => {\n  const hash = crypto.createHash(\"sha256\");\n  hash.update(password);\n  return hash.digest(\"hex\");\n};\n\nconst credentialsAreValid = async (username, password) => {\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  if (!user) return false;\n  return hashPassword(password) === user.passwordHash;\n};\n\nconst authenticationMiddleware = async (ctx, next) => {\n  try {\n    const authHeader = ctx.request.headers.authorization;\n    const credentials = Buffer.from(\n      authHeader.slice(\"basic\".length + 1),\n      \"base64\"\n    ).toString();\n    const [username, password] = credentials.split(\":\");\n\n    const validCredentialsSent = await credentialsAreValid(username, password);\n    if (!validCredentialsSent) throw new Error(\"invalid credentials\");\n  } catch (e) {\n    ctx.status = 401;\n    ctx.body = { message: \"please provide valid credentials\" };\n    return;\n  }\n\n  await next();\n};\n\nmodule.exports = {\n  hashPassword,\n  credentialsAreValid,\n  authenticationMiddleware\n};\n"
  },
  {
    "path": "chapter8/server/authenticationController.test.js",
    "content": "const crypto = require(\"crypto\");\nconst {\n  hashPassword,\n  credentialsAreValid,\n  authenticationMiddleware\n} = require(\"./authenticationController\");\nconst { user: globalUser } = require(\"./userTestUtils\");\n\ndescribe(\"hashPassword\", () => {\n  test(\"hashing passwords\", () => {\n    const plainTextPassword = \"password_example\";\n    const hash = crypto.createHash(\"sha256\");\n    hash.update(plainTextPassword);\n    const expectedHash = hash.digest(\"hex\");\n    expect(hashPassword(plainTextPassword)).toBe(expectedHash);\n  });\n});\n\ndescribe(\"credentialsAreValid\", () => {\n  test(\"validating credentials\", async () => {\n    expect(await credentialsAreValid(globalUser.username, \"a_password\")).toBe(\n      true\n    );\n  });\n});\n\ndescribe(\"authenticationMiddleware\", () => {\n  test(\"returning an error if the credentials are not valid\", async () => {\n    const fakeAuth = Buffer.from(\"invalid:credentials\").toString(\"base64\");\n    const ctx = {\n      request: {\n        headers: { authorization: `Basic ${fakeAuth}` }\n      }\n    };\n\n    const next = jest.fn();\n    await authenticationMiddleware(ctx, next);\n    expect(next.mock.calls).toHaveLength(0);\n    expect(ctx).toEqual({\n      ...ctx,\n      status: 401,\n      body: { message: \"please provide valid credentials\" }\n    });\n  });\n\n  test(\"authenticating properly\", async () => {\n    const ctx = {\n      request: {\n        headers: { authorization: globalUser.authHeader }\n      }\n    };\n\n    const next = jest.fn();\n    await authenticationMiddleware(ctx, next);\n    expect(next.mock.calls).toHaveLength(1);\n  });\n});\n"
  },
  {
    "path": "chapter8/server/cartController.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { removeFromInventory } = require(\"./inventoryController\");\nconst logger = require(\"./logger\");\n\nconst addItemToCart = async (username, itemName) => {\n  await removeFromInventory(itemName);\n\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  if (!user) {\n    const userNotFound = new Error(\"user not found\");\n    userNotFound.code = 404;\n  }\n\n  const itemEntry = await db\n    .select()\n    .from(\"carts_items\")\n    .where({ userId: user.id, itemName })\n    .first();\n\n  if (itemEntry && itemEntry.quantity + 1 > 3) {\n    const limitError = new Error(\n      \"You can't have more than three units of an item in your cart\"\n    );\n    limitError.code = 400;\n    throw limitError;\n  }\n\n  if (itemEntry) {\n    await db(\"carts_items\")\n      .increment(\"quantity\")\n      .update({ updatedAt: new Date().toISOString() })\n      .where({\n        userId: itemEntry.userId,\n        itemName\n      });\n  } else {\n    await db(\"carts_items\").insert({\n      userId: user.id,\n      itemName,\n      quantity: 1,\n      updatedAt: new Date().toISOString()\n    });\n  }\n\n  logger.log(`${itemName} added to ${username}'s cart`);\n  return db\n    .select(\"itemName\", \"quantity\")\n    .from(\"carts_items\")\n    .where({ userId: user.id });\n};\n\nconst hoursInMs = n => 1000 * 60 * 60 * n;\n\nconst removeStaleItems = async () => {\n  const fourHoursAgo = new Date(Date.now() - hoursInMs(4)).toISOString();\n\n  const staleItems = await db\n    .select()\n    .from(\"carts_items\")\n    .where(\"updatedAt\", \"<\", fourHoursAgo);\n\n  if (staleItems.length === 0) return;\n\n  // Put stale items back in the inventory\n  const inventoryUpdates = staleItems.map(staleItem =>\n    db(\"inventory\")\n      .increment(\"quantity\", staleItem.quantity)\n      .where({ itemName: staleItem.itemName })\n  );\n  await Promise.all(inventoryUpdates);\n\n  // Delete stale items from cart\n  const staleItemTuples = staleItems.map(i => [i.itemName, i.userId]);\n  await db(\"carts_items\")\n    .del()\n    .whereIn([\"itemName\", \"userId\"], staleItemTuples);\n};\n\nconst monitorStaleItems = () => setInterval(removeStaleItems, hoursInMs(2));\n\nmodule.exports = { addItemToCart, monitorStaleItems };\n"
  },
  {
    "path": "chapter8/server/cartController.test.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { addItemToCart, monitorStaleItems } = require(\"./cartController\");\nconst { hashPassword } = require(\"./authenticationController\");\nconst { user: globalUser } = require(\"./userTestUtils\");\nconst FakeTimers = require(\"@sinonjs/fake-timers\");\n\nconst fs = require(\"fs\");\n\ndescribe(\"addItemToCart\", () => {\n  beforeEach(() => {\n    fs.writeFileSync(\"/tmp/logs.out\", \"\");\n  });\n\n  test(\"adding unavailable items to cart\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 0 });\n\n    try {\n      await addItemToCart(globalUser.username, \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(\"cheesecake is unavailable\");\n      expectedError.code = 400;\n\n      expect(e).toEqual(expectedError);\n    }\n\n    const finalCartContent = await db\n      .select(\"carts_items.*\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n\n    expect(finalCartContent).toEqual([]);\n    expect.assertions(2);\n  });\n\n  test(\"adding items above limit to cart\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 1 });\n    await db(\"carts_items\").insert({\n      userId: globalUser.id,\n      itemName: \"cheesecake\",\n      quantity: 3\n    });\n\n    try {\n      await addItemToCart(globalUser.username, \"cheesecake\");\n    } catch (e) {\n      const expectedError = new Error(\n        \"You can't have more than three units of an item in your cart\"\n      );\n      expectedError.code = 400;\n      expect(e).toEqual(expectedError);\n    }\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n\n    expect(finalCartContent).toEqual([{ itemName: \"cheesecake\", quantity: 3 }]);\n    expect.assertions(2);\n  });\n\n  test(\"logging added items\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 1 });\n    await db(\"carts_items\").insert({\n      userId: globalUser.id,\n      itemName: \"cheesecake\",\n      quantity: 1\n    });\n\n    await addItemToCart(globalUser.username, \"cheesecake\");\n\n    const logs = fs.readFileSync(\"/tmp/logs.out\", \"utf-8\");\n    expect(logs).toContain(\n      `cheesecake added to ${globalUser.username}'s cart\\n`\n    );\n  });\n});\n\nconst withRetries = async fn => {\n  // Capture the assertion error since Jest does not export it\n  const JestAssertionError = (() => {\n    try {\n      expect(false).toBe(true);\n    } catch (e) {\n      return e.constructor;\n    }\n  })();\n\n  try {\n    await fn();\n  } catch (e) {\n    if (e.constructor === JestAssertionError) {\n      // Wait 100ms before retrying\n      await new Promise(resolve => setTimeout(resolve, 100));\n      await withRetries(fn);\n    } else {\n      throw e;\n    }\n  }\n};\n\ndescribe(\"timers\", () => {\n  const hoursInMs = n => 1000 * 60 * 60 * n;\n\n  let clock;\n  beforeEach(() => {\n    clock = FakeTimers.install({ toFake: [\"Date\", \"setInterval\"] });\n  });\n\n  afterEach(() => {\n    clock = clock.uninstall();\n  });\n\n  test(\"removing stale items\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 1 });\n    await addItemToCart(globalUser.username, \"cheesecake\");\n\n    clock.tick(hoursInMs(4));\n    timer = monitorStaleItems();\n    clock.tick(hoursInMs(2));\n\n    await withRetries(async () => {\n      const finalCartContent = await db\n        .select()\n        .from(\"carts_items\")\n        .join(\"users\", \"users.id\", \"carts_items.userId\")\n        .where(\"users.username\", globalUser.username);\n\n      expect(finalCartContent).toEqual([]);\n    });\n\n    await withRetries(async () => {\n      const inventoryContent = await db\n        .select(\"itemName\", \"quantity\")\n        .from(\"inventory\");\n\n      expect(inventoryContent).toEqual([\n        { itemName: \"cheesecake\", quantity: 1 }\n      ]);\n    });\n  });\n});\n"
  },
  {
    "path": "chapter8/server/dbConnection.js",
    "content": "const environmentName = process.env.NODE_ENV;\nconst db = require(\"knex\")(require(\"./knexfile\")[environmentName]);\n\nconst closeConnection = () => db.destroy();\n\nmodule.exports = {\n  db,\n  closeConnection\n};\n"
  },
  {
    "path": "chapter8/server/disconnectFromDb.js",
    "content": "const { db } = require(\"./dbConnection\");\n\nafterAll(() => db.destroy());\n"
  },
  {
    "path": "chapter8/server/inventoryController.js",
    "content": "const { db } = require(\"./dbConnection\");\n\nconst removeFromInventory = async itemName => {\n  const inventoryEntry = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName })\n    .first();\n\n  if (!inventoryEntry || inventoryEntry.quantity === 0) {\n    const err = new Error(`${itemName} is unavailable`);\n    err.code = 400;\n    throw err;\n  }\n\n  await db(\"inventory\")\n    .decrement(\"quantity\")\n    .where({ itemName });\n};\n\nmodule.exports = { removeFromInventory };\n"
  },
  {
    "path": "chapter8/server/jest.config.js",
    "content": "module.exports = {\n  testEnvironment: \"node\",\n  globalSetup: \"./migrateDatabases.js\",\n  setupFilesAfterEnv: [\n    \"<rootDir>/truncateTables.js\",\n    \"<rootDir>/seedUser.js\",\n    \"<rootDir>/disconnectFromDb.js\"\n  ]\n};\n"
  },
  {
    "path": "chapter8/server/knexfile.js",
    "content": "module.exports = {\n  test: {\n    client: \"sqlite3\",\n    connection: { filename: \"./test.sqlite\" },\n    useNullAsDefault: true\n  },\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"./dev.sqlite\" },\n    useNullAsDefault: true\n  }\n};\n"
  },
  {
    "path": "chapter8/server/logger.js",
    "content": "const fs = require(\"fs\");\n\nconst logger = {\n  log: msg => fs.appendFileSync(\"/tmp/logs.out\", msg + \"\\n\")\n};\n\nmodule.exports = logger;\n"
  },
  {
    "path": "chapter8/server/migrateDatabases.js",
    "content": "const environmentName = process.env.NODE_ENV || \"test\";\nconst environmentConfig = require(\"./knexfile\")[environmentName];\nconst db = require(\"knex\")(environmentConfig);\n\nmodule.exports = async () => {\n  // Migrate the database to the latest state\n  await db.migrate.latest();\n\n  // Close the connection to the database so that tests won't hang\n  await db.destroy();\n};\n"
  },
  {
    "path": "chapter8/server/migrations/20200325082401_initial_schema.js",
    "content": "exports.up = async knex => {\n  await knex.schema.createTable(\"users\", table => {\n    table.increments(\"id\");\n    table.string(\"username\");\n    table.unique(\"username\");\n    table.string(\"email\");\n    table.string(\"passwordHash\");\n  });\n\n  await knex.schema.createTable(\"carts_items\", table => {\n    table.integer(\"userId\").references(\"users.id\");\n    table.string(\"itemName\");\n    table.unique(\"itemName\");\n    table.integer(\"quantity\");\n  });\n\n  await knex.schema.createTable(\"inventory\", table => {\n    table.increments(\"id\");\n    table.string(\"itemName\");\n    table.unique(\"itemName\");\n    table.integer(\"quantity\");\n  });\n};\n\nexports.down = async knex => {\n  await knex.schema.dropTable(\"users\");\n  await knex.schema.dropTable(\"carts_items\");\n  await knex.schema.dropTable(\"inventory\");\n};\n"
  },
  {
    "path": "chapter8/server/migrations/20200331210311_updatedAt_field.js",
    "content": "exports.up = knex => {\n  return knex.schema.alterTable(\"carts_items\", table => {\n    table.timestamp(\"updatedAt\");\n  });\n};\n\nexports.down = knex => {\n  return knex.schema.alterTable(\"carts_items\", table => {\n    table.dropColumn(\"updatedAt\");\n  });\n};\n"
  },
  {
    "path": "chapter8/server/package.json",
    "content": "{\n  \"name\": \"chapter5_server\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"test\": \"jest --runInBand\",\n    \"start\": \"cross-env NODE_ENV=development node server.js\",\n    \"migrate:dev\": \"knex migrate:latest --env development\",\n    \"seed:dev\": \"knex seed:run\"\n  },\n  \"devDependencies\": {\n    \"@sinonjs/fake-timers\": \"github:sinonjs/fake-timers\",\n    \"jest\": \"^24.9.0\",\n    \"supertest\": \"^4.0.2\"\n  },\n  \"dependencies\": {\n    \"@koa/cors\": \"^3.0.0\",\n    \"cross-env\": \"^7.0.2\",\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"knex\": \"^0.20.13\",\n    \"koa\": \"^2.11.0\",\n    \"koa-body-parser\": \"^1.1.2\",\n    \"koa-router\": \"^7.4.0\",\n    \"koa-socket-2\": \"^1.2.0\",\n    \"nock\": \"^12.0.3\",\n    \"socket.io\": \"^2.3.0\",\n    \"sqlite3\": \"^4.1.1\"\n  },\n  \"main\": \"alertController.spec.js\",\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"description\": \"\"\n}\n"
  },
  {
    "path": "chapter8/server/seedUser.js",
    "content": "const { createUser } = require(\"./userTestUtils\");\n\nbeforeEach(createUser);\n"
  },
  {
    "path": "chapter8/server/seeds/initial_inventory.js",
    "content": "exports.seed = async knex => {\n  await knex(\"inventory\").del();\n  return knex(\"inventory\").insert([\n    { itemName: \"cheesecake\", quantity: 8 },\n    { itemName: \"apple pie\", quantity: 2 },\n    { itemName: \"carrot cake\", quantity: 5 }\n  ]);\n};\n"
  },
  {
    "path": "chapter8/server/server.js",
    "content": "const fetch = require(\"isomorphic-fetch\");\nconst Koa = require(\"koa\");\nconst http = require(\"http\");\nconst IO = require(\"koa-socket-2\");\nconst cors = require(\"@koa/cors\");\nconst Router = require(\"koa-router\");\nconst bodyParser = require(\"koa-body-parser\");\n\nconst { db } = require(\"./dbConnection\");\n\nconst { addItemToCart } = require(\"./cartController\");\nconst {\n  hashPassword,\n  authenticationMiddleware\n} = require(\"./authenticationController\");\n\nconst PORT = process.env.NODE_ENV === \"test\" ? 5000 : 3000;\n\nconst app = new Koa();\nconst io = new IO();\nio.attach(app);\n\nconst router = new Router();\n\napp.use(cors());\n\napp.use(bodyParser());\n\napp.use(async (ctx, next) => {\n  if (ctx.url.startsWith(\"/carts\")) {\n    return await authenticationMiddleware(ctx, next);\n  }\n\n  await next();\n});\n\nrouter.put(\"/users/:username\", async ctx => {\n  const { username } = ctx.params;\n  const { email, password } = ctx.request.body;\n\n  const userAlreadyExists = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n\n  if (userAlreadyExists) {\n    ctx.body = { message: `${username} already exists` };\n    ctx.status = 409;\n    return;\n  }\n\n  await db(\"users\").insert({\n    username,\n    email,\n    passwordHash: hashPassword(password)\n  });\n\n  return (ctx.body = { message: `${username} created successfully` });\n});\n\nrouter.post(\"/carts/:username/items\", async ctx => {\n  const { username } = ctx.params;\n  const { item, quantity } = ctx.request.body;\n\n  for (let i = 0; i < quantity; i++) {\n    try {\n      const newItems = await addItemToCart(username, item);\n      ctx.body = newItems;\n    } catch (e) {\n      ctx.body = { message: e.message };\n      ctx.status = e.code;\n      return;\n    }\n  }\n});\n\nrouter.delete(\"/carts/:username/items/:item\", async ctx => {\n  const { username, item } = ctx.params;\n  const user = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n\n  if (!user) {\n    ctx.body = { message: \"user not found\" };\n    ctx.status = 404;\n    return;\n  }\n\n  const itemEntry = await db\n    .select()\n    .from(\"carts_items\")\n    .where({ userId: user.id, itemName: item })\n    .first();\n\n  if (!itemEntry || itemEntry.quantity === 0) {\n    ctx.body = { message: `${item} is not in the cart` };\n    ctx.status = 400;\n    return;\n  }\n\n  await db(\"carts_items\")\n    .decrement(\"quantity\")\n    .where({ userId: user.id, itemName: item });\n\n  const inventoryEntry = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName: item })\n    .first();\n  if (inventoryEntry) {\n    await db(\"inventory\")\n      .increment(\"quantity\")\n      .where({ userId: itemEntry.userId, itemName: item });\n  } else {\n    await db(\"inventory\").insert({ itemName: item, quantity: 1 });\n  }\n\n  ctx.body = await db\n    .select(\"itemName\", \"quantity\")\n    .from(\"carts_items\")\n    .where({ userId: user.id });\n});\n\nrouter.post(\"/inventory/:itemName\", async ctx => {\n  const { itemName } = ctx.params;\n  const { quantity } = ctx.request.body;\n  const clientId = ctx.request.headers[\"x-socket-client-id\"];\n\n  const current = await db\n    .select(\"itemName\", \"quantity\")\n    .from(\"inventory\")\n    .where({ itemName })\n    .first();\n\n  const itemExists = current && current.quantity > 0;\n  const newRecord = {\n    itemName,\n    quantity: (itemExists ? current.quantity : 0) + quantity\n  };\n\n  if (current) {\n    await db(\"inventory\")\n      .increment(\"quantity\", quantity)\n      .where({ itemName });\n  } else {\n    await db(\"inventory\").insert(newRecord);\n  }\n\n  Object.entries(io.socket.sockets.connected).forEach(([id, socket]) => {\n    if (id === clientId) return;\n    socket.emit(\"add_item\", { itemName, quantity });\n  });\n\n  ctx.body = newRecord;\n});\n\nrouter.delete(\"/inventory/:itemName\", async ctx => {\n  const { itemName } = ctx.params;\n  const { quantity } = ctx.request.body;\n\n  const current = await db\n    .select(\"itemName\", \"quantity\")\n    .from(\"inventory\")\n    .where({ itemName })\n    .first();\n\n  const canDelete = current && current.quantity > quantity;\n\n  if (canDelete) {\n    await db(\"inventory\")\n      .decrement(\"quantity\", quantity)\n      .where({ itemName });\n    ctx.body = { message: `Removed ${quantity} units of ${itemName}` };\n  } else {\n    ctx.status = 404;\n    ctx.body = {\n      message: `There aren't ${quantity} units of ${itemName} available.`\n    };\n  }\n});\n\nrouter.get(\"/inventory\", async ctx => {\n  const inventoryContent = await db\n    .select(\"itemName\", \"quantity\")\n    .from(\"inventory\")\n    .where(\"quantity\", \">\", 0)\n    .orderBy(\"quantity\", \"desc\");\n\n  ctx.body = inventoryContent.reduce((acc, { itemName, quantity }) => {\n    return { ...acc, [itemName]: quantity };\n  }, {});\n});\n\nrouter.get(\"/inventory/:itemName\", async ctx => {\n  const { itemName } = ctx.params;\n\n  const response = await fetch(`http://recipepuppy.com/api?i=${itemName}`);\n  const { title, href, results: recipes } = await response.json();\n  const inventoryItem = await db\n    .select()\n    .from(\"inventory\")\n    .where({ itemName })\n    .first();\n\n  ctx.body = {\n    ...inventoryItem,\n    info: `Data obtained from ${title} - ${href}`,\n    recipes\n  };\n});\n\napp.use(router.routes());\n\nmodule.exports = { app: app.listen(PORT, \"127.0.0.1\") };\n"
  },
  {
    "path": "chapter8/server/server.test.js",
    "content": "const { user: globalUser } = require(\"./userTestUtils\");\nconst { db } = require(\"./dbConnection\");\nconst request = require(\"supertest\");\nconst { app } = require(\"./server.js\");\nconst { hashPassword } = require(\"./authenticationController.js\");\nconst nock = require(\"nock\");\n\nafterAll(() => app.close());\n\ndescribe(\"add items to a cart\", () => {\n  test(\"adding available items\", async () => {\n    await db(\"inventory\").insert({ itemName: \"cheesecake\", quantity: 3 });\n    const response = await request(app)\n      .post(`/carts/${globalUser.username}/items`)\n      .set(\"authorization\", globalUser.authHeader)\n      .send({ item: \"cheesecake\", quantity: 3 })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    const newItems = [{ itemName: \"cheesecake\", quantity: 3 }];\n    expect(response.body).toEqual(newItems);\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(0);\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n\n    expect(finalCartContent).toEqual(newItems);\n  });\n\n  test(\"adding unavailable items\", async () => {\n    const response = await request(app)\n      .post(`/carts/${globalUser.username}/items`)\n      .set(\"authorization\", globalUser.authHeader)\n      .send({ item: \"cheesecake\", quantity: 1 })\n      .expect(400)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"cheesecake is unavailable\"\n    });\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n    expect(finalCartContent).toEqual([]);\n  });\n});\n\ndescribe(\"removing items from a cart\", () => {\n  test(\"removing existing items\", async () => {\n    await db(\"carts_items\").insert({\n      userId: globalUser.id,\n      itemName: \"cheesecake\",\n      quantity: 1\n    });\n\n    const response = await request(app)\n      .del(`/carts/${globalUser.username}/items/cheesecake`)\n      .set(\"authorization\", globalUser.authHeader)\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    const expectedFinalContent = [{ itemName: \"cheesecake\", quantity: 0 }];\n\n    expect(response.body).toEqual(expectedFinalContent);\n\n    const finalCartContent = await db\n      .select(\"carts_items.itemName\", \"carts_items.quantity\")\n      .from(\"carts_items\")\n      .join(\"users\", \"users.id\", \"carts_items.userId\")\n      .where(\"users.username\", globalUser.username);\n    expect(finalCartContent).toEqual(expectedFinalContent);\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(1);\n  });\n\n  test(\"removing non-existing items\", async () => {\n    await db(\"inventory\").insert({\n      itemName: \"cheesecake\",\n      quantity: 0\n    });\n\n    const response = await request(app)\n      .del(`/carts/${globalUser.username}/items/cheesecake`)\n      .set(\"authorization\", globalUser.authHeader)\n      .expect(400)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"cheesecake is not in the cart\"\n    });\n\n    const { quantity: inventoryCheesecakes } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"cheesecake\" })\n      .first();\n    expect(inventoryCheesecakes).toEqual(0);\n  });\n});\n\ndescribe(\"create accounts\", () => {\n  test(\"creating a new account\", async () => {\n    const response = await request(app)\n      .put(\"/users/another_user\")\n      .send({ email: \"another_user@example.org\", password: \"a_password\" })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: \"another_user created successfully\"\n    });\n\n    const savedUser = await db\n      .select(\"email\", \"passwordHash\")\n      .from(\"users\")\n      .where({ username: \"another_user\" })\n      .first();\n\n    expect(savedUser).toEqual({\n      email: \"another_user@example.org\",\n      passwordHash: hashPassword(\"a_password\")\n    });\n  });\n\n  test(\"creating a duplicate account\", async () => {\n    const response = await request(app)\n      .put(`/users/${globalUser.username}`)\n      .send({ email: globalUser.email, password: \"a_password\" })\n      .expect(409)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      message: `${globalUser.username} already exists`\n    });\n  });\n});\n\ndescribe(\"list inventory items\", () => {\n  const eggs = { itemName: \"eggs\", quantity: 3 };\n  const applePie = { itemName: \"apple pie\", quantity: 1 };\n  const carrotCake = { itemName: \"carrot cake\", quantity: 0 };\n\n  beforeEach(async () => {\n    await db(\"inventory\").insert([eggs, applePie, carrotCake]);\n  });\n\n  test(\"fetching all available items\", async () => {\n    const { body } = await request(app)\n      .get(\"/inventory\")\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(body).toEqual({ eggs: 3, \"apple pie\": 1 });\n  });\n});\n\ndescribe(\"add inventory items\", () => {\n  test(\"adding a new item\", async () => {\n    const { body } = await request(app)\n      .post(\"/inventory/eggs\")\n      .send({ quantity: 3 })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(body).toEqual({ itemName: \"eggs\", quantity: 3 });\n\n    expect(\n      await db\n        .select(\"itemName\", \"quantity\")\n        .from(\"inventory\")\n        .where(\"itemName\", \"eggs\")\n        .first()\n    ).toEqual({ itemName: \"eggs\", quantity: 3 });\n  });\n\n  test(\"adding an existing item\", async () => {\n    const eggs = { itemName: \"eggs\", quantity: 2 };\n    await db(\"inventory\").insert(eggs);\n\n    const { body } = await request(app)\n      .post(\"/inventory/eggs\")\n      .send({ quantity: 3 })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(body).toEqual({ itemName: \"eggs\", quantity: 5 });\n\n    expect(\n      await db\n        .select(\"itemName\", \"quantity\")\n        .from(\"inventory\")\n        .where(\"itemName\", \"eggs\")\n        .first()\n    ).toEqual({ itemName: \"eggs\", quantity: 5 });\n  });\n});\n\ndescribe(\"remove inventory items\", () => {\n  beforeEach(async () => {\n    await db(\"inventory\").insert({ itemName: \"eggs\", quantity: 3 });\n  });\n\n  test(\"removing an item\", async () => {\n    const { body } = await request(app)\n      .del(\"/inventory/eggs\")\n      .send({ quantity: 2 })\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(body).toEqual({\n      message: \"Removed 2 units of eggs\"\n    });\n\n    expect(\n      await db\n        .select(\"itemName\", \"quantity\")\n        .from(\"inventory\")\n        .where(\"itemName\", \"eggs\")\n        .first()\n    ).toEqual({ itemName: \"eggs\", quantity: 1 });\n  });\n\n  test(\"removing more than the inventory quantity\", async () => {\n    const { body } = await request(app)\n      .del(\"/inventory/eggs\")\n      .send({ quantity: 4 })\n      .expect(404)\n      .expect(\"Content-Type\", /json/);\n\n    expect(body).toEqual({\n      message: \"There aren't 4 units of eggs available.\"\n    });\n\n    expect(\n      await db\n        .select(\"itemName\", \"quantity\")\n        .from(\"inventory\")\n        .where(\"itemName\", \"eggs\")\n        .first()\n    ).toEqual({ itemName: \"eggs\", quantity: 3 });\n  });\n});\n\ndescribe(\"fetch inventory items\", () => {\n  const eggs = { itemName: \"eggs\", quantity: 3 };\n  const applePie = { itemName: \"apple pie\", quantity: 1 };\n\n  beforeEach(async () => {\n    await db(\"inventory\").insert([eggs, applePie]);\n    const { id: eggsId } = await db\n      .select()\n      .from(\"inventory\")\n      .where({ itemName: \"eggs\" })\n      .first();\n    eggs.id = eggsId;\n  });\n\n  test(\"fetching an item from the inventory\", async () => {\n    const eggsResponse = {\n      title: \"FakeAPI\",\n      href: \"example.org\",\n      results: [{ name: \"Omelette du Fromage\" }]\n    };\n\n    nock(\"http://recipepuppy.com\")\n      .get(\"/api\")\n      .query({ i: \"eggs\" })\n      .reply(200, eggsResponse);\n\n    const response = await request(app)\n      .get(`/inventory/eggs`)\n      .expect(200)\n      .expect(\"Content-Type\", /json/);\n\n    expect(response.body).toEqual({\n      ...eggs,\n      info: `Data obtained from ${eggsResponse.title} - ${eggsResponse.href}`,\n      recipes: eggsResponse.results\n    });\n  });\n});\n"
  },
  {
    "path": "chapter8/server/truncateTables.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst tablesToTruncate = [\"users\", \"inventory\", \"carts_items\"];\n\nbeforeEach(() => {\n  return Promise.all(tablesToTruncate.map(t => db(t).truncate()));\n});\n"
  },
  {
    "path": "chapter8/server/userTestUtils.js",
    "content": "const { db } = require(\"./dbConnection\");\nconst { hashPassword } = require(\"./authenticationController\");\n\nconst username = \"test_user\";\nconst password = \"a_password\";\nconst passwordHash = hashPassword(password);\nconst email = \"test_user@example.org\";\nconst validAuth = Buffer.from(`${username}:${password}`).toString(\"base64\");\nconst authHeader = `Basic ${validAuth}`;\n\nconst user = {\n  username,\n  password,\n  email,\n  authHeader\n};\n\nconst createUser = async () => {\n  await db(\"users\").insert({ username, email, passwordHash });\n  const { id } = await db\n    .select()\n    .from(\"users\")\n    .where({ username })\n    .first();\n  user.id = id;\n};\n\nmodule.exports = { user, createUser };\n"
  },
  {
    "path": "chapter9/1_the_philosophy_behind_tdd/1_what_tdd_is/1_small_test/calculateCartPrice.js",
    "content": "const calculateCartPrice = () => 7;\n\nmodule.exports = { calculateCartPrice };\n"
  },
  {
    "path": "chapter9/1_the_philosophy_behind_tdd/1_what_tdd_is/1_small_test/calculateCartPrice.test.js",
    "content": "const { calculateCartPrice } = require(\"./calculateCartPrice\");\n\ntest(\"calculating total values\", () => {\n  expect(calculateCartPrice([1, 1, 2, 3])).toBe(7);\n});\n"
  },
  {
    "path": "chapter9/1_the_philosophy_behind_tdd/1_what_tdd_is/1_small_test/package.json",
    "content": "{\n  \"name\": \"1_small_test\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"jest\": \"^26.1.0\"\n  }\n}\n"
  },
  {
    "path": "chapter9/1_the_philosophy_behind_tdd/1_what_tdd_is/2_partial_test/calculateCartPrice.js",
    "content": "const calculateCartPrice = prices => {\n  return prices.reduce((sum, price) => {\n    return sum + price;\n  }, 0);\n};\n\nmodule.exports = { calculateCartPrice };\n"
  },
  {
    "path": "chapter9/1_the_philosophy_behind_tdd/1_what_tdd_is/2_partial_test/calculateCartPrice.test.js",
    "content": "const { calculateCartPrice } = require(\"./calculateCartPrice\");\n\ntest(\"calculating total values\", () => {\n  expect(calculateCartPrice([1, 1, 2, 3])).toBe(7);\n  expect(calculateCartPrice([3, 5, 8])).toBe(16);\n  expect(calculateCartPrice([13, 21])).toBe(34);\n  expect(calculateCartPrice([55])).toBe(55);\n});\n"
  },
  {
    "path": "chapter9/1_the_philosophy_behind_tdd/1_what_tdd_is/2_partial_test/package.json",
    "content": "{\n  \"name\": \"2_partial_test\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"jest\": \"^26.1.0\"\n  }\n}\n"
  },
  {
    "path": "chapter9/1_the_philosophy_behind_tdd/1_what_tdd_is/3_extra_test/calculateCartPrice.js",
    "content": "const calculateCartPrice = (prices, discountPercentage) => {\n  const total = prices.reduce((sum, price) => {\n    return sum + price;\n  }, 0);\n\n  return discountPercentage\n    ? ((100 - discountPercentage) / 100) * total\n    : total;\n};\n\nmodule.exports = { calculateCartPrice };\n"
  },
  {
    "path": "chapter9/1_the_philosophy_behind_tdd/1_what_tdd_is/3_extra_test/calculateCartPrice.test.js",
    "content": "const { calculateCartPrice } = require(\"./calculateCartPrice\");\n\ntest(\"calculating total values\", () => {\n  expect(calculateCartPrice([1, 1, 2, 3])).toBe(7);\n  expect(calculateCartPrice([3, 5, 8])).toBe(16);\n  expect(calculateCartPrice([13, 21])).toBe(34);\n  expect(calculateCartPrice([55])).toBe(55);\n});\n\ntest(\"applying a discount\", () => {\n  expect(calculateCartPrice([1, 2, 3], 50)).toBe(3);\n  expect(calculateCartPrice([2, 5, 5], 25)).toBe(9);\n  expect(calculateCartPrice([9, 21], 10)).toBe(27);\n  expect(calculateCartPrice([50, 50], 100)).toBe(0);\n});\n"
  },
  {
    "path": "chapter9/1_the_philosophy_behind_tdd/1_what_tdd_is/3_extra_test/package.json",
    "content": "{\n  \"name\": \"3_extra_test\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"jest\": \"^26.1.0\"\n  }\n}\n"
  },
  {
    "path": "chapter9/1_the_philosophy_behind_tdd/1_what_tdd_is/4_handling_edge_cases/calculateCartPrice.js",
    "content": "const calculateCartPrice = (prices, discountPercentage) => {\n  const total = prices.reduce((sum, price) => {\n    return sum + price;\n  }, 0);\n\n  return typeof discountPercentage === \"number\" && !isNaN(discountPercentage)\n    ? ((100 - discountPercentage) / 100) * total\n    : total;\n};\n\nmodule.exports = { calculateCartPrice };\n"
  },
  {
    "path": "chapter9/1_the_philosophy_behind_tdd/1_what_tdd_is/4_handling_edge_cases/calculateCartPrice.test.js",
    "content": "const { calculateCartPrice } = require(\"./calculateCartPrice\");\n\ntest(\"calculating total values\", () => {\n  expect(calculateCartPrice([1, 1, 2, 3])).toBe(7);\n  expect(calculateCartPrice([3, 5, 8])).toBe(16);\n  expect(calculateCartPrice([13, 21])).toBe(34);\n  expect(calculateCartPrice([55])).toBe(55);\n});\n\ntest(\"applying a discount\", () => {\n  expect(calculateCartPrice([1, 2, 3], 50)).toBe(3);\n  expect(calculateCartPrice([2, 5, 5], 25)).toBe(9);\n  expect(calculateCartPrice([9, 21], 10)).toBe(27);\n  expect(calculateCartPrice([50, 50], 100)).toBe(0);\n});\n\ntest(\"handling strings\", () => {\n  expect(calculateCartPrice([1, 2, 3], \"string\")).toBe(6);\n});\n\ntest(\"handling NaN\", () => {\n  expect(calculateCartPrice([1, 2, 3], NaN)).toBe(6);\n});\n"
  },
  {
    "path": "chapter9/1_the_philosophy_behind_tdd/1_what_tdd_is/4_handling_edge_cases/package.json",
    "content": "{\n  \"name\": \"4_handling_edge_cases\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"jest\": \"^26.1.0\"\n  }\n}\n"
  },
  {
    "path": "chapter9/1_the_philosophy_behind_tdd/2_adjusting_iteration_size/1_bigger_steps/calculateCartPrice.js",
    "content": "const calculateCartPrice = (prices, discountPercentage) => {\n  const total = prices.reduce((sum, price) => {\n    return sum + price;\n  }, 0);\n\n  return discountPercentage\n    ? ((100 - discountPercentage) / 100) * total\n    : total;\n};\n\nmodule.exports = { calculateCartPrice };\n"
  },
  {
    "path": "chapter9/1_the_philosophy_behind_tdd/2_adjusting_iteration_size/1_bigger_steps/calculateCartPrice.test.js",
    "content": "const { calculateCartPrice } = require(\"./calculateCartPrice\");\n\ntest(\"calculating total values\", () => {\n  expect(calculateCartPrice([1, 1, 2, 3])).toBe(7);\n  expect(calculateCartPrice([3, 5, 8])).toBe(16);\n  expect(calculateCartPrice([13, 21])).toBe(34);\n  expect(calculateCartPrice([55])).toBe(55);\n});\n\ntest(\"applying a discount\", () => {\n  expect(calculateCartPrice([1, 2, 3], 50)).toBe(3);\n  expect(calculateCartPrice([2, 5, 5], 25)).toBe(9);\n  expect(calculateCartPrice([9, 21], 10)).toBe(27);\n  expect(calculateCartPrice([50, 50], 100)).toBe(0);\n});\n"
  },
  {
    "path": "chapter9/1_the_philosophy_behind_tdd/2_adjusting_iteration_size/1_bigger_steps/package.json",
    "content": "{\n  \"name\": \"2_adjusting_iteration_size\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"jest\": \"^26.1.0\"\n  }\n}\n"
  },
  {
    "path": "chapter9/1_the_philosophy_behind_tdd/2_adjusting_iteration_size/1_bigger_steps/pickMostExpensive.js",
    "content": "const { calculateCartPrice } = require(\"./calculateCartPrice\");\n\nconst pickMostExpensive = carts => {\n  let mostExpensivePrice = 0;\n  let mostExpensiveCart = null;\n\n  for (let i = 0; i < carts.length; i++) {\n    const currentCart = carts[i];\n    const currentCartPrice = calculateCartPrice(currentCart);\n    if (currentCartPrice >= mostExpensivePrice) {\n      mostExpensivePrice = currentCartPrice;\n      mostExpensiveCart = currentCart;\n    }\n  }\n\n  return mostExpensiveCart;\n};\n\nmodule.exports = { pickMostExpensive };\n"
  },
  {
    "path": "chapter9/1_the_philosophy_behind_tdd/2_adjusting_iteration_size/1_bigger_steps/pickMostExpensive.test.js",
    "content": "const { pickMostExpensive } = require(\"./pickMostExpensive\");\n\ntest(\"picking the most expensive cart\", () => {\n  expect(pickMostExpensive([[3, 2, 1, 4], [5], [50]])).toEqual([50]);\n  expect(pickMostExpensive([[2, 8, 9], [0], [20]])).toEqual([20]);\n  expect(pickMostExpensive([[0], [0], [0]])).toEqual([0]);\n  expect(pickMostExpensive([[], [5], []])).toEqual([5]);\n});\n\ntest(\"null for an empty cart array\", () => {\n  expect(pickMostExpensive([])).toEqual(null);\n});\n"
  },
  {
    "path": "chapter9/2_writing_a_js_module_using_tdd/1_generating_item_rows/inventoryReport.js",
    "content": "const generateItemRow = ({ name, quantity, price }) => {\n  if (quantity === 0 || price === 0) return null;\n  return `${name},${quantity},${price},${price * quantity}`;\n};\n\nmodule.exports = { generateItemRow };\n"
  },
  {
    "path": "chapter9/2_writing_a_js_module_using_tdd/1_generating_item_rows/inventoryReport.test.js",
    "content": "const { generateItemRow } = require(\"./inventoryReport\");\n\ntest(\"generating an item's row\", () => {\n  expect(generateItemRow({ name: \"macaroon\", quantity: 12, price: 3 })).toBe(\n    \"macaroon,12,3,36\"\n  );\n  expect(generateItemRow({ name: \"cheesecake\", quantity: 6, price: 12 })).toBe(\n    \"cheesecake,6,12,72\"\n  );\n  expect(generateItemRow({ name: \"apple pie\", quantity: 5, price: 15 })).toBe(\n    \"apple pie,5,15,75\"\n  );\n});\n\ntest(\"ommitting soldout items\", () => {\n  expect(generateItemRow({ name: \"macaroon\", quantity: 0, price: 3 })).toBe(\n    null\n  );\n  expect(generateItemRow({ name: \"cheesecake\", quantity: 0, price: 12 })).toBe(\n    null\n  );\n});\n\ntest(\"ommitting free items\", () => {\n  expect(\n    generateItemRow({ name: \"plastic cups\", quantity: 99, price: 0 })\n  ).toBe(null);\n  expect(generateItemRow({ name: \"napkins\", quantity: 200, price: 0 })).toBe(\n    null\n  );\n});\n"
  },
  {
    "path": "chapter9/2_writing_a_js_module_using_tdd/1_generating_item_rows/package.json",
    "content": "{\n  \"name\": \"1_generating_item_rows\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"jest\": \"^26.1.0\"\n  }\n}\n"
  },
  {
    "path": "chapter9/2_writing_a_js_module_using_tdd/2_generating_total_row/inventoryReport.js",
    "content": "const generateItemRow = ({ name, quantity, price }) => {\n  if (quantity === 0 || price === 0) return null;\n  return `${name},${quantity},${price},${price * quantity}`;\n};\n\nconst generateTotalRow = items => {\n  const total = items.reduce(\n    (t, { price, quantity }) => t + price * quantity,\n    0\n  );\n  return `Total,,,${total}`;\n};\n\nmodule.exports = { generateItemRow, generateTotalRow };\n"
  },
  {
    "path": "chapter9/2_writing_a_js_module_using_tdd/2_generating_total_row/inventoryReport.test.js",
    "content": "const { generateItemRow, generateTotalRow } = require(\"./inventoryReport\");\n\ndescribe(\"generateItemRow\", () => {\n  test(\"generating an item's row\", () => {\n    expect(generateItemRow({ name: \"macaroon\", quantity: 12, price: 3 })).toBe(\n      \"macaroon,12,3,36\"\n    );\n    expect(\n      generateItemRow({ name: \"cheesecake\", quantity: 6, price: 12 })\n    ).toBe(\"cheesecake,6,12,72\");\n    expect(generateItemRow({ name: \"apple pie\", quantity: 5, price: 15 })).toBe(\n      \"apple pie,5,15,75\"\n    );\n  });\n\n  test(\"ommitting soldout items\", () => {\n    expect(generateItemRow({ name: \"macaroon\", quantity: 0, price: 3 })).toBe(\n      null\n    );\n    expect(\n      generateItemRow({ name: \"cheesecake\", quantity: 0, price: 12 })\n    ).toBe(null);\n  });\n\n  test(\"ommitting free items\", () => {\n    expect(\n      generateItemRow({ name: \"plastic cups\", quantity: 99, price: 0 })\n    ).toBe(null);\n    expect(generateItemRow({ name: \"napkins\", quantity: 200, price: 0 })).toBe(\n      null\n    );\n  });\n});\n\ndescribe(\"generateTotalRow\", () => {\n  test(\"generating a total row\", () => {\n    const items = [\n      { name: \"apple pie\", quantity: 3, price: 15 },\n      { name: \"plastic cups\", quantity: 0, price: 55 },\n      { name: \"macaroon\", quantity: 12, price: 3 },\n      { name: \"cheesecake\", quantity: 0, price: 12 }\n    ];\n\n    expect(generateTotalRow(items)).toBe(\"Total,,,81\");\n    expect(generateTotalRow(items.slice(1))).toBe(\"Total,,,36\");\n    expect(generateTotalRow(items.slice(3))).toBe(\"Total,,,0\");\n  });\n});\n"
  },
  {
    "path": "chapter9/2_writing_a_js_module_using_tdd/2_generating_total_row/package.json",
    "content": "{\n  \"name\": \"2_generating_total_row\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"jest\": \"^26.1.0\"\n  }\n}\n"
  },
  {
    "path": "chapter9/2_writing_a_js_module_using_tdd/3_creating_report/inventoryReport.js",
    "content": "const fs = require(\"fs\");\n\nconst generateItemRow = ({ name, quantity, price }) => {\n  if (quantity === 0 || price === 0) return null;\n  return `${name},${quantity},${price},${price * quantity}`;\n};\n\nconst generateTotalRow = items => {\n  const total = items.reduce(\n    (t, { price, quantity }) => t + price * quantity,\n    0\n  );\n  return `Total,,,${total}`;\n};\n\nconst createInventoryValuesReport = items => {\n  const itemRows = items.map(generateItemRow).join(\"\\n\");\n  const totalRow = generateTotalRow(items);\n  const reportContents = itemRows + \"\\n\" + totalRow;\n  fs.writeFileSync(\"/tmp/inventoryValues.csv\", reportContents);\n};\n\nmodule.exports = {\n  generateItemRow,\n  generateTotalRow,\n  createInventoryValuesReport\n};\n"
  },
  {
    "path": "chapter9/2_writing_a_js_module_using_tdd/3_creating_report/inventoryReport.test.js",
    "content": "const fs = require(\"fs\");\nconst {\n  generateItemRow,\n  generateTotalRow,\n  createInventoryValuesReport\n} = require(\"./inventoryReport\");\n\ndescribe(\"generateItemRow\", () => {\n  test(\"generating an item's row\", () => {\n    expect(generateItemRow({ name: \"macaroon\", quantity: 12, price: 3 })).toBe(\n      \"macaroon,12,3,36\"\n    );\n    expect(\n      generateItemRow({ name: \"cheesecake\", quantity: 6, price: 12 })\n    ).toBe(\"cheesecake,6,12,72\");\n    expect(generateItemRow({ name: \"apple pie\", quantity: 5, price: 15 })).toBe(\n      \"apple pie,5,15,75\"\n    );\n  });\n\n  test(\"ommitting soldout items\", () => {\n    expect(generateItemRow({ name: \"macaroon\", quantity: 0, price: 3 })).toBe(\n      null\n    );\n    expect(\n      generateItemRow({ name: \"cheesecake\", quantity: 0, price: 12 })\n    ).toBe(null);\n  });\n\n  test(\"ommitting free items\", () => {\n    expect(\n      generateItemRow({ name: \"plastic cups\", quantity: 99, price: 0 })\n    ).toBe(null);\n    expect(generateItemRow({ name: \"napkins\", quantity: 200, price: 0 })).toBe(\n      null\n    );\n  });\n});\n\ndescribe(\"generateTotalRow\", () => {\n  test(\"generating a total row\", () => {\n    const items = [\n      { name: \"apple pie\", quantity: 3, price: 15 },\n      { name: \"plastic cups\", quantity: 0, price: 55 },\n      { name: \"macaroon\", quantity: 12, price: 3 },\n      { name: \"cheesecake\", quantity: 0, price: 12 }\n    ];\n\n    expect(generateTotalRow(items)).toBe(\"Total,,,81\");\n    expect(generateTotalRow(items.slice(1))).toBe(\"Total,,,36\");\n    expect(generateTotalRow(items.slice(3))).toBe(\"Total,,,0\");\n  });\n});\n\ndescribe(\"createInventoryValuesReport\", () => {\n  test(\"creating reports\", () => {\n    const items = [\n      { name: \"apple pie\", quantity: 3, price: 15 },\n      { name: \"cheesecake\", quantity: 2, price: 12 },\n      { name: \"macaroon\", quantity: 20, price: 3 }\n    ];\n\n    createInventoryValuesReport(items);\n    expect(fs.readFileSync(\"/tmp/inventoryValues.csv\", \"utf8\")).toBe(\n      \"apple pie,3,15,45\\ncheesecake,2,12,24\\nmacaroon,20,3,60\\nTotal,,,129\"\n    );\n\n    createInventoryValuesReport(items.slice(1));\n    expect(fs.readFileSync(\"/tmp/inventoryValues.csv\", \"utf8\")).toBe(\n      \"cheesecake,2,12,24\\nmacaroon,20,3,60\\nTotal,,,84\"\n    );\n\n    createInventoryValuesReport(items.slice(2));\n    expect(fs.readFileSync(\"/tmp/inventoryValues.csv\", \"utf8\")).toBe(\n      \"macaroon,20,3,60\\nTotal,,,60\"\n    );\n  });\n});\n"
  },
  {
    "path": "chapter9/2_writing_a_js_module_using_tdd/3_creating_report/package.json",
    "content": "{\n  \"name\": \"3_creating_report\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"jest\": \"^26.1.0\"\n  }\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"testing-javascript-applications\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"lint:fix\": \"prettier --write .\",\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/lucasfcosta/testing-javascript-applications.git\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"bugs\": {\n    \"url\": \"https://github.com/lucasfcosta/testing-javascript-applications/issues\"\n  },\n  \"homepage\": \"https://github.com/lucasfcosta/testing-javascript-applications#readme\",\n  \"devDependencies\": {\n    \"husky\": \"^3.1.0\",\n    \"prettier\": \"^1.19.1\",\n    \"pretty-quick\": \"^2.0.1\"\n  },\n  \"husky\": {\n    \"hooks\": {\n      \"pre-commit\": \"pretty-quick --staged\"\n    }\n  }\n}\n"
  }
]