Repository: JanPalasek/pretty-jupyter (language: python)

Issue:
Test the visual appearance after generating the report
- Selenium?
- Something better?


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7c160e0..e4f0e45 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,4 +1,4 @@
-name: Build
+name: build
 
 on:
   - push
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 159f409..fab4efb 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,4 +1,4 @@
-name: Release
+name: release
 
 on:
   release:
diff --git a/requirements.in b/requirements.in
index b904204..e365fb6 100644
--- a/requirements.in
+++ b/requirements.in
@@ -7,4 +7,7 @@ pandas==1.4.2
 matplotlib==3.5.2
 seaborn==0.11.2
 nbconvert==6.5.0
-jinja2==3.1.2
\ No newline at end of file
+jinja2==3.1.2
+requests==2.28.1
+webdriver-manager==3.8.1
+selenium==4.3.0
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 3a5f249..a16f1c3 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,20 +10,35 @@ argon2-cffi-bindings==21.2.0
     # via argon2-cffi
 asttokens==2.0.5
     # via stack-data
+async-generator==1.10
+    # via
+    #   trio
+    #   trio-websocket
 atomicwrites==1.4.0
     # via pytest
 attrs==21.2.0
     # via
     #   jsonschema
+    #   outcome
     #   pytest
+    #   trio
 backcall==0.2.0
     # via ipython
 beautifulsoup4==4.11.1
     # via nbconvert
 bleach==5.0.0
     # via nbconvert
+certifi==2022.6.15
+    # via
+    #   requests
+    #   urllib3
 cffi==1.15.0
-    # via argon2-cffi-bindings
+    # via
+    #   argon2-cffi-bindings
+    #   cryptography
+    #   trio
+charset-normalizer==2.1.0
+    # via requests
 click==8.0.4
     # via -r requirements.in
 colorama==0.4.4
@@ -32,6 +47,10 @@ colorama==0.4.4
     #   ipython
     #   pytest
     #   tox
+cryptography==37.0.4
+    # via
+    #   pyopenssl
+    #   urllib3
 cycler==0.11.0
     # via matplotlib
 debugpy==1.6.0
@@ -56,6 +75,13 @@ filelock==3.6.0
     #   virtualenv
 fonttools==4.33.3
     # via matplotlib
+h11==0.13.0
+    # via wsproto
+idna==3.3
+    # via
+    #   requests
+    #   trio
+    #   urllib3
 iniconfig==1.1.1
     # via pytest
 ipykernel==6.15.0
@@ -154,6 +180,8 @@ numpy==1.22.4
     #   pandas
     #   scipy
     #   seaborn
+outcome==1.2.0
+    # via trio
 packaging==20.9
     # via
     #   ipykernel
@@ -202,12 +230,16 @@ pygments==2.12.0
     #   jupyter-console
     #   nbconvert
     #   qtconsole
+pyopenssl==22.0.0
+    # via urllib3
 pyparsing==2.4.7
     # via
     #   matplotlib
     #   packaging
 pyrsistent==0.18.1
     # via jsonschema
+pysocks==1.7.1
+    # via urllib3
 pytest==6.2.3
     # via -r requirements.in
 python-dateutil==2.8.2
@@ -215,6 +247,8 @@ python-dateutil==2.8.2
     #   jupyter-client
     #   matplotlib
     #   pandas
+python-dotenv==0.20.0
+    # via webdriver-manager
 pytz==2022.1
     # via pandas
 pywin32==304
@@ -231,10 +265,16 @@ qtconsole==5.3.1
     # via jupyter
 qtpy==2.1.0
     # via qtconsole
+requests==2.28.1
+    # via
+    #   -r requirements.in
+    #   webdriver-manager
 scipy==1.8.1
     # via seaborn
 seaborn==0.11.2
     # via -r requirements.in
+selenium==4.3.0
+    # via -r requirements.in
 send2trash==1.8.0
     # via notebook
 six==1.16.0
