Spaces:
Sleeping
Sleeping
rmm
commited on
Commit
·
fc76ddb
1
Parent(s):
a9c8ccb
test: visual test of presented content, persistent to tab switching
Browse files- seleniumbase runs the testing, clicking/uploading etc, and then
looking for the image elements that are generated once the ML
inference is complete
- added a property to one button so selenium can locate it
- .gitignore +1 -0
- src/main.py +2 -1
- tests/test_visual_main.py +244 -0
.gitignore
CHANGED
@@ -142,6 +142,7 @@ venv.bak/
|
|
142 |
|
143 |
# mkdocs documentation
|
144 |
/site
|
|
|
145 |
|
146 |
# mypy
|
147 |
.mypy_cache/
|
|
|
142 |
|
143 |
# mkdocs documentation
|
144 |
/site
|
145 |
+
docs/site
|
146 |
|
147 |
# mypy
|
148 |
.mypy_cache/
|
src/main.py
CHANGED
@@ -226,7 +226,8 @@ def main() -> None:
|
|
226 |
|
227 |
if st.session_state.workflow_fsm.is_in_state('data_entry_validated'):
|
228 |
# show the button, enabled. If pressed, we start the ML model (And advance state)
|
229 |
-
if tab_inference.button("Identify with cetacean classifier"
|
|
|
230 |
cetacean_classifier = AutoModelForImageClassification.from_pretrained(
|
231 |
"Saving-Willy/cetacean-classifier",
|
232 |
revision=classifier_revision,
|
|
|
226 |
|
227 |
if st.session_state.workflow_fsm.is_in_state('data_entry_validated'):
|
228 |
# show the button, enabled. If pressed, we start the ML model (And advance state)
|
229 |
+
if tab_inference.button("Identify with cetacean classifier",
|
230 |
+
key="button_infer_ceteans"):
|
231 |
cetacean_classifier = AutoModelForImageClassification.from_pretrained(
|
232 |
"Saving-Willy/cetacean-classifier",
|
233 |
revision=classifier_revision,
|
tests/test_visual_main.py
ADDED
@@ -0,0 +1,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pathlib import Path
|
2 |
+
import time
|
3 |
+
import pytest
|
4 |
+
from seleniumbase import BaseCase
|
5 |
+
from selenium.webdriver.common.by import By
|
6 |
+
from selenium.webdriver.support.ui import WebDriverWait
|
7 |
+
from selenium.webdriver.support import expected_conditions as EC
|
8 |
+
|
9 |
+
BaseCase.main(__name__, __file__)
|
10 |
+
|
11 |
+
# Set the paths to the images and csv file
|
12 |
+
repo_path = Path(__file__).resolve().parents[1]
|
13 |
+
imgpath = repo_path / "tests/data/rand_images"
|
14 |
+
img_f1 = imgpath / "img_001.jpg"
|
15 |
+
img_f2 = imgpath / "img_002.jpg"
|
16 |
+
img_f3 = imgpath / "img_003.jpg"
|
17 |
+
#csvpath = repo_path / "tests/data/test_csvs"
|
18 |
+
#csv_f1 = csvpath / "debian.csv"
|
19 |
+
|
20 |
+
mk_visible = """
|
21 |
+
var input = document.querySelector('[data-testid="stFileUploaderDropzoneInput"]');
|
22 |
+
input.style.display = 'block';
|
23 |
+
input.style.opacity = '1';
|
24 |
+
input.style.visibility = 'visible';
|
25 |
+
"""
|
26 |
+
|
27 |
+
def wait_for_element(self, by, selector, timeout=10):
|
28 |
+
# example usage:
|
29 |
+
# element = self.wait_for_element(By.XPATH, "//p[contains(text(), 'Species for observation')]")
|
30 |
+
|
31 |
+
return WebDriverWait(self.driver, timeout).until(
|
32 |
+
EC.presence_of_element_located((by, selector))
|
33 |
+
)
|
34 |
+
|
35 |
+
|
36 |
+
def find_all_button_paths(self):
|
37 |
+
buttons = self.find_elements("button")
|
38 |
+
for button in buttons:
|
39 |
+
print(f"\nButton found:")
|
40 |
+
print(f"Text: {button.text.strip()}")
|
41 |
+
print(f"HTML: {button.get_attribute('outerHTML')}")
|
42 |
+
print("-" * 50)
|
43 |
+
|
44 |
+
def check_columns_and_images(self, exp_cols:int, exp_imgs:int=4):
|
45 |
+
# Find all columns
|
46 |
+
columns = self.find_elements("div[class*='stColumn']")
|
47 |
+
|
48 |
+
# Check number of columns
|
49 |
+
assert len(columns) == exp_cols, f"Expected exp_cols columns but found {len(columns)}"
|
50 |
+
|
51 |
+
# Check images in each column
|
52 |
+
for i, column in enumerate(columns, 1):
|
53 |
+
# Find all images within this column's image containers
|
54 |
+
images = self.find_elements(
|
55 |
+
f"div[class*='stColumn']:nth-child({i}) div[data-testid='stImageContainer'] img"
|
56 |
+
)
|
57 |
+
|
58 |
+
# Check number of images in this column
|
59 |
+
assert len(images) == exp_imgs, f"Column {i} has {len(images)} images instead of {exp_imgs}"
|
60 |
+
|
61 |
+
|
62 |
+
def analyze_species_columns_debug(self):
|
63 |
+
# First, just try to find any divs
|
64 |
+
all_divs = self.find_elements(By.TAG_NAME, "div")
|
65 |
+
print(f"Found {len(all_divs)} total divs")
|
66 |
+
|
67 |
+
# Then try to find stColumn divs
|
68 |
+
column_divs = self.find_elements(By.XPATH, "//div[contains(@class, 'stColumn')]")
|
69 |
+
print(f"Found {len(column_divs)} column divs")
|
70 |
+
|
71 |
+
# Try to find any elements containing our text, without class restrictions
|
72 |
+
text_elements = self.find_elements(
|
73 |
+
By.XPATH, "//*[contains(text(), 'Species for observation')]"
|
74 |
+
)
|
75 |
+
print(f"Found {len(text_elements)} elements with 'Species for observation' text")
|
76 |
+
|
77 |
+
# If we found text elements, print their tag names and class names to help debug
|
78 |
+
for elem in text_elements:
|
79 |
+
print(f"Tag: {elem.tag_name}, Class: {elem.get_attribute('class')}")
|
80 |
+
|
81 |
+
def analyze_species_columns(self, exp_cols:int, exp_imgs:int=4, exp_visible:bool=True):
|
82 |
+
# Find all columns that contain the specific text pattern
|
83 |
+
cur_tab = get_selected_tab(self)
|
84 |
+
print(f"Current tab: {cur_tab['text']} ({cur_tab['id']})" )
|
85 |
+
|
86 |
+
#"div[class*='stColumn']//div[contains(text(), 'Species for observation')]"
|
87 |
+
spec_labels = self.find_elements(
|
88 |
+
By.XPATH,
|
89 |
+
"//p[contains(text(), 'Species for observation')]"
|
90 |
+
)
|
91 |
+
|
92 |
+
# This gets us the text containers, need to go back up to the column
|
93 |
+
species_columns = [lbl.find_element(By.XPATH, "./ancestor::div[contains(@class, 'stColumn')]")
|
94 |
+
for lbl in spec_labels]
|
95 |
+
|
96 |
+
print(f" Found {len(species_columns)} species columns (total {len(spec_labels)} species labels)")
|
97 |
+
assert len(species_columns) == exp_cols, f"Expected {exp_cols} columns but found {len(species_columns)}"
|
98 |
+
|
99 |
+
|
100 |
+
for i, column in enumerate(species_columns, 1):
|
101 |
+
# Get the species number text
|
102 |
+
species_text = column.find_element(
|
103 |
+
#By.XPATH, ".//div[contains(text(), 'Species for observation')]"
|
104 |
+
By.XPATH, ".//p[contains(text(), 'Species for observation')]"
|
105 |
+
)
|
106 |
+
print(f" Analyzing col {i}:{species_text.text} {species_text.get_attribute('outerHTML')} | ")
|
107 |
+
|
108 |
+
# Find images in this specific column
|
109 |
+
images = column.find_elements(
|
110 |
+
By.XPATH, ".//div[@data-testid='stImageContainer']//img"
|
111 |
+
)
|
112 |
+
print(f" - Contains {len(images)} images (expected: {exp_imgs})")
|
113 |
+
assert len(images) == exp_imgs, f"Column {i} has {len(images)} images instead of {exp_imgs}"
|
114 |
+
|
115 |
+
# now let's refine the search to find the images that are actually displayed
|
116 |
+
visible_images = [img for img in column.find_elements(
|
117 |
+
By.XPATH, ".//div[@data-testid='stImageContainer']//img"
|
118 |
+
) if img.is_displayed()]
|
119 |
+
print(f" - Contains {len(visible_images)} visible images")
|
120 |
+
if exp_visible:
|
121 |
+
assert len(visible_images) == exp_imgs, f"Column {i} has {len(visible_images)} visible images instead of {exp_imgs}"
|
122 |
+
else:
|
123 |
+
assert len(visible_images) == 0, f"Column {i} has {len(visible_images)} visible images instead of 0"
|
124 |
+
|
125 |
+
|
126 |
+
# even more strict test for visibility
|
127 |
+
# for img in images:
|
128 |
+
# style = img.get_attribute('style')
|
129 |
+
# computed_style = self.driver.execute_script(
|
130 |
+
# "return window.getComputedStyle(arguments[0])", img
|
131 |
+
# )
|
132 |
+
# print(f"Style: {style}")
|
133 |
+
# print(f"Visibility: {computed_style['visibility']}")
|
134 |
+
# print(f"Opacity: {computed_style['opacity']}")
|
135 |
+
|
136 |
+
def get_selected_tab(self):
|
137 |
+
selected_tab = self.find_element(
|
138 |
+
By.XPATH, "//div[@data-testid='stTabs']//button[@aria-selected='true']"
|
139 |
+
)
|
140 |
+
# Get the tab text
|
141 |
+
tab_text = selected_tab.find_element(By.TAG_NAME, "p").text
|
142 |
+
# Get the tab index (might be useful)
|
143 |
+
tab_id = selected_tab.get_attribute("id") # Usually ends with "-tab-X" where X is the index
|
144 |
+
return {
|
145 |
+
"text": tab_text,
|
146 |
+
"id": tab_id,
|
147 |
+
"element": selected_tab
|
148 |
+
}
|
149 |
+
|
150 |
+
def switch_tab(self, tab_number):
|
151 |
+
# Click the tab
|
152 |
+
self.click(f"div[data-testid='stTabs'] button[id$='-tab-{tab_number}'] p")
|
153 |
+
|
154 |
+
# Verify the switch
|
155 |
+
selected_tab = get_selected_tab(self)
|
156 |
+
if selected_tab["id"].endswith(f"-tab-{tab_number}"):
|
157 |
+
print(f"Successfully switched to tab {tab_number}: {selected_tab['text']}")
|
158 |
+
else:
|
159 |
+
raise Exception(f"Failed to switch to tab {tab_number}, current tab is {selected_tab['text']}")
|
160 |
+
|
161 |
+
class RecorderTest(BaseCase):
|
162 |
+
|
163 |
+
@pytest.mark.visual_slow
|
164 |
+
def test_species_presentation(self):
|
165 |
+
# this test goes through several steps of the workflow, primarily to get to the point
|
166 |
+
# that species columns are displayed.
|
167 |
+
# - setup steps:
|
168 |
+
# - open the app
|
169 |
+
# - upload two images
|
170 |
+
# - validate the data entry
|
171 |
+
# - click the infer button, wait for ML
|
172 |
+
# - the real test steps:
|
173 |
+
# - check the species columns are displayed
|
174 |
+
# - switch to another tab, check the columns are not displayed
|
175 |
+
# - switch back to the first tab, check the columns are displayed again
|
176 |
+
|
177 |
+
self.open("http://localhost:8501/")
|
178 |
+
time.sleep(4) # even in demo mode, on full script this is needed
|
179 |
+
# (the folium maps cause the scripts to rerun, which means the wait_for_element finds it, but
|
180 |
+
# the reload is going on and this makes the upload files (send_keys) command fail)
|
181 |
+
|
182 |
+
# make the file_uploader block visible -- for some reason even though we can see it, selenium can't...
|
183 |
+
wait_for_element(self, By.CSS_SELECTOR, '[data-testid="stFileUploaderDropzoneInput"]')
|
184 |
+
self.execute_script(mk_visible)
|
185 |
+
# send a list of files
|
186 |
+
self.send_keys(
|
187 |
+
'input[data-testid="stFileUploaderDropzoneInput"]',
|
188 |
+
"\n".join([str(img_f1), str(img_f2)]),
|
189 |
+
)
|
190 |
+
|
191 |
+
# advance to the next step, by clicking the validate button (wait for it first)
|
192 |
+
wait_for_element(self, By.XPATH, "//button//strong[contains(text(), 'Validate')]")
|
193 |
+
self.click('button strong:contains("Validate")')
|
194 |
+
# validate the progress via the text display
|
195 |
+
self.assert_exact_text("Progress: 2/5. Current: data_entry_validated.", 'div[data-testid="stMarkdownContainer"] p em')
|
196 |
+
|
197 |
+
# check the tab bar is there, and the titles are correct
|
198 |
+
expected_texts = [
|
199 |
+
"Cetecean classifier", "Hotdog classifier", "Map",
|
200 |
+
"Dev:coordinates", "Log", "Beautiful cetaceans"
|
201 |
+
]
|
202 |
+
self.assert_element("div[data-testid='stTabs']")
|
203 |
+
|
204 |
+
for i, text in enumerate(expected_texts):
|
205 |
+
selector = f"div[data-testid='stTabs'] button[id$='-tab-{i}'] p"
|
206 |
+
print(f"{i=}, {text=}, {selector=}")
|
207 |
+
self.assert_text(text, selector)
|
208 |
+
break # just do one, this is slow while debuggin
|
209 |
+
|
210 |
+
# dbg: look for buttons, find out which props will isolate the right one.
|
211 |
+
# find_all_button_paths(self)
|
212 |
+
|
213 |
+
self.assert_element(".st-key-button_infer_ceteans button")
|
214 |
+
self.click(".st-key-button_infer_ceteans button")
|
215 |
+
|
216 |
+
# check the state has advanced
|
217 |
+
self.assert_exact_text("Progress: 3/5. Current: ml_classification_completed.",
|
218 |
+
'div[data-testid="stMarkdownContainer"] p em')
|
219 |
+
|
220 |
+
# on the inference tab, check the columns and images are rendered correctly
|
221 |
+
# - normally it is selected by default, but we can switch to it to be sure
|
222 |
+
# - then we do the test for the right number of columns and images per col,
|
223 |
+
# which should be visible
|
224 |
+
switch_tab(self, 0)
|
225 |
+
analyze_species_columns(self, exp_cols=2, exp_imgs=4, exp_visible=True)
|
226 |
+
|
227 |
+
# now, we want to select another tab, check somethign is present?
|
228 |
+
# then go back, and re-check the columns and images are re-rendered.
|
229 |
+
switch_tab(self, 4)
|
230 |
+
assert get_selected_tab(self)["id"].endswith("-tab-4")
|
231 |
+
|
232 |
+
# now we click the refresh button
|
233 |
+
self.click('button[data-testid="stBaseButton-secondary"]')
|
234 |
+
# and then select the first tab again
|
235 |
+
switch_tab(self, 0)
|
236 |
+
assert get_selected_tab(self)["id"].endswith("-tab-0")
|
237 |
+
# and check the columns and images are re-rendered
|
238 |
+
analyze_species_columns(self, exp_cols=2, exp_imgs=4, exp_visible=True)
|
239 |
+
|
240 |
+
# now go to some other tab, and check the columns and images are not visible
|
241 |
+
switch_tab(self, 2)
|
242 |
+
assert get_selected_tab(self)["id"].endswith("-tab-2")
|
243 |
+
analyze_species_columns(self, exp_cols=2, exp_imgs=4, exp_visible=False)
|
244 |
+
|