diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d399253fc..980a51731 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -28,6 +28,12 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Install Cypress Test Framework + run: npm install cypress - name: Install Nox Dependencies run: | python -m pip install poetry nox nox-poetry pyparsing==3.0.4 diff --git a/docs/contributing.rst b/docs/contributing.rst index 4e6777650..4143f51b6 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -101,6 +101,91 @@ Running Tests pip install -r docs/requirements.txt make test +Running JavaScript Tests in Python Test Files +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +**Setup Cypress Locally** + +* Install Node JS on your computer and ensure it can be accessed through the CMD. +* Install Cypress using the npm package manager by running npm install cypress. Read more from this link https://docs.cypress.io/guides/getting-started/installing-cypress#npm-install. +* Verify if Cypress is installed correctly and is executable by running: npx cypress verify. Read more from this link https://docs.cypress.io/guides/guides/command-line. +* If everything is successful then we can use Cypress. + +**Enable Cypress Test in Python Test Files** + +* First, create a test folder to store your Cypress JS test files (files should end with: ``*.cy.js``). For each Cypress JS test file, you will need to write the Cypress JS test cases in the file. You can read more from the `Cypress Docs `_. You can also refer to the ``tests/js_test/js-test-sn-collapse-button.cy.js`` file as reference. +* In your Python test files, you must mark every JS related test case with the marker - ``jstest`` and you also need to pass the ``spec_pattern`` key-value pair as part of the ``test_app`` fixture parameter. For example, your test case could look like the below: + .. code-block:: python + + # test_js_code + + import pytest + + + @pytest.mark.jstest + @pytest.mark.parametrize( + "test_app", + [ + { + "buildername": "html", + "srcdir": "doc_test/variant_doc", + "tags": ["tag_a"], + "spec_pattern": "js_test/js-test-sn-collapse-button.cy.js" + } + ], + indirect=True, + ) + def test_collapse_button_in_docs(test_app): + ... + + .. note:: + + The ``spec_pattern`` key is required to ensure Cypress locates your test files or folder. Visit this link for more info on how to set the `spec_pattern `_. + +* After you have set the ``spec_pattern`` key-value pair as part of the ``test_app`` fixture parameter, you can call the ``app.test_js()`` in your Python test case to run the JS test for the ``spec_pattern`` you provided. For example, you can use it like below: + .. code-block:: python + + # test_js_code + + import pytest + + + @pytest.mark.jstest + @pytest.mark.parametrize( + "test_app", + [ + { + "buildername": "html", + "srcdir": "doc_test/variant_doc", + "tags": ["tag_a"], + "spec_pattern": "js_test/js-test-sn-collapse-button.cy.js" + } + ], + indirect=True, + ) + def test_collapse_button_in_docs(test_app): + """Check if the Sphinx-Needs collapse button works in the provided documentation source.""" + app = test_app + app.build() + + # Call `app.test_js()` to run the JS test for a particular specPattern + js_test_result = app.test_js() + + # Check the return code and stdout + assert js_test_result["returncode"] == 0 + assert "All specs passed!" in js_test_result["stdout"].decode("utf-8") + + .. note:: + + ``app.test_js()`` will return a dictionary object containing the ``returncode``, ``stdout``, and ``stderr``. Example: + + .. code-block:: python + + return { + "returncode": 0, + "stdout": "Test passed string", + "stderr": "Errors encountered, + } + Linting & Formatting -------------------- diff --git a/poetry.lock b/poetry.lock index e2a68f25a..a1f1f885a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1993,4 +1993,4 @@ immaterial = [] [metadata] lock-version = "2.0" python-versions = ">=3.7.0,<4.0" -content-hash = "27f4d83ca2c7761c9440fc0979fe8975749d3b2ce6ff44c276fa9b2948f8deb8" +content-hash = "27f4d83ca2c7761c9440fc0979fe8975749d3b2ce6ff44c276fa9b2948f8deb8" \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 681b756e1..0a910139f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,14 @@ """Pytest conftest module containing common test configuration and fixtures.""" +import json +import os.path import shutil +import subprocess +from pathlib import Path from tempfile import mkdtemp +from typing import Any, Dict import pytest +from sphinx.application import Sphinx from sphinx.testing.path import path pytest_plugins = "sphinx.testing.fixtures" @@ -15,6 +21,82 @@ def copy_srcdir_to_tmpdir(srcdir, tmp): return tmproot +def get_abspath(relpath): + if relpath and isinstance(relpath, str): + abspath = Path(__file__).parent.joinpath(relpath).resolve() + return str(abspath) + return relpath + + +def test_js(self) -> Dict[str, Any]: + cypress_testpath = get_abspath(self.spec_pattern) + + if not cypress_testpath and not (os.path.isabs(cypress_testpath) and os.path.exists(cypress_testpath)): + return { + "returncode": 1, + "stdout": None, + "stderr": f"The spec_pattern you provided cannot be found. (spec_pattern: {self.spec_pattern})", + } + + js_test_config = { + "specPattern": cypress_testpath, + "supportFile": get_abspath("js_test/cypress/support/e2e.js"), + "fixturesFolder": False, + "baseUrl": "http://localhost:65323", + } + + cypress_config = f"{json.dumps(js_test_config)}" + cypress_config_file = get_abspath("js_test/cypress.config.js") + + # Start the HTTP server using subprocess + server_process = subprocess.Popen(["python", "-m", "http.server", "-d", f"{self.outdir}", "65323"]) + + try: + # Run the Cypress test command + completed_process = subprocess.run( + [ + "npx", + "cypress", + "run", + "--browser", + "chrome", + "--config-file", + rf"{cypress_config_file}", + "--config", + rf"{cypress_config}", + ], + capture_output=True, + ) + + # To stop the server, we can terminate the process + server_process.terminate() + server_process.wait(timeout=5) # Wait for up to 5 seconds for the process to exit + # print("Server stopped successfully.") + + # Send back return code, stdout, and stderr + return { + "returncode": completed_process.returncode, + "stdout": completed_process.stdout, + "stderr": completed_process.stderr, + } + except subprocess.TimeoutExpired: + server_process.kill() + return { + "returncode": 1, + "stdout": None, + "stderr": "Server forcibly terminated due to timeout.", + } + except (Exception, subprocess.CalledProcessError) as e: + # Stop server when an exception occurs + server_process.terminate() + server_process.wait(timeout=5) # Wait for up to 5 seconds for the process to exit + return { + "returncode": 1, + "stdout": "Server stopped due to error.", + "stderr": e, + } + + @pytest.fixture(scope="function") def test_app(make_app, request): # We create a temp-folder on our own, as the util-functions from sphinx and pytest make troubles. @@ -31,8 +113,8 @@ def test_app(make_app, request): srcdir = builder_params.get("srcdir") src_dir = copy_srcdir_to_tmpdir(srcdir, sphinx_test_tempdir) - # return sphinx.testing fixture make_app and new srcdir which in sphinx_test_tempdir - app = make_app( + # return sphinx.testing fixture make_app and new srcdir which is in sphinx_test_tempdir + app: Sphinx = make_app( buildername=builder_params.get("buildername", "html"), srcdir=src_dir, freshenv=builder_params.get("freshenv"), @@ -43,6 +125,10 @@ def test_app(make_app, request): docutilsconf=builder_params.get("docutilsconf"), parallel=builder_params.get("parallel", 0), ) + # Add the spec_pattern as an attribute to the Sphinx app object + app.spec_pattern = builder_params.get("spec_pattern", "") + # Add the test_js() function as an attribute to the Sphinx app object + app.test_js = test_js.__get__(app, Sphinx) yield app diff --git a/tests/doc_test/variant_doc/index.rst b/tests/doc_test/variant_doc/index.rst index 81983e196..ed09e7453 100644 --- a/tests/doc_test/variant_doc/index.rst +++ b/tests/doc_test/variant_doc/index.rst @@ -35,10 +35,3 @@ Variant Handling Test :maxdepth: 2 :caption: Contents: - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/tests/js_test/js-test-sn-collapse-button.cy.js b/tests/js_test/js-test-sn-collapse-button.cy.js new file mode 100644 index 000000000..93131f9e6 --- /dev/null +++ b/tests/js_test/js-test-sn-collapse-button.cy.js @@ -0,0 +1,86 @@ +describe('Test Sphinx Needs Collapse', () => { + it('Visit Sphinx Needs Homepage', () => { + // 1. Given a user visits http://localhost:65323/ + cy.visit('/') + + cy.get('table.need span.needs.needs_collapse').each(($el, index, $list) => { + // 2. When page loads, select all elements that matches the selector `table.need span.needs.needs_collapse` + + var id = $el.attr("id"); + var parts = id.split("__"); + var rows = parts.slice(2); + + var table = $el.closest('table'); + var need_table_id = table.closest("div[id^=SNCB-]").attr("id"); + + // 3. Check if the id of the element contains show or hide + if (parts[1] == "show") { + cy.get($el).within(() => { + // 4. Then check if `span.needs.visible` has the class `collapse_is_hidden` + cy.get('span.needs.visible').should('have.class', 'collapse_is_hidden') + }) + } else { + cy.get($el).within(() => { + // 4. Then check if `span.needs.collapse` has the class `collapse_is_hidden` + cy.get('span.needs.collapsed').should('have.class', 'collapse_is_hidden') + }) + + for (var row in rows) { + // 5. And check if `#${need_table_id} table tr.${rows[row]}` has the class `collapse_is_hidden` + cy.get(`#${need_table_id} table tr.${rows[row]}`).should('have.class', 'collapse_is_hidden') + } + } + }) + }) +}) + +describe('Test Sphinx Needs Collapse Click', () => { + it('Visit Sphinx Needs Directive page', () => { + // 1. Given a user visits http://localhost:65323/ + cy.visit('/') + + cy.get('table.need span.needs.needs_collapse').each(($el, index, $list) => { + // 2. When page loads, select all elements that matches the selector `table.need span.needs.needs_collapse` + + var id = $el.attr("id"); + var parts = id.split("__"); + var rows = parts.slice(2); + + var table = $el.closest('table'); + var need_table_id = table.closest("div[id^=SNCB-]").attr("id"); + + if (parts[1] == "show") { + // 3. Click collapse/expand button + cy.get($el).click() + + for (var row in rows) { + // 4. And check if `#${need_table_id} table tr.${rows[row]}` has the class `collapse_is_hidden` + cy.get(`#${need_table_id} table tr.${rows[row]}`).should('have.class', 'collapse_is_hidden') + } + + cy.get($el).within(() => { + // 5. Then check if `span.needs.collapse` has the class `collapse_is_hidden` + cy.get('span.needs.collapsed').should('have.class', 'collapse_is_hidden') + // 6. And check if `span.needs.visible` has the class `collapse_is_hidden` + cy.get('span.needs.visible').should('not.have.class', 'collapse_is_hidden') + }) + } else{ + // 3. Click collapse/expand button + cy.get($el).click() + + for (var row in rows) { + // 4. And check if `#${need_table_id} table tr.${rows[row]}` has the class `collapse_is_hidden` + cy.get(`#${need_table_id} table tr.${rows[row]}`).should('not.have.class', 'collapse_is_hidden') + } + + cy.get($el).within(() => { + // 5. Then check if `span.needs.collapse` has the class `collapse_is_hidden` + cy.get('span.needs.collapsed').should('not.have.class', 'collapse_is_hidden') + // 6. Check if `span.needs.visible` has the class `collapse_is_hidden` + cy.get('span.needs.visible').should('have.class', 'collapse_is_hidden') + }) + } + + }) + }) +}) \ No newline at end of file diff --git a/tests/test_js_code.py b/tests/test_js_code.py new file mode 100644 index 000000000..a80648f9f --- /dev/null +++ b/tests/test_js_code.py @@ -0,0 +1,27 @@ +import pytest + + +@pytest.mark.jstest +@pytest.mark.parametrize( + "test_app", + [ + { + "buildername": "html", + "srcdir": "doc_test/variant_doc", + "tags": ["tag_a"], + "spec_pattern": "js_test/js-test-sn-collapse-button.cy.js", + } + ], + indirect=True, +) +def test_collapse_button_in_docs(test_app): + """Check if the Sphinx-Needs collapse button works in the provided documentation source.""" + app = test_app + app.build() + + # Call `app.test_js()` to run the JS test for a particular specPattern + js_test_result = app.test_js() + + # Check the return code and stdout + assert js_test_result["returncode"] == 0 + assert "All specs passed!" in js_test_result["stdout"].decode("utf-8")