@@ -244,6 +284,10 @@ six==1.16.0
     #   python-dateutil
     #   tox
     #   virtualenv
+sniffio==1.2.0
+    # via trio
+sortedcontainers==2.4.0
+    # via trio
 soupsieve==2.3.2.post1
     # via beautifulsoup4
 stack-data==0.3.0
@@ -277,16 +321,30 @@ traitlets==5.3.0
     #   nbformat
     #   notebook
     #   qtconsole
+trio==0.21.0
+    # via
+    #   selenium
+    #   trio-websocket
+trio-websocket==0.9.2
+    # via selenium
+urllib3[secure,socks]==1.26.10
+    # via
+    #   requests
+    #   selenium
 virtualenv==20.13.2
     # via tox
 wcwidth==0.2.5
     # via prompt-toolkit
+webdriver-manager==3.8.1
+    # via -r requirements.in
 webencodings==0.5.1
     # via
     #   bleach
     #   tinycss2
 widgetsnbextension==3.6.0
     # via ipywidgets
+wsproto==1.1.0
+    # via trio-websocket
 
 # The following packages are considered to be unsafe in a requirements file:
 # setuptools
diff --git a/tox.ini b/tox.ini
index 7248353..83bee70 100644
--- a/tox.ini
+++ b/tox.ini
@@ -9,9 +9,12 @@ python =
     3.9: py39
 
 [testenv]
+passenv = *
 deps =
     pytest
+    requests
+    webdriver-manager
+    selenium
 commands =
     # run tests
-    pretty-jupyter install-dev
     pytest tests/


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..0c537ae
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,30 @@
+import pytest
+from selenium.webdriver.chrome.options import Options
+from selenium.webdriver.chrome.service import Service
+from webdriver_manager.chrome import ChromeDriverManager
+from webdriver_manager.core.utils import ChromeType
+import selenium.webdriver
+
+
+@pytest.fixture()
+def driver():
+    chrome_service = Service(ChromeDriverManager(chrome_type=ChromeType.GOOGLE).install())
+
+    chrome_options = Options()
+    options = [
+        "--headless",
+        "--disable-gpu",
+        "--window-size=1920,1080",
+        "--ignore-certificate-errors",
+        "--disable-extensions",
+        "--no-sandbox",
+        "--disable-dev-shm-usage"
+    ]
+    for option in options:
+        chrome_options.add_argument(option)
+
+    driver = selenium.webdriver.Chrome(executable_path=chrome_service.path, options=chrome_options)
+
+    yield driver
+
+    driver.close()
\ No newline at end of file
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 5400f73..c9b4476 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -2,19 +2,110 @@
 import pytest
 from pretty_jupyter.console import cli
 import os
+from selenium.webdriver.common.by import By
 
 @pytest.fixture
 def input_path():
     return "tests/fixture/notebook.ipynb"
 
 
-def test_nbconvert(input_path, tmpdir):
-    out_path = os.path.join(tmpdir, "actual.html")
+def test_nbconvert(input_path, tmpdir, driver):
+    out_path = os.path.normpath(os.path.join(tmpdir, "actual.html"))
 
     runner = CliRunner()
     result = runner.invoke(cli, ["nbconvert", input_path, "--out", out_path])
     
     assert result.exit_code == 0
 
