Best Python code snippet using avocado_python
filters_tests.py
Source:filters_tests.py
1"""Filter Tests."""2from collections import OrderedDict3import mock4from django.test import TestCase5from serpng.lib.utils.dotted_dict import DottedDict6import filters7# Mock settings8settings = DottedDict({9 'SHUA_COOKIE': '',10 'FILTERS_KEY': '',11 'DEFAULT_FILTERS_STATE': ''12})13# Stubs for serpng.lib.cookie_handler.get_cookie_value_by_key.14def return_empty_preference_filter_state(request, shua_cookie, filters_key):15 """Returns an empty string."""16 return ''17def return_cookie_with_normal_filter(request, shua_cookie, filters_key):18 """Returns a cookie value that indicates that the education-level filter is open."""19 return '1:fed-1'20def return_cookie_with_collapsed_filter(request, shua_cookie, filters_key):21 """Returns a cookie value that indicates that the education-level filter is collapsed."""22 return '1:fed-0'23def return_cookie_with_expanded_filter(request, shua_cookie, filters_key):24 """Returns a cookie value that indicates that the education-level filter is expanded."""25 return '1:fed-2'26# Mock request objects to be used in the tests.27class MockEmptyRequest:28 """Empty mock request."""29 pass30class MockRequest:31 """Mock request with configs attribute that contains filter configurations."""32 def __init__(self):33 self.configs = DottedDict({34 'EXPOSED_FILTERS': (35 'date_posted',36 'miles_radius',37 'ranked_list',38 'sortable_title',39 'education_level'40 ),41 'BASIC_FILTERS': (42 'date_posted',43 'miles_radius'44 )45 })46 self.filters_variations_abtest_group = None47# Mock search_result_json dictionaries to be used in the tests.48SEARCH_RESULT_JSON_EMPTY = {}49SEARCH_RESULT_JSON_RESET_FILTERS_BASIC = {50 'reset_filters_url': '/a/jobs/list/q-cook/l-94043'51}52SEARCH_RESULT_JSON_RESET_FILTERS_WITH_MI = {53 'reset_filters_url': '/a/jobs/list/q-cook/l-94043/mi-50'54}55SEARCH_RESULT_JSON_RESET_FILTERS_WITH_FDB = {56 'reset_filters_url': '/a/jobs/list/q-cook/l-94043/fdb-14'57}58SEARCH_RESULT_JSON_RESET_FILTERS_WITH_MI_AND_FDB = {59 'reset_filters_url': '/a/jobs/list/q-cook/l-94043/mi-50/fdb-14'60}61SEARCH_RESULT_JSON_WITH_APPLIED_FILTERS = {62 'primary_applied_filters': ['i', 'am', 'not', 'empty']63}64SEARCH_RESULT_JSON_WITH_FILTERS = {65 'primary_parametric_fields': {66 'date-posted': {},67 'miles-radius': {},68 'ranked-list': {},69 'sortable-title': {},70 'education-level': {}71 }72}73SEARCH_RESULT_JSON_WITH_UNEXPOSED_FILTER = {74 'primary_parametric_fields': {75 'date-posted': {},76 'miles-radius': {},77 'ranked-list': {},78 'sortable-title': {},79 'education-level': {},80 'experience-level': {}81 }82}83SEARCH_RESULT_JSON_WITH_APPLIED_FILTER = {84 'primary_parametric_fields': {85 'date-posted': {},86 'miles-radius': {},87 'ranked-list': {},88 'sortable-title': {},89 'education-level': {}90 },91 'primary_applied_filters': [92 {'canonical_name': 'education-level'}93 ]94}95SEARCH_RESULT_JSON_WITH_FILTERS_GET_PARAM = {96 'primary_parametric_fields': {97 'date-posted': {98 'get_parameter': 'fdb'99 },100 'miles-radius': {101 'get_parameter': 'mi'102 },103 'ranked-list': {104 'get_parameter': 'frl'105 },106 'sortable-title': {107 'get_parameter': 'fft'108 },109 'education-level': {110 'get_parameter': 'fed'111 }112 }113}114SEARCH_RESULT_JSON_WITH_BAD_RANKED_LIST_FILTERS = {115 'primary_parametric_fields': {116 'date-posted': {},117 'miles-radius': {},118 'ranked-list': {119 'filter_values_array': [120 {'url_path': '/a/jobs/list/q-pixar/frl-fortunemostadmired'},121 {'url_path': '/a/jobs/list/q-pixar/frl-diversity50'},122 {'url_path': '/a/jobs/list/q-pixar/frl-fortune100best'}123 ]124 },125 'sortable-title': {},126 'education-level': {}127 }128}129# Tests130class FiltersTestCase(TestCase):131 """Filters TestCase"""132 # pylint: disable=R0904133 @mock.patch(target='serpng.lib.cookie_handler.get_cookie_value_by_key', new=return_empty_preference_filter_state)134 def test_get_reset_all_filters_url_empty(self):135 """Method 'get_reset_all_filters_url' should return empty string when search_result_json is empty."""136 test_filters = filters.Filters(137 request=MockEmptyRequest(),138 search_result_json=SEARCH_RESULT_JSON_EMPTY)139 self.assertEqual(test_filters.get_reset_all_filters_url(), '')140 @mock.patch(target='serpng.lib.cookie_handler.get_cookie_value_by_key', new=return_empty_preference_filter_state)141 def test_get_reset_all_filters_url_basic(self):142 """Method 'get_reset_all_filters_url' should return correct url with vanilla filter applied."""143 test_filters = filters.Filters(144 request=MockEmptyRequest(),145 search_result_json=SEARCH_RESULT_JSON_RESET_FILTERS_BASIC)146 self.assertEqual(test_filters.get_reset_all_filters_url(), '/a/jobs/list/q-cook/l-94043')147 @mock.patch(target='serpng.lib.cookie_handler.get_cookie_value_by_key', new=return_empty_preference_filter_state)148 def test_get_reset_all_filters_url_with_mi(self):149 """Method 'get_reset_all_filters_url' should return correct url when miles-radius filter is applied."""150 test_filters = filters.Filters(151 request=MockEmptyRequest(),152 search_result_json=SEARCH_RESULT_JSON_RESET_FILTERS_WITH_MI)153 self.assertEqual(test_filters.get_reset_all_filters_url(), '/a/jobs/list/q-cook/l-94043')154 @mock.patch(target='serpng.lib.cookie_handler.get_cookie_value_by_key', new=return_empty_preference_filter_state)155 def test_get_reset_all_filters_url_with_fdb(self):156 """Method 'get_reset_all_filters_url' should return correct url when date-posted filter is applied."""157 test_filters = filters.Filters(158 request=MockEmptyRequest(),159 search_result_json=SEARCH_RESULT_JSON_RESET_FILTERS_WITH_FDB)160 self.assertEqual(test_filters.get_reset_all_filters_url(), '/a/jobs/list/q-cook/l-94043')161 @mock.patch(target='serpng.lib.cookie_handler.get_cookie_value_by_key', new=return_empty_preference_filter_state)162 def test_get_reset_all_filters_url_with_mi_and_fdb(self):163 """Method 'get_reset_all_filters_url' should return correct url when both miles-radius and date-posted filters are applied."""164 test_filters = filters.Filters(165 request=MockEmptyRequest(),166 search_result_json=SEARCH_RESULT_JSON_RESET_FILTERS_WITH_MI_AND_FDB)167 self.assertEqual(test_filters.get_reset_all_filters_url(), '/a/jobs/list/q-cook/l-94043')168 @mock.patch(target='serpng.lib.cookie_handler.get_cookie_value_by_key', new=return_empty_preference_filter_state)169 def test_has_any_applied_filters_empty(self):170 """Method 'has_any_applied_filters' should correctly return False when search_result_json is empty."""171 test_filters = filters.Filters(172 request=MockEmptyRequest(),173 search_result_json=SEARCH_RESULT_JSON_EMPTY)174 self.assertFalse(test_filters.has_any_applied_filters())175 @mock.patch(target='serpng.lib.cookie_handler.get_cookie_value_by_key', new=return_empty_preference_filter_state)176 def test_has_any_applied_filters_with_applied_filters(self):177 """Method 'has_any_applied_filters' should correctly return True a filter is applied."""178 test_filters = filters.Filters(179 request=MockEmptyRequest(),180 search_result_json=SEARCH_RESULT_JSON_WITH_APPLIED_FILTERS)181 self.assertTrue(test_filters.has_any_applied_filters())182 @mock.patch(target='serpng.lib.cookie_handler.get_cookie_value_by_key', new=return_empty_preference_filter_state)183 def test_has_any_applied_filters_with_mi(self):184 """Method 'has_any_applied_filters' should correctly return True when miles-radius filter is applied."""185 test_filters = filters.Filters(186 request=MockEmptyRequest(),187 search_result_json=SEARCH_RESULT_JSON_RESET_FILTERS_WITH_MI)188 self.assertTrue(test_filters.has_any_applied_filters())189 @mock.patch(target='serpng.lib.cookie_handler.get_cookie_value_by_key', new=return_empty_preference_filter_state)190 def test_has_any_applied_filters_with_fdb(self):191 """Method 'has_any_applied_filters' should correctly return True when date-posted filter is applied."""192 test_filters = filters.Filters(193 request=MockEmptyRequest(),194 search_result_json=SEARCH_RESULT_JSON_RESET_FILTERS_WITH_FDB)195 self.assertTrue(test_filters.has_any_applied_filters())196 @mock.patch(target='serpng.lib.cookie_handler.get_cookie_value_by_key', new=return_empty_preference_filter_state)197 def test_filters_normal_settings(self):198 """Vanilla filters should be correctly displayed."""199 test_filters = filters.Filters(200 request=MockRequest(),201 search_result_json=SEARCH_RESULT_JSON_WITH_FILTERS)202 expected_filters = OrderedDict()203 basic_filters = OrderedDict()204 basic_filters['date_posted'] = {'state': 'expanded'}205 basic_filters['miles_radius'] = {'state': 'expanded'}206 expected_filters['basic_filters'] = basic_filters207 more_filters = OrderedDict()208 more_filters['ranked_list'] = OrderedDict({'state': 'collapsed'})209 more_filters['sortable_title'] = {'state': 'collapsed'}210 more_filters['education_level'] = {'state': 'collapsed'}211 expected_filters['more_filters'] = more_filters212 expected_filters['more_filters_state'] = 'collapsed'213 actual_filters = test_filters.get_filters_for_display()214 self.assertEqual(actual_filters, expected_filters)215 @mock.patch(target='serpng.lib.cookie_handler.get_cookie_value_by_key', new=return_empty_preference_filter_state)216 def test_filters_unexposed_filter(self):217 """Unexposed filters should not be displayed."""218 test_filters = filters.Filters(219 request=MockRequest(),220 search_result_json=SEARCH_RESULT_JSON_WITH_UNEXPOSED_FILTER)221 expected_filters = OrderedDict()222 basic_filters = OrderedDict()223 basic_filters['date_posted'] = {'state': 'expanded'}224 basic_filters['miles_radius'] = {'state': 'expanded'}225 expected_filters['basic_filters'] = basic_filters226 more_filters = OrderedDict()227 more_filters['ranked_list'] = OrderedDict({'state': 'collapsed'})228 more_filters['sortable_title'] = {'state': 'collapsed'}229 more_filters['education_level'] = {'state': 'collapsed'}230 expected_filters['more_filters'] = more_filters231 expected_filters['more_filters_state'] = 'collapsed'232 actual_filters = test_filters.get_filters_for_display()233 self.assertEqual(actual_filters, expected_filters)234 @mock.patch(target='serpng.lib.cookie_handler.get_cookie_value_by_key', new=return_empty_preference_filter_state)235 def test_filters_applied_filter(self):236 """Filter states should correctly reflect applied filters."""237 test_filters = filters.Filters(238 request=MockRequest(),239 search_result_json=SEARCH_RESULT_JSON_WITH_APPLIED_FILTER)240 expected_filters = OrderedDict()241 basic_filters = OrderedDict()242 basic_filters['date_posted'] = {'state': 'expanded'}243 basic_filters['miles_radius'] = {'state': 'expanded'}244 expected_filters['basic_filters'] = basic_filters245 more_filters = OrderedDict()246 more_filters['ranked_list'] = OrderedDict({'state': 'collapsed'})247 more_filters['sortable_title'] = {'state': 'collapsed'}248 more_filters['education_level'] = {'state': ''}249 expected_filters['more_filters'] = more_filters250 expected_filters['more_filters_state'] = 'expanded'251 actual_filters = test_filters.get_filters_for_display()252 self.assertEqual(actual_filters, expected_filters)253 @mock.patch(target='serpng.lib.cookie_handler.get_cookie_value_by_key', new=return_cookie_with_normal_filter)254 def test_filters_cookies_has_normal_filter(self):255 """Filter states in cookies should be correctly copied over."""256 test_filters = filters.Filters(257 request=MockRequest(),258 search_result_json=SEARCH_RESULT_JSON_WITH_FILTERS_GET_PARAM)259 expected_filters = OrderedDict()260 basic_filters = OrderedDict()261 basic_filters['date_posted'] = {'get_parameter': 'fdb', 'state': 'expanded'}262 basic_filters['miles_radius'] = {'get_parameter': 'mi', 'state': 'expanded'}263 expected_filters['basic_filters'] = basic_filters264 more_filters = OrderedDict()265 ranked_list = OrderedDict()266 ranked_list['get_parameter'] = 'frl'267 ranked_list['state'] = 'collapsed'268 more_filters['ranked_list'] = ranked_list269 more_filters['sortable_title'] = {'get_parameter': 'fft', 'state': 'collapsed'}270 more_filters['education_level'] = {'get_parameter': 'fed', 'state': ''}271 expected_filters['more_filters'] = more_filters272 expected_filters['more_filters_state'] = 'expanded'273 actual_filters = test_filters.get_filters_for_display()274 self.assertEqual(actual_filters, expected_filters)275 @mock.patch(target='serpng.lib.cookie_handler.get_cookie_value_by_key', new=return_cookie_with_collapsed_filter)276 def test_filters_cookies_has_collapsed_filter(self):277 """Filter states in cookies should be correctly copied over."""278 test_filters = filters.Filters(279 request=MockRequest(),280 search_result_json=SEARCH_RESULT_JSON_WITH_FILTERS_GET_PARAM)281 expected_filters = OrderedDict()282 basic_filters = OrderedDict()283 basic_filters['date_posted'] = {'get_parameter': 'fdb', 'state': 'expanded'}284 basic_filters['miles_radius'] = {'get_parameter': 'mi', 'state': 'expanded'}285 expected_filters['basic_filters'] = basic_filters286 more_filters = OrderedDict()287 ranked_list = OrderedDict()288 ranked_list['get_parameter'] = 'frl'289 ranked_list['state'] = 'collapsed'290 more_filters['ranked_list'] = ranked_list291 more_filters['sortable_title'] = {'get_parameter': 'fft', 'state': 'collapsed'}292 more_filters['education_level'] = {'get_parameter': 'fed', 'state': 'collapsed'}293 expected_filters['more_filters'] = more_filters294 expected_filters['more_filters_state'] = 'collapsed'295 actual_filters = test_filters.get_filters_for_display()296 self.assertEqual(actual_filters, expected_filters)297 @mock.patch(target='serpng.lib.cookie_handler.get_cookie_value_by_key', new=return_cookie_with_expanded_filter)298 def test_filters_cookies_has_expanded_filter(self):299 """Filter states in cookies should be correctly copied over."""300 test_filters = filters.Filters(301 request=MockRequest(),302 search_result_json=SEARCH_RESULT_JSON_WITH_FILTERS_GET_PARAM)303 expected_filters = OrderedDict()304 basic_filters = OrderedDict()305 basic_filters['date_posted'] = {'get_parameter': 'fdb', 'state': 'expanded'}306 basic_filters['miles_radius'] = {'get_parameter': 'mi', 'state': 'expanded'}307 expected_filters['basic_filters'] = basic_filters308 more_filters = OrderedDict()309 ranked_list = OrderedDict()310 ranked_list['get_parameter'] = 'frl'311 ranked_list['state'] = 'collapsed'312 more_filters['ranked_list'] = ranked_list313 more_filters['sortable_title'] = {'get_parameter': 'fft', 'state': 'collapsed'}314 more_filters['education_level'] = {'get_parameter': 'fed', 'state': 'expanded'}315 expected_filters['more_filters'] = more_filters316 expected_filters['more_filters_state'] = 'expanded'317 actual_filters = test_filters.get_filters_for_display()...
make_gtest_filter.py
Source:make_gtest_filter.py
1#!/usr/bin/env python2# Copyright (c) 2018 The Chromium Authors. All rights reserved.3# Use of this source code is governed by a BSD-style license that can be4# found in the LICENSE file.5"""Reads lines from files or stdin and identifies C++ tests.6Outputs a filter that can be used with --gtest_filter or a filter file to7run only the tests identified.8Usage:9Outputs filter for all test fixtures in a directory. --class-only avoids an10overly long filter string.11$ cat components/mycomp/**test.cc | make_gtest_filter.py --class-only12Outputs filter for all tests in a file.13$ make_gtest_filter.py ./myfile_unittest.cc14Outputs filter for only test at line 12315$ make_gtest_filter.py --line=123 ./myfile_unittest.cc16Formats output as a GTest filter file.17$ make_gtest_filter.py ./myfile_unittest.cc --as-filter-file18Use a JSON failure summary as the input.19$ make_gtest_filter.py summary.json --from-failure-summary20Elide the filter list using wildcards when possible.21$ make_gtest_filter.py summary.json --from-failure-summary --wildcard-compress22"""23from __future__ import print_function24import argparse25import collections26import fileinput27import json28import re29import sys30class TrieNode:31 def __init__(self):32 # The number of strings which terminated on or underneath this node.33 self.num_strings = 034 # The prefix subtries which follow |this|, keyed by their next character.35 self.children = {}36def PascalCaseSplit(input_string):37 current_term = []38 prev_char = ''39 for current_char in input_string:40 is_boundary = prev_char != '' and \41 ((current_char.isupper() and prev_char.islower()) or \42 (current_char.isalpha() != prev_char.isalpha()) or \43 (current_char.isalnum() != prev_char.isalnum()))44 prev_char = current_char45 if is_boundary:46 yield ''.join(current_term)47 current_term = []48 current_term.append(current_char)49 if len(current_term) > 0:50 yield ''.join(current_term)51def TrieInsert(trie, value):52 """Inserts the characters of 'value' into a trie, with every edge representing53 a single character. An empty child set indicates end-of-string."""54 for term in PascalCaseSplit(value):55 trie.num_strings = trie.num_strings + 156 if term in trie.children:57 trie = trie.children[term]58 else:59 subtrie = TrieNode()60 trie.children[term] = subtrie61 trie = subtrie62 trie.num_strings = trie.num_strings + 163def ComputeWildcardsFromTrie(trie, min_depth, min_cases):64 """Computes a list of wildcarded test case names from a trie using a depth65 first traversal."""66 WILDCARD = '*'67 # Stack of values to process, initialized with the root node.68 # The first item of the tuple is the substring represented by the traversal so69 # far.70 # The second item of the tuple is the TrieNode itself.71 # The third item is the depth of the traversal so far.72 to_process = [('', trie, 0)]73 while len(to_process) > 0:74 cur_prefix, cur_trie, cur_depth = to_process.pop()75 assert (cur_trie.num_strings != 0)76 if len(cur_trie.children) == 0:77 # No more children == we're at the end of a string.78 yield cur_prefix79 elif (cur_depth == min_depth) and \80 cur_trie.num_strings > min_cases:81 # Trim traversal of this path if the path is deep enough and there82 # are enough entries to warrant elision.83 yield cur_prefix + WILDCARD84 else:85 # Traverse all children of this node.86 for term, subtrie in cur_trie.children.items():87 to_process.append((cur_prefix + term, subtrie, cur_depth + 1))88def CompressWithWildcards(test_list, min_depth, min_cases):89 """Given a list of SUITE.CASE names, generates an exclusion list using90 wildcards to reduce redundancy.91 For example:92 Foo.TestOne93 Foo.TestTwo94 becomes:95 Foo.Test*"""96 suite_tries = {}97 # First build up a trie based representations of all test case names,98 # partitioned per-suite.99 for case in test_list:100 suite_name, test = case.split('.')101 if not suite_name in suite_tries:102 suite_tries[suite_name] = TrieNode()103 TrieInsert(suite_tries[suite_name], test)104 output = []105 # Go through the suites' tries and generate wildcarded representations106 # of the cases.107 for suite in suite_tries.items():108 suite_name, cases_trie = suite109 for case_wildcard in ComputeWildcardsFromTrie(cases_trie, min_depth, \110 min_cases):111 output.append("{}.{}".format(suite_name, case_wildcard))112 output.sort()113 return output114def GetFailedTestsFromTestLauncherSummary(summary):115 failures = set()116 for iteration in summary['per_iteration_data']:117 for case_name, results in iteration.items():118 for result in results:119 if result['status'] == 'FAILURE':120 failures.add(case_name)121 return list(failures)122def main():123 parser = argparse.ArgumentParser()124 parser.add_argument(125 '--input-format',126 choices=['swarming_summary', 'test_launcher_summary', 'test_file'],127 default='test_file')128 parser.add_argument('--output-format',129 choices=['file', 'args'],130 default='args')131 parser.add_argument('--wildcard-compress', action='store_true')132 parser.add_argument(133 '--wildcard-min-depth',134 type=int,135 default=1,136 help="Minimum number of terms in a case before a wildcard may be " +137 "used, so that prefixes are not excessively broad.")138 parser.add_argument(139 '--wildcard-min-cases',140 type=int,141 default=3,142 help="Minimum number of cases in a filter before folding into a " +143 "wildcard, so as to not create wildcards needlessly for small "144 "numbers of similarly named test failures.")145 parser.add_argument('--line', type=int)146 parser.add_argument('--class-only', action='store_true')147 parser.add_argument(148 '--as-exclusions',149 action='store_true',150 help='Generate exclusion rules for test cases, instead of inclusions.')151 args, left = parser.parse_known_args()152 test_filters = []153 if args.input_format == 'swarming_summary':154 # Decode the JSON files separately and combine their contents.155 test_filters = []156 for json_file in left:157 test_filters.extend(json.loads('\n'.join(open(json_file, 'r'))))158 if args.wildcard_compress:159 test_filters = CompressWithWildcards(test_filters,160 args.wildcard_min_depth,161 args.wildcard_min_cases)162 elif args.input_format == 'test_launcher_summary':163 # Decode the JSON files separately and combine their contents.164 test_filters = []165 for json_file in left:166 test_filters.extend(167 GetFailedTestsFromTestLauncherSummary(168 json.loads('\n'.join(open(json_file, 'r')))))169 if args.wildcard_compress:170 test_filters = CompressWithWildcards(test_filters,171 args.wildcard_min_depth,172 args.wildcard_min_cases)173 else:174 file_input = fileinput.input(left)175 if args.line:176 # If --line is used, restrict text to a few lines around the requested177 # line.178 requested_line = args.line179 selected_lines = []180 for line in file_input:181 if (fileinput.lineno() >= requested_line182 and fileinput.lineno() <= requested_line + 1):183 selected_lines.append(line)184 txt = ''.join(selected_lines)185 else:186 txt = ''.join(list(file_input))187 # This regex is not exhaustive, and should be updated as needed.188 rx = re.compile(189 r'^(?:TYPED_)?(?:IN_PROC_BROWSER_)?TEST(_F|_P)?\(\s*(\w+)\s*' + \190 r',\s*(\w+)\s*\)',191 flags=re.DOTALL | re.M)192 tests = []193 for m in rx.finditer(txt):194 tests.append(m.group(2) + '.' + m.group(3))195 # Note: Test names have the following structures:196 # * FixtureName.TestName197 # * InstantiationName/FixtureName.TestName/##198 # Since this script doesn't parse instantiations, we generate filters to199 # match either regular tests or instantiated tests.200 if args.wildcard_compress:201 test_filters = CompressWithWildcards(tests, args.wildcard_min_depth,202 args.wildcard_min_cases)203 elif args.class_only:204 fixtures = set([t.split('.')[0] for t in tests])205 test_filters = [c + '.*' for c in fixtures] + \206 ['*/' + c + '.*/*' for c in fixtures]207 else:208 test_filters = ['*/' + c + '/*' for c in tests]209 if args.as_exclusions:210 test_filters = ['-' + x for x in test_filters]211 if args.output_format == 'file':212 print('\n'.join(test_filters))213 else:214 print(':'.join(test_filters))215 return 0216if __name__ == '__main__':...
Learn to execute automation testing from scratch with LambdaTest Learning Hub. Right from setting up the prerequisites to run your first automation test, to following best practices and diving deeper into advanced test scenarios. LambdaTest Learning Hubs compile a list of step-by-step guides to help you be proficient with different test automation frameworks i.e. Selenium, Cypress, TestNG etc.
You could also refer to video tutorials over LambdaTest YouTube channel to get step by step demonstration from industry experts.
Get 100 minutes of automation test minutes FREE!!