Skip to content

Commit

Permalink
[tests] JavaScript: refactor test fixtures (#12102)
Browse files Browse the repository at this point in the history
This PR allows to serve JavaScript test fixtures using a fixture-based logic as for Python tests.

Co-authored-by: Bénédikt Tran <[email protected]>
  • Loading branch information
jayaddison and picnixz authored Jun 13, 2024
1 parent 4fbd368 commit 568e26c
Show file tree
Hide file tree
Showing 18 changed files with 177 additions and 40 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ doc/_build/
doc/locale/
tests/.coverage
tests/build/
tests/js/roots/*/_build
tests/test-server.lock
utils/regression_test.js

Expand Down
1 change: 1 addition & 0 deletions .ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ output-format = "full"

extend-exclude = [
"tests/roots/*",
"tests/js/roots/*",
"build/*",
"doc/_build/*",
"sphinx/search/*",
Expand Down
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ Bugs fixed
Testing
-------

* karma: refactor HTML search tests to use fixtures generated by Sphinx.
Patch by James Addison.

Release 7.3.7 (released Apr 19, 2024)
=====================================

Expand Down
6 changes: 6 additions & 0 deletions doc/internals/contributing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -338,3 +338,9 @@ Debugging tips
Minified files in ``sphinx/search/minified-js/*.js`` are generated from
non-minified ones using ``uglifyjs`` (installed via npm), with ``-m``
option to enable mangling.

* The ``searchindex.js`` files found in the ``tests/js/fixtures/*`` directories
are generated by using the standard Sphinx HTML builder on the corresponding
input projects found in ``tests/js/roots/*``. The fixtures provide test data
used by the Sphinx JavaScript unit tests, and can be regenerated by running
the ``utils/generate_js_fixtures.py`` script.
2 changes: 2 additions & 0 deletions karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ module.exports = function(config) {

// list of files / patterns to load in the browser
files: [
{ pattern: 'tests/js/fixtures/**/*.js', included: false, served: true },
'tests/js/documentation_options.js',
'tests/js/language_data.js',
'sphinx/themes/basic/static/doctools.js',
'sphinx/themes/basic/static/searchtools.js',
'sphinx/themes/basic/static/sphinx_highlight.js',
Expand Down
1 change: 1 addition & 0 deletions tests/js/fixtures/cpp/searchindex.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions tests/js/fixtures/multiterm/searchindex.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions tests/js/fixtures/partial/searchindex.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions tests/js/language_data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* language_data.js
* ~~~~~~~~~~~~~~~~
*
* This script contains the language-specific data used by searchtools.js,
* namely the list of stopwords, stemmer, scorer and splitter.
*
* :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/

var stopwords = [];


/* Non-minified version is copied as a separate JS file, if available */

/**
* Dummy stemmer for languages without stemming rules.
*/
var Stemmer = function() {
this.stemWord = function(w) {
return w;
}
}

Empty file added tests/js/roots/cpp/conf.py
Empty file.
10 changes: 10 additions & 0 deletions tests/js/roots/cpp/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
This is a sample C++ project used to generate a search engine index fixture.

.. cpp:class:: public Sphinx

The description of Sphinx class.

Indexing and querying the term C++ can be challenging, because search-related
tokenization often drops punctuation and mathematical characters (they occur
frequently on the web and would inflate the cardinality and size of web search
indexes).
Empty file.
13 changes: 13 additions & 0 deletions tests/js/roots/multiterm/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Main Page
=========

This is the main page of the ``multiterm`` test project.

This document is used as a test fixture to check that the search functionality
included when projects are built into an HTML output format can successfully
match this document when a search query containing multiple terms is performed.

At the time-of-writing this message, the application doesn't support "phrase
queries" -- queries that require all of the contained terms to appear adjacent
to each other and in the same order in the document as in the query; perhaps it
will do in future?
Empty file added tests/js/roots/partial/conf.py
Empty file.
9 changes: 9 additions & 0 deletions tests/js/roots/partial/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
sphinx_utils module
===================

Partial (also known as "prefix") matches on document titles should be possible
using the JavaScript search functionality included when HTML documentation
projects are built.

This document provides a sample reStructuredText input to confirm that partial
title matching is possible.
93 changes: 53 additions & 40 deletions tests/js/searchtools.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
describe('Basic html theme search', function() {

function loadFixture(name) {
req = new XMLHttpRequest();
req.open("GET", `base/tests/js/fixtures/${name}`, false);
req.send(null);
return req.responseText;
}

describe('terms search', function() {

it('should find "C++" when in index', function() {
index = {
docnames:["index"],
filenames:["index.rst"],
terms:{'c++':0},
titles:["&lt;no title&gt;"],
titleterms:{}
}
Search.setIndex(index);
searchterms = ['c++'];
excluded = [];
terms = index.terms;
titleterms = index.titleterms;
eval(loadFixture("cpp/searchindex.js"));

[_searchQuery, searchterms, excluded, ..._remainingItems] = Search._parseQuery('C++');
terms = Search._index.terms;
titleterms = Search._index.titleterms;

hits = [[
"index",
Expand All @@ -28,22 +28,11 @@ describe('Basic html theme search', function() {
});

it('should be able to search for multiple terms', function() {
index = {
alltitles: {
'Main Page': [[0, 'main-page']],
},
docnames:["index"],
filenames:["index.rst"],
terms:{main:0, page:0},
titles:["Main Page"],
titleterms:{ main:0, page:0 }
}
Search.setIndex(index);

searchterms = ['main', 'page'];
excluded = [];
terms = index.terms;
titleterms = index.titleterms;
eval(loadFixture("multiterm/searchindex.js"));

[_searchQuery, searchterms, excluded, ..._remainingItems] = Search._parseQuery('main page');
terms = Search._index.terms;
titleterms = Search._index.titleterms;
hits = [[
'index',
'Main Page',
Expand All @@ -55,18 +44,11 @@ describe('Basic html theme search', function() {
});

it('should partially-match "sphinx" when in title index', function() {
index = {
docnames:["index"],
filenames:["index.rst"],
terms:{'useful': 0, 'utilities': 0},
titles:["sphinx_utils module"],
titleterms:{'sphinx_utils': 0}
}
Search.setIndex(index);
searchterms = ['sphinx'];
excluded = [];
terms = index.terms;
titleterms = index.titleterms;
eval(loadFixture("partial/searchindex.js"));

[_searchQuery, searchterms, excluded, ..._remainingItems] = Search._parseQuery('sphinx');
terms = Search._index.terms;
titleterms = Search._index.titleterms;

hits = [[
"index",
Expand All @@ -81,6 +63,37 @@ describe('Basic html theme search', function() {

});

describe('aggregation of search results', function() {

it('should combine document title and document term matches', function() {
eval(loadFixture("multiterm/searchindex.js"));

searchParameters = Search._parseQuery('main page');

// fixme: duplicate result due to https://github.com/sphinx-doc/sphinx/issues/11961
hits = [
[
'index',
'Main Page',
'',
null,
15,
'index.rst'
],
[
'index',
'Main Page',
'#main-page',
null,
100,
'index.rst'
]
];
expect(Search._performSearch(...searchParameters)).toEqual(hits);
});

});

});

describe("htmlToText", function() {
Expand Down
16 changes: 16 additions & 0 deletions tests/test_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@

from sphinx.search import IndexBuilder

from tests.utils import TESTS_ROOT

JAVASCRIPT_TEST_ROOTS = list((TESTS_ROOT / 'js' / 'roots').iterdir())


class DummyEnvironment:
def __init__(self, version, domains):
Expand Down Expand Up @@ -346,3 +350,15 @@ def assert_is_sorted(item, path: str):
assert item == sorted(item), f'{err_path} is not sorted'
for i, child in enumerate(item):
assert_is_sorted(child, f'{path}[{i}]')


@pytest.mark.parametrize('directory', JAVASCRIPT_TEST_ROOTS)
def test_check_js_search_indexes(make_app, sphinx_test_tempdir, directory):
app = make_app('html', srcdir=directory, builddir=sphinx_test_tempdir / directory.name)
app.build()

fresh_searchindex = (app.outdir / 'searchindex.js')
existing_searchindex = (TESTS_ROOT / 'js' / 'fixtures' / directory.name / 'searchindex.js')

msg = f"Search index fixture {existing_searchindex} does not match regenerated copy."
assert fresh_searchindex.read_bytes() == existing_searchindex.read_bytes(), msg
34 changes: 34 additions & 0 deletions utils/generate_js_fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env python3

import subprocess
from pathlib import Path

SPHINX_ROOT = Path(__file__).resolve().parent.parent
TEST_JS_FIXTURES = SPHINX_ROOT / 'tests' / 'js' / 'fixtures'
TEST_JS_ROOTS = SPHINX_ROOT / 'tests' / 'js' / 'roots'


def build(srcdir: Path) -> None:
cmd = (
'sphinx-build',
'--fresh-env',
'--quiet',
*('--builder', 'html'),
f'{srcdir}',
f'{srcdir}/_build',
)
subprocess.run(cmd, check=True, capture_output=True)


for directory in TEST_JS_ROOTS.iterdir():
searchindex = directory / '_build' / 'searchindex.js'
destination = TEST_JS_FIXTURES / directory.name / 'searchindex.js'

print(f'Building {directory} ... ', end='')
build(directory)
print('done')

print(f'Moving {searchindex} to {destination} ... ', end='')
destination.parent.mkdir(exist_ok=True)
searchindex.replace(destination)
print('done')

0 comments on commit 568e26c

Please sign in to comment.