TirSystem/git-workflows: OpenGrep / opengrep (push) Skipped
TirSystem/git-workflows: Publish wiki / publish-wiki (push) Skipped
TirSystem/git-workflows: Sync GitHub mirror metadata / sync-metadata (push) Skipped
TirSystem/git-workflows: Publish accepted milestones / publish (push) Skipped
TirSystem/git-workflows: Validate / validate (push) Skipped
TirSystem/git-workflows: Validate / validate (pull_request) Successful in 42s
TirSystem/git-workflows: OpenGrep / opengrep (pull_request) Successful in 4m48s
Selenium bot (src/tinder_bot) for the Day 50 lecture steps, unit tests, a mock-site Docker e2e harness for screenshots, and README plus wiki in English, Danish and Bahasa Malaysia.
142 lines
5.2 KiB
Python
142 lines
5.2 KiB
Python
"""Run the bot against the static mock TinDog site and screenshot each step.
|
|
|
|
Usage: ``python tests/e2e/run_e2e.py --out /out``. Run it in the Docker
|
|
container (see README.md), never on the host desktop.
|
|
"""
|
|
|
|
import argparse
|
|
import logging
|
|
import sys
|
|
import threading
|
|
from functools import partial
|
|
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
|
from pathlib import Path
|
|
from time import sleep
|
|
|
|
from selenium.webdriver.remote.webdriver import WebDriver
|
|
from selenium.webdriver.support import expected_conditions as ec
|
|
from selenium.webdriver.support.ui import WebDriverWait
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT / "src"))
|
|
|
|
from tinder_bot import locators # noqa: E402
|
|
from tinder_bot.config import Settings # noqa: E402
|
|
from tinder_bot.driver import create_driver # noqa: E402
|
|
from tinder_bot.steps import like_once, sign_in_with_facebook # noqa: E402
|
|
|
|
SITE_DIR = Path(__file__).resolve().parent / "site"
|
|
LIKING_SHOT_AFTER = 2
|
|
MATCH_EVERY = 4 # must equal MATCH_EVERY in site/app.js
|
|
|
|
log = logging.getLogger("e2e")
|
|
|
|
|
|
class QuietHandler(SimpleHTTPRequestHandler):
|
|
"""Static file handler that does not log each request."""
|
|
|
|
def log_message(self, format: str, *args: object) -> None: # noqa: A002
|
|
return
|
|
|
|
|
|
def start_server() -> tuple[ThreadingHTTPServer, str]:
|
|
"""Serve the mock site on a free port; return the server and its URL."""
|
|
handler = partial(QuietHandler, directory=str(SITE_DIR))
|
|
server = ThreadingHTTPServer(("127.0.0.1", 0), handler)
|
|
threading.Thread(target=server.serve_forever, daemon=True).start()
|
|
return server, f"http://127.0.0.1:{server.server_address[1]}/index.html"
|
|
|
|
|
|
def shoot(driver: WebDriver, out: Path, name: str) -> None:
|
|
"""Save a screenshot of the current window as ``name``."""
|
|
driver.save_screenshot(str(out / name))
|
|
log.info("saved %s", out / name)
|
|
|
|
|
|
def wait_visible(driver: WebDriver, locator: locators.Locator, timeout: float) -> None:
|
|
WebDriverWait(driver, timeout).until(ec.visibility_of_element_located(locator))
|
|
|
|
|
|
def click(driver: WebDriver, locator: locators.Locator, timeout: float) -> None:
|
|
WebDriverWait(driver, timeout).until(ec.element_to_be_clickable(locator)).click()
|
|
|
|
|
|
def shoot_popup(driver: WebDriver, out: Path, timeout: float) -> None:
|
|
"""Screenshot the Facebook window, then return to the main window."""
|
|
main = driver.current_window_handle
|
|
WebDriverWait(driver, timeout).until(lambda d: len(d.window_handles) > 1)
|
|
popup = next(h for h in driver.window_handles if h != main)
|
|
driver.switch_to.window(popup)
|
|
wait_visible(driver, locators.FACEBOOK_EMAIL, timeout)
|
|
shoot(driver, out, "03-facebook-popup.png")
|
|
driver.switch_to.window(main)
|
|
|
|
|
|
def like_all(driver: WebDriver, settings: Settings, out: Path) -> int:
|
|
"""Like ``max_likes`` profiles, screenshotting mid-run and the match pop-up."""
|
|
liked = 0
|
|
match_shot_taken = False
|
|
while liked < settings.max_likes:
|
|
like_once(driver, settings, sleep)
|
|
liked += 1
|
|
if liked == LIKING_SHOT_AFTER:
|
|
shoot(driver, out, "07-liking.png")
|
|
if liked % MATCH_EVERY == 0 and not match_shot_taken:
|
|
wait_visible(driver, locators.BACK_TO_TINDER, settings.timeout)
|
|
shoot(driver, out, "08-match-popup.png")
|
|
match_shot_taken = True
|
|
sleep(settings.like_delay)
|
|
shoot(driver, out, "09-done.png")
|
|
return liked
|
|
|
|
|
|
def run(out: Path) -> int:
|
|
"""Drive the whole flow against the mock site; return the likes given."""
|
|
server, url = start_server()
|
|
settings = Settings(
|
|
url=url,
|
|
facebook_email="bot@example.com",
|
|
facebook_password="not-a-real-password",
|
|
like_delay=0.2,
|
|
retry_delay=0.5,
|
|
)
|
|
driver = create_driver(headless=True)
|
|
try:
|
|
driver.get(settings.url)
|
|
wait_visible(driver, locators.LOGIN_BUTTON, settings.timeout)
|
|
shoot(driver, out, "01-home.png")
|
|
click(driver, locators.LOGIN_BUTTON, settings.timeout)
|
|
wait_visible(driver, locators.FACEBOOK_BUTTON, settings.timeout)
|
|
shoot(driver, out, "02-login-modal.png")
|
|
click(driver, locators.FACEBOOK_BUTTON, settings.timeout)
|
|
shoot_popup(driver, out, settings.timeout)
|
|
sign_in_with_facebook(driver, settings)
|
|
for locator, name in (
|
|
(locators.ALLOW_LOCATION, "04-location.png"),
|
|
(locators.NOT_INTERESTED, "05-notifications.png"),
|
|
(locators.ACCEPT_COOKIES, "06-cookies.png"),
|
|
):
|
|
wait_visible(driver, locator, settings.timeout)
|
|
shoot(driver, out, name)
|
|
click(driver, locator, settings.timeout)
|
|
return like_all(driver, settings, out)
|
|
finally:
|
|
driver.quit()
|
|
server.shutdown()
|
|
|
|
|
|
def main() -> int:
|
|
"""Parse arguments, run the flow and return the process exit code."""
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--out", type=Path, required=True, help="screenshot directory")
|
|
args = parser.parse_args()
|
|
logging.basicConfig(level=logging.INFO)
|
|
args.out.mkdir(parents=True, exist_ok=True)
|
|
liked = run(args.out)
|
|
log.info("liked %d profiles", liked)
|
|
return 0 if liked == Settings().max_likes else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|