Skip to content

Commit bbc25cb

Browse files
committed
UI: generalize autocompletion to work for multi-choice fields (code review)
1 parent f910e0a commit bbc25cb

File tree

8 files changed

+102
-68
lines changed

8 files changed

+102
-68
lines changed

strictdoc/backend/sdoc/models/document.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,4 +237,4 @@ def get_options_for_choice(
237237
field, GrammarElementFieldMultipleChoice
238238
):
239239
return field.options
240-
return []
240+
raise AssertionError(f"Must not reach here: {field}")

strictdoc/export/html/_static/controllers/autocompletable_field_controller.js

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
ready: Boolean,
1616
url: String,
1717
minLength: Number,
18-
delay: { type: Number, default: 300 },
18+
delay: { type: Number, default: 10 },
1919
queryParam: { type: String, default: "q" },
2020
multipleChoice: Boolean,
2121
}
@@ -38,6 +38,8 @@
3838

3939
this.onInputChange = debounce(this.onInputChange, this.delayValue)
4040

41+
this.autocompletable.addEventListener("input", this.onInputChange)
42+
4143
autocompletable.addEventListener("keydown", (event) => {
4244
const handler = this[`on${event.key}Keydown`]
4345
if (handler) handler(event)
@@ -61,18 +63,6 @@
6163
}
6264
});
6365

64-
autocompletable.addEventListener("input", (event) => {
65-
const query = autocompletable.innerText.trim()
66-
if (query && query.length >= this.minLengthValue) {
67-
this.fetchResults(query)
68-
} else {
69-
this.hideAndRemoveOptions()
70-
}
71-
72-
const text = filterSingleLine(this.autocompletable.innerText)
73-
this.hidden.value = text
74-
});
75-
7666
this.results.addEventListener("mousedown", this.onResultsMouseDown)
7767
this.results.addEventListener("click", this.onResultsClick)
7868

@@ -193,7 +183,7 @@
193183
this.autocompletable.innerText = suggestion
194184
this.hidden.value = suggestion
195185

196-
// some shenanigans to move the cursor to the end
186+
// Move the cursor to the end of the input.
197187
this.autocompletable.focus();
198188
const range = document.createRange();
199189
range.selectNodeContents(this.autocompletable);
@@ -234,6 +224,18 @@
234224
}, { once: true })
235225
}
236226

