Spaces:
Sleeping
Sleeping
rmm
commited on
Commit
·
7e35515
1
Parent(s):
f10ce15
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 |
+
|