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.

Files changed (25) hide show
  1. codemie_test_harness/tests/assistant/datasource/test_code_datasource.py +2 -2
  2. codemie_test_harness/tests/assistant/tools/git/test_assistant_with_git_tools.py +14 -14
  3. codemie_test_harness/tests/conftest.py +34 -12
  4. codemie_test_harness/tests/e2e/test_e2e.py +2 -2
  5. codemie_test_harness/tests/ui/assistants/test_create_assistant.py +13 -13
  6. codemie_test_harness/tests/ui/assistants/test_edit_assistant.py +200 -0
  7. codemie_test_harness/tests/ui/datasource/test_create_datasource.py +2 -2
  8. codemie_test_harness/tests/ui/datasource/test_edit_datasource.py +2 -2
  9. codemie_test_harness/tests/ui/datasource/test_view_datasource.py +2 -2
  10. codemie_test_harness/tests/ui/pageobject/assistants/assistant_mcp_server.py +171 -0
  11. codemie_test_harness/tests/ui/pageobject/assistants/assistant_sidebar.py +140 -0
  12. codemie_test_harness/tests/ui/pageobject/assistants/assistant_view_page.py +256 -0
  13. codemie_test_harness/tests/ui/pageobject/assistants/assistants_page.py +63 -0
  14. codemie_test_harness/tests/ui/pageobject/assistants/{create_assistant_page.py → create_edit_assistant_page.py} +379 -95
  15. codemie_test_harness/tests/ui/test_data/assistant_test_data.py +347 -18
  16. codemie_test_harness/tests/utils/constants.py +1 -1
  17. codemie_test_harness/tests/utils/webhook_utils.py +10 -0
  18. codemie_test_harness/tests/webhook/__init__.py +0 -0
  19. codemie_test_harness/tests/webhook/test_webhook_service.py +225 -0
  20. codemie_test_harness/tests/workflow/assistant_tools/git/test_workflow_with_assistant_git_tools.py +14 -14
  21. codemie_test_harness/tests/workflow/virtual_assistant_tools/git/test_workflow_with_git_tools.py +14 -14
  22. {codemie_test_harness-0.1.205.dist-info → codemie_test_harness-0.1.207.dist-info}/METADATA +2 -2
  23. {codemie_test_harness-0.1.205.dist-info → codemie_test_harness-0.1.207.dist-info}/RECORD +25 -18
  24. {codemie_test_harness-0.1.205.dist-info → codemie_test_harness-0.1.207.dist-info}/WHEEL +0 -0
  25. {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