OrderIpadProTask

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Developer Laptop (Mac)",
            **kwargs,
        )

class OrderIpadMiniTask(OrderHardwareTask):
    config_path = ORDER_IPAD_MINI_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="iPad mini",
            **kwargs,
        )

class OrderIpadProTask(OrderHardwareTask):
    config_path = ORDER_IPAD_PRO_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="iPad pro",
            **kwargs,
        )

class OrderSalesLaptopTask(OrderHardwareTask):
    config_path = ORDER_SALES_LAPTOP_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Sales Laptop",
            **kwargs,
        )

class OrderStandardLaptopTask(OrderHardwareTask):
    config_path = ORDER_STANDARD_LAPTOP_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Standard Laptop",


OrderSalesLaptopTask

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="iPad mini",
            **kwargs,
        )

class OrderIpadProTask(OrderHardwareTask):
    config_path = ORDER_IPAD_PRO_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="iPad pro",
            **kwargs,
        )

class OrderSalesLaptopTask(OrderHardwareTask):
    config_path = ORDER_SALES_LAPTOP_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Sales Laptop",
            **kwargs,
        )

class OrderStandardLaptopTask(OrderHardwareTask):
    config_path = ORDER_STANDARD_LAPTOP_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Standard Laptop",
            **kwargs,
        )

class OrderAppleWatchTask(OrderHardwareTask):
    config_path = ORDER_APPLE_WATCH_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Apple Watch",


OrderStandardLaptopTask

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="iPad pro",
            **kwargs,
        )

class OrderSalesLaptopTask(OrderHardwareTask):
    config_path = ORDER_SALES_LAPTOP_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Sales Laptop",
            **kwargs,
        )

class OrderStandardLaptopTask(OrderHardwareTask):
    config_path = ORDER_STANDARD_LAPTOP_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Standard Laptop",
            **kwargs,
        )

class OrderAppleWatchTask(OrderHardwareTask):
    config_path = ORDER_APPLE_WATCH_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Apple Watch",
            **kwargs,
        )

class OrderAppleMacBookPro15Task(OrderHardwareTask):
    config_path = ORDER_APPLE_MAC_BOOK_PRO15_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Apple MacBook Pro 15",


OrderAppleWatchTask

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Sales Laptop",
            **kwargs,
        )

class OrderStandardLaptopTask(OrderHardwareTask):
    config_path = ORDER_STANDARD_LAPTOP_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Standard Laptop",
            **kwargs,
        )

class OrderAppleWatchTask(OrderHardwareTask):
    config_path = ORDER_APPLE_WATCH_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Apple Watch",
            **kwargs,
        )

class OrderAppleMacBookPro15Task(OrderHardwareTask):
    config_path = ORDER_APPLE_MAC_BOOK_PRO15_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Apple MacBook Pro 15",
            **kwargs,
        )

class OrderDevelopmentLaptopPCTask(OrderHardwareTask):
    config_path = ORDER_DEVELOPMENT_LAPTOP_PC_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Development Laptop (PC)",


    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Standard Laptop",
            **kwargs,
        )

class OrderAppleWatchTask(OrderHardwareTask):
    config_path = ORDER_APPLE_WATCH_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Apple Watch",
            **kwargs,
        )

class OrderAppleMacBookPro15Task(OrderHardwareTask):
    config_path = ORDER_APPLE_MAC_BOOK_PRO15_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Apple MacBook Pro 15",
            **kwargs,
        )

class OrderDevelopmentLaptopPCTask(OrderHardwareTask):
    config_path = ORDER_DEVELOPMENT_LAPTOP_PC_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Development Laptop (PC)",
            **kwargs,
        )

class OrderLoanerLaptopTask(OrderHardwareTask):
    config_path = ORDER_LOANER_LAPTOP_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Loaner Laptop",


OrderDevelopmentLaptopPCTask

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Apple Watch",
            **kwargs,
        )

class OrderAppleMacBookPro15Task(OrderHardwareTask):
    config_path = ORDER_APPLE_MAC_BOOK_PRO15_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Apple MacBook Pro 15",
            **kwargs,
        )

class OrderDevelopmentLaptopPCTask(OrderHardwareTask):
    config_path = ORDER_DEVELOPMENT_LAPTOP_PC_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Development Laptop (PC)",
            **kwargs,
        )

class OrderLoanerLaptopTask(OrderHardwareTask):
    config_path = ORDER_LOANER_LAPTOP_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Loaner Laptop",
            **kwargs,
        )

__TASKS__ = [
    var
    for var in locals().values()
    if isinstance(var, type) and issubclass(var, OrderHardwareTask) and var is not OrderHardwareTask
]


OrderLoanerLaptopTask

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Apple MacBook Pro 15",
            **kwargs,
        )

class OrderDevelopmentLaptopPCTask(OrderHardwareTask):
    config_path = ORDER_DEVELOPMENT_LAPTOP_PC_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Development Laptop (PC)",
            **kwargs,
        )

class OrderLoanerLaptopTask(OrderHardwareTask):
    config_path = ORDER_LOANER_LAPTOP_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Loaner Laptop",
            **kwargs,
        )

__TASKS__ = [
    var
    for var in locals().values()
    if isinstance(var, type) and issubclass(var, OrderHardwareTask) and var is not OrderHardwareTask
]


            *args,
            fixed_request_item="Apple MacBook Pro 15",
            **kwargs,
        )

class OrderDevelopmentLaptopPCTask(OrderHardwareTask):
    config_path = ORDER_DEVELOPMENT_LAPTOP_PC_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Development Laptop (PC)",
            **kwargs,
        )

class OrderLoanerLaptopTask(OrderHardwareTask):
    config_path = ORDER_LOANER_LAPTOP_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Loaner Laptop",
            **kwargs,
        )

