1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
from __future__ import annotations
import pytest
from pygitweb.static_tests.js_runtime import JsError, JsPage
_FORM = (
'<input id="search-patterns">'
'<input id="search-paths">'
'<input id="search-globs">'
'<select id="search-sort"><option value=""></option><option value="path">path</option></select>'
'<input id="search-max-count">'
'<input id="search-heading" type="checkbox">'
'<input id="search-multiline" type="checkbox">'
'<div id="search-status"></div>'
'<div id="search-results"></div>'
'<details id="search-advanced"></details>'
)
def _ready(js: JsPage, href: str = "http://127.0.0.1/search") -> None:
js.run("window.SEARCH_API_URL = '/api/search';")
js.set_location(href)
js.set_html(_FORM)
def test_load_throws_when_a_control_is_missing(js: JsPage) -> None:
js.run("window.SEARCH_API_URL = '/api/search';")
js.set_html("<div></div>")
with pytest.raises(JsError):
js.load("search-page.js")
def test_read_url_fills_the_form_and_opens_advanced_filters(js: JsPage) -> None:
js.route("/api/search", body={"results": []})
_ready(
js,
"http://127.0.0.1/search?patterns=foo&paths=src&globs=*.py&sort=path&max_count=5&heading=true&multiline=true",
)
js.load("search-page.js")
assert js.prop("#search-patterns", "value") == "foo"
assert js.prop("#search-paths", "value") == "src"
assert js.prop("#search-globs", "value") == "*.py"
assert js.prop("#search-sort", "value") == "path"
assert js.prop("#search-max-count", "value") == "5"
assert js.prop("#search-heading", "checked") is True
assert js.prop("#search-multiline", "checked") is True
assert js.prop("#search-advanced", "open") is True
def test_empty_pattern_clears_results_and_the_query_string(js: JsPage) -> None:
_ready(js, "http://127.0.0.1/search?patterns=old")
js.load("search-page.js")
js.set_prop("#search-patterns", "value", " ")
js.dispatch("#search-patterns", "keydown", key="Enter")
assert js.text("#search-status") == ""
assert js.prop("#search-results", "innerHTML") == ""
assert "patterns" not in js.value("location.search")
def test_results_are_escaped_and_pluralized(js: JsPage) -> None:
js.route("/api/search", body={"results": ["<script>", "ok"]})
_ready(js, "http://127.0.0.1/search?patterns=foo")
js.load("search-page.js")
assert js.text("#search-status") == "2 matches"
assert "<script>" in js.prop("#search-results", "innerHTML")
assert "<script>" not in js.prop("#search-results", "innerHTML")
assert "is-loading" not in js.classes("#search-results")
def test_a_single_match_uses_the_singular_label(js: JsPage) -> None:
js.route("/api/search", body={"results": ["one"]})
_ready(js, "http://127.0.0.1/search?patterns=foo&heading=false")
js.set_html(_FORM)
js.load("search-page.js")
assert js.text("#search-status") == "1 match"
url = next(str(item["url"]) for item in js.requests() if "/api/search" in str(item["url"]))
assert "heading=false" in url
assert "patterns=foo" in url
def test_missing_results_count_as_zero(js: JsPage) -> None:
js.route("/api/search", body={})
_ready(js, "http://127.0.0.1/search?patterns=foo")
js.load("search-page.js")
assert js.text("#search-status") == "0 matches"
assert js.prop("#search-results", "innerHTML") == ""
def test_error_detail_is_shown(js: JsPage) -> None:
js.route("/api/search", status=400, body={"detail": "bad pattern"}, status_text="Bad")
_ready(js, "http://127.0.0.1/search?patterns=foo")
js.load("search-page.js")
assert js.text("#search-status") == "Error 400: bad pattern"
def test_error_without_json_uses_the_status_text(js: JsPage) -> None:
js.route("/api/search", status=502, json_error=True, status_text="Bad Gateway")
_ready(js, "http://127.0.0.1/search?patterns=foo")
js.load("search-page.js")
assert js.text("#search-status") == "Error 502: Bad Gateway"
def test_network_error_is_shown(js: JsPage) -> None:
js.route("/api/search", error="offline")
_ready(js, "http://127.0.0.1/search?patterns=foo")
js.load("search-page.js")
assert js.text("#search-status") == "Error: offline"
assert "is-loading" not in js.classes("#search-results")
def test_a_newer_search_aborts_the_previous_one(js: JsPage) -> None:
js.route("/api/search", hang=True)
_ready(js, "http://127.0.0.1/search?patterns=foo")
js.load("search-page.js")
assert js.text("#search-status") == "Searching…"
js.set_prop("#search-patterns", "value", "bar")
js.dispatch("#search-patterns", "keydown", key="Enter")
assert js.text("#search-status") == "Searching…"
js.fulfill("/api/search", body={"results": ["bar"]})
assert js.text("#search-status") == "1 match"
assert "Error" not in js.text("#search-status")
def test_input_debounces_and_other_controls_search_too(js: JsPage) -> None:
js.route("/api/search", body={"results": ["x"]})
_ready(js)
js.load("search-page.js")
js.set_prop("#search-patterns", "value", "foo")
js.set_prop("#search-paths", "value", "src")
js.set_prop("#search-globs", "value", "*.py")
js.set_prop("#search-max-count", "value", "")
js.set_prop("#search-sort", "value", "path")
js.set_prop("#search-heading", "checked", True)
js.set_prop("#search-multiline", "checked", True)
js.dispatch("#search-patterns", "input")
assert js.text("#search-status") == ""
js.tick()
url = next(str(item["url"]) for item in js.requests() if "patterns=foo" in str(item["url"]))
assert "paths=src" in url
assert "globs=*.py" in url
assert "sort=path" in url
assert "max_count" not in url
assert "heading=true" in url
assert "multiline=true" in url
assert js.value("location.search").find("patterns=foo") >= 0
def test_other_keys_do_not_run_a_search_immediately(js: JsPage) -> None:
_ready(js)
js.load("search-page.js")
js.set_prop("#search-patterns", "value", "foo")
js.dispatch("#search-patterns", "keydown", key="a")
assert js.text("#search-status") == ""