browsergym-workarena 0.1.0rc7__py3-none-any.whl → 0.2.1__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.
Files changed (35) hide show
  1. browsergym/workarena/__init__.py +3 -2
  2. browsergym/workarena/api/ui_themes.py +35 -0
  3. browsergym/workarena/api/user.py +153 -0
  4. browsergym/workarena/api/utils.py +1 -1
  5. browsergym/workarena/config.py +43 -1
  6. browsergym/workarena/data_files/setup_files/ui_themes/workarena_themes.xml +2313 -0
  7. browsergym/workarena/data_files/task_configs/all_menu.json +94 -94
  8. browsergym/workarena/data_files/task_configs/dashboard_retrieval_minmax_task.json +1 -0
  9. browsergym/workarena/data_files/task_configs/dashboard_retrieval_value_task.json +1 -0
  10. browsergym/workarena/data_files/task_configs/filter_service_catalog_item_list_task.json +7985 -7981
  11. browsergym/workarena/data_files/task_configs/impersonation_users.json +2 -2
  12. browsergym/workarena/data_files/task_configs/report_retrieval_minmax_task.json +1 -0
  13. browsergym/workarena/data_files/task_configs/report_retrieval_value_task.json +1 -0
  14. browsergym/workarena/install.py +620 -155
  15. browsergym/workarena/tasks/base.py +85 -26
  16. browsergym/workarena/tasks/dashboard.py +620 -0
  17. browsergym/workarena/tasks/form.py +121 -85
  18. browsergym/workarena/tasks/knowledge.py +30 -14
  19. browsergym/workarena/tasks/list.py +121 -67
  20. browsergym/workarena/tasks/navigation.py +18 -16
  21. browsergym/workarena/tasks/scripts/generate_dashboard_configs.py +272 -0
  22. browsergym/workarena/tasks/scripts/generate_forms.py +2 -2
  23. browsergym/workarena/tasks/scripts/list.py +2 -2
  24. browsergym/workarena/tasks/scripts/validate.py +2 -2
  25. browsergym/workarena/tasks/service_catalog.py +106 -74
  26. browsergym/workarena/tasks/utils/form.py +5 -3
  27. browsergym/workarena/tasks/utils/js_utils.js +123 -2
  28. browsergym/workarena/tasks/utils/string.py +15 -0
  29. browsergym/workarena/tasks/utils/utils.py +20 -0
  30. browsergym/workarena/utils.py +31 -2
  31. {browsergym_workarena-0.1.0rc7.dist-info → browsergym_workarena-0.2.1.dist-info}/METADATA +15 -5
  32. {browsergym_workarena-0.1.0rc7.dist-info → browsergym_workarena-0.2.1.dist-info}/RECORD +35 -24
  33. {browsergym_workarena-0.1.0rc7.dist-info → browsergym_workarena-0.2.1.dist-info}/WHEEL +1 -1
  34. {browsergym_workarena-0.1.0rc7.dist-info → browsergym_workarena-0.2.1.dist-info}/entry_points.txt +0 -0
  35. {browsergym_workarena-0.1.0rc7.dist-info → browsergym_workarena-0.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -8,11 +8,11 @@ import json
8
8
  import logging
9
9
  import playwright.sync_api
10
10
  import re
11
- import urllib.parse
12
11
 
13
12
  from playwright.sync_api import Page
14
13
  from tenacity import retry, retry_if_exception_type, stop_after_delay
15
- from typing import Tuple
14
+ from typing import List, Tuple
15
+ from urllib import parse
16
16
  from warnings import warn
17
17
 
18
18
  from ..api.utils import table_api_call, table_column_info
@@ -40,27 +40,28 @@ from ..config import (
40
40
  )
41
41
  from .base import AbstractServiceNowTask
42
42
  from .utils.form import fill_text
43
+ from .utils.utils import check_url_suffix_match
43
44
 
44
45
 
45
46
  LISTS = {
46
47
  "alm_asset": {
47
- "url": "/now/nav/ui/classic/params/target/alm_asset_list.do%3Fsysparm_view%3Ditam_workspace%26sysparm_userpref.alm_asset_list.view%3Ditam_workspace%26sysparm_userpref.alm_asset.view%3Ditam_workspace%26sysparm_query%3D%26sysparm_fixed_query%3D",
48
+ "url": "/now/nav/ui/classic/params/target/alm_asset_list.do",
48
49
  "forbidden_fields": ["sys_class_name"],
49
50
  },
50
51
  "alm_hardware": {
51
- "url": "/now/nav/ui/classic/params/target/alm_hardware_list.do%3Fsysparm_view%3Ditam_workspace%26sysparm_userpref.alm_hardware_list.view%3Ditam_workspace%26sysparm_userpref.alm_hardware.view%3Ditam_workspace%3D%26sysparm_query%3Dinstall_status%253D6%255Esubstatus%253Dpre_allocated",
52
+ "url": "/now/nav/ui/classic/params/target/alm_hardware_list.do",
52
53
  "forbidden_fields": [],
53
54
  },
54
55
  "change_request": {
55
- "url": "/now/nav/ui/classic/params/target/change_request_list.do%3Fsysparm_view%3Dsow%26sysparm_userpref.change_request_list.view%3Dsow%26sysparm_userpref.change_request.view%3Dsow%26sysparm_query%3D%26sysparm_fixed_query%3D",
56
+ "url": "/now/nav/ui/classic/params/target/change_request_list.do",
56
57
  "forbidden_fields": [],
57
58
  },
58
59
  "incident": {
59
- "url": "/now/nav/ui/classic/params/target/incident_list.do%3Fsysparm_query%3Dactive%253Dtrue%26sysparm_first_row%3D1%26sysparm_view%3DMajor%2520Incidents",
60
+ "url": "/now/nav/ui/classic/params/target/incident_list.do",
60
61
  "forbidden_fields": [],
61
62
  },
62
63
  "sys_user": {
63
- "url": "/now/nav/ui/classic/params/target/sys_user_list.do%3Fsysparm_view%3D%26sysparm_userpref.sys_user_list.view%3D%26sysparm_userpref.sys_user.view%3D%26sysparm_query%3Dactive%253Dtrue%255Ecompany%253D81fd65ecac1d55eb42a426568fc87a63",
64
+ "url": "/now/nav/ui/classic/params/target/sys_user_list.do",
64
65
  "forbidden_fields": [
65
66
  "sys_class_name",
66
67
  "roles",
@@ -70,13 +71,16 @@ LISTS = {
70
71
  ],
71
72
  },
72
73
  "sc_cat_item": {
73
- "url": "/now/nav/ui/classic/params/target/sc_cat_item_list.do%3Fsysparm_view%3D%26sysparm_userpref.sc_cat_item_list.view%3D%26sysparm_userpref.sc_cat_item.view%3D%26sysparm_query%3D%26sysparm_fixed_query%3D",
74
+ "url": "/now/nav/ui/classic/params/target/sc_cat_item_list.do",
74
75
  "forbidden_fields": ["roles", "sc_catalogs"],
75
76
  },
76
77
  }
77
78
 
78
79
 
79
80
  class ServiceNowListTask(AbstractServiceNowTask):
81
+ def get_init_scripts(self) -> List[str]:
82
+ return super().get_init_scripts() + ["registerGsftMainLoaded();"]
83
+
80
84
  def _get_visible_list(self, page: Page):
81
85
  self._wait_for_ready(page)
82
86
 
@@ -164,16 +168,6 @@ class ServiceNowListTask(AbstractServiceNowTask):
164
168
  page.wait_for_function("window.gsft_main.GlideList2 !== undefined")
165
169
  logging.debug("Detected Glide list API ready")
166
170
 
167
- def pre_setup(self, seed: int, page: Page):
168
- super().pre_setup(seed, page)
169
-
170
- self._add_init_scripts_to_context_and_reload(
171
- page,
172
- [
173
- "registerGsftMainLoaded();",
174
- ],
175
- )
176
-
177
171
 
178
172
  class SortListTask(ServiceNowListTask):
179
173
  """
@@ -181,6 +175,8 @@ class SortListTask(ServiceNowListTask):
181
175
 
182
176
  Parameters:
183
177
  -----------
178
+ seed: int
179
+ Random seed
184
180
  instance: SNowInstance
185
181
  The instance to use.
186
182
  list_url: str
@@ -201,6 +197,7 @@ class SortListTask(ServiceNowListTask):
201
197
 
202
198
  def __init__(
203
199
  self,
200
+ seed: int,
204
201
  instance=None,
205
202
  list_url="",
206
203
  forbidden_fields=[],
@@ -208,7 +205,7 @@ class SortListTask(ServiceNowListTask):
208
205
  config_path: str = None,
209
206
  expected_fields_path: str = None,
210
207
  ) -> None:
211
- super().__init__(instance=instance, start_rel_url=list_url)
208
+ super().__init__(seed=seed, instance=instance, start_rel_url=list_url)
212
209
  self.min_sort_len = 1
213
210
  self.max_sort_len = 3
214
211
  self.forbidden_fields = forbidden_fields
@@ -219,28 +216,33 @@ class SortListTask(ServiceNowListTask):
219
216
  with open(expected_fields_path, "r") as f:
220
217
  self.expected_fields = set(json.load(f))
221
218
 
222
- def setup(self, seed: int, page: Page) -> tuple[str, dict]:
223
- self.pre_setup(seed, page)
224
- self._wait_for_ready(page)
225
- # Extract the list from the page
226
- self.list_info = self._extract_list_info(page)
227
- visible_columns = set(self.list_info["fields"].split(","))
228
- config = self.fixed_config if self.fixed_config else self.random.choice(self.all_configs)
219
+ def setup_goal(self, page: Page) -> tuple[str, dict]:
220
+ super().setup_goal(page=page)
229
221
 
222
+ # Get the task configuration
223
+ config = self.fixed_config if self.fixed_config else self.random.choice(self.all_configs)
230
224
  self.sort_fields = config["sort_fields"]
231
225
  self.sort_dirs = config["sort_dirs"]
232
- goal = config["goal"]
233
- # Ensure that the fields that need to be sorted are visible
234
- assert (
235
- set(self.sort_fields) <= visible_columns and visible_columns == self.expected_fields
236
- ), f"Fields {self.sort_fields} are not all visible in the list. Re-run workarena-install to correct this."
237
226
 
227
+ # Get the task goal
228
+ goal = config["goal"]
238
229
  info = {}
239
230
 
240
231
  return goal, info
241
232
 
233
+ def start(self, page: Page) -> None:
234
+ super().start(page)
235
+ self._wait_for_ready(page)
236
+
237
+ # Ensure that the fields that need to be sorted are visible (task feasibility check)
238
+ self.list_info = self._extract_list_info(page)
239
+ visible_columns = set(self.list_info["fields"].split(","))
240
+ assert (
241
+ set(self.sort_fields) <= visible_columns and visible_columns == self.expected_fields
242
+ ), f"Fields {self.sort_fields} are not all visible in the list. Re-run workarena-install to correct this."
243
+
242
244
  def _generate_all_configs(self, seed: int, page: Page, n_fields_to_sort: int):
243
- self.pre_setup(seed, page)
245
+ self.setup(seed=seed, page=page)
244
246
  self._wait_for_ready(page)
245
247
  list_info = self._extract_list_info(page)
246
248
 
@@ -277,8 +279,8 @@ class SortListTask(ServiceNowListTask):
277
279
 
278
280
  return all_configs
279
281
 
280
- def _generate_random_config(self, seed: int, page: Page):
281
- self.pre_setup(seed, page)
282
+ def _generate_random_config(self, page: Page):
283
+ self.setup(page=page)
282
284
  self._wait_for_ready(page)
283
285
  self.list_info = self._extract_list_info(page)
284
286
  # Get available fields
@@ -325,7 +327,7 @@ class SortListTask(ServiceNowListTask):
325
327
  return goal, info
326
328
 
327
329
  def cheat(self, page: Page, chat_messages: list[str]) -> None:
328
- super().cheat(page, chat_messages)
330
+ super().cheat(page=page, chat_messages=chat_messages)
329
331
  self._wait_for_ready(page)
330
332
 
331
333
  iframe, _, _ = self._get_visible_list(page)
@@ -387,8 +389,19 @@ class SortListTask(ServiceNowListTask):
387
389
  def validate(
388
390
  self, page: playwright.sync_api.Page, chat_messages: list[str]
389
391
  ) -> Tuple[float, bool, str, dict]:
392
+ right_url = check_url_suffix_match(
393
+ page, expected_url=self.start_url[: self.start_url.find("%3F")], task=self
394
+ )
395
+ if not right_url:
396
+ return (
397
+ 0,
398
+ False,
399
+ "",
400
+ {
401
+ "message": f"The page is not in the right URL to validate task {self.__class__.__name__}."
402
+ },
403
+ )
390
404
  self._wait_for_ready(page)
391
-
392
405
  if len(self.sort_fields) == 1:
393
406
  # XXX: Treat this as a separate case because the user may have sorted by clicking
394
407
  # on the column header. In that case, the URL will not contain the ORDERBY.
@@ -409,9 +422,9 @@ class SortListTask(ServiceNowListTask):
409
422
  else:
410
423
  # pre-process the URL
411
424
  page_url = page.evaluate("() => window.location.href")
412
- page_url = urllib.parse.unquote(page_url)
413
- page_query = urllib.parse.urlparse(page_url).query
414
- page_qs = urllib.parse.parse_qs(page_query)
425
+ page_url = parse.unquote(page_url)
426
+ page_query = parse.urlparse(page_url).query
427
+ page_qs = parse.parse_qs(page_query)
415
428
 
416
429
  # make sure "sysparm_query" is present
417
430
  if "sysparm_query" not in page_qs:
@@ -464,6 +477,7 @@ class FilterListTask(ServiceNowListTask):
464
477
 
465
478
  def __init__(
466
479
  self,
480
+ seed: int,
467
481
  instance=None,
468
482
  list_url="",
469
483
  fixed_config: dict = None,
@@ -472,7 +486,7 @@ class FilterListTask(ServiceNowListTask):
472
486
  ) -> None:
473
487
  self.min_filter_len = 2
474
488
  self.max_filter_len = 5
475
- super().__init__(instance=instance, start_rel_url=list_url)
489
+ super().__init__(seed=seed, instance=instance, start_rel_url=list_url)
476
490
  self.fixed_config = fixed_config
477
491
  if config_path:
478
492
  with open(config_path, "r") as f:
@@ -480,23 +494,18 @@ class FilterListTask(ServiceNowListTask):
480
494
  with open(expected_fields_path, "r") as f:
481
495
  self.expected_fields = set(json.load(f))
482
496
 
483
- def setup(self, seed: int, page: Page) -> tuple[str, dict]:
484
- self.pre_setup(seed, page)
485
- self._wait_for_ready(page)
486
- config = self.fixed_config if self.fixed_config else self.random.choice(self.all_configs)
497
+ def setup_goal(self, page: Page) -> tuple[str, dict]:
498
+ super().setup_goal(page=page)
487
499
 
500
+ # Get the task configuration
501
+ config = self.fixed_config if self.fixed_config else self.random.choice(self.all_configs)
488
502
  self.filter_columns = config["filter_columns"]
489
503
  self.filter_values = config["filter_values"]
490
504
  self.filter_kind = config["filter_kind"]
491
505
  self.list_info = config["list_info"]
492
506
  self.filter_len = len(self.filter_columns)
493
- visible_list_info = self._extract_list_info(page)
494
- visible_columns = set(visible_list_info["fields"].split(","))
495
- # Assert that required fields are visible
496
- assert (
497
- set(self.filter_columns) <= visible_columns and visible_columns == self.expected_fields
498
- ), f"Fields {self.filter_columns} are not all visible in the list. Re-run workarena-install to correct this."
499
- # generate goal
507
+
508
+ # Generate goal
500
509
  goal = (
501
510
  f"Create a filter for the list to extract all entries where "
502
511
  + f" {'and' if self.filter_kind == 'AND' else 'or'} ".join(
@@ -511,8 +520,20 @@ class FilterListTask(ServiceNowListTask):
511
520
 
512
521
  return goal, info
513
522
 
514
- def _generate_random_config(self, seed: int, page: Page):
515
- self.pre_setup(seed, page)
523
+ def start(self, page: Page) -> None:
524
+ super().start(page)
525
+
526
+ self._wait_for_ready(page)
527
+
528
+ # Assert that required fields are visible (task feasibility check)
529
+ visible_list_info = self._extract_list_info(page)
530
+ visible_columns = set(visible_list_info["fields"].split(","))
531
+ assert (
532
+ set(self.filter_columns) <= visible_columns and visible_columns == self.expected_fields
533
+ ), f"Fields {self.filter_columns} are not all visible in the list. Re-run workarena-install to correct this."
534
+
535
+ def _generate_random_config(self, page: Page):
536
+ self.setup(page=page)
516
537
  self._wait_for_ready(page)
517
538
 
518
539
  # Extract the list from the page
@@ -594,7 +615,7 @@ class FilterListTask(ServiceNowListTask):
594
615
  return goal, {}
595
616
 
596
617
  def cheat(self, page: Page, chat_messages: list[str]) -> None:
597
- super().cheat(page, chat_messages)
618
+ super().cheat(page=page, chat_messages=chat_messages)
598
619
  self._wait_for_ready(page)
599
620
 
600
621
  iframe, _, _ = self._get_visible_list(page)
@@ -686,8 +707,17 @@ class FilterListTask(ServiceNowListTask):
686
707
  Note: current implementation is limited to AND and OR filters (single type per filter) with equality operators
687
708
 
688
709
  """
710
+ right_url = check_url_suffix_match(page, expected_url=self.start_url, task=self)
711
+ if not right_url:
712
+ return (
713
+ 0,
714
+ False,
715
+ "",
716
+ {
717
+ "message": f"The page is not in the right URL to validate task {self.__class__.__name__}."
718
+ },
719
+ )
689
720
  self._wait_for_ready(page)
690
-
691
721
  if self.filter_kind not in ["AND", "OR"]:
692
722
  raise NotImplementedError("Only AND and OR filters are supported.")
693
723
  # Excludes AND because that's the default and its sep is ^ which matches everywhere
@@ -767,11 +797,13 @@ class FilterListTask(ServiceNowListTask):
767
797
  class FilterAssetListTask(FilterListTask):
768
798
  def __init__(
769
799
  self,
800
+ seed: int,
770
801
  instance=None,
771
802
  fixed_config: dict = None,
772
803
  ) -> None:
773
804
  super().__init__(
774
- instance,
805
+ seed=seed,
806
+ instance=instance,
775
807
  list_url=LISTS["alm_asset"]["url"],
776
808
  fixed_config=fixed_config,
777
809
  config_path=FILTER_ASSET_LIST_CONFIG_PATH,
@@ -782,11 +814,13 @@ class FilterAssetListTask(FilterListTask):
782
814
  class FilterChangeRequestListTask(FilterListTask):
783
815
  def __init__(
784
816
  self,
817
+ seed: int,
785
818
  instance=None,
786
819
  fixed_config: dict = None,
787
820
  ) -> None:
788
821
  super().__init__(
789
- instance,
822
+ seed=seed,
823
+ instance=instance,
790
824
  list_url=LISTS["change_request"]["url"],
791
825
  fixed_config=fixed_config,
792
826
  config_path=FILTER_CHANGE_REQUEST_LIST_CONFIG_PATH,
@@ -797,11 +831,13 @@ class FilterChangeRequestListTask(FilterListTask):
797
831
  class FilterHardwareListTask(FilterListTask):
798
832
  def __init__(
799
833
  self,
834
+ seed: int,
800
835
  instance=None,
801
836
  fixed_config: dict = None,
802
837
  ) -> None:
803
838
  super().__init__(
804
- instance,
839
+ seed=seed,
840
+ instance=instance,
805
841
  list_url=LISTS["alm_hardware"]["url"],
806
842
  fixed_config=fixed_config,
807
843
  config_path=FILTER_HARDWARE_LIST_CONFIG_PATH,
@@ -812,11 +848,13 @@ class FilterHardwareListTask(FilterListTask):
812
848
  class FilterIncidentListTask(FilterListTask):
813
849
  def __init__(
814
850
  self,
851
+ seed: int,
815
852
  instance=None,
816
853
  fixed_config: dict = None,
817
854
  ) -> None:
818
855
  super().__init__(
819
- instance,
856
+ seed=seed,
857
+ instance=instance,
820
858
  list_url=LISTS["incident"]["url"],
821
859
  fixed_config=fixed_config,
822
860
  config_path=FILTER_INCIDENT_LIST_CONFIG_PATH,
@@ -827,11 +865,13 @@ class FilterIncidentListTask(FilterListTask):
827
865
  class FilterServiceCatalogItemListTask(FilterListTask):
828
866
  def __init__(
829
867
  self,
868
+ seed: int,
830
869
  instance=None,
831
870
  fixed_config: dict = None,
832
871
  ) -> None:
833
872
  super().__init__(
834
- instance,
873
+ seed=seed,
874
+ instance=instance,
835
875
  list_url=LISTS["sc_cat_item"]["url"],
836
876
  fixed_config=fixed_config,
837
877
  config_path=FILTER_SERVICE_CATALOG_ITEM_LIST_CONFIG_PATH,
@@ -842,11 +882,13 @@ class FilterServiceCatalogItemListTask(FilterListTask):
842
882
  class FilterUserListTask(FilterListTask):
843
883
  def __init__(
844
884
  self,
885
+ seed: int,
845
886
  instance=None,
846
887
  fixed_config: dict = None,
847
888
  ) -> None:
848
889
  super().__init__(
849
- instance,
890
+ seed=seed,
891
+ instance=instance,
850
892
  list_url=LISTS["sys_user"]["url"],
851
893
  fixed_config=fixed_config,
852
894
  config_path=FILTER_USER_LIST_CONFIG_PATH,
@@ -857,11 +899,13 @@ class FilterUserListTask(FilterListTask):
857
899
  class SortAssetListTask(SortListTask):
858
900
  def __init__(
859
901
  self,
902
+ seed: int,
860
903
  instance=None,
861
904
  fixed_config: dict = None,
862
905
  ) -> None:
863
906
  super().__init__(
864
- instance,
907
+ seed=seed,
908
+ instance=instance,
865
909
  list_url=LISTS["alm_asset"]["url"],
866
910
  forbidden_fields=LISTS["alm_asset"]["forbidden_fields"],
867
911
  fixed_config=fixed_config,
@@ -873,11 +917,13 @@ class SortAssetListTask(SortListTask):
873
917
  class SortChangeRequestListTask(SortListTask):
874
918
  def __init__(
875
919
  self,
920
+ seed: int,
876
921
  instance=None,
877
922
  fixed_config: dict = None,
878
923
  ) -> None:
879
924
  super().__init__(
880
- instance,
925
+ seed=seed,
926
+ instance=instance,
881
927
  list_url=LISTS["change_request"]["url"],
882
928
  forbidden_fields=LISTS["change_request"]["forbidden_fields"],
883
929
  fixed_config=fixed_config,
@@ -889,11 +935,13 @@ class SortChangeRequestListTask(SortListTask):
889
935
  class SortHardwareListTask(SortListTask):
890
936
  def __init__(
891
937
  self,
938
+ seed: int,
892
939
  instance=None,
893
940
  fixed_config: dict = None,
894
941
  ) -> None:
895
942
  super().__init__(
896
- instance,
943
+ seed=seed,
944
+ instance=instance,
897
945
  list_url=LISTS["alm_hardware"]["url"],
898
946
  forbidden_fields=LISTS["alm_hardware"]["forbidden_fields"],
899
947
  fixed_config=fixed_config,
@@ -905,11 +953,13 @@ class SortHardwareListTask(SortListTask):
905
953
  class SortIncidentListTask(SortListTask):
906
954
  def __init__(
907
955
  self,
956
+ seed: int,
908
957
  instance=None,
909
958
  fixed_config: dict = None,
910
959
  ) -> None:
911
960
  super().__init__(
912
- instance,
961
+ seed=seed,
962
+ instance=instance,
913
963
  list_url=LISTS["incident"]["url"],
914
964
  forbidden_fields=LISTS["incident"]["forbidden_fields"],
915
965
  fixed_config=fixed_config,
@@ -921,11 +971,13 @@ class SortIncidentListTask(SortListTask):
921
971
  class SortServiceCatalogItemListTask(SortListTask):
922
972
  def __init__(
923
973
  self,
974
+ seed: int,
924
975
  instance=None,
925
976
  fixed_config: dict = None,
926
977
  ) -> None:
927
978
  super().__init__(
928
- instance,
979
+ seed=seed,
980
+ instance=instance,
929
981
  list_url=LISTS["sc_cat_item"]["url"],
930
982
  forbidden_fields=LISTS["sc_cat_item"]["forbidden_fields"],
931
983
  fixed_config=fixed_config,
@@ -937,11 +989,13 @@ class SortServiceCatalogItemListTask(SortListTask):
937
989
  class SortUserListTask(SortListTask):
938
990
  def __init__(
939
991
  self,
992
+ seed: int,
940
993
  instance=None,
941
994
  fixed_config: dict = None,
942
995
  ) -> None:
943
996
  super().__init__(
944
- instance,
997
+ seed=seed,
998
+ instance=instance,
945
999
  list_url=LISTS["sys_user"]["url"],
946
1000
  forbidden_fields=LISTS["sys_user"]["forbidden_fields"],
947
1001
  fixed_config=fixed_config,
@@ -34,27 +34,29 @@ class AllMenuTask(AbstractServiceNowTask):
34
34
 
35
35
  """
36
36
 
37
- def __init__(self, instance: SNowInstance = None, fixed_config: dict = None) -> None:
38
- super().__init__(instance=instance, start_rel_url="/now/nav/ui/home")
37
+ def __init__(self, seed: int, instance: SNowInstance = None, fixed_config: dict = None) -> None:
38
+ super().__init__(seed=seed, instance=instance, start_rel_url="/now/nav/ui/home")
39
39
  self.fixed_config = fixed_config
40
40
  with open(ALL_MENU_PATH, "r") as f:
41
41
  self.all_configs = json.load(f)
42
42
 
43
- def setup(self, page: Page, seed: int = None) -> tuple[str, dict]:
44
- self.pre_setup(seed, page)
43
+ def setup_goal(self, page: Page) -> tuple[str, dict]:
44
+ super().setup_goal(page=page)
45
+
46
+ # Get task configuration
45
47
  self.module = (
46
48
  self.fixed_config if self.fixed_config else self.random.choice(self.all_configs)
47
49
  )
48
50
  self.final_url = self.instance.snow_url + self.module["url"]
49
51
 
50
- # generate goal
52
+ # Generate goal
51
53
  goal = f'Navigate to the "{self.module["module"]}" module of the "{self.module["application"]}" application.'
52
54
  info = {}
53
55
 
54
56
  return goal, info
55
57
 
56
58
  def cheat(self, page: Page, chat_messages: list[str]) -> None:
57
- super().cheat(page, chat_messages)
59
+ super().cheat(page=page, chat_messages=chat_messages)
58
60
 
59
61
  menu_button = page.locator('div[aria-label="All"]')
60
62
  if menu_button.get_attribute("aria-expanded").lower() != "true":
@@ -74,7 +76,7 @@ class AllMenuTask(AbstractServiceNowTask):
74
76
  path = [m.strip() for m in self.module["module"].split(">")]
75
77
  # Navigate to the application's location in the menu and select its parent
76
78
  locator = menu.get_by_label(self.module["application"], exact=True).and_(
77
- menu.get_by_role("menuitem")
79
+ menu.get_by_role("button")
78
80
  )
79
81
  locator = locator.locator("xpath=ancestor::div[contains(@class, 'snf-collapsible-list')]")
80
82
  for module in path[:-1]:
@@ -106,6 +108,7 @@ class AllMenuTask(AbstractServiceNowTask):
106
108
  def validate(
107
109
  self, page: playwright.sync_api.Page, chat_messages: list[str]
108
110
  ) -> Tuple[float, bool, str, dict]:
111
+ page.wait_for_load_state("domcontentloaded")
109
112
 
110
113
  # Get the current URL and the final URL
111
114
  current_url = urlunparse(urlparse(unquote(page.evaluate("() => window.location.href"))))
@@ -136,35 +139,34 @@ class ImpersonationTask(AbstractServiceNowTask):
136
139
 
137
140
  """
138
141
 
139
- def __init__(self, instance=None, fixed_config: dict = None) -> None:
140
- super().__init__(instance=instance, start_rel_url="/now/nav/ui/home")
142
+ def __init__(self, seed: int, instance=None, fixed_config: dict = None) -> None:
143
+ super().__init__(seed=seed, instance=instance, start_rel_url="/now/nav/ui/home")
141
144
  self.fixed_config = fixed_config
142
145
  with open(IMPERSONATION_CONFIG_PATH, "r") as f:
143
146
  self.all_configs = json.load(f)
144
147
 
145
- def setup(self, page: Page, seed: int = None) -> tuple[str, dict]:
146
- self.pre_setup(seed, page)
147
- # Retrieve the list of users from the instance
148
- # XXX: We exclude the admin to avoid problems with validation (task would always be valid by default)
148
+ def setup_goal(self, page: Page) -> tuple[str, dict]:
149
+ super().setup_goal(page=page)
150
+
151
+ # Get task configuration
149
152
  self.user_full_name = (
150
153
  self.fixed_config if self.fixed_config else self.random.choice(self.all_configs)
151
154
  )
152
155
  assert self.user_full_name in self.all_configs
153
156
 
154
- # generate goal
157
+ # Generate goal
155
158
  goal = f"Impersonate the user {self.user_full_name}."
156
159
  info = {}
157
160
 
158
161
  return goal, info
159
162
 
160
163
  def cheat(self, page: Page, chat_messages: list[str]) -> None:
161
- super().cheat(page, chat_messages)
164
+ super().cheat(page=page, chat_messages=chat_messages)
162
165
  impersonate_user(self.user_full_name, page)
163
166
 
164
167
  def validate(
165
168
  self, page: playwright.sync_api.Page, chat_messages: list[str]
166
169
  ) -> Tuple[float, bool, str, dict]:
167
-
168
170
  user_info = self.page.evaluate("window.NOW")["user"]
169
171
 
170
172
  # If the current user is not being impersonated, fail.