__TASKS__ = [
    var
    for var in locals().values()
    if isinstance(var, type) and issubclass(var, OrderHardwareTask) and var is not OrderHardwareTask
]


            instance=instance,
            start_rel_url="/now/nav/ui/classic/params/target/catalog_home.do%3Fsysparm_view%3Dcatalog_default",
            final_rel_url="/now/nav/ui/classic/params/target/com.glideapp.servicecatalog_checkout_view_v2.do",
        )

        if fixed_request_item is not None and fixed_config is not None:
            if fixed_request_item != fixed_config["item"]:
                raise ValueError(f"'fixed_request_item' and 'fixed_config[\"item\"]' do not match")

        self.fixed_config = fixed_config
        self.config = None
        self.fixed_request_item = fixed_request_item
        self.config_only_in_desc = config_only_in_desc

        self.js_prefix = "gsft_main"
        self.js_api_forms = "g_form"
        self.all_configs = self.all_configs()
        self.__dict__.update(kwargs)

    @classmethod
    def all_configs(cls) -> List[dict]:
        with open(cls.config_path, "r") as f:
            return json.load(f)

    def _wait_for_ready(self, page: Page, wait_for_form_api: bool = False) -> None:
        """
        Waits for the the main iframe to be loaded

        """
        logging.debug(f"Waiting for {self.js_prefix} to be fully loaded")
        page.wait_for_function(
            f"typeof window.{self.js_prefix} !== 'undefined' && window.{self.js_prefix}.WORKARENA_LOAD_COMPLETE"
        )
        logging.debug(f"Detected {self.js_prefix} ready")

        if wait_for_form_api:
            logging.debug("Waiting for Glide form API to be available")
            page.wait_for_function(f"window.{self.form_js_selector}")
            logging.debug("Detected Glide form API ready")

    @property
    def form_js_selector(self):
        return self.js_prefix + "." + self.js_api_forms


        if fixed_request_item is not None and fixed_config is not None:
            if fixed_request_item != fixed_config["item"]:
                raise ValueError(f"'fixed_request_item' and 'fixed_config[\"item\"]' do not match")

        self.fixed_config = fixed_config
        self.config = None
        self.fixed_request_item = fixed_request_item
        self.config_only_in_desc = config_only_in_desc

        self.js_prefix = "gsft_main"
        self.js_api_forms = "g_form"
        self.all_configs = self.all_configs()
        self.__dict__.update(kwargs)

    @classmethod
    def all_configs(cls) -> List[dict]:
        with open(cls.config_path, "r") as f:
            return json.load(f)

    def _wait_for_ready(self, page: Page, wait_for_form_api: bool = False) -> None:
        """
        Waits for the the main iframe to be loaded

        """
        logging.debug(f"Waiting for {self.js_prefix} to be fully loaded")
        page.wait_for_function(
            f"typeof window.{self.js_prefix} !== 'undefined' && window.{self.js_prefix}.WORKARENA_LOAD_COMPLETE"
        )
        logging.debug(f"Detected {self.js_prefix} ready")

        if wait_for_form_api:
            logging.debug("Waiting for Glide form API to be available")
            page.wait_for_function(f"window.{self.form_js_selector}")
            logging.debug("Detected Glide form API ready")

    @property
    def form_js_selector(self):
        return self.js_prefix + "." + self.js_api_forms

    def get_init_scripts(self) -> List[str]:
        return super().get_init_scripts() + [
            "registerGsftMainLoaded()",
            self._get_disable_add_to_cart_script(),
            self._get_remove_top_items_panel_script(),
        ]

    def _get_disable_add_to_cart_script(self):
        """
        Disables the 'Add to Cart' button on the service catalog page
        This is necessary so that agents running in parallel do not interfere with each other (cart is shared between sessions)

        """
        script = """
            function disableAddToCartButton() {


        with open(cls.config_path, "r") as f:
            return json.load(f)

    def _wait_for_ready(self, page: Page, wait_for_form_api: bool = False) -> None:
        """
        Waits for the the main iframe to be loaded

        """
        logging.debug(f"Waiting for {self.js_prefix} to be fully loaded")
        page.wait_for_function(
            f"typeof window.{self.js_prefix} !== 'undefined' && window.{self.js_prefix}.WORKARENA_LOAD_COMPLETE"
        )
        logging.debug(f"Detected {self.js_prefix} ready")

        if wait_for_form_api:
            logging.debug("Waiting for Glide form API to be available")
            page.wait_for_function(f"window.{self.form_js_selector}")
            logging.debug("Detected Glide form API ready")

    @property
    def form_js_selector(self):
        return self.js_prefix + "." + self.js_api_forms

    def get_init_scripts(self) -> List[str]:
        return super().get_init_scripts() + [
            "registerGsftMainLoaded()",
            self._get_disable_add_to_cart_script(),
            self._get_remove_top_items_panel_script(),
        ]

    def _get_disable_add_to_cart_script(self):
        """
        Disables the 'Add to Cart' button on the service catalog page
        This is necessary so that agents running in parallel do not interfere with each other (cart is shared between sessions)

        """
        script = """
            function disableAddToCartButton() {
                waLog('Searching for top items panel...', 'disableAddToCartButton');
                let button = document.querySelector('button[aria-label="Add to Cart"]');
                if (button) {
                    button.disabled = true;


    def _wait_for_ready(self, page: Page, wait_for_form_api: bool = False) -> None:
        """
        Waits for the the main iframe to be loaded

        """
        logging.debug(f"Waiting for {self.js_prefix} to be fully loaded")
        page.wait_for_function(
            f"typeof window.{self.js_prefix} !== 'undefined' && window.{self.js_prefix}.WORKARENA_LOAD_COMPLETE"
        )
        logging.debug(f"Detected {self.js_prefix} ready")

        if wait_for_form_api:
            logging.debug("Waiting for Glide form API to be available")
            page.wait_for_function(f"window.{self.form_js_selector}")
            logging.debug("Detected Glide form API ready")

    @property
    def form_js_selector(self):
        return self.js_prefix + "." + self.js_api_forms

    def get_init_scripts(self) -> List[str]:
        return super().get_init_scripts() + [
            "registerGsftMainLoaded()",
            self._get_disable_add_to_cart_script(),
            self._get_remove_top_items_panel_script(),
        ]

    def _get_disable_add_to_cart_script(self):
        """
        Disables the 'Add to Cart' button on the service catalog page
        This is necessary so that agents running in parallel do not interfere with each other (cart is shared between sessions)

        """
        script = """
            function disableAddToCartButton() {
                waLog('Searching for top items panel...', 'disableAddToCartButton');
                let button = document.querySelector('button[aria-label="Add to Cart"]');
                if (button) {
                    button.disabled = true;
                    waLog('WorkArena: Disabled the "Add to Cart" button', 'disableAddToCartButton');
                } else {
                    waLog('WorkArena: Could not find the "Add to Cart" button', 'disableAddToCartButton');
                }
            }

            runInGsftMainOnlyAndProtectByURL(disableAddToCartButton, 'glideapp.servicecatalog_cat_item_view.do');


            f"typeof window.{self.js_prefix} !== 'undefined' && window.{self.js_prefix}.WORKARENA_LOAD_COMPLETE"
        )
        logging.debug(f"Detected {self.js_prefix} ready")

        if wait_for_form_api:
            logging.debug("Waiting for Glide form API to be available")
            page.wait_for_function(f"window.{self.form_js_selector}")
            logging.debug("Detected Glide form API ready")

    @property
    def form_js_selector(self):
        return self.js_prefix + "." + self.js_api_forms

    def get_init_scripts(self) -> List[str]:
        return super().get_init_scripts() + [
            "registerGsftMainLoaded()",
            self._get_disable_add_to_cart_script(),
            self._get_remove_top_items_panel_script(),
        ]

    def _get_disable_add_to_cart_script(self):
        """
        Disables the 'Add to Cart' button on the service catalog page
        This is necessary so that agents running in parallel do not interfere with each other (cart is shared between sessions)

        """
        script = """
            function disableAddToCartButton() {
                waLog('Searching for top items panel...', 'disableAddToCartButton');
                let button = document.querySelector('button[aria-label="Add to Cart"]');
                if (button) {
                    button.disabled = true;
                    waLog('WorkArena: Disabled the "Add to Cart" button', 'disableAddToCartButton');
                } else {
                    waLog('WorkArena: Could not find the "Add to Cart" button', 'disableAddToCartButton');
                }
            }

            runInGsftMainOnlyAndProtectByURL(disableAddToCartButton, 'glideapp.servicecatalog_cat_item_view.do');
        """
        return script

    def _get_remove_top_items_panel_script(self):
        """Get script that removes the 'top items' panel that sometimes on the landing page of service catalog
        Disables the 'Top Requests' panel that sometimes appears on the landing page of the service catalog
        Runs in a loop to keep checking for the host element and shadow root
        URL is secured by running only on the catalog_home page; this is a heuristic to avoid running on other pages
        and does not check that the URL is an exact match, as moving back and forth between pages can cause the URL
        to change, but catalog_home will always be present.
        """
        script = """
            function removeTopItemsPanel() {
                waLog('Searching for top items panel...', 'removeTopItemsPanel');
                let headings = Array.from(document.querySelectorAll('[role="heading"]'));
                headings.forEach((heading) => {
                    if (heading.textContent.includes("Top Requests")) {
                        let parentDiv = heading.closest('div.drag_section');
                        if (parentDiv) {
                            parentDiv.remove();
                            waLog('Removed parent div for heading: ' + heading.textContent, 'removeTopItemsPanel');
                        }


        Disables the 'Add to Cart' button on the service catalog page
        This is necessary so that agents running in parallel do not interfere with each other (cart is shared between sessions)

        """
        script = """
            function disableAddToCartButton() {
                waLog('Searching for top items panel...', 'disableAddToCartButton');
                let button = document.querySelector('button[aria-label="Add to Cart"]');
                if (button) {
                    button.disabled = true;
                    waLog('WorkArena: Disabled the "Add to Cart" button', 'disableAddToCartButton');
                } else {
                    waLog('WorkArena: Could not find the "Add to Cart" button', 'disableAddToCartButton');
                }
            }

            runInGsftMainOnlyAndProtectByURL(disableAddToCartButton, 'glideapp.servicecatalog_cat_item_view.do');
        """
        return script

    def _get_remove_top_items_panel_script(self):
        """Get script that removes the 'top items' panel that sometimes on the landing page of service catalog
        Disables the 'Top Requests' panel that sometimes appears on the landing page of the service catalog
        Runs in a loop to keep checking for the host element and shadow root
        URL is secured by running only on the catalog_home page; this is a heuristic to avoid running on other pages
        and does not check that the URL is an exact match, as moving back and forth between pages can cause the URL
        to change, but catalog_home will always be present.
        """
        script = """
            function removeTopItemsPanel() {
                waLog('Searching for top items panel...', 'removeTopItemsPanel');
                let headings = Array.from(document.querySelectorAll('[role="heading"]'));
                headings.forEach((heading) => {
                    if (heading.textContent.includes("Top Requests")) {
                        let parentDiv = heading.closest('div.drag_section');
                        if (parentDiv) {
                            parentDiv.remove();
                            waLog('Removed parent div for heading: ' + heading.textContent, 'removeTopItemsPanel');
                        }
                    }
                });
            }

            runInGsftMainOnlyAndProtectByURL(removeTopItemsPanel, `catalog_home`);
            """
        return script

    def setup_goal(self, page: Page) -> tuple[str, dict]:
        super().setup_goal(page=page)

        # Get the task configuration
        assert self.all_configs is not None, "No configuration available for the task."
        self.config = (
            self.fixed_config if self.fixed_config else self.random.choice(self.all_configs)
        )
        self.requested_item = self.config["item"]
        self.short_description = self.config["description"]
        self.quantity = self.config["quantity"]
        self.requested_configuration = self.config["configuration"]

        # Generate goal
        if self.config_only_in_desc:
            goal = self.get_pretty_printed_description()
        else:
            goal = f'Go to the hardware store and order {self.quantity} "{self.requested_item}"'
        if len(self.requested_configuration) > 0:


        """
        script = """
            function removeTopItemsPanel() {
                waLog('Searching for top items panel...', 'removeTopItemsPanel');
                let headings = Array.from(document.querySelectorAll('[role="heading"]'));
                headings.forEach((heading) => {
                    if (heading.textContent.includes("Top Requests")) {
                        let parentDiv = heading.closest('div.drag_section');
                        if (parentDiv) {
                            parentDiv.remove();
                            waLog('Removed parent div for heading: ' + heading.textContent, 'removeTopItemsPanel');
                        }
                    }
                });
            }

            runInGsftMainOnlyAndProtectByURL(removeTopItemsPanel, `catalog_home`);
            """
        return script

    def setup_goal(self, page: Page) -> tuple[str, dict]:
        super().setup_goal(page=page)

        # Get the task configuration
        assert self.all_configs is not None, "No configuration available for the task."
        self.config = (
            self.fixed_config if self.fixed_config else self.random.choice(self.all_configs)
        )
        self.requested_item = self.config["item"]
        self.short_description = self.config["description"]
        self.quantity = self.config["quantity"]
        self.requested_configuration = self.config["configuration"]

        # Generate goal
        if self.config_only_in_desc:
            goal = self.get_pretty_printed_description()
        else:
            goal = f'Go to the hardware store and order {self.quantity} "{self.requested_item}"'
        if len(self.requested_configuration) > 0:
            goal += f" with configuration {dict((k, v[1]) for k, v in self.requested_configuration.items())}"
        info = {}

        # Used to keep track of the sysid of the request for validation
        self.request_sysid = None

        return goal, info

    def cheat(self, page: Page, chat_messages: list[str]) -> None:
        super().cheat(page=page, chat_messages=chat_messages)
        self._wait_for_ready(page=page)

        iframe = page.frame(self.js_prefix)

        # Find hardware buttons
        element = iframe.wait_for_selector("a:text('Hardware')", strict=True)
        element.click()
        self._wait_for_ready(page=page)

        element = iframe.wait_for_selector(f"h2:has-text('{self.requested_item}')", strict=True)
        element.click()
        self._wait_for_ready(page=page, wait_for_form_api=True)

        quantity_input = iframe.wait_for_selector("#quantity", strict=True)
        quantity_input.select_option(str(self.quantity))

        editable_fields = page.evaluate(f"{self.form_js_selector}.getEditableFields()")


cheat

        )
        self.requested_item = self.config["item"]
        self.short_description = self.config["description"]
        self.quantity = self.config["quantity"]
        self.requested_configuration = self.config["configuration"]

        # Generate goal
        if self.config_only_in_desc:
            goal = self.get_pretty_printed_description()
        else:
            goal = f'Go to the hardware store and order {self.quantity} "{self.requested_item}"'
        if len(self.requested_configuration) > 0:
            goal += f" with configuration {dict((k, v[1]) for k, v in self.requested_configuration.items())}"
        info = {}

        # Used to keep track of the sysid of the request for validation
        self.request_sysid = None

        return goal, info

    def cheat(self, page: Page, chat_messages: list[str]) -> None:
        super().cheat(page=page, chat_messages=chat_messages)
        self._wait_for_ready(page=page)

        iframe = page.frame(self.js_prefix)

        # Find hardware buttons
        element = iframe.wait_for_selector("a:text('Hardware')", strict=True)
        element.click()
        self._wait_for_ready(page=page)

        element = iframe.wait_for_selector(f"h2:has-text('{self.requested_item}')", strict=True)
        element.click()
        self._wait_for_ready(page=page, wait_for_form_api=True)

        quantity_input = iframe.wait_for_selector("#quantity", strict=True)
        quantity_input.select_option(str(self.quantity))

        editable_fields = page.evaluate(f"{self.form_js_selector}.getEditableFields()")

        lookup_map = {}
        for idx, field in enumerate(editable_fields):
            control_text = self._get_control_description(page, field)
            lookup_map[control_text] = field

        for field_label, (element, value) in self.requested_configuration.items():
            element_id = lookup_map[field_label]
            control_type = page.evaluate(f"{self.form_js_selector}.getControl('{element_id}').type")

            if control_type in ("radio",):
                num_options = page.evaluate(
                    f"{self.form_js_selector}.getControls('{element_id}').length"
                )
                for i in range(num_options):
                    control_handle = page.evaluate_handle(
                        f"{self.form_js_selector}.getControls('{element_id}')[{i}]"
                    )
                    control_text = control_handle.evaluate("e => e.parentElement.innerText")
                    if control_text.startswith(
                        value
                    ):  # the page changes the text dynamically adding subtract/add to the text
                        control_id = control_handle.get_attribute("id")
                        iframe.wait_for_selector(f'label[for="{control_id}"]', strict=True).click()
                        break
            elif control_type == "hidden":
                element_control = page.evaluate_handle(
                    f"{self.form_js_selector}.getControl('{element_id}')"
                )
                element_value = element_control.evaluate("e => e.value")
                if element_value != str(value).lower():
                    label_id = f"ni.{element_id}_label"
                    element_label = iframe.wait_for_selector(
                        f'label[id="{label_id}"]', strict=True, timeout=1_000
                    )
                    element_label.click()
            elif control_type in ("textarea", "text"):
                element_control = page.evaluate_handle(
                    f"{self.form_js_selector}.getControl('{element_id}')"
                ).as_element()  # this look superfluous
                element_id = element_control.get_attribute("id")  # this look superfluous
                text_element = iframe.query_selector(f'[id="{element_id}"]')
                text_element.click()
                fill_text(page=page, input_field=text_element, value=value, iframe=iframe)

            elif control_type == "select-one":
                iframe.locator(f"id={element_id}").select_option(value)
            else:
                raise ValueError(f"Unknown control type {control_type}")

        order_now_button = iframe.wait_for_selector("#oi_order_now_button", strict=True)

        with page.expect_navigation():
            order_now_button.click()

    def _generate_random_config(self, page: Page):
        """Generate a random configuration for the task"""
        self.task_is_setup = (
            False  # This is a hack to avoid raising an exception in the setup method
        )
        self.setup(page=page, do_start=False)
        if self.fixed_request_item:
            self.requested_item = self.fixed_request_item
        else:
            # ... choose a random item to order
            self.requested_item = self.random.choice(list(META_CONFIGS.keys()))

        meta_config = META_CONFIGS[self.requested_item]
        self.fixed_config = {
            "item": self.requested_item,
            "description": meta_config["desc"],
            "quantity": self.random.randint(1, 11),
            "configuration": {
                ctrl_name: (ctrl_type, self.random.choice(values))


                    element_label.click()
            elif control_type in ("textarea", "text"):
                element_control = page.evaluate_handle(
                    f"{self.form_js_selector}.getControl('{element_id}')"
                ).as_element()  # this look superfluous
                element_id = element_control.get_attribute("id")  # this look superfluous
                text_element = iframe.query_selector(f'[id="{element_id}"]')
                text_element.click()
                fill_text(page=page, input_field=text_element, value=value, iframe=iframe)

            elif control_type == "select-one":
                iframe.locator(f"id={element_id}").select_option(value)
            else:
                raise ValueError(f"Unknown control type {control_type}")

        order_now_button = iframe.wait_for_selector("#oi_order_now_button", strict=True)

        with page.expect_navigation():
            order_now_button.click()

    def _generate_random_config(self, page: Page):
        """Generate a random configuration for the task"""
        self.task_is_setup = (
            False  # This is a hack to avoid raising an exception in the setup method
        )
        self.setup(page=page, do_start=False)
        if self.fixed_request_item:
            self.requested_item = self.fixed_request_item
        else:
            # ... choose a random item to order
            self.requested_item = self.random.choice(list(META_CONFIGS.keys()))

        meta_config = META_CONFIGS[self.requested_item]
        self.fixed_config = {
            "item": self.requested_item,
            "description": meta_config["desc"],
            "quantity": self.random.randint(1, 11),
            "configuration": {
                ctrl_name: (ctrl_type, self.random.choice(values))
                for ctrl_name, (ctrl_type, values) in meta_config["options"].items()
            },
        }
        self.setup(page=page, do_start=True)

    def _get_control_description(self, page, field):
        """
        Get the description of a control (e.g., the text of a radio button)
        """
        # Wait for everything to be ready
        self._wait_for_ready(page, wait_for_form_api=True)

        control_type = page.evaluate(f"{self.form_js_selector}.getControl('{field}').type")
        if control_type in ("radio", "select-one"):
            return page.evaluate(f"{self.form_js_selector}.getLabelOf('{field}')")
        elif control_type in ("textarea", "hidden", "text"):
            control_handle = page.evaluate_handle(f"{self.form_js_selector}.getControl('{field}')")
            # The control text is not always in the control itself, but in a parent element.
            # Being a heuristic, we try to find it by going up the DOM tree.
            # This is up to the page implementation, 5 is an arbitrary number.
            for depth in range(5):
                control_text = control_handle.evaluate("e => e.innerText")
                if control_text != "":
                    break


        )
        self.setup(page=page, do_start=False)
        if self.fixed_request_item:
            self.requested_item = self.fixed_request_item
        else:
            # ... choose a random item to order
            self.requested_item = self.random.choice(list(META_CONFIGS.keys()))

        meta_config = META_CONFIGS[self.requested_item]
        self.fixed_config = {
            "item": self.requested_item,
            "description": meta_config["desc"],
            "quantity": self.random.randint(1, 11),
            "configuration": {
                ctrl_name: (ctrl_type, self.random.choice(values))
                for ctrl_name, (ctrl_type, values) in meta_config["options"].items()
            },
        }
        self.setup(page=page, do_start=True)

    def _get_control_description(self, page, field):
        """
        Get the description of a control (e.g., the text of a radio button)
        """
        # Wait for everything to be ready
        self._wait_for_ready(page, wait_for_form_api=True)

        control_type = page.evaluate(f"{self.form_js_selector}.getControl('{field}').type")
        if control_type in ("radio", "select-one"):
            return page.evaluate(f"{self.form_js_selector}.getLabelOf('{field}')")
        elif control_type in ("textarea", "hidden", "text"):
            control_handle = page.evaluate_handle(f"{self.form_js_selector}.getControl('{field}')")
            # The control text is not always in the control itself, but in a parent element.
            # Being a heuristic, we try to find it by going up the DOM tree.
            # This is up to the page implementation, 5 is an arbitrary number.
            for depth in range(5):
                control_text = control_handle.evaluate("e => e.innerText")
                if control_text != "":
                    break
                control_handle = control_handle.evaluate_handle("e => e.parentElement")
            else:
                raise ValueError(f"Could not find control text for {field}")
        else:
            raise ValueError(f"Unknown control type {control_type}")
        return control_text

    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in L3 compositional tasks.
        called by subclasses
        """
        class_name = self.__class__.__name__
        class_name = class_name.replace("Task", "")
        # Split the words
        words = re.findall(r"[A-Z][^A-Z]*", class_name)
        class_name_formatted = " ".join(words)
        task_specs = {
            "Quantity": self.config["quantity"],
            "Configuration": self.config["configuration"],
        }
        if self.config_only_in_desc:
            task_info = f"- Order the item in the following quantities and with the following configuration:\n"
        else:
            task_specs["Description"] = self.config["description"]
            task_info = f"- {class_name_formatted} with the following specifications:\n"


        control_type = page.evaluate(f"{self.form_js_selector}.getControl('{field}').type")
        if control_type in ("radio", "select-one"):
            return page.evaluate(f"{self.form_js_selector}.getLabelOf('{field}')")
        elif control_type in ("textarea", "hidden", "text"):
            control_handle = page.evaluate_handle(f"{self.form_js_selector}.getControl('{field}')")
            # The control text is not always in the control itself, but in a parent element.
            # Being a heuristic, we try to find it by going up the DOM tree.
            # This is up to the page implementation, 5 is an arbitrary number.
            for depth in range(5):
                control_text = control_handle.evaluate("e => e.innerText")
                if control_text != "":
                    break
                control_handle = control_handle.evaluate_handle("e => e.parentElement")
            else:
                raise ValueError(f"Could not find control text for {field}")
        else:
            raise ValueError(f"Unknown control type {control_type}")
        return control_text

    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in L3 compositional tasks.
        called by subclasses
        """
        class_name = self.__class__.__name__
        class_name = class_name.replace("Task", "")
        # Split the words
        words = re.findall(r"[A-Z][^A-Z]*", class_name)
        class_name_formatted = " ".join(words)
        task_specs = {
            "Quantity": self.config["quantity"],
            "Configuration": self.config["configuration"],
        }
        if self.config_only_in_desc:
            task_info = f"- Order the item in the following quantities and with the following configuration:\n"
        else:
            task_specs["Description"] = self.config["description"]
            task_info = f"- {class_name_formatted} with the following specifications:\n"
        for k, v in task_specs.items():
            # Some values might be empty - like the configuration of the apple watch. It is more natural to exclude them
            if not v:
                continue
            # If the value is a dictionary, print it in a nested way
            if isinstance(v, dict):
                task_info += f"    - {k}:\n"
                for k2, v2 in v.items():
                    task_info += f"        - {k2}: {v2[1]}\n"
            else:
                task_info += f"    - {k}: {v}\n"

        return task_info

    def teardown(self) -> None:
        """
        Deletes the request (and automatically all its items)
        """
        self._wait_for_ready(self.page)

        if hasattr(self, "request_sysid") and self.request_sysid is not None:
            db_delete_from_table(
                instance=self.instance, sys_id=self.request_sysid, table="sc_request"
            )

    def validate(self, page: Page, chat_messages: list[str]) -> tuple[int, bool, str, dict]:

        # Retrieve the request sysid from the URL
        current_url = parse.urlparse(parse.unquote(page.evaluate("() => window.location.href")))
        (self.request_sysid,) = parse.parse_qs(current_url.query).get("sysparm_sys_id", [None])
        if self.request_sysid is None:
            return (
                0,


teardown

        }
        if self.config_only_in_desc:
            task_info = f"- Order the item in the following quantities and with the following configuration:\n"
        else:
            task_specs["Description"] = self.config["description"]
            task_info = f"- {class_name_formatted} with the following specifications:\n"
        for k, v in task_specs.items():
            # Some values might be empty - like the configuration of the apple watch. It is more natural to exclude them
            if not v:
                continue
            # If the value is a dictionary, print it in a nested way
            if isinstance(v, dict):
                task_info += f"    - {k}:\n"
                for k2, v2 in v.items():
                    task_info += f"        - {k2}: {v2[1]}\n"
            else:
                task_info += f"    - {k}: {v}\n"

        return task_info

    def teardown(self) -> None:
        """
        Deletes the request (and automatically all its items)
        """
        self._wait_for_ready(self.page)

        if hasattr(self, "request_sysid") and self.request_sysid is not None:
            db_delete_from_table(
                instance=self.instance, sys_id=self.request_sysid, table="sc_request"
            )

    def validate(self, page: Page, chat_messages: list[str]) -> tuple[int, bool, str, dict]:

        # Retrieve the request sysid from the URL
        current_url = parse.urlparse(parse.unquote(page.evaluate("() => window.location.href")))
        (self.request_sysid,) = parse.parse_qs(current_url.query).get("sysparm_sys_id", [None])
        if self.request_sysid is None:
            return (
                0,
                False,
                "",
                {"message": "The request was not created, the sysid is not in the URL."},
            )

        # Short sleep to make sure the data is saved in the DB
        # TODO: improve this (noted in issue 291)
        sleep(3)
        r = get_request_by_id(instance=self.instance, sysid=self.request_sysid)
        if r is None:
            return 0, False, "", {"message": "The request is not in the database."}


validate

            if isinstance(v, dict):
                task_info += f"    - {k}:\n"
                for k2, v2 in v.items():
                    task_info += f"        - {k2}: {v2[1]}\n"
            else:
                task_info += f"    - {k}: {v}\n"

        return task_info

    def teardown(self) -> None:
        """
        Deletes the request (and automatically all its items)
        """
        self._wait_for_ready(self.page)

        if hasattr(self, "request_sysid") and self.request_sysid is not None:
            db_delete_from_table(
                instance=self.instance, sys_id=self.request_sysid, table="sc_request"
            )

    def validate(self, page: Page, chat_messages: list[str]) -> tuple[int, bool, str, dict]:

        # Retrieve the request sysid from the URL
        current_url = parse.urlparse(parse.unquote(page.evaluate("() => window.location.href")))
        (self.request_sysid,) = parse.parse_qs(current_url.query).get("sysparm_sys_id", [None])
        if self.request_sysid is None:
            return (
                0,
                False,
                "",
                {"message": "The request was not created, the sysid is not in the URL."},
            )

        # Short sleep to make sure the data is saved in the DB
        # TODO: improve this (noted in issue 291)
        sleep(3)
        r = get_request_by_id(instance=self.instance, sysid=self.request_sysid)
        if r is None:
            return 0, False, "", {"message": "The request is not in the database."}

        if len(r["items"]) == 0:
            return 0, False, "", {"message": "No items were requested."}

        if len(r["items"]) > 1:
            error_msg = (
                "Multiple kinds of items were requested, but only a single one was expected."
            )
            return (
                0,
                True,
                error_msg,
                {"message": error_msg},
            )
        else:
            (first_item,) = r["items"]

        if first_item["short_description"].lower() != self.short_description.lower():
            error_msg = "The requested item is incorrect."
            return 0, True, error_msg, {"message": error_msg}

        if first_item["quantity"] != str(self.quantity):
            error_msg = "The requested quantity is incorrect."
            return 0, True, error_msg, {"message": error_msg}

        options = first_item["options"]
        for k, (element_type, value) in self.requested_configuration.items():
            if element_type == "checkbox" or element_type == "radio":
                if not option_match_heuristic(value, options[k]):
                    error_msg = (
                        f"The requested {k} is incorrect, expected {value} but got {options[k]}."
                    )
                    return (
                        0,
                        True,
                        error_msg,
                        {"message": error_msg},
                    )
            elif element_type == "textarea":
                if value.lower() not in options[k].lower():
                    error_msg = (
                        f"The requested {k} is incorrect, expected {value} but got {options[k]}."
                    )
                    return (
                        0,
                        True,
                        error_msg,
                        {"message": error_msg},
                    )

        return (
            1,
            True,
            "Nice work, thank you!",
            {"message": "Task completed successfully."},
        )

def option_match_heuristic(value, option):
    def _process(x):
        x = str(x).lower()
        x = x.replace("_", "")
        x = x.replace(" ", "")
        return x

    return _process(value) == _process(option)

class OrderDeveloperLaptopTask(OrderHardwareTask):
    config_path = ORDER_DEVELOPER_LAPTOP_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Developer Laptop (Mac)",
            **kwargs,


                if value.lower() not in options[k].lower():
                    error_msg = (
                        f"The requested {k} is incorrect, expected {value} but got {options[k]}."
                    )
                    return (
                        0,
                        True,
                        error_msg,
                        {"message": error_msg},
                    )

        return (
            1,
            True,
            "Nice work, thank you!",
            {"message": "Task completed successfully."},
        )

def option_match_heuristic(value, option):
    def _process(x):
        x = str(x).lower()
        x = x.replace("_", "")
        x = x.replace(" ", "")
        return x

    return _process(value) == _process(option)

class OrderDeveloperLaptopTask(OrderHardwareTask):
    config_path = ORDER_DEVELOPER_LAPTOP_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            fixed_request_item="Developer Laptop (Mac)",
            **kwargs,
        )

class OrderIpadMiniTask(OrderHardwareTask):
    config_path = ORDER_IPAD_MINI_TASK_CONFIG_PATH

    def __init__(self, *args, **kwargs):
        super().__init__(


src.browsergym.workarena.tasks.form

import inspect
import json
import logging
import playwright.sync_api
import re

from collections import OrderedDict
from english_words import get_english_words_set
from faker import Faker

fake = Faker()
from playwright.sync_api._generated import Page
from tenacity import retry, stop_after_delay, retry_if_exception_type
from time import sleep
from typing import List, Tuple
from urllib import parse

from .base import AbstractServiceNowTask
from .comp_building_block import CompositionalBuildingBlockTask

from ..api.utils import (
    db_delete_from_table,
    table_api_call,
    table_column_info,
    HTTPError,
)
from ..config import (
    SNOW_BROWSER_TIMEOUT,
    # Paths to the configuration files
    CREATE_CHANGE_REQUEST_CONFIG_PATH,
    CREATE_HARDWARE_CONFIG_PATH,
    CREATE_INCIDENT_CONFIG_PATH,
    CREATE_PROBLEM_CONFIG_PATH,
    CREATE_USER_CONFIG_PATH,
    # Paths to the expected fields files
    EXPECTED_CHANGE_REQUEST_FORM_FIELDS_PATH,
    EXPECTED_HARDWARE_FORM_FIELDS_PATH,
    EXPECTED_INCIDENT_FORM_FIELDS_PATH,
    EXPECTED_PROBLEM_FORM_FIELDS_PATH,
    EXPECTED_USER_FORM_FIELDS_PATH,
    EXPECTED_REQUEST_ITEM_FORM_FIELDS_PATH,
)
from ..instance import SNowInstance
from .utils.form import fill_text
from .utils.utils import check_url_suffix_match, prettyprint_enum

ENGLISH_WORDS = list(get_english_words_set(["web2"]))

class ServiceNowFormTask(AbstractServiceNowTask):
    """
    Generic task for record manipulation (create/edit) in a table using a Glide form.

    Class attributes:
    -----------------
    config_path: str
        Path to the JSON file containing all possible configurations for the task. Defined in subclasses
    expected_fields_path: str
        Path to the JSON file containing all expected fields for the task. Defined in subclasses

    Parameters:
    -----------------
    form_url: str
        The URL of the form to use to create the record.
    instance: SNowInstance
        The instance on which to create the record.
    extra_mandatory_fields: List
        List of fields that should be marked as mandatory in the form (overrides the page specification).
    unique_valued_fields: dict
        Dictionary of fields that should have a unique value. Keys are the field names and values are functions
        used to make the fields unique (e.g., appending self.unique).
    fixed_config: dict
        Configuration to use for the task. If provided, the task will use the provided configuration instead of
        selecting a random one. See browsergym/workarena/data_files/task_configs/create_hardware_asset_task.json
        for an example of a configuration file.
    check_record_created: bool
        Whether to check if the record is created in cheat. This step uses the localStorage to get the sys_id, which is None when creating multiple forms. Hence, we bypass this step in the cheat.
    """

    config_path = None
    expected_fields_path = None

    def __init__(
        self,
        form_url: str,
        table_label: str,
        instance: SNowInstance = None,
        extra_mandatory_fields: List = [],
        prohibited_fields: List = [],
        unique_valued_fields: dict = {},
        fixed_config: dict = None,
        check_record_created: bool = True,
        seed: int = None,
    ) -> None:
        # The type of fields that we support interacting with
        self.supported_types = [
            "boolean",
            "choice",
            "email",
            "integer",
            "ph_number",
            "reference",
            "string",
        ]
        self.string_types = ["email", "ph_number", "string"]

        # Javascript variable names for the form API
        self.js_prefix = "gsft_main"
        self.js_api_forms = "g_form"
        self.form_js_selector = self.js_prefix + "." + self.js_api_forms

        # Extra mandatory fields (overriding the page specification)
        self.extra_mandatory_fields = extra_mandatory_fields

        # Prohibited fields: fields that we shouldn't interact with
        self.prohibited_fields = prohibited_fields
        self.table_metadata = None
        self.fields = None
        self.mandatory_fields = None
        self.optional_fields = None

        super().__init__(seed=seed, instance=instance, start_rel_url=form_url)

        self.form_url = form_url

        # Table pretty printed name
        self.table_label = table_label
        self.table_name = self.form_url.split("/")[-1].split(".do")[0]

        # Key in which the sys_id of the created record will be stored in the local storage
        self.session_sys_id_field = f"{id(self)}.record_sys_id"

        # Fields that should have a unique value (will append them with a uuid)
        self.unique_valued_fields = unique_valued_fields

        # Fixed configuration
        # We set the task fields, template record and created sysids to allow for easy access in compositional task creation
        self.fixed_config = fixed_config
        self.template_record = None
        self.task_fields = None
        self.fields = None
        self.protected_fields = None  # Fields that should not be edited
        if fixed_config is not None:
            self._set_required_config_attributes(fixed_config)

        self.n_extra_fields = None
        self.created_sysids = []
        if self.config_path:
            self.all_configs = self.all_configs()
        if self.expected_fields_path:
            with open(self.expected_fields_path, "r") as f:
                self.expected_fields = json.load(f)
        self.check_record_created = check_record_created

    @classmethod
    def all_configs(cls) -> List[dict]:
        with open(cls.config_path, "r") as f:
            return json.load(f)

    def _get_form(self, page):
        """
        Loads a bunch of info about the form on a page into object variables
        """
        # Extract Glide table information
        logging.debug("Extracting Glide table metadata")
        # ... expand reference fields
        # XXX: We need to expand reference fields and the referenced field is missing from the
        # form's client-side info so we are going to use the meta API to get that info.
        self.table_metadata = table_column_info(instance=self.instance, table=self.table_name)
        # ... augment with rendered metadata
        # XXX: Additional useful info is present in the rendered HTML. We extract it from there.
        for f in self.table_metadata:
            loc = page.frame(name=self.js_prefix).locator(f"#sys_display.{self.table_name}.{f}")
            if loc.count() > 0:
                # Check if the field is dependent on another field
                self.table_metadata[f]["dependent_on_field"] = loc.first.get_attribute(
                    "data-dependent"
                )

        # Get the table's pretty-printed label
        logging.debug("Extracting table pretty-printed title")
        self.table_label = table_api_call(
            instance=self.instance,
            table="sys_db_object",
            params={
                "sysparm_query": f"name={self.table_name}",
            },
        )["result"][0]["label"].lower()

    def _get_fields(self, page: Page) -> None:
        """
        Get the form fields; split them into mandatory and optional
        """
        page.wait_for_function(
            f"typeof window.{self.js_prefix} !== 'undefined' && window.{self.js_prefix}.WORKARENA_LOAD_COMPLETE",
        )

        # Get the form fields
        def is_field_visible(field):
            return page.evaluate(
                f"""
                {self.form_js_selector}.isVisible(
                    {self.form_js_selector}.getGlideUIElement('{field}'),
                    {self.form_js_selector}.getControl('{field}')
                );"""
            )

        logging.debug("Extracting valid form fields")
        editable_fields = page.evaluate(f"{self.form_js_selector}.getEditableFields()")
        field_elements = page.evaluate(f"{self.form_js_selector}.elements")
        all_fields = [f["fieldName"] for f in field_elements]
        self.fields = {
            f["fieldName"]: f
            for f in field_elements
            if f["fieldName"] in editable_fields
            and f["fieldName"] not in self.prohibited_fields
            and f["type"] in self.supported_types
            and self.table_metadata[f["fieldName"]].get(
                "dependent_on_field", ""
            )  # Don't add a field that depends on one that we'll edit
            not in editable_fields
        }
        # ... and their labels
        for f in self.fields:
            self.fields[f]["label"] = page.evaluate(f"{self.form_js_selector}.getLabelOf('{f}')")

        # Split them into mandatory and optional
        self.mandatory_fields = [f for f in self.fields.keys() if self.fields[f]["mandatory"]]
        self.optional_fields = [f for f in set(self.fields.keys()) - set(self.mandatory_fields)]

        # Sanity check
        assert len(self.fields) > 0, "No fields found on page."
        assert len(editable_fields) > 0, "No editable fields found on page."
        # ... check that the script that marks some fields as mandatory worked
        assert set(self.extra_mandatory_fields) <= set(
            self.mandatory_fields
        ), "Some extra mandatory fields are not mandatory in the form."
        # ... check that the script that makes some fields read-only worked
        assert all(
            f not in self.fields for f in self.prohibited_fields
        ), "Some prohibited fields are editable in the form."
        # ... check that all the fields that the config expects are present and that extra fields are not visible
        all_visible_fields = set([f for f in all_fields if is_field_visible(f)])
        expected_visible_fields = set([f for f in self.expected_fields if is_field_visible(f)])
        set_diff = all_visible_fields.union(
            expected_visible_fields
        ) - all_visible_fields.intersection(expected_visible_fields)
        assert (
            len(set_diff) == 0
        ), f"The fields {set_diff} are either missing or unexpectedly visible on the form. Re-run 'workarena-install' to correct this."

    def _preprocess_fields(self, field, value):
        """
        Do some preprocessing on loaded fields

        For example, we don't want to load old dates since they won't match newly created entries

        """
        logging.debug(f"Preprocessing field {field}")
        if field not in self.fields:
            return value

        field_type = self.table_metadata[field]["type"]

        # Extract display values for reference fields
        if field_type == "reference" and isinstance(value, dict):
            value = value["display_value"]

        # Remove date/time/username from journal entries
        elif field_type == "journal_input" and re.match(
            r"^20\d{2}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]) ([01]\d|2[0-3]):([0-5]\d):([0-5]\d) - .*",
            value,
        ):
            value = value[value.index("\n") + 1 :].strip().replace("\n", "").replace("\r", "")

        # Any other text-based input
        elif field_type in self.string_types:
            if value is not None:
                value = value.strip().replace("\n", "").replace("\r", "")

        return value

    def _wait_for_ready(self, page: Page, iframe_only=False) -> None:
        """
        Waits for the main iframe and APIs to be fully loaded

        Parameters:
        ----------
        page: playwright.sync_api.Page
            The page on which to wait for the iframe to be loaded
        iframe_only: bool
            If True, only wait for the iframe to be loaded. If False, also wait for the APIs to be available.

        """
        logging.debug(f"Waiting for {self.js_prefix} to be fully loaded")
        try:
            page.wait_for_function(
                f"typeof window.{self.js_prefix} !== 'undefined' && window.{self.js_prefix}.WORKARENA_LOAD_COMPLETE",
            )
        except:
            page.wait_for_load_state("networkidle")
            return
        logging.debug(f"Detected {self.js_prefix} ready")

        if not iframe_only:
            logging.debug("Waiting for Glide form API to be available")
            page.wait_for_function(f"window.{self.form_js_selector}")
            logging.debug("Detected Glide form API ready")

            logging.debug("Waiting for Glide tabs API to be available")
            page.wait_for_function(
                f"typeof window.{self.js_prefix}.g_tabs2Sections !== 'undefined'"
            )
            logging.debug("Detected Glide tabs API ready")

    def get_init_scripts(self) -> List[str]:
        # Extract expected URL suffix
        url_suffix = parse.urlparse(self.start_url).path.split("/")[-1]
        url_suffix = self.table_name

        # Add a few initialization scripts
        return super().get_init_scripts() + [
            "registerGsftMainLoaded();",
            # ... Mark the extra mandatory fields as such
            f"""
                function addFormMandatoryFields() {{
                    waLog('Setting mandatory fields', 'addFormMandatoryFields');
                    {";".join([f"{self.js_api_forms}.setMandatory('{f}', true)" for f in self.extra_mandatory_fields])}
                    waLog('Mandatory fields set successfully.', 'addFormMandatoryFields');
                }}

                runInGsftMainOnlyAndProtectByURL(addFormMandatoryFields, '{url_suffix}');
                """,
            f"""
                function patchSubmitButton() {{
                    waLog('Attempting to override form submit function', 'patchSubmitButton');
                    // Save the original function if it hasn't been saved yet
                    if(typeof old_gsftSubmit == 'undefined'){{
                        old_gsftSubmit = new Function('return ' + gsftSubmit.toString())();
                        waLog('Saved original submit function', 'patchSubmitButton');
                    }}

                    // Override the function to save the sys_id in the local storage
                    gsftSubmit = function(control, form, action_name) {{
                        localStorage['{self.session_sys_id_field}'] = {self.js_api_forms}.getUniqueValue();
                        old_gsftSubmit(control, form, action_name);
                    }};
                    waLog('Patched submit function. All done.', 'patchSubmitButton');
                }}

                runInGsftMainOnlyAndProtectByURL(patchSubmitButton, '{url_suffix}');
                """,
            # Ensure that only the expected fields are changed
            f"""
                function monitorChangeOnFields() {{
                    let predefinedList = {json.dumps(self.protected_fields)};
                    console.log('Predefined list: ' + predefinedList);
                    document.querySelectorAll("input, select, textarea").forEach((e) => {{
                        // Get the field name - some fields are like incident.xyz.field_name
                        let fieldName = e.name.split('.').pop();
                        if (!predefinedList.includes(fieldName)) {{
                            e.addEventListener("change", () => {{
                                window.WORKARENA_BAD_FIELD_CHANGED = true;
                                console.log("Field " + e.name + " changed and was not expected to.");
                            }})
                            waLog('Added change listener to field ' + e.name, 'monitorChangeOnFields');
                        }}
                    }})
                }}

                runInGsftMainOnlyAndProtectByURL(monitorChangeOnFields, '{url_suffix}');
            """,
        ]

    def start(self, page: Page) -> None:
        super().start(page)
        self._wait_for_ready(page)
        self._get_form(page)

    def _fill_fields(
        self,
        page: Page,
        iframe: playwright.sync_api.Frame,
        task_fields: List[str],
        update: bool = False,
    ) -> None:
        """
        Fill the fields in the form with the values from the template record. The fields to fill are specified in the
        task_fields list. Update is a flag that indicates if the task is an update task.
        """
        # XXX We need to ensure the table metadata as well as fields are set
        # before we can proceed with the cheat function
        if self.table_metadata is None:
            self._get_form(page)
        if self.fields is None:
            self._get_fields(page)

        # From now on, we assume we are on the form page
        self._wait_for_ready(page)

        # Retry on TypeError since in very rare occasions, element evaluates to null, which raises a TypeError
        @retry(
            stop=stop_after_delay(SNOW_BROWSER_TIMEOUT // 1000),
            retry=retry_if_exception_type(TypeError),
        )
        def show_field_tab(field):
            """
            Finds the control that allows to show the section where a field is located
            and clicks on it.

            """
            section = page.evaluate(
                f"""() => {{
                    const element = {self.form_js_selector}.getElement('{field}');
                    const ancestors = element.ancestors();
                    for (let ancestor of ancestors) {{
                        // Ancestor IDs are of the form "section-<section name>"
                        if (ancestor.id.startsWith('section-')) {{
                            return ancestor.id;
                        }}
                    }}
                    return null;  // Return null if no matching ancestor is found
                }}"""
            )
            section_id = section.split("-")[-1]
            tab_sections = {
                s.split(".")[-1]: i
                for i, s in enumerate(page.evaluate(f"{self.js_prefix}.g_tabs2Sections.tabIDs"))
            }

            # If the section is not in the tabs do nothing (it's probably the main section)
            if section_id not in tab_sections:
                return

            page.evaluate_handle(
                f"""{self.js_prefix}.g_tabs2Sections.tabsTabs[
                                                    {tab_sections[section_id]}
                                                ].element"""
            ).click(force=True)

        for field in task_fields:
            # Get the field's input control
            control = iframe.get_by_label(
                page.evaluate(f"{self.form_js_selector}.getLabelOf('{field}')"),
                exact=True,
            )
            if control.count() > 1:
                control = control.nth(0)
            # If the field is in a section, click on its header to make it visible
            show_field_tab(field)

            # Some fields are marked as string by the API but accept selection-based input
            # We use the select tag condition to match these fields. Others are marked as integers.
            if self.table_metadata[field]["type"] == "choice":
                control.select_option(str(self.template_record[field]))

            # Checkboxes
            elif self.table_metadata[field]["type"] == "boolean":
                control.set_checked(1 if self.template_record[field] == "true" else 0)

            # Any text-based input
            else:
                fill_text(
                    page=page,
                    iframe=iframe,
                    input_field=control,
                    value=self.template_record[field],
                )

        # Click on the submit button
        page.wait_for_timeout(1000)
        if update:
            iframe.locator("#sysverb_update").click()
        else:
            iframe.locator("#sysverb_insert").click()

        # Check if the record was created
        if self.check_record_created:
            # This does not work if multiple forms are created at once. The localStorage returns null after t
# ... truncated ...


ServiceNowFormTask

    CREATE_HARDWARE_CONFIG_PATH,
    CREATE_INCIDENT_CONFIG_PATH,
    CREATE_PROBLEM_CONFIG_PATH,
    CREATE_USER_CONFIG_PATH,
    # Paths to the expected fields files
    EXPECTED_CHANGE_REQUEST_FORM_FIELDS_PATH,
    EXPECTED_HARDWARE_FORM_FIELDS_PATH,
    EXPECTED_INCIDENT_FORM_FIELDS_PATH,
    EXPECTED_PROBLEM_FORM_FIELDS_PATH,
    EXPECTED_USER_FORM_FIELDS_PATH,
    EXPECTED_REQUEST_ITEM_FORM_FIELDS_PATH,
)
from ..instance import SNowInstance
from .utils.form import fill_text
from .utils.utils import check_url_suffix_match, prettyprint_enum

ENGLISH_WORDS = list(get_english_words_set(["web2"]))

class ServiceNowFormTask(AbstractServiceNowTask):
    """
    Generic task for record manipulation (create/edit) in a table using a Glide form.

    Class attributes:
    -----------------
    config_path: str
        Path to the JSON file containing all possible configurations for the task. Defined in subclasses
    expected_fields_path: str
        Path to the JSON file containing all expected fields for the task. Defined in subclasses

    Parameters:
    -----------------
    form_url: str
        The URL of the form to use to create the record.
    instance: SNowInstance
        The instance on which to create the record.
    extra_mandatory_fields: List
        List of fields that should be marked as mandatory in the form (overrides the page specification).
    unique_valued_fields: dict
        Dictionary of fields that should have a unique value. Keys are the field names and values are functions
        used to make the fields unique (e.g., appending self.unique).
    fixed_config: dict
        Configuration to use for the task. If provided, the task will use the provided configuration instead of
        selecting a random one. See browsergym/workarena/data_files/task_configs/create_hardware_asset_task.json
        for an example of a configuration file.
    check_record_created: bool
        Whether to check if the record is created in cheat. This step uses the localStorage to get the sys_id, which is None when creating multiple forms. Hence, we bypass this step in the cheat.
    """

    config_path = None
    expected_fields_path = None

    def __init__(
        self,
        form_url: str,
        table_label: str,
        instance: SNowInstance = None,
        extra_mandatory_fields: List = [],
        prohibited_fields: List = [],
        unique_valued_fields: dict = {},
        fixed_config: dict = None,
        check_record_created: bool = True,
        seed: int = None,
    ) -> None:
        # The type of fields that we support interacting with
        self.supported_types = [
            "boolean",
            "choice",
            "email",
            "integer",
            "ph_number",
            "reference",
            "string",
        ]
        self.string_types = ["email", "ph_number", "string"]

        # Javascript variable names for the form API
        self.js_prefix = "gsft_main"
        self.js_api_forms = "g_form"
        self.form_js_selector = self.js_prefix + "." + self.js_api_forms

        # Extra mandatory fields (overriding the page specification)
        self.extra_mandatory_fields = extra_mandatory_fields

        # Prohibited fields: fields that we shouldn't interact with
        self.prohibited_fields = prohibited_fields
        self.table_metadata = None
        self.fields = None
        self.mandatory_fields = None
        self.optional_fields = None

        super().__init__(seed=seed, instance=instance, start_rel_url=form_url)

        self.form_url = form_url

        # Table pretty printed name
        self.table_label = table_label
        self.table_name = self.form_url.split("/")[-1].split(".do")[0]

        # Key in which the sys_id of the created record will be stored in the local storage
        self.session_sys_id_field = f"{id(self)}.record_sys_id"

        # Fields that should have a unique value (will append them with a uuid)
        self.unique_valued_fields = unique_valued_fields

        # Fixed configuration
        # We set the task fields, template record and created sysids to allow for easy access in compositional task creation
        self.fixed_config = fixed_config
        self.template_record = None
        self.task_fields = None
        self.fields = None
        self.protected_fields = None  # Fields that should not be edited
        if fixed_config is not None:
            self._set_required_config_attributes(fixed_config)

        self.n_extra_fields = None
        self.created_sysids = []
        if self.config_path:
            self.all_configs = self.all_configs()
        if self.expected_fields_path:
            with open(self.expected_fields_path, "r") as f:
                self.expected_fields = json.load(f)
        self.check_record_created = check_record_created

    @classmethod
    def all_configs(cls) -> List[dict]:
        with open(cls.config_path, "r") as f:
            return json.load(f)

    def _get_form(self, page):
        """
        Loads a bunch of info about the form on a page into object variables
        """
        # Extract Glide table information
        logging.debug("Extracting Glide table metadata")
        # ... expand reference fields
        # XXX: We need to expand reference fields and the referenced field is missing from the
        # form's client-side info so we are going to use the meta API to get that info.
        self.table_metadata = table_column_info(instance=self.instance, table=self.table_name)
        # ... augment with rendered metadata
        # XXX: Additional useful info is present in the rendered HTML. We extract it from there.
        for f in self.table_metadata:
            loc = page.frame(name=self.js_prefix).locator(f"#sys_display.{self.table_name}.{f}")
            if loc.count() > 0:
                # Check if the field is dependent on another field
                self.table_metadata[f]["dependent_on_field"] = loc.first.get_attribute(
                    "data-dependent"
                )

        # Get the table's pretty-printed label
        logging.debug("Extracting table pretty-printed title")
        self.table_label = table_api_call(
            instance=self.instance,
            table="sys_db_object",
            params={
                "sysparm_query": f"name={self.table_name}",
            },
        )["result"][0]["label"].lower()

    def _get_fields(self, page: Page) -> None:
        """
        Get the form fields; split them into mandatory and optional
        """
        page.wait_for_function(
            f"typeof window.{self.js_prefix} !== 'undefined' && window.{self.js_prefix}.WORKARENA_LOAD_COMPLETE",
        )

        # Get the form fields
        def is_field_visible(field):
            return page.evaluate(
                f"""
                {self.form_js_selector}.isVisible(
                    {self.form_js_selector}.getGlideUIElement('{field}'),
                    {self.form_js_selector}.getControl('{field}')
                );"""
            )

        logging.debug("Extracting valid form fields")
        editable_fields = page.evaluate(f"{self.form_js_selector}.getEditableFields()")
        field_elements = page.evaluate(f"{self.form_js_selector}.elements")
        all_fields = [f["fieldName"] for f in field_elements]
        self.fields = {
            f["fieldName"]: f
            for f in field_elements
            if f["fieldName"] in editable_fields
            and f["fieldName"] not in self.prohibited_fields
            and f["type"] in self.supported_types
            and self.table_metadata[f["fieldName"]].get(
                "dependent_on_field", ""
            )  # Don't add a field that depends on one that we'll edit
            not in editable_fields
        }
        # ... and their labels
        for f in self.fields:
            self.fields[f]["label"] = page.evaluate(f"{self.form_js_selector}.getLabelOf('{f}')")

        # Split them into mandatory and optional
        self.mandatory_fields = [f for f in self.fields.keys() if self.fields[f]["mandatory"]]
        self.optional_fields = [f for f in set(self.fields.keys()) - set(self.mandatory_fields)]

        # Sanity check
        assert len(self.fields) > 0, "No fields found on page."
        assert len(editable_fields) > 0, "No editable fields found on page."
        # ... check that the script that marks some fields as mandatory worked
        assert set(self.extra_mandatory_fields) <= set(
            self.mandatory_fields
        ), "Some extra mandatory fields are not mandatory in the form."
        # ... check that the script that makes some fields read-only worked
        assert all(
            f not in self.fields for f in self.prohibited_fields
        ), "Some prohibited fields are editable in the form."
        # ... check that all the fields that the config expects are present and that extra fields are not visible
        all_visible_fields = set([f for f in all_fields if is_field_visible(f)])
        expected_visible_fields = set([f for f in self.expected_fields if is_field_visible(f)])
        set_diff = all_visible_fields.union(
            expected_visible_fields
        ) - all_visible_fields.intersection(expected_visible_fields)
        assert (
            len(set_diff) == 0
        ), f"The fields {set_diff} are either missing or unexpectedly visible on the form. Re-run 'workarena-install' to correct this."

    def _preprocess_fields(self, field, value):
        """
        Do some preprocessing on loaded fields

        For example, we don't want to load old dates since they won't match newly created entries

        """
        logging.debug(f"Preprocessing field {field}")
        if field not in self.fields:
            return value

        field_type = self.table_metadata[field]["type"]

        # Extract display values for reference fields
        if field_type == "reference" and isinstance(value, dict):
            value = value["display_value"]

        # Remove date/time/username from journal entries
        elif field_type == "journal_input" and re.match(
            r"^20\d{2}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]) ([01]\d|2[0-3]):([0-5]\d):([0-5]\d) - .*",
            value,
        ):
            value = value[value.index("\n") + 1 :].strip().replace("\n", "").replace("\r", "")

        # Any other text-based input
        elif field_type in self.string_types:
            if value is not None:
                value = value.strip().replace("\n", "").replace("\r", "")

        return value

    def _wait_for_ready(self, page: Page, iframe_only=False) -> None:
        """
        Waits for the main iframe and APIs to be fully loaded

        Parameters:
        ----------
        page: playwright.sync_api.Page
            The page on which to wait for the iframe to be loaded
        iframe_only: bool
            If True, only wait for the iframe to be loaded. If False, also wait for the APIs to be available.

        """
        logging.debug(f"Waiting for {self.js_prefix} to be fully loaded")
        try:
            page.wait_for_function(
                f"typeof window.{self.js_prefix} !== 'undefined' && window.{self.js_prefix}.WORKARENA_LOAD_COMPLETE",
            )
        except:
            page.wait_for_load_state("networkidle")
            return
        logging.debug(f"Detected {self.js_prefix} ready")

        if not iframe_only:
            logging.debug("Waiting for Glide form API to be available")
            page.wait_for_function(f"window.{self.form_js_selector}")
            logging.debug("Detected Glide form API ready")

            logging.debug("Waiting for Glide tabs API to be available")
            page.wait_for_function(
                f"typeof window.{self.js_prefix}.g_tabs2Sections !== 'undefined'"
            )
            logging.debug("Detected Glide tabs API ready")

    def get_init_scripts(self) -> List[str]:
        # Extract expected URL suffix
        url_suffix = parse.urlparse(self.start_url).path.split("/")[-1]
        url_suffix = self.table_name

        # Add a few initialization scripts
        return super().get_init_scripts() + [
            "registerGsftMainLoaded();",
            # ... Mark the extra mandatory fields as such
            f"""
                function addFormMandatoryFields() {{
                    waLog('Setting mandatory fields', 'addFormMandatoryFields');
                    {";".join([f"{self.js_api_forms}.setMandatory('{f}', true)" for f in self.extra_mandatory_fields])}
                    waLog('Mandatory fields set successfully.', 'addFormMandatoryFields');
                }}

                runInGsftMainOnlyAndProtectByURL(addFormMandatoryFields, '{url_suffix}');
                """,
            f"""
                function patchSubmitButton() {{
                    waLog('Attempting to override form submit function', 'patchSubmitButton');
                    // Save the original function if it hasn't been saved yet
                    if(typeof old_gsftSubmit == 'undefined'){{
                        old_gsftSubmit = new Function('return ' + gsftSubmit.toString())();
                        waLog('Saved original submit function', 'patchSubmitButton');
                    }}

                    // Override the function to save the sys_id in the local storage
                    gsftSubmit = function(control, form, action_name) {{
                        localStorage['{self.session_sys_id_field}'] = {self.js_api_forms}.getUniqueValue();
                        old_gsftSubmit(control, form, action_name);
                    }};
                    waLog('Patched submit function. All done.', 'patchSubmitButton');
                }}

                runInGsftMainOnlyAndProtectByURL(patchSubmitButton, '{url_suffix}');
                """,
            # Ensure that only the expected fields are changed
            f"""
                function monitorChangeOnFields() {{
                    let predefinedList = {json.dumps(self.protected_fields)};
                    console.log('Predefined list: ' + predefinedList);
                    document.querySelectorAll("input, select, textarea").forEach((e) => {{
                        // Get the field name - some fields are like incident.xyz.field_name
                        let fieldName = e.name.split('.').pop();
                        if (!predefinedList.includes(fieldName)) {{
                            e.addEventListener("change", () => {{
                                window.WORKARENA_BAD_FIELD_CHANGED = true;
                                console.log("Field " + e.name + " changed and was not expected to.");
                            }})
                            waLog('Added change listener to field ' + e.name, 'monitorChangeOnFields');
                        }}
                    }})
                }}

                runInGsftMainOnlyAndProtectByURL(monitorChangeOnFields, '{url_suffix}');
            """,
        ]

    def start(self, page: Page) -> None:
        super().start(page)
        self._wait_for_ready(page)
        self._get_form(page)

    def _fill_fields(
        self,
        page: Page,
        iframe: playwright.sync_api.Frame,
        task_fields: List[str],
        update: bool = False,
    ) -> None:
        """
        Fill the fields in the form with the values from the template record. The fields to fill are specified in the
        task_fields list. Update is a flag that indicates if the task is an update task.
        """
        # XXX We need to ensure the table metadata as well as fields are set
        # before we can proceed with the cheat function
        if self.table_metadata is None:
            self._get_form(page)
        if self.fields is None:
            self._get_fields(page)

        # From now on, we assume we are on the form page
        self._wait_for_ready(page)

        # Retry on TypeError since in very rare occasions, element evaluates to null, which raises a TypeError
        @retry(
            stop=stop_after_delay(SNOW_BROWSER_TIMEOUT // 1000),
            retry=retry_if_exception_type(TypeError),
        )
        def show_field_tab(field):
            """
            Finds the control that allows to show the section where a field is located
            and clicks on it.

            """
            section = page.evaluate(
                f"""() => {{
                    const element = {self.form_js_selector}.getElement('{field}');
                    const ancestors = element.ancestors();
                    for (let ancestor of ancestors) {{
                        // Ancestor IDs are of the form "section-<section name>"
                        if (ancestor.id.startsWith('section-')) {{
                            return ancestor.id;
                        }}
                    }}
                    return null;  // Return null if no matching ancestor is found
                }}"""
            )
            section_id = section.split("-")[-1]
            tab_sections = {
                s.split(".")[-1]: i
                for i, s in enumerate(page.evaluate(f"{self.js_prefix}.g_tabs2Sections.tabIDs"))
            }

            # If the section is not in the tabs do nothing (it's probably the main section)
            if section_id not in tab_sections:
                return

            page.evaluate_handle(
                f"""{self.js_prefix}.g_tabs2Sections.tabsTabs[
                                                    {tab_sections[section_id]}
                                                ].element"""
            ).click(force=True)

        for field in task_fields:
            # Get the field's input control
            control = iframe.get_by_label(
                page.evaluate(f"{self.form_js_selector}.getLabelOf('{field}')"),
                exact=True,
            )
            if control.count() > 1:
                control = control.nth(0)
            # If the field is in a section, click on its header to make it visible
            show_field_tab(field)

            # Some fields are marked as string by the API but accept selection-based input
            # We use the select tag condition to match these fields. Others are marked as integers.
            if self.table_metadata[field]["type"] == "choice":
                control.select_option(str(self.template_record[field]))

            # Checkboxes
            elif self.table_metadata[field]["type"] == "boolean":
                control.set_checked(1 if self.template_record[field] == "true" else 0)

            # Any text-based input
            else:
                fill_text(
                    page=page,
                    iframe=iframe,
                    input_field=control,
                    value=self.template_record[field],
                )

        # Click on the submit button
        page.wait_for_timeout(1000)
        if update:
            iframe.locator("#sysverb_update").click()
        else:
            iframe.locator("#sysverb_insert").click()

        # Check if the record was created
        if self.check_record_created:
            # This does not work if multiple forms are created at once. The localStorage returns null after the first form
            for attempt in range(5):
                # in update tasks, the sys_id is already known as the asset is created from the start
                if update:
                    sys_id = self.record_sys_id
                else:
                    sys_id = page.evaluate("localStorage").get(self.session_sys_id_field, None)

                # Pull the record from the database
                record = table_api_call(
                    instance=self.instance,
                    table=self.table_name,
                    params={
                        "sysparm_query": f"sys_id={sys_id}",
                        "sysparm_display_value": True,
                    },
                )["result"]
                if len(r
# ... truncated ...