-
Notifications
You must be signed in to change notification settings - Fork 0
Added locators for UI autotests (CORE-13504) #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| TIMEOUT_MAX = 3000 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| from .base_locators import BaseLocators, SidebarNavigationLocators | ||
| from .login_page_locators import LoginPageLocators | ||
| from .dashboard_page_locators import DashboardPageLocators | ||
| from .nodes_locators import NodesPageLocators, NodesListLocators | ||
| from .settings_locators import SettingsPageLocators, SettingsFormLocators | ||
| from .monitoring_locators import ( | ||
| MonitoringPageLocators, | ||
| MonitoringCommandsLocators, | ||
| MonitoringMetricsLocators | ||
| ) | ||
|
|
||
| __all__ = [ | ||
| # Base | ||
| "BaseLocators", | ||
| "SidebarNavigationLocators", | ||
| # Pages | ||
| "LoginPageLocators", | ||
| "DashboardPageLocators", | ||
| "NodesPageLocators", | ||
| "NodesListLocators", | ||
| "SettingsPageLocators", | ||
| "SettingsFormLocators", | ||
| "MonitoringPageLocators", | ||
| "MonitoringCommandsLocators", | ||
| "MonitoringMetricsLocators", | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| """Base locators for common UI elements across all pages.""" | ||
|
|
||
|
|
||
| class BaseLocators: | ||
| """Common locators shared across all pages.""" | ||
|
|
||
| # App container | ||
| APP_CONTAINER = "#app" | ||
| CP_SHELL = ".cp-shell" | ||
|
|
||
| # Sidebar | ||
| SIDEBAR = ".sidebar" | ||
| SIDEBAR_TOP = ".sidebar-top" | ||
| SIDEBAR_BOTTOM = ".sidebar-bottom" | ||
| SIDEBAR_LOGO_BUTTON = ".sidebar-logo-button" | ||
| SIDEBAR_VERSION = ".sidebar-version" | ||
|
|
||
| # Navigation | ||
| SIDEBAR_NAV = ".sidebar-nav" | ||
| SIDEBAR_NAV_ITEM = ".sidebar-nav-item" | ||
| SIDEBAR_NAV_ITEM_ACTIVE = ".sidebar-nav-item-active" | ||
| SIDEBAR_NAV_LABEL = ".sidebar-nav-label" | ||
| SIDEBAR_NAV_LABEL_ACTIVE = ".sidebar-nav-label-active" | ||
| SIDEBAR_NAV_ICON = ".sidebar-nav-icon" | ||
|
|
||
| # Main content | ||
| CP_SHELL_MAIN = ".cp-shell-main" | ||
|
|
||
| # SVG icons | ||
| SVG_INLINE_ICON = ".svg-inline-icon" | ||
| SVG_ICON = ".svg-icon" | ||
|
|
||
|
|
||
| class SidebarNavigationLocators: | ||
| """Locators for sidebar navigation items.""" | ||
|
|
||
| # Navigation items by icon class | ||
| NODES_NAV_ITEM = ".svg-inline-icon--nodes" | ||
| MONITORING_NAV_ITEM = ".svg-inline-icon--statistics" | ||
| DOCS_NAV_ITEM = ".svg-inline-icon--documentation" | ||
| ACTIVITY_NAV_ITEM = ".svg-inline-icon--bell" | ||
| SETTINGS_NAV_ITEM = ".svg-inline-icon--settings" | ||
|
|
||
| # Navigation items by label text | ||
| @staticmethod | ||
| def nav_item_by_label(label: str) -> str: | ||
| """Get navigation item by label text.""" | ||
| return f"button.sidebar-nav-item:has(.sidebar-nav-label:text('{label}'))" | ||
|
|
||
| # Active navigation item | ||
| ACTIVE_NAV_ITEM = ".sidebar-nav-item-active" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| """Locators for Monitoring page elements.""" | ||
|
|
||
|
|
||
| class MonitoringPageLocators: | ||
| """Locators specific to the Monitoring page.""" | ||
|
|
||
| # Page structure | ||
| MONITORING_WRAPPER = "div[data-v-3904b986]" | ||
| MONITORING_HEADER = ".monitoring-header" | ||
| MONITORING_TITLE = ".monitoring-title" | ||
| MONITORING_SUBTITLE = ".monitoring-subtitle" | ||
|
|
||
| # Sections | ||
| MONITORING_SECTION = ".monitoring-section" | ||
| MONITORING_SECTION_TITLE = ".monitoring-section-title" | ||
|
|
||
| # Steps card | ||
| MONITORING_STEPS_CARD = ".monitoring-steps-card" | ||
| MONITORING_STEP = ".monitoring-step" | ||
| MONITORING_STEP_NUMBER = ".monitoring-step-number" | ||
| MONITORING_STEP_CONTENT = ".monitoring-step-content" | ||
| MONITORING_STEP_TITLE = ".monitoring-step-title" | ||
| MONITORING_STEP_TEXT = ".monitoring-step-text" | ||
|
|
||
| # Command section | ||
| MONITORING_COMMAND_ROW = ".monitoring-command-row" | ||
| MONITORING_COMMAND_TEXT = ".monitoring-command-text" | ||
| MONITORING_ICON_BUTTON = ".monitoring-icon-button" | ||
|
|
||
| # Buttons | ||
| COPY_BUTTON = "button:has(.monitoring-icon-button)" | ||
|
|
||
| # Specific steps | ||
| @staticmethod | ||
| def step_by_number(number: int) -> str: | ||
| """Get monitoring step by number.""" | ||
| return f".monitoring-step:has(.monitoring-step-number:text('{number}'))" | ||
|
|
||
| INSTALL_EXPORTERS_STEP = ".monitoring-step:has(.monitoring-step-title:text('Install Exporters'))" | ||
| CONFIGURE_GRAFANA_STEP = ".monitoring-step:has(.monitoring-step-title:text('Configure Grafana'))" | ||
| ADD_DATASOURCE_STEP = ".monitoring-step:has(.monitoring-step-title:text('Add Data Source'))" | ||
|
|
||
| # Code/Command blocks | ||
| CODE_BLOCK = "code" | ||
| COMMAND_CODE = "code.monitoring-command-text" | ||
|
|
||
| # Links | ||
| MONITORING_LINK = ".monitoring-link" | ||
| EXTERNAL_LINK = "a[target='_blank']" | ||
|
|
||
| # Documentation links | ||
| GRAFANA_DOCS_LINK = "a:has-text('Grafana')" | ||
| PROMETHEUS_DOCS_LINK = "a:has-text('Prometheus')" | ||
|
|
||
| # Metrics section | ||
| METRICS_SECTION = ".metrics-section" | ||
| METRICS_CARD = ".metrics-card" | ||
| METRICS_TITLE = ".metrics-title" | ||
| METRICS_VALUE = ".metrics-value" | ||
| METRICS_CHART = ".metrics-chart" | ||
|
|
||
| # Dashboard section | ||
| DASHBOARD_SECTION = ".dashboard-section" | ||
| DASHBOARD_CARD = ".dashboard-card" | ||
| DASHBOARD_TITLE = ".dashboard-title" | ||
| DASHBOARD_PREVIEW = ".dashboard-preview" | ||
|
|
||
|
|
||
| class MonitoringCommandsLocators: | ||
| """Locators for monitoring commands and code snippets.""" | ||
|
|
||
| # Command types | ||
| KUBECTL_COMMAND = "code:has-text('kubectl')" | ||
| CURL_COMMAND = "code:has-text('curl')" | ||
| DOCKER_COMMAND = "code:has-text('docker')" | ||
|
|
||
| # Copy functionality | ||
| COPY_ICON = ".monitoring-icon-button svg" | ||
| COPY_SUCCESS_MESSAGE = ".copy-success" | ||
| COPY_ERROR_MESSAGE = ".copy-error" | ||
|
|
||
| # Command sections | ||
| @staticmethod | ||
| def command_by_text(text: str) -> str: | ||
| """Get command element by partial text.""" | ||
| return f"code:has-text('{text}')" | ||
|
|
||
| # Specific commands | ||
| INSTALL_EXPORTERS_COMMAND = "code:has-text('curl -sSL https://get.chainstack.com/monitoring')" | ||
| KUBECTL_APPLY_COMMAND = "code:has-text('kubectl apply')" | ||
|
|
||
| # Command output | ||
| COMMAND_OUTPUT = ".command-output" | ||
| COMMAND_SUCCESS = ".command-success" | ||
| COMMAND_ERROR = ".command-error" | ||
|
|
||
|
|
||
| class MonitoringMetricsLocators: | ||
| """Locators for monitoring metrics and dashboards.""" | ||
|
|
||
| # Metrics display | ||
| METRIC_ITEM = ".metric-item" | ||
| METRIC_LABEL = ".metric-label" | ||
| METRIC_VALUE_TEXT = ".metric-value-text" | ||
| METRIC_UNIT = ".metric-unit" | ||
| METRIC_TREND = ".metric-trend" | ||
|
|
||
| # Chart elements | ||
| CHART_CONTAINER = ".chart-container" | ||
| CHART_CANVAS = "canvas" | ||
| CHART_LEGEND = ".chart-legend" | ||
| CHART_TOOLTIP = ".chart-tooltip" | ||
|
|
||
| # Time range selector | ||
| TIME_RANGE_SELECTOR = ".time-range-selector" | ||
| TIME_RANGE_BUTTON = ".time-range-button" | ||
| TIME_RANGE_CUSTOM = ".time-range-custom" | ||
|
|
||
| # Refresh controls | ||
| REFRESH_BUTTON = "button:has-text('Refresh')" | ||
| AUTO_REFRESH_TOGGLE = ".auto-refresh-toggle" | ||
| REFRESH_INTERVAL = ".refresh-interval" | ||
|
|
||
| # Export options | ||
| EXPORT_BUTTON = "button:has-text('Export')" | ||
| EXPORT_CSV = "button:has-text('Export CSV')" | ||
| EXPORT_PNG = "button:has-text('Export PNG')" | ||
| EXPORT_PDF = "button:has-text('Export PDF')" | ||
|
|
||
| # Filters | ||
| METRICS_FILTER = ".metrics-filter" | ||
| FILTER_BY_NODE = ".filter-by-node" | ||
| FILTER_BY_TYPE = ".filter-by-type" | ||
| FILTER_BY_STATUS = ".filter-by-status" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| """Locators for Nodes page elements.""" | ||
|
|
||
|
|
||
| class NodesPageLocators: | ||
| """Locators specific to the Nodes page.""" | ||
|
|
||
| # Page wrapper | ||
| NODES_WRAPPER = ".nodes-wrapper" | ||
|
|
||
| # First login screen | ||
| NODES_FIRST_LOGIN = ".nodes-first-login" | ||
| NODES_TITLE = ".nodes-title" | ||
| NODES_SUBTITLE = ".nodes-subtitle" | ||
| NODES_DESCRIPTION = ".nodes-description" | ||
| NODES_HEADER = ".nodes-header" | ||
| NODES_HEADER_TEXT = ".nodes-header-text" | ||
|
|
||
| # Buttons | ||
| NODES_CREATE_BUTTON = ".nodes-create-button" | ||
| NODES_LINK_BUTTON = ".nodes-link-button" | ||
| PRIMARY_BUTTON = ".primary-button" | ||
|
|
||
| # Bottom cards | ||
| NODES_BOTTOM_CARDS = ".nodes-bottom-cards" | ||
| INFO_CARD = ".info-card" | ||
| INFO_CARD_HEADER = ".info-card-header" | ||
| INFO_CARD_TITLE = ".info-card-title" | ||
| INFO_CARD_ICON = ".info-card-icon" | ||
| INFO_CARD_TEXT = ".info-card-text" | ||
|
|
||
| # Specific buttons by text | ||
| @staticmethod | ||
| def button_by_text(text: str) -> str: | ||
| """Get button by text content.""" | ||
| return f"button:has-text('{text}')" | ||
|
|
||
| # Create node button | ||
| CREATE_NODE_BUTTON = "button:has-text('Create node')" | ||
| READ_MORE_BUTTON = "button:has-text('Read more about deploying nodes')" | ||
|
|
||
| # Info cards | ||
| LEARN_ABOUT_NODES_CARD = "button.info-card:has(.info-card-title:text('Learn about nodes'))" | ||
| DOCUMENTATION_CARD = "button.info-card:has(.info-card-title:text('Documentation'))" | ||
| SUPPORT_CARD = "button.info-card:has(.info-card-title:text('Support'))" | ||
|
|
||
|
|
||
| class NodesListLocators: | ||
| """Locators for nodes list view.""" | ||
|
|
||
| # List page | ||
| NODES_LIST_PAGE = ".nodes-list-page" | ||
| NODES_LIST_HEADER = ".nodes-list-header" | ||
| NODES_LIST_TITLE = ".nodes-list-title" | ||
|
|
||
| # Tabs | ||
| NODES_LIST_TABS = ".nodes-list-tabs" | ||
| NODES_LIST_TAB = ".nodes-list-tab" | ||
| NODES_LIST_TAB_ACTIVE = ".nodes-list-tab-active" | ||
|
|
||
| # Node items | ||
| NODE_ITEM = ".node-item" | ||
| NODE_ITEM_HEADER = ".node-item-header" | ||
| NODE_ITEM_TITLE = ".node-item-title" | ||
| NODE_ITEM_STATUS = ".node-item-status" | ||
| NODE_ITEM_ACTIONS = ".node-item-actions" | ||
|
|
||
| # Filters and search | ||
| NODES_SEARCH = ".nodes-search" | ||
| NODES_FILTER = ".nodes-filter" | ||
| NODES_SORT = ".nodes-sort" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| """Locators for Settings page elements.""" | ||
|
|
||
|
|
||
| class SettingsPageLocators: | ||
| """Locators specific to the Settings page.""" | ||
|
|
||
| # Page structure | ||
| SETTINGS_WRAPPER = ".nodes-list-page" | ||
| SETTINGS_HEADER = ".nodes-list-header" | ||
| SETTINGS_TITLE = ".nodes-list-title" | ||
|
|
||
| # Tabs | ||
| SETTINGS_TABS = ".nodes-list-tabs" | ||
| SETTINGS_TAB = ".nodes-list-tab" | ||
| SETTINGS_TAB_ACTIVE = ".nodes-list-tab-active" | ||
| PERSONAL_TAB = "button.nodes-list-tab:has-text('Personal')" | ||
|
|
||
| # Welcome/Settings section | ||
| WELCOME_RIGHT = ".welcome-right" | ||
| WELCOME_CARD = ".welcome-card" | ||
| WELCOME_CARD_TITLE = ".welcome-card-title" | ||
| WELCOME_CARD_SECTION = ".welcome-card-section" | ||
| WELCOME_CARD_SECTION_GRID = ".welcome-card-section-grid" | ||
| WELCOME_CARD_SECTION_TITLE = ".welcome-card-section-title" | ||
| WELCOME_CARD_SECTION_SUBTITLE = ".welcome-card-section-subtitle" | ||
|
|
||
| # Form fields | ||
| FORM_FIELD = ".form-field" | ||
| WELCOME_INPUT = ".welcome-input" | ||
| FORM_INPUT_WRAP = ".form-input-wrap" | ||
| FORM_INPUT = ".form-input" | ||
| FORM_FLOATING_LABEL = ".form-floating-label" | ||
| FORM_INPUT_HAS_VALUE = ".form-input.has-value" | ||
| FORM_LABEL_FLOATED = ".form-floating-label.is-floated" | ||
|
|
||
| # Specific input fields | ||
| USERNAME_INPUT = "input[autocomplete='username']" | ||
| EMAIL_INPUT = "input[autocomplete='email']" | ||
| PASSWORD_INPUT = "input[type='password']" | ||
|
|
||
| # Buttons | ||
| SAVE_BUTTON = "button:has-text('Save')" | ||
| CANCEL_BUTTON = "button:has-text('Cancel')" | ||
| CHANGE_PASSWORD_BUTTON = "button:has-text('Change password')" | ||
|
|
||
| # Sections by title | ||
| @staticmethod | ||
| def section_by_title(title: str) -> str: | ||
| """Get section by title text.""" | ||
| return f".welcome-card:has(.welcome-card-title:text('{title}'))" | ||
|
|
||
| PERSONAL_INFO_SECTION = ".welcome-card:has(.welcome-card-title:text('Personal Information'))" | ||
| SECURITY_SECTION = ".welcome-card:has(.welcome-card-title:text('Security'))" | ||
| PREFERENCES_SECTION = ".welcome-card:has(.welcome-card-title:text('Preferences'))" | ||
|
|
||
| # Input by placeholder | ||
| @staticmethod | ||
| def input_by_placeholder(placeholder: str) -> str: | ||
| """Get input field by placeholder text on parent form-field.""" | ||
| return f".form-field[placeholder='{placeholder}'] .form-input" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems still incorrect. The placeholder attribute belongs to elements, not to .form-field wrapper. This selector will never match anything.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, because actual input has |
||
|
|
||
| @staticmethod | ||
| def input_by_label(label: str) -> str: | ||
| """Get input field by label text.""" | ||
| return f".form-field:has(label:text('{label}')) input" | ||
|
|
||
|
|
||
| class SettingsFormLocators: | ||
| """Locators for form elements in Settings.""" | ||
|
|
||
| # Form validation | ||
| FORM_ERROR = ".form-error" | ||
| FORM_SUCCESS = ".form-success" | ||
| FORM_HELPER_TEXT = ".form-helper-text" | ||
|
|
||
| # Input states | ||
| INPUT_INVALID = "input[aria-invalid='true']" | ||
| INPUT_VALID = "input[aria-invalid='false']" | ||
| INPUT_DISABLED = "input:disabled" | ||
| INPUT_READONLY = "input:read-only" | ||
|
|
||
| # Field groups | ||
| FIELD_GROUP = ".field-group" | ||
| FIELD_ROW = ".field-row" | ||
| FIELD_COLUMN = ".field-column" | ||
|
|
||
| # Action buttons | ||
| SUBMIT_BUTTON = "button[type='submit']" | ||
| RESET_BUTTON = "button[type='reset']" | ||
|
|
||
| # Modals/Dialogs | ||
| MODAL = ".modal" | ||
| MODAL_HEADER = ".modal-header" | ||
| MODAL_BODY = ".modal-body" | ||
| MODAL_FOOTER = ".modal-footer" | ||
| MODAL_CLOSE = ".modal-close" | ||
|
|
||
| # Confirmation dialogs | ||
| CONFIRM_DIALOG = ".confirm-dialog" | ||
| CONFIRM_YES = "button:has-text('Yes')" | ||
| CONFIRM_NO = "button:has-text('No')" | ||
| CONFIRM_OK = "button:has-text('OK')" | ||
| CONFIRM_CANCEL = "button:has-text('Cancel')" | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fragile selector:
data-v-*attributes are auto-generated by Vue.MONITORING_WRAPPER = "div[data-v-3904b986]"uses a Vue scoped CSS hash that will change when the component is recompiled. This makes the locator brittle and likely to break across builds.Consider using a stable selector such as a semantic class name,
data-testidattribute, or another structural selector.🐛 Suggested fix
🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All selectors will be revised once we have a stable UI without many fixes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@0xc7c5, understood! That makes sense—it's practical to wait until the UI is more stable before investing time in refactoring all the selectors. Feel free to revisit this when you're ready to lock down the locator strategy. 👍