master 26dfe4572bcb cached
852 files
773.9 KB
239.6k tokens
94 symbols
1 requests
Download .txt
Showing preview only (992K chars total). Download the full file or copy to clipboard to get everything.
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
================================================
<h1 align=center>Testing JavaScript Applications</h1>
<h4 align=center><i>A <a href="https://www.manning.com/">Manning</a> book by <a href="https://www.lucasfcosta.com">Lucas da Costa</a></i></h4>

<br>

<h5 align=center>➡ Available at <a href="https://www.manning.com/books/testing-javascript-applications">Manning.com</a></h5>

<br>

## 📕 About this book

Testing JavaScript Applications will help you write high-quality software in less time, with more confidence.

In the last five years, I have been deeply involved in the JavaScript testing scene. I am a core-maintainer of both Chai.js and Sinon.js, two of the most popular testing libraries in JavaScript, and I closely follow projects like Jest and Mocha. In this book, I expect to teach you what I've learned during those years in which I've been involved in vetting and implementing features, defining best-practices, and designing the libraries that thousands of people use every day.

Throughout this book's pages, you will learn how to write effective tests through various diagrams and practical examples we'll build together. Because I believe the best way to learn something is by doing it yourself, **I've put in this repository all of the book's examples**, so that you can experiment with them on your own and compare your solutions to the ones which I've implemented.

Besides covering specific tools, like Jest, and techniques, like TDD, it will teach you how to think about tests from a business perspective. You will learn what to take into account when designing tests, and how to make optimal decisions for _your_ specific context.

I've written Testing JavaScript Applications thinking mostly about Junior Developers. They are the ones who will benefit the most from this book's approach to tests, which covers both the _"hows"_ and the _"whys"_ of writing automated tests.

Even though Junior Developers will be the ones who will benefit the most from this book, it also contains topics which cater to senior and mid-level developers. It includes my thoughts on how tests impact a business, how they structure relationships within teams, and other aspects involved in building what I'd call "a culture of quality".

To get the most out of "Testing JavaScript Applications", you must have a basic understanding of JavaScript. You should know how to use objects, functions, callbacks, and, especially, Promises. Basic knowledge of CSS and HTML is also required for the chapters in which we'll test a front-end application.

Because I've tried to make the examples in this book as close to reality as possible, there will be chapters in which we'll test a Node.js back-end application, and others in which we'll test a React application. Therefore, it will be necessary to have elementary knowledge about these tools. Reading their "getting started" guides should take you approximately 15 minutes each, and will be enough for you to follow along with the testing examples.

If you have any questions, comments, or suggestions, I'd love to hear them. With your invaluable feedback, we'll build a better book together.

I wish you all a productive, pleasant, and exciting journey.

_— Lucas da Costa_

<br>

## 📁 About this repository

**This repository contains all of the examples in the book _Testing JavaScript Applications_**.

I have organised examples in a separate folder for each chapter and, within a chapter, I've separated them by section. Sometimes, even within sections, you will find sub-divisions with the multiple stages of an exercise or with different approaches to solving the same problem.

<br>

## 💻 Running these examples

I've built these examples using [Node.js](https://nodejs.org) v12 and [NPM](https://www.npmjs.com) v6.

Before executing any of these examples, `cd` into the folder you want to try and run `npm install` to install its dependencies.

