Repository: lucasfcosta/testing-javascript-applications
Branch: master
Commit: 26dfe4572bcb
Files: 852
Total size: 773.9 KB
Directory structure:
gitextract_4jsmhnkg/
├── .gitignore
├── LICENSE
├── README.md
├── chapter11/
│ ├── 1_writing_end_to_end_tests/
│ │ ├── 1_setting_up_cypress/
│ │ │ ├── cypress/
│ │ │ │ ├── fixtures/
│ │ │ │ │ └── example.json
│ │ │ │ ├── integration/
│ │ │ │ │ └── examples/
│ │ │ │ │ ├── actions.spec.js
│ │ │ │ │ ├── aliasing.spec.js
│ │ │ │ │ ├── assertions.spec.js
│ │ │ │ │ ├── connectors.spec.js
│ │ │ │ │ ├── cookies.spec.js
│ │ │ │ │ ├── cypress_api.spec.js
│ │ │ │ │ ├── files.spec.js
│ │ │ │ │ ├── local_storage.spec.js
│ │ │ │ │ ├── location.spec.js
│ │ │ │ │ ├── misc.spec.js
│ │ │ │ │ ├── navigation.spec.js
│ │ │ │ │ ├── network_requests.spec.js
│ │ │ │ │ ├── querying.spec.js
│ │ │ │ │ ├── spies_stubs_clocks.spec.js
│ │ │ │ │ ├── traversal.spec.js
│ │ │ │ │ ├── utilities.spec.js
│ │ │ │ │ ├── viewport.spec.js
│ │ │ │ │ ├── waiting.spec.js
│ │ │ │ │ └── window.spec.js
│ │ │ │ ├── plugins/
│ │ │ │ │ └── index.js
│ │ │ │ └── support/
│ │ │ │ ├── commands.js
│ │ │ │ └── index.js
│ │ │ ├── cypress.json
│ │ │ └── package.json
│ │ ├── 2_writing_your_first_tests/
│ │ │ ├── cypress/
│ │ │ │ ├── dbConnection.js
│ │ │ │ ├── fixtures/
│ │ │ │ │ └── example.json
│ │ │ │ ├── integration/
│ │ │ │ │ └── itemSubmission.spec.js
│ │ │ │ ├── knexfile.js
│ │ │ │ ├── plugins/
│ │ │ │ │ ├── dbPlugin.js
│ │ │ │ │ └── index.js
│ │ │ │ └── support/
│ │ │ │ ├── commands.js
│ │ │ │ └── index.js
│ │ │ ├── cypress.json
│ │ │ └── package.json
│ │ └── 3_sending_http_requests/
│ │ ├── cypress/
│ │ │ ├── dbConnection.js
│ │ │ ├── fixtures/
│ │ │ │ └── example.json
│ │ │ ├── integration/
│ │ │ │ ├── itemListUpdates.spec.js
│ │ │ │ └── itemSubmission.spec.js
│ │ │ ├── knexfile.js
│ │ │ ├── plugins/
│ │ │ │ ├── dbPlugin.js
│ │ │ │ └── index.js
│ │ │ └── support/
│ │ │ ├── commands.js
│ │ │ └── index.js
│ │ ├── cypress.json
│ │ └── package.json
│ ├── 2_best_practices_for_end_to_end_tests/
│ │ ├── 1_page_objects/
│ │ │ ├── cypress/
│ │ │ │ ├── dbConnection.js
│ │ │ │ ├── fixtures/
│ │ │ │ │ └── example.json
│ │ │ │ ├── integration/
│ │ │ │ │ ├── itemListUpdates.spec.js
│ │ │ │ │ └── itemSubmission.spec.js
│ │ │ │ ├── knexfile.js
│ │ │ │ ├── pageObjects/
│ │ │ │ │ └── inventoryManagement.js
│ │ │ │ ├── plugins/
│ │ │ │ │ ├── dbPlugin.js
│ │ │ │ │ └── index.js
│ │ │ │ └── support/
│ │ │ │ ├── commands.js
│ │ │ │ └── index.js
│ │ │ ├── cypress.json
│ │ │ └── package.json
│ │ └── 2_application_actions/
│ │ ├── cypress/
│ │ │ ├── dbConnection.js
│ │ │ ├── fixtures/
│ │ │ │ └── example.json
│ │ │ ├── integration/
│ │ │ │ ├── itemListUpdates.spec.js
│ │ │ │ └── itemSubmission.spec.js
│ │ │ ├── knexfile.js
│ │ │ ├── pageObjects/
│ │ │ │ └── inventoryManagement.js
│ │ │ ├── plugins/
│ │ │ │ ├── dbPlugin.js
│ │ │ │ └── index.js
│ │ │ └── support/
│ │ │ ├── commands.js
│ │ │ └── index.js
│ │ ├── cypress.json
│ │ └── package.json
│ ├── 3_dealing_with_flakiness/
│ │ ├── 1_avoiding_waiting_for_fixed_amounts_of_time/
│ │ │ ├── cypress/
│ │ │ │ ├── dbConnection.js
│ │ │ │ ├── fixtures/
│ │ │ │ │ └── example.json
│ │ │ │ ├── integration/
│ │ │ │ │ ├── itemListUpdates.spec.js
│ │ │ │ │ └── itemSubmission.spec.js
│ │ │ │ ├── knexfile.js
│ │ │ │ ├── pageObjects/
│ │ │ │ │ └── inventoryManagement.js
│ │ │ │ ├── plugins/
│ │ │ │ │ ├── dbPlugin.js
│ │ │ │ │ └── index.js
│ │ │ │ └── support/
│ │ │ │ ├── commands.js
│ │ │ │ └── index.js
│ │ │ ├── cypress.json
│ │ │ └── package.json
│ │ └── 2_stubbing_uncontrollable_factors/
│ │ ├── cypress/
│ │ │ ├── dbConnection.js
│ │ │ ├── fixtures/
│ │ │ │ └── example.json
│ │ │ ├── integration/
│ │ │ │ ├── itemListUpdates.spec.js
│ │ │ │ └── itemSubmission.spec.js
│ │ │ ├── knexfile.js
│ │ │ ├── pageObjects/
│ │ │ │ └── inventoryManagement.js
│ │ │ ├── plugins/
│ │ │ │ ├── dbPlugin.js
│ │ │ │ └── index.js
│ │ │ └── support/
│ │ │ ├── commands.js
│ │ │ └── index.js
│ │ ├── cypress.json
│ │ └── package.json
│ ├── 4_visual_regression_tests/
│ │ ├── cypress/
│ │ │ ├── dbConnection.js
│ │ │ ├── fixtures/
│ │ │ │ └── example.json
│ │ │ ├── integration/
│ │ │ │ ├── itemList.spec.js
│ │ │ │ ├── itemListUpdates.spec.js
│ │ │ │ └── itemSubmission.spec.js
│ │ │ ├── knexfile.js
│ │ │ ├── pageObjects/
│ │ │ │ └── inventoryManagement.js
│ │ │ ├── plugins/
│ │ │ │ ├── dbPlugin.js
│ │ │ │ └── index.js
│ │ │ └── support/
│ │ │ ├── commands.js
│ │ │ └── index.js
│ │ ├── cypress.json
│ │ └── package.json
│ ├── client/
│ │ ├── domController.js
│ │ ├── domController.test.js
│ │ ├── index.html
│ │ ├── inventoryController.js
│ │ ├── inventoryController.test.js
│ │ ├── jest.config.js
│ │ ├── main.js
│ │ ├── main.test.js
│ │ ├── package.json
│ │ ├── setupGlobalFetch.js
│ │ ├── setupJestDom.js
│ │ ├── socket.js
│ │ ├── socket.test.js
│ │ ├── testSocketServer.js
│ │ └── testUtils.js
│ └── server/
│ ├── README.md
│ ├── authenticationController.js
│ ├── authenticationController.test.js
│ ├── cartController.js
│ ├── cartController.test.js
│ ├── dbConnection.js
│ ├── disconnectFromDb.js
│ ├── inventoryController.js
│ ├── jest.config.js
│ ├── knexfile.js
│ ├── logger.js
│ ├── migrateDatabases.js
│ ├── migrations/
│ │ ├── 20200325082401_initial_schema.js
│ │ └── 20200331210311_updatedAt_field.js
│ ├── package.json
│ ├── seedUser.js
│ ├── seeds/
│ │ └── initial_inventory.js
│ ├── server.js
│ ├── server.test.js
│ ├── truncateTables.js
│ └── userTestUtils.js
├── chapter13/
│ └── 1_type_systems/
│ ├── 1_no_types/
│ │ ├── orderQueue.js
│ │ ├── orderQueue.spec.js
│ │ └── package.json
│ └── 2_with_types/
│ ├── orderQueue.js
│ ├── orderQueue.spec.js
│ ├── orderQueue.spec.ts
│ ├── orderQueue.ts
│ ├── package.json
│ └── tsconfig.json
├── chapter2/
│ ├── 2_unit_tests/
│ │ ├── 1_raw_tests/
│ │ │ ├── Cart.js
│ │ │ └── Cart.test.js
│ │ ├── 2_node_assert/
│ │ │ ├── Cart.js
│ │ │ └── Cart.test.js
│ │ ├── 3_jest_multiple_tests/
│ │ │ ├── Cart.js
│ │ │ ├── Cart.test.js
│ │ │ └── package.json
│ │ ├── 4_jest_assertions/
│ │ │ ├── Cart.js
│ │ │ ├── Cart.test.js
│ │ │ └── package.json
│ │ └── 5_npm_scripts/
│ │ ├── Cart.js
│ │ ├── Cart.test.js
│ │ └── package.json
│ ├── 3_integration_tests/
│ │ ├── 1_knex_tests_promise/
│ │ │ ├── cart.js
│ │ │ ├── cart.test.js
│ │ │ ├── dbConnection.js
│ │ │ ├── knexfile.js
│ │ │ ├── migrations/
│ │ │ │ └── 20191230210750_create_carts.js
│ │ │ └── package.json
│ │ ├── 2_knex_tests_done_cb/
│ │ │ ├── cart.js
│ │ │ ├── cart.test.js
│ │ │ ├── dbConnection.js
│ │ │ ├── knexfile.js
│ │ │ ├── migrations/
│ │ │ │ └── 20191230210750_create_carts.js
│ │ │ └── package.json
│ │ └── 3_knex_tests_hooks/
│ │ ├── cart.js
│ │ ├── cart.test.js
│ │ ├── dbConnection.js
│ │ ├── knexfile.js
│ │ ├── migrations/
│ │ │ └── 20191230210750_create_carts.js
│ │ └── package.json
│ ├── 4_end_to_end_tests/
│ │ ├── 1_http_api_tests/
│ │ │ ├── package.json
│ │ │ ├── server.js
│ │ │ └── server.test.js
│ │ └── 2_http_api_with_remove_item/
│ │ ├── package.json
│ │ ├── server.js
│ │ └── server.test.js
│ └── 5_tests_cost_and_revenue/
│ ├── 1_good_vs_bad/
│ │ ├── badly_written.test.js
│ │ ├── package.json
│ │ ├── server.js
│ │ └── well_written.test.js
│ └── 2_test_coupling/
│ ├── package.json
│ ├── pow.test.js
│ ├── pow_loop.js
│ └── pow_recursive.js
├── chapter3/
│ ├── 1_organising_test_suites/
│ │ ├── 1_breaking_down_tests_big_tests/
│ │ │ ├── package.json
│ │ │ ├── server.js
│ │ │ └── server.test.js
│ │ ├── 2_breaking_down_tests_small_tests/
│ │ │ ├── package.json
│ │ │ ├── server.js
│ │ │ └── server.test.js
│ │ └── 3_global_hooks/
│ │ ├── dummy.test.js
│ │ ├── globalSetup.js
│ │ ├── globalTeardown.js
│ │ ├── jest.config.js
│ │ └── package.json
│ ├── 2_writing_good_assertions/
│ │ ├── 1_assertion_checks/
│ │ │ ├── inventoryController.js
│ │ │ ├── inventoryController.test.js
│ │ │ └── package.json
│ │ ├── 2_assertion_checks_toThrow/
│ │ │ ├── inventoryController.js
│ │ │ ├── inventoryController.test.js
│ │ │ └── package.json
│ │ ├── 3_loose_assertions/
│ │ │ ├── inventoryController.js
│ │ │ ├── inventoryController.test.js
│ │ │ └── package.json
│ │ ├── 4_asymmetric_matchers/
│ │ │ ├── inventoryController.js
│ │ │ ├── inventoryController.test.js
│ │ │ └── package.json
│ │ ├── 5_manual_assertions/
│ │ │ ├── inventoryController.js
│ │ │ ├── inventoryController.test.js
│ │ │ └── package.json
│ │ ├── 6_custom_matchers/
│ │ │ ├── inventoryController.js
│ │ │ ├── inventoryController.test.js
│ │ │ ├── jest.config.js
│ │ │ └── package.json
│ │ └── 7_circular_assertions/
│ │ ├── inventoryController.js
│ │ ├── package.json
│ │ ├── server.js
│ │ └── server.test.js
│ ├── 3_mocks_stubs_and_spies/
│ │ ├── 1_mocking_objects/
│ │ │ ├── inventoryController.js
│ │ │ ├── inventoryController.test.js
│ │ │ ├── logger.js
│ │ │ └── package.json
│ │ ├── 2_mocking_imports/
│ │ │ ├── inventoryController.js
│ │ │ ├── inventoryController.test.js
│ │ │ ├── logger.js
│ │ │ └── package.json
│ │ └── 3_manual_mocks/
│ │ ├── __mocks__/
│ │ │ └── logger.js
│ │ ├── inventoryController.js
│ │ ├── inventoryController.test.js
│ │ ├── logger.js
│ │ └── package.json
│ └── 4_code_coverage/
│ ├── 1_measuring_code_coverage/
│ │ ├── __mocks__/
│ │ │ └── logger.js
│ │ ├── inventoryController.js
│ │ ├── inventoryController.test.js
│ │ ├── logger.js
│ │ └── package.json
│ └── 2_what_coverage_is_good_for/
│ ├── math.js
│ ├── math.test.js
│ └── package.json
├── chapter4/
│ ├── 1_setting_up_a_test_environment/
│ │ └── 1_exposing_modules/
│ │ ├── 1_end_to_end_tests/
│ │ │ ├── package.json
│ │ │ ├── server.js
│ │ │ └── server.test.js
│ │ ├── 2_integration_tests/
│ │ │ ├── cartController.js
│ │ │ ├── cartController.test.js
│ │ │ ├── inventoryController.js
│ │ │ ├── logger.js
│ │ │ ├── package.json
│ │ │ ├── server.js
│ │ │ └── server.test.js
│ │ └── 3_unit_tests/
│ │ ├── cartController.js
│ │ ├── cartController.test.js
│ │ ├── inventoryController.js
│ │ ├── logger.js
│ │ ├── package.json
│ │ ├── server.js
│ │ └── server.test.js
│ ├── 2_testing_http_endpoints/
│ │ ├── 1_using_supertest/
│ │ │ ├── cartController.js
│ │ │ ├── cartController.test.js
│ │ │ ├── inventoryController.js
│ │ │ ├── logger.js
│ │ │ ├── package.json
│ │ │ ├── server.js
│ │ │ └── server.test.js
│ │ └── 2_testing_middlewares/
│ │ ├── authenticationController.js
│ │ ├── authenticationController.test.js
│ │ ├── cartController.js
│ │ ├── cartController.test.js
│ │ ├── inventoryController.js
│ │ ├── logger.js
│ │ ├── package.json
│ │ ├── server.js
│ │ └── server.test.js
│ └── 3_dealing_with_external_dependencies/
│ ├── 1_database_integrations/
│ │ ├── authenticationController.js
│ │ ├── authenticationController.test.js
│ │ ├── cartController.js
│ │ ├── cartController.test.js
│ │ ├── dbConnection.js
│ │ ├── inventoryController.js
│ │ ├── knexfile.js
│ │ ├── logger.js
│ │ ├── migrations/
│ │ │ └── 20200325082401_initial_schema.js
│ │ ├── package.json
│ │ ├── server.js
│ │ └── server.test.js
│ ├── 2_separate_database_instances/
│ │ ├── authenticationController.js
│ │ ├── authenticationController.test.js
│ │ ├── cartController.js
│ │ ├── cartController.test.js
│ │ ├── dbConnection.js
│ │ ├── inventoryController.js
│ │ ├── knexfile.js
│ │ ├── logger.js
│ │ ├── migrations/
│ │ │ └── 20200325082401_initial_schema.js
│ │ ├── package.json
│ │ ├── server.js
│ │ └── server.test.js
│ ├── 3_maitaining_a_pristine_state/
│ │ ├── authenticationController.js
│ │ ├── authenticationController.test.js
│ │ ├── cartController.js
│ │ ├── cartController.test.js
│ │ ├── dbConnection.js
│ │ ├── disconnectFromDb.js
│ │ ├── inventoryController.js
│ │ ├── jest.config.js
│ │ ├── knexfile.js
│ │ ├── logger.js
│ │ ├── migrateDatabases.js
│ │ ├── migrations/
│ │ │ └── 20200325082401_initial_schema.js
│ │ ├── package.json
│ │ ├── seedUser.js
│ │ ├── server.js
│ │ ├── server.test.js
│ │ ├── truncateTables.js
│ │ └── userTestUtils.js
│ ├── 4_integrations_with_other_apis/
│ │ ├── authenticationController.js
│ │ ├── authenticationController.test.js
│ │ ├── cartController.js
│ │ ├── cartController.test.js
│ │ ├── dbConnection.js
│ │ ├── disconnectFromDb.js
│ │ ├── inventoryController.js
│ │ ├── jest.config.js
│ │ ├── knexfile.js
│ │ ├── logger.js
│ │ ├── migrateDatabases.js
│ │ ├── migrations/
│ │ │ └── 20200325082401_initial_schema.js
│ │ ├── package.json
│ │ ├── seedUser.js
│ │ ├── server.js
│ │ ├── server.test.js
│ │ ├── truncateTables.js
│ │ └── userTestUtils.js
│ ├── 5_using_mocks_to_avoid_requests/
│ │ ├── authenticationController.js
│ │ ├── authenticationController.test.js
│ │ ├── cartController.js
│ │ ├── cartController.test.js
│ │ ├── dbConnection.js
│ │ ├── disconnectFromDb.js
│ │ ├── inventoryController.js
│ │ ├── jest.config.js
│ │ ├── knexfile.js
│ │ ├── logger.js
│ │ ├── migrateDatabases.js
│ │ ├── migrations/
│ │ │ └── 20200325082401_initial_schema.js
│ │ ├── package.json
│ │ ├── seedUser.js
│ │ ├── server.js
│ │ ├── server.test.js
│ │ ├── truncateTables.js
│ │ └── userTestUtils.js
│ └── 6_using_nock_to_avoid_requests/
│ ├── authenticationController.js
│ ├── authenticationController.test.js
│ ├── cartController.js
│ ├── cartController.test.js
│ ├── dbConnection.js
│ ├── disconnectFromDb.js
│ ├── inventoryController.js
│ ├── jest.config.js
│ ├── knexfile.js
│ ├── logger.js
│ ├── migrateDatabases.js
│ ├── migrations/
│ │ └── 20200325082401_initial_schema.js
│ ├── package.json
│ ├── seedUser.js
│ ├── server.js
│ ├── server.test.js
│ ├── truncateTables.js
│ └── userTestUtils.js
├── chapter5/
│ └── 1_eliminating_non_determinism/
│ ├── 1_shared_resources/
│ │ ├── countModule.js
│ │ ├── decrement.test.js
│ │ ├── increment.test.js
│ │ └── package.json
│ ├── 2_resource_pools/
│ │ ├── countModule.js
│ │ ├── decrement.test.js
│ │ ├── increment.test.js
│ │ ├── instancePool.js
│ │ └── package.json
│ └── 3_dealing_with_time/
│ ├── authenticationController.js
│ ├── authenticationController.test.js
│ ├── cartController.js
│ ├── cartController.test.js
│ ├── dbConnection.js
│ ├── disconnectFromDb.js
│ ├── inventoryController.js
│ ├── jest.config.js
│ ├── knexfile.js
│ ├── logger.js
│ ├── migrateDatabases.js
│ ├── migrations/
│ │ ├── 20200325082401_initial_schema.js
│ │ └── 20200331210311_updatedAt_field.js
│ ├── package.json
│ ├── seedUser.js
│ ├── server.js
│ ├── server.test.js
│ ├── truncateTables.js
│ └── userTestUtils.js
├── chapter6/
│ ├── 1_introducing_jsdom/
│ │ ├── 1_pure_html/
│ │ │ ├── index.html
│ │ │ └── main.js
│ │ ├── 2_jsdom/
│ │ │ ├── example.js
│ │ │ ├── index.html
│ │ │ ├── main.js
│ │ │ ├── package.json
│ │ │ └── page.js
│ │ └── 3_jest_jsdom/
│ │ ├── index.html
│ │ ├── jest.config.js
│ │ ├── main.js
│ │ ├── main.test.js
│ │ └── package.json
│ ├── 2_asserting_on_the_dom/
│ │ ├── 1_finding_elements_by_dom_structure/
│ │ │ ├── domController.js
│ │ │ ├── domController.test.js
│ │ │ ├── index.html
│ │ │ ├── inventoryController.js
│ │ │ ├── inventoryController.test.js
│ │ │ ├── main.js
│ │ │ └── package.json
│ │ ├── 2_finding_elements_by_id/
│ │ │ ├── domController.js
│ │ │ ├── domController.test.js
│ │ │ ├── index.html
│ │ │ ├── inventoryController.js
│ │ │ ├── inventoryController.test.js
│ │ │ ├── main.js
│ │ │ └── package.json
│ │ ├── 3_robust_element_queries/
│ │ │ ├── domController.js
│ │ │ ├── domController.test.js
│ │ │ ├── index.html
│ │ │ ├── inventoryController.js
│ │ │ ├── inventoryController.test.js
│ │ │ ├── main.js
│ │ │ └── package.json
│ │ ├── 4_finding_with_dom_testing_library/
│ │ │ ├── domController.js
│ │ │ ├── domController.test.js
│ │ │ ├── index.html
│ │ │ ├── inventoryController.js
│ │ │ ├── inventoryController.test.js
│ │ │ ├── main.js
│ │ │ └── package.json
│ │ └── 5_writing_better_dom_assertions/
│ │ ├── domController.js
│ │ ├── domController.test.js
│ │ ├── index.html
│ │ ├── inventoryController.js
│ │ ├── inventoryController.test.js
│ │ ├── jest.config.js
│ │ ├── main.js
│ │ ├── package.json
│ │ └── setupJestDom.js
│ ├── 3_handling_events/
│ │ ├── 1_handling_raw_events/
│ │ │ ├── domController.js
│ │ │ ├── domController.test.js
│ │ │ ├── index.html
│ │ │ ├── inventoryController.js
│ │ │ ├── inventoryController.test.js
│ │ │ ├── jest.config.js
│ │ │ ├── main.js
│ │ │ ├── main.test.js
│ │ │ ├── package.json
│ │ │ └── setupJestDom.js
│ │ ├── 2_bubbling_up_events/
│ │ │ ├── domController.js
│ │ │ ├── domController.test.js
│ │ │ ├── index.html
│ │ │ ├── inventoryController.js
│ │ │ ├── inventoryController.test.js
│ │ │ ├── jest.config.js
│ │ │ ├── main.js
│ │ │ ├── main.test.js
│ │ │ ├── package.json
│ │ │ └── setupJestDom.js
│ │ └── 3_dom_testing_library_events/
│ │ ├── domController.js
│ │ ├── domController.test.js
│ │ ├── index.html
│ │ ├── inventoryController.js
│ │ ├── inventoryController.test.js
│ │ ├── jest.config.js
│ │ ├── main.js
│ │ ├── main.test.js
│ │ ├── package.json
│ │ └── setupJestDom.js
│ ├── 4_testing_and_browser_apis/
│ │ ├── 1_localstorage/
│ │ │ ├── domController.js
│ │ │ ├── domController.test.js
│ │ │ ├── index.html
│ │ │ ├── inventoryController.js
│ │ │ ├── inventoryController.test.js
│ │ │ ├── jest.config.js
│ │ │ ├── main.js
│ │ │ ├── main.test.js
│ │ │ ├── package.json
│ │ │ └── setupJestDom.js
│ │ ├── 2_history_api/
│ │ │ ├── domController.js
│ │ │ ├── domController.test.js
│ │ │ ├── index.html
│ │ │ ├── inventoryController.js
│ │ │ ├── inventoryController.test.js
│ │ │ ├── jest.config.js
│ │ │ ├── main.js
│ │ │ ├── main.test.js
│ │ │ ├── package.json
│ │ │ ├── setupJestDom.js
│ │ │ └── testUtils.js
│ │ └── server/
│ │ ├── README.md
│ │ ├── authenticationController.js
│ │ ├── authenticationController.test.js
│ │ ├── cartController.js
│ │ ├── cartController.test.js
│ │ ├── dbConnection.js
│ │ ├── disconnectFromDb.js
│ │ ├── inventoryController.js
│ │ ├── jest.config.js
│ │ ├── knexfile.js
│ │ ├── logger.js
│ │ ├── migrateDatabases.js
│ │ ├── migrations/
│ │ │ ├── 20200325082401_initial_schema.js
│ │ │ └── 20200331210311_updatedAt_field.js
│ │ ├── package.json
│ │ ├── seedUser.js
│ │ ├── seeds/
│ │ │ └── initial_inventory.js
│ │ ├── server.js
│ │ ├── server.test.js
│ │ ├── truncateTables.js
│ │ └── userTestUtils.js
│ └── 5_web_sockets_and_http_requests/
│ ├── 1_http_requests/
│ │ ├── domController.js
│ │ ├── domController.test.js
│ │ ├── index.html
│ │ ├── inventoryController.js
│ │ ├── inventoryController.test.js
│ │ ├── jest.config.js
│ │ ├── main.js
│ │ ├── main.test.js
│ │ ├── package.json
│ │ ├── setupGlobalFetch.js
│ │ ├── setupJestDom.js
│ │ └── testUtils.js
│ ├── 2_web_sockets/
│ │ ├── domController.js
│ │ ├── domController.test.js
│ │ ├── index.html
│ │ ├── inventoryController.js
│ │ ├── inventoryController.test.js
│ │ ├── jest.config.js
│ │ ├── main.js
│ │ ├── main.test.js
│ │ ├── package.json
│ │ ├── setupGlobalFetch.js
│ │ ├── setupJestDom.js
│ │ ├── socket.js
│ │ ├── socket.test.js
│ │ ├── testSocketServer.js
│ │ └── testUtils.js
│ └── server/
│ ├── README.md
│ ├── authenticationController.js
│ ├── authenticationController.test.js
│ ├── cartController.js
│ ├── cartController.test.js
│ ├── dbConnection.js
│ ├── disconnectFromDb.js
│ ├── inventoryController.js
│ ├── jest.config.js
│ ├── knexfile.js
│ ├── logger.js
│ ├── migrateDatabases.js
│ ├── migrations/
│ │ ├── 20200325082401_initial_schema.js
│ │ └── 20200331210311_updatedAt_field.js
│ ├── package.json
│ ├── seedUser.js
│ ├── seeds/
│ │ └── initial_inventory.js
│ ├── server.js
│ ├── server.test.js
│ ├── truncateTables.js
│ └── userTestUtils.js
├── chapter7/
│ ├── 1_setting_up_a_test_environment/
│ │ ├── 1_createElement_calls/
│ │ │ ├── index.html
│ │ │ ├── index.js
│ │ │ └── package.json
│ │ ├── 2_transforming_jsx/
│ │ │ ├── index.html
│ │ │ ├── index.jsx
│ │ │ └── package.json
│ │ └── 3_setting_up_jest/
│ │ ├── App.jsx
│ │ ├── app.test.js
│ │ ├── babel.config.js
│ │ ├── index.html
│ │ ├── index.jsx
│ │ ├── jest.config.js
│ │ └── package.json
│ ├── 2_an_overview_of_react_testing_libraries/
│ │ ├── 1_react_testing_utilities/
│ │ │ ├── App.jsx
│ │ │ ├── App.test.jsx
│ │ │ ├── babel.config.js
│ │ │ ├── index.html
│ │ │ ├── index.jsx
│ │ │ ├── jest.config.js
│ │ │ ├── package.json
│ │ │ └── setupJestDom.js
│ │ └── 2_react_testing_library/
│ │ ├── App.jsx
│ │ ├── App.test.jsx
│ │ ├── ItemForm.jsx
│ │ ├── ItemForm.test.jsx
│ │ ├── ItemList.jsx
│ │ ├── ItemList.test.jsx
│ │ ├── babel.config.js
│ │ ├── constants.js
│ │ ├── index.html
│ │ ├── index.jsx
│ │ ├── jest.config.js
│ │ ├── package.json
│ │ ├── setupGlobalFetch.js
│ │ └── setupJestDom.js
│ └── server/
│ ├── README.md
│ ├── authenticationController.js
│ ├── authenticationController.test.js
│ ├── cartController.js
│ ├── cartController.test.js
│ ├── dbConnection.js
│ ├── disconnectFromDb.js
│ ├── inventoryController.js
│ ├── jest.config.js
│ ├── knexfile.js
│ ├── logger.js
│ ├── migrateDatabases.js
│ ├── migrations/
│ │ ├── 20200325082401_initial_schema.js
│ │ └── 20200331210311_updatedAt_field.js
│ ├── package.json
│ ├── seedUser.js
│ ├── seeds/
│ │ └── initial_inventory.js
│ ├── server.js
│ ├── server.test.js
│ ├── truncateTables.js
│ └── userTestUtils.js
├── chapter8/
│ ├── 1_testing_component_interaction/
│ │ ├── 1_component_integration_tests/
│ │ │ ├── App.jsx
│ │ │ ├── App.test.jsx
│ │ │ ├── ItemForm.jsx
│ │ │ ├── ItemForm.test.jsx
│ │ │ ├── ItemList.jsx
│ │ │ ├── ItemList.test.jsx
│ │ │ ├── babel.config.js
│ │ │ ├── constants.js
│ │ │ ├── index.html
│ │ │ ├── index.jsx
│ │ │ ├── jest.config.js
│ │ │ ├── package.json
│ │ │ ├── setupGlobalFetch.js
│ │ │ └── setupJestDom.js
│ │ └── 2_stubbing_components/
│ │ ├── App.jsx
│ │ ├── App.test.jsx
│ │ ├── ItemForm.jsx
│ │ ├── ItemForm.test.jsx
│ │ ├── ItemList.jsx
│ │ ├── ItemList.test.jsx
│ │ ├── __mocks__/
│ │ │ └── react-spring/
│ │ │ └── renderprops.jsx
│ │ ├── babel.config.js
│ │ ├── constants.js
│ │ ├── index.html
│ │ ├── index.jsx
│ │ ├── jest.config.js
│ │ ├── package.json
│ │ ├── setupGlobalFetch.js
│ │ └── setupJestDom.js
│ ├── 2_snapshot_testing/
│ │ ├── 1_component_snapshots/
│ │ │ ├── ActionLog.jsx
│ │ │ ├── ActionLog.test.jsx
│ │ │ ├── App.jsx
│ │ │ ├── App.test.jsx
│ │ │ ├── ItemForm.jsx
│ │ │ ├── ItemForm.test.jsx
│ │ │ ├── ItemList.jsx
│ │ │ ├── ItemList.test.jsx
│ │ │ ├── __mocks__/
│ │ │ │ └── react-spring/
│ │ │ │ └── renderprops.jsx
│ │ │ ├── __snapshots__/
│ │ │ │ ├── ActionLog.test.jsx.snap
│ │ │ │ └── App.test.jsx.snap
│ │ │ ├── babel.config.js
│ │ │ ├── constants.js
│ │ │ ├── index.html
│ │ │ ├── index.jsx
│ │ │ ├── jest.config.js
│ │ │ ├── package.json
│ │ │ ├── setupGlobalFetch.js
│ │ │ └── setupJestDom.js
│ │ └── 2_snapshots_beyond_components/
│ │ ├── __snapshots__/
│ │ │ └── generate_report.test.js.snap
│ │ ├── generate_report.js
│ │ ├── generate_report.test.js
│ │ └── package.json
│ ├── 3_testing_styles/
│ │ ├── 1_css_classes/
│ │ │ ├── ActionLog.jsx
│ │ │ ├── ActionLog.test.jsx
│ │ │ ├── App.jsx
│ │ │ ├── App.test.jsx
│ │ │ ├── ItemForm.jsx
│ │ │ ├── ItemForm.test.jsx
│ │ │ ├── ItemList.jsx
│ │ │ ├── ItemList.test.jsx
│ │ │ ├── __mocks__/
│ │ │ │ └── react-spring/
│ │ │ │ └── renderprops.jsx
│ │ │ ├── __snapshots__/
│ │ │ │ ├── ActionLog.test.jsx.snap
│ │ │ │ └── App.test.jsx.snap
│ │ │ ├── babel.config.js
│ │ │ ├── constants.js
│ │ │ ├── index.html
│ │ │ ├── index.jsx
│ │ │ ├── jest.config.js
│ │ │ ├── package.json
│ │ │ ├── setupGlobalFetch.js
│ │ │ ├── setupJestDom.js
│ │ │ └── styles.css
│ │ ├── 2_style_props/
│ │ │ ├── ActionLog.jsx
│ │ │ ├── ActionLog.test.jsx
│ │ │ ├── App.jsx
│ │ │ ├── App.test.jsx
│ │ │ ├── ItemForm.jsx
│ │ │ ├── ItemForm.test.jsx
│ │ │ ├── ItemList.jsx
│ │ │ ├── ItemList.test.jsx
│ │ │ ├── __mocks__/
│ │ │ │ └── react-spring/
│ │ │ │ └── renderprops.jsx
│ │ │ ├── __snapshots__/
│ │ │ │ ├── ActionLog.test.jsx.snap
│ │ │ │ └── App.test.jsx.snap
│ │ │ ├── babel.config.js
│ │ │ ├── constants.js
│ │ │ ├── index.html
│ │ │ ├── index.jsx
│ │ │ ├── jest.config.js
│ │ │ ├── package.json
│ │ │ ├── setupGlobalFetch.js
│ │ │ ├── setupJestDom.js
│ │ │ └── styles.css
│ │ └── 3_css_in_js_snapshots/
│ │ ├── ActionLog.jsx
│ │ ├── ActionLog.test.jsx
│ │ ├── App.jsx
│ │ ├── App.test.jsx
│ │ ├── ItemForm.jsx
│ │ ├── ItemForm.test.jsx
│ │ ├── ItemList.jsx
│ │ ├── ItemList.test.jsx
│ │ ├── __mocks__/
│ │ │ └── react-spring/
│ │ │ └── renderprops.jsx
│ │ ├── __snapshots__/
│ │ │ ├── ActionLog.test.jsx.snap
│ │ │ ├── App.test.jsx.snap
│ │ │ └── ItemList.test.jsx.snap
│ │ ├── babel.config.js
│ │ ├── constants.js
│ │ ├── index.html
│ │ ├── index.jsx
│ │ ├── jest.config.js
│ │ ├── package.json
│ │ ├── setupGlobalFetch.js
│ │ ├── setupJestDom.js
│ │ ├── setupJestEmotion.js
│ │ └── styles.css
│ ├── 4_component_stories/
│ │ ├── 1_stories/
│ │ │ ├── .storybook/
│ │ │ │ └── main.js
│ │ │ ├── ActionLog.jsx
│ │ │ ├── ActionLog.stories.jsx
│ │ │ ├── ActionLog.test.jsx
│ │ │ ├── App.jsx
│ │ │ ├── App.test.jsx
│ │ │ ├── ItemForm.jsx
│ │ │ ├── ItemForm.stories.jsx
│ │ │ ├── ItemForm.test.jsx
│ │ │ ├── ItemList.jsx
│ │ │ ├── ItemList.stories.jsx
│ │ │ ├── ItemList.test.jsx
│ │ │ ├── __mocks__/
│ │ │ │ └── react-spring/
│ │ │ │ └── renderprops.jsx
│ │ │ ├── __snapshots__/
│ │ │ │ ├── ActionLog.test.jsx.snap
│ │ │ │ ├── App.test.jsx.snap
│ │ │ │ └── ItemList.test.jsx.snap
│ │ │ ├── babel.config.js
│ │ │ ├── constants.js
│ │ │ ├── index.html
│ │ │ ├── index.jsx
│ │ │ ├── jest.config.js
│ │ │ ├── package.json
│ │ │ ├── setupGlobalFetch.js
│ │ │ ├── setupJestDom.js
│ │ │ ├── setupJestEmotion.js
│ │ │ └── styles.css
│ │ └── 2_documentation/
│ │ ├── .storybook/
│ │ │ └── main.js
│ │ ├── ActionLog.jsx
│ │ ├── ActionLog.stories.jsx
│ │ ├── ActionLog.test.jsx
│ │ ├── App.jsx
│ │ ├── App.test.jsx
│ │ ├── ItemForm.jsx
│ │ ├── ItemForm.stories.jsx
│ │ ├── ItemForm.test.jsx
│ │ ├── ItemList.jsx
│ │ ├── ItemList.stories.jsx
│ │ ├── ItemList.stories.mdx
│ │ ├── ItemList.test.jsx
│ │ ├── __mocks__/
│ │ │ └── react-spring/
│ │ │ └── renderprops.jsx
│ │ ├── __snapshots__/
│ │ │ ├── ActionLog.test.jsx.snap
│ │ │ ├── App.test.jsx.snap
│ │ │ └── ItemList.test.jsx.snap
│ │ ├── babel.config.js
│ │ ├── constants.js
│ │ ├── index.html
│ │ ├── index.jsx
│ │ ├── jest.config.js
│ │ ├── package.json
│ │ ├── setupGlobalFetch.js
│ │ ├── setupJestDom.js
│ │ ├── setupJestEmotion.js
│ │ └── styles.css
│ └── server/
│ ├── README.md
│ ├── authenticationController.js
│ ├── authenticationController.test.js
│ ├── cartController.js
│ ├── cartController.test.js
│ ├── dbConnection.js
│ ├── disconnectFromDb.js
│ ├── inventoryController.js
│ ├── jest.config.js
│ ├── knexfile.js
│ ├── logger.js
│ ├── migrateDatabases.js
│ ├── migrations/
│ │ ├── 20200325082401_initial_schema.js
│ │ └── 20200331210311_updatedAt_field.js
│ ├── package.json
│ ├── seedUser.js
│ ├── seeds/
│ │ └── initial_inventory.js
│ ├── server.js
│ ├── server.test.js
│ ├── truncateTables.js
│ └── userTestUtils.js
├── chapter9/
│ ├── 1_the_philosophy_behind_tdd/
│ │ ├── 1_what_tdd_is/
│ │ │ ├── 1_small_test/
│ │ │ │ ├── calculateCartPrice.js
│ │ │ │ ├── calculateCartPrice.test.js
│ │ │ │ └── package.json
│ │ │ ├── 2_partial_test/
│ │ │ │ ├── calculateCartPrice.js
│ │ │ │ ├── calculateCartPrice.test.js
│ │ │ │ └── package.json
│ │ │ ├── 3_extra_test/
│ │ │ │ ├── calculateCartPrice.js
│ │ │ │ ├── calculateCartPrice.test.js
│ │ │ │ └── package.json
│ │ │ └── 4_handling_edge_cases/
│ │ │ ├── calculateCartPrice.js
│ │ │ ├── calculateCartPrice.test.js
│ │ │ └── package.json
│ │ └── 2_adjusting_iteration_size/
│ │ └── 1_bigger_steps/
│ │ ├── calculateCartPrice.js
│ │ ├── calculateCartPrice.test.js
│ │ ├── package.json
│ │ ├── pickMostExpensive.js
│ │ └── pickMostExpensive.test.js
│ └── 2_writing_a_js_module_using_tdd/
│ ├── 1_generating_item_rows/
│ │ ├── inventoryReport.js
│ │ ├── inventoryReport.test.js
│ │ └── package.json
│ ├── 2_generating_total_row/
│ │ ├── inventoryReport.js
│ │ ├── inventoryReport.test.js
│ │ └── package.json
│ └── 3_creating_report/
│ ├── inventoryReport.js
│ ├── inventoryReport.test.js
│ └── package.json
└── package.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# SQLite Databases
*.sqlite
# .DS_Store
.DS_Store
# Built JS bundles
bundle.js
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
element", () => {
// https://on.cypress.io/select
// at first, no option should be selected
cy.get(".action-select").should("have.value", "--Select a fruit--");
// Select option(s) with matching text content
cy.get(".action-select").select("apples");
// confirm the apples were selected
// note that each value starts with "fr-" in our HTML
cy.get(".action-select").should("have.value", "fr-apples");
cy.get(".action-select-multiple")
.select(["apples", "oranges", "bananas"])
// when getting multiple values, invoke "val" method first
.invoke("val")
.should("deep.equal", ["fr-apples", "fr-oranges", "fr-bananas"]);
// Select option(s) with matching value
cy.get(".action-select")
.select("fr-bananas")
// can attach an assertion right away to the element
.should("have.value", "fr-bananas");
cy.get(".action-select-multiple")
.select(["fr-apples", "fr-oranges", "fr-bananas"])
.invoke("val")
.should("deep.equal", ["fr-apples", "fr-oranges", "fr-bananas"]);
// assert the selected values include oranges
cy.get(".action-select-multiple")
.invoke("val")
.should("include", "fr-oranges");
});
it(".scrollIntoView() - scroll an element into view", () => {
// https://on.cypress.io/scrollintoview
// normally all of these buttons are hidden,
// because they're not within
// the viewable area of their parent
// (we need to scroll to see them)
cy.get("#scroll-horizontal button").should("not.be.visible");
// scroll the button into view, as if the user had scrolled
cy.get("#scroll-horizontal button")
.scrollIntoView()
.should("be.visible");
cy.get("#scroll-vertical button").should("not.be.visible");
// Cypress handles the scroll direction needed
cy.get("#scroll-vertical button")
.scrollIntoView()
.should("be.visible");
cy.get("#scroll-both button").should("not.be.visible");
// Cypress knows to scroll to the right and down
cy.get("#scroll-both button")
.scrollIntoView()
.should("be.visible");
});
it(".trigger() - trigger an event on a DOM element", () => {
// https://on.cypress.io/trigger
// To interact with a range input (slider)
// we need to set its value & trigger the
// event to signal it changed
// Here, we invoke jQuery's val() method to set
// the value and trigger the 'change' event
cy.get(".trigger-input-range")
.invoke("val", 25)
.trigger("change")
.get("input[type=range]")
.siblings("p")
.should("have.text", "25");
});
it("cy.scrollTo() - scroll the window or element to a position", () => {
// https://on.cypress.io/scrollTo
// You can scroll to 9 specific positions of an element:
// -----------------------------------
// | topLeft top topRight |
// | |
// | |
// | |
// | left center right |
// | |
// | |
// | |
// | bottomLeft bottom bottomRight |
// -----------------------------------
// if you chain .scrollTo() off of cy, we will
// scroll the entire window
cy.scrollTo("bottom");
cy.get("#scrollable-horizontal").scrollTo("right");
// or you can scroll to a specific coordinate:
// (x axis, y axis) in pixels
cy.get("#scrollable-vertical").scrollTo(250, 250);
// or you can scroll to a specific percentage
// of the (width, height) of the element
cy.get("#scrollable-both").scrollTo("75%", "25%");
// control the easing of the scroll (default is 'swing')
cy.get("#scrollable-vertical").scrollTo("center", { easing: "linear" });
// control the duration of the scroll (in ms)
cy.get("#scrollable-both").scrollTo("center", { duration: 2000 });
});
});
================================================
FILE: chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/aliasing.spec.js
================================================
///
context("Aliasing", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/commands/aliasing");
});
it(".as() - alias a DOM element for later use", () => {
// https://on.cypress.io/as
// Alias a DOM element for use later
// We don't have to traverse to the element
// later in our code, we reference it with @
cy.get(".as-table")
.find("tbody>tr")
.first()
.find("td")
.first()
.find("button")
.as("firstBtn");
// when we reference the alias, we place an
// @ in front of its name
cy.get("@firstBtn").click();
cy.get("@firstBtn")
.should("have.class", "btn-success")
.and("contain", "Changed");
});
it(".as() - alias a route for later use", () => {
// Alias the route to wait for its response
cy.server();
cy.route("GET", "comments/*").as("getComment");
// we have code that gets a comment when
// the button is clicked in scripts.js
cy.get(".network-btn").click();
// https://on.cypress.io/wait
cy.wait("@getComment")
.its("status")
.should("eq", 200);
});
});
================================================
FILE: chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/assertions.spec.js
================================================
///
context("Assertions", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/commands/assertions");
});
describe("Implicit Assertions", () => {
it(".should() - make an assertion about the current subject", () => {
// https://on.cypress.io/should
cy.get(".assertion-table")
.find("tbody tr:last")
.should("have.class", "success")
.find("td")
.first()
// checking the text of the element in various ways
.should("have.text", "Column content")
.should("contain", "Column content")
.should("have.html", "Column content")
// chai-jquery uses "is()" to check if element matches selector
.should("match", "td")
// to match text content against a regular expression
// first need to invoke jQuery method text()
// and then match using regular expression
.invoke("text")
.should("match", /column content/i);
// a better way to check element's text content against a regular expression
// is to use "cy.contains"
// https://on.cypress.io/contains
cy.get(".assertion-table")
.find("tbody tr:last")
// finds first element with text content matching regular expression
.contains("td", /column content/i)
.should("be.visible");
// for more information about asserting element's text
// see https://on.cypress.io/using-cypress-faq#How-do-I-get-an-element’s-text-contents
});
it(".and() - chain multiple assertions together", () => {
// https://on.cypress.io/and
cy.get(".assertions-link")
.should("have.class", "active")
.and("have.attr", "href")
.and("include", "cypress.io");
});
});
describe("Explicit Assertions", () => {
// https://on.cypress.io/assertions
it("expect - make an assertion about a specified subject", () => {
// We can use Chai's BDD style assertions
expect(true).to.be.true;
const o = { foo: "bar" };
expect(o).to.equal(o);
expect(o).to.deep.equal({ foo: "bar" });
// matching text using regular expression
expect("FooBar").to.match(/bar$/i);
});
it("pass your own callback function to should()", () => {
// Pass a function to should that can have any number
// of explicit assertions within it.
// The ".should(cb)" function will be retried
// automatically until it passes all your explicit assertions or times out.
cy.get(".assertions-p")
.find("p")
.should($p => {
// https://on.cypress.io/$
// return an array of texts from all of the p's
// @ts-ignore TS6133 unused variable
const texts = $p.map((i, el) => Cypress.$(el).text());
// jquery map returns jquery object
// and .get() convert this to simple array
const paragraphs = texts.get();
// array should have length of 3
expect(paragraphs, "has 3 paragraphs").to.have.length(3);
// use second argument to expect(...) to provide clear
// message with each assertion
expect(paragraphs, "has expected text in each paragraph").to.deep.eq([
"Some text from first p",
"More text from second p",
"And even more text from third p"
]);
});
});
it("finds element by class name regex", () => {
cy.get(".docs-header")
.find("div")
// .should(cb) callback function will be retried
.should($div => {
expect($div).to.have.length(1);
const className = $div[0].className;
expect(className).to.match(/heading-/);
})
// .then(cb) callback is not retried,
// it either passes or fails
.then($div => {
expect($div, "text content").to.have.text("Introduction");
});
});
it("can throw any error", () => {
cy.get(".docs-header")
.find("div")
.should($div => {
if ($div.length !== 1) {
// you can throw your own errors
throw new Error("Did not find 1 element");
}
const className = $div[0].className;
if (!className.match(/heading-/)) {
throw new Error(`Could not find class "heading-" in ${className}`);
}
});
});
it("matches unknown text between two elements", () => {
/**
* Text from the first element.
* @type {string}
*/
let text;
/**
* Normalizes passed text,
* useful before comparing text with spaces and different capitalization.
* @param {string} s Text to normalize
*/
const normalizeText = s => s.replace(/\s/g, "").toLowerCase();
cy.get(".two-elements")
.find(".first")
.then($first => {
// save text from the first element
text = normalizeText($first.text());
});
cy.get(".two-elements")
.find(".second")
.should($div => {
// we can massage text before comparing
const secondText = normalizeText($div.text());
expect(secondText, "second text").to.equal(text);
});
});
it("assert - assert shape of an object", () => {
const person = {
name: "Joe",
age: 20
};
assert.isObject(person, "value is object");
});
it("retries the should callback until assertions pass", () => {
cy.get("#random-number").should($div => {
const n = parseFloat($div.text());
expect(n)
.to.be.gte(1)
.and.be.lte(10);
});
});
});
});
================================================
FILE: chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/connectors.spec.js
================================================
///
context("Connectors", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/commands/connectors");
});
it(".each() - iterate over an array of elements", () => {
// https://on.cypress.io/each
cy.get(".connectors-each-ul>li").each(($el, index, $list) => {
console.log($el, index, $list);
});
});
it(".its() - get properties on the current subject", () => {
// https://on.cypress.io/its
cy.get(".connectors-its-ul>li")
// calls the 'length' property yielding that value
.its("length")
.should("be.gt", 2);
});
it(".invoke() - invoke a function on the current subject", () => {
// our div is hidden in our script.js
// $('.connectors-div').hide()
// https://on.cypress.io/invoke
cy.get(".connectors-div")
.should("be.hidden")
// call the jquery method 'show' on the 'div.container'
.invoke("show")
.should("be.visible");
});
it(".spread() - spread an array as individual args to callback function", () => {
// https://on.cypress.io/spread
const arr = ["foo", "bar", "baz"];
cy.wrap(arr).spread((foo, bar, baz) => {
expect(foo).to.eq("foo");
expect(bar).to.eq("bar");
expect(baz).to.eq("baz");
});
});
describe(".then()", () => {
it("invokes a callback function with the current subject", () => {
// https://on.cypress.io/then
cy.get(".connectors-list > li").then($lis => {
expect($lis, "3 items").to.have.length(3);
expect($lis.eq(0), "first item").to.contain("Walk the dog");
expect($lis.eq(1), "second item").to.contain("Feed the cat");
expect($lis.eq(2), "third item").to.contain("Write JavaScript");
});
});
it("yields the returned value to the next command", () => {
cy.wrap(1)
.then(num => {
expect(num).to.equal(1);
return 2;
})
.then(num => {
expect(num).to.equal(2);
});
});
it("yields the original subject without return", () => {
cy.wrap(1)
.then(num => {
expect(num).to.equal(1);
// note that nothing is returned from this callback
})
.then(num => {
// this callback receives the original unchanged value 1
expect(num).to.equal(1);
});
});
it("yields the value yielded by the last Cypress command inside", () => {
cy.wrap(1)
.then(num => {
expect(num).to.equal(1);
// note how we run a Cypress command
// the result yielded by this Cypress command
// will be passed to the second ".then"
cy.wrap(2);
})
.then(num => {
// this callback receives the value yielded by "cy.wrap(2)"
expect(num).to.equal(2);
});
});
});
});
================================================
FILE: chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/cookies.spec.js
================================================
///
context("Cookies", () => {
beforeEach(() => {
Cypress.Cookies.debug(true);
cy.visit("https://example.cypress.io/commands/cookies");
// clear cookies again after visiting to remove
// any 3rd party cookies picked up such as cloudflare
cy.clearCookies();
});
it("cy.getCookie() - get a browser cookie", () => {
// https://on.cypress.io/getcookie
cy.get("#getCookie .set-a-cookie").click();
// cy.getCookie() yields a cookie object
cy.getCookie("token").should("have.property", "value", "123ABC");
});
it("cy.getCookies() - get browser cookies", () => {
// https://on.cypress.io/getcookies
cy.getCookies().should("be.empty");
cy.get("#getCookies .set-a-cookie").click();
// cy.getCookies() yields an array of cookies
cy.getCookies()
.should("have.length", 1)
.should(cookies => {
// each cookie has these properties
expect(cookies[0]).to.have.property("name", "token");
expect(cookies[0]).to.have.property("value", "123ABC");
expect(cookies[0]).to.have.property("httpOnly", false);
expect(cookies[0]).to.have.property("secure", false);
expect(cookies[0]).to.have.property("domain");
expect(cookies[0]).to.have.property("path");
});
});
it("cy.setCookie() - set a browser cookie", () => {
// https://on.cypress.io/setcookie
cy.getCookies().should("be.empty");
cy.setCookie("foo", "bar");
// cy.getCookie() yields a cookie object
cy.getCookie("foo").should("have.property", "value", "bar");
});
it("cy.clearCookie() - clear a browser cookie", () => {
// https://on.cypress.io/clearcookie
cy.getCookie("token").should("be.null");
cy.get("#clearCookie .set-a-cookie").click();
cy.getCookie("token").should("have.property", "value", "123ABC");
// cy.clearCookies() yields null
cy.clearCookie("token").should("be.null");
cy.getCookie("token").should("be.null");
});
it("cy.clearCookies() - clear browser cookies", () => {
// https://on.cypress.io/clearcookies
cy.getCookies().should("be.empty");
cy.get("#clearCookies .set-a-cookie").click();
cy.getCookies().should("have.length", 1);
// cy.clearCookies() yields null
cy.clearCookies();
cy.getCookies().should("be.empty");
});
});
================================================
FILE: chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/cypress_api.spec.js
================================================
///
context("Cypress.Commands", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/cypress-api");
});
// https://on.cypress.io/custom-commands
it(".add() - create a custom command", () => {
Cypress.Commands.add(
"console",
{
prevSubject: true
},
(subject, method) => {
// the previous subject is automatically received
// and the commands arguments are shifted
// allow us to change the console method used
method = method || "log";
// log the subject to the console
// @ts-ignore TS7017
console[method]("The subject is", subject);
// whatever we return becomes the new subject
// we don't want to change the subject so
// we return whatever was passed in
return subject;
}
);
// @ts-ignore TS2339
cy.get("button")
.console("info")
.then($button => {
// subject is still $button
});
});
});
context("Cypress.Cookies", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/cypress-api");
});
// https://on.cypress.io/cookies
it(".debug() - enable or disable debugging", () => {
Cypress.Cookies.debug(true);
// Cypress will now log in the console when
// cookies are set or cleared
cy.setCookie("fakeCookie", "123ABC");
cy.clearCookie("fakeCookie");
cy.setCookie("fakeCookie", "123ABC");
cy.clearCookie("fakeCookie");
cy.setCookie("fakeCookie", "123ABC");
});
it(".preserveOnce() - preserve cookies by key", () => {
// normally cookies are reset after each test
cy.getCookie("fakeCookie").should("not.be.ok");
// preserving a cookie will not clear it when
// the next test starts
cy.setCookie("lastCookie", "789XYZ");
Cypress.Cookies.preserveOnce("lastCookie");
});
it(".defaults() - set defaults for all cookies", () => {
// now any cookie with the name 'session_id' will
// not be cleared before each new test runs
Cypress.Cookies.defaults({
whitelist: "session_id"
});
});
});
context("Cypress.Server", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/cypress-api");
});
// Permanently override server options for
// all instances of cy.server()
// https://on.cypress.io/cypress-server
it(".defaults() - change default config of server", () => {
Cypress.Server.defaults({
delay: 0,
force404: false
});
});
});
context("Cypress.arch", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/cypress-api");
});
it("Get CPU architecture name of underlying OS", () => {
// https://on.cypress.io/arch
expect(Cypress.arch).to.exist;
});
});
context("Cypress.config()", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/cypress-api");
});
it("Get and set configuration options", () => {
// https://on.cypress.io/config
let myConfig = Cypress.config();
expect(myConfig).to.have.property("animationDistanceThreshold", 5);
expect(myConfig).to.have.property("baseUrl", null);
expect(myConfig).to.have.property("defaultCommandTimeout", 4000);
expect(myConfig).to.have.property("requestTimeout", 5000);
expect(myConfig).to.have.property("responseTimeout", 30000);
expect(myConfig).to.have.property("viewportHeight", 660);
expect(myConfig).to.have.property("viewportWidth", 1000);
expect(myConfig).to.have.property("pageLoadTimeout", 60000);
expect(myConfig).to.have.property("waitForAnimations", true);
expect(Cypress.config("pageLoadTimeout")).to.eq(60000);
// this will change the config for the rest of your tests!
Cypress.config("pageLoadTimeout", 20000);
expect(Cypress.config("pageLoadTimeout")).to.eq(20000);
Cypress.config("pageLoadTimeout", 60000);
});
});
context("Cypress.dom", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/cypress-api");
});
// https://on.cypress.io/dom
it(".isHidden() - determine if a DOM element is hidden", () => {
let hiddenP = Cypress.$(".dom-p p.hidden").get(0);
let visibleP = Cypress.$(".dom-p p.visible").get(0);
// our first paragraph has css class 'hidden'
expect(Cypress.dom.isHidden(hiddenP)).to.be.true;
expect(Cypress.dom.isHidden(visibleP)).to.be.false;
});
});
context("Cypress.env()", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/cypress-api");
});
// We can set environment variables for highly dynamic values
// https://on.cypress.io/environment-variables
it("Get environment variables", () => {
// https://on.cypress.io/env
// set multiple environment variables
Cypress.env({
host: "veronica.dev.local",
api_server: "http://localhost:8888/v1/"
});
// get environment variable
expect(Cypress.env("host")).to.eq("veronica.dev.local");
// set environment variable
Cypress.env("api_server", "http://localhost:8888/v2/");
expect(Cypress.env("api_server")).to.eq("http://localhost:8888/v2/");
// get all environment variable
expect(Cypress.env()).to.have.property("host", "veronica.dev.local");
expect(Cypress.env()).to.have.property(
"api_server",
"http://localhost:8888/v2/"
);
});
});
context("Cypress.log", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/cypress-api");
});
it("Control what is printed to the Command Log", () => {
// https://on.cypress.io/cypress-log
});
});
context("Cypress.platform", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/cypress-api");
});
it("Get underlying OS name", () => {
// https://on.cypress.io/platform
expect(Cypress.platform).to.be.exist;
});
});
context("Cypress.version", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/cypress-api");
});
it("Get current version of Cypress being run", () => {
// https://on.cypress.io/version
expect(Cypress.version).to.be.exist;
});
});
context("Cypress.spec", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/cypress-api");
});
it("Get current spec information", () => {
// https://on.cypress.io/spec
// wrap the object so we can inspect it easily by clicking in the command log
cy.wrap(Cypress.spec).should("include.keys", [
"name",
"relative",
"absolute"
]);
});
});
================================================
FILE: chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/files.spec.js
================================================
///
/// JSON fixture file can be loaded directly using
// the built-in JavaScript bundler
// @ts-ignore
const requiredExample = require("../../fixtures/example");
context("Files", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/commands/files");
});
beforeEach(() => {
// load example.json fixture file and store
// in the test context object
cy.fixture("example.json").as("example");
});
it("cy.fixture() - load a fixture", () => {
// https://on.cypress.io/fixture
// Instead of writing a response inline you can
// use a fixture file's content.
cy.server();
cy.fixture("example.json").as("comment");
// when application makes an Ajax request matching "GET comments/*"
// Cypress will intercept it and reply with object
// from the "comment" alias
cy.route("GET", "comments/*", "@comment").as("getComment");
// we have code that gets a comment when
// the button is clicked in scripts.js
cy.get(".fixture-btn").click();
cy.wait("@getComment")
.its("responseBody")
.should("have.property", "name")
.and("include", "Using fixtures to represent data");
// you can also just write the fixture in the route
cy.route("GET", "comments/*", "fixture:example.json").as("getComment");
// we have code that gets a comment when
// the button is clicked in scripts.js
cy.get(".fixture-btn").click();
cy.wait("@getComment")
.its("responseBody")
.should("have.property", "name")
.and("include", "Using fixtures to represent data");
// or write fx to represent fixture
// by default it assumes it's .json
cy.route("GET", "comments/*", "fx:example").as("getComment");
// we have code that gets a comment when
// the button is clicked in scripts.js
cy.get(".fixture-btn").click();
cy.wait("@getComment")
.its("responseBody")
.should("have.property", "name")
.and("include", "Using fixtures to represent data");
});
it("cy.fixture() or require - load a fixture", function() {
// we are inside the "function () { ... }"
// callback and can use test context object "this"
// "this.example" was loaded in "beforeEach" function callback
expect(this.example, "fixture in the test context").to.deep.equal(
requiredExample
);
// or use "cy.wrap" and "should('deep.equal', ...)" assertion
// @ts-ignore
cy.wrap(this.example, "fixture vs require").should(
"deep.equal",
requiredExample
);
});
it("cy.readFile() - read file contents", () => {
// https://on.cypress.io/readfile
// You can read a file and yield its contents
// The filePath is relative to your project's root.
cy.readFile("cypress.json").then(json => {
expect(json).to.be.an("object");
});
});
it("cy.writeFile() - write to a file", () => {
// https://on.cypress.io/writefile
// You can write to a file
// Use a response from a request to automatically
// generate a fixture file for use later
cy.request("https://jsonplaceholder.cypress.io/users").then(response => {
cy.writeFile("cypress/fixtures/users.json", response.body);
});
cy.fixture("users").should(users => {
expect(users[0].name).to.exist;
});
// JavaScript arrays and objects are stringified
// and formatted into text.
cy.writeFile("cypress/fixtures/profile.json", {
id: 8739,
name: "Jane",
email: "jane@example.com"
});
cy.fixture("profile").should(profile => {
expect(profile.name).to.eq("Jane");
});
});
});
================================================
FILE: chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/local_storage.spec.js
================================================
///
context("Local Storage", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/commands/local-storage");
});
// Although local storage is automatically cleared
// in between tests to maintain a clean state
// sometimes we need to clear the local storage manually
it("cy.clearLocalStorage() - clear all data in local storage", () => {
// https://on.cypress.io/clearlocalstorage
cy.get(".ls-btn")
.click()
.should(() => {
expect(localStorage.getItem("prop1")).to.eq("red");
expect(localStorage.getItem("prop2")).to.eq("blue");
expect(localStorage.getItem("prop3")).to.eq("magenta");
});
// clearLocalStorage() yields the localStorage object
cy.clearLocalStorage().should(ls => {
expect(ls.getItem("prop1")).to.be.null;
expect(ls.getItem("prop2")).to.be.null;
expect(ls.getItem("prop3")).to.be.null;
});
// Clear key matching string in Local Storage
cy.get(".ls-btn")
.click()
.should(() => {
expect(localStorage.getItem("prop1")).to.eq("red");
expect(localStorage.getItem("prop2")).to.eq("blue");
expect(localStorage.getItem("prop3")).to.eq("magenta");
});
cy.clearLocalStorage("prop1").should(ls => {
expect(ls.getItem("prop1")).to.be.null;
expect(ls.getItem("prop2")).to.eq("blue");
expect(ls.getItem("prop3")).to.eq("magenta");
});
// Clear keys matching regex in Local Storage
cy.get(".ls-btn")
.click()
.should(() => {
expect(localStorage.getItem("prop1")).to.eq("red");
expect(localStorage.getItem("prop2")).to.eq("blue");
expect(localStorage.getItem("prop3")).to.eq("magenta");
});
cy.clearLocalStorage(/prop1|2/).should(ls => {
expect(ls.getItem("prop1")).to.be.null;
expect(ls.getItem("prop2")).to.be.null;
expect(ls.getItem("prop3")).to.eq("magenta");
});
});
});
================================================
FILE: chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/location.spec.js
================================================
///
context("Location", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/commands/location");
});
it("cy.hash() - get the current URL hash", () => {
// https://on.cypress.io/hash
cy.hash().should("be.empty");
});
it("cy.location() - get window.location", () => {
// https://on.cypress.io/location
cy.location().should(location => {
expect(location.hash).to.be.empty;
expect(location.href).to.eq(
"https://example.cypress.io/commands/location"
);
expect(location.host).to.eq("example.cypress.io");
expect(location.hostname).to.eq("example.cypress.io");
expect(location.origin).to.eq("https://example.cypress.io");
expect(location.pathname).to.eq("/commands/location");
expect(location.port).to.eq("");
expect(location.protocol).to.eq("https:");
expect(location.search).to.be.empty;
});
});
it("cy.url() - get the current URL", () => {
// https://on.cypress.io/url
cy.url().should("eq", "https://example.cypress.io/commands/location");
});
});
================================================
FILE: chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/misc.spec.js
================================================
///
context("Misc", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/commands/misc");
});
it(".end() - end the command chain", () => {
// https://on.cypress.io/end
// cy.end is useful when you want to end a chain of commands
// and force Cypress to re-query from the root element
cy.get(".misc-table").within(() => {
// ends the current chain and yields null
cy.contains("Cheryl")
.click()
.end();
// queries the entire table again
cy.contains("Charles").click();
});
});
it("cy.exec() - execute a system command", () => {
// execute a system command.
// so you can take actions necessary for
// your test outside the scope of Cypress.
// https://on.cypress.io/exec
// we can use Cypress.platform string to
// select appropriate command
// https://on.cypress/io/platform
cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`);
// on CircleCI Windows build machines we have a failure to run bash shell
// https://github.com/cypress-io/cypress/issues/5169
// so skip some of the tests by passing flag "--env circle=true"
const isCircleOnWindows =
Cypress.platform === "win32" && Cypress.env("circle");
if (isCircleOnWindows) {
cy.log("Skipping test on CircleCI");
return;
}
// cy.exec problem on Shippable CI
// https://github.com/cypress-io/cypress/issues/6718
const isShippable =
Cypress.platform === "linux" && Cypress.env("shippable");
if (isShippable) {
cy.log("Skipping test on ShippableCI");
return;
}
cy.exec("echo Jane Lane")
.its("stdout")
.should("contain", "Jane Lane");
if (Cypress.platform === "win32") {
cy.exec("print cypress.json")
.its("stderr")
.should("be.empty");
} else {
cy.exec("cat cypress.json")
.its("stderr")
.should("be.empty");
cy.exec("pwd")
.its("code")
.should("eq", 0);
}
});
it("cy.focused() - get the DOM element that has focus", () => {
// https://on.cypress.io/focused
cy.get(".misc-form")
.find("#name")
.click();
cy.focused().should("have.id", "name");
cy.get(".misc-form")
.find("#description")
.click();
cy.focused().should("have.id", "description");
});
context("Cypress.Screenshot", function() {
it("cy.screenshot() - take a screenshot", () => {
// https://on.cypress.io/screenshot
cy.screenshot("my-image");
});
it("Cypress.Screenshot.defaults() - change default config of screenshots", function() {
Cypress.Screenshot.defaults({
blackout: [".foo"],
capture: "viewport",
clip: { x: 0, y: 0, width: 200, height: 200 },
scale: false,
disableTimersAndAnimations: true,
screenshotOnRunFailure: true,
onBeforeScreenshot() {},
onAfterScreenshot() {}
});
});
});
it("cy.wrap() - wrap an object", () => {
// https://on.cypress.io/wrap
cy.wrap({ foo: "bar" })
.should("have.property", "foo")
.and("include", "bar");
});
});
================================================
FILE: chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/navigation.spec.js
================================================
///
context("Navigation", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io");
cy.get(".navbar-nav")
.contains("Commands")
.click();
cy.get(".dropdown-menu")
.contains("Navigation")
.click();
});
it("cy.go() - go back or forward in the browser's history", () => {
// https://on.cypress.io/go
cy.location("pathname").should("include", "navigation");
cy.go("back");
cy.location("pathname").should("not.include", "navigation");
cy.go("forward");
cy.location("pathname").should("include", "navigation");
// clicking back
cy.go(-1);
cy.location("pathname").should("not.include", "navigation");
// clicking forward
cy.go(1);
cy.location("pathname").should("include", "navigation");
});
it("cy.reload() - reload the page", () => {
// https://on.cypress.io/reload
cy.reload();
// reload the page without using the cache
cy.reload(true);
});
it("cy.visit() - visit a remote url", () => {
// https://on.cypress.io/visit
// Visit any sub-domain of your current domain
// Pass options to the visit
cy.visit("https://example.cypress.io/commands/navigation", {
timeout: 50000, // increase total time for the visit to resolve
onBeforeLoad(contentWindow) {
// contentWindow is the remote page's window object
expect(typeof contentWindow === "object").to.be.true;
},
onLoad(contentWindow) {
// contentWindow is the remote page's window object
expect(typeof contentWindow === "object").to.be.true;
}
});
});
});
================================================
FILE: chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/network_requests.spec.js
================================================
///
context("Network Requests", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/commands/network-requests");
});
// Manage AJAX / XHR requests in your app
it("cy.server() - control behavior of network requests and responses", () => {
// https://on.cypress.io/server
cy.server().should(server => {
// the default options on server
// you can override any of these options
expect(server.delay).to.eq(0);
expect(server.method).to.eq("GET");
expect(server.status).to.eq(200);
expect(server.headers).to.be.null;
expect(server.response).to.be.null;
expect(server.onRequest).to.be.undefined;
expect(server.onResponse).to.be.undefined;
expect(server.onAbort).to.be.undefined;
// These options control the server behavior
// affecting all requests
// pass false to disable existing route stubs
expect(server.enable).to.be.true;
// forces requests that don't match your routes to 404
expect(server.force404).to.be.false;
// whitelists requests from ever being logged or stubbed
expect(server.whitelist).to.be.a("function");
});
cy.server({
method: "POST",
delay: 1000,
status: 422,
response: {}
});
// any route commands will now inherit the above options
// from the server. anything we pass specifically
// to route will override the defaults though.
});
it("cy.request() - make an XHR request", () => {
// https://on.cypress.io/request
cy.request("https://jsonplaceholder.cypress.io/comments").should(
response => {
expect(response.status).to.eq(200);
// the server sometimes gets an extra comment posted from another machine
// which gets returned as 1 extra object
expect(response.body)
.to.have.property("length")
.and.be.oneOf([500, 501]);
expect(response).to.have.property("headers");
expect(response).to.have.property("duration");
}
);
});
it("cy.request() - verify response using BDD syntax", () => {
cy.request("https://jsonplaceholder.cypress.io/comments").then(response => {
// https://on.cypress.io/assertions
expect(response)
.property("status")
.to.equal(200);
expect(response)
.property("body")
.to.have.property("length")
.and.be.oneOf([500, 501]);
expect(response).to.include.keys("headers", "duration");
});
});
it("cy.request() with query parameters", () => {
// will execute request
// https://jsonplaceholder.cypress.io/comments?postId=1&id=3
cy.request({
url: "https://jsonplaceholder.cypress.io/comments",
qs: {
postId: 1,
id: 3
}
})
.its("body")
.should("be.an", "array")
.and("have.length", 1)
.its("0") // yields first element of the array
.should("contain", {
postId: 1,
id: 3
});
});
it("cy.request() - pass result to the second request", () => {
// first, let's find out the userId of the first user we have
cy.request("https://jsonplaceholder.cypress.io/users?_limit=1")
.its("body") // yields the response object
.its("0") // yields the first element of the returned list
// the above two commands its('body').its('0')
// can be written as its('body.0')
// if you do not care about TypeScript checks
.then(user => {
expect(user)
.property("id")
.to.be.a("number");
// make a new post on behalf of the user
cy.request("POST", "https://jsonplaceholder.cypress.io/posts", {
userId: user.id,
title: "Cypress Test Runner",
body:
"Fast, easy and reliable testing for anything that runs in a browser."
});
})
// note that the value here is the returned value of the 2nd request
// which is the new post object
.then(response => {
expect(response)
.property("status")
.to.equal(201); // new entity created
expect(response)
.property("body")
.to.contain({
title: "Cypress Test Runner"
});
// we don't know the exact post id - only that it will be > 100
// since JSONPlaceholder has built-in 100 posts
expect(response.body)
.property("id")
.to.be.a("number")
.and.to.be.gt(100);
// we don't know the user id here - since it was in above closure
// so in this test just confirm that the property is there
expect(response.body)
.property("userId")
.to.be.a("number");
});
});
it("cy.request() - save response in the shared test context", () => {
// https://on.cypress.io/variables-and-aliases
cy.request("https://jsonplaceholder.cypress.io/users?_limit=1")
.its("body")
.its("0") // yields the first element of the returned list
.as("user") // saves the object in the test context
.then(function() {
// NOTE 👀
// By the time this callback runs the "as('user')" command
// has saved the user object in the test context.
// To access the test context we need to use
// the "function () { ... }" callback form,
// otherwise "this" points at a wrong or undefined object!
cy.request("POST", "https://jsonplaceholder.cypress.io/posts", {
userId: this.user.id,
title: "Cypress Test Runner",
body:
"Fast, easy and reliable testing for anything that runs in a browser."
})
.its("body")
.as("post"); // save the new post from the response
})
.then(function() {
// When this callback runs, both "cy.request" API commands have finished
// and the test context has "user" and "post" objects set.
// Let's verify them.
expect(this.post, "post has the right user id")
.property("userId")
.to.equal(this.user.id);
});
});
it("cy.route() - route responses to matching requests", () => {
// https://on.cypress.io/route
let message = "whoa, this comment does not exist";
cy.server();
// Listen to GET to comments/1
cy.route("GET", "comments/*").as("getComment");
// we have code that gets a comment when
// the button is clicked in scripts.js
cy.get(".network-btn").click();
// https://on.cypress.io/wait
cy.wait("@getComment")
.its("status")
.should("eq", 200);
// Listen to POST to comments
cy.route("POST", "/comments").as("postComment");
// we have code that posts a comment when
// the button is clicked in scripts.js
cy.get(".network-post").click();
cy.wait("@postComment").should(xhr => {
expect(xhr.requestBody).to.include("email");
expect(xhr.requestHeaders).to.have.property("Content-Type");
expect(xhr.responseBody).to.have.property(
"name",
"Using POST in cy.route()"
);
});
// Stub a response to PUT comments/ ****
cy.route({
method: "PUT",
url: "comments/*",
status: 404,
response: { error: message },
delay: 500
}).as("putComment");
// we have code that puts a comment when
// the button is clicked in scripts.js
cy.get(".network-put").click();
cy.wait("@putComment");
// our 404 statusCode logic in scripts.js executed
cy.get(".network-put-comment").should("contain", message);
});
});
================================================
FILE: chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/querying.spec.js
================================================
///
context("Querying", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/commands/querying");
});
// The most commonly used query is 'cy.get()', you can
// think of this like the '$' in jQuery
it("cy.get() - query DOM elements", () => {
// https://on.cypress.io/get
cy.get("#query-btn").should("contain", "Button");
cy.get(".query-btn").should("contain", "Button");
cy.get("#querying .well>button:first").should("contain", "Button");
// ↲
// Use CSS selectors just like jQuery
cy.get('[data-test-id="test-example"]').should("have.class", "example");
// 'cy.get()' yields jQuery object, you can get its attribute
// by invoking `.attr()` method
cy.get('[data-test-id="test-example"]')
.invoke("attr", "data-test-id")
.should("equal", "test-example");
// or you can get element's CSS property
cy.get('[data-test-id="test-example"]')
.invoke("css", "position")
.should("equal", "static");
// or use assertions directly during 'cy.get()'
// https://on.cypress.io/assertions
cy.get('[data-test-id="test-example"]')
.should("have.attr", "data-test-id", "test-example")
.and("have.css", "position", "static");
});
it("cy.contains() - query DOM elements with matching content", () => {
// https://on.cypress.io/contains
cy.get(".query-list")
.contains("bananas")
.should("have.class", "third");
// we can pass a regexp to `.contains()`
cy.get(".query-list")
.contains(/^b\w+/)
.should("have.class", "third");
cy.get(".query-list")
.contains("apples")
.should("have.class", "first");
// passing a selector to contains will
// yield the selector containing the text
cy.get("#querying")
.contains("ul", "oranges")
.should("have.class", "query-list");
cy.get(".query-button")
.contains("Save Form")
.should("have.class", "btn");
});
it(".within() - query DOM elements within a specific element", () => {
// https://on.cypress.io/within
cy.get(".query-form").within(() => {
cy.get("input:first").should("have.attr", "placeholder", "Email");
cy.get("input:last").should("have.attr", "placeholder", "Password");
});
});
it("cy.root() - query the root DOM element", () => {
// https://on.cypress.io/root
// By default, root is the document
cy.root().should("match", "html");
cy.get(".query-ul").within(() => {
// In this within, the root is now the ul DOM element
cy.root().should("have.class", "query-ul");
});
});
it("best practices - selecting elements", () => {
// https://on.cypress.io/best-practices#Selecting-Elements
cy.get("[data-cy=best-practices-selecting-elements]").within(() => {
// Worst - too generic, no context
cy.get("button").click();
// Bad. Coupled to styling. Highly subject to change.
cy.get(".btn.btn-large").click();
// Average. Coupled to the `name` attribute which has HTML semantics.
cy.get("[name=submission]").click();
// Better. But still coupled to styling or JS event listeners.
cy.get("#main").click();
// Slightly better. Uses an ID but also ensures the element
// has an ARIA role attribute
cy.get("#main[role=button]").click();
// Much better. But still coupled to text content that may change.
cy.contains("Submit").click();
// Best. Insulated from all changes.
cy.get("[data-cy=submit]").click();
});
});
});
================================================
FILE: chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/spies_stubs_clocks.spec.js
================================================
///
// remove no check once Cypress.sinon is typed
// https://github.com/cypress-io/cypress/issues/6720
context("Spies, Stubs, and Clock", () => {
it("cy.spy() - wrap a method in a spy", () => {
// https://on.cypress.io/spy
cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
const obj = {
foo() {}
};
const spy = cy.spy(obj, "foo").as("anyArgs");
obj.foo();
expect(spy).to.be.called;
});
it("cy.spy() retries until assertions pass", () => {
cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
const obj = {
/**
* Prints the argument passed
* @param x {any}
*/
foo(x) {
console.log("obj.foo called with", x);
}
};
cy.spy(obj, "foo").as("foo");
setTimeout(() => {
obj.foo("first");
}, 500);
setTimeout(() => {
obj.foo("second");
}, 2500);
cy.get("@foo").should("have.been.calledTwice");
});
it("cy.stub() - create a stub and/or replace a function with stub", () => {
// https://on.cypress.io/stub
cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
const obj = {
/**
* prints both arguments to the console
* @param a {string}
* @param b {string}
*/
foo(a, b) {
console.log("a", a, "b", b);
}
};
const stub = cy.stub(obj, "foo").as("foo");
obj.foo("foo", "bar");
expect(stub).to.be.called;
});
it("cy.clock() - control time in the browser", () => {
// https://on.cypress.io/clock
// create the date in UTC so its always the same
// no matter what local timezone the browser is running in
const now = new Date(Date.UTC(2017, 2, 14)).getTime();
cy.clock(now);
cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
cy.get("#clock-div")
.click()
.should("have.text", "1489449600");
});
it("cy.tick() - move time in the browser", () => {
// https://on.cypress.io/tick
// create the date in UTC so its always the same
// no matter what local timezone the browser is running in
const now = new Date(Date.UTC(2017, 2, 14)).getTime();
cy.clock(now);
cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
cy.get("#tick-div")
.click()
.should("have.text", "1489449600");
cy.tick(10000); // 10 seconds passed
cy.get("#tick-div")
.click()
.should("have.text", "1489449610");
});
it("cy.stub() matches depending on arguments", () => {
// see all possible matchers at
// https://sinonjs.org/releases/latest/matchers/
const greeter = {
/**
* Greets a person
* @param {string} name
*/
greet(name) {
return `Hello, ${name}!`;
}
};
cy.stub(greeter, "greet")
.callThrough() // if you want non-matched calls to call the real method
.withArgs(Cypress.sinon.match.string)
.returns("Hi")
.withArgs(Cypress.sinon.match.number)
.throws(new Error("Invalid name"));
expect(greeter.greet("World")).to.equal("Hi");
// @ts-ignore
expect(() => greeter.greet(42)).to.throw("Invalid name");
expect(greeter.greet).to.have.been.calledTwice;
// non-matched calls goes the actual method
// @ts-ignore
expect(greeter.greet()).to.equal("Hello, undefined!");
});
it("matches call arguments using Sinon matchers", () => {
// see all possible matchers at
// https://sinonjs.org/releases/latest/matchers/
const calculator = {
/**
* returns the sum of two arguments
* @param a {number}
* @param b {number}
*/
add(a, b) {
return a + b;
}
};
const spy = cy.spy(calculator, "add").as("add");
expect(calculator.add(2, 3)).to.equal(5);
// if we want to assert the exact values used during the call
expect(spy).to.be.calledWith(2, 3);
// let's confirm "add" method was called with two numbers
expect(spy).to.be.calledWith(
Cypress.sinon.match.number,
Cypress.sinon.match.number
);
// alternatively, provide the value to match
expect(spy).to.be.calledWith(
Cypress.sinon.match(2),
Cypress.sinon.match(3)
);
// match any value
expect(spy).to.be.calledWith(Cypress.sinon.match.any, 3);
// match any value from a list
expect(spy).to.be.calledWith(Cypress.sinon.match.in([1, 2, 3]), 3);
/**
* Returns true if the given number is event
* @param {number} x
*/
const isEven = x => x % 2 === 0;
// expect the value to pass a custom predicate function
// the second argument to "sinon.match(predicate, message)" is
// shown if the predicate does not pass and assertion fails
expect(spy).to.be.calledWith(Cypress.sinon.match(isEven, "isEven"), 3);
/**
* Returns a function that checks if a given number is larger than the limit
* @param {number} limit
* @returns {(x: number) => boolean}
*/
const isGreaterThan = limit => x => x > limit;
/**
* Returns a function that checks if a given number is less than the limit
* @param {number} limit
* @returns {(x: number) => boolean}
*/
const isLessThan = limit => x => x < limit;
// you can combine several matchers using "and", "or"
expect(spy).to.be.calledWith(
Cypress.sinon.match.number,
Cypress.sinon
.match(isGreaterThan(2), "> 2")
.and(Cypress.sinon.match(isLessThan(4), "< 4"))
);
expect(spy).to.be.calledWith(
Cypress.sinon.match.number,
Cypress.sinon
.match(isGreaterThan(200), "> 200")
.or(Cypress.sinon.match(3))
);
// matchers can be used from BDD assertions
cy.get("@add").should(
"have.been.calledWith",
Cypress.sinon.match.number,
Cypress.sinon.match(3)
);
// you can alias matchers for shorter test code
const { match: M } = Cypress.sinon;
cy.get("@add").should("have.been.calledWith", M.number, M(3));
});
});
================================================
FILE: chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/traversal.spec.js
================================================
///
context("Traversal", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/commands/traversal");
});
it(".children() - get child DOM elements", () => {
// https://on.cypress.io/children
cy.get(".traversal-breadcrumb")
.children(".active")
.should("contain", "Data");
});
it(".closest() - get closest ancestor DOM element", () => {
// https://on.cypress.io/closest
cy.get(".traversal-badge")
.closest("ul")
.should("have.class", "list-group");
});
it(".eq() - get a DOM element at a specific index", () => {
// https://on.cypress.io/eq
cy.get(".traversal-list>li")
.eq(1)
.should("contain", "siamese");
});
it(".filter() - get DOM elements that match the selector", () => {
// https://on.cypress.io/filter
cy.get(".traversal-nav>li")
.filter(".active")
.should("contain", "About");
});
it(".find() - get descendant DOM elements of the selector", () => {
// https://on.cypress.io/find
cy.get(".traversal-pagination")
.find("li")
.find("a")
.should("have.length", 7);
});
it(".first() - get first DOM element", () => {
// https://on.cypress.io/first
cy.get(".traversal-table td")
.first()
.should("contain", "1");
});
it(".last() - get last DOM element", () => {
// https://on.cypress.io/last
cy.get(".traversal-buttons .btn")
.last()
.should("contain", "Submit");
});
it(".next() - get next sibling DOM element", () => {
// https://on.cypress.io/next
cy.get(".traversal-ul")
.contains("apples")
.next()
.should("contain", "oranges");
});
it(".nextAll() - get all next sibling DOM elements", () => {
// https://on.cypress.io/nextall
cy.get(".traversal-next-all")
.contains("oranges")
.nextAll()
.should("have.length", 3);
});
it(".nextUntil() - get next sibling DOM elements until next el", () => {
// https://on.cypress.io/nextuntil
cy.get("#veggies")
.nextUntil("#nuts")
.should("have.length", 3);
});
it(".not() - remove DOM elements from set of DOM elements", () => {
// https://on.cypress.io/not
cy.get(".traversal-disabled .btn")
.not("[disabled]")
.should("not.contain", "Disabled");
});
it(".parent() - get parent DOM element from DOM elements", () => {
// https://on.cypress.io/parent
cy.get(".traversal-mark")
.parent()
.should("contain", "Morbi leo risus");
});
it(".parents() - get parent DOM elements from DOM elements", () => {
// https://on.cypress.io/parents
cy.get(".traversal-cite")
.parents()
.should("match", "blockquote");
});
it(".parentsUntil() - get parent DOM elements from DOM elements until el", () => {
// https://on.cypress.io/parentsuntil
cy.get(".clothes-nav")
.find(".active")
.parentsUntil(".clothes-nav")
.should("have.length", 2);
});
it(".prev() - get previous sibling DOM element", () => {
// https://on.cypress.io/prev
cy.get(".birds")
.find(".active")
.prev()
.should("contain", "Lorikeets");
});
it(".prevAll() - get all previous sibling DOM elements", () => {
// https://on.cypress.io/prevAll
cy.get(".fruits-list")
.find(".third")
.prevAll()
.should("have.length", 2);
});
it(".prevUntil() - get all previous sibling DOM elements until el", () => {
// https://on.cypress.io/prevUntil
cy.get(".foods-list")
.find("#nuts")
.prevUntil("#veggies")
.should("have.length", 3);
});
it(".siblings() - get all sibling DOM elements", () => {
// https://on.cypress.io/siblings
cy.get(".traversal-pills .active")
.siblings()
.should("have.length", 2);
});
});
================================================
FILE: chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/utilities.spec.js
================================================
///
context("Utilities", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/utilities");
});
it("Cypress._ - call a lodash method", () => {
// https://on.cypress.io/_
cy.request("https://jsonplaceholder.cypress.io/users").then(response => {
let ids = Cypress._.chain(response.body)
.map("id")
.take(3)
.value();
expect(ids).to.deep.eq([1, 2, 3]);
});
});
it("Cypress.$ - call a jQuery method", () => {
// https://on.cypress.io/$
let $li = Cypress.$(".utility-jquery li:first");
cy.wrap($li)
.should("not.have.class", "active")
.click()
.should("have.class", "active");
});
it("Cypress.Blob - blob utilities and base64 string conversion", () => {
// https://on.cypress.io/blob
cy.get(".utility-blob").then($div => {
// https://github.com/nolanlawson/blob-util#imgSrcToDataURL
// get the dataUrl string for the javascript-logo
return Cypress.Blob.imgSrcToDataURL(
"https://example.cypress.io/assets/img/javascript-logo.png",
undefined,
"anonymous"
).then(dataUrl => {
// create an element and set its src to the dataUrl
let img = Cypress.$(" ", { src: dataUrl });
// need to explicitly return cy here since we are initially returning
// the Cypress.Blob.imgSrcToDataURL promise to our test
// append the image
$div.append(img);
cy.get(".utility-blob img")
.click()
.should("have.attr", "src", dataUrl);
});
});
});
it("Cypress.minimatch - test out glob patterns against strings", () => {
// https://on.cypress.io/minimatch
let matching = Cypress.minimatch("/users/1/comments", "/users/*/comments", {
matchBase: true
});
expect(matching, "matching wildcard").to.be.true;
matching = Cypress.minimatch("/users/1/comments/2", "/users/*/comments", {
matchBase: true
});
expect(matching, "comments").to.be.false;
// ** matches against all downstream path segments
matching = Cypress.minimatch("/foo/bar/baz/123/quux?a=b&c=2", "/foo/**", {
matchBase: true
});
expect(matching, "comments").to.be.true;
// whereas * matches only the next path segment
matching = Cypress.minimatch("/foo/bar/baz/123/quux?a=b&c=2", "/foo/*", {
matchBase: false
});
expect(matching, "comments").to.be.false;
});
it("Cypress.moment() - format or parse dates using a moment method", () => {
// https://on.cypress.io/moment
const time = Cypress.moment("2014-04-25T19:38:53.196Z")
.utc()
.format("h:mm A");
expect(time).to.be.a("string");
cy.get(".utility-moment")
.contains("3:38 PM")
.should("have.class", "badge");
// the time in the element should be between 3pm and 5pm
const start = Cypress.moment("3:00 PM", "LT");
const end = Cypress.moment("5:00 PM", "LT");
cy.get(".utility-moment .badge").should($el => {
// parse American time like "3:38 PM"
const m = Cypress.moment($el.text().trim(), "LT");
// display hours + minutes + AM|PM
const f = "h:mm A";
expect(
m.isBetween(start, end),
`${m.format(f)} should be between ${start.format(f)} and ${end.format(
f
)}`
).to.be.true;
});
});
it("Cypress.Promise - instantiate a bluebird promise", () => {
// https://on.cypress.io/promise
let waited = false;
/**
* @return Bluebird
*/
function waitOneSecond() {
// return a promise that resolves after 1 second
// @ts-ignore TS2351 (new Cypress.Promise)
return new Cypress.Promise((resolve, reject) => {
setTimeout(() => {
// set waited to true
waited = true;
// resolve with 'foo' string
resolve("foo");
}, 1000);
});
}
cy.then(() => {
// return a promise to cy.then() that
// is awaited until it resolves
// @ts-ignore TS7006
return waitOneSecond().then(str => {
expect(str).to.eq("foo");
expect(waited).to.be.true;
});
});
});
});
================================================
FILE: chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/viewport.spec.js
================================================
///
context("Viewport", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/commands/viewport");
});
it("cy.viewport() - set the viewport size and dimension", () => {
// https://on.cypress.io/viewport
cy.get("#navbar").should("be.visible");
cy.viewport(320, 480);
// the navbar should have collapse since our screen is smaller
cy.get("#navbar").should("not.be.visible");
cy.get(".navbar-toggle")
.should("be.visible")
.click();
cy.get(".nav")
.find("a")
.should("be.visible");
// lets see what our app looks like on a super large screen
cy.viewport(2999, 2999);
// cy.viewport() accepts a set of preset sizes
// to easily set the screen to a device's width and height
// We added a cy.wait() between each viewport change so you can see
// the change otherwise it is a little too fast to see :)
cy.viewport("macbook-15");
cy.wait(200);
cy.viewport("macbook-13");
cy.wait(200);
cy.viewport("macbook-11");
cy.wait(200);
cy.viewport("ipad-2");
cy.wait(200);
cy.viewport("ipad-mini");
cy.wait(200);
cy.viewport("iphone-6+");
cy.wait(200);
cy.viewport("iphone-6");
cy.wait(200);
cy.viewport("iphone-5");
cy.wait(200);
cy.viewport("iphone-4");
cy.wait(200);
cy.viewport("iphone-3");
cy.wait(200);
// cy.viewport() accepts an orientation for all presets
// the default orientation is 'portrait'
cy.viewport("ipad-2", "portrait");
cy.wait(200);
cy.viewport("iphone-4", "landscape");
cy.wait(200);
// The viewport will be reset back to the default dimensions
// in between tests (the default can be set in cypress.json)
});
});
================================================
FILE: chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/waiting.spec.js
================================================
///
context("Waiting", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/commands/waiting");
});
// BE CAREFUL of adding unnecessary wait times.
// https://on.cypress.io/best-practices#Unnecessary-Waiting
// https://on.cypress.io/wait
it("cy.wait() - wait for a specific amount of time", () => {
cy.get(".wait-input1").type("Wait 1000ms after typing");
cy.wait(1000);
cy.get(".wait-input2").type("Wait 1000ms after typing");
cy.wait(1000);
cy.get(".wait-input3").type("Wait 1000ms after typing");
cy.wait(1000);
});
it("cy.wait() - wait for a specific route", () => {
cy.server();
// Listen to GET to comments/1
cy.route("GET", "comments/*").as("getComment");
// we have code that gets a comment when
// the button is clicked in scripts.js
cy.get(".network-btn").click();
// wait for GET comments/1
cy.wait("@getComment")
.its("status")
.should("eq", 200);
});
});
================================================
FILE: chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/window.spec.js
================================================
///
context("Window", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/commands/window");
});
it("cy.window() - get the global window object", () => {
// https://on.cypress.io/window
cy.window().should("have.property", "top");
});
it("cy.document() - get the document object", () => {
// https://on.cypress.io/document
cy.document()
.should("have.property", "charset")
.and("eq", "UTF-8");
});
it("cy.title() - get the title", () => {
// https://on.cypress.io/title
cy.title().should("include", "Kitchen Sink");
});
});
================================================
FILE: chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/plugins/index.js
================================================
///
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
/**
* @type {Cypress.PluginConfig}
*/
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
};
================================================
FILE: chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/support/commands.js
================================================
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
================================================
FILE: chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/support/index.js
================================================
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import "./commands";
// Alternatively you can use CommonJS syntax:
// require('./commands')
================================================
FILE: chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress.json
================================================
{}
================================================
FILE: chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/package.json
================================================
{
"name": "1_setting_up_cypress",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"cypress:open": "cypress open"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"cypress": "^4.12.1"
}
}
================================================
FILE: chapter11/1_writing_end_to_end_tests/2_writing_your_first_tests/cypress/dbConnection.js
================================================
const environmentName = process.env.NODE_ENV;
const db = require("knex")(require("./knexfile")[environmentName]);
const closeConnection = () => db.destroy();
module.exports = {
db,
closeConnection
};
================================================
FILE: chapter11/1_writing_end_to_end_tests/2_writing_your_first_tests/cypress/fixtures/example.json
================================================
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}
================================================
FILE: chapter11/1_writing_end_to_end_tests/2_writing_your_first_tests/cypress/integration/itemSubmission.spec.js
================================================
describe("item submission", () => {
beforeEach(() => cy.task("emptyInventory"));
it("can add items through the form", () => {
cy.visit("http://localhost:8080");
cy.get('input[placeholder="Item name"]').type("cheesecake");
cy.get('input[placeholder="Quantity"]').type("10");
cy.get('button[type="submit"]')
.contains("Add to inventory")
.click();
cy.get("li").contains("cheesecake - Quantity: 10");
});
it("can update an item's quantity", () => {
cy.task("seedItem", { itemName: "cheesecake", quantity: 5 });
cy.visit("http://localhost:8080");
cy.get('input[placeholder="Item name"]').type("cheesecake");
cy.get('input[placeholder="Quantity"]').type("10");
cy.get('button[type="submit"]')
.contains("Add to inventory")
.click();
cy.get("li").contains("cheesecake - Quantity: 15");
});
it("can undo submitted items", () => {
cy.visit("http://localhost:8080");
cy.get('input[placeholder="Item name"]').type("cheesecake");
cy.get('input[placeholder="Quantity"]').type("10");
cy.get('button[type="submit"]')
.contains("Add to inventory")
.click();
cy.get('input[placeholder="Quantity"]')
.clear()
.type("5");
cy.get('button[type="submit"]')
.contains("Add to inventory")
.click();
cy.get("button")
.contains("Undo")
.click();
cy.get("p")
.then(p => {
return Array.from(p).filter(p => {
return p.innerText.includes(
'The inventory has been updated - {"cheesecake":10}'
);
});
})
.should("have.length", 2);
});
it("saves each submission to the action log", () => {
cy.visit("http://localhost:8080");
cy.get('input[placeholder="Item name"]').type("cheesecake");
cy.get('input[placeholder="Quantity"]').type("10");
cy.get('button[type="submit"]')
.contains("Add to inventory")
.click();
cy.get('input[placeholder="Quantity"]')
.clear()
.type("5");
cy.get('button[type="submit"]')
.contains("Add to inventory")
.click();
cy.get("button")
.contains("Undo")
.click();
cy.get("p").contains("The inventory has been updated - {}");
cy.get("p")
.then(p => {
return Array.from(p).filter(p => {
return p.innerText.includes(
'The inventory has been updated - {"cheesecake":10}'
);
});
})
.should("have.length", 2);
cy.get("p").contains('The inventory has been updated - {"cheesecake":15}');
});
describe("given a user enters an invalid item name", () => {
it("disables the form's submission button", () => {
cy.visit("http://localhost:8080");
cy.get('input[placeholder="Item name"]').type("boat");
cy.get('input[placeholder="Quantity"]').type("10");
cy.get('button[type="submit"]')
.contains("Add to inventory")
.should("be.disabled");
});
});
});
================================================
FILE: chapter11/1_writing_end_to_end_tests/2_writing_your_first_tests/cypress/knexfile.js
================================================
module.exports = {
development: {
client: "sqlite3",
connection: { filename: "../../server/dev.sqlite" },
useNullAsDefault: true
}
};
================================================
FILE: chapter11/1_writing_end_to_end_tests/2_writing_your_first_tests/cypress/plugins/dbPlugin.js
================================================
const { db } = require("../dbConnection");
const dbPlugin = (on, config) => {
on(
"task",
{
emptyInventory: () => db("inventory").truncate(),
seedItem: itemRow => db("inventory").insert(itemRow)
},
config
);
return config;
};
module.exports = dbPlugin;
================================================
FILE: chapter11/1_writing_end_to_end_tests/2_writing_your_first_tests/cypress/plugins/index.js
================================================
///
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
const dbPlugin = require("./dbPlugin");
module.exports = (on, config) => {
dbPlugin(on, config);
};
================================================
FILE: chapter11/1_writing_end_to_end_tests/2_writing_your_first_tests/cypress/support/commands.js
================================================
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
================================================
FILE: chapter11/1_writing_end_to_end_tests/2_writing_your_first_tests/cypress/support/index.js
================================================
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import "./commands";
// Alternatively you can use CommonJS syntax:
// require('./commands')
================================================
FILE: chapter11/1_writing_end_to_end_tests/2_writing_your_first_tests/cypress.json
================================================
{
"nodeVersion": "system"
}
================================================
FILE: chapter11/1_writing_end_to_end_tests/2_writing_your_first_tests/package.json
================================================
{
"name": "2_writing_your_first_tests",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"cypress:open": "NODE_ENV=development cypress open",
"cypress:run": "NODE_ENV=development cypress run"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"cypress": "^4.12.1",
"knex": "^0.20.13",
"sqlite3": "4.1.1"
}
}
================================================
FILE: chapter11/1_writing_end_to_end_tests/3_sending_http_requests/cypress/dbConnection.js
================================================
const environmentName = process.env.NODE_ENV;
const db = require("knex")(require("./knexfile")[environmentName]);
const closeConnection = () => db.destroy();
module.exports = {
db,
closeConnection
};
================================================
FILE: chapter11/1_writing_end_to_end_tests/3_sending_http_requests/cypress/fixtures/example.json
================================================
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}
================================================
FILE: chapter11/1_writing_end_to_end_tests/3_sending_http_requests/cypress/integration/itemListUpdates.spec.js
================================================
describe("item list updates", () => {
beforeEach(() => cy.task("emptyInventory"));
describe("when the application loads for the first time", () => {
it("loads the initial list of items", () => {
cy.addItem("cheesecake", 2);
cy.addItem("apple pie", 5);
cy.addItem("carrot cake", 96);
cy.visit("http://localhost:8080");
cy.get("li").contains("cheesecake - Quantity: 2");
cy.get("li").contains("apple pie - Quantity: 5");
cy.get("li").contains("carrot cake - Quantity: 96");
});
});
describe("as other users add items", () => {
it("updates the item list", () => {
cy.visit("http://localhost:8080");
cy.wait(2000);
cy.addItem("cheesecake", 22);
cy.get("li").contains("cheesecake - Quantity: 22");
});
});
});
================================================
FILE: chapter11/1_writing_end_to_end_tests/3_sending_http_requests/cypress/integration/itemSubmission.spec.js
================================================
describe("item submission", () => {
beforeEach(() => cy.task("emptyInventory"));
it("can add items through the form", () => {
cy.visit("http://localhost:8080");
cy.get('input[placeholder="Item name"]').type("cheesecake");
cy.get('input[placeholder="Quantity"]').type("10");
cy.get('button[type="submit"]')
.contains("Add to inventory")
.click();
cy.get("li").contains("cheesecake - Quantity: 10");
});
it("can update an item's quantity", () => {
cy.task("seedItem", { itemName: "cheesecake", quantity: 5 });
cy.visit("http://localhost:8080");
cy.get('input[placeholder="Item name"]').type("cheesecake");
cy.get('input[placeholder="Quantity"]').type("10");
cy.get('button[type="submit"]')
.contains("Add to inventory")
.click();
cy.get("li").contains("cheesecake - Quantity: 15");
});
it("can undo submitted items", () => {
cy.visit("http://localhost:8080");
cy.get('input[placeholder="Item name"]').type("cheesecake");
cy.get('input[placeholder="Quantity"]').type("10");
cy.get('button[type="submit"]')
.contains("Add to inventory")
.click();
cy.get('input[placeholder="Quantity"]')
.clear()
.type("5");
cy.get('button[type="submit"]')
.contains("Add to inventory")
.click();
cy.get("button")
.contains("Undo")
.click();
cy.get("p")
.then(p => {
return Array.from(p).filter(p => {
return p.innerText.includes(
'The inventory has been updated - {"cheesecake":10}'
);
});
})
.should("have.length", 2);
});
it("saves each submission to the action log", () => {
cy.visit("http://localhost:8080");
cy.get('input[placeholder="Item name"]').type("cheesecake");
cy.get('input[placeholder="Quantity"]').type("10");
cy.get('button[type="submit"]')
.contains("Add to inventory")
.click();
cy.get('input[placeholder="Quantity"]')
.clear()
.type("5");
cy.get('button[type="submit"]')
.contains("Add to inventory")
.click();
cy.get("button")
.contains("Undo")
.click();
cy.get("p").contains("The inventory has been updated - {}");
cy.get("p").contains('The inventory has been updated - {"cheesecake":15}');
cy.get("p")
.then(p => {
return Array.from(p).filter(p => {
return p.innerText.includes(
'The inventory has been updated - {"cheesecake":10}'
);
});
})
.should("have.length", 2);
});
describe("given a user enters an invalid item name", () => {
it("disables the form's submission button", () => {
cy.visit("http://localhost:8080");
cy.get('input[placeholder="Item name"]').type("boat");
cy.get('input[placeholder="Quantity"]').type("10");
cy.get('button[type="submit"]')
.contains("Add to inventory")
.should("be.disabled");
});
});
});
================================================
FILE: chapter11/1_writing_end_to_end_tests/3_sending_http_requests/cypress/knexfile.js
================================================
module.exports = {
development: {
client: "sqlite3",
connection: { filename: "../../server/dev.sqlite" },
useNullAsDefault: true
}
};
================================================
FILE: chapter11/1_writing_end_to_end_tests/3_sending_http_requests/cypress/plugins/dbPlugin.js
================================================
const { db } = require("../dbConnection");
const dbPlugin = (on, config) => {
on(
"task",
{
emptyInventory: () => db("inventory").truncate(),
seedItem: itemRow => db("inventory").insert(itemRow)
},
config
);
return config;
};
module.exports = dbPlugin;
================================================
FILE: chapter11/1_writing_end_to_end_tests/3_sending_http_requests/cypress/plugins/index.js
================================================
///
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
const dbPlugin = require("./dbPlugin");
module.exports = (on, config) => {
dbPlugin(on, config);
};
================================================
FILE: chapter11/1_writing_end_to_end_tests/3_sending_http_requests/cypress/support/commands.js
================================================
Cypress.Commands.add("addItem", (itemName, quantity) => {
return cy.request({
url: `http://localhost:3000/inventory/${itemName}`,
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ quantity })
});
});
================================================
FILE: chapter11/1_writing_end_to_end_tests/3_sending_http_requests/cypress/support/index.js
================================================
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import "./commands";
// Alternatively you can use CommonJS syntax:
// require('./commands')
================================================
FILE: chapter11/1_writing_end_to_end_tests/3_sending_http_requests/cypress.json
================================================
{
"nodeVersion": "system"
}
================================================
FILE: chapter11/1_writing_end_to_end_tests/3_sending_http_requests/package.json
================================================
{
"name": "3_sending_http_requests",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"cypress:open": "NODE_ENV=development cypress open",
"cypress:run": "NODE_ENV=development cypress run"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"cypress": "^4.12.1",
"knex": "^0.20.13",
"sqlite3": "4.1.1"
}
}
================================================
FILE: chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/dbConnection.js
================================================
const environmentName = process.env.NODE_ENV;
const db = require("knex")(require("./knexfile")[environmentName]);
const closeConnection = () => db.destroy();
module.exports = {
db,
closeConnection
};
================================================
FILE: chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/fixtures/example.json
================================================
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}
================================================
FILE: chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/integration/itemListUpdates.spec.js
================================================
import { InventoryManagement } from "../pageObjects/inventoryManagement";
describe("item list updates", () => {
beforeEach(() => cy.task("emptyInventory"));
describe("when the application loads for the first time", () => {
it("loads the initial list of items", () => {
cy.addItem("cheesecake", 2);
cy.addItem("apple pie", 5);
cy.addItem("carrot cake", 96);
cy.visit("http://localhost:8080");
InventoryManagement.findItemEntry("cheesecake", "2");
InventoryManagement.findItemEntry("apple pie", "5");
InventoryManagement.findItemEntry("carrot cake", "96");
});
});
describe("as other users add items", () => {
it("updates the item list", () => {
cy.visit("http://localhost:8080");
InventoryManagement.findAction({});
cy.addItem("cheesecake", 22);
InventoryManagement.findItemEntry("cheesecake", "22");
});
});
});
================================================
FILE: chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/integration/itemSubmission.spec.js
================================================
import { InventoryManagement } from "../pageObjects/inventoryManagement";
describe("item submission", () => {
beforeEach(() => cy.task("emptyInventory"));
it("can add items through the form", () => {
InventoryManagement.visit();
InventoryManagement.addItem("cheesecake", "10");
InventoryManagement.findItemEntry("cheesecake", "10");
});
it("can update an item's quantity", () => {
cy.task("seedItem", { itemName: "cheesecake", quantity: 5 });
InventoryManagement.visit();
InventoryManagement.addItem("cheesecake", "10");
InventoryManagement.findItemEntry("cheesecake", "15");
});
it("can undo submitted items", () => {
InventoryManagement.visit();
InventoryManagement.addItem("cheesecake", "10");
InventoryManagement.addItem("cheesecake", "5");
InventoryManagement.undo();
InventoryManagement.findItemEntry("cheesecake", "10");
});
it("saves each submission to the action log", () => {
InventoryManagement.visit();
InventoryManagement.addItem("cheesecake", "10");
InventoryManagement.addItem("cheesecake", "5");
InventoryManagement.undo();
InventoryManagement.findItemEntry("cheesecake", "10");
InventoryManagement.findAction({});
InventoryManagement.findAction({ cheesecake: 10 }).should("have.length", 2);
InventoryManagement.findAction({ cheesecake: 15 });
});
describe("given a user enters an invalid item name", () => {
it("disables the form's submission button", () => {
InventoryManagement.visit();
InventoryManagement.enterItemName("boat");
InventoryManagement.enterQuantity(10);
InventoryManagement.getSubmitButton().should("be.disabled");
});
});
});
================================================
FILE: chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/knexfile.js
================================================
module.exports = {
development: {
client: "sqlite3",
connection: { filename: "../../server/dev.sqlite" },
useNullAsDefault: true
}
};
================================================
FILE: chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/pageObjects/inventoryManagement.js
================================================
export class InventoryManagement {
static visit() {
cy.visit("http://localhost:8080");
}
static enterItemName(itemName) {
return cy
.get('input[placeholder="Item name"]')
.clear()
.type(itemName);
}
static enterQuantity(quantity) {
return cy
.get('input[placeholder="Quantity"]')
.clear()
.type(quantity);
}
static getSubmitButton() {
return cy.get('button[type="submit"]').contains("Add to inventory");
}
static addItem(itemName, quantity) {
InventoryManagement.enterItemName(itemName);
InventoryManagement.enterQuantity(quantity);
InventoryManagement.getSubmitButton().click();
}
static findItemEntry(itemName, quantity) {
return cy.contains("li", `${itemName} - Quantity: ${quantity}`);
}
static undo() {
return cy
.get("button")
.contains("Undo")
.click();
}
static findAction(inventoryState) {
return cy.get("p:not(:nth-of-type(1))").then(p => {
return Array.from(p).filter(p => {
return p.innerText.includes(
`The inventory has been updated - ${JSON.stringify(inventoryState)}`
);
});
});
}
}
================================================
FILE: chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/plugins/dbPlugin.js
================================================
const { db } = require("../dbConnection");
const dbPlugin = (on, config) => {
on(
"task",
{
emptyInventory: () => db("inventory").truncate(),
seedItem: itemRow => db("inventory").insert(itemRow)
},
config
);
return config;
};
module.exports = dbPlugin;
================================================
FILE: chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/plugins/index.js
================================================
///
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
const dbPlugin = require("./dbPlugin");
module.exports = (on, config) => {
dbPlugin(on, config);
};
================================================
FILE: chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/support/commands.js
================================================
Cypress.Commands.add("addItem", (itemName, quantity) => {
return cy.request({
url: `http://localhost:3000/inventory/${itemName}`,
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ quantity })
});
});
================================================
FILE: chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/support/index.js
================================================
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import "./commands";
// Alternatively you can use CommonJS syntax:
// require('./commands')
================================================
FILE: chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress.json
================================================
{
"nodeVersion": "system"
}
================================================
FILE: chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/package.json
================================================
{
"name": "1_page_objects",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"cypress:open": "NODE_ENV=development cypress open",
"cypress:run": "NODE_ENV=development cypress run"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"cypress": "^4.12.1",
"knex": "^0.20.13",
"sqlite3": "4.1.1"
}
}
================================================
FILE: chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/dbConnection.js
================================================
const environmentName = process.env.NODE_ENV;
const db = require("knex")(require("./knexfile")[environmentName]);
const closeConnection = () => db.destroy();
module.exports = {
db,
closeConnection
};
================================================
FILE: chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/fixtures/example.json
================================================
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}
================================================
FILE: chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/integration/itemListUpdates.spec.js
================================================
import { InventoryManagement } from "../pageObjects/inventoryManagement";
describe("item list updates", () => {
beforeEach(() => cy.task("emptyInventory"));
describe("when the application loads for the first time", () => {
it("loads the initial list of items", () => {
cy.addItem("cheesecake", 2);
cy.addItem("apple pie", 5);
cy.addItem("carrot cake", 96);
cy.visit("http://localhost:8080");
InventoryManagement.findItemEntry("cheesecake", "2");
InventoryManagement.findItemEntry("apple pie", "5");
InventoryManagement.findItemEntry("carrot cake", "96");
});
});
describe("as other users add items", () => {
it("updates the item list", () => {
cy.visit("http://localhost:8080");
InventoryManagement.findAction({});
cy.addItem("cheesecake", 22);
InventoryManagement.findItemEntry("cheesecake", "22");
});
});
});
================================================
FILE: chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/integration/itemSubmission.spec.js
================================================
import { InventoryManagement } from "../pageObjects/inventoryManagement";
describe("item submission", () => {
beforeEach(() => cy.task("emptyInventory"));
it("can add items through the form", () => {
InventoryManagement.visit();
InventoryManagement.addItem("cheesecake", "10");
InventoryManagement.findItemEntry("cheesecake", "10");
});
it("can update an item's quantity", () => {
cy.task("seedItem", { itemName: "cheesecake", quantity: 5 });
InventoryManagement.visit();
InventoryManagement.addItem("cheesecake", "10");
InventoryManagement.findItemEntry("cheesecake", "15");
});
it("can undo submitted items", () => {
InventoryManagement.visit();
cy.wait(1000);
InventoryManagement.findAction({});
cy.window().then(({ handleAddItem }) => handleAddItem("cheesecake", 10));
cy.wait(1000);
InventoryManagement.findItemEntry("cheesecake", "10");
cy.window().then(({ handleAddItem }) => handleAddItem("cheesecake", 5));
cy.wait(1000);
InventoryManagement.undo();
InventoryManagement.findItemEntry("cheesecake", "10");
});
it("saves each submission to the action log", () => {
InventoryManagement.visit();
InventoryManagement.addItem("cheesecake", "10");
InventoryManagement.addItem("cheesecake", "5");
InventoryManagement.undo();
InventoryManagement.findItemEntry("cheesecake", "10");
InventoryManagement.findAction({});
InventoryManagement.findAction({ cheesecake: 10 }).should("have.length", 2);
InventoryManagement.findAction({ cheesecake: 15 });
});
describe("given a user enters an invalid item name", () => {
it("disables the form's submission button", () => {
InventoryManagement.visit();
InventoryManagement.enterItemName("boat");
InventoryManagement.enterQuantity(10);
InventoryManagement.getSubmitButton().should("be.disabled");
});
});
});
================================================
FILE: chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/knexfile.js
================================================
module.exports = {
development: {
client: "sqlite3",
connection: { filename: "../../server/dev.sqlite" },
useNullAsDefault: true
}
};
================================================
FILE: chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/pageObjects/inventoryManagement.js
================================================
export class InventoryManagement {
static visit() {
cy.visit("http://localhost:8080");
}
static enterItemName(itemName) {
return cy
.get('input[placeholder="Item name"]')
.clear()
.type(itemName);
}
static enterQuantity(quantity) {
return cy
.get('input[placeholder="Quantity"]')
.clear()
.type(quantity);
}
static getSubmitButton() {
return cy.get('button[type="submit"]').contains("Add to inventory");
}
static addItem(itemName, quantity) {
InventoryManagement.enterItemName(itemName);
InventoryManagement.enterQuantity(quantity);
InventoryManagement.getSubmitButton().click();
}
static findItemEntry(itemName, quantity) {
return cy.contains("li", `${itemName} - Quantity: ${quantity}`);
}
static undo() {
return cy
.get("button")
.contains("Undo")
.click();
}
static findAction(inventoryState) {
return cy.get("p:not(:nth-of-type(1))").then(p => {
return Array.from(p).filter(p => {
return p.innerText.includes(
`The inventory has been updated - ${JSON.stringify(inventoryState)}`
);
});
});
}
}
================================================
FILE: chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/plugins/dbPlugin.js
================================================
const { db } = require("../dbConnection");
const dbPlugin = (on, config) => {
on(
"task",
{
emptyInventory: () => db("inventory").truncate(),
seedItem: itemRow => db("inventory").insert(itemRow)
},
config
);
return config;
};
module.exports = dbPlugin;
================================================
FILE: chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/plugins/index.js
================================================
///
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
const dbPlugin = require("./dbPlugin");
module.exports = (on, config) => {
dbPlugin(on, config);
};
================================================
FILE: chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/support/commands.js
================================================
Cypress.Commands.add("addItem", (itemName, quantity) => {
return cy.request({
url: `http://localhost:3000/inventory/${itemName}`,
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ quantity })
});
});
================================================
FILE: chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/support/index.js
================================================
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import "./commands";
// Alternatively you can use CommonJS syntax:
// require('./commands')
================================================
FILE: chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress.json
================================================
{
"nodeVersion": "system"
}
================================================
FILE: chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/package.json
================================================
{
"name": "2_application_actions",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"cypress:open": "NODE_ENV=development cypress open",
"cypress:run": "NODE_ENV=development cypress run"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"cypress": "^4.12.1",
"knex": "^0.20.13",
"sqlite3": "4.1.1"
}
}
================================================
FILE: chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/dbConnection.js
================================================
const environmentName = process.env.NODE_ENV;
const db = require("knex")(require("./knexfile")[environmentName]);
const closeConnection = () => db.destroy();
module.exports = {
db,
closeConnection
};
================================================
FILE: chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/fixtures/example.json
================================================
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}
================================================
FILE: chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/integration/itemListUpdates.spec.js
================================================
import { InventoryManagement } from "../pageObjects/inventoryManagement";
describe("item list updates", () => {
beforeEach(() => cy.task("emptyInventory"));
describe("when the application loads for the first time", () => {
it("loads the initial list of items", () => {
cy.addItem("cheesecake", 2);
cy.addItem("apple pie", 5);
cy.addItem("carrot cake", 96);
cy.visit("http://localhost:8080");
InventoryManagement.findItemEntry("cheesecake", "2");
InventoryManagement.findItemEntry("apple pie", "5");
InventoryManagement.findItemEntry("carrot cake", "96");
});
});
describe("as other users add items", () => {
it("updates the item list", () => {
cy.server()
.route("http://localhost:3000/inventory")
.as("inventoryRequest");
cy.visit("http://localhost:8080");
cy.wait("@inventoryRequest");
cy.addItem("cheesecake", 22);
InventoryManagement.findItemEntry("cheesecake", "22");
});
});
});
================================================
FILE: chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/integration/itemSubmission.spec.js
================================================
import { InventoryManagement } from "../pageObjects/inventoryManagement";
describe("item submission", () => {
beforeEach(() => cy.task("emptyInventory"));
it("can add items through the form", () => {
InventoryManagement.visit();
InventoryManagement.addItem("cheesecake", "10");
InventoryManagement.findItemEntry("cheesecake", "10");
});
it("can update an item's quantity", () => {
cy.task("seedItem", { itemName: "cheesecake", quantity: 5 });
InventoryManagement.visit();
InventoryManagement.addItem("cheesecake", "10");
InventoryManagement.findItemEntry("cheesecake", "15");
});
it("can undo submitted items", () => {
InventoryManagement.visit();
InventoryManagement.findAction({});
cy.window().then(({ handleAddItem }) => handleAddItem("cheesecake", 10));
InventoryManagement.findAction({ cheesecake: 10 });
cy.window().then(({ handleAddItem }) => handleAddItem("cheesecake", 5));
InventoryManagement.findAction({ cheesecake: 15 });
InventoryManagement.undo();
InventoryManagement.findItemEntry("cheesecake", "10");
});
it("saves each submission to the action log", () => {
InventoryManagement.visit();
InventoryManagement.addItem("cheesecake", "10");
InventoryManagement.addItem("cheesecake", "5");
InventoryManagement.undo();
InventoryManagement.findItemEntry("cheesecake", "10");
InventoryManagement.findAction({}).should("have.length", 1);
InventoryManagement.findAction({ cheesecake: 10 }).should("have.length", 2);
InventoryManagement.findAction({ cheesecake: 15 }).should("have.length", 1);
});
describe("given a user enters an invalid item name", () => {
it("disables the form's submission button", () => {
InventoryManagement.visit();
InventoryManagement.enterItemName("boat");
InventoryManagement.enterQuantity(10);
InventoryManagement.getSubmitButton().should("be.disabled");
});
});
});
================================================
FILE: chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/knexfile.js
================================================
module.exports = {
development: {
client: "sqlite3",
connection: { filename: "../../server/dev.sqlite" },
useNullAsDefault: true
}
};
================================================
FILE: chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/pageObjects/inventoryManagement.js
================================================
export class InventoryManagement {
static visit() {
cy.visit("http://localhost:8080");
}
static enterItemName(itemName) {
return cy
.get('input[placeholder="Item name"]')
.clear()
.type(itemName);
}
static enterQuantity(quantity) {
return cy
.get('input[placeholder="Quantity"]')
.clear()
.type(quantity);
}
static getSubmitButton() {
return cy.get('button[type="submit"]').contains("Add to inventory");
}
static addItem(itemName, quantity) {
InventoryManagement.enterItemName(itemName);
InventoryManagement.enterQuantity(quantity);
InventoryManagement.getSubmitButton().click();
}
static findItemEntry(itemName, quantity) {
return cy.contains("li", `${itemName} - Quantity: ${quantity}`);
}
static undo() {
return cy
.get("button")
.contains("Undo")
.click();
}
static findAction(inventoryState) {
return cy.get("p:not(:nth-of-type(1))").then(p => {
return Array.from(p).filter(p => {
return p.innerText.includes(
`The inventory has been updated - ${JSON.stringify(inventoryState)}`
);
});
});
}
}
================================================
FILE: chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/plugins/dbPlugin.js
================================================
const { db } = require("../dbConnection");
const dbPlugin = (on, config) => {
on(
"task",
{
emptyInventory: () => db("inventory").truncate(),
seedItem: itemRow => db("inventory").insert(itemRow)
},
config
);
return config;
};
module.exports = dbPlugin;
================================================
FILE: chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/plugins/index.js
================================================
///
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
const dbPlugin = require("./dbPlugin");
module.exports = (on, config) => {
dbPlugin(on, config);
};
================================================
FILE: chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/support/commands.js
================================================
import "cypress-wait-until";
Cypress.Commands.add("addItem", (itemName, quantity) => {
return cy.request({
url: `http://localhost:3000/inventory/${itemName}`,
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ quantity })
});
});
================================================
FILE: chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/support/index.js
================================================
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import "./commands";
// Alternatively you can use CommonJS syntax:
// require('./commands')
================================================
FILE: chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress.json
================================================
{
"nodeVersion": "system",
"experimentalFetchPolyfill": true
}
================================================
FILE: chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/package.json
================================================
{
"name": "1_avoiding_waiting_for_fixed_amounts_of_time",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"cypress:open": "NODE_ENV=development cypress open",
"cypress:run": "NODE_ENV=development cypress run"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"cypress": "^4.12.1",
"cypress-wait-until": "^1.7.1",
"knex": "^0.20.13",
"sqlite3": "4.1.1"
}
}
================================================
FILE: chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/dbConnection.js
================================================
const environmentName = process.env.NODE_ENV;
const db = require("knex")(require("./knexfile")[environmentName]);
const closeConnection = () => db.destroy();
module.exports = {
db,
closeConnection
};
================================================
FILE: chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/fixtures/example.json
================================================
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}
================================================
FILE: chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/integration/itemListUpdates.spec.js
================================================
import { InventoryManagement } from "../pageObjects/inventoryManagement";
describe("item list updates", () => {
beforeEach(() => cy.task("emptyInventory"));
describe("when the application loads for the first time", () => {
it("loads the initial list of items", () => {
cy.addItem("cheesecake", 2);
cy.addItem("apple pie", 5);
cy.addItem("carrot cake", 96);
cy.visit("http://localhost:8080");
InventoryManagement.findItemEntry("cheesecake", "2");
InventoryManagement.findItemEntry("apple pie", "5");
InventoryManagement.findItemEntry("carrot cake", "96");
});
});
describe("as other users add items", () => {
it("updates the item list", () => {
cy.server()
.route("http://localhost:3000/inventory")
.as("inventoryRequest");
cy.visit("http://localhost:8080");
cy.wait("@inventoryRequest");
cy.addItem("cheesecake", 22);
InventoryManagement.findItemEntry("cheesecake", "22");
});
});
});
================================================
FILE: chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/integration/itemSubmission.spec.js
================================================
import { InventoryManagement } from "../pageObjects/inventoryManagement";
describe("item submission", () => {
beforeEach(() => cy.task("emptyInventory"));
beforeEach(() => {
cy.server();
cy.route("GET", "/inventory/cheesecake", {
recipes: [
{ href: "http://example.com/always-the-same-url/first-recipe" },
{ href: "http://example.com/always-the-same-url/second-recipe" },
{ href: "http://example.com/always-the-same-url/third-recipe" }
]
});
});
it("can add items through the form", () => {
InventoryManagement.visit();
cy.window().then(w => cy.stub(w.Math, "random").returns(0.5));
InventoryManagement.addItem("cheesecake", "10");
InventoryManagement.findItemEntry("cheesecake", "10")
.get("a")
.should(
"have.attr",
"href",
"http://example.com/always-the-same-url/second-recipe"
);
});
it("can update an item's quantity", () => {
cy.task("seedItem", { itemName: "cheesecake", quantity: 5 });
InventoryManagement.visit();
InventoryManagement.addItem("cheesecake", "10");
InventoryManagement.findItemEntry("cheesecake", "15");
});
it("can undo submitted items", () => {
InventoryManagement.visit();
InventoryManagement.findAction({});
cy.window().then(({ handleAddItem }) => handleAddItem("cheesecake", 10));
InventoryManagement.findAction({ cheesecake: 10 });
cy.window().then(({ handleAddItem }) => handleAddItem("cheesecake", 5));
InventoryManagement.findAction({ cheesecake: 15 });
InventoryManagement.undo();
InventoryManagement.findItemEntry("cheesecake", "10");
});
it("saves each submission to the action log", () => {
InventoryManagement.visit();
InventoryManagement.findAction({});
cy.clock().tick(1000);
InventoryManagement.addItem("cheesecake", "10");
InventoryManagement.findAction({ cheesecake: 10 });
cy.clock().tick(1000);
InventoryManagement.addItem("cheesecake", "5");
InventoryManagement.findAction({ cheesecake: 15 });
cy.clock().tick(1000);
InventoryManagement.undo();
InventoryManagement.findItemEntry("cheesecake", "10");
InventoryManagement.findAction({ cheesecake: 10 });
});
describe("given a user enters an invalid item name", () => {
it("disables the form's submission button", () => {
InventoryManagement.visit();
InventoryManagement.enterItemName("boat");
InventoryManagement.enterQuantity(10);
InventoryManagement.getSubmitButton().should("be.disabled");
});
});
});
================================================
FILE: chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/knexfile.js
================================================
module.exports = {
development: {
client: "sqlite3",
connection: { filename: "../../server/dev.sqlite" },
useNullAsDefault: true
}
};
================================================
FILE: chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/pageObjects/inventoryManagement.js
================================================
export class InventoryManagement {
static visit() {
cy.visit("http://localhost:8080");
}
static enterItemName(itemName) {
return cy
.get('input[placeholder="Item name"]')
.clear()
.type(itemName);
}
static enterQuantity(quantity) {
return cy
.get('input[placeholder="Quantity"]')
.clear()
.type(quantity);
}
static getSubmitButton() {
return cy.get('button[type="submit"]').contains("Add to inventory");
}
static addItem(itemName, quantity) {
InventoryManagement.enterItemName(itemName);
InventoryManagement.enterQuantity(quantity);
InventoryManagement.getSubmitButton().click();
}
static findItemEntry(itemName, quantity) {
return cy.contains("li", `${itemName} - Quantity: ${quantity}`);
}
static undo() {
return cy
.get("button")
.contains("Undo")
.click();
}
static findAction(inventoryState) {
return cy.clock(c => {
const dateText = new Date(c.details().now).toISOString();
return cy
.get("p:not(:nth-of-type(1))")
.contains(
`[${dateText}]` +
" The inventory has been updated - " +
JSON.stringify(inventoryState)
);
});
}
}
================================================
FILE: chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/plugins/dbPlugin.js
================================================
const { db } = require("../dbConnection");
const dbPlugin = (on, config) => {
on(
"task",
{
emptyInventory: () => db("inventory").truncate(),
seedItem: itemRow => db("inventory").insert(itemRow)
},
config
);
return config;
};
module.exports = dbPlugin;
================================================
FILE: chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/plugins/index.js
================================================
///
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
const dbPlugin = require("./dbPlugin");
module.exports = (on, config) => {
dbPlugin(on, config);
};
================================================
FILE: chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/support/commands.js
================================================
import "cypress-wait-until";
Cypress.Commands.add("addItem", (itemName, quantity) => {
return cy.request({
url: `http://localhost:3000/inventory/${itemName}`,
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ quantity })
});
});
================================================
FILE: chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/support/index.js
================================================
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import "./commands";
// Alternatively you can use CommonJS syntax:
// require('./commands')
beforeEach(() => cy.clock(Date.now()).as("fakeTimer"));
================================================
FILE: chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress.json
================================================
{
"nodeVersion": "system",
"experimentalFetchPolyfill": true
}
================================================
FILE: chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/package.json
================================================
{
"name": "2_stubbing_uncontrollable_factors",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"cypress:open": "NODE_ENV=development cypress open",
"cypress:run": "NODE_ENV=development cypress run"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"cypress": "^4.12.1",
"cypress-wait-until": "^1.7.1",
"knex": "^0.20.13",
"sqlite3": "4.1.1"
}
}
================================================
FILE: chapter11/4_visual_regression_tests/cypress/dbConnection.js
================================================
const environmentName = process.env.NODE_ENV;
const db = require("knex")(require("./knexfile")[environmentName]);
const closeConnection = () => db.destroy();
module.exports = {
db,
closeConnection
};
================================================
FILE: chapter11/4_visual_regression_tests/cypress/fixtures/example.json
================================================
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}
================================================
FILE: chapter11/4_visual_regression_tests/cypress/integration/itemList.spec.js
================================================
import { InventoryManagement } from "../pageObjects/inventoryManagement";
const now = new Date(1996, 5, 2).getTime();
describe("item list", () => {
beforeEach(() => cy.task("emptyInventory"));
it("can update an item's quantity", () => {
cy.task("seedItem", { itemName: "cheesecake", quantity: 1 });
InventoryManagement.visit();
InventoryManagement.findItemEntry("cheesecake", "1");
cy.percySnapshot();
});
});
================================================
FILE: chapter11/4_visual_regression_tests/cypress/integration/itemListUpdates.spec.js
================================================
import { InventoryManagement } from "../pageObjects/inventoryManagement";
describe("item list updates", () => {
beforeEach(() => cy.task("emptyInventory"));
describe("when the application loads for the first time", () => {
it.only("loads the initial list of items", () => {
cy.addItem("cheesecake", 2);
cy.addItem("apple pie", 5);
cy.addItem("carrot cake", 96);
cy.visit("http://localhost:8080");
cy.wait(1);
InventoryManagement.findItemEntry("cheesecake", "2");
InventoryManagement.findItemEntry("apple pie", "5");
InventoryManagement.findItemEntry("carrot cake", "96");
});
});
describe("as other users add items", () => {
it("updates the item list", () => {
cy.server()
.route("http://localhost:3000/inventory")
.as("inventoryRequest");
cy.visit("http://localhost:8080");
cy.wait("@inventoryRequest");
cy.addItem("cheesecake", 22);
InventoryManagement.findItemEntry("cheesecake", "22");
});
});
});
================================================
FILE: chapter11/4_visual_regression_tests/cypress/integration/itemSubmission.spec.js
================================================
import { InventoryManagement } from "../pageObjects/inventoryManagement";
describe("item submission", () => {
beforeEach(() => cy.task("emptyInventory"));
beforeEach(() => {
cy.server();
cy.route("GET", "/inventory/cheesecake", {
recipes: [
{ href: "http://example.com/always-the-same-url/first-recipe" },
{ href: "http://example.com/always-the-same-url/second-recipe" },
{ href: "http://example.com/always-the-same-url/third-recipe" }
]
});
});
it("can add items through the form", () => {
InventoryManagement.visit();
cy.window().then(w => cy.stub(w.Math, "random").returns(0.5));
InventoryManagement.addItem("cheesecake", "10");
InventoryManagement.findItemEntry("cheesecake", "10")
.get("a")
.should(
"have.attr",
"href",
"http://example.com/always-the-same-url/second-recipe"
);
});
it("can update an item's quantity", () => {
cy.task("seedItem", { itemName: "cheesecake", quantity: 5 });
InventoryManagement.visit();
InventoryManagement.addItem("cheesecake", "10");
InventoryManagement.findItemEntry("cheesecake", "15");
});
it("can undo submitted items", () => {
InventoryManagement.visit();
InventoryManagement.findAction({});
cy.window().then(({ handleAddItem }) => handleAddItem("cheesecake", 10));
InventoryManagement.findAction({ cheesecake: 10 });
cy.window().then(({ handleAddItem }) => handleAddItem("cheesecake", 5));
InventoryManagement.findAction({ cheesecake: 15 });
InventoryManagement.undo();
InventoryManagement.findItemEntry("cheesecake", "10");
});
it.only("saves each submission to the action log", () => {
cy.clock();
InventoryManagement.visit();
InventoryManagement.findAction({});
cy.clock().tick(2000);
InventoryManagement.addItem("cheesecake", "10");
InventoryManagement.findAction({ cheesecake: 10 });
cy.clock().tick(2000);
InventoryManagement.addItem("cheesecake", "5");
InventoryManagement.findAction({ cheesecake: 15 });
cy.clock().tick(2000);
InventoryManagement.undo();
InventoryManagement.findItemEntry("cheesecake", "10");
InventoryManagement.findAction({ cheesecake: 10 });
});
describe("given a user enters an invalid item name", () => {
it("disables the form's submission button", () => {
InventoryManagement.visit();
InventoryManagement.enterItemName("boat");
InventoryManagement.enterQuantity(10);
InventoryManagement.getSubmitButton().should("be.disabled");
});
});
});
================================================
FILE: chapter11/4_visual_regression_tests/cypress/knexfile.js
================================================
module.exports = {
development: {
client: "sqlite3",
connection: { filename: "../server/dev.sqlite" },
useNullAsDefault: true
}
};
================================================
FILE: chapter11/4_visual_regression_tests/cypress/pageObjects/inventoryManagement.js
================================================
export class InventoryManagement {
static visit() {
cy.visit("http://localhost:8080");
}
static enterItemName(itemName) {
return cy
.get('input[placeholder="Item name"]')
.clear()
.type(itemName);
}
static enterQuantity(quantity) {
return cy
.get('input[placeholder="Quantity"]')
.clear()
.type(quantity);
}
static getSubmitButton() {
return cy.get('button[type="submit"]').contains("Add to inventory");
}
static addItem(itemName, quantity) {
InventoryManagement.enterItemName(itemName);
InventoryManagement.enterQuantity(quantity);
InventoryManagement.getSubmitButton().click();
}
static findItemEntry(itemName, quantity) {
return cy.contains("li", `${itemName} - Quantity: ${quantity}`);
}
static undo() {
return cy
.get("button")
.contains("Undo")
.click();
}
static findAction(inventoryState) {
return cy.clock(c => {
const dateText = new Date(c.details().now).toISOString();
return cy
.get("p:not(:nth-of-type(1))")
.contains(
`[${dateText}]` +
" The inventory has been updated - " +
JSON.stringify(inventoryState)
);
});
}
}
================================================
FILE: chapter11/4_visual_regression_tests/cypress/plugins/dbPlugin.js
================================================
const { db } = require("../dbConnection");
const dbPlugin = (on, config) => {
on(
"task",
{
emptyInventory: () => db("inventory").truncate(),
seedItem: itemRow => db("inventory").insert(itemRow)
},
config
);
return config;
};
module.exports = dbPlugin;
================================================
FILE: chapter11/4_visual_regression_tests/cypress/plugins/index.js
================================================
///
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
const percyHealthCheck = require("@percy/cypress/task");
const dbPlugin = require("./dbPlugin");
module.exports = (on, config) => {
dbPlugin(on, config);
on("task", percyHealthCheck);
};
================================================
FILE: chapter11/4_visual_regression_tests/cypress/support/commands.js
================================================
import "@percy/cypress";
import "cypress-wait-until";
Cypress.Commands.add("addItem", (itemName, quantity) => {
return cy.request({
url: `http://localhost:3000/inventory/${itemName}`,
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ quantity })
});
});
================================================
FILE: chapter11/4_visual_regression_tests/cypress/support/index.js
================================================
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import "./commands";
// Alternatively you can use CommonJS syntax:
// require('./commands')
beforeEach(() => cy.clock(Date.now()).as("fakeTimer"));
================================================
FILE: chapter11/4_visual_regression_tests/cypress.json
================================================
{
"nodeVersion": "system",
"experimentalFetchPolyfill": true
}
================================================
FILE: chapter11/4_visual_regression_tests/package.json
================================================
{
"name": "4_visual_regression_tests",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"cypress:open": "NODE_ENV=development cypress open",
"cypress:run": "NODE_ENV=development percy exec -- cypress run"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@percy/cypress": "^2.3.1",
"cypress": "^4.12.1",
"cypress-wait-until": "^1.7.1",
"knex": "^0.20.13",
"sqlite3": "4.1.1"
}
}
================================================
FILE: chapter11/client/domController.js
================================================
const { API_ADDR, addItem, data } = require("./inventoryController");
const updateItemList = inventory => {
if (inventory === null) return;
localStorage.setItem("inventory", JSON.stringify(inventory));
const inventoryList = window.document.getElementById("item-list");
// Clears the list
inventoryList.innerHTML = "";
Object.entries(inventory).forEach(async ([itemName, quantity]) => {
const listItem = window.document.createElement("li");
const listLink = window.document.createElement("a");
listItem.appendChild(listLink);
const recipeResponse = await fetch(`${API_ADDR}/inventory/${itemName}`);
const recipeList = (await recipeResponse.json()).recipes;
const randomRecipe = Math.floor(Math.random() * recipeList.length - 1) + 1;
listLink.innerHTML = `${itemName} - Quantity: ${quantity}`;
listLink.href = recipeList[randomRecipe]
? recipeList[randomRecipe].href
: "#";
if (quantity < 5) {
listItem.className = "almost-soldout";
}
inventoryList.appendChild(listItem);
});
const inventoryContents = JSON.stringify(inventory);
const p = window.document.createElement("p");
p.innerHTML = `[${new Date().toISOString()}] The inventory has been updated - ${inventoryContents}`;
window.document.body.appendChild(p);
};
const handleAddItem = event => {
// Prevent the page from reloading as it would by default
event.preventDefault();
const { name, quantity } = event.target.elements;
addItem(name.value, parseInt(quantity.value, 10));
history.pushState({ inventory: { ...data.inventory } }, document.title);
updateItemList(data.inventory);
};
if (window.Cypress) {
window.handleAddItem = (name, quantity) => {
const e = {
preventDefault: () => {},
target: {
elements: {
name: { value: name },
quantity: { value: quantity }
}
}
};
return handleAddItem(e);
};
}
const validItems = ["cheesecake", "apple pie", "carrot cake"];
const checkFormValues = () => {
const itemName = document.querySelector(`input[name="name"]`).value;
const quantity = document.querySelector(`input[name="quantity"]`).value;
const itemNameIsEmpty = itemName === "";
const itemNameIsInvalid = !validItems.includes(itemName);
const quantityIsEmpty = quantity === "";
const errorMsg = window.document.getElementById("error-msg");
if (itemNameIsEmpty) {
errorMsg.innerHTML = "";
} else if (itemNameIsInvalid) {
errorMsg.innerHTML = `${itemName} is not a valid item.`;
} else {
errorMsg.innerHTML = `${itemName} is valid!`;
}
const submitButton = document.querySelector(`button[type="submit"]`);
if (itemNameIsEmpty || itemNameIsInvalid || quantityIsEmpty) {
submitButton.disabled = true;
} else {
submitButton.disabled = false;
}
};
const handleUndo = () => {
if (history.state === null) return;
history.back();
};
const handlePopstate = () => {
data.inventory = history.state ? history.state.inventory : {};
updateItemList(data.inventory);
};
module.exports = {
updateItemList,
handleAddItem,
checkFormValues,
handleUndo,
handlePopstate
};
================================================
FILE: chapter11/client/domController.test.js
================================================
const nock = require("nock");
const fs = require("fs");
const initialHtml = fs.readFileSync("./index.html");
const { getByText, screen } = require("@testing-library/dom");
const {
updateItemList,
handleAddItem,
checkFormValues,
handleUndo,
handlePopstate
} = require("./domController");
const { clearHistoryHook, detachPopstateHandlers } = require("./testUtils");
const { API_ADDR, data } = require("./inventoryController");
beforeEach(() => {
document.body.innerHTML = initialHtml;
});
describe("updateItemList", () => {
beforeEach(() => localStorage.clear());
test("updates the DOM with the inventory items", () => {
const inventory = {
cheesecake: 5,
"apple pie": 2,
"carrot cake": 6
};
updateItemList(inventory);
const itemList = document.getElementById("item-list");
expect(itemList.childNodes).toHaveLength(3);
expect(getByText(itemList, "cheesecake - Quantity: 5")).toBeInTheDocument();
expect(getByText(itemList, "apple pie - Quantity: 2")).toBeInTheDocument();
expect(
getByText(itemList, "carrot cake - Quantity: 6")
).toBeInTheDocument();
});
test("highlighting in red elements whose quantity is below five", () => {
const inventory = { cheesecake: 5, "apple pie": 2, "carrot cake": 6 };
updateItemList(inventory);
expect(screen.getByText("apple pie - Quantity: 2")).toHaveStyle({
color: "red"
});
});
test("adding a paragraph indicating what was the update", () => {
const inventory = { cheesecake: 5, "apple pie": 2 };
updateItemList(inventory);
expect(
screen.getByText(
`The inventory has been updated - ${JSON.stringify(inventory)}`
)
).toBeTruthy();
});
test("updates the localStorage with the inventory", () => {
const inventory = { cheesecake: 5, "apple pie": 2 };
updateItemList(inventory);
expect(localStorage.getItem("inventory")).toEqual(
JSON.stringify(inventory)
);
});
test("does not update the inventory when passing null", () => {
localStorage.setItem("inventory", JSON.stringify({ cheesecake: 5 }));
updateItemList(null);
expect(localStorage.getItem("inventory")).toEqual(
JSON.stringify({ cheesecake: 5 })
);
});
});
describe("handleAddItem", () => {
beforeEach(() => (data.inventory = {}));
test("adding items to the page", () => {
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 6 }))
.reply(200);
const event = {
preventDefault: jest.fn(),
target: {
elements: {
name: { value: "cheesecake" },
quantity: { value: "6" }
}
}
};
handleAddItem(event);
// Checking if the form's default reload is prevent
expect(event.preventDefault.mock.calls).toHaveLength(1);
const itemList = document.getElementById("item-list");
expect(getByText(itemList, "cheesecake - Quantity: 6")).toBeInTheDocument();
if (!nock.isDone())
throw new Error("POST /inventory/cheesecake was not reached");
});
test("updating the application's history", () => {
nock(API_ADDR)
.post(/inventory\/.*$/)
.reply(200);
const event = {
preventDefault: jest.fn(),
target: {
elements: {
name: { value: "cheesecake" },
quantity: { value: "6" }
}
}
};
handleAddItem(event);
expect(history.state).toEqual({ inventory: { cheesecake: 6 } });
});
});
describe("checkFormValues", () => {
test("entering valid item values", () => {
document.querySelector(`input[name="name"]`).value = "cheesecake";
document.querySelector(`input[name="quantity"]`).value = "1";
checkFormValues();
expect(screen.getByText("Add to inventory")).toBeEnabled();
});
test("entering invalid item names", () => {
document.querySelector(`input[name="name"]`).value = "invalid";
document.querySelector(`input[name="quantity"]`).value = "1";
checkFormValues();
expect(screen.getByText("Add to inventory")).toBeDisabled();
document.querySelector(`input[name="name"]`).value = "cheesecake";
document.querySelector(`input[name="quantity"]`).value = "";
checkFormValues();
expect(screen.getByText("Add to inventory")).toBeDisabled();
});
});
describe("tests with history", () => {
beforeEach(() => jest.spyOn(window, "addEventListener"));
afterEach(detachPopstateHandlers);
beforeEach(clearHistoryHook);
describe("handleUndo", () => {
test("going back from a non-initial state", done => {
window.addEventListener("popstate", () => {
expect(history.state).toEqual(null);
done();
});
history.pushState({ inventory: { cheesecake: 5 } }, "title");
handleUndo();
});
test("going back from an initial state", () => {
jest.spyOn(history, "back");
handleUndo();
// This assertion doesn't care about whether
// a call to `history.back` would have finished,
// it only checks whether it's been called
expect(history.back.mock.calls).toHaveLength(0);
});
});
describe("handlePopstate", () => {
test("updating the item list with the current state", () => {
history.pushState(
{ inventory: { cheesecake: 5, "carrot cake": 2 } },
"title"
);
handlePopstate();
const itemList = document.getElementById("item-list");
expect(itemList.childNodes).toHaveLength(2);
expect(
getByText(itemList, "cheesecake - Quantity: 5")
).toBeInTheDocument();
expect(
getByText(itemList, "carrot cake - Quantity: 2")
).toBeInTheDocument();
});
});
});
================================================
FILE: chapter11/client/index.html
================================================
Inventory Manager
Inventory Contents
Undo
================================================
FILE: chapter11/client/inventoryController.js
================================================
const data = { inventory: {} };
const API_ADDR = "http://localhost:3000";
const addItem = (itemName, quantity) => {
const { client } = require("./socket");
const currentQuantity = data.inventory[itemName] || 0;
data.inventory[itemName] = currentQuantity + quantity;
fetch(`${API_ADDR}/inventory/${itemName}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-socket-client-id": client.id
},
body: JSON.stringify({ quantity })
});
return data.inventory;
};
module.exports = { API_ADDR, data, addItem };
================================================
FILE: chapter11/client/inventoryController.test.js
================================================
const nock = require("nock");
const { API_ADDR, addItem, data } = require("./inventoryController");
const { start, stop } = require("./testSocketServer");
const { client, connect } = require("./socket");
afterEach(() => {
if (!nock.isDone()) {
nock.cleanAll();
throw new Error("Not all mocked endpoints received requests.");
}
});
describe("addItem", () => {
beforeEach(() => (data.inventory = {}));
test("adding new items to the inventory", () => {
// Respond to all post requests
// to POST /inventory/:itemName
nock(API_ADDR)
.post(/inventory\/.*$/)
.reply(200);
addItem("cheesecake", 5);
expect(data.inventory.cheesecake).toBe(5);
});
test("sending requests when adding new items", () => {
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 5 }))
.reply(200);
addItem("cheesecake", 5);
});
describe("live-updates", () => {
beforeAll(start);
beforeAll(async () => {
nock.cleanAll();
await connect();
});
afterAll(stop);
test("sending a x-socket-client-id header", () => {
const clientId = client.id;
nock(API_ADDR, { reqheaders: { "x-socket-client-id": clientId } })
.post(/inventory\/.*$/)
.reply(200);
addItem("cheesecake", 5);
});
});
});
================================================
FILE: chapter11/client/jest.config.js
================================================
module.exports = {
setupFilesAfterEnv: [
"/setupGlobalFetch.js",
"/setupJestDom.js"
]
};
================================================
FILE: chapter11/client/main.js
================================================
const { connect } = require("./socket");
const {
handleAddItem,
checkFormValues,
handleUndo,
handlePopstate,
updateItemList
} = require("./domController");
const { API_ADDR, data } = require("./inventoryController");
const form = document.getElementById("add-item-form");
form.addEventListener("submit", handleAddItem);
form.addEventListener("input", checkFormValues);
const undoButton = document.getElementById("undo-button");
undoButton.addEventListener("click", handleUndo);
window.addEventListener("popstate", handlePopstate);
// Run `checkFormValues` once to see if the initial state is valid
checkFormValues();
const loadInitialData = async () => {
try {
const inventoryResponse = await fetch(`${API_ADDR}/inventory`);
data.inventory = await inventoryResponse.json();
return updateItemList(data.inventory);
} catch (e) {
// Restore the inventory if the request fails
const storedInventory = JSON.parse(localStorage.getItem("inventory"));
if (storedInventory) {
data.inventory = storedInventory;
updateItemList(data.inventory);
}
}
};
connect();
module.exports = loadInitialData();
================================================
FILE: chapter11/client/main.test.js
================================================
const nock = require("nock");
const fs = require("fs");
const initialHtml = fs.readFileSync("./index.html");
const { screen, getByText, fireEvent } = require("@testing-library/dom");
const { API_ADDR } = require("./inventoryController");
const { clearHistoryHook, detachPopstateHandlers } = require("./testUtils.js");
beforeEach(clearHistoryHook);
beforeEach(() => localStorage.clear());
beforeEach(async () => {
document.body.innerHTML = initialHtml;
// You must execute main.js again so that it can attach the
// event listener to the form every time the body changes.
// Here you must use `jest.resetModules` because otherwise
// Jest will have cached `main.js` and it will _not_ run again.
jest.resetModules();
nock(API_ADDR)
.get("/inventory")
.replyWithError({ code: 500 });
await require("./main");
// You can only spy on `window.addEventListener` after `main.js`
// has been executed. Otherwise `detachPopstateHandlers` will
// also detach the handlers that `main.js` attached to the page.
jest.spyOn(window, "addEventListener");
});
afterEach(detachPopstateHandlers);
afterEach(() => {
if (!nock.isDone()) {
nock.cleanAll();
throw new Error("Not all mocked endpoints received requests.");
}
});
test("persists items between sessions", async () => {
nock(API_ADDR)
.post(/inventory\/.*$/)
.reply(200);
nock(API_ADDR)
.get("/inventory")
.replyWithError({ code: 500 });
const submitBtn = screen.getByText("Add to inventory");
const itemField = screen.getByPlaceholderText("Item name");
fireEvent.input(itemField, {
target: { value: "cheesecake" },
bubbles: true
});
const quantityField = screen.getByPlaceholderText("Quantity");
fireEvent.input(quantityField, { target: { value: "6" }, bubbles: true });
fireEvent.click(submitBtn);
const itemListBefore = document.getElementById("item-list");
expect(itemListBefore.childNodes).toHaveLength(1);
expect(
getByText(itemListBefore, "cheesecake - Quantity: 6")
).toBeInTheDocument();
// This is equivalent to reloading the page
document.body.innerHTML = initialHtml;
jest.resetModules();
await require("./main");
const itemListAfter = document.getElementById("item-list");
expect(itemListAfter.childNodes).toHaveLength(1);
expect(
getByText(itemListAfter, "cheesecake - Quantity: 6")
).toBeInTheDocument();
});
describe("adding items", () => {
test("updating the item list", () => {
nock(API_ADDR)
.post(/inventory\/.*$/)
.reply(200);
const submitBtn = screen.getByText("Add to inventory");
const itemField = screen.getByPlaceholderText("Item name");
fireEvent.input(itemField, {
target: { value: "cheesecake" },
bubbles: true
});
const quantityField = screen.getByPlaceholderText("Quantity");
fireEvent.input(quantityField, { target: { value: "6" }, bubbles: true });
fireEvent.click(submitBtn);
const itemList = document.getElementById("item-list");
expect(getByText(itemList, "cheesecake - Quantity: 6")).toBeInTheDocument();
});
test("sending a request to update the item list", () => {
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 6 }))
.reply(200);
const submitBtn = screen.getByText("Add to inventory");
const itemField = screen.getByPlaceholderText("Item name");
fireEvent.input(itemField, {
target: { value: "cheesecake" },
bubbles: true
});
const quantityField = screen.getByPlaceholderText("Quantity");
fireEvent.input(quantityField, { target: { value: "6" }, bubbles: true });
fireEvent.click(submitBtn);
if (!nock.isDone())
throw new Error("POST /inventory/cheesecake was not reached");
});
test("undo to one item", done => {
// You must specify the encoded URL here because
// nock struggles with encoded urls
nock(API_ADDR)
.post("/inventory/carrot%20cake")
.reply(200);
nock(API_ADDR)
.post("/inventory/cheesecake")
.reply(200);
const itemField = screen.getByPlaceholderText("Item name");
const quantityField = screen.getByPlaceholderText("Quantity");
const submitBtn = screen.getByText("Add to inventory");
fireEvent.input(itemField, {
target: { value: "cheesecake" },
bubbles: true
});
fireEvent.input(quantityField, { target: { value: "6" }, bubbles: true });
fireEvent.click(submitBtn);
fireEvent.input(itemField, {
target: { value: "carrot cake" },
bubbles: true
});
fireEvent.input(quantityField, { target: { value: "5" }, bubbles: true });
fireEvent.click(submitBtn);
window.addEventListener("popstate", () => {
const itemList = document.getElementById("item-list");
expect(itemList.children).toHaveLength(1);
expect(
getByText(itemList, "cheesecake - Quantity: 6")
).toBeInTheDocument();
done();
});
fireEvent.click(screen.getByText("Undo"));
});
test("undo to empty list", done => {
nock(API_ADDR)
.post(/inventory\/.*$/)
.reply(200);
const submitBtn = screen.getByText("Add to inventory");
const itemField = screen.getByPlaceholderText("Item name");
fireEvent.input(itemField, {
target: { value: "cheesecake" },
bubbles: true
});
const quantityField = screen.getByPlaceholderText("Quantity");
fireEvent.input(quantityField, { target: { value: "6" }, bubbles: true });
fireEvent.click(submitBtn);
expect(history.state).toEqual({ inventory: { cheesecake: 6 } });
window.addEventListener("popstate", () => {
const itemList = document.getElementById("item-list");
expect(itemList).toBeEmpty();
done();
});
fireEvent.click(screen.getByText("Undo"));
});
});
describe("item name validation", () => {
test("entering valid item names ", () => {
const itemField = screen.getByPlaceholderText("Item name");
fireEvent.input(itemField, {
target: { value: "cheesecake" },
bubbles: true
});
expect(screen.getByText("cheesecake is valid!")).toBeInTheDocument();
});
test("entering invalid item names ", () => {
const itemField = screen.getByPlaceholderText("Item name");
fireEvent.input(itemField, { target: { value: "book" }, bubbles: true });
expect(screen.getByText("book is not a valid item.")).toBeInTheDocument();
});
});
================================================
FILE: chapter11/client/package.json
================================================
{
"name": "1_http_requests",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "http-server ./",
"test": "jest",
"build": "browserify main.js -o bundle.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@testing-library/dom": "^7.2.2",
"@testing-library/jest-dom": "^5.5.0",
"browserify": "^16.5.1",
"http-server": "^0.12.1",
"isomorphic-fetch": "^2.2.1",
"jest": "^24.9.0",
"nock": "^12.0.3",
"socket.io": "^2.3.0"
},
"dependencies": {
"http-shutdown": "^1.2.2",
"socket.io-client": "^2.3.0"
}
}
================================================
FILE: chapter11/client/setupGlobalFetch.js
================================================
const fetch = require("isomorphic-fetch");
global.window.fetch = fetch;
================================================
FILE: chapter11/client/setupJestDom.js
================================================
const jestDom = require("@testing-library/jest-dom");
expect.extend(jestDom);
================================================
FILE: chapter11/client/socket.js
================================================
const { API_ADDR, data } = require("./inventoryController");
const { updateItemList } = require("./domController");
const client = { id: null };
const io = require("socket.io-client");
const handleAddItemMsg = ({ itemName, quantity }) => {
const currentQuantity = data.inventory[itemName] || 0;
data.inventory[itemName] = currentQuantity + quantity;
return updateItemList(data.inventory);
};
const connect = () => {
return new Promise(resolve => {
const socket = io(API_ADDR);
socket.on("connect", () => {
client.id = socket.id;
resolve(socket);
});
socket.on("add_item", handleAddItemMsg);
});
};
module.exports = { client, connect, handleAddItemMsg };
================================================
FILE: chapter11/client/socket.test.js
================================================
const nock = require("nock");
const fs = require("fs");
const initialHtml = fs.readFileSync("./index.html");
const { getByText } = require("@testing-library/dom");
const { data } = require("./inventoryController");
const { start, stop, sendMsg } = require("./testSocketServer");
const { handleAddItemMsg, connect } = require("./socket");
beforeEach(() => {
document.body.innerHTML = initialHtml;
});
beforeEach(() => {
data.inventory = {};
});
describe("handleAddItemMsg", () => {
test("updating the inventory and the item list", () => {
handleAddItemMsg({ itemName: "cheesecake", quantity: 6 });
expect(data.inventory).toEqual({ cheesecake: 6 });
const itemList = document.getElementById("item-list");
expect(itemList.childNodes).toHaveLength(1);
expect(getByText(itemList, "cheesecake - Quantity: 6")).toBeInTheDocument();
});
});
describe("handling real messages", () => {
beforeAll(start);
beforeAll(async () => {
nock.cleanAll();
await connect();
});
afterAll(stop);
test("handling add_item messages", async () => {
sendMsg("add_item", { itemName: "cheesecake", quantity: 6 });
await new Promise(resolve => setTimeout(resolve, 1000));
expect(data.inventory).toEqual({ cheesecake: 6 });
const itemList = document.getElementById("item-list");
expect(itemList.childNodes).toHaveLength(1);
expect(getByText(itemList, "cheesecake - Quantity: 6")).toBeInTheDocument();
});
});
================================================
FILE: chapter11/client/testSocketServer.js
================================================
const server = require("http").createServer();
const io = require("socket.io")(server);
const sendMsg = (msgType, content) => {
io.sockets.emit(msgType, content);
};
const start = () =>
new Promise(resolve => {
server.listen(3000, resolve);
});
const stop = () =>
new Promise(resolve => {
server.close(resolve);
});
module.exports = { start, stop, sendMsg };
================================================
FILE: chapter11/client/testUtils.js
================================================
const clearHistoryHook = done => {
const clearHistory = () => {
if (history.state === null) {
window.removeEventListener("popstate", clearHistory);
return done();
}
history.back();
};
window.addEventListener("popstate", clearHistory);
clearHistory();
};
const detachPopstateHandlers = () => {
const popstateListeners = window.addEventListener.mock.calls.filter(
([eventName]) => {
return eventName === "popstate";
}
);
popstateListeners.forEach(([eventName, handlerFn]) => {
window.removeEventListener(eventName, handlerFn);
});
jest.restoreAllMocks();
};
module.exports = { clearHistoryHook, detachPopstateHandlers };
================================================
FILE: chapter11/server/README.md
================================================
# Chapter 5 Server
To 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.
In case you want to update the back-end from Chapter 4 yourself, here's the list of changes I've done:
- For the server to accept the requests coming from the client, you'll need to use [`@koa/cors`](https://github.com/koajs/cors)
- 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.
- 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.
- At `GET /inventory` I have added a route which lists all items in the inventory.
- 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
- I've used `koa-socket-2` to add support for `socket.io`
- The `POST /inventory/:itemName` will now push updates to all clients but the one which added an item.
================================================
FILE: chapter11/server/authenticationController.js
================================================
const crypto = require("crypto");
const { db } = require("./dbConnection");
const hashPassword = password => {
const hash = crypto.createHash("sha256");
hash.update(password);
return hash.digest("hex");
};
const credentialsAreValid = async (username, password) => {
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) return false;
return hashPassword(password) === user.passwordHash;
};
const authenticationMiddleware = async (ctx, next) => {
try {
const authHeader = ctx.request.headers.authorization;
const credentials = Buffer.from(
authHeader.slice("basic".length + 1),
"base64"
).toString();
const [username, password] = credentials.split(":");
const validCredentialsSent = await credentialsAreValid(username, password);
if (!validCredentialsSent) throw new Error("invalid credentials");
} catch (e) {
ctx.status = 401;
ctx.body = { message: "please provide valid credentials" };
return;
}
await next();
};
module.exports = {
hashPassword,
credentialsAreValid,
authenticationMiddleware
};
================================================
FILE: chapter11/server/authenticationController.test.js
================================================
const crypto = require("crypto");
const {
hashPassword,
credentialsAreValid,
authenticationMiddleware
} = require("./authenticationController");
const { user: globalUser } = require("./userTestUtils");
describe("hashPassword", () => {
test("hashing passwords", () => {
const plainTextPassword = "password_example";
const hash = crypto.createHash("sha256");
hash.update(plainTextPassword);
const expectedHash = hash.digest("hex");
expect(hashPassword(plainTextPassword)).toBe(expectedHash);
});
});
describe("credentialsAreValid", () => {
test("validating credentials", async () => {
expect(await credentialsAreValid(globalUser.username, "a_password")).toBe(
true
);
});
});
describe("authenticationMiddleware", () => {
test("returning an error if the credentials are not valid", async () => {
const fakeAuth = Buffer.from("invalid:credentials").toString("base64");
const ctx = {
request: {
headers: { authorization: `Basic ${fakeAuth}` }
}
};
const next = jest.fn();
await authenticationMiddleware(ctx, next);
expect(next.mock.calls).toHaveLength(0);
expect(ctx).toEqual({
...ctx,
status: 401,
body: { message: "please provide valid credentials" }
});
});
test("authenticating properly", async () => {
const ctx = {
request: {
headers: { authorization: globalUser.authHeader }
}
};
const next = jest.fn();
await authenticationMiddleware(ctx, next);
expect(next.mock.calls).toHaveLength(1);
});
});
================================================
FILE: chapter11/server/cartController.js
================================================
const { db } = require("./dbConnection");
const { removeFromInventory } = require("./inventoryController");
const logger = require("./logger");
const addItemToCart = async (username, itemName) => {
await removeFromInventory(itemName);
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) {
const userNotFound = new Error("user not found");
userNotFound.code = 404;
}
const itemEntry = await db
.select()
.from("carts_items")
.where({ userId: user.id, itemName })
.first();
if (itemEntry && itemEntry.quantity + 1 > 3) {
const limitError = new Error(
"You can't have more than three units of an item in your cart"
);
limitError.code = 400;
throw limitError;
}
if (itemEntry) {
await db("carts_items")
.increment("quantity")
.update({ updatedAt: new Date().toISOString() })
.where({
userId: itemEntry.userId,
itemName
});
} else {
await db("carts_items").insert({
userId: user.id,
itemName,
quantity: 1,
updatedAt: new Date().toISOString()
});
}
logger.log(`${itemName} added to ${username}'s cart`);
return db
.select("itemName", "quantity")
.from("carts_items")
.where({ userId: user.id });
};
const hoursInMs = n => 1000 * 60 * 60 * n;
const removeStaleItems = async () => {
const fourHoursAgo = new Date(Date.now() - hoursInMs(4)).toISOString();
const staleItems = await db
.select()
.from("carts_items")
.where("updatedAt", "<", fourHoursAgo);
if (staleItems.length === 0) return;
// Put stale items back in the inventory
const inventoryUpdates = staleItems.map(staleItem =>
db("inventory")
.increment("quantity", staleItem.quantity)
.where({ itemName: staleItem.itemName })
);
await Promise.all(inventoryUpdates);
// Delete stale items from cart
const staleItemTuples = staleItems.map(i => [i.itemName, i.userId]);
await db("carts_items")
.del()
.whereIn(["itemName", "userId"], staleItemTuples);
};
const monitorStaleItems = () => setInterval(removeStaleItems, hoursInMs(2));
module.exports = { addItemToCart, monitorStaleItems };
================================================
FILE: chapter11/server/cartController.test.js
================================================
const { db } = require("./dbConnection");
const { addItemToCart, monitorStaleItems } = require("./cartController");
const { hashPassword } = require("./authenticationController");
const { user: globalUser } = require("./userTestUtils");
const FakeTimers = require("@sinonjs/fake-timers");
const fs = require("fs");
describe("addItemToCart", () => {
beforeEach(() => {
fs.writeFileSync("/tmp/logs.out", "");
});
test("adding unavailable items to cart", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 0 });
try {
await addItemToCart(globalUser.username, "cheesecake");
} catch (e) {
const expectedError = new Error("cheesecake is unavailable");
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
const finalCartContent = await db
.select("carts_items.*")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([]);
expect.assertions(2);
});
test("adding items above limit to cart", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 1 });
await db("carts_items").insert({
userId: globalUser.id,
itemName: "cheesecake",
quantity: 3
});
try {
await addItemToCart(globalUser.username, "cheesecake");
} catch (e) {
const expectedError = new Error(
"You can't have more than three units of an item in your cart"
);
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([{ itemName: "cheesecake", quantity: 3 }]);
expect.assertions(2);
});
test("logging added items", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 1 });
await db("carts_items").insert({
userId: globalUser.id,
itemName: "cheesecake",
quantity: 1
});
await addItemToCart(globalUser.username, "cheesecake");
const logs = fs.readFileSync("/tmp/logs.out", "utf-8");
expect(logs).toContain(
`cheesecake added to ${globalUser.username}'s cart\n`
);
});
});
const withRetries = async fn => {
// Capture the assertion error since Jest does not export it
const JestAssertionError = (() => {
try {
expect(false).toBe(true);
} catch (e) {
return e.constructor;
}
})();
try {
await fn();
} catch (e) {
if (e.constructor === JestAssertionError) {
// Wait 100ms before retrying
await new Promise(resolve => setTimeout(resolve, 100));
await withRetries(fn);
} else {
throw e;
}
}
};
describe("timers", () => {
const hoursInMs = n => 1000 * 60 * 60 * n;
let clock;
beforeEach(() => {
clock = FakeTimers.install({ toFake: ["Date", "setInterval"] });
});
afterEach(() => {
clock = clock.uninstall();
});
test("removing stale items", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 1 });
await addItemToCart(globalUser.username, "cheesecake");
clock.tick(hoursInMs(4));
timer = monitorStaleItems();
clock.tick(hoursInMs(2));
await withRetries(async () => {
const finalCartContent = await db
.select()
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([]);
});
await withRetries(async () => {
const inventoryContent = await db
.select("itemName", "quantity")
.from("inventory");
expect(inventoryContent).toEqual([
{ itemName: "cheesecake", quantity: 1 }
]);
});
});
});
================================================
FILE: chapter11/server/dbConnection.js
================================================
const environmentName = process.env.NODE_ENV;
const db = require("knex")(require("./knexfile")[environmentName]);
const closeConnection = () => db.destroy();
module.exports = {
db,
closeConnection
};
================================================
FILE: chapter11/server/disconnectFromDb.js
================================================
const { db } = require("./dbConnection");
afterAll(() => db.destroy());
================================================
FILE: chapter11/server/inventoryController.js
================================================
const { db } = require("./dbConnection");
const removeFromInventory = async itemName => {
const inventoryEntry = await db
.select()
.from("inventory")
.where({ itemName })
.first();
if (!inventoryEntry || inventoryEntry.quantity === 0) {
const err = new Error(`${itemName} is unavailable`);
err.code = 400;
throw err;
}
await db("inventory")
.decrement("quantity")
.where({ itemName });
};
module.exports = { removeFromInventory };
================================================
FILE: chapter11/server/jest.config.js
================================================
module.exports = {
testEnvironment: "node",
globalSetup: "./migrateDatabases.js",
setupFilesAfterEnv: [
"/truncateTables.js",
"/seedUser.js",
"/disconnectFromDb.js"
]
};
================================================
FILE: chapter11/server/knexfile.js
================================================
module.exports = {
test: {
client: "sqlite3",
connection: { filename: "./test.sqlite" },
useNullAsDefault: true
},
development: {
client: "sqlite3",
connection: { filename: "./dev.sqlite" },
useNullAsDefault: true
}
};
================================================
FILE: chapter11/server/logger.js
================================================
const fs = require("fs");
const logger = {
log: msg => fs.appendFileSync("/tmp/logs.out", msg + "\n")
};
module.exports = logger;
================================================
FILE: chapter11/server/migrateDatabases.js
================================================
const environmentName = process.env.NODE_ENV || "test";
const environmentConfig = require("./knexfile")[environmentName];
const db = require("knex")(environmentConfig);
module.exports = async () => {
// Migrate the database to the latest state
await db.migrate.latest();
// Close the connection to the database so that tests won't hang
await db.destroy();
};
================================================
FILE: chapter11/server/migrations/20200325082401_initial_schema.js
================================================
exports.up = async knex => {
await knex.schema.createTable("users", table => {
table.increments("id");
table.string("username");
table.unique("username");
table.string("email");
table.string("passwordHash");
});
await knex.schema.createTable("carts_items", table => {
table.integer("userId").references("users.id");
table.string("itemName");
table.unique("itemName");
table.integer("quantity");
});
await knex.schema.createTable("inventory", table => {
table.increments("id");
table.string("itemName");
table.unique("itemName");
table.integer("quantity");
});
};
exports.down = async knex => {
await knex.schema.dropTable("users");
await knex.schema.dropTable("carts_items");
await knex.schema.dropTable("inventory");
};
================================================
FILE: chapter11/server/migrations/20200331210311_updatedAt_field.js
================================================
exports.up = knex => {
return knex.schema.alterTable("carts_items", table => {
table.timestamp("updatedAt");
});
};
exports.down = knex => {
return knex.schema.alterTable("carts_items", table => {
table.dropColumn("updatedAt");
});
};
================================================
FILE: chapter11/server/package.json
================================================
{
"name": "chapter5_server",
"version": "1.0.0",
"scripts": {
"test": "jest --runInBand",
"start": "cross-env NODE_ENV=development node server.js",
"migrate:dev": "knex migrate:latest --env development",
"seed:dev": "knex seed:run"
},
"devDependencies": {
"@sinonjs/fake-timers": "github:sinonjs/fake-timers",
"jest": "^24.9.0",
"supertest": "^4.0.2"
},
"dependencies": {
"@koa/cors": "^3.0.0",
"cross-env": "^7.0.2",
"isomorphic-fetch": "^2.2.1",
"knex": "^0.20.13",
"koa": "^2.11.0",
"koa-body-parser": "^1.1.2",
"koa-router": "^7.4.0",
"koa-socket-2": "^1.2.0",
"nock": "^12.0.3",
"socket.io": "^2.3.0",
"sqlite3": "^4.1.1"
},
"main": "alertController.spec.js",
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}
================================================
FILE: chapter11/server/seedUser.js
================================================
const { createUser } = require("./userTestUtils");
beforeEach(createUser);
================================================
FILE: chapter11/server/seeds/initial_inventory.js
================================================
exports.seed = async knex => {
await knex("inventory").del();
return knex("inventory").insert([
{ itemName: "cheesecake", quantity: 8 },
{ itemName: "apple pie", quantity: 2 },
{ itemName: "carrot cake", quantity: 5 }
]);
};
================================================
FILE: chapter11/server/server.js
================================================
const fetch = require("isomorphic-fetch");
const Koa = require("koa");
const http = require("http");
const IO = require("koa-socket-2");
const cors = require("@koa/cors");
const Router = require("koa-router");
const bodyParser = require("koa-body-parser");
const { db } = require("./dbConnection");
const { addItemToCart } = require("./cartController");
const {
hashPassword,
authenticationMiddleware
} = require("./authenticationController");
const PORT = process.env.NODE_ENV === "test" ? 5000 : 3000;
const app = new Koa();
const io = new IO();
io.attach(app);
const router = new Router();
app.use(cors());
app.use(bodyParser());
app.use(async (ctx, next) => {
if (ctx.url.startsWith("/carts")) {
return await authenticationMiddleware(ctx, next);
}
await next();
});
router.put("/users/:username", async ctx => {
const { username } = ctx.params;
const { email, password } = ctx.request.body;
const userAlreadyExists = await db
.select()
.from("users")
.where({ username })
.first();
if (userAlreadyExists) {
ctx.body = { message: `${username} already exists` };
ctx.status = 409;
return;
}
await db("users").insert({
username,
email,
passwordHash: hashPassword(password)
});
return (ctx.body = { message: `${username} created successfully` });
});
router.post("/carts/:username/items", async ctx => {
const { username } = ctx.params;
const { item, quantity } = ctx.request.body;
for (let i = 0; i < quantity; i++) {
try {
const newItems = await addItemToCart(username, item);
ctx.body = newItems;
} catch (e) {
ctx.body = { message: e.message };
ctx.status = e.code;
return;
}
}
});
router.delete("/carts/:username/items/:item", async ctx => {
const { username, item } = ctx.params;
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) {
ctx.body = { message: "user not found" };
ctx.status = 404;
return;
}
const itemEntry = await db
.select()
.from("carts_items")
.where({ userId: user.id, itemName: item })
.first();
if (!itemEntry || itemEntry.quantity === 0) {
ctx.body = { message: `${item} is not in the cart` };
ctx.status = 400;
return;
}
await db("carts_items")
.decrement("quantity")
.where({ userId: user.id, itemName: item });
const inventoryEntry = await db
.select()
.from("inventory")
.where({ itemName: item })
.first();
if (inventoryEntry) {
await db("inventory")
.increment("quantity")
.where({ userId: itemEntry.userId, itemName: item });
} else {
await db("inventory").insert({ itemName: item, quantity: 1 });
}
ctx.body = await db
.select("itemName", "quantity")
.from("carts_items")
.where({ userId: user.id });
});
router.post("/inventory/:itemName", async ctx => {
const { itemName } = ctx.params;
const { quantity } = ctx.request.body;
const clientId = ctx.request.headers["x-socket-client-id"];
const current = await db
.select("itemName", "quantity")
.from("inventory")
.where({ itemName })
.first();
const itemExists = current && current.quantity > 0;
const newRecord = {
itemName,
quantity: (itemExists ? current.quantity : 0) + quantity
};
if (current) {
await db("inventory")
.increment("quantity", quantity)
.where({ itemName });
} else {
await db("inventory").insert(newRecord);
}
Object.entries(io.socket.sockets.connected).forEach(([id, socket]) => {
if (id === clientId) return;
socket.emit("add_item", { itemName, quantity });
});
ctx.body = newRecord;
});
router.delete("/inventory/:itemName", async ctx => {
const { itemName } = ctx.params;
const { quantity } = ctx.request.body;
const current = await db
.select("itemName", "quantity")
.from("inventory")
.where({ itemName })
.first();
const canDelete = current && current.quantity > quantity;
if (canDelete) {
await db("inventory")
.decrement("quantity", quantity)
.where({ itemName });
ctx.body = { message: `Removed ${quantity} units of ${itemName}` };
} else {
ctx.status = 404;
ctx.body = {
message: `There aren't ${quantity} units of ${itemName} available.`
};
}
});
router.get("/inventory", async ctx => {
const inventoryContent = await db
.select("itemName", "quantity")
.from("inventory")
.where("quantity", ">", 0)
.orderBy("quantity", "desc");
ctx.body = inventoryContent.reduce((acc, { itemName, quantity }) => {
return { ...acc, [itemName]: quantity };
}, {});
});
router.get("/inventory/:itemName", async ctx => {
const { itemName } = ctx.params;
const response = await fetch(`http://recipepuppy.com/api?i=${itemName}`);
const { title, href, results: recipes } = await response.json();
const inventoryItem = await db
.select()
.from("inventory")
.where({ itemName })
.first();
ctx.body = {
...inventoryItem,
info: `Data obtained from ${title} - ${href}`,
recipes
};
});
app.use(router.routes());
module.exports = { app: app.listen(PORT, "127.0.0.1") };
================================================
FILE: chapter11/server/server.test.js
================================================
const { user: globalUser } = require("./userTestUtils");
const { db } = require("./dbConnection");
const request = require("supertest");
const { app } = require("./server.js");
const { hashPassword } = require("./authenticationController.js");
const nock = require("nock");
afterAll(() => app.close());
describe("add items to a cart", () => {
test("adding available items", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 3 });
const response = await request(app)
.post(`/carts/${globalUser.username}/items`)
.set("authorization", globalUser.authHeader)
.send({ item: "cheesecake", quantity: 3 })
.expect(200)
.expect("Content-Type", /json/);
const newItems = [{ itemName: "cheesecake", quantity: 3 }];
expect(response.body).toEqual(newItems);
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(0);
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual(newItems);
});
test("adding unavailable items", async () => {
const response = await request(app)
.post(`/carts/${globalUser.username}/items`)
.set("authorization", globalUser.authHeader)
.send({ item: "cheesecake", quantity: 1 })
.expect(400)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "cheesecake is unavailable"
});
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([]);
});
});
describe("removing items from a cart", () => {
test("removing existing items", async () => {
await db("carts_items").insert({
userId: globalUser.id,
itemName: "cheesecake",
quantity: 1
});
const response = await request(app)
.del(`/carts/${globalUser.username}/items/cheesecake`)
.set("authorization", globalUser.authHeader)
.expect(200)
.expect("Content-Type", /json/);
const expectedFinalContent = [{ itemName: "cheesecake", quantity: 0 }];
expect(response.body).toEqual(expectedFinalContent);
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual(expectedFinalContent);
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(1);
});
test("removing non-existing items", async () => {
await db("inventory").insert({
itemName: "cheesecake",
quantity: 0
});
const response = await request(app)
.del(`/carts/${globalUser.username}/items/cheesecake`)
.set("authorization", globalUser.authHeader)
.expect(400)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "cheesecake is not in the cart"
});
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(0);
});
});
describe("create accounts", () => {
test("creating a new account", async () => {
const response = await request(app)
.put("/users/another_user")
.send({ email: "another_user@example.org", password: "a_password" })
.expect(200)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "another_user created successfully"
});
const savedUser = await db
.select("email", "passwordHash")
.from("users")
.where({ username: "another_user" })
.first();
expect(savedUser).toEqual({
email: "another_user@example.org",
passwordHash: hashPassword("a_password")
});
});
test("creating a duplicate account", async () => {
const response = await request(app)
.put(`/users/${globalUser.username}`)
.send({ email: globalUser.email, password: "a_password" })
.expect(409)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: `${globalUser.username} already exists`
});
});
});
describe("list inventory items", () => {
const eggs = { itemName: "eggs", quantity: 3 };
const applePie = { itemName: "apple pie", quantity: 1 };
const carrotCake = { itemName: "carrot cake", quantity: 0 };
beforeEach(async () => {
await db("inventory").insert([eggs, applePie, carrotCake]);
});
test("fetching all available items", async () => {
const { body } = await request(app)
.get("/inventory")
.expect(200)
.expect("Content-Type", /json/);
expect(body).toEqual({ eggs: 3, "apple pie": 1 });
});
});
describe("add inventory items", () => {
test("adding a new item", async () => {
const { body } = await request(app)
.post("/inventory/eggs")
.send({ quantity: 3 })
.expect(200)
.expect("Content-Type", /json/);
expect(body).toEqual({ itemName: "eggs", quantity: 3 });
expect(
await db
.select("itemName", "quantity")
.from("inventory")
.where("itemName", "eggs")
.first()
).toEqual({ itemName: "eggs", quantity: 3 });
});
test("adding an existing item", async () => {
const eggs = { itemName: "eggs", quantity: 2 };
await db("inventory").insert(eggs);
const { body } = await request(app)
.post("/inventory/eggs")
.send({ quantity: 3 })
.expect(200)
.expect("Content-Type", /json/);
expect(body).toEqual({ itemName: "eggs", quantity: 5 });
expect(
await db
.select("itemName", "quantity")
.from("inventory")
.where("itemName", "eggs")
.first()
).toEqual({ itemName: "eggs", quantity: 5 });
});
});
describe("remove inventory items", () => {
beforeEach(async () => {
await db("inventory").insert({ itemName: "eggs", quantity: 3 });
});
test("removing an item", async () => {
const { body } = await request(app)
.del("/inventory/eggs")
.send({ quantity: 2 })
.expect(200)
.expect("Content-Type", /json/);
expect(body).toEqual({
message: "Removed 2 units of eggs"
});
expect(
await db
.select("itemName", "quantity")
.from("inventory")
.where("itemName", "eggs")
.first()
).toEqual({ itemName: "eggs", quantity: 1 });
});
test("removing more than the inventory quantity", async () => {
const { body } = await request(app)
.del("/inventory/eggs")
.send({ quantity: 4 })
.expect(404)
.expect("Content-Type", /json/);
expect(body).toEqual({
message: "There aren't 4 units of eggs available."
});
expect(
await db
.select("itemName", "quantity")
.from("inventory")
.where("itemName", "eggs")
.first()
).toEqual({ itemName: "eggs", quantity: 3 });
});
});
describe("fetch inventory items", () => {
const eggs = { itemName: "eggs", quantity: 3 };
const applePie = { itemName: "apple pie", quantity: 1 };
beforeEach(async () => {
await db("inventory").insert([eggs, applePie]);
const { id: eggsId } = await db
.select()
.from("inventory")
.where({ itemName: "eggs" })
.first();
eggs.id = eggsId;
});
test("fetching an item from the inventory", async () => {
const eggsResponse = {
title: "FakeAPI",
href: "example.org",
results: [{ name: "Omelette du Fromage" }]
};
nock("http://recipepuppy.com")
.get("/api")
.query({ i: "eggs" })
.reply(200, eggsResponse);
const response = await request(app)
.get(`/inventory/eggs`)
.expect(200)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
...eggs,
info: `Data obtained from ${eggsResponse.title} - ${eggsResponse.href}`,
recipes: eggsResponse.results
});
});
});
================================================
FILE: chapter11/server/truncateTables.js
================================================
const { db } = require("./dbConnection");
const tablesToTruncate = ["users", "inventory", "carts_items"];
beforeEach(() => {
return Promise.all(tablesToTruncate.map(t => db(t).truncate()));
});
================================================
FILE: chapter11/server/userTestUtils.js
================================================
const { db } = require("./dbConnection");
const { hashPassword } = require("./authenticationController");
const username = "test_user";
const password = "a_password";
const passwordHash = hashPassword(password);
const email = "test_user@example.org";
const validAuth = Buffer.from(`${username}:${password}`).toString("base64");
const authHeader = `Basic ${validAuth}`;
const user = {
username,
password,
email,
authHeader
};
const createUser = async () => {
await db("users").insert({ username, email, passwordHash });
const { id } = await db
.select()
.from("users")
.where({ username })
.first();
user.id = id;
};
module.exports = { user, createUser };
================================================
FILE: chapter13/1_type_systems/1_no_types/orderQueue.js
================================================
const state = {
deliveries: []
};
const addToDeliveryQueue = order => {
if (order.status !== "done") {
throw new Error("Can't add unfinished orders to the delivery queue.");
}
state.deliveries.push(order);
};
module.exports = { state, addToDeliveryQueue };
================================================
FILE: chapter13/1_type_systems/1_no_types/orderQueue.spec.js
================================================
const { state, addToDeliveryQueue } = require("./orderQueue");
test("adding unfinished orders to the queue", () => {
state.deliveries = [];
const newOrder = {
items: ["cheesecake"],
status: "in progress"
};
expect(() => addToDeliveryQueue(newOrder)).toThrow();
expect(state.deliveries).toEqual([]);
});
================================================
FILE: chapter13/1_type_systems/1_no_types/package.json
================================================
{
"name": "1_no_types",
"version": "1.0.0",
"description": "",
"main": "orderQueue.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^26.6.0"
}
}
================================================
FILE: chapter13/1_type_systems/2_with_types/orderQueue.js
================================================
"use strict";
exports.__esModule = true;
exports.addToDeliveryQueue = exports.state = void 0;
exports.state = {
deliveries: []
};
var addToDeliveryQueue = function(order) {
exports.state.deliveries.push(order);
};
exports.addToDeliveryQueue = addToDeliveryQueue;
================================================
FILE: chapter13/1_type_systems/2_with_types/orderQueue.spec.js
================================================
"use strict";
exports.__esModule = true;
var orderQueue_1 = require("./orderQueue");
test("adding finished items to the queue", function() {
orderQueue_1.state.deliveries = [];
var newOrder = {
items: ["cheesecake"],
status: "done"
};
orderQueue_1.addToDeliveryQueue(newOrder);
expect(orderQueue_1.state.deliveries).toEqual([newOrder]);
});
================================================
FILE: chapter13/1_type_systems/2_with_types/orderQueue.spec.ts
================================================
import { state, addToDeliveryQueue, DoneOrder } from "./orderQueue";
test("adding finished items to the queue", () => {
state.deliveries = [];
const newOrder: DoneOrder = {
items: ["cheesecake"],
status: "done"
};
addToDeliveryQueue(newOrder);
expect(state.deliveries).toEqual([newOrder]);
});
================================================
FILE: chapter13/1_type_systems/2_with_types/orderQueue.ts
================================================
type OrderItems = { 0: string } & Array;
type Order = {
status: "in progress" | "done";
items: OrderItems;
};
export type DoneOrder = Order & { status: "done" };
export const state: { deliveries: Array } = {
deliveries: []
};
export const addToDeliveryQueue = (order: DoneOrder) => {
state.deliveries.push(order);
};
================================================
FILE: chapter13/1_type_systems/2_with_types/package.json
================================================
{
"name": "1_no_types",
"version": "1.0.0",
"description": "",
"main": "orderQueue.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/jest": "^26.0.15",
"jest": "^26.6.0"
},
"dependencies": {
"typescript": "^4.1.2"
}
}
================================================
FILE: chapter13/1_type_systems/2_with_types/tsconfig.json
================================================
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
// "outDir": "./", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true /* Skip type checking of declaration files. */,
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
}
}
================================================
FILE: chapter2/2_unit_tests/1_raw_tests/Cart.js
================================================
class Cart {
constructor() {
this.items = [];
}
addToCart(item) {
this.items.push(item);
}
}
module.exports = Cart;
================================================
FILE: chapter2/2_unit_tests/1_raw_tests/Cart.test.js
================================================
const Cart = require("./Cart.js");
const cart = new Cart();
cart.addToCart("cheesecake");
const hasOneItem = cart.items.length === 1;
const hasACheesecake = cart.items[0] === "cheesecake";
if (hasOneItem && hasACheesecake) {
console.log("The addToCart function can add an item to the cart");
} else {
const actualContent = cart.items.join(", ");
console.error("The addToCart function didn't do what we expect!");
console.error(`Here is the actual content of the cart: ${actualContent}`);
throw new Error("Test failed!");
}
================================================
FILE: chapter2/2_unit_tests/2_node_assert/Cart.js
================================================
class Cart {
constructor() {
this.items = [];
}
addToCart(item) {
this.items.push(item);
}
}
module.exports = Cart;
================================================
FILE: chapter2/2_unit_tests/2_node_assert/Cart.test.js
================================================
const assert = require("assert");
const Cart = require("./Cart.js");
const cart = new Cart();
cart.addToCart("cheesecake");
assert.deepStrictEqual(cart.items, ["cheesecake"]);
console.log("The addToCart function can add an item to the cart");
================================================
FILE: chapter2/2_unit_tests/3_jest_multiple_tests/Cart.js
================================================
class Cart {
constructor() {
this.items = [];
}
addToCart(item) {
this.items.push(item);
}
removeFromCart(item) {
for (let i = 0; i < this.items.length; i++) {
const currentItem = this.items[i];
if (currentItem === item) {
this.items.splice(i, 1);
}
}
}
}
module.exports = Cart;
================================================
FILE: chapter2/2_unit_tests/3_jest_multiple_tests/Cart.test.js
================================================
const assert = require("assert");
const Cart = require("./Cart.js");
test("The addToCart function can add an item to the cart", () => {
const cart = new Cart();
cart.addToCart("cheesecake");
assert.deepStrictEqual(cart.items, ["cheesecake"]);
});
test("The addToCart function can add an item to the cart", () => {
const cart = new Cart();
cart.addToCart("cheesecake");
cart.removeFromCart("cheesecake");
assert.deepStrictEqual(cart.items, []);
});
================================================
FILE: chapter2/2_unit_tests/3_jest_multiple_tests/package.json
================================================
{
"name": "3_jest_multiple_tests",
"version": "1.0.0",
"description": "",
"main": "Cart.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
================================================
FILE: chapter2/2_unit_tests/4_jest_assertions/Cart.js
================================================
class Cart {
constructor() {
this.items = [];
}
addToCart(item) {
this.items.push(item);
}
removeFromCart(item) {
for (let i = 0; i < this.items.length; i++) {
const currentItem = this.items[i];
if (currentItem === item) {
this.items.splice(i, 1);
}
}
}
}
module.exports = Cart;
================================================
FILE: chapter2/2_unit_tests/4_jest_assertions/Cart.test.js
================================================
const Cart = require("./Cart.js");
test("The addToCart function can add an item to the cart", () => {
const cart = new Cart();
cart.addToCart("cheesecake");
expect(cart.items).toEqual(["cheesecake"]);
});
test("The removeFromCart function can remove an item from the cart", () => {
const cart = new Cart();
cart.addToCart("cheesecake");
cart.removeFromCart("cheesecake");
expect(cart.items).toEqual([]);
});
================================================
FILE: chapter2/2_unit_tests/4_jest_assertions/package.json
================================================
{
"name": "4_jest_assertions",
"version": "1.0.0",
"description": "",
"main": "Cart.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
================================================
FILE: chapter2/2_unit_tests/5_npm_scripts/Cart.js
================================================
class Cart {
constructor() {
this.items = [];
}
addToCart(item) {
this.items.push(item);
}
removeFromCart(item) {
for (let i = 0; i < this.items.length; i++) {
const currentItem = this.items[i];
if (currentItem === item) {
this.items.splice(i, 1);
}
}
}
}
module.exports = Cart;
================================================
FILE: chapter2/2_unit_tests/5_npm_scripts/Cart.test.js
================================================
const Cart = require("./Cart.js");
test("The addToCart function can add an item to the cart", () => {
const cart = new Cart();
cart.addToCart("cheesecake");
expect(cart.items).toEqual(["cheesecake"]);
});
test("The addToCart function can add an item to the cart", () => {
const cart = new Cart();
cart.addToCart("cheesecake");
cart.removeFromCart("cheesecake");
expect(cart.items).toEqual([]);
});
================================================
FILE: chapter2/2_unit_tests/5_npm_scripts/package.json
================================================
{
"name": "5_global_jest",
"version": "1.0.0",
"scripts": {
"test": "jest"
},
"devDependencies": {
"jest": "^24.9.0"
}
}
================================================
FILE: chapter2/3_integration_tests/1_knex_tests_promise/cart.js
================================================
const { db } = require("./dbConnection");
const createCart = username => {
return db("carts").insert({ username });
};
const addItem = (cartId, itemName) => {
return db("carts_items").insert({ cartId, itemName });
};
module.exports = {
createCart,
addItem
};
================================================
FILE: chapter2/3_integration_tests/1_knex_tests_promise/cart.test.js
================================================
const { db, closeConnection } = require("./dbConnection");
const { createCart } = require("./cart");
test("createCart creates a cart for a username", async () => {
await db("carts").truncate();
await createCart("Lucas da Costa");
const result = await db.select("username").from("carts");
expect(result).toEqual([{ username: "Lucas da Costa" }]);
await closeConnection();
});
================================================
FILE: chapter2/3_integration_tests/1_knex_tests_promise/dbConnection.js
================================================
const db = require("knex")(require("./knexfile").development);
const closeConnection = () => db.destroy();
module.exports = {
db,
closeConnection
};
================================================
FILE: chapter2/3_integration_tests/1_knex_tests_promise/knexfile.js
================================================
module.exports = {
development: {
client: "sqlite3",
connection: { filename: "./dev.sqlite" },
useNullAsDefault: true
}
};
================================================
FILE: chapter2/3_integration_tests/1_knex_tests_promise/migrations/20191230210750_create_carts.js
================================================
exports.up = async knex => {
await knex.schema.createTable("carts", table => {
table.increments("id");
table.string("username");
});
await knex.schema.createTable("carts_items", table => {
table.integer("cartId").references("carts.id");
table.string("itemName");
});
};
exports.down = async knex => {
await knex.schema.dropTable("carts");
await knex.schema.dropTable("carts_items");
};
================================================
FILE: chapter2/3_integration_tests/1_knex_tests_promise/package.json
================================================
{
"name": "1_knex_tests_promise",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"knex": "^0.20.6",
"sqlite3": "^4.1.1"
},
"devDependencies": {
"jest": "^24.9.0"
}
}
================================================
FILE: chapter2/3_integration_tests/2_knex_tests_done_cb/cart.js
================================================
const { db } = require("./dbConnection");
const createCart = username => {
return db("carts").insert({ username });
};
const addItem = (cartId, itemName) => {
return db("carts_items").insert({ cartId, itemName });
};
module.exports = {
createCart,
addItem
};
================================================
FILE: chapter2/3_integration_tests/2_knex_tests_done_cb/cart.test.js
================================================
const { db, closeConnection } = require("./dbConnection");
const { createCart } = require("./cart");
test("createCart creates a cart for a username", done => {
db("carts")
.truncate()
.then(() => createCart("Lucas da Costa"))
.then(() => db.select("username").from("carts"))
.then(result => {
expect(result).toEqual([{ username: "Lucas da Costa" }]);
})
.then(closeConnection)
.then(done);
});
================================================
FILE: chapter2/3_integration_tests/2_knex_tests_done_cb/dbConnection.js
================================================
const db = require("knex")(require("./knexfile").development);
const closeConnection = () => db.destroy();
module.exports = {
db,
closeConnection
};
================================================
FILE: chapter2/3_integration_tests/2_knex_tests_done_cb/knexfile.js
================================================
module.exports = {
development: {
client: "sqlite3",
connection: { filename: "./dev.sqlite" },
useNullAsDefault: true
}
};
================================================
FILE: chapter2/3_integration_tests/2_knex_tests_done_cb/migrations/20191230210750_create_carts.js
================================================
exports.up = async knex => {
await knex.schema.createTable("carts", table => {
table.increments("id");
table.string("username");
});
await knex.schema.createTable("carts_items", table => {
table.integer("cartId").references("carts.id");
table.string("itemName");
});
};
exports.down = async knex => {
await knex.schema.dropTable("carts");
await knex.schema.dropTable("carts_items");
};
================================================
FILE: chapter2/3_integration_tests/2_knex_tests_done_cb/package.json
================================================
{
"name": "2_knex_tests_done_cb",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"knex": "^0.20.6",
"sqlite3": "^4.1.1"
},
"devDependencies": {
"jest": "^24.9.0"
}
}
================================================
FILE: chapter2/3_integration_tests/3_knex_tests_hooks/cart.js
================================================
const { db } = require("./dbConnection");
const createCart = username => {
return db("carts").insert({ username });
};
const addItem = (cartId, itemName) => {
return db("carts_items").insert({ cartId, itemName });
};
module.exports = {
createCart,
addItem
};
================================================
FILE: chapter2/3_integration_tests/3_knex_tests_hooks/cart.test.js
================================================
const { db, closeConnection } = require("./dbConnection");
const { createCart, addItem } = require("./cart");
beforeEach(async () => {
await db("carts_items").truncate();
await db("carts").truncate();
});
afterAll(async () => await closeConnection());
test("createCart creates a cart for a username", async () => {
await createCart("Lucas da Costa");
const result = await db.select("username").from("carts");
expect(result).toEqual([{ username: "Lucas da Costa" }]);
});
test("addItem adds an item to the cart", async () => {
const username = "Lucas da Costa";
await createCart(username);
const { id: cartId } = await db
.select()
.from("carts")
.where({ username });
await addItem(cartId, "cheesecake");
const result = await db.select("itemName").from("carts_items");
expect(result).toEqual([{ cartId, itemName: "cheesecake" }]);
});
================================================
FILE: chapter2/3_integration_tests/3_knex_tests_hooks/dbConnection.js
================================================
const db = require("knex")(require("./knexfile").development);
const closeConnection = () => db.destroy();
module.exports = {
db,
closeConnection
};
================================================
FILE: chapter2/3_integration_tests/3_knex_tests_hooks/knexfile.js
================================================
module.exports = {
development: {
client: "sqlite3",
connection: { filename: "./dev.sqlite" },
useNullAsDefault: true
}
};
================================================
FILE: chapter2/3_integration_tests/3_knex_tests_hooks/migrations/20191230210750_create_carts.js
================================================
exports.up = async knex => {
await knex.schema.createTable("carts", table => {
table.increments("id");
table.string("username");
});
await knex.schema.createTable("carts_items", table => {
table.integer("cartId").references("carts.id");
table.string("itemName");
});
};
exports.down = async knex => {
await knex.schema.dropTable("carts");
await knex.schema.dropTable("carts_items");
};
================================================
FILE: chapter2/3_integration_tests/3_knex_tests_hooks/package.json
================================================
{
"name": "1_knex_tests_promise",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"knex": "^0.20.6",
"sqlite3": "^4.1.1"
},
"devDependencies": {
"jest": "^24.9.0"
}
}
================================================
FILE: chapter2/4_end_to_end_tests/1_http_api_tests/package.json
================================================
{
"name": "1_http_api_tests",
"version": "1.0.0",
"scripts": {
"test": "jest"
},
"devDependencies": {
"isomorphic-fetch": "^2.2.1",
"jest": "^24.9.0"
},
"dependencies": {
"koa": "^2.11.0",
"koa-router": "^7.4.0"
}
}
================================================
FILE: chapter2/4_end_to_end_tests/1_http_api_tests/server.js
================================================
const Koa = require("koa");
const Router = require("koa-router");
const app = new Koa();
const router = new Router();
const carts = new Map();
router.get("/carts/:username/items", ctx => {
const cart = carts.get(ctx.params.username);
cart ? (ctx.body = cart) : (ctx.status = 404);
});
router.post("/carts/:username/items/:item", ctx => {
const { username, item } = ctx.params;
const newItems = (carts.get(username) || []).concat(item);
carts.set(username, newItems);
ctx.body = newItems;
});
app.use(router.routes());
module.exports = app.listen(3000);
================================================
FILE: chapter2/4_end_to_end_tests/1_http_api_tests/server.test.js
================================================
const app = require("./server");
const fetch = require("isomorphic-fetch");
const apiRoot = "http://localhost:3000";
const addItem = (username, item) => {
return fetch(`${apiRoot}/carts/${username}/items/${item}`, {
method: "POST"
});
};
const getItems = username => {
return fetch(`${apiRoot}/carts/${username}/items`, { method: "GET" });
};
test("adding items to a cart", async () => {
const initialItemsResponse = await getItems("lucas");
expect(initialItemsResponse.status).toBe(404);
const addItemResponse = await addItem("lucas", "cheesecake");
expect(await addItemResponse.json()).toEqual(["cheesecake"]);
const finalItemsResponse = await getItems("lucas");
expect(await finalItemsResponse.json()).toEqual(["cheesecake"]);
});
afterAll(() => app.close());
================================================
FILE: chapter2/4_end_to_end_tests/2_http_api_with_remove_item/package.json
================================================
{
"name": "1_http_api_tests",
"version": "1.0.0",
"scripts": {
"test": "jest"
},
"devDependencies": {
"isomorphic-fetch": "^2.2.1",
"jest": "^26.6.0"
},
"dependencies": {
"koa": "^2.11.0",
"koa-router": "^7.4.0"
}
}
================================================
FILE: chapter2/4_end_to_end_tests/2_http_api_with_remove_item/server.js
================================================
const Koa = require("koa");
const Router = require("koa-router");
const app = new Koa();
const router = new Router();
let carts = new Map();
router.get("/carts/:username/items", ctx => {
const cart = carts.get(ctx.params.username);
cart ? (ctx.body = cart) : (ctx.status = 404);
});
router.post("/carts/:username/items/:item", ctx => {
const { username, item } = ctx.params;
const newItems = (carts.get(username) || []).concat(item);
carts.set(username, newItems);
ctx.body = newItems;
});
router.delete("/carts/:username/items/:item", ctx => {
const { username, item } = ctx.params;
const newItems = (carts.get(username) || []).filter(i => i !== item);
carts.set(username, newItems);
ctx.body = newItems;
});
app.use(router.routes());
// This method is designed especifically for testability.
// Because we keep `carts` in memory, we must reset it back
// to its inital state by deleting all items in it.
// If you were dealing with a database, you'd have to do
// something similar in your tests by ensuring the database
// is reset to its initial state before each test.
const resetState = () => {
carts = new Map();
};
module.exports = {
app: app.listen(3000),
resetState
};
================================================
FILE: chapter2/4_end_to_end_tests/2_http_api_with_remove_item/server.test.js
================================================
const { app, resetState } = require("./server");
const fetch = require("isomorphic-fetch");
const apiRoot = "http://localhost:3000";
const addItem = (username, item) => {
return fetch(`${apiRoot}/carts/${username}/items/${item}`, {
method: "POST"
});
};
const removeItem = (username, item) => {
return fetch(`${apiRoot}/carts/${username}/items/${item}`, {
method: "DELETE"
});
};
const getItems = username => {
return fetch(`${apiRoot}/carts/${username}/items`, { method: "GET" });
};
test("adding items to a cart", async () => {
const initialItemsResponse = await getItems("lucas");
expect(initialItemsResponse.status).toBe(404);
const addItemResponse = await addItem("lucas", "cheesecake");
expect(await addItemResponse.json()).toEqual(["cheesecake"]);
const finalItemsResponse = await getItems("lucas");
expect(await finalItemsResponse.json()).toEqual(["cheesecake"]);
});
test("removing items from a cart", async () => {
const initialItemsResponse = await getItems("lucas");
expect(initialItemsResponse.status).toBe(404);
await addItem("lucas", "cheesecake");
const removeItemsResponse = await removeItem("lucas", "cheesecake");
expect(await removeItemsResponse.json()).toEqual([]);
const finalItemsResponse = await getItems("lucas");
expect(await finalItemsResponse.json()).toEqual([]);
});
// We must clean-up our server's state before each test.
// If you kept state in a database, you'd need to ensure
// your database is reset to its initial state.
beforeEach(() => resetState());
afterAll(() => app.close());
================================================
FILE: chapter2/5_tests_cost_and_revenue/1_good_vs_bad/badly_written.test.js
================================================
const { app, resetState } = require("./server");
const fetch = require("isomorphic-fetch");
test("adding items to a cart", done => {
resetState();
return fetch(`http://localhost:3000/carts/lucas/items`, {
method: "GET"
})
.then(initialItemsResponse => {
expect(initialItemsResponse.status).toEqual(404);
return fetch(`http://localhost:3000/carts/lucas/items/cheesecake`, {
method: "POST"
}).then(response => response.json());
})
.then(addItemResponse => {
expect(addItemResponse).toEqual(["cheesecake"]);
return fetch(`http://localhost:3000/carts/lucas/items`, {
method: "GET"
}).then(response => response.json());
})
.then(finalItemsResponse => {
expect(finalItemsResponse).toEqual(["cheesecake"]);
})
.then(() => {
app.close();
done();
});
});
================================================
FILE: chapter2/5_tests_cost_and_revenue/1_good_vs_bad/package.json
================================================
{
"name": "1_good_vs_bad",
"version": "1.0.0",
"scripts": {
"test-good": "jest well_written.test.js",
"test-bad": "jest badly_written.test.js"
},
"devDependencies": {
"isomorphic-fetch": "^2.2.1",
"jest": "^24.9.0"
},
"dependencies": {
"koa": "^2.11.0",
"koa-router": "^7.4.0"
}
}
================================================
FILE: chapter2/5_tests_cost_and_revenue/1_good_vs_bad/server.js
================================================
const Koa = require("koa");
const Router = require("koa-router");
const app = new Koa();
const router = new Router();
let carts = new Map();
router.get("/carts/:username/items", ctx => {
const cart = carts.get(ctx.params.username);
cart ? (ctx.body = cart) : (ctx.status = 404);
});
router.post("/carts/:username/items/:item", ctx => {
const { username, item } = ctx.params;
const newItems = (carts.get(username) || []).concat(item);
carts.set(username, newItems);
ctx.body = newItems;
});
router.delete("/carts/:username/items/:item", ctx => {
const { username, item } = ctx.params;
const newItems = (carts.get(username) || []).filter(i => i !== item);
carts.set(username, newItems);
ctx.body = newItems;
});
app.use(router.routes());
// This method is designed especifically for testability.
// Because we keep `carts` in memory, we must reset it back
// to its inital state by deleting all items in it.
// If you were dealing with a database, you'd have to do
// something similar in your tests by ensuring the database
// is reset to its initial state before each test.
const resetState = () => {
carts = new Map();
};
module.exports = {
app: app.listen(3000),
resetState
};
================================================
FILE: chapter2/5_tests_cost_and_revenue/1_good_vs_bad/well_written.test.js
================================================
const { app, resetState } = require("./server");
const fetch = require("isomorphic-fetch");
const apiRoot = "http://localhost:3000";
const addItem = (username, item) => {
return fetch(`${apiRoot}/carts/${username}/items/${item}`, {
method: "POST"
});
};
const getItems = username => {
return fetch(`${apiRoot}/carts/${username}/items`, { method: "GET" });
};
beforeEach(() => resetState());
afterAll(() => app.close());
test("adding items to a cart", async () => {
const initialItemsResponse = await getItems("lucas");
expect(initialItemsResponse.status).toBe(404);
const addItemResponse = await addItem("lucas", "cheesecake");
expect(await addItemResponse.json()).toEqual(["cheesecake"]);
const finalItemsResponse = await getItems("lucas");
expect(await finalItemsResponse.json()).toEqual(["cheesecake"]);
});
================================================
FILE: chapter2/5_tests_cost_and_revenue/2_test_coupling/package.json
================================================
{
"name": "2_test_coupling",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^24.9.0"
}
}
================================================
FILE: chapter2/5_tests_cost_and_revenue/2_test_coupling/pow.test.js
================================================
//const pow = require("./pow_recursive");
const pow = require("./pow_loop");
test("calculates powers", () => {
expect(pow(2, 0)).toBe(1);
expect(pow(2, -3)).toBe(0.125);
expect(pow(2, 2)).toBe(4);
expect(pow(2, 5)).toBe(32);
expect(pow(0, 5)).toBe(0);
expect(pow(1, 4)).toBe(1);
});
================================================
FILE: chapter2/5_tests_cost_and_revenue/2_test_coupling/pow_loop.js
================================================
const pow = (a, b) => {
let result = 1;
for (let i = 0; i < Math.abs(b); i++) {
if (b < 0) result = result / a;
if (b > 0) result = result * a;
}
return result;
};
module.exports = pow;
================================================
FILE: chapter2/5_tests_cost_and_revenue/2_test_coupling/pow_recursive.js
================================================
const pow = (a, b, acc = 1) => {
if (b === 0) return acc;
const nextB = b < 0 ? b + 1 : b - 1;
const nextAcc = b < 0 ? acc / a : acc * a;
return pow(a, nextB, nextAcc);
};
module.exports = pow;
================================================
FILE: chapter3/1_organising_test_suites/1_breaking_down_tests_big_tests/package.json
================================================
{
"name": "1_breaking_down_tests_big_tests",
"version": "1.0.0",
"scripts": {
"test": "jest"
},
"devDependencies": {
"isomorphic-fetch": "^2.2.1",
"jest": "^24.9.0"
},
"dependencies": {
"koa": "^2.11.0",
"koa-router": "^7.4.0"
}
}
================================================
FILE: chapter3/1_organising_test_suites/1_breaking_down_tests_big_tests/server.js
================================================
const Koa = require("koa");
const Router = require("koa-router");
const app = new Koa();
const router = new Router();
const carts = new Map();
const inventory = new Map();
router.post("/carts/:username/items/:item", ctx => {
const { username, item } = ctx.params;
if (!inventory.get(item)) {
ctx.status = 404;
return;
}
inventory.set(item, inventory.get(item) - 1);
const newItems = (carts.get(username) || []).concat(item);
carts.set(username, newItems);
ctx.body = newItems;
});
app.use(router.routes());
module.exports = {
app: app.listen(3000),
inventory,
carts
};
================================================
FILE: chapter3/1_organising_test_suites/1_breaking_down_tests_big_tests/server.test.js
================================================
const { app, inventory, carts } = require("./server");
const fetch = require("isomorphic-fetch");
const apiRoot = "http://localhost:3000";
const addItem = (username, item) => {
return fetch(`${apiRoot}/carts/${username}/items/${item}`, {
method: "POST"
});
};
describe("addItem", () => {
test("adding items to a cart", async () => {
inventory.set("cheesecake", 1);
const addItemResponse = await addItem("lucas", "cheesecake");
expect(await addItemResponse.json()).toEqual(["cheesecake"]);
expect(inventory.get("cheesecake")).toBe(0);
expect(carts.get("lucas")).toEqual(["cheesecake"]);
const failedAddItem = await addItem("lucas", "cheesecake");
expect(failedAddItem.status).toBe(404);
});
});
afterAll(() => app.close());
================================================
FILE: chapter3/1_organising_test_suites/2_breaking_down_tests_small_tests/package.json
================================================
{
"name": "2_breaking_down_tests_small_tests",
"version": "1.0.0",
"scripts": {
"test": "jest"
},
"devDependencies": {
"isomorphic-fetch": "^2.2.1",
"jest": "^24.9.0"
},
"dependencies": {
"koa": "^2.11.0",
"koa-router": "^7.4.0"
}
}
================================================
FILE: chapter3/1_organising_test_suites/2_breaking_down_tests_small_tests/server.js
================================================
const Koa = require("koa");
const Router = require("koa-router");
const app = new Koa();
const router = new Router();
const carts = new Map();
const inventory = new Map();
router.post("/carts/:username/items/:item", ctx => {
const { username, item } = ctx.params;
if (!inventory.get(item)) {
ctx.status = 404;
return;
}
inventory.set(item, inventory.get(item) - 1);
const newItems = (carts.get(username) || []).concat(item);
carts.set(username, newItems);
ctx.body = newItems;
});
app.use(router.routes());
module.exports = {
app: app.listen(3000),
inventory,
carts
};
================================================
FILE: chapter3/1_organising_test_suites/2_breaking_down_tests_small_tests/server.test.js
================================================
const { app, inventory, carts } = require("./server");
const fetch = require("isomorphic-fetch");
const apiRoot = "http://localhost:3000";
const addItem = (username, item) => {
return fetch(`${apiRoot}/carts/${username}/items/${item}`, {
method: "POST"
});
};
describe("addItem", () => {
beforeEach(() => carts.forEach((value, key) => carts.delete(key)));
beforeEach(() => inventory.set("cheesecake", 1));
test("correct response", async () => {
const addItemResponse = await addItem("lucas", "cheesecake");
expect(await addItemResponse.json()).toEqual(["cheesecake"]);
});
test("inventory update", async () => {
await addItem("lucas", "cheesecake");
expect(inventory.get("cheesecake")).toBe(0);
});
test("cart update", async () => {
await addItem("keith", "cheesecake");
expect(carts.get("keith")).toEqual(["cheesecake"]);
});
test("soldout items", async () => {
inventory.set("cheesecake", 0);
const failedAddItem = await addItem("lucas", "cheesecake");
expect(failedAddItem.status).toBe(404);
});
});
afterAll(() => app.close());
================================================
FILE: chapter3/1_organising_test_suites/3_global_hooks/dummy.test.js
================================================
describe("placeholder tests", () => {
test("placeholder 1", () => {});
test("placeholder 2", () => {});
test("placeholder 3", () => {});
});
================================================
FILE: chapter3/1_organising_test_suites/3_global_hooks/globalSetup.js
================================================
const setup = () => {
global._accessibleOnTeardown = "Look, I was set on the setup file";
console.log("\nsetup executed\n");
};
module.exports = setup;
================================================
FILE: chapter3/1_organising_test_suites/3_global_hooks/globalTeardown.js
================================================
const teardown = () => {
console.log(`The value set on setup was: ${global._accessibleOnTeardown}`);
console.log("teardown executed");
};
module.exports = teardown;
================================================
FILE: chapter3/1_organising_test_suites/3_global_hooks/jest.config.js
================================================
module.exports = {
globalSetup: "./globalSetup.js",
globalTeardown: "./globalTeardown.js",
testEnvironment: "node"
};
================================================
FILE: chapter3/1_organising_test_suites/3_global_hooks/package.json
================================================
{
"name": "1_organising_test_suites",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^25.1.0"
}
}
================================================
FILE: chapter3/2_writing_good_assertions/1_assertion_checks/inventoryController.js
================================================
const inventory = new Map();
const addToInventory = (item, n) => {
if (typeof n !== "number") throw new Error("quantity must be a number");
const currentQuantity = inventory.get(item) || 0;
const newQuantity = currentQuantity + n;
inventory.set(item, newQuantity);
return newQuantity;
};
module.exports = { inventory, addToInventory };
================================================
FILE: chapter3/2_writing_good_assertions/1_assertion_checks/inventoryController.test.js
================================================
const { inventory, addToInventory } = require("./inventoryController");
beforeEach(() => inventory.set("cheesecake", 0));
test("cancels operation for invalid quantities", () => {
expect.assertions(2);
try {
addToInventory("cheesecake", "not a number");
} catch (e) {
expect(inventory.get("cheesecake")).toBe(0);
}
expect(Array.from(inventory.entries())).toHaveLength(1);
});
================================================
FILE: chapter3/2_writing_good_assertions/1_assertion_checks/package.json
================================================
{
"name": "1_assertion_checks",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^25.1.0"
}
}
================================================
FILE: chapter3/2_writing_good_assertions/2_assertion_checks_toThrow/inventoryController.js
================================================
const inventory = new Map();
const addToInventory = (item, n) => {
if (typeof n !== "number") throw new Error("quantity must be a number");
const currentQuantity = inventory.get(item) || 0;
const newQuantity = currentQuantity + n;
inventory.set(item, newQuantity);
return newQuantity;
};
module.exports = { inventory, addToInventory };
================================================
FILE: chapter3/2_writing_good_assertions/2_assertion_checks_toThrow/inventoryController.test.js
================================================
const { inventory, addToInventory } = require("./inventoryController");
beforeEach(() => inventory.set("cheesecake", 0));
test("cancels operation for invalid quantities", () => {
expect(() => addToInventory("cheesecake", "not a number")).not.toThrow();
expect(inventory.get("cheesecake")).toBe(0);
expect(Array.from(inventory.entries())).toHaveLength(1);
});
================================================
FILE: chapter3/2_writing_good_assertions/2_assertion_checks_toThrow/package.json
================================================
{
"name": "1_assertion_checks",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^25.1.0"
}
}
================================================
FILE: chapter3/2_writing_good_assertions/3_loose_assertions/inventoryController.js
================================================
const inventory = new Map();
const addToInventory = (item, n) => {
if (typeof n !== "number") throw new Error("quantity must be a number");
const currentQuantity = inventory.get(item) || 0;
const newQuantity = currentQuantity + n;
inventory.set(item, newQuantity);
return newQuantity;
};
module.exports = { inventory, addToInventory };
================================================
FILE: chapter3/2_writing_good_assertions/3_loose_assertions/inventoryController.test.js
================================================
const { inventory, addToInventory } = require("./inventoryController");
beforeEach(() => {
inventory.forEach((value, key) => inventory.delete(key));
});
test("returned value", () => {
const result = addToInventory("cheesecake", 2);
expect(typeof result).toBe("number");
});
================================================
FILE: chapter3/2_writing_good_assertions/3_loose_assertions/package.json
================================================
{
"name": "2_loose_assertions",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^25.1.0"
}
}
================================================
FILE: chapter3/2_writing_good_assertions/4_asymmetric_matchers/inventoryController.js
================================================
const inventory = new Map();
const addToInventory = (item, n) => {
if (typeof n !== "number") throw new Error("quantity must be a number");
const currentQuantity = inventory.get(item) || 0;
const newQuantity = currentQuantity + n;
inventory.set(item, newQuantity);
return newQuantity;
};
const getInventory = () => {
const contentArray = Array.from(inventory.entries());
const contents = contentArray.reduce((contents, [name, quantity]) => {
return { ...contents, [name]: quantity };
}, {});
return { ...contents, generatedAt: new Date() };
};
module.exports = { inventory, addToInventory, getInventory };
================================================
FILE: chapter3/2_writing_good_assertions/4_asymmetric_matchers/inventoryController.test.js
================================================
const { inventory, getInventory } = require("./inventoryController");
test("inventory contents", () => {
inventory
.set("cheesecake", 1)
.set("macarroon", 3)
.set("croissant", 3)
.set("eclaire", 7);
const result = getInventory();
expect(result).toEqual({
cheesecake: 1,
macarroon: 3,
croissant: 3,
eclaire: 7,
generatedAt: expect.any(Date)
});
});
================================================
FILE: chapter3/2_writing_good_assertions/4_asymmetric_matchers/package.json
================================================
{
"name": "3_asymmetric_matchers",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^25.1.0"
}
}
================================================
FILE: chapter3/2_writing_good_assertions/5_manual_assertions/inventoryController.js
================================================
const inventory = new Map();
const addToInventory = (item, n) => {
if (typeof n !== "number") throw new Error("quantity must be a number");
const currentQuantity = inventory.get(item) || 0;
const newQuantity = currentQuantity + n;
inventory.set(item, newQuantity);
return newQuantity;
};
const getInventory = () => {
const contentArray = Array.from(inventory.entries());
const contents = contentArray.reduce((contents, [name, quantity]) => {
return { ...contents, [name]: quantity };
}, {});
// To make the tests in this folder pass, update this
// line so that it doesn't set the new Date's year to 3000.
return { ...contents, generatedAt: new Date(new Date().setYear(3000)) };
};
module.exports = { inventory, addToInventory, getInventory };
================================================
FILE: chapter3/2_writing_good_assertions/5_manual_assertions/inventoryController.test.js
================================================
const { getInventory } = require("./inventoryController");
// This test _will_ fail.
// Here I'm trying to demonstrate Jest's output
// when an assertion like `.toBe(true)` fails.
test("generatedAt in the past", () => {
const result = getInventory();
const currentTime = Date.now() + 1;
const isPastTimestamp = result.generatedAt.getTime() <= currentTime;
expect(isPastTimestamp).toBe(true);
});
================================================
FILE: chapter3/2_writing_good_assertions/5_manual_assertions/package.json
================================================
{
"name": "4_manual_assertions",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^25.1.0"
}
}
================================================
FILE: chapter3/2_writing_good_assertions/6_custom_matchers/inventoryController.js
================================================
const inventory = new Map();
const addToInventory = (item, n) => {
if (typeof n !== "number") throw new Error("quantity must be a number");
const currentQuantity = inventory.get(item) || 0;
const newQuantity = currentQuantity + n;
inventory.set(item, newQuantity);
return newQuantity;
};
const getInventory = () => {
const contentArray = Array.from(inventory.entries());
const contents = contentArray.reduce((contents, [name, quantity]) => {
return { ...contents, [name]: quantity };
}, {});
return { ...contents, generatedAt: new Date(new Date().setYear(3000)) };
};
module.exports = { inventory, addToInventory, getInventory };
================================================
FILE: chapter3/2_writing_good_assertions/6_custom_matchers/inventoryController.test.js
================================================
const { getInventory } = require("./inventoryController");
test("generatedAt in the past", () => {
const result = getInventory();
const currentTime = new Date(Date.now() + 1);
expect(result.generatedAt).toBeBefore(currentTime);
});
================================================
FILE: chapter3/2_writing_good_assertions/6_custom_matchers/jest.config.js
================================================
module.exports = {
testEnvironment: "node",
setupFilesAfterEnv: ["jest-extended"]
};
================================================
FILE: chapter3/2_writing_good_assertions/6_custom_matchers/package.json
================================================
{
"name": "5_custom_matchers",
"version": "1.0.0",
"description": "",
"main": "jest.config.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^25.1.0",
"jest-extended": "^0.11.5"
}
}
================================================
FILE: chapter3/2_writing_good_assertions/7_circular_assertions/inventoryController.js
================================================
const inventory = new Map();
const getInventory = () => {
const contentArray = Array.from(inventory.entries());
const contents = contentArray.reduce((contents, [name]) => {
return { ...contents, [name]: 1000 };
}, {});
return { ...contents, generatedAt: new Date() };
};
module.exports = { inventory, getInventory };
================================================
FILE: chapter3/2_writing_good_assertions/7_circular_assertions/package.json
================================================
{
"name": "6_circular_assertions",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"isomorphic-fetch": "^2.2.1",
"jest": "^25.1.0"
},
"dependencies": {
"koa": "^2.11.0",
"koa-router": "^8.0.8"
}
}
================================================
FILE: chapter3/2_writing_good_assertions/7_circular_assertions/server.js
================================================
const Koa = require("koa");
const Router = require("koa-router");
const { getInventory } = require("./inventoryController");
const app = new Koa();
const router = new Router();
router.get("/inventory", ctx => (ctx.body = getInventory()));
app.use(router.routes());
module.exports = app.listen(3000);
================================================
FILE: chapter3/2_writing_good_assertions/7_circular_assertions/server.test.js
================================================
const app = require("./server");
const fetch = require("isomorphic-fetch");
const { inventory, getInventory } = require("./inventoryController");
const apiRoot = "http://localhost:3000";
const sendGetInventoryRequest = () => {
return fetch(`${apiRoot}/inventory`, { method: "GET" });
};
test("fetching inventory", async () => {
inventory.set("cheesecake", 1).set("macarroon", 2);
const getInventoryResponse = await sendGetInventoryRequest("lucas");
const expected = { ...getInventory(), generatedAt: expect.anything() };
expect(await getInventoryResponse.json()).toEqual(expected);
});
afterAll(() => app.close());
================================================
FILE: chapter3/3_mocks_stubs_and_spies/1_mocking_objects/inventoryController.js
================================================
const logger = require("./logger");
const inventory = new Map();
const addToInventory = (item, quantity) => {
if (typeof quantity !== "number") {
logger.logError(
{ quantity },
"could not add item to inventory because quantity was not a number"
);
throw new Error("quantity must be a number");
}
const currentQuantity = inventory.get(item) || 0;
const newQuantity = currentQuantity + quantity;
inventory.set(item, newQuantity);
logger.logInfo(
{ item, quantity, memoryUsage: process.memoryUsage().rss },
"item added to the inventory"
);
return newQuantity;
};
const getInventory = () => {
const contentArray = Array.from(inventory.entries());
const contents = contentArray.reduce((contents, [name, quantity]) => {
return { ...contents, [name]: quantity };
}, {});
logger.logInfo({ contents }, "inventory items fetched");
return { ...contents, generatedAt: new Date(new Date().setYear(3000)) };
};
module.exports = { inventory, addToInventory, getInventory };
================================================
FILE: chapter3/3_mocks_stubs_and_spies/1_mocking_objects/inventoryController.test.js
================================================
const logger = require("./logger");
const {
inventory,
addToInventory,
getInventory
} = require("./inventoryController");
// Clearing the inventory before each test
beforeEach(() => {
inventory.forEach((value, key) => inventory.delete(key));
});
beforeAll(() => jest.spyOn(logger, "logInfo").mockImplementation(jest.fn()));
beforeAll(() => jest.spyOn(logger, "logError").mockImplementation(jest.fn()));
afterEach(() => jest.resetAllMocks());
describe("addToInventory", () => {
beforeEach(() => {
jest
.spyOn(process, "memoryUsage")
.mockReturnValue({ rss: 123456, heapTotal: 1, heapUsed: 2, external: 3 });
});
test("logging new items", () => {
addToInventory("cheesecake", 2);
expect(logger.logInfo.mock.calls).toHaveLength(1);
const firstCallArgs = logger.logInfo.mock.calls[0];
const [firstArg, secondArg] = firstCallArgs;
expect(firstArg).toEqual({
item: "cheesecake",
quantity: 2,
memoryUsage: 123456
});
expect(secondArg).toEqual("item added to the inventory");
});
test("logging logErrors", () => {
try {
addToInventory("cheesecake", "not a number");
} catch (e) {
// No-op
}
expect(logger.logError.mock.calls).toHaveLength(1);
const firstCallArgs = logger.logError.mock.calls[0];
const [firstArg, secondArg] = firstCallArgs;
expect(firstArg).toEqual({ quantity: "not a number" });
expect(secondArg).toEqual(
"could not add item to inventory because quantity was not a number"
);
});
});
describe("getInventory", () => {
test("logging fetches", () => {
inventory.set("cheesecake", 2);
getInventory("cheesecake", 2);
expect(logger.logInfo.mock.calls).toHaveLength(1);
const firstCallArgs = logger.logInfo.mock.calls[0];
const [firstArg, secondArg] = firstCallArgs;
expect(firstArg).toEqual({ contents: { cheesecake: 2 } });
expect(secondArg).toEqual("inventory items fetched");
});
});
================================================
FILE: chapter3/3_mocks_stubs_and_spies/1_mocking_objects/logger.js
================================================
const pino = require("pino");
const pinoInstance = pino();
const logger = {
logInfo: pinoInstance.info.bind(pinoInstance),
logError: pinoInstance.error.bind(pinoInstance)
};
module.exports = logger;
================================================
FILE: chapter3/3_mocks_stubs_and_spies/1_mocking_objects/package.json
================================================
{
"name": "1_mocking_objects",
"version": "1.0.0",
"description": "",
"main": "inventoryController.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^25.1.0"
},
"dependencies": {
"pino": "^5.16.0"
}
}
================================================
FILE: chapter3/3_mocks_stubs_and_spies/2_mocking_imports/inventoryController.js
================================================
const { logInfo, logError } = require("./logger");
const inventory = new Map();
const addToInventory = (item, quantity) => {
if (typeof quantity !== "number") {
logError(
{ quantity },
"could not add item to inventory because quantity was not a number"
);
throw new Error("quantity must be a number");
}
const currentQuantity = inventory.get(item) || 0;
const newQuantity = currentQuantity + quantity;
inventory.set(item, newQuantity);
logInfo(
{ item, quantity, memoryUsage: process.memoryUsage().rss },
"item added to the inventory"
);
return newQuantity;
};
const getInventory = () => {
const contentArray = Array.from(inventory.entries());
const contents = contentArray.reduce((contents, [name, quantity]) => {
return { ...contents, [name]: quantity };
}, {});
logInfo({ contents }, "inventory items fetched");
return { ...contents, generatedAt: new Date(new Date().setYear(3000)) };
};
module.exports = { inventory, addToInventory, getInventory };
================================================
FILE: chapter3/3_mocks_stubs_and_spies/2_mocking_imports/inventoryController.test.js
================================================
const logger = require("./logger");
const {
inventory,
addToInventory,
getInventory
} = require("./inventoryController");
jest.mock("./logger", () => ({
logInfo: jest.fn(),
logError: jest.fn()
}));
// Clearing the inventory before each test
beforeEach(() => {
inventory.forEach((value, key) => inventory.delete(key));
});
afterEach(() => jest.resetAllMocks());
describe("addToInventory", () => {
beforeEach(() => {
jest
.spyOn(process, "memoryUsage")
.mockReturnValue({ rss: 123456, heapTotal: 1, heapUsed: 2, external: 3 });
});
test("logging new items", () => {
addToInventory("cheesecake", 2);
expect(logger.logInfo.mock.calls).toHaveLength(1);
const firstCallArgs = logger.logInfo.mock.calls[0];
const [firstArg, secondArg] = firstCallArgs;
expect(firstArg).toEqual({
item: "cheesecake",
quantity: 2,
memoryUsage: 123456
});
expect(secondArg).toEqual("item added to the inventory");
});
test("logging logErrors", () => {
try {
addToInventory("cheesecake", "not a number");
} catch (e) {
// No-op
}
expect(logger.logError.mock.calls).toHaveLength(1);
const firstCallArgs = logger.logError.mock.calls[0];
const [firstArg, secondArg] = firstCallArgs;
expect(firstArg).toEqual({ quantity: "not a number" });
expect(secondArg).toEqual(
"could not add item to inventory because quantity was not a number"
);
});
});
describe("getInventory", () => {
test("logging fetches", () => {
inventory.set("cheesecake", 2);
getInventory("cheesecake", 2);
expect(logger.logInfo.mock.calls).toHaveLength(1);
const firstCallArgs = logger.logInfo.mock.calls[0];
const [firstArg, secondArg] = firstCallArgs;
expect(firstArg).toEqual({ contents: { cheesecake: 2 } });
expect(secondArg).toEqual("inventory items fetched");
});
});
================================================
FILE: chapter3/3_mocks_stubs_and_spies/2_mocking_imports/logger.js
================================================
const pino = require("pino");
const pinoInstance = pino();
const logger = {
logInfo: pinoInstance.info.bind(pinoInstance),
logError: pinoInstance.error.bind(pinoInstance)
};
module.exports = logger;
================================================
FILE: chapter3/3_mocks_stubs_and_spies/2_mocking_imports/package.json
================================================
{
"name": "2_mocking_imports",
"version": "1.0.0",
"description": "",
"main": "inventoryController.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^25.1.0"
},
"dependencies": {
"pino": "^5.16.0"
}
}
================================================
FILE: chapter3/3_mocks_stubs_and_spies/3_manual_mocks/__mocks__/logger.js
================================================
module.exports = {
logInfo: jest.fn(),
logError: jest.fn()
};
================================================
FILE: chapter3/3_mocks_stubs_and_spies/3_manual_mocks/inventoryController.js
================================================
const { logInfo, logError } = require("./logger");
const inventory = new Map();
const addToInventory = (item, quantity) => {
if (typeof quantity !== "number") {
logError(
{ quantity },
"could not add item to inventory because quantity was not a number"
);
throw new Error("quantity must be a number");
}
const currentQuantity = inventory.get(item) || 0;
const newQuantity = currentQuantity + quantity;
inventory.set(item, newQuantity);
logInfo(
{ item, quantity, memoryUsage: process.memoryUsage().rss },
"item added to the inventory"
);
return newQuantity;
};
const getInventory = () => {
const contentArray = Array.from(inventory.entries());
const contents = contentArray.reduce((contents, [name, quantity]) => {
return { ...contents, [name]: quantity };
}, {});
logInfo({ contents }, "inventory items fetched");
return { ...contents, generatedAt: new Date(new Date().setYear(3000)) };
};
module.exports = { inventory, addToInventory, getInventory };
================================================
FILE: chapter3/3_mocks_stubs_and_spies/3_manual_mocks/inventoryController.test.js
================================================
const logger = require("./logger");
const {
inventory,
addToInventory,
getInventory
} = require("./inventoryController");
// Clearing the inventory before each test
beforeEach(() => {
inventory.forEach((value, key) => inventory.delete(key));
});
afterEach(() => jest.resetAllMocks());
jest.mock("./logger");
describe("addToInventory", () => {
beforeEach(() => {
jest
.spyOn(process, "memoryUsage")
.mockReturnValue({ rss: 123456, heapTotal: 1, heapUsed: 2, external: 3 });
});
test("logging new items", () => {
addToInventory("cheesecake", 2);
expect(logger.logInfo.mock.calls).toHaveLength(1);
const firstCallArgs = logger.logInfo.mock.calls[0];
const [firstArg, secondArg] = firstCallArgs;
expect(firstArg).toEqual({
item: "cheesecake",
quantity: 2,
memoryUsage: 123456
});
expect(secondArg).toEqual("item added to the inventory");
});
test("logging logErrors", () => {
try {
addToInventory("cheesecake", "not a number");
} catch (e) {
// No-op
}
expect(logger.logError.mock.calls).toHaveLength(1);
const firstCallArgs = logger.logError.mock.calls[0];
const [firstArg, secondArg] = firstCallArgs;
expect(firstArg).toEqual({ quantity: "not a number" });
expect(secondArg).toEqual(
"could not add item to inventory because quantity was not a number"
);
});
});
describe("getInventory", () => {
test("logging fetches", () => {
inventory.set("cheesecake", 2);
getInventory("cheesecake", 2);
expect(logger.logInfo.mock.calls).toHaveLength(1);
const firstCallArgs = logger.logInfo.mock.calls[0];
const [firstArg, secondArg] = firstCallArgs;
expect(firstArg).toEqual({ contents: { cheesecake: 2 } });
expect(secondArg).toEqual("inventory items fetched");
});
});
================================================
FILE: chapter3/3_mocks_stubs_and_spies/3_manual_mocks/logger.js
================================================
const pino = require("pino");
const pinoInstance = pino();
const logger = {
logInfo: pinoInstance.info.bind(pinoInstance),
logError: pinoInstance.error.bind(pinoInstance)
};
module.exports = logger;
================================================
FILE: chapter3/3_mocks_stubs_and_spies/3_manual_mocks/package.json
================================================
{
"name": "3_manual_mocks",
"version": "1.0.0",
"description": "",
"main": "inventoryController.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^26.6.0"
},
"dependencies": {
"pino": "^5.16.0"
}
}
================================================
FILE: chapter3/4_code_coverage/1_measuring_code_coverage/__mocks__/logger.js
================================================
module.exports = {
logInfo: jest.fn(),
logError: jest.fn()
};
================================================
FILE: chapter3/4_code_coverage/1_measuring_code_coverage/inventoryController.js
================================================
const { logInfo, logError } = require("./logger");
const inventory = new Map();
const addToInventory = (item, quantity) => {
if (typeof quantity !== "number") {
logError(
{ quantity },
"could not add item to inventory because quantity was not a number"
);
throw new Error("quantity must be a number");
}
const currentQuantity = inventory.get(item) || 0;
const newQuantity = currentQuantity + quantity;
inventory.set(item, newQuantity);
logInfo(
{ item, quantity, memoryUsage: process.memoryUsage().rss },
"item added to the inventory"
);
return newQuantity;
};
module.exports = { inventory, addToInventory };
================================================
FILE: chapter3/4_code_coverage/1_measuring_code_coverage/inventoryController.test.js
================================================
const { inventory, addToInventory } = require("./inventoryController");
jest.mock("./logger");
beforeEach(() => inventory.clear());
describe("addToInventory", () => {
test("passing valid arguments", () => {
addToInventory("cheesecake", 2);
});
test("passing invalid arguments", () => {
try {
addToInventory("cheesecake", "should throw");
} catch (e) {
// ...
}
});
});
================================================
FILE: chapter3/4_code_coverage/1_measuring_code_coverage/logger.js
================================================
const pino = require("pino");
const pinoInstance = pino();
const logger = {
logInfo: pinoInstance.info.bind(pinoInstance),
logError: pinoInstance.error.bind(pinoInstance)
};
module.exports = logger;
================================================
FILE: chapter3/4_code_coverage/1_measuring_code_coverage/package.json
================================================
{
"name": "1_measuring_code_coverage",
"version": "1.0.0",
"description": "",
"main": "inventoryController.js",
"scripts": {
"test": "jest",
"coverage": "jest --coverage"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^25.1.0"
},
"dependencies": {
"pino": "^5.16.0"
}
}
================================================
FILE: chapter3/4_code_coverage/2_what_coverage_is_good_for/math.js
================================================
function sumOrDivide(a, b) {
if (a % 2 === 0 && b % 2 === 0) {
return a + b;
} else {
return a / b;
}
}
================================================
FILE: chapter3/4_code_coverage/2_what_coverage_is_good_for/math.test.js
================================================
test("sum", () => {
sumOrDivide(2, 4);
// WARNING: No assertions!
});
test("multiply", () => {
sumOrDivide(2, 6);
// WARNING: No assertions!
});
================================================
FILE: chapter3/4_code_coverage/2_what_coverage_is_good_for/package.json
================================================
{
"name": "2_what_coverage_is_good_for",
"version": "1.0.0",
"description": "",
"main": "inventoryController.js",
"scripts": {
"test": "jest",
"coverage": "jest --coverage"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^25.1.0"
},
"dependencies": {
"pino": "^5.16.0"
}
}
================================================
FILE: chapter4/1_setting_up_a_test_environment/1_exposing_modules/1_end_to_end_tests/package.json
================================================
{
"name": "1_end_to_end_tests",
"version": "1.0.0",
"scripts": {
"test": "jest"
},
"devDependencies": {
"isomorphic-fetch": "^2.2.1",
"jest": "^24.9.0"
},
"dependencies": {
"koa": "^2.11.0",
"koa-router": "^7.4.0"
}
}
================================================
FILE: chapter4/1_setting_up_a_test_environment/1_exposing_modules/1_end_to_end_tests/server.js
================================================
const Koa = require("koa");
const Router = require("koa-router");
const app = new Koa();
const router = new Router();
let carts = new Map();
let inventory = new Map();
router.get("/carts/:username/items", ctx => {
const cart = carts.get(ctx.params.username);
cart ? (ctx.body = cart) : (ctx.status = 404);
});
router.post("/carts/:username/items/:item", ctx => {
const { username, item } = ctx.params;
const isAvailable = inventory.has(item) && inventory.get(item) > 0;
if (!isAvailable) {
ctx.body = { message: `${item} is unavailable` };
ctx.status = 400;
return;
}
const newItems = (carts.get(username) || []).concat(item);
carts.set(username, newItems);
inventory.set(item, inventory.get(item) - 1);
ctx.body = newItems;
});
router.delete("/carts/:username/items/:item", ctx => {
const { username, item } = ctx.params;
if (!carts.has(username) || !carts.get(username).includes(item)) {
ctx.body = { message: `${item} is not in the cart` };
ctx.status = 400;
return;
}
const newItems = (carts.get(username) || []).filter(i => i !== item);
inventory.set(item, (inventory.get(item) || 0) + 1);
carts.set(username, newItems);
ctx.body = newItems;
});
app.use(router.routes());
module.exports = { app: app.listen(3000), carts, inventory };
================================================
FILE: chapter4/1_setting_up_a_test_environment/1_exposing_modules/1_end_to_end_tests/server.test.js
================================================
const { app, carts, inventory } = require("./server.js");
const fetch = require("isomorphic-fetch");
const apiRoot = "http://localhost:3000";
afterAll(() => app.close());
afterEach(() => inventory.clear());
afterEach(() => carts.clear());
describe("add items to a cart", () => {
test("adding available items", async () => {
inventory.set("cheesecake", 1);
const response = await fetch(
`${apiRoot}/carts/test_user/items/cheesecake`,
{ method: "POST" }
);
expect(response.status).toEqual(200);
expect(await response.json()).toEqual(["cheesecake"]);
expect(inventory.get("cheesecake")).toEqual(0);
expect(carts.get("test_user")).toEqual(["cheesecake"]);
});
test("adding unavailable items", async () => {
carts.set("test_user", []);
const response = await fetch(
`${apiRoot}/carts/test_user/items/cheesecake`,
{ method: "POST" }
);
expect(response.status).toEqual(400);
expect(await response.json()).toEqual({
message: "cheesecake is unavailable"
});
expect(carts.get("test_user")).toEqual([]);
});
});
describe("removing items from a cart", () => {
test("removing existing items", async () => {
carts.set("test_user", ["cheesecake"]);
const response = await fetch(
`${apiRoot}/carts/test_user/items/cheesecake`,
{ method: "DELETE" }
);
expect(response.status).toEqual(200);
expect(await response.json()).toEqual([]);
expect(carts.get("test_user")).toEqual([]);
expect(inventory.get("cheesecake")).toEqual(1);
});
test("removing non-existing items", async () => {
inventory.set("cheesecake", 0);
carts.set("test_user", []);
const response = await fetch(
`${apiRoot}/carts/test_user/items/cheesecake`,
{ method: "DELETE" }
);
expect(response.status).toEqual(400);
expect(await response.json()).toEqual({
message: "cheesecake is not in the cart"
});
expect(inventory.get("cheesecake")).toEqual(0);
});
});
================================================
FILE: chapter4/1_setting_up_a_test_environment/1_exposing_modules/2_integration_tests/cartController.js
================================================
const { removeFromInventory } = require("./inventoryController");
const logger = require("./logger");
const carts = new Map();
const addItemToCart = (username, item) => {
removeFromInventory(item);
const newItems = (carts.get(username) || []).concat(item);
carts.set(username, newItems);
logger.log(`${item} added to ${username}'s cart`);
return newItems;
};
module.exports = { addItemToCart, carts };
================================================
FILE: chapter4/1_setting_up_a_test_environment/1_exposing_modules/2_integration_tests/cartController.test.js
================================================
const { inventory } = require("./inventoryController");
const { carts, addItemToCart } = require("./cartController");
const fs = require("fs");
afterEach(() => inventory.clear());
afterEach(() => carts.clear());
describe("addItemToCart", () => {
beforeEach(() => {
fs.writeFileSync("/tmp/logs.out", "");
});
test("adding unavailable items to cart", () => {
carts.set("test_user", []);
inventory.set("cheesecake", 0);
try {
addItemToCart("test_user", "cheesecake");
} catch (e) {
const expectedError = new Error(`cheesecake is unavailable`);
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
expect(carts.get("test_user")).toEqual([]);
expect.assertions(2);
});
test("logging added items", () => {
carts.set("test_user", []);
inventory.set("cheesecake", 1);
addItemToCart("test_user", "cheesecake");
const logs = fs.readFileSync("/tmp/logs.out", "utf-8");
expect(logs).toContain("cheesecake added to test_user's cart\n");
});
});
================================================
FILE: chapter4/1_setting_up_a_test_environment/1_exposing_modules/2_integration_tests/inventoryController.js
================================================
const inventory = new Map();
const removeFromInventory = item => {
if (!inventory.has(item) || !inventory.get(item) > 0) {
const err = new Error(`${item} is unavailable`);
err.code = 400;
throw err;
}
inventory.set(item, inventory.get(item) - 1);
};
module.exports = { removeFromInventory, inventory };
================================================
FILE: chapter4/1_setting_up_a_test_environment/1_exposing_modules/2_integration_tests/logger.js
================================================
const fs = require("fs");
const logger = {
log: msg => fs.appendFileSync("/tmp/logs.out", msg + "\n")
};
module.exports = logger;
================================================
FILE: chapter4/1_setting_up_a_test_environment/1_exposing_modules/2_integration_tests/package.json
================================================
{
"name": "1_integration-tests",
"version": "1.0.0",
"scripts": {
"test": "jest"
},
"devDependencies": {
"isomorphic-fetch": "^2.2.1",
"jest": "^24.9.0"
},
"dependencies": {
"koa": "^2.11.0",
"koa-router": "^7.4.0"
}
}
================================================
FILE: chapter4/1_setting_up_a_test_environment/1_exposing_modules/2_integration_tests/server.js
================================================
const Koa = require("koa");
const Router = require("koa-router");
const { carts, addItemToCart } = require("./cartController");
const { inventory } = require("./inventoryController");
const app = new Koa();
const router = new Router();
router.get("/carts/:username/items", ctx => {
const cart = carts.get(ctx.params.username);
cart ? (ctx.body = cart) : (ctx.status = 404);
});
router.post("/carts/:username/items/:item", ctx => {
try {
const { username, item } = ctx.params;
const newItems = addItemToCart(username, item);
ctx.body = newItems;
} catch (e) {
ctx.body = { message: e.message };
ctx.status = e.code;
return;
}
});
router.delete("/carts/:username/items/:item", ctx => {
const { username, item } = ctx.params;
if (!carts.has(username) || !carts.get(username).includes(item)) {
ctx.body = { message: `${item} is not in the cart` };
ctx.status = 400;
return;
}
const newItems = (carts.get(username) || []).filter(i => i !== item);
inventory.set(item, (inventory.get(item) || 0) + 1);
carts.set(username, newItems);
ctx.body = newItems;
});
app.use(router.routes());
module.exports = { app: app.listen(3000), carts, inventory };
================================================
FILE: chapter4/1_setting_up_a_test_environment/1_exposing_modules/2_integration_tests/server.test.js
================================================
const { app } = require("./server.js");
const { inventory } = require("./inventoryController.js");
const { carts } = require("./cartController.js");
const fetch = require("isomorphic-fetch");
const apiRoot = "http://localhost:3000";
afterAll(() => app.close());
afterEach(() => inventory.clear());
afterEach(() => carts.clear());
describe("add items to a cart", () => {
test("adding available items", async () => {
inventory.set("cheesecake", 1);
const response = await fetch(
`${apiRoot}/carts/test_user/items/cheesecake`,
{ method: "POST" }
);
expect(response.status).toEqual(200);
expect(await response.json()).toEqual(["cheesecake"]);
expect(inventory.get("cheesecake")).toEqual(0);
expect(carts.get("test_user")).toEqual(["cheesecake"]);
});
test("adding unavailable items", async () => {
carts.set("test_user", []);
const response = await fetch(
`${apiRoot}/carts/test_user/items/cheesecake`,
{ method: "POST" }
);
expect(response.status).toEqual(400);
expect(await response.json()).toEqual({
message: "cheesecake is unavailable"
});
expect(carts.get("test_user")).toEqual([]);
});
});
describe("removing items from a cart", () => {
test("removing existing items", async () => {
carts.set("test_user", ["cheesecake"]);
const response = await fetch(
`${apiRoot}/carts/test_user/items/cheesecake`,
{ method: "DELETE" }
);
expect(response.status).toEqual(200);
expect(await response.json()).toEqual([]);
expect(carts.get("test_user")).toEqual([]);
expect(inventory.get("cheesecake")).toEqual(1);
});
test("removing non-existing items", async () => {
inventory.set("cheesecake", 0);
carts.set("test_user", []);
const response = await fetch(
`${apiRoot}/carts/test_user/items/cheesecake`,
{ method: "DELETE" }
);
expect(response.status).toEqual(400);
expect(await response.json()).toEqual({
message: "cheesecake is not in the cart"
});
expect(inventory.get("cheesecake")).toEqual(0);
});
});
================================================
FILE: chapter4/1_setting_up_a_test_environment/1_exposing_modules/3_unit_tests/cartController.js
================================================
const { inventory, removeFromInventory } = require("./inventoryController");
const logger = require("./logger");
const carts = new Map();
const addItemToCart = (username, item) => {
removeFromInventory(item);
const newItems = (carts.get(username) || []).concat(item);
if (!compliesToItemLimit(newItems)) {
inventory.set(item, inventory.get(item) + 1);
const limitError = new Error(
"You can't have more than three units of an item in your cart"
);
limitError.code = 400;
throw limitError;
}
carts.set(username, newItems);
logger.log(`${item} added to ${username}'s cart`);
return newItems;
};
const compliesToItemLimit = cart => {
const unitsPerItem = cart.reduce((itemMap, itemName) => {
const quantity = (itemMap[itemName] || 0) + 1;
return { ...itemMap, [itemName]: quantity };
}, {});
return Object.values(unitsPerItem).every(quantity => quantity <= 3);
};
module.exports = { addItemToCart, carts, compliesToItemLimit };
================================================
FILE: chapter4/1_setting_up_a_test_environment/1_exposing_modules/3_unit_tests/cartController.test.js
================================================
const { inventory } = require("./inventoryController");
const {
carts,
addItemToCart,
compliesToItemLimit
} = require("./cartController");
const fs = require("fs");
afterEach(() => inventory.clear());
afterEach(() => carts.clear());
describe("addItemToCart", () => {
beforeEach(() => {
fs.writeFileSync("/tmp/logs.out", "");
});
test("adding unavailable items to cart", () => {
carts.set("test_user", []);
inventory.set("cheesecake", 0);
try {
addItemToCart("test_user", "cheesecake");
} catch (e) {
const expectedError = new Error("cheesecake is unavailable");
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
expect(carts.get("test_user")).toEqual([]);
expect.assertions(2);
});
test("adding items above limit to cart", () => {
const initialCartContent = ["cheesecake", "cheesecake", "cheesecake"];
carts.set("test_user", initialCartContent);
inventory.set("cheesecake", 1);
try {
addItemToCart("test_user", "cheesecake");
} catch (e) {
const expectedError = new Error(
"You can't have more than three units of an item in your cart"
);
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
expect(carts.get("test_user")).toEqual(initialCartContent);
expect(inventory.get('cheesecake')).toEqual(1);
expect.assertions(3);
});
test("logging added items", () => {
carts.set("test_user", []);
inventory.set("cheesecake", 1);
addItemToCart("test_user", "cheesecake");
const logs = fs.readFileSync("/tmp/logs.out", "utf-8");
expect(logs).toContain("cheesecake added to test_user's cart\n");
});
});
describe("compliesToItemLimit", () => {
test("returns true for cards with no more than 3 items of a kind", () => {
const cart = ["cheesecake", "cheesecake", "almond brownie", "apple pie"];
expect(compliesToItemLimit(cart)).toBe(true);
});
test("returns false for cards with no more than 3 items of a kind", () => {
const cart = [
"cheesecake",
"cheesecake",
"almond brownie",
"cheesecake",
"cheesecake"
];
expect(compliesToItemLimit(cart)).toBe(false);
});
});
================================================
FILE: chapter4/1_setting_up_a_test_environment/1_exposing_modules/3_unit_tests/inventoryController.js
================================================
const inventory = new Map();
const removeFromInventory = item => {
if (!inventory.has(item) || !inventory.get(item) > 0) {
const err = new Error(`${item} is unavailable`);
err.code = 400;
throw err;
}
inventory.set(item, inventory.get(item) - 1);
};
module.exports = { removeFromInventory, inventory };
================================================
FILE: chapter4/1_setting_up_a_test_environment/1_exposing_modules/3_unit_tests/logger.js
================================================
const fs = require("fs");
const logger = {
log: msg => fs.appendFileSync("/tmp/logs.out", msg + "\n")
};
module.exports = logger;
================================================
FILE: chapter4/1_setting_up_a_test_environment/1_exposing_modules/3_unit_tests/package.json
================================================
{
"name": "1_unit_tests",
"version": "1.0.0",
"scripts": {
"test": "jest"
},
"devDependencies": {
"isomorphic-fetch": "^2.2.1",
"jest": "^24.9.0"
},
"dependencies": {
"koa": "^2.11.0",
"koa-router": "^7.4.0"
}
}
================================================
FILE: chapter4/1_setting_up_a_test_environment/1_exposing_modules/3_unit_tests/server.js
================================================
const Koa = require("koa");
const Router = require("koa-router");
const { carts, addItemToCart } = require("./cartController");
const { inventory } = require("./inventoryController");
const app = new Koa();
const router = new Router();
router.get("/carts/:username/items", ctx => {
const cart = carts.get(ctx.params.username);
cart ? (ctx.body = cart) : (ctx.status = 404);
});
router.post("/carts/:username/items/:item", ctx => {
try {
const { username, item } = ctx.params;
const newItems = addItemToCart(username, item);
ctx.body = newItems;
} catch (e) {
ctx.body = { message: e.message };
ctx.status = e.code;
return;
}
});
router.delete("/carts/:username/items/:item", ctx => {
const { username, item } = ctx.params;
if (!carts.has(username) || !carts.get(username).includes(item)) {
ctx.body = { message: `${item} is not in the cart` };
ctx.status = 400;
return;
}
const newItems = (carts.get(username) || []).filter(i => i !== item);
inventory.set(item, (inventory.get(item) || 0) + 1);
carts.set(username, newItems);
ctx.body = newItems;
});
app.use(router.routes());
module.exports = { app: app.listen(3000), carts, inventory };
================================================
FILE: chapter4/1_setting_up_a_test_environment/1_exposing_modules/3_unit_tests/server.test.js
================================================
const { app } = require("./server.js");
const { inventory } = require("./inventoryController.js");
const { carts } = require("./cartController.js");
const fetch = require("isomorphic-fetch");
const apiRoot = "http://localhost:3000";
afterAll(() => app.close());
afterEach(() => inventory.clear());
afterEach(() => carts.clear());
describe("add items to a cart", () => {
test("adding available items", async () => {
inventory.set("cheesecake", 1);
const response = await fetch(
`${apiRoot}/carts/test_user/items/cheesecake`,
{ method: "POST" }
);
expect(response.status).toEqual(200);
expect(await response.json()).toEqual(["cheesecake"]);
expect(inventory.get("cheesecake")).toEqual(0);
expect(carts.get("test_user")).toEqual(["cheesecake"]);
});
test("adding unavailable items", async () => {
carts.set("test_user", []);
const response = await fetch(
`${apiRoot}/carts/test_user/items/cheesecake`,
{ method: "POST" }
);
expect(response.status).toEqual(400);
expect(await response.json()).toEqual({
message: "cheesecake is unavailable"
});
expect(carts.get("test_user")).toEqual([]);
});
});
describe("removing items from a cart", () => {
test("removing existing items", async () => {
carts.set("test_user", ["cheesecake"]);
const response = await fetch(
`${apiRoot}/carts/test_user/items/cheesecake`,
{ method: "DELETE" }
);
expect(response.status).toEqual(200);
expect(await response.json()).toEqual([]);
expect(carts.get("test_user")).toEqual([]);
expect(inventory.get("cheesecake")).toEqual(1);
});
test("removing non-existing items", async () => {
inventory.set("cheesecake", 0);
carts.set("test_user", []);
const response = await fetch(
`${apiRoot}/carts/test_user/items/cheesecake`,
{ method: "DELETE" }
);
expect(response.status).toEqual(400);
expect(await response.json()).toEqual({
message: "cheesecake is not in the cart"
});
expect(inventory.get("cheesecake")).toEqual(0);
});
});
================================================
FILE: chapter4/2_testing_http_endpoints/1_using_supertest/cartController.js
================================================
const { removeFromInventory } = require("./inventoryController");
const logger = require("./logger");
const carts = new Map();
const addItemToCart = (username, item) => {
removeFromInventory(item);
const newItems = (carts.get(username) || []).concat(item);
if (!compliesToItemLimit(newItems)) {
const limitError = new Error(
"You can't have more than three units of an item in your cart"
);
limitError.code = 400;
throw limitError;
}
carts.set(username, newItems);
logger.log(`${item} added to ${username}'s cart`);
return newItems;
};
const compliesToItemLimit = cart => {
const unitsPerItem = cart.reduce((itemMap, itemName) => {
const quantity = (itemMap[itemName] || 0) + 1;
return { ...itemMap, [itemName]: quantity };
}, {});
return Object.values(unitsPerItem).every(quantity => quantity <= 3);
};
module.exports = { addItemToCart, carts, compliesToItemLimit };
================================================
FILE: chapter4/2_testing_http_endpoints/1_using_supertest/cartController.test.js
================================================
const { inventory } = require("./inventoryController");
const {
carts,
addItemToCart,
compliesToItemLimit
} = require("./cartController");
const fs = require("fs");
afterEach(() => inventory.clear());
afterEach(() => carts.clear());
describe("addItemToCart", () => {
beforeEach(() => {
fs.writeFileSync("/tmp/logs.out", "");
});
test("adding unavailable items to cart", () => {
carts.set("test_user", []);
inventory.set("cheesecake", 0);
try {
addItemToCart("test_user", "cheesecake");
} catch (e) {
const expectedError = new Error("cheesecake is unavailable");
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
expect(carts.get("test_user")).toEqual([]);
expect.assertions(2);
});
test("adding items above limit to cart", () => {
const initialCartContent = ["cheesecake", "cheesecake", "cheesecake"];
carts.set("test_user", initialCartContent);
inventory.set("cheesecake", 1);
try {
addItemToCart("test_user", "cheesecake");
} catch (e) {
const expectedError = new Error(
"You can't have more than three units of an item in your cart"
);
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
expect(carts.get("test_user")).toEqual(initialCartContent);
expect.assertions(2);
});
test("logging added items", () => {
carts.set("test_user", []);
inventory.set("cheesecake", 1);
addItemToCart("test_user", "cheesecake");
const logs = fs.readFileSync("/tmp/logs.out", "utf-8");
expect(logs).toContain("cheesecake added to test_user's cart\n");
});
});
describe("compliesToItemLimit", () => {
test("returns true for cards with no more than 3 items of a kind", () => {
const cart = ["cheesecake", "cheesecake", "almond brownie", "apple pie"];
expect(compliesToItemLimit(cart)).toBe(true);
});
test("returns true for cards with no more than 3 items of a kind", () => {
const cart = [
"cheesecake",
"cheesecake",
"almond brownie",
"cheesecake",
"cheesecake"
];
expect(compliesToItemLimit(cart)).toBe(false);
});
});
================================================
FILE: chapter4/2_testing_http_endpoints/1_using_supertest/inventoryController.js
================================================
const inventory = new Map();
const removeFromInventory = item => {
if (!inventory.has(item) || !inventory.get(item) > 0) {
const err = new Error(`${item} is unavailable`);
err.code = 400;
throw err;
}
inventory.set(item, inventory.get(item) - 1);
};
module.exports = { removeFromInventory, inventory };
================================================
FILE: chapter4/2_testing_http_endpoints/1_using_supertest/logger.js
================================================
const fs = require("fs");
const logger = {
log: msg => fs.appendFileSync("/tmp/logs.out", msg + "\n")
};
module.exports = logger;
================================================
FILE: chapter4/2_testing_http_endpoints/1_using_supertest/package.json
================================================
{
"name": "2_using_supertest",
"version": "1.0.0",
"scripts": {
"test": "jest"
},
"devDependencies": {
"jest": "^24.9.0",
"supertest": "^4.0.2"
},
"dependencies": {
"koa": "^2.11.0",
"koa-body-parser": "^1.1.2",
"koa-router": "^7.4.0"
}
}
================================================
FILE: chapter4/2_testing_http_endpoints/1_using_supertest/server.js
================================================
const Koa = require("koa");
const Router = require("koa-router");
const bodyParser = require("koa-body-parser");
const { carts, addItemToCart } = require("./cartController");
const { inventory } = require("./inventoryController");
const app = new Koa();
const router = new Router();
app.use(bodyParser());
router.get("/carts/:username/items", ctx => {
const cart = carts.get(ctx.params.username);
cart ? (ctx.body = cart) : (ctx.status = 404);
});
router.post("/carts/:username/items", ctx => {
const { username } = ctx.params;
const { item, quantity } = ctx.request.body;
for (let i = 0; i < quantity; i++) {
try {
const newItems = addItemToCart(username, item);
ctx.body = newItems;
} catch (e) {
ctx.body = { message: e.message };
ctx.status = e.code;
return;
}
}
});
router.delete("/carts/:username/items/:item", ctx => {
const { username, item } = ctx.params;
if (!carts.has(username) || !carts.get(username).includes(item)) {
ctx.body = { message: `${item} is not in the cart` };
ctx.status = 400;
return;
}
const newItems = (carts.get(username) || []).filter(i => i !== item);
inventory.set(item, (inventory.get(item) || 0) + 1);
carts.set(username, newItems);
ctx.body = newItems;
});
app.use(router.routes());
module.exports = { app: app.listen(3000), carts, inventory };
================================================
FILE: chapter4/2_testing_http_endpoints/1_using_supertest/server.test.js
================================================
const request = require("supertest");
const { app } = require("./server.js");
const { inventory } = require("./inventoryController.js");
const { carts } = require("./cartController.js");
afterAll(() => app.close());
afterEach(() => inventory.clear());
afterEach(() => carts.clear());
describe("add items to a cart", () => {
test("adding available items", async () => {
inventory.set("cheesecake", 3);
const response = await request(app)
.post("/carts/test_user/items")
.send({ item: "cheesecake", quantity: 3 })
.expect(200)
.expect("Content-Type", /json/);
const newItems = ["cheesecake", "cheesecake", "cheesecake"];
expect(response.body).toEqual(newItems);
expect(inventory.get("cheesecake")).toEqual(0);
expect(carts.get("test_user")).toEqual(newItems);
});
test("adding unavailable items", async () => {
carts.set("test_user", []);
const response = await request(app)
.post("/carts/test_user/items")
.send({ item: "cheesecake", quantity: 1 })
.expect(400)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "cheesecake is unavailable"
});
expect(carts.get("test_user")).toEqual([]);
});
});
describe("removing items from a cart", () => {
test("removing existing items", async () => {
carts.set("test_user", ["cheesecake"]);
const response = await request(app)
.del("/carts/test_user/items/cheesecake")
.expect(200)
.expect("Content-Type", /json/);
expect(response.body).toEqual([]);
expect(carts.get("test_user")).toEqual([]);
expect(inventory.get("cheesecake")).toEqual(1);
});
test("removing non-existing items", async () => {
inventory.set("cheesecake", 0);
carts.set("test_user", []);
const response = await request(app)
.del("/carts/test_user/items/cheesecake")
.expect(400)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "cheesecake is not in the cart"
});
expect(inventory.get("cheesecake")).toEqual(0);
});
});
================================================
FILE: chapter4/2_testing_http_endpoints/2_testing_middlewares/authenticationController.js
================================================
const crypto = require("crypto");
const users = new Map();
const hashPassword = password => {
const hash = crypto.createHash("sha256");
hash.update(password);
return hash.digest("hex");
};
const credentialsAreValid = (username, password) => {
const userExists = users.has(username);
if (!userExists) return false;
const currentPasswordHash = users.get(username).passwordHash;
return hashPassword(password) === currentPasswordHash;
};
const authenticationMiddleware = async (ctx, next) => {
try {
const authHeader = ctx.request.headers.authorization;
const credentials = Buffer.from(
authHeader.slice("basic".length + 1),
"base64"
).toString();
const [username, password] = credentials.split(":");
if (!credentialsAreValid(username, password)) {
throw new Error("invalid credentials");
}
} catch (e) {
ctx.status = 401;
ctx.body = { message: "please provide valid credentials" };
return;
}
await next();
};
module.exports = {
users,
hashPassword,
credentialsAreValid,
authenticationMiddleware
};
================================================
FILE: chapter4/2_testing_http_endpoints/2_testing_middlewares/authenticationController.test.js
================================================
const crypto = require("crypto");
const {
users,
hashPassword,
credentialsAreValid,
authenticationMiddleware
} = require("./authenticationController");
afterEach(() => users.clear());
describe("hashPassword", () => {
test("hashing passwords", () => {
const plainTextPassword = "password_example";
const hash = crypto.createHash("sha256");
hash.update(plainTextPassword);
const expectedHash = hash.digest("hex");
expect(hashPassword(plainTextPassword)).toBe(expectedHash);
});
});
describe("credentialsAreValid", () => {
test("validating credentials", () => {
users.set("test_user", {
email: "test_user@example.org",
passwordHash: hashPassword("a_password")
});
expect(credentialsAreValid("test_user", "a_password")).toBe(true);
});
});
describe("authenticationMiddleware", () => {
test("returning an error if the credentials are not valid", async () => {
const fakeAuth = Buffer.from("invalid:credentials").toString("base64");
const ctx = {
request: {
headers: { authorization: `Basic ${fakeAuth}` }
}
};
const next = jest.fn();
await authenticationMiddleware(ctx, next);
expect(next.mock.calls).toHaveLength(0);
expect(ctx).toEqual({
...ctx,
status: 401,
body: { message: "please provide valid credentials" }
});
});
test("authenticating properly", async () => {
users.set("test_user", {
email: "test_user@example.org",
passwordHash: hashPassword("a_password")
});
const validAuth = Buffer.from("test_user:a_password").toString("base64");
const ctx = {
request: {
headers: { authorization: `Basic ${validAuth}` }
}
};
const next = jest.fn();
await authenticationMiddleware(ctx, next);
expect(next.mock.calls).toHaveLength(1);
});
});
================================================
FILE: chapter4/2_testing_http_endpoints/2_testing_middlewares/cartController.js
================================================
const { removeFromInventory } = require("./inventoryController");
const logger = require("./logger");
const carts = new Map();
const addItemToCart = (username, item) => {
removeFromInventory(item);
const newItems = (carts.get(username) || []).concat(item);
if (!compliesToItemLimit(newItems)) {
const limitError = new Error(
"You can't have more than three units of an item in your cart"
);
limitError.code = 400;
throw limitError;
}
carts.set(username, newItems);
logger.log(`${item} added to ${username}'s cart`);
return newItems;
};
const compliesToItemLimit = cart => {
const unitsPerItem = cart.reduce((itemMap, itemName) => {
const quantity = (itemMap[itemName] || 0) + 1;
return { ...itemMap, [itemName]: quantity };
}, {});
return Object.values(unitsPerItem).every(quantity => quantity <= 3);
};
module.exports = { addItemToCart, carts, compliesToItemLimit };
================================================
FILE: chapter4/2_testing_http_endpoints/2_testing_middlewares/cartController.test.js
================================================
const { inventory } = require("./inventoryController");
const {
carts,
addItemToCart,
compliesToItemLimit
} = require("./cartController");
const fs = require("fs");
afterEach(() => inventory.clear());
afterEach(() => carts.clear());
describe("addItemToCart", () => {
beforeEach(() => {
fs.writeFileSync("/tmp/logs.out", "");
});
test("adding unavailable items to cart", () => {
carts.set("test_user", []);
inventory.set("cheesecake", 0);
try {
addItemToCart("test_user", "cheesecake");
} catch (e) {
const expectedError = new Error("cheesecake is unavailable");
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
expect(carts.get("test_user")).toEqual([]);
expect.assertions(2);
});
test("adding items above limit to cart", () => {
const initialCartContent = ["cheesecake", "cheesecake", "cheesecake"];
carts.set("test_user", initialCartContent);
inventory.set("cheesecake", 1);
try {
addItemToCart("test_user", "cheesecake");
} catch (e) {
const expectedError = new Error(
"You can't have more than three units of an item in your cart"
);
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
expect(carts.get("test_user")).toEqual(initialCartContent);
expect.assertions(2);
});
test("logging added items", () => {
carts.set("test_user", []);
inventory.set("cheesecake", 1);
addItemToCart("test_user", "cheesecake");
const logs = fs.readFileSync("/tmp/logs.out", "utf-8");
expect(logs).toContain("cheesecake added to test_user's cart\n");
});
});
describe("compliesToItemLimit", () => {
test("returns true for cards with no more than 3 items of a kind", () => {
const cart = ["cheesecake", "cheesecake", "almond brownie", "apple pie"];
expect(compliesToItemLimit(cart)).toBe(true);
});
test("returns true for cards with no more than 3 items of a kind", () => {
const cart = [
"cheesecake",
"cheesecake",
"almond brownie",
"cheesecake",
"cheesecake"
];
expect(compliesToItemLimit(cart)).toBe(false);
});
});
================================================
FILE: chapter4/2_testing_http_endpoints/2_testing_middlewares/inventoryController.js
================================================
const inventory = new Map();
const removeFromInventory = item => {
if (!inventory.has(item) || !inventory.get(item) > 0) {
const err = new Error(`${item} is unavailable`);
err.code = 400;
throw err;
}
inventory.set(item, inventory.get(item) - 1);
};
module.exports = { removeFromInventory, inventory };
================================================
FILE: chapter4/2_testing_http_endpoints/2_testing_middlewares/logger.js
================================================
const fs = require("fs");
const logger = {
log: msg => fs.appendFileSync("/tmp/logs.out", msg + "\n")
};
module.exports = logger;
================================================
FILE: chapter4/2_testing_http_endpoints/2_testing_middlewares/package.json
================================================
{
"name": "2_testing_middlewares",
"version": "1.0.0",
"scripts": {
"test": "jest"
},
"devDependencies": {
"jest": "^24.9.0",
"supertest": "^4.0.2"
},
"dependencies": {
"koa": "^2.11.0",
"koa-body-parser": "^1.1.2",
"koa-router": "^7.4.0"
}
}
================================================
FILE: chapter4/2_testing_http_endpoints/2_testing_middlewares/server.js
================================================
const Koa = require("koa");
const Router = require("koa-router");
const bodyParser = require("koa-body-parser");
const { carts, addItemToCart } = require("./cartController");
const { inventory } = require("./inventoryController");
const {
users,
hashPassword,
authenticationMiddleware
} = require("./authenticationController");
const app = new Koa();
const router = new Router();
app.use(bodyParser());
app.use(async (ctx, next) => {
if (ctx.url.startsWith("/carts")) {
return await authenticationMiddleware(ctx, next);
}
await next();
});
router.put("/users/:username", ctx => {
const { username } = ctx.params;
const { email, password } = ctx.request.body;
const userAlreadyExists = users.has(username);
if (userAlreadyExists) {
ctx.body = { message: `${username} already exists` };
ctx.status = 409;
return;
}
users.set(username, { email, passwordHash: hashPassword(password) });
return (ctx.body = { message: `${username} created successfully` });
});
router.post("/carts/:username/items", ctx => {
const { username } = ctx.params;
const { item, quantity } = ctx.request.body;
for (let i = 0; i < quantity; i++) {
try {
const newItems = addItemToCart(username, item);
ctx.body = newItems;
} catch (e) {
ctx.body = { message: e.message };
ctx.status = e.code;
return;
}
}
});
router.delete("/carts/:username/items/:item", ctx => {
const { username, item } = ctx.params;
if (!carts.has(username) || !carts.get(username).includes(item)) {
ctx.body = { message: `${item} is not in the cart` };
ctx.status = 400;
return;
}
const newItems = (carts.get(username) || []).filter(i => i !== item);
inventory.set(item, (inventory.get(item) || 0) + 1);
carts.set(username, newItems);
ctx.body = newItems;
});
app.use(router.routes());
module.exports = { app: app.listen(3000), carts, inventory };
================================================
FILE: chapter4/2_testing_http_endpoints/2_testing_middlewares/server.test.js
================================================
const request = require("supertest");
const { app } = require("./server.js");
const { inventory } = require("./inventoryController.js");
const { carts } = require("./cartController.js");
const { users, hashPassword } = require("./authenticationController.js");
afterAll(() => app.close());
afterEach(() => inventory.clear());
afterEach(() => carts.clear());
afterEach(() => users.clear());
const user = "test_user";
const password = "a_password";
const validAuth = Buffer.from(`${user}:${password}`).toString("base64");
const authHeader = `Basic ${validAuth}`;
const createUser = () => {
users.set(user, {
email: "test_user@example.org",
passwordHash: hashPassword(password)
});
};
describe("add items to a cart", () => {
beforeEach(createUser);
test("adding available items", async () => {
inventory.set("cheesecake", 3);
const response = await request(app)
.post("/carts/test_user/items")
.set("authorization", authHeader)
.send({ item: "cheesecake", quantity: 3 })
.expect(200)
.expect("Content-Type", /json/);
const newItems = ["cheesecake", "cheesecake", "cheesecake"];
expect(response.body).toEqual(newItems);
expect(inventory.get("cheesecake")).toEqual(0);
expect(carts.get("test_user")).toEqual(newItems);
});
test("adding unavailable items", async () => {
carts.set("test_user", []);
const response = await request(app)
.post("/carts/test_user/items")
.set("authorization", authHeader)
.send({ item: "cheesecake", quantity: 1 })
.expect(400)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "cheesecake is unavailable"
});
expect(carts.get("test_user")).toEqual([]);
});
});
describe("removing items from a cart", () => {
beforeEach(createUser);
test("removing existing items", async () => {
carts.set("test_user", ["cheesecake"]);
const response = await request(app)
.del("/carts/test_user/items/cheesecake")
.set("authorization", authHeader)
.expect(200)
.expect("Content-Type", /json/);
expect(response.body).toEqual([]);
expect(carts.get("test_user")).toEqual([]);
expect(inventory.get("cheesecake")).toEqual(1);
});
test("removing non-existing items", async () => {
inventory.set("cheesecake", 0);
carts.set("test_user", []);
const response = await request(app)
.del("/carts/test_user/items/cheesecake")
.set("authorization", authHeader)
.expect(400)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "cheesecake is not in the cart"
});
expect(inventory.get("cheesecake")).toEqual(0);
});
});
describe("create accounts", () => {
test("creating a new account", async () => {
const response = await request(app)
.put("/users/test_user")
.send({ email: "test_user@example.org", password: "a_password" })
.expect(200)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "test_user created successfully"
});
expect(users.get("test_user")).toEqual({
email: "test_user@example.org",
passwordHash: hashPassword("a_password")
});
});
test("creating a duplicate account", async () => {
users.set("test_user", {
email: "test_user@example.org",
passwordHash: hashPassword("a_password")
});
const response = await request(app)
.put("/users/test_user")
.send({ email: "test_user@example.org", password: "a_password" })
.expect(409)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "test_user already exists"
});
});
});
================================================
FILE: chapter4/3_dealing_with_external_dependencies/1_database_integrations/authenticationController.js
================================================
const crypto = require("crypto");
const { db } = require("./dbConnection");
const hashPassword = password => {
const hash = crypto.createHash("sha256");
hash.update(password);
return hash.digest("hex");
};
const credentialsAreValid = async (username, password) => {
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) return false;
return hashPassword(password) === user.passwordHash;
};
const authenticationMiddleware = async (ctx, next) => {
try {
const authHeader = ctx.request.headers.authorization;
const credentials = Buffer.from(
authHeader.slice("basic".length + 1),
"base64"
).toString();
const [username, password] = credentials.split(":");
const validCredentialsSent = await credentialsAreValid(username, password);
if (!validCredentialsSent) throw new Error("invalid credentials");
} catch (e) {
ctx.status = 401;
ctx.body = { message: "please provide valid credentials" };
return;
}
await next();
};
module.exports = {
hashPassword,
credentialsAreValid,
authenticationMiddleware
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/1_database_integrations/authenticationController.test.js
================================================
const { db, closeConnection } = require("./dbConnection");
const crypto = require("crypto");
const {
hashPassword,
credentialsAreValid,
authenticationMiddleware
} = require("./authenticationController");
beforeEach(() => db("users").truncate());
describe("hashPassword", () => {
test("hashing passwords", () => {
const plainTextPassword = "password_example";
const hash = crypto.createHash("sha256");
hash.update(plainTextPassword);
const expectedHash = hash.digest("hex");
expect(hashPassword(plainTextPassword)).toBe(expectedHash);
});
});
describe("credentialsAreValid", () => {
test("validating credentials", async () => {
await db("users").insert({
username: "test_user",
email: "test_user@example.org",
passwordHash: hashPassword("a_password")
});
expect(await credentialsAreValid("test_user", "a_password")).toBe(true);
});
});
describe("authenticationMiddleware", () => {
test("returning an error if the credentials are not valid", async () => {
const fakeAuth = Buffer.from("invalid:credentials").toString("base64");
const ctx = {
request: {
headers: { authorization: `Basic ${fakeAuth}` }
}
};
const next = jest.fn();
await authenticationMiddleware(ctx, next);
expect(next.mock.calls).toHaveLength(0);
expect(ctx).toEqual({
...ctx,
status: 401,
body: { message: "please provide valid credentials" }
});
});
test("authenticating properly", async () => {
await db("users").insert({
username: "test_user",
email: "test_user@example.org",
passwordHash: hashPassword("a_password")
});
const validAuth = Buffer.from("test_user:a_password").toString("base64");
const ctx = {
request: {
headers: { authorization: `Basic ${validAuth}` }
}
};
const next = jest.fn();
await authenticationMiddleware(ctx, next);
expect(next.mock.calls).toHaveLength(1);
});
});
================================================
FILE: chapter4/3_dealing_with_external_dependencies/1_database_integrations/cartController.js
================================================
const { db } = require("./dbConnection");
const { removeFromInventory } = require("./inventoryController");
const logger = require("./logger");
const addItemToCart = async (username, itemName) => {
await removeFromInventory(itemName);
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) {
const userNotFound = new Error("user not found");
userNotFound.code = 404;
}
const itemEntry = await db
.select()
.from("carts_items")
.where({ userId: user.id, itemName })
.first();
if (itemEntry && itemEntry.quantity + 1 > 3) {
const limitError = new Error(
"You can't have more than three units of an item in your cart"
);
limitError.code = 400;
throw limitError;
}
if (itemEntry) {
await db("carts_items")
.increment("quantity")
.where({ userId: itemEntry.userId, itemName });
} else {
await db("carts_items").insert({
userId: user.id,
itemName,
quantity: 1
});
}
logger.log(`${itemName} added to ${username}'s cart`);
return db
.select("itemName", "quantity")
.from("carts_items")
.where({ userId: user.id });
};
module.exports = { addItemToCart };
================================================
FILE: chapter4/3_dealing_with_external_dependencies/1_database_integrations/cartController.test.js
================================================
const { db, closeConnection } = require("./dbConnection");
const { addItemToCart } = require("./cartController");
const { hashPassword } = require("./authenticationController");
const fs = require("fs");
beforeEach(() => db("users").truncate());
beforeEach(() => db("carts_items").truncate());
beforeEach(() => db("inventory").truncate());
describe("addItemToCart", () => {
beforeEach(() => {
fs.writeFileSync("/tmp/logs.out", "");
});
test("adding unavailable items to cart", async () => {
await db("users").insert({
username: "test_user",
email: "test_user@example.org",
passwordHash: hashPassword("a_password")
});
await db("inventory").insert({ itemName: "cheesecake", quantity: 0 });
try {
await addItemToCart("test_user", "cheesecake");
} catch (e) {
const expectedError = new Error("cheesecake is unavailable");
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
const finalCartContent = await db
.select("carts_items.*")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", "test_user");
expect(finalCartContent).toEqual([]);
expect.assertions(2);
});
test("adding items above limit to cart", async () => {
await db("users").insert({
username: "test_user",
email: "test_user@example.org",
passwordHash: hashPassword("a_password")
});
const { id: userId } = await db
.select()
.from("users")
.where({ username: "test_user" })
.first();
await db("inventory").insert({ itemName: "cheesecake", quantity: 1 });
await db("carts_items").insert({
userId,
itemName: "cheesecake",
quantity: 3
});
try {
await addItemToCart("test_user", "cheesecake");
} catch (e) {
const expectedError = new Error(
"You can't have more than three units of an item in your cart"
);
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", "test_user");
expect(finalCartContent).toEqual([{ itemName: "cheesecake", quantity: 3 }]);
expect.assertions(2);
});
test("logging added items", async () => {
await db("users").insert({
username: "test_user",
email: "test_user@example.org",
passwordHash: hashPassword("a_password")
});
const { id: userId } = await db
.select()
.from("users")
.where({ username: "test_user" })
.first();
await db("inventory").insert({ itemName: "cheesecake", quantity: 1 });
await db("carts_items").insert({
userId,
itemName: "cheesecake",
quantity: 1
});
await addItemToCart("test_user", "cheesecake");
const logs = fs.readFileSync("/tmp/logs.out", "utf-8");
expect(logs).toContain("cheesecake added to test_user's cart\n");
});
});
================================================
FILE: chapter4/3_dealing_with_external_dependencies/1_database_integrations/dbConnection.js
================================================
const knex = require("knex");
const knexConfig = require("./knexfile").development;
const db = knex(knexConfig);
const closeConnection = () => db.destroy();
module.exports = {
db,
closeConnection
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/1_database_integrations/inventoryController.js
================================================
const { db } = require("./dbConnection");
const removeFromInventory = async itemName => {
const inventoryEntry = await db
.select()
.from("inventory")
.where({ itemName })
.first();
if (!inventoryEntry || inventoryEntry.quantity === 0) {
const err = new Error(`${itemName} is unavailable`);
err.code = 400;
throw err;
}
await db("inventory")
.decrement("quantity")
.where({ itemName });
};
module.exports = { removeFromInventory };
================================================
FILE: chapter4/3_dealing_with_external_dependencies/1_database_integrations/knexfile.js
================================================
module.exports = {
development: {
client: "sqlite3",
connection: { filename: "./dev.sqlite" },
useNullAsDefault: true
}
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/1_database_integrations/logger.js
================================================
const fs = require("fs");
const logger = {
log: msg => fs.appendFileSync("/tmp/logs.out", msg + "\n")
};
module.exports = logger;
================================================
FILE: chapter4/3_dealing_with_external_dependencies/1_database_integrations/migrations/20200325082401_initial_schema.js
================================================
exports.up = async knex => {
await knex.schema.createTable("users", table => {
table.increments("id");
table.string("username");
table.unique("username");
table.string("email");
table.string("passwordHash");
});
await knex.schema.createTable("carts_items", table => {
table.integer("userId").references("users.id");
table.string("itemName");
table.unique("itemName");
table.integer("quantity");
});
await knex.schema.createTable("inventory", table => {
table.increments("id");
table.string("itemName");
table.unique("itemName");
table.integer("quantity");
});
};
exports.down = async knex => {
await knex.schema.dropTable("users");
await knex.schema.dropTable("carts_items");
await knex.schema.dropTable("inventory");
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/1_database_integrations/package.json
================================================
{
"name": "1_database_integrations",
"version": "1.0.0",
"scripts": {
"test": "jest --runInBand"
},
"devDependencies": {
"jest": "^24.9.0",
"supertest": "^4.0.2"
},
"dependencies": {
"knex": "^0.20.13",
"koa": "^2.11.0",
"koa-body-parser": "^1.1.2",
"koa-router": "^7.4.0",
"sqlite3": "^4.1.1"
}
}
================================================
FILE: chapter4/3_dealing_with_external_dependencies/1_database_integrations/server.js
================================================
const Koa = require("koa");
const Router = require("koa-router");
const bodyParser = require("koa-body-parser");
const { db } = require("./dbConnection");
const { addItemToCart } = require("./cartController");
const {
hashPassword,
authenticationMiddleware
} = require("./authenticationController");
const app = new Koa();
const router = new Router();
app.use(bodyParser());
app.use(async (ctx, next) => {
if (ctx.url.startsWith("/carts")) {
return await authenticationMiddleware(ctx, next);
}
await next();
});
router.put("/users/:username", async ctx => {
const { username } = ctx.params;
const { email, password } = ctx.request.body;
const userAlreadyExists = await db
.select()
.from("users")
.where({ username })
.first();
if (userAlreadyExists) {
ctx.body = { message: `${username} already exists` };
ctx.status = 409;
return;
}
await db("users").insert({
username,
email,
passwordHash: hashPassword(password)
});
return (ctx.body = { message: `${username} created successfully` });
});
router.post("/carts/:username/items", async ctx => {
const { username } = ctx.params;
const { item, quantity } = ctx.request.body;
for (let i = 0; i < quantity; i++) {
try {
const newItems = await addItemToCart(username, item);
ctx.body = newItems;
} catch (e) {
ctx.body = { message: e.message };
ctx.status = e.code;
return;
}
}
});
router.delete("/carts/:username/items/:item", async ctx => {
const { username, item } = ctx.params;
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) {
ctx.body = { message: "user not found" };
ctx.status = 404;
return;
}
const itemEntry = await db
.select()
.from("carts_items")
.where({ userId: user.id, itemName: item })
.first();
if (!itemEntry || itemEntry.quantity === 0) {
ctx.body = { message: `${item} is not in the cart` };
ctx.status = 400;
return;
}
await db("carts_items")
.decrement("quantity")
.where({ userId: user.id, itemName: item });
const inventoryEntry = await db
.select()
.from("inventory")
.where({ itemName: item })
.first();
if (inventoryEntry) {
await db("inventory")
.increment("quantity")
.where({ userId: itemEntry.userId, itemName: item });
} else {
await db("inventory").insert({ itemName: item, quantity: 1 });
}
ctx.body = await db
.select("itemName", "quantity")
.from("carts_items")
.where({ userId: user.id });
});
app.use(router.routes());
module.exports = { app: app.listen(3000) };
================================================
FILE: chapter4/3_dealing_with_external_dependencies/1_database_integrations/server.test.js
================================================
const { db, closeConnection } = require("./dbConnection");
const request = require("supertest");
const { app } = require("./server.js");
const { hashPassword } = require("./authenticationController.js");
afterAll(() => app.close());
beforeEach(() => db("users").truncate());
beforeEach(() => db("carts_items").truncate());
beforeEach(() => db("inventory").truncate());
const username = "test_user";
const password = "a_password";
const validAuth = Buffer.from(`${username}:${password}`).toString("base64");
const authHeader = `Basic ${validAuth}`;
const createUser = async () => {
return await db("users").insert({
username,
email: "test_user@example.org",
passwordHash: hashPassword(password)
});
};
describe("add items to a cart", () => {
beforeEach(createUser);
test("adding available items", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 3 });
const response = await request(app)
.post("/carts/test_user/items")
.set("authorization", authHeader)
.send({ item: "cheesecake", quantity: 3 })
.expect(200)
.expect("Content-Type", /json/);
const newItems = [{ itemName: "cheesecake", quantity: 3 }];
expect(response.body).toEqual(newItems);
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(0);
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", "test_user");
expect(finalCartContent).toEqual(newItems);
});
test("adding unavailable items", async () => {
const response = await request(app)
.post("/carts/test_user/items")
.set("authorization", authHeader)
.send({ item: "cheesecake", quantity: 1 })
.expect(400)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "cheesecake is unavailable"
});
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", "test_user");
expect(finalCartContent).toEqual([]);
});
});
describe("removing items from a cart", () => {
let userId;
beforeEach(async () => {
userId = (await createUser())[0];
});
test("removing existing items", async () => {
await db("carts_items").insert({
userId,
itemName: "cheesecake",
quantity: 1
});
const response = await request(app)
.del("/carts/test_user/items/cheesecake")
.set("authorization", authHeader)
.expect(200)
.expect("Content-Type", /json/);
const expectedFinalContent = [{ itemName: "cheesecake", quantity: 0 }];
expect(response.body).toEqual(expectedFinalContent);
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", "test_user");
expect(finalCartContent).toEqual(expectedFinalContent);
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(1);
});
test("removing non-existing items", async () => {
await db("inventory").insert({
itemName: "cheesecake",
quantity: 0
});
const response = await request(app)
.del("/carts/test_user/items/cheesecake")
.set("authorization", authHeader)
.expect(400)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "cheesecake is not in the cart"
});
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(0);
});
});
describe("create accounts", () => {
test("creating a new account", async () => {
const response = await request(app)
.put("/users/test_user")
.send({ email: "test_user@example.org", password: "a_password" })
.expect(200)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "test_user created successfully"
});
const savedUser = await db
.select("email", "passwordHash")
.from("users")
.where({ username: "test_user" })
.first();
expect(savedUser).toEqual({
email: "test_user@example.org",
passwordHash: hashPassword("a_password")
});
});
test("creating a duplicate account", async () => {
await createUser();
const response = await request(app)
.put("/users/test_user")
.send({ email: "test_user@example.org", password: "a_password" })
.expect(409)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "test_user already exists"
});
});
});
================================================
FILE: chapter4/3_dealing_with_external_dependencies/2_separate_database_instances/authenticationController.js
================================================
const crypto = require("crypto");
const { db } = require("./dbConnection");
const hashPassword = password => {
const hash = crypto.createHash("sha256");
hash.update(password);
return hash.digest("hex");
};
const credentialsAreValid = async (username, password) => {
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) return false;
return hashPassword(password) === user.passwordHash;
};
const authenticationMiddleware = async (ctx, next) => {
try {
const authHeader = ctx.request.headers.authorization;
const credentials = Buffer.from(
authHeader.slice("basic".length + 1),
"base64"
).toString();
const [username, password] = credentials.split(":");
const validCredentialsSent = await credentialsAreValid(username, password);
if (!validCredentialsSent) throw new Error("invalid credentials");
} catch (e) {
ctx.status = 401;
ctx.body = { message: "please provide valid credentials" };
return;
}
await next();
};
module.exports = {
hashPassword,
credentialsAreValid,
authenticationMiddleware
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/2_separate_database_instances/authenticationController.test.js
================================================
const { db, closeConnection } = require("./dbConnection");
const crypto = require("crypto");
const {
hashPassword,
credentialsAreValid,
authenticationMiddleware
} = require("./authenticationController");
beforeEach(() => db("users").truncate());
describe("hashPassword", () => {
test("hashing passwords", () => {
const plainTextPassword = "password_example";
const hash = crypto.createHash("sha256");
hash.update(plainTextPassword);
const expectedHash = hash.digest("hex");
expect(hashPassword(plainTextPassword)).toBe(expectedHash);
});
});
describe("credentialsAreValid", () => {
test("validating credentials", async () => {
await db("users").insert({
username: "test_user",
email: "test_user@example.org",
passwordHash: hashPassword("a_password")
});
expect(await credentialsAreValid("test_user", "a_password")).toBe(true);
});
});
describe("authenticationMiddleware", () => {
test("returning an error if the credentials are not valid", async () => {
const fakeAuth = Buffer.from("invalid:credentials").toString("base64");
const ctx = {
request: {
headers: { authorization: `Basic ${fakeAuth}` }
}
};
const next = jest.fn();
await authenticationMiddleware(ctx, next);
expect(next.mock.calls).toHaveLength(0);
expect(ctx).toEqual({
...ctx,
status: 401,
body: { message: "please provide valid credentials" }
});
});
test("authenticating properly", async () => {
await db("users").insert({
username: "test_user",
email: "test_user@example.org",
passwordHash: hashPassword("a_password")
});
const validAuth = Buffer.from("test_user:a_password").toString("base64");
const ctx = {
request: {
headers: { authorization: `Basic ${validAuth}` }
}
};
const next = jest.fn();
await authenticationMiddleware(ctx, next);
expect(next.mock.calls).toHaveLength(1);
});
});
================================================
FILE: chapter4/3_dealing_with_external_dependencies/2_separate_database_instances/cartController.js
================================================
const { db } = require("./dbConnection");
const { removeFromInventory } = require("./inventoryController");
const logger = require("./logger");
const addItemToCart = async (username, itemName) => {
await removeFromInventory(itemName);
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) {
const userNotFound = new Error("user not found");
userNotFound.code = 404;
}
const itemEntry = await db
.select()
.from("carts_items")
.where({ userId: user.id, itemName })
.first();
if (itemEntry && itemEntry.quantity + 1 > 3) {
const limitError = new Error(
"You can't have more than three units of an item in your cart"
);
limitError.code = 400;
throw limitError;
}
if (itemEntry) {
await db("carts_items")
.increment("quantity")
.where({ userId: itemEntry.userId, itemName });
} else {
await db("carts_items").insert({
userId: user.id,
itemName,
quantity: 1
});
}
logger.log(`${itemName} added to ${username}'s cart`);
return db
.select("itemName", "quantity")
.from("carts_items")
.where({ userId: user.id });
};
module.exports = { addItemToCart };
================================================
FILE: chapter4/3_dealing_with_external_dependencies/2_separate_database_instances/cartController.test.js
================================================
const { db, closeConnection } = require("./dbConnection");
const { addItemToCart } = require("./cartController");
const { hashPassword } = require("./authenticationController");
const fs = require("fs");
beforeEach(() => db("users").truncate());
beforeEach(() => db("carts_items").truncate());
beforeEach(() => db("inventory").truncate());
describe("addItemToCart", () => {
beforeEach(() => {
fs.writeFileSync("/tmp/logs.out", "");
});
test("adding unavailable items to cart", async () => {
await db("users").insert({
username: "test_user",
email: "test_user@example.org",
passwordHash: hashPassword("a_password")
});
await db("inventory").insert({ itemName: "cheesecake", quantity: 0 });
try {
await addItemToCart("test_user", "cheesecake");
} catch (e) {
const expectedError = new Error("cheesecake is unavailable");
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
const finalCartContent = await db
.select("carts_items.*")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", "test_user");
expect(finalCartContent).toEqual([]);
expect.assertions(2);
});
test("adding items above limit to cart", async () => {
await db("users").insert({
username: "test_user",
email: "test_user@example.org",
passwordHash: hashPassword("a_password")
});
const { id: userId } = await db
.select()
.from("users")
.where({ username: "test_user" })
.first();
await db("inventory").insert({ itemName: "cheesecake", quantity: 1 });
await db("carts_items").insert({
userId,
itemName: "cheesecake",
quantity: 3
});
try {
await addItemToCart("test_user", "cheesecake");
} catch (e) {
const expectedError = new Error(
"You can't have more than three units of an item in your cart"
);
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", "test_user");
expect(finalCartContent).toEqual([{ itemName: "cheesecake", quantity: 3 }]);
expect.assertions(2);
});
test("logging added items", async () => {
await db("users").insert({
username: "test_user",
email: "test_user@example.org",
passwordHash: hashPassword("a_password")
});
const { id: userId } = await db
.select()
.from("users")
.where({ username: "test_user" })
.first();
await db("inventory").insert({ itemName: "cheesecake", quantity: 1 });
await db("carts_items").insert({
userId,
itemName: "cheesecake",
quantity: 1
});
await addItemToCart("test_user", "cheesecake");
const logs = fs.readFileSync("/tmp/logs.out", "utf-8");
expect(logs).toContain("cheesecake added to test_user's cart\n");
});
});
================================================
FILE: chapter4/3_dealing_with_external_dependencies/2_separate_database_instances/dbConnection.js
================================================
const knex = require("knex");
const knexConfig = require("./knexfile").development;
const db = knex(knexConfig);
const closeConnection = () => db.destroy();
module.exports = {
db,
closeConnection
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/2_separate_database_instances/inventoryController.js
================================================
const { db } = require("./dbConnection");
const removeFromInventory = async itemName => {
const inventoryEntry = await db
.select()
.from("inventory")
.where({ itemName })
.first();
if (!inventoryEntry || inventoryEntry.quantity === 0) {
const err = new Error(`${itemName} is unavailable`);
err.code = 400;
throw err;
}
await db("inventory")
.decrement("quantity")
.where({ itemName });
};
module.exports = { removeFromInventory };
================================================
FILE: chapter4/3_dealing_with_external_dependencies/2_separate_database_instances/knexfile.js
================================================
module.exports = {
test: {
client: "sqlite3",
connection: { filename: "./test.sqlite" },
useNullAsDefault: true
},
development: {
client: "sqlite3",
connection: { filename: "./dev.sqlite" },
useNullAsDefault: true
}
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/2_separate_database_instances/logger.js
================================================
const fs = require("fs");
const logger = {
log: msg => fs.appendFileSync("/tmp/logs.out", msg + "\n")
};
module.exports = logger;
================================================
FILE: chapter4/3_dealing_with_external_dependencies/2_separate_database_instances/migrations/20200325082401_initial_schema.js
================================================
exports.up = async knex => {
await knex.schema.createTable("users", table => {
table.increments("id");
table.string("username");
table.unique("username");
table.string("email");
table.string("passwordHash");
});
await knex.schema.createTable("carts_items", table => {
table.integer("userId").references("users.id");
table.string("itemName");
table.unique("itemName");
table.integer("quantity");
});
await knex.schema.createTable("inventory", table => {
table.increments("id");
table.string("itemName");
table.unique("itemName");
table.integer("quantity");
});
};
exports.down = async knex => {
await knex.schema.dropTable("users");
await knex.schema.dropTable("carts_items");
await knex.schema.dropTable("inventory");
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/2_separate_database_instances/package.json
================================================
{
"name": "2_separate_database_instances",
"version": "1.0.0",
"scripts": {
"test": "jest --runInBand"
},
"devDependencies": {
"jest": "^24.9.0",
"supertest": "^4.0.2"
},
"dependencies": {
"knex": "^0.20.13",
"koa": "^2.11.0",
"koa-body-parser": "^1.1.2",
"koa-router": "^7.4.0",
"sqlite3": "^4.1.1"
}
}
================================================
FILE: chapter4/3_dealing_with_external_dependencies/2_separate_database_instances/server.js
================================================
const Koa = require("koa");
const Router = require("koa-router");
const bodyParser = require("koa-body-parser");
const { db } = require("./dbConnection");
const { addItemToCart } = require("./cartController");
const {
hashPassword,
authenticationMiddleware
} = require("./authenticationController");
const app = new Koa();
const router = new Router();
app.use(bodyParser());
app.use(async (ctx, next) => {
if (ctx.url.startsWith("/carts")) {
return await authenticationMiddleware(ctx, next);
}
await next();
});
router.put("/users/:username", async ctx => {
const { username } = ctx.params;
const { email, password } = ctx.request.body;
const userAlreadyExists = await db
.select()
.from("users")
.where({ username })
.first();
if (userAlreadyExists) {
ctx.body = { message: `${username} already exists` };
ctx.status = 409;
return;
}
await db("users").insert({
username,
email,
passwordHash: hashPassword(password)
});
return (ctx.body = { message: `${username} created successfully` });
});
router.post("/carts/:username/items", async ctx => {
const { username } = ctx.params;
const { item, quantity } = ctx.request.body;
for (let i = 0; i < quantity; i++) {
try {
const newItems = await addItemToCart(username, item);
ctx.body = newItems;
} catch (e) {
ctx.body = { message: e.message };
ctx.status = e.code;
return;
}
}
});
router.delete("/carts/:username/items/:item", async ctx => {
const { username, item } = ctx.params;
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) {
ctx.body = { message: "user not found" };
ctx.status = 404;
return;
}
const itemEntry = await db
.select()
.from("carts_items")
.where({ userId: user.id, itemName: item })
.first();
if (!itemEntry || itemEntry.quantity === 0) {
ctx.body = { message: `${item} is not in the cart` };
ctx.status = 400;
return;
}
await db("carts_items")
.decrement("quantity")
.where({ userId: user.id, itemName: item });
const inventoryEntry = await db
.select()
.from("inventory")
.where({ itemName: item })
.first();
if (inventoryEntry) {
await db("inventory")
.increment("quantity")
.where({ userId: itemEntry.userId, itemName: item });
} else {
await db("inventory").insert({ itemName: item, quantity: 1 });
}
ctx.body = await db
.select("itemName", "quantity")
.from("carts_items")
.where({ userId: user.id });
});
app.use(router.routes());
module.exports = { app: app.listen(3000) };
================================================
FILE: chapter4/3_dealing_with_external_dependencies/2_separate_database_instances/server.test.js
================================================
const { db, closeConnection } = require("./dbConnection");
const request = require("supertest");
const { app } = require("./server.js");
const { hashPassword } = require("./authenticationController.js");
afterAll(() => app.close());
beforeEach(() => db("users").truncate());
beforeEach(() => db("carts_items").truncate());
beforeEach(() => db("inventory").truncate());
const username = "test_user";
const password = "a_password";
const validAuth = Buffer.from(`${username}:${password}`).toString("base64");
const authHeader = `Basic ${validAuth}`;
const createUser = async () => {
return await db("users").insert({
username,
email: "test_user@example.org",
passwordHash: hashPassword(password)
});
};
describe("add items to a cart", () => {
beforeEach(createUser);
test("adding available items", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 3 });
const response = await request(app)
.post("/carts/test_user/items")
.set("authorization", authHeader)
.send({ item: "cheesecake", quantity: 3 })
.expect(200)
.expect("Content-Type", /json/);
const newItems = [{ itemName: "cheesecake", quantity: 3 }];
expect(response.body).toEqual(newItems);
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(0);
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", "test_user");
expect(finalCartContent).toEqual(newItems);
});
test("adding unavailable items", async () => {
const response = await request(app)
.post("/carts/test_user/items")
.set("authorization", authHeader)
.send({ item: "cheesecake", quantity: 1 })
.expect(400)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "cheesecake is unavailable"
});
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", "test_user");
expect(finalCartContent).toEqual([]);
});
});
describe("removing items from a cart", () => {
let userId;
beforeEach(async () => {
userId = (await createUser())[0];
});
test("removing existing items", async () => {
await db("carts_items").insert({
userId,
itemName: "cheesecake",
quantity: 1
});
const response = await request(app)
.del("/carts/test_user/items/cheesecake")
.set("authorization", authHeader)
.expect(200)
.expect("Content-Type", /json/);
const expectedFinalContent = [{ itemName: "cheesecake", quantity: 0 }];
expect(response.body).toEqual(expectedFinalContent);
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", "test_user");
expect(finalCartContent).toEqual(expectedFinalContent);
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(1);
});
test("removing non-existing items", async () => {
await db("inventory").insert({
itemName: "cheesecake",
quantity: 0
});
const response = await request(app)
.del("/carts/test_user/items/cheesecake")
.set("authorization", authHeader)
.expect(400)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "cheesecake is not in the cart"
});
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(0);
});
});
describe("create accounts", () => {
test("creating a new account", async () => {
const response = await request(app)
.put("/users/test_user")
.send({ email: "test_user@example.org", password: "a_password" })
.expect(200)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "test_user created successfully"
});
const savedUser = await db
.select("email", "passwordHash")
.from("users")
.where({ username: "test_user" })
.first();
expect(savedUser).toEqual({
email: "test_user@example.org",
passwordHash: hashPassword("a_password")
});
});
test("creating a duplicate account", async () => {
await createUser();
const response = await request(app)
.put("/users/test_user")
.send({ email: "test_user@example.org", password: "a_password" })
.expect(409)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "test_user already exists"
});
});
});
================================================
FILE: chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/authenticationController.js
================================================
const crypto = require("crypto");
const { db } = require("./dbConnection");
const hashPassword = password => {
const hash = crypto.createHash("sha256");
hash.update(password);
return hash.digest("hex");
};
const credentialsAreValid = async (username, password) => {
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) return false;
return hashPassword(password) === user.passwordHash;
};
const authenticationMiddleware = async (ctx, next) => {
try {
const authHeader = ctx.request.headers.authorization;
const credentials = Buffer.from(
authHeader.slice("basic".length + 1),
"base64"
).toString();
const [username, password] = credentials.split(":");
const validCredentialsSent = await credentialsAreValid(username, password);
if (!validCredentialsSent) throw new Error("invalid credentials");
} catch (e) {
ctx.status = 401;
ctx.body = { message: "please provide valid credentials" };
return;
}
await next();
};
module.exports = {
hashPassword,
credentialsAreValid,
authenticationMiddleware
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/authenticationController.test.js
================================================
const crypto = require("crypto");
const {
hashPassword,
credentialsAreValid,
authenticationMiddleware
} = require("./authenticationController");
const { user: globalUser } = require("./userTestUtils");
describe("hashPassword", () => {
test("hashing passwords", () => {
const plainTextPassword = "password_example";
const hash = crypto.createHash("sha256");
hash.update(plainTextPassword);
const expectedHash = hash.digest("hex");
expect(hashPassword(plainTextPassword)).toBe(expectedHash);
});
});
describe("credentialsAreValid", () => {
test("validating credentials", async () => {
expect(await credentialsAreValid(globalUser.username, "a_password")).toBe(
true
);
});
});
describe("authenticationMiddleware", () => {
test("returning an error if the credentials are not valid", async () => {
const fakeAuth = Buffer.from("invalid:credentials").toString("base64");
const ctx = {
request: {
headers: { authorization: `Basic ${fakeAuth}` }
}
};
const next = jest.fn();
await authenticationMiddleware(ctx, next);
expect(next.mock.calls).toHaveLength(0);
expect(ctx).toEqual({
...ctx,
status: 401,
body: { message: "please provide valid credentials" }
});
});
test("authenticating properly", async () => {
const ctx = {
request: {
headers: { authorization: globalUser.authHeader }
}
};
const next = jest.fn();
await authenticationMiddleware(ctx, next);
expect(next.mock.calls).toHaveLength(1);
});
});
================================================
FILE: chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/cartController.js
================================================
const { db } = require("./dbConnection");
const { removeFromInventory } = require("./inventoryController");
const logger = require("./logger");
const addItemToCart = async (username, itemName) => {
await removeFromInventory(itemName);
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) {
const userNotFound = new Error("user not found");
userNotFound.code = 404;
}
const itemEntry = await db
.select()
.from("carts_items")
.where({ userId: user.id, itemName })
.first();
if (itemEntry && itemEntry.quantity + 1 > 3) {
const limitError = new Error(
"You can't have more than three units of an item in your cart"
);
limitError.code = 400;
throw limitError;
}
if (itemEntry) {
await db("carts_items")
.increment("quantity")
.where({ userId: itemEntry.userId, itemName });
} else {
await db("carts_items").insert({
userId: user.id,
itemName,
quantity: 1
});
}
logger.log(`${itemName} added to ${username}'s cart`);
return db
.select("itemName", "quantity")
.from("carts_items")
.where({ userId: user.id });
};
module.exports = { addItemToCart };
================================================
FILE: chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/cartController.test.js
================================================
const { db } = require("./dbConnection");
const { addItemToCart } = require("./cartController");
const { hashPassword } = require("./authenticationController");
const { user: globalUser } = require("./userTestUtils");
const fs = require("fs");
describe("addItemToCart", () => {
beforeEach(() => {
fs.writeFileSync("/tmp/logs.out", "");
});
test("adding unavailable items to cart", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 0 });
try {
await addItemToCart(globalUser.username, "cheesecake");
} catch (e) {
const expectedError = new Error("cheesecake is unavailable");
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
const finalCartContent = await db
.select("carts_items.*")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([]);
expect.assertions(2);
});
test("adding items above limit to cart", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 1 });
await db("carts_items").insert({
userId: globalUser.id,
itemName: "cheesecake",
quantity: 3
});
try {
await addItemToCart(globalUser.username, "cheesecake");
} catch (e) {
const expectedError = new Error(
"You can't have more than three units of an item in your cart"
);
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([{ itemName: "cheesecake", quantity: 3 }]);
expect.assertions(2);
});
test("logging added items", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 1 });
await db("carts_items").insert({
userId: globalUser.id,
itemName: "cheesecake",
quantity: 1
});
await addItemToCart(globalUser.username, "cheesecake");
const logs = fs.readFileSync("/tmp/logs.out", "utf-8");
expect(logs).toContain(
`cheesecake added to ${globalUser.username}'s cart\n`
);
});
});
================================================
FILE: chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/dbConnection.js
================================================
const environmentName = process.env.NODE_ENV;
const knex = require("knex");
const knexConfig = require("./knexfile")[environmentName];
const db = knex(knexConfig);
const closeConnection = () => db.destroy();
module.exports = {
db,
closeConnection
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/disconnectFromDb.js
================================================
const { db } = require("./dbConnection");
afterAll(() => db.destroy());
================================================
FILE: chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/inventoryController.js
================================================
const { db } = require("./dbConnection");
const removeFromInventory = async itemName => {
const inventoryEntry = await db
.select()
.from("inventory")
.where({ itemName })
.first();
if (!inventoryEntry || inventoryEntry.quantity === 0) {
const err = new Error(`${itemName} is unavailable`);
err.code = 400;
throw err;
}
await db("inventory")
.decrement("quantity")
.where({ itemName });
};
module.exports = { removeFromInventory };
================================================
FILE: chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/jest.config.js
================================================
module.exports = {
testEnvironment: "node",
globalSetup: "./migrateDatabases.js",
setupFilesAfterEnv: [
"/truncateTables.js",
"/seedUser.js",
"/disconnectFromDb.js"
]
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/knexfile.js
================================================
module.exports = {
test: {
client: "sqlite3",
connection: { filename: "./test.sqlite" },
useNullAsDefault: true
},
development: {
client: "sqlite3",
connection: { filename: "./dev.sqlite" },
useNullAsDefault: true
}
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/logger.js
================================================
const fs = require("fs");
const logger = {
log: msg => fs.appendFileSync("/tmp/logs.out", msg + "\n")
};
module.exports = logger;
================================================
FILE: chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/migrateDatabases.js
================================================
const environmentName = process.env.NODE_ENV || "test";
const environmentConfig = require("./knexfile")[environmentName];
const db = require("knex")(environmentConfig);
module.exports = async () => {
// Migrate the database to the latest state
await db.migrate.latest();
// Close the connection to the database so that tests won't hang
await db.destroy();
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/migrations/20200325082401_initial_schema.js
================================================
exports.up = async knex => {
await knex.schema.createTable("users", table => {
table.increments("id");
table.string("username");
table.unique("username");
table.string("email");
table.string("passwordHash");
});
await knex.schema.createTable("carts_items", table => {
table.integer("userId").references("users.id");
table.string("itemName");
table.unique("itemName");
table.integer("quantity");
});
await knex.schema.createTable("inventory", table => {
table.increments("id");
table.string("itemName");
table.unique("itemName");
table.integer("quantity");
});
};
exports.down = async knex => {
await knex.schema.dropTable("users");
await knex.schema.dropTable("carts_items");
await knex.schema.dropTable("inventory");
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/package.json
================================================
{
"name": "3_maintaining_a_pristine_state",
"version": "1.0.0",
"scripts": {
"test": "jest --runInBand"
},
"devDependencies": {
"jest": "^24.9.0",
"supertest": "^4.0.2"
},
"dependencies": {
"knex": "^0.20.13",
"koa": "^2.11.0",
"koa-body-parser": "^1.1.2",
"koa-router": "^7.4.0",
"sqlite3": "^4.1.1"
}
}
================================================
FILE: chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/seedUser.js
================================================
const { createUser } = require("./userTestUtils");
beforeEach(createUser);
================================================
FILE: chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/server.js
================================================
const Koa = require("koa");
const Router = require("koa-router");
const bodyParser = require("koa-body-parser");
const { db } = require("./dbConnection");
const { addItemToCart } = require("./cartController");
const {
hashPassword,
authenticationMiddleware
} = require("./authenticationController");
const app = new Koa();
const router = new Router();
app.use(bodyParser());
app.use(async (ctx, next) => {
if (ctx.url.startsWith("/carts")) {
return await authenticationMiddleware(ctx, next);
}
await next();
});
router.put("/users/:username", async ctx => {
const { username } = ctx.params;
const { email, password } = ctx.request.body;
const userAlreadyExists = await db
.select()
.from("users")
.where({ username })
.first();
if (userAlreadyExists) {
ctx.body = { message: `${username} already exists` };
ctx.status = 409;
return;
}
await db("users").insert({
username,
email,
passwordHash: hashPassword(password)
});
return (ctx.body = { message: `${username} created successfully` });
});
router.post("/carts/:username/items", async ctx => {
const { username } = ctx.params;
const { item, quantity } = ctx.request.body;
for (let i = 0; i < quantity; i++) {
try {
const newItems = await addItemToCart(username, item);
ctx.body = newItems;
} catch (e) {
ctx.body = { message: e.message };
ctx.status = e.code;
return;
}
}
});
router.delete("/carts/:username/items/:item", async ctx => {
const { username, item } = ctx.params;
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) {
ctx.body = { message: "user not found" };
ctx.status = 404;
return;
}
const itemEntry = await db
.select()
.from("carts_items")
.where({ userId: user.id, itemName: item })
.first();
if (!itemEntry || itemEntry.quantity === 0) {
ctx.body = { message: `${item} is not in the cart` };
ctx.status = 400;
return;
}
await db("carts_items")
.decrement("quantity")
.where({ userId: user.id, itemName: item });
const inventoryEntry = await db
.select()
.from("inventory")
.where({ itemName: item })
.first();
if (inventoryEntry) {
await db("inventory")
.increment("quantity")
.where({ userId: itemEntry.userId, itemName: item });
} else {
await db("inventory").insert({ itemName: item, quantity: 1 });
}
ctx.body = await db
.select("itemName", "quantity")
.from("carts_items")
.where({ userId: user.id });
});
app.use(router.routes());
module.exports = { app: app.listen(3000) };
================================================
FILE: chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/server.test.js
================================================
const { user: globalUser } = require("./userTestUtils");
const { db } = require("./dbConnection");
const request = require("supertest");
const { app } = require("./server.js");
const { hashPassword } = require("./authenticationController.js");
afterAll(() => app.close());
describe("add items to a cart", () => {
test("adding available items", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 3 });
const response = await request(app)
.post(`/carts/${globalUser.username}/items`)
.set("authorization", globalUser.authHeader)
.send({ item: "cheesecake", quantity: 3 })
.expect(200)
.expect("Content-Type", /json/);
const newItems = [{ itemName: "cheesecake", quantity: 3 }];
expect(response.body).toEqual(newItems);
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(0);
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual(newItems);
});
test("adding unavailable items", async () => {
const response = await request(app)
.post(`/carts/${globalUser.username}/items`)
.set("authorization", globalUser.authHeader)
.send({ item: "cheesecake", quantity: 1 })
.expect(400)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "cheesecake is unavailable"
});
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([]);
});
});
describe("removing items from a cart", () => {
test("removing existing items", async () => {
await db("carts_items").insert({
userId: globalUser.id,
itemName: "cheesecake",
quantity: 1
});
const response = await request(app)
.del(`/carts/${globalUser.username}/items/cheesecake`)
.set("authorization", globalUser.authHeader)
.expect(200)
.expect("Content-Type", /json/);
const expectedFinalContent = [{ itemName: "cheesecake", quantity: 0 }];
expect(response.body).toEqual(expectedFinalContent);
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual(expectedFinalContent);
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(1);
});
test("removing non-existing items", async () => {
await db("inventory").insert({
itemName: "cheesecake",
quantity: 0
});
const response = await request(app)
.del(`/carts/${globalUser.username}/items/cheesecake`)
.set("authorization", globalUser.authHeader)
.expect(400)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "cheesecake is not in the cart"
});
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(0);
});
});
describe("create accounts", () => {
test("creating a new account", async () => {
const response = await request(app)
.put("/users/another_user")
.send({ email: "another_user@example.org", password: "a_password" })
.expect(200)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "another_user created successfully"
});
const savedUser = await db
.select("email", "passwordHash")
.from("users")
.where({ username: "another_user" })
.first();
expect(savedUser).toEqual({
email: "another_user@example.org",
passwordHash: hashPassword("a_password")
});
});
test("creating a duplicate account", async () => {
const response = await request(app)
.put(`/users/${globalUser.username}`)
.send({ email: globalUser.email, password: "a_password" })
.expect(409)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: `${globalUser.username} already exists`
});
});
});
================================================
FILE: chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/truncateTables.js
================================================
const { db } = require("./dbConnection");
const tablesToTruncate = ["users", "inventory", "carts_items"];
beforeEach(() => {
return Promise.all(tablesToTruncate.map(t => db(t).truncate()));
});
================================================
FILE: chapter4/3_dealing_with_external_dependencies/3_maitaining_a_pristine_state/userTestUtils.js
================================================
const { db } = require("./dbConnection");
const { hashPassword } = require("./authenticationController");
const username = "test_user";
const password = "a_password";
const passwordHash = hashPassword(password);
const email = "test_user@example.org";
const validAuth = Buffer.from(`${username}:${password}`).toString("base64");
const authHeader = `Basic ${validAuth}`;
const user = {
username,
password,
email,
authHeader
};
const createUser = async () => {
await db("users").insert({ username, email, passwordHash });
const { id } = await db
.select()
.from("users")
.where({ username })
.first();
user.id = id;
};
module.exports = { user, createUser };
================================================
FILE: chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/authenticationController.js
================================================
const { user: globalUser } = require("./userTestUtils");
const crypto = require("crypto");
const { db } = require("./dbConnection");
const hashPassword = password => {
const hash = crypto.createHash("sha256");
hash.update(password);
return hash.digest("hex");
};
const credentialsAreValid = async (username, password) => {
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) return false;
return hashPassword(password) === user.passwordHash;
};
const authenticationMiddleware = async (ctx, next) => {
try {
const authHeader = ctx.request.headers.authorization;
const credentials = Buffer.from(
authHeader.slice("basic".length + 1),
"base64"
).toString();
const [username, password] = credentials.split(":");
const validCredentialsSent = await credentialsAreValid(username, password);
if (!validCredentialsSent) throw new Error("invalid credentials");
} catch (e) {
ctx.status = 401;
ctx.body = { message: "please provide valid credentials" };
return;
}
await next();
};
module.exports = {
hashPassword,
credentialsAreValid,
authenticationMiddleware
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/authenticationController.test.js
================================================
const crypto = require("crypto");
const {
hashPassword,
credentialsAreValid,
authenticationMiddleware
} = require("./authenticationController");
const { user: globalUser } = require("./userTestUtils");
describe("hashPassword", () => {
test("hashing passwords", () => {
const plainTextPassword = "password_example";
const hash = crypto.createHash("sha256");
hash.update(plainTextPassword);
const expectedHash = hash.digest("hex");
expect(hashPassword(plainTextPassword)).toBe(expectedHash);
});
});
describe("credentialsAreValid", () => {
test("validating credentials", async () => {
expect(await credentialsAreValid(globalUser.username, "a_password")).toBe(
true
);
});
});
describe("authenticationMiddleware", () => {
test("returning an error if the credentials are not valid", async () => {
const fakeAuth = Buffer.from("invalid:credentials").toString("base64");
const ctx = {
request: {
headers: { authorization: `Basic ${fakeAuth}` }
}
};
const next = jest.fn();
await authenticationMiddleware(ctx, next);
expect(next.mock.calls).toHaveLength(0);
expect(ctx).toEqual({
...ctx,
status: 401,
body: { message: "please provide valid credentials" }
});
});
test("authenticating properly", async () => {
const ctx = {
request: {
headers: { authorization: globalUser.authHeader }
}
};
const next = jest.fn();
await authenticationMiddleware(ctx, next);
expect(next.mock.calls).toHaveLength(1);
});
});
================================================
FILE: chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/cartController.js
================================================
const { db } = require("./dbConnection");
const { removeFromInventory } = require("./inventoryController");
const logger = require("./logger");
const addItemToCart = async (username, itemName) => {
await removeFromInventory(itemName);
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) {
const userNotFound = new Error("user not found");
userNotFound.code = 404;
}
const itemEntry = await db
.select()
.from("carts_items")
.where({ userId: user.id, itemName })
.first();
if (itemEntry && itemEntry.quantity + 1 > 3) {
const limitError = new Error(
"You can't have more than three units of an item in your cart"
);
limitError.code = 400;
throw limitError;
}
if (itemEntry) {
await db("carts_items")
.increment("quantity")
.where({ userId: itemEntry.userId, itemName });
} else {
await db("carts_items").insert({
userId: user.id,
itemName,
quantity: 1
});
}
logger.log(`${itemName} added to ${username}'s cart`);
return db
.select("itemName", "quantity")
.from("carts_items")
.where({ userId: user.id });
};
module.exports = { addItemToCart };
================================================
FILE: chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/cartController.test.js
================================================
const { db } = require("./dbConnection");
const { addItemToCart } = require("./cartController");
const { hashPassword } = require("./authenticationController");
const { user: globalUser } = require("./userTestUtils");
const fs = require("fs");
describe("addItemToCart", () => {
beforeEach(() => {
fs.writeFileSync("/tmp/logs.out", "");
});
test("adding unavailable items to cart", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 0 });
try {
await addItemToCart(globalUser.username, "cheesecake");
} catch (e) {
const expectedError = new Error("cheesecake is unavailable");
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
const finalCartContent = await db
.select("carts_items.*")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([]);
expect.assertions(2);
});
test("adding items above limit to cart", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 1 });
await db("carts_items").insert({
userId: globalUser.id,
itemName: "cheesecake",
quantity: 3
});
try {
await addItemToCart(globalUser.username, "cheesecake");
} catch (e) {
const expectedError = new Error(
"You can't have more than three units of an item in your cart"
);
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([{ itemName: "cheesecake", quantity: 3 }]);
expect.assertions(2);
});
test("logging added items", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 1 });
await db("carts_items").insert({
userId: globalUser.id,
itemName: "cheesecake",
quantity: 1
});
await addItemToCart(globalUser.username, "cheesecake");
const logs = fs.readFileSync("/tmp/logs.out", "utf-8");
expect(logs).toContain(
`cheesecake added to ${globalUser.username}'s cart\n`
);
});
});
================================================
FILE: chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/dbConnection.js
================================================
const environmentName = process.env.NODE_ENV;
const knex = require("knex");
const knexConfig = require("./knexfile")[environmentName];
const db = knex(knexConfig);
const closeConnection = () => db.destroy();
module.exports = {
db,
closeConnection
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/disconnectFromDb.js
================================================
const { db } = require("./dbConnection");
afterAll(() => db.destroy());
================================================
FILE: chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/inventoryController.js
================================================
const { db } = require("./dbConnection");
const removeFromInventory = async itemName => {
const inventoryEntry = await db
.select()
.from("inventory")
.where({ itemName })
.first();
if (!inventoryEntry || inventoryEntry.quantity === 0) {
const err = new Error(`${itemName} is unavailable`);
err.code = 400;
throw err;
}
await db("inventory")
.decrement("quantity")
.where({ itemName });
};
module.exports = { removeFromInventory };
================================================
FILE: chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/jest.config.js
================================================
module.exports = {
testEnvironment: "node",
globalSetup: "./migrateDatabases.js",
setupFilesAfterEnv: [
"/truncateTables.js",
"/seedUser.js",
"/disconnectFromDb.js"
]
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/knexfile.js
================================================
module.exports = {
test: {
client: "sqlite3",
connection: { filename: "./test.sqlite" },
useNullAsDefault: true
},
development: {
client: "sqlite3",
connection: { filename: "./dev.sqlite" },
useNullAsDefault: true
}
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/logger.js
================================================
const fs = require("fs");
const logger = {
log: msg => fs.appendFileSync("/tmp/logs.out", msg + "\n")
};
module.exports = logger;
================================================
FILE: chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/migrateDatabases.js
================================================
const environmentName = process.env.NODE_ENV || "test";
const environmentConfig = require("./knexfile")[environmentName];
const db = require("knex")(environmentConfig);
module.exports = async () => {
// Migrate the database to the latest state
await db.migrate.latest();
// Close the connection to the database so that tests won't hang
await db.destroy();
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/migrations/20200325082401_initial_schema.js
================================================
exports.up = async knex => {
await knex.schema.createTable("users", table => {
table.increments("id");
table.string("username");
table.unique("username");
table.string("email");
table.string("passwordHash");
});
await knex.schema.createTable("carts_items", table => {
table.integer("userId").references("users.id");
table.string("itemName");
table.unique("itemName");
table.integer("quantity");
});
await knex.schema.createTable("inventory", table => {
table.increments("id");
table.string("itemName");
table.unique("itemName");
table.integer("quantity");
});
};
exports.down = async knex => {
await knex.schema.dropTable("users");
await knex.schema.dropTable("carts_items");
await knex.schema.dropTable("inventory");
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/package.json
================================================
{
"name": "4_integrations_with_other_apis",
"version": "1.0.0",
"scripts": {
"test": "jest --runInBand"
},
"devDependencies": {
"jest": "^24.9.0",
"supertest": "^4.0.2"
},
"dependencies": {
"isomorphic-fetch": "^2.2.1",
"knex": "^0.20.13",
"koa": "^2.11.0",
"koa-body-parser": "^1.1.2",
"koa-router": "^7.4.0",
"sqlite3": "^4.1.1"
}
}
================================================
FILE: chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/seedUser.js
================================================
const { createUser } = require("./userTestUtils");
beforeEach(createUser);
================================================
FILE: chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/server.js
================================================
const fetch = require("isomorphic-fetch");
const Koa = require("koa");
const Router = require("koa-router");
const bodyParser = require("koa-body-parser");
const { db } = require("./dbConnection");
const { addItemToCart } = require("./cartController");
const {
hashPassword,
authenticationMiddleware
} = require("./authenticationController");
const app = new Koa();
const router = new Router();
app.use(bodyParser());
app.use(async (ctx, next) => {
if (ctx.url.startsWith("/carts")) {
return await authenticationMiddleware(ctx, next);
}
await next();
});
router.put("/users/:username", async ctx => {
const { username } = ctx.params;
const { email, password } = ctx.request.body;
const userAlreadyExists = await db
.select()
.from("users")
.where({ username })
.first();
if (userAlreadyExists) {
ctx.body = { message: `${username} already exists` };
ctx.status = 409;
return;
}
await db("users").insert({
username,
email,
passwordHash: hashPassword(password)
});
return (ctx.body = { message: `${username} created successfully` });
});
router.post("/carts/:username/items", async ctx => {
const { username } = ctx.params;
const { item, quantity } = ctx.request.body;
for (let i = 0; i < quantity; i++) {
try {
const newItems = await addItemToCart(username, item);
ctx.body = newItems;
} catch (e) {
ctx.body = { message: e.message };
ctx.status = e.code;
return;
}
}
});
router.delete("/carts/:username/items/:item", async ctx => {
const { username, item } = ctx.params;
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) {
ctx.body = { message: "user not found" };
ctx.status = 404;
return;
}
const itemEntry = await db
.select()
.from("carts_items")
.where({ userId: user.id, itemName: item })
.first();
if (!itemEntry || itemEntry.quantity === 0) {
ctx.body = { message: `${item} is not in the cart` };
ctx.status = 400;
return;
}
await db("carts_items")
.decrement("quantity")
.where({ userId: user.id, itemName: item });
const inventoryEntry = await db
.select()
.from("inventory")
.where({ itemName: item })
.first();
if (inventoryEntry) {
await db("inventory")
.increment("quantity")
.where({ userId: itemEntry.userId, itemName: item });
} else {
await db("inventory").insert({ itemName: item, quantity: 1 });
}
ctx.body = await db
.select("itemName", "quantity")
.from("carts_items")
.where({ userId: user.id });
});
router.get("/inventory/:itemName", async ctx => {
const { itemName } = ctx.params;
const response = await fetch(`http://recipepuppy.com/api?i=${itemName}`);
const { title, href, results: recipes } = await response.json();
const inventoryItem = await db
.select()
.from("inventory")
.where({ itemName })
.first();
ctx.body = {
...inventoryItem,
info: `Data obtained from ${title} - ${href}`,
recipes
};
});
app.use(router.routes());
module.exports = { app: app.listen(3000) };
================================================
FILE: chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/server.test.js
================================================
const fetch = require("isomorphic-fetch");
const { user: globalUser } = require("./userTestUtils");
const { db } = require("./dbConnection");
const request = require("supertest");
const { app } = require("./server.js");
const { hashPassword } = require("./authenticationController.js");
afterAll(() => app.close());
describe("add items to a cart", () => {
test("adding available items", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 3 });
const response = await request(app)
.post(`/carts/${globalUser.username}/items`)
.set("authorization", globalUser.authHeader)
.send({ item: "cheesecake", quantity: 3 })
.expect(200)
.expect("Content-Type", /json/);
const newItems = [{ itemName: "cheesecake", quantity: 3 }];
expect(response.body).toEqual(newItems);
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(0);
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual(newItems);
});
test("adding unavailable items", async () => {
const response = await request(app)
.post(`/carts/${globalUser.username}/items`)
.set("authorization", globalUser.authHeader)
.send({ item: "cheesecake", quantity: 1 })
.expect(400)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "cheesecake is unavailable"
});
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([]);
});
});
describe("removing items from a cart", () => {
test("removing existing items", async () => {
await db("carts_items").insert({
userId: globalUser.id,
itemName: "cheesecake",
quantity: 1
});
const response = await request(app)
.del(`/carts/${globalUser.username}/items/cheesecake`)
.set("authorization", globalUser.authHeader)
.expect(200)
.expect("Content-Type", /json/);
const expectedFinalContent = [{ itemName: "cheesecake", quantity: 0 }];
expect(response.body).toEqual(expectedFinalContent);
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual(expectedFinalContent);
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(1);
});
test("removing non-existing items", async () => {
await db("inventory").insert({
itemName: "cheesecake",
quantity: 0
});
const response = await request(app)
.del(`/carts/${globalUser.username}/items/cheesecake`)
.set("authorization", globalUser.authHeader)
.expect(400)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "cheesecake is not in the cart"
});
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(0);
});
});
describe("create accounts", () => {
test("creating a new account", async () => {
const response = await request(app)
.put("/users/another_user")
.send({ email: "another_user@example.org", password: "a_password" })
.expect(200)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "another_user created successfully"
});
const savedUser = await db
.select("email", "passwordHash")
.from("users")
.where({ username: "another_user" })
.first();
expect(savedUser).toEqual({
email: "another_user@example.org",
passwordHash: hashPassword("a_password")
});
});
test("creating a duplicate account", async () => {
const response = await request(app)
.put(`/users/${globalUser.username}`)
.send({ email: globalUser.email, password: "a_password" })
.expect(409)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: `${globalUser.username} already exists`
});
});
});
describe("fetch inventory items", () => {
const eggs = { itemName: "eggs", quantity: 3 };
const applePie = { itemName: "apple pie", quantity: 1 };
beforeEach(async () => {
await db("inventory").insert([eggs, applePie]);
const { id: eggsId } = await db
.select()
.from("inventory")
.where({ itemName: "eggs" })
.first();
eggs.id = eggsId;
});
test("can fetch an item from the inventory", async () => {
const thirdPartyResponse = await fetch("http://recipepuppy.com/api?i=eggs");
const { title, href, results: recipes } = await thirdPartyResponse.json();
const response = await request(app)
.get(`/inventory/eggs`)
.expect(200)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
...eggs,
info: `Data obtained from ${title} - ${href}`,
recipes
});
});
});
================================================
FILE: chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/truncateTables.js
================================================
const { db } = require("./dbConnection");
const tablesToTruncate = ["users", "inventory", "carts_items"];
beforeEach(() => {
return Promise.all(tablesToTruncate.map(t => db(t).truncate()));
});
================================================
FILE: chapter4/3_dealing_with_external_dependencies/4_integrations_with_other_apis/userTestUtils.js
================================================
const { db } = require("./dbConnection");
const { hashPassword } = require("./authenticationController");
const username = "test_user";
const password = "a_password";
const passwordHash = hashPassword(password);
const email = "test_user@example.org";
const validAuth = Buffer.from(`${username}:${password}`).toString("base64");
const authHeader = `Basic ${validAuth}`;
const user = {
username,
password,
email,
authHeader
};
const createUser = async () => {
await db("users").insert({ username, email, passwordHash });
const { id } = await db
.select()
.from("users")
.where({ username })
.first();
user.id = id;
};
module.exports = { user, createUser };
================================================
FILE: chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/authenticationController.js
================================================
const { user: globalUser } = require("./userTestUtils");
const crypto = require("crypto");
const { db } = require("./dbConnection");
const hashPassword = password => {
const hash = crypto.createHash("sha256");
hash.update(password);
return hash.digest("hex");
};
const credentialsAreValid = async (username, password) => {
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) return false;
return hashPassword(password) === user.passwordHash;
};
const authenticationMiddleware = async (ctx, next) => {
try {
const authHeader = ctx.request.headers.authorization;
const credentials = Buffer.from(
authHeader.slice("basic".length + 1),
"base64"
).toString();
const [username, password] = credentials.split(":");
const validCredentialsSent = await credentialsAreValid(username, password);
if (!validCredentialsSent) throw new Error("invalid credentials");
} catch (e) {
ctx.status = 401;
ctx.body = { message: "please provide valid credentials" };
return;
}
await next();
};
module.exports = {
hashPassword,
credentialsAreValid,
authenticationMiddleware
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/authenticationController.test.js
================================================
const crypto = require("crypto");
const {
hashPassword,
credentialsAreValid,
authenticationMiddleware
} = require("./authenticationController");
const { user: globalUser } = require("./userTestUtils");
describe("hashPassword", () => {
test("hashing passwords", () => {
const plainTextPassword = "password_example";
const hash = crypto.createHash("sha256");
hash.update(plainTextPassword);
const expectedHash = hash.digest("hex");
expect(hashPassword(plainTextPassword)).toBe(expectedHash);
});
});
describe("credentialsAreValid", () => {
test("validating credentials", async () => {
expect(await credentialsAreValid(globalUser.username, "a_password")).toBe(
true
);
});
});
describe("authenticationMiddleware", () => {
test("returning an error if the credentials are not valid", async () => {
const fakeAuth = Buffer.from("invalid:credentials").toString("base64");
const ctx = {
request: {
headers: { authorization: `Basic ${fakeAuth}` }
}
};
const next = jest.fn();
await authenticationMiddleware(ctx, next);
expect(next.mock.calls).toHaveLength(0);
expect(ctx).toEqual({
...ctx,
status: 401,
body: { message: "please provide valid credentials" }
});
});
test("authenticating properly", async () => {
const ctx = {
request: {
headers: { authorization: globalUser.authHeader }
}
};
const next = jest.fn();
await authenticationMiddleware(ctx, next);
expect(next.mock.calls).toHaveLength(1);
});
});
================================================
FILE: chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/cartController.js
================================================
const { db } = require("./dbConnection");
const { removeFromInventory } = require("./inventoryController");
const logger = require("./logger");
const addItemToCart = async (username, itemName) => {
await removeFromInventory(itemName);
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) {
const userNotFound = new Error("user not found");
userNotFound.code = 404;
}
const itemEntry = await db
.select()
.from("carts_items")
.where({ userId: user.id, itemName })
.first();
if (itemEntry && itemEntry.quantity + 1 > 3) {
const limitError = new Error(
"You can't have more than three units of an item in your cart"
);
limitError.code = 400;
throw limitError;
}
if (itemEntry) {
await db("carts_items")
.increment("quantity")
.where({ userId: itemEntry.userId, itemName });
} else {
await db("carts_items").insert({
userId: user.id,
itemName,
quantity: 1
});
}
logger.log(`${itemName} added to ${username}'s cart`);
return db
.select("itemName", "quantity")
.from("carts_items")
.where({ userId: user.id });
};
module.exports = { addItemToCart };
================================================
FILE: chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/cartController.test.js
================================================
const { db } = require("./dbConnection");
const { addItemToCart } = require("./cartController");
const { hashPassword } = require("./authenticationController");
const { user: globalUser } = require("./userTestUtils");
const fs = require("fs");
describe("addItemToCart", () => {
beforeEach(() => {
fs.writeFileSync("/tmp/logs.out", "");
});
test("adding unavailable items to cart", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 0 });
try {
await addItemToCart(globalUser.username, "cheesecake");
} catch (e) {
const expectedError = new Error("cheesecake is unavailable");
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
const finalCartContent = await db
.select("carts_items.*")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([]);
expect.assertions(2);
});
test("adding items above limit to cart", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 1 });
await db("carts_items").insert({
userId: globalUser.id,
itemName: "cheesecake",
quantity: 3
});
try {
await addItemToCart(globalUser.username, "cheesecake");
} catch (e) {
const expectedError = new Error(
"You can't have more than three units of an item in your cart"
);
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([{ itemName: "cheesecake", quantity: 3 }]);
expect.assertions(2);
});
test("logging added items", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 1 });
await db("carts_items").insert({
userId: globalUser.id,
itemName: "cheesecake",
quantity: 1
});
await addItemToCart(globalUser.username, "cheesecake");
const logs = fs.readFileSync("/tmp/logs.out", "utf-8");
expect(logs).toContain(
`cheesecake added to ${globalUser.username}'s cart\n`
);
});
});
================================================
FILE: chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/dbConnection.js
================================================
const environmentName = process.env.NODE_ENV;
const knex = require("knex");
const knexConfig = require("./knexfile")[environmentName];
const db = knex(knexConfig);
const closeConnection = () => db.destroy();
module.exports = {
db,
closeConnection
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/disconnectFromDb.js
================================================
const { db } = require("./dbConnection");
afterAll(() => db.destroy());
================================================
FILE: chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/inventoryController.js
================================================
const { db } = require("./dbConnection");
const removeFromInventory = async itemName => {
const inventoryEntry = await db
.select()
.from("inventory")
.where({ itemName })
.first();
if (!inventoryEntry || inventoryEntry.quantity === 0) {
const err = new Error(`${itemName} is unavailable`);
err.code = 400;
throw err;
}
await db("inventory")
.decrement("quantity")
.where({ itemName });
};
module.exports = { removeFromInventory };
================================================
FILE: chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/jest.config.js
================================================
module.exports = {
testEnvironment: "node",
globalSetup: "./migrateDatabases.js",
setupFilesAfterEnv: [
"/truncateTables.js",
"/seedUser.js",
"/disconnectFromDb.js"
]
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/knexfile.js
================================================
module.exports = {
test: {
client: "sqlite3",
connection: { filename: "./test.sqlite" },
useNullAsDefault: true
},
development: {
client: "sqlite3",
connection: { filename: "./dev.sqlite" },
useNullAsDefault: true
}
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/logger.js
================================================
const fs = require("fs");
const logger = {
log: msg => fs.appendFileSync("/tmp/logs.out", msg + "\n")
};
module.exports = logger;
================================================
FILE: chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/migrateDatabases.js
================================================
const environmentName = process.env.NODE_ENV || "test";
const environmentConfig = require("./knexfile")[environmentName];
const db = require("knex")(environmentConfig);
module.exports = async () => {
// Migrate the database to the latest state
await db.migrate.latest();
// Close the connection to the database so that tests won't hang
await db.destroy();
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/migrations/20200325082401_initial_schema.js
================================================
exports.up = async knex => {
await knex.schema.createTable("users", table => {
table.increments("id");
table.string("username");
table.unique("username");
table.string("email");
table.string("passwordHash");
});
await knex.schema.createTable("carts_items", table => {
table.integer("userId").references("users.id");
table.string("itemName");
table.unique("itemName");
table.integer("quantity");
});
await knex.schema.createTable("inventory", table => {
table.increments("id");
table.string("itemName");
table.unique("itemName");
table.integer("quantity");
});
};
exports.down = async knex => {
await knex.schema.dropTable("users");
await knex.schema.dropTable("carts_items");
await knex.schema.dropTable("inventory");
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/package.json
================================================
{
"name": "5_using_mocks_to_avoid_requests",
"version": "1.0.0",
"scripts": {
"test": "jest --runInBand"
},
"devDependencies": {
"jest": "^24.9.0",
"jest-when": "^2.7.0",
"supertest": "^4.0.2"
},
"dependencies": {
"isomorphic-fetch": "^2.2.1",
"knex": "^0.20.13",
"koa": "^2.11.0",
"koa-body-parser": "^1.1.2",
"koa-router": "^7.4.0",
"sqlite3": "^4.1.1"
}
}
================================================
FILE: chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/seedUser.js
================================================
const { createUser } = require("./userTestUtils");
beforeEach(createUser);
================================================
FILE: chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/server.js
================================================
const fetch = require("isomorphic-fetch");
const Koa = require("koa");
const Router = require("koa-router");
const bodyParser = require("koa-body-parser");
const { db } = require("./dbConnection");
const { addItemToCart } = require("./cartController");
const {
hashPassword,
authenticationMiddleware
} = require("./authenticationController");
const app = new Koa();
const router = new Router();
app.use(bodyParser());
app.use(async (ctx, next) => {
if (ctx.url.startsWith("/carts")) {
return await authenticationMiddleware(ctx, next);
}
await next();
});
router.put("/users/:username", async ctx => {
const { username } = ctx.params;
const { email, password } = ctx.request.body;
const userAlreadyExists = await db
.select()
.from("users")
.where({ username })
.first();
if (userAlreadyExists) {
ctx.body = { message: `${username} already exists` };
ctx.status = 409;
return;
}
await db("users").insert({
username,
email,
passwordHash: hashPassword(password)
});
return (ctx.body = { message: `${username} created successfully` });
});
router.post("/carts/:username/items", async ctx => {
const { username } = ctx.params;
const { item, quantity } = ctx.request.body;
for (let i = 0; i < quantity; i++) {
try {
const newItems = await addItemToCart(username, item);
ctx.body = newItems;
} catch (e) {
ctx.body = { message: e.message };
ctx.status = e.code;
return;
}
}
});
router.delete("/carts/:username/items/:item", async ctx => {
const { username, item } = ctx.params;
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) {
ctx.body = { message: "user not found" };
ctx.status = 404;
return;
}
const itemEntry = await db
.select()
.from("carts_items")
.where({ userId: user.id, itemName: item })
.first();
if (!itemEntry || itemEntry.quantity === 0) {
ctx.body = { message: `${item} is not in the cart` };
ctx.status = 400;
return;
}
await db("carts_items")
.decrement("quantity")
.where({ userId: user.id, itemName: item });
const inventoryEntry = await db
.select()
.from("inventory")
.where({ itemName: item })
.first();
if (inventoryEntry) {
await db("inventory")
.increment("quantity")
.where({ userId: itemEntry.userId, itemName: item });
} else {
await db("inventory").insert({ itemName: item, quantity: 1 });
}
ctx.body = await db
.select("itemName", "quantity")
.from("carts_items")
.where({ userId: user.id });
});
router.get("/inventory/:itemName", async ctx => {
const { itemName } = ctx.params;
const response = await fetch(`http://recipepuppy.com/api?i=${itemName}`);
const { title, href, results: recipes } = await response.json();
const inventoryItem = await db
.select()
.from("inventory")
.where({ itemName })
.first();
ctx.body = {
...inventoryItem,
info: `Data obtained from ${title} - ${href}`,
recipes
};
});
app.use(router.routes());
module.exports = { app: app.listen(3000) };
================================================
FILE: chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/server.test.js
================================================
const fetch = require("isomorphic-fetch");
const { user: globalUser } = require("./userTestUtils");
const { db } = require("./dbConnection");
const request = require("supertest");
const { app } = require("./server.js");
const { hashPassword } = require("./authenticationController.js");
const { when } = require("jest-when");
afterAll(() => app.close());
jest.mock("isomorphic-fetch");
describe("add items to a cart", () => {
test("adding available items", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 3 });
const response = await request(app)
.post(`/carts/${globalUser.username}/items`)
.set("authorization", globalUser.authHeader)
.send({ item: "cheesecake", quantity: 3 })
.expect(200)
.expect("Content-Type", /json/);
const newItems = [{ itemName: "cheesecake", quantity: 3 }];
expect(response.body).toEqual(newItems);
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(0);
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual(newItems);
});
test("adding unavailable items", async () => {
const response = await request(app)
.post(`/carts/${globalUser.username}/items`)
.set("authorization", globalUser.authHeader)
.send({ item: "cheesecake", quantity: 1 })
.expect(400)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "cheesecake is unavailable"
});
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([]);
});
});
describe("removing items from a cart", () => {
test("removing existing items", async () => {
await db("carts_items").insert({
userId: globalUser.id,
itemName: "cheesecake",
quantity: 1
});
const response = await request(app)
.del(`/carts/${globalUser.username}/items/cheesecake`)
.set("authorization", globalUser.authHeader)
.expect(200)
.expect("Content-Type", /json/);
const expectedFinalContent = [{ itemName: "cheesecake", quantity: 0 }];
expect(response.body).toEqual(expectedFinalContent);
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual(expectedFinalContent);
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(1);
});
test("removing non-existing items", async () => {
await db("inventory").insert({
itemName: "cheesecake",
quantity: 0
});
const response = await request(app)
.del(`/carts/${globalUser.username}/items/cheesecake`)
.set("authorization", globalUser.authHeader)
.expect(400)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "cheesecake is not in the cart"
});
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(0);
});
});
describe("create accounts", () => {
test("creating a new account", async () => {
const response = await request(app)
.put("/users/another_user")
.send({ email: "another_user@example.org", password: "a_password" })
.expect(200)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "another_user created successfully"
});
const savedUser = await db
.select("email", "passwordHash")
.from("users")
.where({ username: "another_user" })
.first();
expect(savedUser).toEqual({
email: "another_user@example.org",
passwordHash: hashPassword("a_password")
});
});
test("creating a duplicate account", async () => {
const response = await request(app)
.put(`/users/${globalUser.username}`)
.send({ email: globalUser.email, password: "a_password" })
.expect(409)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: `${globalUser.username} already exists`
});
});
});
describe("fetch inventory items", () => {
const eggs = { itemName: "eggs", quantity: 3 };
const applePie = { itemName: "apple pie", quantity: 1 };
beforeEach(async () => {
await db("inventory").insert([eggs, applePie]);
const { id: eggsId } = await db
.select()
.from("inventory")
.where({ itemName: "eggs" })
.first();
eggs.id = eggsId;
});
test("can fetch an item from the inventory", async () => {
const eggsResponse = {
title: "FakeAPI",
href: "example.org",
results: [{ name: "Omelette du Fromage" }]
};
fetch.mockRejectedValue("Not used as expected!");
when(fetch)
.calledWith("http://recipepuppy.com/api?i=eggs")
.mockResolvedValue({
json: jest.fn().mockResolvedValue(eggsResponse)
});
const response = await request(app)
.get(`/inventory/eggs`)
.expect(200)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
...eggs,
info: `Data obtained from ${eggsResponse.title} - ${eggsResponse.href}`,
recipes: eggsResponse.results
});
});
});
================================================
FILE: chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/truncateTables.js
================================================
const { db } = require("./dbConnection");
const tablesToTruncate = ["users", "inventory", "carts_items"];
beforeEach(() => {
return Promise.all(tablesToTruncate.map(t => db(t).truncate()));
});
================================================
FILE: chapter4/3_dealing_with_external_dependencies/5_using_mocks_to_avoid_requests/userTestUtils.js
================================================
const { db } = require("./dbConnection");
const { hashPassword } = require("./authenticationController");
const username = "test_user";
const password = "a_password";
const passwordHash = hashPassword(password);
const email = "test_user@example.org";
const validAuth = Buffer.from(`${username}:${password}`).toString("base64");
const authHeader = `Basic ${validAuth}`;
const user = {
username,
password,
email,
authHeader
};
const createUser = async () => {
await db("users").insert({ username, email, passwordHash });
const { id } = await db
.select()
.from("users")
.where({ username })
.first();
user.id = id;
};
module.exports = { user, createUser };
================================================
FILE: chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/authenticationController.js
================================================
const crypto = require("crypto");
const { db } = require("./dbConnection");
const hashPassword = password => {
const hash = crypto.createHash("sha256");
hash.update(password);
return hash.digest("hex");
};
const credentialsAreValid = async (username, password) => {
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) return false;
return hashPassword(password) === user.passwordHash;
};
const authenticationMiddleware = async (ctx, next) => {
try {
const authHeader = ctx.request.headers.authorization;
const credentials = Buffer.from(
authHeader.slice("basic".length + 1),
"base64"
).toString();
const [username, password] = credentials.split(":");
const validCredentialsSent = await credentialsAreValid(username, password);
if (!validCredentialsSent) throw new Error("invalid credentials");
} catch (e) {
ctx.status = 401;
ctx.body = { message: "please provide valid credentials" };
return;
}
await next();
};
module.exports = {
hashPassword,
credentialsAreValid,
authenticationMiddleware
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/authenticationController.test.js
================================================
const crypto = require("crypto");
const {
hashPassword,
credentialsAreValid,
authenticationMiddleware
} = require("./authenticationController");
const { user: globalUser } = require("./userTestUtils");
describe("hashPassword", () => {
test("hashing passwords", () => {
const plainTextPassword = "password_example";
const hash = crypto.createHash("sha256");
hash.update(plainTextPassword);
const expectedHash = hash.digest("hex");
expect(hashPassword(plainTextPassword)).toBe(expectedHash);
});
});
describe("credentialsAreValid", () => {
test("validating credentials", async () => {
expect(await credentialsAreValid(globalUser.username, "a_password")).toBe(
true
);
});
});
describe("authenticationMiddleware", () => {
test("returning an error if the credentials are not valid", async () => {
const fakeAuth = Buffer.from("invalid:credentials").toString("base64");
const ctx = {
request: {
headers: { authorization: `Basic ${fakeAuth}` }
}
};
const next = jest.fn();
await authenticationMiddleware(ctx, next);
expect(next.mock.calls).toHaveLength(0);
expect(ctx).toEqual({
...ctx,
status: 401,
body: { message: "please provide valid credentials" }
});
});
test("authenticating properly", async () => {
const ctx = {
request: {
headers: { authorization: globalUser.authHeader }
}
};
const next = jest.fn();
await authenticationMiddleware(ctx, next);
expect(next.mock.calls).toHaveLength(1);
});
});
================================================
FILE: chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/cartController.js
================================================
const { db } = require("./dbConnection");
const { removeFromInventory } = require("./inventoryController");
const logger = require("./logger");
const addItemToCart = async (username, itemName) => {
await removeFromInventory(itemName);
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) {
const userNotFound = new Error("user not found");
userNotFound.code = 404;
}
const itemEntry = await db
.select()
.from("carts_items")
.where({ userId: user.id, itemName })
.first();
if (itemEntry && itemEntry.quantity + 1 > 3) {
const limitError = new Error(
"You can't have more than three units of an item in your cart"
);
limitError.code = 400;
throw limitError;
}
if (itemEntry) {
await db("carts_items")
.increment("quantity")
.where({ userId: itemEntry.userId, itemName });
} else {
await db("carts_items").insert({
userId: user.id,
itemName,
quantity: 1
});
}
logger.log(`${itemName} added to ${username}'s cart`);
return db
.select("itemName", "quantity")
.from("carts_items")
.where({ userId: user.id });
};
module.exports = { addItemToCart };
================================================
FILE: chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/cartController.test.js
================================================
const { db } = require("./dbConnection");
const { addItemToCart } = require("./cartController");
const { hashPassword } = require("./authenticationController");
const { user: globalUser } = require("./userTestUtils");
const fs = require("fs");
describe("addItemToCart", () => {
beforeEach(() => {
fs.writeFileSync("/tmp/logs.out", "");
});
test("adding unavailable items to cart", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 0 });
try {
await addItemToCart(globalUser.username, "cheesecake");
} catch (e) {
const expectedError = new Error("cheesecake is unavailable");
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
const finalCartContent = await db
.select("carts_items.*")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([]);
expect.assertions(2);
});
test("adding items above limit to cart", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 1 });
await db("carts_items").insert({
userId: globalUser.id,
itemName: "cheesecake",
quantity: 3
});
try {
await addItemToCart(globalUser.username, "cheesecake");
} catch (e) {
const expectedError = new Error(
"You can't have more than three units of an item in your cart"
);
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([{ itemName: "cheesecake", quantity: 3 }]);
expect.assertions(2);
});
test("logging added items", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 1 });
await db("carts_items").insert({
userId: globalUser.id,
itemName: "cheesecake",
quantity: 1
});
await addItemToCart(globalUser.username, "cheesecake");
const logs = fs.readFileSync("/tmp/logs.out", "utf-8");
expect(logs).toContain(
`cheesecake added to ${globalUser.username}'s cart\n`
);
});
});
================================================
FILE: chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/dbConnection.js
================================================
const environmentName = process.env.NODE_ENV;
const knex = require("knex");
const knexConfig = require("./knexfile")[environmentName];
const db = knex(knexConfig);
const closeConnection = () => db.destroy();
module.exports = {
db,
closeConnection
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/disconnectFromDb.js
================================================
const { db } = require("./dbConnection");
afterAll(() => db.destroy());
================================================
FILE: chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/inventoryController.js
================================================
const { db } = require("./dbConnection");
const removeFromInventory = async itemName => {
const inventoryEntry = await db
.select()
.from("inventory")
.where({ itemName })
.first();
if (!inventoryEntry || inventoryEntry.quantity === 0) {
const err = new Error(`${itemName} is unavailable`);
err.code = 400;
throw err;
}
await db("inventory")
.decrement("quantity")
.where({ itemName });
};
module.exports = { removeFromInventory };
================================================
FILE: chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/jest.config.js
================================================
module.exports = {
testEnvironment: "node",
globalSetup: "./migrateDatabases.js",
setupFilesAfterEnv: [
"/truncateTables.js",
"/seedUser.js",
"/disconnectFromDb.js"
]
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/knexfile.js
================================================
module.exports = {
test: {
client: "sqlite3",
connection: { filename: "./test.sqlite" },
useNullAsDefault: true
},
development: {
client: "sqlite3",
connection: { filename: "./dev.sqlite" },
useNullAsDefault: true
}
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/logger.js
================================================
const fs = require("fs");
const logger = {
log: msg => fs.appendFileSync("/tmp/logs.out", msg + "\n")
};
module.exports = logger;
================================================
FILE: chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/migrateDatabases.js
================================================
const environmentName = process.env.NODE_ENV || "test";
const environmentConfig = require("./knexfile")[environmentName];
const db = require("knex")(environmentConfig);
module.exports = async () => {
// Migrate the database to the latest state
await db.migrate.latest();
// Close the connection to the database so that tests won't hang
await db.destroy();
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/migrations/20200325082401_initial_schema.js
================================================
exports.up = async knex => {
await knex.schema.createTable("users", table => {
table.increments("id");
table.string("username");
table.unique("username");
table.string("email");
table.string("passwordHash");
});
await knex.schema.createTable("carts_items", table => {
table.integer("userId").references("users.id");
table.string("itemName");
table.unique("itemName");
table.integer("quantity");
});
await knex.schema.createTable("inventory", table => {
table.increments("id");
table.string("itemName");
table.unique("itemName");
table.integer("quantity");
});
};
exports.down = async knex => {
await knex.schema.dropTable("users");
await knex.schema.dropTable("carts_items");
await knex.schema.dropTable("inventory");
};
================================================
FILE: chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/package.json
================================================
{
"name": "4_integrations_with_other_apis",
"version": "1.0.0",
"scripts": {
"test": "jest --runInBand"
},
"devDependencies": {
"jest": "^24.9.0",
"supertest": "^4.0.2"
},
"dependencies": {
"isomorphic-fetch": "^2.2.1",
"knex": "^0.20.13",
"koa": "^2.11.0",
"koa-body-parser": "^1.1.2",
"koa-router": "^7.4.0",
"nock": "^12.0.3",
"sqlite3": "^4.1.1"
}
}
================================================
FILE: chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/seedUser.js
================================================
const { createUser } = require("./userTestUtils");
beforeEach(createUser);
================================================
FILE: chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/server.js
================================================
const fetch = require("isomorphic-fetch");
const Koa = require("koa");
const Router = require("koa-router");
const bodyParser = require("koa-body-parser");
const { db } = require("./dbConnection");
const { addItemToCart } = require("./cartController");
const {
hashPassword,
authenticationMiddleware
} = require("./authenticationController");
const app = new Koa();
const router = new Router();
app.use(bodyParser());
app.use(async (ctx, next) => {
if (ctx.url.startsWith("/carts")) {
return await authenticationMiddleware(ctx, next);
}
await next();
});
router.put("/users/:username", async ctx => {
const { username } = ctx.params;
const { email, password } = ctx.request.body;
const userAlreadyExists = await db
.select()
.from("users")
.where({ username })
.first();
if (userAlreadyExists) {
ctx.body = { message: `${username} already exists` };
ctx.status = 409;
return;
}
await db("users").insert({
username,
email,
passwordHash: hashPassword(password)
});
return (ctx.body = { message: `${username} created successfully` });
});
router.post("/carts/:username/items", async ctx => {
const { username } = ctx.params;
const { item, quantity } = ctx.request.body;
for (let i = 0; i < quantity; i++) {
try {
const newItems = await addItemToCart(username, item);
ctx.body = newItems;
} catch (e) {
ctx.body = { message: e.message };
ctx.status = e.code;
return;
}
}
});
router.delete("/carts/:username/items/:item", async ctx => {
const { username, item } = ctx.params;
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) {
ctx.body = { message: "user not found" };
ctx.status = 404;
return;
}
const itemEntry = await db
.select()
.from("carts_items")
.where({ userId: user.id, itemName: item })
.first();
if (!itemEntry || itemEntry.quantity === 0) {
ctx.body = { message: `${item} is not in the cart` };
ctx.status = 400;
return;
}
await db("carts_items")
.decrement("quantity")
.where({ userId: user.id, itemName: item });
const inventoryEntry = await db
.select()
.from("inventory")
.where({ itemName: item })
.first();
if (inventoryEntry) {
await db("inventory")
.increment("quantity")
.where({ userId: itemEntry.userId, itemName: item });
} else {
await db("inventory").insert({ itemName: item, quantity: 1 });
}
ctx.body = await db
.select("itemName", "quantity")
.from("carts_items")
.where({ userId: user.id });
});
router.get("/inventory/:itemName", async ctx => {
const { itemName } = ctx.params;
const response = await fetch(`http://recipepuppy.com/api?i=${itemName}`);
const { title, href, results: recipes } = await response.json();
const inventoryItem = await db
.select()
.from("inventory")
.where({ itemName })
.first();
ctx.body = {
...inventoryItem,
info: `Data obtained from ${title} - ${href}`,
recipes
};
});
app.use(router.routes());
module.exports = { app: app.listen(3000) };
================================================
FILE: chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/server.test.js
================================================
const { user: globalUser } = require("./userTestUtils");
const { db } = require("./dbConnection");
const request = require("supertest");
const { app } = require("./server.js");
const { hashPassword } = require("./authenticationController.js");
const nock = require("nock");
afterAll(() => app.close());
describe("add items to a cart", () => {
test("adding available items", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 3 });
const response = await request(app)
.post(`/carts/${globalUser.username}/items`)
.set("authorization", globalUser.authHeader)
.send({ item: "cheesecake", quantity: 3 })
.expect(200)
.expect("Content-Type", /json/);
const newItems = [{ itemName: "cheesecake", quantity: 3 }];
expect(response.body).toEqual(newItems);
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(0);
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual(newItems);
});
test("adding unavailable items", async () => {
const response = await request(app)
.post(`/carts/${globalUser.username}/items`)
.set("authorization", globalUser.authHeader)
.send({ item: "cheesecake", quantity: 1 })
.expect(400)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "cheesecake is unavailable"
});
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([]);
});
});
describe("removing items from a cart", () => {
test("removing existing items", async () => {
await db("carts_items").insert({
userId: globalUser.id,
itemName: "cheesecake",
quantity: 1
});
const response = await request(app)
.del(`/carts/${globalUser.username}/items/cheesecake`)
.set("authorization", globalUser.authHeader)
.expect(200)
.expect("Content-Type", /json/);
const expectedFinalContent = [{ itemName: "cheesecake", quantity: 0 }];
expect(response.body).toEqual(expectedFinalContent);
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual(expectedFinalContent);
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(1);
});
test("removing non-existing items", async () => {
await db("inventory").insert({
itemName: "cheesecake",
quantity: 0
});
const response = await request(app)
.del(`/carts/${globalUser.username}/items/cheesecake`)
.set("authorization", globalUser.authHeader)
.expect(400)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "cheesecake is not in the cart"
});
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(0);
});
});
describe("create accounts", () => {
test("creating a new account", async () => {
const response = await request(app)
.put("/users/another_user")
.send({ email: "another_user@example.org", password: "a_password" })
.expect(200)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "another_user created successfully"
});
const savedUser = await db
.select("email", "passwordHash")
.from("users")
.where({ username: "another_user" })
.first();
expect(savedUser).toEqual({
email: "another_user@example.org",
passwordHash: hashPassword("a_password")
});
});
test("creating a duplicate account", async () => {
const response = await request(app)
.put(`/users/${globalUser.username}`)
.send({ email: globalUser.email, password: "a_password" })
.expect(409)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: `${globalUser.username} already exists`
});
});
});
describe("fetch inventory items", () => {
const eggs = { itemName: "eggs", quantity: 3 };
const applePie = { itemName: "apple pie", quantity: 1 };
beforeEach(async () => {
await db("inventory").insert([eggs, applePie]);
const { id: eggsId } = await db
.select()
.from("inventory")
.where({ itemName: "eggs" })
.first();
eggs.id = eggsId;
});
beforeEach(() => {
nock.cleanAll();
});
afterEach(() => {
if (!nock.isDone()) {
throw new Error("Not all mocked endpoints received requests.");
}
});
test("can fetch an item from the inventory", async () => {
const eggsResponse = {
title: "FakeAPI",
href: "example.org",
results: [{ name: "Omelette du Fromage" }]
};
nock("http://recipepuppy.com")
.get("/api")
.query({ i: "eggs" })
.reply(200, eggsResponse);
const response = await request(app)
.get(`/inventory/eggs`)
.expect(200)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
...eggs,
info: `Data obtained from ${eggsResponse.title} - ${eggsResponse.href}`,
recipes: eggsResponse.results
});
});
});
================================================
FILE: chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/truncateTables.js
================================================
const { db } = require("./dbConnection");
const tablesToTruncate = ["users", "inventory", "carts_items"];
beforeEach(() => {
return Promise.all(tablesToTruncate.map(t => db(t).truncate()));
});
================================================
FILE: chapter4/3_dealing_with_external_dependencies/6_using_nock_to_avoid_requests/userTestUtils.js
================================================
const { db } = require("./dbConnection");
const { hashPassword } = require("./authenticationController");
const username = "test_user";
const password = "a_password";
const passwordHash = hashPassword(password);
const email = "test_user@example.org";
const validAuth = Buffer.from(`${username}:${password}`).toString("base64");
const authHeader = `Basic ${validAuth}`;
const user = {
username,
password,
email,
authHeader
};
const createUser = async () => {
await db("users").insert({ username, email, passwordHash });
const { id } = await db
.select()
.from("users")
.where({ username })
.first();
user.id = id;
};
module.exports = { user, createUser };
================================================
FILE: chapter5/1_eliminating_non_determinism/1_shared_resources/countModule.js
================================================
const fs = require("fs");
const filepath = "./state.txt";
const getState = () => parseInt(fs.readFileSync(filepath), 10);
const setState = n => fs.writeFileSync(filepath, n);
const increment = () => fs.writeFileSync(filepath, getState() + 1);
const decrement = () => fs.writeFileSync(filepath, getState() - 1);
module.exports = { getState, setState, increment, decrement };
================================================
FILE: chapter5/1_eliminating_non_determinism/1_shared_resources/decrement.test.js
================================================
const { getState, setState, decrement } = require("./countModule");
test("decrementing the state 10 times", () => {
setState(0);
for (let i = 0; i < 10; i++) {
decrement();
}
expect(getState()).toBe(-10);
});
================================================
FILE: chapter5/1_eliminating_non_determinism/1_shared_resources/increment.test.js
================================================
const { getState, setState, increment } = require("./countModule");
test("incrementing the state 10 times", () => {
setState(0);
for (let i = 0; i < 10; i++) {
increment();
}
expect(getState()).toBe(10);
});
================================================
FILE: chapter5/1_eliminating_non_determinism/1_shared_resources/package.json
================================================
{
"name": "1_shared_resources",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^26.6.0"
}
}
================================================
FILE: chapter5/1_eliminating_non_determinism/2_resource_pools/countModule.js
================================================
const fs = require("fs");
const init = filepath => {
const getState = () => {
return parseInt(fs.readFileSync(filepath, "utf-8"), 10);
};
const setState = n => fs.writeFileSync(filepath, n);
const increment = () => fs.writeFileSync(filepath, getState() + 1);
const decrement = () => fs.writeFileSync(filepath, getState() - 1);
return { getState, setState, increment, decrement };
};
module.exports = { init };
================================================
FILE: chapter5/1_eliminating_non_determinism/2_resource_pools/decrement.test.js
================================================
const pool = require("./instancePool");
const instance = pool.getInstance(process.env.JEST_WORKER_ID);
const { setState, getState, decrement } = instance;
test("decrementing the state 10 times", () => {
setState(0);
for (let i = 0; i < 10; i++) {
decrement();
}
expect(getState()).toBe(-10);
});
================================================
FILE: chapter5/1_eliminating_non_determinism/2_resource_pools/increment.test.js
================================================
const pool = require("./instancePool");
const instance = pool.getInstance(process.env.JEST_WORKER_ID);
const { setState, getState, increment } = instance;
test("incrementing the state 10 times", () => {
setState(0);
for (let i = 0; i < 10; i++) {
increment();
}
expect(getState()).toBe(10);
});
================================================
FILE: chapter5/1_eliminating_non_determinism/2_resource_pools/instancePool.js
================================================
const { init } = require("./countModule");
const instancePool = {};
const getInstance = workerId => {
if (!instancePool[workerId]) {
instancePool[workerId] = init(`/tmp/test_state_${workerId}.txt`);
}
return instancePool[workerId];
};
module.exports = { getInstance };
================================================
FILE: chapter5/1_eliminating_non_determinism/2_resource_pools/package.json
================================================
{
"name": "2_resource_pools",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^25.2.3"
}
}
================================================
FILE: chapter5/1_eliminating_non_determinism/3_dealing_with_time/authenticationController.js
================================================
const crypto = require("crypto");
const { db } = require("./dbConnection");
const hashPassword = password => {
const hash = crypto.createHash("sha256");
hash.update(password);
return hash.digest("hex");
};
const credentialsAreValid = async (username, password) => {
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) return false;
return hashPassword(password) === user.passwordHash;
};
const authenticationMiddleware = async (ctx, next) => {
try {
const authHeader = ctx.request.headers.authorization;
const credentials = Buffer.from(
authHeader.slice("basic".length + 1),
"base64"
).toString();
const [username, password] = credentials.split(":");
const validCredentialsSent = await credentialsAreValid(username, password);
if (!validCredentialsSent) throw new Error("invalid credentials");
} catch (e) {
ctx.status = 401;
ctx.body = { message: "please provide valid credentials" };
return;
}
await next();
};
module.exports = {
hashPassword,
credentialsAreValid,
authenticationMiddleware
};
================================================
FILE: chapter5/1_eliminating_non_determinism/3_dealing_with_time/authenticationController.test.js
================================================
const crypto = require("crypto");
const {
hashPassword,
credentialsAreValid,
authenticationMiddleware
} = require("./authenticationController");
const { user: globalUser } = require("./userTestUtils");
describe("hashPassword", () => {
test("hashing passwords", () => {
const plainTextPassword = "password_example";
const hash = crypto.createHash("sha256");
hash.update(plainTextPassword);
const expectedHash = hash.digest("hex");
expect(hashPassword(plainTextPassword)).toBe(expectedHash);
});
});
describe("credentialsAreValid", () => {
test("validating credentials", async () => {
expect(await credentialsAreValid(globalUser.username, "a_password")).toBe(
true
);
});
});
describe("authenticationMiddleware", () => {
test("returning an error if the credentials are not valid", async () => {
const fakeAuth = Buffer.from("invalid:credentials").toString("base64");
const ctx = {
request: {
headers: { authorization: `Basic ${fakeAuth}` }
}
};
const next = jest.fn();
await authenticationMiddleware(ctx, next);
expect(next.mock.calls).toHaveLength(0);
expect(ctx).toEqual({
...ctx,
status: 401,
body: { message: "please provide valid credentials" }
});
});
test("authenticating properly", async () => {
const ctx = {
request: {
headers: { authorization: globalUser.authHeader }
}
};
const next = jest.fn();
await authenticationMiddleware(ctx, next);
expect(next.mock.calls).toHaveLength(1);
});
});
================================================
FILE: chapter5/1_eliminating_non_determinism/3_dealing_with_time/cartController.js
================================================
const { db } = require("./dbConnection");
const { removeFromInventory } = require("./inventoryController");
const logger = require("./logger");
const addItemToCart = async (username, itemName) => {
await removeFromInventory(itemName);
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) {
const userNotFound = new Error("user not found");
userNotFound.code = 404;
}
const itemEntry = await db
.select()
.from("carts_items")
.where({ userId: user.id, itemName })
.first();
if (itemEntry && itemEntry.quantity + 1 > 3) {
const limitError = new Error(
"You can't have more than three units of an item in your cart"
);
limitError.code = 400;
throw limitError;
}
if (itemEntry) {
await db("carts_items")
.increment("quantity")
.update({ updatedAt: new Date().toISOString() })
.where({
userId: itemEntry.userId,
itemName
});
} else {
await db("carts_items").insert({
userId: user.id,
itemName,
quantity: 1,
updatedAt: new Date().toISOString()
});
}
logger.log(`${itemName} added to ${username}'s cart`);
return db
.select("itemName", "quantity")
.from("carts_items")
.where({ userId: user.id });
};
const hoursInMs = n => 1000 * 60 * 60 * n;
const removeStaleItems = async () => {
const fourHoursAgo = new Date(Date.now() - hoursInMs(4)).toISOString();
const staleItems = await db
.select()
.from("carts_items")
.where("updatedAt", "<", fourHoursAgo);
if (staleItems.length === 0) return;
// Put stale items back in the inventory
const inventoryUpdates = staleItems.map(staleItem =>
db("inventory")
.increment("quantity", staleItem.quantity)
.where({ itemName: staleItem.itemName })
);
await Promise.all(inventoryUpdates);
// Delete stale items from cart
const staleItemTuples = staleItems.map(i => [i.itemName, i.userId]);
await db("carts_items")
.del()
.whereIn(["itemName", "userId"], staleItemTuples);
};
const monitorStaleItems = () => setInterval(removeStaleItems, hoursInMs(2));
module.exports = { addItemToCart, monitorStaleItems };
================================================
FILE: chapter5/1_eliminating_non_determinism/3_dealing_with_time/cartController.test.js
================================================
const { db } = require("./dbConnection");
const { addItemToCart, monitorStaleItems } = require("./cartController");
const { hashPassword } = require("./authenticationController");
const { user: globalUser } = require("./userTestUtils");
const FakeTimers = require("@sinonjs/fake-timers");
const fs = require("fs");
describe("addItemToCart", () => {
beforeEach(() => {
fs.writeFileSync("/tmp/logs.out", "");
});
test("adding unavailable items to cart", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 0 });
try {
await addItemToCart(globalUser.username, "cheesecake");
} catch (e) {
const expectedError = new Error("cheesecake is unavailable");
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
const finalCartContent = await db
.select("carts_items.*")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([]);
expect.assertions(2);
});
test("adding items above limit to cart", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 1 });
await db("carts_items").insert({
userId: globalUser.id,
itemName: "cheesecake",
quantity: 3
});
try {
await addItemToCart(globalUser.username, "cheesecake");
} catch (e) {
const expectedError = new Error(
"You can't have more than three units of an item in your cart"
);
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([{ itemName: "cheesecake", quantity: 3 }]);
expect.assertions(2);
});
test("logging added items", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 1 });
await db("carts_items").insert({
userId: globalUser.id,
itemName: "cheesecake",
quantity: 1
});
await addItemToCart(globalUser.username, "cheesecake");
const logs = fs.readFileSync("/tmp/logs.out", "utf-8");
expect(logs).toContain(
`cheesecake added to ${globalUser.username}'s cart\n`
);
});
});
const withRetries = async fn => {
// Capture the assertion error since Jest does not export it
const JestAssertionError = (() => {
try {
expect(false).toBe(true);
} catch (e) {
return e.constructor;
}
})();
try {
await fn();
} catch (e) {
if (e.constructor === JestAssertionError) {
// Wait 100ms before retrying
await new Promise(resolve => setTimeout(resolve, 100));
await withRetries(fn);
} else {
throw e;
}
}
};
describe("timers", () => {
const hoursInMs = n => 1000 * 60 * 60 * n;
let clock;
beforeEach(() => {
clock = FakeTimers.install({ toFake: ["Date", "setInterval"] });
});
afterEach(() => {
clock = clock.uninstall();
});
test("removing stale items", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 1 });
await addItemToCart(globalUser.username, "cheesecake");
clock.tick(hoursInMs(4));
timer = monitorStaleItems();
clock.tick(hoursInMs(2));
await withRetries(async () => {
const finalCartContent = await db
.select()
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([]);
});
await withRetries(async () => {
const inventoryContent = await db
.select("itemName", "quantity")
.from("inventory");
expect(inventoryContent).toEqual([
{ itemName: "cheesecake", quantity: 1 }
]);
});
});
});
================================================
FILE: chapter5/1_eliminating_non_determinism/3_dealing_with_time/dbConnection.js
================================================
const environmentName = process.env.NODE_ENV;
const knex = require("knex");
const knexConfig = require("./knexfile")[environmentName];
const db = knex(knexConfig);
const closeConnection = () => db.destroy();
module.exports = {
db,
closeConnection
};
================================================
FILE: chapter5/1_eliminating_non_determinism/3_dealing_with_time/disconnectFromDb.js
================================================
const { db } = require("./dbConnection");
afterAll(() => db.destroy());
================================================
FILE: chapter5/1_eliminating_non_determinism/3_dealing_with_time/inventoryController.js
================================================
const { db } = require("./dbConnection");
const removeFromInventory = async itemName => {
const inventoryEntry = await db
.select()
.from("inventory")
.where({ itemName })
.first();
if (!inventoryEntry || inventoryEntry.quantity === 0) {
const err = new Error(`${itemName} is unavailable`);
err.code = 400;
throw err;
}
await db("inventory")
.decrement("quantity")
.where({ itemName });
};
module.exports = { removeFromInventory };
================================================
FILE: chapter5/1_eliminating_non_determinism/3_dealing_with_time/jest.config.js
================================================
module.exports = {
testEnvironment: "node",
globalSetup: "./migrateDatabases.js",
setupFilesAfterEnv: [
"/truncateTables.js",
"/seedUser.js",
"/disconnectFromDb.js"
]
};
================================================
FILE: chapter5/1_eliminating_non_determinism/3_dealing_with_time/knexfile.js
================================================
module.exports = {
test: {
client: "sqlite3",
connection: { filename: "./test.sqlite" },
useNullAsDefault: true
},
development: {
client: "sqlite3",
connection: { filename: "./dev.sqlite" },
useNullAsDefault: true
}
};
================================================
FILE: chapter5/1_eliminating_non_determinism/3_dealing_with_time/logger.js
================================================
const fs = require("fs");
const logger = {
log: msg => fs.appendFileSync("/tmp/logs.out", msg + "\n")
};
module.exports = logger;
================================================
FILE: chapter5/1_eliminating_non_determinism/3_dealing_with_time/migrateDatabases.js
================================================
const environmentName = process.env.NODE_ENV || "test";
const environmentConfig = require("./knexfile")[environmentName];
const db = require("knex")(environmentConfig);
module.exports = async () => {
// Migrate the database to the latest state
await db.migrate.latest();
// Close the connection to the database so that tests won't hang
await db.destroy();
};
================================================
FILE: chapter5/1_eliminating_non_determinism/3_dealing_with_time/migrations/20200325082401_initial_schema.js
================================================
exports.up = async knex => {
await knex.schema.createTable("users", table => {
table.increments("id");
table.string("username");
table.unique("username");
table.string("email");
table.string("passwordHash");
});
await knex.schema.createTable("carts_items", table => {
table.integer("userId").references("users.id");
table.string("itemName");
table.unique("itemName");
table.integer("quantity");
});
await knex.schema.createTable("inventory", table => {
table.increments("id");
table.string("itemName");
table.unique("itemName");
table.integer("quantity");
});
};
exports.down = async knex => {
await knex.schema.dropTable("users");
await knex.schema.dropTable("carts_items");
await knex.schema.dropTable("inventory");
};
================================================
FILE: chapter5/1_eliminating_non_determinism/3_dealing_with_time/migrations/20200331210311_updatedAt_field.js
================================================
exports.up = knex => {
return knex.schema.alterTable("carts_items", table => {
table.timestamp("updatedAt");
});
};
exports.down = knex => {
return knex.schema.alterTable("carts_items", table => {
table.dropColumn("updatedAt");
});
};
================================================
FILE: chapter5/1_eliminating_non_determinism/3_dealing_with_time/package.json
================================================
{
"name": "3_dealing_with_time",
"version": "1.0.0",
"scripts": {
"test": "jest --runInBand",
"start": "node server.js"
},
"devDependencies": {
"@sinonjs/fake-timers": "github:sinonjs/fake-timers",
"jest": "^26.6.0",
"supertest": "^4.0.2"
},
"dependencies": {
"isomorphic-fetch": "^2.2.1",
"knex": "^0.20.13",
"koa": "^2.11.0",
"koa-body-parser": "^1.1.2",
"koa-router": "^7.4.0",
"nock": "^12.0.3",
"sqlite3": "^4.1.1"
},
"main": "alertController.spec.js",
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}
================================================
FILE: chapter5/1_eliminating_non_determinism/3_dealing_with_time/seedUser.js
================================================
const { createUser } = require("./userTestUtils");
beforeEach(createUser);
================================================
FILE: chapter5/1_eliminating_non_determinism/3_dealing_with_time/server.js
================================================
const fetch = require("isomorphic-fetch");
const Koa = require("koa");
const Router = require("koa-router");
const bodyParser = require("koa-body-parser");
const { db } = require("./dbConnection");
const { addItemToCart } = require("./cartController");
const {
hashPassword,
authenticationMiddleware
} = require("./authenticationController");
const app = new Koa();
const router = new Router();
app.use(bodyParser());
app.use(async (ctx, next) => {
if (ctx.url.startsWith("/carts")) {
return await authenticationMiddleware(ctx, next);
}
await next();
});
router.put("/users/:username", async ctx => {
const { username } = ctx.params;
const { email, password } = ctx.request.body;
const userAlreadyExists = await db
.select()
.from("users")
.where({ username })
.first();
if (userAlreadyExists) {
ctx.body = { message: `${username} already exists` };
ctx.status = 409;
return;
}
await db("users").insert({
username,
email,
passwordHash: hashPassword(password)
});
return (ctx.body = { message: `${username} created successfully` });
});
router.post("/carts/:username/items", async ctx => {
const { username } = ctx.params;
const { item, quantity } = ctx.request.body;
for (let i = 0; i < quantity; i++) {
try {
const newItems = await addItemToCart(username, item);
ctx.body = newItems;
} catch (e) {
ctx.body = { message: e.message };
ctx.status = e.code;
return;
}
}
});
router.delete("/carts/:username/items/:item", async ctx => {
const { username, item } = ctx.params;
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) {
ctx.body = { message: "user not found" };
ctx.status = 404;
return;
}
const itemEntry = await db
.select()
.from("carts_items")
.where({ userId: user.id, itemName: item })
.first();
if (!itemEntry || itemEntry.quantity === 0) {
ctx.body = { message: `${item} is not in the cart` };
ctx.status = 400;
return;
}
await db("carts_items")
.decrement("quantity")
.where({ userId: user.id, itemName: item });
const inventoryEntry = await db
.select()
.from("inventory")
.where({ itemName: item })
.first();
if (inventoryEntry) {
await db("inventory")
.increment("quantity")
.where({ userId: itemEntry.userId, itemName: item });
} else {
await db("inventory").insert({ itemName: item, quantity: 1 });
}
ctx.body = await db
.select("itemName", "quantity")
.from("carts_items")
.where({ userId: user.id });
});
router.get("/inventory/:itemName", async ctx => {
const { itemName } = ctx.params;
const response = await fetch(`http://recipepuppy.com/api?i=${itemName}`);
const { title, href, results: recipes } = await response.json();
const inventoryItem = await db
.select()
.from("inventory")
.where({ itemName })
.first();
ctx.body = {
...inventoryItem,
info: `Data obtained from ${title} - ${href}`,
recipes
};
});
app.use(router.routes());
module.exports = { app: app.listen(3000) };
================================================
FILE: chapter5/1_eliminating_non_determinism/3_dealing_with_time/server.test.js
================================================
const { user: globalUser } = require("./userTestUtils");
const { db } = require("./dbConnection");
const request = require("supertest");
const { app } = require("./server.js");
const { hashPassword } = require("./authenticationController.js");
const nock = require("nock");
afterAll(() => app.close());
describe("add items to a cart", () => {
test("adding available items", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 3 });
const response = await request(app)
.post(`/carts/${globalUser.username}/items`)
.set("authorization", globalUser.authHeader)
.send({ item: "cheesecake", quantity: 3 })
.expect(200)
.expect("Content-Type", /json/);
const newItems = [{ itemName: "cheesecake", quantity: 3 }];
expect(response.body).toEqual(newItems);
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(0);
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual(newItems);
});
test("adding unavailable items", async () => {
const response = await request(app)
.post(`/carts/${globalUser.username}/items`)
.set("authorization", globalUser.authHeader)
.send({ item: "cheesecake", quantity: 1 })
.expect(400)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "cheesecake is unavailable"
});
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([]);
});
});
describe("removing items from a cart", () => {
test("removing existing items", async () => {
await db("carts_items").insert({
userId: globalUser.id,
itemName: "cheesecake",
quantity: 1
});
const response = await request(app)
.del(`/carts/${globalUser.username}/items/cheesecake`)
.set("authorization", globalUser.authHeader)
.expect(200)
.expect("Content-Type", /json/);
const expectedFinalContent = [{ itemName: "cheesecake", quantity: 0 }];
expect(response.body).toEqual(expectedFinalContent);
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual(expectedFinalContent);
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(1);
});
test("removing non-existing items", async () => {
await db("inventory").insert({
itemName: "cheesecake",
quantity: 0
});
const response = await request(app)
.del(`/carts/${globalUser.username}/items/cheesecake`)
.set("authorization", globalUser.authHeader)
.expect(400)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "cheesecake is not in the cart"
});
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(0);
});
});
describe("create accounts", () => {
test("creating a new account", async () => {
const response = await request(app)
.put("/users/another_user")
.send({ email: "another_user@example.org", password: "a_password" })
.expect(200)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "another_user created successfully"
});
const savedUser = await db
.select("email", "passwordHash")
.from("users")
.where({ username: "another_user" })
.first();
expect(savedUser).toEqual({
email: "another_user@example.org",
passwordHash: hashPassword("a_password")
});
});
test("creating a duplicate account", async () => {
const response = await request(app)
.put(`/users/${globalUser.username}`)
.send({ email: globalUser.email, password: "a_password" })
.expect(409)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: `${globalUser.username} already exists`
});
});
});
describe("fetch inventory items", () => {
const eggs = { itemName: "eggs", quantity: 3 };
const applePie = { itemName: "apple pie", quantity: 1 };
beforeEach(async () => {
await db("inventory").insert([eggs, applePie]);
const { id: eggsId } = await db
.select()
.from("inventory")
.where({ itemName: "eggs" })
.first();
eggs.id = eggsId;
});
test("can fetch an item from the inventory", async () => {
const eggsResponse = {
title: "FakeAPI",
href: "example.org",
results: [{ name: "Omelette du Fromage" }]
};
nock("http://recipepuppy.com")
.get("/api")
.query({ i: "eggs" })
.reply(200, eggsResponse);
const response = await request(app)
.get(`/inventory/eggs`)
.expect(200)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
...eggs,
info: `Data obtained from ${eggsResponse.title} - ${eggsResponse.href}`,
recipes: eggsResponse.results
});
});
});
================================================
FILE: chapter5/1_eliminating_non_determinism/3_dealing_with_time/truncateTables.js
================================================
const { db } = require("./dbConnection");
const tablesToTruncate = ["users", "inventory", "carts_items"];
beforeEach(() => {
return Promise.all(tablesToTruncate.map(t => db(t).truncate()));
});
================================================
FILE: chapter5/1_eliminating_non_determinism/3_dealing_with_time/userTestUtils.js
================================================
const { db } = require("./dbConnection");
const { hashPassword } = require("./authenticationController");
const username = "test_user";
const password = "a_password";
const passwordHash = hashPassword(password);
const email = "test_user@example.org";
const validAuth = Buffer.from(`${username}:${password}`).toString("base64");
const authHeader = `Basic ${validAuth}`;
const user = {
username,
password,
email,
authHeader
};
const createUser = async () => {
await db("users").insert({ username, email, passwordHash });
const { id } = await db
.select()
.from("users")
.where({ username })
.first();
user.id = id;
};
module.exports = { user, createUser };
================================================
FILE: chapter6/1_introducing_jsdom/1_pure_html/index.html
================================================
Inventory Manager
Cheesecakes: 0
Add cheesecake
================================================
FILE: chapter6/1_introducing_jsdom/1_pure_html/main.js
================================================
let data = { cheesecakes: 0 };
const incrementCount = () => {
data.cheesecakes++;
window.document.getElementById("count").innerText = data.cheesecakes;
};
const incrementButton = window.document.getElementById("increment-button");
incrementButton.addEventListener("click", incrementCount);
================================================
FILE: chapter6/1_introducing_jsdom/2_jsdom/example.js
================================================
const page = require("./page");
console.log("Initial page body:");
console.log(page.window.document.body.innerHTML);
console.log("Initial contents of the count element:");
console.log(page.window.document.getElementById("count").innerHTML);
// Changing the count element's content
page.window.document.getElementById("count").innerHTML = 1337;
console.log("Updated contents of the count element:");
console.log(page.window.document.getElementById("count").innerHTML);
// Appending a paragraph to the page
const paragraph = page.window.document.createElement("p");
paragraph.innerText = "Look, I'm a new paragraph";
page.window.document.body.appendChild(paragraph);
console.log("Final page body:");
console.log(page.window.document.body.innerHTML);
================================================
FILE: chapter6/1_introducing_jsdom/2_jsdom/index.html
================================================
Inventory Manager
Cheesecakes: 0
Add cheesecake
================================================
FILE: chapter6/1_introducing_jsdom/2_jsdom/main.js
================================================
let data = { cheesecakes: 0 };
const incrementCount = () => {
data.cheesecakes++;
window.document.getElementById("count").innerText = data.cheesecakes;
};
const incrementButton = window.document.getElementById("increment-button");
incrementButton.addEventListener("click", incrementCount);
================================================
FILE: chapter6/1_introducing_jsdom/2_jsdom/package.json
================================================
{
"name": "2_jsdom",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"jsdom": "^16.2.2"
}
}
================================================
FILE: chapter6/1_introducing_jsdom/2_jsdom/page.js
================================================
const fs = require("fs");
const { JSDOM } = require("jsdom");
const html = fs.readFileSync("./index.html");
const page = new JSDOM(html);
module.exports = page;
================================================
FILE: chapter6/1_introducing_jsdom/3_jest_jsdom/index.html
================================================
Inventory Manager
Cheesecakes: 0
Add cheesecake
================================================
FILE: chapter6/1_introducing_jsdom/3_jest_jsdom/jest.config.js
================================================
module.exports = {
testEnvironment: "jsdom"
};
================================================
FILE: chapter6/1_introducing_jsdom/3_jest_jsdom/main.js
================================================
let data = { cheesecakes: 0 };
const incrementCount = () => {
data.cheesecakes++;
window.document.getElementById("count").innerText = data.cheesecakes;
};
const incrementButton = window.document.getElementById("increment-button");
incrementButton.addEventListener("click", incrementCount);
module.exports = { incrementCount, data };
================================================
FILE: chapter6/1_introducing_jsdom/3_jest_jsdom/main.test.js
================================================
const fs = require("fs");
window.document.body.innerHTML = fs.readFileSync("./index.html");
const { incrementCount, data } = require("./main");
describe("incrementCount", () => {
test("incrementing the count", () => {
data.cheesecakes = 0;
incrementCount();
expect(data.cheesecakes).toBe(1);
});
});
================================================
FILE: chapter6/1_introducing_jsdom/3_jest_jsdom/package.json
================================================
{
"name": "3_jest_jsdom",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"test": "jest",
"build": "browserify main.js -o bundle.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {},
"devDependencies": {
"browserify": "^16.5.1",
"jest": "^26.6.0"
}
}
================================================
FILE: chapter6/2_asserting_on_the_dom/1_finding_elements_by_dom_structure/domController.js
================================================
const updateItemList = inventory => {
const inventoryList = window.document.getElementById("item-list");
// Clears the list
inventoryList.innerHTML = "";
Object.entries(inventory).forEach(([itemName, quantity]) => {
const listItem = window.document.createElement("li");
listItem.innerHTML = `${itemName} - Quantity: ${quantity}`;
inventoryList.appendChild(listItem);
});
};
module.exports = { updateItemList };
================================================
FILE: chapter6/2_asserting_on_the_dom/1_finding_elements_by_dom_structure/domController.test.js
================================================
const fs = require("fs");
document.body.innerHTML = fs.readFileSync("./index.html");
const { updateItemList } = require("./domController");
describe("updateItemList", () => {
test("updates the DOM with the inventory items", () => {
const inventory = {
cheesecake: 5,
"apple pie": 2,
"carrot cake": 6
};
updateItemList(inventory);
const itemList = document.querySelector("body > ul");
expect(itemList.childNodes).toHaveLength(3);
// The `childNodes` property has a `length`, but it's _not_ an Array
const nodesText = Array.from(itemList.childNodes).map(
node => node.innerHTML
);
expect(nodesText).toContain("cheesecake - Quantity: 5");
expect(nodesText).toContain("apple pie - Quantity: 2");
expect(nodesText).toContain("carrot cake - Quantity: 6");
});
});
================================================
FILE: chapter6/2_asserting_on_the_dom/1_finding_elements_by_dom_structure/index.html
================================================
Inventory Manager
Inventory Contents
================================================
FILE: chapter6/2_asserting_on_the_dom/1_finding_elements_by_dom_structure/inventoryController.js
================================================
const data = { inventory: {} };
const addItem = (itemName, quantity) => {
const currentQuantity = data.inventory[itemName] || 0;
data.inventory[itemName] = currentQuantity + quantity;
};
module.exports = { data, addItem };
================================================
FILE: chapter6/2_asserting_on_the_dom/1_finding_elements_by_dom_structure/inventoryController.test.js
================================================
const { addItem, data } = require("./inventoryController");
describe("addItem", () => {
test("adding new items to the inventory", () => {
data.inventory = {};
addItem("cheesecake", 5);
expect(data.inventory.cheesecake).toBe(5);
});
});
================================================
FILE: chapter6/2_asserting_on_the_dom/1_finding_elements_by_dom_structure/main.js
================================================
const { addItem, data } = require("./inventoryController");
const { updateItemList } = require("./domController");
addItem("cheesecake", 3);
addItem("apple pie", 8);
addItem("carrot cake", 7);
updateItemList(data.inventory);
================================================
FILE: chapter6/2_asserting_on_the_dom/1_finding_elements_by_dom_structure/package.json
================================================
{
"name": "1_finding_elements_by_dom_structure",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "http-server ./",
"test": "jest",
"build": "browserify main.js -o bundle.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"browserify": "^16.5.1",
"http-server": "^0.12.1",
"jest": "^24.9.0"
}
}
================================================
FILE: chapter6/2_asserting_on_the_dom/2_finding_elements_by_id/domController.js
================================================
const updateItemList = inventory => {
const inventoryList = window.document.getElementById("item-list");
// Clears the list
inventoryList.innerHTML = "";
Object.entries(inventory).forEach(([itemName, quantity]) => {
const listItem = window.document.createElement("li");
listItem.innerHTML = `${itemName} - Quantity: ${quantity}`;
inventoryList.appendChild(listItem);
});
};
module.exports = { updateItemList };
================================================
FILE: chapter6/2_asserting_on_the_dom/2_finding_elements_by_id/domController.test.js
================================================
const fs = require("fs");
document.body.innerHTML = fs.readFileSync("./index.html");
const { updateItemList } = require("./domController");
describe("updateItemList", () => {
test("updates the DOM with the inventory items", () => {
const inventory = {
cheesecake: 5,
"apple pie": 2,
"carrot cake": 6
};
updateItemList(inventory);
const itemList = document.getElementById("item-list");
expect(itemList.childNodes).toHaveLength(3);
// The `childNodes` property has a `length`, but it's _not_ an Array
const nodesText = Array.from(itemList.childNodes).map(
node => node.innerHTML
);
expect(nodesText).toContain("cheesecake - Quantity: 5");
expect(nodesText).toContain("apple pie - Quantity: 2");
expect(nodesText).toContain("carrot cake - Quantity: 6");
});
});
================================================
FILE: chapter6/2_asserting_on_the_dom/2_finding_elements_by_id/index.html
================================================
Inventory Manager
Inventory Contents
================================================
FILE: chapter6/2_asserting_on_the_dom/2_finding_elements_by_id/inventoryController.js
================================================
const data = { inventory: {} };
const addItem = (itemName, quantity) => {
const currentQuantity = data.inventory[itemName] || 0;
data.inventory[itemName] = currentQuantity + quantity;
};
module.exports = { data, addItem };
================================================
FILE: chapter6/2_asserting_on_the_dom/2_finding_elements_by_id/inventoryController.test.js
================================================
const { addItem, data } = require("./inventoryController");
describe("addItem", () => {
test("adding new items to the inventory", () => {
data.inventory = {};
addItem("cheesecake", 5);
expect(data.inventory.cheesecake).toBe(5);
});
});
================================================
FILE: chapter6/2_asserting_on_the_dom/2_finding_elements_by_id/main.js
================================================
const { addItem, data } = require("./inventoryController");
const { updateItemList } = require("./domController");
addItem("cheesecake", 3);
addItem("apple pie", 8);
addItem("carrot cake", 7);
updateItemList(data.inventory);
================================================
FILE: chapter6/2_asserting_on_the_dom/2_finding_elements_by_id/package.json
================================================
{
"name": "2_finding_elements_by_id",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "http-server ./",
"test": "jest",
"build": "browserify main.js -o bundle.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"browserify": "^16.5.1",
"http-server": "^0.12.1",
"jest": "^24.9.0"
}
}
================================================
FILE: chapter6/2_asserting_on_the_dom/3_robust_element_queries/domController.js
================================================
const updateItemList = inventory => {
const inventoryList = window.document.getElementById("item-list");
// Clears the list
inventoryList.innerHTML = "";
Object.entries(inventory).forEach(([itemName, quantity]) => {
const listItem = window.document.createElement("li");
listItem.innerHTML = `${itemName} - Quantity: ${quantity}`;
inventoryList.appendChild(listItem);
});
const inventoryContents = JSON.stringify(inventory);
const p = window.document.createElement("p");
p.innerHTML = `The inventory has been updated - ${inventoryContents}`;
window.document.body.appendChild(p);
};
module.exports = { updateItemList };
================================================
FILE: chapter6/2_asserting_on_the_dom/3_robust_element_queries/domController.test.js
================================================
const fs = require("fs");
const initialHtml = fs.readFileSync("./index.html");
const { updateItemList } = require("./domController");
beforeEach(() => {
document.body.innerHTML = initialHtml;
});
describe("updateItemList", () => {
test("updates the DOM with the inventory items", () => {
const inventory = {
cheesecake: 5,
"apple pie": 2,
"carrot cake": 6
};
updateItemList(inventory);
const itemList = document.getElementById("item-list");
expect(itemList.childNodes).toHaveLength(3);
// The `childNodes` property has a `length`, but it's _not_ an Array
const nodesText = Array.from(itemList.childNodes).map(
node => node.innerHTML
);
expect(nodesText).toContain("cheesecake - Quantity: 5");
expect(nodesText).toContain("apple pie - Quantity: 2");
expect(nodesText).toContain("carrot cake - Quantity: 6");
});
test("adding a paragraph indicating what was the update", () => {
const inventory = { cheesecake: 5, "apple pie": 2 };
updateItemList(inventory);
const paragraphs = Array.from(document.querySelectorAll("p"));
const updateParagraphs = paragraphs.filter(p => {
return p.innerHTML.includes("The inventory has been updated");
});
expect(updateParagraphs).toHaveLength(1);
expect(updateParagraphs[0].innerHTML).toBe(
`The inventory has been updated - ${JSON.stringify(inventory)}`
);
});
});
================================================
FILE: chapter6/2_asserting_on_the_dom/3_robust_element_queries/index.html
================================================
Inventory Manager
Inventory Contents
================================================
FILE: chapter6/2_asserting_on_the_dom/3_robust_element_queries/inventoryController.js
================================================
const data = { inventory: {} };
const addItem = (itemName, quantity) => {
const currentQuantity = data.inventory[itemName] || 0;
data.inventory[itemName] = currentQuantity + quantity;
};
module.exports = { data, addItem };
================================================
FILE: chapter6/2_asserting_on_the_dom/3_robust_element_queries/inventoryController.test.js
================================================
const { addItem, data } = require("./inventoryController");
describe("addItem", () => {
test("adding new items to the inventory", () => {
data.inventory = {};
addItem("cheesecake", 5);
expect(data.inventory.cheesecake).toBe(5);
});
});
================================================
FILE: chapter6/2_asserting_on_the_dom/3_robust_element_queries/main.js
================================================
const { addItem, data } = require("./inventoryController");
const { updateItemList } = require("./domController");
addItem("cheesecake", 3);
addItem("apple pie", 8);
addItem("carrot cake", 7);
updateItemList(data.inventory);
================================================
FILE: chapter6/2_asserting_on_the_dom/3_robust_element_queries/package.json
================================================
{
"name": "3_robust_element_queries",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "http-server ./",
"test": "jest",
"build": "browserify main.js -o bundle.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"browserify": "^16.5.1",
"http-server": "^0.12.1",
"jest": "^24.9.0"
}
}
================================================
FILE: chapter6/2_asserting_on_the_dom/4_finding_with_dom_testing_library/domController.js
================================================
const updateItemList = inventory => {
const inventoryList = window.document.getElementById("item-list");
// Clears the list
inventoryList.innerHTML = "";
Object.entries(inventory).forEach(([itemName, quantity]) => {
const listItem = window.document.createElement("li");
listItem.innerHTML = `${itemName} - Quantity: ${quantity}`;
inventoryList.appendChild(listItem);
});
const inventoryContents = JSON.stringify(inventory);
const p = window.document.createElement("p");
p.innerHTML = `The inventory has been updated - ${inventoryContents}`;
window.document.body.appendChild(p);
};
module.exports = { updateItemList };
================================================
FILE: chapter6/2_asserting_on_the_dom/4_finding_with_dom_testing_library/domController.test.js
================================================
const fs = require("fs");
const initialHtml = fs.readFileSync("./index.html");
const { getByText, screen } = require("@testing-library/dom");
const { updateItemList } = require("./domController");
beforeEach(() => {
document.body.innerHTML = initialHtml;
});
describe("updateItemList", () => {
test("updates the DOM with the inventory items", () => {
const inventory = {
cheesecake: 5,
"apple pie": 2,
"carrot cake": 6
};
updateItemList(inventory);
const itemList = document.getElementById("item-list");
expect(itemList.childNodes).toHaveLength(3);
expect(getByText(itemList, "cheesecake - Quantity: 5")).toBeTruthy();
expect(getByText(itemList, "apple pie - Quantity: 2")).toBeTruthy();
expect(getByText(itemList, "carrot cake - Quantity: 6")).toBeTruthy();
});
test("adding a paragraph indicating what was the update", () => {
const inventory = { cheesecake: 5, "apple pie": 2 };
updateItemList(inventory);
expect(
screen.getByText(
`The inventory has been updated - ${JSON.stringify(inventory)}`
)
).toBeTruthy();
});
});
================================================
FILE: chapter6/2_asserting_on_the_dom/4_finding_with_dom_testing_library/index.html
================================================
Inventory Manager
Inventory Contents
================================================
FILE: chapter6/2_asserting_on_the_dom/4_finding_with_dom_testing_library/inventoryController.js
================================================
const data = { inventory: {} };
const addItem = (itemName, quantity) => {
const currentQuantity = data.inventory[itemName] || 0;
data.inventory[itemName] = currentQuantity + quantity;
};
module.exports = { data, addItem };
================================================
FILE: chapter6/2_asserting_on_the_dom/4_finding_with_dom_testing_library/inventoryController.test.js
================================================
const { addItem, data } = require("./inventoryController");
describe("addItem", () => {
test("adding new items to the inventory", () => {
data.inventory = {};
addItem("cheesecake", 5);
expect(data.inventory.cheesecake).toBe(5);
});
});
================================================
FILE: chapter6/2_asserting_on_the_dom/4_finding_with_dom_testing_library/main.js
================================================
const { addItem, data } = require("./inventoryController");
const { updateItemList } = require("./domController");
addItem("cheesecake", 3);
addItem("apple pie", 8);
addItem("carrot cake", 7);
updateItemList(data.inventory);
================================================
FILE: chapter6/2_asserting_on_the_dom/4_finding_with_dom_testing_library/package.json
================================================
{
"name": "4_finding_with_dom_testing_library",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "http-server ./",
"test": "jest",
"build": "browserify main.js -o bundle.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@testing-library/dom": "^7.2.2",
"browserify": "^16.5.1",
"http-server": "^0.12.1",
"jest": "^26.6.0"
}
}
================================================
FILE: chapter6/2_asserting_on_the_dom/5_writing_better_dom_assertions/domController.js
================================================
const updateItemList = inventory => {
const inventoryList = window.document.getElementById("item-list");
// Clears the list
inventoryList.innerHTML = "";
Object.entries(inventory).forEach(([itemName, quantity]) => {
const listItem = window.document.createElement("li");
listItem.innerHTML = `${itemName} - Quantity: ${quantity}`;
if (quantity < 5) {
listItem.className = "almost-soldout";
}
inventoryList.appendChild(listItem);
});
const inventoryContents = JSON.stringify(inventory);
const p = window.document.createElement("p");
p.innerHTML = `The inventory has been updated - ${inventoryContents}`;
window.document.body.appendChild(p);
};
module.exports = { updateItemList };
================================================
FILE: chapter6/2_asserting_on_the_dom/5_writing_better_dom_assertions/domController.test.js
================================================
const fs = require("fs");
const initialHtml = fs.readFileSync("./index.html");
const { getByText, screen } = require("@testing-library/dom");
const { updateItemList } = require("./domController");
beforeEach(() => {
document.body.innerHTML = initialHtml;
});
describe("updateItemList", () => {
test("updates the DOM with the inventory items", () => {
const inventory = {
cheesecake: 5,
"apple pie": 2,
"carrot cake": 6
};
updateItemList(inventory);
const itemList = document.getElementById("item-list");
expect(itemList.childNodes).toHaveLength(3);
expect(getByText(itemList, "cheesecake - Quantity: 5")).toBeInTheDocument();
expect(getByText(itemList, "apple pie - Quantity: 2")).toBeInTheDocument();
expect(
getByText(itemList, "carrot cake - Quantity: 6")
).toBeInTheDocument();
});
test("highlighting in red elements whose quantity is below five", () => {
const inventory = { cheesecake: 5, "apple pie": 2, "carrot cake": 6 };
updateItemList(inventory);
expect(screen.getByText("apple pie - Quantity: 2")).toHaveStyle({
color: "red"
});
});
test("adding a paragraph indicating what was the update", () => {
const inventory = { cheesecake: 5, "apple pie": 2 };
updateItemList(inventory);
expect(
screen.getByText(
`The inventory has been updated - ${JSON.stringify(inventory)}`
)
).toBeTruthy();
});
});
================================================
FILE: chapter6/2_asserting_on_the_dom/5_writing_better_dom_assertions/index.html
================================================
Inventory Manager
Inventory Contents
================================================
FILE: chapter6/2_asserting_on_the_dom/5_writing_better_dom_assertions/inventoryController.js
================================================
const data = { inventory: {} };
const addItem = (itemName, quantity) => {
const currentQuantity = data.inventory[itemName] || 0;
data.inventory[itemName] = currentQuantity + quantity;
};
module.exports = { data, addItem };
================================================
FILE: chapter6/2_asserting_on_the_dom/5_writing_better_dom_assertions/inventoryController.test.js
================================================
const { addItem, data } = require("./inventoryController");
describe("addItem", () => {
test("adding new items to the inventory", () => {
data.inventory = {};
addItem("cheesecake", 5);
expect(data.inventory.cheesecake).toBe(5);
});
});
================================================
FILE: chapter6/2_asserting_on_the_dom/5_writing_better_dom_assertions/jest.config.js
================================================
module.exports = {
setupFilesAfterEnv: ["./setupJestDom.js"]
};
================================================
FILE: chapter6/2_asserting_on_the_dom/5_writing_better_dom_assertions/main.js
================================================
const { addItem, data } = require("./inventoryController");
const { updateItemList } = require("./domController");
addItem("cheesecake", 3);
addItem("apple pie", 8);
addItem("carrot cake", 7);
updateItemList(data.inventory);
================================================
FILE: chapter6/2_asserting_on_the_dom/5_writing_better_dom_assertions/package.json
================================================
{
"name": "5_writing_better_dom_assertions",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "http-server ./",
"test": "jest",
"build": "browserify main.js -o bundle.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@testing-library/dom": "^7.2.2",
"@testing-library/jest-dom": "^5.5.0",
"browserify": "^16.5.1",
"http-server": "^0.12.1",
"jest": "^24.9.0"
}
}
================================================
FILE: chapter6/2_asserting_on_the_dom/5_writing_better_dom_assertions/setupJestDom.js
================================================
const jestDom = require("@testing-library/jest-dom");
expect.extend(jestDom);
================================================
FILE: chapter6/3_handling_events/1_handling_raw_events/domController.js
================================================
const { addItem, data } = require("./inventoryController");
const updateItemList = inventory => {
const inventoryList = window.document.getElementById("item-list");
// Clears the list
inventoryList.innerHTML = "";
Object.entries(inventory).forEach(([itemName, quantity]) => {
const listItem = window.document.createElement("li");
listItem.innerHTML = `${itemName} - Quantity: ${quantity}`;
if (quantity < 5) {
listItem.className = "almost-soldout";
}
inventoryList.appendChild(listItem);
});
const inventoryContents = JSON.stringify(inventory);
const p = window.document.createElement("p");
p.innerHTML = `The inventory has been updated - ${inventoryContents}`;
window.document.body.appendChild(p);
};
const handleAddItem = event => {
// Prevent the page from reloading as it would by default
event.preventDefault();
const { name, quantity } = event.target.elements;
addItem(name.value, parseInt(quantity.value, 10));
updateItemList(data.inventory);
};
const validItems = ["cheesecake", "apple pie", "carrot cake"];
const handleItemName = event => {
const itemName = event.target.value;
const errorMsg = window.document.getElementById("error-msg");
if (itemName === "") {
errorMsg.innerHTML = "";
} else if (!validItems.includes(itemName)) {
errorMsg.innerHTML = `${itemName} is not a valid item.`;
} else {
errorMsg.innerHTML = `${itemName} is valid!`;
}
};
module.exports = { updateItemList, handleAddItem, handleItemName };
================================================
FILE: chapter6/3_handling_events/1_handling_raw_events/domController.test.js
================================================
const fs = require("fs");
const initialHtml = fs.readFileSync("./index.html");
const { getByText, screen } = require("@testing-library/dom");
const {
updateItemList,
handleAddItem,
handleItemName
} = require("./domController");
beforeEach(() => {
document.body.innerHTML = initialHtml;
});
describe("updateItemList", () => {
test("updates the DOM with the inventory items", () => {
const inventory = {
cheesecake: 5,
"apple pie": 2,
"carrot cake": 6
};
updateItemList(inventory);
const itemList = document.getElementById("item-list");
expect(itemList.childNodes).toHaveLength(3);
expect(getByText(itemList, "cheesecake - Quantity: 5")).toBeInTheDocument();
expect(getByText(itemList, "apple pie - Quantity: 2")).toBeInTheDocument();
expect(
getByText(itemList, "carrot cake - Quantity: 6")
).toBeInTheDocument();
});
test("highlighting in red elements whose quantity is below five", () => {
const inventory = { cheesecake: 5, "apple pie": 2, "carrot cake": 6 };
updateItemList(inventory);
expect(screen.getByText("apple pie - Quantity: 2")).toHaveStyle({
color: "red"
});
});
test("adding a paragraph indicating what was the update", () => {
const inventory = { cheesecake: 5, "apple pie": 2 };
updateItemList(inventory);
expect(
screen.getByText(
`The inventory has been updated - ${JSON.stringify(inventory)}`
)
).toBeTruthy();
});
});
describe("handleAddItem", () => {
test("adding items to the page", () => {
const event = {
preventDefault: jest.fn(),
target: {
elements: {
name: { value: "cheesecake" },
quantity: { value: "6" }
}
}
};
handleAddItem(event);
// Checking if the form's default reload is prevent
expect(event.preventDefault.mock.calls).toHaveLength(1);
const itemList = document.getElementById("item-list");
expect(getByText(itemList, "cheesecake - Quantity: 6")).toBeInTheDocument();
});
});
describe("handleItemName", () => {
test("entering valid item names", () => {
const event = {
preventDefault: jest.fn(),
target: { value: "cheesecake" }
};
handleItemName(event);
expect(screen.getByText("cheesecake is valid!")).toBeInTheDocument();
});
test("entering invalid item names", () => {
const event = {
preventDefault: jest.fn(),
target: { value: "book" }
};
handleItemName(event);
expect(screen.getByText("book is not a valid item.")).toBeInTheDocument();
});
});
================================================
FILE: chapter6/3_handling_events/1_handling_raw_events/index.html
================================================
Inventory Manager
Inventory Contents
================================================
FILE: chapter6/3_handling_events/1_handling_raw_events/inventoryController.js
================================================
const data = { inventory: {} };
const addItem = (itemName, quantity) => {
const currentQuantity = data.inventory[itemName] || 0;
data.inventory[itemName] = currentQuantity + quantity;
return data.inventory;
};
module.exports = { data, addItem };
================================================
FILE: chapter6/3_handling_events/1_handling_raw_events/inventoryController.test.js
================================================
const { addItem, data } = require("./inventoryController");
describe("addItem", () => {
test("adding new items to the inventory", () => {
data.inventory = {};
addItem("cheesecake", 5);
expect(data.inventory.cheesecake).toBe(5);
});
});
================================================
FILE: chapter6/3_handling_events/1_handling_raw_events/jest.config.js
================================================
module.exports = {
setupFilesAfterEnv: ["./setupJestDom.js"]
};
================================================
FILE: chapter6/3_handling_events/1_handling_raw_events/main.js
================================================
const { handleAddItem, handleItemName } = require("./domController");
const form = document.getElementById("add-item-form");
form.addEventListener("submit", handleAddItem);
const itemInput = document.querySelector(`input[name="name"]`);
itemInput.addEventListener("input", handleItemName);
================================================
FILE: chapter6/3_handling_events/1_handling_raw_events/main.test.js
================================================
const fs = require("fs");
const initialHtml = fs.readFileSync("./index.html");
const { screen, getByText } = require("@testing-library/dom");
beforeEach(() => {
document.body.innerHTML = initialHtml;
// You must execute main.js again so that it can attach the
// event listener to the form every time the body changes.
// Here you must use `jest.resetModules` because otherwise
// Jest will have cached `main.js` and it will _not_ run again.
jest.resetModules();
require("./main");
});
test("adding items through the form", () => {
screen.getByPlaceholderText("Item name").value = "cheesecake";
screen.getByPlaceholderText("Quantity").value = "6";
const event = new Event("submit");
const form = document.getElementById("add-item-form");
form.dispatchEvent(event);
const itemList = document.getElementById("item-list");
expect(getByText(itemList, "cheesecake - Quantity: 6")).toBeInTheDocument();
});
describe("item name validation", () => {
test("entering valid item names ", () => {
const itemField = screen.getByPlaceholderText("Item name");
itemField.value = "cheesecake";
const inputEvent = new Event("input");
itemField.dispatchEvent(inputEvent);
expect(screen.getByText("cheesecake is valid!")).toBeInTheDocument();
});
test("entering invalid item names ", () => {
const itemField = screen.getByPlaceholderText("Item name");
itemField.value = "book";
const inputEvent = new Event("input");
itemField.dispatchEvent(inputEvent);
expect(screen.getByText("book is not a valid item.")).toBeInTheDocument();
});
});
================================================
FILE: chapter6/3_handling_events/1_handling_raw_events/package.json
================================================
{
"name": "1_handling_raw_events",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "http-server ./",
"test": "jest",
"build": "browserify main.js -o bundle.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@testing-library/dom": "^7.2.2",
"@testing-library/jest-dom": "^5.5.0",
"browserify": "^16.5.1",
"http-server": "^0.12.1",
"jest": "^24.9.0"
}
}
================================================
FILE: chapter6/3_handling_events/1_handling_raw_events/setupJestDom.js
================================================
const jestDom = require("@testing-library/jest-dom");
expect.extend(jestDom);
================================================
FILE: chapter6/3_handling_events/2_bubbling_up_events/domController.js
================================================
const { addItem, data } = require("./inventoryController");
const updateItemList = inventory => {
const inventoryList = window.document.getElementById("item-list");
// Clears the list
inventoryList.innerHTML = "";
Object.entries(inventory).forEach(([itemName, quantity]) => {
const listItem = window.document.createElement("li");
listItem.innerHTML = `${itemName} - Quantity: ${quantity}`;
if (quantity < 5) {
listItem.className = "almost-soldout";
}
inventoryList.appendChild(listItem);
});
const inventoryContents = JSON.stringify(inventory);
const p = window.document.createElement("p");
p.innerHTML = `The inventory has been updated - ${inventoryContents}`;
window.document.body.appendChild(p);
};
const handleAddItem = event => {
// Prevent the page from reloading as it would by default
event.preventDefault();
const { name, quantity } = event.target.elements;
addItem(name.value, parseInt(quantity.value, 10));
updateItemList(data.inventory);
};
const validItems = ["cheesecake", "apple pie", "carrot cake"];
const checkFormValues = () => {
const itemName = document.querySelector(`input[name="name"]`).value;
const quantity = document.querySelector(`input[name="quantity"]`).value;
const itemNameIsEmpty = itemName === "";
const itemNameIsInvalid = !validItems.includes(itemName);
const quantityIsEmpty = quantity === "";
const errorMsg = window.document.getElementById("error-msg");
if (itemNameIsEmpty) {
errorMsg.innerHTML = "";
} else if (itemNameIsInvalid) {
errorMsg.innerHTML = `${itemName} is not a valid item.`;
} else {
errorMsg.innerHTML = `${itemName} is valid!`;
}
const submitButton = document.querySelector(`button[type="submit"]`);
if (itemNameIsEmpty || itemNameIsInvalid || quantityIsEmpty) {
submitButton.disabled = true;
} else {
submitButton.disabled = false;
}
};
module.exports = { updateItemList, handleAddItem, checkFormValues };
================================================
FILE: chapter6/3_handling_events/2_bubbling_up_events/domController.test.js
================================================
const fs = require("fs");
const initialHtml = fs.readFileSync("./index.html");
const { getByText, screen } = require("@testing-library/dom");
const {
updateItemList,
handleAddItem,
checkFormValues
} = require("./domController");
beforeEach(() => {
document.body.innerHTML = initialHtml;
});
describe("updateItemList", () => {
test("updates the DOM with the inventory items", () => {
const inventory = {
cheesecake: 5,
"apple pie": 2,
"carrot cake": 6
};
updateItemList(inventory);
const itemList = document.getElementById("item-list");
expect(itemList.childNodes).toHaveLength(3);
expect(getByText(itemList, "cheesecake - Quantity: 5")).toBeInTheDocument();
expect(getByText(itemList, "apple pie - Quantity: 2")).toBeInTheDocument();
expect(
getByText(itemList, "carrot cake - Quantity: 6")
).toBeInTheDocument();
});
test("highlighting in red elements whose quantity is below five", () => {
const inventory = { cheesecake: 5, "apple pie": 2, "carrot cake": 6 };
updateItemList(inventory);
expect(screen.getByText("apple pie - Quantity: 2")).toHaveStyle({
color: "red"
});
});
test("adding a paragraph indicating what was the update", () => {
const inventory = { cheesecake: 5, "apple pie": 2 };
updateItemList(inventory);
expect(
screen.getByText(
`The inventory has been updated - ${JSON.stringify(inventory)}`
)
).toBeTruthy();
});
});
describe("handleAddItem", () => {
test("adding items to the page", () => {
const event = {
preventDefault: jest.fn(),
target: {
elements: {
name: { value: "cheesecake" },
quantity: { value: "6" }
}
}
};
handleAddItem(event);
// Checking if the form's default reload is prevent
expect(event.preventDefault.mock.calls).toHaveLength(1);
const itemList = document.getElementById("item-list");
expect(getByText(itemList, "cheesecake - Quantity: 6")).toBeInTheDocument();
});
});
describe("checkFormValues", () => {
test("entering valid item values", () => {
document.querySelector(`input[name="name"]`).value = "cheesecake";
document.querySelector(`input[name="quantity"]`).value = "1";
checkFormValues();
expect(screen.getByText("Add to inventory")).toBeEnabled();
});
test("entering invalid item names", () => {
document.querySelector(`input[name="name"]`).value = "invalid";
document.querySelector(`input[name="quantity"]`).value = "1";
checkFormValues();
expect(screen.getByText("Add to inventory")).toBeDisabled();
document.querySelector(`input[name="name"]`).value = "cheesecake";
document.querySelector(`input[name="quantity"]`).value = "";
checkFormValues();
expect(screen.getByText("Add to inventory")).toBeDisabled();
});
});
================================================
FILE: chapter6/3_handling_events/2_bubbling_up_events/index.html
================================================
Inventory Manager
Inventory Contents
================================================
FILE: chapter6/3_handling_events/2_bubbling_up_events/inventoryController.js
================================================
const data = { inventory: {} };
const addItem = (itemName, quantity) => {
const currentQuantity = data.inventory[itemName] || 0;
data.inventory[itemName] = currentQuantity + quantity;
return data.inventory;
};
module.exports = { data, addItem };
================================================
FILE: chapter6/3_handling_events/2_bubbling_up_events/inventoryController.test.js
================================================
const { addItem, data } = require("./inventoryController");
describe("addItem", () => {
test("adding new items to the inventory", () => {
data.inventory = {};
addItem("cheesecake", 5);
expect(data.inventory.cheesecake).toBe(5);
});
});
================================================
FILE: chapter6/3_handling_events/2_bubbling_up_events/jest.config.js
================================================
module.exports = {
setupFilesAfterEnv: ["./setupJestDom.js"]
};
================================================
FILE: chapter6/3_handling_events/2_bubbling_up_events/main.js
================================================
const { handleAddItem, checkFormValues } = require("./domController");
const form = document.getElementById("add-item-form");
form.addEventListener("submit", handleAddItem);
form.addEventListener("input", checkFormValues);
// Run `checkFormValues` once to see if the initial state is valid
checkFormValues();
================================================
FILE: chapter6/3_handling_events/2_bubbling_up_events/main.test.js
================================================
const fs = require("fs");
const initialHtml = fs.readFileSync("./index.html");
const { screen, getByText } = require("@testing-library/dom");
beforeEach(() => {
document.body.innerHTML = initialHtml;
// You must execute main.js again so that it can attach the
// event listener to the form every time the body changes.
// Here you must use `jest.resetModules` because otherwise
// Jest will have cached `main.js` and it will _not_ run again.
jest.resetModules();
require("./main");
});
test("adding items through the form", () => {
screen.getByPlaceholderText("Item name").value = "cheesecake";
screen.getByPlaceholderText("Quantity").value = "6";
const event = new Event("submit");
const form = document.getElementById("add-item-form");
form.dispatchEvent(event);
const itemList = document.getElementById("item-list");
expect(getByText(itemList, "cheesecake - Quantity: 6")).toBeInTheDocument();
});
describe("item name validation", () => {
test("entering valid item names ", () => {
const itemField = screen.getByPlaceholderText("Item name");
itemField.value = "cheesecake";
const inputEvent = new Event("input", { bubbles: true });
itemField.dispatchEvent(inputEvent);
expect(screen.getByText("cheesecake is valid!")).toBeInTheDocument();
});
test("entering invalid item names ", () => {
const itemField = screen.getByPlaceholderText("Item name");
itemField.value = "book";
const inputEvent = new Event("input", { bubbles: true });
itemField.dispatchEvent(inputEvent);
expect(screen.getByText("book is not a valid item.")).toBeInTheDocument();
});
});
================================================
FILE: chapter6/3_handling_events/2_bubbling_up_events/package.json
================================================
{
"name": "1_handling_raw_events",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "http-server ./",
"test": "jest",
"build": "browserify main.js -o bundle.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@testing-library/dom": "^7.2.2",
"@testing-library/jest-dom": "^5.5.0",
"browserify": "^16.5.1",
"http-server": "^0.12.1",
"jest": "^24.9.0"
}
}
================================================
FILE: chapter6/3_handling_events/2_bubbling_up_events/setupJestDom.js
================================================
const jestDom = require("@testing-library/jest-dom");
expect.extend(jestDom);
================================================
FILE: chapter6/3_handling_events/3_dom_testing_library_events/domController.js
================================================
const { addItem, data } = require("./inventoryController");
const updateItemList = inventory => {
const inventoryList = window.document.getElementById("item-list");
// Clears the list
inventoryList.innerHTML = "";
Object.entries(inventory).forEach(([itemName, quantity]) => {
const listItem = window.document.createElement("li");
listItem.innerHTML = `${itemName} - Quantity: ${quantity}`;
if (quantity < 5) {
listItem.className = "almost-soldout";
}
inventoryList.appendChild(listItem);
});
const inventoryContents = JSON.stringify(inventory);
const p = window.document.createElement("p");
p.innerHTML = `The inventory has been updated - ${inventoryContents}`;
window.document.body.appendChild(p);
};
const handleAddItem = event => {
// Prevent the page from reloading as it would by default
event.preventDefault();
const { name, quantity } = event.target.elements;
addItem(name.value, parseInt(quantity.value, 10));
updateItemList(data.inventory);
};
const validItems = ["cheesecake", "apple pie", "carrot cake"];
const checkFormValues = () => {
const itemName = document.querySelector(`input[name="name"]`).value;
const quantity = document.querySelector(`input[name="quantity"]`).value;
const itemNameIsEmpty = itemName === "";
const itemNameIsInvalid = !validItems.includes(itemName);
const quantityIsEmpty = quantity === "";
const errorMsg = window.document.getElementById("error-msg");
if (itemNameIsEmpty) {
errorMsg.innerHTML = "";
} else if (itemNameIsInvalid) {
errorMsg.innerHTML = `${itemName} is not a valid item.`;
} else {
errorMsg.innerHTML = `${itemName} is valid!`;
}
const submitButton = document.querySelector(`button[type="submit"]`);
if (itemNameIsEmpty || itemNameIsInvalid || quantityIsEmpty) {
submitButton.disabled = true;
} else {
submitButton.disabled = false;
}
};
module.exports = { updateItemList, handleAddItem, checkFormValues };
================================================
FILE: chapter6/3_handling_events/3_dom_testing_library_events/domController.test.js
================================================
const fs = require("fs");
const initialHtml = fs.readFileSync("./index.html");
const { getByText, screen } = require("@testing-library/dom");
const {
updateItemList,
handleAddItem,
checkFormValues
} = require("./domController");
beforeEach(() => {
document.body.innerHTML = initialHtml;
});
describe("updateItemList", () => {
test("updates the DOM with the inventory items", () => {
const inventory = {
cheesecake: 5,
"apple pie": 2,
"carrot cake": 6
};
updateItemList(inventory);
const itemList = document.getElementById("item-list");
expect(itemList.childNodes).toHaveLength(3);
expect(getByText(itemList, "cheesecake - Quantity: 5")).toBeInTheDocument();
expect(getByText(itemList, "apple pie - Quantity: 2")).toBeInTheDocument();
expect(
getByText(itemList, "carrot cake - Quantity: 6")
).toBeInTheDocument();
});
test("highlighting in red elements whose quantity is below five", () => {
const inventory = { cheesecake: 5, "apple pie": 2, "carrot cake": 6 };
updateItemList(inventory);
expect(screen.getByText("apple pie - Quantity: 2")).toHaveStyle({
color: "red"
});
});
test("adding a paragraph indicating what was the update", () => {
const inventory = { cheesecake: 5, "apple pie": 2 };
updateItemList(inventory);
expect(
screen.getByText(
`The inventory has been updated - ${JSON.stringify(inventory)}`
)
).toBeTruthy();
});
});
describe("handleAddItem", () => {
test("adding items to the page", () => {
const event = {
preventDefault: jest.fn(),
target: {
elements: {
name: { value: "cheesecake" },
quantity: { value: "6" }
}
}
};
handleAddItem(event);
// Checking if the form's default reload is prevent
expect(event.preventDefault.mock.calls).toHaveLength(1);
const itemList = document.getElementById("item-list");
expect(getByText(itemList, "cheesecake - Quantity: 6")).toBeInTheDocument();
});
});
describe("checkFormValues", () => {
test("entering valid item values", () => {
document.querySelector(`input[name="name"]`).value = "cheesecake";
document.querySelector(`input[name="quantity"]`).value = "1";
checkFormValues();
expect(screen.getByText("Add to inventory")).toBeEnabled();
});
test("entering invalid item names", () => {
document.querySelector(`input[name="name"]`).value = "invalid";
document.querySelector(`input[name="quantity"]`).value = "1";
checkFormValues();
expect(screen.getByText("Add to inventory")).toBeDisabled();
document.querySelector(`input[name="name"]`).value = "cheesecake";
document.querySelector(`input[name="quantity"]`).value = "";
checkFormValues();
expect(screen.getByText("Add to inventory")).toBeDisabled();
});
});
================================================
FILE: chapter6/3_handling_events/3_dom_testing_library_events/index.html
================================================
Inventory Manager
Inventory Contents
================================================
FILE: chapter6/3_handling_events/3_dom_testing_library_events/inventoryController.js
================================================
const data = { inventory: {} };
const addItem = (itemName, quantity) => {
const currentQuantity = data.inventory[itemName] || 0;
data.inventory[itemName] = currentQuantity + quantity;
return data.inventory;
};
module.exports = { data, addItem };
================================================
FILE: chapter6/3_handling_events/3_dom_testing_library_events/inventoryController.test.js
================================================
const { addItem, data } = require("./inventoryController");
describe("addItem", () => {
test("adding new items to the inventory", () => {
data.inventory = {};
addItem("cheesecake", 5);
expect(data.inventory.cheesecake).toBe(5);
});
});
================================================
FILE: chapter6/3_handling_events/3_dom_testing_library_events/jest.config.js
================================================
module.exports = {
setupFilesAfterEnv: ["./setupJestDom.js"]
};
================================================
FILE: chapter6/3_handling_events/3_dom_testing_library_events/main.js
================================================
const { handleAddItem, checkFormValues } = require("./domController");
const form = document.getElementById("add-item-form");
form.addEventListener("submit", handleAddItem);
form.addEventListener("input", checkFormValues);
// Run `checkFormValues` once to see if the initial state is valid
checkFormValues();
================================================
FILE: chapter6/3_handling_events/3_dom_testing_library_events/main.test.js
================================================
const fs = require("fs");
const initialHtml = fs.readFileSync("./index.html");
const { screen, getByText, fireEvent } = require("@testing-library/dom");
beforeEach(() => {
document.body.innerHTML = initialHtml;
// You must execute main.js again so that it can attach the
// event listener to the form every time the body changes.
// Here you must use `jest.resetModules` because otherwise
// Jest will have cached `main.js` and it will _not_ run again.
jest.resetModules();
require("./main");
});
test("adding items through the form", () => {
const itemField = screen.getByPlaceholderText("Item name");
fireEvent.input(itemField, {
target: { value: "cheesecake" },
bubbles: true
});
const quantityField = screen.getByPlaceholderText("Quantity");
fireEvent.input(quantityField, { target: { value: "6" }, bubbles: true });
const submitBtn = screen.getByText("Add to inventory");
fireEvent.click(submitBtn);
const itemList = document.getElementById("item-list");
expect(getByText(itemList, "cheesecake - Quantity: 6")).toBeInTheDocument();
});
describe("item name validation", () => {
test("entering valid item names ", () => {
const itemField = screen.getByPlaceholderText("Item name");
fireEvent.input(itemField, {
target: { value: "cheesecake" },
bubbles: true
});
expect(screen.getByText("cheesecake is valid!")).toBeInTheDocument();
});
test("entering invalid item names ", () => {
const itemField = screen.getByPlaceholderText("Item name");
fireEvent.input(itemField, { target: { value: "book" }, bubbles: true });
expect(screen.getByText("book is not a valid item.")).toBeInTheDocument();
});
});
================================================
FILE: chapter6/3_handling_events/3_dom_testing_library_events/package.json
================================================
{
"name": "1_handling_raw_events",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "http-server ./",
"test": "jest",
"build": "browserify main.js -o bundle.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@testing-library/dom": "^7.2.2",
"@testing-library/jest-dom": "^5.5.0",
"browserify": "^16.5.1",
"http-server": "^0.12.1",
"jest": "^24.9.0"
}
}
================================================
FILE: chapter6/3_handling_events/3_dom_testing_library_events/setupJestDom.js
================================================
const jestDom = require("@testing-library/jest-dom");
expect.extend(jestDom);
================================================
FILE: chapter6/4_testing_and_browser_apis/1_localstorage/domController.js
================================================
const { addItem, data } = require("./inventoryController");
const updateItemList = inventory => {
if (inventory === null) return;
localStorage.setItem("inventory", JSON.stringify(inventory));
const inventoryList = window.document.getElementById("item-list");
// Clears the list
inventoryList.innerHTML = "";
Object.entries(inventory).forEach(([itemName, quantity]) => {
const listItem = window.document.createElement("li");
listItem.innerHTML = `${itemName} - Quantity: ${quantity}`;
if (quantity < 5) {
listItem.className = "almost-soldout";
}
inventoryList.appendChild(listItem);
});
const inventoryContents = JSON.stringify(inventory);
const p = window.document.createElement("p");
p.innerHTML = `The inventory has been updated - ${inventoryContents}`;
window.document.body.appendChild(p);
};
const handleAddItem = event => {
// Prevent the page from reloading as it would by default
event.preventDefault();
const { name, quantity } = event.target.elements;
addItem(name.value, parseInt(quantity.value, 10));
updateItemList(data.inventory);
};
const validItems = ["cheesecake", "apple pie", "carrot cake"];
const checkFormValues = () => {
const itemName = document.querySelector(`input[name="name"]`).value;
const quantity = document.querySelector(`input[name="quantity"]`).value;
const itemNameIsEmpty = itemName === "";
const itemNameIsInvalid = !validItems.includes(itemName);
const quantityIsEmpty = quantity === "";
const errorMsg = window.document.getElementById("error-msg");
if (itemNameIsEmpty) {
errorMsg.innerHTML = "";
} else if (itemNameIsInvalid) {
errorMsg.innerHTML = `${itemName} is not a valid item.`;
} else {
errorMsg.innerHTML = `${itemName} is valid!`;
}
const submitButton = document.querySelector(`button[type="submit"]`);
if (itemNameIsEmpty || itemNameIsInvalid || quantityIsEmpty) {
submitButton.disabled = true;
} else {
submitButton.disabled = false;
}
};
module.exports = { updateItemList, handleAddItem, checkFormValues };
================================================
FILE: chapter6/4_testing_and_browser_apis/1_localstorage/domController.test.js
================================================
const fs = require("fs");
const initialHtml = fs.readFileSync("./index.html");
const { getByText, screen } = require("@testing-library/dom");
const {
updateItemList,
handleAddItem,
checkFormValues
} = require("./domController");
beforeEach(() => {
document.body.innerHTML = initialHtml;
});
describe("updateItemList", () => {
beforeEach(() => localStorage.clear());
test("updates the DOM with the inventory items", () => {
const inventory = {
cheesecake: 5,
"apple pie": 2,
"carrot cake": 6
};
updateItemList(inventory);
const itemList = document.getElementById("item-list");
expect(itemList.childNodes).toHaveLength(3);
expect(getByText(itemList, "cheesecake - Quantity: 5")).toBeInTheDocument();
expect(getByText(itemList, "apple pie - Quantity: 2")).toBeInTheDocument();
expect(
getByText(itemList, "carrot cake - Quantity: 6")
).toBeInTheDocument();
});
test("highlighting in red elements whose quantity is below five", () => {
const inventory = { cheesecake: 5, "apple pie": 2, "carrot cake": 6 };
updateItemList(inventory);
expect(screen.getByText("apple pie - Quantity: 2")).toHaveStyle({
color: "red"
});
});
test("adding a paragraph indicating what was the update", () => {
const inventory = { cheesecake: 5, "apple pie": 2 };
updateItemList(inventory);
expect(
screen.getByText(
`The inventory has been updated - ${JSON.stringify(inventory)}`
)
).toBeTruthy();
});
test("updates the localStorage with the inventory", () => {
const inventory = { cheesecake: 5, "apple pie": 2 };
updateItemList(inventory);
expect(localStorage.getItem("inventory")).toEqual(
JSON.stringify(inventory)
);
});
test("does not update the inventory when passing null", () => {
localStorage.setItem("inventory", JSON.stringify({ cheesecake: 5 }));
updateItemList(null);
expect(localStorage.getItem("inventory")).toEqual(
JSON.stringify({ cheesecake: 5 })
);
});
});
describe("handleAddItem", () => {
test("adding items to the page", () => {
const event = {
preventDefault: jest.fn(),
target: {
elements: {
name: { value: "cheesecake" },
quantity: { value: "6" }
}
}
};
handleAddItem(event);
// Checking if the form's default reload is prevent
expect(event.preventDefault.mock.calls).toHaveLength(1);
const itemList = document.getElementById("item-list");
expect(getByText(itemList, "cheesecake - Quantity: 6")).toBeInTheDocument();
});
});
describe("checkFormValues", () => {
test("entering valid item values", () => {
document.querySelector(`input[name="name"]`).value = "cheesecake";
document.querySelector(`input[name="quantity"]`).value = "1";
checkFormValues();
expect(screen.getByText("Add to inventory")).toBeEnabled();
});
test("entering invalid item names", () => {
document.querySelector(`input[name="name"]`).value = "invalid";
document.querySelector(`input[name="quantity"]`).value = "1";
checkFormValues();
expect(screen.getByText("Add to inventory")).toBeDisabled();
document.querySelector(`input[name="name"]`).value = "cheesecake";
document.querySelector(`input[name="quantity"]`).value = "";
checkFormValues();
expect(screen.getByText("Add to inventory")).toBeDisabled();
});
});
================================================
FILE: chapter6/4_testing_and_browser_apis/1_localstorage/index.html
================================================
Inventory Manager
Inventory Contents
================================================
FILE: chapter6/4_testing_and_browser_apis/1_localstorage/inventoryController.js
================================================
const data = { inventory: {} };
const addItem = (itemName, quantity) => {
const currentQuantity = data.inventory[itemName] || 0;
data.inventory[itemName] = currentQuantity + quantity;
return data.inventory;
};
module.exports = { data, addItem };
================================================
FILE: chapter6/4_testing_and_browser_apis/1_localstorage/inventoryController.test.js
================================================
const { addItem, data } = require("./inventoryController");
describe("addItem", () => {
test("adding new items to the inventory", () => {
data.inventory = {};
addItem("cheesecake", 5);
expect(data.inventory.cheesecake).toBe(5);
});
});
================================================
FILE: chapter6/4_testing_and_browser_apis/1_localstorage/jest.config.js
================================================
module.exports = {
setupFilesAfterEnv: ["./setupJestDom.js"]
};
================================================
FILE: chapter6/4_testing_and_browser_apis/1_localstorage/main.js
================================================
const {
handleAddItem,
checkFormValues,
updateItemList
} = require("./domController");
const { data } = require("./inventoryController");
const form = document.getElementById("add-item-form");
form.addEventListener("submit", handleAddItem);
form.addEventListener("input", checkFormValues);
// Run `checkFormValues` once to see if the initial state is valid
checkFormValues();
// Restore the inventory when the page loads
const storedInventory = JSON.parse(localStorage.getItem("inventory"));
if (storedInventory) {
data.inventory = storedInventory;
updateItemList(data.inventory);
}
================================================
FILE: chapter6/4_testing_and_browser_apis/1_localstorage/main.test.js
================================================
const fs = require("fs");
const initialHtml = fs.readFileSync("./index.html");
const { screen, getByText, fireEvent } = require("@testing-library/dom");
beforeEach(() => localStorage.clear());
beforeEach(() => {
document.body.innerHTML = initialHtml;
// You must execute main.js again so that it can attach the
// event listener to the form every time the body changes.
// Here you must use `jest.resetModules` because otherwise
// Jest will have cached `main.js` and it will _not_ run again.
jest.resetModules();
require("./main");
});
test("persists items between sessions", () => {
const itemField = screen.getByPlaceholderText("Item name");
fireEvent.input(itemField, {
target: { value: "cheesecake" },
bubbles: true
});
const quantityField = screen.getByPlaceholderText("Quantity");
fireEvent.input(quantityField, { target: { value: "6" }, bubbles: true });
const submitBtn = screen.getByText("Add to inventory");
fireEvent.click(submitBtn);
const itemListBefore = document.getElementById("item-list");
expect(itemListBefore.childNodes).toHaveLength(1);
expect(
getByText(itemListBefore, "cheesecake - Quantity: 6")
).toBeInTheDocument();
// This is equivalent to reloading the page
document.body.innerHTML = initialHtml;
jest.resetModules();
require("./main");
const itemListAfter = document.getElementById("item-list");
expect(itemListAfter.childNodes).toHaveLength(1);
expect(
getByText(itemListAfter, "cheesecake - Quantity: 6")
).toBeInTheDocument();
});
describe("adding items", () => {
test("updating the item list", () => {
const itemField = screen.getByPlaceholderText("Item name");
const submitBtn = screen.getByText("Add to inventory");
fireEvent.input(itemField, {
target: { value: "cheesecake" },
bubbles: true
});
const quantityField = screen.getByPlaceholderText("Quantity");
fireEvent.input(quantityField, { target: { value: "6" }, bubbles: true });
fireEvent.click(submitBtn);
const itemList = document.getElementById("item-list");
expect(getByText(itemList, "cheesecake - Quantity: 6")).toBeInTheDocument();
});
});
describe("item name validation", () => {
test("entering valid item names ", () => {
const itemField = screen.getByPlaceholderText("Item name");
fireEvent.input(itemField, {
target: { value: "cheesecake" },
bubbles: true
});
expect(screen.getByText("cheesecake is valid!")).toBeInTheDocument();
});
test("entering invalid item names ", () => {
const itemField = screen.getByPlaceholderText("Item name");
fireEvent.input(itemField, { target: { value: "book" }, bubbles: true });
expect(screen.getByText("book is not a valid item.")).toBeInTheDocument();
});
});
================================================
FILE: chapter6/4_testing_and_browser_apis/1_localstorage/package.json
================================================
{
"name": "2_localstorage",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "http-server ./",
"test": "jest",
"build": "browserify main.js -o bundle.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@testing-library/dom": "^7.2.2",
"@testing-library/jest-dom": "^5.5.0",
"browserify": "^16.5.1",
"http-server": "^0.12.1",
"jest": "^24.9.0"
}
}
================================================
FILE: chapter6/4_testing_and_browser_apis/1_localstorage/setupJestDom.js
================================================
const jestDom = require("@testing-library/jest-dom");
expect.extend(jestDom);
================================================
FILE: chapter6/4_testing_and_browser_apis/2_history_api/domController.js
================================================
const { addItem, data } = require("./inventoryController");
const updateItemList = inventory => {
if (inventory === null) return;
localStorage.setItem("inventory", JSON.stringify(inventory));
const inventoryList = window.document.getElementById("item-list");
// Clears the list
inventoryList.innerHTML = "";
Object.entries(inventory).forEach(([itemName, quantity]) => {
const listItem = window.document.createElement("li");
listItem.innerHTML = `${itemName} - Quantity: ${quantity}`;
if (quantity < 5) {
listItem.className = "almost-soldout";
}
inventoryList.appendChild(listItem);
});
const inventoryContents = JSON.stringify(inventory);
const p = window.document.createElement("p");
p.innerHTML = `The inventory has been updated - ${inventoryContents}`;
window.document.body.appendChild(p);
};
const handleAddItem = event => {
// Prevent the page from reloading as it would by default
event.preventDefault();
const { name, quantity } = event.target.elements;
addItem(name.value, parseInt(quantity.value, 10));
history.pushState({ inventory: { ...data.inventory } }, document.title);
updateItemList(data.inventory);
};
const validItems = ["cheesecake", "apple pie", "carrot cake"];
const checkFormValues = () => {
const itemName = document.querySelector(`input[name="name"]`).value;
const quantity = document.querySelector(`input[name="quantity"]`).value;
const itemNameIsEmpty = itemName === "";
const itemNameIsInvalid = !validItems.includes(itemName);
const quantityIsEmpty = quantity === "";
const errorMsg = window.document.getElementById("error-msg");
if (itemNameIsEmpty) {
errorMsg.innerHTML = "";
} else if (itemNameIsInvalid) {
errorMsg.innerHTML = `${itemName} is not a valid item.`;
} else {
errorMsg.innerHTML = `${itemName} is valid!`;
}
const submitButton = document.querySelector(`button[type="submit"]`);
if (itemNameIsEmpty || itemNameIsInvalid || quantityIsEmpty) {
submitButton.disabled = true;
} else {
submitButton.disabled = false;
}
};
const handleUndo = () => {
if (history.state === null) return;
history.back();
};
const handlePopstate = () => {
data.inventory = history.state ? history.state.inventory : {};
updateItemList(data.inventory);
};
module.exports = {
updateItemList,
handleAddItem,
checkFormValues,
handleUndo,
handlePopstate
};
================================================
FILE: chapter6/4_testing_and_browser_apis/2_history_api/domController.test.js
================================================
const fs = require("fs");
const initialHtml = fs.readFileSync("./index.html");
const { getByText, screen } = require("@testing-library/dom");
const {
updateItemList,
handleAddItem,
checkFormValues,
handleUndo,
handlePopstate
} = require("./domController");
const { clearHistoryHook, detachPopstateHandlers } = require("./testUtils");
const { data } = require("./inventoryController");
beforeEach(() => {
document.body.innerHTML = initialHtml;
});
describe("updateItemList", () => {
beforeEach(() => localStorage.clear());
test("updates the DOM with the inventory items", () => {
const inventory = {
cheesecake: 5,
"apple pie": 2,
"carrot cake": 6
};
updateItemList(inventory);
const itemList = document.getElementById("item-list");
expect(itemList.childNodes).toHaveLength(3);
expect(getByText(itemList, "cheesecake - Quantity: 5")).toBeInTheDocument();
expect(getByText(itemList, "apple pie - Quantity: 2")).toBeInTheDocument();
expect(
getByText(itemList, "carrot cake - Quantity: 6")
).toBeInTheDocument();
});
test("highlighting in red elements whose quantity is below five", () => {
const inventory = { cheesecake: 5, "apple pie": 2, "carrot cake": 6 };
updateItemList(inventory);
expect(screen.getByText("apple pie - Quantity: 2")).toHaveStyle({
color: "red"
});
});
test("adding a paragraph indicating what was the update", () => {
const inventory = { cheesecake: 5, "apple pie": 2 };
updateItemList(inventory);
expect(
screen.getByText(
`The inventory has been updated - ${JSON.stringify(inventory)}`
)
).toBeTruthy();
});
test("updates the localStorage with the inventory", () => {
const inventory = { cheesecake: 5, "apple pie": 2 };
updateItemList(inventory);
expect(localStorage.getItem("inventory")).toEqual(
JSON.stringify(inventory)
);
});
test("does not update the inventory when passing null", () => {
localStorage.setItem("inventory", JSON.stringify({ cheesecake: 5 }));
updateItemList(null);
expect(localStorage.getItem("inventory")).toEqual(
JSON.stringify({ cheesecake: 5 })
);
});
});
describe("handleAddItem", () => {
beforeEach(() => (data.inventory = {}));
test("adding items to the page", () => {
const event = {
preventDefault: jest.fn(),
target: {
elements: {
name: { value: "cheesecake" },
quantity: { value: "6" }
}
}
};
handleAddItem(event);
// Checking if the form's default reload is prevent
expect(event.preventDefault.mock.calls).toHaveLength(1);
const itemList = document.getElementById("item-list");
expect(getByText(itemList, "cheesecake - Quantity: 6")).toBeInTheDocument();
});
test("updating the application's history", () => {
const event = {
preventDefault: jest.fn(),
target: {
elements: {
name: { value: "cheesecake" },
quantity: { value: "6" }
}
}
};
handleAddItem(event);
expect(history.state).toEqual({ inventory: { cheesecake: 6 } });
});
});
describe("checkFormValues", () => {
test("entering valid item values", () => {
document.querySelector(`input[name="name"]`).value = "cheesecake";
document.querySelector(`input[name="quantity"]`).value = "1";
checkFormValues();
expect(screen.getByText("Add to inventory")).toBeEnabled();
});
test("entering invalid item names", () => {
document.querySelector(`input[name="name"]`).value = "invalid";
document.querySelector(`input[name="quantity"]`).value = "1";
checkFormValues();
expect(screen.getByText("Add to inventory")).toBeDisabled();
document.querySelector(`input[name="name"]`).value = "cheesecake";
document.querySelector(`input[name="quantity"]`).value = "";
checkFormValues();
expect(screen.getByText("Add to inventory")).toBeDisabled();
});
});
describe("tests with history", () => {
beforeEach(() => jest.spyOn(window, "addEventListener"));
afterEach(detachPopstateHandlers);
beforeEach(clearHistoryHook);
describe("handleUndo", () => {
test("going back from a non-initial state", done => {
window.addEventListener("popstate", () => {
expect(history.state).toEqual(null);
done();
});
history.pushState({ inventory: { cheesecake: 5 } }, "title");
handleUndo();
});
test("going back from an initial state", () => {
jest.spyOn(history, "back");
handleUndo();
// This assertion doesn't care about whether
// a call to `history.back` would have finished,
// it only checks whether it's been called
expect(history.back.mock.calls).toHaveLength(0);
});
});
describe("handlePopstate", () => {
test("updating the item list with the current state", () => {
history.pushState(
{ inventory: { cheesecake: 5, "carrot cake": 2 } },
"title"
);
handlePopstate();
const itemList = document.getElementById("item-list");
expect(itemList.childNodes).toHaveLength(2);
expect(
getByText(itemList, "cheesecake - Quantity: 5")
).toBeInTheDocument();
expect(
getByText(itemList, "carrot cake - Quantity: 2")
).toBeInTheDocument();
});
});
});
================================================
FILE: chapter6/4_testing_and_browser_apis/2_history_api/index.html
================================================
Inventory Manager
Inventory Contents
Undo
================================================
FILE: chapter6/4_testing_and_browser_apis/2_history_api/inventoryController.js
================================================
const data = { inventory: {} };
const addItem = (itemName, quantity) => {
const currentQuantity = data.inventory[itemName] || 0;
data.inventory[itemName] = currentQuantity + quantity;
return data.inventory;
};
module.exports = { data, addItem };
================================================
FILE: chapter6/4_testing_and_browser_apis/2_history_api/inventoryController.test.js
================================================
const { addItem, data } = require("./inventoryController");
describe("addItem", () => {
test("adding new items to the inventory", () => {
data.inventory = {};
addItem("cheesecake", 5);
expect(data.inventory.cheesecake).toBe(5);
});
});
================================================
FILE: chapter6/4_testing_and_browser_apis/2_history_api/jest.config.js
================================================
module.exports = {
setupFilesAfterEnv: ["./setupJestDom.js"]
};
================================================
FILE: chapter6/4_testing_and_browser_apis/2_history_api/main.js
================================================
const {
handleAddItem,
checkFormValues,
handleUndo,
handlePopstate,
updateItemList
} = require("./domController");
const { data } = require("./inventoryController");
const form = document.getElementById("add-item-form");
form.addEventListener("submit", handleAddItem);
form.addEventListener("input", checkFormValues);
const undoButton = document.getElementById("undo-button");
undoButton.addEventListener("click", handleUndo);
window.addEventListener("popstate", handlePopstate);
// Run `checkFormValues` once to see if the initial state is valid
checkFormValues();
// Restore the inventory when the page loads
const storedInventory = JSON.parse(localStorage.getItem("inventory"));
if (storedInventory) {
data.inventory = storedInventory;
updateItemList(data.inventory);
}
================================================
FILE: chapter6/4_testing_and_browser_apis/2_history_api/main.test.js
================================================
const fs = require("fs");
const initialHtml = fs.readFileSync("./index.html");
const { screen, getByText, fireEvent } = require("@testing-library/dom");
const { clearHistoryHook, detachPopstateHandlers } = require("./testUtils.js");
beforeEach(clearHistoryHook);
beforeEach(() => localStorage.clear());
beforeEach(() => {
document.body.innerHTML = initialHtml;
// You must execute main.js again so that it can attach the
// event listener to the form every time the body changes.
// Here you must use `jest.resetModules` because otherwise
// Jest will have cached `main.js` and it will _not_ run again.
jest.resetModules();
require("./main");
// You can only spy on `window.addEventListener` after `main.js`
// has been executed. Otherwise `detachPopstateHandlers` will
// also detach the handlers that `main.js` attached to the page.
jest.spyOn(window, "addEventListener");
});
afterEach(detachPopstateHandlers);
test("persists items between sessions", () => {
const itemField = screen.getByPlaceholderText("Item name");
const submitBtn = screen.getByText("Add to inventory");
fireEvent.input(itemField, {
target: { value: "cheesecake" },
bubbles: true
});
const quantityField = screen.getByPlaceholderText("Quantity");
fireEvent.input(quantityField, { target: { value: "6" }, bubbles: true });
fireEvent.click(submitBtn);
const itemListBefore = document.getElementById("item-list");
expect(itemListBefore.childNodes).toHaveLength(1);
expect(
getByText(itemListBefore, "cheesecake - Quantity: 6")
).toBeInTheDocument();
// This is equivalent to reloading the page
document.body.innerHTML = initialHtml;
jest.resetModules();
require("./main");
const itemListAfter = document.getElementById("item-list");
expect(itemListAfter.childNodes).toHaveLength(1);
expect(
getByText(itemListAfter, "cheesecake - Quantity: 6")
).toBeInTheDocument();
});
describe("adding items", () => {
test("updating the item list", () => {
const itemField = screen.getByPlaceholderText("Item name");
const submitBtn = screen.getByText("Add to inventory");
fireEvent.input(itemField, {
target: { value: "cheesecake" },
bubbles: true
});
const quantityField = screen.getByPlaceholderText("Quantity");
fireEvent.input(quantityField, { target: { value: "6" }, bubbles: true });
fireEvent.click(submitBtn);
const itemList = document.getElementById("item-list");
expect(getByText(itemList, "cheesecake - Quantity: 6")).toBeInTheDocument();
});
test("undo to one item", done => {
const itemField = screen.getByPlaceholderText("Item name");
const quantityField = screen.getByPlaceholderText("Quantity");
const submitBtn = screen.getByText("Add to inventory");
fireEvent.input(itemField, {
target: { value: "cheesecake" },
bubbles: true
});
fireEvent.input(quantityField, { target: { value: "6" }, bubbles: true });
fireEvent.click(submitBtn);
fireEvent.input(itemField, {
target: { value: "carrot cake" },
bubbles: true
});
fireEvent.input(quantityField, { target: { value: "5" }, bubbles: true });
fireEvent.click(submitBtn);
window.addEventListener("popstate", () => {
const itemList = document.getElementById("item-list");
expect(itemList.children).toHaveLength(1);
expect(
getByText(itemList, "cheesecake - Quantity: 6")
).toBeInTheDocument();
done();
});
fireEvent.click(screen.getByText("Undo"));
});
test("undo to empty list", done => {
const itemField = screen.getByPlaceholderText("Item name");
const submitBtn = screen.getByText("Add to inventory");
fireEvent.input(itemField, {
target: { value: "cheesecake" },
bubbles: true
});
const quantityField = screen.getByPlaceholderText("Quantity");
fireEvent.input(quantityField, { target: { value: "6" }, bubbles: true });
fireEvent.click(submitBtn);
expect(history.state).toEqual({ inventory: { cheesecake: 6 } });
window.addEventListener("popstate", () => {
const itemList = document.getElementById("item-list");
expect(itemList).toBeEmpty();
done();
});
fireEvent.click(screen.getByText("Undo"));
});
});
describe("item name validation", () => {
test("entering valid item names ", () => {
const itemField = screen.getByPlaceholderText("Item name");
fireEvent.input(itemField, {
target: { value: "cheesecake" },
bubbles: true
});
expect(screen.getByText("cheesecake is valid!")).toBeInTheDocument();
});
test("entering invalid item names ", () => {
const itemField = screen.getByPlaceholderText("Item name");
fireEvent.input(itemField, { target: { value: "book" }, bubbles: true });
expect(screen.getByText("book is not a valid item.")).toBeInTheDocument();
});
});
================================================
FILE: chapter6/4_testing_and_browser_apis/2_history_api/package.json
================================================
{
"name": "2_localstorage",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "http-server ./",
"test": "jest",
"build": "browserify main.js -o bundle.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@testing-library/dom": "^7.2.2",
"@testing-library/jest-dom": "^5.5.0",
"browserify": "^16.5.1",
"http-server": "^0.12.1",
"jest": "^24.9.0"
}
}
================================================
FILE: chapter6/4_testing_and_browser_apis/2_history_api/setupJestDom.js
================================================
const jestDom = require("@testing-library/jest-dom");
expect.extend(jestDom);
================================================
FILE: chapter6/4_testing_and_browser_apis/2_history_api/testUtils.js
================================================
const clearHistoryHook = done => {
const clearHistory = () => {
if (history.state === null) {
window.removeEventListener("popstate", clearHistory);
return done();
}
history.back();
};
window.addEventListener("popstate", clearHistory);
clearHistory();
};
const detachPopstateHandlers = () => {
const popstateListeners = window.addEventListener.mock.calls.filter(
([eventName]) => {
return eventName === "popstate";
}
);
popstateListeners.forEach(([eventName, handlerFn]) => {
window.removeEventListener(eventName, handlerFn);
});
jest.restoreAllMocks();
};
module.exports = { clearHistoryHook, detachPopstateHandlers };
================================================
FILE: chapter6/4_testing_and_browser_apis/server/README.md
================================================
# Chapter 5 Server
To 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.
In case you want to update the back-end from Chapter 4 yourself, here's the list of changes I've done:
- For the server to accept the requests coming from the client, you'll need to use [`@koa/cors`](https://github.com/koajs/cors)
- 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.
- 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.
- At `GET /inventory` I have added a route which lists all items in the inventory.
================================================
FILE: chapter6/4_testing_and_browser_apis/server/authenticationController.js
================================================
const crypto = require("crypto");
const { db } = require("./dbConnection");
const hashPassword = password => {
const hash = crypto.createHash("sha256");
hash.update(password);
return hash.digest("hex");
};
const credentialsAreValid = async (username, password) => {
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) return false;
return hashPassword(password) === user.passwordHash;
};
const authenticationMiddleware = async (ctx, next) => {
try {
const authHeader = ctx.request.headers.authorization;
const credentials = Buffer.from(
authHeader.slice("basic".length + 1),
"base64"
).toString();
const [username, password] = credentials.split(":");
const validCredentialsSent = await credentialsAreValid(username, password);
if (!validCredentialsSent) throw new Error("invalid credentials");
} catch (e) {
ctx.status = 401;
ctx.body = { message: "please provide valid credentials" };
return;
}
await next();
};
module.exports = {
hashPassword,
credentialsAreValid,
authenticationMiddleware
};
================================================
FILE: chapter6/4_testing_and_browser_apis/server/authenticationController.test.js
================================================
const crypto = require("crypto");
const {
hashPassword,
credentialsAreValid,
authenticationMiddleware
} = require("./authenticationController");
const { user: globalUser } = require("./userTestUtils");
describe("hashPassword", () => {
test("hashing passwords", () => {
const plainTextPassword = "password_example";
const hash = crypto.createHash("sha256");
hash.update(plainTextPassword);
const expectedHash = hash.digest("hex");
expect(hashPassword(plainTextPassword)).toBe(expectedHash);
});
});
describe("credentialsAreValid", () => {
test("validating credentials", async () => {
expect(await credentialsAreValid(globalUser.username, "a_password")).toBe(
true
);
});
});
describe("authenticationMiddleware", () => {
test("returning an error if the credentials are not valid", async () => {
const fakeAuth = Buffer.from("invalid:credentials").toString("base64");
const ctx = {
request: {
headers: { authorization: `Basic ${fakeAuth}` }
}
};
const next = jest.fn();
await authenticationMiddleware(ctx, next);
expect(next.mock.calls).toHaveLength(0);
expect(ctx).toEqual({
...ctx,
status: 401,
body: { message: "please provide valid credentials" }
});
});
test("authenticating properly", async () => {
const ctx = {
request: {
headers: { authorization: globalUser.authHeader }
}
};
const next = jest.fn();
await authenticationMiddleware(ctx, next);
expect(next.mock.calls).toHaveLength(1);
});
});
================================================
FILE: chapter6/4_testing_and_browser_apis/server/cartController.js
================================================
const { db } = require("./dbConnection");
const { removeFromInventory } = require("./inventoryController");
const logger = require("./logger");
const addItemToCart = async (username, itemName) => {
await removeFromInventory(itemName);
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) {
const userNotFound = new Error("user not found");
userNotFound.code = 404;
}
const itemEntry = await db
.select()
.from("carts_items")
.where({ userId: user.id, itemName })
.first();
if (itemEntry && itemEntry.quantity + 1 > 3) {
const limitError = new Error(
"You can't have more than three units of an item in your cart"
);
limitError.code = 400;
throw limitError;
}
if (itemEntry) {
await db("carts_items")
.increment("quantity")
.update({ updatedAt: new Date().toISOString() })
.where({
userId: itemEntry.userId,
itemName
});
} else {
await db("carts_items").insert({
userId: user.id,
itemName,
quantity: 1,
updatedAt: new Date().toISOString()
});
}
logger.log(`${itemName} added to ${username}'s cart`);
return db
.select("itemName", "quantity")
.from("carts_items")
.where({ userId: user.id });
};
const hoursInMs = n => 1000 * 60 * 60 * n;
const removeStaleItems = async () => {
const fourHoursAgo = new Date(Date.now() - hoursInMs(4)).toISOString();
const staleItems = await db
.select()
.from("carts_items")
.where("updatedAt", "<", fourHoursAgo);
if (staleItems.length === 0) return;
// Put stale items back in the inventory
const inventoryUpdates = staleItems.map(staleItem =>
db("inventory")
.increment("quantity", staleItem.quantity)
.where({ itemName: staleItem.itemName })
);
await Promise.all(inventoryUpdates);
// Delete stale items from cart
const staleItemTuples = staleItems.map(i => [i.itemName, i.userId]);
await db("carts_items")
.del()
.whereIn(["itemName", "userId"], staleItemTuples);
};
const monitorStaleItems = () => setInterval(removeStaleItems, hoursInMs(2));
module.exports = { addItemToCart, monitorStaleItems };
================================================
FILE: chapter6/4_testing_and_browser_apis/server/cartController.test.js
================================================
const { db } = require("./dbConnection");
const { addItemToCart, monitorStaleItems } = require("./cartController");
const { hashPassword } = require("./authenticationController");
const { user: globalUser } = require("./userTestUtils");
const FakeTimers = require("@sinonjs/fake-timers");
const fs = require("fs");
describe("addItemToCart", () => {
beforeEach(() => {
fs.writeFileSync("/tmp/logs.out", "");
});
test("adding unavailable items to cart", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 0 });
try {
await addItemToCart(globalUser.username, "cheesecake");
} catch (e) {
const expectedError = new Error("cheesecake is unavailable");
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
const finalCartContent = await db
.select("carts_items.*")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([]);
expect.assertions(2);
});
test("adding items above limit to cart", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 1 });
await db("carts_items").insert({
userId: globalUser.id,
itemName: "cheesecake",
quantity: 3
});
try {
await addItemToCart(globalUser.username, "cheesecake");
} catch (e) {
const expectedError = new Error(
"You can't have more than three units of an item in your cart"
);
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([{ itemName: "cheesecake", quantity: 3 }]);
expect.assertions(2);
});
test("logging added items", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 1 });
await db("carts_items").insert({
userId: globalUser.id,
itemName: "cheesecake",
quantity: 1
});
await addItemToCart(globalUser.username, "cheesecake");
const logs = fs.readFileSync("/tmp/logs.out", "utf-8");
expect(logs).toContain(
`cheesecake added to ${globalUser.username}'s cart\n`
);
});
});
const withRetries = async fn => {
// Capture the assertion error since Jest does not export it
const JestAssertionError = (() => {
try {
expect(false).toBe(true);
} catch (e) {
return e.constructor;
}
})();
try {
await fn();
} catch (e) {
if (e.constructor === JestAssertionError) {
// Wait 100ms before retrying
await new Promise(resolve => setTimeout(resolve, 100));
await withRetries(fn);
} else {
throw e;
}
}
};
describe("timers", () => {
const hoursInMs = n => 1000 * 60 * 60 * n;
let clock;
beforeEach(() => {
clock = FakeTimers.install({ toFake: ["Date", "setInterval"] });
});
afterEach(() => {
clock = clock.uninstall();
});
test("removing stale items", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 1 });
await addItemToCart(globalUser.username, "cheesecake");
clock.tick(hoursInMs(4));
timer = monitorStaleItems();
clock.tick(hoursInMs(2));
await withRetries(async () => {
const finalCartContent = await db
.select()
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([]);
});
await withRetries(async () => {
const inventoryContent = await db
.select("itemName", "quantity")
.from("inventory");
expect(inventoryContent).toEqual([
{ itemName: "cheesecake", quantity: 1 }
]);
});
});
});
================================================
FILE: chapter6/4_testing_and_browser_apis/server/dbConnection.js
================================================
const environmentName = process.env.NODE_ENV;
const db = require("knex")(require("./knexfile")[environmentName]);
const closeConnection = () => db.destroy();
module.exports = {
db,
closeConnection
};
================================================
FILE: chapter6/4_testing_and_browser_apis/server/disconnectFromDb.js
================================================
const { db } = require("./dbConnection");
afterAll(() => db.destroy());
================================================
FILE: chapter6/4_testing_and_browser_apis/server/inventoryController.js
================================================
const { db } = require("./dbConnection");
const removeFromInventory = async itemName => {
const inventoryEntry = await db
.select()
.from("inventory")
.where({ itemName })
.first();
if (!inventoryEntry || inventoryEntry.quantity === 0) {
const err = new Error(`${itemName} is unavailable`);
err.code = 400;
throw err;
}
await db("inventory")
.decrement("quantity")
.where({ itemName });
};
module.exports = { removeFromInventory };
================================================
FILE: chapter6/4_testing_and_browser_apis/server/jest.config.js
================================================
module.exports = {
testEnvironment: "node",
globalSetup: "./migrateDatabases.js",
setupFilesAfterEnv: [
"/truncateTables.js",
"/seedUser.js",
"/disconnectFromDb.js"
]
};
================================================
FILE: chapter6/4_testing_and_browser_apis/server/knexfile.js
================================================
module.exports = {
test: {
client: "sqlite3",
connection: { filename: "./test.sqlite" },
useNullAsDefault: true
},
development: {
client: "sqlite3",
connection: { filename: "./dev.sqlite" },
useNullAsDefault: true
}
};
================================================
FILE: chapter6/4_testing_and_browser_apis/server/logger.js
================================================
const fs = require("fs");
const logger = {
log: msg => fs.appendFileSync("/tmp/logs.out", msg + "\n")
};
module.exports = logger;
================================================
FILE: chapter6/4_testing_and_browser_apis/server/migrateDatabases.js
================================================
const environmentName = process.env.NODE_ENV || "test";
const environmentConfig = require("./knexfile")[environmentName];
const db = require("knex")(environmentConfig);
module.exports = async () => {
// Migrate the database to the latest state
await db.migrate.latest();
// Close the connection to the database so that tests won't hang
await db.destroy();
};
================================================
FILE: chapter6/4_testing_and_browser_apis/server/migrations/20200325082401_initial_schema.js
================================================
exports.up = async knex => {
await knex.schema.createTable("users", table => {
table.increments("id");
table.string("username");
table.unique("username");
table.string("email");
table.string("passwordHash");
});
await knex.schema.createTable("carts_items", table => {
table.integer("userId").references("users.id");
table.string("itemName");
table.unique("itemName");
table.integer("quantity");
});
await knex.schema.createTable("inventory", table => {
table.increments("id");
table.string("itemName");
table.unique("itemName");
table.integer("quantity");
});
};
exports.down = async knex => {
await knex.schema.dropTable("users");
await knex.schema.dropTable("carts_items");
await knex.schema.dropTable("inventory");
};
================================================
FILE: chapter6/4_testing_and_browser_apis/server/migrations/20200331210311_updatedAt_field.js
================================================
exports.up = knex => {
return knex.schema.alterTable("carts_items", table => {
table.timestamp("updatedAt");
});
};
exports.down = knex => {
return knex.schema.alterTable("carts_items", table => {
table.dropColumn("updatedAt");
});
};
================================================
FILE: chapter6/4_testing_and_browser_apis/server/package.json
================================================
{
"name": "4_integrations_with_other_apis",
"version": "1.0.0",
"scripts": {
"test": "jest --runInBand",
"start": "cross-env NODE_ENV=development node server.js",
"migrate:dev": "knex migrate:latest --env development",
"seed:dev": "knex seed:run"
},
"devDependencies": {
"@sinonjs/fake-timers": "github:sinonjs/fake-timers",
"jest": "^24.9.0",
"supertest": "^4.0.2"
},
"dependencies": {
"@koa/cors": "^3.0.0",
"cross-env": "^7.0.2",
"isomorphic-fetch": "^2.2.1",
"knex": "^0.20.13",
"koa": "^2.11.0",
"koa-body-parser": "^1.1.2",
"koa-router": "^7.4.0",
"nock": "^12.0.3",
"sqlite3": "^4.1.1"
},
"main": "alertController.spec.js",
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}
================================================
FILE: chapter6/4_testing_and_browser_apis/server/seedUser.js
================================================
const { createUser } = require("./userTestUtils");
beforeEach(createUser);
================================================
FILE: chapter6/4_testing_and_browser_apis/server/seeds/initial_inventory.js
================================================
exports.seed = async knex => {
await knex("inventory").del();
return knex("inventory").insert([
{ itemName: "cheesecake", quantity: 8 },
{ itemName: "apple pie", quantity: 2 },
{ itemName: "carrot cake", quantity: 5 }
]);
};
================================================
FILE: chapter6/4_testing_and_browser_apis/server/server.js
================================================
const fetch = require("isomorphic-fetch");
const Koa = require("koa");
const cors = require("@koa/cors");
const Router = require("koa-router");
const bodyParser = require("koa-body-parser");
const { db } = require("./dbConnection");
const { addItemToCart } = require("./cartController");
const {
hashPassword,
authenticationMiddleware
} = require("./authenticationController");
const PORT = process.env.NODE_ENV === "test" ? 5000 : 3000;
const app = new Koa();
const router = new Router();
app.use(cors());
app.use(bodyParser());
app.use(async (ctx, next) => {
if (ctx.url.startsWith("/carts")) {
return await authenticationMiddleware(ctx, next);
}
await next();
});
router.put("/users/:username", async ctx => {
const { username } = ctx.params;
const { email, password } = ctx.request.body;
const userAlreadyExists = await db
.select()
.from("users")
.where({ username })
.first();
if (userAlreadyExists) {
ctx.body = { message: `${username} already exists` };
ctx.status = 409;
return;
}
await db("users").insert({
username,
email,
passwordHash: hashPassword(password)
});
return (ctx.body = { message: `${username} created successfully` });
});
router.post("/carts/:username/items", async ctx => {
const { username } = ctx.params;
const { item, quantity } = ctx.request.body;
for (let i = 0; i < quantity; i++) {
try {
const newItems = await addItemToCart(username, item);
ctx.body = newItems;
} catch (e) {
ctx.body = { message: e.message };
ctx.status = e.code;
return;
}
}
});
router.delete("/carts/:username/items/:item", async ctx => {
const { username, item } = ctx.params;
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) {
ctx.body = { message: "user not found" };
ctx.status = 404;
return;
}
const itemEntry = await db
.select()
.from("carts_items")
.where({ userId: user.id, itemName: item })
.first();
if (!itemEntry || itemEntry.quantity === 0) {
ctx.body = { message: `${item} is not in the cart` };
ctx.status = 400;
return;
}
await db("carts_items")
.decrement("quantity")
.where({ userId: user.id, itemName: item });
const inventoryEntry = await db
.select()
.from("inventory")
.where({ itemName: item })
.first();
if (inventoryEntry) {
await db("inventory")
.increment("quantity")
.where({ userId: itemEntry.userId, itemName: item });
} else {
await db("inventory").insert({ itemName: item, quantity: 1 });
}
ctx.body = await db
.select("itemName", "quantity")
.from("carts_items")
.where({ userId: user.id });
});
router.post("/inventory/:itemName", async ctx => {
const { itemName } = ctx.params;
const { quantity } = ctx.request.body;
const current = await db
.select("itemName", "quantity")
.from("inventory")
.where({ itemName })
.first();
const itemExists = current && current.quantity > 0;
const newRecord = {
itemName,
quantity: (itemExists ? current.quantity : 0) + quantity
};
if (current) {
await db("inventory")
.increment("quantity", quantity)
.where({ itemName });
} else {
await db("inventory").insert(newRecord);
}
ctx.body = newRecord;
});
router.get("/inventory", async ctx => {
ctx.body = await db
.select("itemName", "quantity")
.from("inventory")
.where("quantity", ">", 0)
.orderBy("quantity", "desc");
});
router.get("/inventory/:itemName", async ctx => {
const { itemName } = ctx.params;
const response = await fetch(`http://recipepuppy.com/api?i=${itemName}`);
const { title, href, results: recipes } = await response.json();
const inventoryItem = await db
.select()
.from("inventory")
.where({ itemName })
.first();
ctx.body = {
...inventoryItem,
info: `Data obtained from ${title} - ${href}`,
recipes
};
});
app.use(router.routes());
module.exports = { app: app.listen(PORT) };
================================================
FILE: chapter6/4_testing_and_browser_apis/server/server.test.js
================================================
const { user: globalUser } = require("./userTestUtils");
const { db } = require("./dbConnection");
const request = require("supertest");
const { app } = require("./server.js");
const { hashPassword } = require("./authenticationController.js");
const nock = require("nock");
afterAll(() => app.close());
describe("add items to a cart", () => {
test("adding available items", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 3 });
const response = await request(app)
.post(`/carts/${globalUser.username}/items`)
.set("authorization", globalUser.authHeader)
.send({ item: "cheesecake", quantity: 3 })
.expect(200)
.expect("Content-Type", /json/);
const newItems = [{ itemName: "cheesecake", quantity: 3 }];
expect(response.body).toEqual(newItems);
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(0);
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual(newItems);
});
test("adding unavailable items", async () => {
const response = await request(app)
.post(`/carts/${globalUser.username}/items`)
.set("authorization", globalUser.authHeader)
.send({ item: "cheesecake", quantity: 1 })
.expect(400)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "cheesecake is unavailable"
});
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([]);
});
});
describe("removing items from a cart", () => {
test("removing existing items", async () => {
await db("carts_items").insert({
userId: globalUser.id,
itemName: "cheesecake",
quantity: 1
});
const response = await request(app)
.del(`/carts/${globalUser.username}/items/cheesecake`)
.set("authorization", globalUser.authHeader)
.expect(200)
.expect("Content-Type", /json/);
const expectedFinalContent = [{ itemName: "cheesecake", quantity: 0 }];
expect(response.body).toEqual(expectedFinalContent);
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual(expectedFinalContent);
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(1);
});
test("removing non-existing items", async () => {
await db("inventory").insert({
itemName: "cheesecake",
quantity: 0
});
const response = await request(app)
.del(`/carts/${globalUser.username}/items/cheesecake`)
.set("authorization", globalUser.authHeader)
.expect(400)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "cheesecake is not in the cart"
});
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(0);
});
});
describe("create accounts", () => {
test("creating a new account", async () => {
const response = await request(app)
.put("/users/another_user")
.send({ email: "another_user@example.org", password: "a_password" })
.expect(200)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "another_user created successfully"
});
const savedUser = await db
.select("email", "passwordHash")
.from("users")
.where({ username: "another_user" })
.first();
expect(savedUser).toEqual({
email: "another_user@example.org",
passwordHash: hashPassword("a_password")
});
});
test("creating a duplicate account", async () => {
const response = await request(app)
.put(`/users/${globalUser.username}`)
.send({ email: globalUser.email, password: "a_password" })
.expect(409)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: `${globalUser.username} already exists`
});
});
});
describe("list inventory items", () => {
const eggs = { itemName: "eggs", quantity: 3 };
const applePie = { itemName: "apple pie", quantity: 1 };
const carrotCake = { itemName: "carrot cake", quantity: 0 };
beforeEach(async () => {
await db("inventory").insert([eggs, applePie, carrotCake]);
});
test("fetching all available items", async () => {
const { body } = await request(app)
.get("/inventory")
.expect(200)
.expect("Content-Type", /json/);
expect(body).toEqual([eggs, applePie]);
});
});
describe("add inventory items", () => {
test("adding a new item", async () => {
const { body } = await request(app)
.post("/inventory/eggs")
.send({ quantity: 3 })
.expect(200)
.expect("Content-Type", /json/);
expect(body).toEqual({ itemName: "eggs", quantity: 3 });
expect(
await db
.select("itemName", "quantity")
.from("inventory")
.where("itemName", "eggs")
.first()
).toEqual({ itemName: "eggs", quantity: 3 });
});
test("adding an existing item", async () => {
const eggs = { itemName: "eggs", quantity: 2 };
await db("inventory").insert(eggs);
const { body } = await request(app)
.post("/inventory/eggs")
.send({ quantity: 3 })
.expect(200)
.expect("Content-Type", /json/);
expect(body).toEqual({ itemName: "eggs", quantity: 5 });
expect(
await db
.select("itemName", "quantity")
.from("inventory")
.where("itemName", "eggs")
.first()
).toEqual({ itemName: "eggs", quantity: 5 });
});
});
describe("fetch inventory items", () => {
const eggs = { itemName: "eggs", quantity: 3 };
const applePie = { itemName: "apple pie", quantity: 1 };
beforeEach(async () => {
await db("inventory").insert([eggs, applePie]);
const { id: eggsId } = await db
.select()
.from("inventory")
.where({ itemName: "eggs" })
.first();
eggs.id = eggsId;
});
test("fetching an item from the inventory", async () => {
const eggsResponse = {
title: "FakeAPI",
href: "example.org",
results: [{ name: "Omelette du Fromage" }]
};
nock("http://recipepuppy.com")
.get("/api")
.query({ i: "eggs" })
.reply(200, eggsResponse);
const response = await request(app)
.get(`/inventory/eggs`)
.expect(200)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
...eggs,
info: `Data obtained from ${eggsResponse.title} - ${eggsResponse.href}`,
recipes: eggsResponse.results
});
});
});
================================================
FILE: chapter6/4_testing_and_browser_apis/server/truncateTables.js
================================================
const { db } = require("./dbConnection");
const tablesToTruncate = ["users", "inventory", "carts_items"];
beforeEach(() => {
return Promise.all(tablesToTruncate.map(t => db(t).truncate()));
});
================================================
FILE: chapter6/4_testing_and_browser_apis/server/userTestUtils.js
================================================
const { db } = require("./dbConnection");
const { hashPassword } = require("./authenticationController");
const username = "test_user";
const password = "a_password";
const passwordHash = hashPassword(password);
const email = "test_user@example.org";
const validAuth = Buffer.from(`${username}:${password}`).toString("base64");
const authHeader = `Basic ${validAuth}`;
const user = {
username,
password,
email,
authHeader
};
const createUser = async () => {
await db("users").insert({ username, email, passwordHash });
const { id } = await db
.select()
.from("users")
.where({ username })
.first();
user.id = id;
};
module.exports = { user, createUser };
================================================
FILE: chapter6/5_web_sockets_and_http_requests/1_http_requests/domController.js
================================================
const { addItem, data } = require("./inventoryController");
const updateItemList = inventory => {
if (inventory === null) return;
localStorage.setItem("inventory", JSON.stringify(inventory));
const inventoryList = window.document.getElementById("item-list");
// Clears the list
inventoryList.innerHTML = "";
Object.entries(inventory).forEach(([itemName, quantity]) => {
const listItem = window.document.createElement("li");
listItem.innerHTML = `${itemName} - Quantity: ${quantity}`;
if (quantity < 5) {
listItem.className = "almost-soldout";
}
inventoryList.appendChild(listItem);
});
const inventoryContents = JSON.stringify(inventory);
const p = window.document.createElement("p");
p.innerHTML = `The inventory has been updated - ${inventoryContents}`;
window.document.body.appendChild(p);
};
const handleAddItem = event => {
// Prevent the page from reloading as it would by default
event.preventDefault();
const { name, quantity } = event.target.elements;
addItem(name.value, parseInt(quantity.value, 10));
history.pushState({ inventory: { ...data.inventory } }, document.title);
updateItemList(data.inventory);
};
const validItems = ["cheesecake", "apple pie", "carrot cake"];
const checkFormValues = () => {
const itemName = document.querySelector(`input[name="name"]`).value;
const quantity = document.querySelector(`input[name="quantity"]`).value;
const itemNameIsEmpty = itemName === "";
const itemNameIsInvalid = !validItems.includes(itemName);
const quantityIsEmpty = quantity === "";
const errorMsg = window.document.getElementById("error-msg");
if (itemNameIsEmpty) {
errorMsg.innerHTML = "";
} else if (itemNameIsInvalid) {
errorMsg.innerHTML = `${itemName} is not a valid item.`;
} else {
errorMsg.innerHTML = `${itemName} is valid!`;
}
const submitButton = document.querySelector(`button[type="submit"]`);
if (itemNameIsEmpty || itemNameIsInvalid || quantityIsEmpty) {
submitButton.disabled = true;
} else {
submitButton.disabled = false;
}
};
const handleUndo = () => {
if (history.state === null) return;
history.back();
};
const handlePopstate = () => {
data.inventory = history.state ? history.state.inventory : {};
updateItemList(data.inventory);
};
module.exports = {
updateItemList,
handleAddItem,
checkFormValues,
handleUndo,
handlePopstate
};
================================================
FILE: chapter6/5_web_sockets_and_http_requests/1_http_requests/domController.test.js
================================================
const nock = require("nock");
const fs = require("fs");
const initialHtml = fs.readFileSync("./index.html");
const { getByText, screen } = require("@testing-library/dom");
const {
updateItemList,
handleAddItem,
checkFormValues,
handleUndo,
handlePopstate
} = require("./domController");
const { clearHistoryHook, detachPopstateHandlers } = require("./testUtils");
const { API_ADDR, data } = require("./inventoryController");
beforeEach(() => {
document.body.innerHTML = initialHtml;
});
describe("updateItemList", () => {
beforeEach(() => localStorage.clear());
test("updates the DOM with the inventory items", () => {
const inventory = {
cheesecake: 5,
"apple pie": 2,
"carrot cake": 6
};
updateItemList(inventory);
const itemList = document.getElementById("item-list");
expect(itemList.childNodes).toHaveLength(3);
expect(getByText(itemList, "cheesecake - Quantity: 5")).toBeInTheDocument();
expect(getByText(itemList, "apple pie - Quantity: 2")).toBeInTheDocument();
expect(
getByText(itemList, "carrot cake - Quantity: 6")
).toBeInTheDocument();
});
test("highlighting in red elements whose quantity is below five", () => {
const inventory = { cheesecake: 5, "apple pie": 2, "carrot cake": 6 };
updateItemList(inventory);
expect(screen.getByText("apple pie - Quantity: 2")).toHaveStyle({
color: "red"
});
});
test("adding a paragraph indicating what was the update", () => {
const inventory = { cheesecake: 5, "apple pie": 2 };
updateItemList(inventory);
expect(
screen.getByText(
`The inventory has been updated - ${JSON.stringify(inventory)}`
)
).toBeTruthy();
});
test("updates the localStorage with the inventory", () => {
const inventory = { cheesecake: 5, "apple pie": 2 };
updateItemList(inventory);
expect(localStorage.getItem("inventory")).toEqual(
JSON.stringify(inventory)
);
});
test("does not update the inventory when passing null", () => {
localStorage.setItem("inventory", JSON.stringify({ cheesecake: 5 }));
updateItemList(null);
expect(localStorage.getItem("inventory")).toEqual(
JSON.stringify({ cheesecake: 5 })
);
});
});
describe("handleAddItem", () => {
beforeEach(() => (data.inventory = {}));
test("adding items to the page", () => {
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 6 }))
.reply(200);
const event = {
preventDefault: jest.fn(),
target: {
elements: {
name: { value: "cheesecake" },
quantity: { value: "6" }
}
}
};
handleAddItem(event);
// Checking if the form's default reload is prevent
expect(event.preventDefault.mock.calls).toHaveLength(1);
const itemList = document.getElementById("item-list");
expect(getByText(itemList, "cheesecake - Quantity: 6")).toBeInTheDocument();
if (!nock.isDone())
throw new Error("POST /inventory/cheesecake was not reached");
});
test("updating the application's history", () => {
nock(API_ADDR)
.post(/inventory\/.*$/)
.reply(200);
const event = {
preventDefault: jest.fn(),
target: {
elements: {
name: { value: "cheesecake" },
quantity: { value: "6" }
}
}
};
handleAddItem(event);
expect(history.state).toEqual({ inventory: { cheesecake: 6 } });
});
});
describe("checkFormValues", () => {
test("entering valid item values", () => {
document.querySelector(`input[name="name"]`).value = "cheesecake";
document.querySelector(`input[name="quantity"]`).value = "1";
checkFormValues();
expect(screen.getByText("Add to inventory")).toBeEnabled();
});
test("entering invalid item names", () => {
document.querySelector(`input[name="name"]`).value = "invalid";
document.querySelector(`input[name="quantity"]`).value = "1";
checkFormValues();
expect(screen.getByText("Add to inventory")).toBeDisabled();
document.querySelector(`input[name="name"]`).value = "cheesecake";
document.querySelector(`input[name="quantity"]`).value = "";
checkFormValues();
expect(screen.getByText("Add to inventory")).toBeDisabled();
});
});
describe("tests with history", () => {
beforeEach(() => jest.spyOn(window, "addEventListener"));
afterEach(detachPopstateHandlers);
beforeEach(clearHistoryHook);
describe("handleUndo", () => {
test("going back from a non-initial state", done => {
window.addEventListener("popstate", () => {
expect(history.state).toEqual(null);
done();
});
history.pushState({ inventory: { cheesecake: 5 } }, "title");
handleUndo();
});
test("going back from an initial state", () => {
jest.spyOn(history, "back");
handleUndo();
// This assertion doesn't care about whether
// a call to `history.back` would have finished,
// it only checks whether it's been called
expect(history.back.mock.calls).toHaveLength(0);
});
});
describe("handlePopstate", () => {
test("updating the item list with the current state", () => {
history.pushState(
{ inventory: { cheesecake: 5, "carrot cake": 2 } },
"title"
);
handlePopstate();
const itemList = document.getElementById("item-list");
expect(itemList.childNodes).toHaveLength(2);
expect(
getByText(itemList, "cheesecake - Quantity: 5")
).toBeInTheDocument();
expect(
getByText(itemList, "carrot cake - Quantity: 2")
).toBeInTheDocument();
});
});
});
================================================
FILE: chapter6/5_web_sockets_and_http_requests/1_http_requests/index.html
================================================
Inventory Manager
Inventory Contents
Undo
================================================
FILE: chapter6/5_web_sockets_and_http_requests/1_http_requests/inventoryController.js
================================================
const data = { inventory: {} };
const API_ADDR = "http://localhost:3000";
const addItem = (itemName, quantity) => {
const currentQuantity = data.inventory[itemName] || 0;
data.inventory[itemName] = currentQuantity + quantity;
fetch(`${API_ADDR}/inventory/${itemName}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ quantity })
});
return data.inventory;
};
module.exports = { API_ADDR, data, addItem };
================================================
FILE: chapter6/5_web_sockets_and_http_requests/1_http_requests/inventoryController.test.js
================================================
const nock = require("nock");
const { API_ADDR, addItem, data } = require("./inventoryController");
describe("addItem", () => {
test("adding new items to the inventory", () => {
// Respond to all post requests
// to POST /inventory/:itemName
nock(API_ADDR)
.post(/inventory\/.*$/)
.reply(200);
addItem("cheesecake", 5);
expect(data.inventory.cheesecake).toBe(5);
});
test("sending requests when adding new items", () => {
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 5 }))
.reply(200);
addItem("cheesecake", 5);
if (!nock.isDone())
throw new Error("POST /inventory/cheesecake was not reached");
});
});
================================================
FILE: chapter6/5_web_sockets_and_http_requests/1_http_requests/jest.config.js
================================================
module.exports = {
setupFilesAfterEnv: [
"/setupGlobalFetch.js",
"/setupJestDom.js"
]
};
================================================
FILE: chapter6/5_web_sockets_and_http_requests/1_http_requests/main.js
================================================
const {
handleAddItem,
checkFormValues,
handleUndo,
handlePopstate,
updateItemList
} = require("./domController");
const { API_ADDR, data } = require("./inventoryController");
const form = document.getElementById("add-item-form");
form.addEventListener("submit", handleAddItem);
form.addEventListener("input", checkFormValues);
const undoButton = document.getElementById("undo-button");
undoButton.addEventListener("click", handleUndo);
window.addEventListener("popstate", handlePopstate);
// Run `checkFormValues` once to see if the initial state is valid
checkFormValues();
const loadInitialData = async () => {
try {
const inventoryResponse = await fetch(`${API_ADDR}/inventory`);
data.inventory = await inventoryResponse.json();
return updateItemList(data.inventory);
} catch (e) {
// Restore the inventory if the request fails
const storedInventory = JSON.parse(localStorage.getItem("inventory"));
if (storedInventory) {
data.inventory = storedInventory;
updateItemList(data.inventory);
}
}
};
module.exports = loadInitialData();
================================================
FILE: chapter6/5_web_sockets_and_http_requests/1_http_requests/main.test.js
================================================
const nock = require("nock");
const fs = require("fs");
const initialHtml = fs.readFileSync("./index.html");
const { screen, getByText, fireEvent } = require("@testing-library/dom");
const { API_ADDR } = require("./inventoryController");
const { clearHistoryHook, detachPopstateHandlers } = require("./testUtils.js");
beforeEach(clearHistoryHook);
beforeEach(() => localStorage.clear());
beforeEach(async () => {
document.body.innerHTML = initialHtml;
// You must execute main.js again so that it can attach the
// event listener to the form every time the body changes.
// Here you must use `jest.resetModules` because otherwise
// Jest will have cached `main.js` and it will _not_ run again.
jest.resetModules();
nock(API_ADDR)
.get("/inventory")
.replyWithError({ code: 500 });
await require("./main");
// You can only spy on `window.addEventListener` after `main.js`
// has been executed. Otherwise `detachPopstateHandlers` will
// also detach the handlers that `main.js` attached to the page.
jest.spyOn(window, "addEventListener");
});
afterEach(detachPopstateHandlers);
afterEach(() => {
if (!nock.isDone()) {
nock.cleanAll();
throw new Error("Not all mocked endpoints received requests.");
}
});
test("persists items between sessions", async () => {
nock(API_ADDR)
.post(/inventory\/.*$/)
.reply(200);
nock(API_ADDR)
.get("/inventory")
.replyWithError({ code: 500 });
const itemField = screen.getByPlaceholderText("Item name");
const submitBtn = screen.getByText("Add to inventory");
fireEvent.input(itemField, {
target: { value: "cheesecake" },
bubbles: true
});
const quantityField = screen.getByPlaceholderText("Quantity");
fireEvent.input(quantityField, { target: { value: "6" }, bubbles: true });
fireEvent.click(submitBtn);
const itemListBefore = document.getElementById("item-list");
expect(itemListBefore.childNodes).toHaveLength(1);
expect(
getByText(itemListBefore, "cheesecake - Quantity: 6")
).toBeInTheDocument();
// This is equivalent to reloading the page
document.body.innerHTML = initialHtml;
jest.resetModules();
await require("./main");
const itemListAfter = document.getElementById("item-list");
expect(itemListAfter.childNodes).toHaveLength(1);
expect(
getByText(itemListAfter, "cheesecake - Quantity: 6")
).toBeInTheDocument();
});
describe("adding items", () => {
test("updating the item list", () => {
nock(API_ADDR)
.post(/inventory\/.*$/)
.reply(200);
const itemField = screen.getByPlaceholderText("Item name");
const submitBtn = screen.getByText("Add to inventory");
fireEvent.input(itemField, {
target: { value: "cheesecake" },
bubbles: true
});
const quantityField = screen.getByPlaceholderText("Quantity");
fireEvent.input(quantityField, { target: { value: "6" }, bubbles: true });
fireEvent.click(submitBtn);
const itemList = document.getElementById("item-list");
expect(getByText(itemList, "cheesecake - Quantity: 6")).toBeInTheDocument();
});
test("sending a request to update the item list", () => {
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 6 }))
.reply(200);
const submitBtn = screen.getByText("Add to inventory");
const itemField = screen.getByPlaceholderText("Item name");
fireEvent.input(itemField, {
target: { value: "cheesecake" },
bubbles: true
});
const quantityField = screen.getByPlaceholderText("Quantity");
fireEvent.input(quantityField, { target: { value: "6" }, bubbles: true });
fireEvent.click(submitBtn);
if (!nock.isDone())
throw new Error("POST /inventory/cheesecake was not reached");
});
test("undo to one item", done => {
// You must specify the encoded URL here because
// nock struggles with encoded urls
nock(API_ADDR)
.post("/inventory/carrot%20cake")
.reply(200);
nock(API_ADDR)
.post("/inventory/cheesecake")
.reply(200);
const itemField = screen.getByPlaceholderText("Item name");
const quantityField = screen.getByPlaceholderText("Quantity");
const submitBtn = screen.getByText("Add to inventory");
fireEvent.input(itemField, {
target: { value: "cheesecake" },
bubbles: true
});
fireEvent.input(quantityField, { target: { value: "6" }, bubbles: true });
fireEvent.click(submitBtn);
fireEvent.input(itemField, {
target: { value: "carrot cake" },
bubbles: true
});
fireEvent.input(quantityField, { target: { value: "5" }, bubbles: true });
fireEvent.click(submitBtn);
window.addEventListener("popstate", () => {
const itemList = document.getElementById("item-list");
expect(itemList.children).toHaveLength(1);
expect(
getByText(itemList, "cheesecake - Quantity: 6")
).toBeInTheDocument();
done();
});
fireEvent.click(screen.getByText("Undo"));
});
test("undo to empty list", done => {
nock(API_ADDR)
.post(/inventory\/.*$/)
.reply(200);
const itemField = screen.getByPlaceholderText("Item name");
const submitBtn = screen.getByText("Add to inventory");
fireEvent.input(itemField, {
target: { value: "cheesecake" },
bubbles: true
});
const quantityField = screen.getByPlaceholderText("Quantity");
fireEvent.input(quantityField, { target: { value: "6" }, bubbles: true });
fireEvent.click(submitBtn);
expect(history.state).toEqual({ inventory: { cheesecake: 6 } });
window.addEventListener("popstate", () => {
const itemList = document.getElementById("item-list");
expect(itemList).toBeEmpty();
done();
});
fireEvent.click(screen.getByText("Undo"));
});
});
describe("item name validation", () => {
test("entering valid item names ", () => {
const itemField = screen.getByPlaceholderText("Item name");
fireEvent.input(itemField, {
target: { value: "cheesecake" },
bubbles: true
});
expect(screen.getByText("cheesecake is valid!")).toBeInTheDocument();
});
test("entering invalid item names ", () => {
const itemField = screen.getByPlaceholderText("Item name");
fireEvent.input(itemField, { target: { value: "book" }, bubbles: true });
expect(screen.getByText("book is not a valid item.")).toBeInTheDocument();
});
});
================================================
FILE: chapter6/5_web_sockets_and_http_requests/1_http_requests/package.json
================================================
{
"name": "1_http_requests",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "http-server ./",
"test": "jest",
"build": "browserify main.js -o bundle.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@testing-library/dom": "^7.2.2",
"@testing-library/jest-dom": "^5.5.0",
"browserify": "^16.5.1",
"http-server": "^0.12.1",
"isomorphic-fetch": "^2.2.1",
"jest": "^24.9.0",
"nock": "^12.0.3"
}
}
================================================
FILE: chapter6/5_web_sockets_and_http_requests/1_http_requests/setupGlobalFetch.js
================================================
const fetch = require("isomorphic-fetch");
global.window.fetch = fetch;
================================================
FILE: chapter6/5_web_sockets_and_http_requests/1_http_requests/setupJestDom.js
================================================
const jestDom = require("@testing-library/jest-dom");
expect.extend(jestDom);
================================================
FILE: chapter6/5_web_sockets_and_http_requests/1_http_requests/testUtils.js
================================================
const clearHistoryHook = done => {
const clearHistory = () => {
if (history.state === null) {
window.removeEventListener("popstate", clearHistory);
return done();
}
history.back();
};
window.addEventListener("popstate", clearHistory);
clearHistory();
};
const detachPopstateHandlers = () => {
const popstateListeners = window.addEventListener.mock.calls.filter(
([eventName]) => {
return eventName === "popstate";
}
);
popstateListeners.forEach(([eventName, handlerFn]) => {
window.removeEventListener(eventName, handlerFn);
});
jest.restoreAllMocks();
};
module.exports = { clearHistoryHook, detachPopstateHandlers };
================================================
FILE: chapter6/5_web_sockets_and_http_requests/2_web_sockets/domController.js
================================================
const { addItem, data } = require("./inventoryController");
const updateItemList = inventory => {
if (inventory === null) return;
localStorage.setItem("inventory", JSON.stringify(inventory));
const inventoryList = window.document.getElementById("item-list");
// Clears the list
inventoryList.innerHTML = "";
Object.entries(inventory).forEach(([itemName, quantity]) => {
const listItem = window.document.createElement("li");
listItem.innerHTML = `${itemName} - Quantity: ${quantity}`;
if (quantity < 5) {
listItem.className = "almost-soldout";
}
inventoryList.appendChild(listItem);
});
const inventoryContents = JSON.stringify(inventory);
const p = window.document.createElement("p");
p.innerHTML = `The inventory has been updated - ${inventoryContents}`;
window.document.body.appendChild(p);
};
const handleAddItem = event => {
// Prevent the page from reloading as it would by default
event.preventDefault();
const { name, quantity } = event.target.elements;
addItem(name.value, parseInt(quantity.value, 10));
history.pushState({ inventory: { ...data.inventory } }, document.title);
updateItemList(data.inventory);
};
const validItems = ["cheesecake", "apple pie", "carrot cake"];
const checkFormValues = () => {
const itemName = document.querySelector(`input[name="name"]`).value;
const quantity = document.querySelector(`input[name="quantity"]`).value;
const itemNameIsEmpty = itemName === "";
const itemNameIsInvalid = !validItems.includes(itemName);
const quantityIsEmpty = quantity === "";
const errorMsg = window.document.getElementById("error-msg");
if (itemNameIsEmpty) {
errorMsg.innerHTML = "";
} else if (itemNameIsInvalid) {
errorMsg.innerHTML = `${itemName} is not a valid item.`;
} else {
errorMsg.innerHTML = `${itemName} is valid!`;
}
const submitButton = document.querySelector(`button[type="submit"]`);
if (itemNameIsEmpty || itemNameIsInvalid || quantityIsEmpty) {
submitButton.disabled = true;
} else {
submitButton.disabled = false;
}
};
const handleUndo = () => {
if (history.state === null) return;
history.back();
};
const handlePopstate = () => {
data.inventory = history.state ? history.state.inventory : {};
updateItemList(data.inventory);
};
module.exports = {
updateItemList,
handleAddItem,
checkFormValues,
handleUndo,
handlePopstate
};
================================================
FILE: chapter6/5_web_sockets_and_http_requests/2_web_sockets/domController.test.js
================================================
const nock = require("nock");
const fs = require("fs");
const initialHtml = fs.readFileSync("./index.html");
const { getByText, screen } = require("@testing-library/dom");
const {
updateItemList,
handleAddItem,
checkFormValues,
handleUndo,
handlePopstate
} = require("./domController");
const { clearHistoryHook, detachPopstateHandlers } = require("./testUtils");
const { API_ADDR, data } = require("./inventoryController");
beforeEach(() => {
document.body.innerHTML = initialHtml;
});
describe("updateItemList", () => {
beforeEach(() => localStorage.clear());
test("updates the DOM with the inventory items", () => {
const inventory = {
cheesecake: 5,
"apple pie": 2,
"carrot cake": 6
};
updateItemList(inventory);
const itemList = document.getElementById("item-list");
expect(itemList.childNodes).toHaveLength(3);
expect(getByText(itemList, "cheesecake - Quantity: 5")).toBeInTheDocument();
expect(getByText(itemList, "apple pie - Quantity: 2")).toBeInTheDocument();
expect(
getByText(itemList, "carrot cake - Quantity: 6")
).toBeInTheDocument();
});
test("highlighting in red elements whose quantity is below five", () => {
const inventory = { cheesecake: 5, "apple pie": 2, "carrot cake": 6 };
updateItemList(inventory);
expect(screen.getByText("apple pie - Quantity: 2")).toHaveStyle({
color: "red"
});
});
test("adding a paragraph indicating what was the update", () => {
const inventory = { cheesecake: 5, "apple pie": 2 };
updateItemList(inventory);
expect(
screen.getByText(
`The inventory has been updated - ${JSON.stringify(inventory)}`
)
).toBeTruthy();
});
test("updates the localStorage with the inventory", () => {
const inventory = { cheesecake: 5, "apple pie": 2 };
updateItemList(inventory);
expect(localStorage.getItem("inventory")).toEqual(
JSON.stringify(inventory)
);
});
test("does not update the inventory when passing null", () => {
localStorage.setItem("inventory", JSON.stringify({ cheesecake: 5 }));
updateItemList(null);
expect(localStorage.getItem("inventory")).toEqual(
JSON.stringify({ cheesecake: 5 })
);
});
});
describe("handleAddItem", () => {
beforeEach(() => (data.inventory = {}));
test("adding items to the page", () => {
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 6 }))
.reply(200);
const event = {
preventDefault: jest.fn(),
target: {
elements: {
name: { value: "cheesecake" },
quantity: { value: "6" }
}
}
};
handleAddItem(event);
// Checking if the form's default reload is prevent
expect(event.preventDefault.mock.calls).toHaveLength(1);
const itemList = document.getElementById("item-list");
expect(getByText(itemList, "cheesecake - Quantity: 6")).toBeInTheDocument();
if (!nock.isDone())
throw new Error("POST /inventory/cheesecake was not reached");
});
test("updating the application's history", () => {
nock(API_ADDR)
.post(/inventory\/.*$/)
.reply(200);
const event = {
preventDefault: jest.fn(),
target: {
elements: {
name: { value: "cheesecake" },
quantity: { value: "6" }
}
}
};
handleAddItem(event);
expect(history.state).toEqual({ inventory: { cheesecake: 6 } });
});
});
describe("checkFormValues", () => {
test("entering valid item values", () => {
document.querySelector(`input[name="name"]`).value = "cheesecake";
document.querySelector(`input[name="quantity"]`).value = "1";
checkFormValues();
expect(screen.getByText("Add to inventory")).toBeEnabled();
});
test("entering invalid item names", () => {
document.querySelector(`input[name="name"]`).value = "invalid";
document.querySelector(`input[name="quantity"]`).value = "1";
checkFormValues();
expect(screen.getByText("Add to inventory")).toBeDisabled();
document.querySelector(`input[name="name"]`).value = "cheesecake";
document.querySelector(`input[name="quantity"]`).value = "";
checkFormValues();
expect(screen.getByText("Add to inventory")).toBeDisabled();
});
});
describe("tests with history", () => {
beforeEach(() => jest.spyOn(window, "addEventListener"));
afterEach(detachPopstateHandlers);
beforeEach(clearHistoryHook);
describe("handleUndo", () => {
test("going back from a non-initial state", done => {
window.addEventListener("popstate", () => {
expect(history.state).toEqual(null);
done();
});
history.pushState({ inventory: { cheesecake: 5 } }, "title");
handleUndo();
});
test("going back from an initial state", () => {
jest.spyOn(history, "back");
handleUndo();
// This assertion doesn't care about whether
// a call to `history.back` would have finished,
// it only checks whether it's been called
expect(history.back.mock.calls).toHaveLength(0);
});
});
describe("handlePopstate", () => {
test("updating the item list with the current state", () => {
history.pushState(
{ inventory: { cheesecake: 5, "carrot cake": 2 } },
"title"
);
handlePopstate();
const itemList = document.getElementById("item-list");
expect(itemList.childNodes).toHaveLength(2);
expect(
getByText(itemList, "cheesecake - Quantity: 5")
).toBeInTheDocument();
expect(
getByText(itemList, "carrot cake - Quantity: 2")
).toBeInTheDocument();
});
});
});
================================================
FILE: chapter6/5_web_sockets_and_http_requests/2_web_sockets/index.html
================================================
Inventory Manager
Inventory Contents
Undo
================================================
FILE: chapter6/5_web_sockets_and_http_requests/2_web_sockets/inventoryController.js
================================================
const data = { inventory: {} };
const API_ADDR = "http://localhost:3000";
const addItem = (itemName, quantity) => {
const { client } = require("./socket");
const currentQuantity = data.inventory[itemName] || 0;
data.inventory[itemName] = currentQuantity + quantity;
fetch(`${API_ADDR}/inventory/${itemName}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-socket-client-id": client.id
},
body: JSON.stringify({ quantity })
});
return data.inventory;
};
module.exports = { API_ADDR, data, addItem };
================================================
FILE: chapter6/5_web_sockets_and_http_requests/2_web_sockets/inventoryController.test.js
================================================
const nock = require("nock");
const { API_ADDR, addItem, data } = require("./inventoryController");
const { start, stop } = require("./testSocketServer");
const { client, connect } = require("./socket");
afterEach(() => {
if (!nock.isDone()) {
nock.cleanAll();
throw new Error("Not all mocked endpoints received requests.");
}
});
describe("addItem", () => {
test("adding new items to the inventory", () => {
// Respond to all post requests
// to POST /inventory/:itemName
nock(API_ADDR)
.post(/inventory\/.*$/)
.reply(200);
addItem("cheesecake", 5);
expect(data.inventory.cheesecake).toBe(5);
});
test("sending requests when adding new items", () => {
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 5 }))
.reply(200);
addItem("cheesecake", 5);
});
describe("live-updates", () => {
beforeAll(start);
beforeAll(async () => {
nock.cleanAll();
await connect();
});
afterAll(stop);
test("sending a x-socket-client-id header", () => {
const clientId = client.id;
nock(API_ADDR, { reqheaders: { "x-socket-client-id": clientId } })
.post(/inventory\/.*$/)
.reply(200);
addItem("cheesecake", 5);
});
});
});
================================================
FILE: chapter6/5_web_sockets_and_http_requests/2_web_sockets/jest.config.js
================================================
module.exports = {
setupFilesAfterEnv: [
"/setupGlobalFetch.js",
"/setupJestDom.js"
]
};
================================================
FILE: chapter6/5_web_sockets_and_http_requests/2_web_sockets/main.js
================================================
const { connect } = require("./socket");
const {
handleAddItem,
checkFormValues,
handleUndo,
handlePopstate,
updateItemList
} = require("./domController");
const { API_ADDR, data } = require("./inventoryController");
const form = document.getElementById("add-item-form");
form.addEventListener("submit", handleAddItem);
form.addEventListener("input", checkFormValues);
const undoButton = document.getElementById("undo-button");
undoButton.addEventListener("click", handleUndo);
window.addEventListener("popstate", handlePopstate);
// Run `checkFormValues` once to see if the initial state is valid
checkFormValues();
const loadInitialData = async () => {
try {
const inventoryResponse = await fetch(`${API_ADDR}/inventory`);
data.inventory = await inventoryResponse.json();
return updateItemList(data.inventory);
} catch (e) {
// Restore the inventory if the request fails
const storedInventory = JSON.parse(localStorage.getItem("inventory"));
if (storedInventory) {
data.inventory = storedInventory;
updateItemList(data.inventory);
}
}
};
connect();
module.exports = loadInitialData();
================================================
FILE: chapter6/5_web_sockets_and_http_requests/2_web_sockets/main.test.js
================================================
const nock = require("nock");
const fs = require("fs");
const initialHtml = fs.readFileSync("./index.html");
const { screen, getByText, fireEvent } = require("@testing-library/dom");
const { API_ADDR } = require("./inventoryController");
const { clearHistoryHook, detachPopstateHandlers } = require("./testUtils.js");
beforeEach(clearHistoryHook);
beforeEach(() => localStorage.clear());
beforeEach(async () => {
document.body.innerHTML = initialHtml;
// You must execute main.js again so that it can attach the
// event listener to the form every time the body changes.
// Here you must use `jest.resetModules` because otherwise
// Jest will have cached `main.js` and it will _not_ run again.
jest.resetModules();
nock(API_ADDR)
.get("/inventory")
.replyWithError({ code: 500 });
await require("./main");
// You can only spy on `window.addEventListener` after `main.js`
// has been executed. Otherwise `detachPopstateHandlers` will
// also detach the handlers that `main.js` attached to the page.
jest.spyOn(window, "addEventListener");
});
afterEach(detachPopstateHandlers);
afterEach(() => {
if (!nock.isDone()) {
nock.cleanAll();
throw new Error("Not all mocked endpoints received requests.");
}
});
test("persists items between sessions", async () => {
nock(API_ADDR)
.post(/inventory\/.*$/)
.reply(200);
nock(API_ADDR)
.get("/inventory")
.replyWithError({ code: 500 });
const submitBtn = screen.getByText("Add to inventory");
const itemField = screen.getByPlaceholderText("Item name");
fireEvent.input(itemField, {
target: { value: "cheesecake" },
bubbles: true
});
const quantityField = screen.getByPlaceholderText("Quantity");
fireEvent.input(quantityField, { target: { value: "6" }, bubbles: true });
fireEvent.click(submitBtn);
const itemListBefore = document.getElementById("item-list");
expect(itemListBefore.childNodes).toHaveLength(1);
expect(
getByText(itemListBefore, "cheesecake - Quantity: 6")
).toBeInTheDocument();
// This is equivalent to reloading the page
document.body.innerHTML = initialHtml;
jest.resetModules();
await require("./main");
const itemListAfter = document.getElementById("item-list");
expect(itemListAfter.childNodes).toHaveLength(1);
expect(
getByText(itemListAfter, "cheesecake - Quantity: 6")
).toBeInTheDocument();
});
describe("adding items", () => {
test("updating the item list", () => {
nock(API_ADDR)
.post(/inventory\/.*$/)
.reply(200);
const submitBtn = screen.getByText("Add to inventory");
const itemField = screen.getByPlaceholderText("Item name");
fireEvent.input(itemField, {
target: { value: "cheesecake" },
bubbles: true
});
const quantityField = screen.getByPlaceholderText("Quantity");
fireEvent.input(quantityField, { target: { value: "6" }, bubbles: true });
fireEvent.click(submitBtn);
const itemList = document.getElementById("item-list");
expect(getByText(itemList, "cheesecake - Quantity: 6")).toBeInTheDocument();
});
test("sending a request to update the item list", () => {
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 6 }))
.reply(200);
const submitBtn = screen.getByText("Add to inventory");
const itemField = screen.getByPlaceholderText("Item name");
fireEvent.input(itemField, {
target: { value: "cheesecake" },
bubbles: true
});
const quantityField = screen.getByPlaceholderText("Quantity");
fireEvent.input(quantityField, { target: { value: "6" }, bubbles: true });
fireEvent.click(submitBtn);
if (!nock.isDone())
throw new Error("POST /inventory/cheesecake was not reached");
});
test("undo to one item", done => {
// You must specify the encoded URL here because
// nock struggles with encoded urls
nock(API_ADDR)
.post("/inventory/carrot%20cake")
.reply(200);
nock(API_ADDR)
.post("/inventory/cheesecake")
.reply(200);
const itemField = screen.getByPlaceholderText("Item name");
const quantityField = screen.getByPlaceholderText("Quantity");
const submitBtn = screen.getByText("Add to inventory");
fireEvent.input(itemField, {
target: { value: "cheesecake" },
bubbles: true
});
fireEvent.input(quantityField, { target: { value: "6" }, bubbles: true });
fireEvent.click(submitBtn);
fireEvent.input(itemField, {
target: { value: "carrot cake" },
bubbles: true
});
fireEvent.input(quantityField, { target: { value: "5" }, bubbles: true });
fireEvent.click(submitBtn);
window.addEventListener("popstate", () => {
const itemList = document.getElementById("item-list");
expect(itemList.children).toHaveLength(1);
expect(
getByText(itemList, "cheesecake - Quantity: 6")
).toBeInTheDocument();
done();
});
fireEvent.click(screen.getByText("Undo"));
});
test("undo to empty list", done => {
nock(API_ADDR)
.post(/inventory\/.*$/)
.reply(200);
const submitBtn = screen.getByText("Add to inventory");
const itemField = screen.getByPlaceholderText("Item name");
fireEvent.input(itemField, {
target: { value: "cheesecake" },
bubbles: true
});
const quantityField = screen.getByPlaceholderText("Quantity");
fireEvent.input(quantityField, { target: { value: "6" }, bubbles: true });
fireEvent.click(submitBtn);
expect(history.state).toEqual({ inventory: { cheesecake: 6 } });
window.addEventListener("popstate", () => {
const itemList = document.getElementById("item-list");
expect(itemList).toBeEmpty();
done();
});
fireEvent.click(screen.getByText("Undo"));
});
});
describe("item name validation", () => {
test("entering valid item names ", () => {
const itemField = screen.getByPlaceholderText("Item name");
fireEvent.input(itemField, {
target: { value: "cheesecake" },
bubbles: true
});
expect(screen.getByText("cheesecake is valid!")).toBeInTheDocument();
});
test("entering invalid item names ", () => {
const itemField = screen.getByPlaceholderText("Item name");
fireEvent.input(itemField, { target: { value: "book" }, bubbles: true });
expect(screen.getByText("book is not a valid item.")).toBeInTheDocument();
});
});
================================================
FILE: chapter6/5_web_sockets_and_http_requests/2_web_sockets/package.json
================================================
{
"name": "1_http_requests",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "http-server ./",
"test": "jest",
"build": "browserify main.js -o bundle.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@testing-library/dom": "^7.2.2",
"@testing-library/jest-dom": "^5.5.0",
"browserify": "^16.5.1",
"http-server": "^0.12.1",
"isomorphic-fetch": "^2.2.1",
"jest": "^24.9.0",
"nock": "^12.0.3",
"socket.io": "^2.3.0"
},
"dependencies": {
"http-shutdown": "^1.2.2",
"socket.io-client": "^2.3.0"
}
}
================================================
FILE: chapter6/5_web_sockets_and_http_requests/2_web_sockets/setupGlobalFetch.js
================================================
const fetch = require("isomorphic-fetch");
global.window.fetch = fetch;
================================================
FILE: chapter6/5_web_sockets_and_http_requests/2_web_sockets/setupJestDom.js
================================================
const jestDom = require("@testing-library/jest-dom");
expect.extend(jestDom);
================================================
FILE: chapter6/5_web_sockets_and_http_requests/2_web_sockets/socket.js
================================================
const { API_ADDR, data } = require("./inventoryController");
const { updateItemList } = require("./domController");
const client = { id: null };
const io = require("socket.io-client");
const handleAddItemMsg = ({ itemName, quantity }) => {
const currentQuantity = data.inventory[itemName] || 0;
data.inventory[itemName] = currentQuantity + quantity;
return updateItemList(data.inventory);
};
const connect = () => {
return new Promise(resolve => {
const socket = io(API_ADDR);
socket.on("connect", () => {
client.id = socket.id;
resolve(socket);
});
socket.on("add_item", handleAddItemMsg);
});
};
module.exports = { client, connect, handleAddItemMsg };
================================================
FILE: chapter6/5_web_sockets_and_http_requests/2_web_sockets/socket.test.js
================================================
const nock = require("nock");
const fs = require("fs");
const initialHtml = fs.readFileSync("./index.html");
const { getByText } = require("@testing-library/dom");
const { data } = require("./inventoryController");
const { start, stop, sendMsg } = require("./testSocketServer");
const { handleAddItemMsg, connect } = require("./socket");
beforeEach(() => {
document.body.innerHTML = initialHtml;
});
beforeEach(() => {
data.inventory = {};
});
describe("handleAddItemMsg", () => {
test("updating the inventory and the item list", () => {
handleAddItemMsg({ itemName: "cheesecake", quantity: 6 });
expect(data.inventory).toEqual({ cheesecake: 6 });
const itemList = document.getElementById("item-list");
expect(itemList.childNodes).toHaveLength(1);
expect(getByText(itemList, "cheesecake - Quantity: 6")).toBeInTheDocument();
});
});
describe("handling real messages", () => {
beforeAll(start);
beforeAll(async () => {
nock.cleanAll();
await connect();
});
afterAll(stop);
test("handling add_item messages", async () => {
sendMsg("add_item", { itemName: "cheesecake", quantity: 6 });
await new Promise(resolve => setTimeout(resolve, 1000));
expect(data.inventory).toEqual({ cheesecake: 6 });
const itemList = document.getElementById("item-list");
expect(itemList.childNodes).toHaveLength(1);
expect(getByText(itemList, "cheesecake - Quantity: 6")).toBeInTheDocument();
});
});
================================================
FILE: chapter6/5_web_sockets_and_http_requests/2_web_sockets/testSocketServer.js
================================================
const server = require("http").createServer();
const io = require("socket.io")(server);
const sendMsg = (msgType, content) => {
io.sockets.emit(msgType, content);
};
const start = () =>
new Promise(resolve => {
server.listen(3000, resolve);
});
const stop = () =>
new Promise(resolve => {
server.close(resolve);
});
module.exports = { start, stop, sendMsg };
================================================
FILE: chapter6/5_web_sockets_and_http_requests/2_web_sockets/testUtils.js
================================================
const clearHistoryHook = done => {
const clearHistory = () => {
if (history.state === null) {
window.removeEventListener("popstate", clearHistory);
return done();
}
history.back();
};
window.addEventListener("popstate", clearHistory);
clearHistory();
};
const detachPopstateHandlers = () => {
const popstateListeners = window.addEventListener.mock.calls.filter(
([eventName]) => {
return eventName === "popstate";
}
);
popstateListeners.forEach(([eventName, handlerFn]) => {
window.removeEventListener(eventName, handlerFn);
});
jest.restoreAllMocks();
};
module.exports = { clearHistoryHook, detachPopstateHandlers };
================================================
FILE: chapter6/5_web_sockets_and_http_requests/server/README.md
================================================
# Chapter 5 Server
To 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.
In case you want to update the back-end from Chapter 4 yourself, here's the list of changes I've done:
- For the server to accept the requests coming from the client, you'll need to use [`@koa/cors`](https://github.com/koajs/cors)
- 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.
- 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.
- At `GET /inventory` I have added a route which lists all items in the inventory.
- 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
- I've used `koa-socket-2` to add support for `socket.io`
- The `POST /inventory/:itemName` will now push updates to all clients but the one which added an item.
================================================
FILE: chapter6/5_web_sockets_and_http_requests/server/authenticationController.js
================================================
const crypto = require("crypto");
const { db } = require("./dbConnection");
const hashPassword = password => {
const hash = crypto.createHash("sha256");
hash.update(password);
return hash.digest("hex");
};
const credentialsAreValid = async (username, password) => {
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) return false;
return hashPassword(password) === user.passwordHash;
};
const authenticationMiddleware = async (ctx, next) => {
try {
const authHeader = ctx.request.headers.authorization;
const credentials = Buffer.from(
authHeader.slice("basic".length + 1),
"base64"
).toString();
const [username, password] = credentials.split(":");
const validCredentialsSent = await credentialsAreValid(username, password);
if (!validCredentialsSent) throw new Error("invalid credentials");
} catch (e) {
ctx.status = 401;
ctx.body = { message: "please provide valid credentials" };
return;
}
await next();
};
module.exports = {
hashPassword,
credentialsAreValid,
authenticationMiddleware
};
================================================
FILE: chapter6/5_web_sockets_and_http_requests/server/authenticationController.test.js
================================================
const crypto = require("crypto");
const {
hashPassword,
credentialsAreValid,
authenticationMiddleware
} = require("./authenticationController");
const { user: globalUser } = require("./userTestUtils");
describe("hashPassword", () => {
test("hashing passwords", () => {
const plainTextPassword = "password_example";
const hash = crypto.createHash("sha256");
hash.update(plainTextPassword);
const expectedHash = hash.digest("hex");
expect(hashPassword(plainTextPassword)).toBe(expectedHash);
});
});
describe("credentialsAreValid", () => {
test("validating credentials", async () => {
expect(await credentialsAreValid(globalUser.username, "a_password")).toBe(
true
);
});
});
describe("authenticationMiddleware", () => {
test("returning an error if the credentials are not valid", async () => {
const fakeAuth = Buffer.from("invalid:credentials").toString("base64");
const ctx = {
request: {
headers: { authorization: `Basic ${fakeAuth}` }
}
};
const next = jest.fn();
await authenticationMiddleware(ctx, next);
expect(next.mock.calls).toHaveLength(0);
expect(ctx).toEqual({
...ctx,
status: 401,
body: { message: "please provide valid credentials" }
});
});
test("authenticating properly", async () => {
const ctx = {
request: {
headers: { authorization: globalUser.authHeader }
}
};
const next = jest.fn();
await authenticationMiddleware(ctx, next);
expect(next.mock.calls).toHaveLength(1);
});
});
================================================
FILE: chapter6/5_web_sockets_and_http_requests/server/cartController.js
================================================
const { db } = require("./dbConnection");
const { removeFromInventory } = require("./inventoryController");
const logger = require("./logger");
const addItemToCart = async (username, itemName) => {
await removeFromInventory(itemName);
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) {
const userNotFound = new Error("user not found");
userNotFound.code = 404;
}
const itemEntry = await db
.select()
.from("carts_items")
.where({ userId: user.id, itemName })
.first();
if (itemEntry && itemEntry.quantity + 1 > 3) {
const limitError = new Error(
"You can't have more than three units of an item in your cart"
);
limitError.code = 400;
throw limitError;
}
if (itemEntry) {
await db("carts_items")
.increment("quantity")
.update({ updatedAt: new Date().toISOString() })
.where({
userId: itemEntry.userId,
itemName
});
} else {
await db("carts_items").insert({
userId: user.id,
itemName,
quantity: 1,
updatedAt: new Date().toISOString()
});
}
logger.log(`${itemName} added to ${username}'s cart`);
return db
.select("itemName", "quantity")
.from("carts_items")
.where({ userId: user.id });
};
const hoursInMs = n => 1000 * 60 * 60 * n;
const removeStaleItems = async () => {
const fourHoursAgo = new Date(Date.now() - hoursInMs(4)).toISOString();
const staleItems = await db
.select()
.from("carts_items")
.where("updatedAt", "<", fourHoursAgo);
if (staleItems.length === 0) return;
// Put stale items back in the inventory
const inventoryUpdates = staleItems.map(staleItem =>
db("inventory")
.increment("quantity", staleItem.quantity)
.where({ itemName: staleItem.itemName })
);
await Promise.all(inventoryUpdates);
// Delete stale items from cart
const staleItemTuples = staleItems.map(i => [i.itemName, i.userId]);
await db("carts_items")
.del()
.whereIn(["itemName", "userId"], staleItemTuples);
};
const monitorStaleItems = () => setInterval(removeStaleItems, hoursInMs(2));
module.exports = { addItemToCart, monitorStaleItems };
================================================
FILE: chapter6/5_web_sockets_and_http_requests/server/cartController.test.js
================================================
const { db } = require("./dbConnection");
const { addItemToCart, monitorStaleItems } = require("./cartController");
const { hashPassword } = require("./authenticationController");
const { user: globalUser } = require("./userTestUtils");
const FakeTimers = require("@sinonjs/fake-timers");
const fs = require("fs");
describe("addItemToCart", () => {
beforeEach(() => {
fs.writeFileSync("/tmp/logs.out", "");
});
test("adding unavailable items to cart", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 0 });
try {
await addItemToCart(globalUser.username, "cheesecake");
} catch (e) {
const expectedError = new Error("cheesecake is unavailable");
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
const finalCartContent = await db
.select("carts_items.*")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([]);
expect.assertions(2);
});
test("adding items above limit to cart", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 1 });
await db("carts_items").insert({
userId: globalUser.id,
itemName: "cheesecake",
quantity: 3
});
try {
await addItemToCart(globalUser.username, "cheesecake");
} catch (e) {
const expectedError = new Error(
"You can't have more than three units of an item in your cart"
);
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([{ itemName: "cheesecake", quantity: 3 }]);
expect.assertions(2);
});
test("logging added items", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 1 });
await db("carts_items").insert({
userId: globalUser.id,
itemName: "cheesecake",
quantity: 1
});
await addItemToCart(globalUser.username, "cheesecake");
const logs = fs.readFileSync("/tmp/logs.out", "utf-8");
expect(logs).toContain(
`cheesecake added to ${globalUser.username}'s cart\n`
);
});
});
const withRetries = async fn => {
// Capture the assertion error since Jest does not export it
const JestAssertionError = (() => {
try {
expect(false).toBe(true);
} catch (e) {
return e.constructor;
}
})();
try {
await fn();
} catch (e) {
if (e.constructor === JestAssertionError) {
// Wait 100ms before retrying
await new Promise(resolve => setTimeout(resolve, 100));
await withRetries(fn);
} else {
throw e;
}
}
};
describe("timers", () => {
const hoursInMs = n => 1000 * 60 * 60 * n;
let clock;
beforeEach(() => {
clock = FakeTimers.install({ toFake: ["Date", "setInterval"] });
});
afterEach(() => {
clock = clock.uninstall();
});
test("removing stale items", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 1 });
await addItemToCart(globalUser.username, "cheesecake");
clock.tick(hoursInMs(4));
timer = monitorStaleItems();
clock.tick(hoursInMs(2));
await withRetries(async () => {
const finalCartContent = await db
.select()
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([]);
});
await withRetries(async () => {
const inventoryContent = await db
.select("itemName", "quantity")
.from("inventory");
expect(inventoryContent).toEqual([
{ itemName: "cheesecake", quantity: 1 }
]);
});
});
});
================================================
FILE: chapter6/5_web_sockets_and_http_requests/server/dbConnection.js
================================================
const environmentName = process.env.NODE_ENV;
const db = require("knex")(require("./knexfile")[environmentName]);
const closeConnection = () => db.destroy();
module.exports = {
db,
closeConnection
};
================================================
FILE: chapter6/5_web_sockets_and_http_requests/server/disconnectFromDb.js
================================================
const { db } = require("./dbConnection");
afterAll(() => db.destroy());
================================================
FILE: chapter6/5_web_sockets_and_http_requests/server/inventoryController.js
================================================
const { db } = require("./dbConnection");
const removeFromInventory = async itemName => {
const inventoryEntry = await db
.select()
.from("inventory")
.where({ itemName })
.first();
if (!inventoryEntry || inventoryEntry.quantity === 0) {
const err = new Error(`${itemName} is unavailable`);
err.code = 400;
throw err;
}
await db("inventory")
.decrement("quantity")
.where({ itemName });
};
module.exports = { removeFromInventory };
================================================
FILE: chapter6/5_web_sockets_and_http_requests/server/jest.config.js
================================================
module.exports = {
testEnvironment: "node",
globalSetup: "./migrateDatabases.js",
setupFilesAfterEnv: [
"/truncateTables.js",
"/seedUser.js",
"/disconnectFromDb.js"
]
};
================================================
FILE: chapter6/5_web_sockets_and_http_requests/server/knexfile.js
================================================
module.exports = {
test: {
client: "sqlite3",
connection: { filename: "./test.sqlite" },
useNullAsDefault: true
},
development: {
client: "sqlite3",
connection: { filename: "./dev.sqlite" },
useNullAsDefault: true
}
};
================================================
FILE: chapter6/5_web_sockets_and_http_requests/server/logger.js
================================================
const fs = require("fs");
const logger = {
log: msg => fs.appendFileSync("/tmp/logs.out", msg + "\n")
};
module.exports = logger;
================================================
FILE: chapter6/5_web_sockets_and_http_requests/server/migrateDatabases.js
================================================
const environmentName = process.env.NODE_ENV || "test";
const environmentConfig = require("./knexfile")[environmentName];
const db = require("knex")(environmentConfig);
module.exports = async () => {
// Migrate the database to the latest state
await db.migrate.latest();
// Close the connection to the database so that tests won't hang
await db.destroy();
};
================================================
FILE: chapter6/5_web_sockets_and_http_requests/server/migrations/20200325082401_initial_schema.js
================================================
exports.up = async knex => {
await knex.schema.createTable("users", table => {
table.increments("id");
table.string("username");
table.unique("username");
table.string("email");
table.string("passwordHash");
});
await knex.schema.createTable("carts_items", table => {
table.integer("userId").references("users.id");
table.string("itemName");
table.unique("itemName");
table.integer("quantity");
});
await knex.schema.createTable("inventory", table => {
table.increments("id");
table.string("itemName");
table.unique("itemName");
table.integer("quantity");
});
};
exports.down = async knex => {
await knex.schema.dropTable("users");
await knex.schema.dropTable("carts_items");
await knex.schema.dropTable("inventory");
};
================================================
FILE: chapter6/5_web_sockets_and_http_requests/server/migrations/20200331210311_updatedAt_field.js
================================================
exports.up = knex => {
return knex.schema.alterTable("carts_items", table => {
table.timestamp("updatedAt");
});
};
exports.down = knex => {
return knex.schema.alterTable("carts_items", table => {
table.dropColumn("updatedAt");
});
};
================================================
FILE: chapter6/5_web_sockets_and_http_requests/server/package.json
================================================
{
"name": "chapter5_server",
"version": "1.0.0",
"scripts": {
"test": "jest --runInBand",
"start": "cross-env NODE_ENV=development node server.js",
"migrate:dev": "knex migrate:latest --env development",
"seed:dev": "knex seed:run"
},
"devDependencies": {
"@sinonjs/fake-timers": "github:sinonjs/fake-timers",
"jest": "^24.9.0",
"supertest": "^4.0.2"
},
"dependencies": {
"@koa/cors": "^3.0.0",
"cross-env": "^7.0.2",
"isomorphic-fetch": "^2.2.1",
"knex": "^0.20.13",
"koa": "^2.11.0",
"koa-body-parser": "^1.1.2",
"koa-router": "^7.4.0",
"koa-socket-2": "^1.2.0",
"nock": "^12.0.3",
"socket.io": "^2.3.0",
"sqlite3": "^4.1.1"
},
"main": "alertController.spec.js",
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}
================================================
FILE: chapter6/5_web_sockets_and_http_requests/server/seedUser.js
================================================
const { createUser } = require("./userTestUtils");
beforeEach(createUser);
================================================
FILE: chapter6/5_web_sockets_and_http_requests/server/seeds/initial_inventory.js
================================================
exports.seed = async knex => {
await knex("inventory").del();
return knex("inventory").insert([
{ itemName: "cheesecake", quantity: 8 },
{ itemName: "apple pie", quantity: 2 },
{ itemName: "carrot cake", quantity: 5 }
]);
};
================================================
FILE: chapter6/5_web_sockets_and_http_requests/server/server.js
================================================
const fetch = require("isomorphic-fetch");
const Koa = require("koa");
const http = require("http");
const IO = require("koa-socket-2");
const cors = require("@koa/cors");
const Router = require("koa-router");
const bodyParser = require("koa-body-parser");
const { db } = require("./dbConnection");
const { addItemToCart } = require("./cartController");
const {
hashPassword,
authenticationMiddleware
} = require("./authenticationController");
const PORT = process.env.NODE_ENV === "test" ? 5000 : 3000;
const app = new Koa();
const io = new IO();
io.attach(app);
const router = new Router();
app.use(cors());
app.use(bodyParser());
app.use(async (ctx, next) => {
if (ctx.url.startsWith("/carts")) {
return await authenticationMiddleware(ctx, next);
}
await next();
});
router.put("/users/:username", async ctx => {
const { username } = ctx.params;
const { email, password } = ctx.request.body;
const userAlreadyExists = await db
.select()
.from("users")
.where({ username })
.first();
if (userAlreadyExists) {
ctx.body = { message: `${username} already exists` };
ctx.status = 409;
return;
}
await db("users").insert({
username,
email,
passwordHash: hashPassword(password)
});
return (ctx.body = { message: `${username} created successfully` });
});
router.post("/carts/:username/items", async ctx => {
const { username } = ctx.params;
const { item, quantity } = ctx.request.body;
for (let i = 0; i < quantity; i++) {
try {
const newItems = await addItemToCart(username, item);
ctx.body = newItems;
} catch (e) {
ctx.body = { message: e.message };
ctx.status = e.code;
return;
}
}
});
router.delete("/carts/:username/items/:item", async ctx => {
const { username, item } = ctx.params;
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) {
ctx.body = { message: "user not found" };
ctx.status = 404;
return;
}
const itemEntry = await db
.select()
.from("carts_items")
.where({ userId: user.id, itemName: item })
.first();
if (!itemEntry || itemEntry.quantity === 0) {
ctx.body = { message: `${item} is not in the cart` };
ctx.status = 400;
return;
}
await db("carts_items")
.decrement("quantity")
.where({ userId: user.id, itemName: item });
const inventoryEntry = await db
.select()
.from("inventory")
.where({ itemName: item })
.first();
if (inventoryEntry) {
await db("inventory")
.increment("quantity")
.where({ userId: itemEntry.userId, itemName: item });
} else {
await db("inventory").insert({ itemName: item, quantity: 1 });
}
ctx.body = await db
.select("itemName", "quantity")
.from("carts_items")
.where({ userId: user.id });
});
router.post("/inventory/:itemName", async ctx => {
const { itemName } = ctx.params;
const { quantity } = ctx.request.body;
const clientId = ctx.request.headers["x-socket-client-id"];
const current = await db
.select("itemName", "quantity")
.from("inventory")
.where({ itemName })
.first();
const itemExists = current && current.quantity > 0;
const newRecord = {
itemName,
quantity: (itemExists ? current.quantity : 0) + quantity
};
if (current) {
await db("inventory")
.increment("quantity", quantity)
.where({ itemName });
} else {
await db("inventory").insert(newRecord);
}
Object.entries(io.socket.sockets.connected).forEach(([id, socket]) => {
if (id === clientId) return;
socket.emit("add_item", { itemName, quantity });
});
ctx.body = newRecord;
});
router.delete("/inventory/:itemName", async ctx => {
const { itemName } = ctx.params;
const { quantity } = ctx.request.body;
const current = await db
.select("itemName", "quantity")
.from("inventory")
.where({ itemName })
.first();
const canDelete = current && current.quantity > quantity;
if (canDelete) {
await db("inventory")
.decrement("quantity", quantity)
.where({ itemName });
ctx.body = { message: `Removed ${quantity} units of ${itemName}` };
} else {
ctx.status = 404;
ctx.body = {
message: `There aren't ${quantity} units of ${itemName} available.`
};
}
});
router.get("/inventory", async ctx => {
const inventoryContent = await db
.select("itemName", "quantity")
.from("inventory")
.where("quantity", ">", 0)
.orderBy("quantity", "desc");
ctx.body = inventoryContent.reduce((acc, { itemName, quantity }) => {
return { ...acc, [itemName]: quantity };
}, {});
});
router.get("/inventory/:itemName", async ctx => {
const { itemName } = ctx.params;
const response = await fetch(`http://recipepuppy.com/api?i=${itemName}`);
const { title, href, results: recipes } = await response.json();
const inventoryItem = await db
.select()
.from("inventory")
.where({ itemName })
.first();
ctx.body = {
...inventoryItem,
info: `Data obtained from ${title} - ${href}`,
recipes
};
});
app.use(router.routes());
module.exports = { app: app.listen(PORT, "127.0.0.1") };
================================================
FILE: chapter6/5_web_sockets_and_http_requests/server/server.test.js
================================================
const { user: globalUser } = require("./userTestUtils");
const { db } = require("./dbConnection");
const request = require("supertest");
const { app } = require("./server.js");
const { hashPassword } = require("./authenticationController.js");
const nock = require("nock");
afterAll(() => app.close());
describe("add items to a cart", () => {
test("adding available items", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 3 });
const response = await request(app)
.post(`/carts/${globalUser.username}/items`)
.set("authorization", globalUser.authHeader)
.send({ item: "cheesecake", quantity: 3 })
.expect(200)
.expect("Content-Type", /json/);
const newItems = [{ itemName: "cheesecake", quantity: 3 }];
expect(response.body).toEqual(newItems);
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(0);
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual(newItems);
});
test("adding unavailable items", async () => {
const response = await request(app)
.post(`/carts/${globalUser.username}/items`)
.set("authorization", globalUser.authHeader)
.send({ item: "cheesecake", quantity: 1 })
.expect(400)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "cheesecake is unavailable"
});
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([]);
});
});
describe("removing items from a cart", () => {
test("removing existing items", async () => {
await db("carts_items").insert({
userId: globalUser.id,
itemName: "cheesecake",
quantity: 1
});
const response = await request(app)
.del(`/carts/${globalUser.username}/items/cheesecake`)
.set("authorization", globalUser.authHeader)
.expect(200)
.expect("Content-Type", /json/);
const expectedFinalContent = [{ itemName: "cheesecake", quantity: 0 }];
expect(response.body).toEqual(expectedFinalContent);
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual(expectedFinalContent);
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(1);
});
test("removing non-existing items", async () => {
await db("inventory").insert({
itemName: "cheesecake",
quantity: 0
});
const response = await request(app)
.del(`/carts/${globalUser.username}/items/cheesecake`)
.set("authorization", globalUser.authHeader)
.expect(400)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "cheesecake is not in the cart"
});
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(0);
});
});
describe("create accounts", () => {
test("creating a new account", async () => {
const response = await request(app)
.put("/users/another_user")
.send({ email: "another_user@example.org", password: "a_password" })
.expect(200)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "another_user created successfully"
});
const savedUser = await db
.select("email", "passwordHash")
.from("users")
.where({ username: "another_user" })
.first();
expect(savedUser).toEqual({
email: "another_user@example.org",
passwordHash: hashPassword("a_password")
});
});
test("creating a duplicate account", async () => {
const response = await request(app)
.put(`/users/${globalUser.username}`)
.send({ email: globalUser.email, password: "a_password" })
.expect(409)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: `${globalUser.username} already exists`
});
});
});
describe("list inventory items", () => {
const eggs = { itemName: "eggs", quantity: 3 };
const applePie = { itemName: "apple pie", quantity: 1 };
const carrotCake = { itemName: "carrot cake", quantity: 0 };
beforeEach(async () => {
await db("inventory").insert([eggs, applePie, carrotCake]);
});
test("fetching all available items", async () => {
const { body } = await request(app)
.get("/inventory")
.expect(200)
.expect("Content-Type", /json/);
expect(body).toEqual({ eggs: 3, "apple pie": 1 });
});
});
describe("add inventory items", () => {
test("adding a new item", async () => {
const { body } = await request(app)
.post("/inventory/eggs")
.send({ quantity: 3 })
.expect(200)
.expect("Content-Type", /json/);
expect(body).toEqual({ itemName: "eggs", quantity: 3 });
expect(
await db
.select("itemName", "quantity")
.from("inventory")
.where("itemName", "eggs")
.first()
).toEqual({ itemName: "eggs", quantity: 3 });
});
test("adding an existing item", async () => {
const eggs = { itemName: "eggs", quantity: 2 };
await db("inventory").insert(eggs);
const { body } = await request(app)
.post("/inventory/eggs")
.send({ quantity: 3 })
.expect(200)
.expect("Content-Type", /json/);
expect(body).toEqual({ itemName: "eggs", quantity: 5 });
expect(
await db
.select("itemName", "quantity")
.from("inventory")
.where("itemName", "eggs")
.first()
).toEqual({ itemName: "eggs", quantity: 5 });
});
});
describe("remove inventory items", () => {
beforeEach(async () => {
await db("inventory").insert({ itemName: "eggs", quantity: 3 });
});
test("removing an item", async () => {
const { body } = await request(app)
.del("/inventory/eggs")
.send({ quantity: 2 })
.expect(200)
.expect("Content-Type", /json/);
expect(body).toEqual({
message: "Removed 2 units of eggs"
});
expect(
await db
.select("itemName", "quantity")
.from("inventory")
.where("itemName", "eggs")
.first()
).toEqual({ itemName: "eggs", quantity: 1 });
});
test("removing more than the inventory quantity", async () => {
const { body } = await request(app)
.del("/inventory/eggs")
.send({ quantity: 4 })
.expect(404)
.expect("Content-Type", /json/);
expect(body).toEqual({
message: "There aren't 4 units of eggs available."
});
expect(
await db
.select("itemName", "quantity")
.from("inventory")
.where("itemName", "eggs")
.first()
).toEqual({ itemName: "eggs", quantity: 3 });
});
});
describe("fetch inventory items", () => {
const eggs = { itemName: "eggs", quantity: 3 };
const applePie = { itemName: "apple pie", quantity: 1 };
beforeEach(async () => {
await db("inventory").insert([eggs, applePie]);
const { id: eggsId } = await db
.select()
.from("inventory")
.where({ itemName: "eggs" })
.first();
eggs.id = eggsId;
});
test("fetching an item from the inventory", async () => {
const eggsResponse = {
title: "FakeAPI",
href: "example.org",
results: [{ name: "Omelette du Fromage" }]
};
nock("http://recipepuppy.com")
.get("/api")
.query({ i: "eggs" })
.reply(200, eggsResponse);
const response = await request(app)
.get(`/inventory/eggs`)
.expect(200)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
...eggs,
info: `Data obtained from ${eggsResponse.title} - ${eggsResponse.href}`,
recipes: eggsResponse.results
});
});
});
================================================
FILE: chapter6/5_web_sockets_and_http_requests/server/truncateTables.js
================================================
const { db } = require("./dbConnection");
const tablesToTruncate = ["users", "inventory", "carts_items"];
beforeEach(() => {
return Promise.all(tablesToTruncate.map(t => db(t).truncate()));
});
================================================
FILE: chapter6/5_web_sockets_and_http_requests/server/userTestUtils.js
================================================
const { db } = require("./dbConnection");
const { hashPassword } = require("./authenticationController");
const username = "test_user";
const password = "a_password";
const passwordHash = hashPassword(password);
const email = "test_user@example.org";
const validAuth = Buffer.from(`${username}:${password}`).toString("base64");
const authHeader = `Basic ${validAuth}`;
const user = {
username,
password,
email,
authHeader
};
const createUser = async () => {
await db("users").insert({ username, email, passwordHash });
const { id } = await db
.select()
.from("users")
.where({ username })
.first();
user.id = id;
};
module.exports = { user, createUser };
================================================
FILE: chapter7/1_setting_up_a_test_environment/1_createElement_calls/index.html
================================================
Inventory
================================================
FILE: chapter7/1_setting_up_a_test_environment/1_createElement_calls/index.js
================================================
const ReactDOM = require("react-dom");
const React = require("react");
const header = React.createElement("h1", null, "Inventory Contents");
const App = React.createElement("div", null, header);
ReactDOM.render(App, document.getElementById("app"));
================================================
FILE: chapter7/1_setting_up_a_test_environment/1_createElement_calls/package.json
================================================
{
"name": "1_createelement_calls",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "browserify index.js -o bundle.js",
"start": "http-server ./"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"browserify": "^16.5.1",
"http-server": "^0.12.3"
},
"dependencies": {
"react": "^16.13.1",
"react-dom": "^16.13.1"
}
}
================================================
FILE: chapter7/1_setting_up_a_test_environment/2_transforming_jsx/index.html
================================================
Inventory
================================================
FILE: chapter7/1_setting_up_a_test_environment/2_transforming_jsx/index.jsx
================================================
import ReactDOM from "react-dom";
import React from "react";
const App = () => {
return (
Inventory Contents
);
};
ReactDOM.render( , document.getElementById("app"));
================================================
FILE: chapter7/1_setting_up_a_test_environment/2_transforming_jsx/package.json
================================================
{
"name": "2_transforming_jsx",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "browserify index.jsx -o bundle.js",
"start": "http-server ./"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.9.6",
"@babel/preset-env": "^7.9.6",
"@babel/preset-react": "^7.9.4",
"babelify": "^10.0.0",
"browserify": "^16.5.1",
"http-server": "^0.12.3"
},
"dependencies": {
"react": "^16.13.1",
"react-dom": "^16.13.1"
},
"browserify": {
"transform": [
[
"babelify",
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
}
]
]
}
}
================================================
FILE: chapter7/1_setting_up_a_test_environment/3_setting_up_jest/App.jsx
================================================
import React from "react";
export const App = () => {
return (
Inventory Contents
);
};
================================================
FILE: chapter7/1_setting_up_a_test_environment/3_setting_up_jest/app.test.js
================================================
import App from "./App.jsx";
================================================
FILE: chapter7/1_setting_up_a_test_environment/3_setting_up_jest/babel.config.js
================================================
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: {
node: "current"
}
}
],
"@babel/preset-react"
]
};
================================================
FILE: chapter7/1_setting_up_a_test_environment/3_setting_up_jest/index.html
================================================
Inventory
================================================
FILE: chapter7/1_setting_up_a_test_environment/3_setting_up_jest/index.jsx
================================================
import ReactDOM from "react-dom";
import React from "react";
import { App } from "./App.jsx";
ReactDOM.render( , document.getElementById("app"));
================================================
FILE: chapter7/1_setting_up_a_test_environment/3_setting_up_jest/jest.config.js
================================================
// For a detailed explanation regarding each configuration property, visit:
// https://jestjs.io/docs/en/configuration.html
module.exports = {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "/private/var/folders/p0/0npmk50s57v5sgzb_s9z2xmc0000gn/T/jest_dx",
// Automatically clear mock calls and instances between every test
// clearMocks: false,
// Indicates whether the coverage information should be collected while executing the test
// collectCoverage: false,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
// coverageDirectory: undefined,
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// 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.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "json",
// "jsx",
// "ts",
// "tsx",
// "node"
// ],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
// preset: undefined,
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state between every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state between every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// ""
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
// testEnvironment: "jest-environment-jsdom",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jasmine2",
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
// testURL: "http://localhost",
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
// timers: "real",
// A map from regular expressions to paths to transformers
// transform: undefined,
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
};
================================================
FILE: chapter7/1_setting_up_a_test_environment/3_setting_up_jest/package.json
================================================
{
"name": "3_setting_up_jest",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "browserify index.jsx -o bundle.js",
"start": "http-server ./",
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.9.6",
"@babel/preset-env": "^7.9.6",
"@babel/preset-react": "^7.9.4",
"babelify": "^10.0.0",
"browserify": "^16.5.1",
"http-server": "^0.12.3",
"jest": "^24.9"
},
"dependencies": {
"react": "^16.13.1",
"react-dom": "^16.13.1"
},
"browserify": {
"transform": [
[
"babelify",
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
}
]
]
}
}
================================================
FILE: chapter7/2_an_overview_of_react_testing_libraries/1_react_testing_utilities/App.jsx
================================================
import React from "react";
export const App = () => {
const [cheesecakes, setCheesecake] = React.useState(0);
return (
Inventory Contents
Cheesecakes: {cheesecakes}
setCheesecake(cheesecakes + 1)}>
Add cheesecake
);
};
================================================
FILE: chapter7/2_an_overview_of_react_testing_libraries/1_react_testing_utilities/App.test.jsx
================================================
import React from "react";
import { App } from "./App.jsx";
import { render } from "react-dom";
import { act } from "react-dom/test-utils";
import { screen, fireEvent } from "@testing-library/dom";
const root = document.createElement("div");
document.body.appendChild(root);
test("renders the appropriate header", () => {
act(() => {
render( , root);
});
expect(screen.getByText("Inventory Contents")).toBeInTheDocument();
});
test("increments the number of cheesecakes", () => {
act(() => {
render( , root);
});
expect(screen.getByText("Cheesecakes: 0")).toBeInTheDocument();
const addCheesecakeBtn = screen.getByText("Add cheesecake");
act(() => {
fireEvent.click(addCheesecakeBtn);
});
expect(screen.getByText("Cheesecakes: 1")).toBeInTheDocument();
});
================================================
FILE: chapter7/2_an_overview_of_react_testing_libraries/1_react_testing_utilities/babel.config.js
================================================
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: {
node: "current"
}
}
],
"@babel/preset-react"
]
};
================================================
FILE: chapter7/2_an_overview_of_react_testing_libraries/1_react_testing_utilities/index.html
================================================
Inventory
================================================
FILE: chapter7/2_an_overview_of_react_testing_libraries/1_react_testing_utilities/index.jsx
================================================
import ReactDOM from "react-dom";
import React from "react";
import { App } from "./App.jsx";
ReactDOM.render( , document.getElementById("app"));
================================================
FILE: chapter7/2_an_overview_of_react_testing_libraries/1_react_testing_utilities/jest.config.js
================================================
module.exports = {
setupFilesAfterEnv: ["/setupJestDom.js"]
};
================================================
FILE: chapter7/2_an_overview_of_react_testing_libraries/1_react_testing_utilities/package.json
================================================
{
"name": "1_react_testing_utilities",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "browserify index.jsx -o bundle.js",
"start": "http-server ./",
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.9.6",
"@babel/preset-env": "^7.9.6",
"@babel/preset-react": "^7.9.4",
"@testing-library/dom": "^7.10.1",
"@testing-library/jest-dom": "^5.9.0",
"babelify": "^10.0.0",
"browserify": "^16.5.1",
"http-server": "^0.12.3",
"jest": "^24.9"
},
"dependencies": {
"react": "^16.13.1",
"react-dom": "^16.13.1"
},
"browserify": {
"transform": [
[
"babelify",
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
}
]
]
}
}
================================================
FILE: chapter7/2_an_overview_of_react_testing_libraries/1_react_testing_utilities/setupJestDom.js
================================================
const jestDom = require("@testing-library/jest-dom");
expect.extend(jestDom);
================================================
FILE: chapter7/2_an_overview_of_react_testing_libraries/2_react_testing_library/App.jsx
================================================
import React, { useEffect, useState, useRef } from "react";
import { API_ADDR } from "./constants";
import { ItemForm } from "./ItemForm.jsx";
import { ItemList } from "./ItemList.jsx";
export const App = () => {
const [items, setItems] = useState({});
const isMounted = useRef(null);
useEffect(() => {
isMounted.current = true;
const loadItems = async () => {
const response = await fetch(`${API_ADDR}/inventory`);
const responseBody = await response.json();
if (isMounted.current) setItems(responseBody);
};
loadItems();
return () => (isMounted.current = false);
}, []);
return (
Inventory Contents
);
};
================================================
FILE: chapter7/2_an_overview_of_react_testing_libraries/2_react_testing_library/App.test.jsx
================================================
import React from "react";
import nock from "nock";
import { API_ADDR } from "./constants";
import { App } from "./App.jsx";
import { render, waitFor } from "@testing-library/react";
beforeEach(() => {
nock(API_ADDR)
.get("/inventory")
.reply(200, { cheesecake: 2, croissant: 5, macaroon: 96 });
});
afterEach(() => {
if (!nock.isDone()) {
nock.cleanAll();
throw new Error("Not all mocked endpoints received requests.");
}
});
test("renders the appropriate header", () => {
const { getByText } = render( );
expect(getByText("Inventory Contents")).toBeInTheDocument();
});
test("rendering the server's list of items", async () => {
const { findByText } = render( );
expect(await findByText("cheesecake - Quantity: 2")).toBeInTheDocument();
expect(await findByText("croissant - Quantity: 5")).toBeInTheDocument();
expect(await findByText("macaroon - Quantity: 96")).toBeInTheDocument();
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
});
================================================
FILE: chapter7/2_an_overview_of_react_testing_libraries/2_react_testing_library/ItemForm.jsx
================================================
import React from "react";
import { API_ADDR } from "./constants";
const addItemRequest = (itemName, quantity) => {
fetch(`${API_ADDR}/inventory/${itemName}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ quantity })
});
};
export const ItemForm = () => {
const [itemName, setItemName] = React.useState("");
const [quantity, setQuantity] = React.useState(0);
const onSubmit = async e => {
e.preventDefault();
await addItemRequest(itemName, quantity);
};
return (
);
};
================================================
FILE: chapter7/2_an_overview_of_react_testing_libraries/2_react_testing_library/ItemForm.test.jsx
================================================
import React from "react";
import nock from "nock";
import { API_ADDR } from "./constants";
import { ItemForm } from "./ItemForm.jsx";
import { render, fireEvent } from "@testing-library/react";
test("form's elements", () => {
const { getByText, getByPlaceholderText } = render( );
expect(getByPlaceholderText("Item name")).toBeInTheDocument();
expect(getByPlaceholderText("Quantity")).toBeInTheDocument();
expect(getByText("Add item")).toBeInTheDocument();
});
test("sending requests", () => {
const { getByText, getByPlaceholderText } = render( );
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 2 }))
.reply(200);
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "2" }
});
fireEvent.click(getByText("Add item"));
expect(nock.isDone()).toBe(true);
});
================================================
FILE: chapter7/2_an_overview_of_react_testing_libraries/2_react_testing_library/ItemList.jsx
================================================
import React from "react";
export const ItemList = ({ itemList }) => {
return (
{Object.entries(itemList).map(([itemName, quantity]) => {
return (
{itemName} - Quantity: {quantity}
);
})}
);
};
================================================
FILE: chapter7/2_an_overview_of_react_testing_libraries/2_react_testing_library/ItemList.test.jsx
================================================
import React from "react";
import { ItemList } from "./ItemList.jsx";
import { render } from "@testing-library/react";
test("list items", () => {
const itemList = { cheesecake: 2, croissant: 5, macaroon: 96 };
const { getByText } = render( );
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
expect(getByText("cheesecake - Quantity: 2")).toBeInTheDocument();
expect(getByText("croissant - Quantity: 5")).toBeInTheDocument();
expect(getByText("macaroon - Quantity: 96")).toBeInTheDocument();
});
================================================
FILE: chapter7/2_an_overview_of_react_testing_libraries/2_react_testing_library/babel.config.js
================================================
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: {
node: "current"
}
}
],
"@babel/preset-react"
]
};
================================================
FILE: chapter7/2_an_overview_of_react_testing_libraries/2_react_testing_library/constants.js
================================================
export const API_ADDR = "http://localhost:3000";
================================================
FILE: chapter7/2_an_overview_of_react_testing_libraries/2_react_testing_library/index.html
================================================
Inventory
================================================
FILE: chapter7/2_an_overview_of_react_testing_libraries/2_react_testing_library/index.jsx
================================================
import ReactDOM from "react-dom";
import React from "react";
import { App } from "./App.jsx";
ReactDOM.render( , document.getElementById("app"));
================================================
FILE: chapter7/2_an_overview_of_react_testing_libraries/2_react_testing_library/jest.config.js
================================================
module.exports = {
setupFilesAfterEnv: [
"/setupJestDom.js",
"/setupGlobalFetch.js"
]
};
================================================
FILE: chapter7/2_an_overview_of_react_testing_libraries/2_react_testing_library/package.json
================================================
{
"name": "2_react-testing-library",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "browserify index.jsx -o bundle.js",
"start": "http-server ./",
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.9.6",
"@babel/preset-env": "^7.9.6",
"@babel/preset-react": "^7.9.4",
"@testing-library/dom": "^7.10.1",
"@testing-library/jest-dom": "^5.9.0",
"@testing-library/react": "^10.2.1",
"babelify": "^10.0.0",
"browserify": "^16.5.1",
"core-js": "^2.6.11",
"http-server": "^0.12.3",
"isomorphic-fetch": "^2.2.1",
"jest": "^25.5",
"nock": "^12.0.3"
},
"dependencies": {
"react": "^16.13.1",
"react-dom": "^16.13.1"
},
"browserify": {
"transform": [
[
"babelify",
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 2
}
],
"@babel/preset-react"
]
}
]
]
}
}
================================================
FILE: chapter7/2_an_overview_of_react_testing_libraries/2_react_testing_library/setupGlobalFetch.js
================================================
const fetch = require("isomorphic-fetch");
global.window.fetch = fetch;
================================================
FILE: chapter7/2_an_overview_of_react_testing_libraries/2_react_testing_library/setupJestDom.js
================================================
const jestDom = require("@testing-library/jest-dom");
expect.extend(jestDom);
================================================
FILE: chapter7/server/README.md
================================================
# Chapter 5 Server
To 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.
In case you want to update the back-end from Chapter 4 yourself, here's the list of changes I've done:
- For the server to accept the requests coming from the client, you'll need to use [`@koa/cors`](https://github.com/koajs/cors)
- 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.
- 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.
- At `GET /inventory` I have added a route which lists all items in the inventory.
- 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
- I've used `koa-socket-2` to add support for `socket.io`
- The `POST /inventory/:itemName` will now push updates to all clients but the one which added an item.
================================================
FILE: chapter7/server/authenticationController.js
================================================
const crypto = require("crypto");
const { db } = require("./dbConnection");
const hashPassword = password => {
const hash = crypto.createHash("sha256");
hash.update(password);
return hash.digest("hex");
};
const credentialsAreValid = async (username, password) => {
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) return false;
return hashPassword(password) === user.passwordHash;
};
const authenticationMiddleware = async (ctx, next) => {
try {
const authHeader = ctx.request.headers.authorization;
const credentials = Buffer.from(
authHeader.slice("basic".length + 1),
"base64"
).toString();
const [username, password] = credentials.split(":");
const validCredentialsSent = await credentialsAreValid(username, password);
if (!validCredentialsSent) throw new Error("invalid credentials");
} catch (e) {
ctx.status = 401;
ctx.body = { message: "please provide valid credentials" };
return;
}
await next();
};
module.exports = {
hashPassword,
credentialsAreValid,
authenticationMiddleware
};
================================================
FILE: chapter7/server/authenticationController.test.js
================================================
const crypto = require("crypto");
const {
hashPassword,
credentialsAreValid,
authenticationMiddleware
} = require("./authenticationController");
const { user: globalUser } = require("./userTestUtils");
describe("hashPassword", () => {
test("hashing passwords", () => {
const plainTextPassword = "password_example";
const hash = crypto.createHash("sha256");
hash.update(plainTextPassword);
const expectedHash = hash.digest("hex");
expect(hashPassword(plainTextPassword)).toBe(expectedHash);
});
});
describe("credentialsAreValid", () => {
test("validating credentials", async () => {
expect(await credentialsAreValid(globalUser.username, "a_password")).toBe(
true
);
});
});
describe("authenticationMiddleware", () => {
test("returning an error if the credentials are not valid", async () => {
const fakeAuth = Buffer.from("invalid:credentials").toString("base64");
const ctx = {
request: {
headers: { authorization: `Basic ${fakeAuth}` }
}
};
const next = jest.fn();
await authenticationMiddleware(ctx, next);
expect(next.mock.calls).toHaveLength(0);
expect(ctx).toEqual({
...ctx,
status: 401,
body: { message: "please provide valid credentials" }
});
});
test("authenticating properly", async () => {
const ctx = {
request: {
headers: { authorization: globalUser.authHeader }
}
};
const next = jest.fn();
await authenticationMiddleware(ctx, next);
expect(next.mock.calls).toHaveLength(1);
});
});
================================================
FILE: chapter7/server/cartController.js
================================================
const { db } = require("./dbConnection");
const { removeFromInventory } = require("./inventoryController");
const logger = require("./logger");
const addItemToCart = async (username, itemName) => {
await removeFromInventory(itemName);
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) {
const userNotFound = new Error("user not found");
userNotFound.code = 404;
}
const itemEntry = await db
.select()
.from("carts_items")
.where({ userId: user.id, itemName })
.first();
if (itemEntry && itemEntry.quantity + 1 > 3) {
const limitError = new Error(
"You can't have more than three units of an item in your cart"
);
limitError.code = 400;
throw limitError;
}
if (itemEntry) {
await db("carts_items")
.increment("quantity")
.update({ updatedAt: new Date().toISOString() })
.where({
userId: itemEntry.userId,
itemName
});
} else {
await db("carts_items").insert({
userId: user.id,
itemName,
quantity: 1,
updatedAt: new Date().toISOString()
});
}
logger.log(`${itemName} added to ${username}'s cart`);
return db
.select("itemName", "quantity")
.from("carts_items")
.where({ userId: user.id });
};
const hoursInMs = n => 1000 * 60 * 60 * n;
const removeStaleItems = async () => {
const fourHoursAgo = new Date(Date.now() - hoursInMs(4)).toISOString();
const staleItems = await db
.select()
.from("carts_items")
.where("updatedAt", "<", fourHoursAgo);
if (staleItems.length === 0) return;
// Put stale items back in the inventory
const inventoryUpdates = staleItems.map(staleItem =>
db("inventory")
.increment("quantity", staleItem.quantity)
.where({ itemName: staleItem.itemName })
);
await Promise.all(inventoryUpdates);
// Delete stale items from cart
const staleItemTuples = staleItems.map(i => [i.itemName, i.userId]);
await db("carts_items")
.del()
.whereIn(["itemName", "userId"], staleItemTuples);
};
const monitorStaleItems = () => setInterval(removeStaleItems, hoursInMs(2));
module.exports = { addItemToCart, monitorStaleItems };
================================================
FILE: chapter7/server/cartController.test.js
================================================
const { db } = require("./dbConnection");
const { addItemToCart, monitorStaleItems } = require("./cartController");
const { hashPassword } = require("./authenticationController");
const { user: globalUser } = require("./userTestUtils");
const FakeTimers = require("@sinonjs/fake-timers");
const fs = require("fs");
describe("addItemToCart", () => {
beforeEach(() => {
fs.writeFileSync("/tmp/logs.out", "");
});
test("adding unavailable items to cart", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 0 });
try {
await addItemToCart(globalUser.username, "cheesecake");
} catch (e) {
const expectedError = new Error("cheesecake is unavailable");
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
const finalCartContent = await db
.select("carts_items.*")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([]);
expect.assertions(2);
});
test("adding items above limit to cart", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 1 });
await db("carts_items").insert({
userId: globalUser.id,
itemName: "cheesecake",
quantity: 3
});
try {
await addItemToCart(globalUser.username, "cheesecake");
} catch (e) {
const expectedError = new Error(
"You can't have more than three units of an item in your cart"
);
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([{ itemName: "cheesecake", quantity: 3 }]);
expect.assertions(2);
});
test("logging added items", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 1 });
await db("carts_items").insert({
userId: globalUser.id,
itemName: "cheesecake",
quantity: 1
});
await addItemToCart(globalUser.username, "cheesecake");
const logs = fs.readFileSync("/tmp/logs.out", "utf-8");
expect(logs).toContain(
`cheesecake added to ${globalUser.username}'s cart\n`
);
});
});
const withRetries = async fn => {
// Capture the assertion error since Jest does not export it
const JestAssertionError = (() => {
try {
expect(false).toBe(true);
} catch (e) {
return e.constructor;
}
})();
try {
await fn();
} catch (e) {
if (e.constructor === JestAssertionError) {
// Wait 100ms before retrying
await new Promise(resolve => setTimeout(resolve, 100));
await withRetries(fn);
} else {
throw e;
}
}
};
describe("timers", () => {
const hoursInMs = n => 1000 * 60 * 60 * n;
let clock;
beforeEach(() => {
clock = FakeTimers.install({ toFake: ["Date", "setInterval"] });
});
afterEach(() => {
clock = clock.uninstall();
});
test("removing stale items", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 1 });
await addItemToCart(globalUser.username, "cheesecake");
clock.tick(hoursInMs(4));
timer = monitorStaleItems();
clock.tick(hoursInMs(2));
await withRetries(async () => {
const finalCartContent = await db
.select()
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([]);
});
await withRetries(async () => {
const inventoryContent = await db
.select("itemName", "quantity")
.from("inventory");
expect(inventoryContent).toEqual([
{ itemName: "cheesecake", quantity: 1 }
]);
});
});
});
================================================
FILE: chapter7/server/dbConnection.js
================================================
const environmentName = process.env.NODE_ENV;
const db = require("knex")(require("./knexfile")[environmentName]);
const closeConnection = () => db.destroy();
module.exports = {
db,
closeConnection
};
================================================
FILE: chapter7/server/disconnectFromDb.js
================================================
const { db } = require("./dbConnection");
afterAll(() => db.destroy());
================================================
FILE: chapter7/server/inventoryController.js
================================================
const { db } = require("./dbConnection");
const removeFromInventory = async itemName => {
const inventoryEntry = await db
.select()
.from("inventory")
.where({ itemName })
.first();
if (!inventoryEntry || inventoryEntry.quantity === 0) {
const err = new Error(`${itemName} is unavailable`);
err.code = 400;
throw err;
}
await db("inventory")
.decrement("quantity")
.where({ itemName });
};
module.exports = { removeFromInventory };
================================================
FILE: chapter7/server/jest.config.js
================================================
module.exports = {
testEnvironment: "node",
globalSetup: "./migrateDatabases.js",
setupFilesAfterEnv: [
"/truncateTables.js",
"/seedUser.js",
"/disconnectFromDb.js"
]
};
================================================
FILE: chapter7/server/knexfile.js
================================================
module.exports = {
test: {
client: "sqlite3",
connection: { filename: "./test.sqlite" },
useNullAsDefault: true
},
development: {
client: "sqlite3",
connection: { filename: "./dev.sqlite" },
useNullAsDefault: true
}
};
================================================
FILE: chapter7/server/logger.js
================================================
const fs = require("fs");
const logger = {
log: msg => fs.appendFileSync("/tmp/logs.out", msg + "\n")
};
module.exports = logger;
================================================
FILE: chapter7/server/migrateDatabases.js
================================================
const environmentName = process.env.NODE_ENV || "test";
const environmentConfig = require("./knexfile")[environmentName];
const db = require("knex")(environmentConfig);
module.exports = async () => {
// Migrate the database to the latest state
await db.migrate.latest();
// Close the connection to the database so that tests won't hang
await db.destroy();
};
================================================
FILE: chapter7/server/migrations/20200325082401_initial_schema.js
================================================
exports.up = async knex => {
await knex.schema.createTable("users", table => {
table.increments("id");
table.string("username");
table.unique("username");
table.string("email");
table.string("passwordHash");
});
await knex.schema.createTable("carts_items", table => {
table.integer("userId").references("users.id");
table.string("itemName");
table.unique("itemName");
table.integer("quantity");
});
await knex.schema.createTable("inventory", table => {
table.increments("id");
table.string("itemName");
table.unique("itemName");
table.integer("quantity");
});
};
exports.down = async knex => {
await knex.schema.dropTable("users");
await knex.schema.dropTable("carts_items");
await knex.schema.dropTable("inventory");
};
================================================
FILE: chapter7/server/migrations/20200331210311_updatedAt_field.js
================================================
exports.up = knex => {
return knex.schema.alterTable("carts_items", table => {
table.timestamp("updatedAt");
});
};
exports.down = knex => {
return knex.schema.alterTable("carts_items", table => {
table.dropColumn("updatedAt");
});
};
================================================
FILE: chapter7/server/package.json
================================================
{
"name": "chapter5_server",
"version": "1.0.0",
"scripts": {
"test": "jest --runInBand",
"start": "cross-env NODE_ENV=development node server.js",
"migrate:dev": "knex migrate:latest --env development",
"seed:dev": "knex seed:run"
},
"devDependencies": {
"@sinonjs/fake-timers": "github:sinonjs/fake-timers",
"jest": "^24.9.0",
"supertest": "^4.0.2"
},
"dependencies": {
"@koa/cors": "^3.0.0",
"cross-env": "^7.0.2",
"isomorphic-fetch": "^2.2.1",
"knex": "^0.20.13",
"koa": "^2.11.0",
"koa-body-parser": "^1.1.2",
"koa-router": "^7.4.0",
"koa-socket-2": "^1.2.0",
"nock": "^12.0.3",
"socket.io": "^2.3.0",
"sqlite3": "^4.1.1"
},
"main": "alertController.spec.js",
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}
================================================
FILE: chapter7/server/seedUser.js
================================================
const { createUser } = require("./userTestUtils");
beforeEach(createUser);
================================================
FILE: chapter7/server/seeds/initial_inventory.js
================================================
exports.seed = async knex => {
await knex("inventory").del();
return knex("inventory").insert([
{ itemName: "cheesecake", quantity: 8 },
{ itemName: "apple pie", quantity: 2 },
{ itemName: "carrot cake", quantity: 5 }
]);
};
================================================
FILE: chapter7/server/server.js
================================================
const fetch = require("isomorphic-fetch");
const Koa = require("koa");
const http = require("http");
const IO = require("koa-socket-2");
const cors = require("@koa/cors");
const Router = require("koa-router");
const bodyParser = require("koa-body-parser");
const { db } = require("./dbConnection");
const { addItemToCart } = require("./cartController");
const {
hashPassword,
authenticationMiddleware
} = require("./authenticationController");
const PORT = process.env.NODE_ENV === "test" ? 5000 : 3000;
const app = new Koa();
const io = new IO();
io.attach(app);
const router = new Router();
app.use(cors());
app.use(bodyParser());
app.use(async (ctx, next) => {
if (ctx.url.startsWith("/carts")) {
return await authenticationMiddleware(ctx, next);
}
await next();
});
router.put("/users/:username", async ctx => {
const { username } = ctx.params;
const { email, password } = ctx.request.body;
const userAlreadyExists = await db
.select()
.from("users")
.where({ username })
.first();
if (userAlreadyExists) {
ctx.body = { message: `${username} already exists` };
ctx.status = 409;
return;
}
await db("users").insert({
username,
email,
passwordHash: hashPassword(password)
});
return (ctx.body = { message: `${username} created successfully` });
});
router.post("/carts/:username/items", async ctx => {
const { username } = ctx.params;
const { item, quantity } = ctx.request.body;
for (let i = 0; i < quantity; i++) {
try {
const newItems = await addItemToCart(username, item);
ctx.body = newItems;
} catch (e) {
ctx.body = { message: e.message };
ctx.status = e.code;
return;
}
}
});
router.delete("/carts/:username/items/:item", async ctx => {
const { username, item } = ctx.params;
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) {
ctx.body = { message: "user not found" };
ctx.status = 404;
return;
}
const itemEntry = await db
.select()
.from("carts_items")
.where({ userId: user.id, itemName: item })
.first();
if (!itemEntry || itemEntry.quantity === 0) {
ctx.body = { message: `${item} is not in the cart` };
ctx.status = 400;
return;
}
await db("carts_items")
.decrement("quantity")
.where({ userId: user.id, itemName: item });
const inventoryEntry = await db
.select()
.from("inventory")
.where({ itemName: item })
.first();
if (inventoryEntry) {
await db("inventory")
.increment("quantity")
.where({ userId: itemEntry.userId, itemName: item });
} else {
await db("inventory").insert({ itemName: item, quantity: 1 });
}
ctx.body = await db
.select("itemName", "quantity")
.from("carts_items")
.where({ userId: user.id });
});
router.post("/inventory/:itemName", async ctx => {
const { itemName } = ctx.params;
const { quantity } = ctx.request.body;
const clientId = ctx.request.headers["x-socket-client-id"];
const current = await db
.select("itemName", "quantity")
.from("inventory")
.where({ itemName })
.first();
const itemExists = current && current.quantity > 0;
const newRecord = {
itemName,
quantity: (itemExists ? current.quantity : 0) + quantity
};
if (current) {
await db("inventory")
.increment("quantity", quantity)
.where({ itemName });
} else {
await db("inventory").insert(newRecord);
}
Object.entries(io.socket.sockets.connected).forEach(([id, socket]) => {
if (id === clientId) return;
socket.emit("add_item", { itemName, quantity });
});
ctx.body = newRecord;
});
router.delete("/inventory/:itemName", async ctx => {
const { itemName } = ctx.params;
const { quantity } = ctx.request.body;
const current = await db
.select("itemName", "quantity")
.from("inventory")
.where({ itemName })
.first();
const canDelete = current && current.quantity > quantity;
if (canDelete) {
await db("inventory")
.decrement("quantity", quantity)
.where({ itemName });
ctx.body = { message: `Removed ${quantity} units of ${itemName}` };
} else {
ctx.status = 404;
ctx.body = {
message: `There aren't ${quantity} units of ${itemName} available.`
};
}
});
router.get("/inventory", async ctx => {
const inventoryContent = await db
.select("itemName", "quantity")
.from("inventory")
.where("quantity", ">", 0)
.orderBy("quantity", "desc");
ctx.body = inventoryContent.reduce((acc, { itemName, quantity }) => {
return { ...acc, [itemName]: quantity };
}, {});
});
router.get("/inventory/:itemName", async ctx => {
const { itemName } = ctx.params;
const response = await fetch(`http://recipepuppy.com/api?i=${itemName}`);
const { title, href, results: recipes } = await response.json();
const inventoryItem = await db
.select()
.from("inventory")
.where({ itemName })
.first();
ctx.body = {
...inventoryItem,
info: `Data obtained from ${title} - ${href}`,
recipes
};
});
app.use(router.routes());
module.exports = { app: app.listen(PORT, "127.0.0.1") };
================================================
FILE: chapter7/server/server.test.js
================================================
const { user: globalUser } = require("./userTestUtils");
const { db } = require("./dbConnection");
const request = require("supertest");
const { app } = require("./server.js");
const { hashPassword } = require("./authenticationController.js");
const nock = require("nock");
afterAll(() => app.close());
describe("add items to a cart", () => {
test("adding available items", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 3 });
const response = await request(app)
.post(`/carts/${globalUser.username}/items`)
.set("authorization", globalUser.authHeader)
.send({ item: "cheesecake", quantity: 3 })
.expect(200)
.expect("Content-Type", /json/);
const newItems = [{ itemName: "cheesecake", quantity: 3 }];
expect(response.body).toEqual(newItems);
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(0);
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual(newItems);
});
test("adding unavailable items", async () => {
const response = await request(app)
.post(`/carts/${globalUser.username}/items`)
.set("authorization", globalUser.authHeader)
.send({ item: "cheesecake", quantity: 1 })
.expect(400)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "cheesecake is unavailable"
});
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([]);
});
});
describe("removing items from a cart", () => {
test("removing existing items", async () => {
await db("carts_items").insert({
userId: globalUser.id,
itemName: "cheesecake",
quantity: 1
});
const response = await request(app)
.del(`/carts/${globalUser.username}/items/cheesecake`)
.set("authorization", globalUser.authHeader)
.expect(200)
.expect("Content-Type", /json/);
const expectedFinalContent = [{ itemName: "cheesecake", quantity: 0 }];
expect(response.body).toEqual(expectedFinalContent);
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual(expectedFinalContent);
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(1);
});
test("removing non-existing items", async () => {
await db("inventory").insert({
itemName: "cheesecake",
quantity: 0
});
const response = await request(app)
.del(`/carts/${globalUser.username}/items/cheesecake`)
.set("authorization", globalUser.authHeader)
.expect(400)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "cheesecake is not in the cart"
});
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(0);
});
});
describe("create accounts", () => {
test("creating a new account", async () => {
const response = await request(app)
.put("/users/another_user")
.send({ email: "another_user@example.org", password: "a_password" })
.expect(200)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "another_user created successfully"
});
const savedUser = await db
.select("email", "passwordHash")
.from("users")
.where({ username: "another_user" })
.first();
expect(savedUser).toEqual({
email: "another_user@example.org",
passwordHash: hashPassword("a_password")
});
});
test("creating a duplicate account", async () => {
const response = await request(app)
.put(`/users/${globalUser.username}`)
.send({ email: globalUser.email, password: "a_password" })
.expect(409)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: `${globalUser.username} already exists`
});
});
});
describe("list inventory items", () => {
const eggs = { itemName: "eggs", quantity: 3 };
const applePie = { itemName: "apple pie", quantity: 1 };
const carrotCake = { itemName: "carrot cake", quantity: 0 };
beforeEach(async () => {
await db("inventory").insert([eggs, applePie, carrotCake]);
});
test("fetching all available items", async () => {
const { body } = await request(app)
.get("/inventory")
.expect(200)
.expect("Content-Type", /json/);
expect(body).toEqual({ eggs: 3, "apple pie": 1 });
});
});
describe("add inventory items", () => {
test("adding a new item", async () => {
const { body } = await request(app)
.post("/inventory/eggs")
.send({ quantity: 3 })
.expect(200)
.expect("Content-Type", /json/);
expect(body).toEqual({ itemName: "eggs", quantity: 3 });
expect(
await db
.select("itemName", "quantity")
.from("inventory")
.where("itemName", "eggs")
.first()
).toEqual({ itemName: "eggs", quantity: 3 });
});
test("adding an existing item", async () => {
const eggs = { itemName: "eggs", quantity: 2 };
await db("inventory").insert(eggs);
const { body } = await request(app)
.post("/inventory/eggs")
.send({ quantity: 3 })
.expect(200)
.expect("Content-Type", /json/);
expect(body).toEqual({ itemName: "eggs", quantity: 5 });
expect(
await db
.select("itemName", "quantity")
.from("inventory")
.where("itemName", "eggs")
.first()
).toEqual({ itemName: "eggs", quantity: 5 });
});
});
describe("remove inventory items", () => {
beforeEach(async () => {
await db("inventory").insert({ itemName: "eggs", quantity: 3 });
});
test("removing an item", async () => {
const { body } = await request(app)
.del("/inventory/eggs")
.send({ quantity: 2 })
.expect(200)
.expect("Content-Type", /json/);
expect(body).toEqual({
message: "Removed 2 units of eggs"
});
expect(
await db
.select("itemName", "quantity")
.from("inventory")
.where("itemName", "eggs")
.first()
).toEqual({ itemName: "eggs", quantity: 1 });
});
test("removing more than the inventory quantity", async () => {
const { body } = await request(app)
.del("/inventory/eggs")
.send({ quantity: 4 })
.expect(404)
.expect("Content-Type", /json/);
expect(body).toEqual({
message: "There aren't 4 units of eggs available."
});
expect(
await db
.select("itemName", "quantity")
.from("inventory")
.where("itemName", "eggs")
.first()
).toEqual({ itemName: "eggs", quantity: 3 });
});
});
describe("fetch inventory items", () => {
const eggs = { itemName: "eggs", quantity: 3 };
const applePie = { itemName: "apple pie", quantity: 1 };
beforeEach(async () => {
await db("inventory").insert([eggs, applePie]);
const { id: eggsId } = await db
.select()
.from("inventory")
.where({ itemName: "eggs" })
.first();
eggs.id = eggsId;
});
test("fetching an item from the inventory", async () => {
const eggsResponse = {
title: "FakeAPI",
href: "example.org",
results: [{ name: "Omelette du Fromage" }]
};
nock("http://recipepuppy.com")
.get("/api")
.query({ i: "eggs" })
.reply(200, eggsResponse);
const response = await request(app)
.get(`/inventory/eggs`)
.expect(200)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
...eggs,
info: `Data obtained from ${eggsResponse.title} - ${eggsResponse.href}`,
recipes: eggsResponse.results
});
});
});
================================================
FILE: chapter7/server/truncateTables.js
================================================
const { db } = require("./dbConnection");
const tablesToTruncate = ["users", "inventory", "carts_items"];
beforeEach(() => {
return Promise.all(tablesToTruncate.map(t => db(t).truncate()));
});
================================================
FILE: chapter7/server/userTestUtils.js
================================================
const { db } = require("./dbConnection");
const { hashPassword } = require("./authenticationController");
const username = "test_user";
const password = "a_password";
const passwordHash = hashPassword(password);
const email = "test_user@example.org";
const validAuth = Buffer.from(`${username}:${password}`).toString("base64");
const authHeader = `Basic ${validAuth}`;
const user = {
username,
password,
email,
authHeader
};
const createUser = async () => {
await db("users").insert({ username, email, passwordHash });
const { id } = await db
.select()
.from("users")
.where({ username })
.first();
user.id = id;
};
module.exports = { user, createUser };
================================================
FILE: chapter8/1_testing_component_interaction/1_component_integration_tests/App.jsx
================================================
import React, { useEffect, useState, useRef } from "react";
import { API_ADDR } from "./constants";
import { ItemForm } from "./ItemForm.jsx";
import { ItemList } from "./ItemList.jsx";
export const App = () => {
const [items, setItems] = useState({});
const isMounted = useRef(null);
useEffect(() => {
isMounted.current = true;
const loadItems = async () => {
const response = await fetch(`${API_ADDR}/inventory`);
const responseBody = await response.json();
if (isMounted.current) setItems(responseBody);
};
loadItems();
return () => (isMounted.current = false);
}, []);
const updateItems = (itemAdded, addedQuantity) => {
const currentQuantity = items[itemAdded] || 0;
setItems({ ...items, [itemAdded]: currentQuantity + addedQuantity });
};
return (
Inventory Contents
);
};
================================================
FILE: chapter8/1_testing_component_interaction/1_component_integration_tests/App.test.jsx
================================================
import React from "react";
import nock from "nock";
import { API_ADDR } from "./constants";
import { App } from "./App.jsx";
import { generateItemText } from "./ItemList.jsx";
import { render, fireEvent, waitFor } from "@testing-library/react";
beforeEach(() => {
nock(API_ADDR)
.get("/inventory")
.reply(200, { cheesecake: 2, croissant: 5, macaroon: 96 });
});
afterEach(() => {
if (!nock.isDone()) {
nock.cleanAll();
throw new Error("Not all mocked endpoints received requests.");
}
});
test("renders the appropriate header", () => {
const { getByText } = render( );
expect(getByText("Inventory Contents")).toBeInTheDocument();
});
test("rendering the server's list of items", async () => {
const { getByText } = render( );
await waitFor(() => {
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
});
expect(getByText(generateItemText("cheesecake", 2))).toBeInTheDocument();
expect(getByText(generateItemText("croissant", 5))).toBeInTheDocument();
expect(getByText(generateItemText("macaroon", 96))).toBeInTheDocument();
});
test("updating the list of items with new items", async () => {
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 6 }))
.reply(200);
const { getByText, getByPlaceholderText } = render( );
await waitFor(() => {
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
});
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "6" }
});
fireEvent.click(getByText("Add item"));
await waitFor(() => {
expect(getByText(generateItemText("cheesecake", 8))).toBeInTheDocument();
});
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
expect(getByText(generateItemText("croissant", 5))).toBeInTheDocument();
expect(getByText(generateItemText("macaroon", 96))).toBeInTheDocument();
});
================================================
FILE: chapter8/1_testing_component_interaction/1_component_integration_tests/ItemForm.jsx
================================================
import React from "react";
import { API_ADDR } from "./constants";
const addItemRequest = (itemName, quantity) => {
fetch(`${API_ADDR}/inventory/${itemName}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ quantity })
});
};
export const ItemForm = ({ onItemAdded }) => {
const [itemName, setItemName] = React.useState("");
const [quantity, setQuantity] = React.useState(0);
const onSubmit = async e => {
e.preventDefault();
await addItemRequest(itemName, quantity);
if (onItemAdded) onItemAdded(itemName, quantity);
};
return (
);
};
================================================
FILE: chapter8/1_testing_component_interaction/1_component_integration_tests/ItemForm.test.jsx
================================================
import React from "react";
import nock from "nock";
import { API_ADDR } from "./constants";
import { ItemForm } from "./ItemForm.jsx";
import { render, fireEvent, waitFor } from "@testing-library/react";
test("form's elements", () => {
const { getByText, getByPlaceholderText } = render( );
expect(getByPlaceholderText("Item name")).toBeInTheDocument();
expect(getByPlaceholderText("Quantity")).toBeInTheDocument();
expect(getByText("Add item")).toBeInTheDocument();
});
test("sending requests", () => {
const { getByText, getByPlaceholderText } = render( );
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 2 }))
.reply(200);
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "2" }
});
fireEvent.click(getByText("Add item"));
expect(nock.isDone()).toBe(true);
});
test("invoking the onItemAdded callback", async () => {
const onItemAdded = jest.fn();
const { getByText, getByPlaceholderText } = render(
);
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 2 }))
.reply(200);
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "2" }
});
fireEvent.click(getByText("Add item"));
await waitFor(() => expect(nock.isDone()).toBe(true));
expect(onItemAdded).toHaveBeenCalledTimes(1);
expect(onItemAdded).toHaveBeenCalledWith("cheesecake", 2);
});
================================================
FILE: chapter8/1_testing_component_interaction/1_component_integration_tests/ItemList.jsx
================================================
import React from "react";
export const generateItemText = (itemName, quantity) => {
const capitalizedItemName =
itemName.charAt(0).toUpperCase() + itemName.slice(1);
return `${capitalizedItemName} - Quantity: ${quantity}`;
};
export const ItemList = ({ itemList }) => {
return (
{Object.entries(itemList).map(([itemName, quantity]) => {
return {generateItemText(itemName, quantity)} ;
})}
);
};
================================================
FILE: chapter8/1_testing_component_interaction/1_component_integration_tests/ItemList.test.jsx
================================================
import React from "react";
import { ItemList, generateItemText } from "./ItemList.jsx";
import { render } from "@testing-library/react";
describe("generateItemText", () => {
test("generating an item's text", () => {
expect(generateItemText("cheesecake", 3)).toBe("Cheesecake - Quantity: 3");
expect(generateItemText("apple pie", 22)).toBe("Apple pie - Quantity: 22");
});
});
describe("ItemList Component", () => {
test("list items", () => {
const itemList = { cheesecake: 2, croissant: 5, macaroon: 96 };
const { getByText } = render( );
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
expect(getByText(generateItemText("cheesecake", 2))).toBeInTheDocument();
expect(getByText(generateItemText("croissant", 5))).toBeInTheDocument();
expect(getByText(generateItemText("macaroon", 96))).toBeInTheDocument();
});
});
================================================
FILE: chapter8/1_testing_component_interaction/1_component_integration_tests/babel.config.js
================================================
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: {
node: "current"
}
}
],
"@babel/preset-react"
]
};
================================================
FILE: chapter8/1_testing_component_interaction/1_component_integration_tests/constants.js
================================================
export const API_ADDR = "http://localhost:3000";
================================================
FILE: chapter8/1_testing_component_interaction/1_component_integration_tests/index.html
================================================
Inventory
================================================
FILE: chapter8/1_testing_component_interaction/1_component_integration_tests/index.jsx
================================================
import ReactDOM from "react-dom";
import React from "react";
import { App } from "./App.jsx";
ReactDOM.render( , document.getElementById("app"));
================================================
FILE: chapter8/1_testing_component_interaction/1_component_integration_tests/jest.config.js
================================================
module.exports = {
setupFilesAfterEnv: [
"/setupJestDom.js",
"/setupGlobalFetch.js"
]
};
================================================
FILE: chapter8/1_testing_component_interaction/1_component_integration_tests/package.json
================================================
{
"name": "1_component_integration_tests",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "browserify index.jsx -o bundle.js",
"start": "http-server ./",
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.9.6",
"@babel/preset-env": "^7.9.6",
"@babel/preset-react": "^7.9.4",
"@testing-library/dom": "^7.10.1",
"@testing-library/jest-dom": "^5.9.0",
"@testing-library/react": "^10.2.1",
"babelify": "^10.0.0",
"browserify": "^16.5.1",
"core-js": "^2.6.11",
"http-server": "^0.12.3",
"isomorphic-fetch": "^2.2.1",
"jest": "^25.5",
"nock": "^12.0.3"
},
"dependencies": {
"react": "^16.13.1",
"react-dom": "^16.13.1"
},
"browserify": {
"transform": [
[
"babelify",
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 2
}
],
"@babel/preset-react"
]
}
]
]
}
}
================================================
FILE: chapter8/1_testing_component_interaction/1_component_integration_tests/setupGlobalFetch.js
================================================
const fetch = require("isomorphic-fetch");
global.window.fetch = fetch;
================================================
FILE: chapter8/1_testing_component_interaction/1_component_integration_tests/setupJestDom.js
================================================
const jestDom = require("@testing-library/jest-dom");
expect.extend(jestDom);
================================================
FILE: chapter8/1_testing_component_interaction/2_stubbing_components/App.jsx
================================================
import React, { useEffect, useState, useRef } from "react";
import { API_ADDR } from "./constants";
import { ItemForm } from "./ItemForm.jsx";
import { ItemList } from "./ItemList.jsx";
export const App = () => {
const [items, setItems] = useState({});
const isMounted = useRef(null);
useEffect(() => {
isMounted.current = true;
const loadItems = async () => {
const response = await fetch(`${API_ADDR}/inventory`);
const responseBody = await response.json();
if (isMounted.current) setItems(responseBody);
};
loadItems();
return () => (isMounted.current = false);
}, []);
const updateItems = (itemAdded, addedQuantity) => {
const currentQuantity = items[itemAdded] || 0;
setItems({ ...items, [itemAdded]: currentQuantity + addedQuantity });
};
return (
Inventory Contents
);
};
================================================
FILE: chapter8/1_testing_component_interaction/2_stubbing_components/App.test.jsx
================================================
import React from "react";
import nock from "nock";
import { API_ADDR } from "./constants";
import { App } from "./App.jsx";
import { generateItemText } from "./ItemList.jsx";
import { render, fireEvent, waitFor } from "@testing-library/react";
jest.mock("react-spring/renderprops");
beforeEach(() => {
nock(API_ADDR)
.get("/inventory")
.reply(200, { cheesecake: 2, croissant: 5, macaroon: 96 });
});
afterEach(() => {
if (!nock.isDone()) {
nock.cleanAll();
throw new Error("Not all mocked endpoints received requests.");
}
});
test("renders the appropriate header", () => {
const { getByText } = render( );
expect(getByText("Inventory Contents")).toBeInTheDocument();
});
test("rendering the server's list of items", async () => {
const { getByText } = render( );
await waitFor(() => {
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
});
expect(getByText(generateItemText("cheesecake", 2))).toBeInTheDocument();
expect(getByText(generateItemText("croissant", 5))).toBeInTheDocument();
expect(getByText(generateItemText("macaroon", 96))).toBeInTheDocument();
});
test("updating the list of items with new items", async () => {
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 6 }))
.reply(200);
const { getByText, getByPlaceholderText } = render( );
await waitFor(() => {
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
});
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "6" }
});
fireEvent.click(getByText("Add item"));
await waitFor(() => {
expect(getByText(generateItemText("cheesecake", 8))).toBeInTheDocument();
});
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
expect(getByText(generateItemText("croissant", 5))).toBeInTheDocument();
expect(getByText(generateItemText("macaroon", 96))).toBeInTheDocument();
});
================================================
FILE: chapter8/1_testing_component_interaction/2_stubbing_components/ItemForm.jsx
================================================
import React from "react";
import { API_ADDR } from "./constants";
const addItemRequest = (itemName, quantity) => {
fetch(`${API_ADDR}/inventory/${itemName}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ quantity })
});
};
export const ItemForm = ({ onItemAdded }) => {
const [itemName, setItemName] = React.useState("");
const [quantity, setQuantity] = React.useState(0);
const onSubmit = async e => {
e.preventDefault();
await addItemRequest(itemName, quantity);
if (onItemAdded) onItemAdded(itemName, quantity);
};
return (
);
};
================================================
FILE: chapter8/1_testing_component_interaction/2_stubbing_components/ItemForm.test.jsx
================================================
import React from "react";
import nock from "nock";
import { API_ADDR } from "./constants";
import { ItemForm } from "./ItemForm.jsx";
import { render, fireEvent, waitFor } from "@testing-library/react";
test("form's elements", () => {
const { getByText, getByPlaceholderText } = render( );
expect(getByPlaceholderText("Item name")).toBeInTheDocument();
expect(getByPlaceholderText("Quantity")).toBeInTheDocument();
expect(getByText("Add item")).toBeInTheDocument();
});
test("sending requests", () => {
const { getByText, getByPlaceholderText } = render( );
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 2 }))
.reply(200);
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "2" }
});
fireEvent.click(getByText("Add item"));
expect(nock.isDone()).toBe(true);
});
test("invoking the onItemAdded callback", async () => {
const onItemAdded = jest.fn();
const { getByText, getByPlaceholderText } = render(
);
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 2 }))
.reply(200);
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "2" }
});
fireEvent.click(getByText("Add item"));
await waitFor(() => expect(nock.isDone()).toBe(true));
expect(onItemAdded).toHaveBeenCalledTimes(1);
expect(onItemAdded).toHaveBeenCalledWith("cheesecake", 2);
});
================================================
FILE: chapter8/1_testing_component_interaction/2_stubbing_components/ItemList.jsx
================================================
import React from "react";
import { Transition } from "react-spring/renderprops";
export const generateItemText = (itemName, quantity) => {
const capitalizedItemName =
itemName.charAt(0).toUpperCase() + itemName.slice(1);
return `${capitalizedItemName} - Quantity: ${quantity}`;
};
export const ItemList = ({ itemList }) => {
const items = Object.entries(itemList);
return (
itemName}
from={{ fontSize: 0, opacity: 0 }}
enter={{ fontSize: 18, opacity: 1 }}
>
{([itemName, quantity]) => styleProps => (
{generateItemText(itemName, quantity)}
)}
);
};
================================================
FILE: chapter8/1_testing_component_interaction/2_stubbing_components/ItemList.test.jsx
================================================
import React from "react";
import { ItemList, generateItemText } from "./ItemList.jsx";
import { render } from "@testing-library/react";
jest.mock("react-spring/renderprops");
describe("generateItemText", () => {
test("generating an item's text", () => {
expect(generateItemText("cheesecake", 3)).toBe("Cheesecake - Quantity: 3");
expect(generateItemText("apple pie", 22)).toBe("Apple pie - Quantity: 22");
});
});
describe("ItemList Component", () => {
test("list items", () => {
const itemList = { cheesecake: 2, croissant: 5, macaroon: 96 };
const { getByText } = render( );
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
expect(getByText(generateItemText("cheesecake", 2))).toBeInTheDocument();
expect(getByText(generateItemText("croissant", 5))).toBeInTheDocument();
expect(getByText(generateItemText("macaroon", 96))).toBeInTheDocument();
});
});
================================================
FILE: chapter8/1_testing_component_interaction/2_stubbing_components/__mocks__/react-spring/renderprops.jsx
================================================
const FakeReactSpringTransition = jest.fn(({ items, children }) => {
return items.map(item => {
return children(item)({ fakeStyles: "fake " });
});
});
export { FakeReactSpringTransition as Transition };
================================================
FILE: chapter8/1_testing_component_interaction/2_stubbing_components/babel.config.js
================================================
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: {
node: "current"
}
}
],
"@babel/preset-react"
]
};
================================================
FILE: chapter8/1_testing_component_interaction/2_stubbing_components/constants.js
================================================
export const API_ADDR = "http://localhost:3000";
================================================
FILE: chapter8/1_testing_component_interaction/2_stubbing_components/index.html
================================================
Inventory
================================================
FILE: chapter8/1_testing_component_interaction/2_stubbing_components/index.jsx
================================================
import ReactDOM from "react-dom";
import React from "react";
import { App } from "./App.jsx";
ReactDOM.render( , document.getElementById("app"));
================================================
FILE: chapter8/1_testing_component_interaction/2_stubbing_components/jest.config.js
================================================
module.exports = {
setupFilesAfterEnv: [
"/setupJestDom.js",
"/setupGlobalFetch.js"
]
};
================================================
FILE: chapter8/1_testing_component_interaction/2_stubbing_components/package.json
================================================
{
"name": "2_stubbing_components",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "browserify index.jsx -p esmify -o bundle.js",
"start": "http-server ./",
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.9.6",
"@babel/preset-env": "^7.9.6",
"@babel/preset-react": "^7.9.4",
"@testing-library/dom": "^7.10.1",
"@testing-library/jest-dom": "^5.9.0",
"@testing-library/react": "^10.2.1",
"babelify": "^10.0.0",
"browserify": "^16.5.1",
"core-js": "^2.6.11",
"esmify": "^2.1.1",
"http-server": "^0.12.3",
"isomorphic-fetch": "^2.2.1",
"jest": "^25.5",
"nock": "^12.0.3"
},
"dependencies": {
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-spring": "^8.0.27"
},
"browserify": {
"transform": [
[
"babelify",
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 2
}
],
"@babel/preset-react"
]
}
]
]
}
}
================================================
FILE: chapter8/1_testing_component_interaction/2_stubbing_components/setupGlobalFetch.js
================================================
const fetch = require("isomorphic-fetch");
global.window.fetch = fetch;
================================================
FILE: chapter8/1_testing_component_interaction/2_stubbing_components/setupJestDom.js
================================================
const jestDom = require("@testing-library/jest-dom");
expect.extend(jestDom);
================================================
FILE: chapter8/2_snapshot_testing/1_component_snapshots/ActionLog.jsx
================================================
import React from "react";
export const ActionLog = ({ actions }) => {
return (
Action Log
{actions.map(({ time, message, data }, i) => {
const date = new Date(time).toUTCString();
return (
Date: {date} - Message: {message} - Data: {JSON.stringify(data)}
);
})}
);
};
================================================
FILE: chapter8/2_snapshot_testing/1_component_snapshots/ActionLog.test.jsx
================================================
import React from "react";
import { ActionLog } from "./ActionLog";
import { render } from "@testing-library/react";
const daysToMs = days => days * 24 * 60 * 60 * 1000;
test("logging actions", () => {
const actions = [
{
time: new Date(daysToMs(1)),
message: "Loaded item list",
data: { cheesecake: 2, macaroon: 5 }
},
{
time: new Date(daysToMs(2)),
message: "Item added",
data: { cheesecake: 2 }
},
{
time: new Date(daysToMs(3)),
message: "Item removed",
data: { cheesecake: 1 }
},
{
time: new Date(daysToMs(4)),
message: "Something weird happened",
data: { error: "The cheesecake is a lie" }
}
];
const { container } = render( );
expect(container).toMatchSnapshot();
});
================================================
FILE: chapter8/2_snapshot_testing/1_component_snapshots/App.jsx
================================================
import React, { useEffect, useState, useRef } from "react";
import { API_ADDR } from "./constants";
import { ItemForm } from "./ItemForm.jsx";
import { ItemList } from "./ItemList.jsx";
import { ActionLog } from "./ActionLog.jsx";
export const App = () => {
const [items, setItems] = useState({});
const [actions, setActions] = useState([]);
const isMounted = useRef(null);
useEffect(() => {
isMounted.current = true;
const loadItems = async () => {
const response = await fetch(`${API_ADDR}/inventory`);
const responseBody = await response.json();
if (isMounted.current) {
setItems(responseBody);
setActions(
actions.concat({
time: new Date().toISOString(),
message: "Loaded items from the server",
data: { status: response.status, body: responseBody }
})
);
}
};
loadItems();
return () => (isMounted.current = false);
}, []);
const updateItems = (itemAdded, addedQuantity) => {
const currentQuantity = items[itemAdded] || 0;
setItems({ ...items, [itemAdded]: currentQuantity + addedQuantity });
setActions(
actions.concat({
time: new Date().toISOString(),
message: "Item added",
data: { itemAdded, addedQuantity }
})
);
};
return (
);
};
================================================
FILE: chapter8/2_snapshot_testing/1_component_snapshots/App.test.jsx
================================================
import React from "react";
import nock from "nock";
import { API_ADDR } from "./constants";
import { App } from "./App.jsx";
import { generateItemText } from "./ItemList.jsx";
import { render, fireEvent, waitFor } from "@testing-library/react";
jest.mock("react-spring/renderprops");
beforeEach(() => {
nock(API_ADDR)
.get("/inventory")
.reply(200, { cheesecake: 2, croissant: 5, macaroon: 96 });
});
afterEach(() => {
if (!nock.isDone()) {
nock.cleanAll();
throw new Error("Not all mocked endpoints received requests.");
}
});
test("renders the appropriate header", () => {
const { getByText } = render( );
expect(getByText("Inventory Contents")).toBeInTheDocument();
});
test("rendering the server's list of items", async () => {
const { getByText } = render( );
await waitFor(() => {
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
});
expect(getByText(generateItemText("cheesecake", 2))).toBeInTheDocument();
expect(getByText(generateItemText("croissant", 5))).toBeInTheDocument();
expect(getByText(generateItemText("macaroon", 96))).toBeInTheDocument();
});
test("updating the list of items with new items", async () => {
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 6 }))
.reply(200);
const { getByText, getByPlaceholderText } = render( );
await waitFor(() => {
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
});
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "6" }
});
fireEvent.click(getByText("Add item"));
await waitFor(() => {
expect(getByText(generateItemText("cheesecake", 8))).toBeInTheDocument();
});
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
expect(getByText(generateItemText("croissant", 5))).toBeInTheDocument();
expect(getByText(generateItemText("macaroon", 96))).toBeInTheDocument();
});
test("updating the action log when loading items", async () => {
jest
.spyOn(Date.prototype, "toISOString")
.mockReturnValue("2020-06-20T13:37:00.000Z");
const { getByTestId } = render( );
await waitFor(() => {
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
});
const actionLog = getByTestId("action-log");
expect(actionLog).toMatchSnapshot();
});
test("updating the action log adding an item", async () => {
jest
.spyOn(Date.prototype, "toISOString")
.mockReturnValueOnce("2020-06-20T13:37:00.000Z");
jest
.spyOn(Date.prototype, "toISOString")
.mockReturnValueOnce("2020-06-21T13:37:00.000Z");
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 6 }))
.reply(200);
const { getByTestId, getByText, getByPlaceholderText } = render( );
await waitFor(() => {
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
});
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "6" }
});
fireEvent.click(getByText("Add item"));
await waitFor(() => {
expect(getByText(generateItemText("cheesecake", 8))).toBeInTheDocument();
});
const actionLog = getByTestId("action-log");
expect(actionLog).toMatchSnapshot();
});
================================================
FILE: chapter8/2_snapshot_testing/1_component_snapshots/ItemForm.jsx
================================================
import React from "react";
import { API_ADDR } from "./constants";
const addItemRequest = (itemName, quantity) => {
fetch(`${API_ADDR}/inventory/${itemName}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ quantity })
});
};
export const ItemForm = ({ onItemAdded }) => {
const [itemName, setItemName] = React.useState("");
const [quantity, setQuantity] = React.useState(0);
const onSubmit = async e => {
e.preventDefault();
await addItemRequest(itemName, quantity);
if (onItemAdded) onItemAdded(itemName, quantity);
};
return (
);
};
================================================
FILE: chapter8/2_snapshot_testing/1_component_snapshots/ItemForm.test.jsx
================================================
import React from "react";
import nock from "nock";
import { API_ADDR } from "./constants";
import { ItemForm } from "./ItemForm.jsx";
import { render, fireEvent, waitFor } from "@testing-library/react";
test("form's elements", () => {
const { getByText, getByPlaceholderText } = render( );
expect(getByPlaceholderText("Item name")).toBeInTheDocument();
expect(getByPlaceholderText("Quantity")).toBeInTheDocument();
expect(getByText("Add item")).toBeInTheDocument();
});
test("sending requests", () => {
const { getByText, getByPlaceholderText } = render( );
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 2 }))
.reply(200);
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "2" }
});
fireEvent.click(getByText("Add item"));
expect(nock.isDone()).toBe(true);
});
test("invoking the onItemAdded callback", async () => {
const onItemAdded = jest.fn();
const { getByText, getByPlaceholderText } = render(
);
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 2 }))
.reply(200);
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "2" }
});
fireEvent.click(getByText("Add item"));
await waitFor(() => expect(nock.isDone()).toBe(true));
expect(onItemAdded).toHaveBeenCalledTimes(1);
expect(onItemAdded).toHaveBeenCalledWith("cheesecake", 2);
});
================================================
FILE: chapter8/2_snapshot_testing/1_component_snapshots/ItemList.jsx
================================================
import React from "react";
import { Transition } from "react-spring/renderprops";
export const generateItemText = (itemName, quantity) => {
const capitalizedItemName =
itemName.charAt(0).toUpperCase() + itemName.slice(1);
return `${capitalizedItemName} - Quantity: ${quantity}`;
};
export const ItemList = ({ itemList }) => {
const items = Object.entries(itemList);
return (
itemName}
from={{ fontSize: 0, opacity: 0 }}
enter={{ fontSize: 18, opacity: 1 }}
leave={{ fontSize: 0, opacity: 0 }}
>
{([itemName, quantity]) => styleProps => (
{generateItemText(itemName, quantity)}
)}
);
};
================================================
FILE: chapter8/2_snapshot_testing/1_component_snapshots/ItemList.test.jsx
================================================
import React from "react";
import { ItemList, generateItemText } from "./ItemList.jsx";
import { render } from "@testing-library/react";
jest.mock("react-spring/renderprops");
describe("generateItemText", () => {
test("generating an item's text", () => {
expect(generateItemText("cheesecake", 3)).toBe("Cheesecake - Quantity: 3");
expect(generateItemText("apple pie", 22)).toBe("Apple pie - Quantity: 22");
});
});
describe("ItemList Component", () => {
test("list items", () => {
const itemList = { cheesecake: 2, croissant: 5, macaroon: 96 };
const { getByText } = render( );
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
expect(getByText(generateItemText("cheesecake", 2))).toBeInTheDocument();
expect(getByText(generateItemText("croissant", 5))).toBeInTheDocument();
expect(getByText(generateItemText("macaroon", 96))).toBeInTheDocument();
});
});
================================================
FILE: chapter8/2_snapshot_testing/1_component_snapshots/__mocks__/react-spring/renderprops.jsx
================================================
const FakeReactSpringTransition = jest.fn(({ items, children }) => {
return items.map(item => {
return children(item)({ fakeStyles: "fake " });
});
});
export { FakeReactSpringTransition as Transition };
================================================
FILE: chapter8/2_snapshot_testing/1_component_snapshots/__snapshots__/ActionLog.test.jsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`logging actions 1`] = `
Action Log
Date:
Fri, 02 Jan 1970 00:00:00 GMT
- Message:
Loaded item list
- Data:
{"cheesecake":2,"macaroon":5}
Date:
Sat, 03 Jan 1970 00:00:00 GMT
- Message:
Item added
- Data:
{"cheesecake":2}
Date:
Sun, 04 Jan 1970 00:00:00 GMT
- Message:
Item removed
- Data:
{"cheesecake":1}
Date:
Mon, 05 Jan 1970 00:00:00 GMT
- Message:
Something weird happened
- Data:
{"error":"The cheesecake is a lie"}
`;
================================================
FILE: chapter8/2_snapshot_testing/1_component_snapshots/__snapshots__/App.test.jsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`updating the action log adding an item 1`] = `
Action Log
Date:
Sat, 20 Jun 2020 13:37:00 GMT
- Message:
Loaded items from the server
- Data:
{"status":200,"body":{"cheesecake":2,"croissant":5,"macaroon":96}}
Date:
Sun, 21 Jun 2020 13:37:00 GMT
- Message:
Item added
- Data:
{"itemAdded":"cheesecake","addedQuantity":6}
`;
exports[`updating the action log when loading items 1`] = `
Action Log
Date:
Sat, 20 Jun 2020 13:37:00 GMT
- Message:
Loaded items from the server
- Data:
{"status":200,"body":{"cheesecake":2,"croissant":5,"macaroon":96}}
`;
================================================
FILE: chapter8/2_snapshot_testing/1_component_snapshots/babel.config.js
================================================
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: {
node: "current"
}
}
],
"@babel/preset-react"
]
};
================================================
FILE: chapter8/2_snapshot_testing/1_component_snapshots/constants.js
================================================
export const API_ADDR = "http://localhost:3000";
================================================
FILE: chapter8/2_snapshot_testing/1_component_snapshots/index.html
================================================
Inventory
================================================
FILE: chapter8/2_snapshot_testing/1_component_snapshots/index.jsx
================================================
import ReactDOM from "react-dom";
import React from "react";
import { App } from "./App.jsx";
ReactDOM.render( , document.getElementById("app"));
================================================
FILE: chapter8/2_snapshot_testing/1_component_snapshots/jest.config.js
================================================
module.exports = {
setupFilesAfterEnv: [
"/setupJestDom.js",
"/setupGlobalFetch.js"
]
};
================================================
FILE: chapter8/2_snapshot_testing/1_component_snapshots/package.json
================================================
{
"name": "1_component_snapshots",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "browserify index.jsx -p esmify -o bundle.js",
"start": "http-server ./",
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.9.6",
"@babel/preset-env": "^7.9.6",
"@babel/preset-react": "^7.9.4",
"@testing-library/dom": "^7.10.1",
"@testing-library/jest-dom": "^5.9.0",
"@testing-library/react": "^10.2.1",
"babelify": "^10.0.0",
"browserify": "^16.5.1",
"core-js": "^2.6.11",
"esmify": "^2.1.1",
"http-server": "^0.12.3",
"isomorphic-fetch": "^2.2.1",
"jest": "^25.5",
"nock": "^12.0.3"
},
"dependencies": {
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-spring": "^8.0.27"
},
"browserify": {
"transform": [
[
"babelify",
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 2
}
],
"@babel/preset-react"
]
}
]
]
}
}
================================================
FILE: chapter8/2_snapshot_testing/1_component_snapshots/setupGlobalFetch.js
================================================
const fetch = require("isomorphic-fetch");
global.window.fetch = fetch;
================================================
FILE: chapter8/2_snapshot_testing/1_component_snapshots/setupJestDom.js
================================================
const jestDom = require("@testing-library/jest-dom");
expect.extend(jestDom);
================================================
FILE: chapter8/2_snapshot_testing/2_snapshots_beyond_components/__snapshots__/generate_report.test.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`generating a .txt report 1`] = `
"cheesecake - Quantity: 8 - Value: 176
carrot cake - Quantity: 3 - Value: 54
macaroon - Quantity: 40 - Value: 240
chocolate cake - Quantity: 12 - Value: 204
Total value: 63"
`;
================================================
FILE: chapter8/2_snapshot_testing/2_snapshots_beyond_components/generate_report.js
================================================
const fs = require("fs");
const inventory = [
{ item: "cheesecake", quantity: 8, price: 22 },
{ item: "carrot cake", quantity: 3, price: 18 },
{ item: "macaroon", quantity: 40, price: 6 },
{ item: "chocolate cake", quantity: 12, price: 17 }
];
module.exports.generateReport = items => {
const lines = items.map(({ item, quantity, price }) => {
return `${item} - Quantity: ${quantity} - Value: ${price * quantity}`;
});
const totalValue = items.reduce((sum, { price }) => {
return sum + price;
}, 0);
const content = lines.concat(`Total value: ${totalValue}`).join("\n");
fs.writeFileSync("/tmp/report.txt", content);
};
module.exports.generateReport(inventory);
================================================
FILE: chapter8/2_snapshot_testing/2_snapshots_beyond_components/generate_report.test.js
================================================
const fs = require("fs");
const { generateReport } = require("./generate_report");
test("generating a .txt report", () => {
const inventory = [
{ item: "cheesecake", quantity: 8, price: 22 },
{ item: "carrot cake", quantity: 3, price: 18 },
{ item: "macaroon", quantity: 40, price: 6 },
{ item: "chocolate cake", quantity: 12, price: 17 }
];
generateReport(inventory);
const report = fs.readFileSync("/tmp/report.txt", "utf-8");
expect(report).toMatchSnapshot();
});
================================================
FILE: chapter8/2_snapshot_testing/2_snapshots_beyond_components/package.json
================================================
{
"name": "2_snapshots_beyond_components",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^25.5"
},
"dependencies": {}
}
================================================
FILE: chapter8/3_testing_styles/1_css_classes/ActionLog.jsx
================================================
import React from "react";
export const ActionLog = ({ actions }) => {
return (
Action Log
{actions.map(({ time, message, data }, i) => {
const date = new Date(time).toUTCString();
return (
Date: {date} - Message: {message} - Data: {JSON.stringify(data)}
);
})}
);
};
================================================
FILE: chapter8/3_testing_styles/1_css_classes/ActionLog.test.jsx
================================================
import React from "react";
import { ActionLog } from "./ActionLog";
import { render } from "@testing-library/react";
const daysToMs = days => days * 24 * 60 * 60 * 1000;
test("logging actions", () => {
const actions = [
{
time: new Date(daysToMs(1)),
message: "Loaded item list",
data: { cheesecake: 2, macaroon: 5 }
},
{
time: new Date(daysToMs(2)),
message: "Item added",
data: { cheesecake: 2 }
},
{
time: new Date(daysToMs(3)),
message: "Item removed",
data: { cheesecake: 1 }
},
{
time: new Date(daysToMs(4)),
message: "Something weird happened",
data: { error: "The cheesecake is a lie" }
}
];
const { container } = render( );
expect(container).toMatchSnapshot();
});
================================================
FILE: chapter8/3_testing_styles/1_css_classes/App.jsx
================================================
import React, { useEffect, useState, useRef } from "react";
import { API_ADDR } from "./constants";
import { ItemForm } from "./ItemForm.jsx";
import { ItemList } from "./ItemList.jsx";
import { ActionLog } from "./ActionLog.jsx";
export const App = () => {
const [items, setItems] = useState({});
const [actions, setActions] = useState([]);
const isMounted = useRef(null);
useEffect(() => {
isMounted.current = true;
const loadItems = async () => {
const response = await fetch(`${API_ADDR}/inventory`);
const responseBody = await response.json();
if (isMounted.current) {
setItems(responseBody);
setActions(
actions.concat({
time: new Date().toISOString(),
message: "Loaded items from the server",
data: { status: response.status, body: responseBody }
})
);
}
};
loadItems();
return () => (isMounted.current = false);
}, []);
const updateItems = (itemAdded, addedQuantity) => {
const currentQuantity = items[itemAdded] || 0;
setItems({ ...items, [itemAdded]: currentQuantity + addedQuantity });
setActions(
actions.concat({
time: new Date().toISOString(),
message: "Item added",
data: { itemAdded, addedQuantity }
})
);
};
return (
);
};
================================================
FILE: chapter8/3_testing_styles/1_css_classes/App.test.jsx
================================================
import React from "react";
import nock from "nock";
import { API_ADDR } from "./constants";
import { App } from "./App.jsx";
import { generateItemText } from "./ItemList.jsx";
import { render, fireEvent, waitFor } from "@testing-library/react";
jest.mock("react-spring/renderprops");
beforeEach(() => {
nock(API_ADDR)
.get("/inventory")
.reply(200, { cheesecake: 2, croissant: 5, macaroon: 96 });
});
afterEach(() => {
if (!nock.isDone()) {
nock.cleanAll();
throw new Error("Not all mocked endpoints received requests.");
}
});
test("renders the appropriate header", () => {
const { getByText } = render( );
expect(getByText("Inventory Contents")).toBeInTheDocument();
});
test("rendering the server's list of items", async () => {
const { getByText } = render( );
await waitFor(() => {
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
});
expect(getByText(generateItemText("cheesecake", 2))).toBeInTheDocument();
expect(getByText(generateItemText("croissant", 5))).toBeInTheDocument();
expect(getByText(generateItemText("macaroon", 96))).toBeInTheDocument();
});
test("updating the list of items with new items", async () => {
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 6 }))
.reply(200);
const { getByText, getByPlaceholderText } = render( );
await waitFor(() => {
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
});
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "6" }
});
fireEvent.click(getByText("Add item"));
await waitFor(() => {
expect(getByText(generateItemText("cheesecake", 8))).toBeInTheDocument();
});
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
expect(getByText(generateItemText("croissant", 5))).toBeInTheDocument();
expect(getByText(generateItemText("macaroon", 96))).toBeInTheDocument();
});
test("updating the action log when loading items", async () => {
jest
.spyOn(Date.prototype, "toISOString")
.mockReturnValue("2020-06-20T13:37:00.000Z");
const { getByTestId } = render( );
await waitFor(() => {
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
});
const actionLog = getByTestId("action-log");
expect(actionLog).toMatchSnapshot();
});
test("updating the action log adding an item", async () => {
jest
.spyOn(Date.prototype, "toISOString")
.mockReturnValueOnce("2020-06-20T13:37:00.000Z");
jest
.spyOn(Date.prototype, "toISOString")
.mockReturnValueOnce("2020-06-21T13:37:00.000Z");
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 6 }))
.reply(200);
const { getByTestId, getByText, getByPlaceholderText } = render( );
await waitFor(() => {
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
});
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "6" }
});
fireEvent.click(getByText("Add item"));
await waitFor(() => {
expect(getByText(generateItemText("cheesecake", 8))).toBeInTheDocument();
});
const actionLog = getByTestId("action-log");
expect(actionLog).toMatchSnapshot();
});
================================================
FILE: chapter8/3_testing_styles/1_css_classes/ItemForm.jsx
================================================
import React from "react";
import { API_ADDR } from "./constants";
const addItemRequest = (itemName, quantity) => {
fetch(`${API_ADDR}/inventory/${itemName}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ quantity })
});
};
export const ItemForm = ({ onItemAdded }) => {
const [itemName, setItemName] = React.useState("");
const [quantity, setQuantity] = React.useState(0);
const onSubmit = async e => {
e.preventDefault();
await addItemRequest(itemName, quantity);
if (onItemAdded) onItemAdded(itemName, quantity);
};
return (
);
};
================================================
FILE: chapter8/3_testing_styles/1_css_classes/ItemForm.test.jsx
================================================
import React from "react";
import nock from "nock";
import { API_ADDR } from "./constants";
import { ItemForm } from "./ItemForm.jsx";
import { render, fireEvent, waitFor } from "@testing-library/react";
test("form's elements", () => {
const { getByText, getByPlaceholderText } = render( );
expect(getByPlaceholderText("Item name")).toBeInTheDocument();
expect(getByPlaceholderText("Quantity")).toBeInTheDocument();
expect(getByText("Add item")).toBeInTheDocument();
});
test("sending requests", () => {
const { getByText, getByPlaceholderText } = render( );
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 2 }))
.reply(200);
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "2" }
});
fireEvent.click(getByText("Add item"));
expect(nock.isDone()).toBe(true);
});
test("invoking the onItemAdded callback", async () => {
const onItemAdded = jest.fn();
const { getByText, getByPlaceholderText } = render(
);
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 2 }))
.reply(200);
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "2" }
});
fireEvent.click(getByText("Add item"));
await waitFor(() => expect(nock.isDone()).toBe(true));
expect(onItemAdded).toHaveBeenCalledTimes(1);
expect(onItemAdded).toHaveBeenCalledWith("cheesecake", 2);
});
================================================
FILE: chapter8/3_testing_styles/1_css_classes/ItemList.jsx
================================================
import React from "react";
import { Transition } from "react-spring/renderprops";
export const generateItemText = (itemName, quantity) => {
const capitalizedItemName =
itemName.charAt(0).toUpperCase() + itemName.slice(1);
return `${capitalizedItemName} - Quantity: ${quantity}`;
};
export const ItemList = ({ itemList }) => {
const items = Object.entries(itemList);
return (
itemName}
from={{ fontSize: 0, opacity: 0 }}
enter={{ fontSize: 18, opacity: 1 }}
leave={{ fontSize: 0, opacity: 0 }}
>
{([itemName, quantity]) => styleProps => (
{generateItemText(itemName, quantity)}
)}
);
};
================================================
FILE: chapter8/3_testing_styles/1_css_classes/ItemList.test.jsx
================================================
import React from "react";
import { ItemList, generateItemText } from "./ItemList.jsx";
import { render } from "@testing-library/react";
jest.mock("react-spring/renderprops");
describe("generateItemText", () => {
test("generating an item's text", () => {
expect(generateItemText("cheesecake", 3)).toBe("Cheesecake - Quantity: 3");
expect(generateItemText("apple pie", 22)).toBe("Apple pie - Quantity: 22");
});
});
describe("ItemList Component", () => {
test("list items", () => {
const itemList = { cheesecake: 2, croissant: 5, macaroon: 96 };
const { getByText } = render( );
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
expect(getByText(generateItemText("cheesecake", 2))).toBeInTheDocument();
expect(getByText(generateItemText("croissant", 5))).toBeInTheDocument();
expect(getByText(generateItemText("macaroon", 96))).toBeInTheDocument();
});
test("highlighting items that are almost out of stock", () => {
const itemList = { cheesecake: 2, croissant: 5, macaroon: 96 };
const { getByText } = render( );
const cheesecakeItem = getByText(generateItemText("cheesecake", 2));
expect(cheesecakeItem).toHaveClass("almost-out-of-stock");
});
});
================================================
FILE: chapter8/3_testing_styles/1_css_classes/__mocks__/react-spring/renderprops.jsx
================================================
const FakeReactSpringTransition = jest.fn(({ items, children }) => {
return items.map(item => {
return children(item)({ fakeStyles: "fake " });
});
});
export { FakeReactSpringTransition as Transition };
================================================
FILE: chapter8/3_testing_styles/1_css_classes/__snapshots__/ActionLog.test.jsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`logging actions 1`] = `
Action Log
Date:
Fri, 02 Jan 1970 00:00:00 GMT
- Message:
Loaded item list
- Data:
{"cheesecake":2,"macaroon":5}
Date:
Sat, 03 Jan 1970 00:00:00 GMT
- Message:
Item added
- Data:
{"cheesecake":2}
Date:
Sun, 04 Jan 1970 00:00:00 GMT
- Message:
Item removed
- Data:
{"cheesecake":1}
Date:
Mon, 05 Jan 1970 00:00:00 GMT
- Message:
Something weird happened
- Data:
{"error":"The cheesecake is a lie"}
`;
================================================
FILE: chapter8/3_testing_styles/1_css_classes/__snapshots__/App.test.jsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`updating the action log adding an item 1`] = `
Action Log
Date:
Sat, 20 Jun 2020 13:37:00 GMT
- Message:
Loaded items from the server
- Data:
{"status":200,"body":{"cheesecake":2,"croissant":5,"macaroon":96}}
Date:
Sun, 21 Jun 2020 13:37:00 GMT
- Message:
Item added
- Data:
{"itemAdded":"cheesecake","addedQuantity":6}
`;
exports[`updating the action log when loading items 1`] = `
Action Log
Date:
Sat, 20 Jun 2020 13:37:00 GMT
- Message:
Loaded items from the server
- Data:
{"status":200,"body":{"cheesecake":2,"croissant":5,"macaroon":96}}
`;
================================================
FILE: chapter8/3_testing_styles/1_css_classes/babel.config.js
================================================
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: {
node: "current"
}
}
],
"@babel/preset-react"
]
};
================================================
FILE: chapter8/3_testing_styles/1_css_classes/constants.js
================================================
export const API_ADDR = "http://localhost:3000";
================================================
FILE: chapter8/3_testing_styles/1_css_classes/index.html
================================================
Inventory
================================================
FILE: chapter8/3_testing_styles/1_css_classes/index.jsx
================================================
import ReactDOM from "react-dom";
import React from "react";
import { App } from "./App.jsx";
ReactDOM.render( , document.getElementById("app"));
================================================
FILE: chapter8/3_testing_styles/1_css_classes/jest.config.js
================================================
module.exports = {
setupFilesAfterEnv: [
"/setupJestDom.js",
"/setupGlobalFetch.js"
]
};
================================================
FILE: chapter8/3_testing_styles/1_css_classes/package.json
================================================
{
"name": "1_testing_styles",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "browserify index.jsx -p esmify -o bundle.js",
"start": "http-server ./",
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.9.6",
"@babel/preset-env": "^7.9.6",
"@babel/preset-react": "^7.9.4",
"@testing-library/dom": "^7.10.1",
"@testing-library/jest-dom": "^5.9.0",
"@testing-library/react": "^10.2.1",
"babelify": "^10.0.0",
"browserify": "^16.5.1",
"core-js": "^2.6.11",
"esmify": "^2.1.1",
"http-server": "^0.12.3",
"isomorphic-fetch": "^2.2.1",
"jest": "^25.5",
"nock": "^12.0.3"
},
"dependencies": {
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-spring": "^8.0.27"
},
"browserify": {
"transform": [
[
"babelify",
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 2
}
],
"@babel/preset-react"
]
}
]
]
}
}
================================================
FILE: chapter8/3_testing_styles/1_css_classes/setupGlobalFetch.js
================================================
const fetch = require("isomorphic-fetch");
global.window.fetch = fetch;
================================================
FILE: chapter8/3_testing_styles/1_css_classes/setupJestDom.js
================================================
const jestDom = require("@testing-library/jest-dom");
expect.extend(jestDom);
================================================
FILE: chapter8/3_testing_styles/1_css_classes/styles.css
================================================
.almost-out-of-stock {
font-weight: bold;
color: red;
}
================================================
FILE: chapter8/3_testing_styles/2_style_props/ActionLog.jsx
================================================
import React from "react";
export const ActionLog = ({ actions }) => {
return (
Action Log
{actions.map(({ time, message, data }, i) => {
const date = new Date(time).toUTCString();
return (
Date: {date} - Message: {message} - Data: {JSON.stringify(data)}
);
})}
);
};
================================================
FILE: chapter8/3_testing_styles/2_style_props/ActionLog.test.jsx
================================================
import React from "react";
import { ActionLog } from "./ActionLog";
import { render } from "@testing-library/react";
const daysToMs = days => days * 24 * 60 * 60 * 1000;
test("logging actions", () => {
const actions = [
{
time: new Date(daysToMs(1)),
message: "Loaded item list",
data: { cheesecake: 2, macaroon: 5 }
},
{
time: new Date(daysToMs(2)),
message: "Item added",
data: { cheesecake: 2 }
},
{
time: new Date(daysToMs(3)),
message: "Item removed",
data: { cheesecake: 1 }
},
{
time: new Date(daysToMs(4)),
message: "Something weird happened",
data: { error: "The cheesecake is a lie" }
}
];
const { container } = render( );
expect(container).toMatchSnapshot();
});
================================================
FILE: chapter8/3_testing_styles/2_style_props/App.jsx
================================================
import React, { useEffect, useState, useRef } from "react";
import { API_ADDR } from "./constants";
import { ItemForm } from "./ItemForm.jsx";
import { ItemList } from "./ItemList.jsx";
import { ActionLog } from "./ActionLog.jsx";
export const App = () => {
const [items, setItems] = useState({});
const [actions, setActions] = useState([]);
const isMounted = useRef(null);
useEffect(() => {
isMounted.current = true;
const loadItems = async () => {
const response = await fetch(`${API_ADDR}/inventory`);
const responseBody = await response.json();
if (isMounted.current) {
setItems(responseBody);
setActions(
actions.concat({
time: new Date().toISOString(),
message: "Loaded items from the server",
data: { status: response.status, body: responseBody }
})
);
}
};
loadItems();
return () => (isMounted.current = false);
}, []);
const updateItems = (itemAdded, addedQuantity) => {
const currentQuantity = items[itemAdded] || 0;
setItems({ ...items, [itemAdded]: currentQuantity + addedQuantity });
setActions(
actions.concat({
time: new Date().toISOString(),
message: "Item added",
data: { itemAdded, addedQuantity }
})
);
};
return (
);
};
================================================
FILE: chapter8/3_testing_styles/2_style_props/App.test.jsx
================================================
import React from "react";
import nock from "nock";
import { API_ADDR } from "./constants";
import { App } from "./App.jsx";
import { generateItemText } from "./ItemList.jsx";
import { render, fireEvent, waitFor } from "@testing-library/react";
jest.mock("react-spring/renderprops");
beforeEach(() => {
nock(API_ADDR)
.get("/inventory")
.reply(200, { cheesecake: 2, croissant: 5, macaroon: 96 });
});
afterEach(() => {
if (!nock.isDone()) {
nock.cleanAll();
throw new Error("Not all mocked endpoints received requests.");
}
});
test("renders the appropriate header", () => {
const { getByText } = render( );
expect(getByText("Inventory Contents")).toBeInTheDocument();
});
test("rendering the server's list of items", async () => {
const { getByText } = render( );
await waitFor(() => {
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
});
expect(getByText(generateItemText("cheesecake", 2))).toBeInTheDocument();
expect(getByText(generateItemText("croissant", 5))).toBeInTheDocument();
expect(getByText(generateItemText("macaroon", 96))).toBeInTheDocument();
});
test("updating the list of items with new items", async () => {
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 6 }))
.reply(200);
const { getByText, getByPlaceholderText } = render( );
await waitFor(() => {
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
});
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "6" }
});
fireEvent.click(getByText("Add item"));
await waitFor(() => {
expect(getByText(generateItemText("cheesecake", 8))).toBeInTheDocument();
});
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
expect(getByText(generateItemText("croissant", 5))).toBeInTheDocument();
expect(getByText(generateItemText("macaroon", 96))).toBeInTheDocument();
});
test("updating the action log when loading items", async () => {
jest
.spyOn(Date.prototype, "toISOString")
.mockReturnValue("2020-06-20T13:37:00.000Z");
const { getByTestId } = render( );
await waitFor(() => {
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
});
const actionLog = getByTestId("action-log");
expect(actionLog).toMatchSnapshot();
});
test("updating the action log adding an item", async () => {
jest
.spyOn(Date.prototype, "toISOString")
.mockReturnValueOnce("2020-06-20T13:37:00.000Z");
jest
.spyOn(Date.prototype, "toISOString")
.mockReturnValueOnce("2020-06-21T13:37:00.000Z");
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 6 }))
.reply(200);
const { getByTestId, getByText, getByPlaceholderText } = render( );
await waitFor(() => {
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
});
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "6" }
});
fireEvent.click(getByText("Add item"));
await waitFor(() => {
expect(getByText(generateItemText("cheesecake", 8))).toBeInTheDocument();
});
const actionLog = getByTestId("action-log");
expect(actionLog).toMatchSnapshot();
});
================================================
FILE: chapter8/3_testing_styles/2_style_props/ItemForm.jsx
================================================
import React from "react";
import { API_ADDR } from "./constants";
const addItemRequest = (itemName, quantity) => {
fetch(`${API_ADDR}/inventory/${itemName}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ quantity })
});
};
export const ItemForm = ({ onItemAdded }) => {
const [itemName, setItemName] = React.useState("");
const [quantity, setQuantity] = React.useState(0);
const onSubmit = async e => {
e.preventDefault();
await addItemRequest(itemName, quantity);
if (onItemAdded) onItemAdded(itemName, quantity);
};
return (
);
};
================================================
FILE: chapter8/3_testing_styles/2_style_props/ItemForm.test.jsx
================================================
import React from "react";
import nock from "nock";
import { API_ADDR } from "./constants";
import { ItemForm } from "./ItemForm.jsx";
import { render, fireEvent, waitFor } from "@testing-library/react";
test("form's elements", () => {
const { getByText, getByPlaceholderText } = render( );
expect(getByPlaceholderText("Item name")).toBeInTheDocument();
expect(getByPlaceholderText("Quantity")).toBeInTheDocument();
expect(getByText("Add item")).toBeInTheDocument();
});
test("sending requests", () => {
const { getByText, getByPlaceholderText } = render( );
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 2 }))
.reply(200);
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "2" }
});
fireEvent.click(getByText("Add item"));
expect(nock.isDone()).toBe(true);
});
test("invoking the onItemAdded callback", async () => {
const onItemAdded = jest.fn();
const { getByText, getByPlaceholderText } = render(
);
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 2 }))
.reply(200);
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "2" }
});
fireEvent.click(getByText("Add item"));
await waitFor(() => expect(nock.isDone()).toBe(true));
expect(onItemAdded).toHaveBeenCalledTimes(1);
expect(onItemAdded).toHaveBeenCalledWith("cheesecake", 2);
});
================================================
FILE: chapter8/3_testing_styles/2_style_props/ItemList.jsx
================================================
import React from "react";
import { Transition } from "react-spring/renderprops";
export const generateItemText = (itemName, quantity) => {
const capitalizedItemName =
itemName.charAt(0).toUpperCase() + itemName.slice(1);
return `${capitalizedItemName} - Quantity: ${quantity}`;
};
const almostOutOfStock = {
fontWeight: "bold",
color: "red"
};
export const ItemList = ({ itemList }) => {
const items = Object.entries(itemList);
return (
itemName}
from={{ fontSize: 0, opacity: 0 }}
enter={{ fontSize: 18, opacity: 1 }}
leave={{ fontSize: 0, opacity: 0 }}
>
{([itemName, quantity]) => styleProps => (
{generateItemText(itemName, quantity)}
)}
);
};
================================================
FILE: chapter8/3_testing_styles/2_style_props/ItemList.test.jsx
================================================
import React from "react";
import { ItemList, generateItemText } from "./ItemList.jsx";
import { render } from "@testing-library/react";
jest.mock("react-spring/renderprops");
describe("generateItemText", () => {
test("generating an item's text", () => {
expect(generateItemText("cheesecake", 3)).toBe("Cheesecake - Quantity: 3");
expect(generateItemText("apple pie", 22)).toBe("Apple pie - Quantity: 22");
});
});
describe("ItemList Component", () => {
test("list items", () => {
const itemList = { cheesecake: 2, croissant: 5, macaroon: 96 };
const { getByText } = render( );
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
expect(getByText(generateItemText("cheesecake", 2))).toBeInTheDocument();
expect(getByText(generateItemText("croissant", 5))).toBeInTheDocument();
expect(getByText(generateItemText("macaroon", 96))).toBeInTheDocument();
});
test("highlighting items that are almost out of stock", () => {
const itemList = { cheesecake: 2, croissant: 5, macaroon: 96 };
const { getByText } = render( );
const cheesecakeItem = getByText(generateItemText("cheesecake", 2));
expect(cheesecakeItem).toHaveClass("almost-out-of-stock");
});
});
================================================
FILE: chapter8/3_testing_styles/2_style_props/__mocks__/react-spring/renderprops.jsx
================================================
const FakeReactSpringTransition = jest.fn(({ items, children }) => {
return items.map(item => {
return children(item)({ fakeStyles: "fake " });
});
});
export { FakeReactSpringTransition as Transition };
================================================
FILE: chapter8/3_testing_styles/2_style_props/__snapshots__/ActionLog.test.jsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`logging actions 1`] = `
Action Log
Date:
Fri, 02 Jan 1970 00:00:00 GMT
- Message:
Loaded item list
- Data:
{"cheesecake":2,"macaroon":5}
Date:
Sat, 03 Jan 1970 00:00:00 GMT
- Message:
Item added
- Data:
{"cheesecake":2}
Date:
Sun, 04 Jan 1970 00:00:00 GMT
- Message:
Item removed
- Data:
{"cheesecake":1}
Date:
Mon, 05 Jan 1970 00:00:00 GMT
- Message:
Something weird happened
- Data:
{"error":"The cheesecake is a lie"}
`;
================================================
FILE: chapter8/3_testing_styles/2_style_props/__snapshots__/App.test.jsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`updating the action log adding an item 1`] = `
Action Log
Date:
Sat, 20 Jun 2020 13:37:00 GMT
- Message:
Loaded items from the server
- Data:
{"status":200,"body":{"cheesecake":2,"croissant":5,"macaroon":96}}
Date:
Sun, 21 Jun 2020 13:37:00 GMT
- Message:
Item added
- Data:
{"itemAdded":"cheesecake","addedQuantity":6}
`;
exports[`updating the action log when loading items 1`] = `
Action Log
Date:
Sat, 20 Jun 2020 13:37:00 GMT
- Message:
Loaded items from the server
- Data:
{"status":200,"body":{"cheesecake":2,"croissant":5,"macaroon":96}}
`;
================================================
FILE: chapter8/3_testing_styles/2_style_props/babel.config.js
================================================
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: {
node: "current"
}
}
],
"@babel/preset-react"
]
};
================================================
FILE: chapter8/3_testing_styles/2_style_props/constants.js
================================================
export const API_ADDR = "http://localhost:3000";
================================================
FILE: chapter8/3_testing_styles/2_style_props/index.html
================================================
Inventory
================================================
FILE: chapter8/3_testing_styles/2_style_props/index.jsx
================================================
import ReactDOM from "react-dom";
import React from "react";
import { App } from "./App.jsx";
ReactDOM.render( , document.getElementById("app"));
================================================
FILE: chapter8/3_testing_styles/2_style_props/jest.config.js
================================================
module.exports = {
setupFilesAfterEnv: [
"/setupJestDom.js",
"/setupGlobalFetch.js"
]
};
================================================
FILE: chapter8/3_testing_styles/2_style_props/package.json
================================================
{
"name": "1_testing_styles",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "browserify index.jsx -p esmify -o bundle.js",
"start": "http-server ./",
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.9.6",
"@babel/preset-env": "^7.9.6",
"@babel/preset-react": "^7.9.4",
"@testing-library/dom": "^7.10.1",
"@testing-library/jest-dom": "^5.9.0",
"@testing-library/react": "^10.2.1",
"babelify": "^10.0.0",
"browserify": "^16.5.1",
"core-js": "^2.6.11",
"esmify": "^2.1.1",
"http-server": "^0.12.3",
"isomorphic-fetch": "^2.2.1",
"jest": "^25.5",
"nock": "^12.0.3"
},
"dependencies": {
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-spring": "^8.0.27"
},
"browserify": {
"transform": [
[
"babelify",
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 2
}
],
"@babel/preset-react"
]
}
]
]
}
}
================================================
FILE: chapter8/3_testing_styles/2_style_props/setupGlobalFetch.js
================================================
const fetch = require("isomorphic-fetch");
global.window.fetch = fetch;
================================================
FILE: chapter8/3_testing_styles/2_style_props/setupJestDom.js
================================================
const jestDom = require("@testing-library/jest-dom");
expect.extend(jestDom);
================================================
FILE: chapter8/3_testing_styles/2_style_props/styles.css
================================================
.almost-out-of-stock {
font-weight: bold;
color: red;
}
================================================
FILE: chapter8/3_testing_styles/3_css_in_js_snapshots/ActionLog.jsx
================================================
import React from "react";
export const ActionLog = ({ actions }) => {
return (
Action Log
{actions.map(({ time, message, data }, i) => {
const date = new Date(time).toUTCString();
return (
Date: {date} - Message: {message} - Data: {JSON.stringify(data)}
);
})}
);
};
================================================
FILE: chapter8/3_testing_styles/3_css_in_js_snapshots/ActionLog.test.jsx
================================================
import React from "react";
import { ActionLog } from "./ActionLog";
import { render } from "@testing-library/react";
const daysToMs = days => days * 24 * 60 * 60 * 1000;
test("logging actions", () => {
const actions = [
{
time: new Date(daysToMs(1)),
message: "Loaded item list",
data: { cheesecake: 2, macaroon: 5 }
},
{
time: new Date(daysToMs(2)),
message: "Item added",
data: { cheesecake: 2 }
},
{
time: new Date(daysToMs(3)),
message: "Item removed",
data: { cheesecake: 1 }
},
{
time: new Date(daysToMs(4)),
message: "Something weird happened",
data: { error: "The cheesecake is a lie" }
}
];
const { container } = render( );
expect(container).toMatchSnapshot();
});
================================================
FILE: chapter8/3_testing_styles/3_css_in_js_snapshots/App.jsx
================================================
import React, { useEffect, useState, useRef } from "react";
import { API_ADDR } from "./constants";
import { ItemForm } from "./ItemForm.jsx";
import { ItemList } from "./ItemList.jsx";
import { ActionLog } from "./ActionLog.jsx";
export const App = () => {
const [items, setItems] = useState({});
const [actions, setActions] = useState([]);
const isMounted = useRef(null);
useEffect(() => {
isMounted.current = true;
const loadItems = async () => {
const response = await fetch(`${API_ADDR}/inventory`);
const responseBody = await response.json();
if (isMounted.current) {
setItems(responseBody);
setActions(
actions.concat({
time: new Date().toISOString(),
message: "Loaded items from the server",
data: { status: response.status, body: responseBody }
})
);
}
};
loadItems();
return () => (isMounted.current = false);
}, []);
const updateItems = (itemAdded, addedQuantity) => {
const currentQuantity = items[itemAdded] || 0;
setItems({ ...items, [itemAdded]: currentQuantity + addedQuantity });
setActions(
actions.concat({
time: new Date().toISOString(),
message: "Item added",
data: { itemAdded, addedQuantity }
})
);
};
return (
);
};
================================================
FILE: chapter8/3_testing_styles/3_css_in_js_snapshots/App.test.jsx
================================================
import React from "react";
import nock from "nock";
import { API_ADDR } from "./constants";
import { App } from "./App.jsx";
import { generateItemText } from "./ItemList.jsx";
import { render, fireEvent, waitFor } from "@testing-library/react";
jest.mock("react-spring/renderprops");
beforeEach(() => {
nock(API_ADDR)
.get("/inventory")
.reply(200, { cheesecake: 2, croissant: 5, macaroon: 96 });
});
afterEach(() => {
if (!nock.isDone()) {
nock.cleanAll();
throw new Error("Not all mocked endpoints received requests.");
}
});
test("renders the appropriate header", () => {
const { getByText } = render( );
expect(getByText("Inventory Contents")).toBeInTheDocument();
});
test("rendering the server's list of items", async () => {
const { getByText } = render( );
await waitFor(() => {
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
});
expect(getByText(generateItemText("cheesecake", 2))).toBeInTheDocument();
expect(getByText(generateItemText("croissant", 5))).toBeInTheDocument();
expect(getByText(generateItemText("macaroon", 96))).toBeInTheDocument();
});
test("updating the list of items with new items", async () => {
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 6 }))
.reply(200);
const { getByText, getByPlaceholderText } = render( );
await waitFor(() => {
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
});
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "6" }
});
fireEvent.click(getByText("Add item"));
await waitFor(() => {
expect(getByText(generateItemText("cheesecake", 8))).toBeInTheDocument();
});
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
expect(getByText(generateItemText("croissant", 5))).toBeInTheDocument();
expect(getByText(generateItemText("macaroon", 96))).toBeInTheDocument();
});
test("updating the action log when loading items", async () => {
jest
.spyOn(Date.prototype, "toISOString")
.mockReturnValue("2020-06-20T13:37:00.000Z");
const { getByTestId } = render( );
await waitFor(() => {
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
});
const actionLog = getByTestId("action-log");
expect(actionLog).toMatchSnapshot();
});
test("updating the action log adding an item", async () => {
jest
.spyOn(Date.prototype, "toISOString")
.mockReturnValueOnce("2020-06-20T13:37:00.000Z");
jest
.spyOn(Date.prototype, "toISOString")
.mockReturnValueOnce("2020-06-21T13:37:00.000Z");
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 6 }))
.reply(200);
const { getByTestId, getByText, getByPlaceholderText } = render( );
await waitFor(() => {
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
});
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "6" }
});
fireEvent.click(getByText("Add item"));
await waitFor(() => {
expect(getByText(generateItemText("cheesecake", 8))).toBeInTheDocument();
});
const actionLog = getByTestId("action-log");
expect(actionLog).toMatchSnapshot();
});
================================================
FILE: chapter8/3_testing_styles/3_css_in_js_snapshots/ItemForm.jsx
================================================
import React from "react";
import { API_ADDR } from "./constants";
const addItemRequest = (itemName, quantity) => {
fetch(`${API_ADDR}/inventory/${itemName}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ quantity })
});
};
export const ItemForm = ({ onItemAdded }) => {
const [itemName, setItemName] = React.useState("");
const [quantity, setQuantity] = React.useState(0);
const onSubmit = async e => {
e.preventDefault();
await addItemRequest(itemName, quantity);
if (onItemAdded) onItemAdded(itemName, quantity);
};
return (
);
};
================================================
FILE: chapter8/3_testing_styles/3_css_in_js_snapshots/ItemForm.test.jsx
================================================
import React from "react";
import nock from "nock";
import { API_ADDR } from "./constants";
import { ItemForm } from "./ItemForm.jsx";
import { render, fireEvent, waitFor } from "@testing-library/react";
test("form's elements", () => {
const { getByText, getByPlaceholderText } = render( );
expect(getByPlaceholderText("Item name")).toBeInTheDocument();
expect(getByPlaceholderText("Quantity")).toBeInTheDocument();
expect(getByText("Add item")).toBeInTheDocument();
});
test("sending requests", () => {
const { getByText, getByPlaceholderText } = render( );
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 2 }))
.reply(200);
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "2" }
});
fireEvent.click(getByText("Add item"));
expect(nock.isDone()).toBe(true);
});
test("invoking the onItemAdded callback", async () => {
const onItemAdded = jest.fn();
const { getByText, getByPlaceholderText } = render(
);
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 2 }))
.reply(200);
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "2" }
});
fireEvent.click(getByText("Add item"));
await waitFor(() => expect(nock.isDone()).toBe(true));
expect(onItemAdded).toHaveBeenCalledTimes(1);
expect(onItemAdded).toHaveBeenCalledWith("cheesecake", 2);
});
================================================
FILE: chapter8/3_testing_styles/3_css_in_js_snapshots/ItemList.jsx
================================================
/* @jsx jsx */
import { Transition } from "react-spring/renderprops";
import { css, keyframes, jsx } from "@emotion/core";
export const generateItemText = (itemName, quantity) => {
const capitalizedItemName =
itemName.charAt(0).toUpperCase() + itemName.slice(1);
return `${capitalizedItemName} - Quantity: ${quantity}`;
};
const pulsate = keyframes`
0% { opacity: .3; }
50% { opacity: 1; }
100% { opacity: .3; }
`;
const almostOutOfStock = css`
font-weight: bold;
color: red;
animation: ${pulsate} 2s infinite;
`;
export const ItemList = ({ itemList }) => {
const items = Object.entries(itemList);
return (
itemName}
from={{ fontSize: 0, opacity: 0 }}
enter={{ fontSize: 18, opacity: 1 }}
leave={{ fontSize: 0, opacity: 0 }}
>
{([itemName, quantity]) => styleProps => (
{generateItemText(itemName, quantity)}
)}
);
};
================================================
FILE: chapter8/3_testing_styles/3_css_in_js_snapshots/ItemList.test.jsx
================================================
import React from "react";
import { ItemList, generateItemText } from "./ItemList.jsx";
import { render } from "@testing-library/react";
jest.mock("react-spring/renderprops");
describe("generateItemText", () => {
test("generating an item's text", () => {
expect(generateItemText("cheesecake", 3)).toBe("Cheesecake - Quantity: 3");
expect(generateItemText("apple pie", 22)).toBe("Apple pie - Quantity: 22");
});
});
describe("ItemList Component", () => {
test("list items", () => {
const itemList = { cheesecake: 2, croissant: 5, macaroon: 96 };
const { getByText } = render( );
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
expect(getByText(generateItemText("cheesecake", 2))).toBeInTheDocument();
expect(getByText(generateItemText("croissant", 5))).toBeInTheDocument();
expect(getByText(generateItemText("macaroon", 96))).toBeInTheDocument();
});
test("highlighting items that are almost out of stock", () => {
const itemList = { cheesecake: 2, croissant: 5, macaroon: 96 };
const { getByText } = render( );
const cheesecakeItem = getByText(generateItemText("cheesecake", 2));
expect(cheesecakeItem).toHaveStyle({ color: "red" });
});
});
================================================
FILE: chapter8/3_testing_styles/3_css_in_js_snapshots/__mocks__/react-spring/renderprops.jsx
================================================
const FakeReactSpringTransition = jest.fn(({ items, children }) => {
return items.map(item => {
return children(item)({ fakeStyles: "fake " });
});
});
export { FakeReactSpringTransition as Transition };
================================================
FILE: chapter8/3_testing_styles/3_css_in_js_snapshots/__snapshots__/ActionLog.test.jsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`logging actions 1`] = `
Action Log
Date:
Fri, 02 Jan 1970 00:00:00 GMT
- Message:
Loaded item list
- Data:
{"cheesecake":2,"macaroon":5}
Date:
Sat, 03 Jan 1970 00:00:00 GMT
- Message:
Item added
- Data:
{"cheesecake":2}
Date:
Sun, 04 Jan 1970 00:00:00 GMT
- Message:
Item removed
- Data:
{"cheesecake":1}
Date:
Mon, 05 Jan 1970 00:00:00 GMT
- Message:
Something weird happened
- Data:
{"error":"The cheesecake is a lie"}
`;
================================================
FILE: chapter8/3_testing_styles/3_css_in_js_snapshots/__snapshots__/App.test.jsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`updating the action log adding an item 1`] = `
Action Log
Date:
Sat, 20 Jun 2020 13:37:00 GMT
- Message:
Loaded items from the server
- Data:
{"status":200,"body":{"cheesecake":2,"croissant":5,"macaroon":96}}
Date:
Sun, 21 Jun 2020 13:37:00 GMT
- Message:
Item added
- Data:
{"itemAdded":"cheesecake","addedQuantity":6}
`;
exports[`updating the action log when loading items 1`] = `
Action Log
Date:
Sat, 20 Jun 2020 13:37:00 GMT
- Message:
Loaded items from the server
- Data:
{"status":200,"body":{"cheesecake":2,"croissant":5,"macaroon":96}}
`;
================================================
FILE: chapter8/3_testing_styles/3_css_in_js_snapshots/__snapshots__/ItemList.test.jsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ItemList Component highlighting items that are almost out of stock 1`] = `
@keyframes animation-0 {
0% {
opacity: .3;
}
50% {
opacity: 1;
}
100% {
opacity: .3;
}
}
.emotion-0 {
font-weight: bold;
color: red;
-webkit-animation: animation-0 2s infinite;
animation: animation-0 2s infinite;
}
Cheesecake - Quantity: 2
`;
================================================
FILE: chapter8/3_testing_styles/3_css_in_js_snapshots/babel.config.js
================================================
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: {
node: "current"
}
}
],
"@babel/preset-react"
]
};
================================================
FILE: chapter8/3_testing_styles/3_css_in_js_snapshots/constants.js
================================================
export const API_ADDR = "http://localhost:3000";
================================================
FILE: chapter8/3_testing_styles/3_css_in_js_snapshots/index.html
================================================
Inventory
================================================
FILE: chapter8/3_testing_styles/3_css_in_js_snapshots/index.jsx
================================================
import ReactDOM from "react-dom";
import React from "react";
import { App } from "./App.jsx";
ReactDOM.render( , document.getElementById("app"));
================================================
FILE: chapter8/3_testing_styles/3_css_in_js_snapshots/jest.config.js
================================================
module.exports = {
snapshotSerializers: ["jest-emotion"],
setupFilesAfterEnv: [
"/setupJestDom.js",
"/setupJestEmotion.js",
"/setupGlobalFetch.js"
]
};
================================================
FILE: chapter8/3_testing_styles/3_css_in_js_snapshots/package.json
================================================
{
"name": "1_testing_styles",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "browserify index.jsx -p esmify -o bundle.js",
"start": "http-server ./",
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.9.6",
"@babel/preset-env": "^7.9.6",
"@babel/preset-react": "^7.9.4",
"@testing-library/dom": "^7.10.1",
"@testing-library/jest-dom": "^5.9.0",
"@testing-library/react": "^10.2.1",
"babelify": "^10.0.0",
"browserify": "^16.5.1",
"core-js": "^2.6.11",
"esmify": "^2.1.1",
"http-server": "^0.12.3",
"isomorphic-fetch": "^2.2.1",
"jest": "^25.5",
"jest-emotion": "^10.0.32",
"nock": "^12.0.3"
},
"dependencies": {
"@emotion/core": "^10.0.28",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-spring": "^8.0.27"
},
"browserify": {
"transform": [
[
"babelify",
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 2
}
],
"@babel/preset-react"
]
}
]
]
}
}
================================================
FILE: chapter8/3_testing_styles/3_css_in_js_snapshots/setupGlobalFetch.js
================================================
const fetch = require("isomorphic-fetch");
global.window.fetch = fetch;
================================================
FILE: chapter8/3_testing_styles/3_css_in_js_snapshots/setupJestDom.js
================================================
const jestDom = require("@testing-library/jest-dom");
expect.extend(jestDom);
================================================
FILE: chapter8/3_testing_styles/3_css_in_js_snapshots/setupJestEmotion.js
================================================
const { matchers } = require("jest-emotion");
expect.extend(matchers);
================================================
FILE: chapter8/3_testing_styles/3_css_in_js_snapshots/styles.css
================================================
.almost-out-of-stock {
font-weight: bold;
color: red;
}
================================================
FILE: chapter8/4_component_stories/1_stories/.storybook/main.js
================================================
module.exports = {
stories: ["../**/*.stories.jsx"],
addons: [
"@storybook/addon-actions/register",
"@storybook/addon-knobs/register"
],
webpackFinal: async config => {
return {
...config,
resolve: {
...config.resolve,
alias: {
"core-js/modules": "@storybook/core/node_modules/core-js/modules",
"core-js/features": "@storybook/core/node_modules/core-js/features"
}
}
};
}
};
================================================
FILE: chapter8/4_component_stories/1_stories/ActionLog.jsx
================================================
import React from "react";
export const ActionLog = ({ actions }) => {
return (
Action Log
{actions.map(({ time, message, data }, i) => {
const date = new Date(time).toUTCString();
return (
Date: {date} - Message: {message} - Data: {JSON.stringify(data)}
);
})}
);
};
================================================
FILE: chapter8/4_component_stories/1_stories/ActionLog.stories.jsx
================================================
import React from "react";
import { storiesOf } from "@storybook/react";
import { ActionLog } from "./ActionLog";
const actionLogStories = storiesOf("ActionLog", module);
actionLogStories.add("A log of actions", () => {
return (
);
});
================================================
FILE: chapter8/4_component_stories/1_stories/ActionLog.test.jsx
================================================
import React from "react";
import { ActionLog } from "./ActionLog";
import { render } from "@testing-library/react";
const daysToMs = days => days * 24 * 60 * 60 * 1000;
test("logging actions", () => {
const actions = [
{
time: new Date(daysToMs(1)),
message: "Loaded item list",
data: { cheesecake: 2, macaroon: 5 }
},
{
time: new Date(daysToMs(2)),
message: "Item added",
data: { cheesecake: 2 }
},
{
time: new Date(daysToMs(3)),
message: "Item removed",
data: { cheesecake: 1 }
},
{
time: new Date(daysToMs(4)),
message: "Something weird happened",
data: { error: "The cheesecake is a lie" }
}
];
const { container } = render( );
expect(container).toMatchSnapshot();
});
================================================
FILE: chapter8/4_component_stories/1_stories/App.jsx
================================================
import React, { useEffect, useState, useRef } from "react";
import { API_ADDR } from "./constants";
import { ItemForm } from "./ItemForm.jsx";
import { ItemList } from "./ItemList.jsx";
import { ActionLog } from "./ActionLog.jsx";
export const App = () => {
const [items, setItems] = useState({});
const [actions, setActions] = useState([]);
const isMounted = useRef(null);
useEffect(() => {
isMounted.current = true;
const loadItems = async () => {
const response = await fetch(`${API_ADDR}/inventory`);
const responseBody = await response.json();
if (isMounted.current) {
setItems(responseBody);
setActions(
actions.concat({
time: new Date().toISOString(),
message: "Loaded items from the server",
data: { status: response.status, body: responseBody }
})
);
}
};
loadItems();
return () => (isMounted.current = false);
}, []);
const updateItems = (itemAdded, addedQuantity) => {
const currentQuantity = items[itemAdded] || 0;
setItems({ ...items, [itemAdded]: currentQuantity + addedQuantity });
setActions(
actions.concat({
time: new Date().toISOString(),
message: "Item added",
data: { itemAdded, addedQuantity }
})
);
};
return (
);
};
================================================
FILE: chapter8/4_component_stories/1_stories/App.test.jsx
================================================
import React from "react";
import nock from "nock";
import { API_ADDR } from "./constants";
import { App } from "./App.jsx";
import { generateItemText } from "./ItemList.jsx";
import { render, fireEvent, waitFor } from "@testing-library/react";
jest.mock("react-spring/renderprops");
beforeEach(() => {
nock(API_ADDR)
.get("/inventory")
.reply(200, { cheesecake: 2, croissant: 5, macaroon: 96 });
});
afterEach(() => {
if (!nock.isDone()) {
nock.cleanAll();
throw new Error("Not all mocked endpoints received requests.");
}
});
test("renders the appropriate header", () => {
const { getByText } = render( );
expect(getByText("Inventory Contents")).toBeInTheDocument();
});
test("rendering the server's list of items", async () => {
const { getByText } = render( );
await waitFor(() => {
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
});
expect(getByText(generateItemText("cheesecake", 2))).toBeInTheDocument();
expect(getByText(generateItemText("croissant", 5))).toBeInTheDocument();
expect(getByText(generateItemText("macaroon", 96))).toBeInTheDocument();
});
test("updating the list of items with new items", async () => {
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 6 }))
.reply(200);
const { getByText, getByPlaceholderText } = render( );
await waitFor(() => {
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
});
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "6" }
});
fireEvent.click(getByText("Add item"));
await waitFor(() => {
expect(getByText(generateItemText("cheesecake", 8))).toBeInTheDocument();
});
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
expect(getByText(generateItemText("croissant", 5))).toBeInTheDocument();
expect(getByText(generateItemText("macaroon", 96))).toBeInTheDocument();
});
test("updating the action log when loading items", async () => {
jest
.spyOn(Date.prototype, "toISOString")
.mockReturnValue("2020-06-20T13:37:00.000Z");
const { getByTestId } = render( );
await waitFor(() => {
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
});
const actionLog = getByTestId("action-log");
expect(actionLog).toMatchSnapshot();
});
test("updating the action log adding an item", async () => {
jest
.spyOn(Date.prototype, "toISOString")
.mockReturnValueOnce("2020-06-20T13:37:00.000Z");
jest
.spyOn(Date.prototype, "toISOString")
.mockReturnValueOnce("2020-06-21T13:37:00.000Z");
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 6 }))
.reply(200);
const { getByTestId, getByText, getByPlaceholderText } = render( );
await waitFor(() => {
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
});
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "6" }
});
fireEvent.click(getByText("Add item"));
await waitFor(() => {
expect(getByText(generateItemText("cheesecake", 8))).toBeInTheDocument();
});
const actionLog = getByTestId("action-log");
expect(actionLog).toMatchSnapshot();
});
================================================
FILE: chapter8/4_component_stories/1_stories/ItemForm.jsx
================================================
import React from "react";
import { API_ADDR } from "./constants";
const addItemRequest = (itemName, quantity) => {
fetch(`${API_ADDR}/inventory/${itemName}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ quantity })
});
};
export const ItemForm = ({ onItemAdded }) => {
const [itemName, setItemName] = React.useState("");
const [quantity, setQuantity] = React.useState(0);
const onSubmit = async e => {
e.preventDefault();
await addItemRequest(itemName, quantity);
if (onItemAdded) onItemAdded(itemName, quantity);
};
return (
);
};
================================================
FILE: chapter8/4_component_stories/1_stories/ItemForm.stories.jsx
================================================
import React, { useEffect } from "react";
import fetchMock from "fetch-mock";
import { action } from "@storybook/addon-actions";
import { API_ADDR } from "./constants";
import { ItemForm } from "./ItemForm";
export default {
title: "ItemForm",
component: ItemForm,
includeStories: ["itemForm"]
};
export const itemForm = () => {
const ItemFormStory = () => {
useEffect(() => {
fetchMock.post(`glob:${API_ADDR}/inventory/*`, 200);
return () => fetchMock.restore();
}, []);
return ;
};
return ;
};
================================================
FILE: chapter8/4_component_stories/1_stories/ItemForm.test.jsx
================================================
import React from "react";
import nock from "nock";
import { API_ADDR } from "./constants";
import { ItemForm } from "./ItemForm.jsx";
import { render, fireEvent, waitFor } from "@testing-library/react";
test("form's elements", () => {
const { getByText, getByPlaceholderText } = render( );
expect(getByPlaceholderText("Item name")).toBeInTheDocument();
expect(getByPlaceholderText("Quantity")).toBeInTheDocument();
expect(getByText("Add item")).toBeInTheDocument();
});
test("sending requests", () => {
const { getByText, getByPlaceholderText } = render( );
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 2 }))
.reply(200);
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "2" }
});
fireEvent.click(getByText("Add item"));
expect(nock.isDone()).toBe(true);
});
test("invoking the onItemAdded callback", async () => {
const onItemAdded = jest.fn();
const { getByText, getByPlaceholderText } = render(
);
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 2 }))
.reply(200);
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "2" }
});
fireEvent.click(getByText("Add item"));
await waitFor(() => expect(nock.isDone()).toBe(true));
expect(onItemAdded).toHaveBeenCalledTimes(1);
expect(onItemAdded).toHaveBeenCalledWith("cheesecake", 2);
});
================================================
FILE: chapter8/4_component_stories/1_stories/ItemList.jsx
================================================
/* @jsx jsx */
import { Transition } from "react-spring/renderprops";
import { css, keyframes, jsx } from "@emotion/core";
export const generateItemText = (itemName, quantity) => {
const capitalizedItemName =
itemName.charAt(0).toUpperCase() + itemName.slice(1);
return `${capitalizedItemName} - Quantity: ${quantity}`;
};
const pulsate = keyframes`
0% { opacity: .3; }
50% { opacity: 1; }
100% { opacity: .3; }
`;
const almostOutOfStock = css`
font-weight: bold;
color: red;
animation: ${pulsate} 2s infinite;
`;
export const ItemList = ({ itemList }) => {
const items = Object.entries(itemList);
return (
itemName}
from={{ fontSize: 0, opacity: 0 }}
enter={{ fontSize: 18, opacity: 1 }}
leave={{ fontSize: 0, opacity: 0 }}
>
{([itemName, quantity]) => styleProps => (
{generateItemText(itemName, quantity)}
)}
);
};
================================================
FILE: chapter8/4_component_stories/1_stories/ItemList.stories.jsx
================================================
import React from "react";
import { withKnobs, object } from "@storybook/addon-knobs";
import { ItemList } from "./ItemList";
export default {
title: "ItemList",
component: ItemList,
includeStories: ["staticItemList", "animatedItems"],
decorators: [withKnobs]
};
export const staticItemList = () => (
);
export const animatedItems = () => {
const knobLabel = "Contents";
const knobDefaultValue = { cheesecake: 2, croissant: 5 };
const itemList = object(knobLabel, knobDefaultValue);
return ;
};
================================================
FILE: chapter8/4_component_stories/1_stories/ItemList.test.jsx
================================================
import React from "react";
import { ItemList, generateItemText } from "./ItemList.jsx";
import { render } from "@testing-library/react";
jest.mock("react-spring/renderprops");
describe("generateItemText", () => {
test("generating an item's text", () => {
expect(generateItemText("cheesecake", 3)).toBe("Cheesecake - Quantity: 3");
expect(generateItemText("apple pie", 22)).toBe("Apple pie - Quantity: 22");
});
});
describe("ItemList Component", () => {
test("list items", () => {
const itemList = { cheesecake: 2, croissant: 5, macaroon: 96 };
const { getByText } = render( );
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
expect(getByText(generateItemText("cheesecake", 2))).toBeInTheDocument();
expect(getByText(generateItemText("croissant", 5))).toBeInTheDocument();
expect(getByText(generateItemText("macaroon", 96))).toBeInTheDocument();
});
test("highlighting items that are almost out of stock", () => {
const itemList = { cheesecake: 2, croissant: 5, macaroon: 96 };
const { getByText } = render( );
const cheesecakeItem = getByText(generateItemText("cheesecake", 2));
expect(cheesecakeItem).toMatchSnapshot();
});
});
================================================
FILE: chapter8/4_component_stories/1_stories/__mocks__/react-spring/renderprops.jsx
================================================
const FakeReactSpringTransition = jest.fn(({ items, children }) => {
return items.map(item => {
return children(item)({ fakeStyles: "fake " });
});
});
export { FakeReactSpringTransition as Transition };
================================================
FILE: chapter8/4_component_stories/1_stories/__snapshots__/ActionLog.test.jsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`logging actions 1`] = `
Action Log
Date:
Fri, 02 Jan 1970 00:00:00 GMT
- Message:
Loaded item list
- Data:
{"cheesecake":2,"macaroon":5}
Date:
Sat, 03 Jan 1970 00:00:00 GMT
- Message:
Item added
- Data:
{"cheesecake":2}
Date:
Sun, 04 Jan 1970 00:00:00 GMT
- Message:
Item removed
- Data:
{"cheesecake":1}
Date:
Mon, 05 Jan 1970 00:00:00 GMT
- Message:
Something weird happened
- Data:
{"error":"The cheesecake is a lie"}
`;
================================================
FILE: chapter8/4_component_stories/1_stories/__snapshots__/App.test.jsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`updating the action log adding an item 1`] = `
Action Log
Date:
Sat, 20 Jun 2020 13:37:00 GMT
- Message:
Loaded items from the server
- Data:
{"status":200,"body":{"cheesecake":2,"croissant":5,"macaroon":96}}
Date:
Sun, 21 Jun 2020 13:37:00 GMT
- Message:
Item added
- Data:
{"itemAdded":"cheesecake","addedQuantity":6}
`;
exports[`updating the action log when loading items 1`] = `
Action Log
Date:
Sat, 20 Jun 2020 13:37:00 GMT
- Message:
Loaded items from the server
- Data:
{"status":200,"body":{"cheesecake":2,"croissant":5,"macaroon":96}}
`;
================================================
FILE: chapter8/4_component_stories/1_stories/__snapshots__/ItemList.test.jsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ItemList Component highlighting items that are almost out of stock 1`] = `
@keyframes animation-0 {
0% {
opacity: .3;
}
50% {
opacity: 1;
}
100% {
opacity: .3;
}
}
.emotion-0 {
font-weight: bold;
color: red;
-webkit-animation: animation-0 2s infinite;
animation: animation-0 2s infinite;
}
Cheesecake - Quantity: 2
`;
================================================
FILE: chapter8/4_component_stories/1_stories/babel.config.js
================================================
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: {
node: "current"
}
}
],
"@babel/preset-react"
]
};
================================================
FILE: chapter8/4_component_stories/1_stories/constants.js
================================================
export const API_ADDR = "http://localhost:3000";
================================================
FILE: chapter8/4_component_stories/1_stories/index.html
================================================
Inventory
================================================
FILE: chapter8/4_component_stories/1_stories/index.jsx
================================================
import ReactDOM from "react-dom";
import React from "react";
import { App } from "./App.jsx";
ReactDOM.render( , document.getElementById("app"));
================================================
FILE: chapter8/4_component_stories/1_stories/jest.config.js
================================================
module.exports = {
snapshotSerializers: ["jest-emotion"],
setupFilesAfterEnv: [
"/setupJestDom.js",
"/setupJestEmotion.js",
"/setupGlobalFetch.js"
]
};
================================================
FILE: chapter8/4_component_stories/1_stories/package.json
================================================
{
"name": "1_component_stories",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"storybook": "start-storybook",
"build": "browserify index.jsx -p esmify -o bundle.js",
"start": "http-server ./",
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.9.6",
"@babel/preset-env": "^7.9.6",
"@babel/preset-react": "^7.9.4",
"@storybook/addon-actions": "^6.0.28",
"@storybook/addon-knobs": "^6.0.28",
"@storybook/react": "^6.0.28",
"@testing-library/dom": "^7.10.1",
"@testing-library/jest-dom": "^5.9.0",
"@testing-library/react": "^10.2.1",
"babel-loader": "^8.1.0",
"babelify": "^10.0.0",
"browserify": "^16.5.1",
"core-js": "^2.6.11",
"esmify": "^2.1.1",
"fetch-mock": "^9.10.3",
"http-server": "^0.12.3",
"isomorphic-fetch": "^2.2.1",
"jest": "^25.5",
"jest-emotion": "^10.0.32",
"nock": "^12.0.3"
},
"dependencies": {
"@emotion/core": "^10.0.28",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-spring": "^8.0.27"
},
"browserify": {
"transform": [
[
"babelify",
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 2
}
],
"@babel/preset-react"
]
}
]
]
}
}
================================================
FILE: chapter8/4_component_stories/1_stories/setupGlobalFetch.js
================================================
const fetch = require("isomorphic-fetch");
global.window.fetch = fetch;
================================================
FILE: chapter8/4_component_stories/1_stories/setupJestDom.js
================================================
const jestDom = require("@testing-library/jest-dom");
expect.extend(jestDom);
================================================
FILE: chapter8/4_component_stories/1_stories/setupJestEmotion.js
================================================
const { matchers } = require("jest-emotion");
expect.extend(matchers);
================================================
FILE: chapter8/4_component_stories/1_stories/styles.css
================================================
.almost-out-of-stock {
font-weight: bold;
color: red;
}
================================================
FILE: chapter8/4_component_stories/2_documentation/.storybook/main.js
================================================
module.exports = {
stories: ["../**/*.stories.@(jsx|mdx)"],
addons: [
"@storybook/addon-knobs/register",
"@storybook/addon-actions/register",
{
name: "@storybook/addon-docs",
options: { configureJSX: true }
}
],
webpackFinal: async config => {
return {
...config,
resolve: {
...config.resolve,
alias: {
"core-js/modules": "@storybook/core/node_modules/core-js/modules",
"core-js/features": "@storybook/core/node_modules/core-js/features"
}
}
};
}
};
================================================
FILE: chapter8/4_component_stories/2_documentation/ActionLog.jsx
================================================
import React from "react";
export const ActionLog = ({ actions }) => {
return (
Action Log
{actions.map(({ time, message, data }, i) => {
const date = new Date(time).toUTCString();
return (
Date: {date} - Message: {message} - Data: {JSON.stringify(data)}
);
})}
);
};
================================================
FILE: chapter8/4_component_stories/2_documentation/ActionLog.stories.jsx
================================================
import React from "react";
import { storiesOf } from "@storybook/react";
import { ActionLog } from "./ActionLog";
const actionLogStories = storiesOf("ActionLog", module);
actionLogStories.add("A log of actions", () => {
return (
);
});
================================================
FILE: chapter8/4_component_stories/2_documentation/ActionLog.test.jsx
================================================
import React from "react";
import { ActionLog } from "./ActionLog";
import { render } from "@testing-library/react";
const daysToMs = days => days * 24 * 60 * 60 * 1000;
test("logging actions", () => {
const actions = [
{
time: new Date(daysToMs(1)),
message: "Loaded item list",
data: { cheesecake: 2, macaroon: 5 }
},
{
time: new Date(daysToMs(2)),
message: "Item added",
data: { cheesecake: 2 }
},
{
time: new Date(daysToMs(3)),
message: "Item removed",
data: { cheesecake: 1 }
},
{
time: new Date(daysToMs(4)),
message: "Something weird happened",
data: { error: "The cheesecake is a lie" }
}
];
const { container } = render( );
expect(container).toMatchSnapshot();
});
================================================
FILE: chapter8/4_component_stories/2_documentation/App.jsx
================================================
import React, { useEffect, useState, useRef } from "react";
import { API_ADDR } from "./constants";
import { ItemForm } from "./ItemForm.jsx";
import { ItemList } from "./ItemList.jsx";
import { ActionLog } from "./ActionLog.jsx";
export const App = () => {
const [items, setItems] = useState({});
const [actions, setActions] = useState([]);
const isMounted = useRef(null);
useEffect(() => {
isMounted.current = true;
const loadItems = async () => {
const response = await fetch(`${API_ADDR}/inventory`);
const responseBody = await response.json();
if (isMounted.current) {
setItems(responseBody);
setActions(
actions.concat({
time: new Date().toISOString(),
message: "Loaded items from the server",
data: { status: response.status, body: responseBody }
})
);
}
};
loadItems();
return () => (isMounted.current = false);
}, []);
const updateItems = (itemAdded, addedQuantity) => {
const currentQuantity = items[itemAdded] || 0;
setItems({ ...items, [itemAdded]: currentQuantity + addedQuantity });
setActions(
actions.concat({
time: new Date().toISOString(),
message: "Item added",
data: { itemAdded, addedQuantity }
})
);
};
return (
);
};
================================================
FILE: chapter8/4_component_stories/2_documentation/App.test.jsx
================================================
import React from "react";
import nock from "nock";
import { API_ADDR } from "./constants";
import { App } from "./App.jsx";
import { generateItemText } from "./ItemList.jsx";
import { render, fireEvent, waitFor } from "@testing-library/react";
jest.mock("react-spring/renderprops");
beforeEach(() => {
nock(API_ADDR)
.get("/inventory")
.reply(200, { cheesecake: 2, croissant: 5, macaroon: 96 });
});
afterEach(() => {
if (!nock.isDone()) {
nock.cleanAll();
throw new Error("Not all mocked endpoints received requests.");
}
});
test("renders the appropriate header", () => {
const { getByText } = render( );
expect(getByText("Inventory Contents")).toBeInTheDocument();
});
test("rendering the server's list of items", async () => {
const { getByText } = render( );
await waitFor(() => {
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
});
expect(getByText(generateItemText("cheesecake", 2))).toBeInTheDocument();
expect(getByText(generateItemText("croissant", 5))).toBeInTheDocument();
expect(getByText(generateItemText("macaroon", 96))).toBeInTheDocument();
});
test("updating the list of items with new items", async () => {
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 6 }))
.reply(200);
const { getByText, getByPlaceholderText } = render( );
await waitFor(() => {
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
});
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "6" }
});
fireEvent.click(getByText("Add item"));
await waitFor(() => {
expect(getByText(generateItemText("cheesecake", 8))).toBeInTheDocument();
});
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
expect(getByText(generateItemText("croissant", 5))).toBeInTheDocument();
expect(getByText(generateItemText("macaroon", 96))).toBeInTheDocument();
});
test("updating the action log when loading items", async () => {
jest
.spyOn(Date.prototype, "toISOString")
.mockReturnValue("2020-06-20T13:37:00.000Z");
const { getByTestId } = render( );
await waitFor(() => {
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
});
const actionLog = getByTestId("action-log");
expect(actionLog).toMatchSnapshot();
});
test("updating the action log adding an item", async () => {
jest
.spyOn(Date.prototype, "toISOString")
.mockReturnValueOnce("2020-06-20T13:37:00.000Z");
jest
.spyOn(Date.prototype, "toISOString")
.mockReturnValueOnce("2020-06-21T13:37:00.000Z");
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 6 }))
.reply(200);
const { getByTestId, getByText, getByPlaceholderText } = render( );
await waitFor(() => {
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
});
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "6" }
});
fireEvent.click(getByText("Add item"));
await waitFor(() => {
expect(getByText(generateItemText("cheesecake", 8))).toBeInTheDocument();
});
const actionLog = getByTestId("action-log");
expect(actionLog).toMatchSnapshot();
});
================================================
FILE: chapter8/4_component_stories/2_documentation/ItemForm.jsx
================================================
import React from "react";
import { API_ADDR } from "./constants";
const addItemRequest = (itemName, quantity) => {
fetch(`${API_ADDR}/inventory/${itemName}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ quantity })
});
};
export const ItemForm = ({ onItemAdded }) => {
const [itemName, setItemName] = React.useState("");
const [quantity, setQuantity] = React.useState(0);
const onSubmit = async e => {
e.preventDefault();
await addItemRequest(itemName, quantity);
if (onItemAdded) onItemAdded(itemName, quantity);
};
return (
);
};
================================================
FILE: chapter8/4_component_stories/2_documentation/ItemForm.stories.jsx
================================================
import React, { useEffect } from "react";
import fetchMock from "fetch-mock";
import { action } from "@storybook/addon-actions";
import { API_ADDR } from "./constants";
import { ItemForm } from "./ItemForm";
export default {
title: "ItemForm",
component: ItemForm,
includeStories: ["itemForm"]
};
export const itemForm = () => {
const ItemFormStory = () => {
useEffect(() => {
fetchMock.post(`glob:${API_ADDR}/inventory/*`, 200);
return () => fetchMock.restore();
}, []);
return ;
};
return ;
};
================================================
FILE: chapter8/4_component_stories/2_documentation/ItemForm.test.jsx
================================================
import React from "react";
import nock from "nock";
import { API_ADDR } from "./constants";
import { ItemForm } from "./ItemForm.jsx";
import { render, fireEvent, waitFor } from "@testing-library/react";
test("form's elements", () => {
const { getByText, getByPlaceholderText } = render( );
expect(getByPlaceholderText("Item name")).toBeInTheDocument();
expect(getByPlaceholderText("Quantity")).toBeInTheDocument();
expect(getByText("Add item")).toBeInTheDocument();
});
test("sending requests", () => {
const { getByText, getByPlaceholderText } = render( );
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 2 }))
.reply(200);
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "2" }
});
fireEvent.click(getByText("Add item"));
expect(nock.isDone()).toBe(true);
});
test("invoking the onItemAdded callback", async () => {
const onItemAdded = jest.fn();
const { getByText, getByPlaceholderText } = render(
);
nock(API_ADDR)
.post("/inventory/cheesecake", JSON.stringify({ quantity: 2 }))
.reply(200);
fireEvent.change(getByPlaceholderText("Item name"), {
target: { value: "cheesecake" }
});
fireEvent.change(getByPlaceholderText("Quantity"), {
target: { value: "2" }
});
fireEvent.click(getByText("Add item"));
await waitFor(() => expect(nock.isDone()).toBe(true));
expect(onItemAdded).toHaveBeenCalledTimes(1);
expect(onItemAdded).toHaveBeenCalledWith("cheesecake", 2);
});
================================================
FILE: chapter8/4_component_stories/2_documentation/ItemList.jsx
================================================
/* @jsx jsx */
import { Transition } from "react-spring/renderprops";
import { css, keyframes, jsx } from "@emotion/core";
export const generateItemText = (itemName, quantity) => {
const capitalizedItemName =
itemName.charAt(0).toUpperCase() + itemName.slice(1);
return `${capitalizedItemName} - Quantity: ${quantity}`;
};
const pulsate = keyframes`
0% { opacity: .3; }
50% { opacity: 1; }
100% { opacity: .3; }
`;
const almostOutOfStock = css`
font-weight: bold;
color: red;
animation: ${pulsate} 2s infinite;
`;
export const ItemList = ({ itemList }) => {
const items = Object.entries(itemList);
return (
itemName}
from={{ fontSize: 0, opacity: 0 }}
enter={{ fontSize: 18, opacity: 1 }}
leave={{ fontSize: 0, opacity: 0 }}
>
{([itemName, quantity]) => styleProps => (
{generateItemText(itemName, quantity)}
)}
);
};
================================================
FILE: chapter8/4_component_stories/2_documentation/ItemList.stories.jsx
================================================
import React from "react";
import { withKnobs, object } from "@storybook/addon-knobs";
import { ItemList } from "./ItemList";
export default {
title: "ItemList",
component: ItemList,
includeStories: ["staticItemList", "animatedItems"],
decorators: [withKnobs]
};
export const staticItemList = () => (
);
export const animatedItems = () => {
const knobLabel = "Contents";
const knobDefaultValue = { cheesecake: 2, croissant: 5 };
const itemList = object(knobLabel, knobDefaultValue);
return ;
};
================================================
FILE: chapter8/4_component_stories/2_documentation/ItemList.stories.mdx
================================================
import { Meta, Story, Preview } from "@storybook/addon-docs/blocks";
import { ItemList } from "./ItemList";
# Item list
The `ItemList` component displays a list of inventory items.
It's capable of:
- Animating new items
- Highlighting items which are about to become unavailable
## Props
- An object in which each key represents an item's name, and each value represents its quantity.
================================================
FILE: chapter8/4_component_stories/2_documentation/ItemList.test.jsx
================================================
import React from "react";
import { ItemList, generateItemText } from "./ItemList.jsx";
import { render } from "@testing-library/react";
jest.mock("react-spring/renderprops");
describe("generateItemText", () => {
test("generating an item's text", () => {
expect(generateItemText("cheesecake", 3)).toBe("Cheesecake - Quantity: 3");
expect(generateItemText("apple pie", 22)).toBe("Apple pie - Quantity: 22");
});
});
describe("ItemList Component", () => {
test("list items", () => {
const itemList = { cheesecake: 2, croissant: 5, macaroon: 96 };
const { getByText } = render( );
const listElement = document.querySelector("ul");
expect(listElement.childElementCount).toBe(3);
expect(getByText(generateItemText("cheesecake", 2))).toBeInTheDocument();
expect(getByText(generateItemText("croissant", 5))).toBeInTheDocument();
expect(getByText(generateItemText("macaroon", 96))).toBeInTheDocument();
});
test("highlighting items that are almost out of stock", () => {
const itemList = { cheesecake: 2, croissant: 5, macaroon: 96 };
const { getByText } = render( );
const cheesecakeItem = getByText(generateItemText("cheesecake", 2));
expect(cheesecakeItem).toMatchSnapshot();
});
});
================================================
FILE: chapter8/4_component_stories/2_documentation/__mocks__/react-spring/renderprops.jsx
================================================
const FakeReactSpringTransition = jest.fn(({ items, children }) => {
return items.map(item => {
return children(item)({ fakeStyles: "fake " });
});
});
export { FakeReactSpringTransition as Transition };
================================================
FILE: chapter8/4_component_stories/2_documentation/__snapshots__/ActionLog.test.jsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`logging actions 1`] = `
Action Log
Date:
Fri, 02 Jan 1970 00:00:00 GMT
- Message:
Loaded item list
- Data:
{"cheesecake":2,"macaroon":5}
Date:
Sat, 03 Jan 1970 00:00:00 GMT
- Message:
Item added
- Data:
{"cheesecake":2}
Date:
Sun, 04 Jan 1970 00:00:00 GMT
- Message:
Item removed
- Data:
{"cheesecake":1}
Date:
Mon, 05 Jan 1970 00:00:00 GMT
- Message:
Something weird happened
- Data:
{"error":"The cheesecake is a lie"}
`;
================================================
FILE: chapter8/4_component_stories/2_documentation/__snapshots__/App.test.jsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`updating the action log adding an item 1`] = `
Action Log
Date:
Sat, 20 Jun 2020 13:37:00 GMT
- Message:
Loaded items from the server
- Data:
{"status":200,"body":{"cheesecake":2,"croissant":5,"macaroon":96}}
Date:
Sun, 21 Jun 2020 13:37:00 GMT
- Message:
Item added
- Data:
{"itemAdded":"cheesecake","addedQuantity":6}
`;
exports[`updating the action log when loading items 1`] = `
Action Log
Date:
Sat, 20 Jun 2020 13:37:00 GMT
- Message:
Loaded items from the server
- Data:
{"status":200,"body":{"cheesecake":2,"croissant":5,"macaroon":96}}
`;
================================================
FILE: chapter8/4_component_stories/2_documentation/__snapshots__/ItemList.test.jsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ItemList Component highlighting items that are almost out of stock 1`] = `
@keyframes animation-0 {
0% {
opacity: .3;
}
50% {
opacity: 1;
}
100% {
opacity: .3;
}
}
.emotion-0 {
font-weight: bold;
color: red;
-webkit-animation: animation-0 2s infinite;
animation: animation-0 2s infinite;
}
Cheesecake - Quantity: 2
`;
================================================
FILE: chapter8/4_component_stories/2_documentation/babel.config.js
================================================
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: {
node: "current"
}
}
],
"@babel/preset-react"
]
};
================================================
FILE: chapter8/4_component_stories/2_documentation/constants.js
================================================
export const API_ADDR = "http://localhost:3000";
================================================
FILE: chapter8/4_component_stories/2_documentation/index.html
================================================
Inventory
================================================
FILE: chapter8/4_component_stories/2_documentation/index.jsx
================================================
import ReactDOM from "react-dom";
import React from "react";
import { App } from "./App.jsx";
ReactDOM.render( , document.getElementById("app"));
================================================
FILE: chapter8/4_component_stories/2_documentation/jest.config.js
================================================
module.exports = {
snapshotSerializers: ["jest-emotion"],
setupFilesAfterEnv: [
"/setupJestDom.js",
"/setupJestEmotion.js",
"/setupGlobalFetch.js"
]
};
================================================
FILE: chapter8/4_component_stories/2_documentation/package.json
================================================
{
"name": "1_component_stories",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"storybook": "start-storybook",
"build": "browserify index.jsx -p esmify -o bundle.js",
"start": "http-server ./",
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.9.6",
"@babel/preset-env": "^7.9.6",
"@babel/preset-react": "^7.9.4",
"@storybook/addon-actions": "^6.0.28",
"@storybook/addon-docs": "^5.3.19",
"@storybook/addon-knobs": "^6.0.28",
"@storybook/react": "^6.0.28",
"@testing-library/dom": "^7.10.1",
"@testing-library/jest-dom": "^5.9.0",
"@testing-library/react": "^10.2.1",
"babel-loader": "^8.1.0",
"babelify": "^10.0.0",
"browserify": "^16.5.1",
"core-js": "^2.6.11",
"esmify": "^2.1.1",
"fetch-mock": "^9.10.3",
"http-server": "^0.12.3",
"isomorphic-fetch": "^2.2.1",
"jest": "^25.5",
"jest-emotion": "^10.0.32",
"nock": "^12.0.3",
"react-is": "^16.13.1"
},
"dependencies": {
"@emotion/core": "^10.0.28",
"prop-types": "^15.7.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-spring": "^8.0.27"
},
"browserify": {
"transform": [
[
"babelify",
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 2
}
],
"@babel/preset-react"
]
}
]
]
}
}
================================================
FILE: chapter8/4_component_stories/2_documentation/setupGlobalFetch.js
================================================
const fetch = require("isomorphic-fetch");
global.window.fetch = fetch;
================================================
FILE: chapter8/4_component_stories/2_documentation/setupJestDom.js
================================================
const jestDom = require("@testing-library/jest-dom");
expect.extend(jestDom);
================================================
FILE: chapter8/4_component_stories/2_documentation/setupJestEmotion.js
================================================
const { matchers } = require("jest-emotion");
expect.extend(matchers);
================================================
FILE: chapter8/4_component_stories/2_documentation/styles.css
================================================
.almost-out-of-stock {
font-weight: bold;
color: red;
}
================================================
FILE: chapter8/server/README.md
================================================
# Chapter 5 Server
To 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.
In case you want to update the back-end from Chapter 4 yourself, here's the list of changes I've done:
- For the server to accept the requests coming from the client, you'll need to use [`@koa/cors`](https://github.com/koajs/cors)
- 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.
- 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.
- At `GET /inventory` I have added a route which lists all items in the inventory.
- 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
- I've used `koa-socket-2` to add support for `socket.io`
- The `POST /inventory/:itemName` will now push updates to all clients but the one which added an item.
================================================
FILE: chapter8/server/authenticationController.js
================================================
const crypto = require("crypto");
const { db } = require("./dbConnection");
const hashPassword = password => {
const hash = crypto.createHash("sha256");
hash.update(password);
return hash.digest("hex");
};
const credentialsAreValid = async (username, password) => {
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) return false;
return hashPassword(password) === user.passwordHash;
};
const authenticationMiddleware = async (ctx, next) => {
try {
const authHeader = ctx.request.headers.authorization;
const credentials = Buffer.from(
authHeader.slice("basic".length + 1),
"base64"
).toString();
const [username, password] = credentials.split(":");
const validCredentialsSent = await credentialsAreValid(username, password);
if (!validCredentialsSent) throw new Error("invalid credentials");
} catch (e) {
ctx.status = 401;
ctx.body = { message: "please provide valid credentials" };
return;
}
await next();
};
module.exports = {
hashPassword,
credentialsAreValid,
authenticationMiddleware
};
================================================
FILE: chapter8/server/authenticationController.test.js
================================================
const crypto = require("crypto");
const {
hashPassword,
credentialsAreValid,
authenticationMiddleware
} = require("./authenticationController");
const { user: globalUser } = require("./userTestUtils");
describe("hashPassword", () => {
test("hashing passwords", () => {
const plainTextPassword = "password_example";
const hash = crypto.createHash("sha256");
hash.update(plainTextPassword);
const expectedHash = hash.digest("hex");
expect(hashPassword(plainTextPassword)).toBe(expectedHash);
});
});
describe("credentialsAreValid", () => {
test("validating credentials", async () => {
expect(await credentialsAreValid(globalUser.username, "a_password")).toBe(
true
);
});
});
describe("authenticationMiddleware", () => {
test("returning an error if the credentials are not valid", async () => {
const fakeAuth = Buffer.from("invalid:credentials").toString("base64");
const ctx = {
request: {
headers: { authorization: `Basic ${fakeAuth}` }
}
};
const next = jest.fn();
await authenticationMiddleware(ctx, next);
expect(next.mock.calls).toHaveLength(0);
expect(ctx).toEqual({
...ctx,
status: 401,
body: { message: "please provide valid credentials" }
});
});
test("authenticating properly", async () => {
const ctx = {
request: {
headers: { authorization: globalUser.authHeader }
}
};
const next = jest.fn();
await authenticationMiddleware(ctx, next);
expect(next.mock.calls).toHaveLength(1);
});
});
================================================
FILE: chapter8/server/cartController.js
================================================
const { db } = require("./dbConnection");
const { removeFromInventory } = require("./inventoryController");
const logger = require("./logger");
const addItemToCart = async (username, itemName) => {
await removeFromInventory(itemName);
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) {
const userNotFound = new Error("user not found");
userNotFound.code = 404;
}
const itemEntry = await db
.select()
.from("carts_items")
.where({ userId: user.id, itemName })
.first();
if (itemEntry && itemEntry.quantity + 1 > 3) {
const limitError = new Error(
"You can't have more than three units of an item in your cart"
);
limitError.code = 400;
throw limitError;
}
if (itemEntry) {
await db("carts_items")
.increment("quantity")
.update({ updatedAt: new Date().toISOString() })
.where({
userId: itemEntry.userId,
itemName
});
} else {
await db("carts_items").insert({
userId: user.id,
itemName,
quantity: 1,
updatedAt: new Date().toISOString()
});
}
logger.log(`${itemName} added to ${username}'s cart`);
return db
.select("itemName", "quantity")
.from("carts_items")
.where({ userId: user.id });
};
const hoursInMs = n => 1000 * 60 * 60 * n;
const removeStaleItems = async () => {
const fourHoursAgo = new Date(Date.now() - hoursInMs(4)).toISOString();
const staleItems = await db
.select()
.from("carts_items")
.where("updatedAt", "<", fourHoursAgo);
if (staleItems.length === 0) return;
// Put stale items back in the inventory
const inventoryUpdates = staleItems.map(staleItem =>
db("inventory")
.increment("quantity", staleItem.quantity)
.where({ itemName: staleItem.itemName })
);
await Promise.all(inventoryUpdates);
// Delete stale items from cart
const staleItemTuples = staleItems.map(i => [i.itemName, i.userId]);
await db("carts_items")
.del()
.whereIn(["itemName", "userId"], staleItemTuples);
};
const monitorStaleItems = () => setInterval(removeStaleItems, hoursInMs(2));
module.exports = { addItemToCart, monitorStaleItems };
================================================
FILE: chapter8/server/cartController.test.js
================================================
const { db } = require("./dbConnection");
const { addItemToCart, monitorStaleItems } = require("./cartController");
const { hashPassword } = require("./authenticationController");
const { user: globalUser } = require("./userTestUtils");
const FakeTimers = require("@sinonjs/fake-timers");
const fs = require("fs");
describe("addItemToCart", () => {
beforeEach(() => {
fs.writeFileSync("/tmp/logs.out", "");
});
test("adding unavailable items to cart", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 0 });
try {
await addItemToCart(globalUser.username, "cheesecake");
} catch (e) {
const expectedError = new Error("cheesecake is unavailable");
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
const finalCartContent = await db
.select("carts_items.*")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([]);
expect.assertions(2);
});
test("adding items above limit to cart", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 1 });
await db("carts_items").insert({
userId: globalUser.id,
itemName: "cheesecake",
quantity: 3
});
try {
await addItemToCart(globalUser.username, "cheesecake");
} catch (e) {
const expectedError = new Error(
"You can't have more than three units of an item in your cart"
);
expectedError.code = 400;
expect(e).toEqual(expectedError);
}
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([{ itemName: "cheesecake", quantity: 3 }]);
expect.assertions(2);
});
test("logging added items", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 1 });
await db("carts_items").insert({
userId: globalUser.id,
itemName: "cheesecake",
quantity: 1
});
await addItemToCart(globalUser.username, "cheesecake");
const logs = fs.readFileSync("/tmp/logs.out", "utf-8");
expect(logs).toContain(
`cheesecake added to ${globalUser.username}'s cart\n`
);
});
});
const withRetries = async fn => {
// Capture the assertion error since Jest does not export it
const JestAssertionError = (() => {
try {
expect(false).toBe(true);
} catch (e) {
return e.constructor;
}
})();
try {
await fn();
} catch (e) {
if (e.constructor === JestAssertionError) {
// Wait 100ms before retrying
await new Promise(resolve => setTimeout(resolve, 100));
await withRetries(fn);
} else {
throw e;
}
}
};
describe("timers", () => {
const hoursInMs = n => 1000 * 60 * 60 * n;
let clock;
beforeEach(() => {
clock = FakeTimers.install({ toFake: ["Date", "setInterval"] });
});
afterEach(() => {
clock = clock.uninstall();
});
test("removing stale items", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 1 });
await addItemToCart(globalUser.username, "cheesecake");
clock.tick(hoursInMs(4));
timer = monitorStaleItems();
clock.tick(hoursInMs(2));
await withRetries(async () => {
const finalCartContent = await db
.select()
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([]);
});
await withRetries(async () => {
const inventoryContent = await db
.select("itemName", "quantity")
.from("inventory");
expect(inventoryContent).toEqual([
{ itemName: "cheesecake", quantity: 1 }
]);
});
});
});
================================================
FILE: chapter8/server/dbConnection.js
================================================
const environmentName = process.env.NODE_ENV;
const db = require("knex")(require("./knexfile")[environmentName]);
const closeConnection = () => db.destroy();
module.exports = {
db,
closeConnection
};
================================================
FILE: chapter8/server/disconnectFromDb.js
================================================
const { db } = require("./dbConnection");
afterAll(() => db.destroy());
================================================
FILE: chapter8/server/inventoryController.js
================================================
const { db } = require("./dbConnection");
const removeFromInventory = async itemName => {
const inventoryEntry = await db
.select()
.from("inventory")
.where({ itemName })
.first();
if (!inventoryEntry || inventoryEntry.quantity === 0) {
const err = new Error(`${itemName} is unavailable`);
err.code = 400;
throw err;
}
await db("inventory")
.decrement("quantity")
.where({ itemName });
};
module.exports = { removeFromInventory };
================================================
FILE: chapter8/server/jest.config.js
================================================
module.exports = {
testEnvironment: "node",
globalSetup: "./migrateDatabases.js",
setupFilesAfterEnv: [
"/truncateTables.js",
"/seedUser.js",
"/disconnectFromDb.js"
]
};
================================================
FILE: chapter8/server/knexfile.js
================================================
module.exports = {
test: {
client: "sqlite3",
connection: { filename: "./test.sqlite" },
useNullAsDefault: true
},
development: {
client: "sqlite3",
connection: { filename: "./dev.sqlite" },
useNullAsDefault: true
}
};
================================================
FILE: chapter8/server/logger.js
================================================
const fs = require("fs");
const logger = {
log: msg => fs.appendFileSync("/tmp/logs.out", msg + "\n")
};
module.exports = logger;
================================================
FILE: chapter8/server/migrateDatabases.js
================================================
const environmentName = process.env.NODE_ENV || "test";
const environmentConfig = require("./knexfile")[environmentName];
const db = require("knex")(environmentConfig);
module.exports = async () => {
// Migrate the database to the latest state
await db.migrate.latest();
// Close the connection to the database so that tests won't hang
await db.destroy();
};
================================================
FILE: chapter8/server/migrations/20200325082401_initial_schema.js
================================================
exports.up = async knex => {
await knex.schema.createTable("users", table => {
table.increments("id");
table.string("username");
table.unique("username");
table.string("email");
table.string("passwordHash");
});
await knex.schema.createTable("carts_items", table => {
table.integer("userId").references("users.id");
table.string("itemName");
table.unique("itemName");
table.integer("quantity");
});
await knex.schema.createTable("inventory", table => {
table.increments("id");
table.string("itemName");
table.unique("itemName");
table.integer("quantity");
});
};
exports.down = async knex => {
await knex.schema.dropTable("users");
await knex.schema.dropTable("carts_items");
await knex.schema.dropTable("inventory");
};
================================================
FILE: chapter8/server/migrations/20200331210311_updatedAt_field.js
================================================
exports.up = knex => {
return knex.schema.alterTable("carts_items", table => {
table.timestamp("updatedAt");
});
};
exports.down = knex => {
return knex.schema.alterTable("carts_items", table => {
table.dropColumn("updatedAt");
});
};
================================================
FILE: chapter8/server/package.json
================================================
{
"name": "chapter5_server",
"version": "1.0.0",
"scripts": {
"test": "jest --runInBand",
"start": "cross-env NODE_ENV=development node server.js",
"migrate:dev": "knex migrate:latest --env development",
"seed:dev": "knex seed:run"
},
"devDependencies": {
"@sinonjs/fake-timers": "github:sinonjs/fake-timers",
"jest": "^24.9.0",
"supertest": "^4.0.2"
},
"dependencies": {
"@koa/cors": "^3.0.0",
"cross-env": "^7.0.2",
"isomorphic-fetch": "^2.2.1",
"knex": "^0.20.13",
"koa": "^2.11.0",
"koa-body-parser": "^1.1.2",
"koa-router": "^7.4.0",
"koa-socket-2": "^1.2.0",
"nock": "^12.0.3",
"socket.io": "^2.3.0",
"sqlite3": "^4.1.1"
},
"main": "alertController.spec.js",
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}
================================================
FILE: chapter8/server/seedUser.js
================================================
const { createUser } = require("./userTestUtils");
beforeEach(createUser);
================================================
FILE: chapter8/server/seeds/initial_inventory.js
================================================
exports.seed = async knex => {
await knex("inventory").del();
return knex("inventory").insert([
{ itemName: "cheesecake", quantity: 8 },
{ itemName: "apple pie", quantity: 2 },
{ itemName: "carrot cake", quantity: 5 }
]);
};
================================================
FILE: chapter8/server/server.js
================================================
const fetch = require("isomorphic-fetch");
const Koa = require("koa");
const http = require("http");
const IO = require("koa-socket-2");
const cors = require("@koa/cors");
const Router = require("koa-router");
const bodyParser = require("koa-body-parser");
const { db } = require("./dbConnection");
const { addItemToCart } = require("./cartController");
const {
hashPassword,
authenticationMiddleware
} = require("./authenticationController");
const PORT = process.env.NODE_ENV === "test" ? 5000 : 3000;
const app = new Koa();
const io = new IO();
io.attach(app);
const router = new Router();
app.use(cors());
app.use(bodyParser());
app.use(async (ctx, next) => {
if (ctx.url.startsWith("/carts")) {
return await authenticationMiddleware(ctx, next);
}
await next();
});
router.put("/users/:username", async ctx => {
const { username } = ctx.params;
const { email, password } = ctx.request.body;
const userAlreadyExists = await db
.select()
.from("users")
.where({ username })
.first();
if (userAlreadyExists) {
ctx.body = { message: `${username} already exists` };
ctx.status = 409;
return;
}
await db("users").insert({
username,
email,
passwordHash: hashPassword(password)
});
return (ctx.body = { message: `${username} created successfully` });
});
router.post("/carts/:username/items", async ctx => {
const { username } = ctx.params;
const { item, quantity } = ctx.request.body;
for (let i = 0; i < quantity; i++) {
try {
const newItems = await addItemToCart(username, item);
ctx.body = newItems;
} catch (e) {
ctx.body = { message: e.message };
ctx.status = e.code;
return;
}
}
});
router.delete("/carts/:username/items/:item", async ctx => {
const { username, item } = ctx.params;
const user = await db
.select()
.from("users")
.where({ username })
.first();
if (!user) {
ctx.body = { message: "user not found" };
ctx.status = 404;
return;
}
const itemEntry = await db
.select()
.from("carts_items")
.where({ userId: user.id, itemName: item })
.first();
if (!itemEntry || itemEntry.quantity === 0) {
ctx.body = { message: `${item} is not in the cart` };
ctx.status = 400;
return;
}
await db("carts_items")
.decrement("quantity")
.where({ userId: user.id, itemName: item });
const inventoryEntry = await db
.select()
.from("inventory")
.where({ itemName: item })
.first();
if (inventoryEntry) {
await db("inventory")
.increment("quantity")
.where({ userId: itemEntry.userId, itemName: item });
} else {
await db("inventory").insert({ itemName: item, quantity: 1 });
}
ctx.body = await db
.select("itemName", "quantity")
.from("carts_items")
.where({ userId: user.id });
});
router.post("/inventory/:itemName", async ctx => {
const { itemName } = ctx.params;
const { quantity } = ctx.request.body;
const clientId = ctx.request.headers["x-socket-client-id"];
const current = await db
.select("itemName", "quantity")
.from("inventory")
.where({ itemName })
.first();
const itemExists = current && current.quantity > 0;
const newRecord = {
itemName,
quantity: (itemExists ? current.quantity : 0) + quantity
};
if (current) {
await db("inventory")
.increment("quantity", quantity)
.where({ itemName });
} else {
await db("inventory").insert(newRecord);
}
Object.entries(io.socket.sockets.connected).forEach(([id, socket]) => {
if (id === clientId) return;
socket.emit("add_item", { itemName, quantity });
});
ctx.body = newRecord;
});
router.delete("/inventory/:itemName", async ctx => {
const { itemName } = ctx.params;
const { quantity } = ctx.request.body;
const current = await db
.select("itemName", "quantity")
.from("inventory")
.where({ itemName })
.first();
const canDelete = current && current.quantity > quantity;
if (canDelete) {
await db("inventory")
.decrement("quantity", quantity)
.where({ itemName });
ctx.body = { message: `Removed ${quantity} units of ${itemName}` };
} else {
ctx.status = 404;
ctx.body = {
message: `There aren't ${quantity} units of ${itemName} available.`
};
}
});
router.get("/inventory", async ctx => {
const inventoryContent = await db
.select("itemName", "quantity")
.from("inventory")
.where("quantity", ">", 0)
.orderBy("quantity", "desc");
ctx.body = inventoryContent.reduce((acc, { itemName, quantity }) => {
return { ...acc, [itemName]: quantity };
}, {});
});
router.get("/inventory/:itemName", async ctx => {
const { itemName } = ctx.params;
const response = await fetch(`http://recipepuppy.com/api?i=${itemName}`);
const { title, href, results: recipes } = await response.json();
const inventoryItem = await db
.select()
.from("inventory")
.where({ itemName })
.first();
ctx.body = {
...inventoryItem,
info: `Data obtained from ${title} - ${href}`,
recipes
};
});
app.use(router.routes());
module.exports = { app: app.listen(PORT, "127.0.0.1") };
================================================
FILE: chapter8/server/server.test.js
================================================
const { user: globalUser } = require("./userTestUtils");
const { db } = require("./dbConnection");
const request = require("supertest");
const { app } = require("./server.js");
const { hashPassword } = require("./authenticationController.js");
const nock = require("nock");
afterAll(() => app.close());
describe("add items to a cart", () => {
test("adding available items", async () => {
await db("inventory").insert({ itemName: "cheesecake", quantity: 3 });
const response = await request(app)
.post(`/carts/${globalUser.username}/items`)
.set("authorization", globalUser.authHeader)
.send({ item: "cheesecake", quantity: 3 })
.expect(200)
.expect("Content-Type", /json/);
const newItems = [{ itemName: "cheesecake", quantity: 3 }];
expect(response.body).toEqual(newItems);
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(0);
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual(newItems);
});
test("adding unavailable items", async () => {
const response = await request(app)
.post(`/carts/${globalUser.username}/items`)
.set("authorization", globalUser.authHeader)
.send({ item: "cheesecake", quantity: 1 })
.expect(400)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "cheesecake is unavailable"
});
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual([]);
});
});
describe("removing items from a cart", () => {
test("removing existing items", async () => {
await db("carts_items").insert({
userId: globalUser.id,
itemName: "cheesecake",
quantity: 1
});
const response = await request(app)
.del(`/carts/${globalUser.username}/items/cheesecake`)
.set("authorization", globalUser.authHeader)
.expect(200)
.expect("Content-Type", /json/);
const expectedFinalContent = [{ itemName: "cheesecake", quantity: 0 }];
expect(response.body).toEqual(expectedFinalContent);
const finalCartContent = await db
.select("carts_items.itemName", "carts_items.quantity")
.from("carts_items")
.join("users", "users.id", "carts_items.userId")
.where("users.username", globalUser.username);
expect(finalCartContent).toEqual(expectedFinalContent);
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(1);
});
test("removing non-existing items", async () => {
await db("inventory").insert({
itemName: "cheesecake",
quantity: 0
});
const response = await request(app)
.del(`/carts/${globalUser.username}/items/cheesecake`)
.set("authorization", globalUser.authHeader)
.expect(400)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "cheesecake is not in the cart"
});
const { quantity: inventoryCheesecakes } = await db
.select()
.from("inventory")
.where({ itemName: "cheesecake" })
.first();
expect(inventoryCheesecakes).toEqual(0);
});
});
describe("create accounts", () => {
test("creating a new account", async () => {
const response = await request(app)
.put("/users/another_user")
.send({ email: "another_user@example.org", password: "a_password" })
.expect(200)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: "another_user created successfully"
});
const savedUser = await db
.select("email", "passwordHash")
.from("users")
.where({ username: "another_user" })
.first();
expect(savedUser).toEqual({
email: "another_user@example.org",
passwordHash: hashPassword("a_password")
});
});
test("creating a duplicate account", async () => {
const response = await request(app)
.put(`/users/${globalUser.username}`)
.send({ email: globalUser.email, password: "a_password" })
.expect(409)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
message: `${globalUser.username} already exists`
});
});
});
describe("list inventory items", () => {
const eggs = { itemName: "eggs", quantity: 3 };
const applePie = { itemName: "apple pie", quantity: 1 };
const carrotCake = { itemName: "carrot cake", quantity: 0 };
beforeEach(async () => {
await db("inventory").insert([eggs, applePie, carrotCake]);
});
test("fetching all available items", async () => {
const { body } = await request(app)
.get("/inventory")
.expect(200)
.expect("Content-Type", /json/);
expect(body).toEqual({ eggs: 3, "apple pie": 1 });
});
});
describe("add inventory items", () => {
test("adding a new item", async () => {
const { body } = await request(app)
.post("/inventory/eggs")
.send({ quantity: 3 })
.expect(200)
.expect("Content-Type", /json/);
expect(body).toEqual({ itemName: "eggs", quantity: 3 });
expect(
await db
.select("itemName", "quantity")
.from("inventory")
.where("itemName", "eggs")
.first()
).toEqual({ itemName: "eggs", quantity: 3 });
});
test("adding an existing item", async () => {
const eggs = { itemName: "eggs", quantity: 2 };
await db("inventory").insert(eggs);
const { body } = await request(app)
.post("/inventory/eggs")
.send({ quantity: 3 })
.expect(200)
.expect("Content-Type", /json/);
expect(body).toEqual({ itemName: "eggs", quantity: 5 });
expect(
await db
.select("itemName", "quantity")
.from("inventory")
.where("itemName", "eggs")
.first()
).toEqual({ itemName: "eggs", quantity: 5 });
});
});
describe("remove inventory items", () => {
beforeEach(async () => {
await db("inventory").insert({ itemName: "eggs", quantity: 3 });
});
test("removing an item", async () => {
const { body } = await request(app)
.del("/inventory/eggs")
.send({ quantity: 2 })
.expect(200)
.expect("Content-Type", /json/);
expect(body).toEqual({
message: "Removed 2 units of eggs"
});
expect(
await db
.select("itemName", "quantity")
.from("inventory")
.where("itemName", "eggs")
.first()
).toEqual({ itemName: "eggs", quantity: 1 });
});
test("removing more than the inventory quantity", async () => {
const { body } = await request(app)
.del("/inventory/eggs")
.send({ quantity: 4 })
.expect(404)
.expect("Content-Type", /json/);
expect(body).toEqual({
message: "There aren't 4 units of eggs available."
});
expect(
await db
.select("itemName", "quantity")
.from("inventory")
.where("itemName", "eggs")
.first()
).toEqual({ itemName: "eggs", quantity: 3 });
});
});
describe("fetch inventory items", () => {
const eggs = { itemName: "eggs", quantity: 3 };
const applePie = { itemName: "apple pie", quantity: 1 };
beforeEach(async () => {
await db("inventory").insert([eggs, applePie]);
const { id: eggsId } = await db
.select()
.from("inventory")
.where({ itemName: "eggs" })
.first();
eggs.id = eggsId;
});
test("fetching an item from the inventory", async () => {
const eggsResponse = {
title: "FakeAPI",
href: "example.org",
results: [{ name: "Omelette du Fromage" }]
};
nock("http://recipepuppy.com")
.get("/api")
.query({ i: "eggs" })
.reply(200, eggsResponse);
const response = await request(app)
.get(`/inventory/eggs`)
.expect(200)
.expect("Content-Type", /json/);
expect(response.body).toEqual({
...eggs,
info: `Data obtained from ${eggsResponse.title} - ${eggsResponse.href}`,
recipes: eggsResponse.results
});
});
});
================================================
FILE: chapter8/server/truncateTables.js
================================================
const { db } = require("./dbConnection");
const tablesToTruncate = ["users", "inventory", "carts_items"];
beforeEach(() => {
return Promise.all(tablesToTruncate.map(t => db(t).truncate()));
});
================================================
FILE: chapter8/server/userTestUtils.js
================================================
const { db } = require("./dbConnection");
const { hashPassword } = require("./authenticationController");
const username = "test_user";
const password = "a_password";
const passwordHash = hashPassword(password);
const email = "test_user@example.org";
const validAuth = Buffer.from(`${username}:${password}`).toString("base64");
const authHeader = `Basic ${validAuth}`;
const user = {
username,
password,
email,
authHeader
};
const createUser = async () => {
await db("users").insert({ username, email, passwordHash });
const { id } = await db
.select()
.from("users")
.where({ username })
.first();
user.id = id;
};
module.exports = { user, createUser };
================================================
FILE: chapter9/1_the_philosophy_behind_tdd/1_what_tdd_is/1_small_test/calculateCartPrice.js
================================================
const calculateCartPrice = () => 7;
module.exports = { calculateCartPrice };
================================================
FILE: chapter9/1_the_philosophy_behind_tdd/1_what_tdd_is/1_small_test/calculateCartPrice.test.js
================================================
const { calculateCartPrice } = require("./calculateCartPrice");
test("calculating total values", () => {
expect(calculateCartPrice([1, 1, 2, 3])).toBe(7);
});
================================================
FILE: chapter9/1_the_philosophy_behind_tdd/1_what_tdd_is/1_small_test/package.json
================================================
{
"name": "1_small_test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^26.1.0"
}
}
================================================
FILE: chapter9/1_the_philosophy_behind_tdd/1_what_tdd_is/2_partial_test/calculateCartPrice.js
================================================
const calculateCartPrice = prices => {
return prices.reduce((sum, price) => {
return sum + price;
}, 0);
};
module.exports = { calculateCartPrice };
================================================
FILE: chapter9/1_the_philosophy_behind_tdd/1_what_tdd_is/2_partial_test/calculateCartPrice.test.js
================================================
const { calculateCartPrice } = require("./calculateCartPrice");
test("calculating total values", () => {
expect(calculateCartPrice([1, 1, 2, 3])).toBe(7);
expect(calculateCartPrice([3, 5, 8])).toBe(16);
expect(calculateCartPrice([13, 21])).toBe(34);
expect(calculateCartPrice([55])).toBe(55);
});
================================================
FILE: chapter9/1_the_philosophy_behind_tdd/1_what_tdd_is/2_partial_test/package.json
================================================
{
"name": "2_partial_test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^26.1.0"
}
}
================================================
FILE: chapter9/1_the_philosophy_behind_tdd/1_what_tdd_is/3_extra_test/calculateCartPrice.js
================================================
const calculateCartPrice = (prices, discountPercentage) => {
const total = prices.reduce((sum, price) => {
return sum + price;
}, 0);
return discountPercentage
? ((100 - discountPercentage) / 100) * total
: total;
};
module.exports = { calculateCartPrice };
================================================
FILE: chapter9/1_the_philosophy_behind_tdd/1_what_tdd_is/3_extra_test/calculateCartPrice.test.js
================================================
const { calculateCartPrice } = require("./calculateCartPrice");
test("calculating total values", () => {
expect(calculateCartPrice([1, 1, 2, 3])).toBe(7);
expect(calculateCartPrice([3, 5, 8])).toBe(16);
expect(calculateCartPrice([13, 21])).toBe(34);
expect(calculateCartPrice([55])).toBe(55);
});
test("applying a discount", () => {
expect(calculateCartPrice([1, 2, 3], 50)).toBe(3);
expect(calculateCartPrice([2, 5, 5], 25)).toBe(9);
expect(calculateCartPrice([9, 21], 10)).toBe(27);
expect(calculateCartPrice([50, 50], 100)).toBe(0);
});
================================================
FILE: chapter9/1_the_philosophy_behind_tdd/1_what_tdd_is/3_extra_test/package.json
================================================
{
"name": "3_extra_test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^26.1.0"
}
}
================================================
FILE: chapter9/1_the_philosophy_behind_tdd/1_what_tdd_is/4_handling_edge_cases/calculateCartPrice.js
================================================
const calculateCartPrice = (prices, discountPercentage) => {
const total = prices.reduce((sum, price) => {
return sum + price;
}, 0);
return typeof discountPercentage === "number" && !isNaN(discountPercentage)
? ((100 - discountPercentage) / 100) * total
: total;
};
module.exports = { calculateCartPrice };
================================================
FILE: chapter9/1_the_philosophy_behind_tdd/1_what_tdd_is/4_handling_edge_cases/calculateCartPrice.test.js
================================================
const { calculateCartPrice } = require("./calculateCartPrice");
test("calculating total values", () => {
expect(calculateCartPrice([1, 1, 2, 3])).toBe(7);
expect(calculateCartPrice([3, 5, 8])).toBe(16);
expect(calculateCartPrice([13, 21])).toBe(34);
expect(calculateCartPrice([55])).toBe(55);
});
test("applying a discount", () => {
expect(calculateCartPrice([1, 2, 3], 50)).toBe(3);
expect(calculateCartPrice([2, 5, 5], 25)).toBe(9);
expect(calculateCartPrice([9, 21], 10)).toBe(27);
expect(calculateCartPrice([50, 50], 100)).toBe(0);
});
test("handling strings", () => {
expect(calculateCartPrice([1, 2, 3], "string")).toBe(6);
});
test("handling NaN", () => {
expect(calculateCartPrice([1, 2, 3], NaN)).toBe(6);
});
================================================
FILE: chapter9/1_the_philosophy_behind_tdd/1_what_tdd_is/4_handling_edge_cases/package.json
================================================
{
"name": "4_handling_edge_cases",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^26.1.0"
}
}
================================================
FILE: chapter9/1_the_philosophy_behind_tdd/2_adjusting_iteration_size/1_bigger_steps/calculateCartPrice.js
================================================
const calculateCartPrice = (prices, discountPercentage) => {
const total = prices.reduce((sum, price) => {
return sum + price;
}, 0);
return discountPercentage
? ((100 - discountPercentage) / 100) * total
: total;
};
module.exports = { calculateCartPrice };
================================================
FILE: chapter9/1_the_philosophy_behind_tdd/2_adjusting_iteration_size/1_bigger_steps/calculateCartPrice.test.js
================================================
const { calculateCartPrice } = require("./calculateCartPrice");
test("calculating total values", () => {
expect(calculateCartPrice([1, 1, 2, 3])).toBe(7);
expect(calculateCartPrice([3, 5, 8])).toBe(16);
expect(calculateCartPrice([13, 21])).toBe(34);
expect(calculateCartPrice([55])).toBe(55);
});
test("applying a discount", () => {
expect(calculateCartPrice([1, 2, 3], 50)).toBe(3);
expect(calculateCartPrice([2, 5, 5], 25)).toBe(9);
expect(calculateCartPrice([9, 21], 10)).toBe(27);
expect(calculateCartPrice([50, 50], 100)).toBe(0);
});
================================================
FILE: chapter9/1_the_philosophy_behind_tdd/2_adjusting_iteration_size/1_bigger_steps/package.json
================================================
{
"name": "2_adjusting_iteration_size",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^26.1.0"
}
}
================================================
FILE: chapter9/1_the_philosophy_behind_tdd/2_adjusting_iteration_size/1_bigger_steps/pickMostExpensive.js
================================================
const { calculateCartPrice } = require("./calculateCartPrice");
const pickMostExpensive = carts => {
let mostExpensivePrice = 0;
let mostExpensiveCart = null;
for (let i = 0; i < carts.length; i++) {
const currentCart = carts[i];
const currentCartPrice = calculateCartPrice(currentCart);
if (currentCartPrice >= mostExpensivePrice) {
mostExpensivePrice = currentCartPrice;
mostExpensiveCart = currentCart;
}
}
return mostExpensiveCart;
};
module.exports = { pickMostExpensive };
================================================
FILE: chapter9/1_the_philosophy_behind_tdd/2_adjusting_iteration_size/1_bigger_steps/pickMostExpensive.test.js
================================================
const { pickMostExpensive } = require("./pickMostExpensive");
test("picking the most expensive cart", () => {
expect(pickMostExpensive([[3, 2, 1, 4], [5], [50]])).toEqual([50]);
expect(pickMostExpensive([[2, 8, 9], [0], [20]])).toEqual([20]);
expect(pickMostExpensive([[0], [0], [0]])).toEqual([0]);
expect(pickMostExpensive([[], [5], []])).toEqual([5]);
});
test("null for an empty cart array", () => {
expect(pickMostExpensive([])).toEqual(null);
});
================================================
FILE: chapter9/2_writing_a_js_module_using_tdd/1_generating_item_rows/inventoryReport.js
================================================
const generateItemRow = ({ name, quantity, price }) => {
if (quantity === 0 || price === 0) return null;
return `${name},${quantity},${price},${price * quantity}`;
};
module.exports = { generateItemRow };
================================================
FILE: chapter9/2_writing_a_js_module_using_tdd/1_generating_item_rows/inventoryReport.test.js
================================================
const { generateItemRow } = require("./inventoryReport");
test("generating an item's row", () => {
expect(generateItemRow({ name: "macaroon", quantity: 12, price: 3 })).toBe(
"macaroon,12,3,36"
);
expect(generateItemRow({ name: "cheesecake", quantity: 6, price: 12 })).toBe(
"cheesecake,6,12,72"
);
expect(generateItemRow({ name: "apple pie", quantity: 5, price: 15 })).toBe(
"apple pie,5,15,75"
);
});
test("ommitting soldout items", () => {
expect(generateItemRow({ name: "macaroon", quantity: 0, price: 3 })).toBe(
null
);
expect(generateItemRow({ name: "cheesecake", quantity: 0, price: 12 })).toBe(
null
);
});
test("ommitting free items", () => {
expect(
generateItemRow({ name: "plastic cups", quantity: 99, price: 0 })
).toBe(null);
expect(generateItemRow({ name: "napkins", quantity: 200, price: 0 })).toBe(
null
);
});
================================================
FILE: chapter9/2_writing_a_js_module_using_tdd/1_generating_item_rows/package.json
================================================
{
"name": "1_generating_item_rows",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^26.1.0"
}
}
================================================
FILE: chapter9/2_writing_a_js_module_using_tdd/2_generating_total_row/inventoryReport.js
================================================
const generateItemRow = ({ name, quantity, price }) => {
if (quantity === 0 || price === 0) return null;
return `${name},${quantity},${price},${price * quantity}`;
};
const generateTotalRow = items => {
const total = items.reduce(
(t, { price, quantity }) => t + price * quantity,
0
);
return `Total,,,${total}`;
};
module.exports = { generateItemRow, generateTotalRow };
================================================
FILE: chapter9/2_writing_a_js_module_using_tdd/2_generating_total_row/inventoryReport.test.js
================================================
const { generateItemRow, generateTotalRow } = require("./inventoryReport");
describe("generateItemRow", () => {
test("generating an item's row", () => {
expect(generateItemRow({ name: "macaroon", quantity: 12, price: 3 })).toBe(
"macaroon,12,3,36"
);
expect(
generateItemRow({ name: "cheesecake", quantity: 6, price: 12 })
).toBe("cheesecake,6,12,72");
expect(generateItemRow({ name: "apple pie", quantity: 5, price: 15 })).toBe(
"apple pie,5,15,75"
);
});
test("ommitting soldout items", () => {
expect(generateItemRow({ name: "macaroon", quantity: 0, price: 3 })).toBe(
null
);
expect(
generateItemRow({ name: "cheesecake", quantity: 0, price: 12 })
).toBe(null);
});
test("ommitting free items", () => {
expect(
generateItemRow({ name: "plastic cups", quantity: 99, price: 0 })
).toBe(null);
expect(generateItemRow({ name: "napkins", quantity: 200, price: 0 })).toBe(
null
);
});
});
describe("generateTotalRow", () => {
test("generating a total row", () => {
const items = [
{ name: "apple pie", quantity: 3, price: 15 },
{ name: "plastic cups", quantity: 0, price: 55 },
{ name: "macaroon", quantity: 12, price: 3 },
{ name: "cheesecake", quantity: 0, price: 12 }
];
expect(generateTotalRow(items)).toBe("Total,,,81");
expect(generateTotalRow(items.slice(1))).toBe("Total,,,36");
expect(generateTotalRow(items.slice(3))).toBe("Total,,,0");
});
});
================================================
FILE: chapter9/2_writing_a_js_module_using_tdd/2_generating_total_row/package.json
================================================
{
"name": "2_generating_total_row",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^26.1.0"
}
}
================================================
FILE: chapter9/2_writing_a_js_module_using_tdd/3_creating_report/inventoryReport.js
================================================
const fs = require("fs");
const generateItemRow = ({ name, quantity, price }) => {
if (quantity === 0 || price === 0) return null;
return `${name},${quantity},${price},${price * quantity}`;
};
const generateTotalRow = items => {
const total = items.reduce(
(t, { price, quantity }) => t + price * quantity,
0
);
return `Total,,,${total}`;
};
const createInventoryValuesReport = items => {
const itemRows = items.map(generateItemRow).join("\n");
const totalRow = generateTotalRow(items);
const reportContents = itemRows + "\n" + totalRow;
fs.writeFileSync("/tmp/inventoryValues.csv", reportContents);
};
module.exports = {
generateItemRow,
generateTotalRow,
createInventoryValuesReport
};
================================================
FILE: chapter9/2_writing_a_js_module_using_tdd/3_creating_report/inventoryReport.test.js
================================================
const fs = require("fs");
const {
generateItemRow,
generateTotalRow,
createInventoryValuesReport
} = require("./inventoryReport");
describe("generateItemRow", () => {
test("generating an item's row", () => {
expect(generateItemRow({ name: "macaroon", quantity: 12, price: 3 })).toBe(
"macaroon,12,3,36"
);
expect(
generateItemRow({ name: "cheesecake", quantity: 6, price: 12 })
).toBe("cheesecake,6,12,72");
expect(generateItemRow({ name: "apple pie", quantity: 5, price: 15 })).toBe(
"apple pie,5,15,75"
);
});
test("ommitting soldout items", () => {
expect(generateItemRow({ name: "macaroon", quantity: 0, price: 3 })).toBe(
null
);
expect(
generateItemRow({ name: "cheesecake", quantity: 0, price: 12 })
).toBe(null);
});
test("ommitting free items", () => {
expect(
generateItemRow({ name: "plastic cups", quantity: 99, price: 0 })
).toBe(null);
expect(generateItemRow({ name: "napkins", quantity: 200, price: 0 })).toBe(
null
);
});
});
describe("generateTotalRow", () => {
test("generating a total row", () => {
const items = [
{ name: "apple pie", quantity: 3, price: 15 },
{ name: "plastic cups", quantity: 0, price: 55 },
{ name: "macaroon", quantity: 12, price: 3 },
{ name: "cheesecake", quantity: 0, price: 12 }
];
expect(generateTotalRow(items)).toBe("Total,,,81");
expect(generateTotalRow(items.slice(1))).toBe("Total,,,36");
expect(generateTotalRow(items.slice(3))).toBe("Total,,,0");
});
});
describe("createInventoryValuesReport", () => {
test("creating reports", () => {
const items = [
{ name: "apple pie", quantity: 3, price: 15 },
{ name: "cheesecake", quantity: 2, price: 12 },
{ name: "macaroon", quantity: 20, price: 3 }
];
createInventoryValuesReport(items);
expect(fs.readFileSync("/tmp/inventoryValues.csv", "utf8")).toBe(
"apple pie,3,15,45\ncheesecake,2,12,24\nmacaroon,20,3,60\nTotal,,,129"
);
createInventoryValuesReport(items.slice(1));
expect(fs.readFileSync("/tmp/inventoryValues.csv", "utf8")).toBe(
"cheesecake,2,12,24\nmacaroon,20,3,60\nTotal,,,84"
);
createInventoryValuesReport(items.slice(2));
expect(fs.readFileSync("/tmp/inventoryValues.csv", "utf8")).toBe(
"macaroon,20,3,60\nTotal,,,60"
);
});
});
================================================
FILE: chapter9/2_writing_a_js_module_using_tdd/3_creating_report/package.json
================================================
{
"name": "3_creating_report",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^26.1.0"
}
}
================================================
FILE: package.json
================================================
{
"name": "testing-javascript-applications",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"lint:fix": "prettier --write .",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/lucasfcosta/testing-javascript-applications.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/lucasfcosta/testing-javascript-applications/issues"
},
"homepage": "https://github.com/lucasfcosta/testing-javascript-applications#readme",
"devDependencies": {
"husky": "^3.1.0",
"prettier": "^1.19.1",
"pretty-quick": "^2.0.1"
},
"husky": {
"hooks": {
"pre-commit": "pretty-quick --staged"
}
}
}