cumulusci-plus 5.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cumulusci-plus might be problematic. Click here for more details.
- cumulusci/__about__.py +1 -0
- cumulusci/__init__.py +22 -0
- cumulusci/__main__.py +3 -0
- cumulusci/cli/__init__.py +0 -0
- cumulusci/cli/cci.py +244 -0
- cumulusci/cli/error.py +125 -0
- cumulusci/cli/flow.py +185 -0
- cumulusci/cli/logger.py +72 -0
- cumulusci/cli/org.py +692 -0
- cumulusci/cli/plan.py +181 -0
- cumulusci/cli/project.py +391 -0
- cumulusci/cli/robot.py +116 -0
- cumulusci/cli/runtime.py +190 -0
- cumulusci/cli/service.py +521 -0
- cumulusci/cli/task.py +295 -0
- cumulusci/cli/tests/__init__.py +0 -0
- cumulusci/cli/tests/test_cci.py +545 -0
- cumulusci/cli/tests/test_error.py +170 -0
- cumulusci/cli/tests/test_flow.py +276 -0
- cumulusci/cli/tests/test_logger.py +25 -0
- cumulusci/cli/tests/test_org.py +1438 -0
- cumulusci/cli/tests/test_plan.py +245 -0
- cumulusci/cli/tests/test_project.py +235 -0
- cumulusci/cli/tests/test_robot.py +177 -0
- cumulusci/cli/tests/test_runtime.py +197 -0
- cumulusci/cli/tests/test_service.py +853 -0
- cumulusci/cli/tests/test_task.py +266 -0
- cumulusci/cli/tests/test_ui.py +310 -0
- cumulusci/cli/tests/test_utils.py +122 -0
- cumulusci/cli/tests/utils.py +52 -0
- cumulusci/cli/ui.py +234 -0
- cumulusci/cli/utils.py +150 -0
- cumulusci/conftest.py +181 -0
- cumulusci/core/__init__.py +0 -0
- cumulusci/core/config/BaseConfig.py +5 -0
- cumulusci/core/config/BaseTaskFlowConfig.py +5 -0
- cumulusci/core/config/OrgConfig.py +5 -0
- cumulusci/core/config/ScratchOrgConfig.py +5 -0
- cumulusci/core/config/__init__.py +125 -0
- cumulusci/core/config/base_config.py +111 -0
- cumulusci/core/config/base_task_flow_config.py +82 -0
- cumulusci/core/config/marketing_cloud_service_config.py +83 -0
- cumulusci/core/config/oauth2_service_config.py +17 -0
- cumulusci/core/config/org_config.py +604 -0
- cumulusci/core/config/project_config.py +782 -0
- cumulusci/core/config/scratch_org_config.py +251 -0
- cumulusci/core/config/sfdx_org_config.py +220 -0
- cumulusci/core/config/tests/_test_config_backwards_compatibility.py +33 -0
- cumulusci/core/config/tests/test_config.py +1895 -0
- cumulusci/core/config/tests/test_config_expensive.py +839 -0
- cumulusci/core/config/tests/test_config_util.py +91 -0
- cumulusci/core/config/universal_config.py +88 -0
- cumulusci/core/config/util.py +18 -0
- cumulusci/core/datasets.py +303 -0
- cumulusci/core/debug.py +33 -0
- cumulusci/core/dependencies/__init__.py +55 -0
- cumulusci/core/dependencies/base.py +561 -0
- cumulusci/core/dependencies/dependencies.py +273 -0
- cumulusci/core/dependencies/github.py +177 -0
- cumulusci/core/dependencies/github_resolvers.py +244 -0
- cumulusci/core/dependencies/resolvers.py +580 -0
- cumulusci/core/dependencies/tests/__init__.py +0 -0
- cumulusci/core/dependencies/tests/conftest.py +385 -0
- cumulusci/core/dependencies/tests/test_dependencies.py +950 -0
- cumulusci/core/dependencies/tests/test_github.py +83 -0
- cumulusci/core/dependencies/tests/test_resolvers.py +1027 -0
- cumulusci/core/dependencies/utils.py +13 -0
- cumulusci/core/enums.py +11 -0
- cumulusci/core/exceptions.py +311 -0
- cumulusci/core/flowrunner.py +888 -0
- cumulusci/core/github.py +665 -0
- cumulusci/core/keychain/__init__.py +24 -0
- cumulusci/core/keychain/base_project_keychain.py +441 -0
- cumulusci/core/keychain/encrypted_file_project_keychain.py +945 -0
- cumulusci/core/keychain/environment_project_keychain.py +7 -0
- cumulusci/core/keychain/serialization.py +152 -0
- cumulusci/core/keychain/subprocess_keychain.py +24 -0
- cumulusci/core/keychain/tests/conftest.py +50 -0
- cumulusci/core/keychain/tests/test_base_project_keychain.py +299 -0
- cumulusci/core/keychain/tests/test_encrypted_file_project_keychain.py +1228 -0
- cumulusci/core/metadeploy/__init__.py +0 -0
- cumulusci/core/metadeploy/api.py +88 -0
- cumulusci/core/metadeploy/plans.py +25 -0
- cumulusci/core/metadeploy/tests/test_api.py +276 -0
- cumulusci/core/runtime.py +115 -0
- cumulusci/core/sfdx.py +162 -0
- cumulusci/core/source/__init__.py +16 -0
- cumulusci/core/source/github.py +50 -0
- cumulusci/core/source/local_folder.py +35 -0
- cumulusci/core/source_transforms/__init__.py +0 -0
- cumulusci/core/source_transforms/tests/test_transforms.py +1091 -0
- cumulusci/core/source_transforms/transforms.py +532 -0
- cumulusci/core/tasks.py +404 -0
- cumulusci/core/template_utils.py +59 -0
- cumulusci/core/tests/__init__.py +0 -0
- cumulusci/core/tests/cassettes/TestDatasetsE2E.test_datasets_e2e.yaml +215 -0
- cumulusci/core/tests/cassettes/TestDatasetsE2E.test_datasets_extract_standard_objects.yaml +199 -0
- cumulusci/core/tests/cassettes/TestDatasetsE2E.test_datasets_read_explicit_extract_declaration.yaml +3 -0
- cumulusci/core/tests/fake_remote_repo/cumulusci.yml +32 -0
- cumulusci/core/tests/fake_remote_repo/tasks/directory/example_2.py +6 -0
- cumulusci/core/tests/fake_remote_repo/tasks/example.py +43 -0
- cumulusci/core/tests/fake_remote_repo_2/cumulusci.yml +11 -0
- cumulusci/core/tests/fake_remote_repo_2/tasks/example_3.py +6 -0
- cumulusci/core/tests/test_datasets_e2e.py +386 -0
- cumulusci/core/tests/test_exceptions.py +11 -0
- cumulusci/core/tests/test_flowrunner.py +836 -0
- cumulusci/core/tests/test_github.py +942 -0
- cumulusci/core/tests/test_sfdx.py +138 -0
- cumulusci/core/tests/test_source.py +678 -0
- cumulusci/core/tests/test_tasks.py +262 -0
- cumulusci/core/tests/test_utils.py +141 -0
- cumulusci/core/tests/test_utils_merge_config.py +276 -0
- cumulusci/core/tests/test_versions.py +76 -0
- cumulusci/core/tests/untrusted_repo_child/cumulusci.yml +7 -0
- cumulusci/core/tests/untrusted_repo_child/tasks/untrusted_child.py +6 -0
- cumulusci/core/tests/untrusted_repo_parent/cumulusci.yml +26 -0
- cumulusci/core/tests/untrusted_repo_parent/tasks/untrusted_parent.py +6 -0
- cumulusci/core/tests/utils.py +116 -0
- cumulusci/core/tests/yaml/global.yaml +0 -0
- cumulusci/core/utils.py +402 -0
- cumulusci/core/versions.py +149 -0
- cumulusci/cumulusci.yml +1621 -0
- cumulusci/files/admin_profile.xml +20 -0
- cumulusci/files/delete_excludes.txt +424 -0
- cumulusci/files/templates/project/README.md +12 -0
- cumulusci/files/templates/project/cumulusci.yml +63 -0
- cumulusci/files/templates/project/dot-gitignore +60 -0
- cumulusci/files/templates/project/mapping.yml +45 -0
- cumulusci/files/templates/project/scratch_def.json +25 -0
- cumulusci/oauth/__init__.py +0 -0
- cumulusci/oauth/client.py +400 -0
- cumulusci/oauth/exceptions.py +9 -0
- cumulusci/oauth/salesforce.py +95 -0
- cumulusci/oauth/tests/__init__.py +0 -0
- cumulusci/oauth/tests/cassettes/test_get_device_code.yaml +22 -0
- cumulusci/oauth/tests/cassettes/test_get_device_oauth_token.yaml +74 -0
- cumulusci/oauth/tests/test_client.py +308 -0
- cumulusci/oauth/tests/test_salesforce.py +46 -0
- cumulusci/plugins/__init__.py +3 -0
- cumulusci/plugins/plugin_base.py +93 -0
- cumulusci/plugins/plugin_loader.py +59 -0
- cumulusci/robotframework/CumulusCI.py +340 -0
- cumulusci/robotframework/CumulusCI.robot +7 -0
- cumulusci/robotframework/Performance.py +165 -0
- cumulusci/robotframework/Salesforce.py +936 -0
- cumulusci/robotframework/Salesforce.robot +192 -0
- cumulusci/robotframework/SalesforceAPI.py +416 -0
- cumulusci/robotframework/SalesforcePlaywright.py +220 -0
- cumulusci/robotframework/SalesforcePlaywright.robot +40 -0
- cumulusci/robotframework/__init__.py +2 -0
- cumulusci/robotframework/base_library.py +39 -0
- cumulusci/robotframework/faker_mixin.py +89 -0
- cumulusci/robotframework/form_handlers.py +222 -0
- cumulusci/robotframework/javascript/cci_init.js +34 -0
- cumulusci/robotframework/javascript/cumulusci.js +4 -0
- cumulusci/robotframework/locator_manager.py +197 -0
- cumulusci/robotframework/locators_56.py +88 -0
- cumulusci/robotframework/locators_57.py +5 -0
- cumulusci/robotframework/pageobjects/BasePageObjects.py +433 -0
- cumulusci/robotframework/pageobjects/ObjectManagerPageObject.py +246 -0
- cumulusci/robotframework/pageobjects/PageObjectLibrary.py +45 -0
- cumulusci/robotframework/pageobjects/PageObjects.py +351 -0
- cumulusci/robotframework/pageobjects/__init__.py +12 -0
- cumulusci/robotframework/pageobjects/baseobjects.py +120 -0
- cumulusci/robotframework/perftests/short/collection_perf.robot +105 -0
- cumulusci/robotframework/tests/CustomObjectTestPage.py +10 -0
- cumulusci/robotframework/tests/FooTestPage.py +8 -0
- cumulusci/robotframework/tests/cumulusci/base.robot +40 -0
- cumulusci/robotframework/tests/cumulusci/bulkdata.robot +38 -0
- cumulusci/robotframework/tests/cumulusci/communities.robot +57 -0
- cumulusci/robotframework/tests/cumulusci/datagen.robot +84 -0
- cumulusci/robotframework/tests/salesforce/TestLibraryA.py +24 -0
- cumulusci/robotframework/tests/salesforce/TestLibraryB.py +20 -0
- cumulusci/robotframework/tests/salesforce/TestListener.py +93 -0
- cumulusci/robotframework/tests/salesforce/api.robot +178 -0
- cumulusci/robotframework/tests/salesforce/browsers.robot +143 -0
- cumulusci/robotframework/tests/salesforce/classic.robot +51 -0
- cumulusci/robotframework/tests/salesforce/create_contact.robot +59 -0
- cumulusci/robotframework/tests/salesforce/faker.robot +68 -0
- cumulusci/robotframework/tests/salesforce/forms.robot +172 -0
- cumulusci/robotframework/tests/salesforce/label_locator.robot +244 -0
- cumulusci/robotframework/tests/salesforce/labels.html +33 -0
- cumulusci/robotframework/tests/salesforce/locators.robot +149 -0
- cumulusci/robotframework/tests/salesforce/pageobjects/base_pageobjects.robot +100 -0
- cumulusci/robotframework/tests/salesforce/pageobjects/example_page_object.py +25 -0
- cumulusci/robotframework/tests/salesforce/pageobjects/listing_page.robot +115 -0
- cumulusci/robotframework/tests/salesforce/pageobjects/objectmanager.robot +74 -0
- cumulusci/robotframework/tests/salesforce/pageobjects/pageobjects.robot +171 -0
- cumulusci/robotframework/tests/salesforce/performance.robot +109 -0
- cumulusci/robotframework/tests/salesforce/playwright/javascript_keywords.robot +33 -0
- cumulusci/robotframework/tests/salesforce/playwright/open_test_browser.robot +48 -0
- cumulusci/robotframework/tests/salesforce/playwright/playwright.robot +24 -0
- cumulusci/robotframework/tests/salesforce/playwright/ui.robot +32 -0
- cumulusci/robotframework/tests/salesforce/populate.robot +89 -0
- cumulusci/robotframework/tests/salesforce/test_testlistener.py +37 -0
- cumulusci/robotframework/tests/salesforce/ui.robot +361 -0
- cumulusci/robotframework/tests/test_cumulusci_library.py +304 -0
- cumulusci/robotframework/tests/test_locator_manager.py +158 -0
- cumulusci/robotframework/tests/test_pageobjects.py +291 -0
- cumulusci/robotframework/tests/test_performance.py +38 -0
- cumulusci/robotframework/tests/test_salesforce.py +79 -0
- cumulusci/robotframework/tests/test_salesforce_locators.py +73 -0
- cumulusci/robotframework/tests/test_template_util.py +53 -0
- cumulusci/robotframework/tests/test_utils.py +106 -0
- cumulusci/robotframework/utils.py +283 -0
- cumulusci/salesforce_api/__init__.py +0 -0
- cumulusci/salesforce_api/exceptions.py +23 -0
- cumulusci/salesforce_api/filterable_objects.py +96 -0
- cumulusci/salesforce_api/mc_soap_envelopes.py +89 -0
- cumulusci/salesforce_api/metadata.py +721 -0
- cumulusci/salesforce_api/org_schema.py +571 -0
- cumulusci/salesforce_api/org_schema_models.py +226 -0
- cumulusci/salesforce_api/package_install.py +265 -0
- cumulusci/salesforce_api/package_zip.py +301 -0
- cumulusci/salesforce_api/rest_deploy.py +148 -0
- cumulusci/salesforce_api/retrieve_profile_api.py +301 -0
- cumulusci/salesforce_api/soap_envelopes.py +177 -0
- cumulusci/salesforce_api/tests/__init__.py +0 -0
- cumulusci/salesforce_api/tests/metadata_test_strings.py +24 -0
- cumulusci/salesforce_api/tests/test_metadata.py +1015 -0
- cumulusci/salesforce_api/tests/test_package_install.py +219 -0
- cumulusci/salesforce_api/tests/test_package_zip.py +380 -0
- cumulusci/salesforce_api/tests/test_rest_deploy.py +264 -0
- cumulusci/salesforce_api/tests/test_retrieve_profile_api.py +337 -0
- cumulusci/salesforce_api/tests/test_utils.py +124 -0
- cumulusci/salesforce_api/utils.py +51 -0
- cumulusci/schema/cumulusci.jsonschema.json +782 -0
- cumulusci/tasks/__init__.py +0 -0
- cumulusci/tasks/apex/__init__.py +0 -0
- cumulusci/tasks/apex/anon.py +157 -0
- cumulusci/tasks/apex/batch.py +180 -0
- cumulusci/tasks/apex/testrunner.py +835 -0
- cumulusci/tasks/apex/tests/cassettes/ManualEditTestApexIntegrationTests.test_run_tests__integration_test.yaml +703 -0
- cumulusci/tasks/apex/tests/test_apex_tasks.py +1558 -0
- cumulusci/tasks/base_source_control_task.py +17 -0
- cumulusci/tasks/bulkdata/__init__.py +15 -0
- cumulusci/tasks/bulkdata/base_generate_data_task.py +96 -0
- cumulusci/tasks/bulkdata/dates.py +97 -0
- cumulusci/tasks/bulkdata/delete.py +156 -0
- cumulusci/tasks/bulkdata/extract.py +441 -0
- cumulusci/tasks/bulkdata/extract_dataset_utils/calculate_dependencies.py +117 -0
- cumulusci/tasks/bulkdata/extract_dataset_utils/extract_yml.py +123 -0
- cumulusci/tasks/bulkdata/extract_dataset_utils/hardcoded_default_declarations.py +49 -0
- cumulusci/tasks/bulkdata/extract_dataset_utils/synthesize_extract_declarations.py +283 -0
- cumulusci/tasks/bulkdata/extract_dataset_utils/tests/test_extract_yml.py +142 -0
- cumulusci/tasks/bulkdata/extract_dataset_utils/tests/test_synthesize_extract_declarations.py +575 -0
- cumulusci/tasks/bulkdata/factory_utils.py +134 -0
- cumulusci/tasks/bulkdata/generate.py +4 -0
- cumulusci/tasks/bulkdata/generate_and_load_data.py +232 -0
- cumulusci/tasks/bulkdata/generate_and_load_data_from_yaml.py +19 -0
- cumulusci/tasks/bulkdata/generate_from_yaml.py +183 -0
- cumulusci/tasks/bulkdata/generate_mapping.py +434 -0
- cumulusci/tasks/bulkdata/generate_mapping_utils/dependency_map.py +169 -0
- cumulusci/tasks/bulkdata/generate_mapping_utils/extract_mapping_file_generator.py +45 -0
- cumulusci/tasks/bulkdata/generate_mapping_utils/generate_mapping_from_declarations.py +121 -0
- cumulusci/tasks/bulkdata/generate_mapping_utils/load_mapping_file_generator.py +127 -0
- cumulusci/tasks/bulkdata/generate_mapping_utils/mapping_generator_post_processes.py +53 -0
- cumulusci/tasks/bulkdata/generate_mapping_utils/mapping_transforms.py +139 -0
- cumulusci/tasks/bulkdata/generate_mapping_utils/tests/test_generate_extract_mapping_from_declarations.py +135 -0
- cumulusci/tasks/bulkdata/generate_mapping_utils/tests/test_generate_load_mapping_from_declarations.py +330 -0
- cumulusci/tasks/bulkdata/generate_mapping_utils/tests/test_mapping_generator_post_processes.py +60 -0
- cumulusci/tasks/bulkdata/generate_mapping_utils/tests/test_mapping_transforms.py +188 -0
- cumulusci/tasks/bulkdata/load.py +1196 -0
- cumulusci/tasks/bulkdata/mapping_parser.py +811 -0
- cumulusci/tasks/bulkdata/query_transformers.py +264 -0
- cumulusci/tasks/bulkdata/select_utils.py +792 -0
- cumulusci/tasks/bulkdata/snowfakery.py +753 -0
- cumulusci/tasks/bulkdata/snowfakery_utils/queue_manager.py +478 -0
- cumulusci/tasks/bulkdata/snowfakery_utils/snowfakery_run_until.py +141 -0
- cumulusci/tasks/bulkdata/snowfakery_utils/snowfakery_working_directory.py +53 -0
- cumulusci/tasks/bulkdata/snowfakery_utils/subtask_configurator.py +64 -0
- cumulusci/tasks/bulkdata/step.py +1242 -0
- cumulusci/tasks/bulkdata/tests/__init__.py +0 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestSelect.test_select_random_strategy.yaml +147 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestSelect.test_select_similarity_annoy_strategy.yaml +123 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestSelect.test_select_similarity_select_and_insert_strategy.yaml +313 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestSelect.test_select_similarity_select_and_insert_strategy_bulk.yaml +550 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestSelect.test_select_similarity_strategy.yaml +175 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestSelect.test_select_standard_strategy.yaml +147 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestSnowfakery.test_run_until_records_in_org__multiple_needed.yaml +69 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestSnowfakery.test_run_until_records_in_org__none_needed.yaml +22 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestSnowfakery.test_run_until_records_in_org__one_needed.yaml +24 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestSnowfakery.test_snowfakery_query_salesforce.yaml +25 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestUpdatesIntegrationTests.test_updates_task.yaml +80 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestUpsert.test_simple_upsert__rest.yaml +270 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestUpsert.test_upsert__rest.yaml +267 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestUpsert.test_upsert_complex_external_id_field__rest.yaml +369 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestUpsert.test_upsert_complex_external_id_field_rest__duplicate_error.yaml +204 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestUpsert.test_upsert_complex_fields__bulk.yaml +675 -0
- cumulusci/tasks/bulkdata/tests/dummy_data_factory.py +36 -0
- cumulusci/tasks/bulkdata/tests/integration_test_utils.py +49 -0
- cumulusci/tasks/bulkdata/tests/mapping-oid.yml +87 -0
- cumulusci/tasks/bulkdata/tests/mapping_after.yml +38 -0
- cumulusci/tasks/bulkdata/tests/mapping_poly.yml +34 -0
- cumulusci/tasks/bulkdata/tests/mapping_poly_incomplete.yml +20 -0
- cumulusci/tasks/bulkdata/tests/mapping_poly_wrong.yml +21 -0
- cumulusci/tasks/bulkdata/tests/mapping_select.yml +20 -0
- cumulusci/tasks/bulkdata/tests/mapping_select_invalid_strategy.yml +20 -0
- cumulusci/tasks/bulkdata/tests/mapping_select_invalid_threshold__invalid_number.yml +21 -0
- cumulusci/tasks/bulkdata/tests/mapping_select_invalid_threshold__invalid_strategy.yml +21 -0
- cumulusci/tasks/bulkdata/tests/mapping_select_invalid_threshold__non_float.yml +21 -0
- cumulusci/tasks/bulkdata/tests/mapping_select_missing_priority_fields.yml +22 -0
- cumulusci/tasks/bulkdata/tests/mapping_select_no_priority_fields.yml +18 -0
- cumulusci/tasks/bulkdata/tests/mapping_simple.yml +27 -0
- cumulusci/tasks/bulkdata/tests/mapping_v1.yml +28 -0
- cumulusci/tasks/bulkdata/tests/mapping_v2.yml +21 -0
- cumulusci/tasks/bulkdata/tests/mapping_v3.yml +32 -0
- cumulusci/tasks/bulkdata/tests/mapping_vanilla_sf.yml +69 -0
- cumulusci/tasks/bulkdata/tests/mock_data_factory_without_mapping.py +12 -0
- cumulusci/tasks/bulkdata/tests/person_accounts.yml +23 -0
- cumulusci/tasks/bulkdata/tests/person_accounts_minimal.yml +15 -0
- cumulusci/tasks/bulkdata/tests/recordtypes.yml +8 -0
- cumulusci/tasks/bulkdata/tests/recordtypes_2.yml +6 -0
- cumulusci/tasks/bulkdata/tests/recordtypes_with_ispersontype.yml +8 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/child/child2.yml +3 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/child.yml +4 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/gen_npsp_standard_objects.recipe.yml +89 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/include_parent.yml +3 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/npsp_standard_objects_macros.yml +34 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/options.recipe.yml +6 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/query_snowfakery.recipe.yml +16 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/sf_standard_object_macros.yml +83 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/simple_snowfakery.load.yml +2 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/simple_snowfakery.recipe.yml +13 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/simple_snowfakery_2.load.yml +5 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/simple_snowfakery_channels.load.yml +13 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/simple_snowfakery_channels.recipe.yml +12 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/simple_snowfakery_channels_2.load.yml +13 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/unique_values.recipe.yml +4 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/upsert.recipe.yml +23 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/upsert_2.recipe.yml +29 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/upsert_before.yml +10 -0
- cumulusci/tasks/bulkdata/tests/test_base_generate_data_tasks.py +61 -0
- cumulusci/tasks/bulkdata/tests/test_dates.py +99 -0
- cumulusci/tasks/bulkdata/tests/test_delete.py +404 -0
- cumulusci/tasks/bulkdata/tests/test_extract.py +1311 -0
- cumulusci/tasks/bulkdata/tests/test_factory_utils.py +55 -0
- cumulusci/tasks/bulkdata/tests/test_generate_and_load.py +252 -0
- cumulusci/tasks/bulkdata/tests/test_generate_from_snowfakery_task.py +343 -0
- cumulusci/tasks/bulkdata/tests/test_generatemapping.py +1039 -0
- cumulusci/tasks/bulkdata/tests/test_load.py +3175 -0
- cumulusci/tasks/bulkdata/tests/test_mapping_parser.py +1658 -0
- cumulusci/tasks/bulkdata/tests/test_query_db__joins_self_lookups.yml +12 -0
- cumulusci/tasks/bulkdata/tests/test_query_db_joins_lookups.yml +26 -0
- cumulusci/tasks/bulkdata/tests/test_query_db_joins_lookups_select.yml +48 -0
- cumulusci/tasks/bulkdata/tests/test_select.py +171 -0
- cumulusci/tasks/bulkdata/tests/test_select_utils.py +1057 -0
- cumulusci/tasks/bulkdata/tests/test_snowfakery.py +1153 -0
- cumulusci/tasks/bulkdata/tests/test_step.py +3957 -0
- cumulusci/tasks/bulkdata/tests/test_updates.py +513 -0
- cumulusci/tasks/bulkdata/tests/test_upsert.py +1015 -0
- cumulusci/tasks/bulkdata/tests/test_utils.py +158 -0
- cumulusci/tasks/bulkdata/tests/testdata.db +0 -0
- cumulusci/tasks/bulkdata/tests/update_describe.py +50 -0
- cumulusci/tasks/bulkdata/tests/update_person_accounts.yml +23 -0
- cumulusci/tasks/bulkdata/tests/utils.py +114 -0
- cumulusci/tasks/bulkdata/update_data.py +260 -0
- cumulusci/tasks/bulkdata/upsert_utils.py +130 -0
- cumulusci/tasks/bulkdata/utils.py +249 -0
- cumulusci/tasks/command.py +178 -0
- cumulusci/tasks/connectedapp.py +186 -0
- cumulusci/tasks/create_package_version.py +778 -0
- cumulusci/tasks/datadictionary.py +745 -0
- cumulusci/tasks/dx_convert_from.py +26 -0
- cumulusci/tasks/github/__init__.py +17 -0
- cumulusci/tasks/github/base.py +16 -0
- cumulusci/tasks/github/commit_status.py +13 -0
- cumulusci/tasks/github/merge.py +11 -0
- cumulusci/tasks/github/publish.py +11 -0
- cumulusci/tasks/github/pull_request.py +11 -0
- cumulusci/tasks/github/release.py +11 -0
- cumulusci/tasks/github/release_report.py +11 -0
- cumulusci/tasks/github/tag.py +11 -0
- cumulusci/tasks/github/tests/__init__.py +0 -0
- cumulusci/tasks/github/tests/test_util.py +202 -0
- cumulusci/tasks/github/tests/test_vcs_migration.py +44 -0
- cumulusci/tasks/github/tests/util_github_api.py +666 -0
- cumulusci/tasks/github/util.py +252 -0
- cumulusci/tasks/marketing_cloud/__init__.py +0 -0
- cumulusci/tasks/marketing_cloud/api.py +188 -0
- cumulusci/tasks/marketing_cloud/base.py +38 -0
- cumulusci/tasks/marketing_cloud/deploy.py +345 -0
- cumulusci/tasks/marketing_cloud/get_user_info.py +40 -0
- cumulusci/tasks/marketing_cloud/mc_constants.py +1 -0
- cumulusci/tasks/marketing_cloud/tests/__init__.py +0 -0
- cumulusci/tasks/marketing_cloud/tests/conftest.py +46 -0
- cumulusci/tasks/marketing_cloud/tests/expected-payload.json +110 -0
- cumulusci/tasks/marketing_cloud/tests/test_api.py +97 -0
- cumulusci/tasks/marketing_cloud/tests/test_api_soap_envelopes.py +145 -0
- cumulusci/tasks/marketing_cloud/tests/test_base.py +14 -0
- cumulusci/tasks/marketing_cloud/tests/test_deploy.py +400 -0
- cumulusci/tasks/marketing_cloud/tests/test_get_user_info.py +141 -0
- cumulusci/tasks/marketing_cloud/tests/validation-response.json +39 -0
- cumulusci/tasks/metadata/__init__.py +0 -0
- cumulusci/tasks/metadata/ee_src.py +94 -0
- cumulusci/tasks/metadata/managed_src.py +100 -0
- cumulusci/tasks/metadata/metadata_map.yml +868 -0
- cumulusci/tasks/metadata/modify.py +99 -0
- cumulusci/tasks/metadata/package.py +684 -0
- cumulusci/tasks/metadata/tests/__init__.py +0 -0
- cumulusci/tasks/metadata/tests/package_metadata/namespaced_report_folder/.hidden/.keep +0 -0
- cumulusci/tasks/metadata/tests/package_metadata/namespaced_report_folder/destructiveChanges.xml +9 -0
- cumulusci/tasks/metadata/tests/package_metadata/namespaced_report_folder/package.xml +9 -0
- cumulusci/tasks/metadata/tests/package_metadata/namespaced_report_folder/package_install_uninstall.xml +11 -0
- cumulusci/tasks/metadata/tests/package_metadata/namespaced_report_folder/reports/namespace__TestFolder/TestReport.report +3 -0
- cumulusci/tasks/metadata/tests/sample_package.xml +9 -0
- cumulusci/tasks/metadata/tests/test_ee_src.py +112 -0
- cumulusci/tasks/metadata/tests/test_managed_src.py +111 -0
- cumulusci/tasks/metadata/tests/test_modify.py +123 -0
- cumulusci/tasks/metadata/tests/test_package.py +476 -0
- cumulusci/tasks/metadata_etl/__init__.py +29 -0
- cumulusci/tasks/metadata_etl/base.py +436 -0
- cumulusci/tasks/metadata_etl/duplicate_rules.py +24 -0
- cumulusci/tasks/metadata_etl/field_sets.py +70 -0
- cumulusci/tasks/metadata_etl/help_text.py +92 -0
- cumulusci/tasks/metadata_etl/layouts.py +550 -0
- cumulusci/tasks/metadata_etl/objects.py +68 -0
- cumulusci/tasks/metadata_etl/permissions.py +167 -0
- cumulusci/tasks/metadata_etl/picklists.py +221 -0
- cumulusci/tasks/metadata_etl/remote_site_settings.py +99 -0
- cumulusci/tasks/metadata_etl/sharing.py +138 -0
- cumulusci/tasks/metadata_etl/tests/test_base.py +512 -0
- cumulusci/tasks/metadata_etl/tests/test_duplicate_rules.py +22 -0
- cumulusci/tasks/metadata_etl/tests/test_field_sets.py +156 -0
- cumulusci/tasks/metadata_etl/tests/test_help_text.py +387 -0
- cumulusci/tasks/metadata_etl/tests/test_ip_ranges.py +85 -0
- cumulusci/tasks/metadata_etl/tests/test_layouts.py +858 -0
- cumulusci/tasks/metadata_etl/tests/test_objects.py +236 -0
- cumulusci/tasks/metadata_etl/tests/test_permissions.py +223 -0
- cumulusci/tasks/metadata_etl/tests/test_picklists.py +547 -0
- cumulusci/tasks/metadata_etl/tests/test_remote_site_settings.py +46 -0
- cumulusci/tasks/metadata_etl/tests/test_sharing.py +333 -0
- cumulusci/tasks/metadata_etl/tests/test_value_sets.py +298 -0
- cumulusci/tasks/metadata_etl/value_sets.py +106 -0
- cumulusci/tasks/metadeploy.py +393 -0
- cumulusci/tasks/metaxml.py +88 -0
- cumulusci/tasks/preflight/__init__.py +0 -0
- cumulusci/tasks/preflight/dataset_load.py +49 -0
- cumulusci/tasks/preflight/licenses.py +86 -0
- cumulusci/tasks/preflight/packages.py +14 -0
- cumulusci/tasks/preflight/permsets.py +23 -0
- cumulusci/tasks/preflight/recordtypes.py +16 -0
- cumulusci/tasks/preflight/retrieve_tasks.py +30 -0
- cumulusci/tasks/preflight/settings.py +77 -0
- cumulusci/tasks/preflight/sobjects.py +202 -0
- cumulusci/tasks/preflight/tests/test_dataset_load.py +85 -0
- cumulusci/tasks/preflight/tests/test_licenses.py +174 -0
- cumulusci/tasks/preflight/tests/test_packages.py +14 -0
- cumulusci/tasks/preflight/tests/test_permset_preflights.py +51 -0
- cumulusci/tasks/preflight/tests/test_recordtypes.py +30 -0
- cumulusci/tasks/preflight/tests/test_retrieve_tasks.py +62 -0
- cumulusci/tasks/preflight/tests/test_settings.py +130 -0
- cumulusci/tasks/preflight/tests/test_sobjects.py +231 -0
- cumulusci/tasks/push/README.md +59 -0
- cumulusci/tasks/push/__init__.py +0 -0
- cumulusci/tasks/push/push_api.py +659 -0
- cumulusci/tasks/push/pushfails.py +136 -0
- cumulusci/tasks/push/tasks.py +476 -0
- cumulusci/tasks/push/tests/conftest.py +263 -0
- cumulusci/tasks/push/tests/test_push_api.py +951 -0
- cumulusci/tasks/push/tests/test_push_tasks.py +659 -0
- cumulusci/tasks/release_notes/README.md +63 -0
- cumulusci/tasks/release_notes/__init__.py +0 -0
- cumulusci/tasks/release_notes/exceptions.py +5 -0
- cumulusci/tasks/release_notes/generator.py +137 -0
- cumulusci/tasks/release_notes/parser.py +232 -0
- cumulusci/tasks/release_notes/provider.py +44 -0
- cumulusci/tasks/release_notes/task.py +300 -0
- cumulusci/tasks/release_notes/tests/__init__.py +0 -0
- cumulusci/tasks/release_notes/tests/change_notes/full/example1.md +17 -0
- cumulusci/tasks/release_notes/tests/change_notes/multi/1.txt +1 -0
- cumulusci/tasks/release_notes/tests/change_notes/multi/2.txt +1 -0
- cumulusci/tasks/release_notes/tests/change_notes/multi/3.txt +1 -0
- cumulusci/tasks/release_notes/tests/change_notes/single/1.txt +1 -0
- cumulusci/tasks/release_notes/tests/test_generator.py +582 -0
- cumulusci/tasks/release_notes/tests/test_parser.py +867 -0
- cumulusci/tasks/release_notes/tests/test_provider.py +512 -0
- cumulusci/tasks/release_notes/tests/test_task.py +461 -0
- cumulusci/tasks/release_notes/tests/utils.py +153 -0
- cumulusci/tasks/robotframework/__init__.py +3 -0
- cumulusci/tasks/robotframework/debugger/DebugListener.py +100 -0
- cumulusci/tasks/robotframework/debugger/__init__.py +10 -0
- cumulusci/tasks/robotframework/debugger/model.py +87 -0
- cumulusci/tasks/robotframework/debugger/ui.py +259 -0
- cumulusci/tasks/robotframework/libdoc.py +269 -0
- cumulusci/tasks/robotframework/robotframework.py +392 -0
- cumulusci/tasks/robotframework/stylesheet.css +130 -0
- cumulusci/tasks/robotframework/template.html +109 -0
- cumulusci/tasks/robotframework/tests/TestLibrary.py +18 -0
- cumulusci/tasks/robotframework/tests/TestPageObjects.py +31 -0
- cumulusci/tasks/robotframework/tests/TestResource.robot +8 -0
- cumulusci/tasks/robotframework/tests/failing_tests.robot +16 -0
- cumulusci/tasks/robotframework/tests/performance.robot +23 -0
- cumulusci/tasks/robotframework/tests/test_browser_proxies.py +137 -0
- cumulusci/tasks/robotframework/tests/test_debugger.py +360 -0
- cumulusci/tasks/robotframework/tests/test_robot_parallel.py +141 -0
- cumulusci/tasks/robotframework/tests/test_robotframework.py +860 -0
- cumulusci/tasks/salesforce/BaseRetrieveMetadata.py +58 -0
- cumulusci/tasks/salesforce/BaseSalesforceApiTask.py +45 -0
- cumulusci/tasks/salesforce/BaseSalesforceMetadataApiTask.py +18 -0
- cumulusci/tasks/salesforce/BaseSalesforceTask.py +4 -0
- cumulusci/tasks/salesforce/BaseUninstallMetadata.py +41 -0
- cumulusci/tasks/salesforce/CreateCommunity.py +124 -0
- cumulusci/tasks/salesforce/CreatePackage.py +29 -0
- cumulusci/tasks/salesforce/Deploy.py +240 -0
- cumulusci/tasks/salesforce/DeployBundles.py +88 -0
- cumulusci/tasks/salesforce/DescribeMetadataTypes.py +26 -0
- cumulusci/tasks/salesforce/EnsureRecordTypes.py +202 -0
- cumulusci/tasks/salesforce/GetInstalledPackages.py +8 -0
- cumulusci/tasks/salesforce/ListCommunities.py +40 -0
- cumulusci/tasks/salesforce/ListCommunityTemplates.py +19 -0
- cumulusci/tasks/salesforce/PublishCommunity.py +62 -0
- cumulusci/tasks/salesforce/RetrievePackaged.py +41 -0
- cumulusci/tasks/salesforce/RetrieveReportsAndDashboards.py +82 -0
- cumulusci/tasks/salesforce/RetrieveUnpackaged.py +36 -0
- cumulusci/tasks/salesforce/SOQLQuery.py +39 -0
- cumulusci/tasks/salesforce/UninstallLocal.py +15 -0
- cumulusci/tasks/salesforce/UninstallLocalBundles.py +28 -0
- cumulusci/tasks/salesforce/UninstallLocalNamespacedBundles.py +58 -0
- cumulusci/tasks/salesforce/UninstallPackage.py +32 -0
- cumulusci/tasks/salesforce/UninstallPackaged.py +56 -0
- cumulusci/tasks/salesforce/UpdateAdminProfile.py +8 -0
- cumulusci/tasks/salesforce/__init__.py +79 -0
- cumulusci/tasks/salesforce/activate_flow.py +74 -0
- cumulusci/tasks/salesforce/check_components.py +324 -0
- cumulusci/tasks/salesforce/composite.py +142 -0
- cumulusci/tasks/salesforce/create_permission_sets.py +35 -0
- cumulusci/tasks/salesforce/custom_settings.py +134 -0
- cumulusci/tasks/salesforce/custom_settings_wait.py +132 -0
- cumulusci/tasks/salesforce/enable_prediction.py +107 -0
- cumulusci/tasks/salesforce/insert_record.py +40 -0
- cumulusci/tasks/salesforce/install_package_version.py +242 -0
- cumulusci/tasks/salesforce/license_preflights.py +8 -0
- cumulusci/tasks/salesforce/network_member_group.py +178 -0
- cumulusci/tasks/salesforce/nonsourcetracking.py +228 -0
- cumulusci/tasks/salesforce/org_settings.py +193 -0
- cumulusci/tasks/salesforce/package_upload.py +328 -0
- cumulusci/tasks/salesforce/profiles.py +74 -0
- cumulusci/tasks/salesforce/promote_package_version.py +376 -0
- cumulusci/tasks/salesforce/retrieve_profile.py +195 -0
- cumulusci/tasks/salesforce/salesforce_files.py +244 -0
- cumulusci/tasks/salesforce/sourcetracking.py +507 -0
- cumulusci/tasks/salesforce/tests/__init__.py +3 -0
- cumulusci/tasks/salesforce/tests/test_CreateCommunity.py +278 -0
- cumulusci/tasks/salesforce/tests/test_CreatePackage.py +22 -0
- cumulusci/tasks/salesforce/tests/test_Deploy.py +470 -0
- cumulusci/tasks/salesforce/tests/test_DeployBundles.py +76 -0
- cumulusci/tasks/salesforce/tests/test_EnsureRecordTypes.py +345 -0
- cumulusci/tasks/salesforce/tests/test_ListCommunities.py +84 -0
- cumulusci/tasks/salesforce/tests/test_ListCommunityTemplates.py +49 -0
- cumulusci/tasks/salesforce/tests/test_PackageUpload.py +547 -0
- cumulusci/tasks/salesforce/tests/test_ProfileGrantAllAccess.py +699 -0
- cumulusci/tasks/salesforce/tests/test_PublishCommunity.py +181 -0
- cumulusci/tasks/salesforce/tests/test_RetrievePackaged.py +24 -0
- cumulusci/tasks/salesforce/tests/test_RetrieveReportsAndDashboards.py +56 -0
- cumulusci/tasks/salesforce/tests/test_RetrieveUnpackaged.py +21 -0
- cumulusci/tasks/salesforce/tests/test_SOQLQuery.py +30 -0
- cumulusci/tasks/salesforce/tests/test_UninstallLocal.py +15 -0
- cumulusci/tasks/salesforce/tests/test_UninstallLocalBundles.py +19 -0
- cumulusci/tasks/salesforce/tests/test_UninstallLocalNamespacedBundles.py +22 -0
- cumulusci/tasks/salesforce/tests/test_UninstallPackage.py +19 -0
- cumulusci/tasks/salesforce/tests/test_UninstallPackaged.py +66 -0
- cumulusci/tasks/salesforce/tests/test_UninstallPackagedIncremental.py +127 -0
- cumulusci/tasks/salesforce/tests/test_activate_flow.py +132 -0
- cumulusci/tasks/salesforce/tests/test_base_tasks.py +110 -0
- cumulusci/tasks/salesforce/tests/test_check_components.py +445 -0
- cumulusci/tasks/salesforce/tests/test_composite.py +250 -0
- cumulusci/tasks/salesforce/tests/test_create_permission_sets.py +41 -0
- cumulusci/tasks/salesforce/tests/test_custom_settings.py +227 -0
- cumulusci/tasks/salesforce/tests/test_custom_settings_wait.py +174 -0
- cumulusci/tasks/salesforce/tests/test_describemetadatatypes.py +18 -0
- cumulusci/tasks/salesforce/tests/test_enable_prediction.py +240 -0
- cumulusci/tasks/salesforce/tests/test_insert_record.py +110 -0
- cumulusci/tasks/salesforce/tests/test_install_package_version.py +464 -0
- cumulusci/tasks/salesforce/tests/test_network_member_group.py +444 -0
- cumulusci/tasks/salesforce/tests/test_nonsourcetracking.py +235 -0
- cumulusci/tasks/salesforce/tests/test_org_settings.py +407 -0
- cumulusci/tasks/salesforce/tests/test_profiles.py +202 -0
- cumulusci/tasks/salesforce/tests/test_retrieve_profile.py +287 -0
- cumulusci/tasks/salesforce/tests/test_salesforce_files.py +228 -0
- cumulusci/tasks/salesforce/tests/test_sourcetracking.py +350 -0
- cumulusci/tasks/salesforce/tests/test_trigger_handlers.py +300 -0
- cumulusci/tasks/salesforce/tests/test_update_dependencies.py +509 -0
- cumulusci/tasks/salesforce/tests/util.py +79 -0
- cumulusci/tasks/salesforce/trigger_handlers.py +119 -0
- cumulusci/tasks/salesforce/uninstall_packaged_incremental.py +136 -0
- cumulusci/tasks/salesforce/update_dependencies.py +290 -0
- cumulusci/tasks/salesforce/update_profile.py +339 -0
- cumulusci/tasks/salesforce/users/permsets.py +227 -0
- cumulusci/tasks/salesforce/users/photos.py +162 -0
- cumulusci/tasks/salesforce/users/tests/photo.mock.txt +1 -0
- cumulusci/tasks/salesforce/users/tests/test_permsets.py +950 -0
- cumulusci/tasks/salesforce/users/tests/test_photos.py +373 -0
- cumulusci/tasks/sample_data/capture_sample_data.py +77 -0
- cumulusci/tasks/sample_data/load_sample_data.py +85 -0
- cumulusci/tasks/sample_data/test_capture_sample_data.py +117 -0
- cumulusci/tasks/sample_data/test_load_sample_data.py +121 -0
- cumulusci/tasks/sfdx.py +83 -0
- cumulusci/tasks/tests/__init__.py +1 -0
- cumulusci/tasks/tests/conftest.py +30 -0
- cumulusci/tasks/tests/test_command.py +129 -0
- cumulusci/tasks/tests/test_connectedapp.py +236 -0
- cumulusci/tasks/tests/test_create_package_version.py +847 -0
- cumulusci/tasks/tests/test_datadictionary.py +1575 -0
- cumulusci/tasks/tests/test_dx_convert_from.py +60 -0
- cumulusci/tasks/tests/test_metadeploy.py +624 -0
- cumulusci/tasks/tests/test_metaxml.py +99 -0
- cumulusci/tasks/tests/test_promote_package_version.py +488 -0
- cumulusci/tasks/tests/test_pushfails.py +96 -0
- cumulusci/tasks/tests/test_salesforce.py +72 -0
- cumulusci/tasks/tests/test_sfdx.py +105 -0
- cumulusci/tasks/tests/test_util.py +207 -0
- cumulusci/tasks/util.py +261 -0
- cumulusci/tasks/vcs/__init__.py +19 -0
- cumulusci/tasks/vcs/commit_status.py +58 -0
- cumulusci/tasks/vcs/create_commit_status.py +37 -0
- cumulusci/tasks/vcs/download_extract.py +199 -0
- cumulusci/tasks/vcs/merge.py +298 -0
- cumulusci/tasks/vcs/publish.py +207 -0
- cumulusci/tasks/vcs/pull_request.py +9 -0
- cumulusci/tasks/vcs/release.py +134 -0
- cumulusci/tasks/vcs/release_report.py +105 -0
- cumulusci/tasks/vcs/tag.py +31 -0
- cumulusci/tasks/vcs/tests/github/test_commit_status.py +196 -0
- cumulusci/tasks/vcs/tests/github/test_download_extract.py +896 -0
- cumulusci/tasks/vcs/tests/github/test_merge.py +1118 -0
- cumulusci/tasks/vcs/tests/github/test_publish.py +823 -0
- cumulusci/tasks/vcs/tests/github/test_pull_request.py +29 -0
- cumulusci/tasks/vcs/tests/github/test_release.py +390 -0
- cumulusci/tasks/vcs/tests/github/test_release_report.py +109 -0
- cumulusci/tasks/vcs/tests/github/test_tag.py +90 -0
- cumulusci/tasks/vlocity/exceptions.py +2 -0
- cumulusci/tasks/vlocity/tests/test_vlocity.py +283 -0
- cumulusci/tasks/vlocity/vlocity.py +342 -0
- cumulusci/tests/__init__.py +1 -0
- cumulusci/tests/cassettes/GET_sobjects_Account_PersonAccount_describe.yaml +18 -0
- cumulusci/tests/cassettes/TestIntegrationInfrastructure.test_integration_tests.yaml +19 -0
- cumulusci/tests/pytest_plugins/pytest_sf_orgconnect.py +307 -0
- cumulusci/tests/pytest_plugins/pytest_sf_vcr.py +275 -0
- cumulusci/tests/pytest_plugins/pytest_sf_vcr_serializer.py +160 -0
- cumulusci/tests/pytest_plugins/pytest_typeguard.py +5 -0
- cumulusci/tests/pytest_plugins/test_vcr_string_compressor.py +49 -0
- cumulusci/tests/pytest_plugins/vcr_string_compressor.py +97 -0
- cumulusci/tests/shared_cassettes/GET_sobjects_Account_describe.yaml +18 -0
- cumulusci/tests/shared_cassettes/GET_sobjects_Case_describe.yaml +18 -0
- cumulusci/tests/shared_cassettes/GET_sobjects_Contact_describe.yaml +4838 -0
- cumulusci/tests/shared_cassettes/GET_sobjects_Custom__c_describe.yaml +242 -0
- cumulusci/tests/shared_cassettes/GET_sobjects_Event_describe.yaml +19 -0
- cumulusci/tests/shared_cassettes/GET_sobjects_Global_describe.yaml +1338 -0
- cumulusci/tests/shared_cassettes/GET_sobjects_Lead_describe.yaml +18 -0
- cumulusci/tests/shared_cassettes/GET_sobjects_OpportunityContactRole_describe.yaml +34 -0
- cumulusci/tests/shared_cassettes/GET_sobjects_Opportunity_describe.yaml +1261 -0
- cumulusci/tests/shared_cassettes/GET_sobjects_Organization.yaml +49 -0
- cumulusci/tests/shared_cassettes/vcr_string_templates/batchInfoList_xml.tpl +15 -0
- cumulusci/tests/shared_cassettes/vcr_string_templates/batchInfo_xml.tpl +13 -0
- cumulusci/tests/shared_cassettes/vcr_string_templates/jobInfo_insert_xml.tpl +24 -0
- cumulusci/tests/shared_cassettes/vcr_string_templates/jobInfo_upsert_xml.tpl +25 -0
- cumulusci/tests/test_entry_points.py +20 -0
- cumulusci/tests/test_integration_infrastructure.py +131 -0
- cumulusci/tests/test_main.py +9 -0
- cumulusci/tests/test_schema.py +32 -0
- cumulusci/tests/test_utils.py +657 -0
- cumulusci/tests/test_vcr_serializer.py +134 -0
- cumulusci/tests/uncompressed_cassette.yaml +83 -0
- cumulusci/tests/util.py +344 -0
- cumulusci/utils/__init__.py +731 -0
- cumulusci/utils/classutils.py +9 -0
- cumulusci/utils/collections.py +32 -0
- cumulusci/utils/deprecation.py +11 -0
- cumulusci/utils/encryption.py +31 -0
- cumulusci/utils/fileutils.py +295 -0
- cumulusci/utils/git.py +142 -0
- cumulusci/utils/http/multi_request.py +214 -0
- cumulusci/utils/http/requests_utils.py +103 -0
- cumulusci/utils/http/tests/cassettes/ManualEditTestCompositeParallelSalesforce.test_http_headers.yaml +32 -0
- cumulusci/utils/http/tests/cassettes/TestCompositeParallelSalesforce.test_composite_parallel_salesforce.yaml +65 -0
- cumulusci/utils/http/tests/cassettes/TestCompositeParallelSalesforce.test_errors.yaml +24 -0
- cumulusci/utils/http/tests/cassettes/TestCompositeParallelSalesforce.test_reference_ids.yaml +49 -0
- cumulusci/utils/http/tests/test_multi_request.py +255 -0
- cumulusci/utils/iterators.py +21 -0
- cumulusci/utils/logging.py +128 -0
- cumulusci/utils/metaprogramming.py +10 -0
- cumulusci/utils/options.py +138 -0
- cumulusci/utils/parallel/queries_in_parallel/run_queries_in_parallel.py +29 -0
- cumulusci/utils/parallel/queries_in_parallel/tests/test_run_queries_in_parallel.py +50 -0
- cumulusci/utils/parallel/task_worker_queues/parallel_worker.py +238 -0
- cumulusci/utils/parallel/task_worker_queues/parallel_worker_queue.py +243 -0
- cumulusci/utils/parallel/task_worker_queues/tests/test_parallel_worker.py +353 -0
- cumulusci/utils/salesforce/count_sobjects.py +46 -0
- cumulusci/utils/salesforce/soql.py +17 -0
- cumulusci/utils/salesforce/tests/cassettes/ManualEdit_TestCountSObjects.test_count_sobjects__network_errors.yaml +23 -0
- cumulusci/utils/salesforce/tests/cassettes/TestCountSObjects.test_count_sobjects__errors.yaml +33 -0
- cumulusci/utils/salesforce/tests/cassettes/TestCountSObjects.test_count_sobjects_simple.yaml +29 -0
- cumulusci/utils/salesforce/tests/test_count_sobjects.py +29 -0
- cumulusci/utils/salesforce/tests/test_soql.py +30 -0
- cumulusci/utils/tests/cassettes/ManualEditTestDescribeOrg.test_minimal_schema.yaml +36 -0
- cumulusci/utils/tests/cassettes/ManualEdit_test_describe_to_sql.yaml +191 -0
- cumulusci/utils/tests/test_fileutils.py +284 -0
- cumulusci/utils/tests/test_git.py +85 -0
- cumulusci/utils/tests/test_logging.py +70 -0
- cumulusci/utils/tests/test_option_parsing.py +188 -0
- cumulusci/utils/tests/test_org_schema.py +691 -0
- cumulusci/utils/tests/test_org_schema_models.py +79 -0
- cumulusci/utils/tests/test_waiting.py +25 -0
- cumulusci/utils/version_strings.py +391 -0
- cumulusci/utils/waiting.py +42 -0
- cumulusci/utils/xml/__init__.py +91 -0
- cumulusci/utils/xml/metadata_tree.py +299 -0
- cumulusci/utils/xml/robot_xml.py +114 -0
- cumulusci/utils/xml/salesforce_encoding.py +100 -0
- cumulusci/utils/xml/test/test_metadata_tree.py +251 -0
- cumulusci/utils/xml/test/test_salesforce_encoding.py +173 -0
- cumulusci/utils/yaml/cumulusci_yml.py +401 -0
- cumulusci/utils/yaml/model_parser.py +156 -0
- cumulusci/utils/yaml/safer_loader.py +74 -0
- cumulusci/utils/yaml/tests/bad_cci.yml +5 -0
- cumulusci/utils/yaml/tests/cassettes/TestCumulusciYml.test_validate_url__with_errors.yaml +20 -0
- cumulusci/utils/yaml/tests/test_cumulusci_yml.py +286 -0
- cumulusci/utils/yaml/tests/test_model_parser.py +175 -0
- cumulusci/utils/yaml/tests/test_safer_loader.py +88 -0
- cumulusci/utils/ziputils.py +61 -0
- cumulusci/vcs/base.py +143 -0
- cumulusci/vcs/bootstrap.py +272 -0
- cumulusci/vcs/github/__init__.py +24 -0
- cumulusci/vcs/github/adapter.py +689 -0
- cumulusci/vcs/github/release_notes/generator.py +219 -0
- cumulusci/vcs/github/release_notes/parser.py +151 -0
- cumulusci/vcs/github/release_notes/provider.py +143 -0
- cumulusci/vcs/github/service.py +569 -0
- cumulusci/vcs/github/tests/test_adapter.py +138 -0
- cumulusci/vcs/github/tests/test_service.py +408 -0
- cumulusci/vcs/models.py +586 -0
- cumulusci/vcs/tests/conftest.py +41 -0
- cumulusci/vcs/tests/dummy_service.py +241 -0
- cumulusci/vcs/tests/test_vcs_base.py +687 -0
- cumulusci/vcs/tests/test_vcs_bootstrap.py +727 -0
- cumulusci/vcs/utils/__init__.py +31 -0
- cumulusci/vcs/vcs_source.py +287 -0
- cumulusci_plus-5.0.0.dist-info/METADATA +145 -0
- cumulusci_plus-5.0.0.dist-info/RECORD +744 -0
- cumulusci_plus-5.0.0.dist-info/WHEEL +4 -0
- cumulusci_plus-5.0.0.dist-info/entry_points.txt +3 -0
- cumulusci_plus-5.0.0.dist-info/licenses/AUTHORS.rst +41 -0
- cumulusci_plus-5.0.0.dist-info/licenses/LICENSE +30 -0
|
@@ -0,0 +1,936 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import logging
|
|
3
|
+
import re
|
|
4
|
+
import time
|
|
5
|
+
from pprint import pformat
|
|
6
|
+
|
|
7
|
+
from robot.libraries.BuiltIn import RobotNotRunningError
|
|
8
|
+
from robot.utils import timestr_to_secs
|
|
9
|
+
from selenium.common.exceptions import (
|
|
10
|
+
JavascriptException,
|
|
11
|
+
NoSuchElementException,
|
|
12
|
+
StaleElementReferenceException,
|
|
13
|
+
WebDriverException,
|
|
14
|
+
)
|
|
15
|
+
from selenium.webdriver.common.action_chains import ActionChains
|
|
16
|
+
from selenium.webdriver.common.keys import Keys
|
|
17
|
+
from SeleniumLibrary.errors import ElementNotFound, NoOpenBrowser
|
|
18
|
+
from urllib3.exceptions import ProtocolError
|
|
19
|
+
|
|
20
|
+
from cumulusci.robotframework import locator_manager
|
|
21
|
+
from cumulusci.robotframework.base_library import BaseLibrary
|
|
22
|
+
from cumulusci.robotframework.faker_mixin import FakerMixin
|
|
23
|
+
from cumulusci.robotframework.form_handlers import get_form_handler
|
|
24
|
+
from cumulusci.robotframework.utils import (
|
|
25
|
+
capture_screenshot_on_error,
|
|
26
|
+
get_locator_module_name,
|
|
27
|
+
selenium_retry,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
OID_REGEX = r"^(%2F)?([a-zA-Z0-9]{15,18})$"
|
|
31
|
+
STATUS_KEY = ("status",)
|
|
32
|
+
|
|
33
|
+
lex_locators = {} # will be initialized when Salesforce is instantiated
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@selenium_retry
|
|
37
|
+
class Salesforce(FakerMixin, BaseLibrary):
|
|
38
|
+
"""A keyword library for working with Salesforce Lightning pages
|
|
39
|
+
|
|
40
|
+
While you can import this directly into any suite, the recommended way
|
|
41
|
+
to include this in a test suite is to import the ``Salesforce.robot``
|
|
42
|
+
resource file.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
ROBOT_LIBRARY_SCOPE = "GLOBAL"
|
|
46
|
+
|
|
47
|
+
def __init__(self, debug=False, locators=None):
|
|
48
|
+
super().__init__()
|
|
49
|
+
self.debug = debug
|
|
50
|
+
self._session_records = []
|
|
51
|
+
# Turn off info logging of all http requests
|
|
52
|
+
logging.getLogger("requests.packages.urllib3.connectionpool").setLevel(
|
|
53
|
+
logging.WARN
|
|
54
|
+
)
|
|
55
|
+
if locators:
|
|
56
|
+
lex_locators.update(locators)
|
|
57
|
+
else:
|
|
58
|
+
self._init_locators()
|
|
59
|
+
|
|
60
|
+
def _init_locators(self):
|
|
61
|
+
"""Load the appropriate locator file for the current version
|
|
62
|
+
|
|
63
|
+
If no version can be determined, we'll use the highest numbered
|
|
64
|
+
locator file name.
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
version = int(float(self.salesforce_api.get_latest_api_version()))
|
|
68
|
+
|
|
69
|
+
except RobotNotRunningError:
|
|
70
|
+
# Likely this means we are running in the context of
|
|
71
|
+
# documentation generation. Setting the version to
|
|
72
|
+
# None will result in using the latest version of
|
|
73
|
+
# locators.
|
|
74
|
+
version = None
|
|
75
|
+
|
|
76
|
+
locator_module_name = get_locator_module_name(version)
|
|
77
|
+
self.locators_module = importlib.import_module(locator_module_name)
|
|
78
|
+
lex_locators.update(self.locators_module.lex_locators)
|
|
79
|
+
|
|
80
|
+
def initialize_location_strategies(self):
|
|
81
|
+
"""Initialize the Salesforce custom location strategies
|
|
82
|
+
|
|
83
|
+
Note: This keyword is called automatically from *Open Test Browser*
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
if not self.builtin.get_variable_value(
|
|
87
|
+
"${LOCATION STRATEGIES INITIALIZED}", False
|
|
88
|
+
):
|
|
89
|
+
# this manages strategies based on locators in a dictionary
|
|
90
|
+
locator_manager.register_locators("sf", lex_locators)
|
|
91
|
+
locator_manager.add_location_strategies()
|
|
92
|
+
|
|
93
|
+
# these are more traditional location strategies based on keywords
|
|
94
|
+
# or functions
|
|
95
|
+
self.selenium.add_location_strategy(
|
|
96
|
+
"text", "Salesforce.Locate Element by Text"
|
|
97
|
+
)
|
|
98
|
+
self.selenium.add_location_strategy(
|
|
99
|
+
"title", "Salesforce.Locate Element by Title"
|
|
100
|
+
)
|
|
101
|
+
self.selenium.add_location_strategy("label", self._locate_element_by_label)
|
|
102
|
+
self.builtin.set_suite_variable("${LOCATION STRATEGIES INITIALIZED}", True)
|
|
103
|
+
|
|
104
|
+
@selenium_retry(False)
|
|
105
|
+
def _jsclick(self, locator):
|
|
106
|
+
"""Use javascript to click an element on the page
|
|
107
|
+
|
|
108
|
+
See https://help.salesforce.com/articleView?id=000352057&language=en_US&mode=1&type=1
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
self.selenium.wait_until_page_contains_element(locator)
|
|
112
|
+
self.selenium.wait_until_element_is_enabled(locator)
|
|
113
|
+
for should_retry in (True, False):
|
|
114
|
+
try:
|
|
115
|
+
# Setting the focus first seems to be required as of Spring'20
|
|
116
|
+
# (read: without it, tests started failing in that release). I
|
|
117
|
+
# suspect it's because there is a focusOut handler on form
|
|
118
|
+
# fields which need to be triggered for data to be accepted.
|
|
119
|
+
element = self.selenium.get_webelement(locator)
|
|
120
|
+
self.selenium.driver.execute_script(
|
|
121
|
+
"arguments[0].focus(); arguments[0].click()", element
|
|
122
|
+
)
|
|
123
|
+
return
|
|
124
|
+
except StaleElementReferenceException:
|
|
125
|
+
if should_retry:
|
|
126
|
+
time.sleep(1)
|
|
127
|
+
else:
|
|
128
|
+
raise
|
|
129
|
+
|
|
130
|
+
def create_webdriver_with_retry(self, *args, **kwargs):
|
|
131
|
+
"""Call the Create Webdriver keyword.
|
|
132
|
+
|
|
133
|
+
Retry on connection resets which can happen if custom domain propagation is slow.
|
|
134
|
+
"""
|
|
135
|
+
# Get selenium without referencing selenium.driver which doesn't exist yet
|
|
136
|
+
selenium = self.builtin.get_library_instance("SeleniumLibrary")
|
|
137
|
+
for _ in range(12):
|
|
138
|
+
try:
|
|
139
|
+
return selenium.create_webdriver(*args, **kwargs)
|
|
140
|
+
except ProtocolError:
|
|
141
|
+
# Give browser some more time to start up
|
|
142
|
+
time.sleep(5)
|
|
143
|
+
raise Exception("Could not connect to remote webdriver after 1 minute")
|
|
144
|
+
|
|
145
|
+
@capture_screenshot_on_error
|
|
146
|
+
def click_modal_button(self, title):
|
|
147
|
+
"""Clicks a button in a Lightning modal."""
|
|
148
|
+
locator = lex_locators["modal"]["button"].format(title)
|
|
149
|
+
self.selenium.wait_until_page_contains_element(locator)
|
|
150
|
+
self.selenium.wait_until_element_is_enabled(locator)
|
|
151
|
+
self._jsclick(locator)
|
|
152
|
+
|
|
153
|
+
@capture_screenshot_on_error
|
|
154
|
+
def click_object_button(self, title):
|
|
155
|
+
"""Clicks a button in an object's actions."""
|
|
156
|
+
locator = lex_locators["object"]["button"].format(title=title)
|
|
157
|
+
self._jsclick(locator)
|
|
158
|
+
self.wait_until_modal_is_open()
|
|
159
|
+
|
|
160
|
+
@capture_screenshot_on_error
|
|
161
|
+
def scroll_element_into_view(self, locator):
|
|
162
|
+
"""Scroll the element identified by 'locator'
|
|
163
|
+
|
|
164
|
+
This is a replacement for the keyword of the same name in
|
|
165
|
+
SeleniumLibrary. The SeleniumLibrary implementation uses
|
|
166
|
+
an unreliable method on Firefox. This keyword uses
|
|
167
|
+
a more reliable technique.
|
|
168
|
+
|
|
169
|
+
For more info see https://stackoverflow.com/a/52045231/7432
|
|
170
|
+
"""
|
|
171
|
+
element = self.selenium.get_webelement(locator)
|
|
172
|
+
self.selenium.driver.execute_script(
|
|
173
|
+
"arguments[0].scrollIntoView({behavior: 'auto', block: 'center', inline: 'center'})",
|
|
174
|
+
element,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
@capture_screenshot_on_error
|
|
178
|
+
def load_related_list(self, heading, tries=10):
|
|
179
|
+
"""Scrolls down until the specified related list loads.
|
|
180
|
+
|
|
181
|
+
If the related list isn't found, the keyword will scroll down
|
|
182
|
+
in 100 pixel increments to trigger lightning into loading the
|
|
183
|
+
list. This process of scrolling will be repeated until the
|
|
184
|
+
related list has been loaded or we've tried several times
|
|
185
|
+
(the default is 10 tries)
|
|
186
|
+
|
|
187
|
+
"""
|
|
188
|
+
locator = lex_locators["record"]["related"]["card"].format(heading)
|
|
189
|
+
for i in range(tries):
|
|
190
|
+
try:
|
|
191
|
+
self.scroll_element_into_view(locator)
|
|
192
|
+
return
|
|
193
|
+
except (ElementNotFound, JavascriptException, WebDriverException):
|
|
194
|
+
self.builtin.log(
|
|
195
|
+
f"related list '{heading}' not found; scrolling...", "DEBUG"
|
|
196
|
+
)
|
|
197
|
+
self.selenium.execute_javascript("window.scrollBy(0, 100)")
|
|
198
|
+
self.wait_for_aura()
|
|
199
|
+
raise AssertionError(f"Timed out waiting for related list '{heading}' to load.")
|
|
200
|
+
|
|
201
|
+
@capture_screenshot_on_error
|
|
202
|
+
def click_related_list_button(self, heading, button_title):
|
|
203
|
+
"""Clicks a button in the heading of a related list.
|
|
204
|
+
|
|
205
|
+
Waits for a modal to open after clicking the button.
|
|
206
|
+
"""
|
|
207
|
+
self.load_related_list(heading)
|
|
208
|
+
locator = lex_locators["record"]["related"]["button"].format(
|
|
209
|
+
heading, button_title
|
|
210
|
+
)
|
|
211
|
+
self._jsclick(locator)
|
|
212
|
+
self.wait_until_modal_is_open()
|
|
213
|
+
|
|
214
|
+
@capture_screenshot_on_error
|
|
215
|
+
def click_related_item_link(self, heading, title):
|
|
216
|
+
"""Clicks a link in the related list with the specified heading.
|
|
217
|
+
|
|
218
|
+
This keyword will automatically call *Wait until loading is complete*.
|
|
219
|
+
"""
|
|
220
|
+
self.load_related_list(heading)
|
|
221
|
+
locator = lex_locators["record"]["related"]["link"].format(heading, title)
|
|
222
|
+
try:
|
|
223
|
+
self._jsclick(locator)
|
|
224
|
+
except Exception as e:
|
|
225
|
+
self.builtin.log(f"Exception: {e}", "DEBUG")
|
|
226
|
+
raise Exception(
|
|
227
|
+
f"Unable to find related link under heading '{heading}' with the text '{title}'"
|
|
228
|
+
)
|
|
229
|
+
self.wait_until_loading_is_complete()
|
|
230
|
+
|
|
231
|
+
@capture_screenshot_on_error
|
|
232
|
+
def click_related_item_popup_link(self, heading, title, link):
|
|
233
|
+
"""Clicks a link in the popup menu for a related list item.
|
|
234
|
+
|
|
235
|
+
heading specifies the name of the list,
|
|
236
|
+
title specifies the name of the item,
|
|
237
|
+
and link specifies the name of the link
|
|
238
|
+
"""
|
|
239
|
+
self.load_related_list(heading)
|
|
240
|
+
locator = lex_locators["record"]["related"]["popup_trigger"].format(
|
|
241
|
+
heading, title
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
self.selenium.wait_until_page_contains_element(locator)
|
|
245
|
+
self._jsclick(locator)
|
|
246
|
+
locator = lex_locators["popup"]["link"].format(link)
|
|
247
|
+
self._jsclick(locator)
|
|
248
|
+
self.wait_until_loading_is_complete()
|
|
249
|
+
|
|
250
|
+
@capture_screenshot_on_error
|
|
251
|
+
def close_modal(self):
|
|
252
|
+
"""Closes the open modal"""
|
|
253
|
+
locator = lex_locators["modal"]["close"]
|
|
254
|
+
self._jsclick(locator)
|
|
255
|
+
|
|
256
|
+
@capture_screenshot_on_error
|
|
257
|
+
def current_app_should_be(self, app_name):
|
|
258
|
+
"""Validates the currently selected Salesforce App"""
|
|
259
|
+
locator = lex_locators["app_launcher"]["current_app"].format(app_name)
|
|
260
|
+
elem = self.selenium.get_webelement(locator)
|
|
261
|
+
assert app_name == elem.text, "Expected app to be {} but found {}".format(
|
|
262
|
+
app_name, elem.text
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def get_active_browser_ids(self):
|
|
266
|
+
"""Return the id of all open browser ids"""
|
|
267
|
+
|
|
268
|
+
# This relies on some private data structures, but presently
|
|
269
|
+
# there is no other way. There's been a discussion in the
|
|
270
|
+
# robot slack channels about adding a new keyword that does
|
|
271
|
+
# what this keyword does. When that happens, we can remove
|
|
272
|
+
# this keyword.
|
|
273
|
+
driver_ids = []
|
|
274
|
+
try:
|
|
275
|
+
driver_cache = self.selenium._drivers
|
|
276
|
+
except NoOpenBrowser:
|
|
277
|
+
return []
|
|
278
|
+
|
|
279
|
+
for index, driver in enumerate(driver_cache._connections):
|
|
280
|
+
if driver not in driver_cache._closed:
|
|
281
|
+
# SeleniumLibrary driver ids start at one rather than zero
|
|
282
|
+
driver_ids.append(index + 1)
|
|
283
|
+
return driver_ids
|
|
284
|
+
|
|
285
|
+
def get_current_record_id(self):
|
|
286
|
+
"""Parses the current url to get the object id of the current record.
|
|
287
|
+
Expects url format like: [a-zA-Z0-9]{15,18}
|
|
288
|
+
"""
|
|
289
|
+
url = self.selenium.get_location()
|
|
290
|
+
for part in url.split("/"):
|
|
291
|
+
oid_match = re.match(OID_REGEX, part)
|
|
292
|
+
if oid_match is not None:
|
|
293
|
+
return oid_match.group(2)
|
|
294
|
+
raise AssertionError("Could not parse record id from url: {}".format(url))
|
|
295
|
+
|
|
296
|
+
@capture_screenshot_on_error
|
|
297
|
+
def field_value_should_be(self, label, expected_value):
|
|
298
|
+
"""Verify that the form field for the given label is the expected value
|
|
299
|
+
|
|
300
|
+
Example:
|
|
301
|
+
|
|
302
|
+
| Field value should be Account Name ACME Labs
|
|
303
|
+
"""
|
|
304
|
+
value = self.get_field_value(label)
|
|
305
|
+
self.builtin.should_be_equal(value, expected_value)
|
|
306
|
+
|
|
307
|
+
@capture_screenshot_on_error
|
|
308
|
+
def get_field_value(self, label):
|
|
309
|
+
"""Return the current value of a form field based on the field label"""
|
|
310
|
+
|
|
311
|
+
locator = self._get_input_field_locator(label)
|
|
312
|
+
# this works for both First Name (input) and Account Name (picklist)
|
|
313
|
+
value = self.selenium.get_value(locator)
|
|
314
|
+
|
|
315
|
+
return value
|
|
316
|
+
|
|
317
|
+
def get_locator(self, path, *args, **kwargs):
|
|
318
|
+
"""Returns a rendered locator string from the Salesforce lex_locators
|
|
319
|
+
dictionary. This can be useful if you want to use an element in
|
|
320
|
+
a different way than the built in keywords allow.
|
|
321
|
+
"""
|
|
322
|
+
locator = lex_locators
|
|
323
|
+
for key in path.split("."):
|
|
324
|
+
locator = locator[key]
|
|
325
|
+
return locator.format(*args, **kwargs)
|
|
326
|
+
|
|
327
|
+
def get_related_list_count(self, heading):
|
|
328
|
+
"""Returns the number of items indicated for a related list."""
|
|
329
|
+
locator = lex_locators["record"]["related"]["count"].format(heading)
|
|
330
|
+
count = self.selenium.get_webelement(locator).text
|
|
331
|
+
count = count.replace("(", "").replace(")", "")
|
|
332
|
+
return int(count)
|
|
333
|
+
|
|
334
|
+
def go_to_object_home(self, obj_name):
|
|
335
|
+
"""Navigates to the Home view of a Salesforce Object"""
|
|
336
|
+
url = self.cumulusci.org.lightning_base_url
|
|
337
|
+
url = "{}/lightning/o/{}/home".format(url, obj_name)
|
|
338
|
+
self.selenium.go_to(url)
|
|
339
|
+
self.wait_until_loading_is_complete(lex_locators["actions"])
|
|
340
|
+
|
|
341
|
+
def go_to_object_list(self, obj_name, filter_name=None):
|
|
342
|
+
"""Navigates to the Home view of a Salesforce Object"""
|
|
343
|
+
url = self.cumulusci.org.lightning_base_url
|
|
344
|
+
url = "{}/lightning/o/{}/list".format(url, obj_name)
|
|
345
|
+
if filter_name:
|
|
346
|
+
url += "?filterName={}".format(filter_name)
|
|
347
|
+
self.selenium.go_to(url)
|
|
348
|
+
self.wait_until_loading_is_complete(lex_locators["actions"])
|
|
349
|
+
|
|
350
|
+
def go_to_record_home(self, obj_id):
|
|
351
|
+
"""Navigates to the Home view of a Salesforce Object"""
|
|
352
|
+
url = self.cumulusci.org.lightning_base_url
|
|
353
|
+
url = "{}/lightning/r/{}/view".format(url, obj_id)
|
|
354
|
+
self.selenium.go_to(url)
|
|
355
|
+
self.wait_until_loading_is_complete(lex_locators["actions"])
|
|
356
|
+
|
|
357
|
+
def go_to_setup_home(self):
|
|
358
|
+
"""Navigates to the Home tab of Salesforce Setup"""
|
|
359
|
+
url = self.cumulusci.org.lightning_base_url
|
|
360
|
+
self.selenium.go_to(url + "/lightning/setup/SetupOneHome/home")
|
|
361
|
+
self.wait_until_loading_is_complete()
|
|
362
|
+
|
|
363
|
+
def go_to_setup_object_manager(self):
|
|
364
|
+
"""Navigates to the Object Manager tab of Salesforce Setup"""
|
|
365
|
+
url = self.cumulusci.org.lightning_base_url
|
|
366
|
+
self.selenium.go_to(url + "/lightning/setup/ObjectManager/home")
|
|
367
|
+
self.wait_until_loading_is_complete()
|
|
368
|
+
|
|
369
|
+
@capture_screenshot_on_error
|
|
370
|
+
def header_field_should_have_value(self, label):
|
|
371
|
+
"""Validates that a field in the record header has a text value.
|
|
372
|
+
NOTE: Use other keywords for non-string value types
|
|
373
|
+
"""
|
|
374
|
+
locator = lex_locators["record"]["header"]["field_value"].format(label)
|
|
375
|
+
self.selenium.page_should_contain_element(locator)
|
|
376
|
+
|
|
377
|
+
@capture_screenshot_on_error
|
|
378
|
+
def header_field_should_not_have_value(self, label):
|
|
379
|
+
"""Validates that a field in the record header does not have a value.
|
|
380
|
+
NOTE: Use other keywords for non-string value types
|
|
381
|
+
"""
|
|
382
|
+
locator = lex_locators["record"]["header"]["field_value"].format(label)
|
|
383
|
+
self.selenium.page_should_not_contain_element(locator)
|
|
384
|
+
|
|
385
|
+
@capture_screenshot_on_error
|
|
386
|
+
def header_field_should_have_link(self, label):
|
|
387
|
+
"""Validates that a field in the record header has a link as its value"""
|
|
388
|
+
locator = lex_locators["record"]["header"]["field_value_link"].format(label)
|
|
389
|
+
self.selenium.page_should_contain_element(locator)
|
|
390
|
+
|
|
391
|
+
@capture_screenshot_on_error
|
|
392
|
+
def header_field_should_not_have_link(self, label):
|
|
393
|
+
"""Validates that a field in the record header does not have a link as its value"""
|
|
394
|
+
locator = lex_locators["record"]["header"]["field_value_link"].format(label)
|
|
395
|
+
self.selenium.page_should_not_contain_element(locator)
|
|
396
|
+
|
|
397
|
+
@capture_screenshot_on_error
|
|
398
|
+
def click_header_field_link(self, label):
|
|
399
|
+
"""Clicks a link in record header."""
|
|
400
|
+
locator = lex_locators["record"]["header"]["field_value_link"].format(label)
|
|
401
|
+
self._jsclick(locator)
|
|
402
|
+
|
|
403
|
+
@capture_screenshot_on_error
|
|
404
|
+
def header_field_should_be_checked(self, label):
|
|
405
|
+
"""Validates that a checkbox field in the record header is checked"""
|
|
406
|
+
locator = lex_locators["record"]["header"]["field_value_checked"].format(label)
|
|
407
|
+
self.selenium.page_should_contain_element(locator)
|
|
408
|
+
|
|
409
|
+
@capture_screenshot_on_error
|
|
410
|
+
def header_field_should_be_unchecked(self, label):
|
|
411
|
+
"""Validates that a checkbox field in the record header is unchecked"""
|
|
412
|
+
locator = lex_locators["record"]["header"]["field_value_unchecked"].format(
|
|
413
|
+
label
|
|
414
|
+
)
|
|
415
|
+
self.selenium.page_should_contain_element(locator)
|
|
416
|
+
|
|
417
|
+
def log_browser_capabilities(self, loglevel="INFO"):
|
|
418
|
+
"""Logs all of the browser capabilities as reported by selenium"""
|
|
419
|
+
output = "selenium browser capabilities:\n"
|
|
420
|
+
output += pformat(self.selenium.driver.capabilities, indent=4)
|
|
421
|
+
self.builtin.log(output, level=loglevel)
|
|
422
|
+
|
|
423
|
+
@capture_screenshot_on_error
|
|
424
|
+
def open_app_launcher(self, retry=True):
|
|
425
|
+
"""Opens the Saleforce App Launcher Modal
|
|
426
|
+
|
|
427
|
+
Note: starting with Spring '20 the app launcher button opens a
|
|
428
|
+
menu rather than a modal. To maintain backwards compatibility,
|
|
429
|
+
this keyword will continue to open the modal rather than the
|
|
430
|
+
menu. If you need to interact with the app launcher menu, you
|
|
431
|
+
will need to create a custom keyword.
|
|
432
|
+
|
|
433
|
+
If the retry parameter is true, the keyword will
|
|
434
|
+
close and then re-open the app launcher if it times out
|
|
435
|
+
while waiting for the dialog to open.
|
|
436
|
+
"""
|
|
437
|
+
|
|
438
|
+
self._jsclick("sf:app_launcher.button")
|
|
439
|
+
self.selenium.wait_until_element_is_visible("sf:app_launcher.view_all")
|
|
440
|
+
self._jsclick("sf:app_launcher.view_all")
|
|
441
|
+
self.wait_until_modal_is_open()
|
|
442
|
+
try:
|
|
443
|
+
# the modal may be open, but not yet fully rendered
|
|
444
|
+
# wait until at least one link appears. We've seen that sometimes
|
|
445
|
+
# the dialog hangs prior to any links showing up
|
|
446
|
+
self.selenium.wait_until_element_is_visible(
|
|
447
|
+
"xpath://ul[contains(@class, 'al-modal-list')]//li"
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
except Exception as e:
|
|
451
|
+
# This should never happen, yet it does. Experience has
|
|
452
|
+
# shown that sometimes (at least in spring '20) the modal
|
|
453
|
+
# never renders. Refreshing the modal seems to fix it.
|
|
454
|
+
if retry:
|
|
455
|
+
self.builtin.log(
|
|
456
|
+
f"caught exception {e} waiting for app launcher; retrying", "DEBUG"
|
|
457
|
+
)
|
|
458
|
+
self.selenium.press_keys("sf:modal.is_open", "ESCAPE")
|
|
459
|
+
self.wait_until_modal_is_closed()
|
|
460
|
+
self.open_app_launcher(retry=False)
|
|
461
|
+
else:
|
|
462
|
+
self.builtin.log(
|
|
463
|
+
"caught exception waiting for app launcher; not retrying", "DEBUG"
|
|
464
|
+
)
|
|
465
|
+
raise
|
|
466
|
+
|
|
467
|
+
@capture_screenshot_on_error
|
|
468
|
+
def populate_field(self, name, value):
|
|
469
|
+
"""Enters a value into an input or textarea field.
|
|
470
|
+
|
|
471
|
+
'name' represents the label on the page (eg: "First Name"),
|
|
472
|
+
and 'value' is the new value.
|
|
473
|
+
|
|
474
|
+
Any existing value will be replaced.
|
|
475
|
+
"""
|
|
476
|
+
locator = self._get_input_field_locator(name)
|
|
477
|
+
self._populate_field(locator, value)
|
|
478
|
+
|
|
479
|
+
@capture_screenshot_on_error
|
|
480
|
+
def populate_lookup_field(self, name, value):
|
|
481
|
+
"""Enters a value into a lookup field."""
|
|
482
|
+
input_locator = self._get_input_field_locator(name)
|
|
483
|
+
menu_locator = lex_locators["object"]["field_lookup_link"].format(value)
|
|
484
|
+
|
|
485
|
+
self._populate_field(input_locator, value)
|
|
486
|
+
|
|
487
|
+
for x in range(3):
|
|
488
|
+
self.wait_for_aura()
|
|
489
|
+
try:
|
|
490
|
+
self.selenium.get_webelement(menu_locator)
|
|
491
|
+
except ElementNotFound:
|
|
492
|
+
# Give indexing a chance to catch up
|
|
493
|
+
time.sleep(2)
|
|
494
|
+
field = self.selenium.get_webelement(input_locator)
|
|
495
|
+
field.send_keys(Keys.BACK_SPACE)
|
|
496
|
+
else:
|
|
497
|
+
break
|
|
498
|
+
self.selenium.set_focus_to_element(menu_locator)
|
|
499
|
+
self._jsclick(menu_locator)
|
|
500
|
+
self.wait_for_aura()
|
|
501
|
+
|
|
502
|
+
def _get_input_field_locator(self, name):
|
|
503
|
+
"""Given an input field label, return a locator for the related input field
|
|
504
|
+
|
|
505
|
+
This looks for a <label> element with the given text, or
|
|
506
|
+
a label with a span with the given text. The value of the
|
|
507
|
+
'for' attribute is then extracted from the label and used
|
|
508
|
+
to create a new locator with that id.
|
|
509
|
+
|
|
510
|
+
For example, the locator 'abc123' will be returned
|
|
511
|
+
for the following html:
|
|
512
|
+
|
|
513
|
+
<label for='abc123'>First Name</label>
|
|
514
|
+
-or-
|
|
515
|
+
<label for='abc123'><span>First Name</span>
|
|
516
|
+
"""
|
|
517
|
+
try:
|
|
518
|
+
# we need to make sure that if a modal is open, we only find
|
|
519
|
+
# the input element inside the modal. Otherwise it's possible
|
|
520
|
+
# that the xpath could pick the wrong element.
|
|
521
|
+
self.selenium.get_webelement(lex_locators["modal"]["is_open"])
|
|
522
|
+
modal_prefix = "//div[contains(@class, 'modal-container')]"
|
|
523
|
+
except ElementNotFound:
|
|
524
|
+
modal_prefix = ""
|
|
525
|
+
|
|
526
|
+
locator = modal_prefix + lex_locators["object"]["field_label"].format(
|
|
527
|
+
name, name
|
|
528
|
+
)
|
|
529
|
+
input_element_id = self.selenium.get_element_attribute(locator, "for")
|
|
530
|
+
return input_element_id
|
|
531
|
+
|
|
532
|
+
def _populate_field(self, locator, value):
|
|
533
|
+
self.builtin.log(f"value: {value}' locator: '{locator}'", "DEBUG")
|
|
534
|
+
field = self.selenium.get_webelement(locator)
|
|
535
|
+
self._focus(field)
|
|
536
|
+
if field.get_attribute("value"):
|
|
537
|
+
self._clear(field)
|
|
538
|
+
field.send_keys(value)
|
|
539
|
+
|
|
540
|
+
def _focus(self, element):
|
|
541
|
+
"""Set focus to an element
|
|
542
|
+
|
|
543
|
+
In addition to merely setting the focus, we click the mouse
|
|
544
|
+
to the field in case there are functions tied to that event.
|
|
545
|
+
"""
|
|
546
|
+
self.selenium.set_focus_to_element(element)
|
|
547
|
+
element.click()
|
|
548
|
+
|
|
549
|
+
def _clear(self, element):
|
|
550
|
+
"""Clear the field, using any means necessary
|
|
551
|
+
|
|
552
|
+
This is surprisingly hard to do with a generic solution. Some
|
|
553
|
+
methods work for some components and/or on some browsers but
|
|
554
|
+
not others. Therefore, several techniques are employed.
|
|
555
|
+
"""
|
|
556
|
+
|
|
557
|
+
element.clear()
|
|
558
|
+
self.selenium.driver.execute_script("arguments[0].value = '';", element)
|
|
559
|
+
|
|
560
|
+
# Select all and delete just in case the element didn't get cleared
|
|
561
|
+
element.send_keys(Keys.HOME + Keys.SHIFT + Keys.END)
|
|
562
|
+
element.send_keys(Keys.BACKSPACE)
|
|
563
|
+
|
|
564
|
+
if element.get_attribute("value"):
|
|
565
|
+
# Give the UI a chance to settle down. The sleep appears
|
|
566
|
+
# necessary. Without it, this keyword sometimes fails to work
|
|
567
|
+
# properly. With it, I was able to run 700+ tests without a single
|
|
568
|
+
# failure.
|
|
569
|
+
time.sleep(0.25)
|
|
570
|
+
|
|
571
|
+
# Even after all that, some elements refuse to be cleared out.
|
|
572
|
+
# I'm looking at you, currency fields on Firefox.
|
|
573
|
+
if element.get_attribute("value"):
|
|
574
|
+
self._force_clear(element)
|
|
575
|
+
|
|
576
|
+
def _force_clear(self, element):
|
|
577
|
+
"""Use brute-force to clear an element
|
|
578
|
+
|
|
579
|
+
This moves the cursor to the end of the input field and
|
|
580
|
+
then issues a series of backspace keys to delete the data
|
|
581
|
+
in the field.
|
|
582
|
+
"""
|
|
583
|
+
value = element.get_attribute("value")
|
|
584
|
+
actions = ActionChains(self.selenium.driver)
|
|
585
|
+
actions.move_to_element(element).click().send_keys(Keys.END)
|
|
586
|
+
for character in value:
|
|
587
|
+
actions.send_keys(Keys.BACKSPACE)
|
|
588
|
+
actions.perform()
|
|
589
|
+
|
|
590
|
+
def populate_form(self, **kwargs):
|
|
591
|
+
"""Enters multiple values from a mapping into form fields."""
|
|
592
|
+
for name, value in kwargs.items():
|
|
593
|
+
self.populate_field(name, value)
|
|
594
|
+
|
|
595
|
+
def select_record_type(self, label):
|
|
596
|
+
"""Selects a record type while adding an object."""
|
|
597
|
+
self.wait_until_modal_is_open()
|
|
598
|
+
locator = lex_locators["object"]["record_type_option"].format(label)
|
|
599
|
+
self._jsclick(locator)
|
|
600
|
+
self.selenium.click_button("Next")
|
|
601
|
+
|
|
602
|
+
@capture_screenshot_on_error
|
|
603
|
+
def select_app_launcher_app(self, app_name, timeout=30):
|
|
604
|
+
"""Navigates to a Salesforce App via the App Launcher"""
|
|
605
|
+
locator = lex_locators["app_launcher"]["app_link"].format(app_name)
|
|
606
|
+
self.open_app_launcher()
|
|
607
|
+
self.selenium.wait_until_page_contains_element(locator, timeout)
|
|
608
|
+
self.selenium.set_focus_to_element(locator)
|
|
609
|
+
elem = self.selenium.get_webelement(locator)
|
|
610
|
+
link = elem.find_element_by_xpath("../../..")
|
|
611
|
+
self.selenium.set_focus_to_element(link)
|
|
612
|
+
link.click()
|
|
613
|
+
self.wait_until_modal_is_closed()
|
|
614
|
+
|
|
615
|
+
@capture_screenshot_on_error
|
|
616
|
+
def select_app_launcher_tab(self, tab_name):
|
|
617
|
+
"""Navigates to a tab via the App Launcher"""
|
|
618
|
+
locator = lex_locators["app_launcher"]["tab_link"].format(tab_name)
|
|
619
|
+
self.open_app_launcher()
|
|
620
|
+
self.selenium.wait_until_page_contains_element(locator)
|
|
621
|
+
self.selenium.set_focus_to_element(locator)
|
|
622
|
+
self._jsclick(locator)
|
|
623
|
+
self.wait_until_modal_is_closed()
|
|
624
|
+
|
|
625
|
+
@capture_screenshot_on_error
|
|
626
|
+
def wait_until_modal_is_open(self, timeout=15):
|
|
627
|
+
"""Wait for modal to open"""
|
|
628
|
+
self.selenium.wait_until_page_contains_element(
|
|
629
|
+
lex_locators["modal"]["is_open"],
|
|
630
|
+
timeout,
|
|
631
|
+
error="Expected to see a modal window, but didn't",
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
@capture_screenshot_on_error
|
|
635
|
+
def wait_until_modal_is_closed(self, timeout=15):
|
|
636
|
+
"""Wait for modal to close"""
|
|
637
|
+
self.selenium.wait_until_page_does_not_contain_element(
|
|
638
|
+
lex_locators["modal"]["is_open"], timeout
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
@capture_screenshot_on_error
|
|
642
|
+
def wait_until_loading_is_complete(self, locator=None):
|
|
643
|
+
"""Wait for LEX page to load.
|
|
644
|
+
|
|
645
|
+
(We're actually waiting for the actions ribbon to appear.)
|
|
646
|
+
"""
|
|
647
|
+
locator = lex_locators["body"] if locator is None else locator
|
|
648
|
+
self.selenium.wait_until_page_contains_element(locator)
|
|
649
|
+
self.wait_for_aura()
|
|
650
|
+
# this knowledge article recommends waiting a second. I don't
|
|
651
|
+
# like it, but it seems to help. We should do a wait instead,
|
|
652
|
+
# but I can't figure out what to wait on.
|
|
653
|
+
# https://help.salesforce.com/articleView?id=000352057&language=en_US&mode=1&type=1
|
|
654
|
+
time.sleep(1)
|
|
655
|
+
|
|
656
|
+
@capture_screenshot_on_error
|
|
657
|
+
def wait_until_salesforce_is_ready(self, locator=None, timeout=None, interval=5):
|
|
658
|
+
"""Waits until we are able to render the initial salesforce landing page
|
|
659
|
+
|
|
660
|
+
It will continue to refresh the page until we land on a
|
|
661
|
+
lightning page or until a timeout has been reached. The
|
|
662
|
+
timeout can be specified in any time string supported by robot
|
|
663
|
+
(eg: number of seconds, "3 minutes", etc.). If not specified,
|
|
664
|
+
the default selenium timeout will be used.
|
|
665
|
+
|
|
666
|
+
This keyword will wait a few seconds between each refresh, as
|
|
667
|
+
well as wait after each refresh for the page to fully render
|
|
668
|
+
(ie: it calls wait_for_aura())
|
|
669
|
+
|
|
670
|
+
"""
|
|
671
|
+
|
|
672
|
+
# Note: we can't just ask selenium to wait for an element,
|
|
673
|
+
# because the org might not be availble due to infrastructure
|
|
674
|
+
# issues (eg: the domain not being propagated). In such a case
|
|
675
|
+
# the element will never come. Instead, what we need to do is
|
|
676
|
+
# repeatedly refresh the page until the org responds.
|
|
677
|
+
#
|
|
678
|
+
# This assumes that any lightning page is a valid stopping
|
|
679
|
+
# point. If salesforce starts rendering error pages with
|
|
680
|
+
# lightning, or an org's default home page is not a lightning
|
|
681
|
+
# page, we may have to rethink that strategy.
|
|
682
|
+
|
|
683
|
+
interval = 5 # seconds between each refresh.
|
|
684
|
+
timeout = timeout if timeout else self.selenium.get_selenium_timeout()
|
|
685
|
+
timeout_seconds = timestr_to_secs(timeout)
|
|
686
|
+
start_time = time.time()
|
|
687
|
+
login_url = self.cumulusci.login_url()
|
|
688
|
+
locator = lex_locators["body"] if locator is None else locator
|
|
689
|
+
|
|
690
|
+
while True:
|
|
691
|
+
try:
|
|
692
|
+
self.selenium.wait_for_condition(
|
|
693
|
+
"return (document.readyState == 'complete')"
|
|
694
|
+
)
|
|
695
|
+
self.wait_for_aura()
|
|
696
|
+
|
|
697
|
+
# If the following doesn't throw an error, we're good to go.
|
|
698
|
+
self.selenium.get_webelement(locator)
|
|
699
|
+
break
|
|
700
|
+
|
|
701
|
+
except Exception as e:
|
|
702
|
+
self.builtin.log(
|
|
703
|
+
"caught exception while waiting: {}".format(str(e)), "DEBUG"
|
|
704
|
+
)
|
|
705
|
+
if time.time() - start_time > timeout_seconds:
|
|
706
|
+
self.selenium.log_location()
|
|
707
|
+
raise Exception("Timed out waiting for a lightning page")
|
|
708
|
+
|
|
709
|
+
# known edge cases that can be worked around
|
|
710
|
+
if self._check_for_login_failure():
|
|
711
|
+
continue
|
|
712
|
+
elif self._check_for_classic():
|
|
713
|
+
continue
|
|
714
|
+
|
|
715
|
+
# not a known edge case; take a deep breath and
|
|
716
|
+
# try again.
|
|
717
|
+
time.sleep(interval)
|
|
718
|
+
self.selenium.go_to(login_url)
|
|
719
|
+
|
|
720
|
+
def breakpoint(self):
|
|
721
|
+
"""Serves as a breakpoint for the robot debugger
|
|
722
|
+
|
|
723
|
+
Note: this keyword is a no-op unless the ``robot_debug`` option for
|
|
724
|
+
the task has been set to ``true``. Unless the option has been
|
|
725
|
+
set, this keyword will have no effect on a running test.
|
|
726
|
+
"""
|
|
727
|
+
return None
|
|
728
|
+
|
|
729
|
+
def _check_for_classic(self):
|
|
730
|
+
"""Switch to lightning if we land on a classic page
|
|
731
|
+
|
|
732
|
+
This seems to happen randomly, causing tests to fail
|
|
733
|
+
catastrophically. The idea is to detect such a case and
|
|
734
|
+
auto-click the "switch to lightning" link
|
|
735
|
+
|
|
736
|
+
"""
|
|
737
|
+
try:
|
|
738
|
+
# we don't actually want to wait here, but if we don't
|
|
739
|
+
# explicitly wait, we'll implicitly wait longer than
|
|
740
|
+
# necessary. This needs to be a quick-ish check.
|
|
741
|
+
self.selenium.wait_until_element_is_visible(
|
|
742
|
+
"class:switch-to-lightning", timeout=2
|
|
743
|
+
)
|
|
744
|
+
self.builtin.log(
|
|
745
|
+
"It appears we are on a classic page; attempting to switch to lightning",
|
|
746
|
+
"WARN",
|
|
747
|
+
)
|
|
748
|
+
# this screenshot should be removed at some point,
|
|
749
|
+
# but for now I want to make sure we see what the
|
|
750
|
+
# page looks like if we get here.
|
|
751
|
+
self.selenium.capture_page_screenshot()
|
|
752
|
+
|
|
753
|
+
# just in case there's a modal present we'll try simulating
|
|
754
|
+
# the escape key. Then, click on the switch-to-lightning link
|
|
755
|
+
self.selenium.press_keys(None, "ESC")
|
|
756
|
+
self.builtin.sleep("1 second")
|
|
757
|
+
self.selenium.click_link("class:switch-to-lightning")
|
|
758
|
+
return True
|
|
759
|
+
|
|
760
|
+
except (NoSuchElementException, AssertionError):
|
|
761
|
+
return False
|
|
762
|
+
|
|
763
|
+
def _check_for_login_failure(self):
|
|
764
|
+
"""Handle the case where we land on a login screen
|
|
765
|
+
|
|
766
|
+
Sometimes we get redirected to a login URL rather than
|
|
767
|
+
being logged in, and we've yet to figure out precisely why
|
|
768
|
+
that happens. Experimentation shows that authentication has
|
|
769
|
+
already happened, so in this case we'll try going back to
|
|
770
|
+
the instance url rather than the front door servlet.
|
|
771
|
+
|
|
772
|
+
Admittedly, this is a bit of a hack, but it's better than
|
|
773
|
+
never getting past this redirect.
|
|
774
|
+
"""
|
|
775
|
+
|
|
776
|
+
location = self.selenium.get_location()
|
|
777
|
+
if "//test.salesforce.com" in location or "//login.salesforce.com" in location:
|
|
778
|
+
login_url = self.cumulusci.org.config["instance_url"]
|
|
779
|
+
self.builtin.log(f"setting login_url temporarily to {login_url}", "DEBUG")
|
|
780
|
+
self.selenium.go_to(login_url)
|
|
781
|
+
return True
|
|
782
|
+
return False
|
|
783
|
+
|
|
784
|
+
@capture_screenshot_on_error
|
|
785
|
+
def input_form_data(self, *args):
|
|
786
|
+
"""Fill in one or more labeled input fields fields with data
|
|
787
|
+
|
|
788
|
+
Arguments should be pairs of field labels and values. Labels
|
|
789
|
+
for required fields should not include the asterisk. Labels
|
|
790
|
+
must be exact, including case.
|
|
791
|
+
|
|
792
|
+
This keyword uses the keyword *Locate Element by Label* to
|
|
793
|
+
locate elements. More details about how elements are found are
|
|
794
|
+
in the documentation for that keyword.
|
|
795
|
+
|
|
796
|
+
For most input form fields the actual value string will be
|
|
797
|
+
used. For a checkbox, passing the value "checked" will check
|
|
798
|
+
the checkbox and any other value will uncheck it. Using
|
|
799
|
+
"unchecked" is recommended for clarity. For radiobuttons, you
|
|
800
|
+
must pass the string "selected" for the value.
|
|
801
|
+
|
|
802
|
+
Example:
|
|
803
|
+
|
|
804
|
+
| Input form data
|
|
805
|
+
| ... Opportunity Name The big one # required text field
|
|
806
|
+
| ... Amount 1b # currency field
|
|
807
|
+
| ... Close Date 4/01/2022 # date field
|
|
808
|
+
| ... Private checked # checkbox
|
|
809
|
+
| ... Type New Customer # combobox
|
|
810
|
+
| ... Primary Campaign Source The Big Campaign # picklist
|
|
811
|
+
|
|
812
|
+
Example setting a radio button:
|
|
813
|
+
|
|
814
|
+
In this example, the radiobutton group has the label
|
|
815
|
+
"Who sees this list view?", and one of the radiobuttons
|
|
816
|
+
has the label "All users can see this list view"
|
|
817
|
+
|
|
818
|
+
| Input form data
|
|
819
|
+
| ... Who sees this list view?::All users can see this list view selected
|
|
820
|
+
|
|
821
|
+
This keyword will eventually replace the "populate form"
|
|
822
|
+
keyword once it has been more thoroughly tested in production.
|
|
823
|
+
|
|
824
|
+
"""
|
|
825
|
+
|
|
826
|
+
it = iter(args)
|
|
827
|
+
errors = []
|
|
828
|
+
for label, value in list(zip(it, it)):
|
|
829
|
+
# this uses our custom "label" locator strategy
|
|
830
|
+
locator = f"label:{label}"
|
|
831
|
+
element = self.selenium.get_webelement(locator)
|
|
832
|
+
self.scroll_element_into_view(locator)
|
|
833
|
+
handler = get_form_handler(element, locator)
|
|
834
|
+
try:
|
|
835
|
+
if handler:
|
|
836
|
+
handler.set(value)
|
|
837
|
+
else:
|
|
838
|
+
raise Exception(
|
|
839
|
+
f"No form handler found for '{label}' (tag: '{element.tag_name}')"
|
|
840
|
+
)
|
|
841
|
+
except Exception as e:
|
|
842
|
+
errors.append(f"{label}: {str(e)}")
|
|
843
|
+
|
|
844
|
+
if errors:
|
|
845
|
+
message = "There were errors with the following fields:\n"
|
|
846
|
+
message += "\n".join(errors)
|
|
847
|
+
raise Exception(message)
|
|
848
|
+
|
|
849
|
+
# FIXME: maybe we should automatically set the focus to some
|
|
850
|
+
# other element to trigger any event handlers on the last
|
|
851
|
+
# element? But what should we set the focus to?
|
|
852
|
+
|
|
853
|
+
def _locate_element_by_label(self, browser, locator, tag, constraints):
|
|
854
|
+
"""Find a lightning component, input, or textarea based on a label
|
|
855
|
+
|
|
856
|
+
If the component is inside a fieldset, the fieldset label can
|
|
857
|
+
be prefixed to the label with a double colon in order to
|
|
858
|
+
disambiguate the label. (eg: Other address::First Name)
|
|
859
|
+
|
|
860
|
+
If the label is inside nested ligntning components (eg:
|
|
861
|
+
``<lightning-input>...<lightning-combobox>...<label>``), the
|
|
862
|
+
lightning component closest to the label will be
|
|
863
|
+
returned (in this case, ``lightning-combobox``).
|
|
864
|
+
|
|
865
|
+
If a lightning component cannot be found for the label, an
|
|
866
|
+
attempt will be made to find an input or textarea associated
|
|
867
|
+
with the label.
|
|
868
|
+
|
|
869
|
+
This is registered as a custom locator strategy named "label"
|
|
870
|
+
|
|
871
|
+
Example:
|
|
872
|
+
|
|
873
|
+
The following example is for a form with a formset named
|
|
874
|
+
"Expected Delivery Date", and inside of that a date input field
|
|
875
|
+
with a label of "Date".
|
|
876
|
+
|
|
877
|
+
These examples produce identical results:
|
|
878
|
+
|
|
879
|
+
| ${element}= Locate element by label Expected Delivery Date::Date
|
|
880
|
+
| ${element}= Get webelement label:Expected Delivery Date::Date
|
|
881
|
+
|
|
882
|
+
"""
|
|
883
|
+
|
|
884
|
+
if "::" in locator:
|
|
885
|
+
fieldset, label = [x.strip() for x in locator.split("::", 1)]
|
|
886
|
+
fieldset_prefix = f'//fieldset[.//*[.="{fieldset}"]]'
|
|
887
|
+
else:
|
|
888
|
+
label = locator.strip()
|
|
889
|
+
fieldset_prefix = ""
|
|
890
|
+
|
|
891
|
+
label_xpath = (
|
|
892
|
+
fieldset_prefix
|
|
893
|
+
+ f'//label[descendant-or-self::*[text()[normalize-space() = "{label}"]]]'
|
|
894
|
+
)
|
|
895
|
+
labels = browser.find_elements_by_xpath(label_xpath)
|
|
896
|
+
if not labels:
|
|
897
|
+
return []
|
|
898
|
+
|
|
899
|
+
# For each match, find either the nearest ancestor lightning component,
|
|
900
|
+
# or the component pointed to by the `for` attribute.
|
|
901
|
+
elements = []
|
|
902
|
+
for label_element in labels:
|
|
903
|
+
try:
|
|
904
|
+
# Since we've already waited for the label, there's not much point of waiting
|
|
905
|
+
# for the component or input area. If it's not in the DOM by now, it will
|
|
906
|
+
# probably never be. Famous last words, right?
|
|
907
|
+
orig_wait = self.selenium.set_selenium_implicit_wait(0)
|
|
908
|
+
component = None
|
|
909
|
+
component = label_element.find_element_by_xpath(
|
|
910
|
+
"./ancestor::*[starts-with(local-name(), 'lightning-')][1]"
|
|
911
|
+
)
|
|
912
|
+
except NoSuchElementException:
|
|
913
|
+
component_id = label_element.get_attribute("for")
|
|
914
|
+
if component_id:
|
|
915
|
+
component = browser.find_element_by_id(component_id)
|
|
916
|
+
# else find an input or textarea in a sibling or descendant?
|
|
917
|
+
finally:
|
|
918
|
+
self.selenium.set_selenium_implicit_wait(orig_wait)
|
|
919
|
+
|
|
920
|
+
if component is not None:
|
|
921
|
+
elements.append(component)
|
|
922
|
+
|
|
923
|
+
return elements
|
|
924
|
+
|
|
925
|
+
def select_window(self, locator="MAIN", timeout=None):
|
|
926
|
+
"""Alias for SeleniuimLibrary 'Switch Window'
|
|
927
|
+
|
|
928
|
+
This keyword was removed from SeleniumLibrary 5.x, but some of our
|
|
929
|
+
tests still use this keyword. You can continue to use this,
|
|
930
|
+
but should replace any calls to this keyword with calls to
|
|
931
|
+
'Switch Window' instead.
|
|
932
|
+
"""
|
|
933
|
+
self.builtin.log(
|
|
934
|
+
"'Select Window' is deprecated; use 'Switch Window' instead", "WARN"
|
|
935
|
+
)
|
|
936
|
+
self.selenium.switch_window(locator=locator, timeout=timeout)
|