Skip to content

Commit

Permalink
Merge pull request mozilla#6274 from emilghittasv/playwright-expand-c…
Browse files Browse the repository at this point in the history
…overage-to-search

Playwright add foundation for search tests
  • Loading branch information
emilghittasv authored Oct 7, 2024
2 parents 1219f6a + 4ab2447 commit 0c743ca
Show file tree
Hide file tree
Showing 7 changed files with 464 additions and 55 deletions.
119 changes: 118 additions & 1 deletion playwright_tests/core/utilities.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
from typing import Any, Union

import requests
import time
import re
import json
import random
import os
from datetime import datetime

from nltk import SnowballStemmer
from playwright.sync_api import Page
from playwright_tests.messages.homepage_messages import HomepageMessages
from requests.exceptions import HTTPError

from playwright_tests.pages.top_navbar import TopNavbar
from playwright_tests.test_data.search_synonym import SearchSynonyms


class Utilities:
Expand Down Expand Up @@ -302,3 +306,116 @@ def extract_date_to_digit_format(self, date_str: str) -> int:
"""
date = datetime.strptime(date_str, "%b %d, %Y")
return int(date.strftime("%m%d%Y"))

def tokenize_string(self, text: str) -> list[str]:
"""
This helper function tokenizes the text into individual words and removes any non
alphanumeric characters.
"""
return re.findall(r'\b\w+\b', text.lower())

def stem_tokens(self, tokens: list[str], search_term_locale: str):
"""
This helper function stems each token and returns the list of stemmed tokens.
"""
stemmer = SnowballStemmer(search_term_locale)
return [stemmer.stem(token) for token in tokens]

def search_result_check(self, search_result, search_term, search_term_locale: str,
exact_phrase: bool):
"""
Checks if the search result contains:
1. Any variation of the provided keyword.
2. The search term or any of its synonyms.
3. The exact phrase or any component of the phrase.
4. Variations of the search term by stemming.
5. Variations of the search term by stemming for non-US words.
"""

search_term_split = search_term.lower().split()
search_results_lower = search_result.lower()

# Check if searching for exact phrase.
if exact_phrase:
return self._exact_phrase_check(search_result, search_term)

# Check if keyword variations
if self._contains_keyword_variation(search_results_lower, search_term_split):
print(f"The {search_term} was found in search result variation.")
return True

# Check synonyms of split terms and the whole term
match_found, matching_synonym = self._contains_synonym(search_results_lower, search_term,
search_term_split)
if match_found:
print(f"Search result for {search_term} found in synonym: {matching_synonym}")
return True

# Check if exact phrase match
if ' '.join(search_term_split) in search_results_lower:
print(f"Search results for {search_term} found in exact match")
return True

# Check each term component
if any(term in search_results_lower for term in search_term_split):
print(f"Search result for {search_term} found in a component of the search result")
return True

# Check stemming in search results.
stemmed_tokens = self.stem_tokens(self.tokenize_string(search_result), search_term_locale)
stemmed_search_term = self.stem_tokens(self.tokenize_string(search_term),
search_term_locale)

if any(term in stemmed_tokens for term in stemmed_search_term):
print(f"Search result for {search_term} found in stemmed word")
return True

if self._contains_synonym(search_results_lower, stemmed_search_term, search_term_split)[0]:
print(f"Search result for {search_term} found in stemmed word synonym")
return True

print("Search result not found!")
return False

def _contains_synonym(self, search_result_lower, search_term: Union[str, list[str]],
search_term_split) -> [bool, Any]:
"""
This helper function checks if any synonyms of a given search term or its components
(split term) are present in the search result.
"""
synonyms = None

if isinstance(search_term, list):
for term in search_term:
synonyms = SearchSynonyms.synonym_dict.get(term.lower(), [])
else:
synonyms = SearchSynonyms.synonym_dict.get(search_term.lower(), [])

for term in search_term_split:
synonyms.extend(SearchSynonyms.synonym_dict.get(term, []))

for synonym in synonyms:
if synonym.lower() in search_result_lower:
return True, synonym.lower()
return False, None

def _contains_keyword_variation(self, search_result_lower, search_term_split):
"""
This helper function checks if any variation of the keyword (components of the search term)
are present in the search results. This includes different cases (lowercase or uppercase)
and simple stemmed forms (by removing the last character).
"""
keyword_variations = [
variation
for term in search_term_split
for variation in [term, term.capitalize(), term.upper(), term[:-1],
term[:-1].capitalize()]
]
return any(variation in search_result_lower for variation in keyword_variations)

def _exact_phrase_check(self, search_result: str, search_term: str) -> bool:
search_term = search_term.replace('"', '').lower()
print(f"Search term is: {search_term}")
search_result = search_result.lower()
print(f"Search result is: {search_result}")
return search_term in search_result
141 changes: 112 additions & 29 deletions playwright_tests/pages/search/search_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,48 +3,131 @@


class SearchPage(BasePage):
__search_bar = "//form[@id='support-search-masthead']/input[@id='search-q']"
__search_bar_button = "//form[@id='support-search-masthead']/button[@class='search-button']"
"""
Locators belonging to the searchbar.
"""
__searchbar = "//form[@id='support-search-masthead']/input[@id='search-q']"
__searchbar_search_button = "//form[@id='support-search-masthead']/button"
__search_results_header = "//div[@class='home-search-section--content']/h2"
__popular_searches = "//p[@class='popular-searches']/a"
__search_results_article_titles = "//h3[@class='sumo-card-heading']/a"

"""
Locators belonging to the search results filter
"""
__view_all_filter = "//span[text()='View All']/..[0]"
__help_articles_only_filter = "//span[text()='Help Articles Only']/..[0]"
__community_discussions_only_filter = "//span[text()='Community Discussion Only']/..[0]"

"""
Locators belonging to the search results
"""
__search_results_titles = "//section[@class='topic-list content-box']//a[@class='title']"
__search_results_articles_summary = "//div[@class='topic-article--text']/p"
__search_results_content = "//section[@class='topic-list content-box']"
__all_bolded_article_content = ("//h3[@class='sumo-card-heading']/a/../following-sibling::p/"
"strong")

"""
Locators belonging to the side navbar
"""
__search_results_side_nav_header = "//h3[@class='sidebar-subheading']"
__search_results_side_nav_selected_item = "//ul[@id='product-filter']//li[@class='selected']/a"
__search_results_side_nav_elements = "//ul[@id='product-filter']//a"

"""
General locators
"""
__page_header = "//h1[@class='sumo-page-heading-xl']"

def __init__(self, page: Page):
super().__init__(page)

def _get_text_of_searchbar_field(self) -> str:
return super()._get_text_of_element(self.__search_bar)
"""
Actions against the search results
"""
def click_on_a_particular_popular_search(self, popular_search_option: str):
self._click(f"//p[@class='popular-searches']/a[text()='{popular_search_option}']")

def get_search_result_summary_text_of_a_particular_article(self, article_title) -> str:
return self._get_text_of_element(f"//h3[@class='sumo-card-heading']/"
f"a[normalize-space(text())='{article_title}']/../"
f"../p")

def is_a_particular_article_visible(self, article_title: str) -> bool:
return self._is_element_visible(f"//h3[@class='sumo-card-heading']/"
f"a[normalize-space(text())='{article_title}']")

def click_on_a_particular_article(self, article_title: str):
self._click(f"//h3[@class='sumo-card-heading']/"
f"a[normalize-space(text())='{article_title}']")

def get_all_bolded_content(self) -> list[str]:
return self._get_text_of_elements(self.__all_bolded_article_content)

def get_all_search_results_article_bolded_content(self, article_title: str) -> list[str]:
if "'" in article_title:
parts = article_title.split("'")
if len(parts) > 1:
# Construct XPath using concat function
xpath = (f"//h3[@class='sumo-card-heading']/a[normalize-space(text())=concat("
f"'{parts[0]}', \"'\", '{parts[1]}')]/../following-sibling::p/strong")
else:
# Handle the case where the text ends with a single quote
xpath = (f"//h3[@class='sumo-card-heading']/a[normalize-space(text())=concat("
f"'{parts[0]}', \"'\")]/../following-sibling::p/strong")
else:
# Construct XPath without concat for texts without single quotes

xpath = (f"//h3[@class='sumo-card-heading']/a[normalize-space(text()"
f")='{article_title}']/../following-sibling::p/strong")
return self._get_text_of_elements(xpath)

def get_all_search_results_article_titles(self) -> list[str]:
return self._get_text_of_elements(self.__search_results_titles)

def get_all_search_results_articles_summary(self) -> list[str]:
return self._get_text_of_elements(self.__search_results_articles_summary)

def get_locator_of_a_particular_article(self, article_title: str) -> Locator:
return self._get_element_locator(f"//h3[@class='sumo-card-heading']/"
f"a[normalize-space(text())='{article_title}']")

def is_search_content_section_displayed(self) -> bool:
return self._is_element_visible(self.__search_results_content)

def _type_into_searchbar(self, text: str):
super()._type(self.__search_bar, text, 200)
"""
Actions against the search bar
"""

def _clear_the_searchbar(self):
super()._clear_field(self.__search_bar)
def get_text_of_searchbar_field(self) -> str:
return self._get_element_input_value(self.__searchbar)

def _click_on_search_button(self):
super()._click(self.__search_bar_button)
def fill_into_searchbar(self, text: str):
self._fill(self.__searchbar, text)

def _get_list_of_popular_searches(self) -> list[str]:
return super()._get_text_of_elements(self.__popular_searches)
def clear_the_searchbar(self):
self._clear_field(self.__searchbar)

def _click_on_a_particular_popular_search(self, popular_search_option: str):
super()._click(f"//p[@class='popular-searches']/a[text()='{popular_search_option}']")
def click_on_search_button(self):
self._click(self.__searchbar_search_button)

def _get_search_result_summary_text_of_a_particular_article(self, article_title) -> str:
return super()._get_text_of_element(f"//h3[@class='sumo-card-heading']/"
f"a[normalize-space(text())='{article_title}']/../"
f"../p")
def get_list_of_popular_searches(self) -> list[str]:
return self._get_text_of_elements(self.__popular_searches)

def _click_on_a_particular_article(self, article_title):
super()._click(f"//h3[@class='sumo-card-heading']/"
f"a[normalize-space(text())='{article_title}']")
def click_on_a_popular_search(self, popular_search_name: str):
self._click(f"//p[@class='popular-searches']/a[text()='{popular_search_name}']")

def _get_all_search_results_article_titles(self) -> list[str]:
return super()._get_text_of_elements(self.__search_results_article_titles)
"""
Actions against the side navbar
"""
def get_the_highlighted_side_nav_item(self) -> str:
return self._get_text_of_element(self.__search_results_side_nav_selected_item)

def _get_all_search_results_articles_summary(self) -> list[str]:
return super()._get_text_of_elements(self.__search_results_articles_summary)
def click_on_a_particular_side_nav_item(self, product_name: str):
self._click(f"//ul[@id='product-filter']//a[normalize-space(text())='{product_name}']")

def _get_locator_of_a_particular_article(self, article_title: str) -> Locator:
return super()._get_element_locator(f"//h3[@class='sumo-card-heading']/"
f"a[normalize-space(text())='{article_title}']")
"""
General page actions
"""
def get_search_results_header(self) -> str:
return self._get_text_of_element(self.__search_results_header)
Loading

0 comments on commit 0c743ca

Please sign in to comment.