227+
onInputChange = () => {
228+
const query = this.autocompletable.innerText.trim()
229+
if (query && query.length >= this.minLengthValue) {
230+
this.fetchResults(query)
231+
} else {
232+
this.hideAndRemoveOptions()
233+
}
234+
235+
const text = filterSingleLine(this.autocompletable.innerText)
236+
this.hidden.value = text
237+
}
238+
237239
identifyOptions() {
238240
const prefix = this.results.id || "autocompletable"
239241
const optionsWithoutId = this.results.querySelectorAll(`${optionSelector}:not([id])`)

strictdoc/server/routers/main_router.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2793,14 +2793,26 @@ def get_autocomplete_field_results(
27932793
)
27942794

27952795
if field.gef_type == RequirementFieldType.MULTIPLE_CHOICE:
2796-
# MultipleChoice: Split the query into parts.
2796+
# MultipleChoice: We split the query into its parts:
2797+
#
2798+
# Example User input: "Some Value, Another Value, Yet ano|".
2799+
# parts = ['some value', 'another value', 'yet ano'] # noqa: ERA001
27972800
parts = q.lower().split(",")
27982801

2799-
# only use the last_part for lookup
2802+
# For the lookup, we want to use the only the last, still
2803+
# incomplete part, not the full query:
2804+
#
2805+
# last_part = "yet ano" # noqa: ERA001
2806+
# query_words = ['yet', 'ano'] # noqa: ERA001
28002807
last_part = parts[-1].strip()
28012808
query_words = last_part.split()
28022809

2803-
# Pre-filter: don't suggest choices that were already selected / present in the query.
2810+
# We also filter the already selected choices from the
2811+
# options we are going to be send to the user,
2812+
# as MultipleChoices is a Set, so options shall be
2813+
# selectable at most once.
2814+
#
2815+
# In the example, we would remove 'some value' and 'another value'.
28042816
already_selected = [
28052817
p.strip() for p in parts[:-1] if p.strip()
28062818
]
@@ -2810,7 +2822,7 @@ def get_autocomplete_field_results(
28102822
if choice.lower() not in already_selected
28112823
]
28122824
else:
2813-
# SingleChoice: use all available options.
2825+
# SingleChoice: we use the full query and all available options.
28142826
query_words = q.lower().split()
28152827
filtered_options = all_options
28162828

tests/end2end/helpers/form/form.py

Lines changed: 55 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# pylint: disable=invalid-name
2-
3-
42
from selenium.webdriver import Keys
3+
from selenium.webdriver.common.action_chains import ActionChains
54
from selenium.webdriver.common.by import By
65
from selenium.webdriver.support.ui import WebDriverWait
76

@@ -153,25 +152,36 @@ def do_use_first_autocomplete_result(
153152

154153
field_xpath = f"(//*[@data-testid='{test_id}'])"
155154
element = self.test_case.find_element(field_xpath)
155+
hidden_input = element.find_element(
156+
By.XPATH, 'following-sibling::input[@type="hidden"]'
157+
)
158+
results_ul = hidden_input.find_element(
159+
By.XPATH, "following-sibling::ul[1]"
160+
)
156161

157-
for _ in range(3):
158-
self.test_case.type(field_xpath, f"{field_value}", by=By.XPATH)
159-
element.send_keys(Keys.ARROW_DOWN)
160-
element.send_keys(Keys.RETURN)
162+
# We simulate a user typing the supplied field_value.
163+
self.test_case.type(field_xpath, f"{field_value}", by=By.XPATH)
161164

162-
try:
163-
WebDriverWait(self.test_case.driver, timeout=3).until(
164-
lambda _: field_value.lower() in element.text.lower()
165-
)
166-
break
167-
except Exception:
168-
pass
165+
# We wait until the results <ul> is displayed.
166+
WebDriverWait(self.test_case.driver, 3).until(
167+
lambda _: results_ul.is_displayed()
168+
)
169169

170-
else:
171-
raise AssertionError(
172-
f"The text field could not be filled with the value: "
173-
f"'{field_value}'."
174-
)
170+
# We send Arrow-Down and Enter select the first match.
171+
# The stimulus.js controller uses a debounce of 10ms, we are
172+
# careful to use an larger interval inbetween key presses.
173+
len_before_autocomplete = len(element.text.lower().strip())
174+
select_first_match_action = ActionChains(self.test_case.driver)
175+
select_first_match_action.send_keys(Keys.ARROW_DOWN).pause(
176+
0.1
177+
).send_keys(Keys.RETURN).perform()
178+
179+
# Now wait for the length of the field to increase, this means
180+
# the autocomplete did happen.
181+
WebDriverWait(self.test_case.driver, timeout=3).until(
182+
lambda _: len(element.text.lower().strip())
183+
> len_before_autocomplete
184+
)
175185

176186
def do_append_command_and_use_autocomplete_result_again(
177187
self, test_id: str, field_value: str
@@ -181,26 +191,36 @@ def do_append_command_and_use_autocomplete_result_again(
181191

182192
field_xpath = f"(//*[@data-testid='{test_id}'])"
183193
element = self.test_case.find_element(field_xpath)
194+
hidden_input = element.find_element(
195+
By.XPATH, 'following-sibling::input[@type="hidden"]'
196+
)
197+
results_ul = hidden_input.find_element(
198+
By.XPATH, "following-sibling::ul[1]"
199+
)
184200

185-
for _ in range(3):
186-
element.send_keys(f",{field_value}")
187-
element.send_keys(Keys.ARROW_DOWN)
188-
element.send_keys(Keys.RETURN)
201+
# We simulate a user typing a comma, and the supplied field_value.
202+
element.send_keys(f",{field_value}")
189203

190-
try:
191-
WebDriverWait(self.test_case.driver, timeout=3).until(
192-
lambda _: field_value.lower()
193-
in element.text.lower().split()[-1]
194-
)
195-
break
196-
except Exception:
197-
pass
204+
# We wait until the results <ul> is displayed.
205+
WebDriverWait(self.test_case.driver, 3).until(
206+
lambda _: results_ul.is_displayed()
207+
)
198208

199-
else:
200-
raise AssertionError(
201-
f"The text field could not be filled with the value: "
202-
f"'{field_value}'."
203-
)
209+
# We send Arrow-Down and Enter select the first match.
210+
# The stimulus.js controller uses a debounce of 50ms, we are
211+
# careful to use an larger interval inbetween key presses.
212+
len_before_autocomplete = len(element.text.lower().strip())
213+
select_first_match_action = ActionChains(self.test_case.driver)
214+
select_first_match_action.send_keys(Keys.ARROW_DOWN).pause(
215+
0.1
216+
).send_keys(Keys.RETURN).perform()
217+
218+
# Now wait for the length of the field to increase, this means
219+
# the autocomplete did happen.
220+
WebDriverWait(self.test_case.driver, timeout=3).until(
221+
lambda _: len(element.text.lower().strip())
222+
> len_before_autocomplete
223+
)
204224

205225
def do_use_first_autocomplete_result_mid(
206226
self, mid: MID, test_id: str, field_value: str

tests/end2end/screens/document/create_requirement/_MultipleChoice/create_requirement_MultipleChoice_field_using_autocomplete/expected_output/document.sdoc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ ELEMENTS:
2525
- TITLE: TAGS
2626
TYPE: String
2727
REQUIRED: False
28-
- TITLE: TITLE
29-
TYPE: String
30-
REQUIRED: False
3128
- TITLE: OWNER
3229
TYPE: MultipleChoice(Abigail ACCURACY, Ace ACCREDITATION, Basil BOUNDARY, Clarence COMPLIANCE, Daphne DESIGN, Fiona FRAMEWORK, Felix FUNCTIONAL, Gloria GOVERNANCE, Gertrude GUIDELINE, Hugo HARMONY, Harvey HIERARCHY, Lydia LOGIC, Olivia OBJECTIVE, Mildred METADATA, Nigel NORMATIVE, Penelope PROCEDURE, Rachel RATIONALE, Eleanor RIGOR, Samuel SPECIFICATION, Sylvia STANDARD, Theodore TRACEABILITY, Victor VALIDATION, Walter WORKFLOW)
3330
REQUIRED: False
31+
- TITLE: TITLE
32+
TYPE: String
33+
REQUIRED: False
3434
- TITLE: STATEMENT
3535
TYPE: String
3636
REQUIRED: False
@@ -68,8 +68,8 @@ Shall test foo.
6868
[REQUIREMENT]
6969
UID: REQ-2
7070
STATUS: accepted
71-
TITLE: Requirement 2 XYZ
7271
OWNER: Abigail ACCURACY, Ace ACCREDITATION
72+
TITLE: Requirement 2 XYZ
7373
STATEMENT: >>>
7474
Shall test foo 2.
7575
<<<

tests/end2end/screens/document/create_requirement/_MultipleChoice/create_requirement_MultipleChoice_field_using_autocomplete/input/document.sdoc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ ELEMENTS:
2525
- TITLE: TAGS
2626
TYPE: String
2727
REQUIRED: False
28-
- TITLE: TITLE
29-
TYPE: String
30-
REQUIRED: False
3128
- TITLE: OWNER
3229
TYPE: MultipleChoice(Abigail ACCURACY, Ace ACCREDITATION, Basil BOUNDARY, Clarence COMPLIANCE, Daphne DESIGN, Fiona FRAMEWORK, Felix FUNCTIONAL, Gloria GOVERNANCE, Gertrude GUIDELINE, Hugo HARMONY, Harvey HIERARCHY, Lydia LOGIC, Olivia OBJECTIVE, Mildred METADATA, Nigel NORMATIVE, Penelope PROCEDURE, Rachel RATIONALE, Eleanor RIGOR, Samuel SPECIFICATION, Sylvia STANDARD, Theodore TRACEABILITY, Victor VALIDATION, Walter WORKFLOW)
3330
REQUIRED: False
31+
- TITLE: TITLE
32+
TYPE: String
33+
REQUIRED: False
3434
- TITLE: STATEMENT
3535
TYPE: String
3636
REQUIRED: False

tests/end2end/screens/document/update_requirement/_MultipleChoice/update_requirement_MultipleChoice_field_using_autocomplete/expected_output/document.sdoc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ ELEMENTS:
2525
- TITLE: TAGS
2626
TYPE: String
2727
REQUIRED: False
28-
- TITLE: TITLE
29-
TYPE: String
30-
REQUIRED: False
3128
- TITLE: OWNER
3229
TYPE: MultipleChoice(Abigail ACCURACY, Ace ACCREDITATION, Basil BOUNDARY, Clarence COMPLIANCE, Daphne DESIGN, Fiona FRAMEWORK, Felix FUNCTIONAL, Gloria GOVERNANCE, Gertrude GUIDELINE, Hugo HARMONY, Harvey HIERARCHY, Lydia LOGIC, Olivia OBJECTIVE, Mildred METADATA, Nigel NORMATIVE, Penelope PROCEDURE, Rachel RATIONALE, Eleanor RIGOR, Samuel SPECIFICATION, Sylvia STANDARD, Theodore TRACEABILITY, Victor VALIDATION, Walter WORKFLOW)
3330
REQUIRED: False
31+
- TITLE: TITLE
32+
TYPE: String
33+
REQUIRED: False
3434
- TITLE: STATEMENT
3535
TYPE: String
3636
REQUIRED: False
@@ -61,8 +61,8 @@ ELEMENTS:
6161
[REQUIREMENT]
6262
UID: TEST-1
6363
STATUS: accepted
64-
TITLE: Unit test ABC
6564
OWNER: Abigail ACCURACY, Ace ACCREDITATION
65+
TITLE: Unit test ABC
6666
STATEMENT: >>>
6767
Shall test foo.
6868
<<<

tests/end2end/screens/document/update_requirement/_MultipleChoice/update_requirement_MultipleChoice_field_using_autocomplete/input/document.sdoc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ ELEMENTS:
1717
- TITLE: TAGS
1818
TYPE: String
1919
REQUIRED: False
20-
- TITLE: TITLE
21-
TYPE: String
22-
REQUIRED: False
2320
- TITLE: OWNER
2421
TYPE: MultipleChoice(Abigail ACCURACY, Ace ACCREDITATION, Basil BOUNDARY, Clarence COMPLIANCE, Daphne DESIGN, Fiona FRAMEWORK, Felix FUNCTIONAL, Gloria GOVERNANCE, Gertrude GUIDELINE, Hugo HARMONY, Harvey HIERARCHY, Lydia LOGIC, Olivia OBJECTIVE, Mildred METADATA, Nigel NORMATIVE, Penelope PROCEDURE, Rachel RATIONALE, Eleanor RIGOR, Samuel SPECIFICATION, Sylvia STANDARD, Theodore TRACEABILITY, Victor VALIDATION, Walter WORKFLOW)
2522
REQUIRED: False
23+
- TITLE: TITLE
24+
TYPE: String
25+
REQUIRED: False
2626
- TITLE: STATEMENT
2727
TYPE: String
2828
REQUIRED: False

0 commit comments

Comments
 (0)