Upstream mozilla-firefox/firefox state of the touched files before d3fbb52d97

This commit is contained in:
upstream committed 2026-09-25 12:57:39 +02:00
1 parent edfad4e281
commit 35563aaab9
2 files changed
+678

No files matched your search

@@ -0,0 +1,306 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
// Testing various project search features
"use strict";
requestLongerTimeout(3);
add_task(async function testProjectSearchCloseOnNavigation() {
const dbg = await initDebugger(
"doc-script-switching.html",
"script-switching-01.js"
);
await selectSource(dbg, "script-switching-01.js");
await openProjectSearch(dbg);
ok(
!findElement(dbg, "projectSearchRefreshButton"),
"The refresh button is only visible after having done a search"
);
await doProjectSearch(dbg, "function", 2);
is(getExpandedResultsCount(dbg), 9);
is(dbg.selectors.getActiveSearch(), "project");
const refreshButton = findElement(dbg, "projectSearchRefreshButton");
ok(
refreshButton,
"Refresh button is visible right after search is completed"
);
ok(
!refreshButton.classList.contains("highlight"),
"Refresh button is *not* highlighted by default"
);
await navigate(dbg, "doc-scripts.html");
// Project search is still visible after navigation
is(dbg.selectors.getActiveSearch(), "project");
// With same search results
is(getExpandedResultsCount(dbg), 9);
ok(
refreshButton.classList.contains("highlight"),
"Refresh button is highlighted after navigation"
);
info("Try to open a discarded source");
await clickElement(dbg, "fileMatch");
info("Wait for the warning popup to be visible");
// We are waiting for the popup to be added...
await waitFor(() => dbg.win.document.querySelector(".unavailable-source"));
// ...and verify that the popup is made visible.
await waitFor(() => dbg.win.document.querySelector(".tooltip-shown"));
info("Retry to open the discard source, this should hide the popup");
await clickElement(dbg, "fileMatch");
info("Wait for the popup to be hidden");
// Note that .unavailable-source won't be removed from the DOM
await waitFor(() => !dbg.win.document.querySelector(".tooltip-visible"));
info("Refresh results against the new page");
refreshButton.click();
// Wait for the search to be updated against the new page
await waitForSearchResults(dbg, 5);
is(getExpandedResultsCount(dbg), 39);
ok(
!refreshButton.classList.contains("highlight"),
"Refresh button is no longer highlighted after refreshing the search"
);
});
add_task(async function testSimpleProjectSearch() {
// Start with side panel collapsed so we can assert that the project search keyboard
// shortcut will open it.
await pushPref("devtools.debugger.start-panel-collapsed", true);
const dbg = await initDebugger(
"doc-script-switching.html",
"script-switching-01.js"
);
await openProjectSearch(dbg);
ok(
!!findElementWithSelector(dbg, ".project-text-search"),
"Project search is visible"
);
const searchTerm = "first";
await doProjectSearch(dbg, searchTerm, 1);
const queryMatch = findElement(dbg, "fileMatch").querySelector(
".query-match"
);
is(
queryMatch.innerText,
searchTerm,
"The highlighted text matches the search term"
);
info("Select a result match to open the location in the source");
await clickElement(dbg, "fileMatch");
await waitForSelectedSource(dbg, "script-switching-01.js");
info("Close start sidebar");
const startPanelToggleButtonEl = findElementWithSelector(
dbg,
".toggle-button.start"
);
startPanelToggleButtonEl.click();
await waitFor(() => startPanelToggleButtonEl.classList.contains("collapsed"));
info("Try to open project search again");
await openProjectSearch(dbg);
ok(
!!findElementWithSelector(dbg, ".project-text-search"),
"Project search is visible"
);
});
add_task(async function testMatchesForRegexSearches() {
const dbg = await initDebugger("doc-react.html", "App.js");
await openProjectSearch(dbg);
type(dbg, "import .* from 'react'");
await clickElement(dbg, "projectSearchModifiersRegexMatch");
await waitForSearchResults(dbg, 2);
const queryMatch = findAllElements(dbg, "fileMatch")[1].querySelector(
".query-match"
);
is(
queryMatch.innerText,
"import React, { Component } from 'react'",
"The highlighted text matches the search term"
);
// Turn off the regex modifier so does not break tests below
await clickElement(dbg, "projectSearchModifiersRegexMatch");
});
// Test expanding search results to reveal the search matches.
add_task(async function testExpandSearchResultsToShowMatches() {
const dbg = await initDebugger("doc-react.html", "App.js");
await openProjectSearch(dbg);
await doProjectSearch(dbg, "we", 19);
is(getExpandedResultsCount(dbg), 159);
const collapsedNodes = findAllElements(dbg, "projectSearchCollapsed");
is(collapsedNodes.length, 1);
collapsedNodes[0].click();
is(getExpandedResultsCount(dbg), 367);
});
add_task(async function testSearchModifiers() {
const dbg = await initDebugger("doc-react.html", "App.js");
await openProjectSearch(dbg);
await assertProjectSearchModifier(
dbg,
"projectSearchModifiersCaseSensitive",
"FIELDS",
"case sensitive",
{ resultWithModifierOn: 0, resultWithModifierOff: 2 }
);
await assertProjectSearchModifier(
dbg,
"projectSearchModifiersRegexMatch",
`\\*`,
"regex match",
{ resultWithModifierOn: 12, resultWithModifierOff: 0 }
);
await assertProjectSearchModifier(
dbg,
"projectSearchModifiersWholeWordMatch",
"so",
"whole word match",
{ resultWithModifierOn: 6, resultWithModifierOff: 16 }
);
});
add_task(async function testSearchExcludePatterns() {
const dbg = await initDebugger("doc-react.html", "App.js");
info("Search across all files");
await openProjectSearch(dbg);
let fileResults = await doProjectSearch(dbg, "console", 5);
let resultsFromNodeModules = [...fileResults].filter(result =>
result.innerText.includes("node_modules")
);
is(
resultsFromNodeModules.length,
3,
"3 results were found from node_modules"
);
info("Excludes search results based on multiple search patterns");
await clickElement(dbg, "excludePatternsInput");
type(dbg, "App.js, main.js");
pressKey(dbg, "Enter");
fileResults = await waitForSearchResults(dbg, 3);
const resultsFromAppJS = [...fileResults].filter(result =>
result.innerText.includes("App.js")
);
is(resultsFromAppJS.length, 0, "None of the results is from the App.js file");
const resultsFromMainJS = [...fileResults].filter(result =>
result.innerText.includes("main.js")
);
is(
resultsFromMainJS.length,
0,
"None of the results is from the main.js file"
);
info("Excludes search results from node modules files");
await clearElement(dbg, "excludePatternsInput");
type(dbg, "**/node_modules/**");
pressKey(dbg, "Enter");
fileResults = await waitForSearchResults(dbg, 2);
resultsFromNodeModules = [...fileResults].filter(result =>
result.innerText.includes("node_modules")
);
is(
resultsFromNodeModules.length,
0,
"None of the results is from the node modules files"
);
info("Assert that the exclude pattern is persisted across reloads");
await reloadSelectedTab();
await openProjectSearch(dbg);
const excludePatternsInputElement = await waitForElement(
dbg,
"excludePatternsInput"
);
is(
excludePatternsInputElement.value,
"**/node_modules/**",
"The exclude pattern for node modules is persisted accross reloads"
);
// Clear the fields so that it does not impact on the subsequent tests
await clearElement(dbg, "projectSearchSearchInput");
await clearElement(dbg, "excludePatternsInput");
pressKey(dbg, "Enter");
});
async function assertProjectSearchModifier(
dbg,
searchModifierBtn,
searchTerm,
title,
expected
) {
info(`Assert ${title} search modifier`);
type(dbg, searchTerm);
info(`Turn on the ${title} search modifier option`);
await clickElement(dbg, searchModifierBtn);
let results = await waitForSearchResults(dbg, expected.resultWithModifierOn);
is(
results.length,
expected.resultWithModifierOn,
`${results.length} results where found`
);
info(`Turn off the ${title} search modifier`);
await clickElement(dbg, searchModifierBtn);
results = await waitForSearchResults(dbg, expected.resultWithModifierOff);
is(
results.length,
expected.resultWithModifierOff,
`${results.length} results where found`
);
await clearElement(dbg, "projectSearchSearchInput");
}
@@ -0,0 +1,372 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
"use strict";
const React = require("devtools/client/shared/vendor/react");
const { Component } = React;
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const {
button,
div,
label,
input,
span,
} = require("devtools/client/shared/vendor/react-dom-factories");
const DebuggerImage = require("devtools/client/shared/components/DebuggerImage");
const CloseButton = require("devtools/client/shared/components/CloseButton");
const classnames = require("resource://devtools/client/shared/classnames.js");
const SearchModifiers = require("resource://devtools/client/shared/components/SearchModifiers.js");
const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
const locale = new LocalizationHelper(
"devtools/client/locales/components.properties"
);
const arrowBtn = (onClick, type, className, tooltip) => {
const props = {
className,
key: type,
onClick,
title: tooltip,
type,
};
return button(
props,
React.createElement(DebuggerImage, {
name: type,
})
);
};
class SearchInput extends Component {
static get propTypes() {
return {
count: PropTypes.number.isRequired,
expanded: PropTypes.bool.isRequired,
handleClose: PropTypes.func,
handleNext: PropTypes.func,
handlePrev: PropTypes.func,
hasPrefix: PropTypes.bool.isRequired,
isLoading: PropTypes.bool.isRequired,
onBlur: PropTypes.func,
onChange: PropTypes.func,
onFocus: PropTypes.func,
onHistoryScroll: PropTypes.func,
onKeyDown: PropTypes.func,
onKeyUp: PropTypes.func,
placeholder: PropTypes.string,
query: PropTypes.string,
selectedItemId: PropTypes.string,
shouldFocus: PropTypes.bool,
showClose: PropTypes.bool.isRequired,
showExcludePatterns: PropTypes.bool.isRequired,
excludePatternsLabel: PropTypes.string,
excludePatternsPlaceholder: PropTypes.string,
showErrorEmoji: PropTypes.bool.isRequired,
size: PropTypes.string,
disabled: PropTypes.bool,
summaryMsg: PropTypes.string,
searchKey: PropTypes.string.isRequired,
searchOptions: PropTypes.object.isRequired,
setSearchOptions: PropTypes.func.isRequired,
showSearchModifiers: PropTypes.bool.isRequired,
onToggleSearchModifier: PropTypes.func,
};
}
static defaultProps = {
expanded: false,
hasPrefix: false,
selectedItemId: "",
size: "",
showClose: true,
};
constructor(props) {
super(props);
this.state = {
history: [],
excludePatterns: this.props.showSearchModifiers
? (props.searchOptions?.excludePatterns ?? "")
: null,
};
}
componentDidMount() {
this.setFocus();
}
componentDidUpdate(prevProps) {
if (this.props.shouldFocus && !prevProps.shouldFocus) {
this.setFocus();
}
}
setFocus() {
if (this.$input) {
const _input = this.$input;
_input.focus();
if (!_input.value) {
return;
}
// omit prefix @:# from being selected
const selectStartPos = this.props.hasPrefix ? 1 : 0;
_input.setSelectionRange(selectStartPos, _input.value.length + 1);
}
}
renderArrowButtons() {
const { handleNext, handlePrev } = this.props;
return [
arrowBtn(
handlePrev,
"arrow-up",
classnames("nav-btn", "prev"),
locale.getStr("editor.searchResults.prevResult"),
DebuggerImage
),
arrowBtn(
handleNext,
"arrow-down",
classnames("nav-btn", "next"),
locale.getStr("editor.searchResults.nextResult"),
DebuggerImage
),
];
}
onFocus = e => {
const { onFocus } = this.props;
if (onFocus) {
onFocus(e);
}
};
onBlur = e => {
const { onBlur } = this.props;
if (onBlur) {
onBlur(e);
}
};
onKeyDown = e => {
const { onHistoryScroll, onKeyDown } = this.props;
if (!onHistoryScroll) {
onKeyDown(e);
return;
}
const inputValue = e.target.value;
const { history } = this.state;
const currentHistoryIndex = history.indexOf(inputValue);
if (e.key === "Enter") {
this.saveEnteredTerm(inputValue);
onKeyDown(e);
return;
}
if (e.key === "ArrowUp") {
const previous =
currentHistoryIndex > -1 ? currentHistoryIndex - 1 : history.length - 1;
const previousInHistory = history[previous];
if (previousInHistory) {
e.preventDefault();
onHistoryScroll(previousInHistory);
}
return;
}
if (e.key === "ArrowDown") {
const next = currentHistoryIndex + 1;
const nextInHistory = history[next];
if (nextInHistory) {
onHistoryScroll(nextInHistory);
}
}
};
onExcludeKeyDown = e => {
if (e.key === "Enter") {
this.props.setSearchOptions(this.props.searchKey, {
excludePatterns: this.state.excludePatterns,
});
this.props.onKeyDown(e);
}
};
saveEnteredTerm(query) {
const { history } = this.state;
const previousIndex = history.indexOf(query);
if (previousIndex !== -1) {
history.splice(previousIndex, 1);
}
history.push(query);
this.setState({ history });
}
renderSummaryMsg() {
const { summaryMsg } = this.props;
if (!summaryMsg) {
return null;
}
return div(
{
className: "search-field-summary",
},
summaryMsg
);
}
renderSpinner() {
const { isLoading } = this.props;
if (!isLoading) {
return null;
}
return React.createElement(DebuggerImage, {
name: "loader",
className: "spin",
});
}
renderNav() {
const { count, handleNext, handlePrev } = this.props;
if ((!handleNext && !handlePrev) || !count || count == 1) {
return null;
}
return div(
{
className: "search-nav-buttons",
},
this.renderArrowButtons()
);
}
renderSearchModifiers() {
if (!this.props.showSearchModifiers) {
return null;
}
return React.createElement(SearchModifiers, {
modifiers: this.props.searchOptions,
onToggleSearchModifier: updatedOptions => {
this.props.setSearchOptions(this.props.searchKey, updatedOptions);
this.props.onToggleSearchModifier();
},
});
}
renderExcludePatterns() {
if (!this.props.showExcludePatterns) {
return null;
}
return div(
{
className: classnames("exclude-patterns-field", this.props.size),
},
label(null, this.props.excludePatternsLabel),
input({
placeholder: this.props.excludePatternsPlaceholder,
value: this.state.excludePatterns,
onKeyDown: this.onExcludeKeyDown,
onChange: e =>
this.setState({
excludePatterns: e.target.value,
}),
})
);
}
renderClose() {
if (!this.props.showClose) {
return null;
}
return React.createElement(
React.Fragment,
null,
span({
className: "pipe-divider",
}),
React.createElement(CloseButton, {
handleClick: this.props.handleClose,
buttonClass: this.props.size,
tooltip: locale.getStr("editor.searchResults.close"),
})
);
}
render() {
const {
expanded,
onChange,
onKeyUp,
placeholder,
query,
selectedItemId,
showErrorEmoji,
size,
disabled,
} = this.props;
const inputProps = {
className: classnames({
empty: showErrorEmoji,
}),
disabled,
onChange,
onKeyDown: e => this.onKeyDown(e),
onKeyUp,
onFocus: e => this.onFocus(e),
onBlur: e => this.onBlur(e),
"aria-autocomplete": "list",
"aria-controls": "result-list",
"aria-activedescendant":
expanded && selectedItemId ? `${selectedItemId}-title` : "",
placeholder,
value: query,
spellCheck: false,
ref: c => (this.$input = c),
};
return div(
{
className: "search-outline",
},
div(
{
className: classnames("search-field", size),
role: "combobox",
"aria-haspopup": "listbox",
"aria-owns": "result-list",
"aria-expanded": expanded,
},
React.createElement(DebuggerImage, {
name: "search",
}),
input(inputProps),
this.renderSpinner(),
this.renderSummaryMsg(),
this.renderNav(),
div(
{
className: "search-buttons-bar",
},
this.renderSearchModifiers(),
this.renderClose()
)
),
this.renderExcludePatterns()
);
}
}
module.exports = SearchInput;