-    # TODO: transform to selenium
-    assert os.path.exists(out_path)
+    assert os.path.exists(out_path), "The expected file does not exist."
+
+    url = os.path.normpath(f"file:/{os.path.abspath(out_path)}")
+
+    # go to page
+    driver.get(url)
+
+    # check the title
+    title_xpath = "//h1[@class='title']"
+    assert driver.find_element(By.XPATH, title_xpath).text == "Notebook"
+
+    # test whether there is expected number of headers of level 1
+    h1_xpath = "//div[contains(@class, 'level1') and contains(@class, 'section')]/h1"
+    assert len(driver.find_elements(By.XPATH, h1_xpath)) == 4
+
+    ##########
+    # TABSET #
+    ##########
+    assert driver.find_elements(By.XPATH, h1_xpath)[2].text == "Chapter 1: Tabs"
+
+    tab_section = driver.find_elements(By.XPATH, "//div[contains(@class, 'section') and contains(@class, 'level1')]")[2]
+
+    # select tabs
+    list_tabs = tab_section.find_elements(By.XPATH, "ul[contains(@class, 'nav') and contains(@class, 'nav-pills')]/li")
+
+    assert len(list_tabs) == 3, "There should be 3 tabs."
+    assert list_tabs[1].text == "Tab 2", "The name of the second tab should be 'Tab 2'."
+
+
+    tab_contents = tab_section.find_elements(By.XPATH, """div[contains(@class, 'tab-content')]
+        /div[contains(@class, 'section') and contains(@class, 'level2') and contains(@class, 'tab-pane')]""")
+    assert len(tab_contents) == 3, "There should be 3 tab contents for the tabs."
+
+    assert "active" in tab_contents[0].get_attribute("class"), "First tab should be active at the beginning."
+
+    # click on the second tab
+    list_tabs[1].click()
+
+    assert "active" in tab_contents[1].get_attribute("class"), "Second tab should be active after clicking on it."
+
+    # check that the maths rendered properly
+    assert len(tab_contents[1].find_elements(By.XPATH, "span[contains(@class, 'MJXc-display')]")) > 0, "Math didn't render."
+
+    # check that the maths rendered properly
+    math_text = tab_contents[1].find_element(By.XPATH, "script[contains(@type, 'math/tex')]").get_attribute("innerHTML")
+    assert math_text == r"a \cdot a^2 = \frac{a^5}{a^2} = a^3 = 125"
+    
+    ################
+    # CODE FOLDING #
+    ################
+    jmd_section = driver.find_elements(By.XPATH, "//div[contains(@class, 'section') and contains(@class, 'level1')]")[3]
+
+    code_div = jmd_section.find_elements(By.XPATH, "div[contains(@class, 'py-code-collapse') and contains(@class, 'collapse')]")[0]
+    assert len(code_div.find_elements(By.XPATH, "div[contains(@class, 'pj-input')]//pre/span")) > 0, "Code highlighting does not work."
+
+    #####################
+    # TABLE OF CONTENTS #
+    #####################
+    def get_y_location(driver):
+        scroll_position_script = """
+                var pageY;
+                if (typeof(window.pageYOffset) == 'number') {
+                    pageY = window.pageYOffset;
+                } else {
+                    pageY = document.documentElement.scrollTop;
+                }
+                return pageY;
+            """
+
+        return driver.execute_script(scroll_position_script)
+
+    toc = driver.find_elements(By.XPATH, "//div[@id='TOC']")[0]
+
+    toc_first_level = toc.find_elements(By.XPATH, "ul")
+    assert len(toc_first_level) == 3, "There are 3 chapters."
+
+    # click on the Chapter 2: Jinja Markdown
+    toc_first_level[2].find_element(By.XPATH, "li").click()
+    import time
+    time.sleep(2)
+
+    jmd_title = jmd_section.find_element(By.XPATH, "h1")
+    tab_section_title = tab_section.find_element(By.XPATH, "h1")
+
+    epsilon_px = 10
+    assert abs(jmd_title.location["y"] - get_y_location(driver)) < epsilon_px, "Chapter 2: Jinja Markdown must be visible after clicking on it in TOC."
+    assert tab_section_title.location["y"] < get_y_location(driver), "Chapter 1: Tabs cannot be visible after clicking on chapter 2."
+
+    # go to Chapter 1
+    toc_first_level[1].find_element(By.XPATH, "li").click()
+    time.sleep(2)
+    assert abs(tab_section_title.location["y"] - get_y_location(driver)) < epsilon_px, "Chapter 1: Tabs must be visible after clicking on chapter 1."
\ No newline at end of file