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 ||
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
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.