Most of the examples have an [NPM script](https://www.keithcirkel.co.uk/how-to-use-npm-as-a-build-tool/) named `test`. Which means that you can execute tests for that example by using `npm test`.

<br>

## 🤝 How to contribute

Together, we can build better content.

If you happen to find _any_ problems in _any_ of these examples, feel free to submit a Pull Request explaining what the problem was and how you solved it.

In case you have a _better_ solution for any of the exercises, I'd love to see it. In that case, explain in your PR why you think that the proposed solution is better. Even though I might not agree, I will treat everyone with the respect they deserve, and will carefully read through their thoughts and comments.

<br>

## 🔗 Where to find more about me and my book

I'd love to hear your thoughts on the book and keep in touch with you.

Send me a tweet [@thewizardlucas](https://twitter.com/thewizardlucas) and let's have a chat!

- [lucasfcosta.com - My Personal Website](https://lucasfcosta.com)
- [@thewizardlucas on Twitter](https://twitter.com/thewizardlucas)
- [@lucasfcosta on GitHub](https://github.com/lucasfcosta)
- [LinkedIn](https://www.linkedin.com/in/lucasfdacosta)
- [Manning Books' Website](https://www.manning.com/)

For discussing any topics related to this book, you can email me at testing.javascript.applications@lucasfcosta.com.


================================================
FILE: chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/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/1_setting_up_cypress/cypress/integration/examples/actions.spec.js
================================================
/// <reference types="cypress" />

context("Actions", () => {
  beforeEach(() => {
    cy.visit("https://example.cypress.io/commands/actions");
  });

  // https://on.cypress.io/interacting-with-elements

  it(".type() - type into a DOM element", () => {
    // https://on.cypress.io/type
    cy.get(".action-email")
      .type("fake@email.com")
      .should("have.value", "fake@email.com")

      // .type() with special character sequences
      .type("{leftarrow}{rightarrow}{uparrow}{downarrow}")
      .type("{del}{selectall}{backspace}")

      // .type() with key modifiers
      .type("{alt}{option}") //these are equivalent
      .type("{ctrl}{control}") //these are equivalent
      .type("{meta}{command}{cmd}") //these are equivalent
      .type("{shift}")

      // Delay each keypress by 0.1 sec
      .type("slow.typing@email.com", { delay: 100 })
      .should("have.value", "slow.typing@email.com");

    cy.get(".action-disabled")
      // Ignore error checking prior to type
      // like whether the input is visible or disabled
      .type("disabled error checking", { force: true })
      .should("have.value", "disabled error checking");
  });

  it(".focus() - focus on a DOM element", () => {
    // https://on.cypress.io/focus
    cy.get(".action-focus")
      .focus()
      .should("have.class", "focus")
      .prev()
      .should("have.attr", "style", "color: orange;");
  });

  it(".blur() - blur off a DOM element", () => {
    // https://on.cypress.io/blur
    cy.get(".action-blur")
      .type("About to blur")
      .blur()
      .should("have.class", "error")
      .prev()
      .should("have.attr", "style", "color: red;");
  });

  it(".clear() - clears an input or textarea element", () => {
    // https://on.cypress.io/clear
    cy.get(".action-clear")
      .type("Clear this text")
      .should("have.value", "Clear this text")
      .clear()
      .should("have.value", "");
  });

  it(".submit() - submit a form", () => {
    // https://on.cypress.io/submit
    cy.get(".action-form")
      .find('[type="text"]')
      .type("HALFOFF");

    cy.get(".action-form")
      .submit()
      .next()
      .should("contain", "Your form has been submitted!");
  });

  it(".click() - click on a DOM element", () => {
    // https://on.cypress.io/click
    cy.get(".action-btn").click();

    // You can click on 9 specific positions of an element:
    //  -----------------------------------
    // | topLeft        top       topRight |
    // |                                   |
    // |                                   |
    // |                                   |
    // | left          center        right |
    // |                                   |
    // |                                   |
    // |                                   |
    // | bottomLeft   bottom   bottomRight |
    //  -----------------------------------

    // clicking in the center of the element is the default
    cy.get("#action-canvas").click();

    cy.get("#action-canvas").click("topLeft");
    cy.get("#action-canvas").click("top");
    cy.get("#action-canvas").click("topRight");
    cy.get("#action-canvas").click("left");
    cy.get("#action-canvas").click("right");
    cy.get("#action-canvas").click("bottomLeft");
    cy.get("#action-canvas").click("bottom");
    cy.get("#action-canvas").click("bottomRight");

    // .click() accepts an x and y coordinate
    // that controls where the click occurs :)

    cy.get("#action-canvas")
      .click(80, 75) // click 80px on x coord and 75px on y coord
      .click(170, 75)
      .click(80, 165)
      .click(100, 185)
      .click(125, 190)
      .click(150, 185)
      .click(170, 165);

    // click multiple elements by passing multiple: true
    cy.get(".action-labels>.label").click({ multiple: true });

    // Ignore error checking prior to clicking
    cy.get(".action-opacity>.btn").click({ force: true });
  });

  it(".dblclick() - double click on a DOM element", () => {
    // https://on.cypress.io/dblclick

    // Our app has a listener on 'dblclick' event in our 'scripts.js'
    // that hides the div and shows an input on double click
    cy.get(".action-div")
      .dblclick()
      .should("not.be.visible");
    cy.get(".action-input-hidden").should("be.visible");
  });

  it(".rightclick() - right click on a DOM element", () => {
    // https://on.cypress.io/rightclick

    // Our app has a listener on 'contextmenu' event in our 'scripts.js'
    // that hides the div and shows an input on right click
    cy.get(".rightclick-action-div")
      .rightclick()
      .should("not.be.visible");
    cy.get(".rightclick-action-input-hidden").should("be.visible");
  });

  it(".check() - check a checkbox or radio element", () => {
    // https://on.cypress.io/check

    // By default, .check() will check all
    // matching checkbox or radio elements in succession, one after another
    cy.get('.action-checkboxes [type="checkbox"]')
      .not("[disabled]")
      .check()
      .should("be.checked");

    cy.get('.action-radios [type="radio"]')
      .not("[disabled]")
      .check()
      .should("be.checked");

    // .check() accepts a value argument
    cy.get('.action-radios [type="radio"]')
      .check("radio1")
      .should("be.checked");

    // .check() accepts an array of values
    cy.get('.action-multiple-checkboxes [type="checkbox"]')
      .check(["checkbox1", "checkbox2"])
      .should("be.checked");

    // Ignore error checking prior to checking
    cy.get(".action-checkboxes [disabled]")
      .check({ force: true })
      .should("be.checked");

    cy.get('.action-radios [type="radio"]')
      .check("radio3", { force: true })
      .should("be.checked");
  });

  it(".uncheck() - uncheck a checkbox element", () => {
    // https://on.cypress.io/uncheck

    // By default, .uncheck() will uncheck all matching
    // checkbox elements in succession, one after another
    cy.get('.action-check [type="checkbox"]')
      .not("[disabled]")
      .uncheck()
      .should("not.be.checked");

    // .uncheck() accepts a value argument
    cy.get('.action-check [type="checkbox"]')
      .check("checkbox1")
      .uncheck("checkbox1")
      .should("not.be.checked");

    // .uncheck() accepts an array of values
    cy.get('.action-check [type="checkbox"]')
      .check(["checkbox1", "checkbox3"])
      .uncheck(["checkbox1", "checkbox3"])
      .should("not.be.checked");

    // Ignore error checking prior to unchecking
    cy.get(".action-check [disabled]")
      .uncheck({ force: true })
      .should("not.be.checked");
  });

  it(".select() - select an option in a <select> 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
================================================
/// <reference types="cypress" />

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
================================================
/// <reference types="cypress" />

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 <td> 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 <td> 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
================================================
/// <reference types="cypress" />

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
================================================
/// <reference types="cypress" />

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
================================================
/// <reference types="cypress" />

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
================================================
/// <reference types="cypress" />

/// 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
================================================
/// <reference types="cypress" />

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
================================================
/// <reference types="cypress" />

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
================================================
/// <reference types="cypress" />

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
================================================
/// <reference types="cypress" />

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
================================================
/// <reference types="cypress" />

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
================================================
/// <reference types="cypress" />

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
================================================
/// <reference types="cypress" />
// 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
================================================
/// <reference types="cypress" />

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
================================================
/// <reference types="cypress" />

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 <img> element and set its src to the dataUrl
        let img = Cypress.$("<img />", { 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<string>
     */
    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
================================================
/// <reference types="cypress" />

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
================================================
/// <reference types="cypress" />

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
================================================
/// <reference types="cypress" />

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
================================================
/// <reference types="cypress" />
// ***********************************************************
// 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
================================================
/// <reference types="cypress" />
// ***********************************************************
// 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
================================================
/// <reference types="cypress" />
// ***********************************************************
// 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
================================================
/// <reference types="cypress" />
// ***********************************************************
// 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
================================================
/// <reference types="cypress" />
// ***********************************************************
// 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
================================================
/// <reference types="cypress" />
// ***********************************************************
// 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
================================================
/// <reference types="cypress" />
// ***********************************************************
// 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
================================================
/// <reference types="cypress" />
// ***********************************************************
// 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
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Inventory Manager</title>
    <style>
      .almost-soldout {
        color: red;
      }

      a {
        color: inherit;
      }

      @media only percy {
        p:not(:first-child) {
          visibility: hidden;
        }
      }
    </style>
  </head>
  <body>
    <h1 data-testid="page-header">Inventory Contents</h1>
    <ul id="item-list"></ul>
    <p id="error-msg"></p>
    <form id="add-item-form">
      <input type="text" name="name" placeholder="Item name" />
      <input type="number" name="quantity" placeholder="Quantity" />
      <button type="submit">Add to inventory</button>
    </form>

    <button id="undo-button">Undo</button>

    <script src="bundle.js"></script>
  </body>
</html>


================================================
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: [
    "<rootDir>/setupGlobalFetch.js",
    "<rootDir>/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: [
    "<rootDir>/truncateTables.js",
    "<rootDir>/seedUser.js",
    "<rootDir>/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 || 
Download .txt
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
Download .txt
SYMBOL INDEX (94 symbols across 33 files)

FILE: chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/misc.spec.js
  method onBeforeScreenshot (line 104) | onBeforeScreenshot() {}
  method onAfterScreenshot (line 105) | onAfterScreenshot() {}

FILE: chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/navigation.spec.js
  method onBeforeLoad (line 50) | onBeforeLoad(contentWindow) {
  method onLoad (line 54) | onLoad(contentWindow) {

FILE: chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/spies_stubs_clocks.spec.js
  method foo (line 11) | foo() {}
  method foo (line 29) | foo(x) {
  method foo (line 57) | foo(a, b) {
  method greet (line 110) | greet(name) {
  method add (line 141) | add(a, b) {

FILE: chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/utilities.spec.js
  function waitOneSecond (line 124) | function waitOneSecond() {

FILE: chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/pageObjects/inventoryManagement.js
  class InventoryManagement (line 1) | class InventoryManagement {
    method visit (line 2) | static visit() {
    method enterItemName (line 6) | static enterItemName(itemName) {
    method enterQuantity (line 13) | static enterQuantity(quantity) {
    method getSubmitButton (line 20) | static getSubmitButton() {
    method addItem (line 24) | static addItem(itemName, quantity) {
    method findItemEntry (line 30) | static findItemEntry(itemName, quantity) {
    method undo (line 34) | static undo() {
    method findAction (line 41) | static findAction(inventoryState) {

FILE: chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/pageObjects/inventoryManagement.js
  class InventoryManagement (line 1) | class InventoryManagement {
    method visit (line 2) | static visit() {
    method enterItemName (line 6) | static enterItemName(itemName) {
    method enterQuantity (line 13) | static enterQuantity(quantity) {
    method getSubmitButton (line 20) | static getSubmitButton() {
    method addItem (line 24) | static addItem(itemName, quantity) {
    method findItemEntry (line 30) | static findItemEntry(itemName, quantity) {
    method undo (line 34) | static undo() {
    method findAction (line 41) | static findAction(inventoryState) {

FILE: chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/pageObjects/inventoryManagement.js
  class InventoryManagement (line 1) | class InventoryManagement {
    method visit (line 2) | static visit() {
    method enterItemName (line 6) | static enterItemName(itemName) {
    method enterQuantity (line 13) | static enterQuantity(quantity) {
    method getSubmitButton (line 20) | static getSubmitButton() {
    method addItem (line 24) | static addItem(itemName, quantity) {
    method findItemEntry (line 30) | static findItemEntry(itemName, quantity) {
    method undo (line 34) | static undo() {
    method findAction (line 41) | static findAction(inventoryState) {

FILE: chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/pageObjects/inventoryManagement.js
  class InventoryManagement (line 1) | class InventoryManagement {
    method visit (line 2) | static visit() {
    method enterItemName (line 6) | static enterItemName(itemName) {
    method enterQuantity (line 13) | static enterQuantity(quantity) {
    method getSubmitButton (line 20) | static getSubmitButton() {
    method addItem (line 24) | static addItem(itemName, quantity) {
    method findItemEntry (line 30) | static findItemEntry(itemName, quantity) {
    method undo (line 34) | static undo() {
    method findAction (line 41) | static findAction(inventoryState) {

FILE: chapter11/4_visual_regression_tests/cypress/pageObjects/inventoryManagement.js
  class InventoryManagement (line 1) | class InventoryManagement {
    method visit (line 2) | static visit() {
    method enterItemName (line 6) | static enterItemName(itemName) {
    method enterQuantity (line 13) | static enterQuantity(quantity) {
    method getSubmitButton (line 20) | static getSubmitButton() {
    method addItem (line 24) | static addItem(itemName, quantity) {
    method findItemEntry (line 30) | static findItemEntry(itemName, quantity) {
    method undo (line 34) | static undo() {
    method findAction (line 41) | static findAction(inventoryState) {

FILE: chapter11/client/inventoryController.js
  constant API_ADDR (line 3) | const API_ADDR = "http://localhost:3000";

FILE: chapter11/server/server.js
  constant PORT (line 17) | const PORT = process.env.NODE_ENV === "test" ? 5000 : 3000;

FILE: chapter13/1_type_systems/2_with_types/orderQueue.ts
  type OrderItems (line 1) | type OrderItems = { 0: string } & Array<string>;
  type Order (line 3) | type Order = {
  type DoneOrder (line 8) | type DoneOrder = Order & { status: "done" };

FILE: chapter2/2_unit_tests/1_raw_tests/Cart.js
  class Cart (line 1) | class Cart {
    method constructor (line 2) | constructor() {
    method addToCart (line 6) | addToCart(item) {

FILE: chapter2/2_unit_tests/2_node_assert/Cart.js
  class Cart (line 1) | class Cart {
    method constructor (line 2) | constructor() {
    method addToCart (line 6) | addToCart(item) {

FILE: chapter2/2_unit_tests/3_jest_multiple_tests/Cart.js
  class Cart (line 1) | class Cart {
    method constructor (line 2) | constructor() {
    method addToCart (line 6) | addToCart(item) {
    method removeFromCart (line 10) | removeFromCart(item) {

FILE: chapter2/2_unit_tests/4_jest_assertions/Cart.js
  class Cart (line 1) | class Cart {
    method constructor (line 2) | constructor() {
    method addToCart (line 6) | addToCart(item) {
    method removeFromCart (line 10) | removeFromCart(item) {

FILE: chapter2/2_unit_tests/5_npm_scripts/Cart.js
  class Cart (line 1) | class Cart {
    method constructor (line 2) | constructor() {
    method addToCart (line 6) | addToCart(item) {
    method removeFromCart (line 10) | removeFromCart(item) {

FILE: chapter3/4_code_coverage/2_what_coverage_is_good_for/math.js
  function sumOrDivide (line 1) | function sumOrDivide(a, b) {

FILE: chapter6/4_testing_and_browser_apis/server/server.js
  constant PORT (line 15) | const PORT = process.env.NODE_ENV === "test" ? 5000 : 3000;

FILE: chapter6/5_web_sockets_and_http_requests/1_http_requests/inventoryController.js
  constant API_ADDR (line 3) | const API_ADDR = "http://localhost:3000";

FILE: chapter6/5_web_sockets_and_http_requests/2_web_sockets/inventoryController.js
  constant API_ADDR (line 3) | const API_ADDR = "http://localhost:3000";

FILE: chapter6/5_web_sockets_and_http_requests/server/server.js
  constant PORT (line 17) | const PORT = process.env.NODE_ENV === "test" ? 5000 : 3000;

FILE: chapter7/2_an_overview_of_react_testing_libraries/2_react_testing_library/constants.js
  constant API_ADDR (line 1) | const API_ADDR = "http://localhost:3000";

FILE: chapter7/server/server.js
  constant PORT (line 17) | const PORT = process.env.NODE_ENV === "test" ? 5000 : 3000;

FILE: chapter8/1_testing_component_interaction/1_component_integration_tests/constants.js
  constant API_ADDR (line 1) | const API_ADDR = "http://localhost:3000";

FILE: chapter8/1_testing_component_interaction/2_stubbing_components/constants.js
  constant API_ADDR (line 1) | const API_ADDR = "http://localhost:3000";

FILE: chapter8/2_snapshot_testing/1_component_snapshots/constants.js
  constant API_ADDR (line 1) | const API_ADDR = "http://localhost:3000";

FILE: chapter8/3_testing_styles/1_css_classes/constants.js
  constant API_ADDR (line 1) | const API_ADDR = "http://localhost:3000";

FILE: chapter8/3_testing_styles/2_style_props/constants.js
  constant API_ADDR (line 1) | const API_ADDR = "http://localhost:3000";

FILE: chapter8/3_testing_styles/3_css_in_js_snapshots/constants.js
  constant API_ADDR (line 1) | const API_ADDR = "http://localhost:3000";

FILE: chapter8/4_component_stories/1_stories/constants.js
  constant API_ADDR (line 1) | const API_ADDR = "http://localhost:3000";

FILE: chapter8/4_component_stories/2_documentation/constants.js
  constant API_ADDR (line 1) | const API_ADDR = "http://localhost:3000";

FILE: chapter8/server/server.js
  constant PORT (line 17) | const PORT = process.env.NODE_ENV === "test" ? 5000 : 3000;
Condensed preview — 852 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (931K chars).
[
  {
    "path": ".gitignore",
    "chars": 1692,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs."
  },
  {
    "path": "LICENSE",
    "chars": 11357,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "README.md",
    "chars": 5229,
    "preview": "<h1 align=center>Testing JavaScript Applications</h1>\n<h4 align=center><i>A <a href=\"https://www.manning.com/\">Manning</"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/fixtures/example.json",
    "chars": 155,
    "preview": "{\n  \"name\": \"Using fixtures to represent data\",\n  \"email\": \"hello@cypress.io\",\n  \"body\": \"Fixtures are a great way to mo"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/actions.spec.js",
    "chars": 10705,
    "preview": "/// <reference types=\"cypress\" />\n\ncontext(\"Actions\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/aliasing.spec.js",
    "chars": 1158,
    "preview": "/// <reference types=\"cypress\" />\n\ncontext(\"Aliasing\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypres"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/assertions.spec.js",
    "chars": 5718,
    "preview": "/// <reference types=\"cypress\" />\n\ncontext(\"Assertions\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypr"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/connectors.spec.js",
    "chars": 2865,
    "preview": "/// <reference types=\"cypress\" />\n\ncontext(\"Connectors\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypr"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/cookies.spec.js",
    "chars": 2357,
    "preview": "/// <reference types=\"cypress\" />\n\ncontext(\"Cookies\", () => {\n  beforeEach(() => {\n    Cypress.Cookies.debug(true);\n\n   "
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/cypress_api.spec.js",
    "chars": 6461,
    "preview": "/// <reference types=\"cypress\" />\n\ncontext(\"Cypress.Commands\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://exampl"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/files.spec.js",
    "chars": 3651,
    "preview": "/// <reference types=\"cypress\" />\n\n/// JSON fixture file can be loaded directly using\n// the built-in JavaScript bundler"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/local_storage.spec.js",
    "chars": 1973,
    "preview": "/// <reference types=\"cypress\" />\n\ncontext(\"Local Storage\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.c"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/location.spec.js",
    "chars": 1107,
    "preview": "/// <reference types=\"cypress\" />\n\ncontext(\"Location\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypres"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/misc.spec.js",
    "chars": 3198,
    "preview": "/// <reference types=\"cypress\" />\n\ncontext(\"Misc\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress.io"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/navigation.spec.js",
    "chars": 1648,
    "preview": "/// <reference types=\"cypress\" />\n\ncontext(\"Navigation\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypr"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/network_requests.spec.js",
    "chars": 7598,
    "preview": "/// <reference types=\"cypress\" />\n\ncontext(\"Network Requests\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://exampl"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/querying.spec.js",
    "chars": 3599,
    "preview": "/// <reference types=\"cypress\" />\n\ncontext(\"Querying\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypres"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/spies_stubs_clocks.spec.js",
    "chars": 6084,
    "preview": "/// <reference types=\"cypress\" />\n// remove no check once Cypress.sinon is typed\n// https://github.com/cypress-io/cypres"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/traversal.spec.js",
    "chars": 3832,
    "preview": "/// <reference types=\"cypress\" />\n\ncontext(\"Traversal\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypre"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/utilities.spec.js",
    "chars": 4219,
    "preview": "/// <reference types=\"cypress\" />\n\ncontext(\"Utilities\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypre"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/viewport.spec.js",
    "chars": 1771,
    "preview": "/// <reference types=\"cypress\" />\n\ncontext(\"Viewport\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypres"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/waiting.spec.js",
    "chars": 1005,
    "preview": "/// <reference types=\"cypress\" />\n\ncontext(\"Waiting\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/integration/examples/window.spec.js",
    "chars": 625,
    "preview": "/// <reference types=\"cypress\" />\n\ncontext(\"Window\", () => {\n  beforeEach(() => {\n    cy.visit(\"https://example.cypress."
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/plugins/index.js",
    "chars": 719,
    "preview": "/// <reference types=\"cypress\" />\n// ***********************************************************\n// This example plugins"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/support/commands.js",
    "chars": 838,
    "preview": "// ***********************************************\n// This example commands.js shows you how to\n// create various custom"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress/support/index.js",
    "chars": 671,
    "preview": "// ***********************************************************\n// This example support/index.js is processed and\n// load"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/cypress.json",
    "chars": 3,
    "preview": "{}\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/1_setting_up_cypress/package.json",
    "chars": 264,
    "preview": "{\n  \"name\": \"1_setting_up_cypress\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    "
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/2_writing_your_first_tests/cypress/dbConnection.js",
    "chars": 206,
    "preview": "const environmentName = process.env.NODE_ENV;\nconst db = require(\"knex\")(require(\"./knexfile\")[environmentName]);\n\nconst"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/2_writing_your_first_tests/cypress/fixtures/example.json",
    "chars": 155,
    "preview": "{\n  \"name\": \"Using fixtures to represent data\",\n  \"email\": \"hello@cypress.io\",\n  \"body\": \"Fixtures are a great way to mo"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/2_writing_your_first_tests/cypress/integration/itemSubmission.spec.js",
    "chars": 2980,
    "preview": "describe(\"item submission\", () => {\n  beforeEach(() => cy.task(\"emptyInventory\"));\n\n  it(\"can add items through the form"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/2_writing_your_first_tests/cypress/knexfile.js",
    "chars": 150,
    "preview": "module.exports = {\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"../../server/dev.sqlite\" },\n    "
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/2_writing_your_first_tests/cypress/plugins/dbPlugin.js",
    "chars": 290,
    "preview": "const { db } = require(\"../dbConnection\");\n\nconst dbPlugin = (on, config) => {\n  on(\n    \"task\",\n    {\n      emptyInvent"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/2_writing_your_first_tests/cypress/plugins/index.js",
    "chars": 639,
    "preview": "/// <reference types=\"cypress\" />\n// ***********************************************************\n// This example plugins"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/2_writing_your_first_tests/cypress/support/commands.js",
    "chars": 838,
    "preview": "// ***********************************************\n// This example commands.js shows you how to\n// create various custom"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/2_writing_your_first_tests/cypress/support/index.js",
    "chars": 671,
    "preview": "// ***********************************************************\n// This example support/index.js is processed and\n// load"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/2_writing_your_first_tests/cypress.json",
    "chars": 30,
    "preview": "{\n  \"nodeVersion\": \"system\"\n}\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/2_writing_your_first_tests/package.json",
    "chars": 394,
    "preview": "{\n  \"name\": \"2_writing_your_first_tests\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": "
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/3_sending_http_requests/cypress/dbConnection.js",
    "chars": 206,
    "preview": "const environmentName = process.env.NODE_ENV;\nconst db = require(\"knex\")(require(\"./knexfile\")[environmentName]);\n\nconst"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/3_sending_http_requests/cypress/fixtures/example.json",
    "chars": 155,
    "preview": "{\n  \"name\": \"Using fixtures to represent data\",\n  \"email\": \"hello@cypress.io\",\n  \"body\": \"Fixtures are a great way to mo"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/3_sending_http_requests/cypress/integration/itemListUpdates.spec.js",
    "chars": 800,
    "preview": "describe(\"item list updates\", () => {\n  beforeEach(() => cy.task(\"emptyInventory\"));\n\n  describe(\"when the application l"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/3_sending_http_requests/cypress/integration/itemSubmission.spec.js",
    "chars": 2980,
    "preview": "describe(\"item submission\", () => {\n  beforeEach(() => cy.task(\"emptyInventory\"));\n\n  it(\"can add items through the form"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/3_sending_http_requests/cypress/knexfile.js",
    "chars": 150,
    "preview": "module.exports = {\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"../../server/dev.sqlite\" },\n    "
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/3_sending_http_requests/cypress/plugins/dbPlugin.js",
    "chars": 290,
    "preview": "const { db } = require(\"../dbConnection\");\n\nconst dbPlugin = (on, config) => {\n  on(\n    \"task\",\n    {\n      emptyInvent"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/3_sending_http_requests/cypress/plugins/index.js",
    "chars": 639,
    "preview": "/// <reference types=\"cypress\" />\n// ***********************************************************\n// This example plugins"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/3_sending_http_requests/cypress/support/commands.js",
    "chars": 258,
    "preview": "Cypress.Commands.add(\"addItem\", (itemName, quantity) => {\n  return cy.request({\n    url: `http://localhost:3000/inventor"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/3_sending_http_requests/cypress/support/index.js",
    "chars": 671,
    "preview": "// ***********************************************************\n// This example support/index.js is processed and\n// load"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/3_sending_http_requests/cypress.json",
    "chars": 30,
    "preview": "{\n  \"nodeVersion\": \"system\"\n}\n"
  },
  {
    "path": "chapter11/1_writing_end_to_end_tests/3_sending_http_requests/package.json",
    "chars": 391,
    "preview": "{\n  \"name\": \"3_sending_http_requests\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n "
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/dbConnection.js",
    "chars": 206,
    "preview": "const environmentName = process.env.NODE_ENV;\nconst db = require(\"knex\")(require(\"./knexfile\")[environmentName]);\n\nconst"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/fixtures/example.json",
    "chars": 155,
    "preview": "{\n  \"name\": \"Using fixtures to represent data\",\n  \"email\": \"hello@cypress.io\",\n  \"body\": \"Fixtures are a great way to mo"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/integration/itemListUpdates.spec.js",
    "chars": 908,
    "preview": "import { InventoryManagement } from \"../pageObjects/inventoryManagement\";\n\ndescribe(\"item list updates\", () => {\n  befor"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/integration/itemSubmission.spec.js",
    "chars": 1702,
    "preview": "import { InventoryManagement } from \"../pageObjects/inventoryManagement\";\n\ndescribe(\"item submission\", () => {\n  beforeE"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/knexfile.js",
    "chars": 150,
    "preview": "module.exports = {\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"../../server/dev.sqlite\" },\n    "
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/pageObjects/inventoryManagement.js",
    "chars": 1174,
    "preview": "export class InventoryManagement {\n  static visit() {\n    cy.visit(\"http://localhost:8080\");\n  }\n\n  static enterItemName"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/plugins/dbPlugin.js",
    "chars": 290,
    "preview": "const { db } = require(\"../dbConnection\");\n\nconst dbPlugin = (on, config) => {\n  on(\n    \"task\",\n    {\n      emptyInvent"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/plugins/index.js",
    "chars": 639,
    "preview": "/// <reference types=\"cypress\" />\n// ***********************************************************\n// This example plugins"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/support/commands.js",
    "chars": 258,
    "preview": "Cypress.Commands.add(\"addItem\", (itemName, quantity) => {\n  return cy.request({\n    url: `http://localhost:3000/inventor"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress/support/index.js",
    "chars": 671,
    "preview": "// ***********************************************************\n// This example support/index.js is processed and\n// load"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/cypress.json",
    "chars": 30,
    "preview": "{\n  \"nodeVersion\": \"system\"\n}\n"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/1_page_objects/package.json",
    "chars": 382,
    "preview": "{\n  \"name\": \"1_page_objects\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"cypre"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/dbConnection.js",
    "chars": 206,
    "preview": "const environmentName = process.env.NODE_ENV;\nconst db = require(\"knex\")(require(\"./knexfile\")[environmentName]);\n\nconst"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/fixtures/example.json",
    "chars": 155,
    "preview": "{\n  \"name\": \"Using fixtures to represent data\",\n  \"email\": \"hello@cypress.io\",\n  \"body\": \"Fixtures are a great way to mo"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/integration/itemListUpdates.spec.js",
    "chars": 908,
    "preview": "import { InventoryManagement } from \"../pageObjects/inventoryManagement\";\n\ndescribe(\"item list updates\", () => {\n  befor"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/integration/itemSubmission.spec.js",
    "chars": 1911,
    "preview": "import { InventoryManagement } from \"../pageObjects/inventoryManagement\";\n\ndescribe(\"item submission\", () => {\n  beforeE"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/knexfile.js",
    "chars": 150,
    "preview": "module.exports = {\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"../../server/dev.sqlite\" },\n    "
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/pageObjects/inventoryManagement.js",
    "chars": 1174,
    "preview": "export class InventoryManagement {\n  static visit() {\n    cy.visit(\"http://localhost:8080\");\n  }\n\n  static enterItemName"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/plugins/dbPlugin.js",
    "chars": 290,
    "preview": "const { db } = require(\"../dbConnection\");\n\nconst dbPlugin = (on, config) => {\n  on(\n    \"task\",\n    {\n      emptyInvent"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/plugins/index.js",
    "chars": 639,
    "preview": "/// <reference types=\"cypress\" />\n// ***********************************************************\n// This example plugins"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/support/commands.js",
    "chars": 258,
    "preview": "Cypress.Commands.add(\"addItem\", (itemName, quantity) => {\n  return cy.request({\n    url: `http://localhost:3000/inventor"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress/support/index.js",
    "chars": 671,
    "preview": "// ***********************************************************\n// This example support/index.js is processed and\n// load"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/cypress.json",
    "chars": 30,
    "preview": "{\n  \"nodeVersion\": \"system\"\n}\n"
  },
  {
    "path": "chapter11/2_best_practices_for_end_to_end_tests/2_application_actions/package.json",
    "chars": 389,
    "preview": "{\n  \"name\": \"2_application_actions\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n   "
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/dbConnection.js",
    "chars": 206,
    "preview": "const environmentName = process.env.NODE_ENV;\nconst db = require(\"knex\")(require(\"./knexfile\")[environmentName]);\n\nconst"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/fixtures/example.json",
    "chars": 155,
    "preview": "{\n  \"name\": \"Using fixtures to represent data\",\n  \"email\": \"hello@cypress.io\",\n  \"body\": \"Fixtures are a great way to mo"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/integration/itemListUpdates.spec.js",
    "chars": 1003,
    "preview": "import { InventoryManagement } from \"../pageObjects/inventoryManagement\";\n\ndescribe(\"item list updates\", () => {\n  befor"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/integration/itemSubmission.spec.js",
    "chars": 1957,
    "preview": "import { InventoryManagement } from \"../pageObjects/inventoryManagement\";\n\ndescribe(\"item submission\", () => {\n  beforeE"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/knexfile.js",
    "chars": 150,
    "preview": "module.exports = {\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"../../server/dev.sqlite\" },\n    "
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/pageObjects/inventoryManagement.js",
    "chars": 1174,
    "preview": "export class InventoryManagement {\n  static visit() {\n    cy.visit(\"http://localhost:8080\");\n  }\n\n  static enterItemName"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/plugins/dbPlugin.js",
    "chars": 290,
    "preview": "const { db } = require(\"../dbConnection\");\n\nconst dbPlugin = (on, config) => {\n  on(\n    \"task\",\n    {\n      emptyInvent"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/plugins/index.js",
    "chars": 639,
    "preview": "/// <reference types=\"cypress\" />\n// ***********************************************************\n// This example plugins"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/support/commands.js",
    "chars": 288,
    "preview": "import \"cypress-wait-until\";\n\nCypress.Commands.add(\"addItem\", (itemName, quantity) => {\n  return cy.request({\n    url: `"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress/support/index.js",
    "chars": 671,
    "preview": "// ***********************************************************\n// This example support/index.js is processed and\n// load"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/cypress.json",
    "chars": 67,
    "preview": "{\n  \"nodeVersion\": \"system\",\n  \"experimentalFetchPolyfill\": true\n}\n"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/1_avoiding_waiting_for_fixed_amounts_of_time/package.json",
    "chars": 448,
    "preview": "{\n  \"name\": \"1_avoiding_waiting_for_fixed_amounts_of_time\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index."
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/dbConnection.js",
    "chars": 206,
    "preview": "const environmentName = process.env.NODE_ENV;\nconst db = require(\"knex\")(require(\"./knexfile\")[environmentName]);\n\nconst"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/fixtures/example.json",
    "chars": 155,
    "preview": "{\n  \"name\": \"Using fixtures to represent data\",\n  \"email\": \"hello@cypress.io\",\n  \"body\": \"Fixtures are a great way to mo"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/integration/itemListUpdates.spec.js",
    "chars": 1003,
    "preview": "import { InventoryManagement } from \"../pageObjects/inventoryManagement\";\n\ndescribe(\"item list updates\", () => {\n  befor"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/integration/itemSubmission.spec.js",
    "chars": 2571,
    "preview": "import { InventoryManagement } from \"../pageObjects/inventoryManagement\";\n\ndescribe(\"item submission\", () => {\n  beforeE"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/knexfile.js",
    "chars": 150,
    "preview": "module.exports = {\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"../../server/dev.sqlite\" },\n    "
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/pageObjects/inventoryManagement.js",
    "chars": 1238,
    "preview": "export class InventoryManagement {\n  static visit() {\n    cy.visit(\"http://localhost:8080\");\n  }\n\n  static enterItemName"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/plugins/dbPlugin.js",
    "chars": 290,
    "preview": "const { db } = require(\"../dbConnection\");\n\nconst dbPlugin = (on, config) => {\n  on(\n    \"task\",\n    {\n      emptyInvent"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/plugins/index.js",
    "chars": 639,
    "preview": "/// <reference types=\"cypress\" />\n// ***********************************************************\n// This example plugins"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/support/commands.js",
    "chars": 288,
    "preview": "import \"cypress-wait-until\";\n\nCypress.Commands.add(\"addItem\", (itemName, quantity) => {\n  return cy.request({\n    url: `"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress/support/index.js",
    "chars": 728,
    "preview": "// ***********************************************************\n// This example support/index.js is processed and\n// load"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/cypress.json",
    "chars": 67,
    "preview": "{\n  \"nodeVersion\": \"system\",\n  \"experimentalFetchPolyfill\": true\n}\n"
  },
  {
    "path": "chapter11/3_dealing_with_flakiness/2_stubbing_uncontrollable_factors/package.json",
    "chars": 437,
    "preview": "{\n  \"name\": \"2_stubbing_uncontrollable_factors\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scr"
  },
  {
    "path": "chapter11/4_visual_regression_tests/cypress/dbConnection.js",
    "chars": 206,
    "preview": "const environmentName = process.env.NODE_ENV;\nconst db = require(\"knex\")(require(\"./knexfile\")[environmentName]);\n\nconst"
  },
  {
    "path": "chapter11/4_visual_regression_tests/cypress/fixtures/example.json",
    "chars": 155,
    "preview": "{\n  \"name\": \"Using fixtures to represent data\",\n  \"email\": \"hello@cypress.io\",\n  \"body\": \"Fixtures are a great way to mo"
  },
  {
    "path": "chapter11/4_visual_regression_tests/cypress/integration/itemList.spec.js",
    "chars": 435,
    "preview": "import { InventoryManagement } from \"../pageObjects/inventoryManagement\";\n\nconst now = new Date(1996, 5, 2).getTime();\n\n"
  },
  {
    "path": "chapter11/4_visual_regression_tests/cypress/integration/itemListUpdates.spec.js",
    "chars": 1026,
    "preview": "import { InventoryManagement } from \"../pageObjects/inventoryManagement\";\n\ndescribe(\"item list updates\", () => {\n  befor"
  },
  {
    "path": "chapter11/4_visual_regression_tests/cypress/integration/itemSubmission.spec.js",
    "chars": 2592,
    "preview": "import { InventoryManagement } from \"../pageObjects/inventoryManagement\";\n\ndescribe(\"item submission\", () => {\n  beforeE"
  },
  {
    "path": "chapter11/4_visual_regression_tests/cypress/knexfile.js",
    "chars": 147,
    "preview": "module.exports = {\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"../server/dev.sqlite\" },\n    use"
  },
  {
    "path": "chapter11/4_visual_regression_tests/cypress/pageObjects/inventoryManagement.js",
    "chars": 1238,
    "preview": "export class InventoryManagement {\n  static visit() {\n    cy.visit(\"http://localhost:8080\");\n  }\n\n  static enterItemName"
  },
  {
    "path": "chapter11/4_visual_regression_tests/cypress/plugins/dbPlugin.js",
    "chars": 290,
    "preview": "const { db } = require(\"../dbConnection\");\n\nconst dbPlugin = (on, config) => {\n  on(\n    \"task\",\n    {\n      emptyInvent"
  },
  {
    "path": "chapter11/4_visual_regression_tests/cypress/plugins/index.js",
    "chars": 728,
    "preview": "/// <reference types=\"cypress\" />\n// ***********************************************************\n// This example plugins"
  },
  {
    "path": "chapter11/4_visual_regression_tests/cypress/support/commands.js",
    "chars": 313,
    "preview": "import \"@percy/cypress\";\nimport \"cypress-wait-until\";\n\nCypress.Commands.add(\"addItem\", (itemName, quantity) => {\n  retur"
  },
  {
    "path": "chapter11/4_visual_regression_tests/cypress/support/index.js",
    "chars": 728,
    "preview": "// ***********************************************************\n// This example support/index.js is processed and\n// load"
  },
  {
    "path": "chapter11/4_visual_regression_tests/cypress.json",
    "chars": 67,
    "preview": "{\n  \"nodeVersion\": \"system\",\n  \"experimentalFetchPolyfill\": true\n}\n"
  },
  {
    "path": "chapter11/4_visual_regression_tests/package.json",
    "chars": 475,
    "preview": "{\n  \"name\": \"4_visual_regression_tests\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {"
  },
  {
    "path": "chapter11/client/domController.js",
    "chars": 3159,
    "preview": "const { API_ADDR, addItem, data } = require(\"./inventoryController\");\n\nconst updateItemList = inventory => {\n  if (inven"
  },
  {
    "path": "chapter11/client/domController.test.js",
    "chars": 5686,
    "preview": "const nock = require(\"nock\");\nconst fs = require(\"fs\");\nconst initialHtml = fs.readFileSync(\"./index.html\");\nconst { get"
  },
  {
    "path": "chapter11/client/index.html",
    "chars": 796,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Inventory Manager</title>\n    <style>\n"
  },
  {
    "path": "chapter11/client/inventoryController.js",
    "chars": 565,
    "preview": "const data = { inventory: {} };\n\nconst API_ADDR = \"http://localhost:3000\";\n\nconst addItem = (itemName, quantity) => {\n  "
  },
  {
    "path": "chapter11/client/inventoryController.test.js",
    "chars": 1327,
    "preview": "const nock = require(\"nock\");\nconst { API_ADDR, addItem, data } = require(\"./inventoryController\");\nconst { start, stop "
  },
  {
    "path": "chapter11/client/jest.config.js",
    "chars": 119,
    "preview": "module.exports = {\n  setupFilesAfterEnv: [\n    \"<rootDir>/setupGlobalFetch.js\",\n    \"<rootDir>/setupJestDom.js\"\n  ]\n};\n"
  },
  {
    "path": "chapter11/client/main.js",
    "chars": 1153,
    "preview": "const { connect } = require(\"./socket\");\n\nconst {\n  handleAddItem,\n  checkFormValues,\n  handleUndo,\n  handlePopstate,\n  "
  },
  {
    "path": "chapter11/client/main.test.js",
    "chars": 6426,
    "preview": "const nock = require(\"nock\");\nconst fs = require(\"fs\");\nconst initialHtml = fs.readFileSync(\"./index.html\");\nconst { scr"
  },
  {
    "path": "chapter11/client/package.json",
    "chars": 632,
    "preview": "{\n  \"name\": \"1_http_requests\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"star"
  },
  {
    "path": "chapter11/client/setupGlobalFetch.js",
    "chars": 73,
    "preview": "const fetch = require(\"isomorphic-fetch\");\n\nglobal.window.fetch = fetch;\n"
  },
  {
    "path": "chapter11/client/setupJestDom.js",
    "chars": 79,
    "preview": "const jestDom = require(\"@testing-library/jest-dom\");\n\nexpect.extend(jestDom);\n"
  },
  {
    "path": "chapter11/client/socket.js",
    "chars": 699,
    "preview": "const { API_ADDR, data } = require(\"./inventoryController\");\nconst { updateItemList } = require(\"./domController\");\n\ncon"
  },
  {
    "path": "chapter11/client/socket.test.js",
    "chars": 1459,
    "preview": "const nock = require(\"nock\");\nconst fs = require(\"fs\");\nconst initialHtml = fs.readFileSync(\"./index.html\");\nconst { get"
  },
  {
    "path": "chapter11/client/testSocketServer.js",
    "chars": 381,
    "preview": "const server = require(\"http\").createServer();\nconst io = require(\"socket.io\")(server);\n\nconst sendMsg = (msgType, conte"
  },
  {
    "path": "chapter11/client/testUtils.js",
    "chars": 686,
    "preview": "const clearHistoryHook = done => {\n  const clearHistory = () => {\n    if (history.state === null) {\n      window.removeE"
  },
  {
    "path": "chapter11/server/README.md",
    "chars": 1067,
    "preview": "# Chapter 5 Server\n\nTo better support the client-side application we'll build on Chapter 5, I've had to do a few updates"
  },
  {
    "path": "chapter11/server/authenticationController.js",
    "chars": 1128,
    "preview": "const crypto = require(\"crypto\");\nconst { db } = require(\"./dbConnection\");\n\nconst hashPassword = password => {\n  const "
  },
  {
    "path": "chapter11/server/authenticationController.test.js",
    "chars": 1570,
    "preview": "const crypto = require(\"crypto\");\nconst {\n  hashPassword,\n  credentialsAreValid,\n  authenticationMiddleware\n} = require("
  },
  {
    "path": "chapter11/server/cartController.js",
    "chars": 2216,
    "preview": "const { db } = require(\"./dbConnection\");\nconst { removeFromInventory } = require(\"./inventoryController\");\nconst logger"
  },
  {
    "path": "chapter11/server/cartController.test.js",
    "chars": 3997,
    "preview": "const { db } = require(\"./dbConnection\");\nconst { addItemToCart, monitorStaleItems } = require(\"./cartController\");\ncons"
  },
  {
    "path": "chapter11/server/dbConnection.js",
    "chars": 206,
    "preview": "const environmentName = process.env.NODE_ENV;\nconst db = require(\"knex\")(require(\"./knexfile\")[environmentName]);\n\nconst"
  },
  {
    "path": "chapter11/server/disconnectFromDb.js",
    "chars": 73,
    "preview": "const { db } = require(\"./dbConnection\");\n\nafterAll(() => db.destroy());\n"
  },
  {
    "path": "chapter11/server/inventoryController.js",
    "chars": 480,
    "preview": "const { db } = require(\"./dbConnection\");\n\nconst removeFromInventory = async itemName => {\n  const inventoryEntry = awai"
  },
  {
    "path": "chapter11/server/jest.config.js",
    "chars": 217,
    "preview": "module.exports = {\n  testEnvironment: \"node\",\n  globalSetup: \"./migrateDatabases.js\",\n  setupFilesAfterEnv: [\n    \"<root"
  },
  {
    "path": "chapter11/server/knexfile.js",
    "chars": 251,
    "preview": "module.exports = {\n  test: {\n    client: \"sqlite3\",\n    connection: { filename: \"./test.sqlite\" },\n    useNullAsDefault:"
  },
  {
    "path": "chapter11/server/logger.js",
    "chars": 134,
    "preview": "const fs = require(\"fs\");\n\nconst logger = {\n  log: msg => fs.appendFileSync(\"/tmp/logs.out\", msg + \"\\n\")\n};\n\nmodule.expo"
  },
  {
    "path": "chapter11/server/migrateDatabases.js",
    "chars": 369,
    "preview": "const environmentName = process.env.NODE_ENV || \"test\";\nconst environmentConfig = require(\"./knexfile\")[environmentName]"
  },
  {
    "path": "chapter11/server/migrations/20200325082401_initial_schema.js",
    "chars": 794,
    "preview": "exports.up = async knex => {\n  await knex.schema.createTable(\"users\", table => {\n    table.increments(\"id\");\n    table.s"
  },
  {
    "path": "chapter11/server/migrations/20200331210311_updatedAt_field.js",
    "chars": 252,
    "preview": "exports.up = knex => {\n  return knex.schema.alterTable(\"carts_items\", table => {\n    table.timestamp(\"updatedAt\");\n  });"
  },
  {
    "path": "chapter11/server/package.json",
    "chars": 831,
    "preview": "{\n  \"name\": \"chapter5_server\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"test\": \"jest --runInBand\",\n    \"start\": \"cross-"
  },
  {
    "path": "chapter11/server/seedUser.js",
    "chars": 76,
    "preview": "const { createUser } = require(\"./userTestUtils\");\n\nbeforeEach(createUser);\n"
  },
  {
    "path": "chapter11/server/seeds/initial_inventory.js",
    "chars": 243,
    "preview": "exports.seed = async knex => {\n  await knex(\"inventory\").del();\n  return knex(\"inventory\").insert([\n    { itemName: \"che"
  },
  {
    "path": "chapter11/server/server.js",
    "chars": 5207,
    "preview": "const fetch = require(\"isomorphic-fetch\");\nconst Koa = require(\"koa\");\nconst http = require(\"http\");\nconst IO = require("
  },
  {
    "path": "chapter11/server/server.test.js",
    "chars": 8551,
    "preview": "const { user: globalUser } = require(\"./userTestUtils\");\nconst { db } = require(\"./dbConnection\");\nconst request = requi"
  },
  {
    "path": "chapter11/server/truncateTables.js",
    "chars": 197,
    "preview": "const { db } = require(\"./dbConnection\");\nconst tablesToTruncate = [\"users\", \"inventory\", \"carts_items\"];\n\nbeforeEach(()"
  },
  {
    "path": "chapter11/server/userTestUtils.js",
    "chars": 689,
    "preview": "const { db } = require(\"./dbConnection\");\nconst { hashPassword } = require(\"./authenticationController\");\n\nconst usernam"
  },
  {
    "path": "chapter13/1_type_systems/1_no_types/orderQueue.js",
    "chars": 271,
    "preview": "const state = {\n  deliveries: []\n};\n\nconst addToDeliveryQueue = order => {\n  if (order.status !== \"done\") {\n    throw ne"
  },
  {
    "path": "chapter13/1_type_systems/1_no_types/orderQueue.spec.js",
    "chars": 322,
    "preview": "const { state, addToDeliveryQueue } = require(\"./orderQueue\");\n\ntest(\"adding unfinished orders to the queue\", () => {\n  "
  },
  {
    "path": "chapter13/1_type_systems/1_no_types/package.json",
    "chars": 240,
    "preview": "{\n  \"name\": \"1_no_types\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"orderQueue.js\",\n  \"scripts\": {\n    \"test"
  },
  {
    "path": "chapter13/1_type_systems/2_with_types/orderQueue.js",
    "chars": 267,
    "preview": "\"use strict\";\nexports.__esModule = true;\nexports.addToDeliveryQueue = exports.state = void 0;\nexports.state = {\n  delive"
  },
  {
    "path": "chapter13/1_type_systems/2_with_types/orderQueue.spec.js",
    "chars": 359,
    "preview": "\"use strict\";\nexports.__esModule = true;\nvar orderQueue_1 = require(\"./orderQueue\");\ntest(\"adding finished items to the "
  },
  {
    "path": "chapter13/1_type_systems/2_with_types/orderQueue.spec.ts",
    "chars": 313,
    "preview": "import { state, addToDeliveryQueue, DoneOrder } from \"./orderQueue\";\n\ntest(\"adding finished items to the queue\", () => {"
  },
  {
    "path": "chapter13/1_type_systems/2_with_types/orderQueue.ts",
    "chars": 344,
    "preview": "type OrderItems = { 0: string } & Array<string>;\n\ntype Order = {\n  status: \"in progress\" | \"done\";\n  items: OrderItems;\n"
  },
  {
    "path": "chapter13/1_type_systems/2_with_types/package.json",
    "chars": 323,
    "preview": "{\n  \"name\": \"1_no_types\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"orderQueue.js\",\n  \"scripts\": {\n    \"test"
  },
  {
    "path": "chapter13/1_type_systems/2_with_types/tsconfig.json",
    "chars": 6061,
    "preview": "{\n  \"compilerOptions\": {\n    /* Visit https://aka.ms/tsconfig.json to read more about this file */\n\n    /* Basic Options"
  },
  {
    "path": "chapter2/2_unit_tests/1_raw_tests/Cart.js",
    "chars": 134,
    "preview": "class Cart {\n  constructor() {\n    this.items = [];\n  }\n\n  addToCart(item) {\n    this.items.push(item);\n  }\n}\n\nmodule.ex"
  },
  {
    "path": "chapter2/2_unit_tests/1_raw_tests/Cart.test.js",
    "chars": 538,
    "preview": "const Cart = require(\"./Cart.js\");\n\nconst cart = new Cart();\ncart.addToCart(\"cheesecake\");\n\nconst hasOneItem = cart.item"
  },
  {
    "path": "chapter2/2_unit_tests/2_node_assert/Cart.js",
    "chars": 134,
    "preview": "class Cart {\n  constructor() {\n    this.items = [];\n  }\n\n  addToCart(item) {\n    this.items.push(item);\n  }\n}\n\nmodule.ex"
  },
  {
    "path": "chapter2/2_unit_tests/2_node_assert/Cart.test.js",
    "chars": 246,
    "preview": "const assert = require(\"assert\");\nconst Cart = require(\"./Cart.js\");\n\nconst cart = new Cart();\ncart.addToCart(\"cheesecak"
  },
  {
    "path": "chapter2/2_unit_tests/3_jest_multiple_tests/Cart.js",
    "chars": 336,
    "preview": "class Cart {\n  constructor() {\n    this.items = [];\n  }\n\n  addToCart(item) {\n    this.items.push(item);\n  }\n\n  removeFro"
  },
  {
    "path": "chapter2/2_unit_tests/3_jest_multiple_tests/Cart.test.js",
    "chars": 466,
    "preview": "const assert = require(\"assert\");\nconst Cart = require(\"./Cart.js\");\n\ntest(\"The addToCart function can add an item to th"
  },
  {
    "path": "chapter2/2_unit_tests/3_jest_multiple_tests/package.json",
    "chars": 234,
    "preview": "{\n  \"name\": \"3_jest_multiple_tests\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"Cart.js\",\n  \"scripts\": {\n    "
  },
  {
    "path": "chapter2/2_unit_tests/4_jest_assertions/Cart.js",
    "chars": 336,
    "preview": "class Cart {\n  constructor() {\n    this.items = [];\n  }\n\n  addToCart(item) {\n    this.items.push(item);\n  }\n\n  removeFro"
  },
  {
    "path": "chapter2/2_unit_tests/4_jest_assertions/Cart.test.js",
    "chars": 426,
    "preview": "const Cart = require(\"./Cart.js\");\n\ntest(\"The addToCart function can add an item to the cart\", () => {\n  const cart = ne"
  },
  {
    "path": "chapter2/2_unit_tests/4_jest_assertions/package.json",
    "chars": 230,
    "preview": "{\n  \"name\": \"4_jest_assertions\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"Cart.js\",\n  \"scripts\": {\n    \"tes"
  },
  {
    "path": "chapter2/2_unit_tests/5_npm_scripts/Cart.js",
    "chars": 336,
    "preview": "class Cart {\n  constructor() {\n    this.items = [];\n  }\n\n  addToCart(item) {\n    this.items.push(item);\n  }\n\n  removeFro"
  },
  {
    "path": "chapter2/2_unit_tests/5_npm_scripts/Cart.test.js",
    "chars": 416,
    "preview": "const Cart = require(\"./Cart.js\");\n\ntest(\"The addToCart function can add an item to the cart\", () => {\n  const cart = ne"
  },
  {
    "path": "chapter2/2_unit_tests/5_npm_scripts/package.json",
    "chars": 141,
    "preview": "{\n  \"name\": \"5_global_jest\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"devDependencies\": {\n    \"je"
  },
  {
    "path": "chapter2/3_integration_tests/1_knex_tests_promise/cart.js",
    "chars": 270,
    "preview": "const { db } = require(\"./dbConnection\");\n\nconst createCart = username => {\n  return db(\"carts\").insert({ username });\n}"
  },
  {
    "path": "chapter2/3_integration_tests/1_knex_tests_promise/cart.test.js",
    "chars": 386,
    "preview": "const { db, closeConnection } = require(\"./dbConnection\");\nconst { createCart } = require(\"./cart\");\n\ntest(\"createCart c"
  },
  {
    "path": "chapter2/3_integration_tests/1_knex_tests_promise/dbConnection.js",
    "chars": 155,
    "preview": "const db = require(\"knex\")(require(\"./knexfile\").development);\n\nconst closeConnection = () => db.destroy();\n\nmodule.expo"
  },
  {
    "path": "chapter2/3_integration_tests/1_knex_tests_promise/knexfile.js",
    "chars": 139,
    "preview": "module.exports = {\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"./dev.sqlite\" },\n    useNullAsDe"
  },
  {
    "path": "chapter2/3_integration_tests/1_knex_tests_promise/migrations/20191230210750_create_carts.js",
    "chars": 416,
    "preview": "exports.up = async knex => {\n  await knex.schema.createTable(\"carts\", table => {\n    table.increments(\"id\");\n    table.s"
  },
  {
    "path": "chapter2/3_integration_tests/1_knex_tests_promise/package.json",
    "chars": 317,
    "preview": "{\n  \"name\": \"1_knex_tests_promise\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    "
  },
  {
    "path": "chapter2/3_integration_tests/2_knex_tests_done_cb/cart.js",
    "chars": 270,
    "preview": "const { db } = require(\"./dbConnection\");\n\nconst createCart = username => {\n  return db(\"carts\").insert({ username });\n}"
  },
  {
    "path": "chapter2/3_integration_tests/2_knex_tests_done_cb/cart.test.js",
    "chars": 431,
    "preview": "const { db, closeConnection } = require(\"./dbConnection\");\nconst { createCart } = require(\"./cart\");\n\ntest(\"createCart c"
  },
  {
    "path": "chapter2/3_integration_tests/2_knex_tests_done_cb/dbConnection.js",
    "chars": 155,
    "preview": "const db = require(\"knex\")(require(\"./knexfile\").development);\n\nconst closeConnection = () => db.destroy();\n\nmodule.expo"
  },
  {
    "path": "chapter2/3_integration_tests/2_knex_tests_done_cb/knexfile.js",
    "chars": 139,
    "preview": "module.exports = {\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"./dev.sqlite\" },\n    useNullAsDe"
  },
  {
    "path": "chapter2/3_integration_tests/2_knex_tests_done_cb/migrations/20191230210750_create_carts.js",
    "chars": 416,
    "preview": "exports.up = async knex => {\n  await knex.schema.createTable(\"carts\", table => {\n    table.increments(\"id\");\n    table.s"
  },
  {
    "path": "chapter2/3_integration_tests/2_knex_tests_done_cb/package.json",
    "chars": 317,
    "preview": "{\n  \"name\": \"2_knex_tests_done_cb\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    "
  },
  {
    "path": "chapter2/3_integration_tests/3_knex_tests_hooks/cart.js",
    "chars": 270,
    "preview": "const { db } = require(\"./dbConnection\");\n\nconst createCart = username => {\n  return db(\"carts\").insert({ username });\n}"
  },
  {
    "path": "chapter2/3_integration_tests/3_knex_tests_hooks/cart.test.js",
    "chars": 873,
    "preview": "const { db, closeConnection } = require(\"./dbConnection\");\nconst { createCart, addItem } = require(\"./cart\");\n\nbeforeEac"
  },
  {
    "path": "chapter2/3_integration_tests/3_knex_tests_hooks/dbConnection.js",
    "chars": 155,
    "preview": "const db = require(\"knex\")(require(\"./knexfile\").development);\n\nconst closeConnection = () => db.destroy();\n\nmodule.expo"
  },
  {
    "path": "chapter2/3_integration_tests/3_knex_tests_hooks/knexfile.js",
    "chars": 139,
    "preview": "module.exports = {\n  development: {\n    client: \"sqlite3\",\n    connection: { filename: \"./dev.sqlite\" },\n    useNullAsDe"
  },
  {
    "path": "chapter2/3_integration_tests/3_knex_tests_hooks/migrations/20191230210750_create_carts.js",
    "chars": 416,
    "preview": "exports.up = async knex => {\n  await knex.schema.createTable(\"carts\", table => {\n    table.increments(\"id\");\n    table.s"
  },
  {
    "path": "chapter2/3_integration_tests/3_knex_tests_hooks/package.json",
    "chars": 317,
    "preview": "{\n  \"name\": \"1_knex_tests_promise\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    "
  },
  {
    "path": "chapter2/4_end_to_end_tests/1_http_api_tests/package.json",
    "chars": 252,
    "preview": "{\n  \"name\": \"1_http_api_tests\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"devDependencies\": {\n    "
  },
  {
    "path": "chapter2/4_end_to_end_tests/1_http_api_tests/server.js",
    "chars": 571,
    "preview": "const Koa = require(\"koa\");\nconst Router = require(\"koa-router\");\n\nconst app = new Koa();\nconst router = new Router();\n\n"
  },
  {
    "path": "chapter2/4_end_to_end_tests/1_http_api_tests/server.test.js",
    "chars": 792,
    "preview": "const app = require(\"./server\");\nconst fetch = require(\"isomorphic-fetch\");\n\nconst apiRoot = \"http://localhost:3000\";\n\nc"
  },
  {
    "path": "chapter2/4_end_to_end_tests/2_http_api_with_remove_item/package.json",
    "chars": 252,
    "preview": "{\n  \"name\": \"1_http_api_tests\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"devDependencies\": {\n    "
  },
  {
    "path": "chapter2/4_end_to_end_tests/2_http_api_with_remove_item/server.js",
    "chars": 1213,
    "preview": "const Koa = require(\"koa\");\nconst Router = require(\"koa-router\");\n\nconst app = new Koa();\nconst router = new Router();\n\n"
  },
  {
    "path": "chapter2/4_end_to_end_tests/2_http_api_with_remove_item/server.test.js",
    "chars": 1575,
    "preview": "const { app, resetState } = require(\"./server\");\nconst fetch = require(\"isomorphic-fetch\");\n\nconst apiRoot = \"http://loc"
  },
  {
    "path": "chapter2/5_tests_cost_and_revenue/1_good_vs_bad/badly_written.test.js",
    "chars": 859,
    "preview": "const { app, resetState } = require(\"./server\");\nconst fetch = require(\"isomorphic-fetch\");\n\ntest(\"adding items to a car"
  },
  {
    "path": "chapter2/5_tests_cost_and_revenue/1_good_vs_bad/package.json",
    "chars": 321,
    "preview": "{\n  \"name\": \"1_good_vs_bad\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"test-good\": \"jest well_written.test.js\",\n    \"tes"
  },
  {
    "path": "chapter2/5_tests_cost_and_revenue/1_good_vs_bad/server.js",
    "chars": 1213,
    "preview": "const Koa = require(\"koa\");\nconst Router = require(\"koa-router\");\n\nconst app = new Koa();\nconst router = new Router();\n\n"
  },
  {
    "path": "chapter2/5_tests_cost_and_revenue/1_good_vs_bad/well_written.test.js",
    "chars": 840,
    "preview": "const { app, resetState } = require(\"./server\");\nconst fetch = require(\"isomorphic-fetch\");\n\nconst apiRoot = \"http://loc"
  },
  {
    "path": "chapter2/5_tests_cost_and_revenue/2_test_coupling/package.json",
    "chars": 240,
    "preview": "{\n  \"name\": \"2_test_coupling\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test"
  },
  {
    "path": "chapter2/5_tests_cost_and_revenue/2_test_coupling/pow.test.js",
    "chars": 296,
    "preview": "//const pow = require(\"./pow_recursive\");\nconst pow = require(\"./pow_loop\");\n\ntest(\"calculates powers\", () => {\n  expect"
  },
  {
    "path": "chapter2/5_tests_cost_and_revenue/2_test_coupling/pow_loop.js",
    "chars": 204,
    "preview": "const pow = (a, b) => {\n  let result = 1;\n  for (let i = 0; i < Math.abs(b); i++) {\n    if (b < 0) result = result / a;\n"
  },
  {
    "path": "chapter2/5_tests_cost_and_revenue/2_test_coupling/pow_recursive.js",
    "chars": 203,
    "preview": "const pow = (a, b, acc = 1) => {\n  if (b === 0) return acc;\n  const nextB = b < 0 ? b + 1 : b - 1;\n  const nextAcc = b <"
  }
]

// ... and 652 more files (download for full content)

About this extraction

This page contains the full source code of the lucasfcosta/testing-javascript-applications GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 852 files (773.9 KB), approximately 239.6k tokens, and a symbol index with 94 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!