codemie-test-harness 0.1.205__py3-none-any.whl → 0.1.207__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of codemie-test-harness might be problematic. Click here for more details.
- codemie_test_harness/tests/assistant/datasource/test_code_datasource.py +2 -2
- codemie_test_harness/tests/assistant/tools/git/test_assistant_with_git_tools.py +14 -14
- codemie_test_harness/tests/conftest.py +34 -12
- codemie_test_harness/tests/e2e/test_e2e.py +2 -2
- codemie_test_harness/tests/ui/assistants/test_create_assistant.py +13 -13
- codemie_test_harness/tests/ui/assistants/test_edit_assistant.py +200 -0
- codemie_test_harness/tests/ui/datasource/test_create_datasource.py +2 -2
- codemie_test_harness/tests/ui/datasource/test_edit_datasource.py +2 -2
- codemie_test_harness/tests/ui/datasource/test_view_datasource.py +2 -2
- codemie_test_harness/tests/ui/pageobject/assistants/assistant_mcp_server.py +171 -0
- codemie_test_harness/tests/ui/pageobject/assistants/assistant_sidebar.py +140 -0
- codemie_test_harness/tests/ui/pageobject/assistants/assistant_view_page.py +256 -0
- codemie_test_harness/tests/ui/pageobject/assistants/assistants_page.py +63 -0
- codemie_test_harness/tests/ui/pageobject/assistants/{create_assistant_page.py → create_edit_assistant_page.py} +379 -95
- codemie_test_harness/tests/ui/test_data/assistant_test_data.py +347 -18
- codemie_test_harness/tests/utils/constants.py +1 -1
- codemie_test_harness/tests/utils/webhook_utils.py +10 -0
- codemie_test_harness/tests/webhook/__init__.py +0 -0
- codemie_test_harness/tests/webhook/test_webhook_service.py +225 -0
- codemie_test_harness/tests/workflow/assistant_tools/git/test_workflow_with_assistant_git_tools.py +14 -14
- codemie_test_harness/tests/workflow/virtual_assistant_tools/git/test_workflow_with_git_tools.py +14 -14
- {codemie_test_harness-0.1.205.dist-info → codemie_test_harness-0.1.207.dist-info}/METADATA +2 -2
- {codemie_test_harness-0.1.205.dist-info → codemie_test_harness-0.1.207.dist-info}/RECORD +25 -18
- {codemie_test_harness-0.1.205.dist-info → codemie_test_harness-0.1.207.dist-info}/WHEEL +0 -0
- {codemie_test_harness-0.1.205.dist-info → codemie_test_harness-0.1.207.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
from playwright.sync_api import Page, Locator, expect
|
|
2
|
+
from reportportal_client import step
|
|
3
|
+
from tests.ui.test_data.assistant_test_data import get_minimal_assistant_mcp_config_data
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AssistantMCPIntegrationModal:
|
|
7
|
+
def __init__(self, page: Page):
|
|
8
|
+
self.page = page
|
|
9
|
+
|
|
10
|
+
@property
|
|
11
|
+
def mcp_pop_up(self):
|
|
12
|
+
"""Main dialog container using p-dialog class"""
|
|
13
|
+
return self.page.locator("div.p-dialog[role='dialog']").filter(
|
|
14
|
+
has_text="Add new MCP server"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
def mcp_pop_up_header(self):
|
|
18
|
+
"""Dialog header with 'Add new MCP server' text"""
|
|
19
|
+
return self.mcp_pop_up.locator(
|
|
20
|
+
"h4.text-base.font-semibold:has-text('Add new MCP server')"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# --- Name input ---
|
|
24
|
+
@property
|
|
25
|
+
def name_input(self) -> Locator:
|
|
26
|
+
return self.mcp_pop_up.locator("input[name='name'][placeholder='Name*']")
|
|
27
|
+
|
|
28
|
+
# --- Description textarea ---
|
|
29
|
+
@property
|
|
30
|
+
def description_textarea(self) -> Locator:
|
|
31
|
+
return self.mcp_pop_up.locator(
|
|
32
|
+
"textarea[name='description'][placeholder='Description*']"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# --- Tools Tokens Size Limit input ---
|
|
36
|
+
@property
|
|
37
|
+
def tokens_size_limit_input(self) -> Locator:
|
|
38
|
+
return self.mcp_pop_up.locator("input[name='tokensSizeLimit'][type='number']")
|
|
39
|
+
|
|
40
|
+
# --- Configuration mode select (JSON/Form) ---
|
|
41
|
+
@property
|
|
42
|
+
def config_mode_switch(self) -> Locator:
|
|
43
|
+
return self.mcp_pop_up.locator("div.p-selectbutton.p-button-group")
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def config_mode_json_radio(self) -> Locator:
|
|
47
|
+
return self.config_mode_switch.locator(
|
|
48
|
+
"div.p-button[role='button']:has(span:has-text('JSON'))"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def config_mode_form_radio(self) -> Locator:
|
|
53
|
+
return self.config_mode_switch.locator(
|
|
54
|
+
"div.p-button[role='button']:has(span:has-text('Form'))"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# --- Configuration (JSON) textarea ---
|
|
58
|
+
@property
|
|
59
|
+
def configuration_json_textarea(self) -> Locator:
|
|
60
|
+
return self.mcp_pop_up.locator("textarea[name='configJson']#json-config")
|
|
61
|
+
|
|
62
|
+
# --- Environment Variables dropdown ---
|
|
63
|
+
@property
|
|
64
|
+
def env_vars_dropdown(self) -> Locator:
|
|
65
|
+
return self.mcp_pop_up.locator(
|
|
66
|
+
"div.p-dropdown:has(span.p-dropdown-label:text('Environment Variables'))"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# --- MCP-Connect URL input ---
|
|
70
|
+
@property
|
|
71
|
+
def mcp_connect_url_input(self) -> Locator:
|
|
72
|
+
return self.mcp_pop_up.locator(
|
|
73
|
+
"input[name='connectUrl'][placeholder='https://']"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# --- Buttons ---
|
|
77
|
+
@property
|
|
78
|
+
def cancel_button(self) -> Locator:
|
|
79
|
+
return self.mcp_pop_up.locator(
|
|
80
|
+
"button.bg-button-secondary-bg:has-text('Cancel')"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def test_integration_button(self) -> Locator:
|
|
85
|
+
return self.mcp_pop_up.locator(
|
|
86
|
+
"button.bg-button-secondary-bg:has-text('Test Integration')"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def add_button(self) -> Locator:
|
|
91
|
+
return self.mcp_pop_up.locator("button.bg-button-primary-bg:has-text('Add')")
|
|
92
|
+
|
|
93
|
+
# ---- Action Methods ----
|
|
94
|
+
|
|
95
|
+
@step
|
|
96
|
+
def fill_name(self, value: str):
|
|
97
|
+
self.name_input.fill(value)
|
|
98
|
+
|
|
99
|
+
@step
|
|
100
|
+
def fill_description(self, value: str):
|
|
101
|
+
self.description_textarea.fill(value)
|
|
102
|
+
|
|
103
|
+
@step
|
|
104
|
+
def fill_tokens_size_limit(self, value: int):
|
|
105
|
+
self.tokens_size_limit_input.fill(str(value))
|
|
106
|
+
|
|
107
|
+
@step
|
|
108
|
+
def select_config_mode(self, mode: str):
|
|
109
|
+
if mode.lower() == "json":
|
|
110
|
+
self.config_mode_json_radio.click()
|
|
111
|
+
elif mode.lower() == "form":
|
|
112
|
+
self.config_mode_form_radio.click()
|
|
113
|
+
else:
|
|
114
|
+
raise ValueError("Mode must be 'json' or 'form'")
|
|
115
|
+
|
|
116
|
+
@step
|
|
117
|
+
def fill_configuration_json(self, value: str):
|
|
118
|
+
self.configuration_json_textarea.fill(value)
|
|
119
|
+
|
|
120
|
+
@step
|
|
121
|
+
def open_env_vars_dropdown(self):
|
|
122
|
+
self.env_vars_dropdown.click()
|
|
123
|
+
|
|
124
|
+
@step
|
|
125
|
+
def fill_mcp_connect_url(self, value: str):
|
|
126
|
+
self.mcp_connect_url_input.fill(value)
|
|
127
|
+
|
|
128
|
+
@step
|
|
129
|
+
def click_cancel(self):
|
|
130
|
+
self.cancel_button.click()
|
|
131
|
+
|
|
132
|
+
@step
|
|
133
|
+
def click_test_integration(self):
|
|
134
|
+
self.test_integration_button.click()
|
|
135
|
+
|
|
136
|
+
@step
|
|
137
|
+
def click_add(self):
|
|
138
|
+
self.add_button.click()
|
|
139
|
+
|
|
140
|
+
@step
|
|
141
|
+
def fill_mcp_server_base_form(self):
|
|
142
|
+
test_data = get_minimal_assistant_mcp_config_data()
|
|
143
|
+
self.fill_name(test_data.name)
|
|
144
|
+
self.fill_description(test_data.description)
|
|
145
|
+
self.fill_configuration_json(test_data.configuration)
|
|
146
|
+
self.click_add()
|
|
147
|
+
|
|
148
|
+
# --- Assertions (optional helpers) ---
|
|
149
|
+
|
|
150
|
+
def is_pop_visible(self):
|
|
151
|
+
return self.mcp_pop_up_header().count() > 0
|
|
152
|
+
|
|
153
|
+
@step
|
|
154
|
+
def should_see_name(self, name: str):
|
|
155
|
+
expect(self.name_input).to_have_value(name)
|
|
156
|
+
|
|
157
|
+
@step
|
|
158
|
+
def should_see_description(self, description: str):
|
|
159
|
+
expect(self.description_textarea).to_have_value(description)
|
|
160
|
+
|
|
161
|
+
@step
|
|
162
|
+
def should_see_tokens_size_limit(self, token_size: str):
|
|
163
|
+
expect(self.tokens_size_limit_input).to_have_value(token_size)
|
|
164
|
+
|
|
165
|
+
@step
|
|
166
|
+
def should_see_configuration_json(self, config: str):
|
|
167
|
+
expect(self.configuration_json_textarea).to_have_value(config)
|
|
168
|
+
|
|
169
|
+
@step
|
|
170
|
+
def should_see_mcp_connect_url(self, url: str):
|
|
171
|
+
expect(self.mcp_connect_url_input).to_have_value(url)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from playwright.sync_api import Page, Locator
|
|
2
|
+
from reportportal_client import step
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AssistantSidebar:
|
|
6
|
+
"""Page Object Model for the Assistants Page Sidebar."""
|
|
7
|
+
|
|
8
|
+
def __init__(self, page: Page):
|
|
9
|
+
self.page = page
|
|
10
|
+
|
|
11
|
+
# --- Locators ---
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def main_sidebar(self) -> Locator:
|
|
15
|
+
"""The sidebar <aside>."""
|
|
16
|
+
return self.page.locator("aside.flex.flex-col.border-r.bg-sidebar-gradient")
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def title(self) -> Locator:
|
|
20
|
+
"""Sidebar title: 'Assistants'."""
|
|
21
|
+
return self.main_sidebar.locator("h2:text-is('Assistants')")
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def subtitle(self) -> Locator:
|
|
25
|
+
"""Sidebar subtitle paragraph."""
|
|
26
|
+
return self.main_sidebar.locator("p.text-sm.text-text-secondary")
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def categories_label(self) -> Locator:
|
|
30
|
+
"""The 'Categories' label/heading."""
|
|
31
|
+
return self.main_sidebar.locator("span:text-is('Categories')")
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def categories_list(self) -> Locator:
|
|
35
|
+
"""<ul> under 'Categories'."""
|
|
36
|
+
return self.categories_label.locator("xpath=../../ul")
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def category_items(self) -> Locator:
|
|
40
|
+
"""All category <li> elements."""
|
|
41
|
+
return self.categories_list.locator("li")
|
|
42
|
+
|
|
43
|
+
def category_button(self, name: str) -> Locator:
|
|
44
|
+
"""Button for a category by visible text."""
|
|
45
|
+
return self.categories_list.locator(f"button:has-text('{name}')")
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def filters_label(self) -> Locator:
|
|
49
|
+
"""'Filters' span above filters section."""
|
|
50
|
+
return self.main_sidebar.locator("span:text-is('Filters')")
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def clear_all_filters_button(self) -> Locator:
|
|
54
|
+
"""'Clear all' filter reset button."""
|
|
55
|
+
return self.main_sidebar.locator("button:has-text('Clear all')")
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def search_input(self) -> Locator:
|
|
59
|
+
"""Filter/search input."""
|
|
60
|
+
return self.main_sidebar.locator("input[placeholder='Search']")
|
|
61
|
+
|
|
62
|
+
# Accordion and filter options are unique/complex:
|
|
63
|
+
def accordion_tab(self, tab_title: str) -> Locator:
|
|
64
|
+
"""Accordion tab by title, e.g. 'PROJECT', 'CREATED BY', ..."""
|
|
65
|
+
return self.main_sidebar.locator(
|
|
66
|
+
f".p-accordion-header-text span:has-text('{tab_title.upper()}')"
|
|
67
|
+
).locator("xpath=../../..")
|
|
68
|
+
|
|
69
|
+
def multiselect_filter(self, label: str) -> Locator:
|
|
70
|
+
"""Filter dropdown by its label in filter accordion, e.g. 'Project'."""
|
|
71
|
+
return self.main_sidebar.locator(f".p-multiselect-label:has-text('{label}')")
|
|
72
|
+
|
|
73
|
+
def created_by_input(self) -> Locator:
|
|
74
|
+
"""Input for 'Created By' filter."""
|
|
75
|
+
return self.main_sidebar.locator("input[placeholder='Created By']")
|
|
76
|
+
|
|
77
|
+
def radio_filter_option(self, label: str) -> Locator:
|
|
78
|
+
"""Radio 'Shared' filter options like All/With Project/Not Shared."""
|
|
79
|
+
return (
|
|
80
|
+
self.main_sidebar.locator(f"label.flex.items-center span:text('{label}')")
|
|
81
|
+
.locator("xpath=..")
|
|
82
|
+
.locator("input[type='radio']")
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# --- Methods ---
|
|
86
|
+
|
|
87
|
+
@step
|
|
88
|
+
def click_category(self, name: str):
|
|
89
|
+
"""Clicks on a category item by label."""
|
|
90
|
+
self.category_button(name).click()
|
|
91
|
+
|
|
92
|
+
@step
|
|
93
|
+
def select_filter_tab(self, tab_title: str):
|
|
94
|
+
"""Expands a filter accordion tab by label."""
|
|
95
|
+
tab = self.accordion_tab(tab_title)
|
|
96
|
+
tab.locator("a.p-accordion-header-link").click()
|
|
97
|
+
|
|
98
|
+
@step
|
|
99
|
+
def clear_filters(self):
|
|
100
|
+
"""Clicks the 'Clear all' button in filters."""
|
|
101
|
+
self.clear_all_filters_button.click()
|
|
102
|
+
|
|
103
|
+
@step
|
|
104
|
+
def search(self, text: str):
|
|
105
|
+
"""Sets sidebar search/filter."""
|
|
106
|
+
self.search_input.fill(text)
|
|
107
|
+
|
|
108
|
+
@step
|
|
109
|
+
def select_multiselect_option(self, label: str):
|
|
110
|
+
"""Clicks on a filter dropdown and selects the label (if supported)."""
|
|
111
|
+
ms = self.multiselect_filter(label)
|
|
112
|
+
ms.click()
|
|
113
|
+
ms.locator(f"..//li[.='{label}']").click()
|
|
114
|
+
|
|
115
|
+
@step
|
|
116
|
+
def set_created_by(self, author: str):
|
|
117
|
+
"""Sets the 'Created By' filter text."""
|
|
118
|
+
input_field = self.created_by_input()
|
|
119
|
+
input_field.fill(author)
|
|
120
|
+
|
|
121
|
+
@step
|
|
122
|
+
def select_radio_option(self, option_label: str):
|
|
123
|
+
"""Selects a radio option for shared status."""
|
|
124
|
+
self.radio_filter_option(option_label).check(force=True)
|
|
125
|
+
|
|
126
|
+
@step
|
|
127
|
+
def visible_categories(self):
|
|
128
|
+
"""Returns the names of all visible categories."""
|
|
129
|
+
return [
|
|
130
|
+
self.category_items.nth(i).inner_text().strip()
|
|
131
|
+
for i in range(self.category_items.count())
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
@step
|
|
135
|
+
def visible_filter_radios(self):
|
|
136
|
+
"""Returns the text values of radio options in Shared filter."""
|
|
137
|
+
# Find all labels within the 'Shared' tab
|
|
138
|
+
tab = self.accordion_tab("SHARED")
|
|
139
|
+
radios = tab.locator("label.flex.items-center")
|
|
140
|
+
return [radios.nth(i).inner_text().strip() for i in range(radios.count())]
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
from hamcrest import assert_that, contains_string
|
|
2
|
+
from playwright.sync_api import Page, expect
|
|
3
|
+
from reportportal_client import step
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AssistantViewPage:
|
|
7
|
+
def __init__(self, page: Page):
|
|
8
|
+
self.page = page
|
|
9
|
+
|
|
10
|
+
# ----------------
|
|
11
|
+
# --- LOCATORS ---
|
|
12
|
+
# ----------------
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def assistant_name(self):
|
|
16
|
+
return self.page.locator("h4.name-target")
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def assistant_author(self):
|
|
20
|
+
return self.page.locator(".text-xs.text-text-secondary > p")
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def about_heading(self):
|
|
24
|
+
return self.page.locator("h5.font-bold.text-sm:text('About Assistant:')")
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def about_content(self):
|
|
28
|
+
# Paragraph under "About Assistant:"
|
|
29
|
+
return self.about_heading.locator("xpath=../p")
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def system_instructions_heading(self):
|
|
33
|
+
return self.page.locator(
|
|
34
|
+
"div.flex.bg-new-panel.border.rounded-lg [class*='text-xs']:text('System Instructions')"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def system_instructions_content(self):
|
|
39
|
+
return self.page.locator("div.flex.bg-new-panel.border.rounded-lg > p.text-sm")
|
|
40
|
+
|
|
41
|
+
# --- Overview Block ---
|
|
42
|
+
@property
|
|
43
|
+
def overview_block(self):
|
|
44
|
+
return self.page.locator("p:text-is('OVERVIEW')").locator("xpath=..")
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def overview_project(self):
|
|
48
|
+
return self.overview_block.locator(
|
|
49
|
+
"div.flex:has(p.text-text-tertiary:text('Project:')) > p:not(.text-text-tertiary)"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def overview_shared_status(self):
|
|
54
|
+
return self.overview_block.locator(
|
|
55
|
+
"div.flex:has(p.text-text-tertiary:text('Shared status:')) > p:not(.text-text-tertiary)"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def overview_assistant_id(self):
|
|
60
|
+
return self.overview_block.locator(
|
|
61
|
+
"div.flex.flex-col.gap-2.mt-2.font-semibold input[readonly]"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# --- Links Block ---
|
|
65
|
+
@property
|
|
66
|
+
def access_links_block(self):
|
|
67
|
+
return self.page.locator(
|
|
68
|
+
"p.text-xs.text-text-main.font-semibold:text('ACCESS LINKS')"
|
|
69
|
+
).locator("xpath=..")
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def details_link_input(self):
|
|
73
|
+
return self.access_links_block.locator(
|
|
74
|
+
"div.flex.flex-col.gap-2:has(p:text('Link to assistant details:')) input[readonly]"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def chat_link_input(self):
|
|
79
|
+
return self.access_links_block.locator(
|
|
80
|
+
"div.flex.flex-col.gap-2:has(p:text('Link to start a chat')) input[readonly]"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# --- Configuration ---
|
|
84
|
+
@property
|
|
85
|
+
def configuration_block(self):
|
|
86
|
+
return self.page.locator(
|
|
87
|
+
"p.text-xs.text-text-main.font-semibold:text('CONFIGURATION')"
|
|
88
|
+
).locator("xpath=..")
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def config_llm_model(self):
|
|
92
|
+
return self.configuration_block.locator("p:text('LLM model:')").locator(
|
|
93
|
+
"xpath=../div"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def config_temperature(self):
|
|
98
|
+
return self.configuration_block.locator("p:text('Temperature:')").locator(
|
|
99
|
+
"xpath=../div"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def config_top_p(self):
|
|
104
|
+
return self.configuration_block.locator("p:text('Top P:')").locator(
|
|
105
|
+
"xpath=../div"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def config_additional_datasource_context(self):
|
|
110
|
+
return self.configuration_block.locator(
|
|
111
|
+
"div:has(p:text('Additional datasource context')) div.bg-new-panel.py-1\\.5.px-2"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# --- Tools & Capabilities ---
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def tools_block(self):
|
|
118
|
+
"""Block containing all toolkits under 'TOOLS & CAPABILITIES'."""
|
|
119
|
+
return self.page.locator(
|
|
120
|
+
"p.text-xs.text-text-main.font-semibold:text('TOOLS & CAPABILITIES')"
|
|
121
|
+
).locator("xpath=..")
|
|
122
|
+
|
|
123
|
+
def toolkit_block(self, toolkit_name: str):
|
|
124
|
+
"""Returns the block for the toolkit (e.g., 'Git', 'VCS', etc.)."""
|
|
125
|
+
return self.tools_block.locator(
|
|
126
|
+
f"div.text-xs.flex.flex-col.gap-2:has(p.text-sm:has-text('{toolkit_name}'))"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def toolkit_tool_labels(self, toolkit_name: str):
|
|
130
|
+
"""Returns elements for all tool labels under a given toolkit block."""
|
|
131
|
+
return self.toolkit_block(toolkit_name).locator(
|
|
132
|
+
"div.flex.flex-wrap.gap-2 > div"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def toolkit_tool_label(self, toolkit_name: str, tool_label: str):
|
|
136
|
+
"""Returns the div for a specific tool label (exact match, trimmed)."""
|
|
137
|
+
return self.toolkit_block(toolkit_name).locator(
|
|
138
|
+
f"div.flex.flex-wrap.gap-2 > div:text-is('{tool_label}')"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# --------------------------
|
|
142
|
+
# --- EXPECT/VERIFY ---
|
|
143
|
+
# --------------------------
|
|
144
|
+
|
|
145
|
+
@step
|
|
146
|
+
def should_have_all_form_fields_visible(
|
|
147
|
+
self, name: str, author: str, description: str
|
|
148
|
+
):
|
|
149
|
+
expect(self.assistant_name).to_be_visible()
|
|
150
|
+
expect(self.assistant_name).to_have_text(name)
|
|
151
|
+
expect(self.assistant_author).to_be_visible()
|
|
152
|
+
expect(self.assistant_author).to_have_text(f"by {author}")
|
|
153
|
+
expect(self.about_content).to_be_visible()
|
|
154
|
+
expect(self.about_content).to_have_text(description)
|
|
155
|
+
expect(self.system_instructions_content).to_be_visible()
|
|
156
|
+
return self
|
|
157
|
+
|
|
158
|
+
@step
|
|
159
|
+
def should_have_overview_form_fields_visible(
|
|
160
|
+
self, project: str, status: str, assistant_id: str
|
|
161
|
+
):
|
|
162
|
+
expect(self.overview_project).to_be_visible()
|
|
163
|
+
expect(self.overview_project).to_have_text(project)
|
|
164
|
+
expect(self.overview_shared_status).to_be_visible()
|
|
165
|
+
expect(self.overview_shared_status).to_have_text(status)
|
|
166
|
+
expect(self.overview_assistant_id).to_be_visible()
|
|
167
|
+
expect(self.overview_assistant_id).to_have_value(assistant_id)
|
|
168
|
+
return self
|
|
169
|
+
|
|
170
|
+
@step
|
|
171
|
+
def should_have_access_links_form_fields_visible(
|
|
172
|
+
self, assistant_id: str, assistant_name: str
|
|
173
|
+
):
|
|
174
|
+
expect(self.details_link_input).to_be_visible()
|
|
175
|
+
assert_that(
|
|
176
|
+
self.details_link_input.input_value(), contains_string(assistant_id)
|
|
177
|
+
)
|
|
178
|
+
expect(self.chat_link_input).to_be_visible()
|
|
179
|
+
assert_that(self.chat_link_input.input_value(), contains_string(assistant_name))
|
|
180
|
+
return self
|
|
181
|
+
|
|
182
|
+
@step
|
|
183
|
+
def should_have_configuration_form_fields_visible(
|
|
184
|
+
self, temperature: str, top_p: str
|
|
185
|
+
):
|
|
186
|
+
expect(self.config_temperature).to_be_visible()
|
|
187
|
+
expect(self.config_temperature).to_have_text(temperature)
|
|
188
|
+
expect(self.config_top_p).to_be_visible()
|
|
189
|
+
expect(self.config_top_p).to_have_text(top_p)
|
|
190
|
+
return self
|
|
191
|
+
|
|
192
|
+
@step
|
|
193
|
+
def should_see_assistant_name(self, expected):
|
|
194
|
+
expect(self.assistant_name).to_have_text(expected)
|
|
195
|
+
|
|
196
|
+
@step
|
|
197
|
+
def should_see_assistant_author(self, expected):
|
|
198
|
+
expect(self.assistant_author).to_have_text(expected)
|
|
199
|
+
|
|
200
|
+
@step
|
|
201
|
+
def should_see_about_content(self, expected):
|
|
202
|
+
expect(self.about_content).to_have_text(expected)
|
|
203
|
+
|
|
204
|
+
@step
|
|
205
|
+
def should_see_system_instructions(self, expected):
|
|
206
|
+
expect(self.system_instructions_content).to_have_text(expected)
|
|
207
|
+
|
|
208
|
+
@step
|
|
209
|
+
def should_see_overview_project(self, expected):
|
|
210
|
+
expect(self.overview_project()).to_have_text(expected)
|
|
211
|
+
|
|
212
|
+
@step
|
|
213
|
+
def should_see_overview_shared_status(self, expected):
|
|
214
|
+
expect(self.overview_shared_status()).to_have_text(expected)
|
|
215
|
+
|
|
216
|
+
@step
|
|
217
|
+
def should_see_assistant_id(self, expected):
|
|
218
|
+
expect(self.assistant_id_value).to_have_value(expected)
|
|
219
|
+
|
|
220
|
+
@step
|
|
221
|
+
def should_see_links(self, details_link, chat_link):
|
|
222
|
+
expect(self.details_link_input()).to_have_value(details_link)
|
|
223
|
+
expect(self.chat_link_input()).to_have_value(chat_link)
|
|
224
|
+
|
|
225
|
+
@step
|
|
226
|
+
def should_see_config_llm_model(self, expected):
|
|
227
|
+
expect(self.config_llm_model()).to_have_text(expected)
|
|
228
|
+
|
|
229
|
+
@step
|
|
230
|
+
def should_see_config_temperature(self, expected):
|
|
231
|
+
expect(self.config_temperature()).to_have_text(expected)
|
|
232
|
+
|
|
233
|
+
@step
|
|
234
|
+
def should_see_config_top_p(self, expected):
|
|
235
|
+
expect(self.config_top_p()).to_have_text(expected)
|
|
236
|
+
|
|
237
|
+
@step
|
|
238
|
+
def should_see_toolkit_visible(self, toolkit_name: str):
|
|
239
|
+
"""Assert that the toolkit with the given name is visible."""
|
|
240
|
+
expect(self.toolkit_block(toolkit_name)).to_be_visible()
|
|
241
|
+
|
|
242
|
+
@step
|
|
243
|
+
def should_see_toolkit_contains(self, toolkit_name: str, tool_label: str):
|
|
244
|
+
"""Assert a toolkit contains a tool label (visible)."""
|
|
245
|
+
expect(self.toolkit_tool_label(toolkit_name, tool_label)).to_be_visible()
|
|
246
|
+
|
|
247
|
+
@step
|
|
248
|
+
def should_see_tool_not_present(self, toolkit_name: str, tool_label: str):
|
|
249
|
+
"""Assert a toolkit does NOT contain a tool label."""
|
|
250
|
+
expect(self.toolkit_tool_label(toolkit_name, tool_label)).not_to_be_visible()
|
|
251
|
+
|
|
252
|
+
@step
|
|
253
|
+
def should_see_toolkit_tools(self, toolkit_name: str, expected_tools: list):
|
|
254
|
+
"""Assert all expected tool labels are present in a toolkit."""
|
|
255
|
+
for tool in expected_tools:
|
|
256
|
+
self.expect_toolkit_contains(toolkit_name, tool)
|
|
@@ -78,6 +78,41 @@ class AssistantsPage(BasePage):
|
|
|
78
78
|
"""Clear all filters button."""
|
|
79
79
|
return self.page.locator('button:has-text("Clear all")')
|
|
80
80
|
|
|
81
|
+
@property
|
|
82
|
+
def action_view_details(self):
|
|
83
|
+
"""Dropdown 'View Details' button."""
|
|
84
|
+
return self.page.locator("button").filter(has_text="View Details")
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def action_copy_link(self):
|
|
88
|
+
"""Dropdown 'Copy Link' button."""
|
|
89
|
+
return self.page.locator("button").filter(has_text="Copy Link")
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def action_edit(self):
|
|
93
|
+
"""Dropdown 'Edit' button."""
|
|
94
|
+
return self.page.locator("button").filter(has_text="Edit")
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def action_clone(self):
|
|
98
|
+
"""Dropdown 'Clone' button."""
|
|
99
|
+
return self.page.locator("button").filter(has_text="Clone")
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def action_delete(self):
|
|
103
|
+
"""Dropdown 'Delete' button."""
|
|
104
|
+
return self.page.locator("button").filter(has_text="Delete")
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def action_publish_to_marketplace(self):
|
|
108
|
+
"""Dropdown 'Publish to Marketplace' button."""
|
|
109
|
+
return self.page.locator("button").filter(has_text="Publish to Marketplace")
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def updating_succesful_popup(self):
|
|
113
|
+
"""Updating succesful popup."""
|
|
114
|
+
return self.page.locator(".codemie-toast .codemie-toast-header")
|
|
115
|
+
|
|
81
116
|
# Navigation methods
|
|
82
117
|
@step
|
|
83
118
|
def navigate_to(self):
|
|
@@ -139,6 +174,27 @@ class AssistantsPage(BasePage):
|
|
|
139
174
|
self.get_assistant_card_by_name(name).click()
|
|
140
175
|
return self
|
|
141
176
|
|
|
177
|
+
@step
|
|
178
|
+
def action_dropdown_panel(self, name: str):
|
|
179
|
+
"""Three dot menu."""
|
|
180
|
+
return self.get_assistant_card_by_name(name).locator(
|
|
181
|
+
"div.flex.items-center.relative"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
@step
|
|
185
|
+
def click_assistant_view(self, name: str):
|
|
186
|
+
"""Click on an assistant view by name."""
|
|
187
|
+
self.action_dropdown_panel(name).click()
|
|
188
|
+
self.action_view_details.click()
|
|
189
|
+
return self
|
|
190
|
+
|
|
191
|
+
@step
|
|
192
|
+
def click_assistant_edit(self, name: str):
|
|
193
|
+
"""Click on an assistant edit by name."""
|
|
194
|
+
self.action_dropdown_panel(name).click()
|
|
195
|
+
self.action_edit.click()
|
|
196
|
+
return self
|
|
197
|
+
|
|
142
198
|
# Verification methods
|
|
143
199
|
@step
|
|
144
200
|
def should_be_on_assistants_page(self):
|
|
@@ -195,3 +251,10 @@ class AssistantsPage(BasePage):
|
|
|
195
251
|
"""Verify that an assistant with specific name is not visible."""
|
|
196
252
|
expect(self.get_assistant_card_by_name(name)).to_be_hidden()
|
|
197
253
|
return self
|
|
254
|
+
|
|
255
|
+
@step
|
|
256
|
+
def should_see_updating_popup(self, text: str):
|
|
257
|
+
"""Verify that an update popup is visible."""
|
|
258
|
+
expect(self.updating_succesful_popup).to_be_visible()
|
|
259
|
+
expect(self.updating_succesful_popup).to_have_text(text)
|
|
260
|
+
return self
|